[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:
+27
-4
@@ -225,6 +225,12 @@ func AlarmStatus(w http.ResponseWriter, r *http.Request, payload any) {
|
||||
// 409 lost in transit must not forfeit the alarm: the next check-in has to
|
||||
// find the same row. A trigger that has since been disarmed is not news, so
|
||||
// that case falls through to the normal body (active:false).
|
||||
deviceHash := ""
|
||||
if !c.IsDevice {
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("deviceId")); raw != "" {
|
||||
deviceHash = deviceid.Hash(raw)
|
||||
}
|
||||
}
|
||||
var pending *db.PendingDelivery
|
||||
if c.IsDevice {
|
||||
// The device token already identifies the subscription, so its whole
|
||||
@@ -238,10 +244,6 @@ func AlarmStatus(w http.ResponseWriter, r *http.Request, payload any) {
|
||||
pending = &rows[0]
|
||||
}
|
||||
} else {
|
||||
deviceHash := ""
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("deviceId")); raw != "" {
|
||||
deviceHash = deviceid.Hash(raw)
|
||||
}
|
||||
var err error
|
||||
pending, err = db.PendingTrigger(r.Context(), alertID, c.UserID, deviceHash)
|
||||
if err != nil && !errors.Is(err, db.ErrNotFound) {
|
||||
@@ -261,6 +263,27 @@ func AlarmStatus(w http.ResponseWriter, r *http.Request, payload any) {
|
||||
// not live even if the stored status says "triggered".
|
||||
active = false
|
||||
}
|
||||
// "Active" is a statement about THIS caller, not about the alert's commanded
|
||||
// status. The alert stays triggered until the owner disarms it or every
|
||||
// subscriber acknowledges, so answering a device that already dismissed the
|
||||
// alarm with active:true makes a correct client re-raise the takeover on
|
||||
// every poll — the alarm that will not stay dismissed. A subscriber is
|
||||
// active only while it still owes the newest trigger; the owner (who has no
|
||||
// delivery row) keeps seeing their own alert as live.
|
||||
callerIsOwner := !c.IsDevice && res.Alert.AdminUserID == c.UserID
|
||||
if active && !callerIsOwner {
|
||||
var owed bool
|
||||
if c.IsDevice {
|
||||
owed, err = db.HasPendingDelivery(r.Context(), alertID, c.Sub.ID)
|
||||
} else {
|
||||
owed, err = db.HasPendingDeliveryForUser(r.Context(), alertID, c.UserID, deviceHash)
|
||||
}
|
||||
if err != nil {
|
||||
dbError(w, action, err)
|
||||
return
|
||||
}
|
||||
active = owed
|
||||
}
|
||||
|
||||
view := statusView{
|
||||
Active: active,
|
||||
|
||||
Reference in New Issue
Block a user