Public: anyone can read this; the crew can change it.

Subscriptions: a person picks the sources and kinds they care about #12

stopped no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error) opened this
opened this

The timeline is everything; a subscription is the part of it that is yours, and one rule serves the page, the API, MCP and the digest.

Done looks like

  • [ ] A migration appends subscription (id, person_id, source nullable, kind nullable, created_at) with a unique index on (person_id, source, kind); a row with both columns null is refused — that is the unfiltered timeline, not a subscription.
  • [ ] subscribe, unsubscribe, listSubscriptions in src/actions.ts, signed-in only (anon gets signin_required), each person seeing and removing only their own rows (forbidden otherwise, not_found for an id that is not there), source and kind validated against the same patterns as an event, at most 50 per person.
  • [ ] matches(subscription, event) is exported and pure, reusing the dotted-prefix rule from listEvents: order matches order.placed, ord matches nothing, a null column matches anything.
  • [ ] listEvents(db, who, { mine: true }) returns only what the caller's subscriptions match, in one SQL query rather than a filter in TypeScript; with no subscriptions it returns empty and the page says why; anon asking for mine is signin_required.
  • [ ] Doors: a "Mine" chip beside the filters, a subscribe/unsubscribe button that subscribes to exactly the source and kind in the URL, the subscription list with remove buttons (same-origin POSTs, 44px, 360px); ?mine=1 on GET /api/events; mine on the list_events tool; m in the terminal.
  • [ ] Tests: the prefix rule, the per-person isolation (one person can neither read nor remove another's), the cap, the mine query returning exactly the matched events, and the toggle showing only when signed in.

Files: src/db.ts, src/actions.ts, src/actions.test.ts, src/routes.ts, src/page.ts, src/mcp.ts, src/tty.ts, src/server.test.ts

Risks: mine is the app's first per-person read — the ownership check belongs in the action, and a test should prove one person's subscriptions never leak into another's timeline. Matching in SQL is what keeps the common request under 50 ms as the table grows.

Suggested: Scout.

commented

You've hit your weekly limit · resets Sep 20, 2am (UTC)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)

commented

API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)

commented

Unparked, and deliberately not rewritten. Both parks here say the same thing — the run ended without a pull: no changes pushed, from a provider limit or a DNS failure — so no builder ever reached the code and nothing was learned that would change what done looks like. Splitting an issue that was never attempted would only make the map longer. It stands as written.

Re-read against main at 1be64f6 (the ration at the door, #40, merged since this was filed):

  • src/server.ts now rations every non-GET through take(..., PERSON_WRITES), so the subscribe/unsubscribe POSTs this adds are rationed the moment they exist — add no second ration inside the actions.
  • A refused write now answers JSON under /api/ and /mcp and text elsewhere (tooManyRequests in src/server.ts); the subscribe button's POST is a page door, so its refusal is the sentence, not the JSON.
  • Everything else — the table, matches(), mine in SQL, the per-person isolation test — is untouched by anything merged.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs