dev/cockpit → live: first dev-route feature (plain words, honest freshness, hardened reader) #857
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!857
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "dev/cockpit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Promotion of the first dev-route feature to live (INTENT:2). What the operator sees after this lands: plain fail-closed wording in the Waiting block; a plain sign-in failure page with a sign-in link (no more bare JSON), with login starts logged without values; a header that shows a recognizable identity and never the numeric account id; a version chip that never says dev; Fleet/Gates/Chat tabs and the STOP button off the page until they can act (one-line reverts); the Programs reader that degrades per field instead of blanking the page, carries long texts whole, never silently drops a Sendrast status, and treats a future published_at as stale; an all-clear only when the list state is nothing; snapshot-check CLI for the publisher's pre-flight.
Sandbox proof on the exact head by the dev-route gate (from the base's own copy): sandbox-proof success on
2c736a40— go vet, go test -race, svelte-check, vitest via the gate's own JSON report, fresh build, e2e in a real browser with the Programs and sign-in-failure specs required. One reviewer seat found a real regression (stale empty list shown with a false cause), fixed and verified by a fresh seat on this head. CI runs here on live by the operator's order.Verdict of record — ld-reviewer — head
4c4892b968: APPROVEmodel: omniroute/deepseek-flash
session: 01a1adce-4c18-73e0-bb86-00d2177332df
What I reviewed
The whole cockpit change going to live, at the exact head:
git diff fb8598c9..4c4892b9(merge-base three-dot; the only non-cockpit files inorigin/live..headare live's own newer gits-sbx gate work, and a local dry-run merge of this head intoorigin/livekeepsscripts/gits-sbx-epic-test.shuntouched and merges clean — this PR does not revert the gate script). I readauth/{loginpage.go,oidc.go,session.go,roles.go,middleware.go},programs/{snapshot.go,snapshot_file.go,source.go,types.go},cmd/snapshot-check,cmd/cockpit/main.go, and the shell/programs web changes plus their tests and e2e.I also re-ran the proof myself on the head in a clean worktree, not on trust:
go vet ./...clean;go test ./auth/... ./programs/... ./cmd/...ok;npm ci+vitest run432 passed / 11 skipped (37 files);svelte-check0 errors 0 warnings.Security
error_descriptionis escaped.loginFailureTemplateis anhtml/template,{{.}}renders as text, no<script>, no external resource (the inline<style>is authorized by the production CSP'sstyle-src 'self' 'unsafe-inline'; verified inserver/middleware.go).TestCallbackFailurePageEscapesTheIdPDescriptionis a real test: it asserts the payload is absent and that<script>…is visible to the reader.handleLoginlogs onlydev+request_id; the test binds a JSON logger and asserts the sealed state cookie's own value,nonce,sealed,subject,rolesandnextdo not appear.identityFromClaimsruns first and the sealedIdentity(Subject, Roles) is built from the verified ID token only;nameFromUserInfoadopts onlyName/Email/NamedByUserInfo, refuses asubthat differs from the token's, and fails open to an unnamed header (never to a failed login). Email is consumed nowhere except display andprincipalOf's fallback afterSubject. Timeout is bounded (userInfoTimeout, 5s) and tested against a hanging endpoint.subrequired at callback and atdecodeIdentity;OwnerForSubjectcompares exactly; unmapped/no-identity yieldsdecisions: nullandwaiting.state: not-connected. The display chain's changes are display-only.Correctness and wire-shape cross-check
I walked the shapes
programs/snapshot.go/source.gocan actually emit against every branch ofplain-language.ts; no mismatch found:not-connected+decisions:nullownListRead === falsestate:"nothing"state:"listed",stale:true→ stale branch before thenothingtestdecisionsComplete=false)CollectedOwnersemptied →not-connectedstate:"not-connected",decisionsfilteredpublished_at(>5m)StaleAttrue on every catalog row +waiting.staleServer side has no false all-clear path: the verdict requires
len(own)==0 AND mapped AND collected AND !stale, and the parser emptiesCollectedOwnerswhenever it had to leave an item out. The client independently requiresstate === "nothing", so neither end can grant it alone. Long text is carried verbatim (requiredTextnever trims into emptiness or truncates;TestSnapshotLongTextIsCarriedWhole), a Sendrast status with no epic is kept and named as a Problem, and the chip cannot stay green when the same file's waiting answer dates it old.Findings (none blocking)
auth/loginpage.go:73hardcodes "Reloading this page does not retry the sign-in." The template is also used byloginStartFailure(auth/oidc.go:315), which is served at/auth/login— there, reloading does start a new login, and the "Sign in again" link points at that same URL. Only reachable on an entropy/seal failure, but the sentence is the wrong one on that path.web/src/lib/programs/plain-language.ts:29still promises a missing file says "a written status file that hasn't arrived" — no string says that any more, and the new tests explicitly forbid "hasn't arrived"/"didn't arrive" because it claims a cause. The module header now contradicts its own behaviour.identity-chip.svelte:54tooltip says "signed in via the identity provider" for every named session, including a dev-mode one whose identity comes fromX-Cockpit-Dev-User(the dev badge beside it says exactly the opposite). Cannot ship on live (devis false there), but it is the component whose rule is "never say something untrue".programs/snapshot_file.go:78makes the constructor assign the package-globalSnapshotProblemSink. Documented, and the CLI resets it, but a constructor with a process-wide side effect means a second reader silently wins; a sink field on the reader would be local.identity-chip.test.ts:35has two fixture fields on one line (name_source: 'claim-name',then tab thenroles:), which looks like a botched edit. No prettier gate, so it survives; harmless.web/src/routes/unauthorized/+page.svelte:47-48still paints the raw numericsubject. It is not the header and predates this PR, but it now sits oddly beside the new "no numeric account id" rule — worth a follow-up seat.programs/snapshot.go:612: asendrastblock withstate:"ok"but no usableneeded_aaroncollapses the whole block to "not connected yet", dropping an epic/status the document did write. It is documented as a required measurement and the cause is recorded (never silent), so I am not asking for a change — just noting that "not connected yet" then describes a source the document says is connected.formatAgorenders a future instant as "just now", so a milestoneatahead of the clock reads "last updated just now"; document-level freshness now treats a futurepublished_atas stale, per-row instants do not. Cannot produce a green-over-stale chip (the chip follows the document verdict), but the row sentence is softer than the rule.On the skipped e2e blocks
The fleet/gates/chat/attach/telemetry/STOP blocks are
describe.skipped, not deleted, each with the reason and the one-line reactivation, and the panels stay mounted (panecount 4 asserted). That is defensible for an unreachable tab, and the removals are genuinely one-line reversible (PANE_ORDER,StopControlimport+mount,StopBannerimport+mount). The honest consequence: while those tabs are off, a browser regression in those panes is untested — so re-wiring a pane must un-skip its block in the same change.Tests
They prove behaviour rather than restate it: the escape test asserts both absence of the marker and visibility of the escaped text;
TestLoginStartLogsNoValuesextracts the actual state cookie value and greps for it;plainWaitinghas an iterated regression test acrosslisted/error/unknown states for the all-clear and for the stale-empty fix;TestSnapshotEmptyDecisionsIsNothingWaitingOnlyWhenFreshproves the server half of the same rule; the chip test opens the portal tooltip before asserting the subject is nowhere (so the number cannot hide one hover away); the dashboard/chip tests cover name-repeated-as-subject;snapshot-checkfixtures carry a sentinel word in an untouched field to prove no value is ever printed.No blocking findings. Approving.
Verdict of record — ld-reviewer — head
4c4892b968: APPROVEmodel: omniroute/deepseek-flash
session: 01a1adce-4c25-77d0-a478-0c6dc8744fb0
What I reviewed
The full change going to live:
git diff origin/live..4c4892b9, read as a promotion, not as a PR diff. I checked the merge effect separately, because the two-way diff shows the sbx-gits work as a deletion:git merge-tree origin/live 4c4892b9reports no conflicts, anddev/cockpittouches onlylab-director/**since the merge-basefb8598c9; the gits-sbx/examples scripts therefore stay at live's versions (PR #853) and are not reverted by this merge.Local verification on the exact head (worktree at
4c4892b9, not the shared checkout):go vet ./...clean;go test ./...all packages ok;go test -race ./auth/... ./programs/... ./cmd/snapshot-check/...ok (auth 7.1s).npm ci && npx vitest run: 432 passed | 11 skipped (443) — exactly the gate'sGATE_VITEST_MIN_TOTAL=443, and the fixture↔wire parity guard ran.npm run check(svelte-check): 0 errors, 0 warnings.npx playwright test --list --reporter=json: 85 tests, 50 skipped, 35 expected → above the gate floor (>=33) and under its ceiling (<=52), and the four required titles (three Programs +sign-in failure) are allpassed, not skipped.Security (checked, holds)
auth/loginpage.gois anhtml/templatewith the detail only in an element context;TestCallbackFailurePageEscapesTheIdPDescriptionasserts the<script>payload appears as<script>…, and the page carries no script and no external resource.auth/loginpage.go:44-96.<style>is authorised by the deployed policy (server/middleware.go:165,style-src 'self' 'unsafe-inline', hashedscript-src), so the page is not a blank document in production.auth/oidc.go:98-104logs onlydev+request_id;TestLoginStartLogsNoValuesgreps for the state-cookie value, nonce, sealed cookie, roles, subject andnextin the log.auth/oidc.go:333-380(nameFromUserInfo) only writesName/Email/NamedByUserInfo; roles come from the verified ID token's project-roles claim (auth/roles.go:62-72), and a userinfosubthat differs from the token's is refused. The tests drive a real discovery+JWKS+token+userinfo provider (fakeIdP), not a stub, and assertuserinfoHits == 1and "not asked when the token named somebody".RequireOperator→resolveIdentity/HasRoleuntouched (auth/middleware.go:163-180),decodeIdentitystill refuses a session without a subject (auth/roles.go:102-104).DisplayNamelosing the subject as a display value is presentation only;Subjectsurvives in the sealed identity (asserted).snapshot-checkreads stdin, prints field paths only, and a no-value contract is enforced with SENTINEL fixtures.Correctness (checked against the wire shapes, not just the tests)
I re-derived the states from
programs/snapshot.go+programs/source.goand compared with the branches inweb/src/lib/programs/plain-language.ts, and probed the reader directly:nothingonly withcollected_ownersnaming the viewer and a complete list; a dropped item emptiesCollectedOwners→not-connected; a fresh empty list nobody vouches for →not-connected; an old empty list →listed+stale: true→ the UI'sstalebranch (which correctly runs before thestate !== 'nothing'refusal, the F1 fix). I confirmed the emitted shapes:nothing/stale=false,not-connectedfor absent/incomplete/uncollected,listed/stale=truefor bothpublished_at45m old and 3h in the future.StaleAttreats> MaxClockSkewin the future as stale,Responsemarks every rowstale(so the chip is amber), andsnapshotChipadditionally refuses green when the waiting answer dates the same file old.not-connected; a status with no epic is kept verbatim and recorded as a Problem; long text is carried verbatim (no truncation anywhere) —TestSnapshotLongTextIsCarriedWhole.listed+WAITING_OWN_LIST_UNCOLLECTED; unmapped with others' items →listed+WAITING_NO_OWNER).Findings
No blocking findings.
Nits (none of these should stop the merge):
idkills the whole pane.web/src/lib/components/programs/programs-pane.svelte:270keys the list{#each wait.items as item (item.id)}, while neitherreadDecisionsnorreadDecision(programs/snapshot.go:433-540) checks id uniqueness. I rendered the pane with two decisions sharingid: "P0"on this head: Svelte throwseach_key_duplicateand the pane does not render at all — the one page-level failure the per-item degradation story claims to have closed. Same on live, so not introduced here; the fix is a per-item Problem (fail-closed, which then also withholdsnothing) or keying by index.next, contradicting the new comment.auth/oidc.go:100-108says the line never carries "thenexttarget", and the dev-modeWarnfour lines below logs"next", next.TestLoginStartLogsNoValuesexercises only the non-dev path. Harmless in dev, but the comment overstates the guarantee.formatVersionstill printsdevfor a suffixed tag.web/src/lib/format.ts:115only special-cases the literaldev; a stamped0.1.0-devrenders asv0.1.0-dev, so "the version chip never says dev" holds for the exact word only.snapshot-checkcannot pre-flight the future-published_atrule.cmd/snapshot-check/main.go:56-90calls onlyParseSnapshot, while the future verdict lives inResponse/StaleAt(needs a clock), so a document stamped an hour ahead exits 0 and the cockpit will paint it stale forever. Defensible (the publisher's own clock is the wrong reference), but worth knowing when the pre-flight is used to explain a stale tablet.programs/snapshot_file.go:74-78setsSnapshotProblemSinkfromFileSnapshotOptions.Problems; a second reader built anywhere silently redirects the first one's causes. Documented, and correct for one-reader-per-process, but it is hidden state.web/src/lib/components/shell/identity-chip.test.ts:35hasname_source: 'claim-name',\t\troles: ['operator'],collapsed onto one line; and the updated dashboard identity expectations (web/e2e/panes.spec.ts:365-386) now sit insidetest.describe.skip('the dashboard pane'), so those twonot.toContainText('dev:e2e-operator')assertions no longer run — the behaviour is covered bydashboard-pane.test.ts, but the e2e text is now dead.Intent fit
Plain words on screen (the Waiting block, the Sendrast headline/status split, "unversioned build", "operator account"), no internal identifiers painted (numeric subject gone from chip, tooltip and dashboard strip; program labels only), removals reversible in one line each (
PANE_ORDER, the two STOP mounts, the commented imports) with the gate's skip ceiling rather than silent deletion, and no silent data loss in the reader (long texts whole, Sendrast status never dropped, every degraded field named in a Problem and logged once per published document). Coverage is genuinely withdrawn for the fleet/gates/chat surfaces while those panes stay live — the gate records that as an explicit ceiling, so it is a known trade-off, not a hidden one.Plan Error
Merge note, per El Director: Aaron's yes covers this one fix ("3 - yes."). The first image build failure on this head is still unexplained: logs unreadable, the rerun passed. The two seats shared one brief. Sandbox lock: forgejo/landing.ts sha256 cce961523a6f83922537846888709543b00ef378b284284f708d5b1f079e19fd.