feat(push): suppress notifications to devices where the app is visible (presence-based send-time filter) #162
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab!162
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/160"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the presence-based push-suppression design from the 2026-07-13 grill session. Broadcast Web Push is now dropped at send time for any device whose app is currently visible/focused; every other enrolled device is delivered to exactly as before. Suppression is 100% server-side —
web/public/sw.jsis untouched and still shows every push it receives (ADR-0039's rule stands).What changed
internal/presence(new): in-memoryconn → {deviceHash, visible}registry. The SSE stream owns entry lifetime (Connect on open, deferred Disconnect on handler return), the presence beacon only refines live entries (an unknown conn can never resurrect one), and every staleness path errs toward notifying — a restart clears everything.internal/push:PresenceViewseam injected intoNewSender(nil ⇒ never suppress);Broadcastskips subscriptions whose SHA-256 endpoint hash reads visible, with a Debug log;Senddeliberately bypasses the filter so the Settings Send test always delivers. Also fixes a pre-existing data race: each dispatch now gets its own payload copy (webpush-go pads the slice it is handed in place).internal/httpapi:GET /api/v1/events?conn=<uuid>registers presence happens-before the 200; new authenticatedPOST /api/v1/presence{conn, device, visible}(204 always, strict conn/device validation);X-Lab-Csrfis waived for exactly this one endpoint —navigator.sendBeaconcannot set headers — while the Origin equality check stays fully enforced (verified live: beacon-shape 204, cross-origin 403).web/:sse.tsmints a fresh conn UUID per connection and exposes aconnIDsignal; newlib/deviceHash.ts(crypto.subtle SHA-256 of the push subscription endpoint — the capability URL never travels) andlib/presence.ts(reports on stream open,visibilitychange, and viasendBeacononpagehide); wired to the singleton connection inevents.tsx. A browser without a push subscription sends no presence at all.docs/adr/0044-presence-based-push-suppression.md: the full decision record, referencing ADR-0038/0039.Verification
go test ./...green (incl.-raceon push/presence/httpapi). The only failures areinternal/tmuxxintegration tests, reproduced identically on cleanmainin this sandbox (no tmux pane spawning) — unrelated, CI has real tmux.golangci-lint runclean;gofmt/go vetclean.tsc --noEmit, eslint, prettier all clean.Acceptance mapping
TestBroadcastSuppressesVisibleDeviceTestSendBypassesPresenceTestSSERegistersPresenceForLifetimeOfStreamweb/public/sw.jsunchanged → not in the diffCloses #160
🤖 Generated with Claude Code
https://claude.ai/code/session_01HRzJpVActrGRhH5MXPZ3s9