Archilyzer · Source

archilyzer

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

commit 5d4d2200921bb673425fca00631bfca322ff456b
parent 09088c9fbaa41df654ec15b57c3579f977385fc4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue,  8 Sep 2026 22:21:42 -0400

editor: a kept run panel is not a second Run button, and the alert goes with the warning

Review follow-ups on 09088c9, plus the fourth instance of the same rule.

A panel kept mounted so its log survives is kept for the LOG. Its button had
stayed enabled, so "Persist source video" invited a second re-fetch of a
container already in the store, and each banner invited a re-run of the
operation that had just cleared its own flag. StreamActionLog already takes
`disabled`, so each of the four passes the now-cleared condition to it:
`!!savedVideo`, `!coverage`, `!flagged`, `hasTranscript`. No new prop.

The banners' wrapper also kept `role="alert"` and its warning aria-label after
the warning text was gone — a box announcing itself as an alert named
"incomplete transcript" for a video that is now fine, and one that would still
answer getByLabel("incomplete transcript") (truncated-check.spec.ts:69 asserts
it does not). Both attributes are conditional on the flag now and unchanged
while it holds.

The fourth instance is PerFileTranscribeRow. `hasTranscript` is server-derived
(VideoPanel :360) and this row's own run writes transcript.json, so the refresh
the run fires swapped the panel for "Transcribe disabled — transcript already
present." and destroyed the log. The row itself survives (keyed by file name),
so the fix is local and the same shape as the banners: message and run panel in
separate slots, the panel kept — disabled — once a run has started here.
video-page.spec.ts's existing transcribe-one case now reads the log's closing
line AFTER that message is visible, and asserts the button is not actionable.
Extended in place rather than added, so no test gains a new neighbour.
PerFileTranscodeRow was checked and is NOT an instance: its condition is the
file's extension against a static format list, which a transcode cannot change.

The four cases also get their own 120 s budget (as attribution.spec.ts does).
They spend three sequential waits, and under the config's 30 s a real failure
read "Test timeout of 30000ms exceeded" instead of naming the assertion.

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

Diffstat:
Meditor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx | 54++++++++++++++++++++++++++++++++++++++++++++++--------
Meditor/e2e/download-format-guard.spec.ts | 7+++++++
Meditor/e2e/incomplete-transcript.spec.ts | 9+++++++++
Meditor/e2e/saved-videos.spec.ts | 13++++++++++++-
Meditor/e2e/video-page.spec.ts | 22++++++++++++++++++++++
5 files changed, 96 insertions(+), 9 deletions(-)

