spb/ka-guardian
Public
Python 57.1%
Shell 13.8%
CSS 12%
JavaScript 10%
HTML 7.2%
1/* KA Guardian — poste de commande (/commander) */2const $ = (s) => document.querySelector(s);3const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));4const STATUS_FR = { ok: "ok", degraded: "dégradé", broken: "cassé", stale: "endormi" };5const STATE_FR = { open: "en file", dispatched: "envoi…", fixing: "en cours", watching: "surveillance",6 cooldown: "attente", resolved: "livré", self_healed: "auto-guéri", abandoned: "terminé" };7const KIND_FR = { effort_new: "nouveau connecteur", effort_enrich: "enrichissement", effort_degrade: "inspection dégradés" };8const HINTS = {9 effort_new: "L'agent cartographie l'existant, découvre des sources québécoises non couvertes (Serper), choisit la meilleure et construit le connecteur complet — testé en volume réel.",10 effort_enrich: "L'agent maximise la valeur du connecteur choisi : pagination complète, champs des fiches détail, robustesse, détection des retraits — mesuré avant/après.",11 effort_degrade: "L'agent inspecte tous les connecteurs dégradés de la plateforme (volume < 50 % de la médiane), trie baisse réelle / légitime / blocage, et répare un par un — un commit par connecteur.",12};1314let STATE = null;1516async function load() {17 const r = await fetch("/api/state");18 STATE = await r.json();19 const id = STATE.identity, num = STATE.agent.replace("ka", "");20 const rs = document.documentElement.style;21 rs.setProperty("--agent", id.accent); rs.setProperty("--lime", id.accent);22 if (id.accent_soft) rs.setProperty("--lime-soft", id.accent_soft);23 if (id.accent2) rs.setProperty("--accent-deep", id.accent2);24 document.title = `Commander — ${STATE.agent.toUpperCase()} Guardian`;25 $("#wm-name").textContent = "ka·" + num;26 $("#foot-name").textContent = "ka·" + num;27 $("#hero-label").textContent = `${STATE.agent} — poste de commande · ${id.tagline}`;28 $("#siblings").innerHTML = Object.entries(STATE.siblings).map(([a, v]) =>29 `<a class="gk-badge" href="https://${v.domain}/commander"><span class="dot" style="background:${v.accent}"></span><b>${a}<span class="ka">·G</span></b></a>`).join(" ");30 const svcSel = $("#ef-service");31 if (!svcSel.options.length) {32 svcSel.innerHTML = Object.entries(STATE.services)33 .map(([k, s]) => `<option value="${k}">${esc(s.app)} — ${esc(s.node_alias)}</option>`).join("");34 }35 renderEfforts();36}3738function renderEfforts() {39 const efforts = STATE.incidents.filter((i) => (i.status_detected || "").startsWith("effort"));40 $("#efforts-list").innerHTML = efforts.slice(0, 20).map((i) => {41 const d = JSON.parse(i.detail || "{}");42 return `<div class="inc">43 <div><div class="src">${esc(KIND_FR[i.status_detected] || i.status_detected)} — ${esc((STATE.services[i.service] || {}).app || i.service)}</div>44 <div class="svc">${esc(i.source)}${d.note ? " · « " + esc(d.note.slice(0, 80)) + " »" : ""}${d.max_minutes ? " · ≤" + d.max_minutes + " min" : ""}${d.max_cost_usd ? " · ≤" + d.max_cost_usd + " $" : ""}</div></div>45 <span class="badge b-${esc(i.state)}">${STATE_FR[i.state] || esc(i.state)}</span></div>`;46 }).join("") || `<div class="empty">Aucun effort commandé encore — sois le premier à lui donner du travail.</div>`;47}4849function syncKind() {50 const kind = $("#ef-kind").value;51 $("#ef-source-wrap").hidden = kind !== "effort_enrich";52 $("#kind-hint").textContent = HINTS[kind] || "";53 if (kind === "effort_enrich") fillSources();54}5556async function fillSources() {57 const r = await fetch("/api/connectors/" + $("#ef-service").value);58 const d = await r.json();59 $("#ef-source").innerHTML = d.connectors60 .map((c) => `<option value="${esc(c.source)}">${esc(c.source)} (${STATUS_FR[c.status] || c.status})</option>`).join("")61 || `<option value="">— aucun connecteur connu —</option>`;62}6364async function submitEffort(e) {65 e.preventDefault();66 const msg = $("#ef-msg"), btn = e.target.querySelector("button");67 const token = $("#ef-token").value.trim();68 if (!token) { msg.className = "cmd-msg err"; msg.textContent = "jeton d'opérateur requis"; return; }69 localStorage.setItem("ka_token", token);70 btn.disabled = true; msg.className = "cmd-msg"; msg.textContent = "lancement…";71 try {72 const r = await fetch("/api/admin/effort", {73 method: "POST", headers: { "Content-Type": "application/json", "X-KA-Token": token },74 body: JSON.stringify({75 kind: $("#ef-kind").value, service: $("#ef-service").value,76 source: $("#ef-source").value || "", note: $("#ef-note").value,77 max_minutes: parseInt($("#ef-minutes").value, 10),78 max_cost_usd: parseFloat($("#ef-cost").value),79 }),80 });81 const d = await r.json();82 if (r.ok && d.ok) {83 msg.className = "cmd-msg ok";84 msg.textContent = `effort accepté (${d.incident_id}) — la session démarre d'ici ~5 min. Suis-la sur le flux ⚡`;85 $("#ef-note").value = "";86 load();87 } else {88 msg.className = "cmd-msg err";89 msg.textContent = "refusé: " + (d.detail || r.status);90 }91 } catch (err) {92 msg.className = "cmd-msg err"; msg.textContent = "erreur: " + err;93 } finally { btn.disabled = false; }94}9596$("#ef-kind").addEventListener("change", syncKind);97$("#ef-service").addEventListener("change", () => { if ($("#ef-kind").value === "effort_enrich") fillSources(); });98$("#effort-form").addEventListener("submit", submitEffort);99$("#ef-token").value = localStorage.getItem("ka_token") || "";100load().then(syncKind);101setInterval(load, 30000);102