docs: update documentation to OKF v0.1 format

This commit is contained in:
OpenVelo Agent
2026-07-23 00:13:56 +00:00
parent dcda3dfd4d
commit 77d31e0ee1
8 changed files with 576 additions and 11 deletions
+220
View File
@@ -0,0 +1,220 @@
---
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
---
# Endpoints
All routes are mounted by `ChallengesModule`
(`backend/src/modules/challenges/challenges.module.ts`) and require
authentication (`JwtAuthGuard` is registered globally and these handlers
are not `@Public()`). The DTOs and zod schemas live in
`backend/src/modules/challenges/dto/challenges.dto.ts`.
| Method | Path | Auth | Source |
|--------|-----------------------------------|-------------|-----------------------------------------------------------|
| `GET` | `/api/v1/challenges/board` | Authenticated | `backend/src/modules/challenges/challenges.controller.ts` |
| `GET` | `/api/v1/challenges/status` | Authenticated | Same. |
| `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` |
> The combined `/api/v1/events` SSE is **in addition to** the existing
> `/api/v1/events/status` status-only stream. The two share the same
> authenticated transport but serve different consumers — the
> `ChallengesPage` opens `/api/v1/events` so it can react to `solve`
> frames in addition to status, while the shell header keeps reading
> `/api/v1/events/status` for the LED/countdown only.
# `GET /api/v1/challenges/board`
Authenticated board of enabled challenges grouped by category. Query
string:
| Param | Type | Description |
|-----------|--------|----------------------------------------------------------------------------------------------|
| `include` | string | Comma-separated feature list. Passing `solvers` attaches a per-card `solvers` array (ordered by `solved_at ASC`). |
The handler (`ChallengesService.getBoard`) returns:
```json
{
"columns": [
{
"id": "<category-uuid>",
"abbreviation": "CRY",
"name": "Cryptography",
"iconPath": "/uploads/icons/CRY.png",
"cards": [
{
"id": "<challenge-uuid>",
"name": "RSA Rollers",
"descriptionMd": "...",
"categoryId": "<category-uuid>",
"categoryAbbreviation": "CRY",
"categoryIconPath": "/uploads/icons/CRY.png",
"difficulty": "MEDIUM",
"livePoints": 480,
"solveCount": 4,
"solvedByMe": false,
"solvers": [ /* SolverRow[] when ?include=solvers */ ]
}
]
}
],
"solvedChallengeIds": ["<uuid>", "..."],
"perChallengeLivePoints": { "<uuid>": 480 }
}
```
`livePoints` is computed by
`computeLivePoints(initialPoints, minimumPoints, decaySolves, solveCount)`
(`backend/src/modules/challenges/scoring.util.ts`):
```
if (currentSolveCount >= decaySolves) return minimum;
raw = initial - (initial - minimum) * (currentSolveCount / decaySolves);
return max(minimum, round(raw));
```
Columns are sorted alphabetically by `abbreviation`; cards within a
column are sorted by `difficulty` (`LOW < MEDIUM < HIGH`) then by name.
Only `enabled = 1` rows are returned. The query joins
`challenge` and `category` once and groups in memory.
# `GET /api/v1/challenges/status`
Authenticated event-window snapshot (the same `EventStatePayload`
returned by the legacy `/api/v1/event/status` public endpoint, but
gated by JWT and shaped to the canonical 4-state payload used by the
SPA). Built by
`backend/src/common/services/event-status.service.ts:getState()`.
`ChallengesPage` calls this once on init to populate the event state
before the SSE frame arrives, so the gate UI is never blank on slow
networks.
# `GET /api/v1/challenges/:id`
Single challenge detail with the full `SolverRow[]` for that
challenge. `:id` must be a UUID (validated by `ChallengeIdParamSchema`).
The response shape is:
```json
{
"challenge": { /* BoardCard */ },
"solvers": [ /* SolverRow[] sorted by solvedAtUtc ASC */ ]
}
```
`404 NOT_FOUND` is returned for unknown ids and for disabled
challenges.
# `POST /api/v1/challenges/:id/solves`
Submit a flag. Body is validated by `SolveSubmitBodySchema`
(`{ flag: string, min 1, max 4096 }`). Response:
```json
{
"status": "solved" | "already_solved",
"challenge": { /* BoardCard with solvedByMe=true and the new solveCount */ },
"awarded": {
"position": 1,
"basePoints": 500,
"rankBonus": 15,
"awardedPoints": 515,
"awardedAtUtc": "2026-07-23T00:01:30.000Z"
},
"solvers": [ /* SolverRow[] */ ]
}
```
The full transaction (`ChallengesService.submitFlag`):
1. Loads the challenge + category inside a `dataSource.transaction`.
2. Reads `EventStatusService.getState()`; rejects with
`409 EVENT_NOT_RUNNING` when `state !== 'running'`.
3. Looks for an existing `(challengeId, userId)` `solve` row.
* If found, returns `status: 'already_solved'` with the original
awarded points and the current solver list (no new row is
inserted; the unique index `uq_solve_challenge_user` would block
it anyway).
4. Constant-time compares the submitted flag against `challenge.flag`
via `safeEqualString`. On mismatch rejects with
`400 FLAG_INCORRECT`.
5. Counts existing solves (`position = count + 1`).
6. Computes `basePoints = computeLivePoints(...)` for `position - 1`,
`rankBonus = rankBonusForPosition(position)` (`15 / 10 / 5 / 0`),
`awardedPoints = basePoints + rankBonus`.
7. Inserts the new `solve` row with `is_first` / `is_second` /
`is_third` flags populated.
8. On `SQLITE_CONSTRAINT_UNIQUE` (race) falls back to the
`already_solved` shape.
9. On success, publishes a solve frame via
`SseHubService.emitScoreboard(...)` (see below) so every
`/api/v1/events` subscriber sees the live event.
The flag is **never** returned in any DTO exposed by this controller
(it is read only inside the `dataSource.transaction` and is not
included in `BoardCard`, `SolverRow`, `AwardedSolve`, or
`SolveResponse`).
# `SSE GET /api/v1/events`
Authenticated global SSE stream mounted by
`backend/src/modules/challenges/events.controller.ts`. It merges three
sources and emits `MessageEvent` frames whose `type` is one of:
| `event:` | Payload |
|----------|-------------------------------------------------------------------------------------------------|
| `status` | `{ state, server_time_utc, event_start_utc, event_end_utc, seconds_to_start, seconds_to_end }` |
| `solve` | `{ challenge_id, player_id, player_name, awarded_points, rank_bonus, awarded_at_utc, position, live_points, solve_count, initial_points, minimum_points, decay_solves }` |
Sources:
| Source | Trigger |
|-----------------------------------------|------------------------------------------------------------------------------------|
| `interval(60_000).pipe(startWith(0))` | Refresh the full `status` payload every 60 seconds so a disconnected client self-heals. |
| `SseHubService.event$()` (filter `'general'` topic) | Re-publish the current `status` whenever an admin updates general settings. |
| `SseHubService.event$()` (legacy event payloads) | Legacy fallback for pre-existing event-window broadcasts. |
| `SseHubService.scoreboard$()` | Every fresh `solve` published by `ChallengesService.submitFlag`. |
Consecutive identical `status` frames are suppressed by
`distinctUntilChanged(JSON.stringify)`. The `solve` frames are
appended as-is.
## Solve frame wire format
`SseHubService.emitScoreboard({...})` is called with a flat
`SolveEventPayload` (see
`backend/src/modules/challenges/dto/challenges.dto.ts`). The
controller maps both camelCase and snake_case aliases so a
downstream consumer can read either shape (for example
`challenge_id` or `challengeId`, `player_id` or `userId`,
`awarded_points` or `pointsAwarded`).
# Errors
The challenges endpoints use three additional error codes from
`backend/src/common/errors/error-codes.ts`:
| Code | HTTP | When |
|---------------------|------|------------------------------------------------------------|
| `EVENT_NOT_RUNNING` | 409 | Flag submitted while the event is in `countdown` / `stopped` / `unconfigured`. |
| `FLAG_INCORRECT` | 400 | Submitted flag did not constant-time-equal `challenge.flag`. |
| `CHALLENGE_DISABLED`| — | Reserved for future use (admin-side disabled challenges already return `404 NOT_FOUND` from this controller). |
The full error envelope and middleware are documented in
[REST API Overview](/api/rest-overview.md).
# See also
- [Challenge Tables](/database/challenges.md)
- [System Endpoints](/api/system.md) (legacy `/events/status` and `/event/stream`)
- [Scoreboard Stream](/guides/scoreboard-stream.md)
- [Challenges Board Guide](/guides/challenges-board.md)
- [Backend Module Map](/architecture/backend-modules.md)
+15 -3
View File
@@ -3,7 +3,7 @@ type: api
title: REST API Overview title: REST API Overview
description: Base URL, versioning, auth, CSRF, and the standard error envelope. description: Base URL, versioning, auth, CSRF, and the standard error envelope.
tags: [api, rest, overview, csrf] tags: [api, rest, overview, csrf]
timestamp: 2026-07-21T22:19:08Z timestamp: 2026-07-23T00:10:00Z
--- ---
# Base URL & versioning # Base URL & versioning
@@ -78,7 +78,18 @@ endpoint). Full list lives in `backend/src/common/errors/error-codes.ts`:
`CSRF_INVALID`, `WEAK_PASSWORD`, `USERNAME_TAKEN`, `CSRF_INVALID`, `WEAK_PASSWORD`, `USERNAME_TAKEN`,
`INVALID_CREDENTIALS`, `TOKEN_REVOKED`, `THEME_INVALID`, `INVALID_CREDENTIALS`, `TOKEN_REVOKED`, `THEME_INVALID`,
`REGISTRATIONS_DISABLED`, `INVALID_OLD_PASSWORD`, `PASSWORD_POLICY`, `REGISTRATIONS_DISABLED`, `INVALID_OLD_PASSWORD`, `PASSWORD_POLICY`,
`PASSWORDS_DO_NOT_MATCH`, `INTERNAL`. `PASSWORDS_DO_NOT_MATCH`, `CATEGORY_HAS_CHALLENGES`, `SYSTEM_PROTECTED`,
`CHALLENGE_NAME_TAKEN`, `CHALLENGE_INVALID_CATEGORY`,
`CHALLENGE_INVALID_POINTS`, `CHALLENGE_INVALID_PORT`,
`CHALLENGE_INVALID_IP`, `CHALLENGE_FILE_LIMIT`,
`IMPORT_VALIDATION_FAILED`, `IMPORT_PARTIAL_FAILURE`,
`EVENT_NOT_RUNNING`, `FLAG_INCORRECT`, `CHALLENGE_DISABLED`, `INTERNAL`.
The player-facing codes (`EVENT_NOT_RUNNING`, `FLAG_INCORRECT`) are
emitted by the challenges submit endpoint; see
[Challenges Endpoints](/api/challenges.md) for the full set of HTTP
statuses and the canonical messages produced by
`messageForSolveError`.
# See also # See also
@@ -86,4 +97,5 @@ endpoint). Full list lives in `backend/src/common/errors/error-codes.ts`:
- [Admin Endpoints](/api/admin.md) - [Admin Endpoints](/api/admin.md)
- [Setup Endpoint](/api/setup.md) - [Setup Endpoint](/api/setup.md)
- [System Endpoints](/api/system.md) - [System Endpoints](/api/system.md)
- [Uploads Endpoints](/api/uploads.md) - [Uploads Endpoints](/api/uploads.md)
- [Challenges Endpoints](/api/challenges.md)
+5 -1
View File
@@ -3,7 +3,7 @@ type: architecture
title: Backend Module Map title: Backend Module Map
description: NestJS modules, controllers, services, and how they are wired together. description: NestJS modules, controllers, services, and how they are wired together.
tags: [architecture, backend, nestjs, modules] tags: [architecture, backend, nestjs, modules]
timestamp: 2026-07-21T22:19:08Z timestamp: 2026-07-23T00:10:00Z
--- ---
# Module Map # Module Map
@@ -30,6 +30,7 @@ also registers two global providers:
| `AdminModule` | `backend/src/modules/admin/admin.module.ts` | Four controllers (`AdminController` for `/api/v1/admin/users*`, `AdminGeneralController` for `/api/v1/admin/general/*`, `AdminCategoriesController` for `/api/v1/admin/categories*`, `AdminChallengesController` for `/api/v1/admin/challenges*`) plus their services (`AdminService`, `AdminGeneralService`, `AdminCategoriesService`, `AdminChallengesService`, `ChallengeFilesService`). Gated by `AdminGuard` + `@Roles('admin')`. Imports `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity])`, `AuthModule`, `UsersModule`, `SettingsModule`, and `CommonModule` (for `SseHubService`, `ThemeLoaderService`). | | `AdminModule` | `backend/src/modules/admin/admin.module.ts` | Four controllers (`AdminController` for `/api/v1/admin/users*`, `AdminGeneralController` for `/api/v1/admin/general/*`, `AdminCategoriesController` for `/api/v1/admin/categories*`, `AdminChallengesController` for `/api/v1/admin/challenges*`) plus their services (`AdminService`, `AdminGeneralService`, `AdminCategoriesService`, `AdminChallengesService`, `ChallengeFilesService`). Gated by `AdminGuard` + `@Roles('admin')`. Imports `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity])`, `AuthModule`, `UsersModule`, `SettingsModule`, and `CommonModule` (for `SseHubService`, `ThemeLoaderService`). |
| `UploadsModule` | `backend/src/modules/uploads/uploads.module.ts` | `UploadsController` (`/api/v1/uploads/*`). Admin-only multipart. | | `UploadsModule` | `backend/src/modules/uploads/uploads.module.ts` | `UploadsController` (`/api/v1/uploads/*`). Admin-only multipart. |
| `BlogModule` | `backend/src/modules/blog/blog.module.ts` | `BlogController` (`GET /api/v1/blog/posts`) + `BlogService` (lists `status='published'` rows). | | `BlogModule` | `backend/src/modules/blog/blog.module.ts` | `BlogController` (`GET /api/v1/blog/posts`) + `BlogService` (lists `status='published'` rows). |
| `ChallengesModule` | `backend/src/modules/challenges/challenges.module.ts` | `ChallengesController` (`/api/v1/challenges/{board,status,:id,:id/solves}`) + `ChallengesEventsController` (authenticated SSE `/api/v1/events`) + `ChallengesService` (board, detail, submit, scoring util). |
| `FrontendModule` | `backend/src/frontend/frontend.module.ts` | Registers `SpaFallbackMiddleware` and the uploads static helper. | | `FrontendModule` | `backend/src/frontend/frontend.module.ts` | Registers `SpaFallbackMiddleware` and the uploads static helper. |
# Controllers # Controllers
@@ -46,6 +47,8 @@ also registers two global providers:
| `SystemController` | `/api/v1` | Mixed (bootstrap/event/scoreboard/settings are public; `events/status` SSE is JWT-protected) | `backend/src/modules/system/system.controller.ts` | | `SystemController` | `/api/v1` | Mixed (bootstrap/event/scoreboard/settings are public; `events/status` SSE is JWT-protected) | `backend/src/modules/system/system.controller.ts` |
| `UploadsController` | `/api/v1/uploads` | Admin only | `backend/src/modules/uploads/uploads.controller.ts` | | `UploadsController` | `/api/v1/uploads` | Admin only | `backend/src/modules/uploads/uploads.controller.ts` |
| `BlogController` | `/api/v1/blog` | Public | `backend/src/modules/blog/blog.controller.ts` | | `BlogController` | `/api/v1/blog` | Public | `backend/src/modules/blog/blog.controller.ts` |
| `ChallengesController` | `/api/v1/challenges` | Authenticated | `backend/src/modules/challenges/challenges.controller.ts` |
| `ChallengesEventsController` | `/api/v1/events` | Authenticated (SSE) | `backend/src/modules/challenges/events.controller.ts` |
# Common services # Common services
@@ -63,6 +66,7 @@ also registers two global providers:
| `ThemeLoaderService` | `backend/src/common/utils/theme-loader.service.ts` | Loads + validates the 10 canonical themes. | | `ThemeLoaderService` | `backend/src/common/utils/theme-loader.service.ts` | Loads + validates the 10 canonical themes. |
| `ZodValidationPipe` | `backend/src/common/pipes/zod-validation.pipe.ts` | Replaces `ValidationPipe` for zod-validated DTOs. | | `ZodValidationPipe` | `backend/src/common/pipes/zod-validation.pipe.ts` | Replaces `ValidationPipe` for zod-validated DTOs. |
| `TransformInterceptor` | `backend/src/common/interceptors/transform.interceptor.ts` | Optional response transformer. | | `TransformInterceptor` | `backend/src/common/interceptors/transform.interceptor.ts` | Optional response transformer. |
| `scoring.util` | `backend/src/modules/challenges/scoring.util.ts` | `computeLivePoints`, `rankBonusForPosition`, `compareDifficulty`, and `safeEqualString` (constant-time flag compare). |
# Decorators # Decorators
+14 -3
View File
@@ -2,8 +2,8 @@
type: architecture type: architecture
title: Frontend Structure title: Frontend Structure
description: Angular routes, components, services, guards, interceptors, and authenticated SSE transport. description: Angular routes, components, services, guards, interceptors, and authenticated SSE transport.
tags: [architecture, frontend, angular, shell, sse] tags: [architecture, frontend, angular, shell, sse, challenges, notifications]
timestamp: 2026-07-22T10:32:00Z timestamp: 2026-07-23T00:10:00Z
--- ---
# Routes # Routes
@@ -15,7 +15,7 @@ Routes live in `frontend/src/app/app.routes.ts`:
| `/bootstrap` | `SetupCreateAdminComponent` | — | First-admin creation flow. | | `/bootstrap` | `SetupCreateAdminComponent` | — | First-admin creation flow. |
| `/login` | `LandingComponent` | `landingGuard` | Public landing and authentication modal. | | `/login` | `LandingComponent` | `landingGuard` | Public landing and authentication modal. |
| `/` | `HomeComponent` | `authGuard` | Authenticated shell and child outlet. | | `/` | `HomeComponent` | `authGuard` | Authenticated shell and child outlet. |
| `/challenges` | `ChallengesPage` | inherited | Placeholder challenges page. | | `/challenges` | `ChallengesPage` | inherited | Full challenges board (category columns, modal, flag submit, live SSE). |
| `/scoreboard` | `ScoreboardPage` | inherited | Placeholder scoreboard page. | | `/scoreboard` | `ScoreboardPage` | inherited | Placeholder scoreboard page. |
| `/blog` | `BlogPage` | inherited | Placeholder blog page. | | `/blog` | `BlogPage` | inherited | Placeholder blog page. |
| `/admin` | `AdminUsersComponent` | `adminGuard` | Admin-only child route. | | `/admin` | `AdminUsersComponent` | `adminGuard` | Admin-only child route. |
@@ -33,6 +33,12 @@ Routes live in `frontend/src/app/app.routes.ts`:
| `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Stores the access token used by the authenticated SSE transport; owns the cross-tab invalidation `BroadcastChannel` + `storage`-event fallback and exposes `clearAndBroadcast` / `onPeerInvalidation`. | | `AuthService` | `frontend/src/app/core/services/auth.service.ts` | Stores the access token used by the authenticated SSE transport; owns the cross-tab invalidation `BroadcastChannel` + `storage`-event fallback and exposes `clearAndBroadcast` / `onPeerInvalidation`. |
| `UserStore` | `frontend/src/app/core/services/user.store.ts` | Hydrates the signed-in user and rank data. | | `UserStore` | `frontend/src/app/core/services/user.store.ts` | Hydrates the signed-in user and rank data. |
| `auth-session-events.pure.ts` | `frontend/src/app/core/services/auth-session-events.pure.ts` | Pure cross-tab message encoding/validation and the namespaced channel / storage-key constants. | | `auth-session-events.pure.ts` | `frontend/src/app/core/services/auth-session-events.pure.ts` | Pure cross-tab message encoding/validation and the namespaced channel / storage-key constants. |
| `ChallengesStore` | `frontend/src/app/features/challenges/challenges.store.ts` | Signal store backing `/challenges`: board payload, event state, per-card solve listeners, SSE solve-frame mutation, submit response application. |
| `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. |
| `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). |
# Authenticated SSE wiring # Authenticated SSE wiring
@@ -64,8 +70,11 @@ on destruction.
|---|---| |---|---|
| `frontend/src/app/app.routes.ts` | Registers shell and child routes. | | `frontend/src/app/app.routes.ts` | Registers shell and child routes. |
| `frontend/src/app/core/interceptors/auth.interceptor.ts` | Adds Bearer authentication to Angular HTTP requests. | | `frontend/src/app/core/interceptors/auth.interceptor.ts` | Adds Bearer authentication to Angular HTTP requests. |
| `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Translates `HttpErrorResponse` into friendly messages and pushes them to `NotificationService` (with suppression rules for login + `/challenges/status`). |
| `frontend/src/app/core/services/authenticated-event-source.service.ts` | Adds Bearer authentication to the browser SSE request, which does not use the Angular HTTP interceptor chain. | | `frontend/src/app/core/services/authenticated-event-source.service.ts` | Adds Bearer authentication to the browser SSE request, which does not use the Angular HTTP interceptor chain. |
| `frontend/src/app/core/services/event-status.pure.ts` | Defines the event payload and transport interface. | | `frontend/src/app/core/services/event-status.pure.ts` | Defines the event payload and transport interface. |
| `frontend/src/app/features/challenges/challenges.page.ts` | `/challenges` smart page; owns gate logic, modal lifecycle, SSE wiring, and the countdown-zero reload. |
| `frontend/src/app/features/challenges/challenges.store.ts` | Signal store backing `/challenges`: board, event state, per-card solve listeners, SSE solve-frame mutation, submit response application. |
| `tests/frontend/authenticated-event-source.spec.ts` | Regression coverage for authenticated SSE transport behavior. | | `tests/frontend/authenticated-event-source.spec.ts` | Regression coverage for authenticated SSE transport behavior. |
# Event-status pure helpers # Event-status pure helpers
@@ -111,4 +120,6 @@ for the full contract and tester matrix.
- [Authenticated Shell](/guides/authenticated-shell.md) - [Authenticated Shell](/guides/authenticated-shell.md)
- [Event Window](/guides/event-window.md) - [Event Window](/guides/event-window.md)
- [Challenges Board](/guides/challenges-board.md)
- [Notifications](/guides/notifications.md)
- [System Overview](/architecture/overview.md) - [System Overview](/architecture/overview.md)
+29 -3
View File
@@ -1,9 +1,9 @@
--- ---
type: architecture type: architecture
title: Key Files Index title: Key Files Index
description: One-line responsibility for important source and contract-test files, including strict event-window validation, the public bootstrap SSE listener, and the challenges full-replace import flow. description: One-line responsibility for important source and contract-test files, including strict event-window validation, the public bootstrap SSE listener, the challenges full-replace import flow, and the authenticated player-facing challenges board.
tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import] tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import, board, notifications]
timestamp: 2026-07-22T22:17:11Z timestamp: 2026-07-23T00:10:00Z
--- ---
# Backend # Backend
@@ -31,6 +31,13 @@ timestamp: 2026-07-22T22:17:11Z
| `tests/backend/admin-general-event-window.spec.ts` | Focused contract tests for valid, empty, malformed, equal, and reversed event-window timestamps. | | `tests/backend/admin-general-event-window.spec.ts` | Focused contract tests for valid, empty, malformed, equal, and reversed event-window timestamps. |
| `tests/backend/admin-general-service.spec.ts` | General-settings schema and service tests, including per-field empty datetime failures and settings-event emission. | | `tests/backend/admin-general-service.spec.ts` | General-settings schema and service tests, including per-field empty datetime failures and settings-event emission. |
| `tests/backend/admin-challenges-import-replace.spec.ts` | Contract tests for the confirmed-import full-replace: wipes unrelated pre-existing challenges, unlinks wiped disk files, empty archive, unknown-category skip-then-wipe. | | `tests/backend/admin-challenges-import-replace.spec.ts` | Contract tests for the confirmed-import full-replace: wipes unrelated pre-existing challenges, unlinks wiped disk files, empty archive, unknown-category skip-then-wipe. |
| `backend/src/modules/challenges/challenges.module.ts` | Wires `ChallengesController`, `ChallengesEventsController`, `ChallengesService`, and the four TypeORM entities (`ChallengeEntity`, `CategoryEntity`, `SolveEntity`, `UserEntity`). |
| `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/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/common/errors/error-codes.ts` | Canonical error code map (now includes `EVENT_NOT_RUNNING`, `FLAG_INCORRECT`, `CHALLENGE_DISABLED`). |
# Frontend # Frontend
@@ -65,9 +72,28 @@ timestamp: 2026-07-22T22:17:11Z
| `tests/frontend/authenticated-event-source.spec.ts` | Tests SSE authorization, frame transport behavior, and the `401`/`403` unauthorized path. | | `tests/frontend/authenticated-event-source.spec.ts` | Tests SSE authorization, frame transport behavior, and the `401`/`403` unauthorized path. |
| `tests/frontend/auth-session-events.spec.ts` | Pure tests for cross-tab invalidation message encoding, payload validation, and `storage`-event filtering. | | `tests/frontend/auth-session-events.spec.ts` | Pure tests for cross-tab invalidation message encoding, payload validation, and `storage`-event filtering. |
| `tests/frontend/admin-challenges-import-autoclose.spec.ts` | Pure helpers (`summarizeImportResult`, `importErrorMessage`) and modal auto-close branch for the confirmed-import handler. | | `tests/frontend/admin-challenges-import-autoclose.spec.ts` | Pure helpers (`summarizeImportResult`, `importErrorMessage`) and modal auto-close branch for the confirmed-import handler. |
| `frontend/src/app/features/challenges/challenges.page.ts` | `/challenges` smart page: gate logic (countdown/running/stopped/unconfigured), modal lifecycle, SSE wiring on `/api/v1/events`, and the countdown-zero reload. |
| `frontend/src/app/features/challenges/challenges.store.ts` | Signal store: board, event state, per-card solve listeners, SSE solve-frame mutation, submit response application, stop() lifecycle. |
| `frontend/src/app/features/challenges/challenges.service.ts` | HTTP service for `/api/v1/challenges/{board,status,:id,:id/solves}` returning typed `ApiErrorEnvelope`s. |
| `frontend/src/app/features/challenges/challenges.pure.ts` | Pure types and helpers (`BoardCard`, `SolverRow`, `SolveEventPayload`, `parseSolveEvent`, `mergeSolveEventIntoSolvers`, `messageForSolveError`, `formatDdHhMm`). |
| `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 (difficulty, live points, solve count, `✓` when solved by the player). |
| `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/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`. |
| `tests/frontend/challenges.store.spec.ts` | Signal store: board mutation, live solve merge, listeners, `stop()`. |
| `tests/frontend/notification-interceptor.spec.ts` | Interceptor suppression rules and friendly message mapping. |
| `tests/backend/challenges-board.spec.ts` | Board query shape, ordering, `?include=solvers`. |
| `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. |
# See also # See also
- [Frontend Structure](/architecture/frontend-structure.md) - [Frontend Structure](/architecture/frontend-structure.md)
- [Authenticated Shell](/guides/authenticated-shell.md) - [Authenticated Shell](/guides/authenticated-shell.md)
- [System Endpoints](/api/system.md) - [System Endpoints](/api/system.md)
- [Challenges Endpoints](/api/challenges.md)
- [Challenges Board](/guides/challenges-board.md)
- [Notifications](/guides/notifications.md)
+198
View File
@@ -0,0 +1,198 @@
---
type: guide
title: Challenges Board
description: How a signed-in player navigates the `/challenges` page, opens a challenge modal, submits a flag, and watches live solve updates.
tags: [guide, challenges, board, flag, score, sse, tester]
timestamp: 2026-07-23T00:10:00Z
---
# When this view is available
`/challenges` is rendered by `ChallengesPage`
(`frontend/src/app/features/challenges/challenges.page.ts`) inside the
authenticated shell. The page is gated by:
| Gate | Where |
|---------------------------------------|--------------------------------------------------|
| Signed in (access token + refresh) | `authGuard` in `app.routes.ts`. |
| Event window state | `ChallengesPage.isRunning()` derived from `EventStatusStore`. The board is only shown when `state === 'running'`; otherwise the page renders a gate panel (see below). |
The first time the page mounts it triggers `ChallengesStore.load()`
which calls `GET /api/v1/challenges/board`. It also calls
`GET /api/v1/challenges/status` once to populate the event state
synchronously, then opens the authenticated SSE stream on
`/api/v1/events` for live `solve` frames.
# How to access (tester steps)
1. Initialize the instance and sign in (`/login`).
2. Confirm the SPA routes to `/` and redirects to `/challenges`.
3. Configure an event window (`/admin/general`) whose
`eventStartUtc` is in the past and `eventEndUtc` is in the future
so the event state is `running`.
# Expected behavior
## Board grid (state = `running`)
The page renders an `<h2>Challenges</h2>` followed by a horizontal
flex container (`[data-testid="challenges-grid"]`) of one column per
category.
| Element | Selector | Expected |
|----------------------------------------|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| Category column | `[data-testid="category-column-CR"]` (or any abbreviation) | Header shows the category `icon`, uppercase `abbreviation`, and full `name`; below it is a vertical list of cards. |
| Category column with no challenges | Same | The header renders but the body shows `No challenges yet.` |
| Challenge card | `[data-testid="challenge-card-<uuid>"]` | Click anywhere on the card to open the modal. |
| Difficulty pill | `.diff.diff-LOW` / `.diff-MEDIUM` / `.diff-HIGH` | Green / yellow / red pill matching the challenge difficulty. |
| Live points | `[data-testid="points-<uuid>"]` | `livePoints` from the board payload; updates live as `solve` frames arrive. |
| Solve count | text `N solve` / `N solves` | Number of distinct players who have solved this challenge. |
| Solved-by-me check | `.check` (the `✓` glyph) | Appears on the card once the player has solved it; the card border also flips to the success color. |
Columns are sorted alphabetically by `abbreviation`; cards within a
column are sorted `LOW → MEDIUM → HIGH` then by name (lowercased).
## Gate panel (state != `running`)
When the event is in any other state, the grid is hidden and the page
renders `[data-testid="challenges-gate"]`:
| Event state | Headline | Countdown text (`[data-testid="challenges-countdown"]`) | Helper label |
|---------------|---------------------------|--------------------------------------------------------|-----------------------------------------------------|
| `countdown` | `Event starts in` | `DD:HH:mm` until `eventStartUtc` | `The board will be available once the event is running.` |
| `running` | _(grid shown instead)_ | — | — |
| `stopped` | `Event ended` | `Event ended` | `The board is closed.` |
| `unconfigured`| `Awaiting configuration` | empty | _(none)_ |
When `secondsToStart` or `secondsToEnd` reach zero, the
`ChallengesPage` triggers a full `window.location.reload()` via
`EventStatusStore.reloadAtCountdownZero(...)` so the SPA re-evaluates
the board visibility from the server snapshot.
## Challenge modal
Clicking a card opens `ChallengeModalComponent`
(`[data-testid="challenge-modal-<uuid>"]`). The modal has a dark
backdrop; clicking the backdrop or pressing `Esc` closes it.
| Section | Selector / element | Expected |
|------------------------|----------------------------------------------------------|------------------------------------------------------------------------------------------|
| Header | `<h3>` + pills | Challenge name, category abbreviation pill, difficulty pill (`LOW`/`MEDIUM`/`HIGH`), live points pill, and `Close` button. |
| Solved banner | `[data-testid="modal-solved-banner"]` | Renders `You solved this challenge.` when `card.solvedByMe` is true. |
| Awarded banner | `[data-testid="modal-awarded-banner"]` | `Awarded X pts (base Y + rank bonus Z)` after a successful submit. |
| Notice banner | `.notice` | When the event is not running, shows `Submissions are only accepted while the event is running.` and disables the input. |
| Description | `[innerHTML]` from `MarkdownService.render(...)` | Markdown body, links, code, etc. rendered from `card.descriptionMd`. |
| Flag input | `[data-testid="flag-input"]` | Free text input; disabled while submitting or while the event is not running. |
| Solve button | `[data-testid="solve-button"]` | Disabled while submitting. Submits via `ChallengesStore.submit(...)`. |
| Inline error | `[data-testid="modal-error"]` | Renders the friendly message from `messageForSolveError(parseSolveError(...))`. |
| Solvers list | `[data-testid="modal-solvers"]` | Header `Solvers (N)` then a row per solver (`#position`, name, local-time, awarded points). Top three ranks are coloured gold / silver / bronze; subsequent ranks use the success color. |
| Empty solvers state | `<em>No solvers yet.</em>` | Shown when the challenge has no solves yet. |
## Submitting a flag
1. Type the flag into `[data-testid="flag-input"]`.
2. Click `[data-testid="solve-button"]` (or press Enter inside the
form).
3. The modal calls `ChallengesStore.submit(challengeId, flag)` which
POSTs `{ flag }` to `/api/v1/challenges/:id/solves`.
4. On `200 solved`:
* The flag input is cleared.
* The `Awarded X pts (base Y + rank bonus Z)` banner appears.
* The solvers list re-renders with the player now at position `N`.
* The corresponding card on the board gets the success border and
`✓`.
5. On `200 already_solved` (idempotent re-submit):
* Same banner + solvers list, but the inline error area shows
`You already solved this challenge.`
6. On `400 FLAG_INCORRECT`:
* Inline error: `Incorrect flag. Try again.`
* Flag input is re-enabled so the player can retry.
7. On `409 EVENT_NOT_RUNNING`:
* Inline error: `Submissions are only accepted while the event is running.`
* The notice banner also reflects the event state.
8. On any other error (network, 5xx):
* The `errorNotificationInterceptor` also pushes a top-level
notification in the SPA's notification store (see
[Notifications](/guides/notifications.md)).
## Live solve updates
While the modal is open, the page subscribes to live `solve` frames
for the selected challenge via
`ChallengesStore.registerSolveListener(id, payload => modal.applyLiveSolve(payload))`.
Each `solve` SSE frame:
* Updates `card.livePoints` and `card.solveCount` on the modal.
* Merges the new solver into the existing `solvers` array (sorted by
`solvedAtUtc` ASC) via `mergeSolveEventIntoSolvers`.
* Updates the matching board card (point value + solve count + the
`✓` if the player is the solver).
When the user closes the modal, the listener is unregistered; on
destruction of the page the SSE source is closed via
`ChallengesStore.stop()`.
# Tester flows
## Running event board
1. Configure the event window as running; confirm the board renders
the columns/cards described above.
2. Open and close a card — the modal should mount and unmount cleanly
with no console errors; the SSE source should remain connected.
3. Solve a flag and confirm the awarded banner, the new solver row,
the updated board card (`✓` and new live points), and the new
`livePoints` on the same card across all open clients.
## Countdown / stopped gate
1. Set `eventStartUtc` in the future; confirm the gate panel shows
`Event starts in` and the `DD:HH:mm` countdown.
2. Wait for the countdown to hit zero; the SPA reloads and now shows
the board.
3. Set `eventEndUtc` in the past; confirm the gate shows
`Event ended`.
4. Delete one of the event-window settings; confirm `Awaiting
configuration` and an empty countdown.
## Submission errors
1. Submit an empty string — the modal's local guard skips the call.
2. Submit a wrong flag — expect `Incorrect flag. Try again.`.
3. Submit a correct flag twice — second call returns `already_solved`
and shows both the awarded banner and the
`You already solved this challenge.` inline error.
4. Try to submit while the event is `stopped` — expect
`Submissions are only accepted while the event is running.` and a
matching notice banner.
# Architecture map
| File | Responsibility |
|---------------------------------------------------------|------------------------------------------------------------------------------------------------|
| `frontend/src/app/features/challenges/challenges.page.ts` | Smart page; owns the modal lifecycle, gate logic, SSE wiring, and the countdown-zero reload. |
| `frontend/src/app/features/challenges/challenges.store.ts` | Signal store: board, event state, solve listeners, per-card mutation on submit + SSE solve. |
| `frontend/src/app/features/challenges/challenges.service.ts` | HTTP service: `getBoard`, `getDetail`, `getEventState`, `submit` (returns `ApiErrorEnvelope`). |
| `frontend/src/app/features/challenges/challenges.pure.ts` | Pure types (`BoardCard`, `SolverRow`, `SolveEventPayload`, etc.) + helpers (`sortColumnsByAbbrev`, `formatDdHhMm`, `parseSolveEvent`, `messageForSolveError`). |
| `frontend/src/app/features/challenges/category-column.component.ts` | Renders one category column (header + cards). |
| `frontend/src/app/features/challenges/challenge-card.component.ts` | Renders one challenge card on the board. |
| `frontend/src/app/features/challenges/challenge-modal.component.ts` | Renders the challenge detail modal, flag form, solvers list, and live-solve updates. |
| `frontend/src/app/core/services/event-status.store.ts` | Anchor-based clock + 4-state event store; reused here for the gate logic. |
| `frontend/src/app/core/services/authenticated-event-source.service.ts` | Fetch-based SSE transport used to open `/api/v1/events`. |
| `frontend/src/app/core/services/notification.service.ts` | Signal-backed notification store used by `errorNotificationInterceptor`. |
| `tests/frontend/challenges.pure.spec.ts` | Pure helpers (sorting, parsers, friendly error mapping). |
| `tests/frontend/challenges.store.spec.ts` | Signal store: board mutation, live solve merge, listeners, stop(). |
| `tests/frontend/notification-interceptor.spec.ts` | Interceptor suppression rules and friendly message mapping. |
| `tests/backend/challenges-board.spec.ts` | Board query shape, ordering, and `?include=solvers` flag. |
| `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. |
# See also
- [Challenges Endpoints](/api/challenges.md)
- [Challenge Tables](/database/challenges.md)
- [System Endpoints](/api/system.md) (legacy `/events/status`)
- [Scoreboard Stream](/guides/scoreboard-stream.md)
- [Authenticated Shell](/guides/authenticated-shell.md) (SSE transport)
- [Notifications](/guides/notifications.md) (error interceptor + service)
+85
View File
@@ -0,0 +1,85 @@
---
type: guide
title: Notifications
description: SPA-wide notification store + the global HTTP error interceptor that translates backend error codes and statuses into friendly user-facing messages.
tags: [guide, notifications, error, interceptor, toast, ux]
timestamp: 2026-07-23T00:10:00Z
---
# Components
| Symbol | Path | Responsibility |
|-------------------------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
| `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 catches `HttpErrorResponse`, looks up the error code, and pushes a friendly message into `NotificationService`. |
| `NotificationService.messages` | signal (consumed by future toast UI) | Currently the messages signal is appended in-memory only; the list itself doubles as the integration point for a future toast/banner component. Each push also mirrors the message to the browser console (`console.error` for `error`, `console.info` for `info`). |
# Wiring
`frontend/src/main.ts` registers the interceptor **after** `authInterceptor` so
the auth header is already attached when the error interceptor sees
the request:
```ts
provideHttpClient(
withInterceptors([csrfInterceptor, authInterceptor, errorNotificationInterceptor]),
)
```
The interceptor:
1. Catches every `HttpErrorResponse` thrown from any HTTP call.
2. Maps the response to a friendly message using `friendlyMessage(status, err.error)`.
3. Suppresses notifications for endpoints that have their own
user-facing error presentation (see *Suppression rules* below).
4. Pushes the message via `NotificationService.error(...)`.
5. Re-throws the original error so the caller still receives the
`ApiErrorEnvelope` (e.g. `ChallengesApiService.submit` reads
`envelope.code` to render the modal's inline error).
# Friendly message mapping
`friendlyMessage(status, body)` resolves in this order:
| Source | Message |
|-----------------------------------------------------|------------------------------------------------------------------------------|
| `status === 0` (network / CORS / aborted) | `Network error. Please try again.` |
| `body.code` matches `FRIENDLY_MESSAGES` | The mapped friendly text (see table below). |
| `status === 401` or `403` | `Your session is no longer valid.` (unless suppressed). |
| `status === 404` | `Not found.` |
| `status >= 500` | `Server error. Please try again later.` |
| `body.message` (non-empty) | Falls back to that message. |
| otherwise | `Request failed (<status>).` |
`FRIENDLY_MESSAGES` lookup table:
| Error code | Friendly text |
|-----------------------|------------------------------------------------------------|
| `EVENT_NOT_RUNNING` | `Submissions are only accepted while the event is running.` |
| `FLAG_INCORRECT` | `Incorrect flag.` |
| `UNAUTHORIZED` | `Your session is no longer valid.` |
| `FORBIDDEN` | `You are not allowed to perform this action.` |
| `NOT_FOUND` | `Not found.` |
| `VALIDATION_FAILED` | `Invalid input.` |
# Suppression rules
Some errors are expected and have their own UI. To avoid double-toasting:
| URL pattern | Status | Why |
|---------------------------------------|--------|---------------------------------------------------------------------------------------------------|
| `/api/v1/auth/login` `/csrf` `/register` | `401` | A failed login or first-fetch of the CSRF cookie produces a `401`; the form already renders the inline error. |
| `/api/v1/challenges/status` | `401` | The challenges page uses the snapshot as a bootstrap hint only — the SSE stream (`/api/v1/events`) is the source of truth and will deliver the next frame. |
In both cases the interceptor still re-throws the error so callers can
react locally; only the notification push is skipped.
# See also
- [Challenges Board](/guides/challenges-board.md) (uses
`errorNotificationInterceptor` for non-flag submission failures)
- [Authenticated Shell](/guides/authenticated-shell.md) (cross-tab
invalidation propagates from a 401 SSE response via
`NotificationService` is **not** triggered — the shell uses its own
peer-invalidation channel)
- [REST API Overview](/api/rest-overview.md) (error envelope)
+10 -1
View File
@@ -11,7 +11,7 @@ scoreboard, an event window with a public countdown, theming, and admin
controls. controls.
The docs below are organized by purpose so agents can pull just the slice The docs below are organized by purpose so agents can pull just the slice
they need. Last regenerated 2026-07-22T22:17:11Z. they need. Last regenerated 2026-07-23T00:10:00Z.
# Architecture # Architecture
@@ -50,6 +50,9 @@ they need. Last regenerated 2026-07-22T22:17:11Z.
* [Admin Endpoints](/api/admin.md) - Admin-only endpoints covering user * [Admin Endpoints](/api/admin.md) - Admin-only endpoints covering user
management, general settings (`/api/v1/admin/general/*`), and management, general settings (`/api/v1/admin/general/*`), and
categories (`/api/v1/admin/categories/*`). categories (`/api/v1/admin/categories/*`).
* [Challenges Endpoints](/api/challenges.md) - Authenticated challenge board,
detail, flag submission, event-state snapshot, and the combined
`/api/v1/events` SSE stream.
* [System Endpoints](/api/system.md) - Bootstrap, event status, public SSE * [System Endpoints](/api/system.md) - Bootstrap, event status, public SSE
streams, public event-window settings, and the authenticated streams, public event-window settings, and the authenticated
`/events/status` SSE stream. `/events/status` SSE stream.
@@ -80,6 +83,9 @@ they need. Last regenerated 2026-07-22T22:17:11Z.
* [Authenticated Shell](/guides/authenticated-shell.md) - The header, LED * [Authenticated Shell](/guides/authenticated-shell.md) - The header, LED
+ countdown, quick-jump tabs, user menu, and SSE lifecycle that frame + countdown, quick-jump tabs, user menu, and SSE lifecycle that frame
every signed-in page. every signed-in page.
* [Challenges Board](/guides/challenges-board.md) - How a signed-in
player navigates `/challenges`, opens the modal, submits a flag, and
watches live solve updates.
* [Change Password](/guides/change-password.md) - How a signed-in user * [Change Password](/guides/change-password.md) - How a signed-in user
changes their own password, including every error branch and the changes their own password, including every error branch and the
refresh-token revocation behavior. refresh-token revocation behavior.
@@ -103,3 +109,6 @@ they need. Last regenerated 2026-07-22T22:17:11Z.
truth for the shell status dot's color in each `EventState`. truth for the shell status dot's color in each `EventState`.
* [Scoreboard Stream](/guides/scoreboard-stream.md) - How live solves are * [Scoreboard Stream](/guides/scoreboard-stream.md) - How live solves are
broadcast to clients. broadcast to clients.
* [Notifications](/guides/notifications.md) - The root notification
store + global HTTP error interceptor that translate backend error
codes and statuses into friendly user-facing messages.