AI Implementation feature(863): Challenges Page and Challenge Solve Modal #45
@@ -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)
|
||||
@@ -3,7 +3,7 @@ type: api
|
||||
title: REST API Overview
|
||||
description: Base URL, versioning, auth, CSRF, and the standard error envelope.
|
||||
tags: [api, rest, overview, csrf]
|
||||
timestamp: 2026-07-21T22:19:08Z
|
||||
timestamp: 2026-07-23T00:10:00Z
|
||||
---
|
||||
|
||||
# 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`,
|
||||
`INVALID_CREDENTIALS`, `TOKEN_REVOKED`, `THEME_INVALID`,
|
||||
`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
|
||||
|
||||
@@ -86,4 +97,5 @@ endpoint). Full list lives in `backend/src/common/errors/error-codes.ts`:
|
||||
- [Admin Endpoints](/api/admin.md)
|
||||
- [Setup Endpoint](/api/setup.md)
|
||||
- [System Endpoints](/api/system.md)
|
||||
- [Uploads Endpoints](/api/uploads.md)
|
||||
- [Uploads Endpoints](/api/uploads.md)
|
||||
- [Challenges Endpoints](/api/challenges.md)
|
||||
@@ -3,7 +3,7 @@ type: architecture
|
||||
title: Backend Module Map
|
||||
description: NestJS modules, controllers, services, and how they are wired together.
|
||||
tags: [architecture, backend, nestjs, modules]
|
||||
timestamp: 2026-07-21T22:19:08Z
|
||||
timestamp: 2026-07-23T00:10:00Z
|
||||
---
|
||||
|
||||
# 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`). |
|
||||
| `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). |
|
||||
| `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. |
|
||||
|
||||
# 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` |
|
||||
| `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` |
|
||||
| `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
|
||||
|
||||
@@ -63,6 +66,7 @@ also registers two global providers:
|
||||
| `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. |
|
||||
| `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
|
||||
|
||||
|
||||
@@ -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]
|
||||
timestamp: 2026-07-22T10:32:00Z
|
||||
tags: [architecture, frontend, angular, shell, sse, challenges, notifications]
|
||||
timestamp: 2026-07-23T00:10:00Z
|
||||
---
|
||||
|
||||
# Routes
|
||||
@@ -15,7 +15,7 @@ Routes live in `frontend/src/app/app.routes.ts`:
|
||||
| `/bootstrap` | `SetupCreateAdminComponent` | — | First-admin creation flow. |
|
||||
| `/login` | `LandingComponent` | `landingGuard` | Public landing and authentication modal. |
|
||||
| `/` | `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. |
|
||||
| `/blog` | `BlogPage` | inherited | Placeholder blog page. |
|
||||
| `/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`. |
|
||||
| `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. |
|
||||
| `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
|
||||
|
||||
@@ -64,8 +70,11 @@ on destruction.
|
||||
|---|---|
|
||||
| `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/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/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. |
|
||||
|
||||
# Event-status pure helpers
|
||||
@@ -111,4 +120,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)
|
||||
- [Notifications](/guides/notifications.md)
|
||||
- [System Overview](/architecture/overview.md)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
type: architecture
|
||||
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.
|
||||
tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import]
|
||||
timestamp: 2026-07-22T22:17:11Z
|
||||
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, board, notifications]
|
||||
timestamp: 2026-07-23T00:10:00Z
|
||||
---
|
||||
|
||||
# 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-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. |
|
||||
| `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
|
||||
|
||||
@@ -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/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. |
|
||||
| `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
|
||||
|
||||
- [Frontend Structure](/architecture/frontend-structure.md)
|
||||
- [Authenticated Shell](/guides/authenticated-shell.md)
|
||||
- [System Endpoints](/api/system.md)
|
||||
- [Challenges Endpoints](/api/challenges.md)
|
||||
- [Challenges Board](/guides/challenges-board.md)
|
||||
- [Notifications](/guides/notifications.md)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -11,7 +11,7 @@ scoreboard, an event window with a public countdown, theming, and admin
|
||||
controls.
|
||||
|
||||
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
|
||||
|
||||
@@ -50,6 +50,9 @@ they need. Last regenerated 2026-07-22T22:17:11Z.
|
||||
* [Admin Endpoints](/api/admin.md) - Admin-only endpoints covering user
|
||||
management, general settings (`/api/v1/admin/general/*`), and
|
||||
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
|
||||
streams, public event-window settings, and the authenticated
|
||||
`/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
|
||||
+ countdown, quick-jump tabs, user menu, and SSE lifecycle that frame
|
||||
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
|
||||
changes their own password, including every error branch and the
|
||||
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`.
|
||||
* [Scoreboard Stream](/guides/scoreboard-stream.md) - How live solves are
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user