news-digest: per-item × button + cross-device hidden tray
Adds a small × on each item that hides it from the page. State is
server-side at /output/hidden.json so the same hidden set follows
the user across devices (home, ipad, laptop, work). A "Hidden (N)"
tray at the bottom shows what's hidden on the current page with a
restore button per row; older hidden ids that aren't on this page
sit silently and continue to filter future editions that include
the same article.
Architecture change: news-digest-web swaps from nginx:alpine to a
FastAPI app on uvicorn, built from the same Dockerfile as the
worker. Same image, different command (`uvicorn web:app` overrides
the worker's cron entrypoint via compose). Drops one image dependency,
adds /api/{hidden,hide,restore}.
Item ids are stable 12-char sha1 prefixes (`reddit:<post_id>` /
`miniflux:<entry_id>`) computed in digest.py at render time and
emitted as `data-id` on each .item. The frontend reads /api/hidden
once on load, applies `is-hidden` to matching items, and POSTs
hide/restore on user interaction (optimistic, with rollback on
network error).
Storage: single JSON array at /output/hidden.json, atomic writes
via tempfile + rename, threading.Lock around the read-modify-write
inside the single uvicorn worker. No auth — the digest itself is
unauthenticated on LAN; same trust boundary applies.
Playbook also drops the DOCKER_BUILDKIT=0 fallback now that
ana-docker is on docker-ce 29, and adds three verify steps
(/api/hidden returns a JSON array, app.js is reachable, full
hide/restore round-trip with a synthetic id).
This commit is contained in:
@@ -22,20 +22,21 @@ noisy. This stack:
|
||||
/ showcase / drama / meme / other).
|
||||
5. Renders an HTML page styled with Australis tokens + Fraunces
|
||||
serif headlines.
|
||||
6. Static page is served by a tiny `nginx:alpine`. Cron writes
|
||||
`/output/index.html` atomically; nginx serves whichever copy is
|
||||
there.
|
||||
6. The page is served by a tiny FastAPI app on uvicorn that also
|
||||
exposes `/api/{hidden,hide,restore}` for the per-item × button
|
||||
(state in `/output/hidden.json`, shared across every device the
|
||||
user opens the digest from).
|
||||
|
||||
Two editions per day: 8am and 8pm local. Plus per-edition archives
|
||||
at `/edition-YYYY-MM-DD-{am,pm}.html`.
|
||||
|
||||
## Architecture
|
||||
|
||||
Two containers, both on `traefik-net`, sharing a bind-mounted
|
||||
output dir:
|
||||
Two containers built from the same Dockerfile, both on `traefik-net`,
|
||||
sharing a bind-mounted output dir:
|
||||
|
||||
```
|
||||
news-digest-worker (python:3.12-alpine + cron)
|
||||
news-digest-worker (default ENTRYPOINT — busybox crond)
|
||||
├── busybox crond fires at 0 8,20 * * *
|
||||
├── digest.py:
|
||||
│ ├── miniflux /v1/feeds → discover subreddits
|
||||
@@ -44,13 +45,24 @@ news-digest-worker (python:3.12-alpine + cron)
|
||||
│ ├── llama-swap /v1/chat/completions → batched per source
|
||||
│ └── jinja2 render → /output/index.html (atomic .tmp + rename)
|
||||
│ → /output/edition-2026-04-26-pm.html
|
||||
└── style.css served from /output (copied at deploy)
|
||||
└── style.css / favicon.svg / app.js staged in /output at deploy
|
||||
|
||||
news-digest-web (nginx:alpine)
|
||||
└── serves /output as / on host port 8181
|
||||
└── homepage card via container labels (group=News)
|
||||
news-digest-web (entrypoint overridden → uvicorn web:app)
|
||||
├── / → serve /output as static (index.html as default)
|
||||
├── /api/hidden GET → JSON array of hidden item ids
|
||||
├── /api/hide POST → {id} → adds id to hidden.json
|
||||
├── /api/restore POST → {id} → removes id from hidden.json
|
||||
├── /output/hidden.json — durable state (atomic writes + threading lock)
|
||||
└── homepage card via container labels (group=News)
|
||||
```
|
||||
|
||||
Hidden state is server-side and global per-user (single-user setup):
|
||||
hide an article once and it stays hidden in any future edition that
|
||||
includes the same article. The "Hidden (N)" tray at the bottom of
|
||||
each page shows items hidden FROM THE CURRENT PAGE; older hidden ids
|
||||
that aren't present on this page just sit silently in `hidden.json`
|
||||
and continue to filter future editions.
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user