Files
HIPCTF2/docs/guides/event-window.md
T

99 lines
4.9 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-21T22:19:08Z
---
# 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()` via `EventStatusStore.start(() => new EventSource('/api/v1/events/status', { withCredentials: true }))`
(`frontend/src/app/features/home/home.component.ts`,
`frontend/src/app/core/services/event-status.store.ts`). 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`, zero-padded).
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).
`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.
# 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)