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

12 KiB
Raw Blame History

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.
guide
scoreboard
sse
live
ranking
matrix
event-log
score-graph
tester
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

  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 (startUtcendUtc):

  • 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 (startUtcendUtc) 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 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. The SSE solve frame carries challenge_name / category_abbreviation (sourced from the same challenge + category join that backs the /api/v1/scoreboard/event-log REST 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 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).
  • 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