Row 76 round 2, owner Dewey, candidate manifest 04ecf1b5 on 729da72b.
Carries decisions 99 and 100: Darkwing's and Filbert's round 1 notes,
the README proxy contract line, and a 408 counted as unknown.
Approved by Filbert (27685) and Darkwing (27688); Sage's gate green
03:25-03:29Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
529 lines
31 KiB
Markdown
529 lines
31 KiB
Markdown
# 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:
|
||
|
||
```sh
|
||
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)
|
||
|
||
```sh
|
||
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 required`
|
||
before any route runs, so no bus read or board call is made. An Origin,
|
||
when sent, must be `https://NAME`.
|
||
- A request whose Host is loopback on the Console's port (`127.x`,
|
||
`localhost` or `[::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: NAME` would otherwise skip the sign-in check.
|
||
- Every method other than GET and HEAD answers 403 `read-only`, whatever the
|
||
Host. `/api/seen` and `/api/reply` never reach the board, so Seen and Reply
|
||
fail in the page with that refusal. The one exception is POST
|
||
`/api/bus/message` when 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, `localhost` included, is the
|
||
operator's path and skips the header check, so a proxy that forwarded a
|
||
client's `Host: 127.0.0.1:7332` would 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_errors` with an `error_page`,
|
||
for example). Under `--send` the page reads a 4xx other than 408 as "not
|
||
sent", so a proxy that turned the Console's 503 `outcome-unknown` into 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 it from his own shell (lead decision 94).
|
||
The script takes no `--send`, so a unit it installs is read-only.
|
||
|
||
## Message a role (row 76, #1570)
|
||
|
||
```sh
|
||
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. 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 on `127.0.0.1` reads
|
||
but can't send, and a loopback Host with HEADER is 403 `proxy Host
|
||
refused`, as above.
|
||
- Origin is exactly `https://NAME`, else 403 `cross-origin request refused`.
|
||
- `Sec-Fetch-Site` is sent once and is exactly `same-origin`, else 403
|
||
`cross-site request refused`. Browsers set it, and page script can't.
|
||
- Content-Type is `application/json` and the body is at most 16384 bytes
|
||
and a JSON object with `to` and `body`, optionally `in_reply_to`, and no
|
||
other key. Anything else is 400 `invalid-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 it. 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.
|
||
- 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. The Console doesn't
|
||
show it yet; that view is row 81 (#1575). Jason's next `mosaic talk` in a
|
||
terminal prints it, within talk's limits:
|
||
|
||
- talk receives only after it sends its own message;
|
||
- `--wait 0` returns 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.receive` returns 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 `waitingOnYou` flag 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
|
||
`delivered` receipt, 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`, `agents` and `trail`. 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 shows `mosaic 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`, so `mosaic decide` uses the running bus host's
|
||
business, as the Console does by default. From a Console started with
|
||
`--business` for 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 Tasks table has Requirement and Priority columns, from the `tasks` row's
|
||
`requirement` and `priority` (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's `queue-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
|
||
example `human-required` from 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".
|
||
|
||
## 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.css` and `src/public/icons.svg` are byte copies of
|
||
`docs/design/tokens.css` and `docs/design/icons.svg`; `tests/shell.test.mjs`
|
||
fails if either differs. Edit `docs/design`, rebuild there, then copy.
|
||
- Colours come only from `tokens.css`, selected by `data-palette` and
|
||
`data-mode` on `<html>`. The page sets no colour inline. Appearance System
|
||
removes `data-mode`, so the OS light or dark setting applies; it is the
|
||
default, with Harbor. The choice is kept under `mosaic-console-appearance`.
|
||
- `shell.css` loads last and holds the restyle: radii `--r` 6px and `--r-lg`
|
||
10px, 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.txt` has the URL and sha256
|
||
of the archive and each file, and `jetbrains-mono-OFL.txt` the 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.
|
||
|
||
- `#/queue` lists 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 with
|
||
`scripts/mosaic queue move` in 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.
|
||
- `#/business` shows 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 a `not-configured` refusal with the business module's own text.
|
||
- `#/settings` shows 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` and
|
||
`/api/bus/credentials` are the bus reads (`src/bus.mjs`). Any other method is 405. The subject must match
|
||
the broker's identifier rule, and the list must be `roles` or `recent`, else
|
||
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`.
|
||
|
||
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
|
||
|
||
```sh
|
||
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.
|
||
|
||
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.
|