"use client"; import Link from "next/link"; import { useState } from "react"; import { useRouter } from "next/navigation"; import { MAX_PREVIEW_BRANCH, previewAliasUrl, previewBranchProblem, } from "yt-dlp-transcript-common/lib/pagesDeploy"; import { useHydrated } from "../../operations/components/useOperationsStatus"; import { buildAllStaleAction, buildAndDeployTargetAction, buildTargetAction, cancelPublishRunAction, deployTargetAction, publishNowAction, } from "../lib/publishActions"; import { JobLane } from "./JobLane"; // THE PUBLISH PANEL (release 18): one row per site, then the hub, then the // homepage — each with the four chips the publish status computes (index | // built | deployed | live, publish/publishPlan.ts) and its buttons, each // button one run of publish stages on the `publish` queue // (sites/lib/publishActions.ts). Above the rows: Publish now (what the lane // would run — the stale index, then each policy target, never forced), Build // all stale (every stale site, whatever its policy, no deploys), the lane's // chip, and the plan Publish now would enqueue, so the button is predictable. // // The chips are the server's: a lane that settles refreshes the page. export type PanelChip = { tone: "ok" | "stale" | "blocked" | "busy" | "warn" | "off"; text: string }; export type PanelRow = { // A site id, "_hub" or "_homepage". target: string; kind: "site" | "hub" | "homepage"; title: string; chips: { index: PanelChip; built: PanelChip; deployed: PanelChip; live: PanelChip }; policy: string; next: string; cloudflareProject: string | null; // Why it never deploys (a private site), or null. deployProblem: string | null; // The preview alias of the policy's branch, when known. previewUrl: string | null; // When its bundle was built (ms), or null. builtAt: number | null; // The public URL a production deploy answers on, or null. url: string | null; }; export type PanelPlan = { steps: { kind: string; target: string; reason: string; preview?: string }[]; skipped: { kind: string; target: string; reason: string }[]; }; type Props = { rows: PanelRow[]; index: PanelChip; lane: PanelChip; plan: PanelPlan; // settings.publish.previewBranch: a site row's preview textbox starts on it. previewBranch: string; // ARCHILYZER_SITE_OUT / ARCHILYZER_HOMEPAGE_OUT are set: "Deploy local". siteOut: boolean; homepageOut: boolean; }; const TONE: Record = { ok: "border-success/30 bg-success-soft text-success", stale: "border-warning/30 bg-warning-soft text-warning", blocked: "border-destructive/30 bg-destructive-soft text-destructive", busy: "border-info/30 bg-info-soft text-info", warn: "border-warning/30 bg-warning-soft text-warning", off: "border-border bg-muted text-muted-foreground", }; export function Chip({ name, chip }: { name: string; chip: PanelChip }) { return ( {name} {chip.text} ); } const PRIMARY = "px-3 py-2 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90 disabled:opacity-50"; const SECONDARY = "px-3 py-2 rounded-md border border-border text-sm font-medium hover:bg-muted disabled:opacity-50"; type Lane = { key: number; title: string; subtitle: string; run: () => ReturnType }; function useLane() { const [lane, setLane] = useState(null); const [n, setN] = useState(0); const launch = (l: Omit) => { const key = n + 1; setN(key); setLane({ ...l, key }); }; return { lane, launch }; } export function PublishPanel({ rows, index, lane, plan, previewBranch, siteOut, homepageOut }: Props) { const router = useRouter(); const hydrated = useHydrated(); const { lane: head, launch } = useLane(); return (
{plan.steps.length === 0 ? (

Publish now has nothing to run: everything its policies cover is current.

) : ( <>

Publish now would run, in order:

    {plan.steps.map((s) => (
  1. {s.kind} {s.target} {" "} — {s.reason}
  2. ))}
)} {plan.skipped.length > 0 && (
{plan.skipped.length} left out
    {plan.skipped.map((s) => (
  • {s.kind} {s.target} : {s.reason}
  • ))}
)}
{head && ( router.refresh()} cancelAction={cancelPublishRunAction} /> )}
    {rows.map((row) => (
  • {row.kind === "site" ? ( ) : ( )}
  • ))}
); } function RowHead({ row }: { row: PanelRow }) { return (

{row.title}

{row.kind === "site" && ( {row.target} )} policy: {row.policy}

next: {row.next}

); } function PreviewInput({ value, onChange, placeholder, }: { value: string; onChange: (v: string) => void; placeholder: string; }) { return ( ); } function PreviewProblem({ problem }: { problem: string | null }) { if (problem === null) return null; return ( // Not "preview branch problem": a name that CONTAINS the input's would // make every by-label lookup for the input ambiguous.

{problem}

); } function SiteRow({ row, previewBranch, siteOut, hydrated, }: { row: PanelRow; previewBranch: string; siteOut: boolean; hydrated: boolean; }) { const router = useRouter(); const { lane, launch } = useLane(); const [preview, setPreview] = useState(previewBranch); const branch = preview.trim(); const problem = previewBranchProblem(branch); const project = row.cloudflareProject; const pages = row.deployProblem === null && project !== null; const id = row.target; return (
{pages && ( <> )} {siteOut && row.deployProblem === null && ( )}
{pages && } {pages && problem === null && branch && (

A preview goes to ; production to{" "} {row.url ? : {project}}.

)} {row.deployProblem !== null &&

{row.deployProblem}.

} {row.deployProblem === null && project === null && (

No Cloudflare Pages project — set one on the site's page to deploy.

)} {lane && ( router.refresh()} cancelAction={cancelPublishRunAction} /> )}
); } // The hub's and the homepage's row: the names they have always had ("Build // hub", "Deploy homepage", "Deploy after build", a preview box that starts // empty = production), so the family's two sites read as they did. function FamilyRow({ row, localOut, hydrated }: { row: PanelRow; localOut: boolean; hydrated: boolean }) { const router = useRouter(); const { lane, launch } = useLane(); const [deploy, setDeploy] = useState(false); const [preview, setPreview] = useState(""); const noun = row.kind === "hub" ? "hub" : "homepage"; const target = row.target; const branch = preview.trim(); const problem = branch ? previewBranchProblem(branch) : null; const project = row.cloudflareProject; const alias = branch && problem === null && project ? previewAliasUrl(project, branch) : null; const where = branch ? ({ where: "preview", preview: branch } as const) : ({ where: "production" } as const); const dest = `${project ?? "no project"}${branch ? ` (preview ${branch})` : " (production)"}`; const out = row.kind === "hub" ? ".export-builds/_hub/out" : "homepage/out"; const built = row.builtAt === null ? null : new Date(row.builtAt).toLocaleString(); return (
{localOut && ( )}

{built ? ( <> Deploy {noun} ships {out}, built {built}, ) : ( <> {out} holds no build yet, so Deploy {noun} has nothing to ship. A deploy goes )}{" "} to {project ?? "no Pages project"}{" "} {!branch ? ( <>(production){row.url ? <>: . : "."} ) : alias ? ( <> (preview {branch}): ; the live site is left alone. ) : ( <> (preview {branch}). )}

{row.kind === "homepage" && (

Build homepage also publishes the source mirror (archilyzer source publish) and refuses if a denied string survives the scrub; a refusal removes the last published source, from homepage/out too. Deploy homepage refuses a build whose source was not audited under today’s rules.

)} {lane && ( router.refresh()} cancelAction={cancelPublishRunAction} /> )}
); } function UrlLink({ url }: { url: string }) { return ( {url} ); }