Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Mcommon/views/autoQueueStatus.ts | 22++++++++++++++++++++++
Meditor/app/operations/components/RunnerOperationView.tsx | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Meditor/app/operations/components/dispatch.ts | 8++++++++
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`;