docs: update documentation to OKF v0.1 format

This commit is contained in:
OpenVelo Agent
2026-07-23 05:13:20 +00:00
parent 49d0930780
commit 1576d663d3
6 changed files with 536 additions and 28 deletions
+69 -20
View File
@@ -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)