Implements the notification system foundation and all v1 delivery channels
that need no external infrastructure (specs 00/01/04/05 in
docs/superpowers/plans/notifications/):
Foundation (spec 01):
- episode_availability seeding + per-library seed markers: "newly available"
means newly released to this server, so back-catalog imports and first
scans never flood (verified on dev: 1.13M episodes seeded silently)
- release_events -> profile_series_interest fanout worker with settling
delay, per-series burst caps, FOR UPDATE SKIP LOCKED multi-node claims,
and a guarded last-notified cursor
- interest index maintained via a userstore provider decorator so every
favorites/watchlist/progress mutation path (REST, jellycompat, imports,
playback) feeds it; progress writes only recompute on state transitions
- durable per-profile inbox + read state, forward-sync cursor API,
websocket channel with short-lived single-use handshake tickets
- web UI: sidebar badge, inbox page, toasts, per-profile preferences
- startup/daily tasks: availability seeding, interest rebuild, retention
Outbound webhooks (spec 04):
- Discord embeds (text-only per the v1 privacy contract) and generic
JSON signed Stripe-style with per-webhook secrets
- HTTPS-only + private-destination guard enforced at registration and at
connect time (DNS-rebinding mitigation); URLs/secrets encrypted at rest
- durable per-target outbox enqueued in the fanout transaction, lease-based
claims, 24h exponential retry, 3x-consecutive-4xx auto-disable with an
in-app notice (loop-guarded)
Web push (spec 05):
- VAPID keypair self-provisioned at startup (single atomic JSON setting,
private half encrypted at rest) — no third-party accounts needed
- payloads E2E-encrypted (RFC 8291); 404/410 treated as unsubscribe
- service worker + subscribe flow in Settings -> Notifications
Shared SMTP core (internal/mail):
- feature-agnostic mail.Sender over live email.* settings, STARTTLS or
implicit TLS, encrypted password, admin Email settings page with
synchronous test send; no consumer yet by design (digest is v1.5)
APNs/FCM (specs 02/03) are deferred to v2; the capability endpoint reports
them unavailable so clients render truthfully.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): show admin chrome only on the admin account's primary profile
The top-right ServerActivity indicator and the sidebar Admin section were
gated on the account-level role alone, so every profile on an admin
account — including child profiles — saw admin system notifications and
the indicator polled four admin endpoints on their behalf. Gate both on
the active profile being the household primary, matching the existing
is_primary idiom in SettingsLayout and the server-side quota exemption.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): resolve active profile via useCurrentProfile in admin route gates
RequireAdmin/RequirePrimaryOrAdmin read the profile from useAuth(), but the
admin chrome (AppSidebar, Layout) gates on useCurrentProfile(), which resolves
the selected profile. Use the same source in the route gates so the redirect
and the visible admin UI can never disagree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web,api): centralize acting-admin policy and enforce it server-side
Address code-review findings on the primary-profile admin gate:
- Add isActingAdmin to web/src/lib/permissions.ts as the single
client-side definition of the policy (admin role + primary or no
profile), with a useIsActingAdmin hook on top. Route gates, sidebar,
Layout, and realtime channel gating all use it now, so the gate and
the chrome can no longer disagree on null-profile handling.
- Convert the admin-gated surfaces the original change missed
(MediaItemMenu, EditMetadataDialog images tab, AddToCollectionDialog,
MarkerEditor, theme CatalogBrowser, PersonDetail, SettingsLayout,
ItemDetail content pages) so an admin on a non-primary profile is a
regular viewer everywhere, not just in the sidebar.
- Make the role-derived permission bypass (metadata curation, marker
edit) follow the same policy on both client and server.
- Enforce the policy server-side: RequireActingAdmin middleware refuses
admin routes when the request declares a non-primary profile via
X-Profile-Id, and the metadata-curation middleware holds admins on
non-primary profiles to explicitly assigned permissions.
- Stop spreading the profiles query result from useCurrentProfile so
route gates only re-render when the resolved profile changes, and
make it safe outside AuthProvider.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web,api): fail closed on unresolved profiles in acting-admin policy
Address review feedback on the acting-admin gate:
- Server: actingAdminAllowed now denies when the declared profile cannot
be resolved to one of the caller's profiles, so a bogus X-Profile-Id
can no longer restore admin powers to a non-primary session.
- Client: useIsActingAdmin returns false while a selected profile id has
not yet resolved (e.g. hard refresh before the profiles query
returns), instead of briefly treating it as "no profile selected".
useCurrentProfile exposes hasSelectedProfile to make that state
distinguishable.
- hasPermission/canCurateMetadata/canEditMarkers now require the profile
argument (resolved profile or explicit null), so a missed call site
fails the typecheck instead of silently restoring the admin bypass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>