Per-user read state and subscriptions

Read, starred and position move to entry_state; subscriptions carry each
person's keywords, auto-download, explicit and per-scan limit. The feed
list and unread counts are per person, and the existing library is
adopted by the admin on first start.

The feed URL, folder and schedule stay shared and admin-only: one file
serves everyone, so they describe the file rather than a preference.
Scanning merges subscribers' wants -- anyone wanting an item is enough --
via merge_policy, which is pure and tested.

Also: the test fixture wiped its data directory from every Playwright
worker, deleting the database out from under the running daemon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
2026-09-11 02:17:16 +00:00
parent 4810bb5cfb
commit d46ec73261
7 changed files with 707 additions and 93 deletions

View File

@@ -56,6 +56,38 @@ and until now nothing set them.
---
## 2026-09-11 — Steps B and C: what is yours, what is everyone's
Read, starred and playback position moved out of `entries` into `entry_state (user_id, feed_id,
guid, ...)`; subscriptions became rows in `subscriptions (user_id, feed_id, ...)` carrying **your**
keywords, auto-download, explicit and per-scan limit. The feed list, unread counts, filters and
mark-all-read are all per person now. On first start the existing library is adopted by the admin:
2438 read/starred items and all 86 feeds, so nothing was lost.
The split follows from the file being shared:
* **Yours**: read state, starred, position, keywords, auto-download, explicit, per-scan limit,
and which feeds you see at all.
* **Everyone's**: the feed URL, its download folder, and when it is scanned -- there is one copy of
a file however many people subscribe, so those describe the file, not a preference. Admin-only,
refused with a 403 for anyone else rather than merely hidden.
Scanning merges the subscribers' wants, because one fetch and one file serve them all: an item is
downloaded if **anyone** wants it (any one person's keyword set matching is enough, and one person
taking everything removes the filter), auto-download is on if anyone has it on, and the per-scan cap
is the largest anyone asked for. `merge_policy` is a pure function with a test covering each of
those. Subscribing to a feed someone already has costs no second fetch and no second copy on disk;
unsubscribing takes it off your list alone, and only when the last subscriber leaves does the feed
stop being scanned.
**A test-harness bug worth naming**: Playwright imports the config in every worker, so the fixture's
`prepare()` ran again mid-run and deleted the data directory out from under the daemon. The daemon
kept serving from the unlinked inode while the CLI and any query opened a fresh empty database at
the same path -- which looked exactly like sign-in being broken. Only the launching process wipes
now (a worker has `TEST_WORKER_INDEX`).
---
## 2026-09-10 — Scanning is the operator's decision
The per-feed **Check schedule** picker is gone from feed settings, and the global Settings page is
@@ -123,10 +155,10 @@ on disk. `enclosures.url` is already globally UNIQUE, so the file half is nearly
optionally creates a user -- honoured only from a `trusted_proxies` address, so a LAN client
cannot simply assert it. The existing shared token keeps working and resolves to the admin, so
the healthcheck and any scripts survive. Login page for direct access.
- [ ] **B. Per-user read state.** `entry_state(user_id, feed_id, guid, read, flagged, position)`;
- [x] **B. Per-user read state.** `entry_state(user_id, feed_id, guid, read, flagged, position)`;
the current columns on `entries` migrate into the first user's rows. Unread counts, filters and
playback position all become per user.
- [ ] **C. Per-user subscriptions.** `subscriptions(user_id, feed_id)`. config.toml stays the feed
- [x] **C. Per-user subscriptions.** `subscriptions(user_id, feed_id)`. config.toml stays the feed
catalogue; the UI lists only what you subscribe to. Adding a feed someone else already has costs
nothing. A feed nobody subscribes to stops being scanned but keeps its files.
- [ ] **D. One file, many users.** Auto-download when *any* subscriber wants it; retention never