"use client"; import { useState } from "react"; import type { WidgetSyncPayload } from "yt-dlp-transcript-common/views/widgetSync"; import type { WidgetActionablePayload } from "yt-dlp-transcript-common/views/widgetActionable"; import type { ActiveJobsPayload } from "yt-dlp-transcript-common/views/activeJobs"; import type { WorkersPayload } from "yt-dlp-transcript-common/views/workers"; import { LaneDeck } from "../../components/lanes/LaneDeck"; import { DrainAllButton } from "../../jobs/components/DrainAllButton"; import { RetryAllFailedButton } from "../../jobs/components/RetryAllFailedButton"; import { syncAllChannelsAction, type SyncAllResult } from "../../channels/actions"; import { syncAction } from "../../channels/[slug]/pipelineActions"; import { useSectionFrame } from "./WidgetSection"; // Opt-in interactive controls for the monitor widget. Two independent // capabilities, each behind its own flag: // - `controls` → the four LANE CARDS (transcription, downloads, digest, // backfill — each with its gate and, where it has one, its sweep) plus Drain // and Retry. The deck is the same component the dashboard band renders, so a // pinned widget now has FULL PARITY: it could not start or stop a sweep at // all before, and had no digest control whatsoever. // - `sync` → a channel-aware Sync button: pinned to one channel it syncs just // that channel (draining the per-channel stream); otherwise it sweeps all // channels. // `onWorkersChange` refetches the worker payload so the pause/resume label flips // immediately; `onSynced` refetches the sync payload, which is where the digest // and backfill lane state rides as well as the last-sync/scheduler readouts. export function WidgetControls({ controls, sync, channel, confirmSyncAll, workersData, syncData, jobsData, actionableData, onWorkersChange, onSynced, }: { controls: boolean; sync: boolean; channel?: string; confirmSyncAll: boolean; // The two polled payloads the lanes read, passed whole rather than as scalars: // LaneDeck is the one place that knows which field belongs to which lane, and // threading eight booleans through here is how that knowledge got duplicated // in the first place. Persisted state (the pauses, the sweep flags) rides the // widget's 15s-floor poll, and a click refetches immediately. workersData: WorkersPayload | null; syncData: WidgetSyncPayload | null; // The lanes' DETAIL lines read these two. They ride the widget's own section // flags (`jobs`/`disk` and `actionable`), so in a widget pinned without them // they are null — and a null drops the line rather than rendering a zero, // which is the rule LaneDeck states. jobsData: ActiveJobsPayload | null; actionableData: WidgetActionablePayload | null; onWorkersChange: () => void | Promise; onSynced: () => void | Promise; }) { const { dense } = useSectionFrame(); return (
{controls && ( <> {/* Not lanes: one-shot recovery actions over the whole queue. */}
)} {sync && ( )}
); } // The Sync button. Channel-aware: with `channel` set it runs the streaming // per-channel sync (draining the stream to completion, exactly as // ChannelSyncButton); otherwise it runs the full Sync-all sweep (optionally // gated by a confirm) and briefly surfaces the queued/skipped counts. function SyncButton({ channel, confirmSyncAll, onSynced, }: { channel?: string; confirmSyncAll: boolean; onSynced: () => void | Promise; }) { const [running, setRunning] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); async function handleClick() { setError(null); setResult(null); if (channel) { setRunning(true); try { const res = await syncAction(channel); if (!res.ok) { setError(res.error); return; } // Drain the stream to completion; the job keeps running server-side. const reader = res.stream.getReader(); while (true) { const { done } = await reader.read(); if (done) break; } } catch (e) { setError((e as Error).message); } finally { setRunning(false); await onSynced(); } return; } if (confirmSyncAll && !window.confirm("Sync all channels now?")) return; setRunning(true); try { const res = await syncAllChannelsAction(); setResult(res); } catch (e) { setError((e as Error).message); } finally { setRunning(false); await onSynced(); } } const label = channel ? "Sync" : "Sync all"; const ariaLabel = channel ? `sync ${channel}` : "sync all channels"; return (
{result && ( `${s.slug}: ${s.reason}`).join("\n") } > Queued {result.queued.length} · skipped {result.skipped.length} )} {error && ( {error} )}
); }