From 5fa39aee7c24d3cfb88824f6a91fe6e5df4441b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Mon, 13 Apr 2026 10:29:49 +0200 Subject: [PATCH] processing card: meaningful progress display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues with the old bar: 1. progress state was never cleared between jobs — when a job finished, its 100% bar lingered on the next job's card until that job emitted its first progress event. Clear progress on any job_update where status != 'running', and on the column side ignore progress unless progress.id matches the current job.id. 2. labels were misleading: the left/right times were ffmpeg's *input* timestamp position (how far into the source it had read), not wall- clock elapsed/remaining. For -c copy jobs ripping a 90-min file in 5 wall-seconds, the user saw '0:45 / 90:00' jump straight to '90:00 / 90:00' which looks broken. New display: 'elapsed M:SS N% ~M:SS left'. Elapsed is wall-clock since the job started (re-renders every second), percent comes from ffmpeg input progress as before, ETA is derived from elapsed × (100-p)/p once we have at least 1% to avoid wild guesses. --- src/features/pipeline/PipelinePage.tsx | 12 ++++- src/features/pipeline/ProcessingColumn.tsx | 56 +++++++++++++++------- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/features/pipeline/PipelinePage.tsx b/src/features/pipeline/PipelinePage.tsx index e6f1790..cfaec9b 100644 --- a/src/features/pipeline/PipelinePage.tsx +++ b/src/features/pipeline/PipelinePage.tsx @@ -69,7 +69,17 @@ export function PipelinePage() { }, 1000); }; const es = new EventSource("/api/execute/events"); - es.addEventListener("job_update", scheduleReload); + es.addEventListener("job_update", (e) => { + // When a job leaves 'running' (done / error / cancelled), drop any + // stale progress so the bar doesn't linger on the next job's card. + try { + const upd = JSON.parse((e as MessageEvent).data) as { id: number; status: string }; + if (upd.status !== "running") setProgress(null); + } catch { + /* ignore malformed events */ + } + scheduleReload(); + }); es.addEventListener("job_progress", (e) => { setProgress(JSON.parse((e as MessageEvent).data)); }); diff --git a/src/features/pipeline/ProcessingColumn.tsx b/src/features/pipeline/ProcessingColumn.tsx index 5174cc7..5357a7f 100644 --- a/src/features/pipeline/ProcessingColumn.tsx +++ b/src/features/pipeline/ProcessingColumn.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import { Badge } from "~/shared/components/ui/badge"; import { api } from "~/shared/lib/api"; import { ColumnShell } from "./ColumnShell"; @@ -12,10 +13,31 @@ interface ProcessingColumnProps { export function ProcessingColumn({ items, progress, queueStatus, onMutate }: ProcessingColumnProps) { const job = items[0]; // at most one running job - const formatTime = (s: number) => { - const m = Math.floor(s / 60); - const sec = Math.floor(s % 60); - return `${m}:${String(sec).padStart(2, "0")}`; + // Wall-clock elapsed since the job started — re-renders every second. + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + if (!job) return; + const t = setInterval(() => setNow(Date.now()), 1000); + return () => clearInterval(t); + }, [job]); + + // Only trust progress if it belongs to the current job — stale events from + // a previous job would otherwise show wrong numbers until the new job emits. + const liveProgress = job && progress && progress.id === job.id ? progress : null; + const startedAt = job?.started_at ? new Date(`${job.started_at}Z`).getTime() : null; + const elapsedMs = startedAt ? Math.max(0, now - startedAt) : 0; + const percent = liveProgress && liveProgress.total > 0 ? (liveProgress.seconds / liveProgress.total) * 100 : null; + // ETA = elapsed × (1 - p) / p — only meaningful once we have a few percent + // of progress and the rate has stabilised. + const etaMs = percent != null && percent >= 1 && elapsedMs > 0 ? (elapsedMs * (100 - percent)) / percent : null; + + const formatDuration = (ms: number) => { + const total = Math.floor(ms / 1000); + const h = Math.floor(total / 3600); + const m = Math.floor((total % 3600) / 60); + const s = total % 60; + if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; + return `${m}:${String(s).padStart(2, "0")}`; }; const stop = async () => { @@ -55,21 +77,19 @@ export function ProcessingColumn({ items, progress, queueStatus, onMutate }: Pro {job.job_type} - {progress && progress.total > 0 && ( -
-
- {formatTime(progress.seconds)} - {Math.round((progress.seconds / progress.total) * 100)}% - {formatTime(progress.total)} -
-
-
-
+
+
+ elapsed {formatDuration(elapsedMs)} + {percent != null ? `${Math.round(percent)}%` : "starting…"} + {etaMs != null ? `~${formatDuration(etaMs)} left` : "—"}
- )} +
+
+
+
) : (

No active job