diff --git a/docs/superpowers/specs/2026-05-28-maestro-web-bundle-build.md b/docs/superpowers/specs/2026-05-28-maestro-web-bundle-build.md new file mode 100644 index 0000000..c875af6 --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-maestro-web-bundle-build.md @@ -0,0 +1,131 @@ +# maestro-web: build the SPA bundle at compile time + +**Date:** 2026-05-28 +**Status:** Approved (design) + +## 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`'s `web:` target) tell the operator to run + `cargo build -p maestro-web --release`, which writes `target/release/...`, but + the systemd unit runs `~/.cargo/bin/maestro-web` (from `cargo install`). So + following the placeholder's own advice never updates the running service. +- `include_dir!` does not emit `cargo:rerun-if-changed` for the embedded + directory. Once `maestro-web` is compiled with an empty `web/dist`, rebuilding + after `pnpm build` does **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 to `CARGO_MANIFEST_DIR` (`../../web`). +- Resolve `pnpm` on `PATH`, and confirm `node` is present. + **If either is missing → fail the build** (`panic!` from `build.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/`: + 1. `pnpm install --frozen-lockfile` (a fresh `--git` clone has no + `node_modules`, so install is required). + 2. `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)` from `handle_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 --git` + now Just Works because `build.rs` builds 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-web` crate 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, and `cargo install --git` + on a machine without node. +- `cargo install --git` becomes slower (runs `pnpm 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.rs` is exercised implicitly by every compile; a green + `cargo build -p maestro-web` on a clean checkout (no prior `pnpm build`) that + yields a binary whose `/` serves the real `index.html` is the acceptance + signal. An integration check can assert the served root contains the SPA + mount (`
`) rather than the placeholder string. +- Installer: unit-test the preflight decision (missing-prereq → abort/skip + branch selection) without invoking cargo, following existing + `maestro-installer` test patterns. +- CI itself is the regression test for the toolchain requirement.