pipeline: equal-width columns + per-column clear/stop button
Build and Push Docker Image / build (push) Successful in 39s

Extract a ColumnShell component so all four columns share the same flex-1
basis-0 width (no more 24/16/18/16 rem mix) and the same header layout
(title + count + optional action button on the right).

Per-column actions:
- Review:     'Skip all' → POST /api/review/skip-all (new endpoint, sets all
              pending non-noop plans to skipped in one update)
- Queued:     'Clear'    → POST /api/execute/clear (existing; cancels pending jobs)
- Processing: 'Stop'     → POST /api/execute/stop (new; SIGTERMs the running
              ffmpeg via a tracked Bun.spawn handle, runJob's catch path
              marks the job error and cleans up)
- Done:       'Clear'    → POST /api/execute/clear-completed (existing)

All destructive actions confirm before firing.
This commit is contained in:
2026-04-13 10:08:42 +02:00
parent ec28e43484
commit 4a378eb833
8 changed files with 184 additions and 77 deletions
+48 -38
View File
@@ -1,12 +1,15 @@
import { Badge } from "~/shared/components/ui/badge";
import { api } from "~/shared/lib/api";
import { ColumnShell } from "./ColumnShell";
interface ProcessingColumnProps {
items: any[];
progress?: { id: number; seconds: number; total: number } | null;
queueStatus?: { status: string; until?: string; seconds?: number } | null;
onMutate: () => void;
}
export function ProcessingColumn({ items, progress, queueStatus }: ProcessingColumnProps) {
export function ProcessingColumn({ items, progress, queueStatus, onMutate }: ProcessingColumnProps) {
const job = items[0]; // at most one running job
const formatTime = (s: number) => {
@@ -15,46 +18,53 @@ export function ProcessingColumn({ items, progress, queueStatus }: ProcessingCol
return `${m}:${String(sec).padStart(2, "0")}`;
};
const stop = async () => {
if (!confirm(`Stop the running job "${job?.name}"? It will be marked as error.`)) return;
await api.post("/api/execute/stop");
onMutate();
};
return (
<div className="flex flex-col w-72 min-w-72 min-h-0 bg-gray-50 rounded-lg">
<div className="px-3 py-2 border-b font-medium text-sm">Processing</div>
<div className="flex-1 p-3">
{queueStatus && queueStatus.status !== "running" && (
<div className="mb-3 text-xs text-gray-500 bg-white rounded border p-2">
{queueStatus.status === "paused" && <>Paused until {queueStatus.until}</>}
{queueStatus.status === "sleeping" && <>Sleeping {queueStatus.seconds}s between jobs</>}
{queueStatus.status === "idle" && <>Idle</>}
<ColumnShell
title="Processing"
count={job ? 1 : 0}
action={job ? { label: "Stop", onClick: stop, danger: true } : undefined}
>
{queueStatus && queueStatus.status !== "running" && (
<div className="mb-2 text-xs text-gray-500 bg-white rounded border p-2">
{queueStatus.status === "paused" && <>Paused until {queueStatus.until}</>}
{queueStatus.status === "sleeping" && <>Sleeping {queueStatus.seconds}s between jobs</>}
{queueStatus.status === "idle" && <>Idle</>}
</div>
)}
{job ? (
<div className="rounded border bg-white p-3">
<p className="text-sm font-medium truncate">{job.name}</p>
<div className="flex items-center gap-2 mt-1">
<Badge variant="running">running</Badge>
<Badge variant={job.job_type === "transcode" ? "manual" : "noop"}>{job.job_type}</Badge>
</div>
)}
{job ? (
<div className="rounded border bg-white p-3">
<p className="text-sm font-medium truncate">{job.name}</p>
<div className="flex items-center gap-2 mt-1">
<Badge variant="running">running</Badge>
<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>
{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>
) : (
<p className="text-sm text-gray-400 text-center py-8">No active job</p>
)}
</div>
</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>
)}
</div>
) : (
<p className="text-sm text-gray-400 text-center py-8">No active job</p>
)}
</ColumnShell>
);
}