stacks/news-digest: twice-daily LLM-curated briefing on ana-docker
The Miniflux inbox got noisy after a few subreddits + HN + Lobste.rs.
This stack distills a single static page twice a day — at 0800 and
2000 local — that surfaces only what cleared score + ratio filters,
each item tldr'd by qwen3.5-35-a3b on llama-swap.
Pipeline (digest.py, ~330 lines):
1. Discover subreddits from Miniflux feeds (any reddit.com/r/<sub>/
URL — single source of truth, no duplicated config).
2. Reddit JSON top-of-day per sub. Filter: score >= 50,
upvote_ratio >= 0.85. Cap 8 items per sub.
3. Miniflux /v1/entries for the 'Tech aggregators' category
(HN, Lobste.rs) — last 12 hours.
4. Batched per-source summarization via llama-swap
/v1/chat/completions. Each post gets a one-sentence tldr +
one-word tag (news / tutorial / release / discussion /
question / showcase / drama / meme).
5. Render Jinja2 template. Atomic write to /output/index.html
(.tmp + rename) so partial pages never get served. Per-edition
archive at /output/edition-YYYY-MM-DD-{am,pm}.html.
Two containers:
news-digest-worker python:3.12-alpine + busybox crond
news-digest-web nginx:alpine, port 8181, homepage card via
docker labels (group=News, fits next to Miniflux)
Both bind-mount /opt/docker/data/news-digest as /output and
/usr/share/nginx/html respectively.
Aesthetic — operations-center chrome (Australis cool-mono palette,
JetBrains Mono UPPERCASE eyebrows, mdi-glyph anchor) wrapping
editorial-serif news content (Fraunces variable serif w/ optical
sizes). Two type families that wouldn't normally meet, intentionally
combined: chrome says 'filed at 0800 from the bridge'; headlines say
'this is news, read it like news.' Sticky aurora-glow rule under the
masthead is the only sanctioned Australis gradient.
Edition stamp (AM/PM in big mono Australis-yellow) is the signature
piece — establishes the twice-daily rhythm at a glance.
All filtering + LLM + scheduling knobs in .env. Subreddit list is
implicit (read from Miniflux), so adding a sub = subscribing in
Miniflux, no config edit on this stack.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# news-digest — twice-daily LLM-curated briefing.
|
||||
#
|
||||
# Two containers in this stack:
|
||||
#
|
||||
# news-digest-worker — python + cron, runs digest.py at 0800/2000
|
||||
# local, writes /output/index.html.
|
||||
# news-digest-web — tiny nginx serving the same /output dir.
|
||||
# Homepage card lives on this container's
|
||||
# labels.
|
||||
#
|
||||
# Both bind-mount the same host dir so the worker writes and the
|
||||
# web container serves without IPC. Worker writes atomically
|
||||
# (.tmp + rename), so partial pages never get served.
|
||||
|
||||
services:
|
||||
news-digest-worker:
|
||||
image: local/news-digest:${NEWS_DIGEST_TAG:-v1}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: news-digest-worker
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=${NEWS_DIGEST_TZ:-America/Los_Angeles}
|
||||
- LLAMA_SWAP_URL=${LLAMA_SWAP_URL:-http://10.250.50.54:9292}
|
||||
- LLAMA_SWAP_MODEL=${LLAMA_SWAP_MODEL:-qwen3.5-35-a3b}
|
||||
- LLAMA_SWAP_TIMEOUT=${LLAMA_SWAP_TIMEOUT:-180}
|
||||
- MINIFLUX_URL=${MINIFLUX_URL:-http://miniflux:8080}
|
||||
- MINIFLUX_USER=${MINIFLUX_USER:-lkraven}
|
||||
- MINIFLUX_PASSWORD=${MINIFLUX_PASSWORD}
|
||||
- DIGEST_OUTPUT_DIR=/output
|
||||
- DIGEST_TEMPLATE_DIR=/app/templates
|
||||
- DIGEST_REDDIT_HOURS=${DIGEST_REDDIT_HOURS:-12}
|
||||
- DIGEST_MIN_SCORE=${DIGEST_MIN_SCORE:-50}
|
||||
- DIGEST_MIN_RATIO=${DIGEST_MIN_RATIO:-0.85}
|
||||
- DIGEST_MAX_PER_SUB=${DIGEST_MAX_PER_SUB:-8}
|
||||
- DIGEST_MINIFLUX_HOURS=${DIGEST_MINIFLUX_HOURS:-12}
|
||||
- DIGEST_MINIFLUX_MAX=${DIGEST_MINIFLUX_MAX:-8}
|
||||
- DIGEST_MINIFLUX_TECH_CATEGORY=${DIGEST_MINIFLUX_TECH_CATEGORY:-Tech aggregators}
|
||||
volumes:
|
||||
- ${NEWS_DIGEST_OUTPUT_DIR}:/output
|
||||
networks:
|
||||
- tnet
|
||||
# Cron-driven worker — no healthcheck endpoint. The web container
|
||||
# is what users hit; if the worker dies we'll see stale content.
|
||||
# Restart policy handles transient crashes.
|
||||
|
||||
news-digest-web:
|
||||
image: nginx:alpine
|
||||
container_name: news-digest-web
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- news-digest-worker
|
||||
ports:
|
||||
- "${NEWS_DIGEST_BIND:-0.0.0.0}:${NEWS_DIGEST_PORT}:80"
|
||||
volumes:
|
||||
- ${NEWS_DIGEST_OUTPUT_DIR}:/usr/share/nginx/html:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost/ || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- tnet
|
||||
labels:
|
||||
- homepage.group=News
|
||||
- homepage.name=Daily Digest
|
||||
- homepage.icon=mdi-newspaper-variant-outline
|
||||
- homepage.description=LLM-curated briefing across feeds, twice daily
|
||||
- homepage.href=http://10.250.50.70:${NEWS_DIGEST_PORT}
|
||||
|
||||
networks:
|
||||
tnet:
|
||||
name: traefik-net
|
||||
external: true
|
||||
Reference in New Issue
Block a user