Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39f9cc16e4 | ||
|
|
b4b3032456 | ||
|
|
edef1ea708 | ||
|
|
7523670949 | ||
|
|
d81f328859 | ||
|
|
1a39d601de |
+60
-1
@@ -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).
|
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
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.72.0**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.74.0**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,65 @@ 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é
|
||||||
|
|
||||||
|
- **Barre commune v0.1.4 — palier mobile élargi de 700 à 900 px**
|
||||||
|
(suite de BUG-118) : entre les deux seuils, la barre complète
|
||||||
|
ne tenait pas (fenêtres réduites, tablettes, webviews) alors
|
||||||
|
que les apps sont déjà en mise en page mobile ; le resserré
|
||||||
|
(app courante, alertes, Spark, avatar) s'applique maintenant
|
||||||
|
dès 900 px, et le décalage de `.app-container` suit le même
|
||||||
|
seuil. Le changement d'app reste assuré par le menu du ✦.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.73.0] — 2026-10-21
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
|
||||||
|
- **Menu des applications par la marque ✦** (barre commune
|
||||||
|
**v0.1.3**, #215) : cliquer le ✦ — un peu plus gros (24 px) —
|
||||||
|
ouvre le menu des 4 applications (liens, icônes, badges), sur
|
||||||
|
mobile comme sur bureau. C'est le changement d'app en mobile,
|
||||||
|
où les onglets sont repliés depuis la v0.1.2. Fermeture au
|
||||||
|
clic extérieur ou à Échap. Choix de Bruno (2026-10-21).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [2.72.1] — 2026-10-21
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
|
||||||
|
- **BUG-118 — barre d'apps Fusion en mobile** : la barre commune
|
||||||
|
passe en **v0.1.2** (dracodev-commons) — au palier ≤ 700 px elle
|
||||||
|
ne garde que la marque, l'onglet de l'app courante, les alertes,
|
||||||
|
Spark et l'avatar (avant : onglets et pastilles domaines entiers,
|
||||||
|
barre débordante sur téléphone). Le bouton ☰ de la barre pilote
|
||||||
|
désormais le bon bouton selon la largeur : barre latérale bureau
|
||||||
|
au-delà de 768 px, **vrai hamburger mobile** (tiroir) en dessous —
|
||||||
|
avant, il cliquait le bouton bureau et ne faisait rien sur
|
||||||
|
téléphone, donnant l'impression d'un hamburger mort à côté de
|
||||||
|
celui d'ObsiGate. Test : `tests/test_draco_barre.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.72.0] — 2026-10-20
|
## [2.72.0] — 2026-10-20
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
+3
-3
@@ -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.
|
**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.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.72.0).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.74.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.72.0 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.74.0 | Dernière mise à jour : Septembre 2026*
|
||||||
|
|||||||
@@ -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.
|
**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.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.72.0).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.74.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.72.0 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.74.0 | Last updated: September 2026*
|
||||||
|
|||||||
+1
-1
@@ -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; "
|
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; "
|
"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:; "
|
"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; "
|
"font-src 'self' data: https://fonts.gstatic.com https://esm.sh; "
|
||||||
"worker-src 'self' blob:; "
|
"worker-src 'self' blob:; "
|
||||||
"frame-src 'self' blob:; "
|
"frame-src 'self' blob:; "
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.72.0"
|
version = "2.74.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.72.0"
|
version = "2.74.0"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.72.0",
|
"version": "2.74.0",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ services:
|
|||||||
- VAULT_4_PATH=/vaults/Obsidian_WORKOUT
|
- VAULT_4_PATH=/vaults/Obsidian_WORKOUT
|
||||||
- VAULT_5_NAME=Sessions
|
- VAULT_5_NAME=Sessions
|
||||||
- VAULT_5_PATH=/vaults/SessionsManager
|
- VAULT_5_PATH=/vaults/SessionsManager
|
||||||
- DIR_1_NAME=Bruno
|
|
||||||
- DIR_1_PATH=/vaults/bruno
|
|
||||||
# #194 : dossier personnel par utilisateur = <racine>/home-<user>
|
# #194 : dossier personnel par utilisateur = <racine>/home-<user>
|
||||||
- OBSIGATE_HOME_ROOT=/vaults/Home
|
- OBSIGATE_HOME_ROOT=/vaults/Home
|
||||||
- OBSIGATE_AUTH_ENABLED=true
|
- OBSIGATE_AUTH_ENABLED=true
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
|||||||
| 2026-10-14 | BUG-114 | Correction | `frontend/js/ui.js`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-114 — popup « currentPath is not defined » après suppression/renommage de répertoire.** Les branches répertoire de `confirmDeleteDirectory` (ligne ~1054) et du renommage inline (lignes ~967-968) référençaient `currentPath` nu au lieu de `state.currentPath` (ui.js ne déclare pas `currentPath`, contrairement à app.js) : `ReferenceError` levée dans le `try`, avalée par le `catch`, affichée en toast d'erreur alors que l'opération (DELETE/PATCH) avait réussi. Trois usages corrigés. Vérifié en Playwright sur l'instance E2E (flux réel clic droit → Supprimer) : toast de succès, 0 fantôme dans paths, 0 erreur JS ; validate-imports 42 modules / 373 exports, unit.test.mjs 13/13 | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-10-14 | BUG-114 | Correction | `frontend/js/ui.js`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-114 — popup « currentPath is not defined » après suppression/renommage de répertoire.** Les branches répertoire de `confirmDeleteDirectory` (ligne ~1054) et du renommage inline (lignes ~967-968) référençaient `currentPath` nu au lieu de `state.currentPath` (ui.js ne déclare pas `currentPath`, contrairement à app.js) : `ReferenceError` levée dans le `try`, avalée par le `catch`, affichée en toast d'erreur alors que l'opération (DELETE/PATCH) avait réussi. Trois usages corrigés. Vérifié en Playwright sur l'instance E2E (flux réel clic droit → Supprimer) : toast de succès, 0 fantôme dans paths, 0 erreur JS ; validate-imports 42 modules / 373 exports, unit.test.mjs 13/13 | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
| 2026-10-14 | BUG-115 | Correction | `frontend/editor-poc.html`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-115 — trois ajustements de l'éditeur Forge (aperçu + menus).** (1) Cases à cocher collées au texte : le rendu serveur (mistune task_lists) ne met pas d'espace après l'`<input>` et l'`<style>` de `editor-poc.html` n'avait pas l'équivalent de la règle `.md-content .task-list-item` de `style.css` → deux règles ajoutées (margin-right 6 px, list-style none). (2) Flash de l'aperçu : `renderPreview` écrasait le contenu par « Rendering... » à chaque frappe (debounce 500 ms) — remplacé par un rafraîchissement silencieux (l'ancien rendu reste affiché, spinner seulement au premier rendu) + `_previewDirty` qui rejoue la dernière frappe tombée pendant un fetch (état fini correct, sans clignotement). (3) Menu « dimensions du tableau » coupé sur un document d'une ligne : `.inline-form`/`.lang-picker` en `position: absolute` recevaient des coordonnées **viewport** (`wr.left+pt.x`) → double décalage et clipping par `.editor-pane { overflow: hidden }` ; passés en `position: fixed` avec clamp viewport (convention du menu slash, déjà dans le fichier) ; au passage le clic sur un item qi/slash bullait jusqu'au listener document qui refermait la forme aussitôt ouverte → `stopPropagation`. Vérifié en Playwright sur l'instance E2E (serveur réel) : checkbox `marginRight: 6px`, aucun « Rendering... » observé pendant la frappe et dernier état rendu, forme table entièrement visible (`bottom 193 < vh 900`) et insertion OK. | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-10-14 | BUG-115 | Correction | `frontend/editor-poc.html`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-115 — trois ajustements de l'éditeur Forge (aperçu + menus).** (1) Cases à cocher collées au texte : le rendu serveur (mistune task_lists) ne met pas d'espace après l'`<input>` et l'`<style>` de `editor-poc.html` n'avait pas l'équivalent de la règle `.md-content .task-list-item` de `style.css` → deux règles ajoutées (margin-right 6 px, list-style none). (2) Flash de l'aperçu : `renderPreview` écrasait le contenu par « Rendering... » à chaque frappe (debounce 500 ms) — remplacé par un rafraîchissement silencieux (l'ancien rendu reste affiché, spinner seulement au premier rendu) + `_previewDirty` qui rejoue la dernière frappe tombée pendant un fetch (état fini correct, sans clignotement). (3) Menu « dimensions du tableau » coupé sur un document d'une ligne : `.inline-form`/`.lang-picker` en `position: absolute` recevaient des coordonnées **viewport** (`wr.left+pt.x`) → double décalage et clipping par `.editor-pane { overflow: hidden }` ; passés en `position: fixed` avec clamp viewport (convention du menu slash, déjà dans le fichier) ; au passage le clic sur un item qi/slash bullait jusqu'au listener document qui refermait la forme aussitôt ouverte → `stopPropagation`. Vérifié en Playwright sur l'instance E2E (serveur réel) : checkbox `marginRight: 6px`, aucun « Rendering... » observé pendant la frappe et dernier état rendu, forme table entièrement visible (`bottom 193 < vh 900`) et insertion OK. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
| 2026-10-14 | BUG-116 | Correction | `frontend/editor-poc.html`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-116 — la textarea Forge ne grandissait pas après une insertion programmatique.** Après un tableau de 4 lignes inséré via le menu, l'éditeur restait d'une ligne : le contenu était bien multiligne mais `autoHeight()` n'était rappelé que par l'événement `input`, que `ta.value = …` ne déclenche pas. Les handlers clavier appelaient `autoHeight(); updateLineNumbers();` à la main ; `insertAtCursor` et `wrapSelection` — les deux fonctions par lesquelles passent toutes les insertions (tableau, bloc de code, templates mermaid, checklist, mise en forme) — oubliaient les deux appels. Corrigé à la source dans les deux fonctions plutôt que chez chaque appelant. Vérifié en Playwright (instance E2E, chemin réel qi → Table, 4 lignes sur document d'une ligne) : hauteur 51 → 189 px, gutter = nb de lignes | 🟢 corrigé (en attente vérif utilisateur) |
|
| 2026-10-14 | BUG-116 | Correction | `frontend/editor-poc.html`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-116 — la textarea Forge ne grandissait pas après une insertion programmatique.** Après un tableau de 4 lignes inséré via le menu, l'éditeur restait d'une ligne : le contenu était bien multiligne mais `autoHeight()` n'était rappelé que par l'événement `input`, que `ta.value = …` ne déclenche pas. Les handlers clavier appelaient `autoHeight(); updateLineNumbers();` à la main ; `insertAtCursor` et `wrapSelection` — les deux fonctions par lesquelles passent toutes les insertions (tableau, bloc de code, templates mermaid, checklist, mise en forme) — oubliaient les deux appels. Corrigé à la source dans les deux fonctions plutôt que chez chaque appelant. Vérifié en Playwright (instance E2E, chemin réel qi → Table, 4 lignes sur document d'une ligne) : hauteur 51 → 189 px, gutter = nb de lignes | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
|
| 2026-10-21 | BUG-118 | Correction | `frontend/index.html`, `frontend/draco-barre.css`, `tests/test_draco_barre.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-118 — la barre d'apps Fusion était inutilisable en mobile** (retours de Bruno sur captures téléphone). Deux causes : (1) le palier ≤ 700 px de la CSS commune gardait onglets entiers et pastilles domaines — la barre débordait ; la source dracodev-commons passe en **v0.1.2** (spec des barres, § Mobile : marque, onglet de l'app courante, alertes, Spark, avatar ; rail/recherche/domaines/menu masqués au profit des contrôles natifs). (2) Le bouton ☰ de la barre cliquait `sidebar-toggle-btn` (bureau) au lieu de `hamburger-btn` (tiroir mobile) : sur téléphone il ne faisait rien, doublé du vrai hamburger d'ObsiGate juste en dessous. Rail devenu contextuel (`matchMedia` ≤ 768 px, point de bascule d'ObsiGate). Test de non-régression `tests/test_draco_barre.py` (2 tests) | 🟢 corrigé (en attente vérif utilisateur) |
|
||||||
| 2026-10-15 | BUG-117 + #201 | Correction + feature | `frontend/js/dragdrop.js`, `frontend/js/ui.js`, `frontend/js/upload.js`, `frontend/locales/fr.json`, `frontend/locales/en.json`, `backend/services/mutations.py`, `backend/routers/files_write.py`, `backend/schemas.py`, `tests/test_api_main.py`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/ISSUES_TODOLIST.md` | **BUG-117 — le drag & drop interne de la sidebar était mort en silence.** `onDrop` classait tout dépôt « externe » dès que `dataTransfer.items` était non vide — or un glisser interne dépose 4 formats string (`tree:*`) : chaque déplacement partait dans la branche upload où le filtre `kind === 'file'` ne trouvait rien — aucun appel API, aucun toast. Détection corrigée (vrais fichiers seulement, `dragState` prioritaire). Livré avec **#201 : menu contextuel Déplacer/Dupliquer** (fichiers et dossiers), modal `promptTargetFolder` réutilisé (+ `opts`, emplacement actuel pré-sélectionné), endpoint `POST /api/duplicate/{vault}` (`duplicate_path`, index incrémental, SSE/webhooks, refus inside-self), i18n FR/EN, 4 tests `TestDuplicateApi` + validation Playwright E2E complète | 🟢 corrigé/livré (en attente vérif utilisateur) |
|
| 2026-10-15 | BUG-117 + #201 | Correction + feature | `frontend/js/dragdrop.js`, `frontend/js/ui.js`, `frontend/js/upload.js`, `frontend/locales/fr.json`, `frontend/locales/en.json`, `backend/services/mutations.py`, `backend/routers/files_write.py`, `backend/schemas.py`, `tests/test_api_main.py`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/ISSUES_TODOLIST.md` | **BUG-117 — le drag & drop interne de la sidebar était mort en silence.** `onDrop` classait tout dépôt « externe » dès que `dataTransfer.items` était non vide — or un glisser interne dépose 4 formats string (`tree:*`) : chaque déplacement partait dans la branche upload où le filtre `kind === 'file'` ne trouvait rien — aucun appel API, aucun toast. Détection corrigée (vrais fichiers seulement, `dragState` prioritaire). Livré avec **#201 : menu contextuel Déplacer/Dupliquer** (fichiers et dossiers), modal `promptTargetFolder` réutilisé (+ `opts`, emplacement actuel pré-sélectionné), endpoint `POST /api/duplicate/{vault}` (`duplicate_path`, index incrémental, SSE/webhooks, refus inside-self), i18n FR/EN, 4 tests `TestDuplicateApi` + validation Playwright E2E complète | 🟢 corrigé/livré (en attente vérif utilisateur) |
|
||||||
| 2026-10-09 | #194 | Correction (isolation) | `backend/auth/middleware.py`, `backend/services/recent.py`, `backend/services/search.py`, `backend/routers/search.py`, `backend/routers/config.py`, `backend/routers/conflicts.py`, `backend/routers/files_read.py`, `backend/routers/history.py`, `backend/push.py`, `backend/user_home.py`, `tests/test_user_home.py`, `tests/test_auth_api.py`, `docs/features/user-home-194.md`, `CHANGELOG.md` | **#194 — l'admin voyait le home de chaque utilisateur** (retour utilisateur) : `vaults: ["*"]` ouvrait **tous** les vaults, donc `home-<user>` compris. Correctif à la racine : `check_vault_access` exige un octroi **explicite** pour tout vault `home-*` (nouveau `is_home_vault()`), et les filtres « * » écrits en dur dans les routes ont été remplacés par cet appel unique — dashboard (`routers/config.py`), conflits, liens retour, favoris, abonnements push (`/api/search/replace` utilisait déjà `is_vault_allowed`). `/api/search` filtre désormais les bruts **avant** pagination via `search_vaults(is_allowed=…)` (sinon notes personnelles d'un autre compte dans les résultats). `_grant` n'écarte plus les comptes « * » → l'admin reçoit son propre `home-admin` (auto-réparé au démarrage). Vérifié : `test_user_home.py` 12→**14/14** (dont « * » sans homes + filtrage recherche), `test_auth_api.py` 2/2 (admin ne voit **plus** `home-alice`, alice voit le sien), suite **1705 passed / 2 skipped**, ruff backend 0, mypy 114 fichiers 0, **live Playwright instance 2020** : arborescence admin = `home-admin, TestDir, TestVault` (plus de `home-bruno`), `svg.lucide-home` rendu. Reste ouvert (fuite pré-existante, hors périmètre) : `/api/search/advanced`, `/api/tags`, `/api/tags/suggest`, `/api/tree-search`, `/api/suggest` ne filtrent pas par vault accessible | ✅ livré (en attente vérif utilisateur) |
|
| 2026-10-09 | #194 | Correction (isolation) | `backend/auth/middleware.py`, `backend/services/recent.py`, `backend/services/search.py`, `backend/routers/search.py`, `backend/routers/config.py`, `backend/routers/conflicts.py`, `backend/routers/files_read.py`, `backend/routers/history.py`, `backend/push.py`, `backend/user_home.py`, `tests/test_user_home.py`, `tests/test_auth_api.py`, `docs/features/user-home-194.md`, `CHANGELOG.md` | **#194 — l'admin voyait le home de chaque utilisateur** (retour utilisateur) : `vaults: ["*"]` ouvrait **tous** les vaults, donc `home-<user>` compris. Correctif à la racine : `check_vault_access` exige un octroi **explicite** pour tout vault `home-*` (nouveau `is_home_vault()`), et les filtres « * » écrits en dur dans les routes ont été remplacés par cet appel unique — dashboard (`routers/config.py`), conflits, liens retour, favoris, abonnements push (`/api/search/replace` utilisait déjà `is_vault_allowed`). `/api/search` filtre désormais les bruts **avant** pagination via `search_vaults(is_allowed=…)` (sinon notes personnelles d'un autre compte dans les résultats). `_grant` n'écarte plus les comptes « * » → l'admin reçoit son propre `home-admin` (auto-réparé au démarrage). Vérifié : `test_user_home.py` 12→**14/14** (dont « * » sans homes + filtrage recherche), `test_auth_api.py` 2/2 (admin ne voit **plus** `home-alice`, alice voit le sien), suite **1705 passed / 2 skipped**, ruff backend 0, mypy 114 fichiers 0, **live Playwright instance 2020** : arborescence admin = `home-admin, TestDir, TestVault` (plus de `home-bruno`), `svg.lucide-home` rendu. Reste ouvert (fuite pré-existante, hors périmètre) : `/api/search/advanced`, `/api/tags`, `/api/tags/suggest`, `/api/tree-search`, `/api/suggest` ne filtrent pas par vault accessible | ✅ livré (en attente vérif utilisateur) |
|
||||||
|
|
||||||
|
|||||||
+19
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.72.0 | **Dernière mise à jour :** 2026-10-20
|
> **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
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
@@ -30,6 +30,24 @@
|
|||||||
|
|
||||||
## 🔵 En cours
|
## 🔵 En cours
|
||||||
|
|
||||||
|
### 218. Fusion — panneau Spark intégré (agent commun)
|
||||||
|
|
||||||
|
- **Effort :** ~0,5 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-21 | **Statut :** ✅ livré le 2026-10-21 (2.74.0, redéploiement Docker en attente)
|
||||||
|
- **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
|
### 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)
|
- **Effort :** ~1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-20 | **Statut :** ✅ livré le 2026-10-20 (2.70.0)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
/* Barre d'apps commune dracodev — v0.1.0 (bloc 0, ACC-0.2)
|
/* Barre d'apps commune dracodev — v0.1.0 (bloc 0, ACC-0.2) ;
|
||||||
|
v0.1.2 : palier mobile resserré selon la spec (voir @media 700px) ;
|
||||||
|
v0.1.3 : la marque ✦ (plus grande) ouvre le menu des applications —
|
||||||
|
c'est LE changement d'app en mobile (choix de Bruno, 2026-10-21) ;
|
||||||
|
v0.1.4 : palier mobile élargi de 700 à 900 px (zone morte comblée).
|
||||||
Jetons repris du prototype validé Console v7.2.
|
Jetons repris du prototype validé Console v7.2.
|
||||||
Thème : clair par défaut, sombre via [data-theme="dark"] sur <html>
|
Thème : clair par défaut, sombre via [data-theme="dark"] sur <html>
|
||||||
ou prefers-color-scheme si l'app intégratrice ne force rien. */
|
ou prefers-color-scheme si l'app intégratrice ne force rien. */
|
||||||
@@ -7,6 +11,7 @@
|
|||||||
--bg: #f2f2f0; --surface: #ffffff; --surface-2: #e9eae6; --surface-3: #f6f6f3;
|
--bg: #f2f2f0; --surface: #ffffff; --surface-2: #e9eae6; --surface-3: #f6f6f3;
|
||||||
--ink: #1b1d1a; --ink-2: #5c615a; --ink-3: #787d75; --line: #dedfd9;
|
--ink: #1b1d1a; --ink-2: #5c615a; --ink-3: #787d75; --line: #dedfd9;
|
||||||
--agent: #c2437e; --danger: #cf4444;
|
--agent: #c2437e; --danger: #cf4444;
|
||||||
|
position: relative;
|
||||||
height: 60px; box-sizing: border-box;
|
height: 60px; box-sizing: border-box;
|
||||||
display: grid; grid-template-columns: auto minmax(200px, 520px) auto;
|
display: grid; grid-template-columns: auto minmax(200px, 520px) auto;
|
||||||
align-items: center; gap: 16px; padding: 0 12px;
|
align-items: center; gap: 16px; padding: 0 12px;
|
||||||
@@ -24,9 +29,29 @@
|
|||||||
.draco-bar-centre { justify-content: center; }
|
.draco-bar-centre { justify-content: center; }
|
||||||
.draco-bar-droite { justify-content: flex-end; }
|
.draco-bar-droite { justify-content: flex-end; }
|
||||||
|
|
||||||
/* Marque */
|
/* Marque — bouton du menu des applications (v0.1.3) */
|
||||||
.draco-brand { display: flex; align-items: center; gap: 7px; color: var(--ink); text-decoration: none; font-weight: 800; }
|
.draco-brand { display: flex; align-items: center; gap: 7px; color: var(--ink); text-decoration: none; font-weight: 800; cursor: pointer; border-radius: 10px; padding: 4px 6px; margin: -4px -6px; }
|
||||||
.draco-brand-mark { color: var(--agent); }
|
.draco-brand:hover { background: var(--surface-2); }
|
||||||
|
.draco-brand-mark { color: var(--agent); font-size: 24px; line-height: 1; }
|
||||||
|
|
||||||
|
/* Menu des applications — construit par le JS depuis les onglets */
|
||||||
|
.draco-apps-menu {
|
||||||
|
position: absolute; top: calc(100% + 6px); left: 10px; z-index: 400;
|
||||||
|
min-width: 248px; padding: 6px;
|
||||||
|
background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
|
||||||
|
box-shadow: 0 16px 44px rgb(0 0 0 / .28);
|
||||||
|
}
|
||||||
|
.draco-apps-menu[hidden] { display: none; }
|
||||||
|
.draco-apps-item {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
padding: 8px 10px; border-radius: 10px;
|
||||||
|
color: var(--ink); text-decoration: none;
|
||||||
|
}
|
||||||
|
.draco-apps-item:hover, .draco-apps-item:focus-visible { background: var(--surface-2); }
|
||||||
|
.draco-apps-item[data-active="true"] { background: var(--surface-3); box-shadow: inset 2px 0 0 var(--agent); }
|
||||||
|
.draco-apps-item-txt { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
||||||
|
.draco-apps-item-nom { font-weight: 700; font-size: 14px; }
|
||||||
|
.draco-apps-item-role { font-size: 12px; color: var(--ink-2); }
|
||||||
|
|
||||||
/* Onglets d'apps — accordéon : seule l'app courante est déployée */
|
/* Onglets d'apps — accordéon : seule l'app courante est déployée */
|
||||||
.draco-apptabs { display: flex; gap: 6px; }
|
.draco-apptabs { display: flex; gap: 6px; }
|
||||||
@@ -119,9 +144,23 @@
|
|||||||
.draco-bar { grid-template-columns: auto auto 1fr; }
|
.draco-bar { grid-template-columns: auto auto 1fr; }
|
||||||
.draco-bar-droite { justify-content: flex-end; }
|
.draco-bar-droite { justify-content: flex-end; }
|
||||||
}
|
}
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 900px) {
|
||||||
/* Mobile : la barre du haut se condense ; la navigation d'apps passe
|
/* Mobile (spec des barres, § Mobile) : la barre se resserre sur
|
||||||
en barre basse propre à chaque app (patron du prototype v5+). */
|
l'essentiel — marque, onglet de l'app courante, alertes, Spark,
|
||||||
.draco-apptabs, .draco-domaines-lbl { display: none; }
|
avatar. Le rail, la recherche, les domaines et le menu « ••• »
|
||||||
.draco-bar { height: 52px; }
|
disparaissent : chaque app garde ses contrôles natifs (barre
|
||||||
|
basse, en-tête) — un seul hamburger visible, celui de l'app.
|
||||||
|
Le changement d'app passe par le menu du ✦ (v0.1.3).
|
||||||
|
(v0.1.2 — avant, les onglets entiers et les pastilles domaines
|
||||||
|
restaient affichés et la barre débordait sur téléphone.
|
||||||
|
v0.1.4 — palier élargi de 700 à 900 px : entre les deux, la
|
||||||
|
barre complète ne tenait pas (fenêtres réduites, tablettes,
|
||||||
|
webviews) alors que les apps sont déjà en mise en page mobile.) */
|
||||||
|
.draco-bar { height: 52px; grid-template-columns: auto 1fr auto; }
|
||||||
|
.draco-bar-centre, .draco-domaines, .draco-bar .draco-icon-btn { display: none; }
|
||||||
|
.draco-apptab { display: none; }
|
||||||
|
.draco-bar[data-current="obsigate"] .draco-apptab[data-app="obsigate"],
|
||||||
|
.draco-bar[data-current="flowdeck"] .draco-apptab[data-app="flowdeck"],
|
||||||
|
.draco-bar[data-current="tiktak"] .draco-apptab[data-app="tiktak"],
|
||||||
|
.draco-bar[data-current="kanbante"] .draco-apptab[data-app="kanbante"] { display: flex; }
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-7
@@ -1,4 +1,5 @@
|
|||||||
/* Barre d'apps commune dracodev — comportement (v0.1.0, ACC-0.2)
|
/* Barre d'apps commune dracodev — comportement (v0.1.0, ACC-0.2 ;
|
||||||
|
* v0.1.3 : menu des applications ouvert par la marque ✦).
|
||||||
*
|
*
|
||||||
* Aucun gestionnaire inline : un seul écouteur délégué sur le document
|
* Aucun gestionnaire inline : un seul écouteur délégué sur le document
|
||||||
* (patron validé par le prototype — les onclick du HTML généré sont
|
* (patron validé par le prototype — les onclick du HTML généré sont
|
||||||
@@ -14,6 +15,9 @@
|
|||||||
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
||||||
* alertes, spark, menu, rappel-entete. L'app intégratrice décide de
|
* alertes, spark, menu, rappel-entete. L'app intégratrice décide de
|
||||||
* chacune (ouvrir la palette, le panneau Spark, changer de domaine…).
|
* chacune (ouvrir la palette, le panneau Spark, changer de domaine…).
|
||||||
|
* Action INTERNE (jamais relayée) : apps — la marque ✦ ouvre le menu
|
||||||
|
* des applications, construit depuis les onglets ; c'est le moyen de
|
||||||
|
* changer d'app en mobile, où les onglets sont repliés (v0.1.2).
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -21,15 +25,16 @@
|
|||||||
|
|
||||||
function setCounts(counts) {
|
function setCounts(counts) {
|
||||||
Object.keys(counts || {}).forEach(function (k) {
|
Object.keys(counts || {}).forEach(function (k) {
|
||||||
var b = document.querySelector('.draco-badge[data-badge="' + k + '"]');
|
/* Toutes les occurrences : l'onglet ET sa ligne dans le menu. */
|
||||||
if (!b) return;
|
document.querySelectorAll('.draco-badge[data-badge="' + k + '"]').forEach(function (b) {
|
||||||
b.textContent = counts[k];
|
b.textContent = counts[k];
|
||||||
b.hidden = !(counts[k] > 0);
|
b.hidden = !(counts[k] > 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function setCurrent(app) {
|
function setCurrent(app) {
|
||||||
document.querySelectorAll(".draco-apptab").forEach(function (t) {
|
document.querySelectorAll(".draco-apptab, .draco-apps-item").forEach(function (t) {
|
||||||
t.dataset.active = t.dataset.app === app ? "true" : "false";
|
if (t.dataset.app) t.dataset.active = t.dataset.app === app ? "true" : "false";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function setDomain(dom) {
|
function setDomain(dom) {
|
||||||
@@ -38,8 +43,70 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Menu des applications (v0.1.3) ──────────────────────────────
|
||||||
|
* Construit depuis les onglets présents dans la barre : mêmes
|
||||||
|
* liens, mêmes icônes, mêmes badges. Ouvert par la marque ✦
|
||||||
|
* (data-act="apps"), fermé par un clic extérieur, Échap, ou le
|
||||||
|
* choix d'une application (la navigation suit son cours). */
|
||||||
|
var bar = document.querySelector(".draco-bar");
|
||||||
|
var brand = bar ? bar.querySelector(".draco-brand") : null;
|
||||||
|
var menu = null;
|
||||||
|
|
||||||
|
function buildMenu() {
|
||||||
|
if (!bar || !brand || menu) return;
|
||||||
|
menu = document.createElement("div");
|
||||||
|
menu.className = "draco-apps-menu";
|
||||||
|
menu.hidden = true;
|
||||||
|
menu.setAttribute("role", "menu");
|
||||||
|
menu.setAttribute("aria-label", "Applications");
|
||||||
|
document.querySelectorAll(".draco-apptab").forEach(function (t) {
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.className = "draco-apps-item";
|
||||||
|
a.href = t.getAttribute("href");
|
||||||
|
a.dataset.app = t.dataset.app;
|
||||||
|
a.setAttribute("role", "menuitem");
|
||||||
|
var ico = t.querySelector(".draco-apptab-ico");
|
||||||
|
if (ico) a.appendChild(ico.cloneNode(true));
|
||||||
|
var txt = document.createElement("span");
|
||||||
|
txt.className = "draco-apps-item-txt";
|
||||||
|
var fort = t.querySelector(".draco-apptab-txt strong");
|
||||||
|
var petit = t.querySelector(".draco-apptab-txt small");
|
||||||
|
var nom = document.createElement("span");
|
||||||
|
nom.className = "draco-apps-item-nom";
|
||||||
|
nom.textContent = fort ? fort.textContent : t.dataset.app;
|
||||||
|
txt.appendChild(nom);
|
||||||
|
if (petit) {
|
||||||
|
var role = document.createElement("span");
|
||||||
|
role.className = "draco-apps-item-role";
|
||||||
|
role.textContent = petit.textContent;
|
||||||
|
txt.appendChild(role);
|
||||||
|
}
|
||||||
|
a.appendChild(txt);
|
||||||
|
var badge = t.querySelector(".draco-badge");
|
||||||
|
if (badge) a.appendChild(badge.cloneNode(true));
|
||||||
|
menu.appendChild(a);
|
||||||
|
});
|
||||||
|
bar.appendChild(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMenu(force) {
|
||||||
|
buildMenu();
|
||||||
|
if (!menu) return;
|
||||||
|
var open = typeof force === "boolean" ? force : menu.hidden;
|
||||||
|
menu.hidden = !open;
|
||||||
|
if (brand) brand.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("click", function (ev) {
|
document.addEventListener("click", function (ev) {
|
||||||
var el = ev.target.closest("[data-act]");
|
var el = ev.target.closest("[data-act]");
|
||||||
|
if (el && el.dataset.act === "apps") {
|
||||||
|
ev.preventDefault();
|
||||||
|
toggleMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (menu && !menu.hidden && !ev.target.closest(".draco-apps-menu") && !ev.target.closest(".draco-brand")) {
|
||||||
|
toggleMenu(false);
|
||||||
|
}
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
var act = el.dataset.act;
|
var act = el.dataset.act;
|
||||||
if (act === "domaine") setDomain(el.dataset.dom);
|
if (act === "domaine") setDomain(el.dataset.dom);
|
||||||
@@ -47,9 +114,14 @@
|
|||||||
else document.dispatchEvent(new CustomEvent("draco:bar", { detail: { act: act, el: el } }));
|
else document.dispatchEvent(new CustomEvent("draco:bar", { detail: { act: act, el: el } }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (ev) {
|
||||||
|
if (ev.key === "Escape") toggleMenu(false);
|
||||||
|
});
|
||||||
|
|
||||||
/* API publique : l'app met les compteurs à jour sans recharger. */
|
/* API publique : l'app met les compteurs à jour sans recharger. */
|
||||||
window.DRACO_BAR = { setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain };
|
window.DRACO_BAR = { setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain };
|
||||||
|
|
||||||
|
buildMenu();
|
||||||
if (cfg.current) setCurrent(cfg.current);
|
if (cfg.current) setCurrent(cfg.current);
|
||||||
if (cfg.domain) setDomain(cfg.domain);
|
if (cfg.domain) setDomain(cfg.domain);
|
||||||
if (cfg.counts) setCounts(cfg.counts);
|
if (cfg.counts) setCounts(cfg.counts);
|
||||||
|
|||||||
@@ -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; }
|
||||||
@@ -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 =
|
||||||
|
'<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="border-bottom:1px solid #e4e5e0;padding-top:8px;padding-bottom:8px">' +
|
||||||
|
' <span class="point"></span><span class="txt">Connexion au service…</span>' +
|
||||||
|
'</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]");
|
||||||
|
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 = "<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;color:#6c7268'>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 rapport = m.result ? "<div class='m-rapport'>" + esc(String(m.result).slice(0, 700)) + "</div>" : "";
|
||||||
|
ligne.innerHTML = "<div class='m-entete'><b>" + esc(m.title) + "</b>" +
|
||||||
|
"<span class='m-statut st-" + esc(m.status) + "'>" + esc(STATUTS_MISSION[m.status] || m.status) + "</span></div>" +
|
||||||
|
"<div class='m-meta'>" + 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) : "") + "</div>" + 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 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
+20
-4
@@ -76,6 +76,7 @@
|
|||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
<link rel="stylesheet" href="/static/draco-barre.css" />
|
<link rel="stylesheet" href="/static/draco-barre.css" />
|
||||||
|
<link rel="stylesheet" href="/static/draco-panneau.css" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
|
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
|
||||||
@@ -331,14 +332,14 @@
|
|||||||
|
|
||||||
<!-- Main Application (wrapped for show/hide) -->
|
<!-- Main Application (wrapped for show/hide) -->
|
||||||
<!-- Barre d'apps commune dracodev — Fusion #215 (ACC-1.1).
|
<!-- Barre d'apps commune dracodev — Fusion #215 (ACC-1.1).
|
||||||
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.0
|
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.4
|
||||||
(ui/barre-apps/) : ne pas modifier la structure ici ; toute
|
(ui/barre-apps/) : ne pas modifier la structure ici ; toute
|
||||||
évolution passe par une nouvelle version de commons.
|
évolution passe par une nouvelle version de commons.
|
||||||
CSS/JS : /static/draco-barre.css, /static/draco-barre.js. -->
|
CSS/JS : /static/draco-barre.css, /static/draco-barre.js. -->
|
||||||
<header class="draco-bar" data-current="obsigate">
|
<header class="draco-bar" data-current="obsigate">
|
||||||
<div class="draco-bar-zone draco-bar-gauche">
|
<div class="draco-bar-zone draco-bar-gauche">
|
||||||
<button type="button" class="draco-icon-btn" data-act="rail" aria-label="Barre latérale">☰</button>
|
<button type="button" class="draco-icon-btn" data-act="rail" aria-label="Barre latérale">☰</button>
|
||||||
<a class="draco-brand" href="/" aria-label="dracodev">
|
<a class="draco-brand" href="/" data-act="apps" aria-haspopup="true" aria-expanded="false" aria-label="dracodev — changer d'application" title="Changer d'application">
|
||||||
<span class="draco-brand-mark">✦</span><span class="draco-brand-name">dracodev</span>
|
<span class="draco-brand-mark">✦</span><span class="draco-brand-name">dracodev</span>
|
||||||
</a>
|
</a>
|
||||||
<nav class="draco-apptabs" aria-label="Applications">
|
<nav class="draco-apptabs" aria-label="Applications">
|
||||||
@@ -409,11 +410,25 @@
|
|||||||
setTimeout(function () { d.remove(); }, 3400);
|
setTimeout(function () { d.remove(); }, 3400);
|
||||||
};
|
};
|
||||||
if (act === "domaine") { try { localStorage.setItem("draco_domain", el.dataset.dom); } catch (e) { } return; }
|
if (act === "domaine") { try { localStorage.setItem("draco_domain", el.dataset.dom); } catch (e) { } return; }
|
||||||
if (act === "rail") { var b = document.getElementById("sidebar-toggle-btn"); if (b) b.click(); return; }
|
if (act === "rail") {
|
||||||
|
// Bureau : afficher/masquer la barre latérale.
|
||||||
|
// Mobile (≤ 768 px, point de bascule d'ObsiGate) :
|
||||||
|
// le vrai hamburger, qui ouvre le tiroir — avant
|
||||||
|
// ce correctif (BUG-118), le rail cliquait le
|
||||||
|
// bouton bureau et ne faisait rien sur téléphone.
|
||||||
|
var mobile = window.matchMedia("(max-width: 768px)").matches;
|
||||||
|
var b = document.getElementById(mobile ? "hamburger-btn" : "sidebar-toggle-btn");
|
||||||
|
if (b) b.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (act === "palette") { var s = document.getElementById("search-input"); if (s) { s.focus(); s.scrollIntoView({ block: "center" }); } return; }
|
if (act === "palette") { var s = document.getElementById("search-input"); if (s) { s.focus(); s.scrollIntoView({ block: "center" }); } return; }
|
||||||
if (act === "menu") { var m = document.getElementById("header-menu-btn"); if (m) m.click(); return; }
|
if (act === "menu") { var m = document.getElementById("header-menu-btn"); if (m) m.click(); return; }
|
||||||
if (act === "alertes") { dire("Alertes unifiées : arrive avec la fédération (programme Fusion)."); return; }
|
if (act === "alertes") { dire("Alertes unifiées : 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") {
|
if (act === "rappel-entete") {
|
||||||
var bar = document.querySelector(".draco-bar");
|
var bar = document.querySelector(".draco-bar");
|
||||||
if (bar) bar.style.display = "";
|
if (bar) bar.style.display = "";
|
||||||
@@ -6042,5 +6057,6 @@ curl -X POST https://votre-serveur.com/webhook \
|
|||||||
</div>
|
</div>
|
||||||
<script defer src="/static/draco-barre.js"></script>
|
<script defer src="/static/draco-barre.js"></script>
|
||||||
<script defer src="/static/draco-liens.js"></script>
|
<script defer src="/static/draco-liens.js"></script>
|
||||||
|
<script defer src="/static/draco-panneau.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-1
@@ -14296,7 +14296,7 @@ textarea.file-chat-input {
|
|||||||
/* — Barre d'apps commune dracodev + section « Liens » des notes
|
/* — Barre d'apps commune dracodev + section « Liens » des notes
|
||||||
(Fusion #215, snippet figé dracodev-commons v0.1.0) — */
|
(Fusion #215, snippet figé dracodev-commons v0.1.0) — */
|
||||||
body.has-draco-bar .app-container { height: calc(100vh - 60px); }
|
body.has-draco-bar .app-container { height: calc(100vh - 60px); }
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 900px) {
|
||||||
body.has-draco-bar .app-container { height: calc(100vh - 52px); }
|
body.has-draco-bar .app-container { height: calc(100vh - 52px); }
|
||||||
}
|
}
|
||||||
.draco-liens {
|
.draco-liens {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.72.0",
|
"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.",
|
"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",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""BUG-118 — barre d'apps commune en mobile (Fusion ACC-1, #215).
|
||||||
|
|
||||||
|
Retours de Bruno (2026-10-21) sur téléphone : la barre débordait
|
||||||
|
(pastilles domaines et onglets entiers affichés) et son bouton ☰
|
||||||
|
ne faisait rien — il cliquait le bouton bureau
|
||||||
|
``sidebar-toggle-btn`` au lieu du vrai hamburger mobile
|
||||||
|
``hamburger-btn``. Correctifs verrouillés ici : CSS commune v0.1.2
|
||||||
|
(palier 700 px resserré selon la spec) et rail contextuel dans
|
||||||
|
index.html (mobile ≤ 768 px → hamburger-btn).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
FRONTEND = Path(__file__).resolve().parent.parent / "frontend"
|
||||||
|
|
||||||
|
|
||||||
|
def _css() -> str:
|
||||||
|
return (FRONTEND / "draco-barre.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _index() -> str:
|
||||||
|
return (FRONTEND / "index.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_palier_mobile_resserre_v012():
|
||||||
|
css = _css()
|
||||||
|
assert "@media (max-width: 900px)" in css
|
||||||
|
# Seul l'onglet de l'app courante reste visible en mobile.
|
||||||
|
assert '.draco-bar[data-current="obsigate"] .draco-apptab[data-app="obsigate"]' in css
|
||||||
|
# Domaines et boutons icônes (rail, menu) masqués en mobile.
|
||||||
|
bloc = css.split("@media (max-width: 900px)", 1)[1]
|
||||||
|
assert ".draco-domaines" in bloc
|
||||||
|
assert "display: none" in bloc
|
||||||
|
|
||||||
|
|
||||||
|
def test_menu_des_applications_par_la_marque():
|
||||||
|
# v0.1.3 : la marque ✦ ouvre le menu des 4 applications
|
||||||
|
# (changement d'app en mobile), logo à 24 px.
|
||||||
|
html = _index()
|
||||||
|
assert 'data-act="apps"' in html
|
||||||
|
js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8")
|
||||||
|
assert "draco-apps-menu" in js
|
||||||
|
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 '<link rel="stylesheet" href="/static/draco-panneau.css" />' in html
|
||||||
|
assert '<script defer src="/static/draco-panneau.js"></script>' 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
|
||||||
|
assert '"hamburger-btn"' in html
|
||||||
|
assert '"sidebar-toggle-btn"' in html
|
||||||
Reference in New Issue
Block a user