Files
ObsiGate/frontend/draco-panneau.js
T
Spark 440cfdcc31
CI / lint (push) Successful in 2m53s
CI / security (push) Failing after 1m39s
CI / test (push) Successful in 5m3s
CI / build (push) Successful in 1m39s
CI / e2e (push) Successful in 18m1s
fix: bouton « Console ↗ » dans le panneau Spark #221
2026-10-22 21:20:51 +00:00

703 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Panneau Spark — branchement au service d'agents (Fusion, J1 → Panneau v2).
*
* Parle à l'API du service d'agents (contrat de dracodev-commons) :
* GET /api/v1/me — identité (cookie draco_session)
* POST /api/v1/chat — conversation réelle du noyau (J2) :
* LLM DeepSeek + outils MCP des apps ;
* la réponse arrive avec son HTML
* Markdown rendu côté service (v2)
* GET /api/v1/conversations — discussions façon Muse (v2) : une
* principale perpétuelle + des
* parallèles créées/archivées/
* supprimées par l'utilisateur
* GET /api/v1/conversations/… — historique d'une discussion (les
* messages agent portent leur html)
* GET /api/v1/executors — registre (noyau, sous-agents J3,
* externes J4 : état de configuration)
* POST /api/v1/missions — missions longues (J5)
* Panneau v2 (Lot 1) : rendu Markdown, largeur réglable à la souris
* (persistée), thème clair/sombre aligné sur l'app hôte
* (html[data-theme], repli prefers-color-scheme, suivi en direct).
* Bouton « Console ↗ » (barre des discussions) : ouvre la console web
* du même service (agent.dracodev.net) dans un nouvel onglet, sur la
* discussion courante (?disc=) et avec l'app d'origine (?app=),
* déduite du sous-domaine de l'hôte.
* États honnêtes : service injoignable / session non reconnue / LLM non
* configuré / exécuteur non configuré sont affichés tels quels. */
(function () {
"use strict";
var API = "https://agent.dracodev.net";
var convId = null;
var convKind = null; // 'principale' | 'parallele'
var convTitre = "";
var etatConnu = false;
var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null;
var titreEl = null, tiroirEl = null;
function domaine() {
try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; }
}
function stockerConv() {
try {
if (convId) sessionStorage.setItem("draco_panneau_conv", convId);
else sessionStorage.removeItem("draco_panneau_conv");
} catch (e) { /* stockage indisponible */ }
}
function convStockee() {
try { return sessionStorage.getItem("draco_panneau_conv") || null; }
catch (e) { return null; }
}
/* ── Console web (même service, nouvel onglet) ─────────────── */
function appHote() {
var h = (window.location.hostname || "").split(".")[0];
var correspondance = { tiktak: "tiktak", flowdeck: "flowdeck",
kanbante: "kanbante", og: "obsigate" };
return correspondance[h] || null;
}
function ouvrirConsole() {
var params = [];
var id = convId || convStockee();
if (id) params.push("disc=" + encodeURIComponent(id));
var app = appHote();
if (app) params.push("app=" + encodeURIComponent(app));
window.open(API + "/" + (params.length ? "?" + params.join("&") : ""),
"_blank", "noopener");
}
/* ── Thème : suivre l'app hôte ─────────────────────────────── */
function themeHote() {
var t = document.documentElement.getAttribute("data-theme");
if (t === "dark" || t === "light") return t;
if (document.documentElement.classList.contains("dark") ||
document.body.classList.contains("dark")) return "dark";
try {
if (window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark";
} catch (e) { /* pas de matchMedia */ }
return "light";
}
function appliquerTheme() {
if (root) root.setAttribute("data-draco-theme", themeHote());
}
function surveillerTheme() {
try {
new MutationObserver(appliquerTheme).observe(document.documentElement,
{ attributes: true, attributeFilter: ["data-theme", "class"] });
window.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", appliquerTheme);
} catch (e) { /* surveillance facultative */ }
}
/* ── Largeur réglable ──────────────────────────────────────── */
function largeurStockee() {
try {
var v = parseInt(localStorage.getItem("draco_panneau_largeur") || "", 10);
return isNaN(v) ? null : v;
} catch (e) { return null; }
}
function appliquerLargeur() {
if (!root) return;
var v = largeurStockee();
root.style.width = v ? Math.min(v, Math.round(window.innerWidth * 0.96)) + "px" : "";
}
function brancherPoignee(poignee) {
poignee.addEventListener("pointerdown", function (ev) {
if (window.innerWidth <= 700) return; // mobile : pleine largeur
ev.preventDefault();
var x0 = ev.clientX, w0 = root.getBoundingClientRect().width;
function bouger(e2) {
var w = Math.max(300, Math.min(760, w0 + (x0 - e2.clientX)));
root.style.width = w + "px";
}
function lacher(e2) {
document.removeEventListener("pointermove", bouger);
document.removeEventListener("pointerup", lacher);
var w = Math.round(root.getBoundingClientRect().width);
try { localStorage.setItem("draco_panneau_largeur", String(w)); } catch (e) { /* ok */ }
}
document.addEventListener("pointermove", bouger);
document.addEventListener("pointerup", lacher);
});
poignee.addEventListener("dblclick", function () {
try { localStorage.removeItem("draco_panneau_largeur"); } catch (e) { /* ok */ }
appliquerLargeur();
});
}
function construire() {
root = document.createElement("aside");
root.className = "draco-panneau";
root.setAttribute("aria-label", "Spark, l'agent commun");
root.innerHTML =
'<div class="draco-poignee" title="Glisser pour régler la largeur — double-clic : largeur par défaut"></div>' +
'<div class="draco-panneau-hd">' +
' <span style="font-size:19px;color:#7c5cd6">✦</span>' +
' <div><div class="titre">Spark — agent commun</div>' +
' <div class="soustitre">Service d\'agents dracodev</div></div>' +
' <button type="button" class="fermer" aria-label="Fermer le panneau">✕</button>' +
'</div>' +
'<div class="draco-panneau-hd draco-panneau-etat" style="padding-top:8px;padding-bottom:8px">' +
' <span class="point"></span><span class="txt">Connexion au service…</span>' +
'</div>' +
'<div class="draco-disc-bar">' +
' <span class="d-titre">Discussion principale</span>' +
' <button type="button" class="d-btn" data-disc="liste">☰ Discussions</button>' +
' <button type="button" class="d-btn" data-disc="nouvelle">+ Nouvelle</button>' +
' <button type="button" class="d-btn" data-disc="console" title="Ouvrir la console Spark (agent.dracodev.net) dans un nouvel onglet, sur cette discussion">Console ↗</button>' +
'</div>' +
'<div class="draco-discussions" hidden></div>' +
'<div class="draco-panneau-outils">' +
' <button type="button" data-outil="aujourdhui">☀ Aujourd\'hui</button>' +
' <button type="button" data-outil="approbations">Approbations</button>' +
' <button type="button" data-outil="missions">🚀 Missions</button>' +
'</div>' +
'<div class="draco-panneau-msgs"></div>' +
'<div class="draco-panneau-ft"><form class="draco-panneau-form">' +
' <input type="text" placeholder="Écrire à Spark…" autocomplete="off" aria-label="Message à Spark">' +
' <button type="submit">Envoyer</button></form>' +
' <div class="draco-panneau-note">Conversation réelle, sous-agents (J3) et missions longues (J5) vers les exécuteurs configurés ; toute écriture dans les apps passe par une approbation.</div>' +
'</div>';
document.body.appendChild(root);
msgsEl = root.querySelector(".draco-panneau-msgs");
etatEl = root.querySelector(".draco-panneau-etat");
inputEl = root.querySelector("input");
sendBtn = root.querySelector("button[type=submit]");
titreEl = root.querySelector(".d-titre");
tiroirEl = root.querySelector(".draco-discussions");
root.querySelector(".fermer").addEventListener("click", fermer);
root.querySelector('[data-disc="liste"]').addEventListener("click", basculerTiroir);
root.querySelector('[data-disc="nouvelle"]').addEventListener("click", nouvelleDiscussion);
root.querySelector('[data-disc="console"]').addEventListener("click", ouvrirConsole);
root.querySelector('[data-outil="aujourdhui"]').addEventListener("click", chargerAujourdhui);
root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations);
root.querySelector('[data-outil="missions"]').addEventListener("click", ouvrirMissions);
root.querySelector("form").addEventListener("submit", envoyer);
brancherPoignee(root.querySelector(".draco-poignee"));
appliquerTheme();
surveillerTheme();
appliquerLargeur();
window.addEventListener("resize", appliquerLargeur);
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape" && root.classList.contains("open")) fermer();
});
}
function ajouterMsg(kind, texte) {
var d = document.createElement("div");
d.className = "draco-msg " + kind;
d.textContent = texte;
msgsEl.appendChild(d);
msgsEl.scrollTop = msgsEl.scrollHeight;
return d;
}
/* Message agent rendu en Markdown : le HTML vient du service
* (rendu + sanitizer côté serveur) ; repli texte si absent. */
function ajouterMsgMd(texte, html) {
var d = document.createElement("div");
d.className = "draco-msg agent md";
if (html) d.innerHTML = html;
else d.textContent = texte;
msgsEl.appendChild(d);
msgsEl.scrollTop = msgsEl.scrollHeight;
return d;
}
function majEtat(cls, texte) {
etatEl.className = "draco-panneau-hd draco-panneau-etat " + cls;
etatEl.querySelector(".txt").textContent = texte;
}
function majTitre() {
titreEl.textContent = convTitre || "Discussion principale";
titreEl.title = titreEl.textContent;
}
function verifierIdentite() {
if (etatConnu) return;
fetch(API + "/api/v1/me", { credentials: "include" })
.then(function (r) {
if (r.status === 401) {
majEtat("err", "Session partagée non reconnue");
ajouterMsg("sys", "Le service ne reconnaît pas ta session. Connecte-toi à ObsiGate (session unique draco_session), puis rouvre le panneau. Si le SSO n'est pas encore activé, c'est l'étape d'activation du programme.");
etatConnu = true;
return null;
}
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then(function (me) {
if (!me) return;
etatConnu = true;
majEtat("ok", "Connecté au service — " + (me.name || ("utilisateur " + me.sub)));
})
.catch(function () {
majEtat("err", "Service d'agents injoignable");
ajouterMsg("sys", "Le service d'agents (agent.dracodev.net) ne répond pas. Vérifie que le conteneur dracodev-agent est démarré.");
});
}
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
});
}
/* ── Discussions (façon Muse) ────────────────────────────── */
function chargerListeDiscussions() {
return fetch(API + "/api/v1/conversations?domain=" + encodeURIComponent(domaine()),
{ credentials: "include" })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); });
}
function demarrerDiscussions() {
chargerListeDiscussions()
.then(function (d) {
var precedente = convStockee();
var candidates = (d.paralleles || []).concat(d.principale ? [d.principale] : []);
var trouvee = candidates.filter(function (c) { return c.id === precedente; })[0];
var active = trouvee || d.principale;
if (!active) return;
convId = active.id; convKind = active.kind; convTitre = active.title;
stockerConv(); majTitre();
chargerHistorique();
})
.catch(function () {
ajouterMsg("sys", "Discussions : service injoignable pour le moment.");
});
}
function basculerDiscussion(c) {
convId = c.id; convKind = c.kind; convTitre = c.title;
stockerConv(); majTitre();
tiroirEl.hidden = true;
msgsEl.innerHTML = "";
ajouterMsg("sys", (c.kind === "principale"
? "Discussion principale — le fil sans fin."
: "Discussion parallèle : " + c.title) + " · domaine : " + domaine() + ".");
chargerHistorique();
inputEl.focus();
}
function nouvelleDiscussion() {
fetch(API + "/api/v1/conversations", {
method: "POST", credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ domain: domaine() })
})
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.then(function (c) {
basculerDiscussion(c);
ajouterMsg("sys", "Nouvelle discussion parallèle créée — elle prendra le titre de ton premier message.");
})
.catch(function () { ajouterMsg("sys", "Création impossible (service injoignable ?)."); });
}
function actionDiscussion(id, action) {
var methode = action === "supprimer" ? "DELETE" : "POST";
var url = API + "/api/v1/conversations/" + id + (action === "supprimer" ? "" : "/" + action);
return fetch(url, { method: methode, credentials: "include" })
.then(function (r) {
if (!r.ok) return r.json().catch(function () { return null; }).then(function (e) {
throw new Error(e && e.detail ? e.detail : "HTTP " + r.status);
});
return r.json();
});
}
function ligneDiscussion(c, actions) {
var ligne = document.createElement("div");
ligne.className = "d-item" + (c.id === convId ? " actif" : "");
var meta = document.createElement("div");
meta.className = "d-meta";
var quand = "";
try {
quand = new Date(c.updated_at * 1000).toLocaleDateString("fr-CA",
{ day: "numeric", month: "short" });
} catch (e) { /* date illisible */ }
meta.textContent = (c.kind === "principale" ? "📌 " : "") + c.title;
ligne.appendChild(meta);
var sous = document.createElement("div");
sous.className = "d-sous";
sous.textContent = c.nb_messages + " message(s)" + (quand ? " · " + quand : "") +
(c.kind === "principale" ? " · perpétuelle" : "");
ligne.appendChild(sous);
meta.addEventListener("click", function () { basculerDiscussion(c); });
sous.addEventListener("click", function () { basculerDiscussion(c); });
if (actions && actions.length) {
var btns = document.createElement("div");
btns.className = "d-actions";
actions.forEach(function (a) {
var b = document.createElement("button");
b.type = "button"; b.textContent = a.label;
if (a.danger) b.className = "danger";
b.addEventListener("click", function (ev) {
ev.stopPropagation();
if (a.confirmer && !window.confirm(a.confirmer)) return;
actionDiscussion(c.id, a.action)
.then(function () {
if (c.id === convId && a.action !== "desarchiver") {
chargerListeDiscussions().then(function (d) {
if (d.principale) {
basculerDiscussion(d.principale);
ajouterMsg("sys", a.action === "supprimer"
? "Discussion supprimée — retour à la discussion principale."
: "Discussion archivée — retour à la discussion principale.");
}
});
}
remplirTiroir();
})
.catch(function (e) { ajouterMsg("sys", "Action refusée : " + e.message); });
});
btns.appendChild(b);
});
ligne.appendChild(btns);
}
return ligne;
}
function remplirTiroir() {
tiroirEl.innerHTML = "<div class='d-chargement'>Chargement des discussions…</div>";
chargerListeDiscussions()
.then(function (d) {
tiroirEl.innerHTML = "";
var h = document.createElement("div");
h.className = "d-section";
h.textContent = "Discussion principale";
tiroirEl.appendChild(h);
if (d.principale) tiroirEl.appendChild(ligneDiscussion(d.principale, []));
h = document.createElement("div");
h.className = "d-section";
h.textContent = "Discussions parallèles";
tiroirEl.appendChild(h);
if (!d.paralleles.length) {
var vide = document.createElement("div");
vide.className = "d-vide";
vide.textContent = "Aucune pour le moment — bouton « + Nouvelle ».";
tiroirEl.appendChild(vide);
}
d.paralleles.forEach(function (c) {
tiroirEl.appendChild(ligneDiscussion(c, [
{ label: "Archiver", action: "archive" },
{ label: "Supprimer", action: "supprimer", danger: true,
confirmer: "Supprimer définitivement la discussion « " + c.title + " » ?" }
]));
});
if (d.archivees && d.archivees.length) {
h = document.createElement("div");
h.className = "d-section";
h.textContent = "Archivées";
tiroirEl.appendChild(h);
d.archivees.forEach(function (c) {
tiroirEl.appendChild(ligneDiscussion(c, [
{ label: "Désarchiver", action: "desarchiver" },
{ label: "Supprimer", action: "supprimer", danger: true,
confirmer: "Supprimer définitivement la discussion « " + c.title + " » ?" }
]));
});
}
})
.catch(function () {
tiroirEl.innerHTML = "<div class='d-chargement'>Discussions : service injoignable.</div>";
});
}
function basculerTiroir() {
tiroirEl.hidden = !tiroirEl.hidden;
if (!tiroirEl.hidden) remplirTiroir();
}
function chargerHistorique() {
if (!convId) return;
fetch(API + "/api/v1/conversations/" + convId, { credentials: "include" })
.then(function (r) {
if (r.status === 404) { convId = null; stockerConv(); return null; }
if (!r.ok) return null;
return r.json();
})
.then(function (d) {
if (!d) return;
convKind = d.kind; convTitre = d.title || convTitre;
majTitre();
if (!msgsEl.querySelector(".draco-msg")) {
ajouterMsg("sys", (d.kind === "principale"
? "Discussion principale — le fil sans fin."
: "Discussion parallèle : " + (d.title || "")) + " · domaine : " + domaine() + ".");
}
var sys = msgsEl.querySelector(".draco-msg.sys");
(d.messages || []).forEach(function (m) {
var el = m.role === "user"
? ajouterMsg("user", m.content)
: ajouterMsgMd(m.content, m.html);
if (sys) { msgsEl.insertBefore(el, sys.nextSibling); sys = el; }
});
msgsEl.scrollTop = msgsEl.scrollHeight;
})
.catch(function () { /* historique indisponible : on repart à neuf */ });
}
function chargerAujourdhui() {
var bloc = document.createElement("div");
bloc.className = "draco-auj";
bloc.innerHTML = "<h5>Aujourd'hui — vu du service d'agents</h5><div class='etat-source'>Chargement…</div>";
msgsEl.appendChild(bloc); msgsEl.scrollTop = msgsEl.scrollHeight;
fetch(API + "/api/v1/aujourdhui", { credentials: "include" })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.then(function (d) {
var h = "<h5>Aujourd'hui — " + esc(d.date) + " · domaine " + esc(d.domain) + "</h5>";
var t = d.sources.tiktak;
h += "<h5>Tâches du jour — TikTak</h5>";
if (t.status === "ok") {
if (!t.tasks.length) h += "<div class='etat-source'>Aucune tâche échue aujourd'hui. 🎉</div>";
else h += "<ul>" + t.tasks.map(function (x) { return "<li>" + esc(x.title) + "</li>"; }).join("") + "</ul>";
} else if (t.status === "non_configure") {
h += "<div class='etat-source'>Source non configurée : il manque la clé tck_ du service (voir le .env du conteneur agent).</div>";
} else if (t.status === "non_autorise") {
h += "<div class='etat-source'>Source non autorisée : la clé du service est refusée par TikTak (clé révoquée ?).</div>";
} else {
h += "<div class='etat-source'>Source TikTak " + esc(t.status) + ".</div>";
}
var k = d.sources.kanbante;
h += "<h5>Cartes en Révision — Kanbante</h5>";
if (k.status === "ok") {
if (!k.review.length) h += "<div class='etat-source'>Rien à valider. 🎉</div>";
else h += "<ul>" + k.review.map(function (x) { return "<li>#" + esc(x.number) + " — " + esc(x.title) + "</li>"; }).join("") + "</ul>";
} else if (k.status === "non_autorise") {
h += "<div class='etat-source'>Source non autorisée : Kanbante exige un jeton d'API pour le service — il arrive avec Kanbante 0.25.0, puis AGENT_MCP_KANBANTE_KEY dans le .env de l'agent.</div>";
} else {
h += "<div class='etat-source'>Source Kanbante " + esc(k.status) + ".</div>";
}
bloc.innerHTML = h;
msgsEl.scrollTop = msgsEl.scrollHeight;
})
.catch(function () {
bloc.innerHTML = "<h5>Aujourd'hui</h5><div class='etat-source'>Service injoignable.</div>";
});
}
function chargerApprobations() {
fetch(API + "/api/v1/approvals?status=pending", { credentials: "include" })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.then(function (d) {
if (!d.approvals.length) { ajouterMsg("sys", "Aucune approbation en attente."); return; }
d.approvals.forEach(function (a) {
var bloc = document.createElement("div");
bloc.className = "draco-appro";
bloc.innerHTML = "<div class='titre'>Approbation demandée — " + esc(a.action) + "</div>" +
"<div class='resume'>" + esc(a.summary) + "</div>" +
"<div class='btns'><button type='button' class='go'>Approuver</button>" +
"<button type='button' class='non'>Rejeter</button></div>";
bloc.querySelector(".go").addEventListener("click", function () { decider(a.id, "approved", bloc); });
bloc.querySelector(".non").addEventListener("click", function () { decider(a.id, "rejected", bloc); });
msgsEl.appendChild(bloc);
});
msgsEl.scrollTop = msgsEl.scrollHeight;
})
.catch(function () { ajouterMsg("sys", "Approbations : service injoignable."); });
}
function decider(id, decision, bloc) {
fetch(API + "/api/v1/approvals/" + id + "/decide", {
method: "POST", credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ decision: decision })
})
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.then(function (d) {
bloc.style.opacity = ".55";
bloc.querySelector(".btns").innerHTML = "<span style='font-size:12.5px'>Décision enregistrée : " + esc(d.status) + "</span>";
})
.catch(function () { ajouterMsg("sys", "La décision n'a pas pu être envoyée (service injoignable ?)."); });
}
/* ── Missions longues (J5) ───────────────────────────────────
* Sélecteur d'exécuteur alimenté par le registre du service :
* les externes non configurés sont montrés mais désactivés —
* jamais un lancement qui simule. La liste se rafraîchit à
* l'ouverture et après chaque lancement/annulation. */
var STATUTS_MISSION = {
queued: "En file", running: "En cours…", paused_question: "Question en attente",
succeeded: "Terminée", failed: "Échouée", cancelled: "Annulée"
};
function ouvrirMissions() {
var bloc = document.createElement("div");
bloc.className = "draco-missions";
bloc.innerHTML =
"<h5>Missions longues</h5>" +
"<label>Exécuteur</label><select class='m-exec'><option>Chargement…</option></select>" +
"<label>Titre</label><input type='text' class='m-titre' placeholder='Ex. Veille des projets du matin'>" +
"<label>Consigne</label><textarea class='m-prompt' rows='3' placeholder='Ce que l\u2019exécuteur doit faire…'></textarea>" +
"<label>Carte Kanbante liée (n°, optionnel)</label><input type='number' class='m-carte' min='1' placeholder='—'>" +
"<div class='btns'><button type='button' class='go'>Lancer la mission</button>" +
"<button type='button' class='rafraichir'>Rafraîchir la liste</button></div>" +
"<div class='m-liste'><div class='etat-source'>Chargement des missions…</div></div>";
msgsEl.appendChild(bloc);
msgsEl.scrollTop = msgsEl.scrollHeight;
fetch(API + "/api/v1/executors", { credentials: "include" })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.then(function (d) {
var sel = bloc.querySelector(".m-exec");
sel.innerHTML = "";
d.executors.forEach(function (e) {
if (e.family !== "sous-agent" && e.family !== "externe") return;
var opt = document.createElement("option");
opt.value = e.id;
opt.textContent = e.name + (e.family === "sous-agent" ? " (sous-agent)" : "") +
(e.status === "non_configure" ? " — non configuré" : "");
if (e.status === "non_configure") opt.disabled = true;
sel.appendChild(opt);
});
if (!sel.options.length) sel.innerHTML = "<option value=''>Aucun exécuteur lançable</option>";
})
.catch(function () { bloc.querySelector(".m-exec").innerHTML = "<option value=''>Registre injoignable</option>"; });
bloc.querySelector(".go").addEventListener("click", function () { lancerMission(bloc); });
bloc.querySelector(".rafraichir").addEventListener("click", function () { chargerMissions(bloc); });
chargerMissions(bloc);
}
function chargerMissions(bloc) {
var zone = bloc.querySelector(".m-liste");
fetch(API + "/api/v1/missions", { credentials: "include" })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.then(function (d) {
if (!d.missions.length) { zone.innerHTML = "<div class='etat-source'>Aucune mission pour le moment.</div>"; return; }
zone.innerHTML = "";
d.missions.slice(0, 10).forEach(function (m) {
var ligne = document.createElement("div");
ligne.className = "m-item";
var entete = document.createElement("div");
entete.className = "m-entete";
entete.innerHTML = "<b>" + esc(m.title) + "</b>" +
"<span class='m-statut st-" + esc(m.status) + "'>" + esc(STATUTS_MISSION[m.status] || m.status) + "</span>";
ligne.appendChild(entete);
var meta = document.createElement("div");
meta.className = "m-meta";
meta.textContent = m.executor +
(m.meta && m.meta.duree_s ? " · " + m.meta.duree_s + " s" : "") +
(m.meta && m.meta.cout ? " · coût : " + m.meta.cout : "");
ligne.appendChild(meta);
if (m.result) {
var rapport = document.createElement("div");
rapport.className = "m-rapport md";
if (m.result_html) rapport.innerHTML = m.result_html;
else rapport.textContent = String(m.result).slice(0, 1200);
ligne.appendChild(rapport);
}
if (m.status === "queued" || m.status === "running" || m.status === "paused_question") {
var btn = document.createElement("button");
btn.type = "button"; btn.className = "m-annuler"; btn.textContent = "Annuler";
btn.addEventListener("click", function () {
fetch(API + "/api/v1/missions/" + m.id + "/cancel", {
method: "POST", credentials: "include",
headers: { "Content-Type": "application/json" }, body: "{}"
}).then(function () { chargerMissions(bloc); })
.catch(function () { ajouterMsg("sys", "Annulation non envoyée (service injoignable ?)."); });
});
ligne.appendChild(btn);
}
zone.appendChild(ligne);
});
})
.catch(function () { zone.innerHTML = "<div class='etat-source'>Missions : service injoignable.</div>"; });
}
function lancerMission(bloc) {
var executor = bloc.querySelector(".m-exec").value;
var titre = bloc.querySelector(".m-titre").value.trim();
var prompt = bloc.querySelector(".m-prompt").value.trim();
var carte = bloc.querySelector(".m-carte").value;
if (!executor) { ajouterMsg("sys", "Choisis un exécuteur configuré pour lancer une mission."); return; }
if (!titre || !prompt) { ajouterMsg("sys", "Mission : il faut un titre et une consigne."); return; }
var corps = { executor: executor, title: titre, prompt: prompt, domain: domaine() };
if (carte) corps.kanbante_number = parseInt(carte, 10);
fetch(API + "/api/v1/missions", {
method: "POST", credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(corps)
})
.then(function (r) {
if (!r.ok) return r.json().catch(function () { return null; }).then(function (e) {
throw new Error(e && e.detail ? e.detail : "HTTP " + r.status);
});
return r.json();
})
.then(function () {
ajouterMsg("sys", "Mission « " + titre + " » lancée — suis-la dans la liste des missions.");
chargerMissions(bloc);
})
.catch(function (e) { ajouterMsg("sys", "Lancement refusé : " + e.message); });
}
function envoyer(ev) {
ev.preventDefault();
var texte = inputEl.value.trim();
if (!texte) return;
inputEl.value = "";
ajouterMsg("user", texte);
sendBtn.disabled = true;
var attente = ajouterMsg("agent", "Spark réfléchit…");
fetch(API + "/api/v1/chat", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: texte, domain: domaine(), conversation_id: convId })
})
.then(function (r) {
if (r.status === 401) {
attente.textContent = "Le service ne reconnaît pas ta session partagée (401). Connecte-toi à ObsiGate puis réessaie.";
majEtat("err", "Session partagée non reconnue");
return null;
}
if (!r.ok) {
return r.json().catch(function () { return null; }).then(function (e) {
attente.textContent = (e && e.detail) ? e.detail :
"Le service a répondu HTTP " + r.status + " — réessaie dans un instant.";
return null;
});
}
return r.json();
})
.then(function (d) {
if (!d) return;
convId = d.conversation_id;
convKind = d.kind || convKind;
if (d.title) convTitre = d.title;
stockerConv(); majTitre();
if (d.html) {
attente.className = "draco-msg agent md";
attente.innerHTML = d.html;
} else {
attente.textContent = d.reply;
}
msgsEl.scrollTop = msgsEl.scrollHeight;
if (d.tools_used && d.tools_used.length) {
ajouterMsg("sys", "Outils utilisés : " + d.tools_used.join(", ") + " · modèle : " + (d.model || "?"));
}
majEtat("ok", "Connecté au service — domaine " + d.domain);
})
.catch(function () {
attente.textContent = "Service injoignable. Vérifie que le conteneur dracodev-agent est démarré, puis réessaie.";
majEtat("err", "Service d'agents injoignable");
})
.finally(function () { sendBtn.disabled = false; inputEl.focus(); });
}
function ouvrir() {
if (!root) { construire(); demarrerDiscussions(); }
root.classList.add("open");
appliquerTheme();
verifierIdentite();
setTimeout(function () { inputEl.focus(); }, 300);
}
function fermer() { if (root) root.classList.remove("open"); }
window.DRACO_PANNEAU = {
open: ouvrir,
close: fermer,
toggle: function () {
if (root && root.classList.contains("open")) fermer(); else ouvrir();
}
};
})();