# 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.1**. --- ## [Unreleased] --- ## [2.49.1] — 2026-10-02 ### Corrigé - **Mobile (BUG-103)** : le bas du sidebar de navigation n'est plus masqué par la barre d'outils du bas — `z-index` du sidebar mobile `200` → `950` (au-dessus de `.mobile-toolbar`, `900`). --- ## [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 `……` 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` (+ `` orphelin supprimé), id dupliqué `cfg-partages-publics` retiré du `

`, 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 `
` (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 ``) : 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 `