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.3 KB · 227 lines tsx
Raw Blame History
1/**2 * Trouve-KA — contrôles du robot (pause, seeds, recrawl, blocage)3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 */67"use client";89import { useState, type FormEvent } from "react";10import { Button } from "@/components/ui/button";11import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";12import { Input } from "@/components/ui/input";13import { adminFetch, UnauthorizedError } from "@/lib/admin";1415type Feedback = { kind: "ok" | "err"; text: string } | null;1617interface ControlsProps {18  token: string;19  paused: boolean;20  onUnauthorized: () => void;21  /** Rafraîchit la vue d'ensemble après une action réussie. */22  onRefresh: () => void;23}2425export function Controls({26  token,27  paused,28  onUnauthorized,29  onRefresh,30}: ControlsProps) {31  const [busy, setBusy] = useState<string | null>(null);32  const [feedback, setFeedback] = useState<Feedback>(null);33  const [seeds, setSeeds] = useState("");34  const [recrawlTarget, setRecrawlTarget] = useState("");35  const [blockDomain, setBlockDomain] = useState("");3637  async function run(38    name: string,39    action: () => Promise<unknown>,40    okMessage: string,41  ): Promise<boolean> {42    setBusy(name);43    setFeedback(null);44    try {45      await action();46      setFeedback({ kind: "ok", text: okMessage });47      onRefresh();48      return true;49    } catch (err) {50      if (err instanceof UnauthorizedError) {51        onUnauthorized();52        return false;53      }54      setFeedback({ kind: "err", text: "L'opération a échoué. Réessayez." });55      return false;56    } finally {57      setBusy(null);58    }59  }6061  function togglePause() {62    const path = paused ? "/api/admin/resume" : "/api/admin/pause";63    run(64      "pause",65      () => adminFetch(path, token, { method: "POST" }),66      paused ? "Robot relancé." : "Robot mis en pause.",67    );68  }6970  function submitSeeds(event: FormEvent) {71    event.preventDefault();72    const urls = seeds73      .split("\n")74      .map((line) => line.trim())75      .filter((line) => /^https?:\/\//.test(line));76    if (urls.length === 0) {77      setFeedback({78        kind: "err",79        text: "Entrez au moins une URL valide (une par ligne, http:// ou https://).",80      });81      return;82    }83    run(84      "seeds",85      () =>86        adminFetch("/api/admin/seeds", token, {87          method: "POST",88          body: JSON.stringify({ urls }),89        }),90      `${urls.length} seed(s) ajouté(s) au frontier.`,91    ).then((ok) => {92      if (ok) setSeeds("");93    });94  }9596  function submitRecrawl(event: FormEvent) {97    event.preventDefault();98    const target = recrawlTarget.trim();99    if (!target) return;100    const body = /^https?:\/\//.test(target)101      ? { url: target }102      : { domain: target };103    run(104      "recrawl",105      () =>106        adminFetch("/api/admin/recrawl", token, {107          method: "POST",108          body: JSON.stringify(body),109        }),110      "Recrawl demandé.",111    ).then((ok) => {112      if (ok) setRecrawlTarget("");113    });114  }115116  function submitBlock(event: FormEvent) {117    event.preventDefault();118    const domain = blockDomain.trim();119    if (!domain) return;120    run(121      "block",122      () =>123        adminFetch("/api/admin/domains/block", token, {124          method: "POST",125          body: JSON.stringify({ domain }),126        }),127      `Domaine « ${domain} » bloqué.`,128    ).then((ok) => {129      if (ok) setBlockDomain("");130    });131  }132133  return (134    <Card>135      <CardHeader className="flex items-center justify-between gap-3">136        <CardTitle>Contrôles du robot</CardTitle>137        <Button138          variant={paused ? "default" : "outline"}139          size="sm"140          onClick={togglePause}141          disabled={busy !== null}142        >143          {paused ? "Reprendre le crawl" : "Mettre en pause"}144        </Button>145      </CardHeader>146      <CardContent className="space-y-6">147        <form onSubmit={submitSeeds}>148          <label htmlFor="seeds-admin" className="klabel block">149            Ajouter des seeds (une URL par ligne)150          </label>151          <textarea152            id="seeds-admin"153            rows={3}154            value={seeds}155            onChange={(event) => setSeeds(event.target.value)}156            placeholder={"https://www.quebec.ca\nhttps://www.mamunicipalite.qc.ca"}157            className="field mt-2 text-sm"158          />159          <Button160            type="submit"161            size="sm"162            className="mt-2"163            disabled={busy !== null}164          >165            {busy === "seeds" ? "Ajout…" : "Ajouter au frontier"}166          </Button>167        </form>168169        <form onSubmit={submitRecrawl}>170          <label htmlFor="recrawl-admin" className="klabel block">171            Recrawler une URL ou un domaine172          </label>173          <div className="mt-2 flex flex-col gap-2 sm:flex-row">174            <Input175              id="recrawl-admin"176              value={recrawlTarget}177              onChange={(event) => setRecrawlTarget(event.target.value)}178              placeholder="https://exemple.quebec/page ou exemple.quebec"179              className="flex-1"180            />181            <Button type="submit" size="sm" disabled={busy !== null}>182              {busy === "recrawl" ? "Envoi…" : "Recrawler"}183            </Button>184          </div>185        </form>186187        <form onSubmit={submitBlock}>188          <label htmlFor="block-admin" className="klabel block">189            Bloquer un domaine190          </label>191          <div className="mt-2 flex flex-col gap-2 sm:flex-row">192            <Input193              id="block-admin"194              value={blockDomain}195              onChange={(event) => setBlockDomain(event.target.value)}196              placeholder="spam-exemple.com"197              className="flex-1"198            />199            <Button200              type="submit"201              variant="danger"202              size="sm"203              disabled={busy !== null}204            >205              {busy === "block" ? "Blocage…" : "Bloquer"}206            </Button>207          </div>208        </form>209210        <p aria-live="polite" className="min-h-5 text-sm">211          {feedback ? (212            <span213              className={214                feedback.kind === "ok"215                  ? "font-medium text-green"216                  : "font-medium text-danger"217              }218            >219              {feedback.text}220            </span>221          ) : null}222        </p>223      </CardContent>224    </Card>225  );226}227