Archilyzer · Source

archilyzer

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

commit 71dbb91b579312e8df32a2b0f235bdf69e4dc58c
parent 9432cc210af3cca0c483b4eb06ce436afa55e7ea
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 11 May 2026 20:59:27 -0400

Rethink UI

Diffstat:
Aeditor/app/channels/[slug]/components/StageMasterDetail.tsx | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor/app/channels/[slug]/components/StageRailItem.tsx | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/channels/[slug]/components/StatusHeader.tsx | 2+-
Meditor/app/channels/[slug]/page.tsx | 274+++++++++++++++++++++++++++++++++++++------------------------------------------
Meditor/app/channels/page.tsx | 4++--
Meditor/app/jobs/page.tsx | 4++--
Meditor/app/layout.tsx | 2+-
Meditor/app/page.tsx | 4++--
8 files changed, 290 insertions(+), 154 deletions(-)

diff --git a/editor/app/channels/[slug]/components/StageMasterDetail.tsx b/editor/app/channels/[slug]/components/StageMasterDetail.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useSyncExternalStore, type ReactNode } from "react"; +import type { StageId, StageStatus, StageTone } from "../lib/stageStatus"; +import { StageRailItem } from "./StageRailItem"; + +const TONE_HEADING_BORDER: Record<StageTone, string> = { + neutral: "border-zinc-200 dark:border-zinc-800", + attention: "border-amber-300 dark:border-amber-800", + danger: "border-red-300 dark:border-red-800", + running: "border-blue-300 dark:border-blue-800", + ok: "border-emerald-300 dark:border-emerald-800", +}; + +type Props = { + stages: Record<StageId, StageStatus>; + stageOrder: StageId[]; + panels: Partial<Record<StageId, ReactNode>>; +}; + +function subscribeHash(callback: () => void) { + window.addEventListener("hashchange", callback); + return () => window.removeEventListener("hashchange", callback); +} + +function getHash(): string { + if (typeof window === "undefined") return ""; + return window.location.hash; +} + +function getServerHash(): string { + return ""; +} + +export function StageMasterDetail({ stages, stageOrder, panels }: Props) { + const hash = useSyncExternalStore(subscribeHash, getHash, getServerHash); + const defaultStage = + stageOrder.find((id) => stages[id].defaultOpen) ?? stageOrder[0]; + const hashMatch = /^#stage-(.+)$/.exec(hash); + const fromHash = hashMatch + ? (stageOrder.find((id) => id === hashMatch[1]) ?? null) + : null; + const selected = fromHash ?? defaultStage; + const status = stages[selected]; + const isDanger = selected === "danger"; + + return ( + <div className="grid grid-cols-[14rem_minmax(0,1fr)] gap-6"> + <nav + aria-label="Pipeline stages" + className="flex flex-col gap-0.5 sticky top-32 self-start" + > + {stageOrder.map((id) => ( + <StageRailItem + key={id} + status={stages[id]} + active={id === selected} + href={`#stage-${id}`} + /> + ))} + </nav> + <section + aria-labelledby={`stage-${selected}-heading`} + className={`rounded-md border bg-white dark:bg-zinc-900 ${TONE_HEADING_BORDER[status.tone]}`} + > + <header className="flex items-baseline gap-3 flex-wrap px-4 py-3 border-b border-zinc-100 dark:border-zinc-800"> + <h2 + id={`stage-${selected}-heading`} + className={`text-lg font-semibold ${isDanger ? "text-red-700 dark:text-red-400" : ""}`} + > + {status.title} + </h2> + <span className="text-sm text-zinc-500">{status.summary}</span> + </header> + <div className="px-4 py-4 flex flex-col gap-4">{panels[selected]}</div> + </section> + </div> + ); +} diff --git a/editor/app/channels/[slug]/components/StageRailItem.tsx b/editor/app/channels/[slug]/components/StageRailItem.tsx @@ -0,0 +1,75 @@ +import type { StageStatus, StageTone } from "../lib/stageStatus"; + +const TONE_ACCENT: Record<StageTone, string> = { + neutral: "text-zinc-700 dark:text-zinc-300", + attention: "text-amber-700 dark:text-amber-300", + danger: "text-red-700 dark:text-red-300", + running: "text-blue-700 dark:text-blue-300", + ok: "text-emerald-700 dark:text-emerald-300", +}; + +const TONE_ACTIVE_BG: Record<StageTone, string> = { + neutral: "bg-zinc-100 dark:bg-zinc-800", + attention: "bg-amber-50 dark:bg-amber-950", + danger: "bg-red-50 dark:bg-red-950", + running: "bg-blue-50 dark:bg-blue-950", + ok: "bg-emerald-50 dark:bg-emerald-950", +}; + +const TONE_ACTIVE_BORDER: Record<StageTone, string> = { + neutral: "border-zinc-400 dark:border-zinc-500", + attention: "border-amber-400 dark:border-amber-600", + danger: "border-red-400 dark:border-red-600", + running: "border-blue-400 dark:border-blue-600", + ok: "border-emerald-400 dark:border-emerald-600", +}; + +type Props = { + status: StageStatus; + active: boolean; + href: string; + onSelect?: () => void; +}; + +export function StageRailItem({ status, active, href, onSelect }: Props) { + const bg = active ? TONE_ACTIVE_BG[status.tone] : "hover:bg-zinc-100 dark:hover:bg-zinc-800/60"; + const border = active + ? `border-l-2 ${TONE_ACTIVE_BORDER[status.tone]}` + : "border-l-2 border-transparent"; + return ( + <a + href={href} + aria-current={active ? "page" : undefined} + onClick={onSelect} + className={`flex items-center gap-2 pl-3 pr-2 py-2 rounded-r-md text-sm ${bg} ${border}`} + > + {status.running && ( + <span + aria-hidden="true" + className="inline-block w-2 h-2 rounded-full bg-blue-500 animate-pulse shrink-0" + /> + )} + <span className={`truncate ${active ? "font-semibold" : ""} ${TONE_ACCENT[status.tone]}`}> + {status.title} + </span> + <span className="ml-auto flex items-center gap-1"> + {status.pending > 0 && ( + <span + aria-label={`${status.title} pending count`} + className="font-mono text-xs px-1.5 rounded bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200" + > + {status.pending} + </span> + )} + {status.failed > 0 && ( + <span + aria-label={`${status.title} failed count`} + className="font-mono text-xs px-1.5 rounded bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200" + > + {status.failed} + </span> + )} + </span> + </a> + ); +} diff --git a/editor/app/channels/[slug]/components/StatusHeader.tsx b/editor/app/channels/[slug]/components/StatusHeader.tsx @@ -69,7 +69,7 @@ export function StatusHeader({ </div> <div aria-label="pipeline stage badges" - className="flex flex-wrap gap-2" + className="flex flex-wrap gap-2 lg:hidden" > {badgeIds.map((id) => ( <StageBadge key={id} status={stages[id]} /> diff --git a/editor/app/channels/[slug]/page.tsx b/editor/app/channels/[slug]/page.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from "react"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { readChannelConfig } from "yt-dlp-transcript-common/controller/channels"; @@ -17,6 +18,7 @@ import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; import { ChannelFormClient } from "../components/ChannelFormClient"; import { DeleteChannelForm } from "../components/DeleteChannelForm"; import { PipelineStageCard } from "./components/PipelineStageCard"; +import { StageMasterDetail } from "./components/StageMasterDetail"; import { StatusHeader } from "./components/StatusHeader"; import { CleanupStage } from "./components/stages/CleanupStage"; import { DiagnosticsStage } from "./components/stages/DiagnosticsStage"; @@ -24,7 +26,11 @@ import { DownloadStage } from "./components/stages/DownloadStage"; import { PlaylistStage } from "./components/stages/PlaylistStage"; import { TranscodeStage } from "./components/stages/TranscodeStage"; import { TranscribeStage } from "./components/stages/TranscribeStage"; -import { computeStageStatuses, normalizeBuckets } from "./lib/stageStatus"; +import { + computeStageStatuses, + normalizeBuckets, + type StageId, +} from "./lib/stageStatus"; import { deleteChannelAction, updateChannelAction, @@ -89,6 +95,99 @@ export default async function ChannelDetailPage({ const transcodeApplies = config.handling === "transcribe" && !!config.audioFormat; + const stageOrder: StageId[] = [ + "configure", + "playlist", + "download", + ...(transcodeApplies ? (["transcode"] as const) : []), + "transcribe", + "cleanup", + "diagnostics", + "danger", + ]; + + const panels: Partial<Record<StageId, ReactNode>> = { + configure: ( + <ChannelFormClient + action={ + update as ( + prev: ActionResult, + formData: FormData, + ) => Promise<ActionResult> + } + initial={{ slug, config }} + submitLabel="Save changes" + /> + ), + playlist: ( + <PlaylistStage + slug={slug} + hasUrl={!!config.url} + defaultQueueKey={platformDefaultQueueKey} + existingQueues={existingQueues} + /> + ), + download: ( + <DownloadStage + slug={slug} + hasUrl={!!config.url} + defaultQueueKey={platformDefaultQueueKey} + existingQueues={existingQueues} + undownloadedIds={undownloadedIds} + noTranscriptIds={buckets.noTranscript} + /> + ), + transcribe: ( + <TranscribeStage + slug={slug} + existingQueues={existingQueues} + failedVideoIds={failedVideoIds} + downloadedNoTranscriptIds={buckets.downloadedNoTranscript} + defaultConcurrency={paths.parallelTranscribeLimit} + /> + ), + cleanup: ( + <CleanupStage + slug={slug} + existingQueues={existingQueues} + multipleAudioFormatIds={buckets.multipleAudioFormats} + transcodeApplies={transcodeApplies} + /> + ), + diagnostics: ( + <DiagnosticsStage + slug={slug} + untranscribableIds={buckets.untranscribable} + noMetadataIds={buckets.noMetadata} + missingFromArchiveIds={buckets.missingFromArchive} + duplicateDirIds={buckets.duplicateDirs} + totals={snapshot.totals} + availability={availability} + /> + ), + danger: ( + <DeleteChannelForm + slug={slug} + action={ + del as ( + prev: ActionResult, + formData: FormData, + ) => Promise<ActionResult> + } + /> + ), + }; + if (transcodeApplies) { + panels.transcode = ( + <TranscodeStage + slug={slug} + existingQueues={existingQueues} + failedTranscodingIds={failedTranscodingIds} + untranscodedIds={buckets.untranscoded} + /> + ); + } + return ( <div className="flex flex-col gap-6"> <StatusHeader @@ -100,153 +199,36 @@ export default async function ChannelDetailPage({ transcodeApplies={transcodeApplies} /> - <PipelineStageCard - id="configure" - title="Configure" - summary={stages.configure.summary} - defaultOpen={stages.configure.defaultOpen} - tone={stages.configure.tone} - > - <ChannelFormClient - action={ - update as ( - prev: ActionResult, - formData: FormData, - ) => Promise<ActionResult> - } - initial={{ slug, config }} - submitLabel="Save changes" - /> - </PipelineStageCard> - - <PipelineStageCard - id="playlist" - title="Playlist" - summary={stages.playlist.summary} - defaultOpen={stages.playlist.defaultOpen} - tone={stages.playlist.tone} - running={stages.playlist.running} - > - <PlaylistStage - slug={slug} - hasUrl={!!config.url} - defaultQueueKey={platformDefaultQueueKey} - existingQueues={existingQueues} - /> - </PipelineStageCard> - - <PipelineStageCard - id="download" - title="Download" - summary={stages.download.summary} - defaultOpen={stages.download.defaultOpen} - tone={stages.download.tone} - pending={stages.download.pending} - running={stages.download.running} - > - <DownloadStage - slug={slug} - hasUrl={!!config.url} - defaultQueueKey={platformDefaultQueueKey} - existingQueues={existingQueues} - undownloadedIds={undownloadedIds} - noTranscriptIds={buckets.noTranscript} - /> - </PipelineStageCard> - - {transcodeApplies && ( - <PipelineStageCard - id="transcode" - title="Transcode" - summary={stages.transcode.summary} - defaultOpen={stages.transcode.defaultOpen} - tone={stages.transcode.tone} - pending={stages.transcode.pending} - failed={stages.transcode.failed} - running={stages.transcode.running} - > - <TranscodeStage - slug={slug} - existingQueues={existingQueues} - failedTranscodingIds={failedTranscodingIds} - untranscodedIds={buckets.untranscoded} - /> - </PipelineStageCard> - )} - - <PipelineStageCard - id="transcribe" - title="Transcribe" - summary={stages.transcribe.summary} - defaultOpen={stages.transcribe.defaultOpen} - tone={stages.transcribe.tone} - pending={stages.transcribe.pending} - failed={stages.transcribe.failed} - running={stages.transcribe.running} - > - <TranscribeStage - slug={slug} - existingQueues={existingQueues} - failedVideoIds={failedVideoIds} - downloadedNoTranscriptIds={buckets.downloadedNoTranscript} - defaultConcurrency={paths.parallelTranscribeLimit} - /> - </PipelineStageCard> - - <PipelineStageCard - id="cleanup" - title="Cleanup" - summary={stages.cleanup.summary} - defaultOpen={stages.cleanup.defaultOpen} - tone={stages.cleanup.tone} - pending={stages.cleanup.pending} - running={stages.cleanup.running} - > - <CleanupStage - slug={slug} - existingQueues={existingQueues} - multipleAudioFormatIds={buckets.multipleAudioFormats} - transcodeApplies={transcodeApplies} - /> - </PipelineStageCard> - - <PipelineStageCard - id="diagnostics" - title="Diagnostics" - summary={stages.diagnostics.summary} - defaultOpen={stages.diagnostics.defaultOpen} - tone={stages.diagnostics.tone} - pending={stages.diagnostics.pending} - running={stages.diagnostics.running} - > - <DiagnosticsStage - slug={slug} - untranscribableIds={buckets.untranscribable} - noMetadataIds={buckets.noMetadata} - missingFromArchiveIds={buckets.missingFromArchive} - duplicateDirIds={buckets.duplicateDirs} - totals={snapshot.totals} - availability={availability} - /> - </PipelineStageCard> + {/* Mobile / tablet: stacked details cards */} + <div className="flex flex-col gap-4 lg:hidden"> + {stageOrder.map((id) => { + const s = stages[id]; + return ( + <PipelineStageCard + key={id} + id={id} + title={s.title} + summary={s.summary} + defaultOpen={s.defaultOpen} + tone={s.tone} + pending={s.pending} + failed={s.failed} + running={s.running} + > + {panels[id]} + </PipelineStageCard> + ); + })} + </div> - <PipelineStageCard - id="danger" - title="Danger zone" - summary={stages.danger.summary} - defaultOpen={stages.danger.defaultOpen} - tone={stages.danger.tone} - > - <DeleteChannelForm - slug={slug} - action={ - del as ( - prev: ActionResult, - formData: FormData, - ) => Promise<ActionResult> - } + {/* Desktop (lg+): master/detail with hash-driven selection */} + <div className="hidden lg:block"> + <StageMasterDetail + stages={stages} + stageOrder={stageOrder} + panels={panels} /> - </PipelineStageCard> + </div> </div> ); } diff --git a/editor/app/channels/page.tsx b/editor/app/channels/page.tsx @@ -26,8 +26,8 @@ export default async function ChannelsPage() { No channels yet. </p> ) : ( - <div className="overflow-x-auto -mx-4 md:mx-0"> - <table className="text-sm border-y md:border border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full md:w-auto"> + <div className="overflow-x-auto -mx-4 md:mx-0 md:overflow-visible"> + <table className="text-sm border-y md:border border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full"> <thead className="bg-zinc-100 dark:bg-zinc-900"> <tr> <Th>Slug</Th> diff --git a/editor/app/jobs/page.tsx b/editor/app/jobs/page.tsx @@ -52,8 +52,8 @@ export default async function JobsPage() { No jobs have run yet. </p> ) : ( - <div className="overflow-x-auto -mx-4 md:mx-0"> - <table className="text-sm border-y md:border border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full md:w-auto"> + <div className="overflow-x-auto -mx-4 md:mx-0 md:overflow-visible"> + <table className="text-sm border-y md:border border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full"> <thead className="bg-zinc-100 dark:bg-zinc-900"> <tr> <th className="text-left font-medium px-3 py-2">ID</th> diff --git a/editor/app/layout.tsx b/editor/app/layout.tsx @@ -57,7 +57,7 @@ export default async function RootLayout({ ))} </nav> </aside> - <main className="flex-1 px-4 md:px-6 py-4 md:py-6 max-w-5xl">{children}</main> + <main className="flex-1 min-w-0 px-4 md:px-6 py-4 md:py-6">{children}</main> </body> </html> ); diff --git a/editor/app/page.tsx b/editor/app/page.tsx @@ -59,8 +59,8 @@ export default async function Dashboard() { {channels.length > 0 && ( <section className="flex flex-col gap-3"> <h2 className="text-lg font-semibold">Channels</h2> - <div className="overflow-x-auto -mx-4 md:mx-0"> - <table className="text-sm border-y md:border md:border-zinc-200 dark:md:border-zinc-800 border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full md:w-auto"> + <div className="overflow-x-auto -mx-4 md:mx-0 md:overflow-visible"> + <table className="text-sm border-y md:border md:border-zinc-200 dark:md:border-zinc-800 border-zinc-200 dark:border-zinc-800 md:rounded-md md:overflow-hidden w-full"> <thead className="bg-zinc-100 dark:bg-zinc-900"> <tr> <th className="text-left font-medium px-3 py-2">Slug</th>