Commit graph

4 commits

Author SHA1 Message Date
Ullrich Schäfer
37073eafd7 Spec catchup: drift fixes, account-settings split, notifications archive
Drift (specs aligned to shipped code):
- social-follows: locked-account access rule for /users/:u/followers and
  /users/:u/following (owner + accepted-follower see; non-followers of
  private get 404). Adds the follow→notification lifecycle requirement.
  Fills the placeholder Purpose.
- public-profiles: counts degrade to plain text (not anchors) for viewers
  who can't see the lists. Cross-references social-follows. Fills the
  placeholder Purpose.
- journal-auth slimmed to cookie session + Terms gate. Auth methods moved
  out (see authentication-methods).

Splits:
- account-settings (14-line stub) deleted, content split into:
  - profile-settings (display name, bio, profile_visibility)
  - account-management (email change with verification, account deletion)
  - connected-services (Wahoo + future external integrations)
- authentication-methods split out of journal-auth: passkeys
  (register/login/add/delete), magic links, 6-digit codes
  (login + register), method toggle on register/login forms,
  dev-console fallback.

New specs:
- sse-broker: /api/events, in-process broker, useUnreadNotifications
  hook, Caddy passthrough, multi-process forward-compat contract.

Archived: notifications change → openspec/changes/archive/2026-04-26-notifications.
Promoted the four delta spec files into top-level specs:
- specs/notifications/ (new capability)
- specs/activity-feed/ (added: public activity fan-out)
- specs/journal-landing/ (added: Notifications navbar entry)
- specs/social-follows/ (added: follow→notification lifecycle)

Added openspec/CAPABILITIES.md grouped index covering all 40 specs with
a Conventions section explaining cross-references, naming, and the
catch-up-vs-change rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 02:02:43 +02:00
Ullrich Schäfer
b20c8cca39 Cursor-based pagination for /notifications
Switches `listForUser` from page-offset to cursor (`before` param,
opaque base64 of `{ts, id}`) ordered by `(created_at DESC, id DESC)`
for stable pagination even with simultaneous fan-out inserts.

Returns `{ rows, nextCursor }` instead of a bare array. Loader surfaces
`?before=<cursor>` on a "Load older" link at the bottom of the list,
shown only while `nextCursor !== null`. Default page size 50, capped
at 100. Malformed cursors fall back to "start from top" rather than
400ing — opaque cursors should not be a client validation surface.

Spec drift: delta spec adds three pagination scenarios (cursor pages
forward, tie-stable on identical `created_at`, malformed-cursor
graceful fallback). Design doc gets a new decision section explaining
the cursor choice over page-offset and why we don't compute totals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:42:32 +02:00
Ullrich Schäfer
e61179ab27 Implement notifications + supporting fixes
Adds the notifications system end-to-end (4 types, payload-versioned
JSONB, SSE-based live unread badge, /notifications page, mark-read API,
fan-out job for activity_published, daily 90-day retention purge).
Bell icon in the navbar with unread badge.

Side-findings from exercising the change:
- Add 6-digit magic code to registration (mirrors login UX, mobile
  paste-friendly), with `[Register Magic Link]` console line in dev so
  the code is reachable without a real email transport.
- Manual passkey/magic-link toggle on the register form (login already
  had it).
- Restrict ALPN to http/1.1 in HTTPS dev so React Router's
  singleFetchAction CSRF check (Origin vs. Host) passes — Node doesn't
  synthesize Host from h2's :authority. Plain HTTP dev unaffected.
- Followers/Following routes now use the locked-account rule from the
  profile route (owner + accepted followers see the list; others 404).
  Profile page renders the count chips as plain spans for viewers who
  can't see the lists, so private profiles don't surface dead links.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 01:28:55 +02:00
Ullrich Schäfer
95ac79b093 Propose: notifications
Closes the social loop opened by social-feed: a Pending follower has no
way to know their request was approved, and a follower has no signal at
all that someone they follow just posted. Adds a notifications surface
for three v1 event types — follow_request_approved, follow_received,
activity_published — plus a /notifications page, navbar unread count,
and mark-as-read controls.

Capabilities:
- New: notifications (table, page, badge, generation hooks)
- Modified: social-follows (approve + auto-accept emit)
- Modified: activity-feed (public create fans out)
- Modified: journal-landing (nav entry alongside follow-requests)

Design picks:
- Fan-out-on-write for activity_published (1:N) so /notifications is a
  flat single-table query and "mark read" composes trivially. 1:1
  events insert directly. Cost ceiling documented at 10k followers ×
  50 activities/day = 500k/day, still trivial; revisit only if hot.
- Single notifications table with loose subject_id (no per-type FK);
  renderer dereferences by type. Mastodon-style.
- Two distinct nav entries (Follow requests + Notifications). Pending
  is "act on this", notifications is "this happened" — different
  semantics, kept separate.
- Loader-driven unread count (no real-time channel). Real-time is
  deferred.
- 90-day retention for read rows; unread kept indefinitely.

Out of scope: per-type mute preferences, email/push, real-time,
notifications about routes/replies/mentions, federated notifications.
Each tracked as follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 00:12:35 +02:00