--- 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-23T03:09:00Z --- # Configuration Two `setting` rows control the window: | Setting key | Default (seeded) | Description | |-------------------|------------------|--------------------------------------------| | `eventStartUtc` | `now + 60s` | ISO 8601 instant the event becomes live. | | `eventEndUtc` | `now + 7d` | ISO 8601 instant the event ends. | Both are seeded by `SeedSystemData1700000000100` and editable through `SettingsService`. # State machine `EventStatusService.getState(now)` (`backend/src/common/services/event-status.service.ts`) returns one of four branches: | Condition | `state` | `secondsToStart` | `secondsToEnd` | |----------------------------------------|------------------|--------------------------|--------------------------| | `eventStartUtc` / `eventEndUtc` missing or unparseable | `unconfigured` | `null` | `null` | | `now < eventStartUtc` | `countdown` | `floor((start - now)/s)` | `null` | | `start <= now <= end` | `running` | `null` | `floor((end - now)/s)` | | `now > eventEndUtc` | `stopped` | `null` | `null` | The legacy `EventStatusService.getStatus(now)` shape (`{status: 'Stopped'|'Running', countdownMs, startUtc, endUtc, serverNowUtc}`) is preserved for backward compatibility. `Stopped` is overloaded to mean "still counting down" (`countdownMs = eventStartUtc - now`) when `state === 'countdown'`, and "ended" (`countdownMs = 0`) when `state === 'stopped'` / `unconfigured`. # Endpoints | Endpoint | Auth | Shape | |--------------------------------------------|-------------|-------------------------------------------| | `GET /api/v1/event/status` | `@Public()` | Legacy `{status, countdownMs, startUtc, endUtc, serverNowUtc}`. | | `GET /api/v1/event/stream` | `@Public()` | SSE that emits the legacy payload on a 1-second tick + on hub pushes. | | `GET /api/v1/settings/event` | `@Public()` | Raw `{eventStartUtc, eventEndUtc}` (null when unset). | | `GET /api/v1/events/status` | Authenticated (JWT) | SSE that emits the full `EventStatePayload` on connect + on a 60-second tick + on hub pushes; consecutive identical payloads suppressed via `distinctUntilChanged(JSON.stringify)`. | See [System Endpoints](/api/system.md) for the full request/response shape. # Frontend wiring The authenticated shell subscribes to `/api/v1/events/status` from `HomeComponent.ngOnInit()` through `AuthenticatedEventSourceService.open()` and `EventStatusStore.start(...)` (`frontend/src/app/features/home/home.component.ts`, `frontend/src/app/core/services/authenticated-event-source.service.ts`, `frontend/src/app/core/services/event-status.store.ts`). The transport uses a fetch stream rather than the browser `EventSource` API because it must attach the JWT `Authorization` header. It sends `Accept: text/event-stream`, includes credentials, parses streamed SSE records, buffers early frames, and aborts the request on `close()`. The store: 1. Calls `applyServerStatus(payload)` on every `message` frame (parses JSON, sets `state`, `serverNowUtc`, `eventStartUtc`, `eventEndUtc`, and stores the clock-skew anchor `Date.now() - serverNow`). 2. Runs a local 1-second tick so the `secondsToStart` / `secondsToEnd` computed signals update between SSE pushes. 3. Exposes `countdownText` (computed): `""` for `unconfigured`, `"Event ended"` for `stopped`, and `formatDdHhMm(seconds)` for `countdown` / `running`. The format helper lives in the pure module `event-status.pure.ts` (`DD:HH:mm:ss`, zero-padded). The seconds digit is driven by the local 1-second tick (and the SSE re-push every 60 seconds) so the final minute of the countdown visibly elapses second-by-second on the shell header. The header (`ShellHeaderComponent`) renders an LED with the `led-{state}` class so the colour reflects the current state (the exact theme colours are wired in `styles.css` against the existing `--color-success` / `--color-warning` / `--color-danger` tokens; `unconfigured` uses `--color-secondary`). See the [Event LED colors](/guides/authenticated-shell.md#event-led-colors) table in the Authenticated Shell guide for the full mapping. `HomeComponent.ngOnDestroy()` (and the `DestroyRef` hook in the store 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` / `setting.eventEndUtc`. 2. `SseHubService.publish('event', payload)` fans the new payload out to every subscriber of `/api/v1/event/stream` and (via the `flat()` mapping) `/api/v1/events/status`. 3. Each connected client receives the new state immediately; otherwise it picks up the next 60-second tick. The hub is in-process; multi-replica deployments need a shared pub/sub to fan out across pods (not in scope for this repo). # See also - [Auth and Settings Tables](/database/auth-settings.md) - [System Endpoints](/api/system.md) - [Scoreboard Stream](/guides/scoreboard-stream.md) - [Authenticated Shell](/guides/authenticated-shell.md)