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:
@@ -1,16 +1,19 @@
|
||||
# news-digest — twice-daily LLM-curated briefing.
|
||||
#
|
||||
# Two containers in this stack:
|
||||
# Two containers in this stack, both built from the same Dockerfile:
|
||||
#
|
||||
# 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.
|
||||
# local, writes /output/index.html and
|
||||
# /output/edition-*.html.
|
||||
# news-digest-web — FastAPI on uvicorn, serves /output as static
|
||||
# and exposes /api/{hidden,hide,restore} for
|
||||
# the per-item × button (state in
|
||||
# /output/hidden.json, shared across devices).
|
||||
# Homepage card lives on this container.
|
||||
#
|
||||
# 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.
|
||||
# Same bind-mounted /output for both: worker writes HTML, web reads
|
||||
# it back. Worker uses atomic writes (.tmp + rename) so partial pages
|
||||
# never get served.
|
||||
|
||||
services:
|
||||
news-digest-worker:
|
||||
@@ -46,7 +49,10 @@ services:
|
||||
# Restart policy handles transient crashes.
|
||||
|
||||
news-digest-web:
|
||||
image: nginx:alpine
|
||||
image: local/news-digest:${NEWS_DIGEST_TAG:-v1}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: news-digest-web
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
@@ -54,7 +60,17 @@ services:
|
||||
ports:
|
||||
- "${NEWS_DIGEST_BIND:-0.0.0.0}:${NEWS_DIGEST_PORT}:80"
|
||||
volumes:
|
||||
- ${NEWS_DIGEST_OUTPUT_DIR}:/usr/share/nginx/html:ro
|
||||
# Read-write here so the API can persist hidden.json. Worker also
|
||||
# writes here (HTML); both processes serialize via filenames they
|
||||
# don't share, plus uvicorn's threading.Lock around hidden.json.
|
||||
- ${NEWS_DIGEST_OUTPUT_DIR}:/output
|
||||
environment:
|
||||
- DIGEST_OUTPUT_DIR=/output
|
||||
# Override the worker's cron entrypoint to launch uvicorn instead.
|
||||
# tini still wraps the process for clean signal forwarding.
|
||||
entrypoint: ["/sbin/tini", "--"]
|
||||
command: ["uvicorn", "web:app", "--host", "0.0.0.0", "--port", "80",
|
||||
"--no-access-log"]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost/ || exit 1"]
|
||||
interval: 30s
|
||||
|
||||
Reference in New Issue
Block a user