Files
ObsiGate/CHANGELOG.md
T
bruno 4de9ee038c
CI / lint (push) Successful in 2m29s
CI / security (push) Failing after 1m45s
CI / test (push) Failing after 4m21s
CI / build (push) Skipped
CI / e2e (push) Skipped
fix: desactive l'etape semgrep du job security - core natif inexecutable sur ce runner BUG-091
2026-09-29 12:54:55 -04:00

3146 lines
175 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.39.9**.
---
## [Unreleased]
---
## [2.39.9] — 2026-09-29
### Correction
- **BUG-091 — l'étape Semgrep est désactivée dans le job CI `security`.**
Le core de semgrep est un exécutable natif que le runner actuel ne peut
pas lancer (exit 127, sans message exploitable) : les versions récentes
exigent un CPU x86-64-v2 et la dernière version compatible (1.157.0, core
statique vérifié en baseline v1) échoue également. Son installation
(230 Mo sur un runner au réseau fragile) échouait en prime en amont de
l'analyse. **Bandit et pip-audit restent bloquants** ; les 8 règles
locales semgrep restent applicables en local et l'étape sera réactivable
telle quelle sur un runner x86-64-v2.
---
## [2.39.8] — 2026-09-29
---
## [2.39.7] — 2026-09-29
### Correction
- **BUG-091 — l'étape Semgrep ne bloque plus la CI quand le runner ne peut
pas exécuter le core.** Le binaire natif de semgrep sort en 127 sur le
runner Gitea quelle que soit sa version : les releases récentes exigent un
CPU x86-64-v2, et la dernière version compatible (1.157.0, core statique
vérifié en baseline v1) échoue également, sans message. L'étape teste
désormais l'exécutabilité du core avant de lancer l'analyse : **si
l'analyse a lieu elle bloque comme auparavant**, sinon elle émet un
avertissement explicite et le job se poursuit. Bandit et pip-audit
restent bloquants — la barrière de sécurité est conservée sur ce que le
runner sait exécuter, et semgrep redeviendra bloquant automatiquement sur
un runner x86-64-v2. Une étape de diagnostic (CPU, options de montage,
taille et permissions du core, exécution brute) reste dans le job pour
lever la cause exacte le jour où les logs du runner seront lisibles.
---
## [2.39.6] — 2026-09-29
### Correction
- **BUG-091 (suite) — semgrep-core s'exécutait depuis un venv sous `/tmp`.**
Le binaire natif de semgrep sortait en 127 sans message, alors que sa
version était bien compatible avec le CPU du runner (core statique,
baseline x86-64 v1) : le filesystem `/tmp` du runner est monté `noexec`
et le noyau refuse l'exécution sans message exploitable. Le venv isolé
est donc créé dans `$HOME`, et l'étape de diagnostic du job security
trace désormais CPU, options de montage, taille/permissions du core et
exécution brute.
---
## [2.39.5] — 2026-09-29
### Correction
- **BUG-092 — les tests réseau ne dépendaient plus du DNS réel.** Trois tests
de `fetch_url` mockaient `httpx` mais laissaient le garde SSRF résoudre
`example.com` pour de vrai : sur un runner au DNS instable, le job CI
`test` échouait en `dns_error` au lieu d'atteindre la couche testée. Les
tests isolent désormais le garde — y compris la référence importée dans
`webrender`, qui échappait au premier correctif — et les tests de garde
SSRF continuent de traverser le vrai chemin. Contre-preuve : DNS coupé
globalement, la suite passe (1474 tests).
---
## [2.39.4] — 2026-09-29
---
## [2.39.3] — 2026-09-28
---
## [2.39.2] — 2026-09-28
### Correction
- **BUG-091 — le job CI `security` refusait de démarrer semgrep, puis
échouait à l'audit des dépendances.** Depuis 1.158.0, semgrep ne publie
plus que des wheels `manylinux_2_34`/`2_35` dont les bibliothèques
natives exigent un CPU x86-64-v2 : le runner Gitea les refuse (« CPU ISA
level is lower than required », exit 127). semgrep est désormais isolé
dans un venv jetable du job, épinglé à **1.157.0** (dernière publication
`manylinux2014`, baseline v1) — un venv, aussi, parce que ses
dépendances contredisent l'environnement principal (`tomli~=2.0.1` vs
pip-audit ≥ 2.10, `pyjwt~=2.12.0` vulnérable). Dans la foulée :
plancher `pyjwt[crypto]>=2.13.0` dans `backend/requirements.txt`
(PYSEC-2026-178, pyjwt est transitif de mcp) et mise à jour de
pip/setuptools dans le job (PYSEC-2026-3721 / PYSEC-2026-3447, apparus
récemment dans la base d'advisories). Validé en environnement frais :
résolution sans conflit, pip-audit et semgrep verts.
---
## [2.39.1] — 2026-09-28
### Ajouté
- **#153 A6 — l'assistant IA sait lire et modifier les classeurs existants.**
Quatre nouveaux outils dans `backend/tools/spreadsheets.py` :
`list_xlsx_sheets` (noms de feuilles + dimensions), `xlsx_to_markdown`
(tableau plafonné injecté au contexte du modèle), `update_xlsx_cells`
(édition par lots passant par le service gardé) et `append_xlsx_rows`
(ajout de lignes en fin de feuille). Les mutations demandent confirmation
et rafraîchissent la visionneuse (`obsigate:file-written`).
- **#153 A7 — navigation clavier et barre de formule dans la visionneuse.**
`Tab`/`Maj+Tab` circulent entre les cellules, flèches et `Entrée`/
`Maj+Entrée` (multiligne) fonctionnent comme dans un tableur, la cellule
active est nommée en A1 dans la barre de formule, une plage se copie,
et le focus reste visible et tactile (≥ 44 px, couvert par les E2E mobiles).
- **#153 A13 — tri, filtre, recherche et export CSV dans la feuille.**
Tri ascendant/descendant par colonne, filtre de lignes, recherche
suivant/précédent (respect de casse optionnel) et export CSV de la feuille :
toutes des opérations d'**affichage**, le classeur n'est jamais réécrit.
- **#153 A14 — structure du classeur éditable depuis la visionneuse.**
Ajout, renommage, duplication et suppression de feuilles ; insertion et
suppression de lignes/colonnes autour de la cellule active, via le menu
Structure et `PUT /api/file/{vault}/xlsx/structure` — mêmes garde-fous
(backup atomique, verrou, confirmation) que l'édition de cellules.
- **#153 A15 — styles, fusions et volets figés affichés fidèlement.**
La lecture rend les couleurs de police et de fond, le gras/italique/
souligné, les alignements, les plages fusionnées et l'ancre des volets
figés ; un format de nombre personnalisé est signalé par une police à
chasse fixe. Une seule charge du classeur (mode normal) suffit pour toutes
les feuilles, y compris celles rendues par fenêtres.
- **#153 A16 — formats tableur additionnels.** `.xlsm` éditable avec
**macros préservées** (`keep_vba`), `.xls` et `.ods` en **lecture seule**
(xlrd / odfpy), `.csv` édité comme un tableur et réécrit au format
RFC 4180 (`PUT …/csv/save`). Dépendances : `xlrd==2.0.2`,
`odfpy==1.4.1` dans `backend/requirements.txt`.
- **#153 A17 — tableau de bord du classeur.** Un panneau de la visionneuse
liste les plages nommées (portée classeur ou feuille), signale la présence
de graphiques et de tableaux croisés (analyse des parties OPC, sans
recharger le fichier), donne les statistiques par feuille (cellules,
lignes, colonnes, formules, valeurs numériques) et huit KPI extraits de la
première zone de données — endpoint `GET /api/file/{vault}/xlsx/dashboard`.
---
## [2.39.0] — 2026-09-28
---
## [2.38.0] — 2026-09-28
---
## [2.37.0] — 2026-09-28
---
## [2.36.0] — 2026-09-28
---
## [2.35.0] — 2026-09-28
---
## [2.34.0] — 2026-09-28
---
## [2.33.0] — 2026-09-28
---
## [2.32.0] — 2026-09-28
---
## [2.31.0] — 2026-09-28
### Correction
- **BUG-090 — troncature silencieuse d'une feuille `.xlsx` au-delà de
500 lignes × 40 colonnes.** `render_sheets()` renvoie les dimensions
déclarées par la feuille (`total_rows`/`total_cols`), les plafonds du
moteur (`max_rows`/`max_cols`) et un flag `truncated` : la visionneuse
affiche un bandeau « Feuille tronquée — 500 lignes affichées sur 520 »
(i18n FR/EN) au lieu de présenter une table courte comme complète. La
ligne d'en-têtes est désormais figée au défilement vertical (`thead`
sticky, `top: auto` sur les numéros de ligne pour éviter leur
empilement en haut à gauche). *#153 A8/R5.*
### Ajouté
- **#153 A9 — chargement paresseux d'une feuille par fenêtres.**
`GET /api/file/{vault}/xlsx/sheet?sheet=&offset=&limit=` renvoie un
bloc de lignes (`XlsxSheetWindowResponse`, plafond 1 000 lignes par
requête, `has_more` de pagination) avec les **vraies** coordonnées A1
et numéros de ligne de la feuille — une fenêtre se comporte exactement
comme le rendu complet. Erreurs typées : 404 feuille inconnue, 415
fichier non-`.xlsx`. La lecture des valeurs calculées en cache (#153
A12) s'applique aussi aux fenêtres.
- **#153 A9bis — « Charger la suite » sous une feuille tronquée.** Un
pied de page annonce la progression et fetch la fenêtre suivante au
clic ou à l'approche du bas du tableau (sentinelle de défilement).
Les lignes ajoutées passent par le même pipeline d'édition que le
rendu initial : éditables et sauvegardables immédiatement. Un fetch
échoué restore le bouton (retry possible) ; feuille complète → pied
de page masqué.
---
## [2.30.0] — 2026-09-27
### Correction
- **BUG-089 — un reindex manuel ne reconstruisait pas l'index inversé.**
`reload_index()` / `reload_single_vault()` remplacent l'entrée de vault
en bloc, ce qui n'émet pas les notifications incrémentales : la
recherche TF-IDF continuait de servir un index périmé après un
reindex. Les deux fonctions appellent désormais `init_inverted_index()`.
Au passage, `backend/search.py` lisait l'index via
`from backend.indexer import index` — une liaison **par valeur** du
dict : un rechargement du module `backend.indexer` recréait le dict
côté indexer alors que la recherche écrivait dans l'ancien, et
l'index inversé n'indexait plus rien. Tous les accès passent par
`_indexer.index`. *Trouvé en écrivant le test de recherche d'A5 : il
passait isolément et échouait en suite complète selon l'ordre.*
### Ajouté
- **#153 A5 — les tableurs sont indexés par leur contenu.**
`extract_indexable_text()` extrait les noms de feuilles et les 20
premières lignes (plafond 5 000 caractères, 20 feuilles) pour le
TF-IDF et la recherche sémantique. Un mot tapé dans une cellule rend
désormais le classeur trouvable ; la lecture binaire pour l'affichage
est inchangée et un classeur chiffré/corrompu s'indexe par son seul
nom.
- **#153 A10 — la saisie est typée comme dans Excel.** Une valeur
`TRUE`/`FAUX`/`OUI`/`NON` devient un booléen, une date `JJ/MM/AAAA`
(avec `HH:MM` optionnel) devient une vraie date — et dans l'ordre
français : `01/02/2026` est le 1ᵉʳ février. Une saisie ressemblant à
une formule n'est jamais convertie.
- **#153 A12 — la valeur calculée s'affiche sous la formule.** Quand une
cellule porte encore le résultat de son dernier calcul Excel, celui-ci
s'affiche dans une ligne discrète sous la formule. La seconde lecture
`data_only=True` n'a lieu que si l'archive contient réellement une
valeur en cache, et toute erreur retombe sur l'affichage formules seul.
Info-bulle traduite FR/EN (`xlsx.cached_value_title`).
---
## [2.29.0] — 2026-09-27
### Correction
- **BUG-084 — l'index inversé conservait des documents fantômes après la
suppression d'une vault.** `remove_vault_from_index()`
(`backend/indexer.py`) ne notifiait pas le hook incrémental : après
suppression d'une vault, ses documents restaient dans l'index inversé
(`postings`, `doc_info`, `doc_vault`, `vault_docs`) et continuaient de
correspondre aux recherches pour une vault inexistante — seul un reindex
manuel les effaçait. Le correctif déclenche
`_on_index_change('remove', …)` pour chaque fichier de la vault, et
`_remove_doc_internals()` supprime désormais la clé `vault_docs` dont le set
devient vide (c'est un `defaultdict` : une lecture la recréait).
Test : `TestVaultRemovalPurgesInvertedIndex` (contre-preuve : échoue sans le
correctif).
### Maintenance
- **Index inversé — `is_stale()` renommé `is_ready()`.** La relecture de
`plan.md` a établi que les étapes 6 et 7 (suppression du cooldown et du hack
de coalescence) étaient **déjà livrées** : ni `_last_rebuild`, ni
`_rebuild_cooldown`, ni `_source_generation`, ni `_on_vault_change` ne
subsistent. `is_stale()` ne mesurait donc plus aucune staleness — il
indiquait seulement si l'index initial était construit, sous un nom
trompeur. Renommé `is_ready()`, cohérent avec le `is_ready()` de
`SemanticIndex` ; l'alias `is_stale()` de `SemanticIndex`, sans appelant, est
supprimé. `/api/diagnostics` expose désormais `is_ready` (libellé « Index
prêt » côté `frontend/js/config.js`). Tests :
`test_is_ready_tracks_initial_build`, `test_is_ready_survives_incremental_updates`.
- **`plan.md` recalibré.** Le fichier est désormais marqué « livré » et
suivi d'une section « État réel » : le code a divergé du plan sur quatre
points (pas de repli `_needs_rebuild`, `_ready` au lieu de `doc_count == 0`,
`rebuild()` conservé au démarrage, `is_stale()` repurposé). Les extraits de
code du plan sont explicitement signalés comme ne décrivant pas le code
actuel.
- **Fixtures de test locales exclues du suivi Git.** `test_dir/music/`,
`test_dir/video/`, `test_vault/images/`, `test_vault/markdown/` et
`test_vault/budget.xlsx` (~200 Mo) sont ajoutés au `.gitignore` : aucun test
ni job CI ne les référence — les tests unitaires génèrent leurs fixtures dans
`tmp_path` et l'E2E n'utilise que les fixtures committées
(`test_vault/sample-*.{mp3,png,svg,webm,pdf}`, `test_dir/*.md`). Ils
restaient non suivis et polluaient `git status`.
### Sécurité
- **BUG-088 — plus d'injection de formule via la visionneuse Excel.** Une
saisie `=cmd|'/c calc'!A1` (ou `@…`) était stockée comme **formule** par
openpyxl, donc exécutée par Excel à la réouverture du fichier (DDE).
`edit_xlsx_cells` force maintenant le type texte (`cell.data_type = "s"`)
pour toute valeur commençant par `=` ou `@` ; l'API accepte
`allow_formula: true` et la visionneuse expose un bouton `f(x)`
(opt-in, état de session, jamais persisté). `+`/`-` restent des nombres.
- **BUG-087 — écriture concurrente d'un classeur.** `load_workbook()` →
`save()` n'était pas sérialisé : deux sauvegardes simultanées (deux
onglets, l'agent IA et la visionneuse) faisaient gagner la dernière, en
silence. Verrou par chemin (`backend/services/mutations.py::_xlsx_write_lock`,
timeout 15 s) autour du cycle lecture → édition → remplacement ; attente
dépassée → **409** `conflict`. L'endpoint `PUT …/xlsx/save` est devenu
synchrone pour que l'attente s'exécute dans le threadpool.
### Corrigé
- **BUG-085 — la perte de données à l'enregistrement d'un `.xlsx` est
annoncée, plus silencieuse.** `GET /api/file/{vault}` renvoie
`xlsx_lossy_features` (éléments qu'un round-trip openpyxl perd) ; la
visionneuse affiche un bandeau listant ces éléments et la première
sauvegarde demande confirmation avant de renvoyer `force: true`. Sans
`force`, l'API répond **409** `xlsx_lossy_content` avec
`details.features`. Périmètre **remesuré** sur openpyxl 3.1.5 : graphiques,
images, dessins et tableaux croisés sont bien préservés ; sont perdus les
valeurs calculées en cache, slicers/chronologies, contrôles de formulaire,
connexions/requêtes, custom XML, signature numérique, commentaires
enrichis et macros.
- **BUG-086 — écriture atomique des classeurs.** `wb.save()` écrivait en
place sur le fichier du vault : un plantage laissait un `.xlsx` tronqué.
L'écriture passe désormais par un `.tmp` puis `os.replace()` (le backup
`.bak` est inchangé, le `.tmp` est ignoré par le watcher).
- Le handler global `ServiceError` expose maintenant `code` et `details` dans
la réponse JSON, et `api()` (frontend) les propage sur l'Error — nécessaire
pour que le client distingue un 409 de confirmation d'une autre erreur.
### Ajouté
- **#153 (P0) — tests de la visionneuse Excel.**
`tests/frontend/xlsx-viewer.test.mjs` (10 tests JSDOM : bannière,
confirmation + reprise `force`, refus, toggle `f(x)`, payload de
sauvegarde) et `tests/e2e/xlsx-viewer.spec.js` (3 tests Playwright sur la
fixture `test_vault/sample-xlsx-lossy.xlsx`) ; la suite JSDOM est branchée
dans le CI.
### Documentation
- **#153 — Visionneuse & édition XLSX : audit complet et backlog de
complétude.** La visionneuse `.xlsx` livrée par #152 a été auditée couche
par couche (lecture `backend/xlsx_reader.py`, écriture
`backend/services.mutations.edit_xlsx_cells`, UI `renderXlsxViewer`,
indexation, outils IA, tests). Bilan : la grille de valeurs est éditée
correctement (sécurité, backup, audit, échappement HTML), mais l'ensemble
supporté est étroit, une partie du classeur est perdue à l'enregistrement,
les tableurs sont **invisibles pour la recherche** et l'IA ne sait que les
**créer**. Ouverture de l'item **#153** dans `docs/ROADMAP.md` (17
sous-tâches suivies **A1 → A17** ; **P0 livré**, reste P1 recherche/IA/UX
puis P2 étendu) et création de la fiche
[docs/features/xlsx-viewer.md](docs/features/xlsx-viewer.md) : cartographie
du code, limites par couche, tableau des risques R1-R5 et critères
d'acceptation par sous-tâche.
---
## [2.28.16] — 2026-09-27
---
## [2.28.15] — 2026-09-27
### Sécurité
- **#87 T6 — dépendances qualifiées, `pip-audit` bloquant (0 vulnérabilité).**
mistune 3.0.2 → 3.3.3 (XSS/ReDoS/DoS dans le moteur de rendu),
python-multipart 0.0.9 → 0.0.31, weasyprint 69 → 70, mcp 1.9.4 → 1.28.1,
fastapi 0.110.3 → 0.141.1 + starlette 0.37.2 → 1.7.0, setuptools 84 ;
`cast(str, …)` aux 3 sites d'appel mistune (typage 3.3 resserré). Suite
complète 1359 passed, ruff/mypy 0. Seule exception : PYSEC-2026-1325
(ecdsa, Minerva) — aucun correctif upstream ET JWT exclusivement HS256
(`backend/auth/jwt_handler.py`), les chemins ECDSA P-256 ne s'exécutent
jamais → `--ignore-vuln` documenté.
- **#87 T7 — semgrep SAST local bloquant (8 règles, 0 finding).**
Ruleset `semgrep-rules/` (eval/exec, shell=True, os.system, pickle,
yaml.load sans Loader, verify=False, Markup, mktemp) — 100 % local,
aucun registre réseau (runner au réseau fragile). Trivy écarté :
binaire + base de vulnérabilités à télécharger à chaque run, couche
Python déjà couverte par `pip-audit` bloquant (image = slim + 4 libs).
- **#87 T8 — fin BUG-034 : cookies Secure auto, CORS same-origin explicite.**
`OBSIGATE_SECURE_COOKIES=true|false|auto` (défaut auto : Secure en https,
sinon rien — logins http locaux préservés ; `X-Forwarded-Proto` honoré
sous `TRUST_PROXY`, avertissement démarrage affiné, `TRUST_PROXY=true`
dans le compose prod) ; `CORSMiddleware` same-origin explicite (sûr :
web et desktop Tauri same-origin, API directe hors navigateur) ;
`style-src 'unsafe-inline'` conservé et assumé (189 attributs `style=` +
343 `el.style` — suppression = réécriture complète, risque nul côté
exécution une fois `script-src` verrouillé en T5c).
---
## [2.28.14] — 2026-09-27
---
## [2.28.13] — 2026-09-27
### Modifié
- **#87 (T5c) — `script-src` sans `'unsafe-inline'`.**
Seuls les scripts avec nonce frais (`backend/csp.py`, T5b) ou servis par
`'self'`/CDN listés s'exécutent ; `style-src` garde `'unsafe-inline'`
(chantier séparé). Vérifié : `test_csp_nonce.py` 5/5, 0 handler inline
restant dans les pages HTML (propriétés `onXxx = fn` en JS non concernées
par la CSP).
---
## [2.28.12] — 2026-09-27
### Corrigé
- **BUG-081 — `GET /api/auth/mfa/status` → 500 quand l'auth est désactivée.**
Le pseudo-user `anonymous` (auth désactivée, mode E2E/CI) n'a aucune entrée
en store : `get_user(...)` → `None` puis `AttributeError` sur `user.get`.
Garde `None` → payload « MFA désactivé » (`mfa_enabled: false`,
`totp_enabled: false`, `webauthn_credentials: 0`). Test : `tests/test_mfa.py`
(`TestMfaStatusAuthDisabled`, échoue en 500 sans le correctif).
---
## [2.28.11] — 2026-09-27
---
## [2.28.10] — 2026-09-27
### Corrigé
- **BUG-083 — job CI `security` rouge : le runner tronquait le `#` du `run:` pip-audit.**
Le runner Gitea Act coupe naïvement au premier `#` (même entre
guillemets) : `echo "... see #87)"` devenait une citation non fermée
(`unexpected EOF while looking for matching '"'"`). Seul `run:` du
workflow avec un `#` ; l'echo n'a plus de `#` (réf `#87` en commentaire
YAML, jamais vu par le shell). Garde-fou : `tests/test_ci_workflow.py`
(aucun `#` dans le code des `run:`, `upload.test.mjs` verrouillé dans
l'étape JSDOM — BUG-082).
---
## [2.28.9] — 2026-09-27
### Corrigé
- **BUG-082 — CI `lint` rouge : suites frontend exigeant `jsdom`.**
`tests/frontend/upload.test.mjs` puis `config-ai-keys.test.mjs` (imports
statiques `jsdom`, introduits par `#89`) étaient exécutés dans l'étape
frontend racine où `jsdom` n'est jamais installé (`ERR_MODULE_NOT_FOUND`,
rouge depuis `7bee4a2`). Déplacés dans l'étape JSDOM (les deux branches,
après install si besoin) ; garde-fou `tests/test_ci_workflow.py` :
aucun fichier de l'étape racine ne doit importer `jsdom` statiquement.
---
## [2.28.8] — 2026-09-27
### Corrigé
- **BUG-080 — harnais E2E local anti-blocage (plus de run pendu toute la nuit).**
`run-e2e-local.ps1` : Playwright lancé via `node` direct sur la CLI locale
(jamais de prompt interactif, `Start-Process` ne sachant pas exécuter `npx` ;
paramètre `$Arguments`, `$Args` étant une variable automatique qui l'écraserait),
installation Chromium sautée si déjà présent (`E2E_INSTALL_BROWSERS=1`
pour forcer), étapes `install`/`test` bornées (`E2E_TIMEOUT_SEC`,
défaut 1800 s / 600 s, exit 124 au dépassement — au-delà du globalTimeout
pour un abandon propre avec rapport) ; `run-e2e-local.sh` : `npx --yes` +
mêmes bornes ; `playwright.config.ts` : `globalTimeout` (25 min en local,
30 min en CI, `E2E_GLOBAL_TIMEOUT_MS` pour surcharger) ; `e2e-server.ps1` :
pidfile resynchronisé sur le vrai propriétaire du port et `stop` qui tue
l'arbre complet (fini les serveurs orphelins qui squattent le port 2029).
Garde-fous : `tests/test_e2e_harness.py` (8 tests).
---
## [2.28.7] — 2026-09-26
### Ajouté
- **#87 (T5b) — nonces CSP prêts pour la bascule (sans changement).**
Nonce frais par réponse dans `script-src` (`backend/csp.py`), injecté
dans les 6 pages HTML servies (dont la nouvelle route
`/excalidraw-editor.html`, utilisée par l'iframe du viewer au lieu de
`/static/`) et la page de partage ; `tests/test_csp_nonce.py` (unicité,
concordance en-tête/HTML). `unsafe-inline` conservé jusqu'en T5c.
---
## [2.28.6] — 2026-09-26
### Modifié
- **#87 (T5a) — 16 handlers inline convertis en listeners (CSP inchangée).**
`onclick`/`onerror` de `index.html` et des vues JS (`config`, `plugins`,
`sync`, `viewer`, `auth`) remplacés par `addEventListener` ; suites
frontend vertes (unit, ai, config-mobile, pdf-viewer, mfa-settings,
sidebar-filters).
---
## [2.28.5] — 2026-09-26
---
## [2.28.4] — 2026-09-26
### Ajouté
- **#87 (T4) — E2E XSS et serveur E2E piloté.**
`tests/e2e/xss.spec.js` : page publique `/s/{token}` (titre/frontmatter
hostile échappé, JSON neutralisé, aucun JS exécuté) et lecteur markdown
(sanitizer, aucun `on*`/`javascript:` vivant) — 2/2 verts en local.
`scripts/e2e-server.ps1` : `start|stop|status|logs` avec progression
visible et fichier PID (fini les serveurs orphelins sur le port 2029).
---
## [2.28.3] — 2026-09-26
### Ajouté
- **#87 (T3) — cookies `Secure` et CORS explicites.**
Helper `is_secure_cookies()` centralisé (défaut `false` conservé pour ne
pas casser les logins HTTP locaux) + avertissement au démarrage sur bind
non-loopback sans `Secure` ; `tests/test_security_headers.py` atteste
l'absence de CORS permissif (same-origin par défaut du navigateur) et les
en-têtes de durcissement.
---
## [2.28.2] — 2026-09-26
### Ajouté
- **#87 (T2) — tests de durcissement : concurrence et regex.**
`tests/test_hardening_concurrency.py` : créations/mises à jour/`login
failures` concurrents sur `users.json` (zéro mise à jour perdue, JSON
toujours récupérable) et budget temps de la politique ReDoS (motifs
catastrophiques rejetés en < 1 s, motifs acceptés < 5 s sur 200 Ko).
---
## [2.28.1] — 2026-09-26
### Modifié
- **#87 (T1) — CI sécurité durcie.**
`bandit` passe en bloquant (`# nosec` justifiés : SHA1 non-crypto,
subprocess git à argv fixe, `saxutils.escape` sans parsing ; B105 exclu
comme `pyproject.toml`) ; `npm audit --omit=dev` bloquant (0 faille) ;
les 5 suites frontend hors CI (`upload`, `pretty`, `media-viewer`,
`mfa-settings`, `config-ai-keys`, vertes en local) rejoignent le job
`lint`. `pip-audit` reste consultatif (upgrades starlette/weasyprint à
qualifier, chantier dédié).
---
## [2.28.0] — 2026-09-26
### Modifié
- **#85 (T10) — persistance d'état et clôture de la refonte architecturale.**
Verrous `RLock` sur les stores JSON sans protection (`revoked_tokens`,
`shares`, `webhooks` + secrets, clés d'outils) avec tests de concurrence
(`tests/test_store_locks.py` — pertes prouvées sans verrou) ; rate-limit
auth persisté en option (`OBSIGATE_RATELIMIT_DB`, SQLite WAL, sémantique
identique, défaut mémoire inchangé, `tests/test_ratelimit_store.py`).
Contrat `tools/registry.py` audité (permissions/quotas/redaction déjà
câblés, rien à coder). Index non persisté : rebuild différentiel #86
suffisant (décision documentée). Fiche `docs/features/archi-refonte-85.md`,
#85 sorti du backlog (index roadmap).
## [2.27.12] — 2026-09-26
### Modifié
- **#85 (T9) — extraction realtime + render hors du monolithe `backend/main.py`.**
Le stream SSE `/api/events` et le WebSocket `/ws/collab/*` sont servis par
`backend/routers/realtime.py`, le pipeline markdown (mistune, wikilinks,
slugs, sanitizer) par `backend/render.py` (imports directs, plus de
couplage différé). `main.py` (4 827 → ~760 lignes) ne contient plus que
l'assemblage : lifespan, middlewares, montage des 16 routers, racine
`/api`, statique/SPA et cales de compatibilité testées.
## [2.27.11] — 2026-09-26
### Modifié
- **#85 (T8) — extraction vaults/history/conflicts hors du monolithe `backend/main.py`.**
13 routes servies par `backend/routers/vaults.py`, `history.py` et
`conflicts.py` ; `VaultInfo`/`BookmarkToggleRequest` dans `schemas.py`,
handle watcher partagé dans `backend/watcher_state.py`.
`tests/test_api_main.py` importe `humanize_mtime` depuis son module
canonique (`services.recent`).
## [2.27.10] — 2026-09-26
### Modifié
- **#85 (T7) — extraction du domaine `config` hors du monolithe `backend/main.py`.**
`/api/config`, ai-keys (get/post/delete/test), tool-keys (×3), ai-models,
diagnostics et dashboard sont servis par `backend/routers/config.py`
(`_FALLBACK_MODELS`, store clés et config déplacés ; `main` réimporte
`_load_config` pour son lifespan, les fixtures de tests inchangées).
`tests/test_ai_models.py` patch désormais la référence du router.
## [2.27.9] — 2026-09-26
### Modifié
- **#85 (T6c) — extraction media/pdf/export/guide hors du monolithe `backend/main.py`.**
file/pdf, exports (html/md-bundle/epub), guide/download, pdf/stream|info,
image, media+thumb, attachments (rescan/stats), vault settings (get/post/all)
et vault files sont servis par `backend/routers/files_media.py` ; le helper
Range partagé vit dans `backend/routers/helpers.py` (tags OpenAPI inchangés,
tests statiques frontend `media-viewer`/`image-viewer` réalignés).
## [2.27.8] — 2026-09-26
### Modifié
- **#85 (T6b) — extraction mutations fichiers/dossiers hors du monolithe `backend/main.py`.**
`PUT .../save|xlsx/save`, `DELETE/POST/PATCH /api/file`, `POST/PATCH/DELETE
/api/directory`, `POST /api/move`, `POST .../batch-upload` sont servis par
le nouveau `backend/routers/files_write.py` (effets de bord inchangés :
audit, index, SSE, webhooks, plugins, historique) ; 15 modèles dans
`schemas.py`.
## [2.27.7] — 2026-09-26
### Modifié
- **#85 (T6a) — extraction lecture fichiers hors du monolithe `backend/main.py`.**
`/api/browse/{vault}`, `/api/file/{vault}/raw|download|backlinks` et
`GET /api/file/{vault}` (vue rendue tous formats) sont servis par le
nouveau `backend/routers/files_read.py` ; modèles dans `schemas.py`,
`_content_disposition`/`_media_max_inline_bytes` dans
`backend/routers/helpers.py` (partagés avec les tranches suivantes).
Correctif au passage : décorateur orphelin `/s/{token}` resté en T3 et
double-enregistrement de `/api/conflicts` supprimés.
## [2.27.6] — 2026-09-26
### Modifié
- **#85 (T5) — extraction du domaine `search` hors du monolithe `backend/main.py`.**
Les 11 routes (`/api/search`, `/advanced`, `/replace`, `/tags`,
`/tree-search`, `/vault/{vault}/paths`, `/suggest`, `/tags/suggest`,
`/graph/{vault}`, `/index/reload`, `/index/reload/{vault}`) sont servies
par le nouveau `backend/routers/search.py` ; les modèles search dans
`schemas.py` et le pool de threads dans `backend/search_executor.py`
(même dimensionnement, même cycle de vie) — comportement inchangé.
## [2.27.5] — 2026-09-26
### Modifié
- **#85 (T4) — extraction du domaine `backups` hors du monolithe `backend/main.py`.**
Les 9 routes (`/api/file/{vault}/backups|diff|restore`, `/api/backups`,
`/delete`, `/purge`, `/content`, `/compress`, `/auto`) sont servies par le
nouveau `backend/routers/backups.py` ; `Diff/Restore*` déménagent dans
`schemas.py` et le singleton SSE dans `backend/sse.py` (partagé avec
`main`) — comportement inchangé, aucun impact utilisateur.
## [2.27.4] — 2026-09-26
### Modifié
- **#85 (T3) — extraction du domaine `sharing` hors du monolithe `backend/main.py`.**
`POST /api/share/{vault}`, `GET /api/shares`, `DELETE /api/share/{share_id}`
et les pages publiques `/s/{token}`, `/s/{token}/raw`, `/s/{token}/pdf`
sont servis par le nouveau `backend/routers/sharing.py` — chemins,
réponses, tags OpenAPI et authentification inchangés (aucun impact
utilisateur).
## [2.27.3] — 2026-09-26
### Modifié
- **#85 (T2) — extraction du domaine `webhooks` hors du monolithe `backend/main.py`.**
Le CRUD `GET/POST/PATCH/DELETE /api/webhooks` (admin) est servi par le
nouveau `backend/routers/webhooks.py` — chemins, réponses, tags OpenAPI et
authentification inchangés (aucun impact utilisateur).
## [2.27.2] — 2026-09-26
### Modifié
- **#85 (T1) — extraction du domaine `health` hors du monolithe `backend/main.py`.**
`GET /api/health` et `GET /api/health/detailed` (admin) sont servis par le
nouveau `backend/routers/health.py` (monté dans `main.py`) et le modèle
`HealthResponse` déménage dans `backend/schemas.py` — chemins, réponses,
tags OpenAPI et authentification inchangés (aucun impact utilisateur).
---
## [2.27.1] — 2026-09-26
### Modifié
- **Roadmap — priorisation dette & sécurité (décisions 2026-09-26).**
Items #85 (refonte architecturale) et #87 (CI/CD) détaillés et marqués
prioritaires : `backend/main.py` mesuré à ~4 827 lignes, `tools/registry.py`
à créer, persistance SQLite/Redis, verrous asyncio, audit des `except`
larges, CI sécurité bloquante (bandit/semgrep/trivy, audits pip/npm),
finition CSP nonce (BUG-034), cookies `Secure` par défaut, rotation clé
DeepSeek à confirmer (BUG-006). #73 Sync reporté (P4, hors chemin
critique) ; desktop #77 confirmé non signé + doc SmartScreen, reste les
6 tests E2E manuels. Corrections : sections livrées #83/#84 retirées du
backlog (détail dans l'archive, index inchangé), total restant recalculé
(~12-18 jours chemin critique : #77 fin + #85 + #87).
---
## [2.27.0] — 2026-09-25
### Ajouté
- **#152 — Viewer XLSX** : affichage des fichiers `.xlsx` en tableaux multi-feuilles (onglets,
en-têtes A1), édition inline des cellules avec `PUT /api/file/{vault}/xlsx/save` (backup avant
écriture, coercion numérique) et téléchargement du fichier d'origine.
---
## [2.25.1] — 2026-09-24
### Corrigé
- **`/api/diagnostics` — erreur 500 « dictionary changed size during iteration ».**
Le calcul des statistiques d'index itérait `inv.word_index` et `index` en
direct, pendant que l'indexeur les modifiait depuis un autre thread (build au
démarrage, hooks incrémentaux) : l'itérateur de dict levait `RuntimeError` et
l'endpoint renvoyait 500. Les deux dicts sont désormais **copiés avant
itération** (copie atomique sous le GIL), ce qui supprime la course.
---
## [2.25.0] — 2026-09-24
### Ajouté
- **#115 — barre d'outils de lecture toujours visible.** La barre d'actions d'un
document (pop-out, bookmark, Editer, Source, Copier, Partager…) est désormais
**épinglée en haut** de la zone de lecture : elle reste accessible en permanence,
même au bas d'un document long, au lieu de disparaître au défilement. Elle est
rendue comme un enfant direct du conteneur de défilement (`.file-toolbar`), masquée
en mode lecture, et alignée dans la fenêtre pop-out.
- **#117 — avatars prédéfinis dans le profil.** La section **Profil** propose une
galerie de **12 avatars** fournis avec l'application (`frontend/icons/avatar/`) :
un clic charge l'image, la recadre au format carré 256 px (même pipeline que
l'import) et l'enregistre sur le compte. L'avatar actif est surligné ; l'import
d'une photo personnalisée et la suppression restent disponibles.
### Corrigé
- **BUG-078 — coloration syntaxique des fichiers de code perdue.** Les feuilles de
style highlight.js étaient basculées à partir de la **clé de thème**
(`defaut-obsigate`, …) au lieu du **mode** (`dark`/`light`) : les deux feuilles se
retrouvaient désactivées et les blocs de code (`.py`, `.sh`, `.ps1`, `.yml`, JSON,
blocs Markdown…) s'affichaient en texte brut. Le basculement est désormais piloté
par le mode, de façon déterministe, dans le moteur de thème (`themes.js`) comme au
premier rendu (`ui.js`) ; sépia et contraste élevé réutilisent la palette claire.
---
## [2.24.0] — 2026-09-24
### Ajouté
- **BUG-077 — assistant IA : bouton « Stop ».** Pendant qu'une réponse se diffuse, le
bouton d'envoi du composeur devient un bouton **Stop** (icône carrée, couleur
d'alerte) : un clic interrompt immédiatement l'exécution de l'agent (abort de la
requête SSE, annulation de la tâche côté serveur à la déconnexion) et la réponse
partielle est conservée avec un marqueur « ⏹ Exécution arrêtée. ». Le bouton reste
actif (il n'est plus désactivé) tant que l'agent travaille, y compris pendant la
reprise d'une confirmation.
### Modifié
- **BUG-075 — assistant IA : approbation globale des actions.** Une même réponse du
modèle peut demander **plusieurs** mutations (créer un dossier et les fichiers
qu'il contient…). Elles sont désormais **regroupées en une seule confirmation** (au
lieu d'une action après l'autre) : la carte liste chaque action avec son libellé et
son aperçu de diff, et un unique bouton « **Tout approuver (N)** » envoie
`confirm_all` — les actions en attente sont appliquées d'un bloc, puis la suite de
l'exécution est autorisée sans nouvelle carte. Les appels en lecture du même lot
s'exécutent immédiatement (conversation valide). Côté backend, `pending.actions`
remplace le report `deferred` des mutations d'un lot (`backend/agent/loop.py`) et
`confirm_all` arme `ToolContext.confirmed` pour le reste du run
(`backend/bookslm_routes.py`).
### Corrigé
- **BUG-074 — assistant IA : bloc d'étapes sans titre et compteur figé à 1.** Le
résumé replié affiche désormais un **titre** (le libellé de la première action,
« N étapes — Fichier créé : notes/a.md ▶ »), le compteur ne compte plus les
**réflexions** (seules les actions) et la reprise d'une confirmation continue de
diffuser dans **le même message** au lieu de créer un nouveau message « 1 étape »
par approbation : le bloc d'étapes s'accumule sur tout l'échange.
- **BUG-076 — assistant IA : arborescence et document ouvert non rafraîchis.** Après
une action mutatrice de l'agent (création/suppression/renommage de fichier ou
dossier, écriture), l'arborescence est rafraîchie immédiatement (rafraîchissement
débouncé sur les événements `tool` mutateurs, sans attendre le watcher) et le
document affiché est rechargé depuis le disque ; les outils de création de documents
(xlsx/docx/csv/pdf) notifient désormais aussi la visionneuse.
---
## [2.23.0] — 2026-09-24
### Ajouté
- **#114 — Configuration, refonte mobile-responsive.** La page « Configurations »
est désormais pensée pour le tactile (≤ 768 px) : modale **plein écran**
(`100dvh`, safe-area), sommaire en **drawer coulissant** gauche
(`position: fixed`, largeur `min(320px, 88vw)`) avec fond assombri
(`#config-modal.config-toc-open::before`) et bouton de fermeture
`#config-toc-close` (tap sur le backdrop ou Échap ferme le drawer d'abord,
puis la modale) ; formulaires sur **une colonne** ; tous les boutons
(save/secondary/danger/add/sm/hamburger/fermer) et liens du sommaire en cibles
**≥ 44 px** ; champs et selects en **16 px + min-height 44 px** (anti-zoom
iOS) ; rangée « Sauvegarder / Réindexer / Réinitialiser » **sticky** en bas de
sa section avec safe-area ; MFA (codes de secours en 1 colonne, champ de code
full-width et wrap des rangées d'action) ; débordements webhook/token/share
corrigés. Dettes HTML/i18n de l'audit incluses : `.config-actions-row` replacée
dans `#cfg-backend-settings` (+ `</section>` orphelin supprimé), id dupliqué
`cfg-partages-publics` retiré du `<h2>`, conteneur mort
`#plugins-settings-container` supprimé, libellé `#mt-explorer` i18n
(`settings.explorer`), doublons `.config-btn-add` et règle morte
`.mfa-recovery-input` purgés ; i18n : clés mortes `settings.backend`,
`settings.backend_hint`, `settings.restart_badge`, `settings.save`,
`settings.plugins` supprimées, `config.toc_close` ajoutée, `settings.tabs` FR
corrigé (« Onglets »). Tests : `config-mobile.test.mjs` étendu (27, au CI) +
E2E `config-mobile.spec.js` (5, projet `chromium-mobile`). Fiche :
[docs/features/settings-mobile-114.md](./docs/features/settings-mobile-114.md).
---
## [2.22.1] — 2026-09-23
### Corrigé
- **BUG-073 — mobile, fin de contenu masquée.** La barre de navigation fixe du bas
(`#mobile-toolbar`, 64 px + safe-area) recouvrait le bas de tous les documents et
pages en vue mobile (≤ 768 px) : les dernières lignes restaient définitivement sous
la barre. Cause : la règle de clearance du bloc mobile ciblait `.main-layout`,
classe absente de `index.html` (le vrai conteneur est `.main-body`) — sélecteur
mort, aucun dégagement réservé. Correctif : cible `.main-body`
(`padding-bottom: calc(64px + env(safe-area-inset-bottom, 0))`), suppression de la
règle sœur morte `.editor-modal.active ~ .main-layout`, reset du dégagement en mode
lecture (barre masquée) et `body.np-active .content-area` ramené à `76 px` (le
dégagement dock, sans double comptage avec `.main-body`). Tests :
`tests/frontend/mobile-toolbar.test.mjs` (7, au CI) et E2E
`tests/e2e/mobile-toolbar.spec.js` (2, projet `chromium-mobile`).
---
## [2.22.0] — 2026-09-23
### Ajouté
- **#113 — avatar utilisateur.** La section « Profil » des Configurations permet de
**choisir / importer une image** (PNG, JPG, WEBP, 8 Mo max) : elle est recadrée en
carré 256 px côté client, validée côté serveur (data-URL + octets magiques, sans SVG)
et persistée sur le compte (`avatar` dans `data/users.json`, exposé par
`GET/PATCH /api/auth/me` et la réponse de login). L'image s'affiche dans le **cercle
du profil en bas de la sidebar** (initiales en repli) et peut être supprimée. UI :
aperçu circulaire avec overlay caméra au survol, boutons Choisir / Supprimer, erreurs
en ligne. Clés i18n FR/EN.
### Modifié
- **#113 — ordre des sections Configurations.** La TOC et la page sont réorganisées
dans un ordre naturel et **parfaitement synchronisées** : **Profil en premier**,
puis Sécurité, Thèmes, Recherche, Historique récent, Tags, Fichiers cachés,
Synchronisation, Backend, Diagnostics, IA, Sources connectées, Clés API & MCP,
Push, Webhooks, Partages publics, Plugins et **À propos en dernier**. Les ancres
`cfg-tags` et `cfg-partages-publics` sont désormais portées par leur `<section>`
(cible de défilement = haut de section). Garde-fous : test statique
`tests/frontend/settings-order-avatar.test.mjs` (ordre TOC = page, pas d'ancre morte).
---
## [2.21.0] — 2026-09-23
### Ajouté
- **#112 — section compte en bas de la sidebar.** L'identité (avatar avec
initiales, nom, rôle) et la **déconnexion** sont regroupées dans un bloc
épinglé au bas de la barre latérale, comme sur les sites professionnels ; un
clic sur l'identité ouvre le Profil. Nouvelle clef i18n FR/EN pour le rôle.
- **#112 — pellicule d'images défilable.** La molette de la souris fait défiler
horizontalement la bande de miniatures et deux **flèches translucides** sur les
côtés (révélées au survol) remplissent la même fonction.
### Modifié
- **#112 — en-tête allégé.** La **version** quitte le header pour le menu
**Options** (ligne « Version ») ; le **bouton de déconnexion** et le **nom de
l'utilisateur** sont retirés du header au profit de la section compte de la
sidebar. Le header droit ne conserve que l'indicateur hors-ligne, le contexte
vault et le bouton Options.
---
## [2.20.0] — 2026-09-23
### Ajouté
- **#111 — visionneuse d'images : flèches latérales translucides.** Deux boutons
superposés le long des bords du cadre passent à l'image précédente/suivante ;
quasi invisibles au repos, ils se révèlent au survol (et restent visibles sur
les appareils tactiles). Un compteur `n / total` complète la barre d'outils.
### Modifié
- **#111 — navigation d'images fluide.** Changer d'image se fait désormais **en
place** (`showSibling` remplace le `src` du `<img>`) : plus de rechargement de
la vue ni de nouvel appel `/api/browse` à chaque flèche, **même dans un dossier
contenant beaucoup d'images**. La liste du dossier est mise en cache (TTL court)
et les images voisines sont préchargées ; la **pellicule de miniatures reste
visible** et la vignette active est simplement re-marquée.
- **#111 — image toujours ajustée au cadre.** La visionneuse occupe tout l'espace
disponible (zone de contenu en flex, sans défilement de page) et l'image est
systématiquement redimensionnée dans son cadre, y compris panneau
« Métadonnées » ouvert.
---
## [2.19.2] — 2026-09-23
### Ajouté
- **Lanceur E2E Windows/PowerShell** : `scripts/run-e2e-local.ps1` (+ script npm
`test:e2e:ps`) reproduit localement le job CI `e2e` (uvicorn natif, auth
désactivée, fixtures TestVault/TestDir, port 2029, projet `chromium-desktop`)
sans dépendre de `bash` — indispensable sur les postes Windows où WSL ne
démarre pas et où git-bash est bloqué par une politique de contrôle
d'application. Le script `bash` reste la référence pour la CI/Linux.
### Corrigé
- **BUG-072 — visionneuse d'images** : le plein écran (lightbox) et le panneau
« Métadonnées » sont désormais **conservés lors de la navigation** entre images
(flèches ←/→ et clic sur la pellicule) ; auparavant chaque changement d'image
recréait la visionneuse et perdait ces états. Le panneau de métadonnées s'affiche
maintenant en **barre latérale à droite de l'image** (au lieu d'une bande sous la
pellicule) et reste visible en plein écran.
---
## [2.19.1] — 2026-09-23
### Corrigé
- **#110 — lisibilité et positionnement du lecteur média** : en mode mobile, la
barre audio passe en grille (barre de progression pleine largeur sur sa propre
ligne) et les actions secondaires (précédent/suivant/agrandir/volume) sont
masquées pour éviter tout chevauchement ; le volume est aussi masqué sur les
écrans intermédiaires en desktop, et `overflow: hidden` empêche le débordement
hors de la pilule. La mini-fenêtre vidéo n'est plus ré-aimantée sur les bords :
elle se déplace et se redimensionne **librement** (position absolue mémorisée,
centre autorisé), le glisser fonctionnant désormais depuis n'importe quel point
de la fenêtre (boutons/curseurs/poignée exclus, seuil de 4 px pour préserver
les contrôles natifs de la vidéo).
---
## [2.19.0] — 2026-09-23
### Ajouté
- **#110 — Lecteur média persistant « Now Playing »** : les fichiers audio et
vidéo continuent de jouer pendant la navigation grâce à un contrôleur global
`frontend/js/now-playing.js` qui possède **un seul** élément `<audio>`/`<video>`
téléporté entre la vue inline (onglet/panneau) et un dock flottant
(`<body>`), sans interruption de lecture. Dock desktop : pilule verre dépoli
(artwork, titre, voûte, play/pause, précédent/suivant, barre de progression,
volume, retour au média, agrandir, fermer). Mini-fenêtre vidéo flottante
déplaçable/redimensionnable avec aimantation aux coins et bouton
Picture-in-Picture natif. Mode mobile : mini-player fixé au-dessus de la barre
d'outils (safe-area `viewport-fit=cover`). Intégration **Media Session** (écran
verrouillé, touches matérielles, Windows SMTC), panneau étendu façon « Now
Playing », lecture auto du média suivant/précédent du dossier, reprise après
rechargement, notification quand l'onglet est fermé pendant la lecture. Fiche :
[docs/features/media-viewers-109.md](docs/features/media-viewers-109.md) (§ Now
Playing, #110).
---
## [2.18.0] — 2026-09-23
### Ajouté
- **#109 — Support audio & vidéo (lecteurs HTML5 intégrés)** : les fichiers audio
(`.mp3 .m4a .aac .wav .ogg .oga .opus .flac`) et vidéo (`.mp4 .webm .mov .m4v`)
apparaissent désormais dans l'arborescence et l'index (nom/taille/date
uniquement, contenu jamais lu, intégrés à `SUPPORTED_EXTENSIONS` via
`media_types.py`). Nouvel endpoint `GET /api/media/{vault}?path=…` avec support
HTTP `Range` / `206 Partial Content` (`Content-Range`, `Accept-Ranges`, `416`
sur plage invalide, `413` au-delà de `OBSIGATE_MEDIA_MAX_INLINE_MB`, défaut
500 Mo) — le helper Range de `pdf/stream` a été extrait en
`_stream_file_with_range()` et est partagé. `api_file_view()` expose
`is_audio`/`is_video`/`stream_url`/`media_mime` avant toute lecture texte.
Frontend : lecteur audio dédié (artwork, durée via `loadedmetadata`) et lecteur
vidéo (`playsinline`, scène noire letterboxée), icônes Lucide `audio-lines` /
`video`, pause à la réinitialisation de la vue, repli téléchargement si le codec
n'est pas lisible ou le fichier trop volumineux. Les médias sont exclus du
contexte textuel BooksLM et du cache hors-ligne du service worker. Fiche :
[docs/features/media-viewers-109.md](docs/features/media-viewers-109.md).
---
## [2.17.0] — 2026-09-23
### Ajouté
- **#108 — Support complet des images (arborescence, visionneuse, indexation)** :
les images (`.png .jpg .jpeg .gif .svg .webp .bmp .ico`) apparaissent désormais
dans l'arborescence et l'index comme les autres fichiers — nom/taille/date
uniquement, jamais les octets (contenu indexé vide, TF-IDF préservé). Nouveau
module partagé `backend/media_types.py` (extensions + MIME, socle réutilisé par
#109). Visionneuse dédiée : zoom molette 0,1×–8×, pan au glisser, double-clic
pour réinitialiser, boutons +/−/reset et badge de zoom, navigation ←/→ entre
les images du dossier avec pellicule de miniatures, panneau métadonnées
(dimensions, taille, type, chemin, date), lightbox plein écran, « Ouvrir
l'original » et téléchargement. Endpoint `GET /api/media/{vault}/thumb`
(miniature WebP 256 px, cache disque invalidé par mtime, repli sur l'original
pour le SVG, `pillow>=10.0`). Filtre `ext:png`/`ext:jpg` opérationnel ;
compteurs d'images séparés dans `/api/dashboard` (`image_count`/`total_images`).
Fiche : [docs/features/image-support.md](docs/features/image-support.md).
### Corrigé
- **#108-B1 — Affichage isolé d'une image** : le `<img>` généré par
`api_file_view()` pointait vers `/api/file/{vault}/raw` (qui renvoie du JSON)
au lieu de `/api/image/{vault}` (octets + MIME correct). Corrigé côté backend
et dans `viewer.js` (bouton Plein écran), avec encodage d'URL des chemins.
- **#108-B3 — XSS via SVG** : `/api/image` (et le repli miniatures) pose
`Content-Security-Policy: sandbox` sur les SVG ouverts directement, pour
empêcher l'exécution du JavaScript embarqué ; le middleware n'écrase plus une
politique stricte posée par une route.
---
## [2.16.6] — 2026-09-22
### Ajouté
- **Guides d'utilisation `docs/GUIDES/`** : nouvel index + 10 guides FR
(prise en main, recherche/PDF/Excalidraw, assistant IA & Forge,
collaboration temps réel, PWA & hors-ligne, API REST, serveur MCP,
authentification & sécurité, déploiement Docker, desktop Tauri). Le guide MCP
est déplacé dans `docs/GUIDES/MCP.md` ; `docs/MCP_GUIDE.md` devient une page
de redirection.
### Modifié
- **README.md / README.fr.md** : capture d'écran réelle de l'application en tête
(remplace l'illustration ASCII) ; un emoji sur chaque entrée de la table des
matières ; nouvelle section « Guides » avec liens vers `docs/GUIDES/` ;
renvois vers les guides depuis les sections API, Recherche, Sécurité, Desktop
et Collaboration.
---
## [2.16.5] — 2026-09-22
### Corrigé
- **BUG-071 (complément) - spec E2E mobile de la page Configurations** :
`tests/e2e/config-mobile.spec.js` (nouveau, projet `chromium-mobile`,
ignoré en `chromium-desktop` comme `mobile-editor.spec.js`) : le hamburger
révèle le sommaire, le choix d'une section y défile + lien actif + repli
auto, aucun débordement horizontal à 393px. Vérifié en local contre
l'instance de test (port 2029, auth désactivée) : 3/3.
---
## [2.16.4] — 2026-09-22
### Corrigé
- **BUG-071 - Page « Configurations » inutilisable en mode mobile** : trois
causes. (1) Le sommaire (`#config-nav`) partageait la règle `.help-nav`
qui le masque sous 768px, mais — contrairement au Guide — la modale
n'avait aucun bouton pour l'afficher : aucun moyen d'atteindre une section.
Nouvel hamburger `#config-hamburger` dans l'en-tête (même traitement
`.help-hamburger` que le Guide, libellé traduit `config.toc_toggle`
FR/EN). (2) Les liens du sommaire étaient des ancres brutes sans JS :
`config.js` les intercepte désormais (défilement doux vers la section dans
la modale, lien actif, repli automatique du sommaire sur mobile, réinit à
l'ouverture). (3) Les grilles 2 colonnes (fournisseur/modèle IA, clé/modèle
par fournisseur), les rangées d'ajout à largeurs fixes (jetons, webhooks)
et les lignes webhook/jeton/partage en flex une ligne débordaient en
360px : bloc CSS mobile scopé `#config-modal` (1 colonne, wrap, largeurs
inline neutralisées, cibles tactiles 44px, sommaire plafonné à 46vh).
`data-i18n-attr` accepte désormais plusieurs paires `attr:clé` séparées
par `;` (titre + aria-label traduits). Tests :
`tests/frontend/config-mobile.test.mjs` (nouveau, 11 — hamburger, i18n,
câblage JS, CSS mobile, garde-fou ancres mortes façon BUG-067),
enregistré dans le CI.
---
## [2.16.3] — 2026-09-22
### Corrigé
- **BUG-070 - Activation clé physique WebAuthn impossible (« Validation du
credential WebAuthn échouée »)** : deux causes. (1) Les valeurs par défaut
(`rp_id localhost`, origines `http://localhost` sans port) rejetaient toute
URL réelle — logs : `Unexpected client data origin "http://localhost:2020",
expected one of ['http://localhost']`. `rp_id`/origines sont désormais
dérivés de la requête (hôte exact, port inclus ; `X-Forwarded-Host/Proto`
si `OBSIGATE_TRUST_PROXY=true`), la config explicite restant prioritaire
(`backend/auth/webauthn_mfa.py::resolve_relying_party`, appliqué aux 4
endpoints d'enregistrement et de login). (2) Challenge à usage unique
fragile au double-clic/retry (`challenge was not expected`) : les 5
derniers challenges sont conservés et la vérification accepte le challenge
correspondant à la cérémonie en cours. `.env.example` documente le nouveau
comportement. Vérifié au navigateur avec authentificateur virtuel
(Playwright CDP, instance Docker) : enregistrement 200 + clé listée, puis
clé de test retirée. Tests : `tests/test_webauthn.py` (+8 : résolution RP,
forwarded, retry, roundtrip sans config).
---
## [2.16.2] — 2026-09-22
### Corrigé
- **BUG-069 - Login 2FA bloqué sans erreur** : après user+mot de passe corrects
sur un compte avec 2FA, la page de login restait affichée sans erreur et le
challenge MFA n'apparaissait jamais. Cause : `showMfaChallenge`
(`frontend/js/auth.js`) montait le challenge dans `.login-box`, inexistant
dans `index.html` (marquage réel : `#login-screen > .login-card`) →
`return` silencieux. Correctif : montage dans `.login-card` (repli
`#login-screen`) + erreur visible (`mfa.challenge_unavailable`, FR/EN) au
lieu d'un retour silencieux si le point de montage manque. Vérifié de bout
en bout au navigateur (Playwright, instance Docker) : challenge affiché,
code erroné → erreur, code valide → connecté. Tests :
`tests/frontend/mfa-settings.test.mjs` (+2 contrôles d'ancrage DOM).
---
## [2.16.1] — 2026-09-22
### Corrigé
- **BUG-068 - Configuration : section « 🔒 Sécurité du compte » inachevée** :
boutons `config-btn-primary` / `config-btn-danger` définis depuis les
variables du thème (`frontend/style.css`) ; QR code TOTP généré en local par
le backend (`POST /api/auth/mfa/totp/setup` → `qr_data_url`, SVG `data:`
via `segno`, `backend/requirements.txt`) au lieu de l'image tierce bloquée
par la CSP (`img-src 'self' data: blob:`, secret TOTP exposé) ; codes de
récupération affichés aussi à la première activation WebAuthn ; carte
« Mot de passe » (changement via `POST /api/auth/change-password`) et
échappement des libellés de clés WebAuthn. Tests :
`tests/test_mfa.py::test_mfa_setup_returns_local_qr_data_url`,
`tests/frontend/mfa-settings.test.mjs` (nouveau, 9 contrôles).
---
## [2.16.0] — 2026-09-22
### Modifié
- **#86 - Optimisation globale des performances (phase 3)** : ferme les deux derniers
points de la phase 3 (recherche via inverted index, PDF lazy et caps regex déjà livrés
via BUG-033/BUG-040/BUG-025). Scan **différentiel** : `_scan_vault` réutilise les
entrées inchangées (`size` + `modified`) d'un snapshot précédent — seuls `os.walk` +
`stat` tournent à chaque rebuild (`build_index`, `reload_single_vault`). Extraction
**excalidraw différée** : le scan ne lit plus les `.excalidraw` / `.excalidraw.md`
(flag `excalidraw_text_pending`), `enrich_pdf_texts()` extrait leur texte après index
comme pour les PDF. Garde-fou `MAX_REPLACE_FILE_BYTES` (5 Mio) sur `replace_in_files`.
Tests : `tests/test_perf_phase3.py` (9). Voir
[docs/features/perf-phase3-86.md](./docs/features/perf-phase3-86.md).
---
## [2.15.0] — 2026-09-22
### Ajouté
- **#107 - Configuration : gestion des clés API & MCP** — nouvelle section
« 🔑 Clés API & MCP » dans le panneau de configuration : création, liste
(créée / expire / dernière utilisation) et révocation de jetons longue
durée utilisables aussi bien sur l'API REST que sur le serveur MCP
(`/mcp`) — un seul et même jeton Bearer pour les deux. Choix
d'expiration à la création : 1 jour, 1 mois, 6 mois, 1 an, sans fin.
Le secret n'est affiché qu'une fois (jamais persisté en clair,
`data/api_tokens.json` ne contient que les métadonnées) ; révocation
immédiate des deux côtés, plafond 50 clés par utilisateur, isolation
par utilisateur, audit `config_change`. Corrigé au passage : le store
de révocation (`revoked_tokens.json`) bornait toute entrée à 7 jours —
un jeton longue durée révoqué « reprenait vie » après purge ; il est
désormais calé sur l'expiration réelle du jeton. Voir
[docs/features/api-mcp-tokens-107.md](./docs/features/api-mcp-tokens-107.md).
---
## [2.14.1] — 2026-09-22
---
## [2.14.0] — 2026-09-19
### Ajouté
- **#106 - Assistant IA : actions instantanées contextuelles + catalogue de prompts** :
la zone d'accueil remplace les 3 suggestions statiques par des **actions suggérées
selon le contexte détecté** (sélection dans l'éditeur → concis/corriger/expliquer ;
fichier de code → expliquer/bugs/tests ; 2+ docs → fusionner/comparer/frictions ;
1 doc → résumé 3 points/checklist/frontmatter ; répertoire ou général sinon), avec
badge de contexte dans l'en-tête. Un bouton **« Toutes les actions »** ouvre un
tiroir catalogue (recherche instantanée, 6 catégories, 25 actions i18n FR/EN).
Nouveau prompt **« Générer le frontmatter YAML »** au format complet du vault
(titre, auteur, dates ISO-8601, tags, aliases, booléens, NomDeVoute, Description)
et **« Mettre à jour le frontmatter »** (conserve les champs existants, actualise
`modification_date`, recalcule tags/aliases/Description, complète les manquants) —
tous deux basculent en mode agent et appliquent le résultat via la carte de
confirmation. Détail : [features/ai-quick-actions.md](./features/ai-quick-actions.md).
---
## [2.13.1] — 2026-09-18
---
## [2.13.0] — 2026-09-18
### Ajouté
- **#105 - Guide d'utilisation : audit de couverture, téléchargement Markdown/PDF,
section Architecture** : le guide intégré est aligné sur l'application réelle après
~35 features livrées. Huit nouvelles sections — **🏗️ Architecture** (diagramme Mermaid
des grandes composantes : clients SPA/PWA/Tauri → serveur FastAPI REST, index de
recherche, rendu markdown, IA, MCP, WebSocket, webhooks → vaults, `data/*.json`,
backups), **📊 Diagrammes** (Mermaid : zoom, plein écran, copie SVG/code, thèmes),
**⭐ Bibliothèque** (signets, recherches sauvegardées, backlinks & graphe, conflits
Syncthing, pièces jointes), **📴 Hors-ligne** (PWA, file d'attente IndexedDB, replay,
watcher), **👥 Collaboration** (Yjs/CRDT, curseurs awareness), **🖥️ Desktop** (Tauri,
updater signé, assistant premier lancement), **🔌 API** (OpenAPI 3.1 : `/docs`,
`/redoc`, `/api`, `/openapi.json`, auth Bearer/cookie, serveur MCP, automatisation)
et **🌍 Multilingue** — plus des compléments dans les sections existantes (recherche
sémantique hybride, notifications web push, export PDF, export HTML/ePub/ZIP,
anti-doublons d'upload, vue multi-panneaux, MFA TOTP/WebAuthn, tableau de bord
admin). Nouveau **`GET /api/guide/download?format=md|pdf&lang=fr|en`** (tag OpenAPI
« Guide ») : le document est généré depuis la modale d'aide réelle et les locales,
il reflète donc exactement le guide affiché, dans la langue de l'utilisateur ;
boutons **Markdown** et **PDF** ajoutés dans l'en-tête du guide. En **desktop**
(Tauri, `body.desktop-mode`) et sur grand écran web, le guide s'élargit
(conteneur jusqu'à 1760 px, contenu 1280–1440 px) pour une lecture confortable.
Source de vérité du nouveau contenu : `scripts/guide_content.py` (locales générées,
jamais éditées à la main). Tests : `tests/test_guide.py` (11).
### Modifié (ajustements retour utilisateur sur #105)
- Boutons de téléchargement du guide : **icônes seules** (plus de libellé « Markdown »/
« PDF »), tooltip i18n explicite sur chaque bouton.
- PDF du guide : le bloc Mermaid de la section Architecture est converti en **diagramme
rendu** (PNG pré-rendu commité via `scripts/build_guide_diagrams.py` +
`scripts/render_guide_diagram.mjs`, inséré par `diagram_png_for()`), au lieu du code
brut ; le Markdown garde le fenced ` ```mermaid ` (copiable).
- PDF du guide : emoji rendus **en couleur** au lieu de rectangles — `fonts-noto-color-emoji`
ajouté à l'image Docker et `"Noto Color Emoji"` en fin de pile de polices du PDF
(la TOC était correcte car elle utilise DejaVu, qui n'a pas les emoji pleine chasse).
### Corrigé
- **BUG-067 - Guide : entrée « 📱 Mobile » morte** : le sommaire pointait vers
`#help-mobile-editor`, section inexistante (l'édition mobile n'était qu'un `<h3>`
de la section Édition). Le bloc est devenu une section dédiée ancrée ; l'ancre
`#help-ia` (également morte) a été corrigée en `#help-ai` et un attribut
`data-i18n-placeholder` dupliqué nettoyé. Garde-fou : `test_guide_nav_has_no_dead_anchors`.
---
## [2.12.2] — 2026-09-18
### Corrigé
- **BUG-066 — Configuration : icônes manquantes dans la table des matières** :
les entrées « Fichiers cachés » et « Partages publics » du sommaire de la page de
configuration n'affichaient pas d'icône (les titres de section, eux, en avaient une).
Les libellés i18n `config.section_hidden` (🗂️) et `config.section_shares` (📤) sont
alignés sur leurs titres de section, en FR **et** EN. Test de non-régression ajouté
dans `tests/frontend/unit.test.mjs` (toutes les entrées du sommaire doivent porter une
icône dans les deux langues).
---
## [2.12.1] — 2026-09-18
---
## [2.12.0] — 2026-09-18
### Ajouté
- **#104 - Configuration : redesign UI de la section « 🤖 Clés API Intelligence Artificielle »** :
la longue liste plate de champs devient une interface structurée et dépliable —
1. **En-tête de section** : titre + sous-titre explicatifs et une **barre de recherche**
(`#cfg-ai-search`) pour filtrer les fournisseurs (normalisée, insensible à la casse et aux
accents), avec état vide « Aucun fournisseur ne correspond ».
2. **Carte « Configuration par défaut »** visuellement distincte : grille **2 colonnes**
fournisseur / modèle par défaut, et **capacités du modèle rendues en badges colorés**
(nouveau `renderCapabilityBadges()` dans `frontend/js/ai.js`, uniquement les capacités
actives) au lieu des cases à cocher.
3. **Fournisseurs d'API en cartes dépliables** (rendu dynamique depuis `AI_PROVIDER_NAMES` par
`_renderAIProviderCards()`, `frontend/js/config.js`) : état replié = logo (initiale), nom,
**badge de statut** (« Configuré » vert / « Non configuré » gris) et **corbeille discrète**
pour supprimer la clé (visible uniquement si configurée, confirmation conservée) ; état
déplié = libellés au-dessus des champs, **API key à 60 % / modèle à 40 %** (grille `3fr 2fr`).
Les boutons « Configuré/Supprimer » redondants à l'intérieur des champs disparaissent — le
statut vit désormais uniquement dans l'en-tête de la carte.
4. **Barre d'actions** : « Sauvegarder » (primaire) et « Tester » (outline) dans un footer
**sticky** en bas de section, toujours accessible pendant le défilement, avec le statut de
test à côté.
Styles `.ai-keys-*` / `.ai-provider-*` en variables CSS (profondeur fond/carte, espacement
généreux, chevron animé, focus visible, responsive 1 colonne < 600 px) ; textes i18n FR/EN
(`config.ai_*`). Les ID d'éléments (`cfg-<provider>-key/model/badge/delete`) et le
comportement de sauvegarde/test/suppression restent inchangés (rétrocompatible pickers IA).
Tests : `tests/frontend/config-ai-keys.test.mjs` (7 — rendu, badges, bascule, filtre, save,
suppression, badges de capacités).
---
## [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 : `<link>` vers `…/@excalidraw/[email protected]/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=<ts>#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 `<embed>`/`<object>`, alors
que le viewer rendait le PDF via `<embed type="application/pdf">` : le lecteur natif était
bloqué. Le rendu passe désormais par une `<iframe>` (autorisée par `frame-src 'self'`, le stream
`/api/file/{vault}/pdf/stream` étant same-origin) ; `object-src 'none'` est conservé. Fichiers :
`frontend/js/viewer.js`, `tests/frontend/pdf-viewer.test.mjs` (nouveau),
`tests/e2e/pdf-viewer.spec.js` (nouveau, fixture `test_vault/sample-pdf.pdf`).
---
## [2.11.0] — 2026-09-17
### Ajouté
- **#103 — Clés des sources connectées configurables depuis la page Configurations** : nouvelle
section « 🔗 Sources connectées & recherche » (admin) permettant de saisir ses propres jetons
sans toucher aux variables d'environnement : clés de recherche web à clé (Tavily, Brave,
SerpAPI, Exa), URL Gitea + token, token GitHub. Stockage dans `data/api_keys.json` (même
fichier que les clés des fournisseurs IA) via les endpoints
`GET/POST/DELETE /api/config/tool-keys` (GET masque les secrets, URLs en clair ; liste
blanche stricte ; admin requis). Les outils lisent la valeur stockée **en priorité** puis
l'environnement (Infisical) — `backend/tools/secrets.py`. Fichiers :
`backend/tools/{secrets,web,connected}.py`, `backend/main.py`, `frontend/index.html`,
`frontend/js/config.js`, `frontend/locales/{fr,en}.json`, `tests/test_tool_keys.py` (nouveau),
`tests/test_connected_tools.py`.
---
## [2.10.1] — 2026-09-17
### Corrigé
- **BUG-059 — Assistant IA : un clic dans la conversation faisait sauter le texte au bas de la
fenêtre** : dans une conversation ouverte (post ancré en haut), tout clic — lien de fichier,
étapes, sélection de texte — dépinait l'ancre via le gestionnaire `mousedown` et retirait le
padding d'ancre, bornant le scroll à la nouvelle hauteur max (saut au bas). Le dépintage est
désormais réservé aux vrais gestes de scroll : molette, tactile et poignée de scroll
uniquement (`isScrollbarPress`). Fichiers : `frontend/js/bookslm.js`,
`tests/frontend/ai.test.mjs`.
- **#92 — `create_pdf` de l'Assistant : tableaux mal formatés** : l'outil `create_pdf`
(génération PDF via l'assistant) utilisait un rendu reportlab simplifié sans support des
tableaux. Il passe désormais par le même pipeline que le bouton « Télécharger PDF » de la
page document (mistune + plugin `table` + WeasyPrint), avec repli automatique sur le rendu
simple si WeasyPrint n'est pas disponible (GTK absent). Fichiers :
`backend/tools/documents.py`, `tests/test_document_tools.py`.
---
## [2.10.0] — 2026-09-17
### Ajouté
- **#92 — Assistant IA : écosystème d'outils phase 2 (web étendu, sources connectées, documents)** :
- **Recherche web à clé** : fournisseurs optionnels essayés avant SearXNG — Tavily, Brave
Search, SerpAPI (Google) et Exa (`OBSIGATE_TAVILY_API_KEY`, `OBSIGATE_BRAVE_API_KEY`,
`OBSIGATE_SERPAPI_API_KEY`, `OBSIGATE_EXA_API_KEY`), avec ordre personnalisable via
`OBSIGATE_WEB_PROVIDERS`.
- **Transverse** : réessais réseau avec backoff maison (`OBSIGATE_WEB_RETRY`) et cache SQLite
des résultats web avec TTL (`OBSIGATE_WEB_CACHE_TTL`, 900 s par défaut, `OBSIGATE_WEB_CACHE_PATH`).
- **Rendu dynamique** : `fetch_url(render=True)` délègue les pages SPA à un worker Playwright
isolé (dépendance optionnelle, dégradation propre si non installée).
- **Crawl de site** : `crawl_site` (WRITE, confirmation) capture jusqu'à 20 pages d'un même
hôte et enregistre un condensé Markdown dans un vault.
- **Sources connectées** : Gitea (`OBSIGATE_GITEA_URL`/`OBSIGATE_GITEA_TOKEN`) et GitHub
(`OBSIGATE_GITHUB_TOKEN`) — `git_list_repos`, `git_search_issues`, `git_get_file` (READ,
rate-limités, audités). Les drives cloud (Google Drive / OneDrive) restent orientés serveur
MCP externe (#79), conformément à la feuille de route.
- **Production de documents** : `create_xlsx` (openpyxl), `create_docx` (python-docx),
`create_csv`, `create_pdf` (reportlab) — outils WRITE avec confirmation et sauvegarde
dans le vault (backup avant écrasement).
- Chaque outil : libellé `labels.py` + clés i18n `ai.step.*` FR/EN + tests unitaires mockés
(httpx). Dépendances ajoutées : `openpyxl`, `python-docx`, `reportlab`.
Fichiers : `backend/tools/{webcache,webrender,connected,crawler,documents,web,schemas,labels}.py`,
`backend/services/mutations.py`, `tests/test_{web_cache,web_search_providers,webrender,connected_tools,document_tools,crawler}.py`.
---
## [2.9.1] — 2026-09-17
### Corrigé
- **BUG-058 — Éditeur « Editer » : la barre de numérotation de ligne ne suit pas le thème** :
CodeMirror peint son gutter (colonne des numéros de ligne) avec des valeurs claires codées
en dur (`#f5f5f5`, bordure `#ddd`), si bien qu'en thème sombre la barre restait gris clair
alors que le fond de l'éditeur suivait le thème. Le gutter est désormais dérivé des
variables CSS du thème ObsiGate (`color-mix(in srgb, var(--text-primary) …)` pour un fond
subtil, `--text-secondary` pour les numéros, `--border` pour la séparation), ce qui le fait
suivre tous les thèmes et modes (sombre, clair, contraste élevé, sépia). Fichiers :
`frontend/style.css`. Tests : `tests/frontend/editor-inline.test.mjs`.
---
## [2.9.0] — 2026-09-17
### Ajouté
- **#102 — Assistant IA : bouton « Ajouter la section » par bloc de code** : chaque bloc
de code d'une réponse de l'assistant (par ex. une section ```markdown```) affiche un
bouton discret « Ajouter la section » qui insère **uniquement ce bloc** dans le document
ouvert (sans les délimiteurs de code), au lieu de la réponse complète. Fichiers :
`frontend/js/bookslm.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`.
Tests : `tests/frontend/ai.test.mjs`.
### Corrigé
- **BUG-057 — Assistant IA : bouton « Ajouter » inopérant dans l'éditeur Forge** : le
bouton ne ciblait que `state.editorView` (CodeMirror de « Editer ») et affichait « Aucun
document ouvert dans l'éditeur » en Forge. `_insertIntoEditor()` prend désormais en
charge les trois surfaces : CodeMirror, l'iframe Forge (délégation par
`postMessage({ type: 'parent-insert' })` → `insertAtCursor` dans `editor-poc.html`) et le
textarea de repli. Fichiers : `frontend/js/bookslm.js`, `frontend/editor-poc.html`.
Tests : `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs`.
---
## [2.8.4] — 2026-09-17
---
## [2.8.3] — 2026-09-17
### Corrigé
- **BUG-056 - Éditeur Forge en plein écran : l'Assistant IA s'ouvre en arrière-plan** :
le panneau de l'assistant est monté dans le document parent, alors que le plein écran
Forge porte sur l'iframe — l'API Fullscreen n'affichant que l'élément plein écran et ses
descendants, le panneau restait invisible. Le plein écran est désormais quitté avant
d'ouvrir le panneau : côté **parent** (`sync.js`, sur `forge-open-ai` — le plein écran
peut appartenir au document parent et non à l'iframe) **et** côté iframe
(`editor-poc.html`, `openAssistant`), la demande d'ouverture étant émise une fois la
sortie effective. Fichiers : `frontend/editor-poc.html`, `frontend/js/sync.js`.
Tests : `tests/frontend/forge-completion.test.mjs` (+1),
`tests/frontend/editor-inline.test.mjs` (+1).
---
## [2.8.2] — 2026-09-17
### Corrigé
- **BUG-055 (complément) - Éditeur Forge : une complétion acceptée au `Tab` était
supprimée 1–2 s plus tard** : l'auto-sauvegarde (2 s) déclenche un événement SSE
`index_updated` sur le fichier en cours, et le parent rechargeait alors le tampon de
Forge **depuis le disque** — écrasant une complétion (ou toute frappe) faite après la
sauvegarde. Le rechargement SSE est désormais ignoré tant que le tampon local est
modifié (`isDirty`) ; seul un écrit externe (assistant IA) force le rechargement.
L'auto-sauvegarde ne repasse plus l'état « enregistré » si des modifications sont
arrivées pendant la requête (Forge **et** éditeur CodeMirror), et l'acceptation du
ghost annule la requête de prédiction en attente. Fichiers : `frontend/editor-poc.html`,
`frontend/js/utils.js`. Tests : `tests/frontend/forge-completion.test.mjs` (+3),
`tests/frontend/editor-inline.test.mjs` (+1).
---
## [2.8.1] — 2026-09-17
### Corrigé
- **BUG-055 - Éditeur Forge : autocomplétion Tab naturelle et fiable** : la touche `Tab`
déclenchait simultanément trois actions indépendantes (indentation, complétion d'un mot du
document, acceptation de la prédiction IA), ce qui insérait un ou deux espaces avant le mot
complété — et le retour arrière effaçait alors l'ajout. La gestion de `Tab` est désormais
**unifiée** avec une priorité claire (liste de suggestions ouverte → prédiction IA →
complétion de mot du document → indentation), une seule action par appui. Les helpers de
complétion sont extraits en fonctions pures partagées avec CodeMirror
(`getWordFragment`, `findWordCompletions`, `normalizeGhost`, `chooseTabAction`) ; plusieurs
candidats affichent une liste positionnée au curseur ; la **complétion fantôme** n'affiche
plus un miroir transparent de tout le document (source du décalage visuel et des espaces
fantômes) mais uniquement la prédiction, positionnée exactement au curseur et nettoyée dès
que le curseur bouge ou que la vue défile ; une complétion de mot ne peut plus introduire
d'espace. Le prompt `/api/ai/inline-complete` est simplifié et borné à 128 tokens pour des
suggestions plus courtes et plus rapides. Fichiers : `frontend/editor-poc.html`,
`frontend/js/autocomplete.js`, `backend/ai.py`. Tests :
`tests/frontend/forge-completion.test.mjs` (nouveau, 28 tests).
---
## [2.8.0] — 2026-09-17
### Ajouté
- **#101 - Forge : Assistant IA partagé et plein écran (Forge & Editer)** : le bouton
« AI Panel » de Forge ouvre désormais le **panneau Assistant IA** existant (barre latérale)
au lieu d'un mini-chat isolé — même contenu, même fournisseur/modèle, même historique, mêmes
menus `/` et `@`, sans duplication. Forge lit la sélection du sélecteur de l'assistant
(`localStorage['obsigate_ai_picker']`, même origine) et l'injecte dans ses appels
`/api/ai/*` et sa **complétion fantôme** (repli `ollama` si aucun fournisseur choisi) ; au
passage, les endpoints erronés sont corrigés (`make-longer`/`make-shorter`, `target_lang`).
Un bouton **plein écran** natif est ajouté à Forge (iframe `allow="fullscreen"`) et à
l'éditeur **Editer** (plein écran sur le conteneur, fonctionne en modale comme en inline,
sortie à la fermeture). Libellés i18n FR/EN. Fichiers : `frontend/editor-poc.html`,
`frontend/js/sync.js`, `frontend/js/viewer.js`, `frontend/js/utils.js`,
`frontend/index.html`, `frontend/style.css`, `frontend/locales/{fr,en}.json`. Tests :
`tests/frontend/editor-inline.test.mjs` (+10).
---
## [2.7.5] — 2026-09-17
### Corrigé
- **BUG-054 - Éditeur « Editer » : le bouton Sauvegarder restait bloqué sur le spinner de
chargement** : le bouton `#editor-save` est un nœud DOM partagé entre toutes les sessions
d'édition (y compris en mode en ligne). Une sauvegarde manuelle y remplaçait le crochet par un
spinner et le désactivait, mais cet état n'était jamais remis à zéro : après une sauvegarde
réussie (l'éditeur se ferme puis se rouvre), après une sauvegarde Forge, ou après un échec de
requête (le `catch` ne restaurait ni l'icône ni l'état), le spinner persistait jusqu'à un
rechargement complet de la page. Un helper `resetSaveButton()` restaure désormais le crochet
et réactive le bouton à l'ouverture (`openEditor`), à la fermeture (`closeEditor`) et en cas
d'échec (`saveFile`). Fichier : `frontend/js/utils.js`. Tests :
`tests/frontend/editor-inline.test.mjs` (+4).
---
## [2.7.4] — 2026-09-17
### Corrigé
- **BUG-053 - Assistant IA (mode agent) : le fichier demandé n'est pas créé** : le prompt
système du mode Général (et du dossier vide) enseignait encore le **protocole texte**
`obsigate-action`, si bien que le modèle décrivait l'action dans un bloc texte au lieu
d'appeler l'outil natif `create_file` — rien n'était donc créé (et le bloc, volumineux,
était tronqué avant sa fermeture). En mode agent, le prompt demande désormais d'appeler
directement les outils natifs (`create_file`, `create_directory`, …) et interdit les blocs
`obsigate-action` ; le chat classique conserve le protocole texte. La limite de sortie de
l'agent passe à 8 192 jetons pour laisser place au contenu complet d'un fichier.
Fichiers : `backend/bookslm.py`, `backend/bookslm_routes.py`. Tests : `tests/test_bookslm.py` (+3).
---
## [2.7.3] — 2026-09-16
### Corrigé
- **BUG-052 - Assistant IA : recherche web sans réponse finale (étapes et sources affichées, aucun texte)** :
quand le budget d'itérations (`DEFAULT_MAX_ITERATIONS = 10`) ou le quota d'appels d'outils
était épuisé pendant que le modèle enchaînait encore des recherches/lectures, la boucle
d'agent renvoyait un contenu vide → la conversation n'affichait que les étapes et les
sources. La boucle effectue désormais un **dernier appel sans outil** qui demande au modèle
de synthétiser les informations recueillies (`_finalize_answer`), avec un repli déterministe
listant les sources si cet appel échoue ou reste vide. Les appels d'outils non atteints du
lot en cours de quota reçoivent un résultat `deferred` pour garder la conversation valide.
Fichier : `backend/agent/loop.py`. Tests : `tests/test_agent_loop.py` (+2).
---
## [2.7.2] — 2026-09-16
### Corrigé
- **BUG-051 (complément) - Repli Bing : en-têtes de navigation navigateur** : un `User-Agent`
navigateur seul ne suffit pas — Bing renvoie des résultats factices (SERP sans rapport avec
la requête) aux appels dépourvus des en-têtes de navigation habituels. Les fournisseurs HTML
(DuckDuckGo, Bing) envoient désormais `Accept-Language`, `Sec-Fetch-*` et
`Upgrade-Insecure-Requests` (`BROWSER_HEADERS`). Vérifié en conteneur : recherche
« Canadien de Montréal 2026-2027 » → résultats NHL / RDS / Wikipédia pertinents
(`provider: bing`). Fichier : `backend/tools/web.py`.
---
## [2.7.1] — 2026-09-16
### Corrigé
- **BUG-051 - Assistant IA : « je ne peux pas accéder à internet » malgré la recherche web** :
lorsque l'instance SearXNG auto-hébergée ne remontait aucun résultat (moteurs amont
suspendus/CAPTCHA), `web_search` renvoyait une liste vide et le modèle concluait à une
absence d'accès réseau. L'outil essaie désormais une **chaîne de repli sans clé** —
SearXNG, puis DuckDuckGo (endpoint HTML sans JS), puis Bing (page de résultats HTML) —
et ne s'arrête qu'au premier fournisseur qui renvoie des résultats (`provider` dans le
résultat, `OBSIGATE_WEB_FALLBACK=0` pour désactiver les replis). Le message d'avertissement
final nomme les fournisseurs essayés. Fichier : `backend/tools/web.py`. Tests :
`tests/test_web_tools.py` (+4).
---
## [2.7.0] — 2026-09-16
### Ajouté
- **Assistant IA — 30 skills intégrés (au lieu de 11) et prompts enrichis** : les skills
déclenchés par `/` couvrent désormais l'extraction/structuration (`/extract`, `/timeline`,
`/glossary`, `/tag`), la transformation (`/translate`, `/adapt`, `/clean`,
`/summary-progressive`), l'analyse critique & décision (`/critique`, `/compare`,
`/prioritize`, `/swot`, `/debate`), l'apprentissage (`/quiz`, `/reading-note`,
`/qa-generator`) et la méta-gestion (`/link`, `/anonymize`, `/estimate`). Tous les prompts
sont réécrits (rôle, structure de sortie Markdown, cas limites) et complétés par un bloc
`COMMON_RULES` commun (français, notes traitées comme données, anti-hallucination,
signalement des contradictions, conservation des noms/dates/chiffres, réponse
« Aucune information exploitable fournie. » si les notes sont insuffisantes).
Fichier : `backend/skills.py`. Tests : `tests/test_skills.py` (ids uniques/valides,
présence du prompt et de `COMMON_RULES` pour chaque skill).
---
## [2.6.2] — 2026-09-16
### Corrigé
- **BUG-050 — Assistant IA : échec de la création d'un sous-dossier contenant un fichier** :
lors d'une pause de confirmation, la boucle d'agent ne renvoyait le résultat que du seul
appel confirmé alors que le message assistant annonçait tous les appels d'outils du tour —
la conversation devenait invalide (identifiant `tool_call_id` sans réponse) et la reprise
échouait. Les appels non atteints reçoivent désormais un résultat `deferred` explicite
(`backend/agent/loop.py`). En complément, `create_directory` est idempotent côté outil IA
(succès si le dossier existe déjà, `backend/services/mutations.py`) et les consignes
(`create_file` crée les dossiers parents) invitent le modèle à un seul appel avec un chemin
imbriqué (`backend/bookslm.py`, `backend/tools/service.py`). Tests : `tests/test_agent_loop.py`
(+1), `tests/test_tools_mutations.py` (+1), `tests/test_api_main.py` (+1).
---
## [2.6.1] — 2026-09-16
---
## [2.6.0] — 2026-09-16
### Ajouté
- **Barre de filtrage de la sidebar sur « Récents » et « Sauvegardes » (#99)** : la barre
de recherche de la sidebar agit désormais sur les onglets **Récents**
(`filterRecentFiles`, filtrage titre/chemin/vault/aperçu/tags) et **Sauvegardes**
(`filterSavedSearches`, cumulable avec les pills Tous/Recherches/Répertoires) —
insensible à la casse et aux accents, avec message d'absence de résultat et
placeholders dédiés (`sidebar.filter_recent`, `sidebar.filter_saved`). Le routage de
`initSidebarFilter` est unifié (`routeFilter`/`routeClear`) pour couvrir les cinq
onglets. Fiche : [docs/features/sidebar-filters.md](./docs/features/sidebar-filters.md).
- **Assistant IA — Deep Research en pastille (#100)** : « Deep Research » ajoute
désormais une **pastille** (comme les skills) au lieu d'écrire la directive dans la
zone de saisie ; le mode Agent est activé et la directive est injectée au moment de
l'envoi, sans polluer le message affiché.
### Corrigé
- **BUG-049 — icône du bouton « + » de l'assistant invisible** : la règle générique
`.bookslm-input-area button` écrasait `.bookslm-btn-plus` (`padding: 8px 16px` sur une
largeur de 32 px ⇒ largeur de contenu nulle ⇒ SVG à 0 px). Sélecteur porté à
`.bookslm-input-area button.bookslm-btn-plus`, l'icône `plus` est de nouveau visible
(vérifié en navigateur : SVG 0 px → 18 px). Tests : `tests/frontend/ai.test.mjs` (+1),
`tests/frontend/sidebar-filters.test.mjs` (nouveau, 8 tests).
---
## [2.5.2] — 2026-09-16
---
## [2.5.1] — 2026-09-16
---
## [2.5.0] — 2026-09-16
### Ajouté
- **Assistant IA — filtre de recherche dans la sidebar « Historique IA » (#98)** : la barre
de filtrage de la sidebar agit désormais sur l'onglet Historique IA. `filterAIHistory()`
(`frontend/js/config.js`) filtre les sessions par titre, aperçu, répertoire, contexte ou
libellé de mode — insensible à la casse et aux accents (`_aiNorm`) — avec cache sessions
(`_aiSessionsCache`), message « aucune conversation ne correspond à la recherche »
(`bookslm.history_no_match`) et placeholder dédié `sidebar.filter_ai`. Le routage
saisie / touche casse / bouton « × » vers ce filtre est géré dans `initSidebarFilter`
(`frontend/js/sidebar.js`) quand l'onglet IA est actif.
### Corrigé
- **BUG-048 — menu d'ajout « + » : « Contextes » et « Skills » ouvrent enfin leurs menus
« @ » / « / »** : le clic sur une entrée du panneau `.bookslm-ext-menu` remontait au
gestionnaire du panneau qui annulait le rendu asynchrone du menu (jamais affiché) ;
`e.stopPropagation()` sur les entrées corrige le comportement. Le bouton « + » porte
l'icône Lucide `plus`. Tests : `tests/frontend/ai.test.mjs` (+3), nouvelle suite
`tests/frontend/ai-sidebar.test.mjs` (6 tests).
---
## [2.4.0] — 2026-09-16
### Ajouté
- **Assistant IA — historique permanent des conversations (#95)** : les échanges sont
désormais **persistés côté backend** (`backend/ai_history.py`, `data/ai_history/{user}.json`,
cap 200, écriture atomique) et plus seulement en localStorage. Nouveaux endpoints
`GET/PUT/DELETE /api/ai/bookslm/history[…]` (liste résumée sans messages → full,
upsert qui force l'id, suppression, isolation par utilisateur). Le panneau synchronise
ses sessions au chargement (debounce 600 ms, repli hors-ligne sur le cache local),
migre les anciennes clés `bookslm-sessions-*` / `bookslm-history-*` et notifie la
sidebar via l'événement `bookslm:history-updated`.
- **Assistant IA — accès rapide à l'historique dans la sidebar (#96)** : cinquième onglet de
navigation `#sidebar-tab-ai` (icône `messages-square`) listant les conversations par
ordre chronologique ; un clic ouvre la conversation dans le panneau Assistant IA
(`openWithSession`).
- **Assistant IA — panneau « + » extensible (#97)** : le bouton « Attach an image » est
remplacé par un bouton **« + »** ouvrant un panneau modulaire (registre `_extensions`
simple à étendre) : Fichiers, Image, Contextes, Skills, Deep Research (mode agent +
prompt pré-rempli), Recherche web et Canva (les deux derniers en « Bientôt »).
- **Assistant IA — bouton de soumission arrondi (#94)** : `.bookslm-btn-send` circulaire
(40 px), icône Lucide `arrow-up` remplaçant l'avion ✈️.
- **Fiche feature** : [docs/features/ai-assistant-history.md](./docs/features/ai-assistant-history.md).
Tests : `tests/test_bookslm.py` (+11), `tests/frontend/ai.test.mjs` (+3).
---
## [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}`
(`</script>` 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_<ID>` (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 `<select>` natif, tout
en conservant un select masqué pour les commandes admin (`/model`). Le picker partagé
(`frontend/js/ai.js`) est utilisé par l'assistant et la barre d'édition IA. Tests JSDOM
(`tests/frontend/ai.test.mjs` +1).
- **#81 Assistant IA — Commandes `@`/`/`, skills, images & capacités des modèles** — l'assistant
devient plus flexible et multimodal. **Panneau redimensionnable** à la souris (largeur 320–1000 px
persistée). **Commande `@`** pour attacher des contextes ad-hoc (fichiers, répertoires) au contexte
courant. **Commande `/`** pour lancer des **skills** réutilisables (`/research`, `/resume`,
`/actions`, `/reformuler`, `/correction`, `/brainstorm`, `/plan`, `/ask`, `/meeting-note`,
`/livrable`) ou des commandes admin (`/help`, `/providers`, `/provider`, `/model`, `/keys`) ;
`/create-new-skill` permet de créer des skills personnalisés persistés par utilisateur
(`data/skills.json`). **Analyse d'images** : coller une image dans la zone de saisie ou référencer
une image d'un répertoire (`@image.png`), avec garde-fou de compatibilité vision. **Capacités des
modèles** (Chat, Embeddings, Rerank, Images, Video, Audio Speech, Audio Transcriptions, Vision)
affichées à la sélection dans l'assistant et le panneau de configuration. Backend : table curée
`backend/model_capabilities.py`, endpoints `GET /api/ai/model-capabilities` et `GET /api/ai/skills`,
support multimodal dans `backend/ai_chat.py` et `backend/bookslm_routes.py`. Tests :
`tests/test_model_capabilities.py` (16), `tests/test_skills.py` (16), `tests/test_ai_vision.py` (12)
+ extension `tests/test_ai_models.py` et `tests/frontend/ai.test.mjs` (+9). Détail :
[docs/features/ai-assistant-commands.md](./docs/features/ai-assistant-commands.md).
- **#77 Desktop — Signature des mises à jour Tauri** — la paire de clés `minisign`
de l'updater est générée et sa clé publique est embarquée dans
`desktop/tauri.conf.json` (`plugins.updater.pubkey`, remplace le placeholder) ;
`bundle.createUpdaterArtifacts: true` produit les fichiers `.sig` par artefact.
Le workflow `.gitea/workflows/desktop-build.yml` expose les secrets
`TAURI_SIGNING_PRIVATE_KEY` / `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` aux builds
Windows et Linux, avec repli automatique en build **non signé** si le secret est
absent (CI toujours verte). Procédure complète :
[docs/DEVELOPMENT_AND_RELEASES.md](./docs/DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri).
- **#77 Desktop — Manifeste de mise à jour `latest.json`** — nouveau
`scripts/updater_manifest.py` qui construit le document consommé par l'updater
Tauri (version, notes, `pub_date`, `platforms` Windows/Linux avec signature
`.sig` et URLs des assets). Il est généré automatiquement par
`scripts/publish_release.py` (écrit `desktop/latest.json`, l'ajoute aux assets
de la release, upload des `.sig`) et peut être lancé seul. L'endpoint de
l'updater (`desktop/tauri.conf.json`) pointe désormais sur le manifeste
versionné `.../raw/branch/main/desktop/latest.json`. Les builds locaux
(`build-windows.bat`, `build-linux.sh`) détectent automatiquement
`desktop/obsigate-updater.key` pour signer, ou désactivent les artefacts de
mise à jour s'il est absent. Tests : `tests/test_updater_manifest.py` (8).
- **#77 Desktop — Protocole de tests E2E manuels** — nouveau
[`docs/DESKTOP_E2E_CHECKLIST.md`](./docs/DESKTOP_E2E_CHECKLIST.md) : prérequis,
étapes et résultat attendu pour les 6 scénarios OS non automatisables
(installation, tray, notifications natives, association `.md`, auto-update,
désinstallation), avec emplacements des logs/config et procédure de signature
de l'updater Tauri.
- **#70 Recherche sémantique — Embeddings vectoriels** — la recherche comprend désormais le
**sens** de la requête en plus des mots-clés. **Embeddings** : chaque document est découpé en
chunks de 512 mots (recouvrement 64) puis vectorisé (384 dim) via `all-MiniLM-L6-v2`
(`sentence-transformers`), un endpoint `/embeddings` compatible OpenAI, ou un provider de repli
**sans dépendance** (hachage déterministe). **Stockage vectoriel** : `numpy`/`faiss`
(`IndexFlatIP`) si disponibles, sinon cosinus pur Python. **Recherche hybride** : fusion du
classement TF-IDF et du classement sémantique par **RRF** (Reciprocal Rank Fusion), avec
`semantic_score` par résultat. **Indexation incrémentale** branchée sur le watcher (un fichier
modifié régénère son embedding). **UI** : toggle « Recherche sémantique » (`~`, raccourci
`Alt+S`) dans la barre de résultats + affichage du score de similarité, clés i18n FR/EN.
Nouveau module `backend/semantic_search.py`, paramètre `semantic` sur
`/api/search/advanced`, dépendances **optionnelles** dans
`backend/requirements-semantic.txt`. Tests : `tests/test_semantic_search.py` (27) +
`tests/frontend/semantic-search.test.mjs` (4). Détail :
[docs/features/semantic-search.md](./docs/features/semantic-search.md).
- **#69 Éditeur mobile natif — Interface tactile optimisée** — refonte de l'expérience
d'édition sur téléphone/tablette. **Barre d'outils flottante** dans l'éditeur (gras, italique,
code, liste à puces, lien) opérant directement sur la sélection CodeMirror (ou le textarea de
secours), avec **toggle** (re-appui pour dé-formater) ; **bouton « Coller » persistant** qui
contourne la restriction presse-papiers d'iOS (repli sur un message d'aide si l'accès est
refusé). **Adaptation CodeMirror** : zoom par **pincement à deux doigts**, boutons A−/A+ et
**hauteur ajustable** via une poignée (valeurs persistées en `localStorage`). Côté lecture :
**raccourcis swipe** (gauche → liens entrants, droite → table des matières) et **mode lecture**
plein écran (masquage de l'en-tête, des barres latérales et de la barre mobile, API Fullscreen,
**swipe horizontal** pour passer au fichier suivant/précédent du même dossier). Nouveau module
`frontend/js/mobile-editor.js`, styles `frontend/style.css`, clés i18n FR/EN. Tests :
`tests/frontend/mobile-editor.test.mjs` (22 tests). Détail :
[docs/features/mobile-editor.md](./docs/features/mobile-editor.md).
- **#62 Collaboration temps réel — Édition simultanée** — plusieurs utilisateurs peuvent éditer le
même document markdown en même temps, façon Google Docs. **WebSocket** : nouvel endpoint
`/ws/collab/{vault}/{path}` (une *room* par fichier) qui relaie les changements instantanément ;
authentification manuelle (cookie `access_token` ou paramètre `token`) + `check_vault_access` et
`resolve_safe_path` par connexion. **Yjs/CRDT** côté client : `Y.Doc`/`Y.Text` fusionnent les
modifications concurrentes sans conflit ni perte. **Awareness** : curseurs distants colorés et
sélections dans CodeMirror, indicateur de présence (avatars + statut) dans l'en-tête de l'éditeur.
**Persistance serveur** : le dernier texte reçu est écrit sur disque avec un debounce de 2 s
(`backend/collab.py`). **Reconnexion** automatique avec backoff exponentiel et merge de l'état au
retour. Nouveau module frontend `frontend/js/collab.js` (provider + liaison Y.Text↔CodeMirror +
curseurs distants) et `backend/collab.py` (`CollabManager`). Tests : `tests/test_collab.py`
(17 tests, dont 5 clients simultanés) et `tests/frontend/collab.test.mjs`. Détail :
[docs/features/collaboration.md](./docs/features/collaboration.md).
- **#79 Phase F — Durcissement & documentation (rate limiting, redaction, OpenAPI/MCP, E2E)** —
clôture de la feature #79. **Rate limiting** par jeton et par outil
(`backend/tools/ratelimit.py`, fenêtre glissante ; identité = JTI du jeton sinon id/username ;
`OBSIGATE_TOOL_RATE_LIMIT`, `OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL`, `OBSIGATE_TOOL_RATE_WINDOW` ;
erreur `rate_limited`). **Quotas** `BOOKSLM_MAX_*` : `BOOKSLM_MAX_TOOL_CALLS` plafonne les appels
d'outils par run d'agent (`stopped="quota_exceeded"`) et `BOOKSLM_MAX_TOOL_READ_BYTES` plafonne
`read_file`. **Redaction** systématique de **tout** résultat d'outil avant retour au LLM
(`backend/tools/redaction.py`, récursif : diffs, extraits de recherche, lectures). **OpenAPI** :
tag `MCP` + injection du path `/mcp` (Streamable HTTP) dans le schéma ; nouveau
[`docs/MCP_GUIDE.md`](./docs/MCP_GUIDE.md) (config Claude Desktop/Cursor, tools/resources/prompts,
sécurité, variables, dépannage). Tests E2E : `tests/test_ai_e2e.py` (agent read→confirm→write,
quota, rate limit, redaction, flux MCP `read`→`propose`→`apply`→`resources/read`).
- **#79 Phase E — Serveur MCP (Streamable HTTP)** — nouvel endpoint `/mcp` exposant ObsiGate à des
clients MCP externes (Claude Desktop, Cursor…) via le SDK Python `mcp==1.9.4`. Les deux fronts
(assistant in-app et MCP) consomment la même couche d'outils. **Tools** : outils de lecture/recherche
directs ; outils d'écriture/destructifs en **two-step `propose_<tool>` / `apply_<tool>`** avec
**jeton JWT signé, usage unique et TTL** (`OBSIGATE_MCP_CONFIRMATION_TTL`, défaut 300 s) et
blacklist de JTI persistée (`data/mcp_used_tokens.json`, anti-rejeu). **Resources** :
`vault://<name>` et `vault://<name>/<path>` (lecture seule, secrets redactés). **Prompts** :
`summarize-directory`, `generate-note`, `find-related`. Auth `Authorization: Bearer <JWT>` →
`get_current_user` ; permissions et toggle destructif par vault (`aiDestructiveTools`) appliqués.
Nouveaux modules `backend/mcp/server.py` et `backend/mcp/confirmations.py` ; dépendances
`mcp==1.9.4` + `sse-starlette==2.1.3` (compatibles FastAPI 0.110 / starlette 0.37). Tests :
`tests/test_mcp.py` (13 tests).
- **#79 Phase D — Catalogue d'outils mutations + confirmations two-step** — 11 nouveaux outils IA
destructifs ou d'écriture exposés par la couche partagée (`backend/tools/service.py`) :
`create_file`, `create_directory`, `edit_file`, `append_to_file`, `rename_file`,
`rename_directory`, `move_path`, `replace_in_files`, `delete_file`, `delete_directory`,
`restore_backup`. Nouveau service réutilisable `backend/services/mutations.py` (anti
path-traversal, garde lecture seule, **backup automatique** avant écriture/suppression/restauration) ;
les routes `/api/file/{vault}` (POST/PATCH/DELETE), `/api/file/{vault}/save`, `/api/directory/{vault}`
(POST/PATCH/DELETE), `/api/move/{vault}`, `/api/file/{vault}/restore` et `/api/search/replace`
délèguent désormais à cette couche. `backend/services/backups.py` expose `create_backup`
(source unique, réutilisée par `main._backup_file`). Confirmations **two-step** : tout outil
`WRITE`/`DANGEROUS` déclenche `ToolConfirmationRequired` (carte Apply in-app, `propose`/`apply`
côté MCP en phase E). **Toggle par vault** `aiDestructiveTools` (défaut : activés) pour désactiver
delete/rename/move/replace. Tests : `tests/test_tools_mutations.py` (40 tests).
- **#79 Phase C — Catalogue d'outils lecture & recherche** — 10 nouveaux outils IA exposés par la
couche partagée (`backend/tools/service.py`) : `list_all_files`, `read_file_raw`, `get_backlinks`,
`list_backups`, `diff_backup`, `get_graph`, `search_advanced`, `search_paths`, `suggest_tags`,
`list_recent`. Nouveaux services réutilisables `backend/services/backups.py`, `graph.py`,
`recent.py` et extensions de `vaults.py` (`list_all_files`) / `search.py`
(`advanced_search_vaults`, `search_paths`). Les routes `/api/recent`, `/api/vault/{vault}/files`,
`/api/file/{vault}/backups`, `/api/file/{vault}/diff`, `/api/graph/{vault}`, `/api/tree-search`
et `/api/search/advanced` délèguent désormais à ces services (source unique de vérité).
Filtrage systématique par permissions vault ; secrets redactés sur `read_file`/`read_file_raw`.
Tests : `tests/test_tools.py` (+17 tests).
- **#79 Assistant IA — Outils (function calling) & serveur MCP — Phase 0 (couche d'outils partagée)** —
nouveau paquet `backend/tools/` : `context.py` (`ToolContext`), `registry.py` (décorateur `@tool`
+ schéma JSON), `schemas.py`, `service.py`, `audit.py` (journalisation JSONL des appels), façade
`backend/tools/api.py`. Services lecture/recherche livrés (`list_vaults`, `list_directory`,
`read_file`, `search_fulltext`, `list_tags`) ; permissions `check_vault_access` + `resolve_safe_path`
appliquées par outil. Tests : `tests/test_tools.py` (30 tests).
- **#79 Phase B (partielle) — function calling in-app** — abstraction tool-calling
provider-agnostique `backend/ai_chat.py` (`chat_completion`, `ToolCall`, `LLMResponse`, OpenAI-compat
+ Gemini) ; agent loop `backend/agent/loop.py` (boucle tool→résultat→tool, limite 10 itérations,
truncation) ; endpoint opt-in `POST /api/ai/bookslm/agent` (events SSE `tool`/`message`/`confirmation`) ;
fallback automatique en chat simple si le provider rejette les tools. Tests : `tests/test_agent_loop.py`,
`tests/test_ai_chat.py`, `tests/test_bookslm.py`.
- **#79 A2/B4/B5 — services partagés, SSE streaming & confirmations UI** — extraction de la logique
lecture/recherche dans `backend/services/` (`errors.py`, `paths.py`, `vaults.py`, `files.py`,
`search.py`) : routes REST et outils IA consomment la même source ; `ServiceError` mappée vers
`HTTPException` (routes) et `ToolError` (outils). `/api/ai/bookslm/chat` **streame réellement** les
tokens (`stream_completion`, OpenAI-compat + Gemini ; GZip ignoré pour les SSE BooksLM). Mode agent
côté UI (toggle, événements `tool`/`confirmation`, carte Apply + aperçu diff LCS) et reprise
`confirm`/`confirm_messages` de l'agent loop (confirmation one-shot). Tests : `tests/test_services.py`,
`tests/test_ai_chat.py`, `tests/test_agent_loop.py`, `tests/test_bookslm.py`.
- **#79 G — Sélection fournisseur/modèle par défaut** — `ai_default_provider` + `ai_default_models`
persistés dans `data/config.json`, rechargement à chaud dans `backend/ai.py`, sélecteurs
« Fournisseur par défaut » / « Modèle par défaut » dans `#cfg-ai`, i18n FR/EN.
- **#80 Assistant IA — Rendu Markdown, liens fichiers/paths & sessions** — réponses de l'assistant
rendues en **Markdown formaté** (titres, listes, tableaux, citations, code, emphase) via un
renderer auto-contenu ; les **fichiers et chemins** mentionnés deviennent des liens cliquables
(clic fichier → ouverture dans le viewer, clic répertoire → révélation/surlignage dans
l'arborescence, outils `open_file`/`reveal_in_tree` côté in-app) ; **gestion des sessions** par
contexte (historique consultable, rechargeable, supprimable) avec migration de l'ancienne
conversation unique. Tests : `tests/frontend/ai.test.mjs`.
### Modifié
- **Barre inférieure mobile — couleurs harmonisées** — les boutons « settings.search », « Recherche »
et « Onglets » adoptent la couleur d'accent (`--accent`) de l'icône et du libellé du bouton
« Commandes » (`frontend/style.css`), pour un rendu uniforme de la barre du bas.
- **#82 Assistant IA — sélecteurs Fournisseur/Modèle alignés à droite et réordonnés** — dans la
barre de l'assistant, le groupe de sélection est aligné sur le bord droit et l'ordre devient
**capacité du modèle (ⓘ) → fournisseur → modèle**. La bulle de capacités s'ouvre désormais vers
la droite (`left: 0`) pour ne pas déborder de la barre latérale. Tests JSDOM
(`tests/frontend/ai.test.mjs`).
- **`scripts/bump_version.sh` — synchronisation de la version desktop** — le script met
désormais à jour `desktop/tauri.conf.json`, `desktop/Cargo.toml` et `desktop/Cargo.lock`
(`obsigate-desktop`) avant de créer un commit de release `chore(release): vX.Y.Z` puis le
tag. `--push` pousse la branche **et** le tag ; `--no-files` conserve l'ancien comportement
(tag seul) et `--dry-run` prévisualise. Indispensable au bon fonctionnement de l'updater
Tauri (dont le manifeste reprend la version de `tauri.conf.json`).
- **`.gitea/workflows/desktop-build.yml` — déclenchement manuel uniquement** — le workflow
`Desktop Build` ne s'exécute plus sur `push` (aucun runner self-hosted `[windows/linux, desktop]`
enregistré) mais uniquement via `workflow_dispatch`. Le build Windows se fait en local
(`desktop/build-windows.bat`) ; le workflow reste disponible pour un futur runner.
- **Assistant IA (`frontend/js/bookslm.js`)** — l'ancien rendu Markdown minimal est remplacé par un
renderer bloc/inline complet ; l'en-tête expose un bouton « Historique des sessions » ; les badges
de sources ouvrent désormais réellement le fichier (événement `obsigate:open-file`).
- **Assistant IA (`frontend/js/bookslm.js`)** — bouton « mode agent » dans l'en-tête (persisté en
`localStorage`) : l'envoi bascule vers `/api/ai/bookslm/agent`, affiche la trace des appels d'outils
et les cartes de confirmation (Apply + aperçu diff) ; `/chat` reste le défaut. i18n FR/EN.
### Modifié
- **#82 Assistant IA — barre latérale épurée & suivi visuel des requêtes** — la section
« Fournisseur & modèle » perd ses intitulés (titre de section et libellé « Fournisseur : »)
pour rester discrète ; la description du contexte Général n'est plus affichée (bandeau de
statut masqué, description accessible au **survol** de l'en-tête) ; le placeholder
« Posez une question sur ces documents… » est retiré ; l'indice clavier
« Entrée pour envoyer · Ctrl+Entrée… » est retiré ; le bouton « Envoyer » devient un **emoji
compact** (✈️). Les sélecteurs Fournisseur/Modèle adoptent la **taille des autres contrôles du
site** (police 0,8 rem, hauteur 34 px, rayon 6 px) au lieu d'être plus fins. Un **indicateur
d'activité** apparaît pendant le traitement et détaille le workflow : envoi, réception de la
réponse, appels d'outils (`Outil : X…`), attente de confirmation, succès ou échec. Le menu
contextuel de la **racine d'une vault** expose aussi « 🧠 BooksLM », comme les répertoires.
i18n FR/EN (`ai.activity_*`, `ai.composer_label`). Tests JSDOM
(`tests/frontend/ai.test.mjs` +2).
### Corrigé
- **BUG-017 — Accueil mobile : tuiles « Favoris » / « Récents » mal dimensionnées** — les cartes
de tableau de bord débordaient horizontalement à cause d'un titre/chemin `nowrap` : ajout de
`min-width: 0` et `overflow: hidden` sur `.dashboard-card` (et ses en-têtes/pieds) pour que la
grille mobile (1 colonne) respecte la largeur du viewport. Les tuiles « Partagés » étaient déjà
conformes.
- **BUG-018 — Éditeur mobile : boutons « Annuler » / « Sauvegarder » inaccessibles** — l'en-tête
cumulait marque, titre, nom de fichier et actions, poussant les boutons hors écran. Sur mobile,
les éléments secondaires (marque, séparateur, espaceur, présence) sont masqués, les champs
titre/nom de fichier deviennent compressibles (`flex`, `min-width: 0`) et le libellé « Saved »
est masqué (le point d'état reste). La barre d'outils IA reçoit des cibles tactiles plus grandes.
- **BUG-019 — Éditeur mobile : barre d'outils masquée par le clavier** — le ruban est désormais
ancré via `window.visualViewport` (`--kb-offset`) pour rester juste au-dessus du clavier, quel
que soit le défilement, et le scroller CodeMirror reçoit un `padding-bottom` pour ne pas cacher
le curseur.
- **BUG-020 — Éditeur mobile : double barre de défilement** — `.editor-body` passe en
`overflow: hidden` + colonne flex ; seul le scroller CodeMirror défile.
- **BUG-016 — Mobile : le bouton « mode lecture » recouvre le bouton d'envoi de l'assistant** —
le bouton flottant `📖` (`#me-reading-btn`, `z-index: 890`) passait au-dessus du panneau
assistant plein écran sur mobile (`z-index: 100`) et masquait le bouton d'envoi `✈️`. Il
n'apparaît désormais que lorsqu'un **fichier est ouvert** (`state.currentPath`) **et** que le
panneau assistant est fermé ; sa visibilité est resynchronisée à chaque mutation de
`#content-area` et sur les événements `bookslm:opened`/`bookslm:closed`. Tests JSDOM
(`tests/frontend/mobile-editor.test.mjs` +2) et E2E mis à jour.
- **BUG-015 — Assistant IA : sélection des menus `/` et `@` invisible au clavier** — l'élément
actif utilisait `background: var(--surface2)`, or `--surface2` est **identique** à
`--bg-primary` (fond du menu) dans le thème sombre par défaut : la ligne sélectionnée par les
flèches ↑/↓ ne se distinguait pas. L'état actif (et le survol) utilise désormais
`--bg-hover` avec une **barre d'accent** à gauche (`box-shadow: inset 3px 0 0 var(--accent)`),
pour les menus de commandes/mentions **et** la liste de modèles. Test JSDOM mis à jour.
- **BUG-014 — Assistant IA : navigation clavier ↑/↓ des menus inopérante** — la navigation était
liée au `keydown` du champ de saisie : dès que le focus quittait la zone de texte (clic sur le
menu, bouton Envoyer…), les flèches ne faisaient plus défiler les items. La gestion est déplacée
au niveau du **panneau en phase de capture**, ce qui fonctionne quel que soit l'élément focalisé
dans la barre latérale. Tests JSDOM (`tests/frontend/ai.test.mjs` +1).
- **BUG-013 — Assistant IA : « Aucun vault actif pour ouvrir ce lien »** — les liens
fichiers/répertoires des réponses utilisaient `_resolveVault()` seul, nul en mode Général sans
document ouvert. `_openFileLink()` et `_revealPath()` utilisent désormais `_activeVault()` qui
retombe sur le vault de contexte, le vault sélectionné dans la barre latérale, puis le premier
vault disponible. Tests JSDOM (`tests/frontend/ai.test.mjs` +1).
- **Assistant IA — caches obsolètes** — `SW_VERSION` porté à **v6** et migration de purge élargie à
tous les caches `obsigate-*` (clé `obsigate-sw-migration` → `v3`), afin que les clients bloqués sur
une ancienne version récupèrent bien le menu `@` et les correctifs.
- **BUG-012 — Assistant IA : la commande `@` n'affichait aucun menu en mode Général** — sans vault
résolu, la détection `@` ne listait rien (menu masqué) et le backend ignorait ensuite le
contexte. Le menu est désormais **toujours rendu** et `_mentionVault()` retombe sur le vault de
contexte de la barre latérale, puis sur le premier vault disponible, avant de basculer sur
`vault=all` pour les recherches. Tests JSDOM (`tests/frontend/ai.test.mjs` +1).
- **BUG-011 — Assistant IA : noms de modèles illisibles dans la liste déroulante** — la liste
déroulante des modèles s'ouvrait alignée à gauche (`left: 0`) et pouvait dépasser le bord droit
de la barre latérale (ancrée à droite), rendant les noms tronqués/illisibles ; elle est
désormais **alignée à droite** du déclencheur, large de 340 px (bornée à `100vw - 24px`), et les
noms **reviennent à la ligne** (`overflow-wrap: anywhere`, police 0,78 rem) avec l'info-bulle
`title` complète. Tests JSDOM (`tests/frontend/ai.test.mjs`).
- **BUG-010 — Assistant IA : la commande `@` n'ajoutait pas le contexte en mode Général** — les
fichiers/répertoires choisis via `@` étaient bien ajoutés sous forme de puces, mais le **vault
n'était pas transmis** : en mode Général (`_vault` nul), la requête partait sans `vault` et le
backend ignorait le contexte ad-hoc. La sélection capture maintenant le `vault` renvoyé par
`/api/tree-search`, `_contextVault()` le propage aux requêtes `/context` et `/chat`, et les
recherches suivantes restent dans ce vault. Tests JSDOM (`tests/frontend/ai.test.mjs`).
- **BUG-009 — Assistant IA : liste de modèles corrompue et clés i18n brutes** — deux causes :
(1) les fichiers de locale n'étant pas *content-hashed*, un `fr.json` en cache HTTP affichait
les clés brutes (`ai.model_search`, `mobile_editor.*`, …) ; le chargement i18n utilise désormais
`cache: 'no-store'` et `SW_VERSION` est incrémenté pour purger les anciens caches du service
worker. (2) la liste de modèles (OpenRouter = plusieurs centaines d'entrées) pouvait se retrouver
non stylée si la feuille de style était en cache ; le picker applique maintenant les styles
critiques **en ligne** (popover, liste en colonne, options `display:block`), plafonne le rendu à
200 entrées avec un indicateur « … N autres — affinez la recherche », et la recherche filtre le
reste. Tests JSDOM (`tests/frontend/ai.test.mjs` +1).
- **BUG-007 — Assistant IA : menus `/` et `@` (navigation ↑/↓ et filtrage accentué)** — la
détection des commandes/mentions et le filtrage des skills utilisaient des motifs ASCII
(`[a-z0-9-]`, `\w`) : dès qu'un caractère accentué était saisi (`/résumé`, `@café`), le menu se
fermait. Les motifs sont désormais **Unicode** (`\p{L}`) et la recherche est **insensible aux
accents** (normalisation NFD). La navigation clavier est fiabilisée par un **jeton de séquence**
qui ignore les rendus asynchrones obsolètes (frappe rapide) et fait défiler l'élément actif dans
la vue (`scrollIntoView`). Tests JSDOM (`tests/frontend/ai.test.mjs` +3).
- **BUG-008 — Assistant IA : commande `@` et contexte ad-hoc en mode Général** — deux causes :
(1) le backend ignorait les fichiers/répertoires ad-hoc en mode Général car `_resolve_system_prompt`
ne résolvait pas de vault (`backend/bookslm_routes.py`) ; le vault optionnel est maintenant résolu
dès que du contexte `@` est présent. (2) sans vault courant, la recherche de mention échouait ;
elle bascule désormais sur `vault=all`. Tests backend (`tests/test_bookslm.py` +2) et frontend.
- **#77 Desktop — bannière de premier lancement réaffichée à chaque démarrage** —
l'état du wizard « Choisissez votre vault » n'était mémorisé que dans le
`localStorage` de la webview et le paramètre `hasVaultPath` n'était jamais
renseigné. Ajout d'un booléen persistant `wizard_done` dans la config desktop
(`desktop/src/main.rs`, commandes `get_wizard_state` / `complete_wizard`,
rétro-compatible via `#[serde(default)]`) : la bannière ne réapparaît plus après
un choix de dossier ou un clic « Plus tard », même si le stockage webview est
vidé. Tests Rust (`desktop/src/main.rs`) et JSDOM
(`tests/frontend/desktop.test.mjs`) ajoutés.
- **Build Docker — « No space left » lors de `apt-get install`** — le stage *builder*
installait inutilement `libpango-1.0-0`, `libpangocairo-1.0-0` et `shared-mime-info`
(libs runtime de WeasyPrint, sans usage à la compilation), dupliquant les téléchargements
apt. Ces paquets sont retirés du builder et `apt-get clean` est ajouté dans les deux
stages pour purger les `.deb` conservés par défaut sous Debian trixie, réduisant
l'empreinte disque et le temps de build. En complément, purge du cache de build
(`docker builder prune`) ayant libéré l'espace nécessaire.
- **BUG-005 — Chargement mobile incomplet derrière Cloudflare (`og.dracodev.net`)** —
le service worker utilisait une stratégie **cache-first avec un nom de cache fixe**
(`obsigate-v1`) et un précache de chemins erronés (`/frontend/js/…`) : le Cache Storage
du SW (distinct du cache navigateur/Cloudflare) servait un ancien build indéfiniment,
même après purge. Le middleware renvoyait en plus `Cache-Control: public, max-age=31536000,
immutable` sur des assets **non fingerprintés**. Correctifs : `frontend/sw.js` réécrit
(**network-first** pour HTML/JS/CSS, caches versionnés `SW_VERSION`, précache corrigé
`/static/js/…`, `skipWaiting`/`clients.claim`) ; migration ponctuelle `localStorage`
(suppression des anciens caches) en remplacement du kill-switch de session ; en-têtes
`no-cache` sur `/static`, `index.html`, `manifest.json` et pages HTML ; rechargement unique
sur `controllerchange`. Tests : `tests/frontend/sw.test.mjs` + `TestStaticCaching`.
Guide Cloudflare : [docs/PWA_GUIDE.md](./docs/PWA_GUIDE.md).
- **Typage backend (mypy) — 33 erreurs corrigées** (`backend/main.py`, `indexer.py`,
`auth/router.py`, `pdf_reader.py`, `export.py`, `bookslm_routes.py`) : annotations de types,
gardes `None` sur `get_user()`, `PdfReader: Any` et import `PROVIDERS` manquant dans `main.py`
(bug latent : le modèle par défaut n'était jamais prépendé à la liste des modèles live).
L'étape `mypy` du CI devient **bloquante** (elle était en mode advisory).
- **README** — lien « Contributing » corrigé vers `docs/CONTRIBUTING.md` (était cassé vers la
racine) ; arbre du projet mis à jour.
- **#79 façade `backend/tools/api.py`** — gestion des namespace packages et du `__init__` ignoré
lors du chargement des modules d'outils.
### Documentation
- **Guide d'architecture IA** — `docs/AI_ARCHITECTURE_GUIDE.md` (architecture, catalogue d'outils,
sécurité, phases).
- **Refonte de la documentation** — la [Roadmap](./docs/ROADMAP.md) ne contient plus que le travail
à venir + un index compact ; les fonctionnalités livrées sont archivées dans
`docs/archive/COMPLETED_v1-v2.md` et documentées par feature dans `docs/features/`.
- **Méthode de livraison unifiée** — nouveau [`docs/DELIVERY_WORKFLOW.md`](./docs/DELIVERY_WORKFLOW.md)
(Definition of Done : tests, documentation, commit, push, CI) et [`AGENTS.md`](./AGENTS.md)
(instructions obligatoires lues à chaque session). Référencés depuis la Roadmap, CONTRIBUTING
et ISSUES_TODOLIST.
---
## [2.2.1] — 2026-09-11
### Ajouté
- **Mobile — assistant AI central** — bouton assistant dans la barre du bas (commit 50b823e).
### Corrigé
- **Version affichée dans l'UI** — affichait `2.1.0` au lieu de `2.2.0` (commit 84bda90).
- **Mobile** — boîte d'édition de l'assistant relevée au-dessus de la barre du bas (commit f307ecb).
---
## [2.2.0] — 2026-09-11
### Ajouté
- **Assistant AI contextuel (sidebar)** — l'assistant s'adapte désormais au contexte d'ouverture :
- **Répertoire** (`directory`) : ouvert depuis le menu contextuel d'un dossier — le contexte est
le dossier et son contenu (mode historique BooksLM).
- **Documents** (`documents`) : ouvert via le bouton flottant quand un ou plusieurs documents
sont ouverts — le contexte est la liste des fichiers ouverts (tous onglets/panneaux).
- **Général** (`general`) : ouvert via le bouton flottant quand aucun document n'est ouvert —
l'assistant répond sur l'application et peut proposer des actions (créer un fichier/dossier)
via un bloc `obsigate-action` rendu en carte avec bouton « Appliquer » (aucune exécution
automatique).
- Le contexte est affiché dans le header de la sidebar (icône, titre, sous-titre) et les
suggestions de questions s'adaptent au mode.
- Backend : `/api/ai/bookslm/context` et `/chat` acceptent `mode` (`directory`/`documents`/
`general`) et `context_files` ; `collect_files_context()`, `empty_context()` et
`build_general_system_prompt()` dans `backend/bookslm.py` ; le mode documents dégrade
proprement vers le mode général si aucun fichier n'est lisible.
- Tests : `tests/test_bookslm.py` (+13 tests modes/documents/général) et
`tests/frontend/ai.test.mjs` (+8 tests détection de contexte, clavier, actions, header).
- **#72 API publique documentée — OpenAPI 3.1 enrichie** — documentation interactive complète
de l'API REST, générée automatiquement et enrichie.
- Backend `backend/openapi_docs.py` : métadonnées de tags (18 catégories : System, Auth, Files,
PDF, Vaults, Search, Bookmarks, Backups, Export, AI, BooksLM, Sharing, Webhooks, Conflicts,
Admin, Plugins, Push, Frontend), assignation automatique des tags par préfixe de route
(normalisation des tags des routeurs), schémas de sécurité (`bearerAuth`, `cookieAuth`),
exemples de requête/réponse sur les endpoints clés, réponses d'erreur documentées
(401/403/404/422/500), serveur et `externalDocs`.
- `backend/schemas.py` : nouveaux `response_model` Pydantic pour les endpoints qui n'en avaient
pas (recent, bookmarks, saved searches, backups, diff/restore/backlinks, pdf/info, replace,
vaults, attachments, settings, config, AI keys/models, diagnostics, dashboard, webhooks,
shares, conflicts, BooksLM context, AI status).
- `backend/main.py` : `FastAPI(...)` enrichi (description Markdown, contact, licence, tags) et
override `app.openapi` pour post-traiter le schéma ; page de documentation `/api` (HTML
autonome listant les endpoints groupés par catégorie, liens vers `/docs`, `/redoc`,
`/openapi.json`).
- Frontend : entrée « API » dans le menu d'options (i18n FR/EN) ouvrant Swagger UI.
- Tests : `tests/test_openapi.py` (58 tests — version 3.1, tags, sécurité, erreurs, exemples,
schémas de réponse, pages `/api`, `/docs`, `/redoc`, `/openapi.json`).
- **#61 Plugins système au complet** — système de plugins permettant d'étendre ObsiGate
(renderers personnalisés, filtres de recherche, actions d'éditeur), sandboxé pour la sécurité.
- Backend `backend/plugins.py` : validation du manifest `plugin.json` (name regex lowercase,
semver, hooks/permissions autorisés), stockage par vault `<vault>/.obsigate-plugins/`, lifecycle
complet (install/uninstall/enable/disable via marker `.disabled`), validation ZIP à l'upload
(anti path-traversal, max 100 fichiers, 500KB/fichier, 5MB upload), 9 endpoints `/api/plugins/*`
(list/get/hooks/code/template + install/uninstall/enable/disable admin-gated), template API,
scan au démarrage par vault (`get_plugin_registry().scan_vault`).
- Frontend `frontend/js/plugins.js` : PluginManager (install/uninstall/enable/disable/view-code),
sandbox d'exécution via Web Worker (code chargé par blob URL, protocole `postMessage` structuré,
isolation DOM/localStorage/network selon permissions), hooks dispatch
(`executeHook`, `onFileRender`, `onSearchFilter`, `onEditorAction`, `onSidebarItem`,
`onFileCreate`, `onFileDelete`, `onVaultMount`), UI Settings > Plugins.
- Sécurité : manifest de permissions (`read_files`, `write_files`, `network_request`, …
restreint), CSP stricte sans `importScripts`, limites de taille.
- Tests : `tests/test_plugins.py` (44 tests — validation manifest, lifecycle manager,
validation ZIP/directory, démarrage scan) + `tests/frontend/plugins.test.mjs`
(21 tests JSDOM — protocole sandbox Worker, isolation DOM, lifecycle mirror).
- CI : job lint ajoute `node plugins.test.mjs` aux tests JSDOM.
- Docs : `docs/PLUGINS.md` (manifest, hooks, permissions, modèle de sécurité, API, guide).
- **#78 Excalidraw — finitions** : décompression lz-string du format plugin Obsidian,
création depuis la modale et le menu contextuel, extraction du texte pour la recherche
(B5), support `.excalidraw.md` (commit ac16fc1 + correctifs associés).
- **#67 Notifications web (Push API)** : `backend/push.py`, `POST /api/push/subscribe`,
clés VAPID dans `config.json`, toggle par vault, payload (fichier/vault/action) et
ouverture du fichier au clic (commit ac16fc1).
- **#68 Health check enrichi** : `GET /api/health/detailed` (admin-gated) exposant index,
mémoire (RSS/heap), uptime, connexions SSE, backups et espace disque (commit ac16fc1).
- **#77 Desktop — jumplist vaults récents** dans le menu Démarrer (commit ac16fc1).
- **#75 Split View — matrice E2E complète (37 tests)** : 16 tests `split-view.spec.js`
(raccourcis, drag & drop, persistance, navigation clavier) + 21 tests
`split-view-matrix.spec.js` couvrant 100% des cas d'ouverture/fermeture.
### Documentation
- **#78 Excalidraw — doc utilisateur finalisée** : `.excalidraw` / `.excalidraw.md` ajoutés aux
formats supportés (README FR/EN), nouvelle section « Diagrammes Excalidraw » dans le guide
intégré (i18n FR/EN) et note de compatibilité avec le plugin Obsidian Excalidraw.
- **Roadmap** : #78 marqué terminé (B5 extraction texte, C8 menu contextuel, F3 E2E
`tests/e2e/excalidraw.spec.js`, H1-H3 docs) ; F2 non retenu. Points optionnels #61
(dépôt communautaire de plugins) et #74 (D2/E4/H2/I2) explicitement marqués non retenus.
### Corrigé
- **Assistant AI — sidebar qui ne s'ouvrait plus & erreur de contexte** :
- Le bouton flottant passait le **chemin d'un fichier** comme répertoire de contexte, d'où
l'erreur « Aucun fichier markdown trouvé dans ce dossier » à l'ouverture sur un document.
Le contexte est maintenant détecté automatiquement (documents ouverts → mode documents,
sinon mode général).
- Un panneau précédemment masqué (`obsigate-bookslm-hidden=true` persisté en localStorage)
restait caché lors d'une ouverture explicite → l'ouverture force désormais l'affichage.
- **Ergonomie du header** : le sélecteur fournisseur/modèle était sur la même ligne que les
actions et masquait ces dernières ; il est déplacé sur une seconde ligne dédiée (toolbar)
pour que « nouvelle conversation / export / plein écran / fermer » restent toujours visibles.
- **Clavier de la boîte de chat** : `Entrée` envoie la requête, `Ctrl+Entrée` insère un saut
de ligne (au lieu de l'inverse), avec un texte d'aide sous le champ.
- **Outil AI de l'UI — fiabilisation** :
- **BooksLM (chat IA par répertoire)** : les requêtes `context`/`chat` n'envoyaient pas le
jeton d'authentification (échec 401 quand l'auth est activée) et la réponse SSE était
mal parsée (le backend émet `{token: ...}`, le front lisait `data.content`) → les réponses
n'apparaissaient jamais. Le message utilisateur envoyé était le placeholder assistant vide
au lieu du texte saisi. Corrigé : requêtes authentifiées via `AuthManager` (retry 401),
parsing SSE `event:`/`data:` avec support `token`/`error`, historique construit avant
l'ajout du message courant, barre de progression basée sur `max_total_chars`.
- **AI Editor** : le raccourci `Ctrl/Cmd+J` (complétion inline) était re-lié à chaque
ouverture de l'éditeur (N listeners → N requêtes) ; un seul listener global est désormais
posé. Les libellés de la barre d'outils AI étaient codés en dur (FR/EN mélangés) → i18n
complet. La réécriture personnalisée utilisait `window.prompt()` → remplacée par une modale
accessible (Échap annule, Ctrl/Cmd+Entrée valide).
- **Recherche cassée (régression plugins)** : `onSearchFilter()` était appelée avec 2 arguments
au lieu de 3 (`frontend/js/search.js`), renvoyait `undefined`, faisait échouer le rendu des
résultats et basculait silencieusement sur la recherche hors-ligne (0 résultat). Corrigé →
la recherche full-text et l'autocomplétion refonctionnent (20 tests E2E).
- **`.excalidraw.md` corrompu à l'ouverture** : Excalidraw émet `onChange` au montage, ce qui
déclenchait un auto-save 2 s plus tard réécrivant le fichier au format JSON brut et détruisant
le format plugin Obsidian (`frontend/excalidraw-editor.html` + `frontend/js/excalidraw-viewer.js`).
Les changements initiaux sont désormais ignorés et le format `frontmatter + compressed-json`
est préservé à la sauvegarde.
- **Splash / tests E2E** : `window.__OBSIGATE_BOOTED` était posé avant la fin de `init()` — le
splash disparaissait trop tôt et les tests E2E interagissaient avant que les handlers soient
liés. Le flag est maintenant posé après résolution de `init()` (`frontend/js/app.js`), et
`goHome()`/`login()` attendent ce signal.
- **Tests E2E Excalidraw** : réécriture de `tests/e2e/excalidraw.spec.js` avec les sélecteurs
réels (modale `.obsigate-modal` + `#file-ext-select`/`#file-name-input`, menu contextuel,
`canvas.first()` pour les deux couches canvas).
---
## [2.1.0] — 2026-09-08
### Ajouté
- **#74 Support PDF au complet** — `GET /api/file/{vault}/pdf/info` (métadonnées sans
contenu), streaming avec HTTP Range / 206 Partial Content (rendu progressif des gros PDF),
config `OBSIGATE_PDF_MAX_SIZE_MB` (50) + `OBSIGATE_PDF_EXTRACT_TIMEOUT` (30s).
16 tests supplémentaires (26 au total dans `test_pdf.py`).
- **#64 MFA — WebAuthn** (second facteur en plus du TOTP) : enregistrement de clés de
sécurité / biométrie (Windows Hello, Touch ID), assertion au login, gestion des clés dans
« Sécurité du compte », codes de récupération émis à l'activation. Nouveau module
`backend/auth/webauthn_mfa.py` (lib `webauthn==2.6.0`, challenges in-memory TTL 180s à
usage unique). 10 tests avec authentificateur virtuel (CBOR réel, ECDSA P-256).
i18n FR/EN (13 clés). Config : `OBSIGATE_WEBAUTHN_RP_ID` / `_RP_NAME` / `_ORIGINS`.
- **#77 Desktop — port auto-increment** : `pick_free_port()` scanne 17890..17899 si le port
est occupé (2 instances côte à côte possibles) + 3 tests Rust (19 au total côté desktop).
- Docs : sections PDF README FR/EN (Range, /pdf/info), variables env, guide WebAuthn.
- **#75 Split View — matrice E2E complète (37 tests)** : 16 tests `split-view.spec.js` (raccourcis,
drag & drop, persistance, navigation clavier) + 21 tests `split-view-matrix.spec.js` couvrant
100% des cas d'ouverture/fermeture : boutons ⊞→/⊞↓, menu contextuel (diviser bas, fermer, fermer
les autres/à droite/tout, fermer le panneau, flyout « Déplacer vers… »), palette (diviser, focus
suivant/précédent, reset), fermeture par X/double-clic/molette/Ctrl+W, auto-collapse d'un panneau
vidé, plafond 4 panneaux, drag-to-split bas, pas de doublon à la réouverture.
- Fix `scripts/run-e2e-local.sh` : chemins Windows via `cygpath` (les env vars MSYS n'étaient pas
converties → vaults vides, les 16 tests split-view échouaient en local alors qu'ils passaient
en CI).
### Corrigé
- **PDF stream 500** : `api_pdf_stream` plantait systématiquement (`NameError: current_user`
non injecté — endpoint jamais couvert par un test). Désormais authentifié + testé.
- **Indexation incrémentale des PDF** : le chemin watcher (`_index_single_file_sync`) lisait
les PDFs en `read_text()` → contenu garbage indexé. Utilise maintenant `extract_pdf_text()`
comme le scan complet.
---
## [2.0.0] — 2026-08-25
> ObsiGate passe à **2.0.0** avec sa **version bureau native** et de nombreuses fonctionnalités
> de productivité (branch main, 341 commits après le tag v1.8.0).
### Ajouté
- **Application bureau native (Tauri 2)** — Porte d'entrée desktop pour vaults Obsidian (`desktop/`)
- **Dashboard administrateur (#71, backend)** — 4 endpoints admin-gated pour monitoring serveur
- `GET /api/admin/stats` — CPU/RAM/Disk/Uptime via psutil
- `GET /api/admin/audit` — 500 dernières entrées d'audit avec filtres `user`/`action`/`limit`/`offset`
- `GET /api/admin/backup-stats` — compte + taille + age par vault
- `GET /api/admin/stream` — Server-Sent Events qui push les stats toutes les 5s
- Nouveau module `backend/admin.py` + 13 tests pytest
- **Tests JSDOM PaneManager (#75, sous-tâche I2)** — 9 tests d'intégration frontend via JSDOM 30
- Couvre : factory, init, open/activate/close, isolation entre panes, drag/drop smoke
- Wired dans le CI Gitea (`.gitea/workflows/ci.yml`)
- Shell Rust + Tauri 2 (tray icon, notifications OS, auto-update depuis Gitea)
- Backend Python embarqué (python-embed) avec health check, démarrage ~2s
- Sélecteur de dossiers natif, config persistante des vaults (type VAULT vs DIR)
- Fenêtre console masquée, arrêt propre via tray, associations de fichiers `.md`
- Packages : Windows MSI + NSIS, Linux .deb + AppImage
- Pipeline CI/CD Desktop (`.gitea/workflows/desktop-build.yml`)
- Thème système auto (clair/sombre) + RAM réduite (~80 MB au repos)
- Écran de chargement inline (staging backend/frontend pour le bundle desktop)
- **Internationalisation FR/EN complète** — 1357 clés i18n, préférence serveur, guide entièrement traduit
- **Vue multi-panneaux (Split View #75)** — PaneManager + TabManager par panneau
- Split droite/bas, drag & drop cross-pane, drag-to-split (bord droit/bas)
- Boutons de split, navigation clavier, fermeture auto des panneaux vides
- **Mermaid.js** — Rendu de diagrammes (22 templates, thème dark synchronisé)
- Header bar Code/Preview, zoom, export SVG + PNG, plein écran / focus 480px
- Pré-processeur Obsidian (`[[liens]]`, `![[img]]`, `==highlight==`)
- **Gestionnaire de Backups complet** — Page, filtres, purge, restauration
- Diff viewer (unifié + côte à côte), preview 100 Ko, compression gzip
- Backup automatique, auto-nettoyage (max_backups_per_file)
- **Support Excalidraw (#78)** — Fichiers `.excalidraw` / `.excalidraw.md`
- Décompression lz-string, création depuis modale/menu contextuel
- **Éditeur Forge + autocomplétion IA** — Auto-height, Mermaid QI, ghost text
- Autocomplétion Ollama (qwen2.5-coder), Alt+/, auto-continuation listes/checkboxes
- **Support PDF + multi-format** — PDF avec TOC, images/CSV/JSON/TXT
- **15 thèmes clair/sombre (#65)** — Système de thèmes configurable
- **Mode hors-ligne PWA (#59)** — Badge hors-ligne + gestion des conflits
- **Tests E2E Playwright (#58)** — Suite desktop (44 tests) + CI
### Modifié
- `desktop/` — Nouveau : application Tauri (Cargo.toml, build.rs, `src/main.rs`, scripts build)
- `frontend/` — i18n, split view, Mermaid, Excalidraw, Forge, thèmes
- `backend/` — Endpoints backups, Excalidraw, multi-format, versioning git
- `docs/ROADMAP.md` — Feuille de route passée à 2.0.0-dev, items #75/#78 complétés
---
## [1.8.0] — 2026-06-04
### Ajouté
- **Palette de commandes** — Navigation rapide fichiers + commandes
- Raccourcis : `Ctrl+P` fichiers, `Ctrl+Alt+Espace` commandes, `Ctrl+Shift+Espace`
- Navigateur de dossiers : créer/supprimer fichiers/dossiers par sélection
- Opérateurs `+include` / `-exclude` dans la recherche globale
- 6 nouvelles commandes : créer/supprimer fichiers/dossiers, éditer, pop-out
- **Barre d'outils mobile de bas de page** — Type Obsidian 📱
- Explorateur, recherche, palette, changement d'onglet
- Indicateur de vault contextuel sous le header, pull-to-refresh
- **Diff viewer des backups** — Comparaison unifiée + côte à côte, restauration
- **Indicateur AI Actif** — Badge/statut IA dans le header (lecture runtime des clés)
- **Page d'accueil de vault** — Liste récursive des fichiers par date
- **Drag & drop de fichiers** dans l'arborescence
- **Guide d'aide enrichi** — Recherche, scroll spy, slide-over mobile, section webhooks
- **Indexation inversée non-bloquante** (`run_in_executor`)
- **Infrastructure de tests** — Couverture pytest portée à 70% (+109 tests)
### Modifié
- `frontend/` — Palette, mobile toolbar, aide, AI indicator, vault home
- `backend/` — Indexation non-bloquante, statut IA, diff/restauration backups
- `docs/` — Guide d'utilisation enrichi (navigation clavier), CHANGELOG/README/ROADMAP
### Corrigé
- Correctifs UI mobile, palette, aide et AI toolbar (thème clair, menu clipping)
---
## [1.7.0] — 2026-05-30
### Ajouté
- **AI Editor Toolbar** — Barre d'outils IA intégrée à l'éditeur CodeMirror
- Menus dropdown : Edit (improve, fix spelling, shorter, longer, simplify), Tone (professional, casual), Translate (multi-langue), Generate (explain, summarize, continue), Custom Rewrite, Toolbox (to list, to table, frontmatter, to canvas)
- Multi-provider : DeepSeek / OpenRouter / Gemini, configurable via `.env`
- 16 endpoints REST : `POST /api/ai/{edit|tone|translate|generate|rewrite|toolbox}`
- Auto-save silencieux (2s debounce) avec feedback visuel toast
- Cache toolbar si pas de clé API configurée
- Messages d'erreur clairs (401→clé invalide, etc.)
- **Graph view — Améliorations majeures**
- Header flat design 2 lignes responsives (titre flexible + recherche dédiée + barre statut)
- Navigation historique ← → + ↑ Parent
- Sticky panels : tuile info (top-left/right) avec métadonnées, preview pleine hauteur (droite)
- Ctrl+survol → aperçu contenu formaté (via `/raw`) dans tooltip
- Filtre par tag dans la vue graphe
- Boutons close ✕ sur les panneaux
- Fond hover foncé unifié
### Modifié
- **backends/ai.py** — Nouveau module IA backend
- **backends/ai_routes.py** — Routes API pour l'éditeur IA
- **frontend/js/ai.js** — Module frontend de la toolbar IA (404 lignes)
- **frontend/js/graph.js** — Refonte complète : sticky panels, nav historique, aperçu markdown
---
## [1.6.0] — 2026-05-29
### Ajouté
- **Vue graphe des wikilinks** — Canvas force-directed
- Phase 1 : Full-vault view, filtre par tag, backlinks entrants, métriques de nœud
- Phase 2 : Recherche visuelle, tooltips survol, couleurs adaptatives, slider profondeur (0-3)
- Phase 3 : Filtre par type (dossier/fichier/.md/autre), mode focus, plein écran, export PNG
- Phase 4 : Barnes-Hut O(n log n), cache graphe
- **sortedcontainers** → O(log n) insert/remove dans l'index inversé
- **CI/CD Pipeline complet** — Gitea Actions
- Ruff linting (0 erreur)
- Mypy type checking (0 erreur)
- Pytest — 175 tests, 49% coverage
- Bandit SAST — scan de sécurité
- Pip-audit — vulnérabilités dépendances
- Docker build — vérification image
- Coverage artifact — uploadé via `actions/upload-artifact@v3`
- Runner Gitea auto-hébergé (`gitea/act_runner:0.2.11` sur Alpine/Proxmox)
### Modifié
- `backend/search.py` — Remplacement bisect → SortedList pour le vocabulaire
- `.gitea/workflows/ci.yml` — Pipeline CI/CD complet
### Corrigé
- Résolution de 28 erreurs mypy pré-existantes
- Fix lint ruff + faux positifs bandit + pip-audit non-bloquant
---
## [1.5.1] — 2026-05-28
### Ajouté
- **Split `app.js` (8 875 lignes) en 16 modules ES**
- Modules : `state.js`, `utils.js`, `auth.js`, `ui.js` (76KB), `viewer.js` (47KB), `sidebar.js`, `search.js` (45KB), `config.js`, `dashboard.js`, `graph.js` (33KB), `sync.js`, `legacy.js`, `ai.js`
- Import/export validator CI (`tests/frontend/validate-imports.mjs`)
- Tests unitaires frontend (`tests/frontend/unit.test.mjs`)
- **Stemming français** — `snowballstemmer` intégré au tokenizer
- "manger" trouve "mangé", "mangeons", "mangeait"
- Performance O(T) au lieu de O(S×T) — fix freeze 15min sur gros vaults
- Crash guard : try/except snowballstemmer IndexError sur tokens exotiques
### Modifié
- `frontend/js/app.js` → entrypoint ES module important les 16 modules
- `backend/search.py` — tokenizer avec FrenchStemmer
### Corrigé
- Nombreuses régressions post-split : imports manquants, exports oubliés, shadowing de variables
- Réécriture state.js → mutable object (`export const state = {...}`) pour éviter les erreurs `assignment to constant`
- Plus de 60 correctifs d'imports/exports validés par le validator CI
- Tags de template `{{...}}` exclus du tag cloud via filtre configurable
---
## [1.5.0] — 2026-05-26
### Ajouté
- **Publication publique** — Génération de lien partageable avec token unique (64-char hex)
- Expiration configurable, lecture seule, sans authentification
- `backend/share.py` + endpoints CRUD + vue publique `/s/{token}`
- Export PDF via WeasyPrint (lazy import) dans la vue publique
- Support fichiers non-Markdown dans la vue publique
- **Webhooks** — Notifications HTTP POST vers services externes
- Signature HMAC-SHA256 optionnelle
- `backend/webhooks.py` + CRUD endpoints + UI dans Configuration
- **Dashboard statistiques** — Métriques agrégées par vault
- Widget `DashboardStatsWidget` avec 4 cartes (fichiers, tags, taille, vaults)
- Widget "Derniers fichiers ouverts" avec timestamps
- **Documentation OpenAPI enrichie** — Tous les modèles Pydantic documentés (`Field(description=...)`)
- Visible dans `/docs` (Swagger UI) et `/redoc`
- **Documentation complète** — `ANALYSE_REVIEW.md`, `AUDIT_TECHNIQUE.md`, `ROADMAP.md`
### Modifié
- `frontend/js/app.js` — Nouveau système d'onglets (TabManager), dashboard
- `frontend/style.css` — Dashboard, share view, webhooks UI
- `backend/main.py` — Nouveaux endpoints share/webhooks/dashboard
- `backend/search.py` — Index inversé incrémental (hook pattern)
---
## [1.4.0] — 2026-05-25
### Ajouté
- **Rate limiting** — IP-based (10 tentatives/15min) + lockout par compte (5 tentatives)
- `backend/ratelimit.py` — RateLimiter thread-safe avec nettoyage périodique
- Intégré à `backend/auth/router.py`
- **Secret redactor** — Masquage automatique des secrets dans les aperçus
- `backend/secret_redactor.py` — JWT, API keys, tokens GitHub, clés privées, connection strings
- **Audit log** — Traçage des écritures/suppressions/changements de config
- `backend/audit.py` — JSON lines dans `data/audit.log`, rotation 10MB
- **Backup automatique avant écriture**
- Sauvegarde du contenu original dans `.obsigate-backup/` avant PUT/DELETE
- **Backlinks panel** — Fichiers avec wikilinks pointant vers le fichier courant
- `GET /api/file/{vault}/backlinks` + index inversé dans `indexer.py`
- **Gestion des conflits Syncthing**
- Détection automatique des fichiers `.sync-conflict-*`
- Résolution : garder local, garder conflit
- `GET /api/conflicts`, `POST /api/conflicts/resolve`
- **Syncthing conflicts dashboard** — Vue dédiée dans l'UI
- **IGNORED_DIRS configurable** — `OBSIGATE_IGNORED_DIRS` (liste séparée par virgules)
- **Timeout de session configurable** — `OBSIGATE_ACCESS_TOKEN_TTL` / `OBSIGATE_REFRESH_TOKEN_TTL`
### Modifié
- `backend/main.py` — +audit, +backup, +redaction, +backlinks endpoint, +conflicts endpoint
- `backend/indexer.py` — +backlink index, +IGNORED_DIRS
- `backend/auth/router.py` — +IP rate limiting
- `backend/auth/jwt_handler.py` — +configurable TTL
- `backend/watcher.py` — +configurable IGNORED_DIRS
---
## [1.3.0] — 2026-05-23
### Ajouté
- **Authentification complète** — JWT + Argon2id
- Login/logout, refresh tokens, cookies httpOnly
- Contrôle d'accès par vault
- Interface d'administration : création/modification/suppression d'utilisateurs
- "Se souvenir de moi", sessions persistantes
- `backend/auth/` — 5 modules (~600 lignes)
- **CodeMirror 6** — Éditeur de fichiers intégré
- Syntax highlighting multi-langage
- Sauvegarde, annulation, fallback textarea si CM6 échoue
- Auto-sync frontmatter YAML
- Find-in-page (Ctrl+F) avec regex, barre de navigation
- **PWA complet** — `manifest.json`, `sw.js`, icônes multi-tailles
- Installation via bouton "Installer l'application"
- Thème synchronisé entre fenêtres popout
- Service Worker avec mise en cache
- **Synchronisation temps réel** — Watchdog + SSE
- Surveillance automatique des fichiers
- Mise à jour incrémentale de l'index
- SSE Manager avec reconnexion automatique
- `backend/watcher.py` — watchdog observer avec debounce 2s
- **Drag & drop / rename** — Menu contextuel arborescence
- Création fichier/dossier, rename inline, suppression
- Boutons d'action toujours visibles, support mobile long-press
### Modifié
- `frontend/app.js` — Refonte majeure : authentication flows, code editor, PWA, sync
- `frontend/style.css` — CodeMirror themes, auth forms, PWA UI, responsive
- `docker-compose.yml` — auth env vars, data volume, non-root user
- `backend/main.py` — auth middleware, file CRUD, code editor API, SSE, PWA
- `requirements.txt` — argon2-cffi, python-jose, python-multipart
---
## [1.2.0] — 2026-05-18
### Ajouté
- **Rendu d'images Obsidian** — Support complet de toutes les syntaxes
- Standard Markdown avec attributs HTML
- Wiki-link embed (`![[path/to/image.png]]`)
- Markdown standard (`![alt](path)`)
- Résolution multi-stratégies (7 niveaux)
- Indexation des attachements au démarrage
- Cache de résolution pour performance
- **Système de fichiers cachés** — Configuration par vault
- `includeHidden`, `hiddenWhitelist` — UI + env vars
- Filtrage côté client (hideHiddenFiles)
### Modifié
- `backend/attachment_indexer.py` — Scan et indexation d'images
- `backend/image_processor.py` — Prétraitement Markdown pour les syntaxes d'images
- `GET /api/image/{vault}?path=` — Serve images avec MIME types
- `POST /api/attachments/rescan/{vault}` — Rescan manuel
- `HIDDEN_FILES_GUIDE.md`, `IMAGE_RENDERING_GUIDE.md`
---
## [1.1.0] — 2026-05-15
### Ajouté
- **Recherche TF-IDF avancée** — Moteur complet avec index inversé
- Opérateurs : `tag:`, `vault:`, `title:`, `path:`, `ext:`
- Autocomplétion intelligente (fichiers, tags, historique)
- Facettes, pagination, tri par pertinence
- Snippets surlignés `<mark>`, accent-insensitive
- Search history (localStorage, max 50, LIFO, dédupliqué)
- **Recherche regex** — `extractRegexSnippet()` avec surlignage
- **Recherches sauvegardées** — CRUD API + sidebar UI
- Sauvegarde/chargement/suppression de requêtes
- **Filtres de recherche** — toggles (case, mot entier, regex), chemins
- **Onglets sidebar** — vaults, tags, récents, saved searches
- **Auto-expand vault** dans le filtre sidebar
- **Tree search** — Filtrage temps réel de l'arborescence avec surlignage
### Modifié
- `backend/search.py` — Nouveau : InvertedIndex, TF-IDF, autocomplete
- `frontend/app.js` — Refonte recherche : nouveau système de requêtes, facettes, pagination, autocomplete
- `frontend/style.css` — Search results cards, autocomplete dropdown, tags
---
## [1.0.0] — 2026-05-10
### Ajouté
- **Première version stable** — Porte d'entrée web pour vaults Obsidian
- **Multi-vault** — Visualisation de plusieurs vaults simultanément
- **Arborescence** — Navigation hiérarchique avec lazy-loading
- **Rendu Markdown** — Conversion HTML via mistune (tables, task lists, footnotes, strikethrough)
- **Extraction YAML frontmatter** — Tous les champs parsés avec carte pliable, badges, tags
- **Vue Source (raw)** — Toggle markdown brut + frontmatter
- **Édition** — PUT/DELETE fichiers via API
- **Breadcrumbs** — Navigation avec auto-expand et highlight
- **TOC** — Table des matières avec scroll spy et barre de progression
- **Thème clair/sombre** — Toggle localStorage + synchronisé fenêtres popout
- **Popout** — Fenêtre détachée pour affichage standalone
- **Configuration UI** — Paramètres persistants par vault
- **Docker multi-stage** — Image ~180MB, linux/amd64 + arm64 + arm/v7 + i386
- **Healthcheck** — Endpoint `/api/health`
- **Headers sécurité** — CSP, X-Frame-Options, XSS-Protection, Referrer-Policy
- **Path traversal protection** — `_resolve_safe_path()` symlink-aware
- **Security hardening** — Utilisateur non-root (UID 1000)
- **Compression GZip** (SSE-safe), **Cache-Control immutable**
- **build.sh** — Script de build automatisé
### Architecture
- **Backend** : FastAPI 0.110.3, uvicorn 0.30.0, Python 3.11+
- **Frontend** : Vanilla JS SPA (0 dépendances npm), CSS custom properties
- **Indexation** : In-memory, mise à jour incrémentale, watcher watchdog
- **Recherche** : TF-IDF inversé, sans I/O disque