From 465b72235cf9fa3a15fdb54c2a9801fd08371588 Mon Sep 17 00:00:00 2001 From: Spark Date: Thu, 22 Oct 2026 03:39:40 +0000 Subject: [PATCH] =?UTF-8?q?7.76.0=20=E2=80=94=20panneau=20Spark=20:=20sect?= =?UTF-8?q?ion=20Missions=20(J5),=20assets=20=C3=A0=20jour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Même panneau partagé que TikTak : missions longues, sélecteur d'exécuteur, rapports, annulation --- static/css/draco-panneau.css | 21 +++++++ static/js/draco-panneau.js | 119 ++++++++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/static/css/draco-panneau.css b/static/css/draco-panneau.css index 30a434f..743bc07 100644 --- a/static/css/draco-panneau.css +++ b/static/css/draco-panneau.css @@ -60,3 +60,24 @@ } .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 index 95f6778..1eeeb55 100644 --- a/static/js/draco-panneau.js +++ b/static/js/draco-panneau.js @@ -1,12 +1,16 @@ -/* Panneau Spark — branchement au service d'agents (Fusion, jalons J1-J2). +/* 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é sont affichés tels quels, jamais masqués. */ + * configuré / exécuteur non configuré sont affichés tels quels. */ (function () { "use strict"; var API = "https://agent.dracodev.net"; @@ -37,12 +41,13 @@ '
' + ' ' + ' ' + + ' ' + '
' + '
' + '
' + ' ' + '
' + - '
Jalon J2 : conversation réelle — Spark peut lire tes tâches et tes cartes via les outils des apps ; toute écriture passe par une approbation.
' + + '
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"); @@ -52,6 +57,7 @@ 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(); @@ -177,6 +183,113 @@ .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
" + + "" + + "" + + "" + + "" + + "
" + + "
" + + "
Chargement des missions…
"; + 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 = ""; + }) + .catch(function () { bloc.querySelector(".m-exec").innerHTML = ""; }); + 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;