feat(realtime): live updates across the site — the activity room - #36
Merged
Conversation
…vent The backend half of live-updates-everywhere (#18). A new broadcast-only `activity/<competition_id>` WS room, owned by the competitions module (the tenancy root): a curated allowlist of §3.2 events fans out as tiny `{type: "activity", event}` pings — ids only, never payload bodies — so clients refetch their own permission-filtered REST slices. Authorized like the scoreboard room (challenge_view), recompute skipped when nobody is watching. ticket.*/announcement.* stay off the allowlist (they have tighter-scoped rooms of their own); user.*/role.*/site.* are global-admin domains, not competition activity. New §3.2 event `challenge.attempted`: every *graded* submission, right or wrong (the event half of "every attempt is logged"). Wrong guesses previously emitted nothing, leaving attempt counters (dashboard stats, challenge health, analytics) stale until the next solve. Automation trigger gated view_competition_analytics — others' attempts are staff analytics data. Volume bounded by the submission rate limit. Two ticket-liveness gaps fixed while in there: a thread reply now bumps the staff support room (ticket_updated, no audio cue), and notification frames carry competition_id so the bell can refresh that competition's ticket caches client-side. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The frontend half of #18. `lib/live.ts` (pure, unit-tested — mirroring the data-table split) maps each activity event to the query keys it stales: challenge cards/dialog (solve counts, dynamic value, solver list), the dashboard data widgets (never the layout key), rosters, analytics, module nav gating, and the competition detail (pause banner goes live). Unmapped events are a deliberate no-op — the map is the frontend's allowlist. Invalidations run through a per-key leading+trailing throttle (2.5s): a lone event lands instantly, a burst collapses to at most one refetch per key per window, and the last event is never lost. attempted events deliberately skip the challenge list — heaviest refetch, most frequent event, and someone else's wrong guess doesn't change your cards. `useActivityLive` (use-activity.ts) owns the socket lifecycle and is mounted once in the app shell beside the notification bell — pages never open their own. Since invalidateQueries only refetches *mounted* queries, background pages are just marked stale and fetch fresh on navigation. The bell hook additionally refreshes the competition's ticket caches on ticket-type notification frames, closing the competitor-side support gap. Docs: §3.2 gains challenge.attempted; §4.1 now names the two room idioms (snapshot vs ping) and the activity room's place in them. Verified live against a demo-mode backend + simulator: dashboard tiles, challenge-card solve counts (page total matched the DB exactly), analytics attempts/solves, and the participants roster all moved without a reload; pausing via the API surfaced the banner live; one shell socket, no reconnect churn, no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18
What
One new mechanism instead of five bespoke fixes: a per-competition
activity/<competition_id>WebSocket room that fans out tiny event-name pings, and a frontend map from event names → TanStack Query invalidations. Every surface the issue names now updates live:Bonus liveness for free (one map line each): the pause banner appears live (
competition.updated), module toggles re-gate the nav live, and category/hint/vocab edits propagate.Design
challenge_view), broadcast skipped when nobody's watching.invalidateQueriesonly refetches mounted queries, so background pages are marked stale and fetch fresh on navigation.ticket.*/announcement.*excluded — they have tighter-scoped rooms;user.*/role.*are global-admin domains); the frontend map treats unmapped events as a no-op.lib/live.ts: a lone event lands instantly, a burst collapses to at most one refetch per key per window, the last event is never lost.New §3.2 event:
challenge.attempted(owner-approved)Every graded submission, right or wrong — the event half of §13.2's "every attempt is logged". Wrong guesses previously emitted nothing, so attempt counters could not go live. Automation trigger gated
view_competition_analytics(others' attempts are staff analytics data); volume bounded by the submission rate limit. Deliberately not wired to the challenge-list refetch (heaviest query, most frequent event).Tests
attemptedping, correct submit →attemptedthensolved, ticket events don't reach the room, team-created ping, thread-reply → support bump, notification frame carriescompetition_id.Live verification (demo backend + simulator)
Logged in as admin with the simulator generating traffic — no reloads anywhere:
🤖 Generated with Claude Code