mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
d5c945241d
After acknowledging an alert on Alerts > Overview, an unacknowledge from another session never brought the card back in that tab until a reload, although `/api/alerts/active` and the store both had it unacknowledged. A pure remote acknowledge/unacknowledge round trip (no click in the tab) already worked. ## Cause `useAlertAcknowledgementState` kept its own acknowledgement override and spread it over the store alerts. Its release effect never read the override signal and only re-ran when the `Object.values` memo over the keyed alert store changed, which in-place store writes (`updateAlert`, `reconcile` payloads) do not trigger. The override outlived the server's confirmation and then masked the server's next change. Whether it cleared depended on unrelated alerts appearing or resolving, so the bug was intermittent. The override was redundant: the websocket store's `updateAlert` already writes the optimistic state and holds back stale payloads until the server confirms. The hook now writes through it and keeps no copy. `updateAlert` is required, and its only caller always passed it. ## Store hold fixes (`frontend-modern/src/stores/websocket.ts`) - **Broadcast before response:** the backend broadcasts after writing the HTTP response, so the confirming payload can reach the socket before the response resolves. The hold then waited for a second confirmation, skipped a following remote unacknowledge, and after 15s showed "Server did not confirm the alert acknowledgment in time" without actually re-syncing. Reproduced live. A change the stored alert already shows now sets no hold and keeps the server's `ackTime`/`ackUser`. - **Give-up re-sync:** a hold that gives up now re-syncs from `/api/alerts/active`. If it joins a recovery that the revision fence then discards, it fetches again, since a quiet estate may send no further alert delta. - **Overlapping REST recovery:** a local acknowledgement write supersedes a REST recovery already in flight, whose response may predate the write. The store discards that response and fetches again once it settles. ## Tests - **Hook (`useAlertAcknowledgementState.test.tsx`), over a real `createStore` + `reconcile` store:** - local acknowledge, then remote unacknowledge - local restore, then remote acknowledge - acknowledge, restore and bulk acknowledge The first two fail on the old hook at the remote-change step. - **Overview state (`useAlertOverviewState.test.tsx`):** now backed by a keyed store, and checks that the card returns after a remote unacknowledge. Fails on the old hook. - **Store (`websocket-unified.test.ts`):** covers the broadcast-before-response race, the give-up re-sync, the overlapping-recovery supersede, a give-up joining a fenced-out recovery, and a recovery that predates a held acknowledge. The first two fail on the old store. For the recovery races, removing each mechanism (supersede, its fresh fetch, the re-sync chain) fails the test that pins it. ## Browser check Private mock stack at 1280×900 and 375×812: - **Before:** the card stayed hidden while the header read "2 critical, Acknowledged 1" against a nav badge of 3 critical. - **Reported flow:** a local Acknowledge, then a `curl` unacknowledge, brings the card back within 2s. - **Restore direction:** Unacknowledge in the tab, then a remote acknowledge, moves it back to Acknowledged within 2s. - **Race:** with the acknowledge response held 1.5s so the socket wins, the card returns at once with no toast. With only `websocket.ts` reverted to main, it stayed hidden about 16s and then the toast fired. ## Left as is If the give-up's own REST fetch fails, the tab keeps the local state until the next alert payload. That matches main, where a give-up never re-synced at all.