commit a826ba0c86cac9bc046e8e4c418fc655ad715807
parent 8c576dbf23b74da43352f45ad4e9416364877362
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Tue, 29 Sep 2026 23:36:45 -0400
editor: review L1, L3 — the channel page's statfs of the drive goes through the watchdog; the comments name the detectors as they are
- L1: the Storage stage's free space for a relocated channel is read through
`onDrive(media.target, …)` and shows "—" when refused or given up on.
- L3: /channels, /storage's page shell and its Refresh action no longer say a
child stat is what asks.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
5 files changed, 31 insertions(+), 10 deletions(-)
diff --git a/editor/app/channels/[slug]/components/stages/StorageStage.tsx b/editor/app/channels/[slug]/components/stages/StorageStage.tsx
@@ -93,7 +93,8 @@ type Props = {
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;
+ // Null when the drive did not answer the statfs ("—").
+ freeBytes: number | null;
volumeDir: string;
// Why both buttons are off, or null when they are live. Running/queued jobs
// for this channel, or a relocation marker left by an interrupted move.
@@ -151,7 +152,7 @@ export function StorageStage({
</dd>
<dt className="text-muted-foreground">Free on that volume</dt>
<dd aria-label="free on media volume">
- {formatBytes(freeBytes)}{" "}
+ {freeBytes === null ? "—" : formatBytes(freeBytes)}{" "}
<span className="text-xs text-muted-foreground font-mono">
({volumeDir})
</span>
diff --git a/editor/app/channels/[slug]/page.tsx b/editor/app/channels/[slug]/page.tsx
@@ -45,6 +45,10 @@ import {
inspectChannelMedia,
} from "yt-dlp-transcript-common/lib/channelMedia";
import { MediaNotAnswering } from "./components/MediaNotAnswering";
+import {
+ isDriveNotAnswering,
+ onDrive,
+} from "yt-dlp-transcript-common/lib/storageHealth";
import { getFreeBytes } from "yt-dlp-transcript-common/lib/diskSpace";
import {
platformQueueKey,
@@ -612,7 +616,19 @@ export default async function ChannelDetailPage({
// marker — reading the file again here was a second read of the same
// bytes that could disagree with the status rendered beside it.
const marker = media.marker ?? null;
- const freeBytes = await getFreeBytes(volumeDir);
+ // A statfs of the channel's drive goes through the watchdog
+ // (lib/storageHealth.ts): refused on a stalled location, given up on
+ // after 3 s, and read "—" either way.
+ let freeBytes: number | null;
+ try {
+ freeBytes =
+ volumeDir === media.target && media.target
+ ? await onDrive(media.target, () => getFreeBytes(volumeDir))
+ : await getFreeBytes(volumeDir);
+ } catch (err) {
+ if (!isDriveNotAnswering(err)) throw err;
+ freeBytes = null;
+ }
// The SAME two conditions storageActions.ts refuses on, stated here as
// prose so the button is off with a reason rather than off and silent —
// and stated in the action too, because a disabled button is a courtesy
diff --git a/editor/app/channels/page.tsx b/editor/app/channels/page.tsx
@@ -225,7 +225,8 @@ export default async function ChannelsPage({
// 71 rows on every auto-refresh and the rule is that tables never shell out.
const freeByVolume = await volumeFreeBytes({ paths, locations });
// A DRIVE THAT IS NOT ANSWERING, said on its chip. From memory — the health
- // probe (a child `stat` every 15 s) is what asked; the table asks nothing.
+ // pass (the block device's counters every 15 s) or the watchdog on a read is
+ // what found it (lib/storageHealth.ts); the table asks nothing.
const notAnsweringByVolume: Record<string, string | undefined> = {};
for (const loc of locations) {
const stall = stalledLocation(loc);
diff --git a/editor/app/storage/actions.ts b/editor/app/storage/actions.ts
@@ -250,10 +250,12 @@ export async function refreshStorageLocationAction(
const settings = getSettings();
const location = settings.storage.locations.find((l) => l.id === id);
if (!location) return { ok: false, error: `There is no storage location "${id}".` };
- // IS IT ANSWERING, asked first and out of process (a child `stat` against a
- // 3 s timer): the probe below runs in-process, and on a stalled drive it is
- // not asked at all. The channels' remembered answers go too — the operator
- // has just done something about the drive.
+ // IS IT ANSWERING, asked first and without touching the drive (the block
+ // device's counters in /sys, or a child `stat` raced against 3 s where no
+ // device can be named): the probe below runs in-process, and on a stalled
+ // drive it is not asked at all. The counters give no answer within 10 s of
+ // the pass's last sample. The channels' remembered answers go too — the
+ // operator has just done something about the drive.
await refreshLocationHealth(location);
forgetChannelMedia();
const health = locationHealth(location.id);
diff --git a/editor/app/storage/buildStorage.ts b/editor/app/storage/buildStorage.ts
@@ -87,8 +87,9 @@ export async function buildStorage(): Promise<StorageRowsPayload> {
// situation the operator opened the page to understand. The store's SIZE is
// the only thing cached; its location, status and marker are read fresh every
// render, because those are the safety facts.
- // THE DRIVES THAT ARE NOT ANSWERING, in words. From memory: the health probe
- // (a child `stat`, every 15 s) is what asked.
+ // THE DRIVES THAT ARE NOT ANSWERING, in words. From memory: the health pass
+ // (the block device's counters every 15 s) or the watchdog on a page's read
+ // is what found it (lib/storageHealth.ts).
const now = Date.now();
const notAnswering: Record<string, string> = {};
for (const loc of locations) {