Skip to content

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 follows onAuthChange: 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-counted users/{uid}/blocks listener per uid, consumed by every connection surface through subscribeToActiveConnections. Collapses the 3-5x listener fan-out, shrinks the first-emit gate window (late subscribers get the warm set synchronously), and getActiveConnections reuses the warm set instead of a one-shot read. Deliberately import-light: no blockService, so no E2EE signal graph on this path.
  • #662, block -> report offer, verified and pinned. The fix itself (defer archiveConnection to 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: BlockConfirmationModal now 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/moderation owns the case-status vocabulary, ban/reinstate case patch, per-report denormalization fields, activity-entry shape, and the 500-op batch chunk. Both writers (admin caseService.js, auth-api resyncCaseOnReinstate) build from it; caseBanPatch structurally clears bannedNumber on 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-#679 dev back 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.handleBlockWave still archives immediately; it has no report offer, so the #662 invariant does not apply there.

Built with VitePress