13 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| architecture | Frontend Structure | Angular routes, components, services, guards, interceptors, authenticated SSE transport, and blog management and presentation wiring. |
|
2026-07-23T10:12:24Z |
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 | Live scoreboard page with 4 tabs (Ranking / Matrix / Event Log / Score Graph), live SSE solve updates. |
/blog |
BlogPage |
inherited | Loads and renders published posts with loading, error, and empty states. |
/admin |
AdminShellComponent |
adminGuard |
Admin-only layout; redirects to /admin/general. |
/admin/challenges |
AdminChallengesComponent |
inherited | Sortable/searchable admin Challenges list, Add/Edit modal, import/export. |
/admin/blog |
AdminBlogComponent |
inherited | Admin post table and create/edit/draft/publish/delete workflow. |
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. |
ScoreboardPage |
frontend/src/app/features/scoreboard/scoreboard.page.ts |
Smart page for /scoreboard: holds a ScoreboardStore, calls store.loadAll() + store.wireSse(...) on init, tears them down on destroy, and switches between the 4 tab components via *ngIf on store.activeTab(). |
ScoreboardStore |
frontend/src/app/features/scoreboard/scoreboard.store.ts |
Root-provided signal store backing /scoreboard: holds the four projections, active tab, loading + error state, and the SSE lifecycle (wireSse / stop / exponential reconnect on transport error). Mutates all four tabs from each solve frame via pure helpers (parseSolveEventIntoRanking, mutateMatrixFromSolve, applySolveToGraph, dedupEventLogBySolveId). |
ScoreboardApiService |
frontend/src/app/features/scoreboard/scoreboard.service.ts |
HTTP wrapper around /api/v1/scoreboard/{ranking,matrix,event-log,graph} returning typed ApiErrorEnvelopes. |
scoreboard.pure.ts |
frontend/src/app/features/scoreboard/scoreboard.pure.ts |
Pure types + helpers shared between the store and the four tab components (RankingRow, MatrixView, EventLogRow, GraphView, SolveLivePayload, PLAYER_COLOR_PALETTE, stablePlayerColorIndex, applyRankingSort, parseSolveEventIntoRanking, dedupEventLogBySolveId, applySolveToGraph, mutateMatrixFromSolve, formatSolveDateTime). |
ScoreboardTabsComponent |
frontend/src/app/features/scoreboard/tabs.component.ts |
Horizontal tab strip bound to store.activeTab(); emits (select) to switch tabs. |
RankingComponent / MatrixComponent / EventLogComponent / ScoreGraphComponent |
frontend/src/app/features/scoreboard/ |
Presentational components for the 4 tabs; each binds a single @Input() from ScoreboardStore and uses PLAYER_COLOR_PALETTE + stablePlayerColorIndex to color each player consistently. |
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). |
BlogApiService |
frontend/src/app/core/services/blog.service.ts |
Promise-based HTTP client for the published list and admin CRUD routes. |
BlogPage |
frontend/src/app/features/blog/blog.page.ts |
Smart /blog page that loads published posts and derives loading, error, empty, and list states. |
BlogPresenterComponent |
frontend/src/app/features/blog/blog-presenter.component.ts |
Shared title/date/sanitized-Markdown renderer used by /blog and /login. |
AdminBlogComponent |
frontend/src/app/features/admin/blog/blog.component.ts |
Smart /admin/blog page coordinating list refresh and create/edit/delete modal state. |
BlogFormModalComponent / BlogDeleteModalComponent |
frontend/src/app/features/admin/blog/ |
Admin Markdown editor with live preview and destructive-action confirmation. |
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. |
frontend/src/app/core/services/blog.service.ts |
Calls public and admin blog endpoints with typed payloads. |
frontend/src/app/features/blog/blog-presenter.component.ts |
Shared sanitized Markdown post rendering for authenticated and landing views. |
frontend/src/app/features/admin/blog/blog.component.ts |
Coordinates administrator blog CRUD UI and table refreshes. |
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.