docs: WebUI preview brief, signed-in read-only Console behind the edge proxy

Mos approved the cuts 2026-10-11: forward auth at the edge, a user unit
beside the bus, a preview hostname, read-only. No code until Jason
answers Q91 (amending lead decision 48).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-11 16:53:21 -05:00
co-authored by Claude Opus 5.5
parent c26341b830
commit 620fb174f6
+106
View File
@@ -0,0 +1,106 @@
# WebUI preview behind sign-in (2026-10-11)
Status: written by Sage, lead. It follows `docs/plans/BRIEF-TEMPLATE.md`.
One row. No code starts until Jason answers Q91 (amending lead decision
48 for a signed-in preview reachable beyond localhost). Mos relays the
answer. If the ruling narrows or changes what follows, this brief is
amended first.
## WebUI preview: a signed-in, read-only Console behind the edge proxy
### Problem
Jason asked for an ETA for the refactor WebUI at mosaic.woltje.com. Today
the Console can't be reached from anywhere but the host it runs on, by
design:
- `startServer` refuses to bind to a non-loopback address
(`packages/webui/src/serve.mjs:82`) and answers 403 to any request whose
Host isn't loopback on its own port (`serve.mjs:92`). A reverse proxy
that forwards the public Host is refused.
- The Origin check (`serve.mjs:96`) compares against `http://<Host>`, so a
browser on `https://<public name>` is refused as cross-origin.
- There is no sign-in. Anything that connects to the port reads what a
reader capability reads (`packages/webui/README.md`, the paragraph on
the bus views), and the README forbids a public proxy.
- Two routes write: POST `/api/seen` and `/api/reply` go to the board.
- Nothing runs the Console as a service. It is a foreground `serve`.
Mos approved these cuts for the preview (2026-10-11): sign-in at the edge
through Pocket ID forward auth, not login code in the Console; a systemd
user unit beside the bus; a preview hostname, so the old build at
mosaic.woltje.com stays; read-only; not held for S7 (row 42) or Gate E
(row 5). ops-01 owns the Pocket ID client, the DNS name, TLS, the proxy
and the route from the edge to this host. Jason approves that route
before anything is served.
### Owner and reviewer
Owner: Dewey, who owns the WebUI, after their row 59 review. Reviewers:
Darkwing and Filbert.
### Files owned
- `packages/webui/src/serve.mjs`, `packages/webui/src/cli.mjs`,
`packages/webui/README.md` and `packages/webui/tests/`
- a new `scripts/webui-service.sh` and its unit template, beside
`scripts/bus-service.sh` and `packages/cli/systemd/mosaic-bus.service.in`
- the test for that script, in the package that holds the template
- `docs/TOOLS.md`, the new script's entry only. Row 70 also edits this
file; whichever lands second rebases on the first.
- `agents/dewey/work/queue-<row>/`
### What ships
A preview mode, off by default. Without its flags the Console behaves
exactly as today, and the existing tests show it.
- `serve --public-host NAME --auth-header HEADER --allow-user ID`, all
three or none. NAME is a hostname, no port or scheme. The bind stays
loopback; nothing here listens beyond it.
- In preview mode a request's Host must be NAME, or loopback on the
Console's port as today. An Origin, when sent, must be `https://NAME`
for a NAME request, and the loopback form for a loopback request.
- A NAME request must carry HEADER with a value equal to ID. Missing or
different answers 401 with no bus read made. Loopback requests are
unchanged, so the operator's own browser on the host still works.
- In preview mode every POST answers 403 `read-only`, whatever the Host.
`/api/seen` and `/api/reply` don't reach the board.
- Tests for each refusal: wrong Host, wrong Origin, missing header, wrong
user, a POST to each write route, and a partial flag set refused at
start. A test that the loopback path is unchanged in preview mode.
- `scripts/webui-service.sh render|install|uninstall|status` for a
`mosaic-webui.service` user unit, on the `bus-service.sh` pattern: a
template, write to a temp file then rename, `daemon-reload` only, no
start, no restart. It runs the board's `serve` and the Console's
`serve` with the preview flags (as one unit or two, the packet says
which and why), with values from flags or an env file the script names.
The env file holds no secret: the header name, the user id and the
hostname aren't secrets.
- The README replaces "Do not expose either unauthenticated server
through a public proxy" with what the preview mode requires of the
proxy: it strips HEADER from every client request and sets it only
after Pocket ID signs the user in, it preserves Host, it terminates TLS,
and it is the only thing that reaches the port from off the host. It
says plainly that any local process can still connect and send HEADER,
the same exposure the README already describes for the port.
- The packet runs the Console in preview mode with `curl` against
loopback, showing each refusal and one allowed read, with no network
beyond the host.
### Out of scope
- OIDC or login code in the Console; Pocket ID config; DNS, TLS, the
proxy and the route from the edge (ops-01).
- Resolving decisions or replying from the browser. REQ-DEC-3 keeps
resolution in the CLI; changing that is a ruling and its own row.
- Live-seat conversations (CHAT-07) and any board change.
- A container image, CI deploy, or changes to the bus unit.
- Installing or starting the unit on dragon-lin. Sage does that after the
row lands, only once Jason has ruled on Q91 and approved ops-01's route.
### Gate
Darkwing and Filbert approve on the row's issue naming the candidate
manifest. The webui, control-board and cli node suites and every
`scripts/test-*.sh` green on Sage's gate rerun.