feat: add replay button for download detection jobs and color-coded job type badges

Add download_detection to replayable job types and replay route handler.
Give each job type a unique colored background badge for better visual
distinction in the jobs table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 06:27:04 +01:00
parent 0460ea7c1f
commit d81d941a34
3 changed files with 24 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { apiFetch, IndexJobDto, rebuildIndex, rebuildThumbnails, regenerateThumbnails, startMetadataBatch, startMetadataRefresh, startReadingStatusMatch, startReadingStatusPush } from "@/lib/api";
import { apiFetch, IndexJobDto, rebuildIndex, rebuildThumbnails, regenerateThumbnails, startMetadataBatch, startMetadataRefresh, startReadingStatusMatch, startReadingStatusPush, startDownloadDetection } from "@/lib/api";
export async function POST(
_request: NextRequest,
@@ -35,6 +35,9 @@ export async function POST(
case "reading_status_push":
if (!libraryId) return NextResponse.json({ error: "Library ID required for reading status push" }, { status: 400 });
return NextResponse.json(await startReadingStatusPush(libraryId));
case "download_detection":
if (!libraryId) return NextResponse.json({ error: "Library ID required for download detection" }, { status: 400 });
return NextResponse.json(await startDownloadDetection(libraryId));
default:
return NextResponse.json({ error: `Cannot replay job type: ${job.type}` }, { status: 400 });
}

View File

@@ -38,7 +38,7 @@ interface JobRowProps {
formatDuration: (start: string, end: string | null) => string;
}
const REPLAYABLE_TYPES = new Set(["rebuild", "full_rebuild", "rescan", "scan", "thumbnail_rebuild", "thumbnail_regenerate", "metadata_batch", "metadata_refresh", "reading_status_match", "reading_status_push"]);
const REPLAYABLE_TYPES = new Set(["rebuild", "full_rebuild", "rescan", "scan", "thumbnail_rebuild", "thumbnail_regenerate", "metadata_batch", "metadata_refresh", "reading_status_match", "reading_status_push", "download_detection"]);
export function JobRow({ job, libraryName, highlighted, onCancel, onReplay, formatDate, formatDuration }: JobRowProps) {
const { t } = useTranslation();

View File

@@ -91,12 +91,18 @@ export function StatusBadge({ status, className = "" }: StatusBadgeProps) {
}
// Job type badge
const jobTypeVariants: Record<string, BadgeVariant> = {
rebuild: "primary",
rescan: "primary",
full_rebuild: "warning",
thumbnail_rebuild: "secondary",
thumbnail_regenerate: "warning",
const jobTypeStyles: Record<string, string> = {
rebuild: "bg-blue-500/80 text-white",
rescan: "bg-sky-500/80 text-white",
full_rebuild: "bg-orange-500/80 text-white",
thumbnail_rebuild: "bg-violet-500/80 text-white",
thumbnail_regenerate: "bg-purple-500/80 text-white",
cbr_to_cbz: "bg-rose-500/80 text-white",
metadata_batch: "bg-teal-500/80 text-white",
metadata_refresh: "bg-emerald-500/80 text-white",
reading_status_match: "bg-amber-500/80 text-white",
reading_status_push: "bg-yellow-500/80 text-white",
download_detection: "bg-indigo-500/80 text-white",
};
interface JobTypeBadgeProps {
@@ -107,7 +113,7 @@ interface JobTypeBadgeProps {
export function JobTypeBadge({ type, className = "" }: JobTypeBadgeProps) {
const { t } = useTranslation();
const key = type.toLowerCase();
const variant = jobTypeVariants[key] || "default";
const style = jobTypeStyles[key] || "bg-muted/60 text-muted-foreground";
const jobTypeLabels: Record<string, string> = {
rebuild: t("jobType.rebuild"),
rescan: t("jobType.rescan"),
@@ -119,9 +125,14 @@ export function JobTypeBadge({ type, className = "" }: JobTypeBadgeProps) {
metadata_refresh: t("jobType.metadata_refresh"),
reading_status_match: t("jobType.reading_status_match"),
reading_status_push: t("jobType.reading_status_push"),
download_detection: t("jobType.download_detection"),
};
const label = jobTypeLabels[key] ?? type;
return <Badge variant={variant} className={className}>{label}</Badge>;
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${style} ${className}`}>
{label}
</span>
);
}
// Progress badge (shows percentage)