9.7 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| architecture | Frontend Structure | Angular routes, components, services, guards, interceptors, and authenticated SSE transport. |
|
2026-07-23T00:10:00Z |
Routes
Routes live in frontend/src/app/app.routes.ts:
| Path | Component | Guard | Notes |
|---|---|---|---|
/bootstrap |
SetupCreateAdminComponent |
— | First-admin creation flow. |
/login |
LandingComponent |
landingGuard |
Public landing and authentication modal. |
/ |
HomeComponent |
authGuard |
Authenticated shell and child outlet. |
/challenges |
ChallengesPage |
inherited | Full challenges board (category columns, modal, flag submit, live SSE). |
/scoreboard |
ScoreboardPage |
inherited | Placeholder scoreboard page. |
/blog |
BlogPage |
inherited | Placeholder blog page. |
/admin |
AdminUsersComponent |
adminGuard |
Admin-only child route. |
/admin/challenges |
AdminChallengesComponent |
inherited | Sortable/searchable admin Challenges list, Add/Edit modal, import/export. |
Components and services
| Symbol | Path | Responsibility |
|---|---|---|
HomeComponent |
frontend/src/app/features/home/home.component.ts |
Smart shell; owns navigation signals, user hydration, change-password flow, event-stream lifecycle, peer-invalidation subscription, and per-session resets of both UserStore and ChallengesStore (the latter flushes the per-user board cache on logout / SSE unauthorized / peer-logout). |
ShellHeaderComponent |
frontend/src/app/features/shell/header/shell-header.component.ts |
Renders title, active section, event LED/countdown, and user menu; contains component-scoped LED styling. |
QuickTabsComponent |
frontend/src/app/features/shell/tabs/quick-tabs.component.ts |
Emits navigation events for Challenges, Scoreboard, and Blog. |
EventStatusStore |
frontend/src/app/core/services/event-status.store.ts |
Applies event state frames and computes countdown text; re-exports the pure LED color mapping; wires an optional onUnauthorized callback for the new SSE 401/403 event. |
AuthenticatedEventSourceService |
frontend/src/app/core/services/authenticated-event-source.service.ts |
Opens authenticated SSE over fetch, parsing streamed frames into EventSourceLike events; dispatches a separate 'unauthorized' event for 401/403. |
AuthService |
frontend/src/app/core/services/auth.service.ts |
Stores the access token used by the authenticated SSE transport; owns the cross-tab invalidation BroadcastChannel + storage-event fallback and exposes clearAndBroadcast / onPeerInvalidation. |
UserStore |
frontend/src/app/core/services/user.store.ts |
Hydrates the signed-in user and rank data. |
auth-session-events.pure.ts |
frontend/src/app/core/services/auth-session-events.pure.ts |
Pure cross-tab message encoding/validation and the namespaced channel / storage-key constants. |
ChallengesStore |
frontend/src/app/features/challenges/challenges.store.ts |
Signal store backing /challenges: board payload, event state, per-card solve listeners, SSE solve-frame mutation, submit response application, public reset() for the session-boundary flush, and setMyUserId(id) (now returns the previous id and resets whenever the cached state belongs to an unknown or different user). |
ChallengesApiService |
frontend/src/app/features/challenges/challenges.service.ts |
HTTP wrapper around /api/v1/challenges/{board,status,:id,:id/solves} returning Observable<...> and translating HttpErrorResponse into a typed ApiErrorEnvelope. |
challenges.pure.ts |
frontend/src/app/features/challenges/challenges.pure.ts |
Pure types + helpers for the challenges feature (BoardCard, SolverRow, SolveEventPayload, sorting, parsers, friendly error mapping, formatDdHhMm). |
ChallengeCardComponent / CategoryColumnComponent / ChallengeModalComponent |
frontend/src/app/features/challenges/ |
Presentational components for the board grid, column, and detail modal. |
NotificationService |
frontend/src/app/core/services/notification.service.ts |
Root-provided signal-backed store of { id, kind, message, ts } records; error() / info() push, dismiss(id) / clear() remove. |
errorNotificationInterceptor |
frontend/src/app/core/interceptors/error-notification.interceptor.ts |
Functional HTTP interceptor that pushes a friendly message into NotificationService for every HttpErrorResponse; suppresses duplicate toasts for endpoints with their own error UI (login form, /challenges/status snapshot). |
Authenticated SSE wiring
HomeComponent.ngOnInit() calls EventStatusStore.start() with a factory for
AuthenticatedEventSourceService.open('/api/v1/events/status'). The transport:
- Reads the access token from
AuthService. - Sends
Accept: text/event-stream, optionalAuthorization: Bearer <token>,credentials: 'include', and anAbortSignal. - Reads the response body with a stream reader and decodes UTF-8 chunks.
- Splits complete SSE records on blank lines, joins repeated
data:lines, and dispatchesMessageEventobjects. - Buffers records received before the message listener is attached.
- Inspects the response status before the
ok/bodycheck; on401or403dispatches a separate'unauthorized'event and returns without reading the body. Generic transport failures still dispatch'error'. - Aborts and clears listeners when
close()is called.
EventStatusStore.start(createSource, onUnauthorized?) parses each message as
EventStatePayload, updates its signals, runs a one-second local timer, and —
when onUnauthorized is provided — invokes it on 'unauthorized' events.
HomeComponent passes an onUnauthorized callback that delegates to its
shared handleSessionInvalidated('sse-unauthorized') flow and stops the store
on destruction.
Key integration files
| File | Responsibility |
|---|---|
frontend/src/app/app.routes.ts |
Registers shell and child routes. |
frontend/src/app/core/interceptors/auth.interceptor.ts |
Adds Bearer authentication to Angular HTTP requests. |
frontend/src/app/core/interceptors/error-notification.interceptor.ts |
Translates HttpErrorResponse into friendly messages and pushes them to NotificationService (with suppression rules for login + /challenges/status). |
frontend/src/app/core/services/authenticated-event-source.service.ts |
Adds Bearer authentication to the browser SSE request, which does not use the Angular HTTP interceptor chain. |
frontend/src/app/core/services/event-status.pure.ts |
Defines the event payload and transport interface. |
frontend/src/app/features/challenges/challenges.page.ts |
/challenges smart page; owns gate logic, modal lifecycle, SSE wiring, and the countdown-zero reload. |
frontend/src/app/features/challenges/challenges.store.ts |
Signal store backing /challenges: board, event state, per-card solve listeners, SSE solve-frame mutation, submit response application, public reset(), and per-user setMyUserId flush. |
tests/frontend/authenticated-event-source.spec.ts |
Regression coverage for authenticated SSE transport behavior. |
Event-status pure helpers
frontend/src/app/core/services/event-status.pure.ts owns the small,
no-Angular types and pure functions consumed by the event status pipeline.
EventStatusStore re-exports them so application code can import from a
single path.
| Export | Purpose |
|---|---|
EventState |
Union 'running' | 'countdown' | 'stopped' | 'unconfigured'. |
EventStatePayload |
Server-pushed event window payload shape. |
EventSourceLike |
Minimal interface for any SSE transport the store can drive. |
formatDdHhMm(seconds) |
Pure DD:HH:mm:ss formatter (zero-padded, falls back to 00:00:00:00 for negative / non-finite input). |
deriveCountdownText(state, start, end) |
Pure mapping from event window state to countdown label. |
LED_COLOR_BY_STATE |
Readonly<Record<EventState, string>> mapping each state to its theme CSS variable (var(--color-success), --color-warning, --color-danger, --color-secondary). |
ledBackgroundColor(state) |
Pure accessor returning the background-color value for the LED element. |
ShellHeaderComponent exposes a computed signal ledColor() that calls
ledBackgroundColor(this.eventState()) and binds it to the LED via
[style.background-color]. The component's .led base rule is therefore
shape-only (size, radius, inline-block, vertical alignment); the visible
color always comes from LED_COLOR_BY_STATE and is guaranteed to be a
non-transparent theme token. The four led-<state> classes remain on the
element as selectors/test hooks but no longer own the color.
Cross-tab session invalidation
AuthService (constructed once at root) installs a single
BroadcastChannel('hipctf.auth.v1') and a localStorage storage-event
fallback. The shape of the broadcast payload is defined in
auth-session-events.pure.ts and contains only { type, origin, ts } — no
tokens or user data. clearAndBroadcast() is invoked from logout() and
from the SSE unauthorized callback; peer listeners clear their local state
without rebroadcasting. HomeComponent subscribes via
auth.onPeerInvalidation(...), resets UserStore, closes any open shell
overlays, and navigates to /login. See
Cross-Tab Authenticated Shell Invalidation
for the full contract and tester matrix.