Files
stripstream-librarian/apps/backoffice/app/components/JobProgress.tsx
Froidefond Julien e64848a216 feat: implement thumbnail generation and management
- Remove unused image dependencies from Cargo.lock.
- Update API to handle thumbnail generation and checkup processes.
- Introduce new routes for rebuilding and regenerating thumbnails.
- Enhance job tracking with progress indicators for thumbnail jobs.
- Update front-end components to display thumbnail job status and progress.
- Add backend logic for managing thumbnail jobs and integrating with the API.
- Refactor existing code to accommodate new thumbnail functionalities.
2026-03-08 20:55:12 +01:00

128 lines
3.7 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { StatusBadge, Badge, ProgressBar } from "./ui";
interface ProgressEvent {
job_id: string;
status: string;
current_file: string | null;
progress_percent: number | null;
processed_files: number | null;
total_files: number | null;
stats_json: {
scanned_files: number;
indexed_files: number;
removed_files: number;
errors: number;
} | null;
}
interface JobProgressProps {
jobId: string;
onComplete?: () => void;
}
export function JobProgress({ jobId, onComplete }: JobProgressProps) {
const [progress, setProgress] = useState<ProgressEvent | null>(null);
const [error, setError] = useState<string | null>(null);
const [isComplete, setIsComplete] = useState(false);
useEffect(() => {
const eventSource = new EventSource(`/api/jobs/${jobId}/stream`);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
const progressData: ProgressEvent = {
job_id: data.id,
status: data.status,
current_file: data.current_file,
progress_percent: data.progress_percent,
processed_files: data.processed_files,
total_files: data.total_files,
stats_json: data.stats_json,
};
setProgress(progressData);
if (data.status === "success" || data.status === "failed" || data.status === "cancelled") {
setIsComplete(true);
eventSource.close();
onComplete?.();
}
} catch (err) {
setError("Failed to parse SSE data");
}
};
eventSource.onerror = (err) => {
console.error("SSE error:", err);
eventSource.close();
setError("Connection lost");
};
return () => {
eventSource.close();
};
}, [jobId, onComplete]);
if (error) {
return (
<div className="p-4 bg-destructive/10 text-error rounded-lg text-sm">
Error: {error}
</div>
);
}
if (!progress) {
return (
<div className="p-4 text-muted-foreground text-sm">
Loading progress...
</div>
);
}
const percent = progress.progress_percent ?? 0;
const processed = progress.processed_files ?? 0;
const total = progress.total_files ?? 0;
const isThumbnailsPhase = progress.status === "generating_thumbnails";
const unitLabel = isThumbnailsPhase ? "thumbnails" : "files";
return (
<div className="p-4 bg-card rounded-lg border border-border">
<div className="flex items-center justify-between mb-3">
<StatusBadge status={progress.status} />
{isComplete && (
<Badge variant="success">Complete</Badge>
)}
</div>
<ProgressBar value={percent} showLabel size="lg" className="mb-3" />
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground mb-3">
<span>{processed} / {total} {unitLabel}</span>
{progress.current_file && (
<span className="truncate max-w-md" title={progress.current_file}>
Current: {progress.current_file.length > 40
? progress.current_file.substring(0, 40) + "..."
: progress.current_file}
</span>
)}
</div>
{progress.stats_json && !isThumbnailsPhase && (
<div className="flex flex-wrap gap-3 text-xs">
<Badge variant="primary">Scanned: {progress.stats_json.scanned_files}</Badge>
<Badge variant="success">Indexed: {progress.stats_json.indexed_files}</Badge>
<Badge variant="warning">Removed: {progress.stats_json.removed_files}</Badge>
{progress.stats_json.errors > 0 && (
<Badge variant="error">Errors: {progress.stats_json.errors}</Badge>
)}
</div>
)}
</div>
);
}