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,132 @@
|
||||
# Deploy news-digest (LLM-curated daily briefing) to ana-docker.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/elway ana-docker --playbook playbooks/deploy-news-digest.yaml
|
||||
#
|
||||
# Idempotent — every step is creates-/when-gated; rerun is safe.
|
||||
#
|
||||
# After first deploy, the .env still has CHANGE_ME for the Miniflux
|
||||
# password. Edit it (see stacks/news-digest/README.md), then
|
||||
# `docker compose up -d` again — the container will run the first
|
||||
# digest at startup if /output is empty, so no need to wait for cron.
|
||||
|
||||
vars:
|
||||
compose_dir: /opt/docker/compose/news-digest
|
||||
output_dir: /opt/docker/data/news-digest
|
||||
host_port: "8181"
|
||||
|
||||
steps:
|
||||
# ── host-side dirs ──────────────────────────────────────────────────
|
||||
|
||||
- name: Ensure compose dir exists
|
||||
shell: mkdir -p {{ compose_dir }}
|
||||
creates: "{{ compose_dir }}"
|
||||
|
||||
- name: Ensure output dir exists (bind-mounted into both containers)
|
||||
shell: mkdir -p {{ output_dir }}
|
||||
creates: "{{ output_dir }}"
|
||||
|
||||
# ── deploy build context (compose, env, dockerfile, app, templates) ──
|
||||
|
||||
- name: Upload compose.yaml
|
||||
upload:
|
||||
src: stacks/news-digest/compose.yaml
|
||||
dest: "{{ compose_dir }}/compose.yaml"
|
||||
mode: "0644"
|
||||
|
||||
- name: Upload Dockerfile
|
||||
upload:
|
||||
src: stacks/news-digest/Dockerfile
|
||||
dest: "{{ compose_dir }}/Dockerfile"
|
||||
mode: "0644"
|
||||
|
||||
- name: Upload digest.py
|
||||
upload:
|
||||
src: stacks/news-digest/digest.py
|
||||
dest: "{{ compose_dir }}/digest.py"
|
||||
mode: "0644"
|
||||
|
||||
- name: Upload entrypoint.sh
|
||||
upload:
|
||||
src: stacks/news-digest/entrypoint.sh
|
||||
dest: "{{ compose_dir }}/entrypoint.sh"
|
||||
mode: "0755"
|
||||
|
||||
- name: Upload run-digest.sh
|
||||
upload:
|
||||
src: stacks/news-digest/run-digest.sh
|
||||
dest: "{{ compose_dir }}/run-digest.sh"
|
||||
mode: "0755"
|
||||
|
||||
- name: Upload crontab
|
||||
upload:
|
||||
src: stacks/news-digest/crontab
|
||||
dest: "{{ compose_dir }}/crontab"
|
||||
mode: "0644"
|
||||
|
||||
- name: Ensure templates dir exists
|
||||
shell: mkdir -p {{ compose_dir }}/templates
|
||||
creates: "{{ compose_dir }}/templates"
|
||||
|
||||
- name: Upload templates/digest.html.j2
|
||||
upload:
|
||||
src: stacks/news-digest/templates/digest.html.j2
|
||||
dest: "{{ compose_dir }}/templates/digest.html.j2"
|
||||
mode: "0644"
|
||||
|
||||
- name: Upload templates/style.css
|
||||
upload:
|
||||
src: stacks/news-digest/templates/style.css
|
||||
dest: "{{ compose_dir }}/templates/style.css"
|
||||
mode: "0644"
|
||||
|
||||
- name: Seed .env from template (only if absent)
|
||||
upload:
|
||||
src: stacks/news-digest/.env.example
|
||||
dest: "{{ compose_dir }}/.env"
|
||||
mode: "0644"
|
||||
when: "[ ! -f {{ compose_dir }}/.env ]"
|
||||
|
||||
# ── build + bring up ────────────────────────────────────────────────
|
||||
# The Dockerfile expects style.css to be inside the image too (it's
|
||||
# copied via `COPY templates /app/templates`). nginx serves the
|
||||
# generated index.html alongside its own copy of style.css from
|
||||
# /output, so the worker writes a copy of style.css into /output too.
|
||||
|
||||
- name: docker compose build (~2-3 min first time)
|
||||
shell: |
|
||||
set -o pipefail
|
||||
cd {{ compose_dir }} && docker compose build --progress=plain 2>&1 \
|
||||
| grep -vE '^#[0-9]+ [0-9.]+ (Downloading|Collecting|Requirement|Using cached|Installing collected|Successfully (installed|built)|Saved /|━|Resolved|Prepared|Built)'
|
||||
|
||||
- name: Pre-stage style.css into /output so the first page render works
|
||||
# Worker writes index.html which references "style.css" (relative).
|
||||
# The CSS isn't generated by the worker; nginx serves whichever
|
||||
# copy lands at /usr/share/nginx/html/style.css. Copy the template
|
||||
# CSS into the bind-mounted /output dir at deploy-time.
|
||||
shell: cp -f {{ compose_dir }}/templates/style.css {{ output_dir }}/style.css
|
||||
|
||||
- name: docker compose up -d
|
||||
shell: cd {{ compose_dir }} && docker compose up -d
|
||||
|
||||
- name: Wait for nginx to serve /
|
||||
shell: |
|
||||
for i in $(seq 1 30); do
|
||||
curl -sf -o /dev/null --max-time 3 http://localhost:{{ host_port }}/ && exit 0
|
||||
sleep 2
|
||||
done
|
||||
exit 1
|
||||
changed_when: "false"
|
||||
|
||||
verify:
|
||||
- name: nginx returns 200 on /
|
||||
shell: curl -sf -o /dev/null http://localhost:{{ host_port }}/
|
||||
changed_when: "false"
|
||||
|
||||
- name: Both containers running
|
||||
shell: docker inspect news-digest-worker news-digest-web --format '{{.State.Status}}' | grep -c running | grep -q '^2$'
|
||||
changed_when: "false"
|
||||
|
||||
- name: news-digest-web on traefik-net (homepage discovery)
|
||||
shell: docker inspect news-digest-web --format '{{json .NetworkSettings.Networks}}' | grep -q traefik-net
|
||||
changed_when: "false"
|
||||
Reference in New Issue
Block a user