--- type: architecture title: Frontend Structure description: Angular routes, components, services, guards, interceptors, authenticated SSE transport, and blog management and presentation wiring. tags: [architecture, frontend, angular, shell, sse, challenges, scoreboard, notifications, blog] timestamp: 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 `ApiErrorEnvelope`s. | | `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. `BlogFormModalComponent` also installs a `@HostListener('document:keydown.escape')` handler that closes the modal via `onCancel()` when the modal is open and not currently saving (mirrors the `change-password-modal` Escape contract). | # Authenticated SSE wiring `HomeComponent.ngOnInit()` calls `EventStatusStore.start()` with a factory for `AuthenticatedEventSourceService.open('/api/v1/events/status')`. The transport: 1. Reads the access token from `AuthService`. 2. Sends `Accept: text/event-stream`, optional `Authorization: Bearer `, `credentials: 'include'`, and an `AbortSignal`. 3. Reads the response body with a stream reader and decodes UTF-8 chunks. 4. Splits complete SSE records on blank lines, joins repeated `data:` lines, and dispatches `MessageEvent` objects. 5. Buffers records received before the message listener is attached. 6. Inspects the response status *before* the `ok`/`body` check; on `401` or `403` dispatches a separate `'unauthorized'` event and returns without reading the body. Generic transport failures still dispatch `'error'`. 7. 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>` 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-` 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](/guides/cross-tab-invalidation.md) for the full contract and tester matrix. # See also - [Authenticated Shell](/guides/authenticated-shell.md) - [Event Window](/guides/event-window.md) - [Challenges Board](/guides/challenges-board.md) - [Scoreboard Page](/guides/scoreboard-page.md) - [Notifications](/guides/notifications.md) - [Blog Publishing and Reading](/guides/blog.md) - [System Overview](/architecture/overview.md)