Checkpoint: Préparation recette : ajout du journal classification_errors, exposition de la cause technique des fallbacks IA, enregistrement non bloquant depuis le moteur RSS, écran d’administration Erreurs IA, route protégée et migration 0013. Validation locale : 28 tests, TypeScript et build de production réussis.

This commit is contained in:
Manus
2026-08-18 07:49:11 +00:00
parent e4a389cc67
commit 9acc52da7d
15 changed files with 1432 additions and 5 deletions

View File

@@ -1,4 +1,4 @@
{
"timestamp": 1786998105509,
"version": "de5930f0"
"timestamp": 1787039351851,
"version": "85944a51"
}

View File

@@ -17,6 +17,7 @@ const AAPDashboard = lazy(() => import("./pages/AAPDashboard"));
const Settings = lazy(() => import("./pages/Settings"));
const UsersAdmin = lazy(() => import("./pages/UsersAdmin"));
const ImportLogs = lazy(() => import("./pages/ImportLogs"));
const ClassificationErrors = lazy(() => import("./pages/ClassificationErrors"));
const BoiteAIdees = lazy(() => import("@/pages/BoiteAIdees"));
const RssFeeds = lazy(() => import("@/pages/RssFeeds"));
const AzureCallback = lazy(() => import("@/pages/AzureCallback"));
@@ -104,6 +105,16 @@ function LogsPage() {
);
}
function ClassificationErrorsPage() {
return (
<AuthGuard>
<DashboardWrapper>
<ClassificationErrors />
</DashboardWrapper>
</AuthGuard>
);
}
function BoiteAIdeesPage() {
return (
<AuthGuard>
@@ -139,6 +150,7 @@ function Router() {
<Route path="/admin/settings" component={SettingsPage} />
<Route path="/admin/users" component={UsersPage} />
<Route path="/admin/logs" component={LogsPage} />
<Route path="/admin/classification-errors" component={ClassificationErrorsPage} />
<Route path="/boite-a-idees" component={BoiteAIdeesPage} />
<Route path="/admin/rss" component={RssFeedsPage} />
<Route path="/404" component={NotFound} />

View File

@@ -18,6 +18,7 @@ import {
X,
Lightbulb,
Rss,
AlertTriangle,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@@ -64,6 +65,7 @@ const NAV_GROUPS: NavGroup[] = [
defaultOpen: false,
items: [
{ label: "Logs d'import", href: "/admin/logs", icon: <Activity size={16} />, adminOnly: true },
{ label: "Erreurs IA", href: "/admin/classification-errors", icon: <AlertTriangle size={16} />, adminOnly: true },
{ label: "Utilisateurs", href: "/admin/users", icon: <Users size={16} />, adminOnly: true },
{ label: "Flux RSS", href: "/admin/rss", icon: <Rss size={16} />, adminOnly: true },
{ label: "Paramètres", href: "/admin/settings", icon: <Settings size={16} />, adminOnly: true },

View File

@@ -0,0 +1,145 @@
import { useState } from "react";
import { AlertTriangle, ChevronLeft, ChevronRight, ExternalLink, Loader2, RefreshCw, Rss } from "lucide-react";
import { trpc } from "@/lib/trpc";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
interface ClassificationError {
id: number;
feedName: string;
feedType: "veille" | "aap";
articleTitle: string;
articleUrl: string | null;
errorMessage: string;
occurredAt: Date;
}
const PAGE_SIZE = 25;
const typeConfig = {
veille: { label: "Veille", className: "bg-blue-100 text-blue-800 border-blue-200" },
aap: { label: "AAP", className: "bg-violet-100 text-violet-800 border-violet-200" },
};
function formatDate(value: Date) {
return new Intl.DateTimeFormat("fr-FR", {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(value));
}
/** Rapport des erreurs LLM ayant déclenché le fallback de classification RSS. */
export default function ClassificationErrors() {
const [page, setPage] = useState(1);
const errorsQuery = trpc.rss.classificationErrors.useQuery({ page, pageSize: PAGE_SIZE });
const errors = (errorsQuery.data?.errors ?? []) as ClassificationError[];
const total = errorsQuery.data?.total ?? 0;
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
return (
<div className="p-6 space-y-6 animate-fade-up">
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<AlertTriangle size={22} className="text-amber-600" />
<h1 className="text-2xl font-bold text-foreground">Erreurs de classification</h1>
</div>
<p className="text-sm text-muted-foreground">
Fallbacks IA détectés pendant la lecture des flux RSS. Les articles concernés ont é importés avec les règles de repli.
</p>
</div>
<Button variant="outline" size="sm" className="gap-2" onClick={() => errorsQuery.refetch()} disabled={errorsQuery.isFetching}>
<RefreshCw size={15} className={cn(errorsQuery.isFetching && "animate-spin")} />
Actualiser
</Button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Card className="border-amber-200 bg-amber-50/50">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-amber-700 mb-1">
<AlertTriangle size={16} />
<span className="text-xs font-medium">Fallbacks enregistrés</span>
</div>
<p className="text-2xl font-bold text-amber-800">{total}</p>
</CardContent>
</Card>
<Card className="border-border/50">
<CardContent className="p-4">
<div className="flex items-center gap-2 text-primary mb-1">
<Rss size={16} />
<span className="text-xs font-medium text-muted-foreground">Comportement de sécurité</span>
</div>
<p className="text-sm font-medium text-foreground">Import maintenu via règles de repli</p>
</CardContent>
</Card>
</div>
<Card>
<CardContent className="p-0">
{errorsQuery.isLoading ? (
<div className="flex items-center justify-center py-16"><Loader2 size={28} className="animate-spin text-primary" /></div>
) : errorsQuery.isError ? (
<div className="flex flex-col items-center justify-center py-16 text-center gap-2">
<AlertTriangle size={40} className="text-destructive/60" />
<p className="font-medium text-foreground">Le rapport ne peut pas être chargé</p>
<p className="text-sm text-muted-foreground">{errorsQuery.error.message}</p>
</div>
) : errors.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-center">
<AlertTriangle size={40} className="text-emerald-500/60 mb-3" />
<p className="font-medium text-foreground">Aucune erreur de classification</p>
<p className="text-sm text-muted-foreground mt-1">Les prochains fallbacks IA apparaîtront ici.</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border bg-muted/30">
<th className="text-left px-4 py-3 font-semibold text-muted-foreground whitespace-nowrap">Date</th>
<th className="text-left px-4 py-3 font-semibold text-muted-foreground w-24">Flux</th>
<th className="text-left px-4 py-3 font-semibold text-muted-foreground">Article</th>
<th className="text-left px-4 py-3 font-semibold text-muted-foreground">Cause technique</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{errors.map((error) => {
const type = typeConfig[error.feedType];
return (
<tr key={error.id} className="hover:bg-muted/20 transition-colors align-top">
<td className="px-4 py-3 text-xs text-muted-foreground whitespace-nowrap">{formatDate(error.occurredAt)}</td>
<td className="px-4 py-3">
<div className="space-y-1">
<Badge variant="outline" className={cn("text-xs", type.className)}>{type.label}</Badge>
<p className="text-xs text-muted-foreground max-w-40 truncate" title={error.feedName}>{error.feedName}</p>
</div>
</td>
<td className="px-4 py-3 max-w-sm">
{error.articleUrl ? (
<a href={error.articleUrl} target="_blank" rel="noreferrer" className="inline-flex items-start gap-1 font-medium text-primary hover:underline">
<span>{error.articleTitle}</span><ExternalLink size={13} className="mt-0.5 shrink-0" />
</a>
) : <span className="font-medium text-foreground">{error.articleTitle}</span>}
</td>
<td className="px-4 py-3 text-xs text-destructive max-w-md break-words">{error.errorMessage}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
{totalPages > 1 && (
<div className="flex items-center justify-center gap-2">
<Button variant="outline" size="sm" onClick={() => setPage((value) => Math.max(1, value - 1))} disabled={page === 1}><ChevronLeft size={14} /></Button>
<span className="text-sm text-muted-foreground px-2">Page {page} / {totalPages}</span>
<Button variant="outline" size="sm" onClick={() => setPage((value) => Math.min(totalPages, value + 1))} disabled={page === totalPages}><ChevronRight size={14} /></Button>
</div>
)}
</div>
);
}