processing card: meaningful progress display
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m42s
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m42s
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.
This commit is contained in:
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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
|
||||
<Badge variant={job.job_type === "transcode" ? "manual" : "noop"}>{job.job_type}</Badge>
|
||||
</div>
|
||||
|
||||
{progress && progress.total > 0 && (
|
||||
<div className="mt-3">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>{formatTime(progress.seconds)}</span>
|
||||
<span>{Math.round((progress.seconds / progress.total) * 100)}%</span>
|
||||
<span>{formatTime(progress.total)}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 rounded-full transition-all"
|
||||
style={{ width: `${Math.min(100, (progress.seconds / progress.total) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1 tabular-nums">
|
||||
<span>elapsed {formatDuration(elapsedMs)}</span>
|
||||
<span>{percent != null ? `${Math.round(percent)}%` : "starting…"}</span>
|
||||
<span>{etaMs != null ? `~${formatDuration(etaMs)} left` : "—"}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 rounded-full transition-all"
|
||||
style={{ width: `${percent != null ? Math.min(100, percent) : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400 text-center py-8">No active job</p>
|
||||
|
||||
Reference in New Issue
Block a user