Row 77, round 3 (round 2 rebased onto row 95). Rocko built it. This covers row 75's N1-N5 and Vikunja 146 and 147. `install --copy ID` refuses unless every ExecStart path of the bus unit, read from `systemctl --user show` so drop-ins count, lies inside that copy. It also refuses when the unit isn't loaded, no unit is loaded, ExecStart has no path, or systemctl fails. COPY_PATHS gains packages/tasks/src and packages/tasks/fields.json. Every copy directory is 0555. TOOLS.md and the webui README give the removal command and the rollback floor. Candidate manifest d9880f4c92d1d976027562a03cc5dd07512954e86f49f1da0ebb34e642a955f5 (10 paths, 8 applied by the patch, 2 packet files already committed), base2e26fbe1, packet agents/rocko/work/queue-77/r2b/. Approvals on round 3: Darkwing 27979 and Filbert 27985 (Filbert round 2: 27898). Sage's gate rerun 22:32:51Z-22:35:12Z on459c4fc6plus the patch (row 92 included), manifest 10/10: webui 101, control-board 124, and every scripts/test-*.sh exited 0 (test-release 4, Docker cases skipped at a dead socket). Live at the next Console install and restart (decision 109). Mos makes that call. Co-Authored-By: Claude Opus 5.5 <[email protected]>
37 KiB
Mosaic Console
Piece 4, #1507. The control board is the first and only WebUI screen.
Run
Start the existing board in one terminal, then the WebUI in another:
node packages/control-board/src/cli.mjs serve
node packages/webui/src/cli.mjs serve
Open http://127.0.0.1:7330/. Optional --port N and
--board http://127.0.0.1:7331 select another port or loopback board origin.
--business ID picks the business the bus views read; without it they read
the running bus host's business (mosaic bus start <business>).
Port 0 picks a free port. Ctrl-C stops each foreground server. No daemon,
installation, account, login or deployment is added. The preview mode below
checks one header a sign-in proxy sets; it signs nobody in itself.
Only HTTP loopback board origins are accepted. The WebUI binds to IPv4 loopback by default, rejects a repeated Host (400), nonlocal Host and cross-origin requests, sends no CORS headers, refuses redirects and accepts only JSON object POST bodies up to 4096 bytes (16 KiB for row 76's send, below). Do not put the board behind a public proxy. Put the Console behind one only in the preview mode below, with a proxy that meets its contract.
Preview behind a sign-in proxy (row 74, #1568)
node packages/webui/src/cli.mjs serve --port 7332 \
--public-host NAME --auth-header HEADER --allow-user ID
The three flags go together; one or two refuse at start, before any config
is read. NAME is a DNS name, no scheme or port. HEADER is a header name other
than Host or Origin; Node lowercases incoming header names, so it matches
without regard to case (X-Example-Uid on the flag finds x-example-uid).
ID is the one user id, 1 to 256 visible ASCII characters without a comma,
and the value compare is exact. The Console still binds to loopback only. It
names no identity provider and has no login code: the proxy signs the user
in and limits sign-in to the right group.
In preview mode:
- A request whose Host is exactly NAME must carry HEADER once, with a value
equal to ID. Missing, different or repeated answers 401
sign-in requiredbefore any route runs, so no bus read or board call is made. An Origin, when sent, must behttps://NAME. - A request whose Host is loopback on the Console's port (
127.x,localhostor[::1]) is checked as before and needs no HEADER, so the operator's browser on the host still works. Any other Host is 403. - A request that carries HEADER with any Host but NAME answers 403
proxy Host refused. The operator's browser never sends HEADER; a proxy that rewrote Host does, and it fails on its first signed-in request instead of reaching the loopback path unchecked. - More than one Host header answers 400 before any other check, in this
mode or not. Node keeps the first, so a loopback Host followed by
Host: NAMEwould otherwise skip the sign-in check. - Every method other than GET and HEAD answers 403
read-only, whatever the Host./api/seenand/api/replynever reach the board, so Seen and Reply fail in the page with that refusal. The one exception is POST/api/bus/messagewhen the Console was started with--send(next section). - The GET routes are unchanged: the bus views, the Queue, Business and Settings reads, the board panel and the assets. If the board is down, its panel shows the existing error and the bus views still answer.
What the proxy must do. The Console trusts HEADER only because the proxy controls it:
- It terminates TLS for NAME and is the only thing that reaches the port from off the host.
- It strips HEADER from every client request and sets it only after the identity provider signs the user in.
- It forwards only requests whose Host is NAME, and forwards that Host
unchanged. nginx rewrites Host to the upstream by default; set
proxy_set_header Host $host;(or the literal NAME). Caddy and Traefik keep the client's Host by default. - Any loopback Host on the Console's port,
localhostincluded, is the operator's path and skips the header check, so a proxy that forwarded a client'sHost: 127.0.0.1:7332would let anyone through without signing in. A tunnel whose local end uses the same port number sends such a Host and reaches the Console unchecked. Anything that reaches a tunnel's local end can send a loopback Host without HEADER, so bind that end to loopback. - Every hop on this host that reaches the loopback port is bound by this contract too, not only the edge.
- It passes the Console's status and body through and doesn't rewrite
upstream errors (nginx's
proxy_intercept_errorswith anerror_page, for example). Under--sendthe page reads a 4xx other than 408 as "not sent", so a proxy that turned the Console's 503outcome-unknowninto a 4xx or a redirect would report a message that may exist as not sent.
The local exposure doesn't change ("Data and boundaries"): any process on
the host can still connect to the port and send Host: NAME with HEADER and
ID, or just a loopback Host, and read what a reader capability reads. The
user id is not a secret; the sign-in at the edge is what keeps others out.
Under --send the first of those can also send (next section).
To run the Console as a systemd user unit, scripts/webui-service.sh
renders and installs one with these flags (row 75; docs/TOOLS.md
"Console service"). Jason starts and restarts it, and so may Mos under lead
decision 102, which accepts that sends from a Console Mos started enter the
trail as Jason. Any other agent starting it works around the human proof
(lead decisions 62 and 94). Without --send the unit is read-only. The script takes --send (row 83)
only with the three preview flags; otherwise it exits 1 and writes nothing.
With it, the unit's ExecStart ends with --send, and the Console needs the
system config at start: without it serve refuses with --send needs the bus configured and exits 2, which the unit doesn't restart. status says
whether the installed unit sends.
The unit runs a read-only pinned copy of the Console's paths, not the
working checkout (row 77). scripts/webui-service.sh copy COMMIT makes
one from a commit on refactor, and install --copy ID points the unit
at it, with serve's --checkout naming the checkout, where the queue read
still runs, so the queue view isn't pinned and shows the queue as it is
now. The update step is copy, stop, install --copy ID with the same
flags, start; rollback is the same with the earlier id (one at or after
row 77's landing, since an older cli.mjs refuses --checkout), and
copies are kept (they are read-only, files and directories; remove one
with chmod -R u+w DIR && rm -rf DIR). install --copy ID refuses unless
the bus unit runs from the same copy, because the bus accepts a human
proof only from its own human-cli.mjs. The bus still runs from the
checkout, so the update step can't be used until a later row puts the bus
on the copy. The stop and start are Jason's or Mos's: install and
uninstall refuse while the unit runs. With --auth-header X-authentik-uid the allowed user must be the 64-character Authentik uid
(lead decision 101). status names the copy and whether refactor has
moved past it.
Message a role (row 76, #1570)
node packages/webui/src/cli.mjs serve --port 7332 \
--public-host NAME --auth-header HEADER --allow-user ID --send
--send is off by default. It goes only with the three preview flags and
refuses at start without them. It also needs the system config: without it
the start refuses with --send needs the bus configured, where a read-only
Console would still serve the board. A unit that sends comes from
scripts/webui-service.sh install with --send (above). With --send the
Inbox has a "Message a role" box: a role picker filled from the agents read,
a text box and Send. The message reaches the role as a message from the
human, class INFO,
through message.send on the human transport. It goes the same way as one
sent from the CLI, with class INFO where mosaic talk uses REQUEST (lead
decision 95). Decisions are still answered in a
terminal. There are no other message classes, no corrections and no seat
conversations here.
What --send grants: two kinds of client can send a human instruction to
any role while it runs.
- Anyone the proxy lets through as ID.
- Any local process that reaches the port and sets HEADER to ID with
Host: NAME. The user id is not a secret. This is the same class of exposure as the human CLI (lead decision 62), accepted for the PoC by lead decision 95.
The request is POST /api/bus/message with a JSON object {to, body, in_reply_to?}. Nothing is sent unless all of these hold, each checked on
every request:
- Host is NAME and HEADER is ID, once: otherwise 401
sign-in required. There is no loopback path. The operator's browser on127.0.0.1reads but can't send, and a loopback Host with HEADER is 403proxy Host refused, as above. - Origin is exactly
https://NAME, else 403cross-origin request refused. Sec-Fetch-Siteis sent once and is exactlysame-origin, else 403cross-site request refused. Browsers set it, and page script can't.- Content-Type is
application/jsonand the body is at most 16384 bytes and a JSON object withtoandbody, optionallyin_reply_to, and no other key. Anything else is 400invalid-request.
The Console doesn't judge to or the body beyond that; the broker does,
with message.send's own rules. Its body limit, 32768 characters, is past
the request limit, so the 16 KiB limit binds first. A broker refusal means
nothing was written. It answers { error, message, sent: false } with the
read routes' statuses: 400 for unknown-role, invalid-request,
message-not-found and request-too-large, 403 for human-required and
read-only, 503 for not-configured and no-bus-host, 502 otherwise.
When the transport can't say what happened (a timeout, a child that exits
without a code, an answer that can't be read or that has no message id), the
message may have been written. The answer is 503 outcome-unknown with
sent: "unknown". Nothing retries, in the server or in the page.
Each send that gets past the sign-in check writes one line to the Console's
stdout: the time, send, the user, the to value (JSON-quoted, at most 64
characters), then id=<message id> or error=<code>. The body is never
logged. A request refused at sign-in or by the shared Host and Origin
checks writes no line.
On the page:
- The draft, role and text, survives the ten-second refresh, kept in page memory like a Reply draft. Reloading the page loses it. Send is disabled while a send is pending.
- Sent shows the message id and links its trail,
#/trail/message/<id>: the message and its deliveries. A reply is not in the trail; it shows under the receipt (below). The text is cleared unless it was edited during the send. - Not sent shows the broker's code and keeps the draft. A redirect counts as not sent: the Console never redirects this route, so only the sign-in proxy does, when its session has expired. The page asks for a reload to sign in again, and signs out (row 95, below).
- Outcome unknown says the message may have been sent, keeps the draft, and
has no trail to link, since no id came back. Check the role's side, or
the bus trail from a terminal, before sending again. A 408 counts as
unknown, here and on the board's Reply, since a proxy can answer it
after forwarding the request (Envoy's
stream_idle_timeout).
The role's reply comes back as a message to the human with in_reply_to
set to the sent message's id. Since row 81 (#1575) the Sent receipt lists
those replies, oldest first, each with its role, time, class, body and a
trail link. It reads them with the bus replies read on every render, so a
reply shows on the next ten-second refresh or Read again. "No reply yet"
means the read returned none; "could not be read" means it failed, and the
next refresh tries again. The bus returns at most 50; at 50 the receipt says
only the first 50 show. The list belongs to the last Sent receipt: a new
send starts its own, and reloading the page drops it.
The Console only shows replies. It never receives a message or marks one
delivered or read: replies writes no deliveries row. So Jason's next
mosaic talk in a terminal still prints each reply, within talk's limits:
- talk receives only after it sends its own message;
--wait 0returns straight after the send and receives nothing;- otherwise each receive returns every undelivered message to the human, up
to 100 a call. The broker records each one delivered when
message.receivereturns it, so the next receive doesn't return it again; talk prints each one and marks it read.
Use
- Waiting on you uses the board's
waitingOnYouflag across all projects. - Select a project on the left to filter the session table. Counts show visible rows out of the total when Hide offline or Hide seen hides anything.
- Select an agent to open its inspector. Arrow keys move between table agent buttons, Enter opens, and Close or Escape in the inspector returns focus.
- Seen clears the row from Waiting on you until the board detects new activity. The collapsed Seen section keeps those rows available; Unsee returns them.
- Reply appears only where the board's registration permits it. A failure shows
the board's stderr and keeps the draft. Transport success is the board's
deliveredreceipt, not proof that the seat processed the message. - Refresh runs the existing board scan. Automatic refresh uses the board page's ten-second interval; Pause stops it. Failed refreshes keep the last snapshot with a warning. There are no automatic action retries.
- Palette and appearance come from
tokens.css(see "Shell" below) and persist in this browser when local storage is available. Appearance defaults to System. Settings (row 54) shows the same control and nothing else that changes. - History opens a read-only conversation view for a seat, from its Waiting card, its table row or its inspector (#1507, CHAT-02). It shows the whole branch with nothing clipped. Tool calls, tool results and thinking start collapsed. Session text is always shown as text: Markdown stays as source, and terminal controls, bidi controls and marks show as visible symbols. Reply in the view uses the same board reply path as the inspector.
- The view checks for new entries on the same ten-second refresh, and Pause stops it. It never switches on its own. A fork, a newer session for the seat or a rewritten file each shows a marker with a button to open the other history. Reloading after a rewrite keeps the branch the view was on; if that branch is gone, the view opens the latest one and says so. Session lists older sessions for the seat, and is empty while the list is read or when that read fails. Only repository Pi seats have history; other harnesses say so.
Drafts and receipts stay in page memory, including across refresh, inspector changes and a stale registration. Reloading or closing the page loses them. A pending send stays disabled across refresh; new text typed during a send is not cleared by the earlier send's success. If the proxy loses the response, delivery may be unknown: inspect the seat before sending again.
Inbox, tasks, agents and trails (slice 1 S5, #1522)
The Console sidebar adds Inbox, Tasks and Agents next to the control board,
which stays at #/. Routes: #/inbox, #/inbox/<decision>, #/tasks,
#/tasks/<vikunja:project/task>, #/agents, #/trail/task/<ref>,
#/trail/decision/<id> and #/trail/message/<id> (row 76), with ?kind= filters on a trail. The design
note is agents/dewey/work/wui/SLICE1-VIEWS.md.
- Every view reads the four verbs of the Q1 module (
packages/bus/src/views.mjs, lead decision 56):inbox,tasks,agentsandtrail. The CLI reads the same functions, so both show the same broker data. - Nothing writes but the "Message a role" box under
--send(above). A decision showsmosaic decide <id> <key>for each choice, with Copy, which only puts the command on the clipboard. It is answered in a terminal (REQ-DEC-3, Q4). Seen is not set from the browser either. The command has no--business, somosaic decideuses the running bus host's business, as the Console does by default. From a Console started with--businessfor another business the copied command refuses (no bus host, or no such decision) rather than answering elsewhere. - What no Q1 verb returns is labelled where it would show, for example "Bot names: not in the Q1 module" (lead decision 63). Nothing is guessed.
- A task's list row can only say its current state came from a Vikunja read, since the list has no trail. The task page reads the trail and tells a task the stack never created from one changed in Vikunja after the stack wrote it. A stale poll read is shown in the snapshots and never as current.
- The task page's "Who may write" section lists each field with the verbs that write it and the writer each verb checks: pm, the assigned role or any role (row 61, lead decision 113). Whether a write also needs a resolved decision is the role's authority, on the Business page. If the table read fails, the section says why and the rest of the page still shows.
- The Tasks table has Requirement and Priority columns, from the
tasksrow'srequirementandpriority(row 58, #1547). Either one shows "none" when the row has none. A tombstone's page shows the priority of the last snapshot that read the task. - All bus- and agent-authored text is rendered inert: controls show as control
pictures or
[U+XXXX], and Markdown stays as source. - Views refresh on the board's ten-second interval and stop on Pause. Focus, open sections and the copy status survive a refresh.
- A failed read keeps the last good answer for that read and says it is old,
with the time it was read. A refusal (403,
not-configured,no-bus-host, or row 54'squeue-refused) never shows kept rows (row 53), and it drops every kept read: the Inbox count and the palette's decisions, tasks and queue rows go too, until a read succeeds. A page never read, or refused, shows the failure instead, titled "Bus refused the read" (403: the bus refused the Console's read, for examplehuman-requiredfrom a Console started inside an agent run), "No bus to read" (no system config, or no bus host and no--business) or "The read failed".
Agent state (row 59, #1548)
#/agents adds State and Attention columns. State is the board's mark
(● working, ◐ waiting, – idle) from the bus agents verb, with the number
of messages and since when for a working role. Attention shows
"Input needed:" with the first line of the role's first open decision,
linked to #/inbox/<id>, a blocking chip, and "+N more" to the Inbox when
there are others. The page says what each state means. It writes nothing;
the server passes the rows through unchanged. How the bus derives the state
and its limits: packages/bus/README.md, "Agent state".
Board: held roles and signing out (row 95, #1589)
The Board (#/) opens with Held roles: each role a run holds on the
bus, from the same bus agents read as #/agents, including a claim with
no address ("no address"). Each row shows the role, its state and attention
(as in "Agent state"), the address, the holding run with its harness, and
since when. With send on (--send, "Message a role"), each row has a
Message action: it opens #/inbox with the send form addressed to that role
and the caret in the message. Without send there is no action, and the
section says so. A refused or failed first read says why and shows no roles
rather than a guess; a failed later read keeps the last roles under a stale
warning.
The machine scan's sessions (Waiting on you, Seen, All sessions and the
project list) sit behind Show machine sessions, off by default. While
it is off the page makes no /api/board request and polls nothing for it.
The choice is kept in this browser's local storage under
mosaic-console-board ({"machine":true}), like the appearance; where
storage is refused it starts off on each load. Turning it off drops the
board data and closes an open conversation.
Every Console fetch the page makes uses redirect: 'manual'. The Console
never redirects, so a redirect answer (opaqueredirect) is the sign-in
proxy's: its session has ended. The first one, on a board read, a bus read,
a page read or a send, signs the page out:
- One line says "Signed out. Reload the page to sign in." No NetworkError, failure banner or stale warning is shown for it.
- Everything drawn from a Console read is removed (the board, the held roles, the bus views, an open conversation and the inspector), because none of it can be refreshed. A read that was in flight is dropped when it lands.
- Refresh and Pause are disabled; nothing polls and no route change reads.
- The send form, if open, keeps its draft and receipt so the text isn't lost; a further Send is refused in the page ("signed out"). A board Reply that meets the redirect is not sent, and its inspector closes with the rest of the board, so its draft isn't shown again.
- Reloading the page goes through the sign-in proxy again.
How long a sign-in lasts is the edge's setting (ops-01), not the Console's.
Shell (row 53, #1542)
The Console is restyled to docs/design (IMPLEMENTING.md). It is a restyle
only: no new data, route or write.
src/public/tokens.cssandsrc/public/icons.svgare byte copies ofdocs/design/tokens.cssanddocs/design/icons.svg;tests/shell.test.mjsfails if either differs. Editdocs/design, rebuild there, then copy.- Colours come only from
tokens.css, selected bydata-paletteanddata-modeon<html>. The page sets no colour inline. Appearance System removesdata-mode, so the OS light or dark setting applies; it is the default, with Harbor. The choice is kept undermosaic-console-appearance. shell.cssloads last and holds the restyle: radii--r6px and--r-lg10px, the type scale, tabular numbers, the command bar, section list, dense table, inspector, status marks, class chips, toast, command palette and not-found page. The earlier CSS files are unchanged.- JetBrains Mono 400, 500 and 700 come from the official JetBrains release
(v2.304);
assets/fonts/jetbrains-mono-sources.txthas the URL and sha256 of the archive and each file, andjetbrains-mono-OFL.txtthe licence. Manrope has no 800 file here, so 800 headings render at 700. - Ctrl+K (or the search field) opens the palette over views, inbox decisions, tasks and (row 54) queue rows from the last queue read; arrows move, Enter opens, Esc closes and returns focus. Arrow keys also move between task links in the bus tables.
- Copy reports in a toast. If the clipboard is unavailable, the toast stays until dismissed and the command is selected for Ctrl+C. When no toast shows, the toast is an empty live region, visually hidden but still in the accessibility tree, so the first copy is announced.
- The command bar shows, in UTC, when the board was scanned and the bus read, and says when the last attempt failed. A board refusal drops the last scan and everything drawn from it: the cards, the table, the inspector, the project filter and tree, the counts, the footer and the scan line all say the board was not read. A failed read with no scan held, after a refusal or before the first scan, shows the board empty and says the read failed; it never brings back rows from a refused scan. A refusal also closes an open conversation and clears its history. If that removes what had focus, focus moves to the page.
- From 760px the footer is a one-line status bar and the side column fits between it and the command bar. Below 760px the section list is one strip that scrolls inside itself.
Queue, Business and Settings (row 54, #1543)
Three read-only views, in the section list and the palette. Each has the same five states as the bus views: loading (the skeleton names the source), normal, empty, a failed read over kept data (labelled stale, with its time), and a refusal that shows nothing kept. The command bar names the source: "Queue read …", "Business not read: refused". A refusal shows the code in mono and the module's own message, redacted by the server.
#/queuelists every row with its state, owner, reviewers, brief and last update, filterable by state.#/queue/<id>adds the gate, issues, claim,after, note and review rounds. Rows move only withscripts/mosaic queue movein a terminal; the row page shows that command with a Copy button and has no button that moves a row. The brief is shown as its path and anchor, because Console serves no repository files.#/businessshows the business named by--business, else the running bus host's business: the human, the arbiters as instance and kind ("pm (delivery), cto (technical)"), projects, launch rules, the vars on an allowlist, each role instance with its holder and harness, the authority of each instance for each action as the business package classifies it (resolveInstance,classify), and credentials as metadata only: service, account, role, expiry or rotate-by date, and a state worked out from the date alone. A missing or invalid business file is anot-configuredrefusal with the business module's own text.#/settingsshows appearance (the row 53 control, kept in this browser), the notifier binding by name only, and about: the Console's loopback address,RELEASE, the business and the board URL. A Settings refusal still shows appearance, because it needs no read.
Console never reads a token, a token file or a credential variable, and no
response, page or log carries one, a file path or a Discord channel, guild or
user id. The server redacts every message and string field before it answers
(redactor in src/reads.mjs): the config directory becomes <config>, the
data root <dataRoot>, any other absolute or ~/ path <path>, a 17 to 20
digit run <id>, and a JSON parse error's quote of the file is dropped. The
startup log goes through the same redactor. A path starts at the beginning
of the text or after a space, an opening bracket, a quote, =, ,, < or
:, so file:/x and file:///x are redacted and https://host/x is not.
Known gap: a path stops at the first space, so a path containing a space is
redacted only up to that space and the rest shows. A bare ~ or ~user/
is not treated as a path, and neither is a relative path.
Data and boundaries
GET /api/board, /api/conversations and /api/conversation, and POST
/api/seen and /api/reply proxy only those board paths. Only the two
conversation routes carry their query string; the board validates it. POST
bytes and upstream status/JSON are preserved. GET /api/config returns the
configured board URL for the page's error message. No scanner, registration,
session reader or transport is implemented here. The session reader is
packages/conversation, served by the board.
GET /api/bus/inbox, /api/bus/tasks, /api/bus/agents,
/api/bus/trail?subject=<id>, /api/bus/decisions?list=roles|recent,
/api/bus/credentials and /api/bus/replies?id=<message id> are the bus
reads (src/bus.mjs). Any other method is 405. The subject and the id must
match the broker's identifier rule, and the list must be roles or
recent, else 400. replies (row 81, #1575) is the messages whose
in_reply_to is that id, for the Sent receipt; the bus refuses an id that
names no message (message-not-found, 400). decisions (row 57, #1546) is for the Inbox tabs "Routed to roles" and
"Recent"; "Human and reader paths" in packages/bus/README.md has what each
list holds. credentials (row 60, #1549) is the bus host's credential
status: service, account, role, date and state for each credential it
loaded, never a token, file path or variable name. No page uses it yet; the
Business view still shows the business file's metadata.
Each read runs the S4 human transport (packages/bus/src/human-cli.mjs) as
a child process, so a slow read doesn't stall the server. The bus checks
that child's ancestry, which ends at the Console process; it never sees the
browser or any other client of the port. So while the Console runs,
anything that can connect to its port (7330 by default) reads what a reader
capability reads ("Human and reader paths" in packages/bus/README.md):
the inbox, tasks, agents, trails, the two decision lists and the credential
status. That includes
a T3 seat using curl and a managed S6 session, since S6 doesn't confine
the network. Without --send no route
writes (Q4), so the exposure is reads only; with it, see "Message a role". Slice 1 doesn't change this
(Filbert F1 on #1522). Answers are { rows, at }; failures are { error, message }
with 403 for a refusal, 503 for no bus or no answer, and 502 for an answer
that can't be used. The bus needs the system config
(~/.config/mosaic-dev/config.json); without it the board still serves and
the bus routes answer not-configured.
GET /api/queue, /api/queue/<id>, /api/business and /api/settings are
row 54's reads (src/reads.mjs), under the same Host, Origin and CSP rules.
Any other method is 405, and a queue id that is not a positive integer is
400. The queue is read in a child process (src/queue-read.mjs) through
rows() from packages/queue/src/store.mjs (lead decision 83): the same
read as queue list, with no write and no network. Like list, it takes
the queue lock only to recheck a disagreement before reporting it. The child
imports the store inside its try, so a store that fails to load reports
"the queue read failed". Only the store's exit 2 (invalid data or refused)
forwards the child's text, as queue-refused; any other exit is the fixed
read-failed message "the queue read failed (exit N)", because that stderr
may be Node's own, with a file:// path and a stack. The business
read uses packages/business in process. No read writes, calls out or adds a
dependency. Answers carry at; failures are { error, message } with 503
for not-configured, no-bus-host, queue-refused and read-failed, 404
for a row that isn't there, 400 for a bad address and 502 otherwise. Settings
answers 200 even without a system config, so appearance always works; its
notifier part then says not-configured.
GET /api/task-fields is row 61's read (#1550): the task field table,
packages/tasks/fields.json, loaded and checked by loadFields() on each
request. It is repository data, so it answers without --business or a
system config. The answer is the table itself, with no at. A table that
fails its check is refused whole with 503 read-failed and the loader's
message, which names the file and never a path. Any other method is 405.
Console's shared CSS, Console CSS, brand.js and local Manrope fonts were copied
unchanged from agents/dewey/work/wui/. Font license and source URLs accompany
the files under src/public/assets/fonts/. live.css contains the live-page
adaptations; original mockups and unrelated pending design work stay untouched.
Unsupported mockup fields and controls are listed in docs/plans/DEFERRED.md.
The source tree is a project filter; no invented workspace registration tree
or mockup session hierarchy is presented as live data.
Verify
node --test packages/webui/tests/
node --test packages/control-board/tests/ packages/seat/tests/ packages/ledger/tests/ packages/mosaic/tests/
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/browser.test.mjs
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/conversation.test.mjs
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/bus-browser.test.mjs
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/shell.test.mjs
node --test packages/webui/tests/reads.test.mjs packages/webui/tests/views.test.mjs
Node's test runner and installed /usr/bin/chromium are required. Set CHROMIUM
to another installed Chromium path. No npm download is needed. The copied CDP
helper starts a separate temporary browser profile and removes it on exit.
Tests use temporary board session/registration files and stub the board's
transport. They never send to real seats or read real session logs.
Browser tests exercise the rendered real board fixture, exact counts, project filtering, keyboard return focus, Seen/Unsee, success/failure receipts, draft and caret preservation, pending-send exclusion, storage denial, stale registration, loading/empty/malformed/unreachable states and hostile text. Contrast is measured on 330 rendered samples across ten palettes and three modes. Layouts are checked at 320, 390, 768, 1440 and 2560px. Horizontal scrolling is intentional within the dense table; the page itself must not overflow.
Conversation tests run the real board routes over temporary repository-layout session files. They cover full-length answers, collapsed tools and thinking, hostile content rendered inert, malformed-line and reconcile markers, forks, and the return flow: a send from the view, a tool call and a delayed result while a draft is typed, a peer message, a 4.5-million-character answer split into continuation parts, and a relaunch mid-turn.
Row 95's tests (board-held.test.mjs) seed an in-process broker with one
claim that has an address and one that has none. They check the held roles
with the machine sessions off by default and no /api/board request, the
toggle on and off and its persistence across reloads, Message addressing
the send form to each role, no action without send, and that every fetch
asked for redirect: 'manual'. Then a redirect answer on a board read, an
agents read and a send: one line, no NetworkError, nothing stale left, no
further request on a route change, Refresh or the toggle, and the draft
kept. send-browser.test.mjs also checks the sign-out after its redirect
receipt and that a reload works again. Since the held roles lead the Board,
the shell test's no-scroll check at 1440px uses a 1000px-tall window.
Bus tests (bus.test.mjs, bus-browser.test.mjs) run the routes and the
views against an in-process broker read through its reader session, so every
row has the broker's own shape. humanCall runs against a stub CLI script.
They cover each view, the gap labels, inert hostile text, Copy, the stale
fallback and each failure title, and that no route writes. They never start
the human CLI against a live bus.
Shell tests (shell.test.mjs) check the copies, the font hashes and the kept
strings, and in the browser at 400px: System mode under an emulated dark and
light OS, the empty, stale and refused states of the board and the bus views
(a failed first read drawn empty, a board refusal with the inspector open and a project picked, then a failed
read, a failed session list, and a refusal with a conversation open and where
focus lands after it), not found, the palette, the toast,
keyboard and visible focus, the section strip at 360 and 400px, no sideways
scroll, no script error and no request other than GET. A second test walks
the Board, Inbox, Tasks, Agents and a decision trail through loading (a held
read), normal, a failed read over kept rows, and a refusal by the bus
(human-required) and for want of one (no-bus-host). It then checks that a
refusal empties the palette and the Inbox count, from a view, from the
palette's own reads and from the board page, and that palette labels are text.
Row 54's tests share the seeded fixture in reads-fixture.mjs: a scratch
queue whose row note names an id and token paths shaped like real notes
(absolute, ~/ and after file:; the store refuses < in a note, so the
<…> case is in the redactor test), and a business,
notifier config and Discord binding that hold token files, an environment
variable name, an agent model var holding a path, and guild, channel, bot
and user ids. Row 9's After entry has the {id, when} shape the real queue
stores. reads.test.mjs checks each
route's body and status, the redactor, the credential state, methods and
Origin, and that no body carries any seeded value or the temporary directory;
that a store.mjs with a syntax error, or none, gives the fixed message with
no path; and that the child's timeout, output cap and non-2 exits hold, over a
fake child script.
views.test.mjs walks Queue, a queue row, Business and Settings through the
five states over a stub read, with each refusal code the view can meet, at
400px; checks that a refusal leaves no queue row in the palette, that no view
has a control but Copy and Read again, and that the row page shows the
queue move command, that the 10 s refresh keeps focus on a Settings mirror
select, and that the palette keeps its queue rows after its own inbox and tasks
reads answer; then, over the seeded fixture, that the Arbiters line reads the
business file's object, that neither the page nor
any response it read carries a seeded value, that a missing or invalid
business file shows the module's text, and that the startup log names no
config path.
No root CI workflow is configured for this package. These local tests are not a claim of CI, deployment, live-seat delivery or user acceptance.
User test and rollback
Gate E belongs to Jason on Monday 2026-09-14. Use Console to find who is waiting, open a registered repo seat, reply and see its next answer after a scan. Mark a completion Seen and find it again in Seen. Record any reason to open the board's own page or a repo seat terminal in DEFERRED.md. Pass is Jason's end-of-day say-so, not a test result. Keep #1507 open pending that ruling.
Rollback requires only stopping the WebUI and using the unchanged board at 7331. Revert the scoped WebUI commit to remove it; there is no data migration to undo. Seen and reply actions already taken belong to the board and are not rolled back.