131 lines
6.6 KiB
Markdown
131 lines
6.6 KiB
Markdown
---
|
|
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)
|