diff --git a/CHANGELOG.md b/CHANGELOG.md index add8e06..9ddccd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.73.1**. +> [Unreleased](#unreleased). La dernière version livrée est **2.74.0**. --- @@ -14,6 +14,21 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.74.0] — 2026-10-22 + +### Ajouté + +- **Panneau Spark intégré (#218, Fusion)** — le bouton Spark de la + barre commune ouvre le panneau de l'agent commun (conversation, + Aujourd'hui, approbations, missions longues), comme dans TikTak, + Flowdeck et Kanbante : assets partagés `frontend/draco-panneau.js` + et `frontend/draco-panneau.css` vendangés à l'identique, action + `spark` branchée sur `DRACO_PANNEAU.toggle()`. La CSP nomme + `https://agent.dracodev.net` en `connect-src` (seul hôte ajouté). + Redéploiement Docker requis. + +--- + ## [2.73.1] — 2026-10-21 ### Corrigé diff --git a/README.fr.md b/README.fr.md index f4feac9..92d975a 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.73.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.74.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.73.1). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.74.0). --- -*Projet : ObsiGate | Version : 2.73.1 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.74.0 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 9225c0a..58fe4e2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.73.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.74.0-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.73.1). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.74.0). --- -*Project: ObsiGate | Version: 2.73.1 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.74.0 | Last updated: September 2026* diff --git a/VERSION b/VERSION index 50239e0..04b6fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.73.1 +2.74.0 diff --git a/backend/main.py b/backend/main.py index 782329a..b434fa2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -191,7 +191,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware): f"script-src 'self' 'nonce-{nonce}' blob: https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh https://cdn.jsdelivr.net https://static.cloudflareinsights.com; " "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com https://cdn.jsdelivr.net https://esm.sh; " "img-src 'self' data: blob:; " - "connect-src 'self' blob: https://esm.sh https://unpkg.com https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net; " + "connect-src 'self' blob: https://esm.sh https://unpkg.com https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://agent.dracodev.net; " "font-src 'self' data: https://fonts.gstatic.com https://esm.sh; " "worker-src 'self' blob:; " "frame-src 'self' blob:; " diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 1fd4e8e..aac3a23 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.73.1" +version = "2.74.0" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 1c56dc9..5a521e2 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.73.1" +version = "2.74.0" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 899d8da..228ec59 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.73.1", + "version": "2.74.0", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9e1fc2d..320d86e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.73.1 | **Dernière mise à jour :** 2026-10-21 +> **Version :** 2.74.0 | **Dernière mise à jour :** 2026-10-22 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** @@ -30,6 +30,24 @@ ## 🔵 En cours +### 218. Fusion — panneau Spark intégré (agent commun) + +- **Effort :** ~0,5 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-21 | **Statut :** 🔵 en cours +- **Fiche :** [features/fusion-panneau-spark-218.md](./features/fusion-panneau-spark-218.md) +- **Description :** le bouton Spark de la barre commune (#215) ouvre le + panneau de l'agent commun (service d'agents dracodev : conversation, + Aujourd'hui, approbations, missions longues), comme dans TikTak, + Flowdeck et Kanbante. Assets partagés vendangés à l'identique ; + la CSP nomme le service en `connect-src`. +- **Sous-tâches :** + - [x] Assets `frontend/draco-panneau.js` / `frontend/draco-panneau.css` + vendangés depuis le panneau partagé du programme + - [x] `frontend/index.html` : link CSS + script, action `spark` → + `DRACO_PANNEAU.toggle()` (repli honnête si le panneau ne charge pas) + - [x] CSP `connect-src` : `https://agent.dracodev.net` ajouté + (`backend/main.py`, seul hôte ajouté) + - [x] Tests `test_draco_barre.py` (intégration + CSP verrouillées) + ### 215. Fusion bloc 1 — barre d'apps commune et références croisées des notes - **Effort :** ~1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-20 | **Statut :** ✅ livré le 2026-10-20 (2.70.0) diff --git a/docs/features/fusion-panneau-spark-218.md b/docs/features/fusion-panneau-spark-218.md new file mode 100644 index 0000000..3ca6713 --- /dev/null +++ b/docs/features/fusion-panneau-spark-218.md @@ -0,0 +1,38 @@ +# #218 — Fusion : panneau Spark intégré (agent commun) + +**Contexte.** Le programme Fusion a livré le service d'agents commun +(agent-commun : conversation LLM, lectures/écritures des quatre +applications via approbations, sous-agents, missions longues). Trois +applications affichaient déjà son panneau partagé (TikTak, Flowdeck, +Kanbante) ; ObsiGate, pourtant émettrice de l'identité `draco_session`, +gardait le message d'attente « arrive en phase 4 » sur le bouton Spark +de sa barre commune (#215). + +**Livraison.** + +- Assets vendangés à l'identique des autres applications : + `frontend/draco-panneau.js` et `frontend/draco-panneau.css`, à la + racine de `frontend/` comme les autres assets draco (servis par le + montage `/static`, `Cache-Control: no-cache` déjà en place — aucun + mécanisme de versionnage à ajouter, `index.html` n'est pas un + gabarit Jinja). +- `frontend/index.html` : lien CSS après celui de la barre, script + différé après `draco-liens.js`, action `spark` de + `DRACO_BAR_CONFIG.onAction` branchée sur + `window.DRACO_PANNEAU.toggle()` avec repli explicite si l'asset ne + charge pas. +- CSP (`backend/main.py`, `SecurityHeadersMiddleware`) : + `https://agent.dracodev.net` ajouté à `connect-src` — le panneau + appelle le service en `fetch` avec le cookie de session ; aucun + autre hôte n'est ouvert (précédent : Flowdeck, même exception). + +**Vérifications.** Tests `tests/test_draco_barre.py` étendus +(présence et références des assets, branche spark, hôte CSP) ; +vérification visuelle et E2E à confirmer après redéploiement Docker +(le panneau, `z-index: 400`, cohabite avec des surfaces ObsiGate à +z-index plus élevé — à observer en réel). + +**Hors périmètre.** Les textes du panneau vivent dans l'asset partagé +du programme (français, identique dans les quatre applications) ; ils +ne passent pas par les locales FR/EN d'ObsiGate — écart assumé au +niveau du programme, pas de cette fiche. diff --git a/frontend/draco-panneau.css b/frontend/draco-panneau.css new file mode 100644 index 0000000..743bc07 --- /dev/null +++ b/frontend/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/frontend/draco-panneau.js b/frontend/draco-panneau.js new file mode 100644 index 0000000..1eeeb55 --- /dev/null +++ b/frontend/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…' + + '
' + + '
' + + ' ' + + ' ' + + ' ' + + '
' + + '
' + + '
' + + ' ' + + '
' + + '
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 += ""; + } 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 += ""; + } 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) + "
" + + "
" + + "
"; + 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
" + + "" + + "" + + "" + + "" + + "
" + + "
" + + "
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; + 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(); + } + }; +})(); diff --git a/frontend/index.html b/frontend/index.html index bf1bd85..bcda216 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -76,6 +76,7 @@ + + diff --git a/package.json b/package.json index 849c2cc..48795aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.73.1", + "version": "2.74.0", "description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.", "main": "patch.js", "directories": { diff --git a/tests/test_draco_barre.py b/tests/test_draco_barre.py index 7b6dccc..75c1268 100644 --- a/tests/test_draco_barre.py +++ b/tests/test_draco_barre.py @@ -43,6 +43,29 @@ def test_menu_des_applications_par_la_marque(): assert "font-size: 24px" in _css() +def test_panneau_spark_integre_218(): + # #218 : le bouton Spark de la barre ouvre le panneau de l'agent + # commun (mêmes assets partagés que TikTak / Flowdeck / Kanbante). + html = _index() + assert (FRONTEND / "draco-panneau.js").is_file() + assert (FRONTEND / "draco-panneau.css").is_file() + assert '' in html + assert '' in html + # L'action spark bascule sur le panneau — plus le message d'attente. + assert "window.DRACO_PANNEAU.toggle()" in html + assert "arrive en phase 4 du programme Fusion" not in html + panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8") + assert "https://agent.dracodev.net" in panneau + + +def test_csp_ouvre_le_service_d_agents_218(): + # Le panneau parle au service d'agents : la CSP doit nommer son + # hôte en connect-src, sans ouvrir de générique supplémentaire. + main = (Path(__file__).resolve().parent.parent / "backend" / "main.py").read_text(encoding="utf-8") + ligne = next(l for l in main.splitlines() if "connect-src" in l) + assert "https://agent.dracodev.net" in ligne + + def test_rail_mobile_vers_le_vrai_hamburger(): html = _index() assert 'matchMedia("(max-width: 768px)")' in html