Files
stripstream-librarian/apps/backoffice/app/components/JobProgress.tsx
Froidefond Julien 5f51955f4d feat(indexing): Lot 4 - Progression temps reel, Full Rebuild, Optimisations
- Ajout migrations DB: index_job_errors, library_monitoring, full_rebuild_type
- API: endpoints progression temps reel (/jobs/:id/stream), active jobs, details
- API: support full_rebuild avec suppression donnees existantes
- Indexer: logs detailles avec timing [SCAN][META][PARSER][BDD]
- Indexer: optimisation parsing PDF (lopdf -> pdfinfo) 235x plus rapide
- Indexer: corrections chemins LIBRARIES_ROOT_PATH pour dev local
- Backoffice: composants JobProgress, JobsIndicator (header), JobsList
- Backoffice: SSE streaming pour progression temps reel
- Backoffice: boutons Index/Index Full sur page libraries
- Backoffice: highlight job apres creation avec redirection
- Fix: parsing volume type i32, sync meilisearch cleanup

Perf: parsing PDF passe de 8.7s a 37ms
Perf: indexation 45 fichiers en ~15s vs plusieurs minutes avant
2026-03-06 11:33:32 +01:00

124 lines
3.5 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
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(() => {
// Use SSE via local proxy
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="progress-error">Error: {error}</div>;
}
if (!progress) {
return <div className="progress-loading">Loading progress...</div>;
}
const percent = progress.progress_percent ?? 0;
const processed = progress.processed_files ?? 0;
const total = progress.total_files ?? 0;
return (
<div className="job-progress">
<div className="progress-header">
<span className={`status-badge status-${progress.status}`}>
{progress.status}
</span>
{isComplete && <span className="complete-badge">Complete</span>}
</div>
<div className="progress-bar-container">
<div
className="progress-bar-fill"
style={{ width: `${percent}%` }}
/>
<span className="progress-percent">{percent}%</span>
</div>
<div className="progress-stats">
<span>{processed} / {total} files</span>
{progress.current_file && (
<span className="current-file" title={progress.current_file}>
Current: {progress.current_file.length > 40
? progress.current_file.substring(0, 40) + "..."
: progress.current_file}
</span>
)}
</div>
{progress.stats_json && (
<div className="progress-detailed-stats">
<span>Scanned: {progress.stats_json.scanned_files}</span>
<span>Indexed: {progress.stats_json.indexed_files}</span>
<span>Removed: {progress.stats_json.removed_files}</span>
{progress.stats_json.errors > 0 && (
<span className="error-count">Errors: {progress.stats_json.errors}</span>
)}
</div>
)}
</div>
);
}