release / govulncheck (push) Successful in 42s
release / web (push) Successful in 1m34s
release / go (push) Successful in 1m51s
release / integration (push) Successful in 4m59s
release / android (push) Successful in 5m24s
release / Build signed APK (releases and dev) (push) Successful in 5m32s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m20s
release / Verify release artifacts (tag releases only) (push) Skipped
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>
75 lines
3.2 KiB
SQL
75 lines
3.2 KiB
SQL
-- M489: a persistent, per-user notifications inbox (#726).
|
|
--
|
|
-- The event bus is fire-and-forget: a client that is not connected when a
|
|
-- request completes, or when the scanner marks tracks missing, never hears of
|
|
-- it. A row here is the durable record; the bus only nudges open clients to
|
|
-- come and read it.
|
|
--
|
|
-- `kind` is CHECK-gated (rule 36). A new kind adds its value here, in the
|
|
-- same migration as the code that produces it. The list is mirrored in
|
|
-- internal/notifications/kinds.go, and both CHECKs below must agree with it.
|
|
|
|
CREATE TABLE user_notifications (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
kind text NOT NULL CHECK (kind IN (
|
|
'request_approved',
|
|
'request_rejected',
|
|
'request_completed',
|
|
'request_pending',
|
|
'quarantine_flagged',
|
|
'scan_failed',
|
|
'tracks_missing',
|
|
'duplicates_found',
|
|
'playback_errors'
|
|
)),
|
|
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
read_at timestamptz,
|
|
-- Burst-prone kinds share one key per user. While a row with that key is
|
|
-- unread, a new event updates it in place rather than adding another, so
|
|
-- fourteen missing tracks are one notification with a count.
|
|
coalesce_key text,
|
|
-- Stamped once the row has gone out in an email (or been judged not to
|
|
-- need one), so the digest never sends the same item twice.
|
|
emailed_at timestamptz
|
|
);
|
|
|
|
-- The inbox list, newest first.
|
|
CREATE INDEX user_notifications_user_created_idx
|
|
ON user_notifications (user_id, created_at DESC);
|
|
|
|
-- The unread badge, and the digest's selection of unread rows.
|
|
CREATE INDEX user_notifications_unread_idx
|
|
ON user_notifications (user_id, created_at DESC)
|
|
WHERE read_at IS NULL;
|
|
|
|
-- At most one unread row per coalesce key per user. The upsert in
|
|
-- notifications.sql targets this index.
|
|
CREATE UNIQUE INDEX user_notifications_coalesce_idx
|
|
ON user_notifications (user_id, coalesce_key)
|
|
WHERE read_at IS NULL AND coalesce_key IS NOT NULL;
|
|
|
|
-- Per user, per kind: which channels a kind reaches. A missing row means the
|
|
-- kind's defaults (internal/notifications/kinds.go), so nothing is seeded and
|
|
-- a new kind needs no backfill.
|
|
CREATE TABLE user_notification_prefs (
|
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
kind text NOT NULL CHECK (kind IN (
|
|
'request_approved',
|
|
'request_rejected',
|
|
'request_completed',
|
|
'request_pending',
|
|
'quarantine_flagged',
|
|
'scan_failed',
|
|
'tracks_missing',
|
|
'duplicates_found',
|
|
'playback_errors'
|
|
)),
|
|
inbox boolean NOT NULL,
|
|
phone boolean NOT NULL,
|
|
email boolean NOT NULL,
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (user_id, kind)
|
|
);
|