HOMELAB-1684: feat(cockpit-web): LD3.4 telemetry panes — derived-state visuals + available-now telemetry #585

Merged
aaron merged 5 commits from plane/HOMELAB-1684-derived-state into epic/LD3 2026-10-02 21:08:27 +00:00
Owner

What

LD3.4 — the derived-state vocabulary and the telemetry that exists today. 2 commits, lab-director/cockpit/web/** only — no Go, no ci.yaml, no manifests, no flags. Brief: .rpiv/artifacts/designs/ld3/cockpit-design-brief.md R12 (Routines/Alerts/Meeting/Costs are deferred — recorded as honest deferred states, not built).

1. feat(cockpit-web): the derived-state component system (4348525)

lib/components/state/: StateGlyph + DerivedStateRow + DerivedStateLegend, rendering §5.2's visual language (dim / amber / blue / hatch textures, duration labels, the gate a HELD row is held by with a link) as pure, unit-tested components.

  • unknown is the DEFAULT state on both components: a caller that forgets to derive a state renders the hatched idle one, never green.
  • pending_rule joins the vocabulary as the hardware-gated hole (§5.2's rule text, §6's ApexDirector hero): dashed mark, hatched, idle, never a tick. Deliberately not in DERIVED_STATE_ORDER — §5.2's four registry states keep the legend.
  • The names ⏸ ✋ ◐ ? are carried as data-glyph tokens; the marks are lucide strokes because U+23F8/U+270B paint tofu in Inter and the mono stack (verified in a real browser, LD3.2b).
  • The never-healthy rule is enforced three ways: a vocabulary test over every state · a guard (isHealthy) that takes its table as an argument so it can be tested against a mutated green-unknown table (a check that cannot fail is not a check) · the DOM ([data-derived-state][data-tone="ok"] must be empty, in e2e).
  • The dim moved off the word onto the aria-hidden mark: an ancestor's opacity is invisible to responsive.spec.ts's contrast read, so dimming the whole chip was a silent contrast regression under a passing test.

2. feat(cockpit-web): the telemetry panes … (74f6343)

  • Wave view (§4.2). wave-view.svelte + derived/sandbox-census.ts: rows keyed by the id a registry resolves, with role · model tier · derived-state column · cost column · ticket · LD3.3's attach cell imported, not duplicated. The row set's honesty is derived from the read model's own source rows as a pure function (the dispatcher decides; the bus alone cannot key a row set; an absent row is unwired, not a licence to draw rows). Unwired — today's truth — it prints ONE honest hatched row naming the census, never fake rows and never an empty table.
  • Absent means absent: model/cost/last beat print — with data-measured="false", never 0 and never $0.00.
  • Updates feed (§4.2). Only observed events: read-model ticks the SSE stream actually delivered (bounded local ring, untracked), queue rows waiting on the operator, and this session's own dispositions including refusals. The bus-digest half keeps its banner naming LD1.5b/HOMELAB-1788.
  • Overview counts. derived/fleet-counts.ts: gates-needing-the-operator from the queue's own items, class-E open from the census field named for it; both null unless a read backed them (a returned 0 stays a measurement; an absent one never becomes one).
  • Deferred states, named. Homelab's three infrastructure cells name LD4/observability as their source; the project views gain §6's skeleton (waves · sandbox envs · releases strip · gate markers) with every slot naming its source; ApexDirector's hardware-gated checks hero demonstrates pending_rule; R12's four deferred panes are recorded by name.

The one deviation to review

The honest hatch row renders under the table, not inside its body. The brief says the table renders one honest hatch row set. A nine-column table's min-content is ~500px, so inside the body the sentence was clipped at 390px and in the Fleet Overview's 517px card — the row's entire message was the one thing behind the scroller (screenshot 2 vs 3). Now: the header keeps the shape, the body is where a registry's rows go, and the single honest hatch row carries the glyph, the sentence and the disabled attach affordance at full width where no scroller can clip it. Decided by measuring the rendered page, not by taste.

Two smaller notes: data-value was added to StatCell so the absence rule can be asserted on the value (an unmeasured cell's explanatory sentence legitimately contains digits — "not 41/41", "LD4"); and attach-launcher.svelte's stale (LD3.4) pointer was deliberately not touched (LD3.3's file, its spec asserts the sentence, and this slice still ships no registry read — flagged for the landing review rather than changed under it).

Test plan

cd lab-director/cockpit/web
npx svelte-check --tsconfig ./tsconfig.json   → 0 errors, 0 warnings
npm run test                                  → 32 files passed (32), 277 tests passed (277)
                                                fixture parity guard: 13 tests collected
npm run build                                 → Wrote site to "build"  (exit 0)
npx playwright test                           → 74 passed (1.1m)

Counts, measured not inferred (base = 126af06, PR #584's tip, re-run in a clean worktree):

  • vitest 221 → 277 tests, 25 → 32 files
  • Playwright 62 → 74 specs (new e2e/telemetry.spec.ts = 12)
  • Each commit builds on its own: commit 1 alone typechecks 0/0 and runs 247/28.

New coverage by layer: the vocabulary (7) · StateGlyph (9) · DerivedStateRow (10) · the census verdict (6) · the fleet counts and their absence cases (8) · the wave view incl. the registry-shape path (8) · the updates feed (8) · e2e (12).

The e2e assertions that matter structurally, not textually:

  • no unmeasured VALUE contains a numeral ([data-value] under [data-measured="false"]) — asserted against the live unwired binary and against a route that fulfils the overview with 0s while claiming unwired (the P-COCKPIT-005 regression);
  • no derived-state row carries the healthy tone, and [data-derived-state="unknown"][data-texture="hatch"] is exactly 1;
  • no $ and no standalone 0 in the unwired cards;
  • one wave-row, hatched, its note naming the census — never fabricated rows;
  • the honest row's sentence is geometrically inside the pane at 390px.

Existing specs change only where the UI deliberately did: the derived-state testid now belongs to StateGlyph (the counts are unchanged — 5 glyphs in the fleet pane), and the census-unwired sentence is single-sourced from derived/sandbox-census.ts.

Screenshots

/home/aaron/repos/homelab/.rpiv/artifacts/build/ld3-1684-screenshots/

file what
01-overview-1440.png / 01-overview-390.png the three card groups: absence not zero, the observed tick feed, the named deferred sources
02-wave-view-1440.png / 02-wave-view-390.png the wave header (no wave / care dial = honest hatch), the table, the state vocabulary, the honest row
03-project-apex-1440.png / 03-project-apex-390.png ApexDirector: the pending_rule hole, the §6 skeleton slots, R12's deferred panes

Stacking (for the landing worker)

This branch is stacked on PR #584 (plane/HOMELAB-1683-attach-ui, 126af06) because it imports that slice's attach cell rather than duplicating it. #584 was still OPEN (green, mergeable, under independent review) when this branch was cut, so the diff against epic/LD3 currently includes #584's commits — this slice's delta is 4348525..74f6343. Rebase onto epic/LD3 once #584 lands and the diff becomes the delta alone. Orchestrator notified via the session bus at cut time.

## What **LD3.4 — the derived-state vocabulary and the telemetry that exists today.** 2 commits, `lab-director/cockpit/web/**` only — no Go, no `ci.yaml`, no manifests, no flags. Brief: `.rpiv/artifacts/designs/ld3/cockpit-design-brief.md` **R12** (Routines/Alerts/Meeting/Costs are **deferred** — recorded as honest deferred states, not built). ### 1. `feat(cockpit-web): the derived-state component system` (`4348525`) `lib/components/state/`: `StateGlyph` + `DerivedStateRow` + `DerivedStateLegend`, rendering §5.2's visual language (dim / amber / blue / hatch textures, duration labels, the gate a `HELD` row is held by with a link) as pure, unit-tested components. - **`unknown` is the DEFAULT state** on both components: a caller that forgets to derive a state renders the hatched idle one, never green. - **`pending_rule`** joins the vocabulary as the hardware-gated hole (§5.2's rule text, §6's ApexDirector hero): dashed mark, hatched, idle, never a tick. Deliberately **not** in `DERIVED_STATE_ORDER` — §5.2's four registry states keep the legend. - The names ⏸ ✋ ◐ ? are carried as `data-glyph` tokens; the **marks are lucide strokes** because U+23F8/U+270B paint tofu in Inter *and* the mono stack (verified in a real browser, LD3.2b). - The never-healthy rule is enforced three ways: a vocabulary test over every state · a guard (`isHealthy`) that takes its table as an argument so it can be tested against a **mutated green-`unknown`** table (a check that cannot fail is not a check) · the DOM (`[data-derived-state][data-tone="ok"]` must be empty, in e2e). - The dim moved **off the word onto the `aria-hidden` mark**: an ancestor's `opacity` is invisible to `responsive.spec.ts`'s contrast read, so dimming the whole chip was a silent contrast regression under a passing test. ### 2. `feat(cockpit-web): the telemetry panes …` (`74f6343`) - **Wave view (§4.2).** `wave-view.svelte` + `derived/sandbox-census.ts`: rows keyed by the id a **registry** resolves, with role · model tier · derived-state column · cost column · ticket · **LD3.3's attach cell imported, not duplicated**. The row set's honesty is derived from the read model's own source rows as a pure function (the dispatcher decides; the bus alone cannot key a row set; an *absent* row is unwired, not a licence to draw rows). Unwired — today's truth — it prints **ONE honest hatched row naming the census**, never fake rows and never an empty table. - **Absent means absent**: model/cost/last beat print `—` with `data-measured="false"`, never `0` and never `$0.00`. - **Updates feed (§4.2).** Only observed events: read-model ticks the SSE stream actually delivered (bounded local ring, `untrack`ed), queue rows waiting on the operator, and this session's own dispositions including refusals. The bus-digest half keeps its banner naming LD1.5b/HOMELAB-1788. - **Overview counts.** `derived/fleet-counts.ts`: gates-needing-the-operator from the queue's own items, class-E open from the census field named for it; both `null` unless a read backed them (a returned `0` stays a measurement; an absent one never becomes one). - **Deferred states, named.** Homelab's three infrastructure cells name LD4/observability as their source; the project views gain §6's skeleton (waves · sandbox envs · releases strip · gate markers) with every slot naming its source; ApexDirector's `hardware-gated checks` hero demonstrates `pending_rule`; **R12's four deferred panes are recorded by name**. ## The one deviation to review **The honest hatch row renders under the table, not inside its body.** The brief says the table renders one honest hatch row set. A nine-column table's min-content is ~500px, so inside the body the sentence was clipped **at 390px and in the Fleet Overview's 517px card** — the row's entire message was the one thing behind the scroller (screenshot 2 vs 3). Now: the header keeps the shape, the body is where a registry's rows go, and the single honest hatch row carries the glyph, the sentence and the disabled attach affordance at full width where no scroller can clip it. Decided by measuring the rendered page, not by taste. Two smaller notes: `data-value` was added to `StatCell` so the absence rule can be asserted on the **value** (an unmeasured cell's explanatory sentence legitimately contains digits — "not 41/41", "LD4"); and `attach-launcher.svelte`'s stale **`(LD3.4)`** pointer was deliberately **not** touched (LD3.3's file, its spec asserts the sentence, and this slice still ships no registry read — flagged for the landing review rather than changed under it). ## Test plan ``` cd lab-director/cockpit/web npx svelte-check --tsconfig ./tsconfig.json → 0 errors, 0 warnings npm run test → 32 files passed (32), 277 tests passed (277) fixture parity guard: 13 tests collected npm run build → Wrote site to "build" (exit 0) npx playwright test → 74 passed (1.1m) ``` **Counts, measured not inferred** (base = `126af06`, PR #584's tip, re-run in a clean worktree): - vitest **221 → 277** tests, **25 → 32** files - Playwright **62 → 74** specs (new `e2e/telemetry.spec.ts` = 12) - **Each commit builds on its own**: commit 1 alone typechecks 0/0 and runs 247/28. New coverage by layer: the vocabulary (7) · `StateGlyph` (9) · `DerivedStateRow` (10) · the census verdict (6) · the fleet counts and their absence cases (8) · the wave view incl. the registry-shape path (8) · the updates feed (8) · e2e (12). The e2e assertions that matter structurally, not textually: - **no unmeasured VALUE contains a numeral** (`[data-value]` under `[data-measured="false"]`) — asserted against the live unwired binary *and* against a route that fulfils the overview with `0`s while claiming `unwired` (the P-COCKPIT-005 regression); - **no derived-state row carries the healthy tone**, and `[data-derived-state="unknown"][data-texture="hatch"]` is exactly 1; - **no `$` and no standalone `0`** in the unwired cards; - **one** `wave-row`, hatched, its note naming the census — never fabricated rows; - the honest row's sentence is geometrically inside the pane at 390px. Existing specs change only where the UI deliberately did: the `derived-state` testid now belongs to `StateGlyph` (the counts are unchanged — 5 glyphs in the fleet pane), and the census-unwired sentence is single-sourced from `derived/sandbox-census.ts`. ## Screenshots `/home/aaron/repos/homelab/.rpiv/artifacts/build/ld3-1684-screenshots/` | file | what | |---|---| | `01-overview-1440.png` / `01-overview-390.png` | the three card groups: absence not zero, the observed tick feed, the named deferred sources | | `02-wave-view-1440.png` / `02-wave-view-390.png` | the wave header (no wave / care dial = honest hatch), the table, the state vocabulary, the honest row | | `03-project-apex-1440.png` / `03-project-apex-390.png` | ApexDirector: the `pending_rule` hole, the §6 skeleton slots, R12's deferred panes | ## Stacking (for the landing worker) This branch is **stacked on PR #584** (`plane/HOMELAB-1683-attach-ui`, `126af06`) because it *imports* that slice's attach cell rather than duplicating it. #584 was still OPEN (green, mergeable, under independent review) when this branch was cut, so the diff against `epic/LD3` currently includes #584's commits — **this slice's delta is `4348525..74f6343`**. Rebase onto `epic/LD3` once #584 lands and the diff becomes the delta alone. Orchestrator notified via the session bus at cut time.
HOMELAB-1683: feat(cockpit): LD3.3 sandbox attach bridge, built disabled
Some checks failed
0/0 projects applied successfully.
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 8s
Build Lab Director Cockpit Image / preflight (pull_request) Successful in 5s
CI Review / alert-receiver-test (pull_request) Successful in 20s
CI Review / dispatcher-test (pull_request) Has been cancelled
CI Review / cockpit-test (pull_request) Has been cancelled
CI Review / cockpit-e2e (pull_request) Has been cancelled
Build Lab Director Cockpit Image / build-and-push (pull_request) Has been cancelled
1e59acd576
Adds the attach bridge behind LD_ATTACH_ENABLED (default false): the
cockpit/** mechanism, the server wiring and the refusal matrix, with no
manifests, no RBAC, no CNP, no dispatcher change and no web/**.

attach/ (new):
- Registry seam (read-only interface, recording fixture, and the refusing
  unwired registry that names the unratified RBAC) — the live CRD reads
  wait for the operator's ratification package §4/§5.
- Executor seam + client-go impl: remotecommand.NewSPDYExecutor over
  pods/exec with Tty:true and a TerminalSizeQueue; construction reads a
  kubeconfig (LD_ATTACH_KUBECONFIG, empty = in-cluster) and contacts
  nothing.
- Refusal matrix with a P-ATT-001..008 policy book, the class harness
  allowlist (spawn's own class constants; unknown class refuses), and the
  ld.audit.attach/v1 writer (audit.Entry gains attach kinds, outcomes and
  fields; Validate enforces the schema/kind/outcome pairing both ways and
  keeps sentinel entries free of attach fields).
- Wire() is the one place a kube client can be constructed: disabled
  builds none (asserted by a construction counter), enabled builds one and
  refuses per attempt while the registry is unwired.

server/:
- GET /api/v1/attach/{sandbox}/info and WebSocket /ws/attach/{sandbox},
  registered outside RequireOperator so that a refused handshake is still
  a ledger entry; identity resolution, refusals and the WS protocol all
  audit (open/close/refused), statusLogger gains the Hijack passthrough a
  websocket upgrade needs through the chain.
- One attach_open is answered by exactly one attach_close (a failed dial
  writes the aborted close), and an attach never opens when its open entry
  cannot be written.

config: LD_ATTACH_ENABLED (default false) + LD_ATTACH_KUBECONFIG; a
kubeconfig configured while the flag is off is refused as ambiguous.
main: attach declared in the read-model census, in --check's OK lines and
in the boot log ("no kube client constructed").

Tests: 192 pass under -race (attach 32, audit 9, server 47, config 13 new
or extended); full refusal matrix, WS protocol over a real socket
(upgrade/resize/EOF both ways/abrupt disconnect/protocol error/slow
consumer), ledger pairing and entry JSON shape.
HOMELAB-1683: fix(cockpit): bound the attach socket read frame at the library, label the refusal log by policy
Some checks failed
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 6s
Build Lab Director Cockpit Image / preflight (pull_request) Successful in 4s
CI Review / alert-receiver-test (pull_request) Successful in 19s
Build Lab Director Cockpit Image / build-and-push (pull_request) Successful in 54s
CI Review / dispatcher-test (pull_request) Successful in 2m36s
CI Review / cockpit-test (pull_request) Successful in 2m49s
CI Review / cockpit-e2e (pull_request) Failing after 2m50s
f565272bf0
HOMELAB-1683: fix(cockpit): keep attach out of the read-model census until the pane slice owns its parity guard
Some checks failed
CI Review / helm-validate (pull_request) Successful in 9s
Build Lab Director Cockpit Image / preflight (pull_request) Successful in 8s
CI Review / receiver-test (pull_request) Successful in 9s
CI Review / alert-receiver-test (pull_request) Successful in 29s
Build Lab Director Cockpit Image / build-and-push (pull_request) Successful in 2m6s
CI Review / cockpit-test (pull_request) Failing after 2m43s
CI Review / dispatcher-test (pull_request) Successful in 2m58s
CI Review / cockpit-e2e (pull_request) Failing after 3m16s
e79ed6ff4b
HOMELAB-1683: ci(rerun): no code change — re-trigger cockpit jobs after environment flakes
All checks were successful
CI Review / receiver-test (pull_request) Successful in 6s
CI Review / helm-validate (pull_request) Successful in 9s
Build Lab Director Cockpit Image / preflight (pull_request) Successful in 4s
CI Review / alert-receiver-test (pull_request) Successful in 22s
Build Lab Director Cockpit Image / build-and-push (pull_request) Successful in 2m2s
CI Review / cockpit-e2e (pull_request) Successful in 2m17s
CI Review / cockpit-test (pull_request) Successful in 2m29s
CI Review / dispatcher-test (pull_request) Successful in 2m38s
0/0 projects applied successfully.
ca9022f338
Previous run (1715): cockpit-e2e failed only on the PWA service-worker
activation spec (a known flake on this base; the de-flake is 694e8a7 on the
in-flight three-panes branch) and cockpit-test failed in its npm step
(registry), while the same Go tree passed cockpit-test in run 1709 and
passes go test -race locally. Empty commit to re-run the matrix.
The frontend half of LD3.3 starts at the contract. Every shape in
types/attach.ts was read off the Go it mirrors (attach.Info,
attach.Server*Frame, attach.RefusalKind, server.attachRefusal) and then
VERIFIED by marshalling those structs and diffing the JSON — which is how
two things the Go doc comments get wrong were found:

  - attach.Term carries NO json tags, so the info payload's terminal object
    is {"Columns":120,"Rows":32} (PascalCase), NOT the cols/rows the comment
    above the type claims. normaliseTerm accepts both spellings so a future
    tag addition is a no-op rather than a silent 0x0 geometry.
  - class_without_tty is answered 200, not the refusal kind's own 409: the
    read succeeded, so a 4xx would tell the frontend its request was wrong.

attachApi.info therefore does NOT throw on non-2xx. The backend answers a
decidable refusal as HTTP so a browser's onerror and an operator's curl see
the same JSON — but a browser cannot read a failed WebSocket handshake's
status at all, which is why every 400/401/403/404/409/502/503 is a POSTURE
the button renders rather than a transport failure it retries.

xterm 6.0.0 and @xterm/addon-fit 0.11.0 are pinned exactly (both are
lazily imported, so they stay out of the home screen's first-paint chunk).
formatBytes joins the format helpers for the session status strip.
Three modules, and the split between them is the point.

protocol.ts is the wire grammar, pure: no socket, no store, no DOM. The
backend states it once (attach/protocol.go) and says why in the comment
above it — "the xterm.js pane is the other half of this contract, and a
protocol described in two places is a protocol with two dialects". The rule
the whole file hangs off: binary is data, text is control. A control frame
typed into the shell leaks the cockpit's bookkeeping; keystrokes parsed as
JSON are dropped on the floor. Both are one if-statement away.

Binary detection is STRUCTURAL (ArrayBuffer.isView plus a
Object.prototype.toString tag read) rather than `instanceof ArrayBuffer`,
because instanceof is realm-local and the suite proved it: under jsdom a
Uint8Array from TextEncoder failed that check, and a client that failed it
would print "a message of an unknown type" at an operator instead of their
shell.

posture.ts turns the info answer into the ONE thing a control may be
(enabled, or disabled WITH A REASON) and never guesses. The order of its
checks is load-bearing and was found by a failing test: the handler's
pre-bridge refusal body carries no exists/has_tty at all, so it normalises
to exists:false — and reading that first filed "you are not signed in" as
"unknown sandbox", telling an operator their sandbox does not exist when the
truth is that the cockpit never looked.

session.svelte.ts probes BEFORE it connects, and that is the contract, not
an optimisation. WebSocket.onerror carries no status and no body by spec, so
a client that opened first could not name the reason for the failure it will
hit most often (the flag is off -> 503). Probing first means the refusal is
rendered with no socket at all, no upgrade and no ledger open. The
socket-died-without-a-frame path re-probes and, when the two answers
disagree, says so rather than picking one. Reconnection is deliberately
absent: a PTY is not an SSE read model, and reconnecting would write a
SECOND open entry against the same operator and target.
The UI half: a per-row attach cell, a by-id launcher, an xterm terminal and
the Sandbox Environment View as a modal (design Part 2 §4.1 screen 4 — "what
is this agent doing, and let me type into it").

Three decisions worth a reviewer's attention:

  - The rows carry the affordance, and the LAUNCHER exists because the
    cockpit has no sandbox-list read at all: info is a lookup BY ID, and the
    read model's census names four sources, none of which enumerates
    sandboxes. Rows-only would have shipped an unreachable feature. The
    launcher is the same posture as every other seam here — declare what is
    missing, make what IS possible possible — and the empty row renders a
    disabled button WITH the reason rather than no button, because "the
    registry is not read" and "this sandbox cannot be attached" are
    different facts.

  - The attach column is SECOND, not last, and that was decided by
    measurement: at 1440px the Fleet Overview's middle column is ~520px while
    the table wants ~640px, so a trailing column put the operator's one
    action off the right edge on the screen it was built for.

  - The dialog draws NO terminal until a session has actually opened. A
    refusal renders the status alone; an empty xterm below it would read as a
    shell that has not printed yet, which is the dead pane R6 forbids. It is
    portalled to <body> (the LD3.2b containing-block lesson), and its height
    is the terminal's height — with no terminal it shrinks to its five lines
    of status instead of wasting the screen.

One measurement worth recording, because nothing in review would have found
it: the visually-hidden reason sentence is position:absolute with no
positioned ancestor, so it escaped the table's overflow-x-auto scroll
container and grew the DOCUMENT's scrollWidth to 575px at a 390px viewport
— the exact sideways-scrolling page the 3.1a responsive net exists to catch,
and it caught it. Anchoring the cell (position: relative) keeps the sentence
inside the column the scroller clips.
76 unit tests across five new files, plus 9 more in the existing parity
guard (145 -> 221 total, 20 -> 25 files).

The protocol and session suites run against a hand-written socket, which is
the ONLY way most of these branches can be reached: a refused WebSocket
handshake reports no status and no body by spec, so the pre-upgrade refusal
path cannot be exercised against a real server at all — and it is the path
the flag-off server takes on every single attempt. Every honesty guarantee
is asserted at the layer that owns it, including the ones a passing suite
would otherwise hide: an invalid resize is WITHHELD rather than sent (the
backend closes the session over it), a binary frame that happens to spell
valid JSON is still data, a control frame never reaches the terminal, a late
frame from a replaced socket is ignored, and a fresh session resets every
counter so no previous sandbox's bytes are rendered as this one's.

The parity guard is extended to the attach contract and reads the Go source,
so the fixtures cannot drift behind a stale copy. It pins the Info json tag
set, the Term struct's ABSENCE of tags (the PascalCase trap), the
RefusalKind vocabulary, the status map, the no-TTY 200 override, the
registry wiring list, the DefaultTerm geometry, the close-reason vocabulary,
and the two refusal sentences character for character.

That guard is not ceremony. Two of the failures it and the e2e suite caught
were fixtures asserting payloads the backend never sends: a wiring array on
the disabled answer (the flag-off refusal is raised with an EMPTY wiring
argument, so the key is named in the reason instead) and a paraphrase of the
no-TTY sentence. Both were green in isolation and wrong about the wire.
HOMELAB-1683: test(cockpit-web): attach e2e — three postures, the dialog, 390px
Some checks are pending
Plan in progress...
CI Review / receiver-test (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 9s
Build Lab Director Cockpit Image / preflight (pull_request) Successful in 6s
CI Review / alert-receiver-test (pull_request) Successful in 22s
Build Lab Director Cockpit Image / build-and-push (pull_request) Successful in 2m3s
CI Review / dispatcher-test (pull_request) Successful in 2m22s
CI Review / cockpit-test (pull_request) Successful in 2m39s
CI Review / cockpit-e2e (pull_request) Successful in 2m51s
126af06113
14 new Playwright specs (48 -> 62), against the real cockpit binary in dev
mode serving the real frontend build.

The split is forced by what this build can produce. LD_ATTACH_ENABLED is
false by design (the RBAC/CNP/arming delta is the operator's ratification
package), so the LIVE server answers 503 "attach disabled" on both
transports and records every attempt — the disabled posture is therefore
asserted completely end to end, including reading the response the page
actually received to check `disabled` and `audit_recorded` are true. An
attachable answer is impossible live, so the other two postures run with the
info payload fulfilled in the browser, from the same fixtures the Go tests
assert.

The most valuable spec is the one that follows the real refusal through the
frontend: a fulfilled "attachable" answer opens the dialog, the live server
then refuses the WebSocket upgrade, and the dialog has to land on an honest
end state — the disabled refusal, its policy line and "refused and recorded"
— with NO terminal element in the DOM. It is the R6 rule ("never a dead
black box") as an assertion rather than a promise, and it is also the path a
real operator hits whenever the registry moves under them.

The viewport guard from LD3.2b is parameterised rather than copied, so the
attach dialog — a second portal-to-body surface, mounted from inside a pane
nested under the sticky blurred header — gets the same measured
containing-block assertions at 1440 and 390, plus a structural check that
its parent really is <body>. Measured at the end of this branch: at 390px
the open dialog sits 675..836 inside an 844px viewport with its backdrop at
0..844, and the document scrollWidth is 390 against a 390px clientWidth.
The §5.2 vocabulary as a component pair, pure and unit-tested:

- `StateGlyph` draws one derived state: the mark (lucide strokes — the design's
  ⏸ and ✋ live in the emoji/symbol blocks and neither Inter nor the mono stack
  carries them, so the literal glyphs painted tofu in a real browser), the word,
  and the tone/texture the vocabulary declares.
- `DerivedStateRow` paints the ROW: §5.2's dim / amber / blue / hatch textures,
  the duration label ("parked 23m"), the gate a HELD row is held by (with a link
  when there is one), and the row's own sentence. Column-agnostic — the caller
  supplies its cells as `lead`/`trail` snippets — so the wave table, a future
  project wave list and a future firmware list share one visual language instead
  of three hand-rolled approximations of it.
- `unknown` is the DEFAULT state for both components: a caller that forgets to
  derive a state renders the hatched idle one, never green.
- `pending_rule` joins the vocabulary as the hardware-gated hole (§5.2's rule
  text, §6's ApexDirector hero card): a dashed mark, hatched, idle, never a
  tick. It is deliberately NOT in `DERIVED_STATE_ORDER` — §5.2's four
  registry-derivable states keep the legend unchanged.

The never-healthy rule is enforced three ways: a vocabulary test over every
state, a guard (`isHealthy`) that takes its table as an argument so the guard
itself can be tested against a MUTATED green-unknown table, and the dim moved
off the word onto the aria-hidden mark — an ancestor's opacity is invisible to
the responsive spec's contrast read, so dimming the whole chip was a silent
contrast regression hiding under a passing test.

`StatCell`'s value region gains `data-value`, so a spec can assert the
absence rule on the VALUE alone: an unmeasured cell's explanatory sentence may
legitimately contain digits ("not 41/41"), the value may not.
HOMELAB-1684: feat(cockpit-web): the telemetry panes (wave view, observed updates feed, hardened overview, project skeleton)
Some checks failed
Plan in progress...
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 7s
Build Lab Director Cockpit Image / preflight (pull_request) Successful in 5s
CI Review / alert-receiver-test (pull_request) Successful in 19s
CI Review / dispatcher-test (pull_request) Successful in 2m20s
CI Review / cockpit-test (pull_request) Successful in 2m35s
Build Lab Director Cockpit Image / build-and-push (pull_request) Successful in 2m31s
CI Review / cockpit-e2e (pull_request) Failing after 2m48s
74f6343ac4
R12's scope, built: the derived-state visuals and the telemetry that exists
today. Everything else is an honest deferred state naming its source. `web/**`
only — no Go, no manifests, no flags.

**The wave view** (design §4.2). The sandbox table becomes a real Wave view: a
wave header, then one row per sandbox environment keyed by the id a REGISTRY
resolves — role, model tier, derived-state column, cost column, ticket, and
LD3.3's attach cell imported rather than duplicated. `sandboxCensus(sources)`
derives the row set's honesty from the read model's own source rows as a pure
function: the dispatcher decides (it is the row that would enumerate
sandboxes), the bus alone cannot key a row set, and an ABSENT row is unwired
rather than a licence to draw rows. While it is unwired — today's truth — the
view prints ONE honest hatched row naming the census it waits for: never fake
rows, and never an empty table (which reads as "no sandbox is running", a
measurement nobody made).

Absent means absent: model, cost and last beat print `—` with
`data-measured="false"`, never 0 and never $0.00 — the Costs source (OTel
`gen_ai.usage` + the OmniRoute gateway) is LD4's, and "the fleet spent nothing"
is an affirmative claim.

The honest row is rendered under the table rather than inside its body, decided
by looking at the rendered page rather than the mock: the table's min-content
is ~500px, so at 390px and in the Fleet Overview's 517px card a row of
em-dashes plus a half-visible clause would have hidden the one thing that row
exists to say. The header keeps the shape; the body is where a registry's rows
go.

**The updates feed.** §4.2's digest stream, carrying only events the cockpit
OBSERVED: read-model ticks the SSE stream actually delivered (a bounded local
ring, `untrack`ed so the effect cannot loop), Gates Queue rows that are waiting
on the operator, and this session's own dispositions including refusals. An
unobserved feed renders a hatched "nothing has been observed yet" sentence
rather than an empty list that reads as a quiet fleet; the digest half that
needs the bus ledger (LD1.5b) keeps its banner naming HOMELAB-1788.

**The overview counts.** `derived/fleet-counts.ts` derives the two counts the
screen may show, with the precedence stated once and each chosen to match what
the number MEANS: gates-needing-the-operator comes from the queue's own items
(a row needs you when it carries a decision package), class-E open comes from
the census field named for it. Both are null unless a read backed them — a
returned 0 stays a measurement, an absent one never becomes one.

**The deferred states.** The three homelab cells name their future source
(LD4/observability: the GitOps pins, kube-state-metrics, the resource budget
table) instead of being anonymous holes; the project views gain §6's skeleton
(waves · sandbox environments · releases strip · gate markers) with every slot
naming its source, and ApexDirector's hardware-gated hero demonstrates the
`pending_rule` hole where §6 puts it; R12's four deferred panes (Routines /
Alerts / Meeting / Costs) are recorded by name so their absence is a plan.

Tests: 221 → 277 vitest tests (25 → 32 files); e2e 62 → 74, with a new
`telemetry.spec.ts` asserting ONE honest wave row (never fabricated), the
absence rule on every `data-value` region, no `$` or standalone `0` in the
unwired cards, the never-healthy rule in the DOM, and the honest row's
sentence fully readable at 390px. The two existing specs change only where the
UI deliberately did: the `derived-state` testid now belongs to `StateGlyph`,
and the census-unwired sentence is single-sourced from
`derived/sandbox-census.ts`.
Merge remote-tracking branch 'origin/epic/LD3' into plane/HOMELAB-1684-derived-state
All checks were successful
CI Review / receiver-test (pull_request) Successful in 6s
CI Review / helm-validate (pull_request) Successful in 6s
Build Lab Director Cockpit Image / preflight (pull_request) Successful in 3s
CI Review / alert-receiver-test (pull_request) Successful in 19s
CI Review / dispatcher-test (pull_request) Successful in 2m21s
Build Lab Director Cockpit Image / build-and-push (pull_request) Successful in 2m8s
CI Review / cockpit-test (pull_request) Successful in 2m37s
CI Review / cockpit-e2e (pull_request) Successful in 2m56s
e15f32ab74
Author
Owner

Review — e15f32a · honesty lens (R4 / R12)

verdict: blocker

Fresh-context reviewer (deepseek/deepseek-flash, pi -p --no-session, primary sources only: the diff, R4/R12, design §5.2), then independently re-verified by the landing worker. Base origin/epic/LD3 (76625d2); delta = 26 files, all lab-director/cockpit/web/** (verified). Local at the head: svelte-check 0/0, vitest 33 files / 305 tests green, Playwright 76 passed.

BLOCKER B1 — the never-healthy guard does not cover the paint. components/state/state-glyph.svelte:67-74 hand-copies tone→class in a local tones map, while data-tone comes from the vocabulary, so the guard asserts the name of the tone and never the paint. One-word mutation (idle: 'text-ink-faint' → 'text-status-ok') makes unknown/PARKED/pending_rule render --color-status-ok (green, hue 158) and the whole suite stays green.

Independently reproduced by the landing worker in a scratch tree at e15f32a:

  • mutation B (paint): 33 files / 305 tests passed, 0 failed.
  • mutation A (vocabulary, DERIVED_STATE.unknown.tone: 'idle'→'ok'): 4 files / 8 tests FAIL, exit 1.

So the vocabulary leg is non-vacuous (as claimed) and the paint leg has no coverage at all — grep -rn status-ok src e2e finds no spec asserting a colour class, and isHealthy is referenced only from tests, so nothing at runtime refuses a healthy state. R4/§5.2 says what is rendered may not read healthy; the e2e guard asserts an attribute, not a pixel. The shipped bytes are honest (measured: unknown/PARKED paint oklch(0.65 0.012 272), HELD 78°, DONE-UNSEEN 232° — no green), so this is a guard hole, not a live lie. Related, same family: mutating SOURCE_STATE.unknown.tone: 'idle'→'ok' (rendered at dashboard-pane.svelte:97,332,392, gates-pane.svelte:49) also leaves 305/305 green.
Cheapest closers: assert the class in state-glyph.test.ts for every state (not.toContain('text-status-ok')), or derive tones from DERIVED_STATE[*].tone through one enumerable TONE_CLASS map that a spec walks against the vocabulary.

CONCERN C1 — wave-view.svelte:176 renders {#each sandboxes} without consulting census.wired, and wave-view.test.ts:92-110 renders a PARKED row under an unwired dispatcher: "unwired" is not structurally distinguishable from "rows exist". Production is correct today (dashboard-pane.svelte:375 passes no rows).
CONCERN C2 — stale (LD3.4) attribution in attach/attach-launcher.svelte:85 (also :19, attach-cell.svelte:62) now contradicts the new honest row's sentence; flagged by the builder but with no follow-up ticket. 2-line fix, its own spec asserts the sentence.
CONCERN C3 — dashboard-pane.svelte:450-452 can print 0 / 0 not configured before the first read (a numeral with no read behind it), and the slice's own no-zero e2e probes are scoped to the other two card groups. The landing worker could not reproduce the first paint empirically (the 1s read-model tick supplies the 4 sources first; the observed paint was 4 / 4 not configured), so this is a sub-second window found by reading the template.
CONCERN C4 — fleet-counts.ts:42 outcomes is dead; gates.svelte.ts decide() does not re-read the queue, so "gates needing you" keeps counting a gate the operator just ratified.

Verified clean: no fabricated number anywhere (the only reachable 0 is a queue that was read and holds nothing); the census verdict is a pure function of the read model's own rows (absent/error/bus-alone → unwired) and production prints exactly ONE hatched row naming the census; screens 6–9 are absent (no stub, no mock) and recorded by name in the UI and on HOMELAB-1684; no existing spec weakened (the panes.spec.ts delta is one hunk and it strengthened); delta is web-only.

Claim honesty: the "enforced three ways" sentence is literally true but overstates DOM enforcement (attribute, not paint — B1); "the row set's honesty is derived … as a pure function" is true of the sentence and the wired flag, not of the row set (C1).
Not verified by the reviewer: Playwright (the landing worker ran it: 76 passed).

## Review — e15f32a · honesty lens (R4 / R12) verdict: blocker Fresh-context reviewer (`deepseek/deepseek-flash`, `pi -p --no-session`, primary sources only: the diff, R4/R12, design §5.2), then independently re-verified by the landing worker. Base `origin/epic/LD3` (`76625d2`); delta = 26 files, all `lab-director/cockpit/web/**` (verified). Local at the head: `svelte-check` 0/0, vitest **33 files / 305 tests** green, Playwright **76 passed**. **BLOCKER B1 — the never-healthy guard does not cover the paint.** `components/state/state-glyph.svelte:67-74` hand-copies tone→class in a local `tones` map, while `data-tone` comes from the vocabulary, so the guard asserts the *name* of the tone and never the *paint*. One-word mutation (`idle: 'text-ink-faint'` → `'text-status-ok'`) makes `unknown`/`PARKED`/`pending_rule` render `--color-status-ok` (green, hue 158) and the whole suite stays green. Independently reproduced by the landing worker in a scratch tree at `e15f32a`: - mutation B (paint): `33 files / 305 tests passed`, 0 failed. - mutation A (vocabulary, `DERIVED_STATE.unknown.tone: 'idle'→'ok'`): **4 files / 8 tests FAIL**, exit 1. So the vocabulary leg is non-vacuous (as claimed) and the paint leg has no coverage at all — `grep -rn status-ok src e2e` finds no spec asserting a colour class, and `isHealthy` is referenced only from tests, so nothing at runtime refuses a healthy state. R4/§5.2 says what is *rendered* may not read healthy; the e2e guard asserts an attribute, not a pixel. The shipped bytes are honest (measured: `unknown`/`PARKED` paint `oklch(0.65 0.012 272)`, `HELD` `78°`, `DONE-UNSEEN` `232°` — no green), so this is a guard hole, not a live lie. Related, same family: mutating `SOURCE_STATE.unknown.tone: 'idle'→'ok'` (rendered at `dashboard-pane.svelte:97,332,392`, `gates-pane.svelte:49`) also leaves 305/305 green. Cheapest closers: assert the class in `state-glyph.test.ts` for every state (`not.toContain('text-status-ok')`), or derive `tones` from `DERIVED_STATE[*].tone` through one enumerable `TONE_CLASS` map that a spec walks against the vocabulary. **CONCERN C1** — `wave-view.svelte:176` renders `{#each sandboxes}` without consulting `census.wired`, and `wave-view.test.ts:92-110` renders a `PARKED` row under an unwired dispatcher: "unwired" is not structurally distinguishable from "rows exist". Production is correct today (`dashboard-pane.svelte:375` passes no rows). **CONCERN C2** — stale `(LD3.4)` attribution in `attach/attach-launcher.svelte:85` (also `:19`, `attach-cell.svelte:62`) now contradicts the new honest row's sentence; flagged by the builder but with no follow-up ticket. 2-line fix, its own spec asserts the sentence. **CONCERN C3** — `dashboard-pane.svelte:450-452` can print `0 / 0 not configured` before the first read (a numeral with no read behind it), and the slice's own no-zero e2e probes are scoped to the other two card groups. The landing worker could not reproduce the first paint empirically (the 1s read-model tick supplies the 4 sources first; the observed paint was `4 / 4 not configured`), so this is a sub-second window found by reading the template. **CONCERN C4** — `fleet-counts.ts:42` `outcomes` is dead; `gates.svelte.ts` `decide()` does not re-read the queue, so "gates needing you" keeps counting a gate the operator just ratified. **Verified clean:** no fabricated number anywhere (the only reachable `0` is a queue that was read and holds nothing); the census verdict is a pure function of the read model's own rows (absent/error/bus-alone → unwired) and production prints exactly ONE hatched row naming the census; screens 6–9 are absent (no stub, no mock) and recorded by name in the UI and on HOMELAB-1684; no existing spec weakened (the `panes.spec.ts` delta is one hunk and it strengthened); delta is web-only. **Claim honesty:** the "enforced three ways" sentence is literally true but overstates DOM enforcement (attribute, not paint — B1); "the row set's honesty is derived … as a pure function" is true of the sentence and the `wired` flag, not of the row set (C1). **Not verified by the reviewer:** Playwright (the landing worker ran it: 76 passed).
Author
Owner

Review — e15f32a · craft / regression lens

verdict: pass (approve-with-concerns)

Fresh-context reviewer (deepseek/deepseek-flash, pi -p --no-session), no BLOCKERs. Measured at the head itself: vitest 33 files / 305 tests, Playwright 76 passed, svelte-check 0/0; each authored commit builds on its own; the PR's own base numbers (221→277, 25→32 files, 62→74) are honest for the base it names (126af06) — the higher figure here is the #584 merge.

Clean: the components/state/* pair is pure and prop-driven, defaults to unknown on both components, and is colour-independent (each state carries its label + data-glyph + an accessible title, not a hue alone). The attach cell is imported from #584, not duplicated — no second attach affordance or duplicated refusal/posture logic in the delta. No existing spec was weakened, deleted, skipped or made vacuous (the deleted derived-state-cell.svelte / wave-table.svelte lose no behaviour that is not carried by the new pair + wave-view). No page-level overflow at 1440 or 390.

CONCERNs (no merge impact): C1 — the wave card scrolls horizontally at every width (measured: table 576px inside a 517px body at 1440, 356px at 390), so the builder's "inside the body it clipped" justification is only half the story; C2 — the one reachable row hardcodes the vocabulary (wave-view.svelte:236-241 literal data-derived-state/data-tone/texture + hatch class) instead of using DerivedStateRow, a second source of truth; C3 — the legend's meanings truncate at 390px; C4 — a second definition of "needs a decision package" (fleet-counts.ts isDecidable vs the gates read's own notion); C5 — the digest status is triple-stated with one sentence duplicated across two files. Plus 8 SUGGESTIONs. The pending_rule hero in project-switcher.svelte renders the dashed hole correctly but does not carry data-derived-state (confirmed by DOM probe) — consistent with C4/C2's "hardcodes the vocabulary" theme.

Report: .rpiv/artifacts/build/ld3-1684-review-craft.md.

## Review — e15f32a · craft / regression lens verdict: pass (approve-with-concerns) Fresh-context reviewer (`deepseek/deepseek-flash`, `pi -p --no-session`), no BLOCKERs. Measured at the head itself: vitest **33 files / 305 tests**, Playwright **76 passed**, `svelte-check` 0/0; each authored commit builds on its own; the PR's own base numbers (221→277, 25→32 files, 62→74) are honest for the base it names (`126af06`) — the higher figure here is the #584 merge. **Clean:** the `components/state/*` pair is pure and prop-driven, defaults to `unknown` on both components, and is colour-independent (each state carries its label + `data-glyph` + an accessible title, not a hue alone). The attach cell is **imported** from #584, not duplicated — no second attach affordance or duplicated refusal/posture logic in the delta. No existing spec was weakened, deleted, skipped or made vacuous (the deleted `derived-state-cell.svelte` / `wave-table.svelte` lose no behaviour that is not carried by the new pair + `wave-view`). No page-level overflow at 1440 or 390. **CONCERNs (no merge impact):** C1 — the wave card scrolls horizontally at *every* width (measured: table 576px inside a 517px body at 1440, 356px at 390), so the builder's "inside the body it clipped" justification is only half the story; C2 — the one reachable row hardcodes the vocabulary (`wave-view.svelte:236-241` literal `data-derived-state`/`data-tone`/`texture` + `hatch` class) instead of using `DerivedStateRow`, a second source of truth; C3 — the legend's meanings truncate at 390px; C4 — a second definition of "needs a decision package" (`fleet-counts.ts` `isDecidable` vs the gates read's own notion); C5 — the digest status is triple-stated with one sentence duplicated across two files. Plus 8 SUGGESTIONs. The `pending_rule` hero in `project-switcher.svelte` renders the dashed hole correctly but does **not** carry `data-derived-state` (confirmed by DOM probe) — consistent with C4/C2's "hardcodes the vocabulary" theme. Report: `.rpiv/artifacts/build/ld3-1684-review-craft.md`.
Author
Owner

Landing — e15f32a · scope correction, class-D visual, merge decision (NOT merged)

Stale base corrected at the start of this pass. The branch was cut from 126af06 — before d581628, the commit that closed PR #584's four review blockers — so the diff against epic/LD3 was showing #584's fix reverted (13 files, 1180 insertions, 1504 deletions of noise). Merged origin/epic/LD3 (76625d2) into the branch, clean, zero conflicts, zero file overlap between the 3.4 delta and d581628; pushed as e15f32a. The PR diff is now the slice's real delta: 26 files, web-only, and #584's byte-accounting / refused-vs-unreachable / terminal-mount fixes are intact in the tree. Local at e15f32a: svelte-check 0/0, vitest 33/305, build OK, Playwright 76 passed; combined CI green (run 1742/1743).

Class-D visual (real binary, dev mode, nothing wired; scratch tree for a fixture-only route; 1440 + 390; screenshots + evidence.json in .rpiv/artifacts/build/ld3-1684-landing-screens/):

probe result
the four states + pending_rule as rows (fixture-forced) distinct and non-hue-only: ⏸ dim 272°, ✋ amber 78°, ◐ blue 232°, ? hatch idle, ◌ dashed hatch — no green anywhere (green is 158°)
wave view as shipped wave header + CARE DIAL hatched, ONE hatched row: ? unknown — sandbox census unwired — the registry arrives with the custodian census sources (dispatcher + bus)., disabled attach + no id
Fleet Overview homelab cards no source / not measured with the future source named; no $, no fabricated 0; unmeasured numerals: []; updates feed shows only real read-model ticks; SOURCES 4 / 4 not configured from a real read
ApexDirector §6 pending rule hero (dashed hole, never a tick), skeleton slots naming their sources, R12's screens 6–9 listed by name — no stub, no pretend data
390 px no page overflow on any of the 8 page/viewport combinations; the honest row sits L17 R373 inside 390 — readable, not behind the scroller
hatch legibility the hatch texture + the honest sentence are legible at 2× on both widths

Merge: refused — BLOCKER B1 is open (see the honesty verdict). The defect is a guard hole, not a rendering lie: the shipped bytes paint honestly, but a one-word change to the tone→class map turns unknown green with 305/305 tests green, which is exactly the structural requirement this slice exists to satisfy. Per the landing rule, a blocker verdict after the head blocks the merge; the wiring is otherwise sound and CI is green.

Not merged; no GitOps action taken. Next action for the orchestrator: one fix worker on e15f32a to close B1 (assert the class per state, or derive the tone classes from DERIVED_STATE through one enumerable map that a spec walks) and, cheaply, C2 (the stale (LD3.4) pointer) and C3 (0 / 0 not configured); C1/C4 may be deferred to a follow-up. Then a fresh-context delta review → merge → GitOps (ships in the next image build; nothing deploys — the cockpit image workflow is live-gated).

## Landing — e15f32a · scope correction, class-D visual, merge decision (NOT merged) **Stale base corrected at the start of this pass.** The branch was cut from `126af06` — before `d581628`, the commit that closed PR #584's four review blockers — so the diff against `epic/LD3` was showing #584's fix *reverted* (13 files, 1180 insertions, 1504 deletions of noise). Merged `origin/epic/LD3` (`76625d2`) into the branch, clean, zero conflicts, zero file overlap between the 3.4 delta and `d581628`; pushed as **`e15f32a`**. The PR diff is now the slice's real delta: **26 files, web-only**, and #584's byte-accounting / refused-vs-unreachable / terminal-mount fixes are intact in the tree. Local at `e15f32a`: `svelte-check` 0/0, vitest 33/305, build OK, Playwright 76 passed; **combined CI green** (run 1742/1743). **Class-D visual** (real binary, dev mode, nothing wired; scratch tree for a fixture-only route; 1440 + 390; screenshots + `evidence.json` in `.rpiv/artifacts/build/ld3-1684-landing-screens/`): | probe | result | |---|---| | the four states + `pending_rule` as rows (fixture-forced) | distinct and non-hue-only: ⏸ dim `272°`, ✋ amber `78°`, ◐ blue `232°`, `?` hatch idle, `◌` dashed hatch — **no green anywhere** (green is `158°`) | | wave view as shipped | wave header + `CARE DIAL` hatched, ONE hatched row: `? unknown — sandbox census unwired — the registry arrives with the custodian census sources (dispatcher + bus).`, disabled attach + `no id` | | Fleet Overview | homelab cards `no source` / `not measured` with the future source named; no `$`, no fabricated `0`; `unmeasured numerals: []`; updates feed shows only real read-model ticks; `SOURCES 4 / 4 not configured` from a real read | | ApexDirector §6 | `pending rule` hero (dashed hole, never a tick), skeleton slots naming their sources, **R12's screens 6–9 listed by name** — no stub, no pretend data | | 390 px | no page overflow on any of the 8 page/viewport combinations; the honest row sits `L17 R373` inside 390 — readable, not behind the scroller | | hatch legibility | the hatch texture + the honest sentence are legible at 2× on both widths | **Merge: refused — BLOCKER B1 is open** (see the honesty verdict). The defect is a guard hole, not a rendering lie: the shipped bytes paint honestly, but a one-word change to the tone→class map turns `unknown` green with 305/305 tests green, which is exactly the structural requirement this slice exists to satisfy. Per the landing rule, a blocker verdict after the head blocks the merge; the wiring is otherwise sound and CI is green. **Not merged; no GitOps action taken.** Next action for the orchestrator: one fix worker on `e15f32a` to close B1 (assert the class per state, or derive the tone classes from `DERIVED_STATE` through one enumerable map that a spec walks) and, cheaply, C2 (the stale `(LD3.4)` pointer) and C3 (`0 / 0 not configured`); C1/C4 may be deferred to a follow-up. Then a fresh-context delta review → merge → GitOps (ships in the next image build; nothing deploys — the cockpit image workflow is live-gated).
HOMELAB-1684: fix(cockpit-web): never-healthy paint guard, deferred-pointer, absent numerals (review)
Some checks failed
CI Review / receiver-test (pull_request) Successful in 4s
CI Review / helm-validate (pull_request) Successful in 7s
Build Lab Director Cockpit Image / preflight (pull_request) Successful in 3s
CI Review / alert-receiver-test (pull_request) Successful in 20s
Build Lab Director Cockpit Image / build-and-push (pull_request) Successful in 1m33s
CI Review / dispatcher-test (pull_request) Successful in 2m44s
CI Review / cockpit-test (pull_request) Has been cancelled
CI Review / cockpit-e2e (pull_request) Has been cancelled
cbc11cecd0
HOMELAB-1684: style(cockpit-web): wrap the honest-state import, name the paint leg in the glyph spec (review)
All checks were successful
CI Review / receiver-test (pull_request) Successful in 8s
CI Review / helm-validate (pull_request) Successful in 9s
Build Lab Director Cockpit Image / preflight (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 23s
Build Lab Director Cockpit Image / build-and-push (pull_request) Successful in 2m36s
CI Review / dispatcher-test (pull_request) Successful in 4m37s
CI Review / cockpit-test (pull_request) Successful in 4m37s
CI Review / cockpit-e2e (pull_request) Successful in 5m2s
9780a18b54
Author
Owner

Review — 9780a18 · delta review, fresh context (landing pass 2)

verdict: pass (approve-with-concerns)

Fresh-context reviewer (deepseek/deepseek-flash, pi -p --no-session, primary sources only: the delta, the prior verdicts' acceptance criteria; no builder summary, no implementer context). Repo read-only (git status --porcelain → empty). Delta scope confirmed mechanically: 13 files, 317+/35-, all lab-director/cockpit/web/**. Report: .rpiv/artifacts/build/ld3-1684-delta-review.md.
Baselines at 9780a18: unit 33 files / 312 tests (was 305 at e15f32a; +7) · e2e 77 passed (real Go binary, dev mode, prebuilt SPA).

B1 — CLOSED, and non-vacuous (the mutation was run, both by the reviewer and by me)

honest-state.ts: ABSENT_CLASS is a literal; DERIVED_TONE_CLASS is typed Record<Exclude<Tone,'ok'>, string> so ok is not a key; glyphPaint returns ABSENT_CLASS when texture === 'hatch' (unconditional — short-circuits before any tone lookup) or tone === 'ok'. state-glyph.svelte now has exactly one colour expression, used for both the mark and the word. Green is unreachable from any derived-state name, tone name or texture; data-tone is inert markup (no CSS selector paints on it).

mutation result
DERIVED_TONE_CLASS.idle → 'text-status-ok' (the B1 repro) RED — PAINTS what it declares… + the vocabulary spec
pre-fix component restored: local tones map with idle → green (literal pre-fix tree) RED — 2 failed
DERIVED_STATE.unknown.tone → 'ok' RED — 4 files / 8 tests
SOURCE_STATE.unknown.tone → 'ok' (stayed 305/305 green pre-fix) RED
sourcePaintTone: non-error → 'ok' RED, incl. the DOM chip test
ABSENT_CLASS → green then rebuild + e2e RED — e2e/telemetry.spec.ts:114
state-glyph.svelte reverted to a tones[view.tone] lookup, table unchanged GREEN — a no-op against the current table (identical classes), so it is a refactor, not a route to green. Not a hole.

C2 / C3 — correct, each pinned by a test that fails when reverted

C2: attach-launcher.svelte:86 / attach-cell.svelte:62 name the real source (derived/sandbox-census.ts, CENSUS_SOURCES = ['dispatcher','bus']); no LD3.4 left in rendered text. C3: the pre-read branch renders ? census not read with no numeral, verified live in a real browser (?/api/v1/overview + ?/api/v1/readmodel + ?/api/v1/events down) — see the landing comment. The existing no-numeral probe covers the region; forcing the pre-fix markup makes it report ["0 / 0 not configured"].

Regression scan

35 deleted lines audited — no assertion weakened, deleted or loosened; every removal is comment prose, the C2 rewrites, the prop rewires, the old local tones map or the old header markup. No new always-true assertion: the e2e paint probe asserts healthy !== '' and painted !== '' before comparing, and not.toBeNull() precedes the optional-chained reads. Nothing outside web/**.

CONCERN 1 — the new chip guard asserts the tone name, not the paint (the B1 shape, one hop out)

dashboard-pane.test.ts:127 asserts data-tone !== 'ok', and dashboard-pane.svelte:98-100 claims "only a configured source can render green". The chip's colour comes from a hand-copied map in shell/state-chip.svelte:25-32, untouched by this delta. Measured: mutating state-chip.svelte:29 idle → the green class leaves 312/312 GREEN — every unwired chip paints green and nothing fails. The narrow claim is true (SOURCE_STATE can no longer paint a chip green: mutation (c)/(h) RED); the "render green" half is unenforced.
Why not a blocker: the chip map must legitimately map ok→green for a genuinely configured chip, it predates this delta (no rendered behaviour regressed), B1's two named repros are closed, and the site is outside the frozen acceptance criteria. Recommended follow-up (one line of scope, same shape as this fix): assert the chip's class or computed colour, or drop "paints"/"render green" from that test's name and comment.

CONCERN 2 — same shape at shell/source-row.svelte:26 (mutate unknown → green: 312/312 GREEN; source-row.test.ts asserts only data-source/data-state/hatch/reason, never a colour). Pre-existing; raised because this delta's goal was to make the paint leg non-vacuous.
Finding 3 — the e2e paint probe covers one mark only (wave-row glyph); with DERIVED_TONE_CLASS.idle → green the e2e is 12/12 green while the unit suite is RED. The property is covered; the e2e header comment overstates the e2e probe's reach. Finding 4 — the C3 absent branch is not exercised by the e2e in dev-mode CI (it needs both the overview and the readmodel reads down); the real cover is the new non-vacuous unit test. Finding 5 — removing || tone === 'ok' fails via an undefined argument-type error rather than a paint message.

## Review — 9780a18 · delta review, fresh context (landing pass 2) verdict: pass (approve-with-concerns) Fresh-context reviewer (`deepseek/deepseek-flash`, `pi -p --no-session`, primary sources only: the delta, the prior verdicts' acceptance criteria; no builder summary, no implementer context). Repo read-only (`git status --porcelain` → empty). Delta scope confirmed mechanically: **13 files, 317+/35-, all `lab-director/cockpit/web/**`**. Report: `.rpiv/artifacts/build/ld3-1684-delta-review.md`. Baselines at `9780a18`: unit **33 files / 312 tests** (was 305 at `e15f32a`; +7) · e2e **77 passed** (real Go binary, dev mode, prebuilt SPA). ### B1 — CLOSED, and non-vacuous (the mutation was run, both by the reviewer and by me) `honest-state.ts`: `ABSENT_CLASS` is a literal; `DERIVED_TONE_CLASS` is typed `Record<Exclude<Tone,'ok'>, string>` so **`ok` is not a key**; `glyphPaint` returns `ABSENT_CLASS` when `texture === 'hatch'` (unconditional — short-circuits before any tone lookup) **or** `tone === 'ok'`. `state-glyph.svelte` now has exactly one colour expression, used for both the mark and the word. Green is unreachable from any derived-state name, tone name or texture; `data-tone` is inert markup (no CSS selector paints on it). | mutation | result | |---|---| | `DERIVED_TONE_CLASS.idle → 'text-status-ok'` (**the B1 repro**) | **RED** — `PAINTS what it declares…` + the vocabulary spec | | pre-fix component restored: local `tones` map with `idle → green` (**literal pre-fix tree**) | **RED** — 2 failed | | `DERIVED_STATE.unknown.tone → 'ok'` | **RED** — 4 files / 8 tests | | `SOURCE_STATE.unknown.tone → 'ok'` (**stayed 305/305 green pre-fix**) | **RED** | | `sourcePaintTone`: non-`error` → `'ok'` | **RED**, incl. the DOM chip test | | `ABSENT_CLASS → green` then rebuild + e2e | **RED** — `e2e/telemetry.spec.ts:114` | | `state-glyph.svelte` reverted to a `tones[view.tone]` lookup, table unchanged | GREEN — a no-op against the current table (identical classes), so it is a refactor, not a route to green. Not a hole. | ### C2 / C3 — correct, each pinned by a test that fails when reverted C2: `attach-launcher.svelte:86` / `attach-cell.svelte:62` name the real source (`derived/sandbox-census.ts`, `CENSUS_SOURCES = ['dispatcher','bus']`); no `LD3.4` left in rendered text. C3: the pre-read branch renders `? census not read` with **no numeral**, verified live in a real browser (`?/api/v1/overview` + `?/api/v1/readmodel` + `?/api/v1/events` down) — see the landing comment. The existing no-numeral probe covers the region; forcing the pre-fix markup makes it report `["0 / 0 not configured"]`. ### Regression scan 35 deleted lines audited — **no assertion weakened, deleted or loosened**; every removal is comment prose, the C2 rewrites, the prop rewires, the old local `tones` map or the old header markup. No new always-true assertion: the e2e paint probe asserts `healthy !== ''` and `painted !== ''` before comparing, and `not.toBeNull()` precedes the optional-chained reads. Nothing outside `web/**`. ### CONCERN 1 — the new chip guard asserts the tone *name*, not the *paint* (the B1 shape, one hop out) `dashboard-pane.test.ts:127` asserts `data-tone !== 'ok'`, and `dashboard-pane.svelte:98-100` claims "only a `configured` source can render green". The chip's colour comes from a hand-copied map in `shell/state-chip.svelte:25-32`, untouched by this delta. **Measured:** mutating `state-chip.svelte:29` `idle` → the green class leaves **312/312 GREEN** — every unwired chip paints green and nothing fails. The narrow claim is true (`SOURCE_STATE` can no longer paint a chip green: mutation `(c)`/`(h)` RED); the "render green" half is unenforced. **Why not a blocker:** the chip map must legitimately map `ok`→green for a genuinely `configured` chip, it predates this delta (no rendered behaviour regressed), B1's two named repros are closed, and the site is outside the frozen acceptance criteria. Recommended follow-up (one line of scope, same shape as this fix): assert the chip's class or computed colour, or drop "paints"/"render green" from that test's name and comment. **CONCERN 2** — same shape at `shell/source-row.svelte:26` (mutate `unknown` → green: **312/312 GREEN**; `source-row.test.ts` asserts only `data-source`/`data-state`/`hatch`/reason, never a colour). Pre-existing; raised because this delta's goal was to make the paint leg non-vacuous. **Finding 3** — the e2e paint probe covers one mark only (`wave-row` glyph); with `DERIVED_TONE_CLASS.idle → green` the e2e is 12/12 green while the unit suite is RED. The property is covered; the e2e header comment overstates the e2e probe's reach. **Finding 4** — the C3 absent branch is not exercised by the e2e in dev-mode CI (it needs both the overview and the readmodel reads down); the real cover is the new non-vacuous unit test. **Finding 5** — removing `|| tone === 'ok'` fails via an `undefined` argument-type error rather than a paint message.
Author
Owner

Landing — 9780a18 · visual spot-check + merge

Spot-check (real binary, dev mode, unwired; screenshots in .rpiv/artifacts/build/ld3-1684-landing2-screens/): the unknown-backed row/glyph does not render green — it paints oklch(0.65 0.012 272) against a measured healthy probe of oklch(0.76 0.115 158) (hue 158 = green), hatch texture, data-tone="idle", and the honest sentence naming the custodian census sources, at 1440 and 390; every derived glyph on the page is strictly non-green. The wiring header's absent branch renders ? census not read with no numeral (data-measured="false") when the census is unread — the 0 / 0 C3 window is gone, confirmed live.

## Landing — 9780a18 · visual spot-check + merge **Spot-check** (real binary, dev mode, unwired; screenshots in `.rpiv/artifacts/build/ld3-1684-landing2-screens/`): the unknown-backed row/glyph does **not** render green — it paints `oklch(0.65 0.012 272)` against a measured healthy probe of `oklch(0.76 0.115 158)` (hue 158 = green), hatch texture, `data-tone="idle"`, and the honest sentence naming the custodian census sources, at **1440 and 390**; every derived glyph on the page is strictly non-green. The wiring header's absent branch renders `? census not read` with **no numeral** (`data-measured="false"`) when the census is unread — the `0 / 0` C3 window is gone, confirmed live.
aaron merged commit 009c76b94f into epic/LD3 2026-10-02 21:08:27 +00:00
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!585
No description provided.