Compare commits

..
1 Commits
Author SHA1 Message Date
Spark 8e073438a3 fix: URLs sœurs dérivées de l'environnement de l'agent — le menu inter-apps ne pointe plus la prod depuis la dev BUG-120
CI / security (push) Successful in 1m55s
CI / lint (push) Successful in 3m5s
CI / test (push) Successful in 5m23s
CI / build (push) Successful in 1m46s
CI / e2e (push) Successful in 18m9s
2026-10-26 00:06:16 +00:00
14 changed files with 204 additions and 21 deletions
+5 -2
View File
@@ -108,8 +108,11 @@ DEEPSEEK_MODEL=deepseek-chat
# ── Fusion / dev-prod — URL publiques de l'écosystème (#223) ── # ── Fusion / dev-prod — URL publiques de l'écosystème (#223) ──
# Aucun nom public n'est figé dans le code : ces variables portent les # Aucun nom public n'est figé dans le code : ces variables portent les
# URL des autres applications. Valeurs par défaut (si absentes) = noms # URL des autres applications. Valeurs par défaut (si absentes) = noms
# OFFICIELS de la production. L'environnement dev déclare ses noms en # OFFICIELS de la production. Depuis BUG-120, les URL sœurs se dérivent
# -dev dans son propre .env uniquement, par exemple : # de l'hôte de l'agent (agent<SUFFIXE>.dracodev.net, lu via
# AGENT_EVENEMENTS_URL / DRACO_AGENT_URL) : en dev, rien d'autre à
# déclarer pour obtenir les noms en -dev. Une variable explicite
# reste prioritaire sur la dérivation, par exemple :
# DRACO_TIKTAK_URL=https://tiktak-dev.dracodev.net # DRACO_TIKTAK_URL=https://tiktak-dev.dracodev.net
# DRACO_AGENT_URL=https://agent.dracodev.net # DRACO_AGENT_URL=https://agent.dracodev.net
# DRACO_OBSIGATE_URL=https://og.dracodev.net # DRACO_OBSIGATE_URL=https://og.dracodev.net
+27 -1
View File
@@ -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.78.1**. > [Unreleased](#unreleased). La dernière version livrée est **2.78.2**.
--- ---
@@ -14,6 +14,32 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
--- ---
## [2.78.2] — 2026-10-26
### Corrigé
- **Menu inter-apps : les URL sœurs suivaient l'environnement de
l'agent (BUG-120, suite de #223)** — sur la dev
(`og-dev.dracodev.net`), le `window.DRACO_URLS` servi donnait
l'agent en `-dev` mais les applications sœurs (ObsiGate, Flowdeck,
TikTak, Kanbante) en noms **prod** : seule l'URL de l'agent avait
une dérivation de repli, et le `.env` dev ne déclare pas les quatre
`DRACO_<APP>_URL`. Résolution d'une URL sœur dans
`backend/draco_config.py`, inchangée dans son contrat : (1) la
variable explicite prime ; (2) à défaut, **dérivation
d'environnement** depuis l'hôte de l'agent tel qu'ObsiGate le
connaît déjà (`agent_url()`, qui lit `AGENT_EVENEMENTS_URL`) — si
cet hôte est de la forme `agent<SUFFIXE>.dracodev.net`, l'URL sœur
devient `https://<slug><SUFFIXE>.dracodev.net` (`og`, `flowdeck`,
`tiktak`, `kanbante`) ; hôte hors motif (autre domaine, LAN, IP,
port) → **aucune dérivation**, jamais de lien deviné ; (3) défaut
officiel. L'URL Gitea ne change pas. Aucune variable supplémentaire
à déclarer en dev. Tests `tests/test_draco_config.py` étendus
(dérivation `-dev`, priorité de l'explicite, hôtes hors motif,
non-régression sans env). Redéploiement Docker requis.
---
## [2.78.1] — 2026-10-25 ## [2.78.1] — 2026-10-25
### Corrigé ### Corrigé
+3 -3
View File
@@ -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.
[![Version](https://img.shields.io/badge/Version-2.78.1-blue.svg)]() [![Version](https://img.shields.io/badge/Version-2.78.2-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![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/) [![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/) [![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 ## 📝 Changelog
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.78.1). Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.78.2).
--- ---
*Projet : ObsiGate | Version : 2.78.1 | Dernière mise à jour : Septembre 2026* *Projet : ObsiGate | Version : 2.78.2 | Dernière mise à jour : Septembre 2026*
+3 -3
View File
@@ -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.
[![Version](https://img.shields.io/badge/Version-2.78.1-blue.svg)]() [![Version](https://img.shields.io/badge/Version-2.78.2-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![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/) [![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/) [![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 ## 📝 Changelog
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.78.1). See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.78.2).
--- ---
*Project: ObsiGate | Version: 2.78.1 | Last updated: September 2026* *Project: ObsiGate | Version: 2.78.2 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.78.1 2.78.2
+70 -4
View File
@@ -22,6 +22,16 @@ Variables (voir ``.env.example``) :
distincte de ``OBSIGATE_GITEA_URL`` (source connectée de distincte de ``OBSIGATE_GITEA_URL`` (source connectée de
l'assistant, réglable depuis l'UI, pas une URL d'affichage). l'assistant, réglable depuis l'UI, pas une URL d'affichage).
Résolution d'une URL sœur (BUG-120) : la variable explicite prime ;
à défaut, l'**environnement est dérivé de l'hôte de l'agent** tel
qu'ObsiGate le connaît déjà (:func:`agent_url`) — si cet hôte est de
la forme ``agent<SUFFIXE>.dracodev.net`` (``-dev`` en dev, vide en
prod), l'URL sœur est ``https://<slug><SUFFIXE>.dracodev.net``. Un
hôte hors motif (autre domaine, LAN, IP, port) ne dérive **rien** :
on ne fabrique jamais un lien deviné, le défaut officiel s'applique.
L'installation pointe donc les bons liens selon son environnement
sans déclarer les quatre ``DRACO_<APP>_URL``.
Ce module ne lit l'environnement qu'à l'appel (jamais à l'import) : Ce module ne lit l'environnement qu'à l'appel (jamais à l'import) :
les tests et un changement de ``.env`` suivi d'un redémarrage les tests et un changement de ``.env`` suivi d'un redémarrage
prennent effet sans état figé. prennent effet sans état figé.
@@ -30,6 +40,7 @@ prennent effet sans état figé.
from __future__ import annotations from __future__ import annotations
import os import os
import re
from urllib.parse import urlparse from urllib.parse import urlparse
# Défauts officiels (production future) — jamais de nom en ``-dev`` ici. # Défauts officiels (production future) — jamais de nom en ``-dev`` ici.
@@ -51,6 +62,21 @@ _VARIABLES: dict[str, str] = {
"gitea": "DRACO_GITEA_URL", "gitea": "DRACO_GITEA_URL",
} }
# Slugs d'hôte des applications sœurs pour la dérivation
# d'environnement (BUG-120) : premier label de ``<slug>.dracodev.net``
# (et de ``<slug>-dev.dracodev.net`` en dev). Gitea est **exclue** :
# son URL ne se dérive jamais.
_SLUGS_HOTE: dict[str, str] = {
"obsigate": "og",
"flowdeck": "flowdeck",
"tiktak": "tiktak",
"kanbante": "kanbante",
}
# Premier label de l'hôte de l'agent : ``agent`` suivi d'un suffixe
# d'environnement éventuel (``-dev`` en dev, vide en prod).
_MOTIF_AGENT = re.compile(r"^agent(-[a-z0-9-]*)?$")
def _nettoyer(valeur: str) -> str: def _nettoyer(valeur: str) -> str:
return valeur.strip().rstrip("/") return valeur.strip().rstrip("/")
@@ -87,12 +113,52 @@ def agent_origine() -> str:
return origine(agent_url()) or DEFAUTS["agent"] return origine(agent_url()) or DEFAUTS["agent"]
def _suffixe_environnement() -> str | None:
"""Suffixe d'environnement déduit de l'hôte de l'agent (BUG-120).
Lit l'hôte de :func:`agent_url` — l'origine de l'agent telle
qu'ObsiGate la connaît déjà (``DRACO_AGENT_URL``, à défaut
``AGENT_EVENEMENTS_URL``). Si cet hôte est exactement de la forme
``agent<SUFFIXE>.dracodev.net`` sans port, retourne ``SUFFIXE``
(``-dev`` en dev, ``""`` en prod). Sinon (autre domaine, LAN, IP,
port exotique) retourne ``None`` : aucune dérivation, on ne
fabrique jamais un lien deviné.
"""
u = urlparse(agent_url())
hote = (u.hostname or "").lower()
labels = hote.split(".")
if len(labels) != 3 or labels[1:] != ["dracodev", "net"]:
return None
try:
if u.port is not None:
return None
except ValueError:
return None
motif = _MOTIF_AGENT.match(labels[0])
if motif is None:
return None
return motif.group(1) or ""
def app_url(app: str) -> str: def app_url(app: str) -> str:
"""Base publique d'une application de l'écosystème (sans ``/`` final).""" """Base publique d'une application de l'écosystème (sans ``/`` final).
Résolution d'une URL sœur (BUG-120) : ``DRACO_<APP>_URL``
explicite d'abord ; à défaut, dérivation d'environnement depuis
l'hôte de l'agent (:func:`_suffixe_environnement`) ; à défaut,
le défaut officiel. L'URL Gitea ne se dérive jamais.
"""
if app == "agent": if app == "agent":
return agent_url() return agent_url()
variable = _VARIABLES[app] variable = _VARIABLES[app]
return _nettoyer(os.environ.get(variable, "")) or DEFAUTS[app] explicite = _nettoyer(os.environ.get(variable, ""))
if explicite:
return explicite
if app in _SLUGS_HOTE:
suffixe = _suffixe_environnement()
if suffixe is not None:
return f"https://{_SLUGS_HOTE[app]}{suffixe}.dracodev.net"
return DEFAUTS[app]
def app_hote(app: str) -> str: def app_hote(app: str) -> str:
@@ -105,8 +171,8 @@ def hotes_reconnus() -> dict[str, str]:
Contient **toujours** les hôtes officiels (un lien collé depuis la Contient **toujours** les hôtes officiels (un lien collé depuis la
production reste reconnaissable en dev, et réciproquement) **plus** production reste reconnaissable en dev, et réciproquement) **plus**
l'hôte configuré de chaque application (les noms en ``-dev`` du l'hôte résolu de chaque application (les noms en ``-dev`` du poste,
poste quand ils sont déclarés dans son ``.env``). déclarés dans son ``.env`` ou dérivés de l'hôte de l'agent).
""" """
hotes: dict[str, str] = {} hotes: dict[str, str] = {}
for app, defaut in DEFAUTS.items(): for app, defaut in DEFAUTS.items():
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]] [[package]]
name = "obsigate-desktop" name = "obsigate-desktop"
version = "2.78.1" version = "2.78.2"
dependencies = [ dependencies = [
"chrono", "chrono",
"env_logger", "env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "obsigate-desktop" name = "obsigate-desktop"
version = "2.78.1" version = "2.78.2"
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 -1
View File
@@ -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.78.1", "version": "2.78.2",
"identifier": "com.obsigate.desktop", "identifier": "com.obsigate.desktop",
"build": { "build": {
"frontendDist": "../frontend", "frontendDist": "../frontend",
+2
View File
@@ -226,6 +226,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-116* | Éditeur Forge : après insertion d'un tableau (ex. 4 lignes), la textarea reste d'une seule ligne au lieu de s'agrandir | 🟢 corrigé | P2 | 📱 frontend / éditeur | IA | `frontend/editor-poc.html` (`insertAtCursor`, `wrapSelection`) | Insérer un tableau via le menu : le contenu contient bien les nouvelles lignes mais la hauteur de la textarea ne change pas | Les mutations programmatiques de `ta.value` ne déclenchent pas l'événement `input` — seul handler qui rappelait `autoHeight()`/`updateLineNumbers()` (les handlers clavier, eux, les appelaient à la main). Ajout des deux appels dans `insertAtCursor` et `wrapSelection`, les deux points de passage de toutes les insertions (tableau, code, mermaid, checklist, gras multi-ligne) | Vérifié en Playwright sur l'instance E2E (qi → Table, 4 lignes, sur doc d'une ligne) : hauteur 51 → 189 px, 8 lignes rendues, gutter aligné | ### TODOs techniques (améliorations / nouvelles tâches) | *BUG-116* | Éditeur Forge : après insertion d'un tableau (ex. 4 lignes), la textarea reste d'une seule ligne au lieu de s'agrandir | 🟢 corrigé | P2 | 📱 frontend / éditeur | IA | `frontend/editor-poc.html` (`insertAtCursor`, `wrapSelection`) | Insérer un tableau via le menu : le contenu contient bien les nouvelles lignes mais la hauteur de la textarea ne change pas | Les mutations programmatiques de `ta.value` ne déclenchent pas l'événement `input` — seul handler qui rappelait `autoHeight()`/`updateLineNumbers()` (les handlers clavier, eux, les appelaient à la main). Ajout des deux appels dans `insertAtCursor` et `wrapSelection`, les deux points de passage de toutes les insertions (tableau, code, mermaid, checklist, gras multi-ligne) | Vérifié en Playwright sur l'instance E2E (qi → Table, 4 lignes, sur doc d'une ligne) : hauteur 51 → 189 px, 8 lignes rendues, gutter aligné | ### TODOs techniques (améliorations / nouvelles tâches)
| *BUG-117* | Drag & drop des fichiers dans la sidebar : le déplacement d'un répertoire à un autre ne fait plus rien, sans message | 🟢 corrigé | P1 | 📱 frontend / sidebar | IA | `frontend/js/dragdrop.js` (`onDrop`) | Glisser un fichier sur un dossier : aucune requête `/api/move`, aucun toast | La détection `isExternalFiles` dans `onDrop` (`items.length > 0`, ajoutée par #89) comptait aussi les formats string du drag interne (`tree:vault/path/type` posés par `setData` au `dragstart`) : tout déplacement partait dans la branche upload où rien ne passe le filtre `kind === 'file'` → drop avalé silencieusement. Corrigé à la racine : `dragState` (interne) prioritaire + items réellement fichiers. Livré avec #201 : menu contextuel Déplacer/Dupliquer réutilisant `promptTargetFolder`, emplacement actuel pré-sélectionné | Vérifié Playwright instance E2E : glisser `note1.md` → `d1` = toast « Moved file to d1 » + chemin déplacé ; 4 tests `TestDuplicateApi` | | *BUG-117* | Drag & drop des fichiers dans la sidebar : le déplacement d'un répertoire à un autre ne fait plus rien, sans message | 🟢 corrigé | P1 | 📱 frontend / sidebar | IA | `frontend/js/dragdrop.js` (`onDrop`) | Glisser un fichier sur un dossier : aucune requête `/api/move`, aucun toast | La détection `isExternalFiles` dans `onDrop` (`items.length > 0`, ajoutée par #89) comptait aussi les formats string du drag interne (`tree:vault/path/type` posés par `setData` au `dragstart`) : tout déplacement partait dans la branche upload où rien ne passe le filtre `kind === 'file'` → drop avalé silencieusement. Corrigé à la racine : `dragState` (interne) prioritaire + items réellement fichiers. Livré avec #201 : menu contextuel Déplacer/Dupliquer réutilisant `promptTargetFolder`, emplacement actuel pré-sélectionné | Vérifié Playwright instance E2E : glisser `note1.md` → `d1` = toast « Moved file to d1 » + chemin déplacé ; 4 tests `TestDuplicateApi` |
| *BUG-119* | Panneau Spark : le « mode console » (surimpression iframe de la console du service d'agents) est bloqué par le navigateur (« This content is blocked ») | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/main.py` (`SecurityHeadersMiddleware`) | Panneau Spark → « Console ⤢ » : l'iframe reste sur la page d'erreur du navigateur | `frame-src 'self' blob: https://agent.dracodev.net` dans la CSP servie ; test `tests/test_security_headers.py::test_csp_frame_src_allows_agent_console` | Cause : `frame-src` n'autorisait pas l'origine du service d'agents, pourtant déjà présente dans `connect-src` (le panneau texte fonctionnait). Aucune autre directive modifiée. Redéploiement Docker requis | | *BUG-119* | Panneau Spark : le « mode console » (surimpression iframe de la console du service d'agents) est bloqué par le navigateur (« This content is blocked ») | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/main.py` (`SecurityHeadersMiddleware`) | Panneau Spark → « Console ⤢ » : l'iframe reste sur la page d'erreur du navigateur | `frame-src 'self' blob: https://agent.dracodev.net` dans la CSP servie ; test `tests/test_security_headers.py::test_csp_frame_src_allows_agent_console` | Cause : `frame-src` n'autorisait pas l'origine du service d'agents, pourtant déjà présente dans `connect-src` (le panneau texte fonctionnait). Aucune autre directive modifiée. Redéploiement Docker requis |
| *BUG-120* | Menu inter-apps : sur l'environnement dev, les URL sœurs servies (`window.DRACO_URLS` : ObsiGate, Flowdeck, TikTak, Kanbante) restent en noms **prod** alors que l'URL de l'agent est bien en `-dev` | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/draco_config.py` (`app_url`, `_suffixe_environnement`) | Sur og-dev : `window.DRACO_URLS` donne `agent: https://agent-dev.dracodev.net` mais `tiktak: https://tiktak.dracodev.net` (prod) — le `.env` dev ne déclare pas les quatre `DRACO_<APP>_URL` | `backend/draco_config.py` : résolution sœur en 3 temps — explicite, puis **dérivation d'environnement** depuis l'hôte de `agent_url()` (`agent<SUFFIXE>.dracodev.net` → `https://<slug><SUFFIXE>.dracodev.net`, slugs `og`/`flowdeck`/`tiktak`/`kanbante` ; hors motif → rien), puis défaut officiel ; Gitea exclue. Tests `tests/test_draco_config.py` (+9 : dérivation `-dev` via `AGENT_EVENEMENTS_URL` et via `DRACO_AGENT_URL`, priorité de l'explicite, 5 hôtes hors motif, suffixe vide, injection) | Suite de #223 : seule l'URL de l'agent avait une dérivation de repli (`AGENT_EVENEMENTS_URL`) ; les sœurs retombaient sur les défauts officiels. Correctif de code demandé par Bruno : l'environnement se déduit de l'agent déjà configuré, sans variable supplémentaire. Redéploiement Docker requis |
| # | Titre | Statut | Priorité | Scope | Assigné | Zone (fichier) | Cmd de repro | Correctif / Commit | Notes | | # | Titre | Statut | Priorité | Scope | Assigné | Zone (fichier) | Cmd de repro | Correctif / Commit | Notes |
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
@@ -359,6 +360,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 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) |
| 2026-10-23 | BUG-119 | Correction | `backend/main.py`, `tests/test_security_headers.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md`, `docs/ROADMAP.md` | **BUG-119 — le « mode console » du panneau Spark V3 (#222) était bloqué par le navigateur** (« This content is blocked ») : la console du service d'agents s'affiche dans une iframe `https://agent.dracodev.net`, mais la CSP servie par `SecurityHeadersMiddleware` limitait `frame-src` à `'self' blob:` — seul le panneau texte passait (`connect-src` autorisait déjà l'origine). Correctif strict : `frame-src 'self' blob: https://agent.dracodev.net`, toutes les autres directives inchangées à l'identique. Test de non-régression `test_csp_frame_src_allows_agent_console` (la CSP servie est analysée et la liste exacte des sources `frame-src` verrouillée). Vérifié : 100 passed (security_headers + csp_nonce + security_hardening + draco_barre + version), ruff propre sur les fichiers touchés (51 erreurs préexistantes ailleurs, inchangées), mypy 0 (120 fichiers) | 🟢 corrigé (en attente vérif utilisateur) | | 2026-10-23 | BUG-119 | Correction | `backend/main.py`, `tests/test_security_headers.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md`, `docs/ROADMAP.md` | **BUG-119 — le « mode console » du panneau Spark V3 (#222) était bloqué par le navigateur** (« This content is blocked ») : la console du service d'agents s'affiche dans une iframe `https://agent.dracodev.net`, mais la CSP servie par `SecurityHeadersMiddleware` limitait `frame-src` à `'self' blob:` — seul le panneau texte passait (`connect-src` autorisait déjà l'origine). Correctif strict : `frame-src 'self' blob: https://agent.dracodev.net`, toutes les autres directives inchangées à l'identique. Test de non-régression `test_csp_frame_src_allows_agent_console` (la CSP servie est analysée et la liste exacte des sources `frame-src` verrouillée). Vérifié : 100 passed (security_headers + csp_nonce + security_hardening + draco_barre + version), ruff propre sur les fichiers touchés (51 erreurs préexistantes ailleurs, inchangées), mypy 0 (120 fichiers) | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-10-25 | BUG-120 (suite #223) | Correction | `backend/draco_config.py`, `tests/test_draco_config.py`, `.env.example`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md`, `docs/ROADMAP.md`, `docs/features/devprod-urls-config-223.md` | **BUG-120 — le menu inter-apps de la dev pointait la prod** : `window.DRACO_URLS` servait l'agent en `-dev` mais les quatre sœurs en noms officiels, le `.env` dev ne déclarant pas les `DRACO_<APP>_URL`. Correctif dans `draco_config.app_url()` : après l'explicite, **dérivation d'environnement** — hôte de `agent_url()` de la forme `agent<SUFFIXE>.dracodev.net` sans port → sœur en `<slug><SUFFIXE>.dracodev.net` ; hors motif (autre domaine, LAN, IP, port) aucune dérivation ; Gitea jamais dérivée. Vérifié : `test_draco_config.py` 16 passed, suite complète **1668 passed / 9 skipped**, ruff 0 et mypy 0 sur les fichiers touchés (51 erreurs ruff préexistantes ailleurs, inchangées) | 🟢 corrigé (en attente vérif utilisateur) |
--- ---
+2 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap # ObsiGate — Roadmap
> **Version :** 2.78.1 | **Dernière mise à jour :** 2026-10-25 > **Version :** 2.78.2 | **Dernière mise à jour :** 2026-10-26
> **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)**
@@ -1149,6 +1149,7 @@
| 168 | Assistant IA — notifications externes Discord / Telegram / SMTP / webhook (déclencheurs, secrets hors config, SSRF-safe) | Unreleased | [features/agent-phase4-166-168-170.md](./features/agent-phase4-166-168-170.md) | | 168 | Assistant IA — notifications externes Discord / Telegram / SMTP / webhook (déclencheurs, secrets hors config, SSRF-safe) | Unreleased | [features/agent-phase4-166-168-170.md](./features/agent-phase4-166-168-170.md) |
| 170 | Assistant IA — tâches planifiées type cron (create/append/notify, interval/daily/once, tick 60 s, échec notifié) | Unreleased | [features/agent-phase4-166-168-170.md](./features/agent-phase4-166-168-170.md) | | 170 | Assistant IA — tâches planifiées type cron (create/append/notify, interval/daily/once, tick 60 s, échec notifié) | Unreleased | [features/agent-phase4-166-168-170.md](./features/agent-phase4-166-168-170.md) |
| BUG-119 | Panneau Spark — « mode console » bloqué par la CSP (`frame-src` sans l'origine du service d'agents) | 2.77.1 | [ISSUES_TODOLIST.md](./ISSUES_TODOLIST.md) | | BUG-119 | Panneau Spark — « mode console » bloqué par la CSP (`frame-src` sans l'origine du service d'agents) | 2.77.1 | [ISSUES_TODOLIST.md](./ISSUES_TODOLIST.md) |
| BUG-120 | Menu inter-apps — les URL sœurs (`window.DRACO_URLS`) dérivent l'environnement de l'hôte de l'agent (`-dev` en dev), explicite prioritaire, Gitea inchangée (suite de #223) | 2.78.2 | [ISSUES_TODOLIST.md](./ISSUES_TODOLIST.md) |
--- ---
+16
View File
@@ -67,6 +67,22 @@ nonce CSP par le mécanisme existant) dans `index.html` au service.
`/static/*`, et service worker **network-first** pour le code — aucun `/static/*`, et service worker **network-first** pour le code — aucun
cache-busting par empreinte n'existe (revalidation systématique). cache-busting par empreinte n'existe (revalidation systématique).
## Correctif BUG-120 (2026-10-25) — dérivation d'environnement des sœurs
Constat sur la dev : le `.env` du poste ne déclare que l'agent (via
`AGENT_EVENEMENTS_URL`) ; les quatre `DRACO_<APP>_URL` absentes
retombaient sur les défauts prod dans `window.DRACO_URLS` — le menu
inter-apps pointait la prod depuis la dev. La résolution d'une URL
sœur (`backend/draco_config.py::app_url`) gagne une étape
intermédiaire, entre l'explicite et le défaut officiel :
**dérivation depuis l'hôte de l'agent** (`agent_url()`) — hôte de la
forme `agent<SUFFIXE>.dracodev.net` sans port → sœur =
`https://<slug><SUFFIXE>.dracodev.net` (slugs : `og`, `flowdeck`,
`tiktak`, `kanbante`). Hôte hors motif (autre domaine, LAN, IP,
port) → aucune dérivation. Gitea exclue. Le dev n'a plus quatre
variables à déclarer ; un `.env` qui les déclare déjà est inchangé
(l'explicite prime).
## Restes assumés ## Restes assumés
- Suffixe SSO `dracodev.net` (`draco_session.validate_return_to`, - Suffixe SSO `dracodev.net` (`draco_session.validate_return_to`,
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "obsigate", "name": "obsigate",
"version": "2.78.1", "version": "2.78.2",
"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": {
+71 -2
View File
@@ -53,6 +53,70 @@ def test_agent_explicite_prime(env_propre):
assert draco_config.agent_url() == "https://agent.example.net" assert draco_config.agent_url() == "https://agent.example.net"
# ------------------------------------------------- BUG-120 : dérivation sœurs
def test_soeurs_derivees_depuis_agent_dev(env_propre):
"""BUG-120 : avec **seul** ``AGENT_EVENEMENTS_URL`` en ``-dev``
posé (aucune ``DRACO_<APP>_URL``), les URL sœurs servies suivent
l'environnement de l'agent ; Gitea ne bouge jamais."""
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
urls = draco_config.urls_publiques()
assert urls["agent"] == "https://agent-dev.dracodev.net"
assert urls["obsigate"] == "https://og-dev.dracodev.net"
assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net"
assert urls["tiktak"] == "https://tiktak-dev.dracodev.net"
assert urls["kanbante"] == "https://kanbante-dev.dracodev.net"
assert urls["gitea"] == "https://git.dracodev.net"
def test_soeurs_derivees_depuis_draco_agent_url(env_propre):
"""BUG-120 : la dérivation lit l'agent via ``agent_url()`` — un
``DRACO_AGENT_URL`` en ``-dev`` suffit aussi."""
env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
assert draco_config.app_url("tiktak") == "https://tiktak-dev.dracodev.net"
assert draco_config.app_url("obsigate") == "https://og-dev.dracodev.net"
def test_soeur_explicite_prime_sur_derivation(env_propre):
"""BUG-120 : une ``DRACO_<APP>_URL`` explicite gagne toujours sur
la dérivation d'environnement ; les autres sœurs dérivent."""
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
env_propre.setenv("DRACO_TIKTAK_URL", "https://taches.example.net/")
assert draco_config.app_url("tiktak") == "https://taches.example.net"
assert draco_config.app_url("flowdeck") == "https://flowdeck-dev.dracodev.net"
assert draco_config.app_url("kanbante") == "https://kanbante-dev.dracodev.net"
@pytest.mark.parametrize("evenements", [
"https://agent.example.net/api/v1/evenements", # autre domaine
"https://agent-dev.example.com/api/v1/evenements", # suffixe hors dracodev.net
"http://deepin.lab.home:8790/api/v1/evenements", # nom LAN + port
"http://192.168.30.101:8790/api/v1/evenements", # IP + port
"https://agent-dev.dracodev.net:8790/api/v1/evenements", # port exotique
])
def test_agent_hors_motif_aucune_derivation(env_propre, evenements):
"""BUG-120 : hôte d'agent hors motif ``agent<SUFFIXE>.dracodev.net``
→ **aucune dérivation** (jamais de lien deviné), défauts officiels."""
env_propre.setenv("AGENT_EVENEMENTS_URL", evenements)
urls = draco_config.urls_publiques()
assert urls["obsigate"] == "https://og.dracodev.net"
assert urls["flowdeck"] == "https://flowdeck.dracodev.net"
assert urls["tiktak"] == "https://tiktak.dracodev.net"
assert urls["kanbante"] == "https://kanbante.dracodev.net"
assert urls["gitea"] == "https://git.dracodev.net"
def test_agent_prod_suffixe_vide_defauts_officiels(env_propre):
"""BUG-120 : hôte d'agent prod (suffixe vide) → les sœurs dérivées
sont exactement les défauts officiels."""
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://agent.dracodev.net/api/v1/evenements")
for app, defaut in draco_config.DEFAUTS.items():
assert draco_config.app_url(app) == defaut
def test_journal_url_suit_la_config(env_propre): def test_journal_url_suit_la_config(env_propre):
from backend import agent_journal from backend import agent_journal
@@ -106,10 +170,15 @@ def test_url_affichage_reconstruite_depuis_config(env_propre, tmp_path):
# ------------------------------------------------------------ injection hôte # ------------------------------------------------------------ injection hôte
def test_index_injecte_draco_urls(client, env_propre): def test_index_injecte_draco_urls(client, env_propre):
"""L'hôte sert index.html avec window.DRACO_URLS issu de sa config.""" """L'hôte sert index.html avec window.DRACO_URLS issu de sa config.
BUG-120 : l'agent en ``-dev`` entraîne les sœurs en ``-dev``
(dérivation d'environnement) — avant, ``tiktak`` restait en prod.
"""
env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net") env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
resp = client.get("/") resp = client.get("/")
assert resp.status_code == 200 assert resp.status_code == 200
assert "window.DRACO_URLS" in resp.text assert "window.DRACO_URLS" in resp.text
assert '"agent": "https://agent-dev.dracodev.net"' in resp.text assert '"agent": "https://agent-dev.dracodev.net"' in resp.text
assert '"tiktak": "https://tiktak.dracodev.net"' in resp.text assert '"tiktak": "https://tiktak-dev.dracodev.net"' in resp.text
assert '"gitea": "https://git.dracodev.net"' in resp.text