"use client"; import Link from "next/link"; import { useCallback, useEffect, useState } from "react"; import type { AutoQueueKind } from "yt-dlp-transcript-common/jobs/autoQueueState"; import type { AutoQueueKindStatus, PlatformCooldownView, PlatformPaceView, SubtitleDeferralView, VideoDeferralView, } from "yt-dlp-transcript-common/views/autoQueueStatus"; import type { LaneWorker } from "yt-dlp-transcript-common/views/autoQueueLanes"; import { InFlightList } from "./InFlightList"; import { LaneHeader } from "./LaneHeader"; import { NextUp } from "./NextUp"; import { PolicyTreeEditor } from "./PolicyTreeEditor"; import { SnoozeControl } from "./SnoozeControl"; import { type Channel, formatClock, formatCooldown, leafOrder } from "./dispatch"; import { clearPlatformHoldAction } from "../pacingActions"; import { drainAutoQueueAction, startAutoQueueAction, stopAutoQueueAction, } from "../actions"; // The idle reasons that mean "paused or gated", not "waiting for work". const LANE_PAUSE_REASONS: ReadonlySet = new Set([ "lane-held", "downloads-paused", "snoozed", "disk-gate", ]); // ONE LANE'S RUNNER, IN FULL — any of the four. // // Lifted VERBATIM out of AutoQueueView's KindLane when the console became one // page per operation. The only thing that changed is what is around it: there // is no lane / — Radix // portals its options and unmounts them while closed, which would take the // page-wide option counts the suite asserts to zero; // * role="status" stays reserved for "Saved."; // * data-hydrated stays on the section. export function RunnerOperationView({ kind, title, status, channels, platforms, buckets, operations, runsOn, onRefresh, }: { kind: AutoQueueKind; title: string; status: AutoQueueKindStatus; channels: Channel[]; platforms: string[]; buckets: string[]; // The operation ids this lane dispatches. Empty for the two bucket lanes; the // ladder draws a select only when there is more than one. operations: string[]; // Delegate workers that can take this lane's work besides this machine. // Read-only; the header draws it, and empty draws nothing. runsOn: LaneWorker[]; onRefresh: () => Promise; }) { const [busy, setBusy] = useState(false); // What the last Clear hold did. Held HERE, not in the strip: a clear that // leaves nothing to list unmounts the strip, and the sentence must outlive it. const [holdNote, setHoldNote] = useState(null); const now = useNow(); const control = useCallback( async (action: "start" | "stop" | "drain") => { setBusy(true); try { // Server actions, not /api/auto-queue/control: that route is behind // the ops token since release 19, and a page holds no token. await (action === "start" ? startAutoQueueAction(kind) : action === "stop" ? stopAutoQueueAction(kind) : drainAutoQueueAction(kind)); await onRefresh(); } finally { setBusy(false); } }, [kind, onRefresh], ); return ( // data-hydrated is a TESTING AFFORDANCE, and a deliberate one. A React state // update that lands before hydration is silently discarded — no error, no // request, nothing — and this repo has lost days to that failure mode more // than once. `now` is null until the client mount effect runs, so it is an // exact "React is live on this subtree" signal that already exists; exposing // it lets a test wait for the page rather than race it.
{kind === "transcription" && (

Auto-transcribe runs at background priority: a manually-triggered transcription preempts it for the next free worker — the in-flight auto transcription finishes (drains), then the manual one runs, then auto resumes.

)} {(status.cooldowns.length > 0 || status.deferred.length > 0 || (status.pace?.length ?? 0) > 0 || (status.subtitleDeferred?.length ?? 0) > 0) && ( )} {holdNote && ( {holdNote} )}
); } // The pick log, cross-referenced against the ladder by ORDINAL rather than by a // leafLabel() id lookup — the rung numbers are right there, so "rule 2" is a // pointer you can follow with your eye. function RecentPicks({ status }: { status: AutoQueueKindStatus }) { const leaves = leafOrder(status.policy.root); return (

Recent picks

