132 lines
7.2 KiB
Markdown
132 lines
7.2 KiB
Markdown
---
|
||
type: guide
|
||
title: Authenticated Shell
|
||
description: How a signed-in user experiences the post-login shell, including authenticated event streaming.
|
||
tags: [guide, shell, header, sse, navigation, tester]
|
||
timestamp: 2026-07-22T09:35:00Z
|
||
---
|
||
|
||
# When this view is available
|
||
|
||
The authenticated shell is the post-login UI rendered by `HomeComponent`
|
||
(`frontend/src/app/features/home/home.component.ts`). It is gated by the client
|
||
`authGuard` and the JWT-protected `/api/v1/events/status` stream.
|
||
|
||
A user who is not signed in, or whose session has expired, is sent to `/login`
|
||
before the shell mounts.
|
||
|
||
# How to access (tester steps)
|
||
|
||
1. Ensure the instance is initialized. If not, follow [First-Run Bootstrap](/guides/bootstrap.md).
|
||
2. Sign in via `/login` as an admin or player.
|
||
3. Confirm the browser lands on `/challenges` and renders the shell.
|
||
|
||
# Expected behavior
|
||
|
||
## Header
|
||
|
||
| Element | Selector | Expected |
|
||
|---|---|---|
|
||
| Page title | `[data-testid="shell-title"]` | Shows the bootstrap `pageTitle`; clicking navigates to `/challenges`. |
|
||
| Active section | `[data-testid="shell-active-section"]` | Shows `Challenges`, `Scoreboard`, `Blog`, or `Admin` based on the URL. |
|
||
| Event LED | `[data-testid="shell-led"]` | A 10×10 circular dot whose color reflects the current event state. See [Event LED colors](#event-led-colors) below. |
|
||
| Countdown | `[data-testid="shell-countdown"]` | Displays `DD:HH:mm`, `Event ended`, or empty for an unconfigured event. |
|
||
| User menu | `[data-testid="user-menu-trigger"]` | Toggles rank, change-password, optional admin, and logout entries. |
|
||
|
||
# Event LED colors
|
||
|
||
The LED is rendered by `ShellHeaderComponent` as a `<span class="led">` with one of four state classes attached via `[class.led-<state>]` and a `[style.background-color]` bound directly from a pure TypeScript helper.
|
||
|
||
| Event state | CSS class | Color token (`[style.background-color]` value) | Approx. hex | Visual |
|
||
|-------------------|-----------------------|------------------------------------------------|-------------|--------|
|
||
| `countdown` | `led led-countdown` | `var(--color-warning)` | `#f59e0b` | Yellow filled dot |
|
||
| `running` | `led led-running` | `var(--color-success)` | `#10b981` | Green filled dot |
|
||
| `stopped` | `led led-stopped` | `var(--color-danger)` | `#ef4444` | Red filled dot |
|
||
| `unconfigured` | `led led-unconfigured`| `var(--color-secondary)` | `#64748b` | Neutral gray filled dot |
|
||
|
||
The mapping is defined in [`frontend/src/app/core/services/event-status.pure.ts`](/architecture/frontend-structure.md#event-status-pure-helpers) as the constant `LED_COLOR_BY_STATE` and exposed through the helper `ledBackgroundColor(state)`. `ShellHeaderComponent` exposes it on the DOM via a `computed` signal called `ledColor()`, so the active theme token resolves at runtime and the LED is never painted with `transparent`.
|
||
|
||
The base `.led` rule paints a 10×10 circular `inline-block`, so the LED
|
||
is always the same size and shape — only the background color changes
|
||
between states. The element also carries
|
||
`aria-label="Event status: <state>"` for assistive tech.
|
||
|
||
# What the tester should see
|
||
|
||
* The 10×10 LED is visible (not transparent, not invisible) in every state.
|
||
* The LED color matches the table above for the active `EventState`.
|
||
* Switching the event window (countdown → running → stopped → unconfigured) flips the dot color in real time.
|
||
* The `aria-label` updates to `Event status: <state>` and never relies on color alone.
|
||
|
||
## Quick tabs
|
||
|
||
The tab strip `[data-testid="quick-tabs"]` contains `Challenges`, `Scoreboard`, and
|
||
`Blog`. Clicking a tab navigates to its matching route; the active tab is marked
|
||
with the active class.
|
||
|
||
# Live event window
|
||
|
||
1. `HomeComponent.ngOnInit()` starts `EventStatusStore` with
|
||
`AuthenticatedEventSourceService.open('/api/v1/events/status')`.
|
||
2. The transport uses `fetch`, sends `Accept: text/event-stream`, includes
|
||
`Authorization: Bearer <access-token>` when available, and sends credentials.
|
||
3. It parses SSE `event:` and `data:` lines, buffers frames until a message listener
|
||
is registered, and dispatches `MessageEvent` payloads.
|
||
4. The store applies each state frame and advances countdown values with a local
|
||
one-second tick between server pushes.
|
||
5. Leaving the shell closes the source, aborts the fetch, and clears the tick.
|
||
|
||
A failed HTTP response, missing stream body, or streaming failure dispatches an
|
||
error event. The shell retains the last event state until a later connection succeeds.
|
||
|
||
# Architecture map
|
||
|
||
| File | Responsibility |
|
||
|---|---|
|
||
| `frontend/src/app/features/home/home.component.ts` | Hosts the shell and starts/stops the authenticated event stream. |
|
||
| `frontend/src/app/core/services/authenticated-event-source.service.ts` | Fetch-based authenticated SSE transport with abort and frame parsing. |
|
||
| `frontend/src/app/core/services/event-status.store.ts` | Applies event frames and derives the live countdown. |
|
||
| `frontend/src/app/features/shell/header/shell-header.component.ts` | Renders event LED, countdown, navigation, and user menu; owns the component-scoped LED shape and state color rules. |
|
||
| `frontend/src/styles.css` | Defines the shared theme color tokens consumed by the shell LED. |
|
||
| `frontend/src/app/features/shell/tabs/quick-tabs.component.ts` | Renders the main navigation tabs. |
|
||
| `tests/frontend/authenticated-event-source.spec.ts` | Verifies authorization headers, SSE frame delivery, and anonymous header omission. |
|
||
|
||
# Tester flows
|
||
|
||
## Live event window
|
||
|
||
1. Configure a future start time and confirm the countdown LED and text appear.
|
||
2. Move the start time into the past and the end time into the future; confirm the
|
||
LED changes to running and the countdown counts toward the end.
|
||
3. Move the end time into the past; confirm `Event ended` and the stopped LED.
|
||
4. Remove either setting; confirm the unconfigured LED and empty countdown.
|
||
|
||
## Navigation and logout
|
||
|
||
1. Click each quick tab and confirm the matching route and active styling.
|
||
2. Open the user menu and click **Logout**.
|
||
3. Confirm navigation to `/login` and that the event stream is closed.
|
||
4. With **two open tabs** of the same browser profile both on authenticated
|
||
routes, log out in one. Confirm the *other* tab also navigates to `/login`
|
||
without any user interaction. See
|
||
[Cross-Tab Authenticated Shell Invalidation](/guides/cross-tab-invalidation.md)
|
||
for the full tester matrix (peer logout, SSE unauthorized, negative cases).
|
||
|
||
## Unauthorized SSE response
|
||
|
||
1. Sign in once and open the authenticated shell.
|
||
2. Trigger an unauthorized response on `/api/v1/events/status` (e.g. by
|
||
revoking the refresh cookie via an admin tool or by expiring the session
|
||
server-side).
|
||
3. Confirm the shell clears, the event stream is closed, and the router
|
||
navigates to `/login`. The same flow in another open tab is propagated
|
||
via the cross-tab broadcast — see
|
||
[Cross-Tab Authenticated Shell Invalidation](/guides/cross-tab-invalidation.md).
|
||
|
||
# See also
|
||
|
||
- [Event Window](/guides/event-window.md)
|
||
- [System Endpoints](/api/system.md)
|
||
- [Frontend Structure](/architecture/frontend-structure.md)
|
||
- [Change Password](/guides/change-password.md)
|