From 4b229acc9b59841c240963ef6217e833b5986bce Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 10:21:31 +0000 Subject: [PATCH] =?UTF-8?q?feat(api):=20API=20pr=C3=AAte=20pour=20une=20IA?= =?UTF-8?q?=20externe=20type=20ChatGPT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675 --- src/app/(dashboard)/settings/page.tsx | 8 + src/app/api/admin/grid-warmup/route.ts | 144 +++++++++ src/app/api/v1/openapi.json/route.ts | 287 ++++++++++++------ .../admin/components/api-connection-info.tsx | 33 +- src/features/admin/components/grid-warmup.tsx | 148 +++++++++ 5 files changed, 529 insertions(+), 91 deletions(-) create mode 100644 src/app/api/admin/grid-warmup/route.ts create mode 100644 src/features/admin/components/grid-warmup.tsx diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx index 001ac14..5c2e17b 100644 --- a/src/app/(dashboard)/settings/page.tsx +++ b/src/app/(dashboard)/settings/page.tsx @@ -10,6 +10,7 @@ import { useEffect } from "react"; import { UserManagement } from "@/features/admin/components/user-management"; import { ApiKeyManagement } from "@/features/admin/components/api-key-management"; import { ApiConnectionInfo } from "@/features/admin/components/api-connection-info"; +import { GridWarmup } from "@/features/admin/components/grid-warmup"; interface OpenRouterModel { id: string; name: string; free: boolean; } @@ -633,6 +634,13 @@ export default function SettingsPage() { +
+ +
+ {/* Save Button — Floating/Sticky style at bottom */}
+ {/* Branchement d'une IA externe (ChatGPT) */} +
+

+ + Connecter une IA externe (ChatGPT) +

+
    +
  1. Créez une clé d'API dans la section ci-dessous et copiez-la.
  2. +
  3. Dans ChatGPT : Créer un GPT → onglet Configurer → Créer une action.
  4. +
  5. + Cliquez sur Importer depuis une URL et collez l'adresse du schéma : +
    {`${base}/openapi.json`}
    + + Ce schéma est public (il ne contient aucune donnée) pour que ChatGPT puisse l'importer. + Votre application doit être joignable depuis Internet. + +
  6. +
  7. + Dans Authentification, choisissez Clé d'API, type{" "} + Personnalisé, nom d'en-tête X-API-Key, + et collez votre clé. +
  8. +
  9. Testez avec une question du type « cherche les produits tapis » — le GPT appellera rechercherProduits.
  10. +
+

+ Si votre domaine public diffère de celui affiché ici, renseignez la variable + d'environnement COLLECTFLOW_PUBLIC_URL : elle fixe l'URL + déclarée dans le schéma. +

+
+ {/* Comportement à connaître */}
diff --git a/src/features/admin/components/grid-warmup.tsx b/src/features/admin/components/grid-warmup.tsx new file mode 100644 index 0000000..861982e --- /dev/null +++ b/src/features/admin/components/grid-warmup.tsx @@ -0,0 +1,148 @@ +"use client"; + +/** + * CollectFlow — Préchauffage des données exposées par l'API. + * + * L'API `/api/v1` ne calcule jamais : elle lit un instantané écrit quand la Grille est + * ouverte dans l'application. Sans ce préchauffage, une IA externe ne verrait que les + * fournisseurs déjà consultés à la main — d'où ce bouton, qui calcule tout le catalogue + * une bonne fois. + */ + +import { useCallback, useEffect, useRef, useState } from "react"; +import { Loader2, Play, CheckCircle, AlertTriangle, Database } from "lucide-react"; + +interface WarmupState { + status: "idle" | "running" | "success" | "error"; + total: number; + done: number; + skipped: number; + failed: number; + currentFournisseur?: string; + finishedAt?: string; + error?: string; + lastErrors: string[]; +} + +export function GridWarmup() { + const [state, setState] = useState(null); + const [error, setError] = useState(null); + const pollRef = useRef | null>(null); + + const running = state?.status === "running"; + + const fetchState = useCallback(async () => { + try { + const res = await fetch("/api/admin/grid-warmup"); + if (res.ok) setState(await res.json()); + } catch { + // Réseau instable : on retentera au tick suivant. + } + }, []); + + // Récupère l'état au montage : un préchauffage peut déjà tourner. + // eslint-disable-next-line react-hooks/set-state-in-effect + useEffect(() => { fetchState(); }, [fetchState]); + + // Suivi de l'avancement tant que le job tourne. + useEffect(() => { + if (!running) return; + let cancelled = false; + const tick = async () => { + await fetchState(); + if (!cancelled) pollRef.current = setTimeout(tick, 2000); + }; + pollRef.current = setTimeout(tick, 2000); + return () => { cancelled = true; if (pollRef.current) clearTimeout(pollRef.current); }; + }, [running, fetchState]); + + const start = async () => { + setError(null); + try { + const res = await fetch("/api/admin/grid-warmup?staleHours=24", { method: "POST" }); + const data = await res.json(); + if (!res.ok) { setError(data.error ?? `Erreur ${res.status}`); return; } + setState(data); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } + }; + + const traites = (state?.done ?? 0) + (state?.skipped ?? 0) + (state?.failed ?? 0); + const pct = state && state.total > 0 ? Math.round((traites / state.total) * 100) : 0; + + return ( +
+

+ L'API ne recalcule jamais : elle lit un instantané écrit quand la Grille est ouverte dans + l'application. Sans préchauffage, une IA externe ne verrait que les fournisseurs déjà consultés. + Ce bouton calcule tous les fournisseurs ; ceux déjà à jour depuis moins de 24 h sont + sautés, il est donc sans risque de le relancer. +

+ +
+ + {state && state.total > 0 && ( + + {traites} / {state.total} ({pct} %) + + )} +
+ + {running && state && ( +
+
+
+
+ {state.currentFournisseur && ( +

+ En cours : {state.currentFournisseur} +

+ )} +
+ )} + + {state?.status === "success" && ( +
+ + + Terminé : {state.done} fournisseur(s) calculé(s),{" "} + {state.skipped} déjà à jour + {state.failed > 0 && <>, {state.failed} en échec}. + +
+ )} + + {state && state.failed > 0 && state.lastErrors.length > 0 && ( +
+ Voir les échecs ({state.lastErrors.length} premiers) +
    + {state.lastErrors.map((e, i) =>
  • {e}
  • )} +
+
+ )} + + {(error || state?.error) && ( +
+ + {error ?? state?.error} +
+ )} + + {state?.status === "idle" && ( +

+ + Aucun préchauffage lancé depuis le démarrage de l'application. +

+ )} +
+ ); +}