{status.picks.length === 0 ? (

Nothing picked yet.

) : (
    {status.picks.slice(0, 15).map((p, i) => { const at = leaves.findIndex((l) => l.id === p.leafId); return (
  • {formatClock(p.at)} {p.channelSlug}/{p.videoId} {at >= 0 ? `rule ${at + 1}` : p.leafId}
  • ); })}
)}
); } // Wall-clock that re-renders each second, starting null so SSR and the first // client render agree (no hydration mismatch from Date.now()). export 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; } // Platforms paused by a rate-limit/network backoff, and the videos deferred // after a rate limit. A manual Sync on a cooling platform is refused until it // lapses; the runner skips it meanwhile. A deferred video is skipped by // auto-download only — a manual Sync / download-missing still fetches it. // // RELEASE 17, SLICE RL adds three lists to the same region: the platforms // HELD (a rate limit that outlasted the cooldown cap — one probe at a time, // and every manual fetch on them refused), the request PACE of every platform // a rate limit slowed, and the videos whose SUBTITLES are deferred (their // media came down; the platform was not backed off for them). // // A
, NOT a
: this sits inside the lane's own //
, and the structural contract above forbids a nested one. The // accessible role and name are the same either way. function CooldownStrip({ cooldowns, deferred, pace, subtitleDeferred, laneRunning, laneHeld, onRefresh, onCleared, now, }: { cooldowns: PlatformCooldownView[]; deferred: VideoDeferralView[]; pace: PlatformPaceView[]; subtitleDeferred: SubtitleDeferralView[]; laneRunning: boolean; // The lane is up but paused or gated (its own hold, the downloads pause, // a snooze, the disk gate): it is not waiting for a video. laneHeld: boolean; onRefresh: () => Promise; onCleared: (message: string) => void; now: number | null; }) { const [clearing, setClearing] = useState(null); const clearHold = async (platform: string) => { setClearing(platform); try { const res = await clearPlatformHoldAction(platform); onCleared(res.ok ? res.message : res.error); await onRefresh(); } finally { setClearing(null); } }; const left = (untilMs: number) => now === null ? null : Math.max(0, Math.ceil((untilMs - now) / 1000)); const paceOf = (platform: string) => pace.find((p) => p.platform === platform)?.sleepRequestsSeconds; const cooling = cooldowns.filter( (c) => !c.hold && (now === null || c.untilMs > now), ); const held = cooldowns.filter((c) => c.hold); return (
{held.length > 0 && ( <> Held — failures outlasted the cooldown cap; auto-download probes once at a time, and manual fetches wait for the next probe:
    {held.map((c) => { const secs = left(c.untilMs); const p = paceOf(c.platform); const overdue = secs !== null && secs <= 0; return (
  • {c.platform} —{" "} {c.hold!.rateLimited ? "rate-limited" : "failing (network)"} , held since {formatClock(c.hold!.sinceMs)} ({c.fails}{" "} failures in a row) {secs !== null && (overdue ? ( laneRunning && !laneHeld ? ( <> , probe due — it waits for a pending video on this platform; a clean manual Sync also lifts it ) : laneRunning ? ( <> , probe overdue — the lane is paused; a clean manual Sync also lifts it ) : ( <> , probe overdue — the lane is off; a clean manual Sync also lifts it ) ) : ( <>, next probe in {formatCooldown(secs)} ))} {p !== undefined && <> · pace {p}s}
  • ); })}
)} {cooling.length > 0 && ( <> Rate-limit cooldown — auto-download and manual sync are paused on:
    {cooling.map((c) => { const secs = left(c.untilMs); const p = paceOf(c.platform); return (
  • {c.platform} {secs !== null && ( <> — {formatCooldown(secs)} left (attempt {c.fails}) )} {p !== undefined && <> · pace {p}s}
  • ); })}
)} {pace.length > 0 && ( <> Request pace — seconds between requests, raised by a rate limit:
    {pace.map((p) => (
  • {p.platform} —{" "} {p.sleepRequestsSeconds}s between requests (base{" "} {p.baseSeconds}s)
  • ))}
)} {deferred.length > 0 && ( <> Deferred videos — skipped by auto-download until:
    {deferred.map((d) => { const secs = left(d.untilMs); return (
  • {d.channelSlug}/{d.videoId} {secs !== null && <> — {formatCooldown(secs)} left}
  • ); })}
)} {subtitleDeferred.length > 0 && ( <> Deferred subtitles — downloaded, but the subtitles answered HTTP 429; Download missing subs retries them after:
    {subtitleDeferred.map((d) => { const secs = left(d.untilMs); return (
  • {d.channelSlug}/{d.videoId} {" "} — {d.count}× {secs !== null && ( <> , {d.held ? "left alone " : ""} {formatCooldown(secs)} {d.held ? "" : " left"} )}
  • ); })}
)}
); }