This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/docs/superpowers/specs/2026-05-28-maestro-web-bundle-build.md
T
Miguel Palhas c856333e4a
ci / rust (push) Successful in 2m40s
docs(maestro): describe the single-host architecture
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.
2026-08-22 10:26:49 +01:00

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. See CLAUDE.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'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 (<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-installer test patterns.
  • CI itself is the regression test for the toolchain requirement.