The browser reached the daemon through maestro-web, a second binary that served the SPA and reverse-proxied /api/* — two TCP connections and a WebSocket frame re-encode on every keystroke and every byte of PTY output. On one machine that hop buys nothing. The daemon now embeds the bundle and serves it off its own listener, behind a default-on `web` feature. `--no-default-features` builds without pnpm or node; MAESTRO_WEB_DIST still takes a prebuilt bundle. The router's fallback answers an unmatched /api/* with 404 rather than the SPA shell, so a client expecting JSON gets a status it can read instead of HTML. Removes maestro-web and the [web] config section. An existing [web] block is ignored, not rejected. The installer deletes a leftover maestro-web.service on upgrade. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
77 lines
2.8 KiB
Markdown
77 lines
2.8 KiB
Markdown
# Maestro web UI
|
|
|
|
Browser UI for Maestro. The daemon embeds this bundle and serves it off its own
|
|
listener, so the page talks back to the same origin over `/api/*` — session
|
|
catalog, spawn/kill, and terminal I/O.
|
|
|
|
## Framework choice: vanilla TypeScript
|
|
|
|
No frontend framework. The UI is small enough that the cost of pulling in
|
|
Preact / Solid / Leptos doesn't pay back. xterm.js does all the heavy
|
|
rendering; everything else is a session tree and a spawn form, both trivial
|
|
in plain DOM.
|
|
|
|
This decision is non-architectural — swap in a framework later without
|
|
touching the daemon if needed.
|
|
|
|
## Layout
|
|
|
|
```
|
|
src/
|
|
main.ts # entry point, hash-route dispatch
|
|
api.ts # daemon HTTP API client
|
|
events.ts # `/api/events` WebSocket subscriber
|
|
catalog.ts # session tree view (groups by working directory)
|
|
attach.ts # xterm.js + attach WS view
|
|
multiview.ts # tabs + split panes
|
|
spawnForm.ts # shared spawn form (preset tiles, project picker, worktree)
|
|
spawnModal.ts # dialog chrome around the spawn form
|
|
theme.ts # dark/light theme toggle + persistence
|
|
types.ts # mirror of `maestro-protocol` JSON shapes
|
|
styles.css # design-system tokens + component styles
|
|
mock.ts # DEV-only review fixture (see Design)
|
|
```
|
|
|
|
## Design
|
|
|
|
The visual system (OKLCH token palette, monospace-first typography, dark+light
|
|
theming, motion, components) is documented in [`../DESIGN.md`](../DESIGN.md);
|
|
the tokens live in the `:root` block of `src/styles.css`. Strategic intent
|
|
(register, brand, principles) is in [`../PRODUCT.md`](../PRODUCT.md). Theme
|
|
follows the OS by default and can be pinned via the header toggle (persisted in
|
|
`localStorage`).
|
|
|
|
`mock.ts` is a DEV-only fixture (stripped from production builds): visit the dev
|
|
server with `?mock` to seed a representative catalog without a running daemon,
|
|
`?mock&spawn` to open the spawn modal, and `?theme=light|dark` to pin a theme.
|
|
|
|
Routing is hash-based:
|
|
- `#/` → catalog
|
|
- `#/attach/<session-id>` → terminal
|
|
|
|
This keeps the static handler dumb (no SPA fallback needed). Concurrent
|
|
attaches: open the same `#/attach/<id>` URL in another tab.
|
|
|
|
## Dev
|
|
|
|
```sh
|
|
pnpm install
|
|
pnpm dev # http://localhost:5173, proxies /api → http://127.0.0.1:7070
|
|
pnpm build # → dist/, embedded into the maestro binary at cargo build time
|
|
```
|
|
|
|
The Vite dev server proxies `/api` and the WebSockets to a local daemon.
|
|
Default is `http://127.0.0.1:7070`; point it elsewhere by exporting
|
|
`MAESTRO_DAEMON_URL` before `pnpm dev`:
|
|
|
|
```sh
|
|
MAESTRO_DAEMON_URL=http://laptop.lan:7070 pnpm dev
|
|
```
|
|
|
|
## Production
|
|
|
|
`pnpm build` writes the bundle to `web/dist/`. Under its default `web` feature
|
|
the daemon embeds that directory at compile time and serves it from `/`,
|
|
alongside the `/api/*` routes. See
|
|
`crates/maestro-daemon/src/static_bundle.rs`.
|