"use client"; import Link from "next/link"; import { useState } from "react"; import type { ActiveJobsPayload } from "yt-dlp-transcript-common/views/activeJobs"; import type { WorkersPayload } from "yt-dlp-transcript-common/views/workers"; import type { WidgetSyncPayload } from "yt-dlp-transcript-common/views/widgetSync"; import type { WidgetActionablePayload } from "yt-dlp-transcript-common/views/widgetActionable"; import { JobsTable } from "../../jobs/components/JobsTable"; import { LaneDeck } from "../lanes/LaneDeck"; import { syncAllChannelsAction, type SyncAllResult } from "../../channels/actions"; import { fmtTime } from "../../widget/lib/relativeTime"; // The hero: a single live instrument readout of the whole pipeline — running/ // queued jobs, worker-pool occupancy + pause state, sync heartbeat + scheduler — // plus the global pause/sync/add controls, and the live job rows underneath. // Updates in place from the cockpit's polls (this is the one signature element). export function PipelineBand({ jobs, workers, sync, actionable, now, onWorkersChange, onSynced, }: { jobs: ActiveJobsPayload | null; workers: WorkersPayload | null; sync: WidgetSyncPayload | null; // Passed straight through to the lane cards, which state each lane's backlog // under its figure. The cockpit already polls it for the Needs-work panel. actionable: WidgetActionablePayload | null; now: number | null; onWorkersChange: () => void | Promise; onSynced: () => void | Promise; }) { const jobList = jobs?.jobs ?? []; const running = jobList.filter((j) => j.status === "running").length; const queued = jobList.filter((j) => j.status === "queued").length; // WHAT IS LEFT UP HERE IS PIPELINE-WIDE, AND ONLY THAT. The digest and // backfill figures went to their own lane cards first; the workers gauge, the // manual downloads pause and the disk gate have now followed them, because // each is one lane's fact and a card that states its own numbers next to its // own switches makes the chip a second, more distant copy. // // `running · queued` stays because it is the whole queue rather than any one // lane, and so do the sync heartbeat and the scheduler. // // The two disk/pause nodes are not deleted — they are CARRIED, aria-labels and // conditions intact, onto the downloads card. disk-space.spec asserts both by // exact label on this page and does not care where on it they are. const lastSyncText = sync == null ? "…" : sync.lastSyncAllAt === null && sync.lastIndividualSyncAt === null ? "never" : fmtTime( Math.max(sync.lastSyncAllAt ?? 0, sync.lastIndividualSyncAt ?? 0), now, false, ) ?? "…"; const schedulerOn = sync?.scheduler.enabled ?? false; return (

Pipeline

{/* THE MONITOR WIDGET IS REACHED FROM THE THING IT MIRRORS, and no longer from the primary nav. It is a PROJECTION of this band's own payload drawn for a second screen — the same lanes, the same figures — and a projection is not a place. See editor/app/lib/nav.ts and plans/editor-operations-ia.md. */} Monitor ↗
0 ? "bg-info animate-pulse motion-reduce:animate-none" : "bg-muted-foreground/40"} label={ <> {running} running {queued > 0 && ( <> {" · "} {queued} queued )} } /> ⟳ synced {lastSyncText} } /> sched {schedulerOn ? "on" : "off"}} />
{/* What is left is not a lane: a one-shot sweep of every channel, and the way in for a channel that does not exist yet. */}
+ Add a channel
{jobList.length > 0 && jobs ? ( // The same one list /jobs draws, with no tail: the band is the head. // The cockpit above already polls /api/view/activeJobs // (DashboardCockpit.tsx) and this component polls it again while // anything is live — the double poll predates this change and is out of // scope here. ) : (

Pipeline idle — no active jobs.

)}
); } function Instrument({ dotClass, label, }: { dotClass: string; label: React.ReactNode; }) { return ( {label} ); } // Sweep every channel now, surfacing the queued/skipped counts briefly. Mirrors // the widget's Sync-all control; refetches the sync readouts on completion. function SyncAllButton({ onSynced, }: { onSynced: () => void | Promise; }) { const [running, setRunning] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); async function handleClick() { setError(null); setResult(null); setRunning(true); try { setResult(await syncAllChannelsAction()); } catch (e) { setError((e as Error).message); } finally { setRunning(false); await onSynced(); } } return ( {result && ( `${s.slug}: ${s.reason}`).join("\n") } > Queued {result.queued.length} · skipped {result.skipped.length} )} {error && ( {error} )} ); }