mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 14:30:13 +00:00
9af942a169
* feat(store): push notification schema and settings store (TASK-3937 part A)
Tables push_devices, user_notifications, user_notification_settings
(sqlite 155 + generated NUL triggers 156, pg 124), the settings model and
store, the migrated.go left-out/refusal entries, and the cascade tests.
The enqueue side is part B.
Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3
* test(store): ScanNUL cost figure counts the push tables' columns (TASK-3937 part A)
Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3
* fix(store): index the push queue's actor, say retention is the dispatcher's, cover the queue in the purge test (TASK-3937 part A r1)
- idx_user_notifications_actor on both dialects: deleting a user runs
actor_user_id's ON DELETE SET NULL against the queue.
- 155's comment no longer claims a 7-day retention that nothing
implements yet: the sweep lands with the dispatcher (TASK-3939).
- user_notifications joins wsChildTables and is seeded by
seedFullWorkspace, so the no-orphan and no-over-purge tests cover it.
Shown able to fail: with workspace_id's cascade removed, the purge
test reports "table user_notifications left 1 rows".
Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3
* fix(store): isolate push_devices' user cascade in a test; dispatcher prose says it is TASK-3939's (TASK-3937 part A r3)
- TestPushDevices_UserCascade binds a device to another user's session
and deletes the device's user, so only push_devices.user_id's cascade
can clear the row. TestPushTables_Cascades could not: account deletion
removes the user's sessions first. Shown able to fail: with the cascade
removed from 155, the delete is refused (FOREIGN KEY constraint failed).
- TestPushTables_Cascades' header lists exactly what it kills.
- migrated.go, 155, models and nulcolumns no longer describe a
dispatcher or 7-day sweep as if they ran today: both land with
TASK-3939, and until then nothing reads or deletes the queue.
Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3
* docs(store): the push-delivery note names the census test; five groups are left out (TASK-3937 part A)
Rebased onto 393d986ea, which carries BUG-3958's
TestMigratedEveryLiveTableIsClassified. Part A's first commit now
classifies its tables there: user_notification_settings refused,
push_devices and user_notifications excluded as "push-delivery". Shown
able to fail: without the push_devices entry, the census test reports
"[push_devices]". The header's group count goes from four to five
with the PUSH DELIVERY STATE group.
Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3
* docs(store): push prose describes the enqueue, registration and dispatcher as owed, not built (TASK-3937 part A r4 L1)
Ruled a fix once part B could not merge in the same window as A
(r4 hunter L1). 155's header, migrated.go's push-delivery notes and
the settings model now say the enqueue is part B's, registration
TASK-3938's and the dispatcher TASK-3939's. nulExcluded's push reasons
gain a header saying they are the writers' contract, and each names the
unit that owes it: device columns TASK-3938, queue columns part B, the
claim and send columns TASK-3939. user_notification_settings keeps
TASK-3937; PutNotificationSettings writes it today.
Claude-Session: https://claude.ai/code/session_01JRXHP9ZVfEN3YEuzmucxP3
78 lines
4.2 KiB
SQL
78 lines
4.2 KiB
SQL
-- Migration 155: push notifications (TASK-3937, PLAN-3908 phase 2 unit 1).
|
|
--
|
|
-- push_devices: one row per signed-in app session that registers a push
|
|
-- token (the register endpoint lands with TASK-3938; nothing writes this
|
|
-- table yet). Every token is an FCM registration token (iOS routes through FCM),
|
|
-- so there is no environment column: FCM picks the APNs gateway itself.
|
|
-- platform stays because the FCM payload differs per platform. The row dies
|
|
-- with its session or its user.
|
|
--
|
|
-- user_notifications: the per-event push queue. Nothing writes it yet: the
|
|
-- enqueue is TASK-3937 part B, and it is to write a row ONLY for a user who
|
|
-- has at least one device, on the transaction of the write that caused it.
|
|
-- dedupe_key is what makes that enqueue idempotent per (user, key). item_id
|
|
-- and comment_id carry no foreign key: the dispatcher is to skip a row whose
|
|
-- subject is gone, and retention to remove the rest. Retention is NOT in this
|
|
-- migration: the 7-day sweep lands with the dispatcher (TASK-3939), and until
|
|
-- then nothing deletes a queue row. workspace_id cascades, so a purged
|
|
-- workspace leaves no queue behind. actor_user_id is indexed because deleting
|
|
-- a user runs its ON DELETE SET NULL against this table.
|
|
--
|
|
-- user_notification_settings: a person's recipient-side filters. No row means
|
|
-- the defaults. The dispatcher (TASK-3939) is to apply them; the enqueue
|
|
-- (part B) is not to.
|
|
--
|
|
-- workspace_read_cursors (149) stays separate: it must exist for every user,
|
|
-- with or without a device, and this queue is to be deleted after 7 days.
|
|
CREATE TABLE IF NOT EXISTS push_devices (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
session_id TEXT NOT NULL UNIQUE REFERENCES sessions(id) ON DELETE CASCADE,
|
|
connection_id TEXT NOT NULL,
|
|
platform TEXT NOT NULL CHECK (platform IN ('ios', 'android')),
|
|
token TEXT NOT NULL,
|
|
app_version TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
UNIQUE (user_id, token)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_push_devices_user ON push_devices(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_notifications (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
item_id TEXT NOT NULL,
|
|
comment_id TEXT,
|
|
kind TEXT NOT NULL CHECK (kind IN ('assigned', 'comment', 'reply', 'reminder', 'mention')),
|
|
actor_user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
|
|
actor_is_agent INTEGER NOT NULL DEFAULT 0 CHECK (actor_is_agent IN (0, 1)),
|
|
dedupe_key TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
push_state TEXT NOT NULL DEFAULT 'pending' CHECK (push_state IN ('pending', 'sent', 'skipped', 'expired')),
|
|
skip_reason TEXT NOT NULL DEFAULT '',
|
|
claim_token TEXT NOT NULL DEFAULT '',
|
|
claimed_at TEXT,
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
sent_at TEXT,
|
|
UNIQUE (user_id, dedupe_key)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_state ON user_notifications(push_state, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_sent ON user_notifications(user_id, sent_at);
|
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_workspace ON user_notifications(workspace_id);
|
|
CREATE INDEX IF NOT EXISTS idx_user_notifications_actor ON user_notifications(actor_user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_notification_settings (
|
|
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
push_enabled INTEGER NOT NULL DEFAULT 1 CHECK (push_enabled IN (0, 1)),
|
|
notify_assigned INTEGER NOT NULL DEFAULT 1 CHECK (notify_assigned IN (0, 1)),
|
|
notify_comments INTEGER NOT NULL DEFAULT 1 CHECK (notify_comments IN (0, 1)),
|
|
notify_replies INTEGER NOT NULL DEFAULT 1 CHECK (notify_replies IN (0, 1)),
|
|
notify_reminders INTEGER NOT NULL DEFAULT 1 CHECK (notify_reminders IN (0, 1)),
|
|
notify_mentions INTEGER NOT NULL DEFAULT 1 CHECK (notify_mentions IN (0, 1)),
|
|
include_agent_activity INTEGER NOT NULL DEFAULT 0 CHECK (include_agent_activity IN (0, 1)),
|
|
updated_at TEXT NOT NULL
|
|
);
|