feat: add replay button on completed jobs in the jobs table

Shows a "Replay" button on non-active jobs that re-creates a new job
of the same type and library. Supports all replayable job types:
rebuild, full_rebuild, rescan, scan, thumbnail_rebuild,
thumbnail_regenerate, metadata_batch, metadata_refresh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 21:18:12 +01:00
parent 2febab2c39
commit 6a838fb840
5 changed files with 66 additions and 2 deletions

View File

@@ -30,11 +30,14 @@ interface JobRowProps {
libraryName: string | undefined;
highlighted?: boolean;
onCancel: (id: string) => void;
onReplay: (id: string) => void;
formatDate: (date: string) => string;
formatDuration: (start: string, end: string | null) => string;
}
export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, formatDuration }: JobRowProps) {
const REPLAYABLE_TYPES = new Set(["rebuild", "full_rebuild", "rescan", "scan", "thumbnail_rebuild", "thumbnail_regenerate", "metadata_batch", "metadata_refresh"]);
export function JobRow({ job, libraryName, highlighted, onCancel, onReplay, formatDate, formatDuration }: JobRowProps) {
const { t } = useTranslation();
const isActive = job.status === "running" || job.status === "pending" || job.status === "extracting_pages" || job.status === "generating_thumbnails";
const [showProgress, setShowProgress] = useState(highlighted || isActive);
@@ -213,7 +216,7 @@ export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, fo
</svg>
{t("jobRow.view")}
</Link>
{(job.status === "pending" || job.status === "running" || job.status === "extracting_pages" || job.status === "generating_thumbnails") && (
{isActive && (
<Button
variant="danger"
size="xs"
@@ -225,6 +228,18 @@ export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, fo
{t("common.cancel")}
</Button>
)}
{!isActive && REPLAYABLE_TYPES.has(job.type) && (
<Button
variant="secondary"
size="xs"
onClick={() => onReplay(job.id)}
>
<svg className="w-3.5 h-3.5 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{t("jobRow.replay")}
</Button>
)}
</div>
</td>
</tr>

View File

@@ -95,6 +95,14 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro
}
};
const handleReplay = async (id: string) => {
const response = await fetch(`/api/jobs/${id}/replay`, { method: "POST" });
if (!response.ok) {
const data = await response.json().catch(() => ({}));
console.error("Failed to replay job:", data?.error ?? response.status);
}
};
return (
<div className="bg-card rounded-xl shadow-sm border border-border/60 overflow-hidden">
<div className="overflow-x-auto">
@@ -119,6 +127,7 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro
libraryName={job.library_id ? libraries.get(job.library_id) : undefined}
highlighted={job.id === highlightJobId}
onCancel={handleCancel}
onReplay={handleReplay}
formatDate={formatDate}
formatDuration={formatDuration}
/>