"use client"; import Link from "next/link"; import { useCallback, useEffect, useState, useTransition } from "react"; import { formatDuration } from "yt-dlp-transcript-common/lib/format"; import { disableWorkerAction, drainWorkerAction, enableWorkerAction, setDefaultWorkersAction, stopWorkerPartialAction, } from "../actions"; import { PauseLaneButton } from "../../components/lanes/pauseControl"; import type { WorkersPayload, WorkerTask, WorkerView, } from "yt-dlp-transcript-common/views/workers"; // The payload shape used to be DECLARED here, in a `"use client"` file, and // the server builder imported it back out — the renderer owning the wire // shape. It lives in `common/views/workers.ts` now, beside the function that // builds it, and every component that names it says so. // Live wall-clock that re-renders once a second; null until mounted so SSR and // the first client render agree (no Date.now() hydration mismatch). function useNow(): number | null { const [now, setNow] = useState(null); useEffect(() => { setNow(Date.now()); const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, []); return now; } const POLL_MS = 1000; export function WorkersView({ initial }: { initial: WorkersPayload }) { const [payload, setPayload] = useState(initial); const [pending, startTransition] = useTransition(); // Transient "Saved as default" confirmation shown after the button runs. const [savedDefault, setSavedDefault] = useState(false); const refetch = useCallback(async () => { try { const res = await fetch("/api/view/workers", { cache: "no-store" }); if (res.ok) setPayload((await res.json()) as WorkersPayload); } catch { // transient — keep polling } }, []); useEffect(() => { let cancelled = false; let timer: ReturnType | null = null; async function tick() { if (!cancelled) await refetch(); if (!cancelled) timer = setTimeout(tick, POLL_MS); } timer = setTimeout(tick, POLL_MS); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, [refetch]); function run(action: () => Promise) { startTransition(async () => { await action(); await refetch(); }); } function saveDefault() { startTransition(async () => { const res = (await setDefaultWorkersAction()) as { ok: boolean }; await refetch(); if (res.ok) setSavedDefault(true); }); } // Clear the "Saved" confirmation a few seconds after it appears. useEffect(() => { if (!savedDefault) return; const id = setTimeout(() => setSavedDefault(false), 3000); return () => clearTimeout(id); }, [savedDefault]); const { workers, paused, defaultEnabledIds } = payload; const defaultSet = new Set(defaultEnabledIds ?? []); return (
{paused && ( Transcriptions paused — batches are waiting; parakeet jobs stop after the current segment and resume on the next run. Resume restores each worker's previous state. )} {savedDefault && ( Saved as default )}
{workers.length === 0 ? (

No workers configured. Add one on the{" "} Settings {" "} page.

) : (
    {workers.map((w) => ( ))}
)}
); } // Token-driven semantic tones so worker state recolors with every base. function stateBadge(w: WorkerView): { label: string; className: string } { if (w.degraded) { return { label: "degraded", className: "bg-destructive-soft text-destructive border-destructive/30", }; } switch (w.state) { case "enabled": return { label: w.busy ? "busy" : "idle", className: "bg-success-soft text-success border-success/30", }; case "draining": return { label: "draining", className: "bg-warning-soft text-warning border-warning/30", }; default: return { label: "disabled", className: "bg-muted text-muted-foreground border-border", }; } } function WorkerCard({ worker: w, pending, run, inDefault, }: { worker: WorkerView; pending: boolean; run: (action: () => Promise) => void; // True when this worker is part of the saved launch default — it will start // enabled on the next server start. inDefault: boolean; }) { const badge = stateBadge(w); const engine = w.kind === "remote" ? "remote" : w.kind === "llm" ? "llm endpoint" : (w.appId ?? "local"); return (
  • {w.name} {badge.label} {engine} priority {w.priority} {w.tags && w.tags.length > 0 && ( tags: {w.tags.join(", ")} )} {inDefault && ( default )} {(w.state === "disabled" || w.state === "draining" || w.degraded) && ( )} {w.busy && w.canStopPartial && ( )} {w.state === "enabled" && !w.degraded && ( <> )} {w.state === "draining" && ( )}
    {w.degraded && (

    Auto-disabled — unreachable or repeated failures. Enable to retry.

    )} {w.tasks.length > 0 && (
      {w.tasks.map((t) => (
    • ))}
    )}
  • ); } function TaskRow({ task }: { task: WorkerTask }) { const now = useNow(); const hasFraction = typeof task.fraction === "number"; const pct = hasFraction ? Math.round((task.fraction as number) * 100) : 0; const elapsed = now === null ? null : formatDuration(Math.max(0, Math.round((now - task.startedAt) / 1000))) || "0:00"; const meta = [elapsed, hasFraction ? `${pct}%` : null, task.detail] .filter(Boolean) .join(" · "); return (
    Transcribing {task.channelSlug ? ( {task.label} ) : ( {task.label} )} {task.channelSlug && ( {" "} in {task.channelSlug} )} {meta}
    {hasFraction ? ( ) : ( // Indeterminate (no parseable progress yet): a full-width amber pulse // reads as "working, no number" rather than a stalled ~33%. )}
    ); }