Files
HIPCTF2/docs/guides/scoreboard-page.md
T
2026-07-23 05:13:20 +00:00

262 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: guide
title: Scoreboard Page
description: 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.
tags: [guide, scoreboard, sse, live, ranking, matrix, event-log, score-graph, tester]
timestamp: 2026-07-23T05:10: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
1. Sign in as any user at `/login`.
2. Click the **Scoreboard** quick tab in the authenticated shell
header (or navigate directly to `/scoreboard`).
3. 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 a
`data-rank="<n>"` attribute.
* Each player name is prefixed with a colored swatch (10-entry
`PLAYER_COLOR_PALETTE` chosen by `stablePlayerColorIndex`) 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
by `solveCount` DESC 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>"` with `title="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>"` and
`data-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>` has `data-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:ss` via
`formatSolveDateTime`.
* The player name.
* The category abbreviation (e.g. `FOR`) followed by the challenge
name.
* `+<awardedPoints>` right-aligned.
* 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 `polyline` element
(`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 (when `endUtc` is known) plateaus at
the final value until `endUtc`.
* Three empty states, switched by `view.state`:
| `state` | Visible text | Test ID |
|---|---|---|
| `unconfigured` | `Awaiting event configuration` | `score-graph-empty` |
| `countdown` | `Not started` | `score-graph-not-started` |
| `running` / `stopped` with empty series | `No solves yet` | `score-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](/guides/challenges-board.md) is also consumed by
`ScoreboardStore` and applied to **all four** tabs:
* **Ranking**: the matching player's `points` increases by
`awardedPoints`, `solvedCount` increments by 1, and the list is
re-sorted (with competition ranks applied). If the player is new,
they are appended with `points = awardedPoints`, `solvedCount = 1`.
* **Matrix**: the cell at `[<playerId>, <challengeId>]` becomes `1`,
`2`, or `3` if the new `position` is in 13, otherwise `'solved'`.
New players are appended to the matrix row list.
* **Event Log**: a new `EventLogRow` is prepended to the list
(deduplicated by `solveId` so reconnects don't duplicate) and the
list is capped at 200.
* **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 `authGuard` on 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 the `color-danger` color.
The active tab is replaced with nothing visible (the tab
components themselves stay in their empty state until the next
successful `loadAll`).
* **SSE drops**: `ScoreboardStore` schedules a reconnect with an
exponential backoff (1s → 2s → 4s → … capped at 30s). When the
reconnect succeeds the next `solve` frame re-applies to whatever
state the page currently has.
* **`401` / `403` SSE response**: dispatched as a separate
`'unauthorized'` event by `AuthenticatedEventSourceService`;
`ScoreboardStore` accepts an optional `onUnauthorized` callback
for the page to plug into the cross-tab invalidation flow (see
[Cross-Tab Authenticated Shell Invalidation](/guides/cross-tab-invalidation.md)).
* **Session change (logout in another tab)**: `HomeComponent`
invalidation flow calls `ChallengesStore.reset()` but not the
scoreboard store — the scoreboard store is reset automatically by
Angular when the `ScoreboardPage` is 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
1. Sign in as a user who has solved 3 challenges across two
categories.
2. Open `/scoreboard`. You see a brief `Loading…` then the **Ranking**
tab active by default.
3. 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).
4. 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.
5. Click **Score Graph**. Your colored polyline climbs from 0 at the
event start to your final cumulative points.
## Watching a live solve
1. With the scoreboard open, in another browser profile (or a second
device) sign in as a different user and submit a correct flag.
2. Within ~1 second the `/api/v1/events` SSE 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.
3. 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/challenges.md) — `/api/v1/scoreboard/*` request/response shapes.
- [Scoreboard Stream](/guides/scoreboard-stream.md) — backend push path for the `solve` SSE frames this page consumes.
- [Challenges Board](/guides/challenges-board.md) — the upstream page whose `submitFlag` produces the `solve` frames.
- [Authenticated Shell](/guides/authenticated-shell.md) — the parent shell, header, and quick-tab strip.
- [Event Window](/guides/event-window.md) — the `startUtc`/`endUtc` boundaries used by the Score Graph tab.
- [Cross-Tab Authenticated Shell Invalidation](/guides/cross-tab-invalidation.md) — the `onUnauthorized` SSE callback this page subscribes to.
- [Key Files Index](/architecture/key-files.md) — the canonical file responsibilities for the scoreboard feature.