mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-14 18:58:17 +02:00
feat(api): API prête pour une IA externe type ChatGPT
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
This commit is contained in:
5 files changed
+529
-91
No files matched your search
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Copy, Check, ExternalLink } from "lucide-react";
|
||||
import { Copy, Check, ExternalLink, Bot } from "lucide-react";
|
||||
|
||||
function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -123,6 +123,37 @@ export function ApiConnectionInfo() {
|
||||
<CodeLine>{`curl -H "X-API-Key: VOTRE_CLE" \\\n "${base}/grid?fournisseur=FOU001&fields=codein,libelle1,totalCa,codeGammeServeur"`}</CodeLine>
|
||||
</div>
|
||||
|
||||
{/* Branchement d'une IA externe (ChatGPT) */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-[12px] font-semibold flex items-center gap-1.5" style={{ color: "var(--text-primary)" }}>
|
||||
<Bot className="w-4 h-4" style={{ color: "var(--accent)" }} />
|
||||
Connecter une IA externe (ChatGPT)
|
||||
</p>
|
||||
<ol className="text-[11px] space-y-1.5 list-decimal pl-4" style={{ color: "var(--text-secondary)" }}>
|
||||
<li>Créez une clé d'API dans la section ci-dessous et copiez-la.</li>
|
||||
<li>Dans ChatGPT : <strong>Créer un GPT</strong> → onglet <strong>Configurer</strong> → <strong>Créer une action</strong>.</li>
|
||||
<li>
|
||||
Cliquez sur <strong>Importer depuis une URL</strong> et collez l'adresse du schéma :
|
||||
<div className="mt-1"><CodeLine>{`${base}/openapi.json`}</CodeLine></div>
|
||||
<span style={{ color: "var(--text-muted)" }}>
|
||||
Ce schéma est public (il ne contient aucune donnée) pour que ChatGPT puisse l'importer.
|
||||
Votre application doit être joignable depuis Internet.
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
Dans <strong>Authentification</strong>, choisissez <strong>Clé d'API</strong>, type{" "}
|
||||
<strong>Personnalisé</strong>, nom d'en-tête <code className="font-mono">X-API-Key</code>,
|
||||
et collez votre clé.
|
||||
</li>
|
||||
<li>Testez avec une question du type « cherche les produits tapis » — le GPT appellera <code className="font-mono">rechercherProduits</code>.</li>
|
||||
</ol>
|
||||
<p className="text-[11px]" style={{ color: "var(--text-muted)" }}>
|
||||
Si votre domaine public diffère de celui affiché ici, renseignez la variable
|
||||
d'environnement <code className="font-mono">COLLECTFLOW_PUBLIC_URL</code> : elle fixe l'URL
|
||||
déclarée dans le schéma.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Comportement à connaître */}
|
||||
<div className="rounded-xl p-3 text-[11px] space-y-1.5"
|
||||
style={{ background: "var(--bg-elevated)", border: "1px solid var(--border)", color: "var(--text-secondary)" }}>
|
||||
|
||||
@@ -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<WarmupState | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const pollRef = useRef<ReturnType<typeof setTimeout> | 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 (
|
||||
<div className="space-y-3">
|
||||
<p className="text-[12px]" style={{ color: "var(--text-secondary)" }}>
|
||||
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 <strong>tous les fournisseurs</strong> ; ceux déjà à jour depuis moins de 24 h sont
|
||||
sautés, il est donc sans risque de le relancer.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2.5">
|
||||
<button
|
||||
onClick={start}
|
||||
disabled={running}
|
||||
className="rounded-lg px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-50 flex items-center gap-1.5"
|
||||
style={{ background: "var(--accent)" }}
|
||||
>
|
||||
{running ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
|
||||
{running ? "Préparation en cours…" : "Préparer les données pour l'API"}
|
||||
</button>
|
||||
{state && state.total > 0 && (
|
||||
<span className="text-[12px] tabular-nums" style={{ color: "var(--text-secondary)" }}>
|
||||
{traites} / {state.total} ({pct} %)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{running && state && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="h-1.5 rounded-full overflow-hidden" style={{ background: "var(--bg-elevated)" }}>
|
||||
<div className="h-full transition-all" style={{ width: `${pct}%`, background: "var(--accent)" }} />
|
||||
</div>
|
||||
{state.currentFournisseur && (
|
||||
<p className="text-[11px] truncate" style={{ color: "var(--text-muted)" }}>
|
||||
En cours : {state.currentFournisseur}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state?.status === "success" && (
|
||||
<div className="flex items-start gap-2 text-[12px]" style={{ color: "var(--text-secondary)" }}>
|
||||
<CheckCircle className="w-4 h-4 shrink-0 mt-px text-emerald-500" />
|
||||
<span>
|
||||
Terminé : <strong>{state.done}</strong> fournisseur(s) calculé(s),{" "}
|
||||
<strong>{state.skipped}</strong> déjà à jour
|
||||
{state.failed > 0 && <>, <strong style={{ color: "var(--accent-error)" }}>{state.failed} en échec</strong></>}.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state && state.failed > 0 && state.lastErrors.length > 0 && (
|
||||
<details className="text-[11px]" style={{ color: "var(--text-muted)" }}>
|
||||
<summary className="cursor-pointer">Voir les échecs ({state.lastErrors.length} premiers)</summary>
|
||||
<ul className="mt-1 pl-4 list-disc space-y-0.5">
|
||||
{state.lastErrors.map((e, i) => <li key={i} className="break-all">{e}</li>)}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{(error || state?.error) && (
|
||||
<div className="flex items-start gap-2 text-[12px]" style={{ color: "var(--accent-error)" }}>
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-px" />
|
||||
<span>{error ?? state?.error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{state?.status === "idle" && (
|
||||
<p className="text-[11px] flex items-center gap-1.5" style={{ color: "var(--text-muted)" }}>
|
||||
<Database className="w-3.5 h-3.5" />
|
||||
Aucun préchauffage lancé depuis le démarrage de l'application.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user