--- 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-23T16:10:00Z --- # When this view is available `/challenges` is rendered by `ChallengesPage` (`frontend/src/app/features/challenges/challenges.page.ts`) inside the authenticated shell. On a fresh database the `SeedSampleChallenges1700000000600` migration loads a representative sample set of challenges so the board has cards the moment an admin configures the event window; an empty board is only possible when the admin deleted every challenge. 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`. The mount order is significant: `ChallengesPage.ngOnInit()` calls `store.setMyUserId(user?.id ?? null)` **before** `store.load()`. If the singleton's cache belongs to an unknown or different user, `setMyUserId` calls `reset()` first so the new board is never blended with a previous user's `solvedByMe`. See [Per-User `solvedByMe` Across the Session Boundary](/guides/challenges-per-user-state.md). # 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 `

Challenges

` 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. The icon's `src` is `/uploads/icons/.png` and is served by the backend's static middleware; on a freshly bootstrapped instance these PNGs are generated at startup by [`seedSystemCategoryIcons`](/architecture/system-category-icons.md) so the column headers never show broken images. | | Category column with no challenges | Same | The header renders but the body shows `No challenges yet.` | | Challenge card | `[data-testid="challenge-card-"]` | Rendered as a real `