import { readFileSync } from "node:fs"; import type { Metadata } from "next"; import Link from "next/link"; import { Changelog } from "yt-dlp-transcript-common/components/Changelog"; import { extractUnreleasedSection, getLatestReleasedVersion, suggestNextVersion, } from "yt-dlp-transcript-common/lib/changelog"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { getHomepageConfig } from "yt-dlp-transcript-common/lib/homepage"; import { listSites } from "yt-dlp-transcript-common/lib/site"; import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; import type { PublishTargetStatus } from "yt-dlp-transcript-common/views/publishStatus"; import { readPublishStatus } from "yt-dlp-transcript-common/publish/publishState"; import { liveJobRows } from "../jobs/active/buildActiveJobs"; import { RunningJobsList } from "../jobs/components/RunningJobsList"; import { BuildButtons } from "./components/BuildButtons"; import { CutReleaseForm } from "./components/CutReleaseForm"; import { HomepageConfigForm } from "./components/HomepageConfigForm"; import { PublishPanel, type PanelRow } from "./components/PublishPanel"; import { DeleteSiteButton, MigrateButton } from "./components/SiteListActions"; export const dynamic = "force-dynamic"; export const metadata: Metadata = { title: "Sites" }; // Which live jobs the Pool section lists: the kinds the Pool's own buttons // enqueue (BuildButtons.tsx), and only those — the index update included // (Build index is a `publish-update-index` stage now, release 18). Every other // publish stage has its console on its row of the Publish panel. const BUILD_KINDS = new Set([ "publish-update-index", "normalize-transcripts", "archive-transcripts", "archive-combined-transcripts", "normalize-live-chat", "archive-live-chat", "archive-combined-live-chat", ]); // The same file the Cut release form rewrites (`exportChangelogFile`, which // EXPORT_CHANGELOG_FILE overrides for the e2e server), or the page would show // one changelog while the form cut another. function loadExportChangelog(): string | null { try { return readFileSync(getPaths().exportChangelogFile, "utf8"); } catch { return null; } } // The family page: the list of sites, then everything that applies to the whole // family rather than to one of them. A site's own controls — its settings, its // charts, its search aliases, its build & deploy — are its TABS, under // /sites/. // // Section order is the noun first, then the lifecycle the retired /deploy page // read as: the list (the family is what this page is about), then the release, // then the batch build. export default async function SitesPage() { const paths = getPaths(); const sites = listSites(paths); const source = loadExportChangelog(); const lastVersion = source ? getLatestReleasedVersion(source) : null; const suggestedVersion = suggestNextVersion(lastVersion); const unreleased = source ? extractUnreleasedSection(source) : ""; const hasUnreleased = unreleased.length > 0; const existingQueues = getRegistry().activeQueueNames(); // Through the one builder, so this list has the same progress bars /jobs does // (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 status = await readPublishStatus(paths); return (

Sites

+ New site
{sites.length === 0 && (

No sites configured yet. If you're upgrading an existing single-site install, migrate your current branding, channel groups, and charts into a first site:

)}
    {sites.map((s) => (
  • {s.siteTitle}
    {s.siteId} · {s.channels.length} channel {s.channels.length === 1 ? "" : "s"} · {s.groups.length} group {s.groups.length === 1 ? "" : "s"} {s.cloudflareProject ? ` · ${s.cloudflareProject}` : ""}
  • ))}
{/* Release: stage the changelog before shipping anything. */}

Release notes

Bullets under ## [Unreleased] in{" "} export/CHANGELOG.md become the next cut release. Cut it before you build & deploy.

{hasUnreleased && source ? (
) : (
No pending changes yet.
)}
{/* Publish: every site, the hub and the homepage as stages (release 18) — the index update shared by all of them, one build and one deploy at a time on the publish queue. Replaces "Build all sites" and the hub's and the homepage's own build sections. */}

Publish

The index is updated once and every site, the hub and the homepage are built from it, each into its own bundle; deploys ship that bundle and check the live site. A site’s policy (its settings: off, build, preview, production) is what Publish now and the{" "} publish lane {" "} do for it; the buttons on a row always run.

({ kind: x.kind, target: x.target, reason: x.reason, ...(x.preview ? { preview: x.preview } : {}), })), skipped: status.plan.skipped, }} previewBranch={status.settings.previewBranch} siteOut={Boolean(process.env.ARCHILYZER_SITE_OUT?.trim())} homepageOut={Boolean(process.env.ARCHILYZER_HOMEPAGE_OUT?.trim())} />
{/* The family's own hub — a site of sorts, but never a [siteId]: its config lives in sites/_homepage, which isValidSiteId rejects. Its build and deploy are its row of the Publish panel above. */}

Hub config

The hub is the export app built in hub mode: one search over every 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’s own (for example{" "} archilyzer-hub). The homepage (Archilyzer’s own site) shares its social links.

{/* The shared pool: corpus-wide, no site involved. */}

Pool

The shared channel pool every site is a selection over: the search index, the stats dataset, the transcript and live-chat sidecars and the downloadable archives. Corpus-wide — no site is involved.

{/* Above the disclosure: a running pool job must be visible without a click. */} {/* Eight job consoles; the family page does not open on them. */}
Pool jobs
); } // The panel's row: what the client needs of a target's status, and no more. function panelRow(t: PublishTargetStatus): PanelRow { return { target: t.target, kind: t.kind, title: t.title, chips: t.chips, policy: t.policy, next: t.next, cloudflareProject: t.cloudflareProject, deployProblem: t.deployProblem ?? null, previewUrl: t.previewUrl ?? null, builtAt: t.built?.builtAt ?? null, url: t.url, }; }