AI Implementation feature(905): Challenges Page and Challenge Solve Modal 1.01 (#47)
This commit was merged in pull request #47.
This commit is contained in:
@@ -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, 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
|
||||
timestamp: 2026-07-23T01:19:00Z
|
||||
---
|
||||
|
||||
# Backend
|
||||
@@ -58,7 +58,7 @@ timestamp: 2026-07-23T00:10:00Z
|
||||
| `frontend/src/app/core/services/authenticated-event-source.service.ts` | Fetch-based authenticated SSE transport with frame parsing, abort support, and a separate `'unauthorized'` event for `401`/`403`. |
|
||||
| `frontend/src/app/core/services/bootstrap-event.service.ts` | Root-provided public SSE listener that subscribes to `/api/v1/event/stream`, filters `topic === 'general'` frames, and triggers `BootstrapService.refresh()` so the landing modal stays in sync with admin general-settings updates. |
|
||||
| `frontend/src/app/core/services/bootstrap-event.pure.ts` | Pure helpers for the public bootstrap SSE listener: `isBootstrapGeneralFrame` predicate, SSE line parser, and the `makeBootstrapEventSource` factory (fetch + `ReadableStream` opener with frame buffering and idempotent `close()`). |
|
||||
| `frontend/src/app/core/services/event-status.store.ts` | Event state signal store, one-second countdown timer, and the optional `onUnauthorized` callback wiring. |
|
||||
| `frontend/src/app/core/services/event-status.store.ts` | Event state signal store, one-second countdown timer, and the optional `onUnauthorized` callback wiring; exposes `start()`/`stop()`/`closeTransport()` for transport lifecycle and `subscribeReloadAtCountdownZero(handler)` (with `reloadAtCountdownZero(handler)` kept as a back-compat wrapper) so the reload handler is owned by its subscriber rather than the transport. |
|
||||
| `frontend/src/app/core/services/event-status.pure.ts` | Event payload types, transport interface (including `'unauthorized'` listener), pure countdown helpers, and the `LED_COLOR_BY_STATE` map used by the shell LED. |
|
||||
| `frontend/src/app/features/home/home.component.ts` | Authenticated shell container; starts user and event state services, subscribes to peer invalidation, and navigates to `/login` when the session is invalidated elsewhere. |
|
||||
| `frontend/src/app/features/shell/header/shell-header.component.ts` | Shell title, status LED (size/shape only — color comes from the pure map via `[style.background-color]`), countdown, and user menu. |
|
||||
@@ -72,7 +72,7 @@ timestamp: 2026-07-23T00:10:00Z
|
||||
| `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.page.ts` | `/challenges` smart page: gate logic (countdown/running/stopped/unconfigured), modal lifecycle, SSE wiring on `/api/v1/events`, and the page-owned countdown-zero reload (handler registered in the constructor with the disposer wired into `DestroyRef`). |
|
||||
| `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`). |
|
||||
|
||||
@@ -66,8 +66,13 @@ renders `[data-testid="challenges-gate"]`:
|
||||
|
||||
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.
|
||||
`EventStatusStore.subscribeReloadAtCountdownZero(...)` so the SPA
|
||||
re-evaluates the board visibility from the server snapshot. The
|
||||
subscription is page-owned: `ChallengesPage` registers the handler in
|
||||
its constructor and wires the returned disposer into its `DestroyRef`
|
||||
so the reload listener is cleared as soon as the page is destroyed,
|
||||
independent of the store's `start()`/`stop()` transport lifecycle (see
|
||||
[Event Window — Auto-reload at the transition boundary](/guides/event-window.md#auto-reload-at-the-transition-boundary)).
|
||||
|
||||
## Challenge modal
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ type: guide
|
||||
title: Event Window
|
||||
description: How the event window state machine is computed and surfaced via REST + SSE.
|
||||
tags: [guide, event, countdown, sse, state-machine]
|
||||
timestamp: 2026-07-22T09:03:53Z
|
||||
timestamp: 2026-07-23T01:19:00Z
|
||||
---
|
||||
|
||||
# Configuration
|
||||
@@ -88,6 +88,24 @@ itself) close the SSE source and clear the tick interval, so leaving
|
||||
the shell (e.g. by logging out and landing on `/login`) tears the
|
||||
stream down cleanly.
|
||||
|
||||
# Auto-reload at the transition boundary
|
||||
|
||||
`ChallengesPage` registers a one-shot handler with
|
||||
`EventStatusStore.subscribeReloadAtCountdownZero(() => window.location.reload())`
|
||||
in its constructor; the returned disposer is wired into its
|
||||
`DestroyRef` so the subscription is cleared when the page is destroyed.
|
||||
The subscription is **page-owned** and independent of the transport
|
||||
lifecycle (`start()`/`stop()` only manage the SSE source + tick +
|
||||
watcher; they never clear the reload handler). The store owns a 1 s
|
||||
watcher (`checkZero`) that fires the handler exactly once when the
|
||||
local-clock countdown reaches zero (`countdown → running`) or the
|
||||
running countdown reaches zero (`running → stopped`). The latch is
|
||||
re-armed on every state transition (countdown→running and
|
||||
running→stopped), and a strict-equal follow-up SSE frame is collapsed
|
||||
so a duplicate zero-countdown payload cannot disarm the latch. After
|
||||
`stop()` (end-of-life teardown) the store is fully reset; a fresh
|
||||
handler subscription re-installs the watcher.
|
||||
|
||||
# Push path
|
||||
|
||||
1. The admin cron / settings update writes to `setting.eventStartUtc` /
|
||||
|
||||
Reference in New Issue
Block a user