Parametres : telechargement des journaux serveur complets

Une extraction Qlik produit plusieurs milliers de lignes de diagnostic.
Un terminal les tronque et n'en laisse que la fin, alors que
l'information decisive — carte du modele Qlik, champs date retenus,
choix des periodes — se trouve en TETE. Diagnostiquer sur la fin du log
revient a travailler a l'aveugle.

- src/lib/log-capture.ts : instrumente console.log/warn/error une fois,
  recopie chaque ligne horodatee dans les captures ouvertes, ecrit au fil
  de l'eau dans le repertoire temporaire (un job qui fait tomber le
  process laisse quand meme son journal). Bornes : 200 000 lignes,
  8 000 caracteres par ligne, 20 fichiers conserves.
- GET /api/logs : liste JSON ; GET /api/logs?id=<jobId> : le fichier
  complet en piece jointe. Admin uniquement.
- La sync Qlik ouvre la capture AVANT son premier log et la ferme dans un
  finally, avec le statut final — les deux modes (fournisseur et produit).
- Parametres : section « Journaux serveur », un bouton Telecharger par
  job, le plus recent en premier, badge « en cours » pour un job vivant.

Aucun secret ne transite dans ces journaux : l'extracteur ne journalise
que le NOM du cookie de session, jamais sa valeur ni le mot de passe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfqUihgixw4K1AmJhizWiu
This commit is contained in:
Claude committed 2026-08-01 06:45:14 +00:00
1 parent 50f883ec98
commit d89d2b3232
5 files changed
+450

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
/**
* CollectFlow — Journaux serveur des jobs (liste + téléchargement).
*
* `GET /api/logs` → liste des journaux disponibles (JSON)
* `GET /api/logs?id=<jobId>` → le journal complet en pièce jointe (text/plain)
*
* Réservé aux admins : un journal d'extraction contient les diagnostics internes
* du modèle Qlik et les codes articles manipulés. Aucun secret n'y transite —
* l'extracteur ne journalise que le NOM du cookie de session, jamais sa valeur,
* et jamais le mot de passe Qlik.
*/
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { listCaptures, readCapture } from "@/lib/log-capture";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
async function requireAdmin(): Promise<NextResponse | null> {
const session = await auth();
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if ((session.user as { role?: string } | undefined)?.role !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
return null;
}
export async function GET(req: NextRequest) {
const refus = await requireAdmin();
if (refus) return refus;
const id = req.nextUrl.searchParams.get("id");
if (!id) {
return NextResponse.json({ success: true, logs: await listCaptures() });
}
const contenu = await readCapture(id);
if (contenu == null) {
return NextResponse.json({ error: "Journal introuvable ou expiré" }, { status: 404 });
}
// Nom de fichier assaini : l'id vient d'un job interne, mais il finit dans
// un en-tête HTTP.
const nom = `${id.replace(/[^A-Za-z0-9_.-]/g, "_")}.log`;
return new NextResponse(contenu, {
status: 200,
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": `attachment; filename="${nom}"`,
"Cache-Control": "no-store",
},
});
}
+10
View File
@@ -7,6 +7,7 @@ import {
buildGridNetworkQlikDateFilter,
QLIK_MONTHS_BACK_DEFAULT,
} from "@/lib/qlik-date-range";
import { startCapture, stopCapture } from "@/lib/log-capture";
// Tâche d'extraction Qlik potentiellement très longue (hypercube paginé).
// On accepte quand même 5 min côté plateforme Next.js, mais on rend la main au
@@ -215,6 +216,10 @@ function filterCentralCodes(rawCodes: Array<string | null | undefined>): {
* au fur et à mesure (compteur fetched, puis upserted, puis finishedAt).
*/
async function runJob(job: QlikSyncJob): Promise<void> {
// Journal téléchargeable depuis Paramètres. Ouvert AVANT le premier log du
// job pour ne pas perdre l'entête (choix des champs, carte du modèle Qlik) —
// c'est précisément la partie qu'un terminal tronque.
startCapture(job.jobId, `sync fournisseur=${job.fournisseur} démarrée le ${job.startedAt}`);
try {
const articles = await pgGetArticlesByFournisseur(job.fournisseur);
// Filtre strict des codes (trim, dedupe, exclusion vides/`-`/codes trop suspects).
@@ -255,6 +260,8 @@ async function runJob(job: QlikSyncJob): Promise<void> {
job.finishedAt = new Date().toISOString();
} catch (e) {
applyJobError(job, e);
} finally {
await stopCapture(job.jobId, `statut final=${job.status}${job.error ? ` — ${job.error}` : ""}`);
}
}
@@ -284,6 +291,7 @@ function applyJobError(job: QlikSyncJob, e: unknown): void {
* exactement le même extracteur, donc le même détail mensuel.
*/
async function runProductJob(job: QlikSyncJob): Promise<void> {
startCapture(job.jobId, `sync produit=${job.codeCentrale} démarrée le ${job.startedAt}`);
try {
const code = job.codeCentrale!;
job.requested = 1;
@@ -312,6 +320,8 @@ async function runProductJob(job: QlikSyncJob): Promise<void> {
console.log(`[api/qlik/sync] job=${job.jobId} produit ${code} — ${count} ligne(s) upsert`);
} catch (e) {
applyJobError(job, e);
} finally {
await stopCapture(job.jobId, `statut final=${job.status}${job.error ? ` — ${job.error}` : ""}`);
}
}