commit 603fefc1d99d97123ac64972332031431a906a52
parent dc5cfc080f44b3e1738024fa0842ef01af19ce33
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 2 Jul 2026 00:56:18 -0400
Add "Skip data rebuild" option to Build static export
Let the editor trigger a build that composes from existing .export-index
staging without regenerating the pool-wide data (index/stats/charts).
A new build:nodata export script has the same body as build (compose:site
&& next build) but a distinct name, so npm's prebuild lifecycle hook (which
runs build:data) doesn't fire. runBuildPhase gains a skipData option that
routes to it, threaded from a new checkbox on BuildExportButton through
buildExportAction. Build & deploy is unchanged (always a full build).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
6 files changed, 40 insertions(+), 3 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **"Build static export" can now skip the data rebuild and compose from existing staging.** The `export/` build normally regenerates the pool-wide data first (its npm `prebuild` hook runs `build:data` = `build:index && build:stats && build:templates`), and the index step (heavy transcript → page-tree + LMDB processing) dominates build time. A new **Skip data rebuild (index, stats, charts)** checkbox on the Build static export control lets you rebuild a site *without* that work — it runs only `compose:site && next build` against the current `.export-index/` staging, which is exactly what you want when re-composing after a code/theme/template change or building a different site from already-staged data. It routes to a new `build:nodata` export script (same body as `build`, but a distinct name so npm's `prebuild` hook doesn't fire); the checkbox reuses the streamed-log/queue/cancel machinery of the existing managed build. Assumes a prior full build produced the staging (do a full build first if the data is stale). Only the build-only control is affected — the one-click **Build & deploy** button always does a full build. See `export/package.json` (`build:nodata`), `editor/app/deploy/buildDeployCore.ts` (`runBuildPhase` `skipData`), `editor/app/build/buildAction.ts` (`buildExportAction`), and `editor/app/deploy/components/BuildExportButton.tsx`.
- **The neutral Base theme is now all-sans.** The default Base family previously inherited the shared Source **Serif** display face for the wordmark and page headings; it now uses the sans face instead, matching the pre-theme all-sans look. This is a shared token change (`html:not([data-theme])` in `common/styles/tokens.css`), so any surface on the Base family — including the editor's default — reads sans; the Archive/Selenized/Swiss families keep their own type voices.
- **A dedicated Cleanup page with a live "reclaimable" sidebar badge and per-channel include/exclude.** Space-reclaim cleaning now has its own home (`/cleanup`, in the Pool nav) instead of being scattered across each channel's detail page. It opens with a serif **reclamation console** readout — the total reclaimable audio across channels, a token-colored breakdown bar (transcribed audio / extra formats / wrong-format), and a per-channel ledger that runs the same `cleanAudioAction` / `cleanExtraAudioFormatsAction` / `removeWrongFormatAudioAction` sweeps (plus failed-list housekeeping) the channel page does — the per-channel `CleanupStage` stays put. The sidebar **Cleanup** item carries an amber badge with the running reclaimable total (e.g. `12.4 GB`), recomputed on each auto-refresh tick. A per-channel **Counted / Excluded** toggle (new `ChannelConfig.excludeFromCleanup`, persisted in `config.json` like `excludeFromBuild`/`excludeFromSync`) holds a channel's space back from the total without disabling its sweeps — e.g. keep a finicky-to-redownload channel's audio around for now. The headline total uses the primary "clean audio" reclaim only (the three sweeps overlap, so they aren't summed). See `editor/app/cleanup/{page.tsx,lib/loadCleanup.ts,components/{ChannelCleanupCard,ChannelCleanupToggle}.tsx}`, `editor/app/channels/actions.ts` (`toggleChannelCleanupInclusionAction`), `editor/app/{lib/nav.ts,layout.tsx}`, and `common/lib/channelConfig.ts`.
- **Badges and alerts are now colorful and theme-aware in every theme.** The design system only tokenized red (`--destructive`); every green/amber/blue status was hardcoded Tailwind palette that ignored the four theme families. New semantic tokens — **`--success` / `--warning` / `--info`** (each with `-foreground` + a soft fill partner) plus `--destructive-soft` — are defined across all eight family blocks (base/archive/terminal/swiss × light/dark) and registered in `@theme inline`. The shared kit `Badge` gains `success` / `warning` / `info` / `brand` soft-filled variants, and a **new `Alert` component** (`common/components/ui/alert.tsx`) replaces ad-hoc notice divs. High-traffic status UI is migrated onto the tokens: channel stage badges (`StageBadge`), worker state badges + dots (`WorkersView`, `MonitorWidget`), the scheduler status column (`SchedulerView`), and the widget disk strip — so each recolors correctly in Archive, Terminal, and Swiss, light and dark. See `common/styles/tokens.css`, `common/components/ui/{badge,alert}.tsx`.
diff --git a/editor/app/build/buildAction.ts b/editor/app/build/buildAction.ts
@@ -63,6 +63,7 @@ export async function buildStatsAction(
export async function buildExportAction(
siteId: string,
queueKey?: string,
+ skipData?: boolean,
): Promise<StreamActionResult> {
const paths = getPaths();
const id = siteId.trim();
@@ -73,12 +74,14 @@ export async function buildExportAction(
// persisted build mode (basic vs docker) via runBuildPhase, and so the same
// child-streaming path is shared with build-then-deploy. Cancel still works:
// the abort signal is forwarded to the child by runChildIntoLog.
+ // `skipData` composes from the existing .export-index staging without
+ // regenerating the pool-wide index/stats/charts.
return runManagedFunction({
kind: "build-export",
queueKey: queueKey === undefined ? DEFAULT_BUILD_QUEUE : queueKey.trim(),
paths,
fn: async (onLog, signal) => {
- const code = await runBuildPhase(onLog, signal, id, paths);
+ const code = await runBuildPhase(onLog, signal, id, paths, { skipData });
if (signal.aborted) return;
if (code !== 0) throw new Error(`Build failed (exit ${code})`);
},
diff --git a/editor/app/deploy/buildDeployCore.ts b/editor/app/deploy/buildDeployCore.ts
@@ -30,6 +30,7 @@ export async function runBuildPhase(
signal: AbortSignal,
siteId: string,
paths: Paths,
+ opts?: { skipData?: boolean },
): Promise<number> {
const { mode } = getSettings().buildPipeline;
if (mode === "docker") {
@@ -38,9 +39,21 @@ export async function runBuildPhase(
"for now. Parallel container builds arrive in a follow-up.\n",
);
}
+ // When skipping the data rebuild, run the `build:nodata` script instead of
+ // `build`. `build:nodata` has the same body (compose:site + next build) but a
+ // different name, so npm's `prebuild` lifecycle hook (which runs build:data =
+ // index/stats/templates) does NOT fire — we compose from the existing
+ // .export-index staging. Assumes a prior full build produced that staging.
+ const skipData = opts?.skipData === true;
+ if (skipData) {
+ onLog(
+ "[notice] Skipping data rebuild (index/stats/charts) — composing from " +
+ "existing .export-index staging.\n",
+ );
+ }
return runChildIntoLog(onLog, signal, {
command: "pnpm",
- args: ["run", "build"],
+ args: ["run", skipData ? "build:nodata" : "build"],
cwd: paths.exportDir,
env: {
...process.env,
diff --git a/editor/app/deploy/components/BuildExportButton.tsx b/editor/app/deploy/components/BuildExportButton.tsx
@@ -17,10 +17,11 @@ const DEFAULT_BUILD_QUEUE = "build";
export function BuildExportButton({ existingQueues, siteId, siteTitle }: Props) {
const [exportQueue, setExportQueue] = useState(DEFAULT_BUILD_QUEUE);
+ const [skipData, setSkipData] = useState(false);
return (
<StreamActionLog
- trigger={() => buildExportAction(siteId as string, exportQueue)}
+ trigger={() => buildExportAction(siteId as string, exportQueue, skipData)}
cancelAction={cancelJobAction}
buttonLabel="Build static export"
runningLabel="Building static export…"
@@ -37,6 +38,18 @@ export function BuildExportButton({ existingQueues, siteId, siteTitle }: Props)
Select a specific site from the sidebar to build.
</p>
)}
+ <label className="flex items-center gap-2 text-sm">
+ <input
+ type="checkbox"
+ checked={skipData}
+ onChange={(e) => setSkipData(e.target.checked)}
+ />
+ Skip data rebuild (index, stats, charts)
+ </label>
+ <p className="text-xs text-muted-foreground">
+ Reuses the existing index/stats — only composes the site and runs
+ next build. Do a full build first if data is stale.
+ </p>
<QueueControl
value={exportQueue}
onChange={setExportQueue}
diff --git a/editor/e2e/build.spec.ts b/editor/e2e/build.spec.ts
@@ -94,4 +94,10 @@ test("build static export lives on the deploy page, not the build page", async (
name: "Build static export",
});
await expect(exportButton).toBeEnabled();
+
+ // The build-only control offers a "skip data rebuild" option (compose from
+ // existing staging without regenerating index/stats/charts). Default off.
+ const skipData = page.getByLabel("Skip data rebuild (index, stats, charts)");
+ await expect(skipData).toBeVisible();
+ await expect(skipData).not.toBeChecked();
});
diff --git a/export/package.json b/export/package.json
@@ -13,6 +13,7 @@
"compose:site": "tsx ../common/bin/compose-site.ts",
"prebuild": "pnpm run build:data",
"build": "pnpm run compose:site && next build",
+ "build:nodata": "pnpm run compose:site && next build",
"start": "serve out",
"lint": "eslint",
"e2e": "playwright test",