Skip to content

Latest commit

 

History

History
157 lines (141 loc) · 10.5 KB

STATE.md

File metadata and controls

157 lines (141 loc) · 10.5 KB

ServiceManager Frontend — Session State

Snapshot for resuming work in a fresh context. Read together with PLAN.md (PLAN.md also carries per-phase "Implementation Notes" subsections).

Status

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

What exists now (file map)

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).

Known debt (deferred when Phase 4 was closed as-is, 2026-09-20)

  • Unit tests: only the app.spec.ts smoke test exists. Not written: status.ts tone mappers (all states + all job results), parseSseFrame (synthetic frames, ordering quirk, partial chunks), services-api error mapping, and service-card/monitor-panel component 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 (backend Cors: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.service in the whitelist).
  • Polish: lazy-load MonitorPanelComponent via @defer (when session()) — bundle initial is 529 kB raw / 118.5 kB transfer, over the 500 kB warning budget (1 MB error clear); MatSidenavModule stays 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).

Backend quirks (observed live — keep when extending the SSE handling)

  • A state-changed frame can precede job-enqueued; a trailing state-changed with jobId:0 can 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 an error frame; disconnecting the stream does NOT cancel the job.

Angular 22 / Material 22 API facts (learned the hard way — do not re-discover)

  • resource() no longer accepts Observables. resource() takes a Promise loader or a streaming stream. For HttpClient observables use rxResource() from @angular/core/rxjs-interop: rxResource<T, R>({ stream: (params) => observable }).
    • R (request type) is the 2nd type arg — use void when there are no params.
    • RxResourceOptions has NO debugName (only core resource() 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)); on html { color-scheme: dark; }. All color tokens compile to light-dark(...) and follow the inherited color-scheme; html.theme-light { color-scheme: light; } flips the mode. Components bundle their own structural CSS — do NOT use the old mat.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(), host object, native control flow — as per best practices.
  • ng new v22 defaults to vitest; this project uses --test-runner=karma (Jasmine, per plan).

Environment / tooling quirks

  • 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 fresh npm install breaks the build with esbuild/lmdb errors, re-approve.
  • ng new --directory /abs/path was interpreted relative to CWD once — check where the scaffold actually landed.
  • angular-cli MCP: list_projects is broken (schema error); run_target (build/test) and devserver_* work fine.
  • No ESLint in the scaffold — there is no lint target. Phase 4's "ng lint" needs ng 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-chrome available.
  • Dev server: npm start (or MCP devserver_start) → port 4200 (pinned in angular.json). Backend runs at :5193 with test unit example-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 in http://localhost:5193/api via fileReplacements (dev → :4200 is cross-origin, so the backend must still send CORS headers). Verified 2026-09-20: prod bundle has location.origin and 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-in WebSocket + fetch) connects to the target from http://127.0.0.1:9222/json, and drives Page.navigate / Runtime.evaluate to click buttons and read innerText, with Network.* / Log.* events for request/error capture. Works well for live SSE flows.

Conventions / decisions in force

  • 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 built mat-card from plain <div>s for this reason; reworked 2026-09-14 onto mat-card-header/mat-card-content/mat-card-actions).
  • AGENTS.md at 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 .service suffix — 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 Notes subsection after each phase.