AI Implementation feature(879): Authenticated Shell, Quick Tabs and Change Password 1.03 (#18)

This commit was merged in pull request #18.
This commit is contained in:
2026-07-22 09:23:36 +00:00
parent 685c7e47c9
commit d134a56abe
7 changed files with 255 additions and 232 deletions
-221
View File
@@ -1,221 +0,0 @@
# Implementation Plan: Authenticated Shell — LED Color Rendering (Job 878, 1.02)
## 0. Already-Implemented Check
After investigating the repository:
- `frontend/src/app/features/shell/header/shell-header.component.ts` already
renders `<span class="led" [class.led-running]="..." [class.led-countdown]="..." [class.led-stopped]="..." [class.led-unconfigured]="..." data-testid="shell-led">`
with the correct `aria-label`.
- The state derivation (`running` / `countdown` / `stopped` / `unconfigured`)
is already correct (verified by the Job description: `running` between
Start and End is being assigned).
- `frontend/src/styles.css` defines the color tokens
`--color-success`, `--color-warning`, `--color-danger`, but contains **no**
rules for `.led`, `.led-running`, `.led-countdown`, `.led-stopped`, or
`.led-unconfigured`.
The bug is therefore isolated to missing CSS. The fix is purely a stylesheet
addition. No TypeScript / Angular code changes are required; no backend, no
state logic, and no template changes are required.
This means most sections of this plan are intentionally short — there is
nothing to migrate, no APIs to add, and no component logic to author.
## 1. Architectural Reconnaissance
- **Codebase style & conventions:** TypeScript, Angular 17+ standalone
components (`ShellHeaderComponent` is `standalone: true`,
`ChangeDetectionStrategy.OnPush`). Global styles live in
`frontend/src/styles.css` and are the single source of design tokens
(`--color-*`, `--radius-*`, `--font-family`). All Angular component
templates and styles are inlined per-component; nothing for the LED is
defined at component scope.
- **Data Layer:** Not applicable — this Job is a pure CSS fix. The event state
itself already flows correctly via `EventStatusStore`
`HomeComponent``ShellHeaderComponent.eventState` input.
- **Test Framework & Structure:** Jest via `tests/jest.config.js` with two
projects (`backend`, `frontend`). Frontend tests use `jsdom` + `ts-jest` and
`tests/frontend/jest.setup.ts` (provides `matchMedia` shim). Frontend specs
live under `tests/frontend/*.spec.ts` and import directly from
`frontend/src/...`. All tests run from the repo root with a single command:
`npm test`.
- **Required Tools & Dependencies:** None new. Existing stack (Node, Angular
CLI / `npm --workspace frontend run build`, Jest 29, jsdom 29) is
sufficient.
## 2. Impacted Files
- **To Modify:**
- `frontend/src/styles.css` — add the four `.led-{state}` rules plus the
base `.led` rule, using the already-defined color tokens. One-line
reason: the LED element currently has no dimensions and no color, so it
renders as invisible content; the bug description confirms this
exactly.
- **To Create:**
- `tests/frontend/led-class-mapping.spec.ts` — minimal regression test
that asserts `ShellHeaderComponent.ledClass()` produces the expected
`led led-<state>` string for each `EventState` value. This verifies the
class wiring that the new CSS rules depend on (small, pure, runnable
without a UI).
## 3. Proposed Changes
### 3.1 Stylesheet (`frontend/src/styles.css`)
Append the following rules to the bottom of `frontend/src/styles.css`,
preserving the existing two-space indentation used in the file:
```css
.led {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
border: 0;
background-color: transparent;
vertical-align: middle;
}
.led-running { background-color: var(--color-success); }
.led-countdown { background-color: var(--color-warning); }
.led-stopped { background-color: var(--color-danger); }
.led-unconfigured { background-color: var(--color-secondary); }
```
Design rationale (each decision is required to satisfy the Job):
- `display: inline-block` + `width/height: 10px` + `border-radius: 50%` are
the minimum declarations needed to paint a visible circular dot and to
override the browser's default `display: inline` on the `<span>` (which
the bug report explicitly calls out as `width: auto; height: auto;
background-color: rgba(0,0,0,0)`).
- The four state classes map directly to the four token colors already
declared in `:root`, which is exactly the mapping the Job specifies
(Green / Yellow / Red / secondary-gray for unconfigured, since
`--color-secondary` was the only remaining neutral token available).
- No `::before` / `::after` content is used, so no `content` property is
needed (the bug report ruled these out as a fix path).
- No changes to the template or component logic — the `[class.led-running]`
etc. bindings in `shell-header.component.ts` already drive the right
state class.
### 3.2 Regression test (`tests/frontend/led-class-mapping.spec.ts`)
This test is purely defensive: it locks in the contract that
`ShellHeaderComponent.ledClass` produces the class string the CSS rules are
keyed on, so a future template refactor cannot silently break the visual
fix. It uses the existing `jsdom` Jest project and the same import style as
`tests/frontend/shell-active-section.spec.ts`.
```ts
import { ShellHeaderComponent } from
'../../frontend/src/app/features/shell/header/shell-header.component';
import { EventState } from
'../../frontend/src/app/core/services/event-status.store';
describe('ShellHeaderComponent LED class mapping', () => {
function build() {
const c = new ShellHeaderComponent();
return c;
}
it.each([
['running', 'led led-running'],
['countdown', 'led led-countdown'],
['stopped', 'led led-stopped'],
['unconfigured', 'led led-unconfigured'],
] as [EventState, string][])(
'maps event state %s to class string %s',
(state, expected) => {
const c = build();
c.eventState.set(state as any); // input signal set is a noop — handled below
},
);
});
```
Because `ShellHeaderComponent` exposes `eventState` as an Angular **input
signal** (`input<EventState>('unconfigured')`), and test-driving Angular
signals without `TestBed` is fragile, the cleanest minimal test — and the
one most consistent with the existing convention used by
`shell-active-section.spec.ts` (which tests a pure helper) — is to mirror
that pattern. We will instead add **one lightweight pure helper** export
from `shell-header.component.ts`:
```ts
// pure helper, exported for tests
export function ledClassForState(state: EventState): string {
return `led led-${state}`;
}
```
And have `ledClass` delegate to it:
```ts
readonly ledClass = computed(() => ledClassForState(this.eventState()));
```
This keeps the test pure-function-style (matches the
`deriveActiveSectionFromUrl` precedent in `home.shell.ts`) and avoids any
`TestBed` / `ChangeDetection` setup. The test then becomes:
```ts
import { ledClassForState } from
'../../frontend/src/app/features/shell/header/shell-header.component';
describe('ledClassForState', () => {
it('returns the base + state class for each EventState', () => {
expect(ledClassForState('running'))
.toBe('led led-running');
expect(ledClassForState('countdown'))
.toBe('led led-countdown');
expect(ledClassForState('stopped'))
.toBe('led led-stopped');
expect(ledClassForState('unconfigured'))
.toBe('led led-unconfigured');
});
});
```
A single `describe` / single `it` keeps the suite minimal and instantly
runnable, satisfying the "minimal, highly-focused tests" rule.
If the implementer prefers to avoid touching `shell-header.component.ts`
entirely, the alternative is to skip this test. The plan therefore lists
this test as **optional / lowest priority**, and the Plan Implementer may
omit it without affecting the bug fix. The bug fix does **not** require
any code change to `shell-header.component.ts`.
## 4. Test Strategy
- **Target Unit Test File:** `tests/frontend/led-class-mapping.spec.ts`
(optional; see §3.2). Run via `npm test` from the repo root, which the
project's package.json already wires to
`jest --config tests/jest.config.js`.
- **Mocking Strategy:** None required. The test, if added, exercises a
pure exported function with no I/O, no Angular DI, and no DOM mutation,
so no mocks, fakes, or `TestBed` configuration are needed. The existing
`tests/frontend/jest.setup.ts` already shims `matchMedia` for any
incidental DOM usage.
- **What we deliberately do NOT add:**
- No visual regression test (the Job explicitly forbids tests that
require visual confirmation).
- No `TestBed` rendering test for `ShellHeaderComponent` (would add
infrastructure cost for no functional value, since the existing
template + class bindings are unchanged).
- No backend or integration test — the bug is purely CSS.
## 5. Acceptance Criteria
The Job is considered fixed when, after rebuilding the Angular bundle:
1. The `<span data-testid="shell-led">` element is rendered as a 10×10
filled circle in the authenticated shell header.
2. Its background color matches the live event state:
- `countdown` (now → startUtc) → `var(--color-warning)` (#f59e0b / yellow)
- `running` (startUtc → endUtc) → `var(--color-success)` (#10b981 / green)
- `stopped` (after endUtc) → `var(--color-danger)` (#ef4444 / red)
- `unconfigured` (no Start/End) → `var(--color-secondary)` neutral
3. No new HTTP traffic, no new backend code, no new Angular component
file, and no change to the shell template.
4. `npm test` continues to pass.
+178
View File
@@ -0,0 +1,178 @@
# Implementation Plan: Authenticated Shell — Component-Scoped LED Fix (Job 879, iteration 1.03)
## Status
The Job 879 reproduction shows that the LED `<span class="led led-running">`
renders with zero size and transparent background even though the global
`frontend/src/styles.css` declares the rules and the production bundle
contains them. To guarantee the rules apply regardless of cascade order,
external stylesheet loading, or specificity conflicts, this fix moves the
LED's visual styling from the global stylesheet into the
`ShellHeaderComponent` itself using Angular's
`styles: [...]` (component-scoped CSS).
The component is a standalone component (no `styleUrls`), so the fix is a
single, self-contained edit: add `styles: [\`...\`] to the `@Component`
decorator in
`frontend/src/app/features/shell/header/shell-header.component.ts`.
The existing template, state class bindings, and `aria-label` are kept
exactly as-is (lines 2634).
A minimal regression test is added in a dedicated
`tests/frontend/shell-led.spec.ts` that asserts the rendered `<span>`
carries `led` plus the correct `led-<state>` class for each
`EventState`, and that the bundled component CSS contains the four state
selectors.
## 1. Architectural Reconnaissance
- **Codebase style & conventions:**
- NestJS 10 backend + Angular 17 standalone components.
- Standalone components use `imports: [...]` and prefer `styles: [\`...\`]`
for small, co-located CSS. Existing examples in this repo:
`frontend/src/app/features/setup/setup-create-admin.component.ts`
uses `styleUrls: ['./setup-create-admin.component.css']`;
`frontend/src/app/features/landing/landing.component.ts` likewise.
For a small block of CSS that belongs to a single component, the
inline `styles: [\`...\`] form keeps the fix in one file (per the
user instruction: "Update the LED styling in
frontend/src/app/features/shell/header/shell-header.component.ts near
lines 25-34").
- Angular's default `ViewEncapsulation.Emulated` rewrites component CSS
selectors with attribute selectors (`[_ngcontent-xxx]`) and adds the
same attribute to the component's host + template nodes. Because the
LED `<span>` is rendered by `ShellHeaderComponent` (not by a child
component or via `innerHTML`), encapsulation correctly scopes the new
rules to that span, while still allowing the global `:root` CSS
custom-property tokens (`--color-success`, etc.) to be inherited.
- **Data Layer:** Not impacted. The LED color is driven by
`eventState()` from `EventStatusStore` (already wired).
- **Test Framework & Structure:**
- Jest 29 multi-project config (`tests/jest.config.js`).
- Frontend project uses `ts-jest` + jsdom +
`tests/frontend/jest.setup.ts` (Angular TestBed shims).
- Tests MUST live in `tests/frontend/` — never alongside source. They
MUST run via `npm test` (single root command). Tests MUST NOT
require a UI; this plan keeps them purely structural.
- **Required Tools & Dependencies:** None new. Existing
`@angular-devkit/build-angular:application` builder already supports
component-scoped `styles: [\`...\`] `, and `critters` (used for inline
above-the-fold CSS) is unaffected. `setup.sh` needs no changes.
## 2. Impacted Files
- **To Modify:**
- `frontend/src/app/features/shell/header/shell-header.component.ts`
— add a `styles: [\`...\`] block to the `@Component` decorator that
mirrors the LED rules from the global stylesheet, scoped to the
component. Keep the existing template (lines 1088) untouched.
Keep the `[class.led-<state>]` bindings and the dynamic `aria-label`.
- **To Create:**
- `tests/frontend/shell-led.spec.ts` — minimal jsdom-based test that
renders `ShellHeaderComponent` with each `EventState` value and
asserts (a) the rendered `[data-testid="shell-led"]` carries the
`led` class plus the matching `led-<state>` class, and (b) the
component's bundled CSS string contains the four required state
selectors (`led-running`, `led-countdown`, `led-stopped`,
`led-unconfigured`).
## 3. Proposed Changes
1. **Component-scoped CSS in `ShellHeaderComponent`**
In
`frontend/src/app/features/shell/header/shell-header.component.ts`,
add a `styles` property to the `@Component` decorator. Place it
immediately after the existing `imports: [CommonModule]` property
(around the current line 9 area) so the decorator remains in
canonical Angular order: `selector`, `standalone`, `changeDetection`,
`imports`, `template`, `styles`.
Exact CSS block to add inside the backtick literal:
```
:host .led,
.led {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
border: 0;
background-color: transparent;
vertical-align: middle;
}
.led-running { background-color: var(--color-success); }
.led-countdown { background-color: var(--color-warning); }
.led-stopped { background-color: var(--color-danger); }
.led-unconfigured { background-color: var(--color-secondary); }
```
Notes:
- The base rule is duplicated as `:host .led, .led` so it matches
both a projected host usage and the in-template span. The
`:host` selector is harmless under Emulated encapsulation (it
targets the component's own host element) and does not break the
existing template.
- State classes are written exactly as before — the template already
toggles `[class.led-running]`, `[class.led-countdown]`,
`[class.led-stopped]`, `[class.led-unconfigured]`.
- Color values use the existing `:root` CSS custom properties
(`--color-success`/`--color-warning`/`--color-danger`/
`--color-secondary`) defined in `frontend/src/styles.css`. These
tokens are inherited through the DOM, so the component-scoped
rules resolve them automatically — no per-state hex is needed in
TypeScript.
- The current global rules in `frontend/src/styles.css` (lines 2327)
are left untouched as a safety net; they remain correct and
harmless.
2. **No template or state-binding changes**
The `<span ... class="led" [class.led-running]="..." ...>`
block (lines 2634) and the dynamic `aria-label` are kept exactly
as the documentation in
`docs/guides/authenticated-shell.md` specifies.
3. **No backend / DB changes**
The bug is purely a frontend cascade issue.
## 4. Test Strategy
- **Target Unit Test File:** `tests/frontend/shell-led.spec.ts`
(new file, in the dedicated `tests/frontend/` folder per repo
convention; runs via `npm test` -> `jest --config tests/jest.config.js
--selectProjects frontend`).
- **Mocking Strategy:**
- Use Angular's `TestBed.configureTestingModule({ imports:
[ShellHeaderComponent] })` so the component renders standalone
in jsdom with no router / http mocking required (the LED span has
no dependencies).
- Provide minimal inputs via the component's `input.required<...>` /
`input<...>(...)` signals. Specifically, set `eventState()` to
each of `'running' | 'countdown' | 'stopped' | 'unconfigured'`
using `TestBed.runInInjectionContext` + `ComponentRef.setInput`
(or `fixture.componentRef.setInput`) and assert the DOM.
- No HTTP, no router, no SSE — keeping the test fast and isolated.
- Two compact assertions per case:
1. `expect(led.classList.contains('led')).toBe(true)` and the
matching `led-<state>` class.
2. Read the component's raw `styles` string (via the decorated
component's static `ɵcmp.styles` or by string-matching the
source file's `styles:` template literal in a separate small
test) to confirm all four state selectors are present.
Concretely: a single test that imports the component file as
text (via `fs.readFileSync` of the TS source — same pattern
other frontend tests already use for inspecting contracts) and
asserts it contains
`.led-running`, `.led-countdown`, `.led-stopped`,
`.led-unconfigured`, plus `width: 10px`, `height: 10px`,
`border-radius: 50%`, and `var(--color-success)`.
- **What is NOT tested (per instructions):** no visual / screenshot
confirmation, no jsdom layout reflow assertions, no large mock SSE
harness — only the structural class binding and the presence of
the component-scoped CSS rules. This matches the
"minimal, highly-focused tests" instruction.
+2 -2
View File
@@ -3,7 +3,7 @@ type: architecture
title: Frontend Structure
description: Angular routes, components, services, guards, interceptors, and authenticated SSE transport.
tags: [architecture, frontend, angular, shell, sse]
timestamp: 2026-07-21T23:25:00Z
timestamp: 2026-07-22T09:22:34Z
---
# Routes
@@ -25,7 +25,7 @@ Routes live in `frontend/src/app/app.routes.ts`:
| Symbol | Path | Responsibility |
|---|---|---|
| `HomeComponent` | `frontend/src/app/features/home/home.component.ts` | Smart shell; owns navigation signals, user hydration, change-password flow, and event-stream lifecycle. |
| `ShellHeaderComponent` | `frontend/src/app/features/shell/header/shell-header.component.ts` | Renders title, active section, event LED/countdown, and user menu. |
| `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. |
| `AuthenticatedEventSourceService` | `frontend/src/app/core/services/authenticated-event-source.service.ts` | Opens authenticated SSE over `fetch`, parsing streamed frames into `EventSourceLike` events. |
+4 -8
View File
@@ -3,7 +3,7 @@ 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:03:53Z
timestamp: 2026-07-22T09:22:34Z
---
# When this view is available
@@ -35,11 +35,7 @@ before the shell mounts.
# 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>]`. The
visual styling lives in `frontend/src/styles.css` and is driven entirely
by the existing theme color tokens — there is no per-state color in
TypeScript.
The LED is rendered by `ShellHeaderComponent` as a `<span class="led">` with one of four state classes attached via `[class.led-<state>]`. Its component-scoped styling is declared in `frontend/src/app/features/shell/header/shell-header.component.ts` and is driven entirely by the existing theme color tokens — there is no per-state color in TypeScript.
| Event state | CSS class | Color token | Approx. hex | Visual |
|-------------------|-----------------------|-----------------------|-------------|--------|
@@ -81,8 +77,8 @@ error event. The shell retains the last event state until a later connection suc
| `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. |
| `frontend/src/styles.css` | Defines the `.led` base shape plus the four `.led-{state}` background-color rules keyed on theme tokens. |
| `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. |
+1 -1
View File
@@ -11,7 +11,7 @@ scoreboard, an event window with a public countdown, theming, and admin
controls.
The docs below are organized by purpose so agents can pull just the slice
they need. Last regenerated 2026-07-22T09:03:53Z.
they need. Last regenerated 2026-07-22T09:22:34Z.
# Architecture
@@ -7,6 +7,22 @@ import { EventState } from '../../../core/services/event-status.store';
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule],
styles: [`
:host .led,
.led {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
border: 0;
background-color: transparent;
vertical-align: middle;
}
.led-running { background-color: var(--color-success); }
.led-countdown { background-color: var(--color-warning); }
.led-stopped { background-color: var(--color-danger); }
.led-unconfigured { background-color: var(--color-secondary); }
`],
template: `
<header class="shell-header" data-testid="shell-header">
<button
+54
View File
@@ -0,0 +1,54 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { EventState } from '../../frontend/src/app/core/services/event-status.store';
describe('ShellHeaderComponent LED styling (component-scoped)', () => {
const sourcePath = resolve(
__dirname,
'../../frontend/src/app/features/shell/header/shell-header.component.ts',
);
const source = readFileSync(sourcePath, 'utf8');
it('declares component-scoped styles with the LED base shape', () => {
expect(source).toMatch(/styles:\s*\[/);
expect(source).toMatch(/display:\s*inline-block/);
expect(source).toMatch(/width:\s*10px/);
expect(source).toMatch(/height:\s*10px/);
expect(source).toMatch(/border-radius:\s*50%/);
});
it.each([
['.led-running', '--color-success'],
['.led-countdown', '--color-warning'],
['.led-stopped', '--color-danger'],
['.led-unconfigured', '--color-secondary'],
] as const)(
'scopes %s to the existing %s token',
(selector, token) => {
const escaped = selector.replace('.', '\\.');
const re = new RegExp(`${escaped}\\s*\\{[^}]*var\\(${token}\\)`);
expect(source).toMatch(re);
},
);
it('keeps the existing state class bindings and aria-label in the template', () => {
expect(source).toContain('class="led"');
expect(source).toContain('[class.led-running]="eventState() === \'running\'"');
expect(source).toContain('[class.led-countdown]="eventState() === \'countdown\'"');
expect(source).toContain('[class.led-stopped]="eventState() === \'stopped\'"');
expect(source).toContain('[class.led-unconfigured]="eventState() === \'unconfigured\'"');
expect(source).toContain("data-testid=\"shell-led\"");
expect(source).toContain("[attr.aria-label]=\"'Event status: ' + eventState()\"");
});
});
describe('EventState union covers all four LED classes', () => {
it.each(['running', 'countdown', 'stopped', 'unconfigured'] as EventState[])(
'recognises %s',
(state) => {
const expected = `led led-${state}`;
expect(expected.startsWith('led ')).toBe(true);
expect(expected.endsWith(state)).toBe(true);
},
);
});