Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 7491a866aff24236e4a4a3e05c6818d4745ef833
parent 2d870074b807ee717c0502ac5f636674ac42972a
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 01:38:21 -0400

sites: a Homepage section after Hub — Build homepage (Deploy after build, unticked), Deploy homepage and a preview branch, as jobs build-homepage / deploy-homepage / build-deploy-homepage

homepageDeployActions.ts mirrors hubActions.ts: build on the build queue,
deploy and build-deploy on the deploy queue, the build-deploy deploying only
on exit 0. Refused before any job: a bad preview name (previewBranchProblem)
and, for a deploy-only, no build in homepage/out (builtHomepageProblem); the
job re-checks both in deployHomepage. HomepageBuildButtons is
HubBuildButtons' shape plus a preview box judged by the same function, and a
line naming what a deploy ships (homepage/out built <when>, to archilyzer,
production or the preview alias, the live URL) and the Build index note. The
Hub paragraph loses its homepage sentence; the Homepage section says it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Aeditor/app/sites/components/HomepageBuildButtons.tsx | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor/app/sites/lib/homepageDeployActions.ts | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/sites/page.tsx | 44+++++++++++++++++++++++++++++++++++++-------
3 files changed, 326 insertions(+), 7 deletions(-)

diff --git a/editor/app/sites/components/HomepageBuildButtons.tsx b/editor/app/sites/components/HomepageBuildButtons.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { useState } from "react"; +import { + MAX_PREVIEW_BRANCH, + previewAliasUrl, + previewBranchProblem, +} from "yt-dlp-transcript-common/lib/pagesDeploy"; +import { PROJECT_URL } from "yt-dlp-transcript-common/lib/project"; +import { + buildAndDeployHomepageAction, + buildHomepageAction, + deployHomepageAction, +} from "../lib/homepageDeployActions"; +import { JobLane } from "./JobLane"; + +type Lane = { + kind: "build" | "build-deploy" | "deploy"; + key: number; + // The preview branch AS LAUNCHED. Editing the field afterwards must not + // change what a running lane says it is deploying to. + preview: string | undefined; +}; + +const TITLE: Record<Lane["kind"], string> = { + build: "Build homepage", + "build-deploy": "Build & deploy homepage", + deploy: "Deploy homepage", +}; + +type Props = { + // The homepage's Pages project — the constant HOMEPAGE_PAGES_PROJECT, passed + // down because it lives beside the deploy code (server-only), not copied. + project: string; + // When homepage/out/index.html was built, formatted by the server; null + // when there is no build there. + builtAt: string | null; +}; + +// The homepage's build and deploy — the `homepage` package, built into +// homepage/out and deployed to its own Pages project. HubBuildButtons' shape +// exactly: one lane at a time (each launch replaces the previous lane; its job +// keeps running and stays on /jobs), and "Deploy after build" starts +// unchecked, because a homepage deploy replaces the software's public site. +// +// Plus a preview branch, which the hub's buttons do not have: empty is +// production, a name is a Cloudflare Pages preview of the same project. The +// name is judged by previewBranchProblem, the function the action and the ops +// route refuse with, so the buttons grey out on exactly the names the server +// would reject. +export function HomepageBuildButtons({ project, builtAt }: Props) { + const [deploy, setDeploy] = useState(false); + const [preview, setPreview] = useState(""); + const [lane, setLane] = useState<Lane | null>(null); + const [run, setRun] = useState(0); + + const branch = preview.trim(); + const problem = branch ? previewBranchProblem(branch) : null; + // Knowable before any deploy: project + branch and nothing else. + const alias = branch && problem === null ? previewAliasUrl(project, branch) : null; + + function launch(kind: Lane["kind"]) { + const next = run + 1; + setRun(next); + setLane({ kind, key: next, preview: branch || undefined }); + } + + function trigger(l: Lane) { + const opts = l.preview ? { previewBranch: l.preview } : undefined; + return l.kind === "build" + ? buildHomepageAction() + : l.kind === "build-deploy" + ? buildAndDeployHomepageAction(opts) + : deployHomepageAction(opts); + } + + return ( + <div role="group" aria-label="Homepage build" className="flex flex-col gap-3"> + <div className="flex flex-wrap items-center gap-3"> + <button + type="button" + onClick={() => launch(deploy ? "build-deploy" : "build")} + // A build alone deploys nothing, so only a build that will deploy + // is held back by a bad preview name. + disabled={deploy && problem !== null} + className="px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50" + > + Build homepage + </button> + <label className="flex items-center gap-2 text-sm text-muted-foreground"> + <input + type="checkbox" + checked={deploy} + onChange={(e) => setDeploy(e.target.checked)} + /> + Deploy after build + </label> + <button + type="button" + onClick={() => launch("deploy")} + disabled={problem !== null} + className="px-3 py-2 rounded-md border border-border text-sm font-medium hover:bg-muted disabled:opacity-50" + > + Deploy homepage + </button> + <label className="flex items-center gap-2 text-sm"> + <span className="text-muted-foreground">Preview branch</span> + <input + type="text" + aria-label="preview branch" + placeholder="none: production" + value={preview} + // Twice the limit, so an over-long name is refused with the + // sentence that says WHY rather than silently truncated. + maxLength={MAX_PREVIEW_BRANCH * 2} + onChange={(e) => setPreview(e.target.value)} + className="w-44 rounded-md border border-border bg-background px-2 py-1 font-mono text-sm" + /> + </label> + </div> + {problem !== null && ( + <p + role="status" + // Not "preview branch problem": a name that CONTAINS the input's + // would make every by-label lookup for the input ambiguous. + aria-label="preview problem" + className="text-sm text-destructive" + > + {problem} + </p> + )} + <p className="text-xs text-muted-foreground" data-testid="homepage-ships"> + {builtAt ? ( + <> + Deploy homepage ships <code>homepage/out</code>, built {builtAt}, + </> + ) : ( + <> + <code>homepage/out</code> holds no build yet, so Deploy homepage has + nothing to ship. A deploy goes + </> + )}{" "} + to <code>{project}</code>{" "} + {!branch ? ( + <> + (production): <UrlLink url={PROJECT_URL} />. + </> + ) : alias ? ( + <> + (preview <code>{branch}</code>): <UrlLink url={alias} />; the live + site is left alone. + </> + ) : ( + <> + (preview <code>{branch}</code>). + </> + )} + </p> + <p className="text-xs text-muted-foreground"> + The homepage reads the search index as it stands: run Build index + (under Pool jobs, below) first when its numbers should move. + </p> + {lane && ( + <JobLane + key={lane.key} + title={TITLE[lane.kind]} + subtitle={ + lane.kind === "build" + ? "homepage/out" + : `homepage/out · ${project}${lane.preview ? ` (preview ${lane.preview})` : " (production)"}` + } + trigger={() => trigger(lane)} + /> + )} + </div> + ); +} + +function UrlLink({ url }: { url: string }) { + return ( + <a className="font-mono underline" href={url} target="_blank" rel="noreferrer"> + {url} + </a> + ); +} diff --git a/editor/app/sites/lib/homepageDeployActions.ts b/editor/app/sites/lib/homepageDeployActions.ts @@ -0,0 +1,104 @@ +"use server"; + +import { builtHomepageProblem } from "yt-dlp-transcript-common/lib/builtExport"; +import { previewBranchProblem } from "yt-dlp-transcript-common/lib/pagesDeploy"; +import { getPaths, type Paths } from "yt-dlp-transcript-common/lib/paths"; +import { + runManagedFunction, + type StreamActionResult, +} from "yt-dlp-transcript-common/jobs/streamCommand"; +import { + buildHomepage, + deployHomepage, + homepageOutDir, +} from "yt-dlp-transcript-common/publish/build"; + +// The homepage's build and deploy, as jobs — the `homepage` package, Archilyzer's +// own site, deployed to the constant Pages project `archilyzer` +// (HOMEPAGE_PAGES_PROJECT). The same three shapes as the hub's (hubActions.ts) +// and the same bodies `archilyzer build homepage` / `deploy homepage` run. +// +// The build writes homepage/out, not export/out, so it cannot race a site +// build for export/ — but `next build` is heavy, and the build queue is what +// serialises heavy builds. The deploys take the deploy queue. +const BUILD_QUEUE = "build"; +const DEPLOY_QUEUE = "deploy"; + +// Refusals a deploy can give BEFORE any job starts: a bad preview name and — +// for a deploy-only — no build in homepage/out. No project check: the project +// is a constant. deployHomepage checks both again when its job runs, because a +// queued deploy can start after the build it was going to ship is gone. +function deployRefusal( + paths: Paths, + previewBranch: string | undefined, + checkBuilt: boolean, +): string | null { + if (previewBranch !== undefined) { + const problem = previewBranchProblem(previewBranch); + if (problem) return problem; + } + if (checkBuilt) return builtHomepageProblem(homepageOutDir(paths)); + return null; +} + +export async function buildHomepageAction(): Promise<StreamActionResult> { + const paths = getPaths(); + return runManagedFunction({ + kind: "build-homepage", + queueKey: BUILD_QUEUE, + paths, + fn: async (onLog, signal) => { + const code = await buildHomepage({ paths, onLog, signal }); + if (signal.aborted) return; + if (code !== 0) throw new Error(`Homepage build failed (exit ${code})`); + }, + }); +} + +// Deploy the homepage already built in homepage/out: production (branch +// `main`), or with `opts.previewBranch` a Cloudflare Pages PREVIEW of the +// `archilyzer` project. +export async function deployHomepageAction(opts?: { + previewBranch?: string; +}): Promise<StreamActionResult> { + const paths = getPaths(); + const refusal = deployRefusal(paths, opts?.previewBranch, true); + if (refusal) return { ok: false, error: refusal }; + const branch = opts?.previewBranch?.trim(); + return runManagedFunction({ + kind: "deploy-homepage", + queueKey: DEPLOY_QUEUE, + paths, + fn: (onLog, signal) => + deployHomepage({ paths, onLog, signal, previewBranch: branch }), + }); +} + +// Build the homepage and, only if that succeeds, deploy it — one job, one log, +// one Cancel, on the deploy queue (as build-deploy-hub is for the hub). A failed +// build never reaches the deploy: homepage/out would still hold the PREVIOUS +// build, and deploying that after a failure would ship old news as new. +export async function buildAndDeployHomepageAction(opts?: { + previewBranch?: string; +}): Promise<StreamActionResult> { + const paths = getPaths(); + // Before the build: learning the preview name is wrong after a build wastes it. + const refusal = deployRefusal(paths, opts?.previewBranch, false); + if (refusal) return { ok: false, error: refusal }; + const branch = opts?.previewBranch?.trim(); + return runManagedFunction({ + kind: "build-deploy-homepage", + queueKey: DEPLOY_QUEUE, + paths, + fn: async (onLog, signal) => { + onLog("=== Build homepage ===\n"); + const code = await buildHomepage({ paths, onLog, signal }); + if (signal.aborted) return; + if (code !== 0) { + throw new Error(`Homepage build failed (exit ${code}) — not deploying.`); + } + onLog("\n=== Deploy homepage ===\n"); + await deployHomepage({ paths, onLog, signal, previewBranch: branch }); + }, + }); +} diff --git a/editor/app/sites/page.tsx b/editor/app/sites/page.tsx @@ -8,10 +8,15 @@ import { suggestNextVersion, } from "yt-dlp-transcript-common/lib/changelog"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; +import { builtHomepageAt } from "yt-dlp-transcript-common/lib/builtExport"; import { getHomepageConfig } from "yt-dlp-transcript-common/lib/homepage"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { listSites } from "yt-dlp-transcript-common/lib/site"; import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; +import { + HOMEPAGE_PAGES_PROJECT, + homepageOutDir, +} from "yt-dlp-transcript-common/publish/build"; import { liveJobRows } from "../jobs/active/buildActiveJobs"; import { RunningJobsList } from "../jobs/components/RunningJobsList"; import { BuildAllSitesButton } from "./components/BuildAllSitesButton"; @@ -19,6 +24,7 @@ import { BuildButtons } from "./components/BuildButtons"; import { BuildModeToggle } from "./components/BuildModeToggle"; import { BuildSitesPanel } from "./components/BuildSitesPanel"; import { CutReleaseForm } from "./components/CutReleaseForm"; +import { HomepageBuildButtons } from "./components/HomepageBuildButtons"; import { HomepageConfigForm } from "./components/HomepageConfigForm"; import { HubBuildButtons } from "./components/HubBuildButtons"; import { DeleteSiteButton, MigrateButton } from "./components/SiteListActions"; @@ -30,8 +36,9 @@ export const metadata: Metadata = { title: "Sites" }; // buttons enqueue (BuildButtons.tsx → sites/lib/buildAction.ts), and only // those. build-export and build-deploy are a site's Publish tab's, build-all // and build-deploy-all are the batch panel's above, build-hub / deploy-hub / -// build-deploy-hub the Hub section's — each has its own console and is not -// repeated here. +// build-deploy-hub the Hub section's, build-homepage / deploy-homepage / +// build-deploy-homepage the Homepage section's — each has its own console and +// is not repeated here. const BUILD_KINDS = new Set([ "build-index", "build-stats", @@ -76,6 +83,7 @@ export default async function SitesPage() { // (it used to drop `progress`, `tasks`, `drainable` and the reorder bounds). const activeJobs = await liveJobRows((j) => BUILD_KINDS.has(j.kind)); const hubConfig = getHomepageConfig(paths); + const homepageBuiltAt = builtHomepageAt(homepageOutDir(paths)); return ( <div className="flex flex-col gap-8"> @@ -192,17 +200,39 @@ export default async function SitesPage() { site that has a public URL, reading each archive where it is published. This config names and brands it, and its Public URL and Cloudflare Pages project are the hub&rsquo;s own (for example{" "} - <code>archilyzer-hub</code>). The <code>homepage</code> package — - Archilyzer&rsquo;s own site, with the docs and the source download - — shares these social links and links to the hub, but deploys to - its own project, <code>archilyzer</code>, with{" "} - <code>archilyzer deploy homepage</code>. + <code>archilyzer-hub</code>). </p> </div> <HomepageConfigForm config={hubConfig} /> <HubBuildButtons project={hubConfig.cloudflareProject ?? null} /> </section> + {/* The software's own site: the `homepage` package, never the hub. It + has no config of its own here — it shares the hub form's social + links — so the section is only its build and deploy. */} + <section className="flex flex-col gap-3 border-t border-border pt-6"> + <div> + <h2 className="text-lg font-semibold">Homepage</h2> + <p className="mt-1 text-sm text-muted-foreground"> + The <code>homepage</code> package is Archilyzer&rsquo;s own site, + with the docs and the source download. It shares the Hub + form&rsquo;s social links and links to the hub, but it builds into{" "} + <code>homepage/out</code> and deploys to its own Pages project,{" "} + <code>{HOMEPAGE_PAGES_PROJECT}</code>. The buttons run the same code + as <code>archilyzer build homepage</code> and{" "} + <code>archilyzer deploy homepage</code>, as jobs. + </p> + </div> + <HomepageBuildButtons + project={HOMEPAGE_PAGES_PROJECT} + builtAt={ + homepageBuiltAt === null + ? null + : new Date(homepageBuiltAt).toLocaleString() + } + /> + </section> + {/* The shared pool: corpus-wide, no site involved. */} <section className="flex flex-col gap-3 border-t border-border pt-6"> <div>