- clic sur le titre: la page d'accueil s'ouvre dans un onglet Accueil (icone Maison) epingle en tete de barre, reactivable d'un clic (onglet propre au panneau actif en split) - page de navigation: menus contextuels des repertoires et des fichiers identiques a ceux de la sidebar, clic sur la racine d'un vault qui ouvre la vue, icone dossier a la couleur de l'arbre de navigation - BUG-101: bloc « Keyboard shortcuts for tabs » duplique dans ui.js (un seul Ctrl+W fermait deux onglets) - BUG-102: le menu contextuel se refermait 10 ms apres son ouverture (scroll de reflow des icones lucide)
3540 lines
199 KiB
Markdown
3540 lines
199 KiB
Markdown
# 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.49.0**.
|
||
|
||
---
|
||
|
||
## [Unreleased]
|
||
|
||
---
|
||
|
||
## [2.49.0] — 2026-10-02
|
||
|
||
### Ajouté
|
||
|
||
- **#158 — onglet Accueil.** Le clic sur le titre ouvre désormais la page d'accueil **dans un
|
||
onglet** (icône Maison) systématiquement épinglé **en tête de barre**, réactivable d'un clic
|
||
comme un onglet normal (le mode split a son propre onglet Accueil dans le panneau actif).
|
||
- **#158 — menus contextuels dans la page de navigation.** Clic droit sur les répertoires et
|
||
les fichiers de la vue : même menu que celui de la sidebar (`ContextMenuManager`), et la
|
||
ligne d'un vault dans l'arbre ouvre à nouveau la page de navigation (racine du vault).
|
||
- **#158 — icône des répertoires alignée sur la sidebar.** La couleur du dossier dans la
|
||
section Répertoires est celle de l'arbre (`var(--accent)`).
|
||
|
||
### Corrigé
|
||
|
||
- **BUG-102 — menu contextuel refermé par son propre affichage.** L'auto-scroll déclenché par
|
||
la reflow des icônes (lucide) à l'ouverture du menu arrivait dans le listener `scroll`
|
||
(capture) et refermait le menu ~10 ms après son apparition : un décalage de 250 ms est
|
||
ignoré, les vrais défilements ferment toujours le menu.
|
||
- **BUG-101 — raccourcis d'onglets déclarés deux fois.** Le bloc « Keyboard shortcuts for
|
||
tabs » existed en double dans `ui.js` (fin de fichier) : un seul `Ctrl+W` fermait **deux**
|
||
onglets (le second refermait celui qui venait d'être ré-activé) et `Ctrl+Tab` sautait un
|
||
onglet. Bloc dupliqué supprimé.
|
||
- **#158 — clic titre ne repliait plus l'arborescence.** L'effacement du filtre de la sidebar
|
||
n'est déclenché que s'il contenait quelque chose (sinon `restoreSidebarTree()` repliait tout
|
||
l'arbre à chaque retour à l'accueil).
|
||
|
||
---
|
||
|
||
## [2.48.0] — 2026-10-02
|
||
|
||
### Ajouté
|
||
|
||
- **#158 — onglet de navigation.** Chaque clic sur un répertoire de l'arbre (mode focus vault
|
||
**et** mode All) ouvre un **onglet de navigation** dédié qui coexiste avec les onglets de
|
||
fichiers : la vue chemin + Récents + fichiers n'est plus écrasée par un document ouvert, et
|
||
change de répertoire en se mettant à jour dans son onglet. La ligne d'un vault reste un
|
||
simple expandeur ; la racine du vault s'atteint depuis la vue (fil d'Ariane, facette Vaults)
|
||
ou le sélecteur de vault.
|
||
- **#158 — contenu de la page de navigation.** La vue liste désormais les **sous-répertoires
|
||
cliquables** et embarque le mécanisme de la page de recherche : panneau de facettes
|
||
**Vaults · Tags · Extensions** (repli partagé, filtre local avec pastille active), tri
|
||
**Pertinence / Date** et bouton **Sauver** (enregistre le répertoire comme recherche
|
||
sauvegardée). `GET /api/vault/{vault}/files` renvoie les `tags` indexés de chaque fichier
|
||
(champ `tags` ajouté à `VaultFileEntry`) pour calculer les facettes.
|
||
|
||
### Corrigé
|
||
|
||
- **BUG-100 — retour Home incomplet.** Le clic sur le titre perdait la section
|
||
**Raccourcis & Astuces** (le template de reconstruction de `showWelcome()` ne la contenait
|
||
pas) et laissait la recherche globale ainsi que le filtre de la sidebar actifs : le bloc
|
||
`#quick-help` est désormais capturé puis réinjecté, et `goHome()` efface la recherche
|
||
(chips, barre de résultats) avant d'afficher l'accueil.
|
||
|
||
---
|
||
|
||
## [2.47.1] — 2026-10-02
|
||
|
||
---
|
||
|
||
## [2.47.0] — 2026-10-01
|
||
|
||
### Ajouté
|
||
|
||
- **#157 — panneau de facettes repliable.** Le bloc **Vaults · Tags · Extensions** des résultats
|
||
de recherche se masque/restaure d'un clic sur un bouton discret (chevron), état mémorisé
|
||
dans `localStorage` et conservé d'une recherche à l'autre.
|
||
|
||
### Corrigé
|
||
|
||
- **#157 — facette « Extensions » absente de l'API.** `SearchFacets` (`backend/schemas.py`)
|
||
ne déclarait que `tags` et `vaults` : la clé `extensions` ajoutée par `backend/search.py`
|
||
était supprimée par le `response_model` avant d'atteindre le navigateur. Champ ajouté,
|
||
plus les tests d'advanced-search pointaient la route inexistante `/api/advanced-search`
|
||
et sautaient en silence — ils ciblent désormais `/api/search/advanced`, avec une assertion
|
||
stricte sur `facets.extensions`.
|
||
- **#157 — bouton de repli des facettes maladroit sur mobile.** En dessous de 768 px, le
|
||
panneau passe en colonne et le bouton s'étirait en pleine largeur d'écran, pivotait
|
||
entièrement à la vertical au clic. Le bouton reste compact (`align-self: flex-start`) et
|
||
**seule l'icône chevron pivote** (transition 120 ms), sur mobile comme sur desktop.
|
||
|
||
---
|
||
|
||
## [2.46.0] — 2026-10-01
|
||
|
||
### Ajouté
|
||
|
||
- **#157 — facette « Extensions » dans les facettes des résultats de recherche.** Le panneau de
|
||
facettes de la page de résultats (Vaults · Tags) gagne un troisième groupe qui liste les
|
||
extensions de fichiers présents dans les résultats (`.md`, `.xlsx`, `.pdf`, …) avec leurs
|
||
compteurs ; cliquer une extension ajoute l'opérateur `ext:` à la requête (en remplaçant un
|
||
`ext:` déjà actif). Le backend calcule `facets.extensions` dans `backend/search.py`.
|
||
|
||
---
|
||
|
||
## [2.45.2] — 2026-10-01
|
||
|
||
---
|
||
|
||
## [2.45.1] — 2026-10-01
|
||
|
||
---
|
||
|
||
## [2.45.0] — 2026-09-30
|
||
|
||
### Ajouté
|
||
|
||
- **#156-A5 — presse-papiers de plage dans l'éditeur Excel.** Copier, couper et coller un
|
||
**bloc** de cellules (TSV), au clavier (`Ctrl+C`/`Ctrl+X`/`Ctrl+V`) comme par le menu
|
||
contextuel ; le collage remplit la plage à partir de la cellule active et la laisse
|
||
sélectionnée. La grille tient son propre presse-papiers (le presse-papiers système est
|
||
synchronisé quand le navigateur l'autorise), le collage est toujours du **texte** — jamais de
|
||
HTML — et il est **annulable**. Un bloc plus large que la grille affichée est tronqué et signalé.
|
||
- **#156-A6 — raccourcis clavier complets.** `Ctrl+S` (enregistrer), `Ctrl+A` (sélectionner la
|
||
feuille rendue), `Suppr` (vider la sélection), `F2` (édition), `Ctrl+Home`/`Ctrl+End` (coins de
|
||
la feuille), `Home`/`End` (bords de la ligne), `PgUp`/`PgDn` (un écran), `Ctrl+flèches` (bout de
|
||
la plage de données) et `Maj+Entrée` (saut de ligne dans la cellule). Une cellule reste en mode
|
||
« navigation » jusqu'à la saisie (ou `F2`) : `Suppr` et `Home`/`End` ne volent donc jamais une
|
||
touche du curseur de texte.
|
||
- **#156-A7 — zone Nom éditable et aide à la saisie.** La zone Nom devient un champ
|
||
(« Atteindre ») : saisir `B12`, `A1:B3` ou `Feuille!A1` puis `Entrée` sélectionne la cellule ou
|
||
la plage (avec changement d'onglet si la feuille est nommée) ; une référence inconnue est
|
||
refusée avec un message et l'adresse précédente restaurée. La barre de formule propose les noms
|
||
de fonctions courants (liste localisée FR/EN).
|
||
- **#156-A8 — mise en forme en écriture.** Un bouton **Mise en forme** ouvre un menu qui
|
||
agit sur la **sélection courante** (une cellule ou une plage) : gras, italique, souligné,
|
||
effacement de la mise en forme, alignement gauche/centré/droite, couleur de police et de fond,
|
||
format de nombre (général, nombre, pourcentage, devise, date, texte), fusion/défusion, volets
|
||
figés/libérés et largeur de colonne / hauteur de ligne. L'écriture passe par une nouvelle route
|
||
`PUT /api/file/{vault}/xlsx/style`, sur le même modèle que `xlsx/save` et `xlsx/structure` :
|
||
verrou par fichier, écriture atomique (`.tmp` + `os.replace`), backup automatique, garde de perte
|
||
(une opération qui effacerait des éléments non préservables est refusée sans `force`) et
|
||
**concurrence optimiste** (`If-Match`, cf. A12). Les mises en forme ne sont pas proposées sur un
|
||
`.csv` (le format n'en porte pas) ni sur une grille en lecture seule. Les couleurs viennent du
|
||
sélecteur natif, jamais d'une palette codée en dur. Tests : `tests/test_xlsx_styles.py` (17) et
|
||
JSDOM (application d'un format, refus d'un CSV, garde de perte).
|
||
- **#156-A9 — le calcul est explicitement hors périmètre, et annoncé.** L'éditeur n'embarque
|
||
aucun moteur de formule : ObsiGate affiche et écrit les formules telles qu'Excel les a
|
||
enregistrées, les cellules dépendantes ne se recalculent pas à l'écran. Le choix est désormais
|
||
**dit à l'utilisateur** : un enregistrement contenant des valeurs commençant par `=` ou `@`
|
||
affiche « enregistrée comme texte » tant que le bouton `f(x)` n'est pas activé (la garde
|
||
anti-DDE de BUG-088 reste en place). La pastille « Formules non recalculées » et la fiche #156
|
||
portent la décision. Tests JSDOM : l'avertissement apparaît, et disparaît une fois `f(x)`
|
||
activé.
|
||
- **#156-A10 — historique annuler/rétablir unifié, conservé au re-rendu.** La pile ne couvrait
|
||
que les éditions de cellules : « Effacer le contenu », le tri, le filtre et les actions de
|
||
**structure** y entrent désormais, avec des entrées typées (`cell`, `order`, `hidden`,
|
||
`structure`). Les piles sont **conservées par fichier** (8 fichiers, `_xlsxHistory`) au lieu
|
||
d'être vidées par un re-rendu ; `Suppr` passe par le même chemin (donc annulable) ; le filtre
|
||
est **coalescé** en une seule entrée par session de frappe ; un rejeu de structure appelle
|
||
l'endpoint en mode silencieux (pas de doublon d'historique ni de bandeau parasite) et une
|
||
action destructrice (suppression de ligne/colonne/feuille) n'est **pas** annulable, faute
|
||
d'inverse. Tests JSDOM : édition → effacement → tri annulés successivement, insertion de ligne
|
||
rejouée par l'endpoint, action destructive non annulable, filtre annulé par un seul `Ctrl+Z`.
|
||
- **#156-A11 — sortie (CSV, Markdown, HTML, impression) et recherche sur tout le classeur.**
|
||
L'export ne se limite plus à la feuille entière : le bouton **CSV** exporte la **sélection**
|
||
quand une plage de plusieurs cellules est active (nom de fichier `Feuille-A1B2.csv`), sinon la
|
||
feuille ; un menu **Exporter** ajoute le **Markdown** (tableau), le **HTML** (document
|
||
autonome) et l'**impression** (la feuille seule, sans le ruban, via un cadre hors écran). Les
|
||
quatre formats suivent le **contenu affiché** (la valeur calculée en cache reste une
|
||
infobulle). L'écart doc/code de #153-A13 (« export de la sélection ») est tranché : la
|
||
sélection est le défaut quand elle existe. La recherche (`Ctrl+F`) parcourt désormais **toutes
|
||
les feuilles** : compteur « n/m · k feuilles » et activation automatique de l'onglet de la
|
||
correspondance atteinte.
|
||
- **#156-A14 — l'assistant IA couvre les trois formats édités et gagne trois outils.** Les
|
||
outils existants acceptent `.xlsm` (écriture avec macros préservées) et `.csv` (lecture et
|
||
écriture, délimiteur conservé) au lieu du seul `.xlsx` ; `update_xlsx_cells` écrit un `.csv`
|
||
via `save_csv_cells`. Nouveaux outils : `search_workbook` (recherche dans toutes les feuilles,
|
||
comptée par feuille), `analyze_range` (nombre, somme, moyenne, min, max d'une plage A1, avec
|
||
échantillon borné) et `edit_xlsx_structure` (ajouter/renommer/dupliquer/supprimer une feuille,
|
||
insérer/supprimer lignes et colonnes) — les deux premiers en risque READ, le dernier conservant
|
||
la confirmation obligatoire des outils mutants. Tests `tests/test_spreadsheet_tools.py` (34).
|
||
|
||
### Modifié
|
||
|
||
- **#156 — Éditeur Excel : audit de complétude, roadmap et 4 défauts enregistrés (documentation
|
||
seule).** Nouvelle fiche `docs/features/xlsx-editor-completeness.md` (audit par couche, manques
|
||
fonctionnels, risques, critères d'acceptation) et item **#156** dans `docs/ROADMAP.md`
|
||
(P0 défauts · P1 presse-papiers/clavier · P2 mise en forme/calcul/undo · P3
|
||
sortie/robustesse/perf). **BUG-096 → BUG-099** ouverts dans `docs/ISSUES_TODOLIST.md` :
|
||
enregistrer un `.csv` depuis la visionneuse lève un `TypeError` (reproduit en JSDOM, aucun
|
||
`PUT …/csv/save` émis) ; « Charger la suite » d'une feuille `.xlsm` tronquée répond **415** ;
|
||
le délimiteur CSV est figé sur `,` alors que l'export écrit `;` ; la sonde de perte plafonnée à
|
||
8 Mo peut laisser passer une écriture qui détruit les valeurs calculées en cache. L'audit
|
||
lui-même ne changeait aucun code ; les correctifs correspondants suivent ci-dessous.
|
||
- **#156-A12 — concurrence optimiste entre processus (`If-Match`, 409 réparable).** Chaque
|
||
lecture renvoie la **version** du fichier (`xlsx_revision` : `mtime_ns` + taille) et les trois
|
||
routes d'écriture (`xlsx/save`, `xlsx/structure`, `csv/save`) acceptent l'en-tête `If-Match` ou
|
||
le champ `if_match` : si le fichier a changé depuis, l'écriture est refusée en **409**
|
||
`conflict` (`details.reason = "stale_revision"`, versions attendue et courante) au lieu
|
||
d'écraser l'autre écrivain. Le bandeau « Réessayer » **relit** le fichier puis rejoue la
|
||
sauvegarde avec la version courante ; les réponses de succès renvoient la nouvelle version, que
|
||
le client enchaîne sur les écritures suivantes. Tests : `TestOptimisticConcurrency` (6) et
|
||
JSDOM (version lue, version rafraîchie par la réponse, relecture avant le retry).
|
||
- **#156-A13 — cache des métadonnées par `mtime`.** `read_workbook_meta()` rechargeait le
|
||
classeur entier à chaque fenêtre servie ; les métadonnées (styles, fusions, volets, largeurs)
|
||
sont désormais mémoïsées par `(chemin, mtime_ns, taille)` (LRU de 8) et invalidées à chaque
|
||
écriture (cellules, structure, CSV). Charger une fenêtre ne rescanne plus le classeur. L'axe
|
||
des colonnes au-delà de `MAX_COLS` reste hors périmètre (voir la fiche #156). Tests :
|
||
`TestMetaCache` (4).
|
||
|
||
### Corrigé
|
||
|
||
- **BUG-096 — enregistrer un `.csv` depuis la visionneuse échoue (`TypeError`, aucun
|
||
`PUT …/csv/save` émis).** Le job de sauvegarde lisait le nom de la feuille dans
|
||
`xlsx_sheets` — absent du payload d'un CSV — donc `sheets[0].name` levait un `TypeError`
|
||
**avant** toute requête : aucune modification de CSV ne pouvait être enregistrée. Le nom est
|
||
désormais résolu avec repli (feuille → nom du fichier) ; l'endpoint `csv/save` est inchangé.
|
||
Tests : JSDOM (un CSV monté **sans** `xlsx_sheets` émet un unique `PUT …/csv/save`).
|
||
- **BUG-097 — « Charger la suite » d'une feuille `.xlsm` renvoyait **415**.** Un `.xlsm`
|
||
s'édite dans la même visionneuse que le `.xlsx`, mais `GET …/xlsx/sheet` refusait tout autre
|
||
format : les lignes au-delà du plafond de 500 étaient inaccessibles. L'endpoint accepte
|
||
désormais `.xlsx` **et** `.xlsm` (les autres formats restent en **415**). Tests :
|
||
`test_xlsx_formats.py` (fenêtre servie pour un `.xlsm`, refus maintenu pour un `.csv`).
|
||
- **BUG-098 — un CSV français (`;`) s'affichait en une seule colonne.** Le délimiteur était
|
||
figé à la virgule, en lecture comme en réécriture. `sniff_csv_delimiter()` détecte `;`, `,`
|
||
ou tabulation (repli : décompte sur la première ligne, puis `,`) et `save_csv_cells()`
|
||
**réutilise le délimiteur détecté** — un CSV point-virgule s'affiche en colonnes distinctes et
|
||
le reste après édition. Tests : `test_xlsx_formats.py` (lecture, écriture, champs entre
|
||
guillemets contenant le séparateur).
|
||
- **BUG-099 — perte silencieuse possible des valeurs calculées en cache (sonde plafonnée).**
|
||
La détection des `<f>…</f><v>…</v>` disposait d'un budget **unique** de 8 Mo partagé entre les
|
||
feuilles : au-delà, un résultat négatif était pris pour une preuve et l'écriture détruisait
|
||
les valeurs mises en cache sans avertissement. Le budget est désormais **par feuille** (4 Mo)
|
||
avec un plafond global (32 Mo), et un budget épuisé est **signalé** (`cached_values_unverified`,
|
||
libellé FR/EN) : la garde de sauvegarde demande une confirmation au lieu de laisser passer.
|
||
Tests : `TestXlsxCachedValueProbe` (budget épuisé → signalé, feuille volumineuse → la suivante
|
||
reste scannée, scan complet propre → aucun faux positif).
|
||
|
||
---
|
||
|
||
## [2.44.1] — 2026-09-29
|
||
|
||
### Sécurité
|
||
|
||
- **BUG-095 — le job CI `security` repasse au vert : `pyjwt` monté au-dessus du dernier
|
||
correctif.** `pip-audit` bloquait sur **CVE-2026-102274** dans `pyjwt` 2.13.0 (correctif
|
||
**2.14.0**) : le plancher introduit par BUG-091 (`>=2.13.0`, qui corrigeait PYSEC-2026-178)
|
||
était devenu insuffisant. Le plancher passe à `pyjwt[crypto]>=2.14.0` — volontairement au-dessus
|
||
de la version préinstallée dans la toolcache du runner (sinon `pip` répond « already satisfied »
|
||
et n'aligne jamais la version). Le garde-fou
|
||
`tests/test_ci_workflow.py::TestDependencySecurityFloors` verrouille ce nouveau plancher
|
||
(contre-preuve : plancher remis à `2.13.0` → test rouge).
|
||
|
||
---
|
||
|
||
## [2.44.0] — 2026-09-29
|
||
|
||
### Ajouté
|
||
|
||
- **#155 — Ergonomie du tableur : menu contextuel (clic droit / appui long) et sélection type
|
||
Excel.** Un **menu contextuel** thémé (nouveau module `frontend/js/xlsx/context-menu.js`)
|
||
s'ouvre au **clic droit** sur une cellule, un numéro de ligne ou un en-tête de colonne, et par
|
||
**appui long** sur mobile : insérer une ligne au-dessus/en dessous, une colonne à gauche/à
|
||
droite (avec `count` pour une plage), supprimer la/les ligne(s) ou colonne(s) après confirmation,
|
||
trier A→Z / Z→A et effacer le contenu. La grille adopte la **sélection d'Excel** : plage par
|
||
**glisser**, extension par **`Maj+clic`** et **`Maj+flèches`**, sélection de la ligne/colonne
|
||
entière au clic sur son en-tête, **curseur croix** sur les cellules et zone **Nom** affichant la
|
||
plage (`A1:B3`). Le tri par simple clic sur l'en-tête laisse place à la sélection (tri déplacé
|
||
dans le menu contextuel). `SW_VERSION` passe à `v29`. Tests JSDOM
|
||
`tests/frontend/xlsx-viewer.test.mjs` (60, dont 8 nouveaux) et E2E (9) verts.
|
||
|
||
---
|
||
|
||
## [2.43.2] — 2026-09-29
|
||
|
||
### Corrigé
|
||
|
||
- **BUG-094 — une feuille `.xlsx` vide ou nouvellement ajoutée est désormais éditable et
|
||
manipulable.** Elle s'affichait comme un simple « Feuille vide » sans aucune cellule : il était
|
||
donc impossible d'y saisir une valeur ou d'y insérer une ligne/colonne (aucune cellule active →
|
||
aucune action de structure). `render_sheets()` substitue maintenant un quadrillage vierge
|
||
**20×8** aux vraies coordonnées A1 (constantes `EMPTY_SHEET_ROWS`/`EMPTY_SHEET_COLS`), et la
|
||
visionneuse retombe sur `{row:1,col:1}` quand aucune cellule n'est active, de sorte que le menu
|
||
Structure propose toujours insérer/supprimer ligne et colonne. Test de non-régression :
|
||
`TestXlsxDisplay::test_empty_sheet_renders_an_editable_blank_grid`.
|
||
|
||
---
|
||
|
||
## [2.43.1] — 2026-09-29
|
||
|
||
### Ajouté
|
||
|
||
- **#154 — Refonte UI/UX de la visionneuse XLSX (Lot 5, clôture) : extraction modulaire et
|
||
finitions.**
|
||
Les unités **sans état** de la visionneuse sont extraites dans `frontend/js/xlsx/` :
|
||
`refs.js` (références A1), `command-bar.js` (onglets + ruban + pastilles d'état) et
|
||
`dashboard.js` (rendu du tableau de bord) — le noyau avec état reste dans `viewer.js` à
|
||
comportement constant. Le tableau de bord devient interactif : **cliquer une plage nommée
|
||
sélectionne et révèle sa première cellule** dans la grille (changement d'onglet si nécessaire).
|
||
L'inspecteur est **redimensionnable** (largeur mémorisée par session). `SW_VERSION` passe à
|
||
`v28` et les nouveaux modules entrent dans le pré-cache. Tests JSDOM
|
||
`tests/frontend/xlsx-viewer.test.mjs` (51) et E2E `tests/e2e/xlsx-viewer.spec.js` (9) verts.
|
||
Aucun changement backend.
|
||
|
||
---
|
||
|
||
## [2.43.0] — 2026-09-29
|
||
|
||
### Ajouté
|
||
|
||
- **#154 — Refonte UI/UX de la visionneuse XLSX (Lot 4) : undo/redo, défilement et accessibilité.**
|
||
Les éditions de cellules peuvent être **annulées / rétablies** (boutons dans le ruban,
|
||
raccourcis `Ctrl+Z`, `Ctrl+Maj+Z`, `Ctrl+Y`). Les fenêtres de lignes d'une feuille tronquée se
|
||
chargent via **`IntersectionObserver`** (repli sur l'écouteur de défilement quand l'API est
|
||
absente). La grille expose désormais une sémantique **ARIA** (`role="grid"` / `row` /
|
||
`gridcell` / `columnheader` / `rowheader`). Tests JSDOM `tests/frontend/xlsx-viewer.test.mjs`
|
||
(47) et E2E `tests/e2e/xlsx-viewer.spec.js` (9) verts. Aucun changement backend.
|
||
|
||
---
|
||
|
||
## [2.42.0] — 2026-09-29
|
||
|
||
### Ajouté
|
||
|
||
- **#154 — Refonte UI/UX de la visionneuse XLSX (Lot 3) : inspecteur droit.**
|
||
Le **Tableau de bord** quitte le flux de la grille pour un **panneau latéral droit repliable**
|
||
(`.xlsx-inspector`) : la grille reste visible à côté. L'en-tête de l'inspecteur porte le titre,
|
||
une entrée **Assistant IA** (ouvre le panneau latéral global existant) et un bouton de fermeture.
|
||
Sous 900 px, l'inspecteur se place sous la grille. Tests JSDOM
|
||
`tests/frontend/xlsx-viewer.test.mjs` (44) et E2E `tests/e2e/xlsx-viewer.spec.js` (9) verts.
|
||
Aucun changement backend.
|
||
|
||
---
|
||
|
||
## [2.41.0] — 2026-09-29
|
||
|
||
### Ajouté
|
||
|
||
- **#154 — Refonte UI/UX de la visionneuse XLSX (Lot 2) : dialogues thémés et conflits non
|
||
bloquants.**
|
||
Les `confirm()` / `prompt()` natifs sont remplacés par des **dialogues intégrés au thème**
|
||
(`showConfirm()` / `showPrompt()` dans `frontend/js/ui.js`, promise-based, réutilisant
|
||
`.obsigate-modal-*`) pour toutes les actions de structure du classeur (ajouter / renommer /
|
||
dupliquer / supprimer une feuille, insérer / supprimer une ligne ou une colonne) et la
|
||
confirmation de perte (`409 xlsx_lossy_content`). Un **conflit d'écriture** (`409 conflict`)
|
||
n'interrompt plus l'utilisateur : un **bandeau non bloquant** propose de réessayer en
|
||
conservant les modifications. Le bouton **Enregistrer** et l'onglet de la feuille concernée
|
||
signalent les modifications non sauvegardées. Tests JSDOM `tests/frontend/xlsx-viewer.test.mjs`
|
||
(42) et E2E `tests/e2e/xlsx-viewer.spec.js` (9) adaptés. Aucun changement backend.
|
||
|
||
---
|
||
|
||
## [2.40.0] — 2026-09-29
|
||
|
||
### Ajouté
|
||
|
||
- **#154 — Refonte UI/UX de la visionneuse XLSX (Lot 1) : ruban de commandes groupé,
|
||
onglets de feuilles permanents avec bouton « + », badges d'état.**
|
||
La vue tableur gagne une barre de commandes segmentée (Formules · Insertion · Vue ·
|
||
Fichier) avec un bouton **Enregistrer** primaire. La barre d'onglets est désormais
|
||
toujours affichée (même à une seule feuille) et un bouton « + » y ajoute une feuille
|
||
(même pipeline `PUT …/xlsx/structure`, re-rendu depuis le serveur). Deux pastilles
|
||
d'état annoncent les limites de la vue : **lecture seule** (`.xls`/`.ods` — plus de
|
||
« + », ni de structure, ni de tableau de bord, ni d'édition) et **formules non
|
||
recalculées**. Des tokens de grille dédiés (`--grid-bg`, `--grid-header-bg`,
|
||
`--grid-header-text`, `--grid-border`, `--grid-zebra`, déclinés dark/light) rendent les
|
||
en-têtes clairement distincts des cellules, avec zébrage, survol et cellule active
|
||
renforcée. Audit UX, architecture cible et plan par lots :
|
||
[docs/features/xlsx-ui-redesign.md](docs/features/xlsx-ui-redesign.md). Tests JSDOM
|
||
`tests/frontend/xlsx-viewer.test.mjs` (41, dont 6 nouveaux). Aucun changement backend.
|
||
|
||
---
|
||
|
||
## [2.39.10] — 2026-09-29
|
||
|
||
### Sécurité
|
||
|
||
- **BUG-093 — deux DoS de ressources dans `pypdf` 6.16.0 (PYSEC-2026-3910,
|
||
PYSEC-2026-3911) corrigés par le plancher `pypdf>=6.16.1`.**
|
||
Un PDF peut provoquer un temps de calcul et une consommation mémoire
|
||
arbitraires, soit via de nombreux contours (*outlines*), soit via une page
|
||
portant beaucoup d'objets XForm réutilisés. ObsiGate est **directement
|
||
exposé** : `backend/pdf_reader.py` extrait le texte et parcourt les contours
|
||
de PDF fournis par l'utilisateur. Le plancher `pypdf>=4.0` ne protégeait
|
||
rien en pratique — l'image du runner Act embarque 6.16.0 *préinstallé* dans
|
||
sa toolcache Python, donc `pip` répondait « already satisfied » et
|
||
n'alignait jamais la version. Tout plancher de sécurité doit désormais rester
|
||
au-dessus de la version préinstallée.
|
||
|
||
### Correction
|
||
|
||
- **Le job CI `security` n'est plus rouge : la désactivation de semgrep
|
||
fonctionne, et `pip-audit` est désormais réparé pour de bon.**
|
||
Le garde-fou `tests/test_ci_workflow.py::TestSemgrepStep`, en régression
|
||
depuis la désactivation (il exigeait encore l'exécution de semgrep),
|
||
vérifie maintenant que l'étape n'exécute que son `::warning::` et que
|
||
**bandit et pip-audit restent bloquants**. Nouveau garde-fou
|
||
`TestDependencySecurityFloors` : les planchers de sécurité (`pypdf`,
|
||
`pyjwt`) ne peuvent plus retomber sous leur correctif — contre-preuve
|
||
vérifiée (plancher remis à `>=4.0` → test rouge).
|
||
|
||
---
|
||
|
||
## [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 (``)
|
||
- 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
|