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:
2026-07-23 05:13:23 +00:00
parent 49d0930780
commit 7c4843d5cc
26 changed files with 3074 additions and 60 deletions
+10 -3
View File
@@ -2,8 +2,8 @@
type: architecture
title: Frontend Structure
description: Angular routes, components, services, guards, interceptors, and authenticated SSE transport.
tags: [architecture, frontend, angular, shell, sse, challenges, notifications]
timestamp: 2026-07-23T00:10:00Z
tags: [architecture, frontend, angular, shell, sse, challenges, scoreboard, notifications]
timestamp: 2026-07-23T05:10:00Z
---
# Routes
@@ -16,7 +16,7 @@ Routes live in `frontend/src/app/app.routes.ts`:
| `/login` | `LandingComponent` | `landingGuard` | Public landing and authentication modal. |
| `/` | `HomeComponent` | `authGuard` | Authenticated shell and child outlet. |
| `/challenges` | `ChallengesPage` | inherited | Full challenges board (category columns, modal, flag submit, live SSE). |
| `/scoreboard` | `ScoreboardPage` | inherited | Placeholder scoreboard page. |
| `/scoreboard` | `ScoreboardPage` | inherited | Live scoreboard page with 4 tabs (Ranking / Matrix / Event Log / Score Graph), live SSE `solve` updates. |
| `/blog` | `BlogPage` | inherited | Placeholder blog page. |
| `/admin` | `AdminUsersComponent` | `adminGuard` | Admin-only child route. |
| `/admin/challenges` | `AdminChallengesComponent` | inherited | Sortable/searchable admin Challenges list, Add/Edit modal, import/export. |
@@ -37,6 +37,12 @@ Routes live in `frontend/src/app/app.routes.ts`:
| `ChallengesApiService` | `frontend/src/app/features/challenges/challenges.service.ts` | HTTP wrapper around `/api/v1/challenges/{board,status,:id,:id/solves}` returning `Observable<...>` and translating `HttpErrorResponse` into a typed `ApiErrorEnvelope`. |
| `challenges.pure.ts` | `frontend/src/app/features/challenges/challenges.pure.ts` | Pure types + helpers for the challenges feature (`BoardCard`, `SolverRow`, `SolveEventPayload`, sorting, parsers, friendly error mapping, `formatDdHhMm`). |
| `ChallengeCardComponent` / `CategoryColumnComponent` / `ChallengeModalComponent` | `frontend/src/app/features/challenges/` | Presentational components for the board grid, column, and detail modal. |
| `ScoreboardPage` | `frontend/src/app/features/scoreboard/scoreboard.page.ts` | Smart page for `/scoreboard`: holds a `ScoreboardStore`, calls `store.loadAll()` + `store.wireSse(...)` on init, tears them down on destroy, and switches between the 4 tab components via `*ngIf` on `store.activeTab()`. |
| `ScoreboardStore` | `frontend/src/app/features/scoreboard/scoreboard.store.ts` | Root-provided signal store backing `/scoreboard`: holds the four projections, active tab, loading + error state, and the SSE lifecycle (`wireSse` / `stop` / exponential reconnect on transport error). Mutates all four tabs from each `solve` frame via pure helpers (`parseSolveEventIntoRanking`, `mutateMatrixFromSolve`, `applySolveToGraph`, `dedupEventLogBySolveId`). |
| `ScoreboardApiService` | `frontend/src/app/features/scoreboard/scoreboard.service.ts` | HTTP wrapper around `/api/v1/scoreboard/{ranking,matrix,event-log,graph}` returning typed `ApiErrorEnvelope`s. |
| `scoreboard.pure.ts` | `frontend/src/app/features/scoreboard/scoreboard.pure.ts` | Pure types + helpers shared between the store and the four tab components (`RankingRow`, `MatrixView`, `EventLogRow`, `GraphView`, `SolveLivePayload`, `PLAYER_COLOR_PALETTE`, `stablePlayerColorIndex`, `applyRankingSort`, `parseSolveEventIntoRanking`, `dedupEventLogBySolveId`, `applySolveToGraph`, `mutateMatrixFromSolve`, `formatSolveDateTime`). |
| `ScoreboardTabsComponent` | `frontend/src/app/features/scoreboard/tabs.component.ts` | Horizontal tab strip bound to `store.activeTab()`; emits `(select)` to switch tabs. |
| `RankingComponent` / `MatrixComponent` / `EventLogComponent` / `ScoreGraphComponent` | `frontend/src/app/features/scoreboard/` | Presentational components for the 4 tabs; each binds a single `@Input()` from `ScoreboardStore` and uses `PLAYER_COLOR_PALETTE` + `stablePlayerColorIndex` to color each player consistently. |
| `NotificationService` | `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. |
| `errorNotificationInterceptor` | `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Functional HTTP interceptor that pushes a friendly message into `NotificationService` for every `HttpErrorResponse`; suppresses duplicate toasts for endpoints with their own error UI (login form, `/challenges/status` snapshot). |
@@ -121,5 +127,6 @@ for the full contract and tester matrix.
- [Authenticated Shell](/guides/authenticated-shell.md)
- [Event Window](/guides/event-window.md)
- [Challenges Board](/guides/challenges-board.md)
- [Scoreboard Page](/guides/scoreboard-page.md)
- [Notifications](/guides/notifications.md)
- [System Overview](/architecture/overview.md)
+19 -1
View File
@@ -35,8 +35,11 @@ timestamp: 2026-07-23T04:05:00Z
| `backend/src/modules/challenges/challenges.controller.ts` | Registers `/api/v1/challenges/{board,status,:id,:id/solves}` (all JWT-protected). |
| `backend/src/modules/challenges/events.controller.ts` | Authenticated SSE `/api/v1/events` merging status ticks + hub `general` pushes + `scoreboard$` solve frames. |
| `backend/src/modules/challenges/challenges.service.ts` | `getBoard` / `getDetail` / `submitFlag` + `dataSource.transaction` for race-safe idempotent solves; publishes `solve` SSE frames on success. |
| `backend/src/modules/challenges/scoreboard.controller.ts` | Registers the four authenticated scoreboard read-only endpoints under `/api/v1/scoreboard/{ranking,matrix,event-log,graph}`. |
| `backend/src/modules/challenges/scoreboard.service.ts` | Builds the `RankingRowDto[]`, `MatrixViewDto`, `EventLogRowDto[]`, and `GraphViewDto` projections used by the `/scoreboard` page; consumes `SettingsService` for the event window and `EventStatusService` for `state`. |
| `backend/src/modules/challenges/dto/scoreboard.dto.ts` | `EventLogQuerySchema` (zod) + TypeScript DTO interfaces (`RankingRowDto`, `MatrixViewDto`, `EventLogRowDto`, `GraphViewDto`, `GraphSeriesDto`, `GraphPointDto`) for the four scoreboard endpoints. |
| `backend/src/modules/challenges/dto/challenges.dto.ts` | zod contracts (`SolveSubmitBodySchema`, `BoardQuerySchema`, `ChallengeIdParamSchema`) + DTO types for the player-facing board. |
| `backend/src/modules/challenges/scoring.util.ts` | Pure `computeLivePoints`, `rankBonusForPosition`, `compareDifficulty`, and constant-time `safeEqualString` for flag comparison. |
| `backend/src/modules/challenges/scoring.util.ts` | Pure `computeLivePoints`, `rankBonusForPosition`, `compareDifficulty`, `computeAwardedPoints` (base + rank bonus), `PLAYER_COLOR_PALETTE` + `stablePlayerColorIndex` (FNV-1a → palette index), and constant-time `safeEqualString` for flag comparison. |
| `backend/src/common/errors/error-codes.ts` | Canonical error code map (now includes `EVENT_NOT_RUNNING`, `FLAG_INCORRECT`, `CHALLENGE_DISABLED`). |
# Frontend
@@ -79,6 +82,16 @@ timestamp: 2026-07-23T04:05:00Z
| `frontend/src/app/features/challenges/category-column.component.ts` | Renders a single category column (header + cards) on the challenges board. |
| `frontend/src/app/features/challenges/challenge-card.component.ts` | Renders a single challenge card on the board as a semantic `<button>` (difficulty, live points, solve count, `✓` when solved by the player). Exposes `data-testid="challenge-card-<id>"`, `data-solved`, `aria-pressed`, an accessible name (`Challenge <name>, solved|not solved`), and an inner `data-testid="challenge-check-<id>"` for the solved glyph. |
| `frontend/src/app/features/challenges/challenge-modal.component.ts` | Challenge detail modal: description, flag form, awarded banner, solvers list, live-solve updates. |
| `frontend/src/app/features/scoreboard/scoreboard.page.ts` | `/scoreboard` smart page: 4 tabs (Ranking / Matrix / Event Log / Score Graph) wired through `ScoreboardStore`, SSE `solve`-frame subscription on `/api/v1/events`, and lifecycle (`loadAll` + `wireSse` on init, `stop()` on destroy). |
| `frontend/src/app/features/scoreboard/scoreboard.store.ts` | Signal store for the four scoreboard projections + active tab + SSE lifecycle; mutates `ranking`, `matrix`, `event-log`, and `graph` state from each `solve` frame via pure helpers (`parseSolveEventIntoRanking`, `mutateMatrixFromSolve`, `applySolveToGraph`, `dedupEventLogBySolveId`); exposes `wireSse`/`stop`/`reset` and a 1s→30s exponential reconnect loop on transport error. |
| `frontend/src/app/features/scoreboard/scoreboard.service.ts` | HTTP client for `/api/v1/scoreboard/{ranking,matrix,event-log,graph}` returning typed `ApiErrorEnvelope`s. |
| `frontend/src/app/features/scoreboard/scoreboard.pure.ts` | Pure types and helpers: `RankingRow`, `MatrixView`, `EventLogRow`, `GraphView`, `SolveLivePayload`, `PLAYER_COLOR_PALETTE`, `stablePlayerColorIndex`, `applyRankingSort`, `parseSolveEventIntoRanking`, `dedupEventLogBySolveId`, `applySolveToGraph`, `mutateMatrixFromSolve`, `formatSolveDateTime`. |
| `frontend/src/app/features/scoreboard/tabs.component.ts` | Horizontal tab strip (`Ranking` / `Matrix` / `Event Log` / `Score Graph`) bound to `ScoreboardStore.activeTab`. |
| `frontend/src/app/features/scoreboard/ranking.component.ts` | Ranking tab table (rank, player with color swatch, solved count, points). |
| `frontend/src/app/features/scoreboard/matrix.component.ts` | Matrix tab — sticky-header, sticky-player grid of cells (`★` gold/silver/bronze, `✓`, blank) with player color swatches. |
| `frontend/src/app/features/scoreboard/event-log.component.ts` | Event Log tab — newest-first `<ol>` of solves with timestamp, player, challenge, awarded points; position 13 use gold/silver/bronze stars, others use a green check. |
| `frontend/src/app/features/scoreboard/score-graph.component.ts` | Score Graph tab — pure-SVG line chart of top 10 cumulative points over the event window with per-player colored polylines and a legend; renders empty / countdown / unconfigured states. |
| `frontend/src/app/features/scoreboard/scoreboard.page.ts` | `/scoreboard` smart page (also listed above): gates the four tabs, wires `store.loadAll()` and `store.wireSse(...)` on init, and tears them down via `store.stop()` on destroy. |
| `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. |
| `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Translates `HttpErrorResponse` into friendly messages and pushes them to `NotificationService` (with suppression for `/api/v1/auth/{login,csrf,register}` and `/api/v1/challenges/status`). |
| `tests/frontend/challenges.pure.spec.ts` | Pure helpers: sorting, parsers, friendly error mapping, `formatDdHhMm` (`DD:HH:mm:ss`). |
@@ -91,6 +104,10 @@ timestamp: 2026-07-23T04:05:00Z
| `tests/backend/challenges-status-rest.spec.ts` | `/api/v1/challenges/status` snapshot shape. |
| `tests/backend/challenges-events-sse.spec.ts` | `/api/v1/events` SSE: status + solve frames, dedup, ordering. |
| `tests/backend/challenges-submit-flag.spec.ts` | Submit flow: correct/incorrect flag, idempotent re-submit, event-state guard, race handling. |
| `tests/backend/scoreboard-controller.spec.ts` | Contract tests for the four `/api/v1/scoreboard/*` endpoints: JWT protection, ranking sort + competition rank, matrix cell assignment (1/2/3/solved/null), event-log `limit` clamp, graph boundaries + top-10 trim + empty state. |
| `tests/backend/scoreboard.service.spec.ts` | Service-level tests covering `computeAwardedPoints` parity with `submitFlag`, color-index stability, matrix ordering, and graph state handling. |
| `tests/frontend/scoreboard.pure.spec.ts` | Pure helpers: `applyRankingSort` (competition-rank numbering), `parseSolveEventIntoRanking` (add-or-increment + sort), `dedupEventLogBySolveId`, `applySolveToGraph` (anchors + plateau + top-10 trim), `mutateMatrixFromSolve`, `stablePlayerColorIndex`, `formatSolveDateTime`. |
| `tests/frontend/scoreboard.store.spec.ts` | Store lifecycle + live merge: `loadAll` aggregation + idempotency, SSE `solve` frame applying to all four tabs, `stop()` teardown, exponential reconnect. |
# See also
@@ -98,5 +115,6 @@ timestamp: 2026-07-23T04:05:00Z
- [Authenticated Shell](/guides/authenticated-shell.md)
- [System Endpoints](/api/system.md)
- [Challenges Endpoints](/api/challenges.md)
- [Scoreboard Page Guide](/guides/scoreboard-page.md)
- [Challenges Board](/guides/challenges-board.md)
- [Notifications](/guides/notifications.md)