Session tree nests by working directory only; the daemon chips in the
header become a session tally, and the spawn form loses its host picker.
API calls move from /api/daemons/:id/* to /api/dirs, /api/recent-dirs,
/api/gitroot, and /api/info.
Maestro is now single-host: one daemon owns the machine's sessions and
clients talk to it directly. Removes the registry crate, the daemon's
registry client/broker/catalog mirror, JWT attach auth, cookie auth, and
the perimeter (T2) transport config.
The daemon API is now unauthenticated on its bind address, same trust
model as tmux. Session and CreateSessionRequest lose daemon_id;
RegistryEvent becomes CatalogEvent with no daemon variants; attach moves
to WS /api/attach/:id with no token.
Cross-host (T2-brokered) attach fails when no shared perimeter is
configured: the daemon rejects the upgrade with a structured 502
(no_perimeter_candidate / no_local_perimeter / perimeter_mismatch /
peer_unreachable), but maestro-web's WS proxy had already upgraded the
browser socket and merely dropped it on the upstream failure — the
attach pane flashed and vanished with no reason shown.
- maestro-web proxy: on a failed upstream handshake, relay the daemon's
error message to the browser as an application close frame (4502 +
reason) instead of a bare drop; parse the structured JSON error body.
- web attach.ts: on a 4000+ close with a reason, show it in the pane
header and terminal body and keep the pane up, rather than auto-closing.
- daemon: WARN at startup when no perimeter_id/private_addr is configured,
since cross-host attach is then unavailable.
- docs/INSTALL.md: document the required matching perimeter_id + per-host
private_addr for cross-host attach.
Closes#39
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SPA fallback returned index.html (HTTP 200, text/html) for ANY unknown
path, including missing /assets/*. index.html was also served with no
Cache-Control, so browsers cached the shell. After a redeploy rotated Vite's
content hashes, a cached shell requested its old /assets/index-OLDHASH.js,
the server answered with the HTML shell, and the browser tried to execute
HTML as JS -> blank page (seen on Android Chrome while the installed PWA,
with its own warm cache, kept working).
- Missing paths that name a file (have an extension) now 404 instead of
falling through to index.html; only extensionless SPA routes fall back.
- index.html / sw.js / manifest / icons: Cache-Control: no-cache.
- Content-hashed assets/: public, max-age=31536000, immutable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `parse_bind_addr`, `default_state_dir(subdir)`, and an `InvalidBindAddr`
variant to `maestro-config`, and route the daemon, registry, and web config
loaders through them. Each crate's hand-rolled `InvalidBindAddr` variant and
duplicated XDG state-dir resolution are gone; their loaders now `?` the shared
helper, which converts into their existing transparent `Inner` variant.
Also adds the shared `BroadcastTuning` struct (used by the next commit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`proxy_http` buffered request bodies with `to_bytes(.., usize::MAX)` and
materialized full upstream responses with `.bytes()`. Anyone reachable on the
bind address could POST a multi-GB body (the cookie middleware admits any
signed cookie) and OOM the process; a large upstream reply did the same.
Cap buffered request bodies at 8 MiB and return 413 on overflow; stream the
upstream response straight through via `bytes_stream()` → `Body::from_stream`
so it's never fully held in RAM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build.rs now builds web/dist before include_dir! evaluates, so the
checked-in .gitkeep placeholder is no longer needed for a fresh checkout
to compile (verified by building with web/dist entirely absent). Ignore
dist/ wholesale and update the static_bundle doc comment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Watch the embedded web/dist output (not just frontend inputs) so a
rebuilt bundle is actually re-embedded; assert pnpm build produced
web/dist/index.html; strengthen the embed test to require a vite-built
hashed asset reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
include_dir! now embeds a freshly built web/dist on every compile; pnpm +
node are hard build requirements. Fixes the placeholder served by
cargo install --git.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New crate crates/maestro-web/: axum server that
* embeds the web/dist SPA bundle via include_dir! (graceful placeholder when
pnpm build hasn't run yet — keeps fresh checkouts compiling)
* reverse-proxies /api/* HTTP to a co-located daemon (default
http://127.0.0.1:7070) via reqwest, stripping hop-by-hop headers
* reverse-proxies /api/attach/:id and /api/events WebSockets via
tokio_tungstenite::connect_async + a tokio task pumping both directions
* reads bind_addr / daemon_url / dev_proxy_target from the new [web]
config section; env-var overrides MAESTRO_WEB_BIND_ADDR / _DAEMON_URL /
_DEV_PROXY_TARGET
Integration tests spin up a mock daemon + maestro-web pair on random ports
and verify HTTP forward, SPA fallback, WS echo, and 502 on unreachable
daemon.
Adds a top-level Justfile (`just web` = pnpm install + pnpm build + cargo
build), crates/maestro-web/README.md (config + trust model + dev workflow),
and a docs/INSTALL.md section on optional web UI install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier today I picked option 1 from NAP-231's open question (run
`maestro-registry` locally on a different port). That was wrong: the
user-facing framing is "local web UI", and option 2 — a thin front-end
that serves the web bundle and reverse-proxies `/api/*` to whatever
registry the daemon is already registered with — is what actually
delivers that.
What the new crate does:
- `crates/maestro-web` ships a single binary `maestro-web`.
- It embeds the web bundle the same way `maestro-registry` does
(rust-embed against `web/dist/`).
- Mounts the bundle at `/` and a reverse proxy at `/api/*` (catch-all).
- HTTP proxy via `reqwest` — forwards method, headers (minus
hop-by-hop), body, and passes upstream response headers (including
`Set-Cookie`) back to the browser.
- WS proxy upgrades both sides and shuttles frames with
`tokio::select!`. Cookies on the initial `WebSocket` upgrade headers
are forwarded so the registry sees the same browser session.
- Reads `MAESTRO_REGISTRY` (upstream URL) and `MAESTRO_WEB_BIND_ADDR`
(default `127.0.0.1:8888`) from env.
- An integration test (`tests/proxy.rs`) stands up a mock upstream,
proxies an HTTP request with cookies through, and round-trips a text
WebSocket frame to verify the WS path.
Installer changes:
- Wizard now `cargo install`s `maestro-web` (not `maestro-registry`)
when the user opts into the web service.
- Unit template switched: `ExecStart=%h/.cargo/bin/maestro-web` plus
`Environment=MAESTRO_REGISTRY=<upstream>` +
`Environment=MAESTRO_WEB_BIND_ADDR=<bind>`.
- New `--web-registry <URL>` flag. Default in interactive mode: read
`registry_url` straight out of `~/.config/maestrod/config.toml`, so
the proxy lines up with what the daemon is using without further
prompting. Non-interactive runs without either of those error out
rather than guess.
Docs:
- `docs/INSTALL.md` updated unit body + flag table.
- `CLAUDE.md` repo-layout block now lists `maestro-web`.
148 tests pass workspace-wide (9 new in maestro-web, including the
proxy integration test; 15 in installer).