diff --git a/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx b/editor/app/channels/[slug]/videos/[id]/components/VideoPanel.tsx @@ -718,6 +718,15 @@ function PerFileTranscribeRow({ replaceAutoSubs?: boolean; }) { const [transcribeQueue, setTranscribeQueue] = useState(""); + // `hasTranscript` is SERVER-derived (VideoPanel :360) and this row's own run + // is what writes transcript.json — so the run's own router.refresh() used to + // swap this panel for the "already present" line and take the log with it. + // Same shape as the two banners: the row survives (it is keyed by file name), + // the message and the run panel are separate slots, and the panel stays + // mounted — disabled, since the transcript now exists — once a run has + // started here. With no run it is still absent whenever a transcript is + // already present, so the DOM is unchanged. + const [ranHere, setRanHere] = useState(false); return ( <div className="flex flex-col gap-2 rounded border border-border p-3"> @@ -739,11 +748,20 @@ function PerFileTranscribeRow({ <p className="text-sm text-muted-foreground"> Transcribe disabled — transcript already present. </p> - ) : ( + ) : null} + {hasTranscript && !ranHere ? null : ( <StreamActionLog - trigger={() => - transcribeOneAction(slug, videoId, file.name, transcribeQueue) - } + key="transcribe-run" + disabled={hasTranscript} + trigger={() => { + setRanHere(true); + return transcribeOneAction( + slug, + videoId, + file.name, + transcribeQueue, + ); + }} cancelAction={cancelJobAction} buttonLabel={ replaceAutoSubs @@ -1093,6 +1111,10 @@ function SourceVideoSection({ {savedVideo && !ranHere ? null : ( <StreamActionLog key="persist-run" + // Mounted after a run so its log survives, but NOT actionable once the + // pointer is on disk: re-running would re-fetch a container the store + // already has. + disabled={!!savedVideo} trigger={() => { setRanHere(true); return redownloadToArchiveAction(slug, videoId, queueKey); @@ -1534,8 +1556,16 @@ function IncompleteTranscriptBanner({ const actionLabel = `Re-download & re-transcribe ${videoId}`; return ( <div - role="alert" - aria-label="incomplete transcript" + // The alert role and its label belong to the WARNING, not to the box that + // outlives it: once the run has cleared the flag this wrapper holds only + // the log, and announcing that as an alert named "incomplete transcript" + // would be a lie to a screen reader (and would keep answering + // getByLabel("incomplete transcript") for a video that is now fine — + // truncated-check.spec.ts:69 asserts exactly that it does not). While the + // flag holds, both attributes are exactly as they were. + {...(coverage + ? { role: "alert", "aria-label": "incomplete transcript" } + : {})} className="flex flex-col gap-3 rounded border px-3 py-2 text-sm border-warning/30 bg-warning-soft text-warning" > {coverage ? ( @@ -1552,6 +1582,9 @@ function IncompleteTranscriptBanner({ ) : null} <StreamActionLog key="run" + // Kept for its log, not for a second run: the transcript is no longer + // truncated once the flag is gone. + disabled={!coverage} trigger={() => { setRanHere(true); return redownloadIncompleteTranscriptAction(slug, videoId, queueKey); @@ -1607,8 +1640,10 @@ function ShortAudioBanner({ const actionLabel = `Re-download ${videoId} as Original`; return ( <div - role="alert" - aria-label="short audio" + // Conditional for the same reason as IncompleteTranscriptBanner above: the + // alert belongs to the warning, and the warning is gone once the + // re-download has cleared the flag. + {...(flagged ? { role: "alert", "aria-label": "short audio" } : {})} className="flex flex-col gap-3 rounded border px-3 py-2 text-sm border-warning/30 bg-warning-soft text-warning" > {flagged ? ( @@ -1629,6 +1664,9 @@ function ShortAudioBanner({ ) : null} <StreamActionLog key="run" + // Kept for its log, not for a second run: the download that cleared the + // flag is the one whose output this is. + disabled={!flagged} trigger={() => { setRanHere(true); return downloadVideoPipelineAction(slug, videoId, queueKey, "original"); diff --git a/editor/e2e/download-format-guard.spec.ts b/editor/e2e/download-format-guard.spec.ts @@ -170,6 +170,8 @@ test("duration guard flags a source-truncated download and keeps the stub", asyn test("the short-audio re-download's log survives the refresh that clears the banner", async ({ page, }) => { + // See saved-videos.spec.ts: sequential waits need their own budget. + test.setTimeout(120_000); const SLUG = "shortaudio-log"; // No `truncaud` sentinel: the re-download lands full-length audio, so the // duration guard passes and the outcome is rewritten as a success. @@ -217,6 +219,11 @@ test("the short-audio re-download's log survives the refresh that clears the ban ).toHaveCount(0, { timeout: 15_000 }); // And the log is STILL on screen, carrying the run's closing line. await expect(log).toContainText("download complete"); + // Kept for its log, not for a second run: the download that cleared the flag + // is the one whose output this is. + await expect( + page.getByRole("button", { name: "Re-download as Original" }), + ).toBeDisabled(); }); test("download format: Odysee auto picks original; YouTube uses bestaudio; channel override forces original", async ({ diff --git a/editor/e2e/incomplete-transcript.spec.ts b/editor/e2e/incomplete-transcript.spec.ts @@ -166,6 +166,9 @@ test("incomplete-transcript filter, glyph, panel banner, and the transcription p test("the fix run's log survives the refresh that clears the banner", async ({ page, }) => { + // See saved-videos.spec.ts: sequential waits, so give the case its own budget + // rather than let the 30 s default mask which assertion failed. + test.setTimeout(120_000); await resetData(); const slug = "incomplete-log"; const id = "vidVttTrunc"; @@ -226,6 +229,12 @@ test("the fix run's log survives the refresh that clears the banner", async ({ }); // And the log is STILL on screen, carrying the run's closing line. await expect(log).toContainText(`Normalized ${slug}/${id}`); + // Kept for its log, not for a second run: the transcript is no longer + // truncated. (Page-scoped, not banner-scoped — the wrapper drops the alert + // label with the warning it belonged to.) + await expect( + page.getByRole("button", { name: "Re-download & re-transcribe" }), + ).toBeDisabled(); }); test("channel bulk bar: clear incomplete resets the video and enables auto-runners", async ({ diff --git a/editor/e2e/saved-videos.spec.ts b/editor/e2e/saved-videos.spec.ts @@ -126,6 +126,10 @@ test("video page shows persisted source status and an unpersist control", async test("the persist run's log survives the refresh that lands the pointer", async ({ page, }) => { + // Three waits in sequence (the run, the pointer landing, the log) — without + // this the config's 30 s budget expires first and the failure reads as a test + // timeout instead of naming the assertion that actually failed. + test.setTimeout(120_000); await resetData("one-transcribe-channel-with-audio"); // redownloadToArchiveAction resolves the source URL from metadata.info.json // or the playlist, and this fixture ships neither. @@ -138,7 +142,12 @@ test("the persist run's log survives the refresh that lands the pointer", async await page.goto(`/channels/${SLUG}/videos/vidA`); // Nothing is persisted yet, so the card starts closed. await page.getByLabel("Source video stage summary").click(); - const run = page.getByRole("button", { name: "Persist source video" }); + // exact: the persisted view's "unpersist source video vidA" button matches + // this name as a substring once the run has landed the pointer. + const run = page.getByRole("button", { + name: "Persist source video", + exact: true, + }); await expect(run).toBeEnabled(); await run.click(); @@ -154,4 +163,6 @@ test("the persist run's log survives the refresh that lands the pointer", async }); // And the log is STILL on screen, carrying the run's closing line. await expect(log).toContainText("Persisted source video to"); + // Kept for its log, not for a second run: the container is in the store now. + await expect(run).toBeDisabled(); }); diff --git a/editor/e2e/video-page.spec.ts b/editor/e2e/video-page.spec.ts @@ -99,6 +99,9 @@ test("failed entry stays in the list across reloads (skip semantics)", async ({ test("transcribe-one writes transcript.json for a specific audio file", async ({ page, }) => { + // The disk poll plus the three UI waits below outrun the config's 30 s budget + // on a slow box, and a budget timeout hides which assertion failed. + test.setTimeout(120_000); await resetData("one-transcribe-channel-with-audio"); await page.goto("/channels/test-transcribe/videos/vidA"); await page.getByRole("button", { name: "Transcribe audio.m4a" }).click(); @@ -113,6 +116,25 @@ test("transcribe-one writes transcript.json for a specific audio file", async ({ { timeout: 30_000 }, ) .toBe(true); + + // AND THE ROW'S LOG SURVIVES THE REFRESH ITS OWN RUN FIRES. `hasTranscript` + // is server-derived, so the transcript this run just wrote makes the row + // render its "already present" line; when that line REPLACED the run panel, + // StreamActionLog was unmounted and the log the operator was reading went + // with it (plans/FACTS.md, "A run log lives in the panel's React state"). + // Asserted only after that line is on screen, so the refresh has provably + // landed — the same happens-before edge attribution.spec.ts takes on the + // freshness pill. + await expect( + page.getByText("Transcribe disabled — transcript already present."), + ).toBeVisible({ timeout: 15_000 }); + await expect(page.getByLabel("Transcribe audio.m4a output")).toContainText( + "Transcribe vidA done in", + ); + // Kept for its log, not for a second run. + await expect( + page.getByRole("button", { name: "Transcribe audio.m4a" }), + ).toBeDisabled(); }); test("transcode keeps source and writes audio.<target>", async ({ page }) => {