AI Implementation feature(865): Scoreboard: Ranking, Matrix, Event Log and Score Graph (#52)
This commit was merged in pull request #52.
This commit is contained in:
@@ -1,37 +1,86 @@
|
||||
---
|
||||
type: guide
|
||||
title: Scoreboard Stream
|
||||
description: How live solves are broadcast to the SPA over Server-Sent Events.
|
||||
tags: [guide, scoreboard, sse, stream]
|
||||
timestamp: 2026-07-21T18:28:00Z
|
||||
description: How fresh solves are broadcast to clients — both the public unauthenticated scoreboard SSE stream and the authenticated combined /api/v1/events stream that the /scoreboard page consumes.
|
||||
tags: [guide, scoreboard, sse, stream, public, authenticated]
|
||||
timestamp: 2026-07-23T05:10:00Z
|
||||
---
|
||||
|
||||
# Endpoint
|
||||
# Overview
|
||||
|
||||
`GET /api/v1/scoreboard/stream` (`@Sse()` handler in
|
||||
`backend/src/modules/system/system.controller.ts`) opens an SSE stream
|
||||
that pushes the public scoreboard projection every time a new solve is
|
||||
recorded.
|
||||
There are **two** scoreboard-related SSE streams in the codebase:
|
||||
|
||||
# Push path
|
||||
1. The **public** stream `GET /api/v1/scoreboard/stream`, owned by
|
||||
`SystemController`
|
||||
(`backend/src/modules/system/system.controller.ts`), is the
|
||||
`@Public()` `@Sse('scoreboard/stream')` handler that pushes a
|
||||
flattened solve payload every time the scoreboard hub publishes
|
||||
a new solve. It is meant for unauthenticated spectators (e.g.
|
||||
landing-page leaderboard widgets).
|
||||
2. The **authenticated** combined stream `GET /api/v1/events`
|
||||
(`backend/src/modules/challenges/events.controller.ts`) merges
|
||||
`status` ticks, `general` settings pushes, and `solve` frames
|
||||
into a single JWT-gated transport. The
|
||||
[Scoreboard Page](/guides/scoreboard-page.md) subscribes here
|
||||
and reacts to the `solve` event type to mutate all four tabs
|
||||
in real time.
|
||||
|
||||
1. The challenge solve path (admin validation, player submit) updates the
|
||||
`solve` table.
|
||||
2. The same code path calls `SseHubService.publish('scoreboard', payload)`
|
||||
# Public endpoint — `GET /api/v1/scoreboard/stream`
|
||||
|
||||
The handler
|
||||
(`backend/src/modules/system/system.controller.ts`) opens an SSE
|
||||
stream that pushes a flattened solve payload every time a new solve
|
||||
is published to the scoreboard hub.
|
||||
|
||||
# Push path (both streams)
|
||||
|
||||
1. The challenge solve path (admin validation, player submit via
|
||||
`ChallengesService.submitFlag`) inserts a new `solve` row.
|
||||
2. The same code path calls
|
||||
`SseHubService.publish('scoreboard', payload)`
|
||||
(`backend/src/common/services/sse-hub.service.ts`).
|
||||
3. Every subscriber to `/api/v1/scoreboard/stream` receives the payload.
|
||||
3. Subscribers to **either** stream receive the payload:
|
||||
* `/api/v1/scoreboard/stream` emits a flattened shape
|
||||
(`challengeId`, `userId`, `pointsAwarded`, `rankBonus`,
|
||||
`solvedAt`) for anonymous spectators.
|
||||
* `/api/v1/events` emits a richer `SolveEventPayload` (with both
|
||||
snake_case and camelCase aliases, plus `position`,
|
||||
`live_points`, `solve_count`, etc.) for the authenticated SPA.
|
||||
|
||||
The hub is in-process; multi-replica deployments need a shared pub/sub
|
||||
to fan out across pods (not in scope for this repo).
|
||||
The hub is in-process; multi-replica deployments need a shared
|
||||
pub/sub to fan out across pods (not in scope for this repo).
|
||||
|
||||
# Frontend consumer
|
||||
# Authenticated consumer — `/scoreboard` page
|
||||
|
||||
The authenticated SPA shell subscribes on mount and updates the scoreboard
|
||||
view whenever a new event arrives. The exact rendering lives inside the
|
||||
authenticated shell's scoreboard component (see
|
||||
[Frontend Structure](/architecture/frontend-structure.md)).
|
||||
The `ScoreboardPage` smart component subscribes to `/api/v1/events`
|
||||
(not the public stream) on `ngOnInit`:
|
||||
|
||||
1. `store.loadAll()` fetches the four projections from
|
||||
`/api/v1/scoreboard/{ranking,matrix,event-log,graph}` in
|
||||
parallel and populates the initial state.
|
||||
2. `store.wireSse(() => authenticatedEventSource.open('/api/v1/events'))`
|
||||
opens the authenticated SSE transport via
|
||||
`AuthenticatedEventSourceService` (which adds the Bearer token
|
||||
and the `'unauthorized'` `401`/`403` channel).
|
||||
3. The store listens for the `solve` event type and mutates all
|
||||
four tabs (Ranking, Matrix, Event Log, Score Graph) using pure
|
||||
helpers in `scoreboard.pure.ts`
|
||||
(`parseSolveEventIntoRanking`, `mutateMatrixFromSolve`,
|
||||
`applySolveToGraph`, `dedupEventLogBySolveId`).
|
||||
4. On transport error the store schedules a reconnect with an
|
||||
exponential backoff (1s → 30s cap); the `solve` event type
|
||||
resumes mutation as soon as the stream is back.
|
||||
5. On `ngOnDestroy`, `store.stop()` aborts the transport and
|
||||
clears the reconnect timer.
|
||||
|
||||
See [Scoreboard Page](/guides/scoreboard-page.md) for the full
|
||||
tester-facing contract (tabs, test IDs, empty states, error states)
|
||||
and [Challenges Endpoints](/api/challenges.md) for the request and
|
||||
response shapes.
|
||||
|
||||
# See also
|
||||
|
||||
- [Scoreboard Page](/guides/scoreboard-page.md)
|
||||
- [Event Window](/guides/event-window.md)
|
||||
- [System Endpoints](/api/system.md)
|
||||
- [Challenges Endpoints](/api/challenges.md)
|
||||
Reference in New Issue
Block a user