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:
+170
-2
@@ -2,8 +2,8 @@
|
||||
type: api
|
||||
title: Challenges Endpoints
|
||||
description: Authenticated challenge board, single-challenge detail, flag submission, event-state snapshot, and the combined authenticated SSE stream that pushes status + solve frames.
|
||||
tags: [api, challenges, board, score, sse, submit]
|
||||
timestamp: 2026-07-23T00:10:00Z
|
||||
tags: [api, challenges, board, score, scoreboard, sse, submit]
|
||||
timestamp: 2026-07-23T05:10:00Z
|
||||
---
|
||||
|
||||
# Endpoints
|
||||
@@ -21,6 +21,10 @@ are not `@Public()`). The DTOs and zod schemas live in
|
||||
| `GET` | `/api/v1/challenges/:id` | Authenticated | Same. |
|
||||
| `POST` | `/api/v1/challenges/:id/solves` | Authenticated | Same. |
|
||||
| `SSE` | `/api/v1/events` | Authenticated (SSE) | `backend/src/modules/challenges/events.controller.ts` |
|
||||
| `GET` | `/api/v1/scoreboard/ranking` | Authenticated | `backend/src/modules/challenges/scoreboard.controller.ts` |
|
||||
| `GET` | `/api/v1/scoreboard/matrix` | Authenticated | Same. |
|
||||
| `GET` | `/api/v1/scoreboard/event-log` | Authenticated | Same. |
|
||||
| `GET` | `/api/v1/scoreboard/graph` | Authenticated | Same. |
|
||||
|
||||
> The combined `/api/v1/events` SSE is **in addition to** the existing
|
||||
> `/api/v1/events/status` status-only stream. The two share the same
|
||||
@@ -202,6 +206,169 @@ downstream consumer can read either shape (for example
|
||||
`challenge_id` or `challengeId`, `player_id` or `userId`,
|
||||
`awarded_points` or `pointsAwarded`).
|
||||
|
||||
# Scoreboard endpoints
|
||||
|
||||
The `ScoreboardController` mounts the four authenticated read-only
|
||||
projections that drive the `/scoreboard` page (Ranking, Matrix,
|
||||
Event Log, Score Graph tabs). All four live under
|
||||
`/api/v1/scoreboard/*` and are served by
|
||||
`backend/src/modules/challenges/scoreboard.controller.ts` via
|
||||
`ScoreboardService`
|
||||
(`backend/src/modules/challenges/scoreboard.service.ts`). The DTO
|
||||
contracts live in
|
||||
`backend/src/modules/challenges/dto/scoreboard.dto.ts` (zod
|
||||
`EventLogQuerySchema` plus TypeScript interfaces for the response
|
||||
shapes).
|
||||
|
||||
All four endpoints:
|
||||
|
||||
* Require a valid JWT (the global `JwtAuthGuard` is active; no
|
||||
`@Public()` is used on the scoreboard controller).
|
||||
* Read exclusively — no mutation — and never expose the `challenge.flag`
|
||||
field or any admin-only data.
|
||||
* Compute `awardedPoints` per row using
|
||||
`computeAwardedPoints(initial, minimum, decaySolves, solveCountBefore)`
|
||||
from `backend/src/modules/challenges/scoring.util.ts`, which is the
|
||||
same `basePoints + rankBonus` arithmetic that `submitFlag` performs
|
||||
when persisting the `solve` row.
|
||||
* Color each player via `stablePlayerColorIndex(playerId)` (FNV-1a
|
||||
hash → 10-entry `PLAYER_COLOR_PALETTE`) so the Ranking, Matrix, and
|
||||
Score Graph share a deterministic swatch per `playerId`.
|
||||
|
||||
## `GET /api/v1/scoreboard/ranking`
|
||||
|
||||
Returns every user (including zero-solve players) ranked by total
|
||||
points, with competition-rank numbering (`1, 2, 2, 4, …` — players with
|
||||
equal `points` share a rank).
|
||||
|
||||
Response (`RankingRowDto[]`):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"rank": 1,
|
||||
"playerId": "<user-uuid>",
|
||||
"playerName": "alice",
|
||||
"solvedCount": 7,
|
||||
"points": 3120,
|
||||
"colorIndex": 4
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`colorIndex` is the same value the frontend uses to tint the
|
||||
per-player swatch in the Ranking, Matrix, and Score Graph tabs so a
|
||||
player keeps their color across all three views.
|
||||
|
||||
## `GET /api/v1/scoreboard/matrix`
|
||||
|
||||
Returns a players × challenges grid for the live scoreboard view.
|
||||
|
||||
Response (`MatrixViewDto`):
|
||||
|
||||
```json
|
||||
{
|
||||
"players": [ /* RankingRowDto[] in ranking order */ ],
|
||||
"challenges": [
|
||||
{
|
||||
"id": "<challenge-uuid>",
|
||||
"name": "RSA Rollers",
|
||||
"solveCount": 4,
|
||||
"categoryAbbreviation": "CRY"
|
||||
}
|
||||
],
|
||||
"cells": {
|
||||
"<player-uuid>": {
|
||||
"<challenge-uuid>": 1,
|
||||
"<challenge-uuid>": 2,
|
||||
"<challenge-uuid>": 3,
|
||||
"<challenge-uuid>": "solved",
|
||||
"<challenge-uuid>": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`cells[playerId][challengeId]` is one of `1` (gold ★, 1st solver),
|
||||
`2` (silver ★, 2nd solver), `3` (bronze ★, 3rd solver), `'solved'`
|
||||
(✓, 4th+ solver), or `null` (not solved). Challenges are sorted by
|
||||
`solveCount` DESC, then by `name` ASC. The service walks the `solve`
|
||||
rows in `solved_at ASC` order per challenge to assign each cell its
|
||||
first solver rank.
|
||||
|
||||
## `GET /api/v1/scoreboard/event-log`
|
||||
|
||||
Recent solves, newest first, joined with the user, challenge, and
|
||||
category so the frontend never needs a second round-trip to render
|
||||
labels.
|
||||
|
||||
Query string (`EventLogQuerySchema`, validated by `ZodValidationPipe`):
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|---------|------|---------|----------------------------------------------|
|
||||
| `limit` | int | `50` | `1 <= limit <= 200`, clamped server-side too. |
|
||||
|
||||
Response (`EventLogRowDto[]`):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"solveId": "<solve-uuid>",
|
||||
"playerId": "<user-uuid>",
|
||||
"playerName": "bob",
|
||||
"challengeId": "<challenge-uuid>",
|
||||
"challengeName": "Forensic Trail",
|
||||
"categoryAbbreviation": "FOR",
|
||||
"position": 1,
|
||||
"awardedPoints": 515,
|
||||
"awardedAtUtc": "2026-07-23T05:01:30.000Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## `GET /api/v1/scoreboard/graph`
|
||||
|
||||
Time-series of cumulative points for the top 10 players over the
|
||||
configured event window, used to drive the Score Graph tab.
|
||||
|
||||
Response (`GraphViewDto`):
|
||||
|
||||
```json
|
||||
{
|
||||
"startUtc": "2026-07-23T05:00:00.000Z",
|
||||
"endUtc": "2026-07-23T09:00:00.000Z",
|
||||
"serverNowUtc": "2026-07-23T05:10:00.000Z",
|
||||
"state": "running",
|
||||
"series": [
|
||||
{
|
||||
"playerId": "<user-uuid>",
|
||||
"playerName": "alice",
|
||||
"colorIndex": 4,
|
||||
"points": [
|
||||
{ "tUtc": "2026-07-23T05:00:00.000Z", "value": 0 },
|
||||
{ "tUtc": "2026-07-23T05:01:30.000Z", "value": 515 },
|
||||
{ "tUtc": "2026-07-23T05:09:50.000Z", "value": 980 },
|
||||
{ "tUtc": "2026-07-23T09:00:00.000Z", "value": 980 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`state` mirrors the 4-state event window (`running` / `countdown` /
|
||||
`stopped` / `unconfigured`) and `startUtc` / `endUtc` come from
|
||||
`SettingsService` via the admin general-settings pipeline. When the
|
||||
event has not yet started (`countdown`) or has no window configured
|
||||
(`unconfigured`), the endpoint still returns `200` with empty
|
||||
`series` and `null` boundaries so the frontend can render the
|
||||
"Awaiting event configuration" / "Not started" empty states.
|
||||
|
||||
Series are sorted by their final cumulative value DESC and trimmed to
|
||||
the top 10. Each series starts with a `{tUtc: startUtc, value: 0}`
|
||||
anchor and (when `endUtc` is known) ends with a
|
||||
`{tUtc: endUtc, value: finalValue}` step so the line plot shows a
|
||||
flat plateau between the last solve and the event end.
|
||||
|
||||
# Errors
|
||||
|
||||
The challenges endpoints use three additional error codes from
|
||||
@@ -221,5 +388,6 @@ The full error envelope and middleware are documented in
|
||||
- [Challenge Tables](/database/challenges.md)
|
||||
- [System Endpoints](/api/system.md) (legacy `/events/status` and `/event/stream`)
|
||||
- [Scoreboard Stream](/guides/scoreboard-stream.md)
|
||||
- [Scoreboard Page Guide](/guides/scoreboard-page.md)
|
||||
- [Challenges Board Guide](/guides/challenges-board.md)
|
||||
- [Backend Module Map](/architecture/backend-modules.md)
|
||||
Reference in New Issue
Block a user