Stage C follow-up sweep: connection/blocks surfaces + shared moderation contract โ
Date: 2026-07-20 Branch: claude/status-check-next-items-w05z6n (off dev) Outcome: Complete. Closes #662, #673, #676, #677. Sibling work #678 (admin evidence viewer) shipped separately the same day as PR #679.
Why โ
The Stage C hardening sweep (#674) merged on 7/20 and left a trailing cluster of follow-up issues around the same connection/blocks surfaces. This session bundled the tractable ones into one themed delivery so the safety-loop fixes land together and the wire contracts stop drifting.
What changed (by issue) โ
- #673, Dashboard subscriptions follow auth state. The Dashboard wired its realtime listeners (lanterns, scheduled lights, waves, connections) from a one-shot
getCurrentUser()at mount; a sign-in resolving after mount left the screen subscription-less until reload. The setup effect now followsonAuthChange: wire on sign-in, tear down and re-wire on account switch, and clear per-user state on transitions so one account's data is never left rendered for another (a privacy property, not just DX). - #676, one shared blocks listener. New
apps/web/src/lib/blockedSet.js: a reference-countedusers/{uid}/blockslistener per uid, consumed by every connection surface throughsubscribeToActiveConnections. Collapses the 3-5x listener fan-out, shrinks the first-emit gate window (late subscribers get the warm set synchronously), andgetActiveConnectionsreuses the warm set instead of a one-shot read. Deliberately import-light: noblockService, so no E2EE signal graph on this path. - #662, block -> report offer, verified and pinned. The fix itself (defer
archiveConnectionto flow completion so the "report them too?" offer survives the chat unmount) had already landed with #669 but nothing referenced the issue. Regression tests now pin the invariant: block does not archive, the offer renders, decline/report each archive exactly once. Plus one real hardening found on the way:BlockConfirmationModalnow swallows the deliberate failed-block rethrow instead of leaking it as an unhandled promise rejection. - #677, single owner for the moderation-case wire contract. New
@lantern/shared/moderationowns the case-status vocabulary, ban/reinstate case patch, per-report denormalization fields, activity-entry shape, and the 500-op batch chunk. Both writers (admincaseService.js, auth-apiresyncCaseOnReinstate) build from it;caseBanPatchstructurally clearsbannedNumberon reinstate, so the drift class the #674 review caught cannot recur.
Tests โ
- New: 10 blocked-set provider tests, 4 block->report flow regression tests, 21 Chat display/interaction tests (Chat.jsx previously had zero; its branch coverage went 34% to 77%), 9 shared moderation-contract tests.
- Full
npm run validate: 30/30 checks green after merging the post-#679devback in (web suite 1203 tests, auth API 377, admin 313, shared 174).
Deliberately left alone โ
- #675 (should Archived Chats show blocked peers?) is a product decision, still open for the operator.
- #678 shipped separately as PR #679 (different risk surface: admin portal + auth API signed URLs; reviewed and merged in isolation).
- The wave-level block path in
Dashboard.handleBlockWavestill archives immediately; it has no report offer, so the #662 invariant does not apply there.