commit 92988bd46de8d84b2c4b56d1b907f59eb0796b0b
parent 4bcd156b67f4f3228bdec05152097c4ad7053fb7
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 26 May 2026 14:44:26 -0400
Move export build to deploy page
Diffstat:
5 files changed, 82 insertions(+), 33 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **"Build static export" moved from the Build page to the top of the Deploy page**, co-locating it with the export changelog preview, "Cut release", and "Deploy static export" so the whole build → review → cut → deploy sequence lives on one page.
- **The embedded single-video view on the channel page is now collapsed by default**, keeping the channel view compact. Clicking a video in the list expands the panel and scrolls to it; navigating away from a video (no video selected) collapses it again. A manual collapse/expand toggle is available on the panel, mirroring the video list's existing collapse.
- **Unlisted videos now get their own clickable list in the channel availability diagnostics**, alongside Deleted/Private/Members-only/Needs-auth/Error. Previously unlisted videos were only shown as a count, so there was no way to jump to the specific videos.
- **Cutting a release now clears `## [Unreleased]` entirely** instead of leaving an empty heading sitting above the new dated section. The `/changelog` and `/deploy` pages stop showing a blank Unreleased block after a release; the heading reappears the next time somebody hand-adds a pending bullet to the file.
diff --git a/editor/app/build/components/BuildButtons.tsx b/editor/app/build/components/BuildButtons.tsx
@@ -13,7 +13,6 @@ import {
archiveCombinedTranscriptsAction,
archiveLiveChatAction,
archiveTranscriptsAction,
- buildExportAction,
buildIndexAction,
normalizeLiveChatAction,
normalizeTranscriptsAction,
@@ -28,7 +27,6 @@ const DEFAULT_BUILD_QUEUE = "build";
export function BuildButtons({ existingQueues }: Props) {
const [indexQueue, setIndexQueue] = useState(DEFAULT_BUILD_QUEUE);
- const [exportQueue, setExportQueue] = useState(DEFAULT_BUILD_QUEUE);
const [normalizeQueue, setNormalizeQueue] = useState(DEFAULT_BUILD_QUEUE);
const [archiveQueue, setArchiveQueue] = useState(DEFAULT_BUILD_QUEUE);
const [combinedQueue, setCombinedQueue] = useState(DEFAULT_BUILD_QUEUE);
@@ -82,32 +80,6 @@ export function BuildButtons({ existingQueues }: Props) {
<section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6">
<div>
- <h2 className="text-lg font-semibold">Build static export</h2>
- <p className="text-sm text-zinc-500">
- Spawns <code>pnpm run build</code> in <code>export/</code> — runs
- the index build, then <code>next build</code> to produce the
- static site at <code>export/out/</code>.
- </p>
- </div>
- <StreamActionLog
- trigger={() => buildExportAction(exportQueue)}
- cancelAction={cancelJobAction}
- buttonLabel="Build static export"
- runningLabel="Building static export…"
- extraControls={
- <QueueControl
- value={exportQueue}
- onChange={setExportQueue}
- defaultQueueKey={DEFAULT_BUILD_QUEUE}
- existingQueues={existingQueues}
- actionLabel="Build static export"
- />
- }
- />
- </section>
-
- <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6">
- <div>
<h2 className="text-lg font-semibold">Normalize transcripts</h2>
<p className="text-sm text-zinc-500">
Walks every video and writes{" "}
diff --git a/editor/app/deploy/components/BuildExportButton.tsx b/editor/app/deploy/components/BuildExportButton.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import { useState } from "react";
+import { StreamActionLog } from "yt-dlp-transcript-common/components/StreamActionLog";
+import { buildExportAction } from "../../build/buildAction";
+import { QueueControl } from "../../components/QueueControl";
+import { cancelJobAction } from "../../jobs/actions";
+
+type Props = {
+ existingQueues: string[];
+};
+
+const DEFAULT_BUILD_QUEUE = "build";
+
+export function BuildExportButton({ existingQueues }: Props) {
+ const [exportQueue, setExportQueue] = useState(DEFAULT_BUILD_QUEUE);
+
+ return (
+ <StreamActionLog
+ trigger={() => buildExportAction(exportQueue)}
+ cancelAction={cancelJobAction}
+ buttonLabel="Build static export"
+ runningLabel="Building static export…"
+ extraControls={
+ <QueueControl
+ value={exportQueue}
+ onChange={setExportQueue}
+ defaultQueueKey={DEFAULT_BUILD_QUEUE}
+ existingQueues={existingQueues}
+ actionLabel="Build static export"
+ />
+ }
+ />
+ );
+}
diff --git a/editor/app/deploy/page.tsx b/editor/app/deploy/page.tsx
@@ -8,6 +8,8 @@ import {
suggestNextVersion,
} from "yt-dlp-transcript-common/lib/changelog";
import { getPaths } from "yt-dlp-transcript-common/lib/paths";
+import { getRegistry } from "yt-dlp-transcript-common/jobs/registry";
+import { BuildExportButton } from "./components/BuildExportButton";
import { CutReleaseForm } from "./components/CutReleaseForm";
import { DeployButton } from "./components/DeployButton";
@@ -41,6 +43,7 @@ export default function DeployPage() {
const unreleased = source ? extractUnreleasedSection(source) : "";
const hasUnreleased = unreleased.length > 0;
const lastDeployedAt = getLastDeployedAt();
+ const existingQueues = getRegistry().activeQueueNames();
return (
<div className="flex flex-col gap-8 max-w-3xl">
@@ -48,6 +51,18 @@ export default function DeployPage() {
<section className="flex flex-col gap-3">
<div>
+ <h2 className="text-lg font-semibold">Build static export</h2>
+ <p className="text-sm text-zinc-500">
+ Spawns <code>pnpm run build</code> in <code>export/</code> — runs
+ the index build, then <code>next build</code> to produce the
+ static site at <code>export/out/</code>.
+ </p>
+ </div>
+ <BuildExportButton existingQueues={existingQueues} />
+ </section>
+
+ <section className="flex flex-col gap-3 border-t border-zinc-200 dark:border-zinc-800 pt-6">
+ <div>
<h2 className="text-lg font-semibold">Pending export changes</h2>
<p className="text-sm text-zinc-500">
Bullets currently sitting under <code>## [Unreleased]</code> in{" "}
@@ -88,8 +103,8 @@ export default function DeployPage() {
<p className="text-sm text-zinc-500">
Runs <code>pnpm run deploy</code> in <code>export/</code>,
publishing the most recently built static site at{" "}
- <code>export/out/</code>. Build first if needed (
- <code>Build → Build static export</code>).
+ <code>export/out/</code>. Build first if needed using{" "}
+ <code>Build static export</code> at the top of this page.
</p>
<p className="text-sm text-zinc-500 mt-1">
Last deployed:{" "}
diff --git a/editor/e2e/build.spec.ts b/editor/e2e/build.spec.ts
@@ -38,10 +38,36 @@ test("build index ingests synthetic video data and emits a transcript page", asy
).toBe(true);
});
-test("build static export button renders and is enabled", async ({ page }) => {
+test("build static export lives on the deploy page, not the build page", async ({
+ page,
+}) => {
await resetData("empty");
+
+ // No longer on the build page.
await page.goto("/build");
- const exportButton = page.getByRole("button", { name: "Build static export" });
+ await expect(
+ page.getByRole("heading", { name: "Build static export" }),
+ ).toHaveCount(0);
+ await expect(page.getByRole("heading", { name: "Build index" })).toBeVisible();
+
+ // Now at the top of the deploy page, above the changelog preview.
+ await page.goto("/deploy");
+ const exportHeading = page.getByRole("heading", {
+ name: "Build static export",
+ });
+ const pendingHeading = page.getByRole("heading", {
+ name: "Pending export changes",
+ });
+ await expect(exportHeading).toBeVisible();
+ await expect(pendingHeading).toBeVisible();
+
+ // The export section comes before the pending-changes section.
+ const exportBox = await exportHeading.boundingBox();
+ const pendingBox = await pendingHeading.boundingBox();
+ expect(exportBox!.y).toBeLessThan(pendingBox!.y);
+
+ const exportButton = page.getByRole("button", {
+ name: "Build static export",
+ });
await expect(exportButton).toBeEnabled();
- await expect(exportButton).toContainText("Build static export");
});