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:
vh
2026-04-26 13:14:13 -07:00
parent 8351e0c325
commit 2e80e69ef5
11 changed files with 1560 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
# news-digest stack tunables. Copy to `.env` on ana-docker before
# deploying and fill in MINIFLUX_PASSWORD.
# ── image pin ────────────────────────────────────────────────────────
# Bump when you change Dockerfile / digest.py / templates / crontab
# to force a clean image build.
NEWS_DIGEST_TAG=v1
# ── network ──────────────────────────────────────────────────────────
# Host port for the static web container (nginx). Container 80.
# Reserved on ana-docker (sample): 5001 Dockge, 7878 task-board,
# 8000 (other), 8025/8088/8090/8100/8380/8780 (various), 8080 Miniflux.
# 8181 free.
NEWS_DIGEST_PORT=8181
NEWS_DIGEST_BIND=0.0.0.0
# Timezone — busybox crond honors this for fire-times.
NEWS_DIGEST_TZ=America/Los_Angeles
# ── llama-swap (LLM summarizer) ──────────────────────────────────────
# Model picked for one-shot summarization quality + low VRAM impact.
# qwen3.5-35-a3b is loaded in the persistent group on ana-ml2.
LLAMA_SWAP_URL=http://10.250.50.54:9292
LLAMA_SWAP_MODEL=qwen3.5-35-a3b
LLAMA_SWAP_TIMEOUT=180
# ── miniflux (feed source for Tech aggregators + subreddit list) ─────
# In-cluster: miniflux container is on traefik-net so we can reach it
# by container hostname. From-host fallback: http://10.250.50.70:8080
MINIFLUX_URL=http://miniflux:8080
MINIFLUX_USER=lkraven
MINIFLUX_PASSWORD=CHANGE_ME
# Category in Miniflux holding non-Reddit feeds (HN, Lobste.rs, etc).
DIGEST_MINIFLUX_TECH_CATEGORY=Tech aggregators
# ── filtering knobs ──────────────────────────────────────────────────
# Reddit: only consider posts created in the last N hours, with
# at least N upvotes and an upvote ratio above threshold.
DIGEST_REDDIT_HOURS=12
DIGEST_MIN_SCORE=50
DIGEST_MIN_RATIO=0.85
DIGEST_MAX_PER_SUB=8
# Miniflux Tech aggregators: same look-back window + cap per source.
DIGEST_MINIFLUX_HOURS=12
DIGEST_MINIFLUX_MAX=8
# ── output dir on host (bind-mounted) ────────────────────────────────
# Separate from /opt/docker/conf/<stack>/ to keep generated content
# distinct from config. Owned by container UID; writes are atomic.
NEWS_DIGEST_OUTPUT_DIR=/opt/docker/data/news-digest