"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { buttonVariants } from "@/components/ui/button"; // --------------------------------------------------------------------------- // The four things that happen after the last clip is judged. // // ONE JOB AT A TIME, and every button here says so rather than queueing: the // server refuses a second one with the name of what is running (409), and the // same rule is why a bench fetch and a cut cannot overlap. They write the same // clips/ directory. // // `k of n` IS THE JOB'S OWN COUNT. The cut is one step per clip, so stepIndex // and steps.length are the progress -- no second counter kept in the browser // that a reload would lose, and a Stop that kills the running step's process // group rather than a promise nobody can interrupt. // // STOP ABANDONS THE REST, it does not undo. Every file here is named for its // clip, so pressing the button again resumes: the clips that have a file are // not in the server's list any more. // --------------------------------------------------------------------------- type StepView = { label: string; argv: string[] }; type JobView = { id: string; kind: string; project: string | null; state: "running" | "done" | "failed"; stepIndex: number; steps: StepView[]; error: string | null; log: string[]; next: number; }; type Variant = { module: string; out: string }; /** Where the deliverables are, and what would refuse a cut, a batch or a move (release 17). */ type StorageView = { mode: "local" | "media" | null; /** UMTOOL_MEDIA_DIR is set in the app's environment. */ tiered: boolean; /** Sentences that stop a move: a link whose drive is not there. */ blocked: string[]; cutBlocked: string[]; shareBlocked: string[]; }; export default function DeliverActions({ project, needCut, notFetched, candidates, nextName, unreviewed, hasApply, hasBuild, variants, storage, }: { project: string; /** Confirmed, no mp4, and a window on this disk that holds it. */ needCut: number; /** Confirmed, no mp4, and nothing cached — a download, not a cut. */ notFetched: number; /** What a batch would ship: confirmed, cut, and not already shared. */ candidates: number; nextName: string; unreviewed: number; hasApply: boolean; hasBuild: boolean; variants: Variant[]; storage: StorageView; }) { const [job, setJob] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const [partial, setPartial] = useState(false); const [name, setName] = useState(nextName); const since = useRef(0); const router = useRouter(); // Adopt a job already running, so a reload does not lose one. Also how a // second tab sees the first tab's cut. useEffect(() => { void fetch("/api/report/deliver?running=1", { cache: "no-store" }) .then((r) => r.json()) .then((j) => { // The registry is process-wide and holds one job, which may be another // project's build. Adopting that here would show its log under this // project's panel. const run = j.running as JobView | null; if (run && run.project === project) { setJob(run); since.current = run.log.length; } }) .catch(() => {}); }, [project]); useEffect(() => { if (!job || job.state !== "running") return; const t = setInterval(async () => { const r = await fetch(`/api/report/deliver?job=${job.id}&since=${since.current}`, { cache: "no-store", }); if (!r.ok) return; const j = (await r.json()) as JobView; since.current = j.next; setJob((prev) => (prev ? { ...j, log: [...prev.log, ...j.log] } : j)); // The counts above this panel are SERVER-RENDERED off the directory, and // the job just changed the directory: a cut that finished leaves "cut 2 // confirmed clips" on screen until something re-reads it. Refreshed // here, once, when the job ends -- not on a timer, which would fight the // log the operator is reading. if (j.state !== "running") router.refresh(); }, 700); return () => clearInterval(t); }, [job, router]); const post = useCallback( async (action: string, extra: Record = {}) => { setBusy(true); setError(null); const r = await fetch("/api/report/deliver", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ project, action, ...extra }), }); const j = (await r.json().catch(() => ({}))) as Record; setBusy(false); if (!r.ok) { setError(String(j.error ?? `HTTP ${r.status}`)); return; } since.current = 0; setJob(j.job as JobView); }, [project], ); const running = job?.state === "running"; const n = job?.steps.length ?? 0; const k = Math.min((job?.stepIndex ?? 0) + 1, n); // MOVE DELIVERABLES: the switch, as a job (release 17). Its direction is the // other side of where they are; the reason it cannot run is said beside it // rather than left to a 409 -- above all while a cut or a batch is writing // into the very directories it would move. const moveTo = storage.mode === "media" ? "local" : "media"; const moveReason = running ? `${job!.kind} is running — move when it has finished` : storage.blocked.length ? storage.blocked.join("; ") : moveTo === "media" && !storage.tiered ? "UMTOOL_MEDIA_DIR is not set in umtool's environment — there is no media root to move to" : null; return (
setName(e.target.value)} className="w-44 rounded border border-[var(--color-line)] bg-[var(--color-bg)] px-1.5 py-0.5 font-mono text-[11px] text-[var(--color-text)]" /> {hasApply && ( {unreviewed > 0 && ( )} )} {hasBuild && variants.length > 0 && ( )} {running && ( )}
{moveReason && (

Move deliverables: {moveReason}

)} {(storage.cutBlocked.length > 0 || storage.shareBlocked.length > 0) && (

{[...new Set([...storage.cutBlocked, ...storage.shareBlocked])].join("; ")}

)} {notFetched > 0 && (

{notFetched} confirmed clip{notFetched === 1 ? " has" : "s have"} nothing cached to cut from — those are a download, listed below.

)} {error && (

{error}

)} {job && (
{job.kind} {job.state === "running" ? `${k} of ${n}` : `${job.state} · ${n} step${n === 1 ? "" : "s"}`} {job.steps[job.stepIndex] && job.state === "running" && ( {job.steps[job.stepIndex].label} )} {job.error && {job.error}}
{/* The log VERBATIM. apply-manifest.py prints the prose lines that cite each clip ruled incorrect, and that listing is the whole point of running it — paraphrasing it here would be this page deciding what the operator has to read. */}
            {job.log.join("\n")}
          
)}
); }