Archilyzer · Source

archilyzer

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

commit 2b28e4f80f9171053c70f660f5d8c7ad0b2276e9
parent 002d49d8be4effcc1823ab8f0c2f02939db3380b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue, 22 Sep 2026 16:00:52 -0400

editor: the channel Storage panel can evict that channel's clip windows

`data/<id>/clips/` was reachable only from /storage, corpus-wide — so the
operator looking at one channel's "audio on disk" figure, deciding whether its
cache is worth keeping, had to leave the page and sweep every drive instead.

The card is the SAME component, mounted with a slug rather than copied: one set
of gates (preview first, tick for "any age"), one wording of the by-age caveat,
one chance to drift. `evictClipWindows` already took an optional slug.

Disabled with the busy sentence the moves on that panel are gated on. The server
is still the guard — `evict-clips` declares `needsMedia` — this just says so
before the click instead of in a job log afterwards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Meditor/app/channels/[slug]/components/stages/StorageStage.tsx | 23+++++++++++++++++++++++
Meditor/app/channels/[slug]/page.tsx | 4++++
Meditor/app/storage/components/ClipWindowsCard.tsx | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Meditor/e2e/channel-storage.spec.ts | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 177 insertions(+), 12 deletions(-)

diff --git a/editor/app/channels/[slug]/components/stages/StorageStage.tsx b/editor/app/channels/[slug]/components/stages/StorageStage.tsx @@ -7,6 +7,7 @@ import { formatBytes } from "yt-dlp-transcript-common/lib/format"; import type { ChannelMediaLocation } from "yt-dlp-transcript-common/lib/channelMedia"; import type { RelocationPreview } from "yt-dlp-transcript-common/controller/relocateChannelMedia"; import { MediaLocationBadge } from "../../../../components/MediaLocationBadge"; +import { ClipWindowsCard } from "../../../../storage/components/ClipWindowsCard"; import { cancelJobAction } from "../../../../jobs/actions"; import { clearRelocationMarkerAction, @@ -86,6 +87,10 @@ type Props = { // when the snapshot predates the field (or there is no snapshot), and rendered // as "—" rather than "0": a zero here would claim a measurement nobody took. mediaBytes: number | null; + // Bytes this channel's `data/<id>/clips/` windows occupy, from the same + // snapshot. Null when the snapshot predates the field, for the same reason + // mediaBytes is: "0 B of clips" is a measurement nobody took. + clipsBytes: number | null; // Free space on the volume the media is on RIGHT NOW — the platter for a // relocated channel, the corpus disk otherwise. freeBytes: number; @@ -113,6 +118,7 @@ export function StorageStage({ location, locationLabel, mediaBytes, + clipsBytes, freeBytes, volumeDir, blockedReason, @@ -215,6 +221,23 @@ export function StorageStage({ : null } /> + + {/* THE ONE THING UNDER data/ THAT NOTHING PRUNES, and the same card + /storage renders corpus-wide — mounted here with this channel's slug + so the sweep is scoped to it. It belongs on the Storage panel rather + than on a cleanup page because it is bytes on a drive, and the + operator reading "audio on disk" above is the one deciding whether + this channel's cache is worth keeping. + + BY AGE, AND THE CARD SAYS SO. Whether a window is still cited is a + fact about a umtool manifest this editor cannot see, so there is no + reference count to consult — an evicted window costs a fetch, not + data. That sentence is the card's, not a copy of it. */} + <ClipWindowsCard + slug={slug} + clipsBytes={clipsBytes} + blockedReason={blockedReason} + /> </div> ); } diff --git a/editor/app/channels/[slug]/page.tsx b/editor/app/channels/[slug]/page.tsx @@ -616,6 +616,10 @@ export default async function ChannelDetailPage({ // snapshot predates the field or does not exist: a 0 would claim a // measurement nobody took. mediaBytes={snapshot.totalAudioBytes ?? null} + // The clips/ share of the same snapshot — the number the evict + // card acts on. Null, not 0, when the field is absent: the + // snapshot may predate it. + clipsBytes={snapshot.totalClipsBytes ?? null} freeBytes={freeBytes} volumeDir={volumeDir} blockedReason={blockedReason} diff --git a/editor/app/storage/components/ClipWindowsCard.tsx b/editor/app/storage/components/ClipWindowsCard.tsx @@ -19,6 +19,13 @@ import { evictClipWindowsAction } from "../actions"; // (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 @@ -52,7 +59,24 @@ import { evictClipWindowsAction } from "../actions"; const AGES = [0, 7, 30, 90, 180] as const; -export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { +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<number>(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 @@ -60,7 +84,10 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { const [previewed, setPreviewed] = useState(false); const [confirmed, setConfirmed] = useState(false); const takesEverything = days === 0; - const canEvict = previewed && (!takesEverything || confirmed); + const blocked = blockedReason !== null; + const canEvict = previewed && (!takesEverything || confirmed) && !blocked; + const scope = slug ? { slug } : {}; + const where = slug ? "in this channel" : "across the corpus"; return ( <article aria-label="clip windows" @@ -72,9 +99,11 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { aria-label="clip windows bytes" className="text-sm text-muted-foreground tabular-nums" > - {clipsBytes > 0 - ? `${formatBytes(clipsBytes)} across the corpus` - : "none measured"} + {clipsBytes === null + ? "not measured" + : clipsBytes > 0 + ? `${formatBytes(clipsBytes)} ${where}` + : "none measured"} </span> </div> @@ -82,8 +111,10 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { A window is a few seconds of a video&rsquo;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{" "} - <code>audio.*</code>. They are already counted in each location&rsquo;s - Media figure above. + <code>audio.*</code>.{" "} + {slug + ? "They are already counted in the audio total above." + : "They are already counted in each location’s Media figure above."} </p> <p aria-label="clip eviction caveat" @@ -95,6 +126,16 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { of getting this wrong is one fetch, not data. Preview first. </p> + {blockedReason && ( + <p + role="status" + aria-label="clip eviction blocked" + className="text-sm rounded border border-border bg-muted px-3 py-2" + > + {blockedReason} + </p> + )} + <label className="flex items-center gap-2 text-sm"> <span className="text-muted-foreground">Older than</span> <select @@ -135,7 +176,11 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { <StreamActionLog key="evict-clips-preview-log" trigger={() => - evictClipWindowsAction({ olderThanDays: days, dryRun: true }) + 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. @@ -146,10 +191,13 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { buttonLabel="Preview eviction" runningLabel="Walking…" label="Preview eviction" + disabled={blocked} /> <StreamActionLog key="evict-clips-log" - trigger={() => evictClipWindowsAction({ olderThanDays: days })} + trigger={() => + evictClipWindowsAction({ ...scope, olderThanDays: days }) + } cancelAction={cancelJobAction} buttonLabel="Evict fetched windows" runningLabel="Evicting…" @@ -159,9 +207,11 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { </div> {!canEvict && ( <p aria-label="clip eviction gate" className="text-xs text-muted-foreground"> - {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."} + {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."} </p> )} </article> diff --git a/editor/e2e/channel-storage.spec.ts b/editor/e2e/channel-storage.spec.ts @@ -4,6 +4,7 @@ import { mkdir, readdir, symlink, + utimes, writeFile, } from "node:fs/promises"; import { join } from "node:path"; @@ -678,3 +679,90 @@ test("a move to an unmounted root refuses before it creates anything", async ({ // The media is still a real directory in the corpus, unmoved. expect((await lstat(dataDir())).isDirectory()).toBe(true); }); + +// EVICTING THIS CHANNEL'S FETCHED CLIP WINDOWS, from the panel that already +// says how many bytes the channel holds. +// +// The card is the SAME component /storage renders corpus-wide, mounted with a +// slug — so the gates, the wording and the by-age caveat are one definition and +// cannot drift. What this spec pins is the half that is new: the sweep is +// scoped to this channel, and the snapshot figure the panel reads follows the +// deletion (the per-channel run carries a `channelSlug`, which is what queues +// the regen; a corpus-wide run deliberately queues none). +test("the Storage panel evicts this channel's old clip windows", async ({ + page, +}) => { + test.setTimeout(120_000); + await resetData("one-youtube-channel-with-data"); + await writeSettings({ minFreeDiskGB: 0 }); + const clipsDir = resolvePath( + `test-transcripts/channels/${SLUG}/data/${VIDEO}/clips`, + ); + await mkdir(clipsDir, { recursive: true }); + const old = join(clipsDir, "10.00-40.00.mp4"); + const recent = join(clipsDir, "60.00-70.00.mp4"); + for (const [file, ageDays, size] of [ + [old, 90, 4096], + [recent, 1, 1024], + ] as const) { + await writeFile(file, "x".repeat(size)); + await writeFile( + file.replace(/\.mp4$/, ".json"), + JSON.stringify({ requestedBy: "umtool", reason: "e2e" }), + ); + const when = new Date(Date.now() - ageDays * 24 * 60 * 60 * 1000); + await utimes(file, when, when); + } + // The report is what measures `totalClipsBytes`, so it has to run AFTER the + // windows exist — and be waited out, because the card is gated on the same + // busy reason the moves are. + await generateReport(page, SLUG); + await quiet(page); + + const clipsBytesOf = async () => + ( + await readJson<{ totalClipsBytes?: number }>( + `test-transcripts/channels/${SLUG}/snapshot.json`, + ) + ).totalClipsBytes ?? 0; + const before = await clipsBytesOf(); + expect(before).toBeGreaterThanOrEqual(5120); + + await page.goto(channelStage(SLUG, "storage")); + // SCOPED, and the card says which scope it is in. "across the corpus" here + // would be a lie about what the button does. + await expect(page.getByLabel("clip windows bytes")).toContainText( + "in this channel", + ); + await expect(page.getByLabel("clip eviction caveat")).toContainText( + "by age only", + ); + + // --- the dry run lists the window and deletes nothing ------------------- + await page.getByLabel("clip eviction age").selectOption("30"); + await expect( + page.getByRole("button", { name: "Evict fetched windows" }), + ).toBeDisabled(); + await page.getByRole("button", { name: "Preview eviction" }).click(); + await expect(page.getByLabel("Preview eviction output")).toContainText( + /Would evict 1 window/, + { timeout: 60_000 }, + ); + expect(await pathExists(old)).toBe(true); + + // --- and the real run removes it ---------------------------------------- + const evict = page.getByRole("button", { name: "Evict fetched windows" }); + await expect(evict).toBeEnabled(); + await evict.click(); + await expect(page.getByLabel("Evict fetched windows output")).toContainText( + /Evicted 1 window/, + { timeout: 60_000 }, + ); + expect(await pathExists(old)).toBe(false); + expect(await pathExists(recent)).toBe(true); + + // THE PANEL'S OWN NUMBER FOLLOWS. A one-channel run carries `channelSlug`, + // so the stale report is regenerated behind it; polled because that regen is + // a queued job, not part of the eviction. + await expect.poll(clipsBytesOf, { timeout: 60_000 }).toBeLessThan(before); +});