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

4.9 KiB

type, title, description, tags, timestamp
type title description tags timestamp
guide Event Window How the event window state machine is computed and surfaced via REST + SSE.
guide
event
countdown
sse
state-machine
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 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