222 lines
9.3 KiB
Markdown
222 lines
9.3 KiB
Markdown
# 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.
|