Rewrites CLAUDE.md as the current-architecture reference, drops the registry from INSTALL/PRODUCT/DESIGN and the web README, deletes the registry Dockerfile, and marks the dated design specs superseded where they describe the registry, JWT auth, or cross-host attach. The installer loses its configure step — every config field now has a default, so a daemon starts with no config file at all.
6.0 KiB
maestro-web: build the SPA bundle at compile time
Date: 2026-05-28 Status: Approved (design)
Partly superseded (2026-08-22). Maestro is now single-host — the bundle is embedded in
maestro-web, not a registry binary. SeeCLAUDE.md.
Problem
maestro-web embeds the SPA bundle at compile time via
include_dir!("$CARGO_MANIFEST_DIR/../../web/dist") (crates/maestro-web/src/static_bundle.rs).
Only web/dist/.gitkeep is checked into git — the real bundle is gitignored and
must be produced by pnpm -C web build.
The installer's default source is cargo install --git (maestro-installer
build_source). A fresh git clone has web/dist containing only .gitkeep,
so include_dir! embeds an empty bundle and the running binary serves the
"bundle not built" placeholder. Every git-based install hits this — it is
guaranteed, not bad luck.
Secondary problems found while diagnosing:
- The placeholder (and
Justfile'sweb:target) tell the operator to runcargo build -p maestro-web --release, which writestarget/release/..., but the systemd unit runs~/.cargo/bin/maestro-web(fromcargo install). So following the placeholder's own advice never updates the running service. include_dir!does not emitcargo:rerun-if-changedfor the embedded directory. Oncemaestro-webis compiled with an emptyweb/dist, rebuilding afterpnpm builddoes not re-embed the bundle — cargo sees no source change and reuses the stale object.
Goal
A compiled maestro-web binary always embeds a real SPA bundle, regardless of
install method (cargo install --git, --path, or the wizard). No placeholder
for installed users.
Design
1. crates/maestro-web/build.rs (new)
Runs during every compile of the crate that cargo deems necessary (gated by the
rerun-if-changed set below).
-
Locate
web/relative toCARGO_MANIFEST_DIR(../../web). -
Resolve
pnpmonPATH, and confirmnodeis present. If either is missing → fail the build (panic!frombuild.rs, which cargo surfaces as a build error) with an actionable message, e.g.:maestro-web requires pnpm + node to build its embedded SPA bundle. Install them (https://pnpm.io) or use a prebuilt binary. -
Run, in
web/:pnpm install --frozen-lockfile(a fresh--gitclone has nonode_modules, so install is required).pnpm build. If either exits non-zero, fail the build and surface stderr.
-
Emit
cargo:rerun-if-changed=for the frontend inputs so the bundle rebuilds exactly when those change (and not on every Rust compile):../../web/src../../web/index.html../../web/package.json../../web/pnpm-lock.yaml../../web/vite.config.ts
This also fixes the staleness bug: changing the frontend now reliably re-embeds.
2. crates/maestro-web/src/static_bundle.rs — unchanged
include_dir! still embeds web/dist, now guaranteed populated by build.rs.
The placeholder path stays as harmless defense-in-depth; in a successful build
it is unreachable. Existing tests remain valid.
3. Installer wizard — handle_web (crates/maestro-installer/src/main.rs)
The web step is already opt-in (interactive prompt, or silent refresh when a
unit already exists). After the user opts in, preflight pnpm + node before
install_crate(WEB_CRATE):
- Interactive, prerequisite missing: prompt
maestro-web needs pnpm + node, which aren't installed. [A]bort install or [S]kip web UI?- Abort → exit the installer non-zero.
- Skip → continue without the web binary; daemon/registry work already done
is left intact. Return
Ok(false)fromhandle_web.
- Non-interactive refresh path (existing unit, no TTY): cannot prompt —
print a clear warning that the web refresh is skipped because pnpm/node are
missing, and leave the existing unit in place. Return
Ok(false). - Both present: proceed exactly as today. The existing
cargo install --gitnow Just Works becausebuild.rsbuilds the bundle inside the temp clone.
This keeps the daemon/registry install unaffected by a missing frontend
toolchain, and never lets a raw build.rs panic leak to the user — the wizard
catches the missing-prereq case first.
4. CI — .gitea/workflows/ci.yml
clippy and test compile maestro-web, which now requires the frontend
toolchain. Add a step (before clippy) that installs Node.js + pnpm and runs
pnpm -C web install --frozen-lockfile, so the workspace builds on the runner.
Node/pnpm install goes in the existing "Install … build deps" step or a new
adjacent step; corepack enable (or a pinned pnpm install) provides pnpm.
5. Justfile — unchanged
The web: target stays as a --release convenience and as the documented
local build command. With build.rs in place its pnpm build step is partly
redundant but harmless.
Consequences accepted
- The
maestro-webcrate cannot be compiled without node + pnpm. This is intentional (hard fail). It affects: workspace CI (addressed in §4), any Rust-only contributor building the full workspace, andcargo install --giton a machine without node. cargo install --gitbecomes slower (runspnpm install+pnpm build), but produces a correct binary — the whole point.
Out of scope
- Shipping a prebuilt release binary (considered, deferred — the build.rs + wizard path covers the reported problem).
- Changing the data-plane / proxy behavior of
maestro-web.
Testing
build.rsis exercised implicitly by every compile; a greencargo build -p maestro-webon a clean checkout (no priorpnpm build) that yields a binary whose/serves the realindex.htmlis the acceptance signal. An integration check can assert the served root contains the SPA mount (<div id="app">) rather than the placeholder string.- Installer: unit-test the preflight decision (missing-prereq → abort/skip
branch selection) without invoking cargo, following existing
maestro-installertest patterns. - CI itself is the regression test for the toolchain requirement.