commit ca4fd1ff99a0f8ab8951c2b76d175ceb63cd8881
parent 4d89abf91a8ae8ee3aa488d51d0e3d21ed2d3001
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 11:07:46 -0400
operations: the cooldown strip is a region and lists deferred videos
autoQueueStatus gains `deferred: VideoDeferralView[]` (live deferrals,
soonest first). The runner page's strip renders when a platform cools OR a
video is deferred, as role="region" "Rate-limit cooldown" (a div, not a
section: the lane section may not nest one), with lists "Platforms in
cooldown" (items unchanged) and "Deferred videos" (channel/video link +
time left). formatCooldown gains an hours arm: 5h 59m, not 359m 58s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 104 insertions(+), 24 deletions(-)
diff --git a/common/views/autoQueueStatus.ts b/common/views/autoQueueStatus.ts
@@ -33,6 +33,14 @@ export type PlatformCooldownView = {
fails: number;
};
+// A video the auto-download pick skips until `untilMs` because it was
+// rate-limited recently (see jobs/unitOutcome.ts). Download kind only.
+export type VideoDeferralView = {
+ videoId: string;
+ channelSlug: string;
+ untilMs: number;
+};
+
export type AutoQueueKindStatus = {
kind: AutoQueueKind;
// THE POLICY AS THE LANE DISPATCHES IT, not as it is stored. Every field is
@@ -58,6 +66,9 @@ export type AutoQueueKindStatus = {
picks: AutoQueuePick[];
// Platforms paused by a rate-limit/network backoff (download kind only).
cooldowns: PlatformCooldownView[];
+ // Videos deferred after a rate limit, soonest to return first (download
+ // kind only; empty elsewhere and whenever none is live).
+ deferred: VideoDeferralView[];
// This runner's lane gate — what the page's pause button draws, and what lets
// the operations rail show a runner HOLDING for the first time.
//
@@ -143,6 +154,16 @@ function buildKind(
.filter(([, e]) => e.until > now)
.map(([platform, e]) => ({ platform, untilMs: e.until, fails: e.fails }))
.sort((a, b) => b.untilMs - a.untilMs);
+ const deferred: VideoDeferralView[] = Object.entries(
+ inputs.state[kind].videoDeferrals ?? {},
+ )
+ .filter(([, d]) => d.until > now)
+ .map(([videoId, d]) => ({
+ videoId,
+ channelSlug: d.channelSlug,
+ untilMs: d.until,
+ }))
+ .sort((a, b) => a.untilMs - b.untilMs || a.videoId.localeCompare(b.videoId));
return {
kind,
policy,
@@ -154,6 +175,7 @@ function buildKind(
nextUp: pending.nextUp,
picks: inputs.state[kind].picks,
cooldowns,
+ deferred,
// THE ASYMMETRY IS TRANSCRIPTION'S ALONE, and it is not a special case for
// "the first lane": its hold is LIVE on the worker pool, while every other
// lane's flag IS its gate. See lib/pauseGates.ts.
diff --git a/editor/app/operations/components/RunnerOperationView.tsx b/editor/app/operations/components/RunnerOperationView.tsx
@@ -1,10 +1,12 @@
"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,
+ VideoDeferralView,
} from "yt-dlp-transcript-common/views/autoQueueStatus";
import type { LaneWorker } from "yt-dlp-transcript-common/views/autoQueueLanes";
import { InFlightList } from "./InFlightList";
@@ -113,8 +115,12 @@ export function RunnerOperationView({
</p>
)}
- {status.cooldowns.length > 0 && (
- <CooldownStrip cooldowns={status.cooldowns} now={now} />
+ {(status.cooldowns.length > 0 || status.deferred.length > 0) && (
+ <CooldownStrip
+ cooldowns={status.cooldowns}
+ deferred={status.deferred}
+ now={now}
+ />
)}
<SnoozeControl
@@ -191,36 +197,80 @@ export function useNow(): number | null {
return now;
}
-// Platforms paused by a rate-limit/network backoff. A manual Sync on one of
-// these is refused until it lapses; the runner skips it meanwhile.
+// 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.
+//
+// A <div role="region">, NOT a <section>: this sits inside the lane's own
+// <section>, and the structural contract above forbids a nested one. The
+// accessible role and name are the same either way.
function CooldownStrip({
cooldowns,
+ deferred,
now,
}: {
cooldowns: PlatformCooldownView[];
+ deferred: VideoDeferralView[];
now: number | null;
}) {
+ const left = (untilMs: number) =>
+ now === null ? null : Math.max(0, Math.ceil((untilMs - now) / 1000));
return (
- <div className="flex flex-col gap-1 rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-sm">
- <span className="font-medium text-warning">
- Rate-limit cooldown — auto-download and manual sync are paused on:
- </span>
- <ul className="flex flex-wrap gap-x-4 gap-y-1 text-warning">
- {cooldowns.map((c) => {
- const secs =
- now === null
- ? null
- : Math.max(0, Math.ceil((c.untilMs - now) / 1000));
- return (
- <li key={c.platform} className="tabular-nums">
- <span className="font-mono">{c.platform}</span>
- {secs !== null && (
- <> — {formatCooldown(secs)} left (attempt {c.fails})</>
- )}
- </li>
- );
- })}
- </ul>
+ <div
+ role="region"
+ aria-label="Rate-limit cooldown"
+ className="flex flex-col gap-1 rounded-md border border-warning/30 bg-warning-soft px-3 py-2 text-sm"
+ >
+ {cooldowns.length > 0 && (
+ <>
+ <span className="font-medium text-warning">
+ Rate-limit cooldown — auto-download and manual sync are paused on:
+ </span>
+ <ul
+ aria-label="Platforms in cooldown"
+ className="flex flex-wrap gap-x-4 gap-y-1 text-warning"
+ >
+ {cooldowns.map((c) => {
+ const secs = left(c.untilMs);
+ return (
+ <li key={c.platform} className="tabular-nums">
+ <span className="font-mono">{c.platform}</span>
+ {secs !== null && (
+ <> — {formatCooldown(secs)} left (attempt {c.fails})</>
+ )}
+ </li>
+ );
+ })}
+ </ul>
+ </>
+ )}
+ {deferred.length > 0 && (
+ <>
+ <span className="font-medium text-warning">
+ Deferred videos — skipped by auto-download until:
+ </span>
+ <ul
+ aria-label="Deferred videos"
+ className="flex flex-wrap gap-x-4 gap-y-1 text-warning"
+ >
+ {deferred.map((d) => {
+ const secs = left(d.untilMs);
+ return (
+ <li key={d.videoId} className="tabular-nums">
+ <Link
+ href={`/channels/${d.channelSlug}/videos/${d.videoId}`}
+ className="font-mono underline underline-offset-2 hover:text-brand"
+ >
+ {d.channelSlug}/{d.videoId}
+ </Link>
+ {secs !== null && <> — {formatCooldown(secs)} left</>}
+ </li>
+ );
+ })}
+ </ul>
+ </>
+ )}
</div>
);
}
diff --git a/editor/app/operations/components/dispatch.ts b/editor/app/operations/components/dispatch.ts
@@ -201,8 +201,16 @@ export function formatRecency(
return key.estimated ? `≈${iso}` : iso;
}
+// A remaining cooldown: 42s, 3m 21s, 5h 59m. The hours arm is for a 6 h video
+// deferral, which would otherwise read "359m 58s"; past an hour the seconds
+// are noise and are dropped.
export function formatCooldown(secs: number): string {
if (secs < 60) return `${secs}s`;
+ if (secs >= 3600) {
+ const h = Math.floor(secs / 3600);
+ const m = Math.floor((secs % 3600) / 60);
+ return m === 0 ? `${h}h` : `${h}h ${m}m`;
+ }
const m = Math.floor(secs / 60);
const s = secs % 60;
return s === 0 ? `${m}m` : `${m}m ${s}s`;