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 — 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