[fix](alarm): make dismissal stick and count the devices
Four defects, three of them the same missing piece of the delivery ledger.
- listSubscriptions scanned nine destinations for ten columns and read a NULL
subscriber_user_id into a uuid. Every alert with an anonymous subscriber
answered 500 ("number of field descriptions must equal number of
destinations, got 10 and 9"), and the client renders a failed read as an
empty list — so 'Subscribed devices' said 0 while the row existed. The
listing now uses the same scan as every other subscription read.
- alarmStatus reported the alert's commanded state, not the caller's own. An
alert stays 'triggered' until the owner disarms or every subscriber acks, so
a device that had already dismissed the alarm was told active:true on every
poll and re-raised its takeover. active is now true only while the caller
still owes the newest trigger; the owner keeps seeing their own alert.
- ackAlarm required an eventId, which the native takeover launch does not
have. A dismissal that cannot name its event now clears the subscription's
whole outstanding set (AckAllDeliveries / AckAllForSubscriber), so the debt
cannot survive because the client could not echo a uuid.
- The device ledger probe (gated on DATABASE_URL, like the lifecycle probe)
pins the per-subscriber debt, the ack-all semantics and the cascade that
takes a device token with the destroyed alert.
Verified against the dev database and through Maestro: 2 subscriptions listed,
device A acked -> its status turns active:false while device B still gets the
409 check-in, ack-all without eventId clears the debt and drains the alert.
This commit is contained in:
@@ -587,9 +587,18 @@ read; send it in the query if you want to pin the subscription)
|
||||
```
|
||||
|
||||
`active` is **derived**, never a ringing timer: it is true only when the newest
|
||||
event is a `trigger` and the stored status is `triggered`. The answer stays
|
||||
correct across restarts and instances, and it stays `true` until the owner
|
||||
disarms or the alert expires. (There *is* a sweeper, but it destroys expired
|
||||
event is a `trigger`, the stored status is `triggered`, **and this caller still
|
||||
owes the alarm** (an unacknowledged delivery row for its subscription). The
|
||||
answer stays correct across restarts and instances, and it stays `true` until
|
||||
the owner disarms, the alert expires, or this caller acknowledges.
|
||||
|
||||
The caller-scoped `active` is what makes dismissal final: an alert stays
|
||||
commanded `triggered` until the owner disarms it or *every* subscriber ack, so a
|
||||
device that already dismissed the alarm would otherwise be told `active: true`
|
||||
on every poll and re-raise its takeover. A client must treat `active: false`
|
||||
with a stale `eventId` as "nothing owed by this device", not as a new trigger.
|
||||
The alert's owner (who has no delivery row) keeps seeing their own alert's
|
||||
commanded state. (There *is* a sweeper, but it destroys expired
|
||||
alerts — it never changes an event's meaning.) An expired or destroyed alert is
|
||||
`ERR_ALERT_NOT_FOUND` here like everywhere else. When there is no event yet the
|
||||
fields are empty and `active` is `false`.
|
||||
@@ -663,12 +672,15 @@ device rides the payload (or the query string), exactly like `joinAlert`:
|
||||
|
||||
```json
|
||||
{ "payload": { "alertId": "…", "eventId": "…", "deviceId": "pixel-7" } }
|
||||
{ "payload": { "alertId": "…", "eventId": "…" } } // device token in X-Device-Token
|
||||
{ "payload": { "alertId": "…" } } // dismiss everything owed
|
||||
```
|
||||
|
||||
Response `200`:
|
||||
|
||||
```json
|
||||
{ "alertId": "…", "eventId": "…", "acked": 1, "destroyed": false }
|
||||
// with no eventId: "eventId": "" and acked counts every row that was cleared
|
||||
```
|
||||
|
||||
- Clearing is **ack-driven**, never response-driven: `alarmStatus` only *reports*
|
||||
@@ -678,7 +690,15 @@ Response `200`:
|
||||
`acked: 0`, never an error. The WebSocket `ack` frame settles the same rows.
|
||||
- `acked` counts the ledger rows cleared for this device. When the ack clears the
|
||||
last unacked row of the alert, the alert drains and is destroyed (reason
|
||||
`delivered`) — `destroyed: true` reports that intended outcome.
|
||||
`delivered`) — `destroyed: true` reports that intended outcome. Note that a
|
||||
destroyed alert takes its subscriptions with it, so a device token for it stops
|
||||
resolving (the next call is `401 ERR_UNAUTHORIZED`, not `404`); clients must
|
||||
treat that as "the subscription is over", not as a retryable failure.
|
||||
- **`eventId` is optional.** Without it the dismissal retires the caller's whole
|
||||
outstanding set on the alert. That exists because the native takeover launch
|
||||
carries only the alert: a client that dismissed an alarm without being able to
|
||||
name its event must still be able to clear the debt, or the replay loop it
|
||||
could not describe keeps ringing forever.
|
||||
- The alert owner has no delivery row, so an owner ack is a well-formed no-op.
|
||||
- Subscribers must pass `deviceId` (raw; the server hashes it). A device can only
|
||||
clear its own row.
|
||||
|
||||
Reference in New Issue
Block a user