diff --git a/frontend/packages/agent/build.mjs b/frontend/packages/agent/build.mjs index e38c1a8a..f14a8ce6 100644 --- a/frontend/packages/agent/build.mjs +++ b/frontend/packages/agent/build.mjs @@ -1,7 +1,7 @@ // esbuild build for @hive/agent. Output layout (`dist/`): // -// dist/index.html served at GET / (the Preact rewrite — -// the old vanilla-JS app.js is deleted) +// dist/index.html served at GET / (the Preact rewrite, +// hyperhive#3685 — app.js is deleted) // dist/stats.html served at GET /stats // dist/screen.html served at GET /screen // dist/static/stats.js served at /static/stats.js (pulls in @@ -50,7 +50,7 @@ await build({ loader: { ".css": "text" }, }); -// The main agent page (the Preact rewrite — the old vanilla-JS app.js, +// The main agent page (hyperhive#3685's Preact rewrite — app.js, // which used to share this build step, is deleted). A separate bundle // from stats.js above rather than folded in: different loader needs // (`.css: 'css'`, real stylesheets a component imports, vs. the raw- diff --git a/frontend/packages/dashboard/build.mjs b/frontend/packages/dashboard/build.mjs index dbacdb03..1c45c232 100644 --- a/frontend/packages/dashboard/build.mjs +++ b/frontend/packages/dashboard/build.mjs @@ -1,18 +1,52 @@ -// esbuild build for @hive/dashboard: one JS+CSS bundle per page (H0M3 at -// `/`, dashboard/flow/logs/core/builds/credentials at their own -// `.html`), each named after its own entry point below — see the -// `entryPoints`/`for` lists for the exact map, not repeated here to -// avoid this comment drifting out of sync with the real build steps. +// esbuild build for @hive/dashboard. Output layout (`dist/`): +// +// dist/index.html H0M3 menu hub — served by the Rust +// router at GET / (the landing page) +// dist/dashboard.html the operator dashboard SPA — served +// at GET /dashboard.html +// dist/flow.html served at GET /flow.html +// dist/logs.html served at GET /logs.html (AGENT/INFRA/SYSTEM) +// dist/core.html served at GET /core.html (C0R3: kept +// state / container load) +// dist/builds.html served at GET /builds.html (BU1LDS: +// rebuild queue / meta inputs / build logs) +// dist/credentials.html served at GET /credentials.html +// (CR3D3NTIALS: matrix + github accounts) +// dist/static/home.js index.html (H0M3) entry — menu tiles + +// matrix-tile gating + identity line +// dist/static/tabs.js dashboard.html entry — tab renderers + +// tab routing + refreshState +// dist/static/flow.js /flow.html entry — broker terminal + +// @-mention composer +// dist/static/logs.js /logs.html entry — agent/infra/system +// log viewer sub-tabs +// dist/static/builds.js /builds.html entry — rebuild queue, +// meta inputs, build log history +// dist/static/credentials.js /credentials.html entry — matrix + +// github account sub-tabs +// dist/static/{home,tabs,flow,logs,builds,credentials}.js.map source map +// siblings +// dist/static/common.css loaded by every page (@hive/shared +// imports + shared typography/badges/ +// buttons/inbox/side-panel) +// dist/static/dashboard.css dashboard.html only (dashboard-specific) +// dist/static/flow.css /flow.html only (flow chrome + composer) +// dist/static/logs.css /logs.html only (log viewer sub-tabs) +// dist/static/builds.css /builds.html only (build lifecycle hub) +// dist/static/home.css index.html (H0M3) only (tile grid) +// dist/static/credentials.css /credentials.html only (matrix + github +// account forms) // // Each JS entry bundles `./common.js` (DOM helpers, Panel singleton, // NOTIF, path linkification) independently — esbuild inlines the shared // module into each bundle rather than emitting a shared chunk (no // `splitting: true`). The Rust binary mounts `dist/` as a // `tower_http::ServeDir` fallback (`append_index_html_on_directories` -// serves index.html at /); the dashboard SPA lives at /dashboard.html -// (a plain file, not a /dashboard/ prefix) so it never shadows the -// exact-match /dashboard/stream + /dashboard/history SSE routes -// registered before the ServeDir fallback. +// serves index.html at /); the layout above keeps every URL the HTML +// files reference reachable without rewriting paths in the HTML. The +// dashboard SPA lives at /dashboard.html (a plain file, not a /dashboard/ +// prefix) so it never shadows the exact-match /dashboard/stream + +// /dashboard/history SSE routes registered before the ServeDir fallback. import { build } from "esbuild"; import { mkdirSync, copyFileSync, rmSync } from "node:fs"; @@ -68,17 +102,18 @@ await build({ jsxImportSource: "preact", }); -// Stream-worker entry. Lives in a separate bundle: SharedWorker +// Stream-worker entry (#448). Lives in a separate bundle: SharedWorker // scripts run in a different global (`self` is the worker scope, no // `window`) so they can't be inlined into tabs.js / flow.js. Output is // at `static/stream-worker.js`; common.js's `openStream` references // `/static/stream-worker.js` as the SharedWorker URL. `format: 'iife'` // matches the classic-script load (`new SharedWorker(url, name)` with -// no `{ type: 'module' }`); Firefox's module SharedWorker support is -// patchy, so keeping the worker as a classic script + IIFE bundle is -// the compatible default. argus's own review nit here: if a future -// contributor adds an `import` to this bundle, the IIFE format will -// surface it as a build error rather than silently shipping broken code. +// no `{ type: 'module' }`); Firefox is the #448 target and module +// SharedWorker support there is patchy, so keeping the worker as a +// classic script + IIFE bundle is the compatible default. argus nit +// on #453: if a future contributor adds an `import` to this bundle, +// the IIFE format will surface it as a build error rather than +// silently shipping broken code. await build({ entryPoints: [src("stream-worker.js")], outdir: staticDir(""), diff --git a/frontend/packages/swarm-ui/build.mjs b/frontend/packages/swarm-ui/build.mjs index 251430cd..41f8a1c1 100644 --- a/frontend/packages/swarm-ui/build.mjs +++ b/frontend/packages/swarm-ui/build.mjs @@ -1,38 +1,44 @@ -// esbuild build for @hive/swarm-ui (project-bootstrap scope). Bundles -// main.tsx + its companion CSS, theme.css, and swarm-ui.css into -// `dist/static/`, content-hashed (`[name]-[hash]`) so a redeploy's new -// bundle gets a new URL, then rewrites those real URLs into index.html -// and sw.js. See each output file's own name in the code below for what -// it's for — not repeated here to avoid this comment drifting out of -// sync with the actual build steps. +// esbuild build for @hive/swarm-ui (project-bootstrap scope). Output +// layout (`dist/`): // -// `colors.css` is the ONE static asset deliberately left UNHASHED, at a -// fixed `dist/static/colors.css`: nix/host-modules/swarm-ui.nix's stylix -// theming swaps that exact path via an `= /static/colors.css` nginx -// location override, no npm/esbuild rebuild involved. Hashing it would -// silently break that swap. See `colors.css`'s own top comment. +// dist/index.html served at GET / +// dist/static/main.js served at /static/main.js (ESM bundle, +// Preact + wouter-preact) +// dist/static/main.js.map source map sibling +// dist/static/main.css every component's own `import +// './Foo.css'` (Shell.css, Panel.css, …), +// folded by esbuild into one companion +// output alongside main.js — no separate +// build step, this falls out of bundling +// main.tsx with `bundle: true` +// dist/static/colors.css served at /static/colors.css +// dist/static/theme.css served at /static/theme.css +// dist/static/swarm-ui.css served at /static/swarm-ui.css — just the +// shared base reset now (@import resolved +// from @hive/shared), NOT component styles +// dist/manifest.webmanifest served at /manifest.webmanifest (PWA) +// dist/sw.js served at /sw.js (PWA) — plain JS, no +// esbuild pass needed (see its own +// top comment for why); copied as-is. // -// `dist/sw.js` keeps its own fixed/unhashed filename too — the browser -// polls this exact URL to detect a SW update, hashing it would make -// every deploy register as a brand new worker. Its `CACHE_VERSION` -// placeholder and shell-asset URLs get rewritten below, same as -// index.html's. -// -// PWA icons are NOT produced here — like favicon.svg, they're -// rasterized from branding/*.svg and copied in by -// nix/packages/swarm-ui.nix's installPhase, outside the npm tree this -// script builds. +// PWA icons (icon-192.png/icon-512.png/icon-512-maskable.png) are NOT +// produced here — like favicon.svg, they're rasterized from +// branding/*.svg and copied in by nix/packages/swarm-ui.nix's +// installPhase, outside the npm tree this script builds. A plain `npm +// run build` here has no `/icon-*.png`, same as it's always had no +// `/favicon.svg`. // // Not yet wired into any Rust binary's `ServeDir` — swarm-controller -// only serves `/health` today; this just makes `dist/` buildable so -// nix/packages/swarm-ui.nix has something to package. `.tsx` → JS: -// esbuild transpiles TypeScript natively (types stripped, not checked) -// — run `npm run typecheck` separately for real type errors. +// only serves `/health` today (see swarm-controller/README.md); this +// just makes `dist/` buildable so nix/packages/swarm-ui.nix has +// something to package. `.tsx` → JS: esbuild transpiles TypeScript +// natively (types stripped, not checked) — run `npm run typecheck` +// (plain `tsc --noEmit`) separately for real type errors; not yet +// wired into CI. import { build } from "esbuild"; -import { mkdirSync, rmSync, readFileSync, writeFileSync } from "node:fs"; -import { createHash } from "node:crypto"; -import { dirname, relative, resolve } from "node:path"; +import { mkdirSync, copyFileSync, rmSync } from "node:fs"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const here = dirname(fileURLToPath(import.meta.url)); @@ -40,23 +46,12 @@ const src = (p) => resolve(here, "src", p); const dist = (p) => resolve(here, "dist", p); const staticDir = (p) => resolve(here, "dist", "static", p); -// Every metafile path esbuild returns is relative to `absWorkingDir` -// (pinned to `here` on every build call below, regardless of the caller's -// own cwd) and always POSIX-style (`dist/static/main-.js`) — turn -// one into the URL nginx actually serves it at. A plain prefix strip, not -// `path.relative`: the latter resolves a relative argument against -// `process.cwd()`, which is exactly the caller-cwd dependency pinning -// `absWorkingDir` on every build call below was meant to avoid. -const toUrl = (metafilePath) => "/" + metafilePath.replace(/^dist\//, ""); - rmSync(dist(""), { recursive: true, force: true }); mkdirSync(staticDir(""), { recursive: true }); -const mainResult = await build({ - absWorkingDir: here, +await build({ entryPoints: [src("main.tsx")], - outdir: staticDir(""), - entryNames: "[name]-[hash]", + outfile: staticDir("main.js"), bundle: true, format: "esm", platform: "browser", @@ -65,103 +60,20 @@ const mainResult = await build({ logLevel: "info", jsx: "automatic", jsxImportSource: "preact", - metafile: true, }); -// Look up a build's real output path for one of its entry points, by the -// entry's own source path — rather than guessing esbuild's hash -// algorithm or assuming an output ordering. `entryPoint` in the metafile -// is relative to `absWorkingDir` (`here`, pinned on every build call -// below); `path.relative` with two absolute arguments never touches -// `process.cwd()`, so this is stable regardless of the caller's own cwd. -const outputFor = (result, entrySrcPath) => { - const wantEntry = relative(here, entrySrcPath); - const found = Object.entries(result.metafile.outputs).find( - ([, o]) => o.entryPoint === wantEntry, - ); - if (!found) { - throw new Error( - `esbuild produced no output for entry point ${wantEntry} -- ` + - "either it failed silently or the metafile's entryPoint " + - "convention changed; update outputFor() to match.", - ); - } - return found; -}; - -// main.tsx's bundle: the JS entry itself, plus its companion CSS output -// (`cssBundle` — every component's own `import './Foo.css'`, folded into -// one file alongside the JS, same as before this hashed). -const [mainJsPath, mainJsInfo] = outputFor(mainResult, src("main.tsx")); -if (!mainJsInfo.cssBundle) { - throw new Error( - "main.tsx produced no companion CSS bundle -- index.html/sw.js still " + - "reference /static/main.css, so this build can no longer keep that " + - "promise. Either a component's own CSS import got removed, or " + - "esbuild's output shape changed; update both the build and the " + - "files that reference /static/main.css together.", - ); +for (const entry of ["colors.css", "theme.css", "swarm-ui.css"]) { + await build({ + entryPoints: [src(entry)], + outfile: staticDir(entry), + bundle: true, + loader: { ".css": "css" }, + logLevel: "info", + }); } -const mainJsUrl = toUrl(mainJsPath); -const mainCssUrl = toUrl(mainJsInfo.cssBundle); -// theme.css / swarm-ui.css: hashed, same as main.tsx above. colors.css is -// deliberately NOT in this list — see the top comment's explanation of -// why it stays at a fixed path. -const hashedCssResult = await build({ - absWorkingDir: here, - entryPoints: [src("theme.css"), src("swarm-ui.css")], - outdir: staticDir(""), - entryNames: "[name]-[hash]", - bundle: true, - loader: { ".css": "css" }, - logLevel: "info", - metafile: true, -}); -const themeCssUrl = toUrl(outputFor(hashedCssResult, src("theme.css"))[0]); -const swarmUiCssUrl = toUrl(outputFor(hashedCssResult, src("swarm-ui.css"))[0]); - -// colors.css: fixed, unhashed path — see the top comment. -await build({ - absWorkingDir: here, - entryPoints: [src("colors.css")], - outfile: staticDir("colors.css"), - bundle: true, - loader: { ".css": "css" }, - logLevel: "info", -}); - -// A build-derived cache-bust version for sw.js's own CACHE_NAME: any -// change to any hashed shell-asset URL (a real content change, or an -// asset added/removed) changes this, which changes CACHE_NAME, which -// makes the SW's own `activate` handler evict the old cache in full — -// no more hand-bumping a CACHE_VERSION const on every shell change. -const buildVersion = createHash("sha256") - .update(JSON.stringify([mainJsUrl, mainCssUrl, themeCssUrl, swarmUiCssUrl])) - .digest("hex") - .slice(0, 12); - -const rewriteAssetUrls = (text) => - text - .replaceAll("/static/main.js", mainJsUrl) - .replaceAll("/static/main.css", mainCssUrl) - .replaceAll("/static/theme.css", themeCssUrl) - .replaceAll("/static/swarm-ui.css", swarmUiCssUrl); - -writeFileSync( - dist("index.html"), - rewriteAssetUrls(readFileSync(src("index.html"), "utf8")), -); -writeFileSync( - dist("sw.js"), - rewriteAssetUrls(readFileSync(src("sw.js"), "utf8")).replace( - '"__BUILD_VERSION__"', - JSON.stringify(buildVersion), - ), -); -writeFileSync( - dist("manifest.webmanifest"), - readFileSync(src("manifest.webmanifest")), -); +copyFileSync(src("index.html"), dist("index.html")); +copyFileSync(src("manifest.webmanifest"), dist("manifest.webmanifest")); +copyFileSync(src("sw.js"), dist("sw.js")); console.log("swarm-ui build ok →", dist("")); diff --git a/frontend/packages/swarm-ui/src/sw.js b/frontend/packages/swarm-ui/src/sw.js index 911b7c02..a7b80c55 100644 --- a/frontend/packages/swarm-ui/src/sw.js +++ b/frontend/packages/swarm-ui/src/sw.js @@ -7,27 +7,26 @@ // a cached fetch would be worse than showing nothing (mara: "scope looks // good" — approving that as a hard rule, not a short-TTL middle ground). // -// Network-first for the shell, not cache-first: most assets below are -// content-hashed (a fresh URL per deploy, so a normal visit always gets -// the current bundle regardless of this SW), but `colors.css` stays -// deliberately unhashed for the host-side stylix theme swap, so this -// SW's own cached copy of it can only ever refresh via a real network -// round-trip. This SW only ever serves its cache on a FAILED fetch -// (offline/flaky network), never in preference to a live response. +// Network-first for the shell, not cache-first: `main.js`/`main.css` are +// unhashed filenames, so a cache-first strategy would risk wedging an +// operator on yesterday's JS after a deploy until they manually cleared +// it (a deploy changes the nix store path serving these but not the URL +// the browser cached against — a separate, already-filed fix). This SW +// only ever serves its cache on a FAILED fetch (offline/flaky network), +// never in preference to a successful network response. // // Plain JS, not TypeScript, deliberately — the DOM lib swarm-ui's own -// tsconfig uses and the WebWorker lib a service worker's globals need -// (`self`, `ExtendableEvent`, `FetchEvent`, `caches`, ...) are mutually -// exclusive in one tsc program, and this file is small enough that a -// second tsconfig just to typecheck it isn't worth the config surface. -// Same treatment `build.mjs` already gets in this package. +// tsconfig uses and the WebWorker lib a service worker's globals +// (`self`, `ExtendableEvent`, `FetchEvent`, `caches`, ...) need are +// mutually exclusive in one tsc program, and this file is small/ +// self-contained enough that a second tsconfig just to typecheck it +// isn't worth the config surface. Same treatment `build.mjs` already +// gets in this package. // -// CACHE_VERSION is filled in by build.mjs from a hash of every hashed -// shell-asset URL below, so any real content change gets a fresh cache -// name and `activate`'s old-cache sweep evicts the previous one in -// full. This literal placeholder is never served; the build always -// replaces it. -const CACHE_VERSION = "__BUILD_VERSION__"; +// CACHE_VERSION is bumped by hand for now — once the filenames above are +// content-hashed, the cache name can derive from the build itself and +// this manual step goes away. +const CACHE_VERSION = "v1"; const CACHE_NAME = `swarm-ui-shell-${CACHE_VERSION}`; // Every navigation (any client-side route wouter handles — /agents, diff --git a/scripts/check-comment-blocks.sh b/scripts/check-comment-blocks.sh index 12efd75b..8aafe6b6 100755 --- a/scripts/check-comment-blocks.sh +++ b/scripts/check-comment-blocks.sh @@ -8,7 +8,7 @@ # exceeds MAX. Runs as its own CI job and IS a required check on the forge # (branch protection) — a hit blocks merge. # -# Scope: tracked *.rs *.nix *.sh *.js *.mjs *.ts *.tsx *.css *.html. Markdown is exempt +# Scope: tracked *.rs *.nix *.sh *.js *.ts *.tsx *.css *.html. Markdown is exempt # (it is prose by nature). Comment forms: `#` line comments (nix/sh), `//` # line comments (rs/js/ts), and `/* */` block comments (nix/rs/js/ts/css) + # `` (html). A blank line separates two line-comment blocks (does NOT @@ -23,12 +23,12 @@ MAX=30 # Collect annotations into a variable (not via xargs/awk exit codes) so a # hit in any xargs batch is preserved — mirrors scripts/check-issue-refs.sh. hits="$( - git ls-files -z '*.rs' '*.nix' '*.sh' '*.js' '*.mjs' '*.ts' '*.tsx' '*.css' '*.html' \ + git ls-files -z '*.rs' '*.nix' '*.sh' '*.js' '*.ts' '*.tsx' '*.css' '*.html' \ | xargs -0 -r awk -v MAX="$MAX" ' function mode_of(fn) { if (fn ~ /\.nix$/) return "nix" if (fn ~ /\.sh$/) return "hash" - if (fn ~ /\.(rs|js|mjs|ts|tsx)$/) return "slash" + if (fn ~ /\.(rs|js|ts|tsx)$/) return "slash" if (fn ~ /\.css$/) return "cstyle" if (fn ~ /\.html$/) return "html" return "" diff --git a/scripts/check-issue-refs.sh b/scripts/check-issue-refs.sh index 245851e5..c79b6105 100755 --- a/scripts/check-issue-refs.sh +++ b/scripts/check-issue-refs.sh @@ -11,8 +11,8 @@ # Emits a CI error annotation per hit, exits 1 if any hit is found. Its own # required CI job (branch protection) — a hit blocks merge. # -# Scope: every tracked `*.rs *.nix *.js *.mjs *.ts *.tsx *.css *.html *.md -# *.yml *.yaml`. CI workflow files (`.forgejo/workflows/*.yml`) are explicitly in +# Scope: every tracked `*.rs *.nix *.js *.ts *.tsx *.css *.html *.md *.yml +# *.yaml`. CI workflow files (`.forgejo/workflows/*.yml`) are explicitly in # scope: a step comment is still a comment. The pattern matches a hash, # 2-5 digits, then a non-alphanumeric char or end-of-line (skips # letter-bearing hex colours and digit-runs-then-letter, e.g. `#24h`; @@ -33,7 +33,7 @@ pattern='#[0-9]{2,5}([^0-9a-zA-Z]|$)|/issues/[0-9]+([^0-9a-zA-Z]|$)' # an empty file list. Lines carrying the `lint:allow` marker are dropped # (legitimate non-tracker hit; see the header). hits="$( - git ls-files -z '*.rs' '*.nix' '*.js' '*.mjs' '*.ts' '*.tsx' '*.css' '*.html' '*.md' \ + git ls-files -z '*.rs' '*.nix' '*.js' '*.ts' '*.tsx' '*.css' '*.html' '*.md' \ '*.yml' '*.yaml' \ | xargs -0 -r grep -nE "$pattern" /dev/null 2>/dev/null \ | grep -v 'lint:allow' || true