12 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| guide | Scoreboard Page | How a signed-in player navigates /scoreboard, uses the four tabs (Ranking, Matrix, Event Log, Score Graph), and watches live solve updates arriving over the authenticated SSE stream. |
|
2026-07-23T05:41:00Z |
Overview
The /scoreboard page (the ScoreboardPage smart component in
frontend/src/app/features/scoreboard/scoreboard.page.ts) renders the
four live scoreboard projections backed by
/api/v1/scoreboard/{ranking,matrix,event-log,graph} and updates
them in real time as new solves come in. It is reachable from the
authenticated shell quick-tab strip (the Scoreboard tab next to
Challenges and Blog).
Navigation
- Sign in as any user at
/login. - Click the Scoreboard quick tab in the authenticated shell
header (or navigate directly to
/scoreboard). - The page mounts, shows a short
Loading…indicator while it fetches all four projections in parallel, then renders the active tab.
You must be authenticated; /scoreboard is a child of the
authenticated HomeComponent shell, so unauthenticated visitors are
redirected to /login by authGuard.
The four tabs
The tab strip (data-testid="scoreboard-tabs") sits directly under
the Scoreboard page heading. There are four mutually-exclusive
tabs:
| Tab | Test ID of the active tab button | Backing component | Underlying endpoint |
|---|---|---|---|
| Ranking | scoreboard-tab-ranking |
RankingComponent |
GET /api/v1/scoreboard/ranking |
| Matrix | scoreboard-tab-matrix |
MatrixComponent |
GET /api/v1/scoreboard/matrix |
| Event Log | scoreboard-tab-event-log |
EventLogComponent |
GET /api/v1/scoreboard/event-log?limit=50 |
| Score Graph | scoreboard-tab-graph |
ScoreGraphComponent |
GET /api/v1/scoreboard/graph |
Click a tab to switch the visible body. The selected tab is styled
with the primary theme color (.active) and the previous tab's
component is destroyed by Angular's *ngIf.
The active tab is stored in ScoreboardStore.activeTab; it defaults
to ranking on first load and is preserved across tab switches but
reset to ranking when ScoreboardStore.reset() runs (e.g. on
session invalidation).
Ranking tab
A table (data-testid="scoreboard-ranking") with columns Rank,
Player, Solved, Points:
- Each row has
data-testid="ranking-row-<playerId>"and adata-rank="<n>"attribute. - Each player name is prefixed with a colored swatch (10-entry
PLAYER_COLOR_PALETTEchosen bystablePlayerColorIndex) so the same player keeps the same color across all four tabs. - Ranking uses competition ranks: equal-point players share the same
rank(1, 2, 2, 4, …). Zero-solve players are still listed at the bottom in user-id order. - Empty state: when no users exist yet, the table is replaced with
the centered text
No players yet(data-testid="ranking-empty").
Matrix tab
A players × challenges grid (data-testid="scoreboard-matrix")
wrapped in a scrollable container
(data-testid="scoreboard-matrix-scroll"):
-
The first column is Player (sticky horizontally) with the same colored swatch as the Ranking tab.
-
Each subsequent column is a challenge, prefixed by its
categoryAbbreviation(e.g.CRY RSA Rollers). Columns are sorted bysolveCountDESC then by challenge name ASC. -
Cells encode the position at which that player solved that challenge:
Cell content Meaning ★ (gold) 1st solver ( data-testid="matrix-cell-<playerId>-<challengeId>"withtitle="1st solver")★ (silver) 2nd solver ★ (bronze) 3rd solver ✓ (green check) 4th+ solver ( title="solved")blank Not solved by this player -
Row and column headers have
data-testid="matrix-row-<playerId>"anddata-testid="matrix-col-<challengeId>"respectively so tests can target them directly. -
Empty state: when no players exist yet, the grid is replaced with the centered text
No players yet(data-testid="matrix-empty").
Event Log tab
A vertically scrolling newest-first <ol>
(data-testid="scoreboard-event-log") capped at 200 entries:
- Each
<li>hasdata-testid="event-log-row-<solveId>". - Each row shows, left-to-right:
- An icon — gold/silver/bronze
★for positions 1/2/3, green✓otherwise. - The local timestamp formatted
YYYY-MM-DD HH:mm:ssviaformatSolveDateTime. - The player name.
- The category abbreviation (e.g.
FOR) followed by the challenge name. +<awardedPoints>right-aligned.
- An icon — gold/silver/bronze
- Empty state: when no solves have been recorded, the list is
replaced with the centered text
No solves yet(data-testid="event-log-empty").
Score Graph tab
A pure-SVG line chart (data-testid="score-graph-svg") of cumulative
points for the top 10 players over the configured event window
(startUtc → endUtc):
- Above the chart, a legend (
data-testid="score-graph-legend-<playerId>") lists each player with their colored swatch. - Each player has a
polylineelement (data-testid="score-graph-line-<playerId>") using the same color index as the Ranking and Matrix tabs. - The X axis is the event window time (
startUtc→endUtc) and the Y axis is cumulative points. Each line starts at(startUtc, 0), steps up at every solve, and (whenendUtcis known) plateaus at the final value untilendUtc. - Three empty states, switched by
view.state:stateVisible text Test ID unconfiguredAwaiting event configurationscore-graph-emptycountdownNot startedscore-graph-not-startedrunning/stoppedwith empty seriesNo solves yetscore-graph-empty
Live updates
On ngOnInit, ScoreboardPage calls store.loadAll() (which fans
out to all four GET /api/v1/scoreboard/* endpoints in parallel) and
then store.wireSse(() => authenticatedEventSource.open('/api/v1/events'))
to subscribe to the existing authenticated combined SSE stream.
The same solve frame that lights up the
Challenges Board is also consumed by
ScoreboardStore and applied to all four tabs:
- Ranking: the matching player's
pointsincreases byawardedPoints,solvedCountincrements by 1, and the list is re-sorted (with competition ranks applied). If the player is new, they are appended withpoints = awardedPoints,solvedCount = 1. - Matrix: the cell at
[<playerId>, <challengeId>]becomes1,2, or3if the newpositionis in 1–3, otherwise'solved'. New players are appended to the matrix row list. - Event Log: a new
EventLogRowis prepended to the list (deduplicated bysolveIdso reconnects don't duplicate) and the list is capped at 200. The SSE solve frame carrieschallenge_name/category_abbreviation(sourced from the samechallenge+categoryjoin that backs the/api/v1/scoreboard/event-logREST projection), so the prepended row's category abbreviation and challenge name are populated immediately — no refresh is required. - Score Graph: the matching player's series receives a new
{tUtc: awardedAtUtc, value: prevValue + awardedPoints}point, the series list is re-sorted by final value DESC and trimmed to the top 10.
On ngOnDestroy, store.stop() aborts the SSE transport and clears
the reconnect timer so no callbacks fire after the page leaves the
DOM.
Errors and edge cases
- Unauthenticated visit: the
authGuardon the parent shell redirects to/login; the scoreboard page itself does not handle this case. - Backend unreachable on initial load:
store.error()is set to the failure message and rendered in the page above the active tab (data-testid="scoreboard-error") with thecolor-dangercolor. The active tab is replaced with nothing visible (the tab components themselves stay in their empty state until the next successfulloadAll). - SSE drops:
ScoreboardStoreschedules a reconnect with an exponential backoff (1s → 2s → 4s → … capped at 30s). When the reconnect succeeds the nextsolveframe re-applies to whatever state the page currently has. 401/403SSE response: dispatched as a separate'unauthorized'event byAuthenticatedEventSourceService;ScoreboardStoreaccepts an optionalonUnauthorizedcallback for the page to plug into the cross-tab invalidation flow (see Cross-Tab Authenticated Shell Invalidation).- Session change (logout in another tab):
HomeComponentinvalidation flow callsChallengesStore.reset()but not the scoreboard store — the scoreboard store is reset automatically by Angular when theScoreboardPageis destroyed as part of the navigation to/login.
Visual elements
| Element | Test ID | Notes |
|---|---|---|
| Page root | scoreboard-page |
Wrapper section. |
| Tab strip | scoreboard-tabs |
Horizontal <nav> with 4 buttons. |
| Per-tab button | scoreboard-tab-<tabId> |
One of ranking / matrix / event-log / graph. |
| Loading indicator | scoreboard-loading |
Shown while store.loading() is true. |
| Error banner | scoreboard-error |
Shown when store.error() is non-null. |
| Ranking table | scoreboard-ranking |
See "Ranking tab" above. |
| Ranking row | ranking-row-<playerId> |
Includes data-rank. |
| Ranking empty | ranking-empty |
"No players yet" copy. |
| Matrix scroll | scoreboard-matrix-scroll |
Scroll wrapper. |
| Matrix table | scoreboard-matrix |
Sticky-header grid. |
| Matrix row | matrix-row-<playerId> |
Sticky-left first column. |
| Matrix column header | matrix-col-<challengeId> |
Sticky-top. |
| Matrix cell | matrix-cell-<playerId>-<challengeId> |
★/✓/blank. |
| Matrix empty | matrix-empty |
"No players yet" copy. |
| Event log list | scoreboard-event-log |
Newest-first <ol>. |
| Event log row | event-log-row-<solveId> |
One per solve. |
| Event log empty | event-log-empty |
"No solves yet" copy. |
| Score graph frame | score-graph |
Wrapper with legend + SVG. |
| Score graph SVG | score-graph-svg |
The line chart. |
| Score graph line | score-graph-line-<playerId> |
Per-player <polyline>. |
| Score graph legend item | score-graph-legend-<playerId> |
Per-player label. |
| Score graph not-started | score-graph-not-started |
"Not started" (countdown state). |
| Score graph empty | score-graph-empty |
"No solves yet" or "Awaiting event configuration". |
Examples
Initial happy path
- Sign in as a user who has solved 3 challenges across two categories.
- Open
/scoreboard. You see a briefLoading…then the Ranking tab active by default. - Click Matrix. The grid shows your row with your colored swatch in the first column and three cells in the matching challenge columns (gold/silver/bronze stars if you were 1st/2nd/3rd, otherwise green checks).
- Click Event Log. Your three solves appear newest-first with
gold/silver/bronze or check icons, the formatted timestamp, the
challenge name, and the
+<points>totals. - Click Score Graph. Your colored polyline climbs from 0 at the event start to your final cumulative points.
Watching a live solve
- With the scoreboard open, in another browser profile (or a second device) sign in as a different user and submit a correct flag.
- Within ~1 second the
/api/v1/eventsSSE frame arrives and the current tab updates without a reload: a new ranking row (or an update to an existing one), a new matrix cell, a new event-log row prepended at the top, and a new step on the score-graph polyline. - The colored swatch for the new player is identical across all four tabs and matches the same player's swatch in the challenges board modal solvers list.
See also
- Challenges Endpoints —
/api/v1/scoreboard/*request/response shapes. - Scoreboard Stream — backend push path for the
solveSSE frames this page consumes. - Challenges Board — the upstream page whose
submitFlagproduces thesolveframes. - Authenticated Shell — the parent shell, header, and quick-tab strip.
- Event Window — the
startUtc/endUtcboundaries used by the Score Graph tab. - Cross-Tab Authenticated Shell Invalidation — the
onUnauthorizedSSE callback this page subscribes to. - Key Files Index — the canonical file responsibilities for the scoreboard feature.