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 — dashboard admin (vue d'ensemble + contrôles + flux live)3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 */67"use client";89import Link from "next/link";10import { useCallback, useEffect, useState } from "react";11import { Controls } from "@/components/admin/controls";12import { HttpBars } from "@/components/admin/http-bars";13import { LiveFeed } from "@/components/admin/live-feed";14import { RecentErrors } from "@/components/admin/recent-errors";15import { TopDomains } from "@/components/admin/top-domains";16import { Stat } from "@/components/stat";17import { Badge } from "@/components/ui/badge";18import { Button } from "@/components/ui/button";19import { adminFetch, UnauthorizedError } from "@/lib/admin";20import { formatNumber } from "@/lib/format";21import type { AdminOverview } from "@/lib/types";2223const REFRESH_MS = 10_000;2425interface AdminDashboardProps {26 token: string;27 onUnauthorized: () => void;28 onLogout: () => void;29}3031export function AdminDashboard({32 token,33 onUnauthorized,34 onLogout,35}: AdminDashboardProps) {36 const [overview, setOverview] = useState<AdminOverview | null>(null);37 const [error, setError] = useState(false);3839 const refresh = useCallback(async () => {40 try {41 const data = await adminFetch<AdminOverview>(42 "/api/admin/overview",43 token,44 );45 setOverview(data);46 setError(false);47 } catch (err) {48 if (err instanceof UnauthorizedError) {49 onUnauthorized();50 return;51 }52 setError(true);53 }54 }, [token, onUnauthorized]);5556 useEffect(() => {57 refresh();58 const id = setInterval(refresh, REFRESH_MS);59 return () => clearInterval(id);60 }, [refresh]);6162 return (63 <div className="flex-1">64 <div className="border-b-[1.5px] border-ink bg-surface-2">65 <div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-3.5">66 <div className="flex items-center gap-3">67 <Link68 href="/"69 className="klabel rounded-sm transition-colors hover:text-ink"70 >71 Trouve-KA72 </Link>73 <span className="text-sm text-ink-3">/</span>74 <h1 className="font-display text-sm font-bold tracking-[-0.02em] text-ink">75 Administration76 </h1>77 {overview ? (78 overview.paused ? (79 <Badge variant="warning">En pause</Badge>80 ) : (81 <span className="stat-chip px-3 py-1 text-[11px]">82 <span className="pulse-dot" aria-hidden="true" />83 En marche84 </span>85 )86 ) : null}87 </div>88 <Button variant="ghost" size="sm" onClick={onLogout}>89 Se déconnecter90 </Button>91 </div>92 </div>9394 <main id="contenu" className="mx-auto max-w-6xl space-y-7 px-4 py-6">95 {error && !overview ? (96 <p className="rounded-ctl border-[1.5px] border-ink bg-danger-soft px-4 py-3 text-sm font-medium text-danger shadow-off-field">97 Impossible de joindre l'API d'administration. Vérifiez que le98 backend tourne, puis rechargez la page.99 </p>100 ) : null}101102 {overview ? (103 <>104 <section aria-labelledby="titre-frontier">105 <h2 id="titre-frontier" className="kicker">106 Frontier107 </h2>108 <div className="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-5">109 <Stat110 label="En attente"111 value={formatNumber(overview.frontier.pending)}112 />113 <Stat114 label="En cours"115 value={formatNumber(overview.frontier.in_progress)}116 />117 <Stat118 label="Complétées"119 value={formatNumber(overview.frontier.done)}120 />121 <Stat122 label="Échouées"123 value={formatNumber(overview.frontier.failed)}124 />125 <Stat126 label="Bloquées"127 value={formatNumber(overview.frontier.blocked)}128 />129 </div>130 </section>131132 <section aria-labelledby="titre-debits">133 <h2 id="titre-debits" className="kicker">134 Débits (dernière heure)135 </h2>136 <div className="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-8">137 <Stat138 label="Téléchargées"139 value={formatNumber(overview.rates.fetched_1h)}140 />141 <Stat142 label="Analysées"143 value={formatNumber(overview.rates.parsed_1h)}144 />145 <Stat146 label="Indexées"147 value={formatNumber(overview.rates.indexed_1h)}148 />149 <Stat150 label="Erreurs"151 value={formatNumber(overview.rates.errors_1h)}152 />153 <Stat154 label="Refus robots"155 value={formatNumber(overview.rates.robots_blocked_1h)}156 />157 <Stat158 label="Doublons"159 value={formatNumber(overview.rates.duplicates_1h)}160 />161 <Stat162 label="Latence fetch p50"163 value={`${formatNumber(overview.latency.fetch_p50_ms)} ms`}164 />165 <Stat166 label="Latence fetch p95"167 value={`${formatNumber(overview.latency.fetch_p95_ms)} ms`}168 />169 </div>170 </section>171172 <div className="grid gap-6 lg:grid-cols-2">173 <HttpBars data={overview.http_status} />174 <TopDomains domains={overview.top_domains} />175 </div>176177 <div className="grid items-start gap-6 lg:grid-cols-2">178 <Controls179 token={token}180 paused={overview.paused}181 onUnauthorized={onUnauthorized}182 onRefresh={refresh}183 />184 <RecentErrors errors={overview.recent_errors} />185 </div>186187 <LiveFeed token={token} onUnauthorized={onUnauthorized} />188 </>189 ) : !error ? (190 <div aria-hidden="true" className="grid animate-pulse gap-3 sm:grid-cols-4">191 {Array.from({ length: 8 }, (_, i) => (192 <div key={i} className="h-24 rounded-card bg-line" />193 ))}194 </div>195 ) : null}196 </main>197 </div>198 );199}200