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).
42 lines
1.6 KiB
Docker
42 lines
1.6 KiB
Docker
# news-digest — base image for two containers in this stack:
|
|
#
|
|
# news-digest-worker — runs alpine's busybox crond + the one-shot
|
|
# digest.py per fire (default ENTRYPOINT).
|
|
# news-digest-web — runs uvicorn web:app (overridden in compose)
|
|
# to serve /output as static + the tiny
|
|
# hidden-items API at /api/*.
|
|
#
|
|
# Single image, two roles selected via compose `command:`.
|
|
# Bind-mounted /output is the shared canvas: worker writes HTML, web
|
|
# serves it.
|
|
|
|
FROM python:3.12-alpine
|
|
|
|
ENV PYTHONUNBUFFERED=1 \
|
|
PYTHONDONTWRITEBYTECODE=1 \
|
|
PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
|
|
# tzdata so $TZ works for cron + datetime; tini so signals propagate cleanly.
|
|
RUN apk add --no-cache tzdata tini bash
|
|
|
|
# fastapi + uvicorn[standard] for the web container; requests + jinja2
|
|
# for the worker. Both shipped in both containers — neither set is
|
|
# heavy enough to justify splitting the image.
|
|
RUN pip install --no-cache-dir requests jinja2 'fastapi>=0.115' 'uvicorn[standard]>=0.30'
|
|
|
|
WORKDIR /app
|
|
COPY digest.py /app/digest.py
|
|
COPY web.py /app/web.py
|
|
COPY templates /app/templates
|
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
COPY run-digest.sh /usr/local/bin/run-digest.sh
|
|
COPY crontab /etc/crontabs/root
|
|
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/run-digest.sh
|
|
|
|
# Sentinel + first-run output dir
|
|
VOLUME /output
|
|
|
|
# Default ENTRYPOINT runs the worker (cron). The web container in
|
|
# compose overrides both entrypoint and command to launch uvicorn.
|
|
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/entrypoint.sh"]
|