webui: serve the Console first screen from the control board (#1507)

This commit is contained in:
2026-09-12 19:37:35 -05:00
parent 7c8e530add
commit ea00ec66d9
29 changed files with 1639 additions and 0 deletions
@@ -0,0 +1,71 @@
# WEBUI-1507-R1
Author Dewey, independent reviewer Filbert, coordinator Darkwing.
Workspace `/mnt/storage/src/mosaic-stack`, branch `refactor`, baseline
355b4308c31c4bd500a844787eb1461f98dd2be3. Jason authorized issue-first,
coordination, commit and push for plan piece 4. Issue #1507 is open; Gate E
remains Jason-owned Monday 2026-09-14. No user acceptance is claimed.
## Candidate
All and only files under `packages/webui/`, pinned in
`webui-1507/manifest.sha256`. Manifest SHA-256:
`509f20e53292d5731c866d84c73cfaf539a89104b38b12a43260075ee7c3cb4d`.
No board files, seat files, comms, scanner, root package or fleet changes.
Darkwing confirmed ownership and named you reviewer, transport rc 0 followed
by his acknowledgement in this session. He released CURRENT and SESSIONS at
fab40f25. Concurrent planning commits f80404f5/355b4308 preserved the intake
status, with the shared-checkout hazard explicitly recorded in DEFERRED.
Original uncommitted WUI drafts, skills and other owners' files are excluded.
Review for exact brief conformance, proxy boundary/refusals, draft and focus
behavior, same-origin/loopback safety, rendering and maintainability. Reproduce
checks and return APPROVED or concrete findings in
`docs/plans/reviews/2026-09-13_webui-verdict.md`, then notify Dewey via
agent-send. Do not edit source or publish. No real board scans or real-seat
messages are needed for this review.
## Evidence
- `node --test packages/webui/tests/`: 6/6, including two Chromium browser tests.
- Clean baseline-plus-candidate copy at `/tmp/webui-1507-clean-8KGiHN`:
`node --test packages/webui/tests/ packages/control-board/tests/
packages/seat/tests/ packages/ledger/tests/ packages/mosaic/tests/`: 208/208.
Output: `webui-1507/clean-tests.txt`.
- Browser checks use the real board scanner/server with temporary fixtures,
including registered, working, waiting, error and offline rows. Only the
board transport is stubbed. Exact counts and Seen/Reply round trips pass.
- Browser keyboard, draft/caret, failed receipt, concurrent send exclusion,
empty/loading/malformed/unreachable, stale registration, hostile/long text
and denied localStorage cases pass. No real messages sent.
- Rendered contrast across ten palettes and three modes: 330 samples pass,
lowest 4.504658476260286:1. Five layout widths 320 through 2560px pass.
- Screenshots in `webui-1507/`: Harbor light page at 320/1440 and Grove dark
inspector at 320/1440. Dewey inspected all but the Harbor 320 screenshot
before this request; that screenshot remains available to inspect.
- CLI argument, upstream redirect, exact POST bytes/status, bad JSON/content
type/size, hostile Host/Origin and asset allowlist cases pass.
- No root CI workflow exists. No task/release/live fleet tests, deployment or
owner acceptance is claimed. Direct refactor publication is authorized.
## Corrections during author verification
The first browser test used a wrongly quoted CSS selector; corrected the test.
The Host test initially used fetch, which did not transmit the overridden Host;
replaced that probe with node:http and verified rejection. The first clean-tree
run exposed a loading-fixture race, ending the held response before the request
arrived; an explicit request promise fixed it, then 208/208 passed. These were
test defects, not waived failures.
## Scope choices and remaining gate
The left project filter uses actual board project names and counts, not mockup
workspace registrations. Dense table fields and inspector detail come only from
the board. Unsupported controller/generation/config/turns/activity and non-board
controls are omitted and listed in DEFERRED. CSS/brand/fonts are copied unchanged;
`live.css` supplies page adaptations. Appearance selectors remain on the one page.
Gate E requires Jason's Monday workday and end-of-day ruling. README gives the
startup commands, test steps and rollback to the unchanged board. Keep #1507
open until acceptance; no artificial issue closure from test results.
@@ -0,0 +1,108 @@
# WEBUI-1507-R1 review verdict
## APPROVED
Filbert, independent reviewer. Author Dewey. Reviewed 2026-09-13 against
Piece 4 in `docs/plans/2026-09-12_control-board-mvp.md` and the request at
`docs/plans/reviews/2026-09-13_webui-review-request.md`.
No blocking findings. Approval applies only to the exact package below. It
passes independent implementation review, not publication, deployment or user
acceptance. Gate E remains Jason's Monday 2026-09-14 ruling. Keep #1507 open
pending that acceptance.
## Exact candidate
HEAD was the requested baseline
`355b4308c31c4bd500a844787eb1461f98dd2be3` during provenance verification.
The candidate was the untracked `packages/webui/` overlay, not a committed tree.
Manifest: `docs/plans/reviews/webui-1507/manifest.sha256`.
Recomputed manifest SHA-256:
`509f20e53292d5731c866d84c73cfaf539a89104b38b12a43260075ee7c3cb4d`.
Its 21 entries cover all and only the 21 package files. All 21 content hashes
passed verification before review, in the independent test copy, and after
checks. This approval incorporates that exact manifest, not later package edits.
Behavior-bearing source hashes:
```text
20f8091e5fb3fcaba583b601ce258999d44afbe569e93c979938ace759b4eef4 packages/webui/src/cli.mjs
c0c1a360ce44a7439e67eea36b9237c59621aa3fd78571a823fd23dc15a950f6 packages/webui/src/serve.mjs
7f30847ba25eb7e2b176df614d7bcae45ab7d86615c84ad44ffc29f0f261759c packages/webui/src/public/app.js
db055d5b8a3710f4164193550ac25b7c5d223dd890ae3e9d73daaada38849557 packages/webui/src/public/index.html
7add449911677c662a05ad5d9c2462f919e759574fe4b85bd13a705cc54f6856 packages/webui/src/public/live.css
```
## Independently reproduced evidence
Created `/tmp/filbert-webui-1507-N4Iiyg` from a read-only `git archive` of
baseline `packages`, `scripts`, and `tools`, then copied only the pinned WebUI
package over it. No unrelated working-tree source entered this test copy.
No dependency installation was needed. Node v26.8.1 and installed Chromium.
Commands run from that copy:
```sh
sha256sum -c manifest.sha256
WEBUI_EVIDENCE=/tmp/filbert-webui-1507-N4Iiyg/screenshots node --test packages/webui/tests/
node --test packages/webui/tests/ packages/control-board/tests/ packages/seat/tests/ packages/ledger/tests/ packages/mosaic/tests/
```
- WebUI: 6 passed, 0 failed, no skips, including both Chromium tests.
- Combined suite: 208 passed, 0 failed, no skips.
- Rendered contrast: 330 samples, no failures, minimum 4.504658476260286:1.
- Layout checks passed at 320, 390, 768, 1440 and 2560px, with and without
the inspector. Table-local horizontal scrolling is intentional.
- Inspected all four independently generated screenshots: Harbor light page
at 320/1440 and Grove dark inspector at 320/1440. Controls and text remain
readable; the narrow inspector scrolls vertically to its reply controls.
- `node --check` passed for both server modules, both public scripts and all
five test modules.
- `cmp` confirmed copied shared CSS, Console CSS, brand.js and all six font,
source and license files are unchanged from Dewey's WUI assets.
Raw local results are `webui-tests.txt`, `combined-tests.txt` and `hashes.txt`
in the independent copy. Screenshots are in its `screenshots/` directory.
These temporary files supplement this durable result summary.
## Review conclusions
The page implements the approved Console layout using board data only. Project
counts and filters do not hide another project's waiting requests. The dense
table and inspector expose the specified fields; unsupported mockup controls
are absent. Palette and appearance remain on this one page. Refresh uses the
board page's ten-second cadence, with Pause and manual refresh, not a new scanner.
The server binds only to loopback, restricts the upstream to an HTTP loopback
origin, normalizes localhost without DNS resolution, rejects hostile Host and
Origin requests, and serves only allowlisted assets. POST routes require a JSON
object and enforce the 4096-byte limit. The proxy preserves accepted POST bytes,
upstream status and JSON text, refuses redirects, and does not retry actions.
Seen and Reply reach only fixture transports during verification.
Rendering escapes dynamic text and attributes. Hostile identifiers do not enter
CSS selectors. Drafts, receipts and caret positions survive refresh; stale
registration removes reply access without discarding the draft. A per-row
in-flight set prevents duplicate sends, and text changed during a send survives
its earlier success. Keyboard row navigation, inspector focus and Escape return
passed. Unreachable or malformed board data produces a warning without erasing
the last valid snapshot. Denied localStorage falls back safely.
The implementation is small and separated into CLI, proxy and page behavior.
Copied design assets remain separate from live-page adaptations. Source review
and tests support the declared boundaries; no board, seat or comms change is
part of this candidate.
## Limits and handoff
The checks used synthetic session and registration files and stubbed transport.
No real board scan, real-seat message, private-session read, credential access,
source edit or publication was performed. Git use was read-only provenance and
archive extraction. Browser evidence is Chromium, not a screen-reader audit or
Jason's workday acceptance. Contrast samples are not an exhaustive accessibility
certification. The README documents in-memory draft loss on reload and uncertain
delivery after a lost response.
Dewey should reverify the manifest before any separately authorized publication.
CURRENT.md remains with its existing owner for the verdict/publication transition.
@@ -0,0 +1,217 @@
✔ loadConfig: missing file throws ConfigError (2.069893ms)
✔ loadConfig: invalid JSON throws ConfigError (0.438417ms)
✔ loadConfig: missing dataRoot throws ConfigError (0.327993ms)
✔ loadConfig: relative dataRoot throws ConfigError (0.325869ms)
✔ loadConfig: valid config returns dataRoot (1.031496ms)
✔ findNewestSession: picks the newest by mtime among two files (0.731378ms)
✔ findNewestSession: finds files in nested subdirectories (0.449258ms)
✔ findNewestSession: returns null for a missing dir (0.190751ms)
✔ readSession: extracts fields, collapses/truncates text, counts a truncated final line (1.108679ms)
✔ readSession: model and provider follow the latest model_change entry or assistant turn; null when the log names neither; scanAgent carries them (2.038489ms)
✔ readSession: lastError carries the assistant errorMessage only when the last assistant turn errored (0.855495ms)
✔ findNewestSession/scan: never read sibling auth or secrets next to a sessions dir (2.03661ms)
✔ deriveState: full state table (0.313836ms)
✔ rule: newest entry is an assistant message with a tool call, after a question-looking text, is working (0.601595ms)
✔ rule: newest entry is a tool result with no assistant text after it is working (0.531506ms)
✔ rule: a finished turn (text-only assistant message, stopReason stop) is waiting (0.405072ms)
✔ task: the first user message of the session, from text blocks (0.491929ms)
✔ task: a plain-string user content is accepted, whitespace collapsed and long text capped (0.381223ms)
✔ task: no user message in the log means null (shown as unknown), never a guess (0.472373ms)
✔ workspace: the live tmux pane path wins; the session cwd is the fallback; neither means null (0.567966ms)
✔ activeProject: basename of the nearest .git directory or .git file above the workspace; none means null (0.643836ms)
✔ scan: the written record carries task, workspace and activeProject (0.745408ms)
✔ registration: overrides task, project and workspace; every source says registration; registered carries the launch fields; the grouping column is untouched (0.812363ms)
✔ registration: empty task and null project/workspace leave the derived values in place; registered is still non-null (0.549512ms)
✔ registration: a record whose pid is gone is stale; derived values win, sources say derived, registered stays with alive false; a pid the probe cannot decide is not stale; pidAlive itself (1.429642ms)
✔ registration: no registration leaves the Gate A fields exactly as before, and registered is null (0.623553ms)
✔ loadRegistrations: a missing seatsDir gives empty lists (0.275783ms)
✔ loadRegistrations: one good record, one malformed JSON, one with an unknown field; a stray file under seatsDir is ignored (1.147462ms)
✔ matchRegistration: matches by sessionsDir, and by realpath through a symlink; sessionsDir null never matches; same seat name with a different sessionsDir does not match (fleet vs repo darkwing) (1.76852ms)
✔ scan: writes the registration override to disk; index.json carries registered and registrationErrors (1.131616ms)
✔ scan: a relative seatsDir throws ConfigError; an omitted seatsDir behaves as before (0.313115ms)
✔ scanAgent: waitingOnYou is true for waiting/error and false otherwise (1.127154ms)
✔ scanAgent: ageSeconds is computed from the injected now (0.302815ms)
✔ scanAgent: sessionFile null and state idle when sessions dir is empty but alive (0.199626ms)
✔ discoverRepoAgents: finds agents with a sessions dir, skips those without, sorted by name (0.381178ms)
✔ discoverFleetAgents: finds agents with a sessions dir, sorted by name, fleet tmux fields (0.405132ms)
✔ scan: writes per-agent files and index.json, rerun overwrites, no leftover tmp files (1.289574ms)
✔ scan: relative boardDir throws ConfigError (0.150536ms)
✔ CLI: scan with assume-alive liveness exits 0, prints board summary, writes board files (89.928423ms)
✔ CLI: missing config exits 2 with a refused: message (80.253972ms)
✔ CLI: unknown command exits 2 (79.841572ms)
✔ CLI: unknown --liveness value exits 2 (90.403332ms)
✔ panesRunPi: true when any trimmed line equals 'pi' (0.356649ms)
✔ panesRunPi: false for bash-only, claude, empty, or node-pi-style lines (0.162436ms)
✔ tmuxIsAlive: a pane running pi is alive (0.297307ms)
✔ tmuxIsAlive: session exists but pi has exited is not alive (0.124004ms)
✔ tmuxIsAlive: no such tmux session is not alive (1.002756ms)
✔ tmuxIsAlive: tmux could not be run at all is unknown (null), never assumed alive (0.141834ms)
✔ tmuxIsAlive: passes -L <socket> only when a socket is given (0.188087ms)
✔ parsePanes: one pane per line, command and optional tab-separated path (0.141459ms)
✔ tmuxInspect: reports the path of the pane running pi, not of a shell pane (0.129298ms)
✔ tmuxInspect: no pi pane, no session, or no tmux gives no workspace and the matching liveness (0.172234ms)
✔ loadSeen: missing file returns {} (1.594824ms)
✔ loadSeen: invalid JSON throws ConfigError (2.10826ms)
✔ loadSeen: a JSON array throws ConfigError (0.343731ms)
✔ loadSeen: a non-string value throws ConfigError (4.345296ms)
✔ markSeen: seen true adds the key and writes seen.json mode 0600, no leftover tmp files (3.563497ms)
✔ markSeen: seen false deletes the key (1.941261ms)
✔ markSeen: missing, empty, or non-string fields throw ConfigError (0.477522ms)
✔ markSeen: project containing '/' throws ConfigError (0.231097ms)
✔ markSeen: non-boolean seen throws ConfigError (0.202643ms)
✔ scanAgent: a seen mark matching the waiting session's lastTimestamp clears waitingOnYou (2.88064ms)
✔ scanAgent: a stale mark (agent wrote something newer) is not seen and waitingOnYou is true (0.617591ms)
✔ scanAgent: a working session with a matching mark is not seen (marks only apply to waiting/error) (1.806863ms)
✔ scanAgent: an error-state session with a matching mark is seen (2.149805ms)
✔ scan: index.seen and waitingOnYou reflect seen.json, which scan never rewrites or deletes (1.045558ms)
✔ scan: a corrupt seen.json makes scan throw ConfigError (fail closed) (0.490992ms)
✔ isLoopbackHost: recognizes loopback hosts (2.380812ms)
✔ isLoopbackHost: rejects non-loopback hosts (6.201413ms)
✔ startServer: refuses a non-loopback host with ConfigError, never opens a socket (4.882151ms)
✔ startServer: serves page, healthz, and a rescanning /api/board (48.899289ms)
✔ startServer: a seatsDir registration overrides the row and index.registered reflects it (11.47682ms)
✔ startServer: /api/board returns 500 JSON with an error field when scan throws (4.702469ms)
✔ CLI: serve refuses a non-loopback host with exit 2 and a refused: message (78.932321ms)
✔ CLI: serve rejects a non-numeric --port with exit 2 (82.056496ms)
✔ CLI: scan still works after the async cli refactor (93.42089ms)
✔ CLI: live serve prints its URL and answers /healthz (134.584373ms)
✔ page.html: esc() escapes every HTML-significant character (1.293554ms)
✔ POST /api/seen marks a row; GET /api/board still shows it seen; seen:false clears it (19.569156ms)
✔ POST /api/seen without a JSON content-type returns 400 and does not write a mark (5.81507ms)
✔ POST /api/seen with invalid JSON returns 400 (8.413609ms)
✔ POST /api/seen with a body over 4096 bytes returns 400 (or resets the connection) and writes no mark (6.922079ms)
✔ POST /api/seen with a missing agent returns 400 (6.492325ms)
✔ POST /api/board returns 405; PUT /api/seen returns 405 (6.785092ms)
✔ CLI: scan --print marks a seen row with 's' and the summary line ends with 'N seen)' (93.27849ms)
✔ page.html: seenControl() escapes rec.project/agent/lastActivity, and the POST uses a JSON content-type (0.666541ms)
✔ page.html: has a collapsed Seen section that lists seen rows with the shared row builder (1.860189ms)
✔ page.html: each project has a Hide seen checkbox (default on) beside Hide offline, with a hidden-count note (0.458153ms)
✔ page.html: a project header reads "N of N" only while a checkbox hides rows (0.301181ms)
✔ page.html: every row shows Task and Active project, derived or the word unknown, with the workspace in the detail (0.522645ms)
✔ page.html: task and active project cells show their source via sourceTag(); the detail has a Registered row via registeredText(); SOURCE_LABEL maps registration to registered; every dynamic value in sourceTag/fromSource/registeredText is escaped (1.596776ms)
✔ POST /api/reply: runs agent-send.sh with -s from the registration, -S <host>:control-board, -m text plus the fixed trailer, no -L on the default socket, MOSAIC_TMUX_SOCKET stripped; answers delivered with the exit code and both streams (66.023811ms)
✔ POST /api/reply: a registration with a tmux socket adds -L <socket> (47.085295ms)
✔ POST /api/reply: a non-zero tool exit is a 200 with delivered false, the exit code and the stderr verbatim (44.906851ms)
✔ POST /api/reply: refusals before the tool runs: empty or blank or long text 400, unknown row 404, no registration 409, stale registration 409, no tmux session 409, bad JSON 400; the tool is never called (30.336458ms)
✔ POST /api/reply: a missing agent-send.sh is a 500 with the path in the error, not a crash (6.232881ms)
✔ replyToRow: DEFAULT_AGENT_SEND is the repository's tools/tmux/agent-send.sh and it is executable (0.196143ms)
✔ page.html: the reply box appears only where canReply() holds (live registration with a tmux session), the detail has a Reply row, the submit posts JSON to /api/reply, receipts and drafts survive a refresh, and every receipt value is escaped (0.713094ms)
✔ real helper GET HTTP 200 preserves exit 0 without credentials (16.87011ms)
✔ real helper POST HTTP 201 preserves exit 0 without credentials (19.904021ms)
✔ real helper GET HTTP 403 preserves exit 1 without credentials (14.960278ms)
✔ fixture git subjects only, follow-ups and three session kinds (150.605163ms)
✔ text and JSON carry same numbers, open and truncated title (323.809392ms)
✔ missing credentials exit 2, no-issues never calls API and shows unknown (256.457428ms)
✔ empty range gives no rows and zero totals (141.564883ms)
✔ inclusive UTC dates, first-line preamble only, role and seat boundaries (116.133329ms)
✔ close-only issue included, even median, missing metadata stays unknown (88.374838ms)
✔ unique commits but per-issue links count multiple tags once each (87.716176ms)
✔ page cap refuses rather than silently undercounting (101.467302ms)
✔ bad API payload not JSON refuses (91.969036ms)
✔ bad API payload {} refuses (86.830577ms)
✔ bad API payload [{"number":1}] refuses (77.291166ms)
✔ partial or malformed session log refuses with location, not content (91.720579ms)
✔ no sessions is an empty table; symlink source refuses (138.05013ms)
✔ reads only refactor even when another branch is checked out (85.265578ms)
✔ invalid dates, reverse dates and duplicate options refuse (46.116248ms)
✔ preamble parsing and issue number boundaries (0.357602ms)
✔ no closed issues with human messages means undefined ratio, not invented zero (0.191973ms)
✔ pure resolution selects current default or explicit enrolled account (3.452427ms)
✔ scope is explicit, bounded and never inferred (3.224534ms)
✔ fork pin is preserved against default change, override, missing account and revocation (2.07011ms)
✔ unenrolled account/provider, missing harness, model expansion and native model ceiling refuse (1.785801ms)
✔ only explicit synthetic credential forms and internal fixture stores admitted (12.853623ms)
✔ two concurrent workspaces of the same agent publish distinct complete private generations (89.218237ms)
✔ same execution ID is exclusively claimed and cannot overwrite a published generation (51.919745ms)
✔ failed generation after-auth preserves prior files, records failure and refuses blind same-ID retry (69.110981ms)
✔ failed generation before-publish preserves prior files, records failure and refuses blind same-ID retry (165.301085ms)
✔ credential lock contention refuses without duplicate side effects (34.503814ms)
✔ symlinked pre-existing final target is refused and never followed (76.344676ms)
✔ invalid registry cannot resolve; no fallback to supplied partial entries (0.585699ms)
✔ post-publication failure records uncertainty, preserves complete generation and prevents replay (90.149481ms)
✔ expired credentials refresh under transaction and subsequent generation reuses rotation (229.155792ms)
✔ refresh failure retains prior generation and store state (80.103824ms)
✔ refresh timeout retains prior generation and store state (166.055716ms)
✔ refresh malformed retains prior generation and store state (68.191722ms)
✔ concurrent refresh on same account refuses contention while unrelated account proceeds (218.21407ms)
✔ invalid refresh options refuse before burning claim (39.334705ms)
✔ fixed fake process rotates both OAuth fields without mutating caller input (51.399762ms)
✔ concurrent isolated processes preserve separate provider credentials (55.271858ms)
✔ fake failure is refused with fixed diagnostics (33.787807ms)
✔ fake malformed is refused with fixed diagnostics (47.268272ms)
✔ fake timeout is refused with fixed diagnostics (108.39385ms)
✔ fake unchanged is refused with fixed diagnostics (64.498115ms)
✔ caller executable/environment injection is rejected before spawning (0.483922ms)
✔ valid fixture tree validates and lists without secrets (112.29331ms)
✔ unknown-field refuses (0.679922ms)
✔ invalid-id refuses uppercase and traversal shapes (0.462347ms)
✔ plain-http baseUrl requires allowInsecureTransport (0.517778ms)
✔ native provider rejects allowInsecureTransport (0.232811ms)
✔ unsupported credential type and kind refuse (0.267028ms)
✔ account provider-path mismatch refuses (1.053032ms)
✔ profile account refs must be provider/account shaped (0.410258ms)
✔ seat selection accepts fork pin field, validates account refs (0.385189ms)
✔ harness manifest id must equal executable (gate 1) (0.471681ms)
✔ CLI validate: duplicate provider id across files refuses (49.292022ms)
✔ CLI validate: missing referenced provider/account refuse (56.231526ms)
✔ CLI validate: broken JSON refuses without secret echo (44.514594ms)
✔ CLI usage errors exit 2 (111.897184ms)
✔ credential.json sibling presence does not break validation and is never read (168.781797ms)
✔ D1 missing, empty and structurally empty roots refuse, no list projection (275.749732ms)
✔ D1 required directory auth cannot be absent (124.619472ms)
✔ D1 required directory auth/providers cannot be absent (136.518861ms)
✔ D1 required directory auth/accounts cannot be absent (117.238735ms)
✔ D1 required directory auth/settings cannot be absent (119.706456ms)
✔ D1 required directory harnesses cannot be absent (97.054141ms)
✔ D1 root file and unreadable metadata refuse (137.663685ms)
✔ D2 no symlink traversal at auth/providers/openai-codex.json (57.104514ms)
✔ D2 no symlink traversal at auth/accounts/openai-codex/homelab-openai (58.8982ms)
✔ D2 no symlink traversal at auth/providers (69.919373ms)
✔ D2 no symlink traversal at auth (66.585499ms)
✔ D2 root and ancestor symlinks and lexical traversal refuse (182.602198ms)
✔ private filesystem modes enforced for root (56.988873ms)
✔ private filesystem modes enforced for auth (54.402276ms)
✔ private filesystem modes enforced for auth/providers/openai-codex.json (55.196762ms)
✔ private filesystem modes enforced for auth/accounts/openai-codex/homelab-openai/account.json (62.434238ms)
✔ D3 numeric version 1 only across all record kinds (1.213381ms)
✔ D4 nested unknown keys and missing per-kind required fields refuse (51.243059ms)
✔ D5 unenrolled default refuses even when account exists (61.905203ms)
✔ D6 provider/account credential type must match (61.029479ms)
✔ D7 every harness endpoint enforces HTTP opt-in and shape (0.495449ms)
✔ D8 URLs reject embedded credentials and unsupported protocols without echo (164.103475ms)
✔ D9 malformed JSON diagnostics contain no content excerpt (61.131821ms)
✔ D10 missing metadata is missing-path, not invalid-json (61.521407ms)
✔ D10 library returns no partial entries on any invalid record (70.423693ms)
✔ null/scalar/array metadata refuses without stack or echo (216.403521ms)
✔ credential sibling is never opened, even when an unreadable symlink (34.349719ms)
✔ oversized metadata refuses before parsing (58.959915ms)
✔ resolveSeat: by name under --repo resolves the repo layout (2.244578ms)
✔ resolveSeat: by path resolves the fleet layout (0.655672ms)
✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (1.497292ms)
✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (1.278671ms)
✔ makeRegistration produces a record that validates; each shape violation throws SeatError (1.478842ms)
✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.781997ms)
✔ updateTask: changes task and updatedAt only, and refuses appropriately (2.864265ms)
✔ CLI launch: registers, execs the fake launch script, and passes args through (40.186244ms)
✔ CLI launch: --harness lands in the record (62.351088ms)
✔ CLI launch: the launch script's own exit code passes through (52.770779ms)
✔ CLI launch: relaunching a seat rewrites the one registration record (103.782698ms)
✔ CLI launch: omitting --task records an empty string, not null (68.271979ms)
✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (201.947039ms)
✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (249.955376ms)
✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.580992ms)
✔ browser edge states: loading, empty, malformed, stale, hostile/long values, in-flight reply and appearance fallback (1340.984357ms)
Rendered contrast: {"failures":[],"count":330,"lowest":4.504658476260286}
✔ served Console browser: real board fixtures, keyboard, drafts, receipts, themes, 320px and failures (1624.334009ms)
✔ loopback host and board origin fail closed (7.093521ms)
✔ real board fixture passes through WebUI; assets and isolated seen/reply work (97.215606ms)
✔ proxy preserves exact request bytes, status and receipt, rejects forms and malformed JSON, never follows redirect (36.079167ms)
✔ unreachable board reports URL; CLI rejects unsupported options (389.105161ms)
tests 208
suites 0
pass 208
fail 0
cancelled 0
skipped 0
todo 0
duration_ms 2604.12737
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@@ -0,0 +1,21 @@
ee81d2b902a0b250f32d997d27085b9c76a7c98acdd0a0b55c65cf9342047b4e packages/webui/package.json
6eca43194632017ffed73d62ba76c74435c4b661f4ec3fc76d8b0bae8bcf9089 packages/webui/README.md
20f8091e5fb3fcaba583b601ce258999d44afbe569e93c979938ace759b4eef4 packages/webui/src/cli.mjs
7f30847ba25eb7e2b176df614d7bcae45ab7d86615c84ad44ffc29f0f261759c packages/webui/src/public/app.js
a30ddcd349703aff7464c34bef3fffdff405ee50c113440d7c8693c02d210972 packages/webui/src/public/assets/fonts/manrope-400.woff2
a30ddcd349703aff7464c34bef3fffdff405ee50c113440d7c8693c02d210972 packages/webui/src/public/assets/fonts/manrope-500.woff2
a30ddcd349703aff7464c34bef3fffdff405ee50c113440d7c8693c02d210972 packages/webui/src/public/assets/fonts/manrope-600.woff2
a30ddcd349703aff7464c34bef3fffdff405ee50c113440d7c8693c02d210972 packages/webui/src/public/assets/fonts/manrope-700.woff2
e01b637272e0cbdfb240184dd98ea5cc671556d9894dae2668d92ab2c906787c packages/webui/src/public/assets/fonts/manrope-OFL.txt
0d9aaaaa963b675d94d8fbee0cf47a8ecf03ea412f8b14aba96416375e5a2bb5 packages/webui/src/public/assets/fonts/manrope-sources.txt
d568df6dca14ae336db3b627639c8181c088649a21ffe992f9bfacc20b5b6238 packages/webui/src/public/brand.js
1ce47447339a274dd51c0168838a5e58e0addaf2200e04625bc1da5bc9279097 packages/webui/src/public/console.css
db055d5b8a3710f4164193550ac25b7c5d223dd890ae3e9d73daaada38849557 packages/webui/src/public/index.html
7add449911677c662a05ad5d9c2462f919e759574fe4b85bd13a705cc54f6856 packages/webui/src/public/live.css
b3e6e240fa93555e30321f897405839856b3438ef44b1cea73d2f5fbd899077e packages/webui/src/public/shared/app.css
c0c1a360ce44a7439e67eea36b9237c59621aa3fd78571a823fd23dc15a950f6 packages/webui/src/serve.mjs
2bf76719da97dfbad4edeeac98d608a4a8b09fccc2c1e42fba7cad61af205b0d packages/webui/tests/browser-edge.test.mjs
3ff14de07e87cc624433db6425b9d05e40d98f61e40f2bbf1fbee7431f9d73c1 packages/webui/tests/browser.mjs
f9cc0cf8603481a11f5eeb90ebc35fc5f1a7fff22a076e8a89eeec12cfd1700c packages/webui/tests/browser.test.mjs
3887958f6c3343ac8d6db800725b75482cc66e559e6eb925b842eb9a5ae13bd2 packages/webui/tests/fixture.mjs
15e77422890bc50cdb700484ca7e16ac32476aa8ec61d5575a6e0a920694bc4c packages/webui/tests/serve.test.mjs