Archilyzer · Source

archilyzer

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

commit 607b04dfc664b90042702a0b493dfcf874511e77
parent eeb781d94d914f5ad90db64c9cad21241018c5c9
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sun, 20 Sep 2026 18:31:55 -0400

changelog: the storage slice, in the operator's terms

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

Diffstat:
Meditor/CHANGELOG.md | 6++++++
Meditor/e2e/storage-locations.spec.ts | 11+++++++++--
2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog ## [Unreleased] +- **A relocate job says how far it has got.** `rsync` has been printing its progress the whole time (`--info=progress2`) and every frame of it went into the job log as a carriage-return redraw of one line — so a 131 GB move and a 3 MB one looked identical from `/jobs`: a spinner. Now each frame is parsed into the **task bar** every other long job on that page already draws, reading `12.3 GB of 45.6 GB · 27 % · 110.50MB/s · ETA 5:32`, and the log gets **one line per 10 %** instead of several thousand frames of one. The percentage is against the tree the job already measured for its space check, not rsync's own — under incremental recursion that one is a percentage of what it has enumerated so far and walks backwards. +- **`/channels` is where storage is managed now.** Two new columns: **Location** (which volume this channel's media is on — *Internal* when it has not moved) and **Size** (every byte under its `data/`, from its last report, sortable biggest-first). Free space is *not* a column, because it is a fact about a disk and not about a channel: there is one read-out per **volume** in a new bar above the rack, and each chip is also a **filter** — `?location=platter` lists exactly the channels on that drive, and `/storage` links straight here with the biggest first. Beside them, **Free up N GB**: type a number, press *Select largest*, and the largest channels still on the internal disk are ticked until the target is met, ready for the Move button that was already there. Channels already on another volume are never picked (moving one frees nothing on the disk you are emptying) and channels whose report carries no size are **skipped and counted** rather than ranked as empty — which would have put the biggest thing on the disk at the bottom of the list. +- **The corpus volume is a row on `/storage`, and it is the first one.** 523 GB on a disk with 67 GB left is not a footnote under the locations that were added to fix it. It shows the channels in place, what they hold and the free space, and links to its own list. It is the one row with nothing to refresh, re-point, mount, edit or delete — and it says so, once, rather than as five greyed buttons. Every location row grows the same size read-out, and a location whose channels have never had a report says **"size unknown until Refresh report"** rather than claiming a 2 TB drive holds nothing. +- **A channel's Storage panel has one verb.** *Move media to…* and *Move back in place* were two sections with two buttons whose availability was the inverse of each other — one decision split across two controls. The corpus volume is now a destination in the same select; picking it moves the media back. While the media is on a location it is the only destination offered, because a move straight from one location to another is refused by the mover itself. +- **The saved-video store can be moved to another drive.** It is the one large thing in the corpus that belongs to no channel, so no channel move could ever reach it. `/storage` now shows it with its size and where it is, and moves it onto a location — and back — by exactly the mechanism a channel uses: the copy is verified before the source is touched, a symlink is left behind, and every reader keeps working unchanged. An interrupted move can be **resumed** rather than restarted, and its marker cleared if it cannot. +- **A drive that disappears now disables and flags the channels on it, and un-does that when it comes back.** Every guard in the app refuses work on unreachable media at the moment the work starts; none of them was a *detector*, so a channel whose drive fell off a cable sat there being refused with nothing anywhere saying why. A five-minute pass now notices, **pauses those channels and marks them** — the tier column shows a *storage* badge with the reason — and restores each one to the tier it had when the drive returns. It never claims a channel you paused yourself, and changing a tier by hand permanently takes it out of the machine's hands. It writes only when something has actually changed, and never arms at all under `ARCHILYZER_IDLE_BOOT`. - **The drives a channel's media lives on are named places now, and one click re-points them.** The cold root used to be a single string typed into Settings, and a relocated channel's `config.dataDir` an absolute path — so when the platter was automounted at `/run/media/user/<uuid>` and came back somewhere else, every channel on it read *unreachable* and the only remedy was SSH and hand edits. **`/storage`** (twelfth entry, under Machine) lists each media root as a **location** with a name, a status and the channels on it: `Available`, `Not mounted`, `Not attached`, or **`Mounted elsewhere`** — which is the one that matters, because it means the disk is here under a different mountpoint, and the row then offers **Re-point**, which rewrites every channel's `data/` symlink and `config.dataDir` and **moves no bytes at all**. There is a **Refresh** per row (a probe is `findmnt`, memoised for ten seconds, and it never writes availability to `settings.json`), a **Mount** for an attached-but-unmounted volume, a per-location **auto re-point** opt-in for operators who would rather it just happened, and a boot pass that checks every location as the editor starts. Identity is the volume's filesystem **UUID**, learned at the last successful probe — in a container there are no block devices to learn it from, every probe **fails open to "unknown", and re-point by path is the whole story** (see RUNNING_IN_DOCKER.md). The old `settings.storage.mediaRoot` **migrates on read** into a one-entry list called *Default*; the Settings field is now a link to the page. Everywhere a move starts, the destination is a **name picked from a list** rather than a path retyped per channel: the channel's Storage panel has a **destination select** showing each drive's current state (with *Another root…* keeping the free-text box), and `/channels`' selection deck has the same select for a whole batch — and what reaches the server is the **id**, never the root, so a page rendered before a re-point cannot aim a batch at a root that has since moved. The badge on every channel row says **`on Platter`** instead of sixty columns of absolute path, or **`on Platter — unreachable`** when the drive is not there. **And an interrupted move can be finished.** The controller has always resumed a half-done copy; nothing in the editor could reach it, so the only offered way out of a killed rsync was *Clear marker* and a full re-copy — which for the incident behind this work meant re-copying 131 GB that was already correctly on the far side. The panel now offers **Resume move** beside it, and the same release closes the three ways that incident happened: the relocate copy raced an auto-queue digest unit that made **no job record**, so "is this channel busy" now asks the lanes as well as the registry, and a lane that finds a relocation marker stops instead of writing into a directory being copied; a sidecar written mid-copy left one directory timestamp differing and `verifyCopy` refused the whole 131 GB, so a drift that is *only* directory mtimes now gets one more `rsync -a` pass and a re-verify (content drift still refuses, and still says the source has not been touched). Also fixed: on `/channels` a dimmed row's **Advanced menu drew underneath the rows below it** — `opacity` on a `<tr>` makes a stacking context, so the row is dimmed cell by cell now, and the cell hosting the popover is left alone. - **The operations poll reads the auto-queue's state file once instead of four times.** Every payload the editor draws — the jobs head, the workers grid, the operations board, the sync schedule, the widget's tiles, the pulse token — used to be computed by a function that did its own reading, so each of the four lanes on `/operations` opened `.auto-queue/state.json` for itself: four parses of the same document every three seconds, on a page whose four lanes were always reading one document. Those builders are pure functions in the shared core now — they are handed the settings, the registry, the scheduler, the pool, the clock and their readings, and they cannot reach disk or construct a singleton, which a layer test enforces rather than a comment asking nicely. The reading happens once, at the edge, and is shared. **Nothing moved that you can see**: same pages, same URLs, same JSON on every endpoint, same numbers — the difference is that each payload now has unit tests of its own (the console's cooldown filter, the pulse token's sensitivity, the workers grid's task grouping), where previously the only way to test one was to render the page that showed it. - **`/channels` is a rack now, with one selection deck and a meter bridge.** The page had two selection bars for one selection — a floating one for Tier and Focus, and a second block below sixty-seven rows for Move media, both saying "N selected" and both offering Clear. There is **one deck**: it docks under the table when you tick a row, carries **Tier**, **Focus** and **Media** side by side, and unmounts when you untick. The destination root lives in its own box beside the button (the button used to carry it in its label, where it truncated to *Move media to…* and you could not read where the files were going). **The table stops spilling off the screen.** It lives in one scroll region: the column headers pin to its top, the checkbox and slug cells pin to its left, and a section's name pins under the headers — so the identity column and the meter bridge header stay on screen while sixteen columns scroll sideways. The six pipeline columns read as **one block** rather than six loose dashes: a shared *Pipeline* eyebrow, a surface behind them, a rule at each end. **Rows are 41 px instead of ~90.** The tier cell is one line, and being held by a focus is a small **held** chip rather than the same orange sentence repeated on sixty-one rows — the sentence is stated once, with a count, on the focus line above the table, and each chip still carries the full reason for a screen reader and on hover. Opening a row's *Advanced* overlays the rows below instead of pushing them down. **The page's caveat is at the top.** The note saying every number here is read from each channel's last report, and how old the oldest one is, used to be the last thing on the page in 11 px type under a floating bar; it is the subtitle beside the title now, with the channel count. The band legend and the Names·A / Names·T explainer moved above the table too, beside *Group by section*. In the header, *Sync every channel*, *Full sweep every channel* and *Update all reports* are outlines under an **Every channel** eyebrow that says what they sweep, and **New channel** is the only filled button. Nothing on disk moved and no control changed its name. diff --git a/editor/e2e/storage-locations.spec.ts b/editor/e2e/storage-locations.spec.ts @@ -339,10 +339,17 @@ test("the corpus volume is a row of its own, with nothing to configure", async ( "not a configured location", ); - // A LOCATION WITH CHANNELS BUT NO REPORTS SAYS SO. `0 B` on a 2 TB drive is a - // claim nobody measured. + // A location with NOTHING on it is genuinely empty, and says so plainly — + // "0 B" is a measurement here, not a missing one. (The "size unknown" wording + // is for a location whose channels have never had a report; that case is + // pinned by views/storage.test.ts, which does not need a browser.) const cold = row(page, "cold"); + await expect(cold.getByLabel("location channels")).toHaveText( + "0 ok / 0 unreachable / 0 moving", + ); await expect(cold.getByLabel("location media bytes")).toHaveText("0 B"); + // Nothing on it, so it has no list to link to. + await expect(cold.getByLabel("list channels on cold")).toHaveCount(0); }); test("the saved-video store moves onto a location, and back", async ({