Commit Graph
2 Commits
Author SHA1 Message Date
connor a001a09fdf Add Dockerfile and deployment for replacing news-backend in place
Two-stage build. The builder bakes the embedding model into the image (~65 MB),
so a cold container needs no outbound access to Hugging Face and starts scoring
in 0.4s -- verified with HF_HUB_OFFLINE=1.

Three things the smoke test caught that would each have failed in production:

- Debian's base image already ships a system account named "news", so useradd
  exits 9 on the collision. The runtime user is "app".
- The image runs as uid 10001 rather than root, so a host-owned volume gives
  SQLite "attempt to write a readonly database". Compose needs the `U` mount
  flag.
- The baked model cache was root-owned, making fastembed log a permission
  warning and discard its tree cache on every start.

Deployment targets the EXISTING service rather than a parallel one: same vhost,
and NTFY_TOPIC=news-triage because the old service defaulted to that rather than
setting it, so the phone subscription keeps working.

The Caddy change is the consequential one and is documented as such: the vhost
moves from LAN-only to public, with /api/feedback/* bypassing Authelia because
ntfy runs action buttons from the phone and a native app cannot complete an
interactive login -- the same exemption ntfy, Immich and Jellyfin already have
on this box. That path is HMAC-signed per (cluster, kind).
2026-09-08 16:38:10 -04:00
connor dc6b6fea7e Phase 4: delivery -- ntfy, working feedback buttons, digest page
ntfy push is deliberately short: a title, one line, and a Click through to the
story. The digest is a WEB PAGE, not a push payload -- the old system sent the
whole report as one long markdown message and ntfy turned it into a .md
attachment, which is what made it miserable to read on a phone.

Feedback buttons finally work. The old ones never fired once: NEWS_API_BASE_URL
was unset so they were dropped behind a single boot WARN, and /api/feedback had
no auth anyway. ntfy runs http actions FROM THE PHONE, so the endpoint must be
public -- which is why each button now carries an HMAC signed over
(cluster_id, kind). A leaked URL authorises one vote on one story rather than
acting as a bearer token for the whole model. Missing config drops the buttons
loudly instead of silently.

Digest prose is one LLM call per day through LiteLLM, and that is the entire
generative-model surface of this system -- clustering, relevance, importance and
vetoes are all embeddings and arithmetic. Feed text is passed as JSON data,
never interpolated into prompt prose, so a headline cannot read as an
instruction. Any failure falls back to the plain list; the digest never depends
on a model being reachable. Verified against the live proxy: 20s, themed
paragraphs, no invented facts.

Digest page is mobile-first, dark-mode aware, and renders from notification_log
so it is a record of what was actually sent.

deploy/README.md documents the Caddy vhost and env the buttons require, and uses
a python healthcheck rather than curl -- the v0.2 image shipped no curl, so its
healthcheck failed forever while the service was fine.

62 tests.
2026-09-08 16:27:27 -04:00