commit d6307e4de2a4a67e0421ab44671101cc061e489e
parent 646b3a951d66ebe915c376f1814cd158130b60b1
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 09:00:20 -0400
editor: /storage's Drive health timing — five fields for settings.storage.health, saved through the one writer and applied at once; the surfaces say the setting, not the constant
A collapsed block at the foot of /storage (HealthTimingForm): each field
empty = its default (the placeholder), a hint per field, the ranges.
saveHealthTimingsAction parses with lib/healthTimingsForm.ts (out of range is
refused with a sentence, not clamped; a default is not written), saves a patch
of the storage block, and applies the timings to the process at once, which
re-arms the health pass on a new interval. The Refresh note, the stalled
line, the media notice and the volume chip's title read the clear count, the
interval and the budget from the accessor. Unit test for the parse (95 → 100).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
13 files changed, 409 insertions(+), 24 deletions(-)
diff --git a/editor/app/channels/[slug]/components/MediaNotAnswering.tsx b/editor/app/channels/[slug]/components/MediaNotAnswering.tsx
@@ -1,8 +1,10 @@
import Link from "next/link";
import {
+ healthTimings,
notAnsweringText,
type LocationHealth,
} from "yt-dlp-transcript-common/lib/storageHealth";
+import { secondsText } from "yt-dlp-transcript-common/lib/storageHealthTimings";
// WHAT A PAGE THAT READS A CHANNEL'S `data/` SHOWS WHILE ITS DRIVE IS NOT
// ANSWERING, instead of reading it.
@@ -26,11 +28,13 @@ export function MediaNotAnswering({
}: {
slug: string;
// The stalled location, or null when the drive is on no location the health
- // state knows and a read of it did not answer within 3 s.
+ // state knows and a read of it did not answer within the budget
+ // (`storage.health.budgetMs`).
stall: LocationHealth | null;
// What would have been shown: "The video list", "This video".
what: string;
}) {
+ const timings = healthTimings();
return (
<div className="flex flex-col gap-3">
<p
@@ -42,13 +46,14 @@ export function MediaNotAnswering({
<>
{what} reads this channel's media, which is on “
{stall.label}” — a drive that is {notAnsweringText(stall)}.
- Nothing is read from it until it answers again (it is checked every
- 15 s).
+ Nothing is read from it until it answers again (it is checked every{" "}
+ {secondsText(timings.passIntervalMs)}).
</>
) : (
<>
{what} reads this channel's media, and a read of its drive did
- not answer within 3 s. Nothing more is read from it on this page.
+ not answer within {secondsText(timings.budgetMs)}. Nothing more is
+ read from it on this page.
</>
)}
</p>
diff --git a/editor/app/channels/[slug]/page.tsx b/editor/app/channels/[slug]/page.tsx
@@ -618,7 +618,7 @@ export default async function ChannelDetailPage({
const marker = media.marker ?? null;
// 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.
+ // after the budget (3 s by default), and read "—" either way.
let freeBytes: number | null;
try {
freeBytes =
diff --git a/editor/app/channels/[slug]/videos/[id]/page.tsx b/editor/app/channels/[slug]/videos/[id]/page.tsx
@@ -87,7 +87,8 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { slug, id } = await params;
// The title is read off the drive; a drive that is not answering is not
- // asked, and one that does not answer in 3 s is given up on.
+ // asked, and one that does not answer within the budget
+ // (`storage.health.budgetMs`, 3 s by default) is given up on.
const drive = (await readChannelConfig(getPaths(), slug))?.dataDir?.trim();
let subject = id;
try {
@@ -117,7 +118,8 @@ export default async function VideoDetailPage({
}
// EVERY READ BELOW IS OF THIS VIDEO'S DIRECTORY, on the channel's drive when
// it is relocated, so they go through the watchdog as one unit: a drive that
- // has not answered them in 3 s is marked stalled and the page says so.
+ // has not answered them within the budget (3 s by default) is marked stalled
+ // and the page says so.
const loadAll = async () => {
const dirData = await loadVideoDir(slug, id);
const meta = await loadMeta(slug, id);
diff --git a/editor/app/channels/[slug]/videos/page.tsx b/editor/app/channels/[slug]/videos/page.tsx
@@ -145,8 +145,9 @@ export default async function ChannelVideosPage({
const channelDataDir = path.join(paths.channelsDir, slug, "data");
// THE READS OF THE DRIVE go through the watchdog when the channel is
- // relocated: a drive that has not answered them in 3 s is marked stalled and
- // the page says so instead (see MediaNotAnswering).
+ // relocated: a drive that has not answered them within the budget
+ // (`storage.health.budgetMs`, 3 s by default) is marked stalled and the page
+ // says so instead (see MediaNotAnswering).
const drive = config.dataDir?.trim();
const onMedia = <T,>(call: () => Promise<T>): Promise<T> =>
drive ? onDrive(drive, call) : call();
diff --git a/editor/app/channels/components/ChannelVolumeBar.tsx b/editor/app/channels/components/ChannelVolumeBar.tsx
@@ -44,6 +44,9 @@ export type ChannelVolume = {
// "not answering since 11:35", while the health probe finds the drive not
// answering (its free space is then not asked either).
notAnswering?: string;
+ // With `notAnswering`: how many clean checks clear it, in words ("twice in a
+ // row"; settings.storage.health.clearAfterCleanPasses).
+ clears?: string;
};
export function ChannelVolumeBar({
@@ -105,7 +108,7 @@ export function ChannelVolumeBar({
}
title={
v.notAnswering
- ? `${v.label}: the drive is ${v.notAnswering}. Pages and polls do not touch it until it answers twice in a row; its channels read "not answering".`
+ ? `${v.label}: the drive is ${v.notAnswering}. Pages and polls do not touch it until it answers ${v.clears ?? "again"}; its channels read "not answering".`
: v.freeBytes === undefined
? `${v.label}: the root is not there — an unmounted drive reports no free space rather than its parent's.`
: `${v.label}: ${v.channels} channel(s) hold ${formatBytes(v.bytes)}${
diff --git a/editor/app/channels/page.tsx b/editor/app/channels/page.tsx
@@ -8,9 +8,11 @@ import {
import { getPaths } from "yt-dlp-transcript-common/lib/paths";
import { inspectChannelMedia } from "yt-dlp-transcript-common/lib/channelMedia";
import {
+ healthTimings,
notAnsweringText,
stalledLocation,
} from "yt-dlp-transcript-common/lib/storageHealth";
+import { clearRuleText } from "yt-dlp-transcript-common/lib/storageHealthTimings";
import {
getSite,
listSiteIds,
@@ -226,8 +228,9 @@ 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
- // 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.
+ // pass (the block device's counters, every `storage.health.passIntervalMs`)
+ // 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);
@@ -306,7 +309,10 @@ export default async function ChannelsPage({
unmeasured: rows.length - measured.length,
freeBytes: freeByVolume[id],
...(notAnsweringByVolume[id]
- ? { notAnswering: notAnsweringByVolume[id] }
+ ? {
+ notAnswering: notAnsweringByVolume[id],
+ clears: clearRuleText(healthTimings().clearAfterCleanPasses),
+ }
: {}),
};
})
diff --git a/editor/app/storage/actions.ts b/editor/app/storage/actions.ts
@@ -36,11 +36,19 @@ import { savedVideosStoreBusyReason } from "./lib/storeBusy";
import { refreshLocationHealth } from "yt-dlp-transcript-common/controller/storageWatch";
import { forgetChannelMedia } from "yt-dlp-transcript-common/lib/channelMedia";
import {
+ applyHealthTimings,
+ healthTimings,
locationHealth,
notAnsweringText,
} from "yt-dlp-transcript-common/lib/storageHealth";
+import {
+ clearRuleText,
+ secondsText,
+} from "yt-dlp-transcript-common/lib/storageHealthTimings";
+import { parseHealthTimingsForm } from "./lib/healthTimingsForm";
-// THE SIX THINGS AN OPERATOR MAY DO TO A STORAGE LOCATION.
+// THE SIX THINGS AN OPERATOR MAY DO TO A STORAGE LOCATION (and, at the end,
+// the drive-health timings every location is judged by).
//
// Five of them are small settings writes or one subprocess and run INLINE:
// their whole output is a sentence, and a queued job with a log would be a
@@ -251,21 +259,24 @@ export async function refreshStorageLocationAction(
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 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.
+ // device's counters in /sys, or a child `stat` raced against
+ // `storage.health.probeTimeoutMs` 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 (by default) 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);
if (health?.state === "stalled") {
revalidateStorage();
+ const timings = healthTimings();
return {
ok: true,
note:
`${location.label}: ${notAnsweringText(health)} — ${health.cause ?? "its root did not answer"}. ` +
- `Pages skip this drive until it answers twice in a row (checked every 15 s).`,
+ `Pages skip this drive until it answers ${clearRuleText(timings.clearAfterCleanPasses)} ` +
+ `(checked every ${secondsText(timings.passIntervalMs)}).`,
};
}
const probe = await probeLocationMemo(location, paths, { refresh: true });
@@ -475,3 +486,45 @@ export async function evictClipWindowsAction(opts: {
...(opts.dryRun ? { dryRun: true } : {}),
});
}
+
+// ---------------------------------------------------------------------------
+// The drive-health timings
+// ---------------------------------------------------------------------------
+
+export type HealthTimingsResult = { ok: true; note: string } | { ok: false; error: string };
+
+// SAVE `settings.storage.health` FROM THE /storage FORM (HealthTimingForm).
+//
+// Parsed by `parseHealthTimingsForm`: an empty field is the default and is not
+// written, and a value out of range is REFUSED with a sentence (the schema
+// would clamp it; a save that stored another number than the one typed would
+// read as a form that did not listen). Then written through the one settings
+// writer, as a patch of the storage block, and APPLIED AT ONCE to this
+// process's health state (`applyHealthTimings`, on globalThis): the next read
+// runs on the new budget and cap, the next answer on the new clear count, the
+// next pass on the new probe timeout, and a new pass interval re-arms the
+// pass's timer now. The pass would apply them anyway, at its next run.
+export async function saveHealthTimingsAction(
+ _prev: HealthTimingsResult | undefined,
+ formData: FormData,
+): Promise<HealthTimingsResult> {
+ const parsed = parseHealthTimingsForm(formData);
+ if (!parsed.ok) return parsed;
+ const settings = getSettings();
+ try {
+ await saveSettings({ storage: { ...settings.storage, health: parsed.health } });
+ } catch (e) {
+ return { ok: false, error: (e as Error).message };
+ }
+ const t = applyHealthTimings(getSettings().storage.health);
+ revalidatePath("/storage");
+ return {
+ ok: true,
+ note:
+ `Saved. A read may take ${secondsText(t.budgetMs)}; drives are checked every ` +
+ `${secondsText(t.passIntervalMs)} (a check waits up to ${secondsText(t.probeTimeoutMs)}); ` +
+ `a drive marked not answering is used again after ` +
+ `${t.clearAfterCleanPasses === 1 ? "one clean check" : `${t.clearAfterCleanPasses} clean checks in a row`}; ` +
+ `${t.inFlightPerLocation} read(s) at once per drive.`,
+ };
+}
diff --git a/editor/app/storage/buildStorage.ts b/editor/app/storage/buildStorage.ts
@@ -4,11 +4,13 @@ import { getRegistry } from "yt-dlp-transcript-common/jobs/registry";
import { getFreeBytes } from "yt-dlp-transcript-common/lib/diskSpace";
import { udisksctlAvailable } from "yt-dlp-transcript-common/lib/storageVolumes";
import {
+ healthTimings,
isDriveNotAnswering,
notAnsweringText,
onDrive,
stalledLocation,
} from "yt-dlp-transcript-common/lib/storageHealth";
+import { clearRuleText } from "yt-dlp-transcript-common/lib/storageHealthTimings";
import { listChannelBriefs } from "yt-dlp-transcript-common/controller/channels";
import {
channelsOnLocation,
@@ -88,9 +90,10 @@ export async function buildStorage(): Promise<StorageRowsPayload> {
// 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 pass
- // (the block device's counters every 15 s) or the watchdog on a page's read
- // is what found it (lib/storageHealth.ts).
+ // (the block device's counters, every `storage.health.passIntervalMs`) or the
+ // watchdog on a page's read is what found it (lib/storageHealth.ts).
const now = Date.now();
+ const clears = clearRuleText(healthTimings().clearAfterCleanPasses);
const notAnswering: Record<string, string> = {};
for (const loc of locations) {
const stall = stalledLocation(loc);
@@ -103,7 +106,7 @@ export async function buildStorage(): Promise<StorageRowsPayload> {
: "";
notAnswering[loc.id] =
`${notAnsweringText(stall, now)} — ${stall.cause ?? "its root did not answer"}. ` +
- `Pages and polls skip this drive until it answers twice in a row.${watched}`;
+ `Pages and polls skip this drive until it answers ${clears}.${watched}`;
}
}
const store = await inspectSavedVideosStore(paths, settings);
diff --git a/editor/app/storage/components/HealthTimingForm.tsx b/editor/app/storage/components/HealthTimingForm.tsx
@@ -0,0 +1,119 @@
+"use client";
+
+import { useActionState } from "react";
+import {
+ HEALTH_TIMING_BOUNDS,
+ HEALTH_TIMING_DEFAULTS,
+ HEALTH_TIMING_HINTS,
+ type StorageHealthSettings,
+} from "yt-dlp-transcript-common/lib/storageHealthTimings";
+import { HEALTH_TIMING_FIELDS } from "../lib/healthTimingsForm";
+import { saveHealthTimingsAction, type HealthTimingsResult } from "../actions";
+
+// THE DRIVE HEALTH TIMING — `settings.storage.health`, the five numbers the
+// editor decides "this drive is mounted and not answering" by
+// (lib/storageHealth.ts). The ruling (release 15, slice DT): when a stall is
+// misjudged under heavy external-disk churn, the operator tunes these rather
+// than the code.
+//
+// COLLAPSED, AND LAST. The defaults suit a healthy disk and most operators will
+// never open it; the locations are what the page is for.
+//
+// A FIELD LEFT EMPTY IS THE DEFAULT, which is its placeholder. A field holds a
+// value only where one was saved, so clearing it goes back to the default (and
+// nothing is written for it). The server refuses a value out of range with a
+// sentence, rather than storing another number than the one typed.
+//
+// TEXT INPUTS, NOT `type="number"`: a number input's own validation blocks the
+// submit on "3.5" with a browser bubble instead of the sentence the action
+// returns, and the action is the one validator.
+//
+// VALUES FROM THE TIMINGS MODULE ONLY (pure): this is a "use client" file.
+//
+// Accessible names: "drive health timing" (the block), the five fields'
+// (lib/healthTimingsForm.ts), "save timing", "timing saved", "timing error" —
+// none contains another (Playwright's getByLabel matches substrings).
+
+export function HealthTimingForm({ stored }: { stored: StorageHealthSettings }) {
+ const [state, formAction, pending] = useActionState<
+ HealthTimingsResult | undefined,
+ FormData
+ >(saveHealthTimingsAction, undefined);
+ const tuned = HEALTH_TIMING_FIELDS.filter((f) => stored[f.key] !== undefined).length;
+
+ return (
+ <details
+ aria-label="drive health timing"
+ className="rounded-xl border border-border bg-card px-4 py-3"
+ >
+ <summary className="cursor-pointer text-base font-semibold">
+ Drive health timing
+ <span className="ml-2 text-sm font-normal text-muted-foreground">
+ {tuned === 0 ? "defaults" : `${tuned} changed from the default`}
+ </span>
+ </summary>
+ <div className="mt-3 flex flex-col gap-3">
+ <p className="text-sm text-muted-foreground max-w-3xl">
+ How the editor decides that a drive is mounted but not answering, and
+ stops reading it until it answers again. The defaults suit a disk in
+ good health; change them only when a busy drive is being called not
+ answering (or a stalled one is not). An empty field is its default.
+ </p>
+ <form action={formAction} className="flex flex-col gap-3">
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
+ {HEALTH_TIMING_FIELDS.map((f) => {
+ const { min, max } = HEALTH_TIMING_BOUNDS[f.key];
+ const unit = f.unit ? ` ${f.unit}` : "";
+ return (
+ <label key={f.key} className="flex flex-col gap-1 text-sm">
+ <span className="font-medium">
+ {f.label}
+ {f.unit && <span className="font-normal text-muted-foreground"> ({f.unit})</span>}
+ </span>
+ <input
+ type="text"
+ inputMode="numeric"
+ name={f.key}
+ aria-label={f.ariaLabel}
+ defaultValue={stored[f.key] === undefined ? "" : String(stored[f.key])}
+ placeholder={String(HEALTH_TIMING_DEFAULTS[f.key])}
+ className="rounded border border-border bg-background px-2 py-1 text-sm font-mono tabular-nums"
+ />
+ <span className="text-xs text-muted-foreground">
+ {HEALTH_TIMING_HINTS[f.key]} Default {HEALTH_TIMING_DEFAULTS[f.key]}
+ {unit}; {min}–{max}
+ {unit}.
+ </span>
+ </label>
+ );
+ })}
+ </div>
+ <div className="flex flex-wrap items-center gap-3">
+ <button
+ type="submit"
+ disabled={pending}
+ aria-label="save timing"
+ className="px-3 py-1.5 rounded-md border border-border text-sm font-medium disabled:opacity-50"
+ >
+ {pending ? "Saving…" : "Save timing"}
+ </button>
+ {state?.ok === true && (
+ <span role="status" aria-label="timing saved" className="text-sm">
+ {state.note}
+ </span>
+ )}
+ {state?.ok === false && (
+ <span
+ role="alert"
+ aria-label="timing error"
+ className="text-sm text-destructive"
+ >
+ {state.error}
+ </span>
+ )}
+ </div>
+ </form>
+ </div>
+ </details>
+ );
+}
diff --git a/editor/app/storage/lib/healthTimingsForm.test.ts b/editor/app/storage/lib/healthTimingsForm.test.ts
@@ -0,0 +1,95 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import {
+ HEALTH_TIMING_DEFAULTS,
+ HEALTH_TIMING_KEYS,
+} from "yt-dlp-transcript-common/lib/storageHealthTimings";
+import { HEALTH_TIMING_FIELDS, parseHealthTimingsForm } from "./healthTimingsForm";
+
+// Run with: pnpm -C editor exec tsx --test "app/**/*.test.ts"
+//
+// THE /storage DRIVE HEALTH TIMING FORM'S PARSE: an empty field is the default
+// and is not written; a value equal to its default is not written either; a
+// whole number in range is; anything else is refused with a sentence naming
+// the field, and nothing is saved.
+
+function form(fields: Record<string, string>): FormData {
+ const f = new FormData();
+ for (const [k, v] of Object.entries(fields)) f.set(k, v);
+ return f;
+}
+
+test("every timing has one field, in the settings' order", () => {
+ assert.deepEqual(
+ HEALTH_TIMING_FIELDS.map((f) => f.key),
+ [...HEALTH_TIMING_KEYS],
+ );
+ const names = HEALTH_TIMING_FIELDS.map((f) => f.ariaLabel);
+ assert.equal(new Set(names).size, names.length);
+ // No accessible name contains another (Playwright's getByLabel is a
+ // substring match).
+ for (const a of names) {
+ for (const b of names) if (a !== b) assert.ok(!a.includes(b), `${a} contains ${b}`);
+ }
+});
+
+test("empty fields are the defaults: nothing is written", () => {
+ assert.deepEqual(parseHealthTimingsForm(form({})), { ok: true, health: {} });
+ assert.deepEqual(
+ parseHealthTimingsForm(form({ budgetMs: "", passIntervalMs: " " })),
+ { ok: true, health: {} },
+ );
+});
+
+test("a value in range is kept, trimmed; one equal to its default is not written", () => {
+ assert.deepEqual(
+ parseHealthTimingsForm(
+ form({
+ budgetMs: " 4000 ",
+ passIntervalMs: String(HEALTH_TIMING_DEFAULTS.passIntervalMs),
+ probeTimeoutMs: "500",
+ clearAfterCleanPasses: "3",
+ inFlightPerLocation: "16",
+ }),
+ ),
+ {
+ ok: true,
+ health: {
+ budgetMs: 4_000,
+ probeTimeoutMs: 500,
+ clearAfterCleanPasses: 3,
+ inFlightPerLocation: 16,
+ },
+ },
+ );
+});
+
+test("out of range is refused with the field, the range and the value — not clamped", () => {
+ assert.deepEqual(parseHealthTimingsForm(form({ budgetMs: "200" })), {
+ ok: false,
+ error: "Read budget must be between 500 and 60000 ms (got 200 ms).",
+ });
+ assert.deepEqual(parseHealthTimingsForm(form({ inFlightPerLocation: "17" })), {
+ ok: false,
+ error: "Reads at once per drive must be between 1 and 16 (got 17).",
+ });
+ assert.deepEqual(parseHealthTimingsForm(form({ clearAfterCleanPasses: "0" })), {
+ ok: false,
+ error: "Clean checks to clear must be between 1 and 10 (got 0).",
+ });
+});
+
+test("not a whole number is refused", () => {
+ for (const bad of ["3.5", "-1", "3e3", "abc", "0x10"]) {
+ const r = parseHealthTimingsForm(form({ passIntervalMs: bad }));
+ assert.equal(r.ok, false, bad);
+ if (!r.ok) {
+ assert.equal(r.error, `Health check interval must be a whole number of milliseconds (got "${bad}").`);
+ }
+ }
+ const count = parseHealthTimingsForm(form({ clearAfterCleanPasses: "two" }));
+ assert.deepEqual(count, {
+ ok: false,
+ error: 'Clean checks to clear must be a whole number (got "two").',
+ });
+});
diff --git a/editor/app/storage/lib/healthTimingsForm.ts b/editor/app/storage/lib/healthTimingsForm.ts
@@ -0,0 +1,91 @@
+import {
+ HEALTH_TIMING_BOUNDS,
+ HEALTH_TIMING_DEFAULTS,
+ type HealthTimingKey,
+ type StorageHealthSettings,
+} from "yt-dlp-transcript-common/lib/storageHealthTimings";
+
+// THE DRIVE HEALTH TIMING FORM ON /storage: its fields' names and units, and
+// the one parse of what was submitted. Pure, and client-safe (it imports only
+// the pure timings module), so the form draws its labels from here and the
+// server action parses with it.
+//
+// AN EMPTY FIELD IS THE DEFAULT. The inputs show each default as a placeholder
+// and hold a value only where the operator set one, so clearing a field puts
+// that timing back on its default — and nothing is written for it
+// (`sanitizeStorageHealth` keeps only what differs from a default).
+//
+// OUT OF RANGE IS REFUSED, NOT CLAMPED. The schema clamps a hand-edited value on
+// read (a read never throws); a save from this form says which field is out of
+// range and writes nothing, because storing another number than the one typed
+// reads as a form that did not listen.
+
+export type HealthTimingField = {
+ key: HealthTimingKey;
+ // The visible label, and the input's accessible name (a contract once an
+ // e2e spec names it).
+ label: string;
+ ariaLabel: string;
+ // "ms", or "" for a count.
+ unit: string;
+};
+
+export const HEALTH_TIMING_FIELDS: readonly HealthTimingField[] = [
+ { key: "budgetMs", label: "Read budget", ariaLabel: "read budget", unit: "ms" },
+ {
+ key: "passIntervalMs",
+ label: "Health check interval",
+ ariaLabel: "health check interval",
+ unit: "ms",
+ },
+ {
+ key: "probeTimeoutMs",
+ label: "Health check timeout",
+ ariaLabel: "health check timeout",
+ unit: "ms",
+ },
+ {
+ key: "clearAfterCleanPasses",
+ label: "Clean checks to clear",
+ ariaLabel: "clean checks to clear",
+ unit: "",
+ },
+ {
+ key: "inFlightPerLocation",
+ label: "Reads at once per drive",
+ ariaLabel: "reads at once per drive",
+ unit: "",
+ },
+];
+
+export type HealthTimingsParse =
+ | { ok: true; health: StorageHealthSettings }
+ | { ok: false; error: string };
+
+// What the form posted, as the stored block: each non-empty field a whole
+// number in its range, and only the ones that differ from their default.
+export function parseHealthTimingsForm(form: {
+ get(name: string): FormDataEntryValue | null;
+}): HealthTimingsParse {
+ const health: StorageHealthSettings = {};
+ for (const field of HEALTH_TIMING_FIELDS) {
+ const raw = form.get(field.key);
+ const text = typeof raw === "string" ? raw.trim() : "";
+ if (text === "") continue;
+ const unit = field.unit ? ` ${field.unit}` : "";
+ const n = Number(text);
+ if (!/^\d+$/.test(text) || !Number.isSafeInteger(n)) {
+ const what = field.unit === "ms" ? "a whole number of milliseconds" : "a whole number";
+ return { ok: false, error: `${field.label} must be ${what} (got "${text}").` };
+ }
+ const { min, max } = HEALTH_TIMING_BOUNDS[field.key];
+ if (n < min || n > max) {
+ return {
+ ok: false,
+ error: `${field.label} must be between ${min} and ${max}${unit} (got ${n}${unit}).`,
+ };
+ }
+ if (n !== HEALTH_TIMING_DEFAULTS[field.key]) health[field.key] = n;
+ }
+ return { ok: true, health };
+}
diff --git a/editor/app/storage/page.tsx b/editor/app/storage/page.tsx
@@ -1,7 +1,9 @@
import type { Metadata } from "next";
import Link from "next/link";
+import { getSettings } from "yt-dlp-transcript-common/lib/settings";
import { buildStorage } from "./buildStorage";
import { StorageLocationsTable } from "./components/StorageLocationsTable";
+import { HealthTimingForm } from "./components/HealthTimingForm";
// FORCE-DYNAMIC, and not as a formality. Every number on this page comes from a
// probe of the machine taken when the page was asked for — whether a disk is
@@ -33,6 +35,10 @@ export default async function StoragePage() {
</p>
<StorageLocationsTable payload={payload} />
+
+ {/* THE TIMINGS EVERY LOCATION IS JUDGED BY (settings.storage.health),
+ collapsed and last: a page-wide setting, not a fact about a row. */}
+ <HealthTimingForm stored={getSettings().storage.health ?? {}} />
</div>
);
}
diff --git a/editor/instrumentation.ts b/editor/instrumentation.ts
@@ -85,7 +85,8 @@ export async function register() {
/* a failed re-pause must not block server readiness */
}
- // THE DRIVE HEALTH PASS, every 15 s — and ON AN IDLE BOOT TOO, like the
+ // THE DRIVE HEALTH PASS, every `storage.health.passIntervalMs` (15 s by
+ // default; a save on /storage re-arms it) — and ON AN IDLE BOOT TOO, like the
// probe below. It reads each location's block device counters (never the
// drive) and keeps, in memory only, which drives are not answering; every
// page and poll asks it before touching a drive, and its watchdog marks a