Archilyzer · Source

archilyzer

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

commit 270f42ec052d0805ff7811ed0eb66bd740310698
parent 2cd9a2aeb67abe6db1eb41a96224a83ea0155600
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 18 Sep 2026 17:38:19 -0400

plans: storage locations — S5 written (per-unit re-check, no tmpfs mount, auto-pause + flag)

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

Diffstat:
Mplans/storage-locations.md | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 126 insertions(+), 0 deletions(-)

diff --git a/plans/storage-locations.md b/plans/storage-locations.md @@ -192,3 +192,129 @@ shas, gate outputs, divergences. - 2026-09-17 — S1 (`storage/locations-s1`, `578867f`→`68d082d`) and S2 (`storage/locations-s2`, `c078e1f`→`79c0071`) reviewed and merged as `d146c51`. Real-host probe matched the plan (available + uuid + fstab warning; mounted-elsewhere with the automount candidate). Follow-up recorded: rename/delete channel actions still check the registry alone (`channels/actions.ts` `renameChannelAction`) — `channelMediaBusyReason` is a drop-in there. - 2026-09-17 — S3 (`storage/locations-s3`, `8824f5b`→`aed94d9`) reviewed and fast-forwarded onto main: `/storage`, controller + views, `repoint-storage-location` job, nav at twelve, boot pass, fake findmnt/udisksctl, `storage-locations.spec.ts`; common 1314/1314, e2e 6/6. Declined: a `spec` on the re-point job record (it is the Retry switch and the kind is not replayable). - 2026-09-17 — S0 (`storage/locations-s0`, `c13c5a8`→`d4ebf1d`) merged as `093e436`: the row dim is per cell, the Tier cell hosts the popover; `channel-priority.spec.ts` proves it with two adjacent paused rows and an `elementFromPoint` owner check (passes with the fix, fails without). Main now holds S0–S3; S4 in flight. + +## S5 — unreachable media: re-check per unit, never materialise a mount, auto-pause + flag + +Operator ask 2026-09-18, after the omnimirror move landed: "Can that gracefully handle the +case if the drive disconnects while the channel is on? Maybe an automatic disable and flag?" +Facts verified at `main` @ `9ac8d38` (read-only Opus investigation, 2026-09-18): + +- **Guards at start exist, four of them**: `common/jobs/streamCommand.ts:266-292` + `refuseForUnreachableMedia` (first statement of `runManagedFunction`, only when + `opts.channelSlug` is set and the kind has `needsMedia`); `autoRunner.ts:610-630` + `buildChannelWork` inspects every channel on every tick (`:617`) and drops it via + `noteSkippedForMedia` (`:625-629`); `channelSnapshot.ts:619`; `operationBatch.ts:1593`. +- **No per-unit re-check.** `autoRunner.ts:1673 run(picked)` and `operationBatch.ts:1723` + (GUARD 5, `next()`) read only the relocation marker (S1). Auto-download units re-enter + `runManagedFunction` per video (`autoRunner.ts:1911-1915`, kind `auto-download-unit`, + `needsMedia`) so they are covered; auto-transcribe (`:1840`) and both operation lanes + (`:1624`) run in-process with no job record. The lane runner jobs (`:2015`) pass no + `channelSlug`, so guard 1 no-ops for them. +- **Writers go through the link** (`downloadOneManaged.ts:437,623,1123` mkdir of + `videoDir`; yt-dlp templates cwd-relative `data/%(id)s/…` at `runYtdlp.ts:258-303` with + `cwd = channelDir` at `:307-312`; sidecar writers do no mkdir). A recursive mkdir + through a DANGLING symlink fails (ENOENT on the leaf, EEXIST on `data`, then `stat` + ENOENT) — reasoned from the code, not executed: **S5 adds the unit test**. +- **The one absolute-path mkdir**: `relocateChannelMedia.ts:657` (`moveOut`) and `:873` + (`moveBack`) `mkdir(target, { recursive: true })`. With the volume absent this creates + `/run/media/user/<uuid>/…/<slug>/data` on tmpfs; the link then stops dangling and every + writer above lands in RAM undetected. +- **Detection is passive**: `autoRunner.ts:554-570` `noteSkippedForMedia` / + `noteMediaReachable` log once per transition and nothing reads them (`:576-578`); + snapshot regen throws and the scheduler keeps the last good `snapshot.json`; badges are + computed per request (`page.tsx:82`, `channels/page.tsx:204`, `[slug]/page.tsx:216`, + `MediaLocationBadge.tsx:51`, `/storage` rollup `storageLocations.ts:127` → + `views/storage.ts:198`). `/review` (`review/page.tsx:27,50`) knows nothing about media. + `runStorageBootPass` runs only at boot (`instrumentation.ts:100-103`); `PROBE_MEMO_MS` + is a request memo, not a timer. +- **Channel-level pause mechanism**: `common/lib/channelPriority.ts:317 isChannelPaused`, + tier `"paused"` (`:70`, filtered at `:254,332,545`). `pauseGates.ts` is lane-level — + wrong granularity. + +### Design + +**(a) Per-unit reachability, same shape as the S1 marker check.** Beside the marker read +at `autoRunner.ts:1673`: `inspectChannelMedia`; status not `ok`/`in-place` → log +`skipping <slug>/<id>: media <status>` and `outcome: "skipped"` (same `finally` semantics +as S1, same comment about bucket vs operation lanes). Beside GUARD 5 at +`operationBatch.ts:1723`: unreachable → stop line + `return null`; generalise S1's +`stoppedForRelocation: boolean` into `stopped?: "relocation" | "media-unreachable"` +(keep the boolean as a derived getter if anything reads it; `operationJobs.ts:147,272` +append the matching phrase). Cost: two stats and one JSON read per unit — state it. + +**(b) The move never materialises a mount.** New `assertRelocationRootPresent(root, +settingsStorage, bins)` in `relocateChannelMedia.ts`, called immediately before both +absolute-path mkdirs (`:657`, `:873`) and from `relocationRootProblem`: `stat(root)` must +be a directory (the move creates only `<root>/<slug>` and `<root>/<slug>/data`, never the +root); and when `locationOfDataDir(root + "/x", locations)` (or `root` equals a location +root) resolves to a location with a `volume.uuid`, `probeLocation` must answer +`available` with a known identity whose uuid matches — otherwise refuse +`destination root <root> is <status> (location "<id>"); mount it or re-point first`. A +root that is nobody's location is stat-only (documented: an unmounted fstab mountpoint +directory is exactly the case a location protects against — add one to `/storage`). The +umtool twin (`cues.mjs`) is unaffected; it only reads. + +**(c) Auto-pause + flag, hung off the transition that already exists.** `channelPriority` +per-channel record gains `autoPaused?: { reason: "media-unreachable"; since: string; +previousTier: Tier }` (type + parser + sanitizer, the lane-migration discipline: an older +binary drops the field and leaves the tier as Paused — nothing lost but the automatic +restore). Pure helpers in `common/lib/channelPriority.ts`: `autoPauseForMedia(settings, +slug, now)` (no-op when the tier is already `paused` or `autoPaused` is set; otherwise +records `previousTier` and sets `paused`) and `restoreAfterMedia(settings, slug)` (no-op +unless `autoPaused` is present; restores `previousTier`, clears the field). A MANUAL tier +change through the existing action clears `autoPaused` (the operator's word wins; a later +return of the drive must not un-pause a channel the operator paused by hand). Wire: +`autoRunner.ts:554-570` `noteSkippedForMedia` → `writeSettings(autoPauseForMedia(…))`; +`noteMediaReachable` → `writeSettings(restoreAfterMedia(…))`. Both are behind the same +once-per-transition latch that already exists there, so a flapping drive writes twice per +flap, not per tick. Idle boot (`ARCHILYZER_IDLE_BOOT`) never writes. Flag surfaces: +`/review` gains a "Media unreachable" section (built in `common/views/review.ts` or the +existing review view: slug, location label, since, "auto-paused"); `MediaLocationBadge` +gets `autoPaused?` → "auto-paused — media unreachable since <date>"; the `/storage` +rollup's `unreachable` count links to that review section; the channel page's tier control +shows the reason beside Paused. Restore is logged on the lane (`[auto] <slug>: media +reachable again, tier restored to <previousTier>`). + +The tick is the probe: `buildChannelWork` inspects every channel per tick whether or not it +has work. Document the one blind spot — if every lane is held by a pause gate no tick +runs, and the flag appears on the next request-time render only. + +### Files + +`common/controller/autoRunner.ts`, `common/controller/operationBatch.ts`, +`common/controller/operationJobs.ts`, `common/controller/relocateChannelMedia.ts`, +`common/lib/channelPriority.ts` (+ test), `common/lib/settings.ts` (sanitizer for the new +field), `common/views/review.ts` (or where `/review` builds), `editor/app/review/page.tsx`, +`editor/app/channels/components/MediaLocationBadge.tsx`, the tier control, the +`/channels` and dashboard row builders (project `autoPaused`), `editor/CHANGELOG.md`, +`RUNNING_IN_DOCKER.md` (one paragraph: a bind mount that vanishes is the same case). + +### Tests + +- `operationBatchRelocation.test.ts` (extend): a target dir removed after the first unit + stops the batch with the media stop line and `stopped === "media-unreachable"`. +- `relocateChannelMedia.test.ts`: root missing → refusal names the root and creates + nothing (assert the parent chain does not exist afterwards); root present but its + location probes `unmounted` (fake findmnt in the tmp dir, as `storageVolumes.test.ts`) + → refusal names the location and status. +- New `channelMedia.test.ts` case (or `lib/danglingLink.test.ts`): `mkdir(join(link, + "x"), { recursive: true })` through a dangling symlink throws ENOENT and creates + nothing — the invariant every writer relies on. +- `channelPriority.test.ts`: auto-pause records `previousTier`; no-op on a manual Paused; + restore clears; a manual tier change clears `autoPaused`; sanitizer round-trip; an older + shape without the field parses. +- `views/review.test.ts` (or the view's test): the section lists auto-paused channels. +- e2e `channel-storage.spec.ts`: seed a relocated channel and an `autoPaused` record → + badge text and `/review` section; Move media to a root whose location is + `unmounted` per `.fake-findmnt.json` → inline refusal naming the location. No e2e for + the lane transition itself (no harness; the unit tests own it). + +### Verification + +The S3/S4 gate list. Full suite on the final tip. Offline against the real host: `tsx -e` +`assertRelocationRootPresent` on the platter root → passes; on `/mnt/platter/x` (absent) +→ refuses; nothing created under `/mnt` afterwards. + +### Status + +- 2026-09-18 — written; dispatch after S4 merges (`storage/locations-s5`).