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 @@
'
' +
' ☀ Aujourd\'hui ' +
' Approbations ' +
+ ' 🚀 Missions ' +
'
' +
'
' +
'' +
- '
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 " +
+ "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;