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. +

+ )} +
+ ); +}