Requests previously only notified the community server channels for
submitted/approved/declined and the requester personally for fulfilled.
This closes the gap and makes request posts addressable:
- New request.approved / request.declined delivery types ride the
operational dispatch path to the requesting profile: inbox, websocket
toast, email, Discord DM, personal webhooks (gated by the existing
notify_requests flag), and web push. Submitted stays broadcast-only
(the requester performed the action themselves). Title/year/decline
reason travel in reason_flags since no catalog item exists yet.
- Request status notices are transactional: digest-mode recipients get
an off-schedule early send (watermark-durable, last_digest_at left
alone) instead of waiting for the digest hour. Per-episode recipients
were already immediate via the dispatch nudge.
- At-most-once per (profile, request, type) via a partial unique index
(migration 20260612100000), mirroring the fulfilled dedupe.
- Server-channel Discord request posts can @mention the requester via
their OAuth-linked identity (notifications.server_channels.
mention_requesters, default off). Resolved lazily in the sweep worker
only when a Discord destination is about to receive the event; the
ping uses content-level mention with pinned allowed_mentions, and the
Discord identity never leaks into generic webhook payloads.
Android/Apple clients render the new inbox types with their generic
fallback until they add them.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- pin the four new sensitive setting keys (SMTP password, Discord
secret/bot token, VAPID keypair) in the encryption audit test so a
future drop from SensitiveSettingKeys fails CI
- bound account-channel digest drains strictly before the stamped
digest time so consecutive digest windows partition rows exactly,
instead of recapping rows created at or after the previous stamp
- keep the events websocket open when an event-frame snapshot fails,
matching the writeSnapshotFrame degrade-gracefully contract
- rename the seed task to Seed Content Availability to match its
episode+movie seeding behavior
- carry poster_source_path into realtime dispatch rows per the
DeliveryRow contract
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-key the email notification channel from login accounts to profiles.
Each profile owns its mode, dispatch watermark, and destination address;
there is deliberately no fallback to the account email, so the account
holder no longer receives mail for every household profile. A profile
receives nothing until its own address is verified.
- Genericize the watermark-sweep engine over a recipient key
(accountChannel[K]): email keys by profile_id, Discord stays on
user_id. Delivery reads move into the channel adapters.
- Custom addresses verify via single-use SHA-256-hashed token links
served by a public endpoint; enabling the channel requires a verified
address, and clearing the address switches the channel off.
- Addresses are globally unique (case-insensitive): rejected when
verified for another profile or matching another account's email or
username. Checked at request time, re-checked at verify time
(first-to-verify wins), backstopped by a partial unique index.
- Every email carries an RFC 8058 one-click unsubscribe link backed by
a per-profile capability token, minted lazily under the claim tx.
- Child profiles cannot set addresses (and so receive no email in v1).
- Verification sends are rate limited (1/min, 10/day per profile);
mail.Message gains custom header support for List-Unsubscribe.
- Migration drops the account-level prefs table without carrying
opt-ins over, so nobody gets surprise emails post-upgrade.
Android/Apple notification settings need follow-up for the new
profile-scoped response shape and address-management endpoints.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds Discord direct messages as a notification channel. Users link their
Discord account via OAuth2 (identify scope only, one-time server-side
state rows); a bot delivers their inbox notifications as DMs.
- Extract the email channel's watermark sweep into a generic
account-channel engine; email and Discord are now thin adapters, so
the SKIP LOCKED claim / watermark-after-send durability logic exists
once.
- New internal/discord REST client (token exchange, identity, open DM,
send message) — no Gateway connection, no new dependencies.
- Opt-in master switch (notifications.discord_enabled, default off)
gates delivery, linking, capability, and the admin settings reveal.
- Admin UI: credentials (secret + bot token encrypted at rest), dev
portal setup checklist, bot invite link buttons, and a test button
that bypasses the settings read cache and is disabled while
credential edits are unsaved.
- DM failures from missing shared guild (Discord 50007) surface as link
health in user settings and self-heal via capped backoff.
- New combined mode (per_episode_and_digest) for email and Discord:
instant sends all day plus a daily digest recapping the whole window
since the previous digest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds email as a notification channel built on the shared SMTP core
(mail.Sender). Email mode is a per-account preference (off, daily
digest, or per-episode) stored in notification_email_prefs; delivery is
an account-watermark sweep over notification_deliveries that dedupes
cross-profile duplicates, advancing the watermark only after a
successful send. Admin controls cover the channel kill switch, the
per-episode allowance (off coerces those accounts to the digest),
digest hour, and an external URL for deep links inside emails.
Availability is advertised through /notifications/capability and the
user settings page gains an Email section for opt-in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>