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. |
|
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:
- Calls
applyServerStatus(payload)on everymessageframe (parses JSON, setsstate,serverNowUtc,eventStartUtc,eventEndUtc, and stores the clock-skew anchorDate.now() - serverNow). - Runs a local 1-second tick so the
secondsToStart/secondsToEndcomputed signals update between SSE pushes. - Exposes
countdownText(computed):""forunconfigured,"Event ended"forstopped, andformatDdHhMm(seconds)forcountdown/running. The format helper lives in the pure moduleevent-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
- The admin cron / settings update writes to
setting.eventStartUtc/setting.eventEndUtc. SseHubService.publish('event', payload)fans the new payload out to every subscriber of/api/v1/event/streamand (via theflat()mapping)/api/v1/events/status.- 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).