-- Copyright (C) 2026 Dcntrlized -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU Affero General Public License as published -- by the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU Affero General Public License for more details. -- -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see . -- 011_delivery_receipts.sql — "received" is not "dismissed". -- -- The delivery ledger had one timestamp (`acked_at`) for two different facts, -- and the clients ack a frame the moment they receive it (that is how a live -- device stops being told about the same alarm on every poll). Because the -- drain-to-empty rule destroys the alert when every row is acked, a single-device -- alert was destroyed 27 ms after the owner triggered it — measured on an -- Android 15 emulator: `triggered` then `destroyed/delivered` in the event log, -- while the phone was still ringing. The owner then could not disarm -- (`ERR_ALERT_NOT_FOUND`), the subscription went with the alert, and its device -- token stopped resolving mid-alarm (401). -- -- So the ledger now records both facts: -- * `delivered_at` — the device has the trigger (a live frame, a replay, or a -- check-in). It stops re-delivery. It is NOT a dismissal. -- * `acked_at` — the person holding the device dismissed it. Only this drives -- the drain, so the alert ends when everyone has dismissed it, never because -- everyone happened to be online. -- -- Additive and idempotent. Existing rows: a row with `acked_at` set keeps that -- fact (it was a dismissal under the old contract too); a pending row simply has -- no receipt yet, which is the correct reading of "we never recorded a delivery". \c dcntrlized DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'alarm') THEN RETURN; END IF; ALTER TABLE alarm.alarm_deliveries ADD COLUMN IF NOT EXISTS delivered_at TIMESTAMPTZ; -- "Who still owes an alarm on this device" is the check-in query; the -- partial index keeps it to the rows that can still answer yes. DROP INDEX IF EXISTS alarm.alarm_deliveries_pending_idx; CREATE INDEX IF NOT EXISTS alarm_deliveries_undelivered_idx ON alarm.alarm_deliveries (alert_id, subscription_id) WHERE acked_at IS NULL AND delivered_at IS NULL; END $$;