Archilyzer · Source

archilyzer

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

commit 6585f829a5356f5d4b595f78850435b483b7ccc6
parent 9b78c15f806b842ec587cb77ec4c32cad31d8698
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 10 Aug 2026 20:52:09 -0400

Let the widget's "+N more channels" actually show them

Both widget channel lists capped at six rows and then printed a count of
what was left. The payloads were never capped, so the hidden channels were
already sitting in client state with no way to reach them.

That line is a button now. Expanded, the list keeps its own max height and
scrolls, so a pinned window shows the whole worklist without growing to the
height of the corpus. The collapsed label is unchanged, and the shared
helper means both lists behave the same way.

The overflow branch had never run in a test — the widget fixtures only ever
produced one channel — so the new spec seeds eight.

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Meditor/app/widget/components/MonitorWidget.tsx | 87++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Meditor/e2e/widget.spec.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++--------
3 files changed, 124 insertions(+), 24 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **The widget's "+N more channels" is now a control instead of dead text.** Both the monitor widget's Needs-work and Needs-cleaning lists show six channels and then said how many more there were, with no way to see them — even though the widget already had every channel in hand. That line is now a button: it expands the full list inside its own scrollable box, so a pinned window shows the whole worklist without stretching to the height of the corpus, and **Show fewer** puts it back. - **The digest backlog that could never start now has a button that clears it, and honest copy about why.** Nearly 2,000 videos across the corpus have a transcript but no compact transcript file beside it, and the digest generator refuses those — so they sat in a "waiting" count on the Digest card under text that said the problem would resolve on its own. It does not. Nothing writes that file automatically for a channel whose captions are *downloaded* rather than transcribed here, which is why one channel alone accounts for 1,683 of them and was showing about 6% digest coverage. The card now says what is actually wrong, says that nothing will fix it unattended, and offers **Normalize transcripts** right there to fix it for that channel — instead of the corpus-wide button on the Build page that walks all 79,000 video folders. The button appears only when there is something for it to do. - **Skipped-video explanations on the Backfill card now match the reason each one was skipped.** The card had one hardcoded sentence about the speaker-capture length limit and showed it for every kind of skipped video, including ones skipped for completely unrelated reasons. Each kind of work now supplies its own explanation and gets its own line. - **There is now one definition of "digested", and every screen reads it.** The digest layer kept its own private tally of what still needed doing, separate from the one the digest runner actually uses — and the two could disagree by an entire channel. They are now the same number. Two consequences you will see immediately. **Channels whose reports were months old start reporting digest work at all**: eleven of them predated the old tally entirely and had been quietly reading as "nothing to do" (they account for about 1,500 videos, most of them in one channel). And **videos with no usable transcript file are no longer offered as work**: they were being handed to the digest runner, which looked at them and immediately put them back. On the current corpus that is 1,989 videos, and 1,683 of them are in a single channel — so nearly all of that channel's apparent digest backlog was work that could never have started. The overall count goes *down* slightly as a result, from 75,613 to 75,199, which is the counter becoming honest rather than anything being skipped. diff --git a/editor/app/widget/components/MonitorWidget.tsx b/editor/app/widget/components/MonitorWidget.tsx @@ -527,6 +527,51 @@ function CleanableStrip({ bytes }: { bytes: number }) { ); } +// Both channel lists cap at a handful of rows so the widget fits a pinned +// window, but the payloads are uncapped — the rest is already in memory, so the +// cap is a display choice the reader can undo. Expanded, the list gets its own +// scroll box rather than growing the widget to the height of the corpus. +function useExpandableList<T>(items: T[], limit: number) { + const [expanded, setExpanded] = useState(false); + // `hidden` is computed against the LIMIT, not against what's shown, so the + // control keeps a label once expanded (otherwise it would vanish mid-use). + const hidden = Math.max(0, items.length - limit); + return { + shown: expanded ? items : items.slice(0, limit), + hidden, + expanded, + toggle: () => setExpanded((e) => !e), + }; +} + +function ListOverflowToggle({ + hidden, + expanded, + onToggle, + noun, +}: { + hidden: number; + expanded: boolean; + onToggle: () => void; + noun: string; +}) { + if (hidden === 0) return null; + return ( + <li> + <button + type="button" + onClick={onToggle} + aria-expanded={expanded} + className="text-xs text-muted-foreground underline underline-offset-2 hover:text-foreground" + > + {expanded + ? "Show fewer" + : `+${hidden} more ${hidden === 1 ? noun : `${noun}s`}`} + </button> + </li> + ); +} + // Compact "needs work" list: channels with videos to download or transcribe, // echoing the ↓ (download) / ✎ (transcribe) verbs used by the task bars so the // worklist reads in the same language. Read-only counts by default; when the @@ -543,8 +588,10 @@ function ActionableStrip({ showTitle: boolean; interactive: boolean; }) { - const shown = channels.slice(0, ACTIONABLE_LIMIT); - const overflow = channels.length - shown.length; + const { shown, hidden, expanded, toggle } = useExpandableList( + channels, + ACTIONABLE_LIMIT, + ); return ( <section aria-label="Needs work" className="flex flex-col gap-1.5"> {showTitle && ( @@ -560,7 +607,9 @@ function ActionableStrip({ {channels.length === 0 ? ( <p className="text-xs text-muted-foreground">Everything&apos;s handled.</p> ) : ( - <ul className="flex flex-col gap-1.5"> + <ul + className={`flex flex-col gap-1.5${expanded ? " max-h-64 overflow-y-auto" : ""}`} + > {shown.map((c) => ( <li key={c.slug} @@ -602,11 +651,12 @@ function ActionableStrip({ )} </li> ))} - {overflow > 0 && ( - <li className="text-xs text-muted-foreground"> - +{overflow} more {overflow === 1 ? "channel" : "channels"} - </li> - )} + <ListOverflowToggle + hidden={hidden} + expanded={expanded} + onToggle={toggle} + noun="channel" + /> </ul> )} </section> @@ -629,8 +679,10 @@ function CleanableChannelsStrip({ showTitle: boolean; interactive: boolean; }) { - const shown = channels.slice(0, CLEANABLE_LIMIT); - const overflow = channels.length - shown.length; + const { shown, hidden, expanded, toggle } = useExpandableList( + channels, + CLEANABLE_LIMIT, + ); return ( <section aria-label="Needs cleaning" className="flex flex-col gap-1.5"> {showTitle && ( @@ -646,7 +698,9 @@ function CleanableChannelsStrip({ {channels.length === 0 ? ( <p className="text-xs text-muted-foreground">Nothing to reclaim.</p> ) : ( - <ul className="flex flex-col gap-1.5"> + <ul + className={`flex flex-col gap-1.5${expanded ? " max-h-64 overflow-y-auto" : ""}`} + > {shown.map((c) => ( <li key={c.slug} @@ -675,11 +729,12 @@ function CleanableChannelsStrip({ )} </li> ))} - {overflow > 0 && ( - <li className="text-xs text-muted-foreground"> - +{overflow} more {overflow === 1 ? "channel" : "channels"} - </li> - )} + <ListOverflowToggle + hidden={hidden} + expanded={expanded} + onToggle={toggle} + noun="channel" + /> </ul> )} </section> diff --git a/editor/e2e/widget.spec.ts b/editor/e2e/widget.spec.ts @@ -10,17 +10,24 @@ import { mkdir, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; import { test, expect } from "@playwright/test"; -import { resetData, resolvePath, writeSettings, generateReport} from "./helpers"; +import { + resetData, + resolvePath, + writeSettings, + writeChannelConfig, + generateReport, +} from "./helpers"; import { baseUrl } from "./baseUrl"; const CLEAN_SLUG = "test-transcribe"; -// Seed the transcribed-with-audio cleanup bucket for CLEAN_SLUG — the same -// pattern cleanup-actionable.spec.ts uses. 2,621,440 bytes → "2.5 MB". -async function seedCleanableChannel(): Promise<void> { - const full = resolvePath( - `test-transcripts/channels/${CLEAN_SLUG}/snapshot.json`, - ); +// Seed the transcribed-with-audio cleanup bucket for a channel — the same +// pattern cleanup-actionable.spec.ts uses. The default 2,621,440 bytes → "2.5 MB". +async function seedCleanableChannel( + slug: string = CLEAN_SLUG, + bytes: number = 2_621_440, +): Promise<void> { + const full = resolvePath(`test-transcripts/channels/${slug}/snapshot.json`); await mkdir(dirname(full), { recursive: true }); await writeFile( full, @@ -42,7 +49,7 @@ async function seedCleanableChannel(): Promise<void> { partialDownloads: [], }, undownloadedIds: [], - cleanupBytes: { transcribedWithAudio: 2_621_440 }, + cleanupBytes: { transcribedWithAudio: bytes }, }, null, 2, @@ -235,6 +242,43 @@ test("cleanlist=1 shows a needs-cleaning list of channels", async ({ page }) => await expect(list).toContainText("2.5 MB"); }); +test("the needs-cleaning list's +N more expands and collapses in place", async ({ + page, +}) => { + await resetData("one-transcribe-channel-with-audio"); + // The list caps at 6 rows; 8 channels puts 2 behind the overflow control. + for (let i = 0; i < 8; i += 1) { + const slug = `bulk-clean-${i}`; + await writeChannelConfig(slug); + await seedCleanableChannel(slug, 1_048_576 * (i + 1)); + } + + await page.goto("/widget?cleanlist=1"); + const list = page.getByRole("region", { name: "Needs cleaning" }); + await expect(list).toBeVisible({ timeout: 10_000 }); + + // Only the row <li>s carry `needs cleaning <slug>`, so this count ignores the + // overflow <li> holding the control. + const rows = list + .getByRole("listitem") + .and(page.locator("[aria-label^='needs cleaning ']")); + await expect(rows).toHaveCount(6, { timeout: 10_000 }); + + const more = list.getByRole("button", { name: "+2 more channels" }); + await expect(more).toBeVisible(); + + // Expanding shows the rest without leaving the widget. + await more.click(); + await expect(rows).toHaveCount(8); + const fewer = list.getByRole("button", { name: "Show fewer" }); + await expect(fewer).toBeVisible(); + + // ...and it collapses back, surviving the ~1s poll in between. + await fewer.click(); + await expect(rows).toHaveCount(6); + await expect(list.getByRole("button", { name: "+2 more channels" })).toBeVisible(); +}); + test("cleanlist=1&controls=1 gives the list per-channel Clean audio actions", async ({ page, }) => {