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:
87
apps/backoffice/app/api/jobs/[id]/stream/route.ts
Normal file
87
apps/backoffice/app/api/jobs/[id]/stream/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params;
|
||||
const apiBaseUrl = process.env.API_BASE_URL || "http://api:8080";
|
||||
const apiToken = process.env.API_BOOTSTRAP_TOKEN;
|
||||
|
||||
if (!apiToken) {
|
||||
return new Response(
|
||||
`data: ${JSON.stringify({ error: "API token not configured" })}\n\n`,
|
||||
{ status: 500, headers: { "Content-Type": "text/event-stream" } }
|
||||
);
|
||||
}
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
// Send initial headers for SSE
|
||||
controller.enqueue(new TextEncoder().encode(""));
|
||||
|
||||
let lastData: string | null = null;
|
||||
let isActive = true;
|
||||
|
||||
const fetchJob = async () => {
|
||||
if (!isActive) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/index/jobs/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const dataStr = JSON.stringify(data);
|
||||
|
||||
// Only send if data changed
|
||||
if (dataStr !== lastData) {
|
||||
lastData = dataStr;
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(`data: ${dataStr}\n\n`)
|
||||
);
|
||||
|
||||
// Stop polling if job is complete
|
||||
if (data.status === "success" || data.status === "failed" || data.status === "cancelled") {
|
||||
isActive = false;
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("SSE fetch error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial fetch
|
||||
await fetchJob();
|
||||
|
||||
// Poll every 500ms while job is active
|
||||
const interval = setInterval(async () => {
|
||||
if (!isActive) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
await fetchJob();
|
||||
}, 500);
|
||||
|
||||
// Cleanup on abort
|
||||
request.signal.addEventListener("abort", () => {
|
||||
isActive = false;
|
||||
clearInterval(interval);
|
||||
controller.close();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user