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%
6.6 KB · 200 lines tsx
Raw Blame History
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