mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
Adapte l'API CollectFlow aux exigences des Actions ChatGPT et garantit que les données soient réellement disponibles pour un consommateur externe. Schéma OpenAPI : - openapi.json devient PUBLIC (sans clé). Il ne décrit que la structure, sans aucune donnée, or ChatGPT importe le schéma par URL avant que la clé ne soit configurée : l'exiger rendait l'import impossible. - servers[0].url est désormais ABSOLUE (une URL relative est rejetée à l'import), construite depuis l'hôte appelant ou COLLECTFLOW_PUBLIC_URL. - operationId sur chaque opération (requis par les Actions), schémas de réponse typés, et descriptions rédigées pour le modèle : quelle gamme utiliser pour raisonner, pourquoi préférer caParMagasinReseau au CA brut, que faire d'un 202 not_ready. Disponibilité des données : - Nouveau préchauffage /api/admin/grid-warmup + bouton dans Paramètres. Sans lui, l'API ne sert que les fournisseurs déjà ouverts à la main dans la Grille — une IA externe n'aurait presque rien vu. Le job calcule tous les fournisseurs séquentiellement (paralléliser saturerait PostgreSQL), saute ceux à jour depuis moins de 24 h et suit son avancement. Documentation : - Paramètres → marche à suivre pas à pas pour brancher un GPT (import du schéma, auth par clé personnalisée X-API-Key), et mention de COLLECTFLOW_PUBLIC_URL quand le domaine public diffère. L'assistant interne et son API api.ffnancy.fr ne sont pas touchés. Vérifié sur PostgreSQL local : schéma servi sans clé en 200, URL absolue, 5 operationId, auth apiKey/X-API-Key, surcharge COLLECTFLOW_PUBLIC_URL effective, 5 endpoints en 200 avec la clé, 401 JSON sans clé, et recherche transversale renvoyant bien plusieurs fournisseurs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { auth } from "@/lib/auth";
|
|
import { pgGetFournisseurs } from "@/lib/pg-ff-client";
|
|
import { getProductRows } from "@/features/grid/api/get-product-rows";
|
|
import { listGridSuppliers } from "@/lib/grid-store";
|
|
|
|
// Le préchauffage parcourt tous les fournisseurs : c'est long par nature. On rend la
|
|
// main immédiatement et le client interroge GET pour suivre l'avancement.
|
|
export const maxDuration = 300;
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
/**
|
|
* Préchauffage de l'instantané de grille (`grid_rows`).
|
|
*
|
|
* L'API `/api/v1` ne calcule jamais : elle lit un instantané écrit quand la Grille est
|
|
* ouverte dans l'application. Sans préchauffage, un consommateur externe (ChatGPT) ne
|
|
* verrait donc que les fournisseurs déjà consultés à la main.
|
|
*
|
|
* Ce job calcule chaque fournisseur une fois, séquentiellement — la persistance se fait
|
|
* en effet de bord de `getProductRows()`. Séquentiel volontairement : le calcul est
|
|
* lourd en SQL, le paralléliser saturerait PostgreSQL.
|
|
*/
|
|
type WarmupStatus = "idle" | "running" | "success" | "error";
|
|
|
|
interface WarmupJob {
|
|
status: WarmupStatus;
|
|
total: number;
|
|
done: number;
|
|
skipped: number;
|
|
failed: number;
|
|
currentFournisseur?: string;
|
|
startedAt: string;
|
|
finishedAt?: string;
|
|
error?: string;
|
|
lastErrors: string[];
|
|
}
|
|
|
|
let job: WarmupJob | null = null;
|
|
|
|
function publicJob(j: WarmupJob | null) {
|
|
if (!j) {
|
|
return { status: "idle" as const, total: 0, done: 0, skipped: 0, failed: 0, lastErrors: [] };
|
|
}
|
|
return j;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
async function runWarmup(j: WarmupJob, staleHours: number): Promise<void> {
|
|
try {
|
|
const fournisseurs = await pgGetFournisseurs();
|
|
// Les fournisseurs déjà calculés récemment sont sautés : un second passage
|
|
// coûte alors quasiment rien, ce qui rend le bouton rejouable sans crainte.
|
|
const existing = await listGridSuppliers();
|
|
const freshLimit = Date.now() - staleHours * 3600_000;
|
|
const fresh = new Set(
|
|
existing
|
|
.filter((s) => s.computedAt && new Date(s.computedAt).getTime() > freshLimit)
|
|
.map((s) => s.codeFournisseur),
|
|
);
|
|
|
|
j.total = fournisseurs.length;
|
|
console.log(`[grid-warmup] ${fournisseurs.length} fournisseurs, ${fresh.size} déjà à jour (< ${staleHours}h)`);
|
|
|
|
for (const f of fournisseurs) {
|
|
if (fresh.has(f.code)) {
|
|
j.skipped++;
|
|
continue;
|
|
}
|
|
j.currentFournisseur = `${f.code} — ${f.nom}`;
|
|
try {
|
|
await getProductRows({ codeFournisseur: f.code, magasin: "TOTAL", forceRefresh: true });
|
|
j.done++;
|
|
} catch (e) {
|
|
j.failed++;
|
|
const msg = `${f.code}: ${e instanceof Error ? e.message.slice(0, 120) : String(e)}`;
|
|
// On garde une trace des derniers échecs sans faire exploser la mémoire.
|
|
if (j.lastErrors.length < 10) j.lastErrors.push(msg);
|
|
console.error("[grid-warmup]", msg);
|
|
}
|
|
}
|
|
|
|
j.currentFournisseur = undefined;
|
|
j.status = "success";
|
|
j.finishedAt = new Date().toISOString();
|
|
console.log(`[grid-warmup] terminé — ${j.done} calculés, ${j.skipped} à jour, ${j.failed} en échec`);
|
|
} catch (e) {
|
|
j.status = "error";
|
|
j.error = e instanceof Error ? e.message : String(e);
|
|
j.finishedAt = new Date().toISOString();
|
|
console.error("[grid-warmup] échec global:", j.error);
|
|
}
|
|
}
|
|
|
|
/** GET /api/admin/grid-warmup — avancement du préchauffage. */
|
|
export async function GET() {
|
|
const denied = await requireAdmin();
|
|
if (denied) return denied;
|
|
return NextResponse.json({ success: true, ...publicJob(job) });
|
|
}
|
|
|
|
/**
|
|
* POST /api/admin/grid-warmup?staleHours=24
|
|
* Démarre le préchauffage en arrière-plan et répond immédiatement.
|
|
*/
|
|
export async function POST(req: NextRequest) {
|
|
const denied = await requireAdmin();
|
|
if (denied) return denied;
|
|
|
|
if (job?.status === "running") {
|
|
return NextResponse.json({ success: true, ...publicJob(job) });
|
|
}
|
|
|
|
const raw = Number(req.nextUrl.searchParams.get("staleHours"));
|
|
const staleHours = Number.isFinite(raw) ? Math.min(720, Math.max(0, raw)) : 24;
|
|
|
|
job = {
|
|
status: "running",
|
|
total: 0,
|
|
done: 0,
|
|
skipped: 0,
|
|
failed: 0,
|
|
startedAt: new Date().toISOString(),
|
|
lastErrors: [],
|
|
};
|
|
const current = job;
|
|
console.log(`[grid-warmup] démarré (staleHours=${staleHours})`);
|
|
|
|
void runWarmup(current, staleHours).catch((e) => {
|
|
current.status = "error";
|
|
current.error = e instanceof Error ? e.message : String(e);
|
|
current.finishedAt = new Date().toISOString();
|
|
});
|
|
|
|
return NextResponse.json({ success: true, ...publicJob(job) });
|
|
}
|