Archilyzer · Source

archilyzer

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

commit 69edae233f4744e52d7391eb8fdd0c3d8b40fd7c
parent 5ebf4015874adcb45426c9f31ee3c20903afa6c1
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  8 Oct 2026 22:52:56 -0400

Merge editor/no-lead-paragraphs (page titles lose their lead paragraphs; /channels' header wraps instead of squeezing)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Meditor/CHANGELOG.md | 1+
Meditor/app/channels/page.tsx | 51++++++++++++++++++++-------------------------------
Meditor/app/operations/page.tsx | 5-----
Meditor/app/review/page.tsx | 3---
Meditor/app/saved-videos/page.tsx | 5+----
Meditor/app/sites/page.tsx | 5-----
Meditor/app/storage/page.tsx | 14+++-----------
Meditor/app/tags/page.tsx | 9++-------
Meditor/app/widget/builder/page.tsx | 10----------
Meditor/app/workers/page.tsx | 6------
10 files changed, 27 insertions(+), 82 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] - **A single file can be transcribed through the editor.** `pnpm ops transcribe` (`POST /api/ops/transcribe`) takes `{"path"}` — an absolute path to a local audio or video file — with `"start"`/`"end"` (seconds) for a window, `"workerId"` (a configured local worker; default: the one auto-transcribe would get) and `"out"` (an absolute path for the result). It runs as a "Transcribe file" job on `/jobs`, through the worker pool and the same engine, model and command line the corpus is transcribed with. A window is cut by ffmpeg to a temporary 16 kHz mono WAV, removed afterwards. The result is `{path, window, worker: {id, name, appId, model, device}, transcriptFormat, cues: [{start, end, text}], text, …}`, cue times on the source file's clock; it ends the job's log, is written to `out` when given, and `--wait` prints it on stdout. Refused before any job: a relative or unreadable path, an `end` not after `start`, an unknown or remote worker, a named worker switched off, an `out` inside the corpus (resolved through symlinks, storage locations included). Nothing is written to the corpus or to settings. +- **Page titles no longer carry a paragraph of explanation.** /channels used to open with three lines of prose about where its numbers come from, and at a narrow window with the sidebar open its four buttons took the row and squeezed that prose to one word a line. It is now one status line — how many channels, how old the oldest report is, how many have none — and when the buttons do not fit beside the title they drop below it. Workers, Sites, Review and Operations lose their lead paragraph; Tags, Storage and Saved videos keep one line each, the instruction (rule edits apply at the next index build; media moves from a channel's Storage panel; the keep-latest window is in a channel's settings); the Monitor widget builder loses its own, which repeated the board's. Needs a restart of the editor. - **Clip windows can be fetched as a list, paced, in one job per platform.** `pnpm ops fetch-windows` (`POST /api/ops/fetch-windows`) takes `{"siteId"}` — every window a site's published reports cite and the disk does not hold — or `{"items": [{"slug", "id", "from", "to", "clipId"?, "reason"?}], "requestedBy", "manifest"?}`, with `"maxHeight"` and `"dryRun"`. A window already on disk is answered at once and joins no job; the rest are grouped by platform queue and fetched as one `fetch-windows` job per platform, so YouTube and Rumble run side by side, each window through the same managed fetch as a single one (cookie policy, auth and HLS retries, provenance). Between two fetches on a platform the job waits that platform's batch gap, at least 30 s and up to half again at random; before each it checks the platform's cooldown and hold and stops when either is set. A 429 backs the platform off and stops the job; one 403 is that window's failure, two in a row back the platform off and stop it. A platform cooling down or held is refused for its group before anything starts. The job is drainable, shows its progress as clip windows, and Retry or running the same body again fetches only what is still missing. A dry run lists the windows per platform, the ones on disk, and the spans no window can fill (a video the source says is deleted, private or members-only, a channel off the site, an unmounted drive). A site's **Reports** tab has **Fetch missing evidence** and **Preview missing evidence** beside **Prepare evidence media**, and umtool's `fetch-via-editor.mjs --all` sends a manifest's whole timeline as one request. A window fetch whose cookie retry runs into a 429 now records the cooldown too. Needs a restart of the editor. - **A social channel can be renamed and deleted from its page.** A posts channel's page (X, Bluesky, a forum thread) had no Danger zone, so it could not be renamed or deleted in the editor at all. It now has the one a video channel has, collapsed under the posts panel and opened by the same `?stage=danger` link: **Rename channel** and **Delete channel**, typing the slug to confirm, refused while the channel is busy, in the same sentences. Needs a restart of the editor. - **A channel can be created, renamed, deleted and put on sites over `pnpm ops`.** `create-channel` is the New channel form (`{"fields": {"name", "handling", "url", …}}`, the same field names as `channel-config`'s `patch`; `"slug"` and `"sites"` optional; the form's "Fetch playlist now", "Fetch posts now" and "Add to top of auto-queue" are off unless asked for, and a job they start is returned so `--wait` follows it). `rename-channel` (`{"slug", "newSlug"}`) and `delete-channel` (`{"slug", "confirm"}`, `confirm` repeating the slug) are the Danger zone's two forms. `channel-config` takes `"sites"` — the whole membership set, `[]` for on no site; an unknown site is refused rather than skipped — and `"excludeFromBuild"` / `"excludeFromCleanup"`, set to the value given rather than toggled, with or without a `patch`. `pnpm ops get channels` lists every channel with its kind, platform and the sites that carry it. Each runs the form's own action, so it refuses what the form refuses, in the same words. Needs a restart of the editor. diff --git a/editor/app/channels/page.tsx b/editor/app/channels/page.tsx @@ -357,55 +357,44 @@ export default async function ChannelsPage({ // fixed chrome and the rack between them is the only thing that moves. // Below md there is no height and the document scrolls as it always has. <div className="flex flex-col gap-3 md:h-[calc(100vh-3rem)] md:min-h-0"> - {/* NOWRAP ON md+, AND THE SUBTITLE IS WHAT GIVES. The freshness note is - three lines of prose, not a tagline: at its max-w-3xl ceiling it - claimed 768px of a 1216px content column, which left no room for the - corpus-wide cluster and dropped it onto a band of its own — the same - undifferentiated row of pills this change exists to remove. So the - left block takes whatever is left and the paragraph rewraps into it - (max-w-3xl stays a ceiling, never a floor), and the cluster keeps its - intrinsic width at the top right. Below md the header still wraps and - the cluster still stacks full-width. */} - <header className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 shrink-0 md:flex-nowrap"> - <div className="min-w-0 md:flex-1"> + {/* THE HEADER WRAPS; IT NEVER SQUEEZES. The title block has a 16rem + basis, so when the corpus-wide cluster cannot sit beside it at full + width the cluster drops onto its own line. It used to be nowrap with + the cluster shrink-0, and at a narrow md+ column (sidebar open) the + title block was left ~60px and its text ran one word per line. */} + <header className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2 shrink-0"> + <div className="min-w-0 flex-[1_1_16rem]"> <h1 className="text-2xl font-semibold">Channels</h1> - {/* THE FRESHNESS NOTE IS THE PAGE'S CAVEAT, so it sits with the - page's title. It used to be the last thing on the page, in 11px - type under a floating bar, ~6,000px below the numbers it is a - caveat ABOUT — which is to say it was written but not said. */} + {/* A STATUS LINE, NOT A LEAD PARAGRAPH. Every pipeline on the page is + read from each channel's last report, so the line says how old the + oldest one is and how many have none (the rack's Report column names + them). */} {channels.length > 0 && ( <p - className="max-w-3xl text-xs text-muted-foreground" + className="text-xs text-muted-foreground" data-testid="channels-freshness" > <span className="tabular-nums">{channels.length}</span>{" "} {channels.length === 1 ? "channel" : "channels"} &middot;{" "} {freshness.oldest ? ( <> - Every pipeline on this page &mdash; downloads, transcripts, - digests and the speaker lanes &mdash; is read from each - channel&rsquo;s last report; the oldest on this page was - generated{" "} + oldest report{" "} <time dateTime={freshness.oldest}> {new Date(freshness.oldest).toLocaleString()} </time> - . A just-finished job can take a moment to show up here. </> ) : ( - <> - No channel on this page has a generated report yet, so every - pipeline reads empty. Run <em>Refresh report</em> from a row - here, or <em>Update all reports</em> above, to populate them. - </> + <>no reports yet &mdash; run <em>Update all reports</em></> )} {freshness.missing.length > 0 && freshness.oldest && ( <> {" "} - No report yet for {freshness.missing.join(", ")} — those rows - read zero - {sections - ? ", and a group figure that counts one of them is marked with a trailing + to say it is a floor rather than a total." - : "."} + &middot;{" "} + <span className="tabular-nums"> + {freshness.missing.length} + </span>{" "} + without a report + {sections ? " (a group figure marked + is a floor)" : ""} </> )} </p> diff --git a/editor/app/operations/page.tsx b/editor/app/operations/page.tsx @@ -27,11 +27,6 @@ export default async function OperationsPage() { <h1 className="font-display text-2xl font-semibold tracking-tight"> Operations </h1> - <p className="text-sm text-muted-foreground"> - Every operation that turns this archive into a derived corpus, and what - each one is doing right now. Open one to set its rules, arm its sweep or - hold its lane. - </p> <OperationsBoard initial={initial} sync={sync} /> </div> ); diff --git a/editor/app/review/page.tsx b/editor/app/review/page.tsx @@ -34,9 +34,6 @@ export default async function ReviewPage() { return ( <div className="flex flex-col gap-6"> <h1 className="text-2xl font-semibold">Review</h1> - <p className="text-sm text-muted-foreground"> - Corpus review — findings a human decides, not work a lane runs. - </p> <AutoPausedSection rows={review.autoPaused} /> <DuplicatesSection report={review.duplicates} diff --git a/editor/app/saved-videos/page.tsx b/editor/app/saved-videos/page.tsx @@ -57,10 +57,7 @@ export default async function SavedVideosPage() { <header className="flex flex-col gap-1"> <h1 className="text-xl font-semibold">Saved videos</h1> <p className="text-sm text-muted-foreground"> - Source video containers persisted by the keep-latest retention rule - live in a separate store, leaving the main data volume holding only - audio + transcripts. Configure a channel&apos;s keep-latest window and - per-channel store dir under that channel&apos;s settings. + Set a channel&apos;s keep-latest window and store dir in its settings. </p> </header> diff --git a/editor/app/sites/page.tsx b/editor/app/sites/page.tsx @@ -94,11 +94,6 @@ export default async function SitesPage() { + New site </Link> </div> - <p className="text-sm text-muted-foreground max-w-2xl"> - Each site is a selection + branding over the shared channel pool. The - same channel can appear on several sites; its downloads are stored once - and reused. - </p> {sites.length === 0 && ( <div className="flex flex-col gap-3 rounded border border-border p-4"> <p className="text-sm"> diff --git a/editor/app/storage/page.tsx b/editor/app/storage/page.tsx @@ -22,17 +22,9 @@ export default async function StoragePage() { <h1 className="text-2xl font-semibold">Storage</h1> </div> - <p className="text-sm text-muted-foreground max-w-3xl"> - A storage location is a named place a channel&rsquo;s media — its big - files, the audio and the raw live chat — may live, usually a second - drive; a channel&rsquo;s text never leaves the corpus volume. A channel - is on a location when its <code>mediaDir</code> is under that - location&rsquo;s root; nothing is tagged, so moving a channel on or off - one is a move, not a setting. When a drive comes back at a different - mountpoint, <strong>re-point</strong> the location: it rewrites every - channel&rsquo;s symlink and <code>mediaDir</code> and moves no bytes. Move media onto a location from - a channel&rsquo;s <Link href="/channels" className="underline">Storage - panel</Link>. + <p className="text-sm text-muted-foreground"> + Move a channel&rsquo;s media onto a location from its{" "} + <Link href="/channels" className="underline">Storage panel</Link>. </p> <StorageLocationsTable payload={payload} /> diff --git a/editor/app/tags/page.tsx b/editor/app/tags/page.tsx @@ -49,13 +49,8 @@ export default async function TagsPage() { <section className="flex flex-col gap-4"> <header className="flex flex-col gap-1"> <h1 className="text-2xl font-semibold">Tags</h1> - <p className="max-w-3xl text-sm text-muted-foreground"> - A curated vocabulary that cuts across channels. A tag lands on a video - three ways: a <strong>rule</strong> re-evaluated at every index build, - a <strong>pin</strong> somebody made by hand, or an import from umtool - — and a <strong>suppression</strong> rejects a rule&apos;s hit. Pins - and suppressions are stored with their provenance; rule hits never - are. Edit a rule, rebuild the index, done. + <p className="text-sm text-muted-foreground"> + Rule edits apply at the next index build. </p> </header> <EditorTagsClient diff --git a/editor/app/widget/builder/page.tsx b/editor/app/widget/builder/page.tsx @@ -27,16 +27,6 @@ export default async function WidgetBuilderPage() { <div className="flex items-center justify-between"> <h1 className="text-2xl font-semibold">Monitor widget</h1> </div> - <p className="max-w-2xl text-sm text-muted-foreground"> - A read-only monitor with no sidebar, sized to sit in a small pinned - window or an embedded <code className="font-mono">&lt;iframe&gt;</code>. - The board below is laid out the way the widget will be — drag the strips - into the order you want to read them in, split them across rows and - columns if you&apos;re giving it the width, and drop the ones you - don&apos;t want into the tray. Give a row the leftover height with the - rail on its left and the sections inside it scroll under their own - headings. Then save it as a preset, or copy the link. - </p> <WidgetBuilder initialPresets={presets} /> </div> ); diff --git a/editor/app/workers/page.tsx b/editor/app/workers/page.tsx @@ -29,12 +29,6 @@ export default async function WorkersPage() { return ( <div className="flex flex-col gap-4"> <h1 className="text-2xl font-semibold">Workers</h1> - <p className="text-sm text-muted-foreground"> - Transcription workers — one slot each. Enable, disable, or drain a worker - to free up a CPU/GPU for other programs, then turn it back on when done. - Disabling every worker pauses running batches (they wait for a worker) - instead of failing. - </p> <WorkersView initial={initial} /> <WorkersConfigForm initial={settings.workers}