Archilyzer · Source

archilyzer

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

commit 4d4aa8b2543272762a152d460be7bbfd9d701fdd
parent 073cd66f98b4b05c36fc09e679ef1e88815e9ddf
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue, 19 May 2026 17:20:39 -0400

Active jobs UI

Diffstat:
Mcommon/controller/channels.ts | 18++++++++++++++++++
Aeditor/app/jobs/active/components/ChannelProgressBar.tsx | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aeditor/app/jobs/active/page.tsx | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/layout.tsx | 1+
4 files changed, 202 insertions(+), 0 deletions(-)

diff --git a/common/controller/channels.ts b/common/controller/channels.ts @@ -94,6 +94,24 @@ export async function channelExists( return exists(path.join(paths.channelsDir, slug, "config.json")); } +export async function readChannelStat( + paths: Paths, + slug: string, +): Promise<ChannelStat | null> { + const config = await readChannelConfig(paths, slug); + if (!config) return null; + const channelDir = path.join(paths.channelsDir, slug); + const counts = await countDataFiles(path.join(channelDir, "data")); + return { + slug, + config, + playlistCount: await countPlaylist(path.join(channelDir, "playlist")), + videoCount: counts.videos, + transcriptCount: counts.transcripts, + downloadCount: counts.downloads, + }; +} + export async function listChannels(paths: Paths): Promise<ChannelStat[]> { let entries: Dirent[]; try { diff --git a/editor/app/jobs/active/components/ChannelProgressBar.tsx b/editor/app/jobs/active/components/ChannelProgressBar.tsx @@ -0,0 +1,59 @@ +type Props = { + playlistCount: number | null; + videoCount: number; + downloadCount: number; + transcriptCount: number; +}; + +export function ChannelProgressBar({ + playlistCount, + videoCount, + downloadCount, + transcriptCount, +}: Props) { + const denom = + playlistCount && playlistCount > 0 + ? playlistCount + : Math.max(videoCount, downloadCount, transcriptCount, 1); + const denomLabel = playlistCount === null ? "?" : String(playlistCount); + const clamp = (n: number) => Math.max(0, Math.min(100, n)); + const transcribedPct = clamp((transcriptCount / denom) * 100); + const downloadedOnlyPct = clamp( + (Math.max(0, downloadCount - transcriptCount) / denom) * 100, + ); + return ( + <div className="flex flex-col gap-1"> + <div + role="progressbar" + aria-valuemin={0} + aria-valuemax={denom} + aria-valuenow={downloadCount} + aria-label={`Downloads ${downloadCount} of ${denomLabel}, transcripts ${transcriptCount} of ${denomLabel}`} + className="relative h-2 w-full overflow-hidden rounded bg-zinc-200 dark:bg-zinc-800" + > + <div + className="absolute inset-y-0 left-0 bg-emerald-500" + style={{ width: `${transcribedPct}%` }} + /> + <div + className="absolute inset-y-0 bg-emerald-200 dark:bg-emerald-800" + style={{ + left: `${transcribedPct}%`, + width: `${downloadedOnlyPct}%`, + }} + /> + </div> + <div className="text-xs text-zinc-500 flex flex-wrap gap-x-3"> + <span> + Downloads: {downloadCount} / {denomLabel} + </span> + <span> + Transcripts: {transcriptCount} / {denomLabel} + </span> + {playlistCount === null && ( + <span className="italic">playlist size unknown</span> + )} + </div> + </div> + ); +} diff --git a/editor/app/jobs/active/page.tsx b/editor/app/jobs/active/page.tsx @@ -0,0 +1,124 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; +import { + readChannelStat, + type ChannelStat, +} from "yt-dlp-transcript-common/controller/channels"; +import { getPaths } from "yt-dlp-transcript-common/lib/paths"; +import { JobsAutoRefresh } from "../components/JobsAutoRefresh"; +import { + RunningJobsList, + type RunningJobsListItem, +} from "../components/RunningJobsList"; +import { ChannelProgressBar } from "./components/ChannelProgressBar"; + +export const dynamic = "force-dynamic"; + +export const metadata: Metadata = { title: "Active jobs" }; + +export default async function ActiveJobsPage() { + const registry = getRegistry(); + const activeJobs: RunningJobsListItem[] = registry + .list() + .filter((j) => j.status === "running" || j.status === "queued") + .map((j) => ({ + id: j.id, + kind: j.kind, + status: j.status as "running" | "queued", + queueKey: j.queueKey, + channelSlug: j.channelSlug, + videoId: j.videoId, + })); + + const channelSlugs = Array.from( + new Set( + activeJobs + .map((j) => j.channelSlug) + .filter((s): s is string => Boolean(s)), + ), + ); + + const paths = getPaths(); + const statResults = await Promise.all( + channelSlugs.map((slug) => readChannelStat(paths, slug)), + ); + const channelStats = new Map<string, ChannelStat>(); + for (let i = 0; i < channelSlugs.length; i++) { + const stat = statResults[i]; + if (stat) channelStats.set(channelSlugs[i], stat); + } + + const jobsBySlug = new Map<string, RunningJobsListItem[]>(); + const unassigned: RunningJobsListItem[] = []; + for (const job of activeJobs) { + if (job.channelSlug) { + const list = jobsBySlug.get(job.channelSlug) ?? []; + list.push(job); + jobsBySlug.set(job.channelSlug, list); + } else { + unassigned.push(job); + } + } + + return ( + <div className="flex flex-col gap-4"> + <JobsAutoRefresh hasNonTerminal={activeJobs.length > 0} /> + <div className="flex items-center justify-between"> + <h1 className="text-2xl font-semibold">Active jobs</h1> + </div> + {activeJobs.length === 0 ? ( + <p className="text-sm text-zinc-500 border border-dashed border-zinc-300 dark:border-zinc-700 rounded p-4"> + No active jobs. + </p> + ) : ( + <div className="flex flex-col gap-4"> + {channelSlugs.map((slug) => { + const jobs = jobsBySlug.get(slug) ?? []; + const stat = channelStats.get(slug); + const displayName = stat?.config.name ?? slug; + return ( + <section + key={slug} + aria-label={`Active jobs for ${displayName}`} + className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded-md p-3 bg-white dark:bg-zinc-900" + > + <div className="flex flex-wrap items-baseline justify-between gap-2"> + <Link + href={`/channels/${slug}`} + className="text-base font-medium underline hover:text-zinc-900 dark:hover:text-zinc-100" + > + {displayName} + </Link> + <span className="font-mono text-xs text-zinc-500">{slug}</span> + </div> + {stat ? ( + <ChannelProgressBar + playlistCount={stat.playlistCount} + videoCount={stat.videoCount} + downloadCount={stat.downloadCount} + transcriptCount={stat.transcriptCount} + /> + ) : ( + <p className="text-xs text-zinc-500 italic"> + Channel stats unavailable. + </p> + )} + <RunningJobsList jobs={jobs} hideChannelSlug /> + </section> + ); + })} + {unassigned.length > 0 && ( + <section + aria-label="Active jobs without a channel" + className="flex flex-col gap-3 border border-zinc-200 dark:border-zinc-800 rounded-md p-3 bg-white dark:bg-zinc-900" + > + <h2 className="text-base font-medium">Other</h2> + <RunningJobsList jobs={unassigned} /> + </section> + )} + </div> + )} + </div> + ); +} diff --git a/editor/app/layout.tsx b/editor/app/layout.tsx @@ -28,6 +28,7 @@ const NAV_LINKS: NavLink[] = [ { href: "/", label: "Dashboard" }, { href: "/channels", label: "Channels" }, { href: "/jobs", label: "Jobs", badgeKey: "jobs" }, + { href: "/jobs/active", label: "Active", badgeKey: "jobs" }, { href: "/build", label: "Build" }, { href: "/settings", label: "Settings" }, ];