Wheelhouse docs

Building the web UI#

ui/ is a React and TypeScript single-page application built by Vite. It has three runtime dependencies — React, React DOM and React Router — and no state library, no component library and no data-fetching library. It talks to the agent over the same public HTTP API a script would use, holds no credential of its own, and is built to a directory the agent serves. Building it is npm ci and vite build; running it in development is a dev server that proxies the agent so the application is same-origin in both places.

The commands#

bash
cd ui
npm install                                        # or npm ci, which CI uses
WHEELHOUSE_AGENT=http://127.0.0.1:8090 npm run dev # http://localhost:3000
npm run build                                      # tsc, then vite build, into ui/dist
npm run preview                                    # serve the built bundle
npm run shots                                      # screenshots against a running agent

npm run build is tsc && vite build — the type check is part of the build, so a type error is a build failure and not a warning. CI runs the two separately so it can report which one failed:

bash
npm ci --no-audit --no-fund
npx tsc --noEmit -p .
npx vite build --logLevel warn

The dev server proxy#

ui/vite.config.ts proxies three paths at whatever WHEELHOUSE_AGENT names, with the WebSocket upgrade enabled on the stream:

PathProxied
/apiYes
/healthYes
/api/streamYes, with ws: true

The default target is http://10.0.0.50:8090, which is the development bench, so set WHEELHOUSE_AGENT unless you are on it. Point it at https://<router>:8443 to drive an appliance from a local dev server.

Same-origin is not a convenience here. The browser holds a session cookie, and mutations echo the session's CSRF token in X-Wheelhouse-CSRF — a cross-origin dev setup would diverge from production exactly where authentication is decided.

What the build produces#

ui/dist/, containing index.html, hashed assets under assets/, the two fonts under fonts/ with their OFL texts, the icons, site.webmanifest and theme-boot.js. That directory is what packaging/build-deb.py copies to /usr/share/wheelhouse/ui, and what --ui-dir points at.

Every route page is code-split. From ui/src/App.tsx:

tsx
const Dashboard = lazy(() => import('./pages/Dashboard'))

The comment above that block records why: statically importing all of them put the whole product in one 535 KB file that a browser had to parse before it could draw the Dashboard, on a box whose CPU is busy routing. The login screen and the shell stay in the main chunk because they are the critical path. One Suspense boundary covers the whole route table, and Loading paints nothing for its first 400 ms, so a navigation off a warm agent looks instant rather than flashing a spinner.

The two fonts#

Inter and JetBrains Mono are shipped with the product, as variable woff2 files under ui/public/fonts/ with their SIL Open Font License texts beside them. The comment in ui/src/index.css gives the reason: a router has no guaranteed egress, so a CDN link would mean the identity collapses to a system fallback on exactly the boxes that need it most.

Theming#

Colours are CSS custom properties defined once per theme in ui/src/index.css and exposed to Tailwind as semantic names in ui/tailwind.config.js:

js
colors: {
  bg: 'rgb(var(--bg) / <alpha-value>)',
  surface: 'rgb(var(--surface) / <alpha-value>)',
  ink: 'rgb(var(--ink) / <alpha-value>)',
  accent: 'rgb(var(--accent) / <alpha-value>)',
  // …
}

Components use bg-surface, text-muted and border-line, never a raw palette class, so one class set renders in both themes. darkMode: 'class', and ui/public/theme-boot.js runs before first paint so the page does not flash the wrong theme.

Screenshots#

ui/scripts/screenshot.mjs drives the built UI in a real browser against a running agent, captures a full-page screenshot per route and collects the console errors:

bash
npm run build
ip netns exec vlab npm run shots -- '/,/nat,/firewall'

It needs playwright-core (a dev dependency) and a Chromium build, and it prints the first fourteen lines of each page's text so a page that rendered empty is obvious. The vlab network namespace is the development bench's, and it is the only place the router, and therefore the agent, is reachable from — see wheelhouse-infra for that.

What is missing, and what that costs you#

ui/ has no automated tests. CI runs tsc --noEmit and vite build and nothing else. docs/ui.md states the consequence plainly:

A change to lib/api.ts or lib/staging.tsx can break every editor in the product with CI still green; test by hand until that is fixed.

Treat those two files, and components/CommitBar.tsx, as the blast radius. A change to any of them wants a browser session against a live router before it is pushed.

See also#

Checked against#

ui/package.json · ui/vite.config.ts · ui/tsconfig.json · ui/tailwind.config.js · ui/src/App.tsx · ui/src/index.css · ui/scripts/screenshot.mjs · ui/public/fonts/README.md · agent/main.go · packaging/build-deb.py · docs/ui.md · .forgejo/workflows/ci.yml

Updated 2026-09-02 development ui react vite