@@ -86,7 +87,11 @@
if (act === "palette") { var p = document.querySelector(".palette-trigger"); if (p) { p.click(); } else { dire("Recherche & commandes : Ctrl+K."); } return; }
if (act === "alertes") { dire("Alertes unifiées : arrive avec la fédération (programme Fusion)."); return; }
if (act === "menu") { dire("Menu commun : arrive avec la fédération (programme Fusion)."); return; }
- if (act === "spark") { dire("Spark, l'agent commun : arrive en phase 4 du programme Fusion."); return; }
+ if (act === "spark") {
+ if (window.DRACO_PANNEAU) { window.DRACO_PANNEAU.toggle(); }
+ else { dire("Spark : le panneau n'a pas pu se charger."); }
+ return;
+ }
if (act === "rappel-entete") {
var bar = document.querySelector(".draco-bar");
if (bar) bar.style.display = "";
@@ -96,3 +101,4 @@
};
+
diff --git a/static/css/draco-panneau.css b/static/css/draco-panneau.css
new file mode 100644
index 0000000..743bc07
--- /dev/null
+++ b/static/css/draco-panneau.css
@@ -0,0 +1,83 @@
+/* Panneau Spark — branchement au service d'agents (Fusion, jalon J1).
+ Composant d'intégration TikTak : le panneau définitif sera vendangé
+ depuis dracodev-commons ; celui-ci prouve le chemin panneau → service. */
+.draco-panneau {
+ position: fixed; top: 0; right: 0; height: 100dvh; width: 388px; max-width: 94vw;
+ background: #fbfbfa; color: #242624; z-index: 400;
+ box-shadow: -12px 0 36px rgb(0 0 0 / .22);
+ display: flex; flex-direction: column;
+ transform: translateX(105%); transition: transform .28s ease;
+ font: 14px/1.5 system-ui, "Segoe UI", sans-serif;
+}
+.draco-panneau.open { transform: translateX(0); }
+.draco-panneau-hd {
+ display: flex; align-items: center; gap: 9px;
+ padding: 13px 15px; border-bottom: 1px solid #e4e5e0; background: #fff;
+}
+.draco-panneau-hd .titre { font-weight: 700; font-size: 15px; }
+.draco-panneau-hd .soustitre { font-size: 12px; color: #6c7268; }
+.draco-panneau-hd .fermer {
+ margin-left: auto; border: 0; background: #eef0ea; border-radius: 8px;
+ width: 30px; height: 30px; cursor: pointer; font-size: 14px;
+}
+.draco-panneau-etat { display: flex; align-items: center; gap: 7px; font-size: 12.5px; }
+.draco-panneau-etat .point { width: 9px; height: 9px; border-radius: 50%; background: #b9bdb2; flex: none; }
+.draco-panneau-etat.ok .point { background: #2ea36b; }
+.draco-panneau-etat.err .point { background: #cf4b32; }
+.draco-panneau-outils { display: flex; gap: 8px; padding: 9px 15px; border-bottom: 1px solid #e4e5e0; background: #fff; }
+.draco-panneau-outils button {
+ border: 1px solid #d6d8d1; background: #f4f5f1; border-radius: 999px;
+ padding: 5px 12px; font: 600 12.5px system-ui; cursor: pointer;
+}
+.draco-panneau-outils button:hover { background: #e9ebe4; }
+.draco-appro {
+ align-self: stretch; background: #fdf6ec; border: 1.6px solid #e8a13c;
+ border-radius: 12px; padding: 10px 12px;
+}
+.draco-appro .titre { font-weight: 700; font-size: 12px; color: #9a6a1c; text-transform: uppercase; letter-spacing: .4px; }
+.draco-appro .resume { margin: 5px 0 8px; }
+.draco-appro .btns { display: flex; gap: 7px; }
+.draco-appro .btns button { border: 0; border-radius: 8px; padding: 6px 11px; font: 600 12.5px system-ui; cursor: pointer; background: #eef0ea; }
+.draco-appro .btns button.go { background: #2ea36b; color: #fff; }
+.draco-auj { align-self: stretch; background: #fff; border: 1px solid #e4e5e0; border-radius: 12px; padding: 10px 12px; }
+.draco-auj h5 { font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: #6c7268; margin: 6px 0 4px; }
+.draco-auj h5:first-child { margin-top: 0; }
+.draco-auj ul { margin: 0; padding-left: 17px; }
+.draco-auj .etat-source { color: #6c7268; font-size: 12.5px; }
+.draco-panneau-msgs { flex: 1; overflow-y: auto; padding: 14px 15px; display: flex; flex-direction: column; gap: 9px; }
+.draco-msg { max-width: 88%; padding: 9px 12px; border-radius: 13px; white-space: pre-wrap; word-wrap: break-word; }
+.draco-msg.user { align-self: flex-end; background: #2f6fed; color: #fff; border-bottom-right-radius: 4px; }
+.draco-msg.agent { align-self: flex-start; background: #eef0ea; border-bottom-left-radius: 4px; }
+.draco-msg.sys { align-self: center; background: none; color: #6c7268; font-size: 12.5px; text-align: center; max-width: 96%; }
+.draco-panneau-ft { border-top: 1px solid #e4e5e0; padding: 11px 12px; background: #fff; }
+.draco-panneau-form { display: flex; gap: 8px; }
+.draco-panneau-form input {
+ flex: 1; border: 1px solid #d6d8d1; border-radius: 10px; padding: 9px 11px; font: inherit;
+}
+.draco-panneau-form button {
+ border: 0; border-radius: 10px; background: #7c5cd6; color: #fff;
+ padding: 9px 14px; font: inherit; font-weight: 600; cursor: pointer;
+}
+.draco-panneau-form button:disabled { opacity: .5; cursor: default; }
+.draco-panneau-note { font-size: 11.5px; color: #8a8f84; margin-top: 8px; text-align: center; }
+.draco-missions { align-self: stretch; background: #fff; border: 1px solid #e4e5e0; border-radius: 12px; padding: 10px 12px; }
+.draco-missions h5 { font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: #6c7268; margin: 0 0 6px; }
+.draco-missions label { display: block; font-size: 12px; font-weight: 600; color: #6c7268; margin: 8px 0 3px; }
+.draco-missions select, .draco-missions input, .draco-missions textarea {
+ width: 100%; box-sizing: border-box; border: 1px solid #d6d8d1; border-radius: 8px;
+ padding: 7px 9px; font: inherit; font-size: 13px; background: #fff;
+}
+.draco-missions .btns { display: flex; gap: 7px; margin-top: 10px; }
+.draco-missions .btns button { border: 0; border-radius: 8px; padding: 6px 11px; font: 600 12.5px system-ui; cursor: pointer; background: #eef0ea; }
+.draco-missions .btns button.go { background: #7c5cd6; color: #fff; }
+.draco-missions .m-liste { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }
+.draco-missions .etat-source { color: #6c7268; font-size: 12.5px; }
+.draco-missions .m-item { border: 1px solid #e4e5e0; border-radius: 10px; padding: 8px 10px; }
+.draco-missions .m-entete { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
+.draco-missions .m-statut { font-size: 11.5px; font-weight: 700; color: #6c7268; white-space: nowrap; }
+.draco-missions .m-statut.st-succeeded { color: #2ea36b; }
+.draco-missions .m-statut.st-failed { color: #cf4b32; }
+.draco-missions .m-statut.st-running, .draco-missions .m-statut.st-paused_question { color: #9a6a1c; }
+.draco-missions .m-meta { font-size: 12px; color: #6c7268; }
+.draco-missions .m-rapport { font-size: 12.5px; margin-top: 5px; white-space: pre-wrap; word-wrap: break-word; }
+.draco-missions .m-annuler { margin-top: 6px; border: 0; border-radius: 7px; padding: 4px 9px; font: 600 12px system-ui; cursor: pointer; background: #f6e3de; color: #a03a28; }
diff --git a/static/js/draco-panneau.js b/static/js/draco-panneau.js
new file mode 100644
index 0000000..1eeeb55
--- /dev/null
+++ b/static/js/draco-panneau.js
@@ -0,0 +1,380 @@
+/* Panneau Spark — branchement au service d'agents (Fusion, J1 → J5).
+ *
+ * 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
+ * GET /api/v1/conversations/… — historique de la conversation
+ * GET /api/v1/executors — registre (noyau, sous-agents J3,
+ * externes J4 : état de configuration)
+ * POST /api/v1/missions — missions longues (J5) : lancement,
+ * suivi, rapport, annulation
+ * É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;
+ try { convId = sessionStorage.getItem("draco_panneau_conv") || null; } catch (e) { /* stockage indispo */ }
+ var etatConnu = false;
+ var historiqueCharge = false;
+ var root = null, msgsEl = null, etatEl = null, inputEl = null, sendBtn = null;
+
+ function domaine() {
+ try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; }
+ }
+
+ function construire() {
+ root = document.createElement("aside");
+ root.className = "draco-panneau";
+ root.setAttribute("aria-label", "Spark, l'agent commun");
+ root.innerHTML =
+ '
' +
+ '
✦ ' +
+ '
Spark — agent commun
' +
+ '
Service d\'agents dracodev
' +
+ '
✕ ' +
+ '
' +
+ '
' +
+ ' Connexion au service… ' +
+ '
' +
+ '
' +
+ ' ☀ Aujourd\'hui ' +
+ ' Approbations ' +
+ ' 🚀 Missions ' +
+ '
' +
+ '
' +
+ '
' +
+ '
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.
' +
+ '
';
+ 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]");
+ root.querySelector(".fermer").addEventListener("click", fermer);
+ 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);
+ document.addEventListener("keydown", function (ev) {
+ if (ev.key === "Escape" && root.classList.contains("open")) fermer();
+ });
+ ajouterMsg("sys", "Conversation avec le service d'agents — domaine : " + domaine() + ".");
+ }
+
+ 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;
+ }
+
+ function majEtat(cls, texte) {
+ etatEl.className = "draco-panneau-hd draco-panneau-etat " + cls;
+ etatEl.querySelector(".txt").textContent = texte;
+ }
+
+ 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 { "&": "&", "<": "<", ">": ">", '"': """ }[c];
+ });
+ }
+
+ function chargerAujourdhui() {
+ var bloc = document.createElement("div");
+ bloc.className = "draco-auj";
+ bloc.innerHTML = "
Aujourd'hui — vu du service d'agents Chargement…
";
+ 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 = "
Aujourd'hui — " + esc(d.date) + " · domaine " + esc(d.domain) + " ";
+ var t = d.sources.tiktak;
+ h += "
Tâches du jour — TikTak ";
+ if (t.status === "ok") {
+ if (!t.tasks.length) h += "
Aucune tâche échue aujourd'hui. 🎉
";
+ else h += "
" + t.tasks.map(function (x) { return "" + esc(x.title) + " "; }).join("") + " ";
+ } else if (t.status === "non_configure") {
+ h += "
Source non configurée : il manque la clé tck_ du service (voir le .env du conteneur agent).
";
+ } else if (t.status === "non_autorise") {
+ h += "
Source non autorisée : la clé du service est refusée par TikTak (clé révoquée ?).
";
+ } else {
+ h += "
Source TikTak " + esc(t.status) + ".
";
+ }
+ var k = d.sources.kanbante;
+ h += "
Cartes en Révision — Kanbante ";
+ if (k.status === "ok") {
+ if (!k.review.length) h += "
Rien à valider. 🎉
";
+ else h += "
" + k.review.map(function (x) { return "#" + esc(x.number) + " — " + esc(x.title) + " "; }).join("") + " ";
+ } else if (k.status === "non_autorise") {
+ h += "
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.
";
+ } else {
+ h += "
Source Kanbante " + esc(k.status) + ".
";
+ }
+ bloc.innerHTML = h;
+ msgsEl.scrollTop = msgsEl.scrollHeight;
+ })
+ .catch(function () {
+ bloc.innerHTML = "
Aujourd'hui Service injoignable.
";
+ });
+ }
+
+ 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 = "
Approbation demandée — " + esc(a.action) + "
" +
+ "
" + esc(a.summary) + "
" +
+ "
Approuver " +
+ "Rejeter
";
+ 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 = "
Décision enregistrée : " + esc(d.status) + " ";
+ })
+ .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 =
+ "
Missions longues " +
+ "
Exécuteur Chargement… " +
+ "
Titre " +
+ "
Consigne " +
+ "
Carte Kanbante liée (n°, optionnel) " +
+ "
Lancer la mission " +
+ "Rafraîchir la liste
" +
+ "
";
+ 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 = "
Aucun exécuteur lançable ";
+ })
+ .catch(function () { bloc.querySelector(".m-exec").innerHTML = "
Registre injoignable "; });
+ 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 = "
Aucune mission pour le moment.
"; return; }
+ zone.innerHTML = "";
+ d.missions.slice(0, 10).forEach(function (m) {
+ var ligne = document.createElement("div");
+ ligne.className = "m-item";
+ var rapport = m.result ? "
" + esc(String(m.result).slice(0, 700)) + "
" : "";
+ ligne.innerHTML = "
" + esc(m.title) + " " +
+ "" + esc(STATUTS_MISSION[m.status] || m.status) + "
" +
+ "
" + esc(m.executor) +
+ (m.meta && m.meta.duree_s ? " · " + esc(m.meta.duree_s) + " s" : "") +
+ (m.meta && m.meta.cout ? " · coût : " + esc(m.meta.cout) : "") + "
" + 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 = "
Missions : service injoignable.
"; });
+ }
+
+ 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 chargerHistorique() {
+ if (historiqueCharge || !convId) return;
+ historiqueCharge = true;
+ fetch(API + "/api/v1/conversations/" + convId, { credentials: "include" })
+ .then(function (r) {
+ if (r.status === 404) {
+ convId = null;
+ try { sessionStorage.removeItem("draco_panneau_conv"); } catch (e) { /* ok */ }
+ return null;
+ }
+ if (!r.ok) return null;
+ return r.json();
+ })
+ .then(function (d) {
+ if (!d || !d.messages || !d.messages.length) return;
+ var sys = msgsEl.querySelector(".draco-msg.sys");
+ d.messages.forEach(function (m) {
+ var el = ajouterMsg(m.role === "user" ? "user" : "agent", m.content);
+ if (sys) msgsEl.insertBefore(el, sys.nextSibling), sys = el;
+ });
+ msgsEl.scrollTop = msgsEl.scrollHeight;
+ })
+ .catch(function () { /* historique indisponible : la conversation repart à neuf */ });
+ }
+
+ 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;
+ try { sessionStorage.setItem("draco_panneau_conv", convId); } catch (e) { /* ok */ }
+ attente.textContent = d.reply;
+ 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();
+ root.classList.add("open");
+ verifierIdentite();
+ chargerHistorique();
+ 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();
+ }
+ };
+})();