# Changelog — ObsiGate Toutes les modifications notables d'ObsiGate sont documentées dans ce fichier. Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section > [Unreleased](#unreleased). La dernière version livrée est **2.3.4**. --- ## [Unreleased] --- ## [2.3.4] — 2026-09-16 ### Documentation - **AGENTS.md — versionnage automatique** : la méthode obligatoire de fin de tâche rappelle désormais que `VERSION` (racine) est la source unique de vérité (`MAJEUR.MINEUR.CORRECTIF`), incrémentée à chaque commit par le hook `.githooks/prepare-commit-msg`, que les dérivés (`package.json`, desktop Tauri, READMEs, `docs/ROADMAP.md`, `CHANGELOG.md`) sont resynchronisés dans le même commit, et que le tag `vX.Y.Z` est publié au push. Ligne ajoutée à la cartographie documentaire. Ménage : suppression des captures de diagnostic (`diag-*.png`, `ogdiag.png`, `state.png`) et du script jetable `tmp-verify-inline.cjs` restés à la racine du dépôt. --- ## [2.3.3] — 2026-09-16 ### Corrigé - **#93 (complément) — garde-fous de session d'édition inline** : une session d'édition (« Editer » ou Forge) qui possède la zone de contenu était détruite par deux chemins qui vident cette zone sans la libérer d'abord — l'**activation d'un onglet** (`TabManager.activate`, `PaneTabManager.activate`) et l'événement SSE **`index_updated`** sur le fichier affiché (un fichier modifié hors de l'app relançait `openFile()` et écrasait le DOM de l'éditeur). L'activation d'onglet appelle désormais `detachInlineEditor()` avant le placeholder de chargement, et l'écriture externe passe par `reloadExternalWrite()` (le **tampon de l'éditeur** est rechargé depuis le disque au lieu d'un re-rendu de la vue lecture). Nouveau helper `queryEditor()` (`editor-inline.js`) : l'en-tête, le pied et la marque voyagent avec le conteneur en mode inline, un `modal.querySelector()` les manquait ; `closeEditor()` remet le conteneur dans la modale **avant** de restaurer en-tête/pied/marque. Fiche : [docs/features/editeur-inline.md](./docs/features/editeur-inline.md). Tests : `tests/frontend/editor-inline.test.mjs` (25). - **Documentation — hooks git obligatoires à l'installation** : `docs/CONTRIBUTING.md` documente l'appel à `scripts/install-hooks.sh` (versionnage automatique) dans les étapes de mise en place d'un clone. --- ## [2.3.2] — 2026-09-16 --- ## [2.3.1] — 2026-09-16 --- ## [2.3.0] — 2026-09-16 ### Ajouté - **#89 Drag & Drop complet de fichiers/dossiers & intégration Assistant IA** — solution complète de glisser-déposer de fichiers individuels, multiples et **dossiers récursifs** depuis l'OS vers le vault (`POST /api/vault/{vault}/batch-upload`, FileSystem API), overlay plein écran à double cible (dépôt vault vs analyse IA), surbrillance des cibles dans l'arborescence, et dropzone BooksLM pour injection de contexte ou pièces jointes. Fiche : [docs/features/drag-and-drop-ai.md](./docs/features/drag-and-drop-ai.md). - **#89 Pretty print fichiers texte** — bouton « Pretty » dans le mode lecture des fichiers non-markdown (code, JSON, configs, logs...) : formatage JSON, numéros de ligne avec gouttière et coloration syntaxique highlight.js alignée sur le thème clair/sombre, avec colorateur générique de secours (chaînes, commentaires, nombres, niveaux de log) quand highlight.js ne reconnaît pas le langage (txt, log, plaintext). - **#88 Assistant IA : contexte applicatif** — l'assistant en mode **Général** reçoit l'état vivant de l'interface (`app_context`) et connaît désormais les documents ouverts, le répertoire et le vault courants, la recherche en cours (requête + premiers résultats affichés) et les fichiers récemment modifiés du utilisateur. Le prompt Général est enrichi d'un bloc « Contexte applicatif actuel » (`build_general_system_prompt(vaults, app_context, recent_files)`). Fiche : [docs/features/ai-app-context.md](./docs/features/ai-app-context.md). - **#90 Barre d'actions du document — regroupement fonctionnel** : les boutons en haut du document sont réordonnés en quatre groupes séparés par des barres verticales (`span.action-sep`) : `[TOC] [pop-out] [Bookmark] | [Editer] [Source] [.md] [Forge] | [Copier] [PDF] [Export] | [Partager]`. Le bouton « Pretty » (fichiers texte) rejoint le groupe édition, les boutons non applicables à un type de fichier sont omis et les spacers d'un groupe vide ne sont pas rendus. Fichiers texte non markdown : `[pop-out] [Bookmark] | [Editer] [Source] [.ext] [Pretty] | [Copier] | [Partager]` (pas de TOC, pas de PDF/Export). La fenêtre pop-out (`popout.html`) est alignée sur le même ordre markdown (groupes nav / édition / export / partage). Tests : `tests/frontend/toolbar-order.test.mjs` (9). - **#91 Assistant IA — zone de discussion façon Notion** : le fil de discussion est reformaté (messages chronologiques, post utilisateur en bulle alignée à droite, réponse assistant pleine largeur sans fond, appels d'outils agent repliés en bloc « N étapes ») ; après l'envoi, la **question est ancrée en haut** de la zone visible (`scrollIntoView` `block:start` + `scroll-margin-top`) et la réponse se diffuse en dessous — le streaming ne provoque plus de saut de scroll ; un libellé **discret** au-dessus de chaque réponse rapporte le fournisseur et le modèle réellement utilisés (SSE `provider`/`model`) ; une **barre d'actions** au survol sous chaque bloc : « Copier » (texte brut + toast) et, pour les réponses, « Ajouter » (insertion dans le document ouvert dans l'éditeur Forge). Complément : la section « N étapes » affiche désormais des libellés humains produits par le backend (`backend/tools/labels.py`, événements SSE en direct pendant l'exécution + ligne « Réflexion »), et deux nouveaux outils web principaux sont disponibles en mode agent : `web_search` (SearXNG auto-hébergé) et `fetch_url` (page publique → texte, garde SSRF). Le reste des catégories d'outils Notion est documenté pour le futur dans la fiche. Fiche : [docs/features/ai-assistant-conversation-ux.md](./docs/features/ai-assistant-conversation-ux.md) Tests : `tests/frontend/ai.test.mjs` (76), `tests/test_tool_labels.py` (8), `tests/test_web_tools.py` (8). - **#91 Assistant IA — section d'étapes enrichie** : l'en-tête affiche « N étapes » suivi d'un **chevron ▶ / ▼** (fin du préfixe « > ») et, pendant l'exécution, un **indicateur animé** de trois points devant le libellé ; la **barre de chargement** au-dessus de la zone de saisie est supprimée (en chat simple, l'indicateur s'affiche dans la bulle de réponse en attente). Chaque note de raisonnement devient une **sous-section « Réflexion ▶ / ▼ »** contenant le texte du modèle, et les recherches web ajoutent une sous-section **« Sources (N) »** avec les **liens cliquables** (le backend émet `sources [{title, url}]` dans l'événement SSE `tool`, résultats `web_search` plafonnés à 8 + page `fetch_url`). Les états d'ouverture (étapes, réflexion, sources) sont mémorisés sur le message : un token re-rendu ne referme jamais ce que l'utilisateur a déplié. `web_search` signale désormais explicitement au modèle une instance SearXNG sans résultat (moteurs suspendus/CAPTCHA) au lieu de le laisser relancer la même recherche jusqu'au quota. Tests : `tests/frontend/ai.test.mjs` (82), `tests/test_bookslm.py::TestToolEventSources` (4), `tests/test_web_tools.py` (10). ### Modifié - **#93 Édition inline — « Editer » et « Forge » remplacent la vue lecture** : le conteneur d'édition (`#editor-container`, CodeMirror ou iframe Forge) est déplacé **dans la zone de contenu du document** (`#content-area`, ou le panneau actif en vue fractionnée) au lieu d'être affiché dans l'overlay plein écran : le mode édition **remplace** le document en mode lecture (bandeau, métadonnées, tags et contenu). L'overlay reste monté mais neutralisé (transparent, sans capture des clics, `z-index` conservé) car le ruban d'édition mobile y est ancré. Retour à la lecture par ✓ (sauvegarde), ✕, Échap ou l'ouverture d'un autre fichier — trois garde-fous libèrent proprement la session (destruction CodeMirror/Yjs, retrait de l'iframe Forge) avant qu'un rendu concurrent n'efface son DOM : `renderFile()`, l'activation d'onglet (`TabManager.activate`, `PaneTabManager.activate`, qui vident la zone avant de rendre) et l'événement SSE `index_updated` (un fichier modifié hors de l'app recharge le **tampon de l'éditeur** au lieu de re-rendre la vue lecture). L'overlay n'est conservé qu'en repli, quand la cible n'est pas le document affiché (ex. fichier créé depuis la palette). Conséquence directe : l'éditeur ne recouvre plus le panneau de l'assistant IA, qui peut donc mettre le document à jour **sous les yeux de l'utilisateur** — `app_context.editing` annonce au modèle le document en cours d'édition (et sa surface), et toute écriture d'outil (`edit_file`, `append_to_file`, `create_file`, `restore_backup`) recharge le document affiché depuis le disque (`obsigate:file-written`) : le tampon de l'éditeur est rafraîchi au lieu d'écraser la modification de l'assistant par son auto-sauvegarde, et l'iframe Forge reçoit `parent-reload`. Aucun identifiant DOM n'a changé (autosave, collab Yjs, barre IA, aperçu Mermaid, Forge intacts). Fiche : [docs/features/editeur-inline.md](./docs/features/editeur-inline.md). Tests : `tests/frontend/editor-inline.test.mjs` (19), `tests/test_bookslm.py::TestGeneralPrompt` (3 nouveaux). ### Corrigé - **BUG-047 — La version affichée ne suivait pas les livraisons** : la version provenait du **dernier tag git**, or aucun tag n'était créé lors des livraisons → l'application restait bloquée sur `2.2.1` alors que 66 commits avaient été livrés (et les numéros codés en dur divergeaient : `package.json` 1.0.0, desktop Tauri 2.0.0, `Dockerfile` 2.2.1, README 1.7.0). Le fichier **`VERSION` (racine du dépôt) devient la source unique de vérité** (`MAJEUR.MINEUR.CORRECTIF`), incrémenté **automatiquement à chaque commit** par le hook versionné `.githooks/prepare-commit-msg` (`!:`/`BREAKING CHANGE` → MAJEUR, `feat` → MINEUR, sinon CORRECTIF) ; `.githooks/post-commit` crée le tag `vX.Y.Z` et `push.followTags` le publie à chaque push. Tous les dérivés sont resynchronisés dans le même commit : `package.json`, desktop Tauri (`tauri.conf.json`, `Cargo.toml`, `Cargo.lock`), `docs/ROADMAP.md`, `README.md`/`README.fr.md` et rotation de `CHANGELOG.md` (`[Unreleased]` → `[X.Y.Z] — date`). Le backend (`backend/version.py`), l'image Docker (`COPY VERSION`) et le desktop Tauri lisent ce même fichier : plus aucun numéro codé en dur dans le pipeline. Outils : `scripts/bump_version.py` (+ `scripts/bump_version.sh`), `scripts/install-hooks.sh`. Tests : `tests/test_version.py`, dont un garde-fou de cohérence globale (VERSION ↔ package.json ↔ desktop ↔ CHANGELOG ↔ ROADMAP ↔ README). Vérifié : `curl /api/health` sur l'instance de test → `2.3.0`. - **BUG-045 — Double barre de défilement dans l'éditeur « Editer »** : sur les documents longs, `#editor-body` (qui gardait `overflow:auto`) et le scroller CodeMirror débordaient simultanément — la barre d'outils IA empilée sous un `.cm-editor` en `height:100%` faisait déborder le corps, et un ancien override global `.cm-scroller` forçait une seconde zone défilante. La surface d'édition est passée en flex column : le corps ne défile plus (`overflow: hidden`), l'éditeur remplit l'espace restant et **seul le scroller CodeMirror défile**. Repli textarea (fallback) cohérent ; la règle mobile BUG-020 est préservée. - **BUG-046 — « Échec de l'action : [object Object] » (assistant IA, ajout de texte au document courant)** : trois causes corrigées ensemble. (1) La carte de confirmation « Appliquer » reprenait un payload dont le message était absent après une première application échouée (continuation `payload:null`) → réponse 422 du backend. (2) Cette réponse 422 porte un `detail` **tableau d'objets** (format validation FastAPI) que `new Error(detail)` réduisait en `[object Object]` dans le toast — nouvelle méthode `_responseError()` qui aplatit tableaux et objets en message lisible. (3) Le prompt des modes documents/dossier ne nommait pas le vault : le modèle inventait parfois `"vault":"test"` et l'outil d'écriture échouait silencieusement — le prompt indique désormais le vault exact et enjoint les outils `edit_file` / `append_to_file` / `create_file` de l'utiliser. Cache service worker `SW_VERSION` v20. - **#91 — Bulle utilisateur trop étroite & ancre d'envoi retardée** : le plafond de largeur était appliqué **deux fois** (wrapper 80 % × bulle 85 % ≈ 68 % du fil), d'où des lignes très courtes ; le plafond est désormais porté par la seule bulle (90 %), le wrapper s'étirant → ~81 % de la largeur du fil (≈48 caractères/ligne au lieu de ~28). Par ailleurs, le rendu du message assistant « placeholder » écrasait la position de défilement juste après l'envoi (l'ancre était annulée : la question restait en bas jusqu'au premier token). `_isLoading` est maintenant posé avant ce rendu et l'ancrage d'envoi est **instantané** — la question passe en haut du fil dès la soumission et y reste pendant tout le streaming. - **#91 — Question complètement en haut** : la marge `scroll-margin-top` (14 px) laissait visible la fin de la réponse précédente au-dessus du post, et une réponse courte ne remplissait pas la fenêtre — le navigateur bloquait alors le défilement et la question restait à mi-hauteur. La marge est supprimée et le `padding-bottom` du fil est augmenté de la place manquante tant que l'ancre est active (retiré au dé-épinglage, aucun remplissage si le fil tient dans la fenêtre). Le rendu final post-streaming conserve aussi l'ancre, et la libération se fait sur molette, toucher **ou** clic dans le fil. Enfin, `overflow-anchor: none` sur le fil neutralise le ré-ancrage automatique de Chrome (le fil est reconstruit à chaque token, ce qui décalait la vue de ~33 px) et une passe de correction recale le défilement d'après la géométrie mesurée. - **#91 — Tag fournisseur/modèle incomplet** : le backend renvoyait le modèle *demandé par le client* (`req.model or ""`), donc une valeur vide dès que le client s'en remet au défaut du fournisseur — le tag se réduisait à « openrouter ». `_effective_model()` résout désormais le modèle réellement utilisé dans les deux flux SSE (`/chat` et `/agent`) : le tag affiche « openrouter · openai/gpt-4o-mini ». Couvert par `tests/test_bookslm.py::TestEffectiveModel` et le test SSE de l'endpoint agent. - **BUG-041 — Assistant IA bloqué sur un répertoire vide** : l'assistant ne renvoie plus `⚠ Error: Aucun fichier markdown trouvé dans ce dossier` (HTTP 404). Le contexte vide dégrade vers le prompt Général augmenté d'un bloc « Dossier vide », et la requête aboutit sans contexte documentaire. - **BUG-042 — Liens de fichiers de l'assistant non fiables** : les liens des réponses suivent désormais une règle déterministe — un simple nom de fichier copie le nom dans le presse-papiers, un chemin de dossier est révélé dans l'arborescence, un chemin de fichier l'ouvre. Le chemin est **résolu contre l'index du vault** (exact → suffixe → basename unique) avant d'agir ; les chemins non résolus copient le nom au lieu d'afficher `File not found`. Les **noms/chemins contenant des espaces** sont pris en charge (`Mon dossier/Ma note.md`) : liens markdown (y compris cibles `<…>` et `%20`), code inline, et mentions brutes liées uniquement si le chemin existe dans l'index du vault. Les **caractères accentués** sont reconnus (classes de caractères Unicode `\p{L}\p{N}\p{M}`, comparaison normalisée NFC : une mention décomposée `e`+accent correspond à une entrée d'index précomposée) et un chemin **préfixé par le nom du vault** (`TestVault/Recettes/Pizza Maison.md`) est résolu puis ouvert dans ce vault, le préfixe étant retiré. - **BUG-043 — Liste des fournisseurs de l'assistant non synchronisée avec la configuration** : ajouter ou supprimer une clé API dans la configuration du projet met désormais à jour **immédiatement** le menu Fournisseur de la barre latérale de l'assistant, sans recharger la page. Le picker ne lisait `/api/ai/status` qu'une fois, à sa construction, et le panneau de l'assistant est un singleton monté pour toute la session : la liste restait figée. Le nouveau `refreshAIPickers()` (`frontend/js/ai.js`) reconstruit chaque picker monté dans son emplacement `.ai-picker-slot` et est appelé après l'enregistrement (`saveAIKeys`) et la suppression (`deleteAIKey`) d'une clé (`frontend/js/config.js`). Un emplacement est conservé même sans fournisseur configuré, donc le **premier** fournisseur ajouté s'y monte aussi. Une sélection dont le fournisseur n'est plus configuré est purgée de `obsigate_ai_picker` (retour au défaut au lieu d'un modèle fantôme). - **BUG-044 — Capacités des modèles erronées (aucun modèle Mistral vision)** : la bulle ⓘ et le sélecteur de modèle par défaut n'affichaient **aucun** modèle Mistral « Vision capable », alors que `GET https://api.mistral.ai/v1/models` en déclare 28 (`mistral-medium`, `mistral-small`, `ministral-*`, `magistral-*`, `mistral-ocr-*`, `mistral-vibe-cli-*`). Cause : la table de capacités était **entièrement statique** et aucun de ses motifs ne correspondait aux familles Mistral actuelles (seul `pixtral`, retiré de l'API, les matchait). ObsiGate lit désormais les capacités **déclarées par le fournisseur** (nouveau `backend/provider_capabilities.py`, snapshot mis en cache par `GET /api/config/ai-models`) : Mistral (`capabilities.completion_chat` / `vision` / `audio_transcription` / `audio_speech`) et OpenRouter (`architecture.input_modalities` / `output_modalities`) sont pris en charge ; la table statique ne sert plus qu'à combler les drapeaux non déclarés et de repli hors ligne. La table est également corrigée (familles vision Mistral, `mistral-ocr` = vision sans chat) et le défaut du fournisseur Mistral ne prétend plus qu'un modèle non reconnu sait produire des embeddings (BUG-044bis : `mistral-large-latest` / `codestral-latest` étaient annoncés « Embeddings »). Conséquence : la porte vision de l'assistant (`frontend/js/bookslm.js`, `backend/bookslm_routes.py`) accepte enfin les images avec un modèle Mistral vision. ### Sécurité - **#84 Consolidation & sécurité — phase 1 (BUG-021 → BUG-034)** — traitement des vulnérabilités de la revue statique du 2026-09-13 : - **BUG-021/022 — XSS stocké** : nouveau sanitizer serveur en liste blanche (`backend/services/sanitizer.py`, stdlib) appliqué au rendu markdown ; échappement systématique du `title`, du frontmatter et du JSON de la page publique `/s/{token}` (`` neutralisé). Tests : `tests/test_security_hardening.py` (9). - **BUG-023 — brute-force MFA** : rate-limit IP + compte et verrouillage de compte sur `mfa/totp/verify`, `mfa/recovery` et `mfa/webauthn/verify` (`_enforce_mfa_rate_limit`). - **BUG-024 — traversal inter-vaults** : `resolve_safe_path` compare désormais les chemins par **segment** (`Path.relative_to` + repli casse-insensible), plus par préfixe de chaîne : `vault` ne peut plus lire `vault-evil`. - **BUG-025 — ReDoS** : validation des regex utilisateur (longueur max, rejet des quantificateurs imbriqués/backrefs), contenu tronqué et nombre de matchs plafonné (`backend/services/regex_safety.py`), appliqué à la recherche avancée et au find/replace. - **BUG-026 — SSRF webhooks** : validation d'URL (HTTPS par défaut, IP privées/boucle interdites, résolution DNS vérifiée au dispatch, redirections non suivies) et **externalisation du secret** dans `data/webhook_secrets.json` (0600) ou variable `OBSIGATE_WEBHOOK_SECRET_` (plus de secret en clair dans `webhooks.json`). - **BUG-027 — sessions** : rotation du refresh token à chaque usage, révocation du JTI de l'access token au logout, et vérification de la révocation dans le middleware. - **BUG-028 — politique de mot de passe** : validation centralisée (8–128 caractères) à la création, à la modification admin et au changement ; `password_changed_at` invalide tous les jetons émis avant un changement de mot de passe. - **BUG-029 — race `users.json`** : verrou `threading.RLock` autour des cycles lecture-modification-écriture. - **BUG-030 — audits** : l'adresse IP réelle du client (`X-Forwarded-For` si `OBSIGATE_TRUST_PROXY=true`) est injectée dans `current_user` et consignée dans les audits. - **BUG-031 — rate-limiter** : budget **par compte** en plus du budget par IP (rotation d'IP neutralisée) ; limite mono-process documentée. - **BUG-032 — indexation** : `_scan_vault` utilise `os.walk(followlinks=False)` et refuse tout symlink sortant de la racine du vault. - **BUG-033 — recherche O(N)** : la recherche simple et l'outil IA `search_fulltext` utilisent l'inverted index (repli sur le scan pendant la construction). - **BUG-034 — CSP & jetons** : le token d'accès n'est plus persisté dans `sessionStorage` (mémoire + cookie `HttpOnly`) ; directives CSP durcies (`object-src`, `base-uri`, `form-action`, `frame-ancestors`). *Reste : migration CSP par nonce (exige la conversion des gestionnaires d'événements inline).* ### Ajouté - **#83 Barre d'outils d'édition mobile — ruban style Obsidian Android** — la barre de mise en forme flottante est remplacée par un **ruban horizontal défilable** ancré juste au-dessus du clavier virtuel : fond anthracite aux coins arrondis, insertion/enrobage au curseur ou sur la sélection. Commandes : annuler/refaire, `[[ ]]` (lien interne), fichiers/modèles, tag `#`, pièce jointe, H1–H6, gras, italique, barré (`~~`), surligné (`==`), code en ligne/bloc, citation, lien externe, listes à puces/numérotées, case à cocher, indenter/désindenter, coller, A−/A+. Une icône **⚙** ouvre un panneau de personnalisation (ajouter / retirer / réordonner, persistance `localStorage`). i18n FR/EN. Helpers purs exportés (`formatChange`, `normalizeRibbonCommands`, `moveRibbonCommand`). Tests JSDOM `tests/frontend/mobile-editor.test.mjs` (+13) et E2E mobile. - **#82 Assistant IA — menu `@` instantané** — nouvel endpoint `GET /api/vault/{vault}/paths` (liste plate et plafonnée de l'index des chemins). Le frontend la récupère **une fois par vault** (préchargée à l'ouverture du panneau) puis filtre **côté client** pendant la frappe : l'affichage des fichiers/répertoires est immédiat, au lieu d'une requête réseau par caractère. Tests : `tests/test_api_main.py` (+2), `tests/frontend/ai.test.mjs` (+1). - **#82 Assistant IA — Section « Fournisseur & modèle » compacte** — la barre de sélection du modèle devient **discrète** : la liste complète des types d'endpoints (Chat, Embeddings, Vision…) est remplacée par un **bouton d'information ⓘ** qui affiche les capacités dans une **bulle** au survol, au clic ou par **appui long** (mobile). Le choix du modèle passe par une **liste déroulante avec recherche** (filtrage insensible aux accents) au lieu d'un `