feat: add retry/delete for jobs
All checks were successful
Build & Push Docker Image / build-and-push (push) Successful in 41s

- db.ts: add resetJob() and deleteJob() statements + exports
- pipeline.ts: export retryJob() — resets job state and re-runs pipeline
- DELETE /api/jobs/[id]: hard-delete terminal jobs (done/failed/cancelled);
  keep cancel-only behavior for active jobs
- POST /api/jobs/[id]/retry: new endpoint; validates failed/cancelled URL job,
  resets and re-runs via retryJob()
- jobs/[id]/+page.svelte: wire Cancel/Retry/Delete buttons with fetch calls;
  fix hardcoded ACCENT → accent store
- jobs/+page.svelte: per-row Retry+Delete icon buttons (visible on hover);
  fix hardcoded ACCENT → accent store

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Giancarmine Salucci
2026-05-06 17:42:54 +02:00
parent 37175ec791
commit d1295ce343
6 changed files with 207 additions and 15 deletions

View File

@@ -4,8 +4,9 @@
import type { Job, Segment } from '$lib/types.js';
import SourceIcon from '$lib/components/SourceIcon.svelte';
import Waveform from '$lib/components/Waveform.svelte';
import { accent } from '$lib/accent.js';
const ACCENT = '#cdf24e';
const ACCENT = $derived($accent.value);
const jobId = $derived($page.params.id);
let job = $state<Job | null>(null);
@@ -108,6 +109,36 @@
const formats = ['srt', 'txt', 'md', 'json'] as const;
const isActive = $derived(!job || !['done', 'failed', 'cancelled'].includes(job.status));
const isTerminal = $derived(job !== null && ['done', 'failed', 'cancelled'].includes(job.status));
const canRetry = $derived(
job !== null &&
['failed', 'cancelled'].includes(job.status) &&
(job.source?.startsWith('http') ?? false)
);
async function cancelJob() {
if (!job) return;
await fetch(`/api/jobs/${job.id}`, { method: 'DELETE' });
await loadJob();
}
async function deleteJob() {
if (!job || !confirm(`Delete job "${job.title || job.id}"?`)) return;
const res = await fetch(`/api/jobs/${job.id}`, { method: 'DELETE' });
if (res.ok) window.location.href = '/';
}
async function retryJobAction() {
if (!job) return;
const res = await fetch(`/api/jobs/${job.id}/retry`, { method: 'POST' });
if (res.ok) {
await loadJob();
openStream();
} else {
const body = await res.json().catch(() => ({}));
alert(body.message ?? 'Retry failed');
}
}
</script>
<svelte:head>
@@ -143,11 +174,17 @@
{#if job.meanVolume != null}· {job.meanVolume.toFixed(1)} dBFS{/if}
</div>
</div>
<div class="job-actions">
{#if isActive}
<form method="POST" action="/api/jobs/{job.id}?_method=DELETE">
<button type="button" class="btn-cancel" aria-label="Cancel job">Cancel</button>
</form>
<button class="btn-job-action danger" onclick={cancelJob} aria-label="Cancel job">Cancel</button>
{/if}
{#if canRetry}
<button class="btn-job-action accent" onclick={retryJobAction} aria-label="Retry job">↺ Retry</button>
{/if}
{#if isTerminal}
<button class="btn-job-action danger" onclick={deleteJob} aria-label="Delete job">Delete</button>
{/if}
</div>
</div>
<!-- ── Progress block ────────────────────────────────── -->
@@ -377,20 +414,37 @@
text-overflow: ellipsis;
}
.btn-cancel {
.job-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.btn-job-action {
padding: 8px 14px;
border-radius: 8px;
border: 1px solid rgba(255, 90, 90, 0.3);
background: rgba(255, 90, 90, 0.08);
color: #ff8a8a;
font-size: 12.5px;
font-family: inherit;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s;
}
.btn-cancel:hover {
.btn-job-action.danger {
border: 1px solid rgba(255, 90, 90, 0.3);
background: rgba(255, 90, 90, 0.08);
color: #ff8a8a;
}
.btn-job-action.danger:hover {
background: rgba(255, 90, 90, 0.15);
}
.btn-job-action.accent {
border: 1px solid color-mix(in oklab, var(--accent) 40%, transparent);
background: color-mix(in oklab, var(--accent) 10%, transparent);
color: var(--accent);
}
.btn-job-action.accent:hover {
background: color-mix(in oklab, var(--accent) 18%, transparent);
}
/* ── Progress card ──────────────────────────────────────── */
.progress-card {