news-digest/README: document multi-tenant onboarding + per-user cron

Adds a "Customizing the run schedule" section (DIGEST_CRON_AM/PM env
vars, edit-and-recreate flow) and a "Multi-tenant: one instance per
teammate" section covering scripts/add-digest-user.sh end to end:
what it does, the per-user file layout on ana-docker, idempotent
schedule/password updates, and the teardown path.

Updated the stale "two editions per day" intro line to note the
schedule is now configurable.
This commit is contained in:
2026-04-28 15:25:04 -07:00
parent d552e289cb
commit 5876399352
+126 -3
View File
@@ -27,8 +27,10 @@ noisy. This stack:
(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`.
Two editions per day by default (0800 / 2000 local), parametrized via
`DIGEST_CRON_AM` / `DIGEST_CRON_PM` env so each per-user instance can
fire on its own schedule. Plus per-edition archives at
`/edition-YYYY-MM-DD-{am,pm}.html`.
## Architecture
@@ -127,12 +129,133 @@ with tool/JSON-mode support give better summarization quality;
## Forcing a fresh digest now
```bash
ssh ana-docker 'docker exec news-digest-worker python3 /app/digest.py'
ssh ana-docker 'docker exec news-digest-worker /usr/local/bin/run-digest.sh'
```
Runs the full pipeline once, ignoring cron. Useful after changing
filtering knobs or adding feeds.
## Customizing the run schedule
Times come from two env vars on the worker, written into the busybox
crontab at container start. Standard 5-field cron syntax.
| Var | Default | Effect |
|---|---|---|
| `DIGEST_CRON_AM` | `0 8 * * *` | morning fire |
| `DIGEST_CRON_PM` | `0 20 * * *` | evening fire |
`busybox crond` honors `$NEWS_DIGEST_TZ` (defaults to
`America/Los_Angeles`), so values are interpreted in the configured TZ.
```bash
# Shift the canonical instance to 7am / 6pm
ssh ana-docker '
cd /opt/docker/compose/news-digest
sed -i "s|^DIGEST_CRON_AM=.*|DIGEST_CRON_AM=0 7 * * *|" .env
sed -i "s|^DIGEST_CRON_PM=.*|DIGEST_CRON_PM=0 18 * * *|" .env
docker compose up -d --force-recreate news-digest-worker
'
```
Verify the rendered crontab:
```bash
ssh ana-docker 'docker exec news-digest-worker cat /etc/crontabs/root'
```
## Multi-tenant: one instance per teammate
Architecture: **shared miniflux + per-user digest stack**. Miniflux
already supports multi-user natively (each user has their own feeds,
categories, hide-state); we layer a separate news-digest stack per
user on its own port + output dir, scoped to that miniflux user's
credentials.
### Onboarding a new user
```bash
# Defaults — 8am / 8pm local, random password
scripts/add-digest-user.sh alice
# Custom hours
scripts/add-digest-user.sh bob --am "0 6 * * *" --pm "0 17 * * *"
# Weekday-only PM run
scripts/add-digest-user.sh carol --pm "30 18 * * 1-5"
# Pin a known password (still creates the miniflux user if missing)
scripts/add-digest-user.sh dan 'pickyourpassword' --am "0 9 * * *"
```
What the script does:
1. Reads miniflux admin creds from
`ana-docker:/opt/docker/compose/miniflux/.env`.
2. Allocates the next free `NEWS_DIGEST_PORT` (scans existing
`news-digest` + `digest-*` `.env` files).
3. Creates the miniflux user via the admin API. Already-exists is
non-fatal (kept; password not reset).
4. Provisions per-user dirs at
`/opt/docker/compose/digest-<user>/` and
`/opt/docker/data/digest-<user>/` (one-time sudo prompt — the
script falls back to printing the manual command if there's no TTY).
5. Materializes a per-user `.env` (inherits `NEWS_DIGEST_TAG` from the
canonical stack so all tenants run the same image).
6. Brings the stack up via `docker compose -p digest-<user> up -d`.
7. Runs `seed-headlines.py` against miniflux as the new user (creates
the World + Local categories with default feeds).
8. Triggers a first digest run so the page isn't blank.
Outputs the digest URL, miniflux login, and rendered cron schedule.
### Per-user file layout
```
ana-docker:
/opt/docker/compose/digest-<user>/ # compose + .env + build context
.env # auto-generated, contains MINIFLUX_PASSWORD
compose.yaml
Dockerfile + digest.py + ... # build context (image is shared/cached)
/opt/docker/data/digest-<user>/ # rendered HTML + per-user hidden.json
index.html
edition-YYYY-MM-DD-{am,pm}.html
hidden.json
.article-cache.json # extracted article text, 7-day TTL
```
Container names: `digest-<user>-worker` and `digest-<user>-web`.
### Updating an existing user's schedule
Re-running the script with the same username is idempotent:
```bash
# Change alice's evening run to 5:30pm
scripts/add-digest-user.sh alice --pm "30 17 * * *"
```
The miniflux user is kept (password unchanged), the .env is
re-materialized with the new schedule, and the worker container is
recreated. Use the same flow to bump cron times, rotate passwords
(by passing a new one explicitly), or rerun feed seeding.
### Removing a user
```bash
ssh ana-docker '
cd /opt/docker/compose/digest-alice
docker compose -p digest-alice down -v
'
# Optional: nuke compose dir + rendered output
ssh -t ana-docker 'sudo rm -rf /opt/docker/compose/digest-alice /opt/docker/data/digest-alice'
# Optional: delete the miniflux user via the admin UI at http://10.250.50.70:8080/
```
The digest is gone immediately; the miniflux account stays around
unless you delete it explicitly (cheap to leave; ~zero resource cost
when no stack is querying its feeds).
## Logs
```bash