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,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