SPB Git

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%
3.7 KB · 125 lines tsx
Raw Blame History
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&nbsp;{event.quebec_score.toFixed(2)}115                  </span>116                ) : null}117              </li>118            ))}119          </ul>120        )}121      </CardContent>122    </Card>123  );124}125