import type { ChannelMediaLocation, ChannelMediaStatus, } from "./channelMedia"; import { locationLabelOfDataDir, type StorageLocation, } from "./storageLocations"; // THE HOLD, in the words the builds, the lanes and the surfaces use. // // The index build (controller/buildIndex.ts) and the stats build // (controller/buildStats.ts) each walk every channel's `data/`. Since release 17 // they read the TEXT tier only and are never held by the media tier: a channel // whose text cannot be read — the retired whole-directory layout (`legacy`), // a `data/` that is not a directory — is HELD by both: not rescanned, and what // the last build knew of it kept, rather than read as a channel with no videos // and removed (lib/channelMedia.ts says why that reading is the dangerous one). // The media hold (an unmounted media drive, a move in progress, a link and a // config that disagree) holds the lanes and jobs that open a big file. This module is only the shared vocabulary: which statuses // hold, why, in words with no path in them (/storage shows the paths), and the // ways out a refusal names. Each build decides for itself what "kept" means. // // Pure: no I/O. The caller asks inspectChannelMedia and passes the answer in. // "ok" and "in-place" are read; every other status holds, including any a later // inspectChannelMedia adds (`legacy` among them). The MEDIA hold: what the // transcription, download and backfill lanes and every media job key on. export function isMediaHeld(status: ChannelMediaStatus): boolean { return status !== "ok" && status !== "in-place"; } // THE TEXT HOLD (release 17): only the retired whole-directory layout holds the // text, because only there is it on another drive. The digest lane keys on // this, so a digest runs on a channel whose media is moving, stalled or // unmounted. (The text guard, `assertChannelTextReadable`, also refuses a // `data/` that is not a directory and a tier migration in flight — conditions // a status alone does not carry; a lane that holds a location asks it.) export function isTextHeld(status: ChannelMediaStatus): boolean { return status === "legacy"; } // Why a channel is held, without the paths inspectChannelMedia's `detail` // carries. // // "Its media is moving" leads the in-transition reason because that is what the // operator is looking at: a `.relocating.json` marker is a move, running or // interrupted, and while it stands every writer of the channel is held — the // four lanes skip it, a media job refuses to start on it, and both builds keep // what they last read of it (release 16 slice RM). export const HELD_REASON: Record = { unreachable: "its media is not reachable (drive not mounted?)", "in-transition": "its media is moving (a move is in progress or was interrupted)", inconsistent: "its data link and its config disagree", stalled: "its drive is not answering (a stalled disk)", legacy: "its media layout is the retired whole-directory one (run archilyzer storage migrate-tier)", ok: "reachable", "in-place": "reachable", }; // THE HOLD AS A SURFACE SAYS IT — "held: its media is moving (…)" — or null for // a channel whose media is read. One wording for the rack's chip, the channel // page's Storage panel and a refused job, so the three cannot drift. It lifts // when the status does: a move that completes or is abandoned removes the // marker, and the next inspect reads the channel again. export function mediaHoldText(status: ChannelMediaStatus): string | null { return isMediaHeld(status) ? `held: ${HELD_REASON[status]}` : null; } // The reason, and the storage location's label when the channel's media is on // one. `mediaDir` is the channel config's (`mediaDir`, or the retired `dataDir` // on a legacy channel); the inspector's target stands in when the config names // none (a move in flight). export function heldReason( media: Pick, mediaDir: string | undefined, locations: StorageLocation[], ): string { const label = locationLabelOfDataDir(mediaDir ?? media.target, locations); return `${HELD_REASON[media.status]}${label ? `, on location "${label}"` : ""}`; } // A held channel named in a refusal, as `slug (why)`, joined. export function describeHeld(held: Map): string { return [...held].map(([slug, why]) => `${slug} (${why})`).join("; "); } // What a refusal tells the operator to do, mounting first. export const HELD_WAYS_OUT = `For each: mount its media and run this again; or repair or re-point its location on /storage; ` + `or finish or clear its move (the channel's Storage panel); or, if it is gone for good, ` + `delete the channel or set excludeFromBuild in its config.`;