Snapshot for resuming work in a fresh context. Read together with
PLAN.md(PLAN.md also carries per-phase "Implementation Notes" subsections).
| Phase | State |
|---|---|
| Phase 1 — Scaffold | ✅ done, committed 6882ba1 |
| Phase 2 — Dashboard core (variant C) | ✅ done, committed 2d367bf |
| Phase 3 — SSE live job monitor | ✅ done, committed 101c7c1 (sidenav mode="over" fix b619954, post-phase fixes f3f4abf) |
| Phase 4 — Polish & verification | ✅ done 2026-09-20 — closed as-is per user decision; deferred items → "Known debt" below |
src/app/
core/
env.ts API_BASE = `${window.location.origin}/api` (production/default)
env.development.ts API_BASE = 'http://localhost:5193/api' (swapped in via
fileReplacements in the development build config)
api/types.ts ServiceStatus, JobResult, ErrorDto, SequenceSuccess/Failure,
SseJobEnqueued/StateChanged/JobCompleted/Error
api/services-api.ts @Service() ServicesApi: list/get/start/stop/startAll/stopAll;
monitorStart(name)/monitorStop(name) → SSE endpoint URL;
describeApiError(err) → {title, message} (ErrorDto, SequenceFailure,
status-0 backend-down, fallbacks)
status.ts activeStateTone(activeState) → success|danger|warning|neutral|progress
unitStateTone(status) (delegates to activeStateTone)
jobResultTone(result) → success|danger|warning (unknown → warning)
relativeTime(iso, now?) → "just now" | "42s ago" | "5m ago" | "3h ago" | toLocaleString()
sse/monitor-stream.ts openMonitorStream(url) → {events: AsyncGenerator<SseEvent>, abort, aborted};
readMonitorEvents (fetch POST + body reader, buffer-to-\n\n);
parseSseFrame(frame) pure helper (event:/data: lines → SseEvent)
theme/theme.service.ts @Service() ThemeService: theme signal, toggle(), class-based
(theme-dark/theme-light on documentElement), localStorage['sm-theme']
features/dashboard/
dashboard.component.ts|.html|.scss
toolbar (title, theme toggle, Refresh, Start All, Stop All),
mat-sidenav-container wrapping content + end sidenav (job monitor),
card grid, loading/empty/error+Retry states,
rxResource list, busy signal, session signal {service, action},
viewChild<MatSidenav> + sidenav (closed) → onSidenavClosed()
(backdrop/Esc close clears session + re-fetches, same semantics
as in-panel Close), re-fetch (services.reload()) after
every action & on monitor close, MatSnackBar for results/errors
service-card.component.ts|.html|.scss
name+description, tone badge, "Job #n" chip + spinner (disables
Start/Stop), "Changed Xm ago", Start/Stop emit, busy input
features/job-monitor/
monitor-panel.component.ts|.html|.scss
consumes monitor AsyncIterable (for await in ngAfterViewInit,
abort in ngOnDestroy); signals status/result/resultTone/jobId/
liveState/log; live tone badge per state-changed; mat-expansion
event log; terminal completed→auto-close(2.5s)/error→red+msg;
fetch TypeError → friendly backend-down message;
"closing does not cancel" copy
app.ts shell → <app-dashboard />
app.config.ts provideRouter + provideHttpClient
Global: src/styles.scss (Material theme + --sm-tone-* vars), src/index.html
(pre-paint theme bootstrap script, <html class="theme-dark"> default).
- Unit tests: only the
app.spec.tssmoke test exists. Not written:status.tstone mappers (all states + all job results),parseSseFrame(synthetic frames, ordering quirk, partial chunks),services-apierror mapping, andservice-card/monitor-panelcomponent tests with Material harnesses. - No lint target —
ng add @angular-eslint(or similar) needed before "ng lint" works. - No
README.md— run steps, dev CORS note (backendCors:AllowedOrigins), production serving note (same-origin or add the production origin). - Start All / Stop All failure path never exercised live (SequenceFailure snackbar
coloring, e.g. via
example-missing.servicein the whitelist). - Polish: lazy-load
MonitorPanelComponentvia@defer (when session())— bundle initial is 529 kB raw / 118.5 kB transfer, over the 500 kB warning budget (1 MB error clear);MatSidenavModulestays eager as it is part of the dashboard layout. In-button spinner while Start All/Stop All is in flight. Periodic tick for the static "Changed Xm ago". Loading skeleton (currently a plain indeterminate spinner).
- A
state-changedframe can precedejob-enqueued; a trailingstate-changedwithjobId:0can arrive after the last real state; a monitor for a missing unit returns 200 with an empty stream (panel shows red error state); timeout arrives as anerrorframe; disconnecting the stream does NOT cancel the job.
resource()no longer accepts Observables.resource()takes a Promiseloaderor a streamingstream. For HttpClient observables userxResource()from@angular/core/rxjs-interop:rxResource<T, R>({ stream: (params) => observable }).R(request type) is the 2nd type arg — usevoidwhen there are noparams.RxResourceOptionshas NOdebugName(only coreresource()does).- Ref API:
.value()/.hasValue()/.isLoading()/.error()/.reload(). - Errors that are Error-like pass through unwrapped (
.error()is the original HttpErrorResponse).
- Material 22 theming:
@use '@angular/material' as mat;+ one@include mat.theme((color: (theme-type: color-scheme, primary: mat.$blue-palette), typography: Roboto, density: 0));onhtml { color-scheme: dark; }. All color tokens compile tolight-dark(...)and follow the inheritedcolor-scheme;html.theme-light { color-scheme: light; }flips the mode. Components bundle their own structural CSS — do NOT use the oldmat.define-standard-theme/all-component-themes/m3-define-palette(gone in v22). App colors:var(--mat-sys-*). @Service()decorator (v22) =@Injectable({providedIn: 'root'}).- OnPush is default in v22 — don't set it explicitly.
input()/output(),hostobject, native control flow — as per best practices. ng newv22 defaults to vitest; this project uses--test-runner=karma(Jasmine, per plan).
- npm 11.19 gates package install scripts. esbuild/lmdb/msgpackr-extract/@parcel/watcher
were approved via
npm install-scripts approve <pkg>(required for builds). If a freshnpm installbreaks the build with esbuild/lmdb errors, re-approve. ng new --directory /abs/pathwas interpreted relative to CWD once — check where the scaffold actually landed.angular-cliMCP:list_projectsis broken (schema error);run_target(build/test) anddevserver_*work fine.- No ESLint in the scaffold — there is no
linttarget. Phase 4's "ng lint" needsng add @angular-eslint(or similar) first. - Headless verification trick (works, used in Phase 2):
google-chrome --headless --no-sandbox --virtual-time-budget=12000 --dump-dom http://localhost:4200/then grep the DOM./usr/bin/google-chromeavailable. - Dev server:
npm start(or MCP devserver_start) → port 4200 (pinned in angular.json). Backend runs at :5193 with test unitexample-sleep.service(long-running, use for live start/stop tests; starting it blocks the blocking endpoint up to 600 s). - API base URL: derived from the page origin in production (
${window.location.origin}/api, same-origin, no CORS); development build swaps inhttp://localhost:5193/apiviafileReplacements(dev → :4200 is cross-origin, so the backend must still send CORS headers). Verified 2026-09-20: prod bundle haslocation.originand no :5193 hardcode, dev bundle has the :5193 URL. - Bundle budget: initial 500 kB warning / 1 MB error (angular.json production). Phase 3 left it at 531 kB raw / 119 kB transfer — over the warning budget; watch the build output (lazy-loading the job-monitor feature is the likely fix if it matters).
- Interactive headless verification (used in Phase 3, beyond the dump-dom trick): launch
google-chrome --headless --no-sandbox --remote-debugging-port=9222 --user-data-dir=… about:blank, then a small Node script (Node 24 has built-inWebSocket+fetch) connects to the target fromhttp://127.0.0.1:9222/json, and drivesPage.navigate/Runtime.evaluateto click buttons and readinnerText, withNetwork.*/Log.*events for request/error capture. Works well for live SSE flows.
- Before writing markup for any
mat-*component, read its reference at.opencode/skills/angular-material/references/<name>.md— the skill SKILL.md index alone is not sufficient (Phase 2 initially builtmat-cardfrom plain<div>s for this reason; reworked 2026-09-14 ontomat-card-header/mat-card-content/mat-card-actions). AGENTS.mdat the repo root documents the local Angular/Angular Material skills and project conventions for fresh sessions.- Build mode: act without asking; verify with build + tests after changes.
- Service names carry
.servicesuffix — use verbatim, never strip/re-append. - Tone mappers must keep unknown-value fallback (D-Bus error names can appear in
SequenceFailureDto.result). - PLAN.md gets a
### Phase N — Implementation Notessubsection after each phase.