AI Implementation feature(909): Challenges Page and Challenge Solve Modal 1.05 (#51)

This commit was merged in pull request #51.
This commit is contained in:
2026-07-23 04:06:49 +00:00
parent 5a6e832cca
commit 49d0930780
8 changed files with 116 additions and 131 deletions
+3 -2
View File
@@ -3,7 +3,7 @@ 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, the challenges full-replace import flow, the authenticated player-facing challenges board, and the per-user `solvedByMe` reset on the session boundary.
tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import, board, notifications, per-user, session]
timestamp: 2026-07-23T03:45:00Z
timestamp: 2026-07-23T04:05:00Z
---
# Backend
@@ -77,7 +77,7 @@ timestamp: 2026-07-23T03:45:00Z
| `frontend/src/app/features/challenges/challenges.service.ts` | HTTP service for `/api/v1/challenges/{board,status,:id,:id/solves}` returning typed `ApiErrorEnvelope`s. `getDetail` always requests `?include=solvers` so the modal can render the solvers list in one round-trip; `getBoard` only attaches `include=solvers` when the caller opts in. |
| `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-card.component.ts` | Renders a single challenge card on the board as a semantic `<button>` (difficulty, live points, solve count, `✓` when solved by the player). Exposes `data-testid="challenge-card-<id>"`, `data-solved`, `aria-pressed`, an accessible name (`Challenge <name>, solved|not solved`), and an inner `data-testid="challenge-check-<id>"` for the solved glyph. |
| `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`). |
@@ -86,6 +86,7 @@ timestamp: 2026-07-23T03:45:00Z
| `tests/frontend/challenges.store.spec.ts` | Signal store: board mutation, live solve merge, listeners, `stop()`. The "marks solvedByMe" and "does not double-count" tests now call `setMyUserId('me-1')` *before* `load()` to exercise the realistic page-mount sequence. |
| `tests/frontend/challenges.store.reset.spec.ts` | Per-user `solvedByMe` regression suite: `reset()` clears board/detail/myUserId/SSE; switching user id flushes the previous board; **orphan board** (cached with no recorded user id) is cleared when PlayerB's id is set; a non-mine solve never flips `solvedByMe` from `false` to `true`; `applySubmitResponse` takes the server value verbatim. |
| `tests/frontend/notification-interceptor.spec.ts` | Interceptor suppression rules and friendly message mapping. |
| `tests/frontend/challenge-card-accessibility.spec.ts` | Challenge-card accessibility contract: keyboard activation, `aria-pressed` toggling, accessible-name composition, and the `data-solved` / `challenge-check-*` test selectors. |
| `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. |
+5 -4
View File
@@ -3,7 +3,7 @@ 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-23T03:09:00Z
timestamp: 2026-07-23T04:05:00Z
---
# When this view is available
@@ -57,11 +57,11 @@ category.
|----------------------------------------|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| 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. |
| Challenge card | `[data-testid="challenge-card-<uuid>"]` | Rendered as a real `<button type="button">`. Keyboard activatable (Tab + Enter/Space), exposes `aria-pressed` and an accessible name `Challenge <name>, solved` / `Challenge <name>, not solved`, and `data-solved="true"\|"false"`. Click anywhere (or press Enter/Space) 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. |
| Solved-by-me check | `[data-testid="challenge-check-<uuid>"]` (the `` glyph, `aria-hidden="true"`) | Appears on the card once the player has solved it; the card border also flips to the success color. The button's `aria-pressed` flips to `true` and the accessible name gains the trailing `, solved`. |
Columns are sorted alphabetically by `abbreviation`; cards within a
column are sorted `LOW → MEDIUM → HIGH` then by name (lowercased).
@@ -196,7 +196,8 @@ destruction of the page the SSE source is closed via
| `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-card.component.ts` | Renders one challenge card on the board as a semantic `<button>` with `aria-pressed`, `aria-label`, `data-testid="challenge-card-<id>"`, `data-solved`, and an inner `data-testid="challenge-check-<id>"` for the solved glyph. |
| `tests/frontend/challenge-card-accessibility.spec.ts` | Keyboard activation (Enter/Space), `aria-pressed` toggling, accessible-name composition, and the `data-solved`/`challenge-check-*` selectors. |
| `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`. |
+2 -2
View File
@@ -3,7 +3,7 @@ type: guide
title: Per-User `solvedByMe` Across the Session Boundary
description: How the `ChallengesStore` signal cache guarantees that PlayerA's solved-by-me flags never leak into PlayerB's view across logout/login, including SSE-unauthorized and peer-logout invalidation.
tags: [guide, challenges, solvedByMe, session, logout, store, tester]
timestamp: 2026-07-23T03:45:00Z
timestamp: 2026-07-23T04:05:00Z
---
# What this feature does
@@ -117,7 +117,7 @@ are the regression targets:
| Element | Selector | After user-switch |
|---|---|---|
| Solved check | `.check` inside `[data-testid="challenge-card-<uuid>"]` | **Absent** for PlayerB if PlayerA solved it. |
| Solved check | `[data-testid="challenge-check-<uuid>"]` (`.check` glyph, `aria-hidden="true"`) inside `[data-testid="challenge-card-<uuid>"]` | **Absent** for PlayerB if PlayerA solved it. The button's `aria-pressed` is also `false` and `data-solved="false"`. |
| Solved border | `[data-testid="challenge-card-<uuid>"]` (border color) | Default border color, not success. |
| Modal solved banner | `[data-testid="modal-solved-banner"]` | **Absent** for PlayerB. |
| Awarded banner | `[data-testid="modal-awarded-banner"]` | **Absent** (no award yet for PlayerB). |
+1 -1
View File
@@ -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-23T03:45:00Z.
they need. Last regenerated 2026-07-23T04:05:00Z.
# Architecture