spb/trouve-ka Public
Trouve-KA — moteur de recherche web indépendant, Québec-first. Crawler distribué, index OpenSearch, ranking bilingue, galerie d'images. En prod : www.trouve-ka.com
Python 76.8%
TypeScript 15.7%
SQL 3.9%
Shell 1.4%
CSS 1.3%
Dockerfile 0.7%
1/**2 * Trouve-KA — flux live du crawl (poll toutes les 3 s)3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 */67"use client";89import { useEffect, useState } from "react";10import { Badge } from "@/components/ui/badge";11import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";12import {13 adminFetch,14 UnauthorizedError,15} from "@/lib/admin";16import { formatTime } from "@/lib/format";17import type { AdminRecentResponse, CrawlEvent } from "@/lib/types";1819const POLL_MS = 3_000;2021function outcomeBadge(outcome: string) {22 switch (outcome) {23 case "indexed":24 return <Badge variant="success">Indexée</Badge>;25 case "duplicate":26 return <Badge>Doublon</Badge>;27 case "error":28 return <Badge variant="danger">Erreur</Badge>;29 case "not_quebec":30 return <Badge>Hors Québec</Badge>;31 case "robots_blocked":32 return <Badge variant="warning">Robots</Badge>;33 default:34 return <Badge>{outcome}</Badge>;35 }36}3738interface LiveFeedProps {39 token: string;40 onUnauthorized: () => void;41}4243export function LiveFeed({ token, onUnauthorized }: LiveFeedProps) {44 const [events, setEvents] = useState<CrawlEvent[] | null>(null);45 const [stalled, setStalled] = useState(false);4647 useEffect(() => {48 let active = true;4950 async function load() {51 try {52 const data = await adminFetch<AdminRecentResponse>(53 "/api/admin/recent?limit=50",54 token,55 );56 if (!active) return;57 setEvents(data.events);58 setStalled(false);59 } catch (err) {60 if (!active) return;61 if (err instanceof UnauthorizedError) {62 onUnauthorized();63 return;64 }65 setStalled(true);66 }67 }6869 load();70 const id = setInterval(load, POLL_MS);71 return () => {72 active = false;73 clearInterval(id);74 };75 }, [token, onUnauthorized]);7677 return (78 <Card>79 <CardHeader className="flex items-baseline justify-between gap-2">80 <CardTitle>Flux live du crawl</CardTitle>81 <span className="klabel">82 {stalled ? "Flux interrompu — reconnexion…" : "Actualisé toutes les 3 s"}83 </span>84 </CardHeader>85 <CardContent>86 {events === null ? (87 <p className="text-sm text-ink-2">Chargement du flux…</p>88 ) : events.length === 0 ? (89 <p className="text-sm text-ink-2">90 Aucun événement récent. Le robot est peut-être en pause.91 </p>92 ) : (93 <ul94 aria-label="Événements récents du robot"95 className="max-h-96 divide-y divide-line overflow-y-auto"96 >97 {events.map((event, index) => (98 <li99 key={`${event.at}-${event.url}-${index}`}100 className="flex items-center gap-3 py-2 text-sm"101 >102 <span className="gk-mono shrink-0 tabular-nums text-xs text-ink-3">103 {formatTime(event.at)}104 </span>105 <span className="shrink-0">{outcomeBadge(event.outcome)}</span>106 <span className="gk-mono shrink-0 tabular-nums text-xs text-ink-2">107 {event.status || "—"}108 </span>109 <span className="min-w-0 flex-1 truncate text-ink">110 {event.title || event.url}111 </span>112 {typeof event.quebec_score === "number" ? (113 <span className="gk-mono shrink-0 tabular-nums text-xs text-ink-2">114 QC {event.quebec_score.toFixed(2)}115 </span>116 ) : null}117 </li>118 ))}119 </ul>120 )}121 </CardContent>122 </Card>123 );124}125