pipeline: equal-width columns + per-column clear/stop button
All checks were successful
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

View File

@@ -20,6 +20,8 @@ const app = new Hono();
// ─── Sequential local queue ──────────────────────────────────────────────────
let queueRunning = false;
let runningProc: ReturnType<typeof Bun.spawn> | null = null;
let runningJobId: number | null = null;
function emitQueueStatus(
status: "running" | "paused" | "sleeping" | "idle",
@@ -255,6 +257,23 @@ app.post("/clear-completed", (c) => {
return c.json({ ok: true, cleared: result.changes });
});
// ─── Stop running job ─────────────────────────────────────────────────────────
app.post("/stop", (c) => {
if (!runningProc || runningJobId == null) {
return c.json({ ok: false, error: "No job is currently running" }, 409);
}
const stoppedId = runningJobId;
try {
runningProc.kill("SIGTERM");
} catch (err) {
logError(`Failed to kill job ${stoppedId}:`, err);
return c.json({ ok: false, error: String(err) }, 500);
}
// runJob's catch path will mark the job error and clean up runningProc.
return c.json({ ok: true, stopped: stoppedId });
});
// ─── SSE ──────────────────────────────────────────────────────────────────────
app.get("/events", (c) => {
@@ -356,6 +375,8 @@ async function runJob(job: Job): Promise<void> {
try {
const proc = Bun.spawn(["sh", "-c", job.command], { stdout: "pipe", stderr: "pipe" });
runningProc = proc;
runningJobId = job.id;
const readStream = async (readable: ReadableStream<Uint8Array>, prefix = "") => {
const reader = readable.getReader();
const decoder = new TextDecoder();
@@ -422,6 +443,9 @@ async function runJob(job: Job): Promise<void> {
.run(fullOutput, job.id);
emitJobUpdate(job.id, "error", fullOutput);
db.prepare("UPDATE review_plans SET status = 'error' WHERE item_id = ?").run(job.item_id);
} finally {
runningProc = null;
runningJobId = null;
}
}