"use client"; import { useState } from "react"; import { StreamActionLog } from "yt-dlp-transcript-common/components/StreamActionLog"; import { formatBytes } from "yt-dlp-transcript-common/lib/format"; import { cancelJobAction } from "../../jobs/actions"; import { evictClipWindowsAction } from "../actions"; // FETCHED CLIP WINDOWS — the one thing in the corpus nothing prunes. // // A window is a few seconds of a video's source media, fetched so the next tool // that wants those seconds does not spend a source's patience again. The // retention sweep is pointer-driven and never sees one; the cleanup lanes are // about `audio.*`. So they accumulate on the platter, forever, and until this // card there was no control that removed one. // // A CARD RATHER THAN A ROW ACTION, for the reason the saved-video store gets // one: eviction is not a fact about a location. It is corpus-wide by default // (the windows are wherever the channels are, on every drive at once), and the // figure it acts on is already in each row's Media line. // // ONE CARD, TWO MOUNTS. `/storage` renders it corpus-wide; a channel's Storage // panel renders the same card with a `slug`, which is the only difference the // controller knows about (`evictClipWindows` takes an optional slug and walks // one channel instead of all of them). Two components would be two sets of // gates, two wordings of the by-age caveat and two chances for one of them to // drift into claiming a reference count nobody has. // // THE LIMITATION IS THE FIRST THING IT SAYS. Whether a window is still wanted // is a fact about a umtool manifest — a report being rendered to video cites // spans — and the editor cannot see those manifests: they live in a umtool // project, possibly on another machine, possibly not written yet. There is no // reference count to consult and no honest way to invent one. So the rule is // "older than N days", and an operator evicting something a report still cites // has spent a fetch, not lost data. They are entitled to know that beforehand. // // PREVIEW FIRST, AND THE PREVIEW IS THE GATE. `dryRun` runs the identical pass // and deletes nothing, so the number in the log is produced by the code that // would do the work rather than by a second estimate that can disagree — and // until one has ANSWERED in this session the destructive button is disabled. // One click should not be able to delete every clip window in the corpus, and // "older than 30 days" over a corpus nobody has looked at is a number the // operator has no way to picture. // // ARMED ON `onSettled`, NEVER IN THE TRIGGER. The trigger returns as soon as // the job is enqueued, so flipping the flag there would let somebody press // Preview and then Evict before a single count had appeared — a gate that // unlocks on the click that opens it is not a gate. // // "ANY AGE" IS A SECOND GATE. `0` means every window on every drive, which is // a legitimate ask (the operator is emptying a disk) and the one setting where // a preview alone is not enough of a pause. It needs the checkbox ticked as // well, every time — the tick resets when the age changes, so it can never be // left armed from an earlier, narrower choice. // // ⚠️ Both panels are rendered UNCONDITIONALLY and only `disabled` changes — // `StreamActionLog` calls router.refresh() the instant a run ends, and a panel // unmounted by its own result takes its log with it (plans/FACTS.md). const AGES = [0, 7, 30, 90, 180] as const; export function ClipWindowsCard({ clipsBytes, slug, blockedReason = null, }: { // Bytes this scope's windows occupy, or null when nothing has measured them // — a channel whose snapshot predates `totalClipsBytes`, or has none. Null is // NOT zero: "none measured" claims a walk that never happened. clipsBytes: number | null; // Present on a channel's Storage panel: every run is scoped to this channel. // Absent on /storage, where the sweep is the whole corpus. slug?: string; // Why both buttons are off, or null. The channel page passes the same busy // sentence its moves are gated on; the server is still the guard // (`evict-clips` declares `needsMedia`), this is the courtesy that says so // before the click rather than in a job log afterwards. blockedReason?: string | null; }) { const [days, setDays] = useState(30); // Reset by any change of age: a preview of "older than 90 days" says nothing // about what "any age" would take, and an armed checkbox from a narrower // choice is exactly the thing this gate exists to stop. const [previewed, setPreviewed] = useState(false); const [confirmed, setConfirmed] = useState(false); const takesEverything = days === 0; const blocked = blockedReason !== null; const canEvict = previewed && (!takesEverything || confirmed) && !blocked; const scope = slug ? { slug } : {}; const where = slug ? "in this channel" : "across the corpus"; return (

Fetched clip windows

{clipsBytes === null ? "not measured" : clipsBytes > 0 ? `${formatBytes(clipsBytes)} ${where}` : "none measured"}

A window is a few seconds of a video’s source media, fetched for another tool and kept beside the video it came from. Nothing prunes one: the retention sweep is pointer-driven and the cleanup lanes are about{" "} audio.*.{" "} {slug ? "They are already counted in the audio total above." : "They are already counted in each location’s Media figure above."}

Eviction is by age only. Nothing here can know whether a report still cites a window — those manifests live in umtool projects this editor cannot see. An evicted window is re-fetchable, so the cost of getting this wrong is one fetch, not data. Preview first.

{blockedReason && (

{blockedReason}

)} {takesEverything && ( )}
evictClipWindowsAction({ ...scope, olderThanDays: days, dryRun: true, }) } // `started` is false when the action refused before a job existed — // a refusal is not a preview, so it arms nothing. onSettled={(started) => { if (started) setPreviewed(true); }} cancelAction={cancelJobAction} buttonLabel="Preview eviction" runningLabel="Walking…" label="Preview eviction" disabled={blocked} /> evictClipWindowsAction({ ...scope, olderThanDays: days }) } cancelAction={cancelJobAction} buttonLabel="Evict fetched windows" runningLabel="Evicting…" label="Evict fetched windows" disabled={!canEvict} />
{!canEvict && (

{blocked ? blockedReason : previewed ? "Tick the box above to evict every window." : "Preview first — the eviction button unlocks once the dry run has reported what it would take."}

)}
); }