docs: update documentation to OKF v0.1 format
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user