M489 step 2.
- GET /api/me/notifications?limit&before: newest first, keyset-paged on
(created_at, id) with an opaque cursor, plus the unread count.
- GET /api/me/notifications/unread-count: the badge's cheap call.
- POST /api/me/notifications/{id}/read and /read-all. Mark-read is
idempotent; another user's id is a 404, the same as a malformed one.
- GET/PUT /api/me/notification-settings: every kind the caller can receive
(admin kinds only for admins) with inbox/phone/email. PUT is partial, so an
offline replay sends only what was touched, and a batch with any invalid
change applies nothing. The response says whether email can be delivered
at all: no address on file, or SMTP not configured. A failed SMTP config
read is a 500, not "not configured".
notifications.Render turns kind + payload into title, body and link on the
server, so the web inbox, the Android inbox, the phone's shade and the email
digest all say the same thing. mailer.Configured lifts Send's readiness
check out so settings can report it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
M489 step 1. The event bus is fire-and-forget, so a client that isn't
connected never hears that a request completed or that tracks went missing.
user_notifications is the durable record; the bus only nudges.
- Migration 0073: user_notifications (kind CHECK-gated, payload jsonb,
read_at, coalesce_key, emailed_at) and user_notification_prefs (per user,
per kind: inbox, phone, email). A missing pref row means the kind's
defaults, so nothing is seeded.
- internal/notifications.Notifier is the only writer. It resolves recipients
(admin kinds reach admins only, and never the excepted user), honours the
inbox pref (phone and email ride on it), writes, and publishes a
contentless notification.created nudge per recipient.
- Burst-prone admin kinds coalesce into one unread row: tracks_missing and
scan_failed add up their counts, duplicates_found and playback_errors take
the latest total. Once read, the next event is a new row.
- Retention: read rows go after 90 days, anything after a year, on the
library_changes compactor's daily shape.
Tests: unit (channel rules, kind table) and integration (recipients, nudge,
coalescing both ways, prefs, owner-scoped idempotent mark-read, every kind
against both schema CHECKs, retention cut-offs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>