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:
2026-03-06 11:33:32 +01:00
parent 82294a1bee
commit 5f51955f4d
29 changed files with 1928 additions and 68 deletions

View File

@@ -1,9 +1,12 @@
import { revalidatePath } from "next/cache";
import { listJobs, fetchLibraries, rebuildIndex, cancelJob, IndexJobDto, LibraryDto } from "../../lib/api";
import { redirect } from "next/navigation";
import { listJobs, fetchLibraries, rebuildIndex, IndexJobDto, LibraryDto } from "../../lib/api";
import { JobsList } from "../components/JobsList";
export const dynamic = "force-dynamic";
export default async function JobsPage() {
export default async function JobsPage({ searchParams }: { searchParams: Promise<{ highlight?: string }> }) {
const { highlight } = await searchParams;
const [jobs, libraries] = await Promise.all([
listJobs().catch(() => [] as IndexJobDto[]),
fetchLibraries().catch(() => [] as LibraryDto[])
@@ -14,17 +17,22 @@ export default async function JobsPage() {
async function triggerRebuild(formData: FormData) {
"use server";
const libraryId = formData.get("library_id") as string;
await rebuildIndex(libraryId || undefined);
const result = await rebuildIndex(libraryId || undefined);
revalidatePath("/jobs");
redirect(`/jobs?highlight=${result.id}`);
}
async function cancelJobAction(formData: FormData) {
async function triggerFullRebuild(formData: FormData) {
"use server";
const id = formData.get("id") as string;
await cancelJob(id);
const libraryId = formData.get("library_id") as string;
const result = await rebuildIndex(libraryId || undefined, true);
revalidatePath("/jobs");
redirect(`/jobs?highlight=${result.id}`);
}
const apiBaseUrl = process.env.API_BASE_URL || "http://api:8080";
const apiToken = process.env.API_BOOTSTRAP_TOKEN || "";
return (
<>
<h1>Index Jobs</h1>
@@ -40,43 +48,23 @@ export default async function JobsPage() {
</select>
<button type="submit">Queue Rebuild</button>
</form>
<form action={triggerFullRebuild} style={{ marginTop: '12px' }}>
<select name="library_id" defaultValue="">
<option value="">All libraries</option>
{libraries.map((lib) => (
<option key={lib.id} value={lib.id}>
{lib.name}
</option>
))}
</select>
<button type="submit" className="full-rebuild-btn">Full Rebuild (Reindex All)</button>
</form>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Library</th>
<th>Type</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{jobs.map((job) => (
<tr key={job.id}>
<td>
<code>{job.id.slice(0, 8)}</code>
</td>
<td>{job.library_id ? libraryMap.get(job.library_id) || job.library_id.slice(0, 8) : "—"}</td>
<td>{job.type}</td>
<td>
<span className={`status-${job.status}`}>{job.status}</span>
{job.error_opt && <span className="error-hint" title={job.error_opt}>!</span>}
</td>
<td>{new Date(job.created_at).toLocaleString()}</td>
<td>
{job.status === "pending" || job.status === "running" ? (
<form action={cancelJobAction}>
<input type="hidden" name="id" value={job.id} />
<button type="submit" className="cancel-btn">Cancel</button>
</form>
) : null}
</td>
</tr>
))}
</tbody>
</table>
<JobsList
initialJobs={jobs}
libraries={libraryMap}
highlightJobId={highlight}
/>
</>
);
}