2 Commits
Author SHA1 Message Date
Admin 0771a69d7d Acknowledge unsubscribes, and nothing re-rings after a restart
Two field reports, one chain: a dismissed alarm came back. Measured on the
emulator, three separate causes, all fixed here.

1. The alarm-clock entry launched an activity. On Android 14+ that launch is
   refused from the background and then *deferred*, so the window surfaced
   later — after the alarm had been acknowledged — and the siren played again
   for a delivered alert. The entry is now a broadcast to AlarmWakeReceiver,
   which only wakes the process and re-posts the takeover notification, and
   ignores an entry that outlived its arming. The one activity PendingIntent
   left is the alarm-clock show intent, which only the user can invoke.

2. A dismissal could be lost. `acknowledgeAlarm` returned early when the
   device identity had not loaded, and the queue's entry required a deviceId,
   so the ack was silently dropped and the ledger still owed the alarm. The
   queue accepts an entry without one (measured: the service takes a
   device-token ack with no deviceId) and the flush resolves the identity.
   Nothing raises — and the armed service does not start listening — until the
   queued dismissals have been read, so a restart cannot ring before the seed.

3. Acknowledging now unsubscribes the device (owner ruling: a dismissal is
   leaving). The ack settles the ledger first, then the subscription row goes;
   the takeover closes itself and Home reports "Acknowledged — this device
   unsubscribed". A leftover subscription was a live address that kept ringing
   on every launch.

Also: an alarm is only sounded for an alert this device still follows (or a
trigger event that actually arrived), and the takeover never renders the raw
alert id as its headline — that fallback is what made a stale launch look like
an alarm for "dbf1a4c2-…".

Verified on the emulator: join → trigger → acknowledge → subscription row gone,
alert delivered, app returns Home by itself; force-stop and reopen is silent
for the whole session (no audio, no notification, no takeover).
2026-10-20 00:48:33 +01:00
Admin 98625adb25 [fix](client): arm in the background, make dismiss final, and leave the takeover
The reported failures are one missing acknowledgement and one missing service
start.

- Alarms only fired with the app in focus: the armed WebSocket lives in the
  native foreground service, and nothing started it — the only entries were a
  button on one alert screen and a toggle in Settings. The provider now arms the
  active subscription itself (asks for POST_NOTIFICATIONS, uses the device token
  the service can hold, throttles a refused start to once per five minutes), and
  Home says whether the background listener is really running instead of
  presenting the on-screen socket as coverage.
- Dismissed alarms came back: the takeover never sent the ack. acknowledgeAlarm
  existed and was never called, so the service still believed this device owed
  the alarm and replayed it on every connect/poll. The acknowledge path now
  persists the dismissal (durably, retried) and records it locally, so a status
  report of the trigger it already dismissed cannot raise the takeover again.
  The record tolerates a dismissal with no event id (the native launch carries
  only the alert) and is pinned to the concrete event as soon as one is known.
- No way back from the takeover: acknowledge now shows a Back to alerts action,
  hardware back works once the device is silent, and the screen leaves by itself
  when the alert ends for this device.
- A destroyed alert answers 401 to its device token (the subscription went with
  it). That is terminal: acknowledgements and the status poll stop retrying the
  dead credential and mark the subscription ended instead of looping.
- Settings' start-listening button built the socket URL without the device
  token, so it could never arm a subscriber's service.

tsc clean, 144 tests (15 new: dismissal memory, background arming, dead
credential, unnamed ack queue).
2026-10-19 19:34:36 +01:00