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.
This commit is contained in:
connor committed 2026-09-08 16:27:27 -04:00
1 parent b852840729
commit dc6b6fea7e
9 files changed
+829

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
# Deploying
Three things must be true before the system is useful, and the third is the one
the previous version got wrong.
## 1. Compose service
```yaml
news:
image: git.rcjohnstone.com/connor/news:latest
restart: unless-stopped
networks: [internal]
environment:
- TZ=America/Louisville
- NEWS_DB=/data/news.db
- NEWS_CONFIG_DIR=/config
# ntfy. The topic must differ from news_triage's during the parallel run.
- NTFY_URL=${NTFY_URL}
- NTFY_TOPIC=news2
- NTFY_USER=${NTFY_USER}
- NTFY_PASS=${NTFY_PASS}
# REQUIRED for feedback buttons -- see section 3.
- NEWS_API_BASE_URL=https://news2.rcjohnstone.com
- NEWS_FEEDBACK_SECRET=${NEWS_FEEDBACK_SECRET}
# One LLM call per day, for the digest prose only.
- NEWS_LITELLM_KEY=${NEWS_LITELLM_KEY}
volumes:
- ./data/news2:/data
- ./config/news2:/config:ro
healthcheck:
# NOT curl. The v0.2 image shipped no curl, so its healthcheck failed
# forever while the service was fine and `podman ps` lied for four days.
test: ["CMD", "python", "-c",
"import urllib.request;urllib.request.urlopen('http://localhost:3000/healthz')"]
interval: 60s
timeout: 5s
retries: 3
start_period: 20s
```
Remember that a bind-mounted config change needs `sudo podman restart news`;
`podman-compose up -d` will not restart a container when only a mounted file
changed. An **environment** change does need the full `up -d`.
## 2. Caddy vhost
```
news2.rcjohnstone.com {
reverse_proxy news:3000
}
```
## 3. Why the feedback buttons need all of this
ntfy executes an action button's HTTP request **from the phone**, not from the
server. So a container-local address can never work, and this is exactly how the
previous version failed: `NEWS_API_BASE_URL` was unset, the buttons were dropped
behind a single boot WARN, and nobody noticed for a week. The result was 32
feedback labels in total -- for a system whose entire premise is learning from
feedback.
Because the endpoint is therefore public, every button 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. Generate the secret once:
```sh
openssl rand -hex 32 # -> NEWS_FEEDBACK_SECRET in ~/.env
```
`news notify` prints a loud warning and omits the buttons if either
`NEWS_API_BASE_URL` or `NEWS_FEEDBACK_SECRET` is missing, rather than shipping
inert buttons.
## 4. Scheduling
Two timers, or one loop inside the container:
```
*/15 * * * * news poll && news notify
45 8 * * * news digest
```
## 5. Running in parallel with news_triage
Use a different ntfy topic and a different vhost, and leave `news_triage`
running. Compare a week of both before retiring it.