# 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.11.6**.
---
## [Unreleased]
---
## [2.11.6] — 2026-09-18
### Corrigé
- **BUG-064 - Éditeur Excalidraw : le diagramme ne s'affiche jamais (canvas vide)** — deux
causes cumulées :
1. **La feuille de style d'Excalidraw n'était jamais chargée** (`@excalidraw/excalidraw` exige
un import CSS explicite). Sans elle, l'éditeur est non stylisé et `.excalidraw` n'a aucune
hauteur fixe : la boucle de redimensionnement d'Excalidraw fait grossir le canvas jusqu'au
plafond codé en dur de `2^25` (33 554 432 px), que le navigateur ne peut pas dessiner → scène
blanche. Correctifs : `` vers `…/@excalidraw/excalidraw@0.18.0/dist/prod/index.css` dans
`frontend/excalidraw-editor.html` et ajout de `https://esm.sh` à `style-src` de la CSP
(`backend/main.py`).
2. `appState.collaborators` est une `Map` sérialisée en objet JSON (`{}`) par l'app Excalidraw /
le plugin Obsidian ; réinjectée via `initialData`, Excalidraw 0.18 appelait `.forEach()` dessus
et plantait (`e.appState.collaborators.forEach is not a function`). `sanitizeAppState()` la
reconvertit en `Map` et écarte la géométrie de viewport importée (`width`, `height`,
`offsetLeft`, `offsetTop`) pour les deux formats (`.excalidraw` et `.excalidraw.md`).
Tests de non-régression : `tests/frontend/excalidraw-viewer.test.mjs` (CSS lié + CSP),
`tests/test_security_hardening.py::TestCspExcalidrawStylesheet`, `tests/e2e/excalidraw.spec.js`
(hauteur de canvas bornée) + fixtures `test_vault/diagram-app-export.excalidraw`.
- **BUG-064 (complément) - Éditeur Excalidraw : pleine largeur quand la navigation est masquée** :
la règle `.sidebar.hidden ~ .content-wrapper .content-area { max-width: 1200px }` (colonne de
lecture centrée) s'appliquait aussi au viewer Excalidraw. Ajout de
`.content-area:has(iframe[src*="excalidraw-editor.html"])` en `max-width: none; margin: 0`,
comme pour les viewers PDF/image (BUG-062). Fichier : `frontend/style.css`.
- **BUG-065 - Excalidraw : l'auto-save rechargeait la page en pleine édition** : chaque
modification déclenchait, 2 s plus tard, un `PUT /api/file/…/save` ; l'écriture émettait
`index_updated` (SSE) qui re-rendait la vue et **recréait l'iframe** — un refresh visible qui
interrompait le dessin. L'auto-save est supprimée (`frontend/js/excalidraw-viewer.js`) :
sauvegarde explicite par le bouton « 💾 Save » ou `Ctrl+S`. En complément, `reloadExternalWrite`
ne re-rend plus la vue quand un iframe Excalidraw est déjà ouvert sur le fichier
(`iframe[data-excalidraw-*]`), et le badge « Modified » ne se déclenche plus sur les
changements d'`appState` (resize, zoom) mais uniquement sur le contenu (signature des éléments).
- **#78 (complément) - Bouton plein écran pour les diagrammes Excalidraw** : nouveau bouton
`#btn-fullscreen` dans la barre d'outils de l'éditeur (`frontend/excalidraw-editor.html`) qui
bascule le mode plein écran natif ; l'iframe est créée avec `allow="fullscreen" allowfullscreen`
(`frontend/js/excalidraw-viewer.js`). Fichiers : `frontend/excalidraw-editor.html`,
`frontend/js/excalidraw-viewer.js`, `tests/frontend/excalidraw-viewer.test.mjs`.
- **#78 (complément) - Barre d'outils Excalidraw en icônes, verticale à droite** : les boutons
Save / PNG / SVG / plein écran passent en **icônes seules** (34×34 px) dans une colonne
**collée au bord droit** (`right: 0`), débutant à `45%` de la hauteur, empilée verticalement,
avec infobulles et `aria-label`. L'icône du bouton Save devient une coche après une sauvegarde
réussie. Fichier : `frontend/excalidraw-editor.html`.
---
## [2.11.5] — 2026-09-17
### Corrigé
- **BUG-063 (complément) — Viewer PDF : la table des matières ne déplaçait toujours pas la
page** : le premier correctif réassignait `iframe.src` avec le seul fragment `#page=N`, ce qui
ne change que le fragment → navigation *same-document* que le lecteur PDF natif ignore (il
n'applique `#page=N` qu'au chargement). Diagnostic en Chrome *headful* par comparaison de
captures. `navigatePdfToPage()` ajoute désormais un paramètre de query horodaté
(`&_pdfpage=#page=N`) pour forcer un vrai rechargement de l'iframe. Fichiers :
`frontend/js/viewer.js`, `tests/frontend/pdf-viewer.test.mjs`, `tests/e2e/pdf-viewer.spec.js`.
---
## [2.11.4] — 2026-09-17
### Corrigé
- **BUG-061 — Assistant IA : le bouton « Plein écran » n'agrandissait plus le panneau** : la
largeur du panneau est écrite en style inline par la poignée de redimensionnement (et par la
largeur persistée en `localStorage`) ; cet inline l'emportait sur la règle
`.bookslm-panel.fullscreen { width: 100vw }`, donc le panneau restait à sa largeur courante.
La règle plein écran est désormais prioritaire (`!important`). Fichier : `frontend/style.css`.
- **BUG-062 — Viewer PDF : largeur incomplète quand la navigation est masquée** : la règle de
colonne de lecture centrée (`.sidebar.hidden … { max-width: 1200px }`) s'appliquait aussi aux
viewers plein cadre. Les conteneurs PDF et image sont maintenant exemptés
(`:has(.pdf-viewer-container)` / `:has(.image-viewer-container)` → `max-width: none`). Fichier :
`frontend/style.css`.
- **BUG-063 — Viewer PDF : la table des matières ne naviguait pas** : les liens faisaient
`contentWindow.location.hash = 'page=N'`, mais le lecteur PDF natif vit dans une fenêtre
`about:blank` et l'affectation n'atteignait jamais le document. Nouveau helper
`navigatePdfToPage()` qui recharge l'iframe avec le fragment `#page=N` ; les entrées portent un
`data-page` et sont câblées par des écouteurs (plus d'`onclick` inline). Fichiers :
`frontend/js/viewer.js`, `frontend/style.css`.
- **Tests** : `tests/frontend/ai.test.mjs` (+1), `tests/frontend/pdf-viewer.test.mjs` (TOC, plein
largeur), `tests/e2e/pdf-viewer.spec.js` (TOC `#page=N`, largeur, fixture
`test_vault/sample-pdf-toc.pdf`).
---
## [2.11.3] — 2026-09-17
### Corrigé
- **BUG-035 — `secret_redactor` : faux positifs sur les hashs hex** : la règle qui masquait
tout jeton hexadécimal de 40 à 64 caractères mutilait les hashs git/SHA légitimes des notes.
Le masquage des chaînes hexadécimales n'a désormais lieu que si un mot-clé de secret
(`secret`, `token`, `key`, `password`, `bearer`…) figure dans les 60 caractères précédents ;
un contexte de hash (`commit`, `sha256`, `hash`, `checksum`, `git`, `etag`…) exempte
explicitement la chaîne. Fichier : `backend/secret_redactor.py`.
- **BUG-036 — Collaboration WebSocket : jeton accepté en query string** : le JWT n'est plus lu
depuis `?token=` (URLs journalisées par les proxies et l'historique navigateur). Le cookie
HttpOnly `access_token`, envoyé automatiquement par le navigateur lors du handshake
same-origin, est le seul transport supporté ; les trames brutes dépassant
`MAX_MESSAGE_CHARS` (16 Mio) sont rejetées avant analyse. Fichier : `backend/collab.py`.
- **BUG-037 — Mode sans authentification** : au démarrage, un avertissement explicite est
journalisé quand `OBSIGATE_AUTH_ENABLED=false`. Le serveur **refuse désormais de démarrer**
s'il est lié à une adresse non-loopback sans l'opt-in explicite `OBSIGATE_ALLOW_INSECURE=true`,
pour empêcher l'exposition publique d'une instance sans authentification (admin anonyme).
Fichiers : `backend/auth/middleware.py`, `backend/main.py`.
- **BUG-038 — Argon2 : coût mémoire recalibré** : `memory_cost` passe de 64 Mio à 19 Mio
(`m=19456 Kio, t=2, p=1`, recommandation OWASP actuelle) pour supprimer le risque
d'épuisement mémoire sous connexions simultanées ; les anciens hachages restent valides et
sont migrés automatiquement (`needs_rehash`). Fichier : `backend/auth/password.py`.
- **BUG-039 — Énumération de comptes au login** : les comptes inconnus, désactivés, verrouillés
et limités par le budget par compte répondent tous un `401 Identifiants invalides` avec un
temps équivalent (hachage factice), au lieu d'un `429`/`403` distinctif ; seul le rate-limit
par IP (non lié à un compte) conserve le `429`. Fichier : `backend/auth/router.py`.
- **BUG-040 — Extraction PDF différée au scan** : `_scan_vault` ne lit plus que les métadonnées
des PDF ; l'extraction de texte intégrale (100 kio) est déléguée à `enrich_pdf_texts()`,
exécutée après la construction de l'index/inverted index (démarrage) et après chaque
réindexation. Un vault contenant de nombreux/gros PDF démarre sans être bloqué ; le texte
reste recherchable une fois l'enrichissement terminé. Fichiers : `backend/indexer.py`,
`backend/main.py`.
- **Tests** : `tests/test_api_main.py` (redactor hex), `tests/test_auth.py` (coût Argon2,
garde-fou d'instance non authentifiée), `tests/test_auth_api.py` (login uniforme),
`tests/test_collab.py` (jeton query rejeté, trame surdimensionnée), `tests/test_pdf.py`
(scan différé + enrichissement).
---
## [2.11.2] — 2026-09-17
### Modifié
- **Tests E2E PDF (BUG-060)** : le helper d'ouverture de fichier de
`tests/e2e/pdf-viewer.spec.js` étend désormais le vault dans l'arborescence s'il est replié
(cas d'une instance avec authentification activée) au lieu de supposer le vault déjà déplié.
---
## [2.11.1] — 2026-09-17
### Corrigé
- **BUG-060 — Viewer PDF : l'affichage des pages ne fonctionnait pas** : au clic sur un fichier
`.pdf`, seule la barre d'outils « PDF — N pages » s'affichait, le corps restant vide. La CSP
durcie en BUG-034 pose `object-src 'none'`, directive qui gouverne `