"use client"; // Vue générale : indicateurs clés, coût par jour (14 jours), modèles les plus // utilisés (30 jours), état de la dernière ingestion. import { Activity } from "lucide-react"; import { PageHeader } from "@/components/app-shell"; import { Badge, Card, EmptyState, ProgressBar, Skeleton } from "@/components/ui"; import { BarChart, ErrorBanner, SectionTitle, StatTile, fmtDate, fmtInt, fmtUSD, useFetchJson, type BarDatum, } from "./shared"; type Overview = { users: number; students: number; activeWeek: number; conversations: number; messages: number; documents: number; chunks: number; flagsOpen: number; apiErrors7d: number; costMonth: number; costToday: number; budgets: { dailyPerUserUSD: number; monthlyPerUserUSD: number; monthlyGlobalUSD: number; dailyRequestsPerUser: number }; lastIngestion: { id: number; started_at: string; finished_at: string | null; status: string; chunks_created: number; files_ingested: number } | null; costByDay: { day: string; cost: number; calls: number }[]; topModels: { model: string; calls: number; cost: number; tin: number | null; tout: number | null }[]; invalidCitations7d: number; }; function ingestionTone(status: string): "green" | "red" | "amber" { const s = status.toLowerCase(); if (["ok", "success", "succès", "completed", "done"].some((k) => s.includes(k))) return "green"; if (["error", "fail", "échec", "erreur"].some((k) => s.includes(k))) return "red"; return "amber"; } export function AdminOverview() { const { data, error, loading, reload } = useFetchJson("/api/admin/overview"); if (loading) { return (
{Array.from({ length: 10 }).map((_, i) => ( ))}
); } if (error || !data) { return (
); } // Série des 14 derniers jours, jours sans usage inclus (valeur 0). const days: BarDatum[] = []; for (let i = 13; i >= 0; i--) { const d = new Date(); d.setDate(d.getDate() - i); const key = d.toISOString().slice(0, 10); const row = data.costByDay.find((r) => r.day === key); days.push({ label: key.slice(5), value: row?.cost ?? 0, hint: `${fmtInt(row?.calls ?? 0)} appels` }); } const budgetRatio = data.budgets.monthlyGlobalUSD > 0 ? data.costMonth / data.budgets.monthlyGlobalUSD : 0; const ingest = data.lastIngestion; return (
0 ? "amber" : "default"} /> 0 ? "red" : "default"} /> 0 ? "amber" : "default"} />

Coût du mois

{fmtUSD(data.costMonth)}

= 0.9 ? "gold" : "brand"} className="mt-2" />

{Math.round(budgetRatio * 100)} % du budget global ({fmtUSD(data.budgets.monthlyGlobalUSD, 0)}/mois)

Coût par jour — 14 derniers jours fmtUSD(v, 4)} emptyLabel="Aucune dépense sur les 14 derniers jours." /> Modèles les plus utilisés {data.topModels.length === 0 ? ( } title="Aucun appel de modèle" description="Les statistiques d'usage apparaîtront après les premières conversations." /> ) : (
{data.topModels.map((m) => ( ))}
Modèle Appels Coût Jetons (entrée / sortie)
{m.model} {fmtInt(m.calls)} {fmtUSD(m.cost, 4)} {fmtInt(m.tin ?? 0)} / {fmtInt(m.tout ?? 0)}
)}
Dernière ingestion du contenu {!ingest ? (

Aucune ingestion n'a encore été exécutée. Lancez-la depuis l'onglet « Cours & contenu ».

) : (
{ingest.status} Démarrée : {fmtDate(ingest.started_at)} Terminée : {ingest.finished_at ? fmtDate(ingest.finished_at) : "en cours…"} Fichiers ingérés : {fmtInt(ingest.files_ingested)} Fragments créés : {fmtInt(ingest.chunks_created)}
)}
); }