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 — panneau client d'état du moteur (auto-refresh 10 s)3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 */67"use client";89import { useEffect, useState } from "react";10import { Stat } from "@/components/stat";11import { Badge } from "@/components/ui/badge";12import { formatNumber } from "@/lib/format";13import type { StatusResponse } from "@/lib/types";1415const REFRESH_MS = 10_000;1617function crawlerBadge(state: string) {18 if (state === "running") {19 return (20 <span className="stat-chip">21 <span className="pulse-dot" aria-hidden="true" />22 Robot en marche23 </span>24 );25 }26 if (state === "paused") {27 return <Badge variant="warning">Robot en pause</Badge>;28 }29 return <Badge>Robot : {state || "état inconnu"}</Badge>;30}3132export function StatusPanel() {33 const [status, setStatus] = useState<StatusResponse | null>(null);34 const [error, setError] = useState(false);35 const [updatedAt, setUpdatedAt] = useState<string>("");3637 useEffect(() => {38 let active = true;3940 async function load() {41 try {42 const res = await fetch("/api/status");43 if (!res.ok) throw new Error(`Erreur ${res.status}`);44 const data = (await res.json()) as StatusResponse;45 if (!active) return;46 setStatus(data);47 setError(false);48 setUpdatedAt(49 new Intl.DateTimeFormat("fr-CA", {50 hour: "2-digit",51 minute: "2-digit",52 second: "2-digit",53 }).format(new Date()),54 );55 } catch {56 if (active) setError(true);57 }58 }5960 load();61 const id = setInterval(load, REFRESH_MS);62 return () => {63 active = false;64 clearInterval(id);65 };66 }, []);6768 if (error && !status) {69 return (70 <p className="mt-8 text-ink-2">71 Impossible de joindre le moteur pour le moment. Réessayez sous peu.72 </p>73 );74 }7576 if (!status) {77 return (78 <div aria-hidden="true" className="mt-8 grid animate-pulse gap-4 sm:grid-cols-3">79 {Array.from({ length: 6 }, (_, i) => (80 <div key={i} className="h-24 rounded-card bg-line" />81 ))}82 </div>83 );84 }8586 return (87 <div className="mt-8">88 {/* Chiffre-vedette : la taille de l'index. */}89 <div className="gk-card p-6">90 <p className="klabel">Pages indexées</p>91 <p className="mt-2 font-display text-5xl font-bold tracking-[-0.03em] text-ink sm:text-6xl">92 {formatNumber(status.pages_indexed)}93 </p>94 <div className="mt-5 flex flex-wrap items-center gap-2.5">95 {crawlerBadge(status.crawler_state)}96 {status.search_ok ? (97 <Badge variant="success">Recherche opérationnelle</Badge>98 ) : (99 <Badge variant="danger">Recherche dégradée</Badge>100 )}101 </div>102 </div>103104 <div className="mt-5 grid gap-4 sm:grid-cols-3">105 <Stat106 label="Domaines connus"107 value={formatNumber(status.domains_count)}108 />109 <Stat110 label="Pages indexées (dernière heure)"111 value={formatNumber(status.indexed_last_hour)}112 />113 <Stat114 label="Pages téléchargées (dernière heure)"115 value={formatNumber(status.fetched_last_hour)}116 />117 <Stat118 label="URL en attente de crawl"119 value={formatNumber(status.frontier_pending)}120 />121 <Stat122 label="URL en cours de crawl"123 value={formatNumber(status.frontier_in_progress)}124 />125 <Stat126 label="Erreurs (dernière heure)"127 value={formatNumber(status.errors_last_hour)}128 />129 </div>130131 <p className="gk-mono mt-5 text-[11px] text-ink-3" aria-live="polite">132 {error133 ? "Mise à jour interrompue — nouvelles tentatives en cours."134 : updatedAt135 ? `Dernière mise à jour à ${updatedAt}.`136 : ""}137 </p>138 </div>139 );140}141