mirror of
https://github.com/myronblair/kino-app
synced 2026-07-27 21:18:43 -05:00
Add live transcode progress with a dedicated now-processing panel
- ffmpeg progress streamed via -progress pipe:1, persisted on the job as a percent - retry/retry-all now mark the old row as superseded instead of deleting it, preserving the failed/cancelled audit trail - frontend shows the running job in its own panel above the list with a live progress bar instead of burying it as just another row
This commit is contained in:
@@ -9,6 +9,7 @@ const STATUS_COLORS = {
|
||||
done: "text-[#86efac]",
|
||||
failed: "text-[#fca5a5]",
|
||||
cancelled: "text-[#8A8A8A]",
|
||||
superseded: "text-[#8A8A8A]",
|
||||
};
|
||||
|
||||
export default function TranscodeQueue() {
|
||||
@@ -26,14 +27,19 @@ export default function TranscodeQueue() {
|
||||
};
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
// Poll while jobs are active
|
||||
// Poll while jobs are active — faster while something is actually running so the
|
||||
// progress bar feels live, slower while only pending jobs are waiting.
|
||||
useEffect(() => {
|
||||
const active = jobs.some((j) => j.status === "running" || j.status === "pending");
|
||||
if (!active) return;
|
||||
const t = setInterval(load, 4000);
|
||||
const running = jobs.some((j) => j.status === "running");
|
||||
const pending = jobs.some((j) => j.status === "pending");
|
||||
if (!running && !pending) return;
|
||||
const t = setInterval(load, running ? 2000 : 4000);
|
||||
return () => clearInterval(t);
|
||||
}, [jobs]);
|
||||
|
||||
const runningJob = jobs.find((j) => j.status === "running");
|
||||
const listedJobs = jobs.filter((j) => j.status !== "running");
|
||||
|
||||
const cancel = async (id) => {
|
||||
try { await api.delete(`/transcode/queue/${id}`); toast.success("Job cancelled"); load(); }
|
||||
catch (err) { toast.error(err.response?.data?.detail || "Could not cancel"); }
|
||||
@@ -116,7 +122,31 @@ export default function TranscodeQueue() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 border border-[#222]">
|
||||
{runningJob && (
|
||||
<div className="mt-8 border border-[#D9381E]/40 bg-[#D9381E]/[0.04] p-5" data-testid="now-processing">
|
||||
<div className="flex items-center justify-between text-[10px] uppercase tracking-[0.3em] text-[#D9381E]">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#D9381E] animate-pulse" />
|
||||
Now Processing
|
||||
</span>
|
||||
<span>{(runningJob.progress || 0).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="mt-2 text-white text-lg truncate" title={runningJob.movie_title}>
|
||||
{runningJob.movie_title || runningJob.movie_id.slice(0, 8)}
|
||||
</div>
|
||||
<div className="mt-1 text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">
|
||||
{runningJob.quality} · {runningJob.triggered_by}
|
||||
</div>
|
||||
<div className="mt-3 h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#D9381E] transition-[width] duration-500 ease-linear rounded-full"
|
||||
style={{ width: `${Math.max(2, runningJob.progress || 0)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 border border-[#222]">
|
||||
<div className="grid grid-cols-12 px-5 py-3 border-b border-[#222] text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">
|
||||
<span className="col-span-5">Movie</span>
|
||||
<span className="col-span-1">Quality</span>
|
||||
@@ -125,9 +155,9 @@ export default function TranscodeQueue() {
|
||||
<span className="col-span-2">Created</span>
|
||||
<span className="col-span-1 text-right">Actions</span>
|
||||
</div>
|
||||
{jobs.length === 0 ? (
|
||||
{listedJobs.length === 0 ? (
|
||||
<div className="p-8 text-center text-[#8A8A8A] text-sm" data-testid="queue-empty">No jobs yet.</div>
|
||||
) : jobs.map((j) => (
|
||||
) : listedJobs.map((j) => (
|
||||
<div key={j.id} className="grid grid-cols-12 items-center px-5 py-3 border-b border-[#222] last:border-b-0 hover:bg-[#0F0F0F] transition-colors" data-testid={`queue-row-${j.id}`}>
|
||||
<div className="col-span-5 text-white truncate" title={j.movie_title}>{j.movie_title || j.movie_id.slice(0, 8)}</div>
|
||||
<span className="col-span-1 text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">{j.quality}</span>
|
||||
@@ -138,11 +168,6 @@ export default function TranscodeQueue() {
|
||||
</span>
|
||||
<span className="col-span-2 text-xs text-[#8A8A8A]">{new Date(j.created_at).toLocaleString()}</span>
|
||||
<div className="col-span-1 flex justify-end gap-2">
|
||||
{j.status === "running" && (
|
||||
<button disabled className="text-[#444] cursor-not-allowed" title="Cannot cancel a running job" aria-label="Running">
|
||||
<X size={14} strokeWidth={1.5} />
|
||||
</button>
|
||||
)}
|
||||
{j.status === "pending" && (
|
||||
<button onClick={() => cancel(j.id)} className="text-[#8A8A8A] hover:text-[#fca5a5]" data-testid={`cancel-${j.id}`} aria-label="Cancel">
|
||||
<X size={14} strokeWidth={1.5} />
|
||||
|
||||
Reference in New Issue
Block a user