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
This commit is contained in:
144
apps/backoffice/app/components/JobsIndicator.tsx
Normal file
144
apps/backoffice/app/components/JobsIndicator.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Job {
|
||||
id: string;
|
||||
status: string;
|
||||
current_file: string | null;
|
||||
progress_percent: number | null;
|
||||
}
|
||||
|
||||
interface JobsIndicatorProps {
|
||||
apiBaseUrl: string;
|
||||
apiToken: string;
|
||||
}
|
||||
|
||||
export function JobsIndicator({ apiBaseUrl, apiToken }: JobsIndicatorProps) {
|
||||
const [activeJobs, setActiveJobs] = useState<Job[]>([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchActiveJobs = async () => {
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/index/jobs/active`, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${apiToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const jobs = await response.json();
|
||||
// Enrich with details for running jobs
|
||||
const jobsWithDetails = await Promise.all(
|
||||
jobs.map(async (job: Job) => {
|
||||
if (job.status === "running") {
|
||||
try {
|
||||
const detailRes = await fetch(`${apiBaseUrl}/index/jobs/${job.id}`, {
|
||||
headers: { "Authorization": `Bearer ${apiToken}` },
|
||||
});
|
||||
if (detailRes.ok) {
|
||||
const detail = await detailRes.json();
|
||||
return { ...job, ...detail };
|
||||
}
|
||||
} catch {
|
||||
// ignore detail fetch errors
|
||||
}
|
||||
}
|
||||
return job;
|
||||
})
|
||||
);
|
||||
setActiveJobs(jobsWithDetails);
|
||||
}
|
||||
} catch {
|
||||
// Silently fail
|
||||
}
|
||||
};
|
||||
|
||||
fetchActiveJobs();
|
||||
const interval = setInterval(fetchActiveJobs, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [apiBaseUrl, apiToken]);
|
||||
|
||||
const pendingCount = activeJobs.filter(j => j.status === "pending").length;
|
||||
const runningCount = activeJobs.filter(j => j.status === "running").length;
|
||||
const totalCount = activeJobs.length;
|
||||
|
||||
if (totalCount === 0) {
|
||||
return (
|
||||
<Link href="/jobs" className="jobs-indicator empty">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
<path d="M8 12h8M12 8v8" />
|
||||
</svg>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="jobs-indicator-container">
|
||||
<button
|
||||
className="jobs-indicator active"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
title={`${totalCount} active job${totalCount !== 1 ? 's' : ''}`}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" />
|
||||
<path d="M8 12h8M12 8v8" />
|
||||
</svg>
|
||||
{totalCount > 0 && (
|
||||
<span className="jobs-badge">
|
||||
{totalCount > 9 ? "9+" : totalCount}
|
||||
</span>
|
||||
)}
|
||||
{runningCount > 0 && (
|
||||
<span className="jobs-pulse" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="jobs-dropdown">
|
||||
<div className="jobs-dropdown-header">
|
||||
<strong>Active Jobs</strong>
|
||||
<Link href="/jobs" onClick={() => setIsOpen(false)}>View all</Link>
|
||||
</div>
|
||||
|
||||
{activeJobs.length === 0 ? (
|
||||
<p className="jobs-empty">No active jobs</p>
|
||||
) : (
|
||||
<ul className="jobs-list">
|
||||
{activeJobs.map(job => (
|
||||
<li key={job.id} className={`job-item job-${job.status}`}>
|
||||
<div className="job-header">
|
||||
<span className={`job-status status-${job.status}`}>
|
||||
{job.status}
|
||||
</span>
|
||||
<code className="job-id">{job.id.slice(0, 8)}</code>
|
||||
</div>
|
||||
{job.status === "running" && job.progress_percent !== null && (
|
||||
<div className="job-mini-progress">
|
||||
<div
|
||||
className="job-progress-bar"
|
||||
style={{ width: `${job.progress_percent}%` }}
|
||||
/>
|
||||
<span>{job.progress_percent}%</span>
|
||||
</div>
|
||||
)}
|
||||
{job.current_file && (
|
||||
<p className="job-file" title={job.current_file}>
|
||||
{job.current_file.length > 30
|
||||
? job.current_file.substring(0, 30) + "..."
|
||||
: job.current_file}
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user