/** * Trouve-KA — flux live du crawl (poll toutes les 3 s) * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ "use client"; import { useEffect, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { adminFetch, UnauthorizedError, } from "@/lib/admin"; import { formatTime } from "@/lib/format"; import type { AdminRecentResponse, CrawlEvent } from "@/lib/types"; const POLL_MS = 3_000; function outcomeBadge(outcome: string) { switch (outcome) { case "indexed": return Indexée; case "duplicate": return Doublon; case "error": return Erreur; case "not_quebec": return Hors Québec; case "robots_blocked": return Robots; default: return {outcome}; } } interface LiveFeedProps { token: string; onUnauthorized: () => void; } export function LiveFeed({ token, onUnauthorized }: LiveFeedProps) { const [events, setEvents] = useState(null); const [stalled, setStalled] = useState(false); useEffect(() => { let active = true; async function load() { try { const data = await adminFetch( "/api/admin/recent?limit=50", token, ); if (!active) return; setEvents(data.events); setStalled(false); } catch (err) { if (!active) return; if (err instanceof UnauthorizedError) { onUnauthorized(); return; } setStalled(true); } } load(); const id = setInterval(load, POLL_MS); return () => { active = false; clearInterval(id); }; }, [token, onUnauthorized]); return ( Flux live du crawl {stalled ? "Flux interrompu — reconnexion…" : "Actualisé toutes les 3 s"} {events === null ? (

Chargement du flux…

) : events.length === 0 ? (

Aucun événement récent. Le robot est peut-être en pause.

) : (
    {events.map((event, index) => (
  • {formatTime(event.at)} {outcomeBadge(event.outcome)} {event.status || "—"} {event.title || event.url} {typeof event.quebec_score === "number" ? ( QC {event.quebec_score.toFixed(2)} ) : null}
  • ))}
)}
); }