feat: completude de l'editeur Excel - mise en forme, sortie, concurrence, cache, outils IA #156
CI / lint (push) Successful in 2m36s
CI / security (push) Successful in 1m49s
CI / test (push) Successful in 4m33s
CI / build (push) Successful in 2m39s
CI / e2e (push) Successful in 15m45s

L'editeur lisait les styles mais ne les ecrivait pas, exportait la feuille
entiere et laissait le dernier-ecrivain gagner entre processus.

- A8 : bouton Mise en forme (gras/italique/souligne, alignements, couleurs via
  selecteur natif, formats de nombre, fusion, volets figes, largeur/hauteur) et
  nouvelle route PUT .../xlsx/style (verrou, backup, swap atomique, garde de
  perte, If-Match) ; A9 : decision "pas de moteur de formule" annoncee dans
  l'UI ; A10 : undo/redo unifie, piles par fichier conservees au re-rendu
- A11 : export de la selection + Markdown/HTML/impression et recherche sur
  toutes les feuilles ; A12 : concurrence optimiste (If-Match -> 409 reparable,
  retry qui relit) ; A13 : cache des metadonnees par (chemin, mtime, taille)
- A14 : outils IA .xlsm/.csv + search_workbook, analyze_range,
  edit_xlsx_structure
- tests : test_xlsx_styles.py (17), test_spreadsheet_tools.py (34),
  TestOptimisticConcurrency/TestMetaCache, JSDOM xlsx-viewer 107/107
This commit is contained in:
2026-09-30 07:04:47 -04:00
parent 1bacfd69d9
commit c5c225a68e
32 changed files with 4615 additions and 302 deletions
+118 -28
View File
@@ -164,9 +164,11 @@ l'application. Si vous refusez, rien n'est écrit.
> Les **graphiques, images et tableaux croisés** sont, eux, bien conservés.
Si le classeur est modifié ailleurs entre-temps (verrou concurrent), ObsiGate
n'interrompt pas votre travail : un bandeau vous propose de **réessayer**
l'enregistrement, vos modifications restant en place.
Si le classeur est modifié ailleurs entre-temps (autre poste, Excel,
synchronisation…), ObsiGate n'interrompt pas votre travail : un bandeau vous
propose de **réessayer**. Le bouton **Réessayer** relit d'abord le fichier pour
récupérer la version courante, puis rejoue l'enregistrement — vos
modifications restent en place pendant tout ce temps.
### Formules
@@ -182,8 +184,14 @@ curl -X PUT "http://localhost:2020/api/file/Recettes/xlsx/save?path=budget.xlsx"
- `allow_formula` : `true` pour écrire une vraie formule (`=B1*2`).
- `force` : `true` pour enregistrer malgré les éléments non préservés
(sinon l'API répond **409** `xlsx_lossy_content`).
- Deux sauvegardes simultanées sur le même fichier : la seconde reçoit
**409** `conflict` au lieu d'écraser la première.
- `if_match` (facultatif) : la **version** du fichier attendue, telle que la
lecture la renvoie (`xlsx_revision` ou `revision`). Si le fichier a changé
depuis, l'écriture est refusée (**409** `conflict`, `details.reason =
"stale_revision"`) au lieu d'écraser le travail de l'autre écrivain ;
relisez le fichier puis renvoyez la nouvelle version. Les trois routes
d'écriture (`xlsx/save`, `xlsx/structure`, `csv/save`) acceptent l'en-tête
`If-Match` ou le champ `if_match` et renvoient la version à jour dans
`revision`.
### Feuilles volumineuses et lecture par fenêtres
@@ -206,14 +214,35 @@ curl "http://localhost:2020/api/file/Recettes/xlsx/sheet?path=budget.xlsx&sheet=
lignes (1 à 1 000 par requête).
- La réponse porte `total_rows`, `truncated` et `has_more` pour paginer.
- Erreurs : **404** si la feuille n'existe pas, **415** si le fichier
n'est pas un `.xlsx`.
n'est ni un `.xlsx` ni un `.xlsm`.
### Fonctions avancées
**Barre de formule et navigation clavier** — la cellule active est nommée en
A1 au-dessus du tableau ; `Tab`/`Maj+Tab` et les flèches circulent entre les
cellules, `Entrée` valide, `Maj+Entrée` insère un retour à la ligne, une
plage se copie telle quelle vers un tableur.
**Barre de formule, zone Nom et navigation clavier** — au-dessus du tableau, la
**zone Nom** affiche l'adresse de la cellule active (`B12`) ou de la plage
sélectionnée (`A1:B3`) et elle est **éditable** (« Atteindre ») : saisissez une
référence puis `Entrée` pour y aller (`B12`, `A1:B3`, `$A$1`, ou `Feuille2!A1`
pour changer d'onglet) ; une référence inconnue est refusée avec un message et
l'adresse précédente est restaurée. La barre de formule reflète la cellule
active et propose les **noms de fonctions** courants pendant la saisie.
`Tab`/`Maj+Tab` et les flèches circulent entre les cellules, `Maj+flèches` étend
la sélection, `Entrée` valide, `Maj+Entrée` insère un saut de ligne **dans** la
cellule, `F2` ouvre la cellule en édition, `Suppr` vide la sélection,
`Échap` restaure la valeur d'origine ; `Origine`/`Fin` vont au bord de la ligne,
`Ctrl+Origine`/`Ctrl+Fin` aux coins de la feuille affichée,
`PgPréc`/`PgSuiv` font défiler d'un écran, `Ctrl+flèches` saute au bout de la
plage de données, `Ctrl+A` sélectionne toute la feuille affichée et `Ctrl+S`
enregistre. Tant que la cellule n'est pas en cours d'édition, `Suppr`
efface la sélection plutôt qu'un caractère.
**Presse-papiers de plage** — copier, couper et coller un **bloc** de cellules
(`Ctrl+C`, `Ctrl+X`, `Ctrl+V`, ou les entrées correspondantes du menu
contextuel) : coller un bloc copié ici **ou depuis Excel** remplit la plage à
partir de la cellule active et la laisse sélectionnée. Le collage est du
**texte** (formules et valeurs recopiées telles quelles) et reste **annulable** ;
« couper » efface la source après le collage (un collage sur place ne l'efface
pas). Un bloc plus large que la grille affichée est tronqué, avec un message.
**Annuler / rétablir** — `Ctrl+Z` (ou le bouton **Annuler** du ruban) revient
sur les dernières éditions de cellules, `Ctrl+Maj+Z` / `Ctrl+Y` les rétablit.
@@ -221,28 +250,65 @@ sur les dernières éditions de cellules, `Ctrl+Maj+Z` / `Ctrl+Y` les rétablit.
**Sélection et menu contextuel** — cliquer une cellule l'active, **glisser**
ou `Maj+clic` sélectionne une plage (affichée dans la zone Nom, ex. `A1:B3`),
et cliquer un **en-tête** sélectionne toute la ligne ou colonne. Un **clic
droit** (ou un **appui long** sur mobile) ouvre un menu : insérer/supprimer
une ligne ou une colonne, trier A→Z / Z→A, effacer le contenu.
droit** (ou un **appui long** sur mobile) ouvre un menu : copier, couper,
coller, insérer/supprimer une ligne ou une colonne, trier A→Z / Z→A, effacer le
contenu.
**Tri, filtre, recherche, export** — le tri (ascendant / descendant) s'applique
depuis le menu contextuel et n'affecte que l'affichage ; les lignes se filtrent,
la recherche (`Ctrl+F` du panneau) va de correspondance en correspondance, et
la feuille s'exporte en CSV. Rien de tout cela ne modifie le classeur.
depuis le menu contextuel et n'affecte que l'affichage ; les lignes se filtrent et
la recherche (`Ctrl+F` du panneau) parcourt **toutes les feuilles** : le compteur
indique le nombre de feuilles concernées et passer sur une correspondance
**active l'onglet** qui la contient.
La sortie propose quatre formats, toujours sur le **contenu affiché** (et jamais
sur les valeurs calculées en cache) :
- **CSV** (bouton `CSV`) — exporte la **sélection** quand une plage de plusieurs
cellules est active (le nom du fichier reprend la plage, ex.
`Fruits-A1B2.csv`), sinon la feuille entière ;
- **Markdown** et **HTML** (menu **Exporter**) — tableau markdown ou document
HTML autonome, mêmes règles de sélection ;
- **Imprimer** (menu **Exporter**) — imprime la feuille ou la sélection seule,
sans le ruban ni les panneaux de l'application.
Rien de tout cela ne modifie le classeur.
**Structure** — le menu **Structure** de la barre d'outils ajoute,
renomme, duplique ou supprime une feuille, et insère/supprime des lignes ou
colonnes autour de la cellule active (`PUT …/xlsx/structure`, backup
automatique et confirmation, comme pour l'édition des cellules).
**Styles et mise en page** — la lecture restitue couleurs de police et de
fond, gras/italique/souligné, alignements, cellules fusionnées et volets
figés ; l'ancrage de la zone figée est conservé au défilement.
**Mise en forme** — le bouton **Mise en forme** ouvre un menu qui agit sur la
**sélection courante** (une cellule ou une plage) :
- **caractère** — gras, italique, souligné, effacer la mise en forme ;
- **alignement** — gauche, centré, droite ;
- **couleurs** — couleur de police et couleur de fond (sélecteur natif, aucune
palette imposée) ;
- **format de nombre** — général, nombre, pourcentage, devise, date, texte ;
- **structure** — fusionner / défusionner les cellules, figer / libérer les
volets, largeur de colonne, hauteur de ligne (fusionner exige une vraie
plage).
L'écriture passe par `PUT …/xlsx/style`, avec les mêmes garanties que l'édition
des cellules : backup automatique, écriture atomique, confirmation si
l'opération détruirait des éléments non préservables (graphiques, valeurs
calculées en cache…) et **détection d'un écrivain externe** (`If-Match` →
message « Réessayer »). Un `.csv` ne portant pas de mise en forme, le bouton
n'y est pas proposé (il est également absent d'une grille en lecture seule).
La lecture restitue par ailleurs les couleurs, polices, cellules fusionnées et
volets figés du fichier ; l'ancrage de la zone figée est conservé au défilement.
Les **commentaires**, liens hypertexte, validation de données, mise en forme
conditionnelle et bordures restent hors périmètre.
**Formats de fichiers** — `.xlsm` s'édite comme un `.xlsx` et ses
**macros sont préservées** à l'enregistrement ; `.xls` et `.ods` s'affichent
en **lecture seule** ; un `.csv` s'ouvre dans la même grille et se réécrit
conformément à la RFC 4180 (les guillemets et séparateurs sont
échappés).
**macros sont préservées** à l'enregistrement (y compris le chargement des
lignes au-delà du plafond) ; `.xls` et `.ods` s'affichent en **lecture
seule** ; un `.csv` s'ouvre dans la même grille et se réécrit conformément à
la RFC 4180 (les guillemets et séparateurs sont échappés). Le **séparateur
du CSV est détecté** (`;`, `,` ou tabulation) à la lecture et **réutilisé à
l'enregistrement** : un fichier exporté par Excel en français (point-virgule)
s'affiche donc en colonnes distinctes et le reste après édition.
**Tableau de bord** — le bouton **Tableau de bord** ouvre un **inspecteur
latéral droit** (la grille reste visible à côté) qui liste les plages
@@ -253,8 +319,13 @@ numériques) avec quelques chiffres clés. Cliquer une **plage nommée**
sélectionne sa première cellule dans la grille, et le panneau est
**redimensionnable**. L'en-tête de l'inspecteur offre
aussi un accès direct à l'**assistant IA**, qui peut ensuite exploiter ces
plages (outils `list_xlsx_sheets`, `xlsx_to_markdown`, `update_xlsx_cells`,
`append_xlsx_rows`).
plages. Ses outils couvrent désormais les **trois formats édités**
(`.xlsx`, `.xlsm`, `.csv`) : `list_xlsx_sheets` et `xlsx_to_markdown` pour lire,
`search_workbook` (recherche dans toutes les feuilles, comptée par feuille),
`analyze_range` (agrégats — nombre, somme, moyenne, min, max — d'une plage A1),
`update_xlsx_cells` et `append_xlsx_rows` pour modifier, et
`edit_xlsx_structure` pour la structure (ajouter/renommer/dupliquer/supprimer
une feuille, insérer/supprimer des lignes ou des colonnes).
### Limites
@@ -262,14 +333,33 @@ plages (outils `list_xlsx_sheets`, `xlsx_to_markdown`, `update_xlsx_cells`,
sous une feuille plus grande, le bouton **« Charger la suite »** (ou le
défilement vers le bas du tableau) ajoute les lignes suivantes par
fenêtres de 500 — elles deviennent aussitôt éditables et
sauvegardables.
sauvegardables. Le chargement paresseux est **vertical uniquement** :
l'axe des colonnes reste tronqué à 40 (les colonnes au-delà ne sont ni
affichées ni exportées).
- Un **format de nombre personnalisé** (devise, pourcentage…) est signalé
par une police à chasse fixe, mais la valeur reste affichée brute.
- L'application de **styles** depuis la visionneuse (mettre en gras,
colorer) n'est pas proposée — seuls les styles existants sont rendus.
par une police à chasse fixe à la lecture ; depuis le bouton **Mise en
forme**, appliquer un format ne change que le format de la cellule, **pas**
la valeur affichée (aucun recalcul n'est fait, cf. les limites d'export
ci-dessous).
- `.xls` et `.ods` restent en lecture seule (convertir vers `.xlsx` pour
éditer) ; les macros d'un `.xlsm` sont conservées mais ne s'exécutent
pas dans ObsiGate.
- La **poignée de recopie** (fill), la multi-sélection `Ctrl+clic` et le
glisser-déposer de lignes/colonnes ne sont pas proposés ; un collage de
plusieurs cellules s'annule **cellule par cellule** (`Ctrl+Z` répété).
- Les exports (CSV, Markdown, HTML, impression) reflètent ce qui est **affiché** :
une feuille tronquée s'exporte tronquée, et les formules sortent telles
qu'enregistrées (aucune valeur calculée n'est recalculée). Un classeur reste
la source de vérité : utilisez **Charger la suite** pour exporter au-delà du
plafond.
- **Aucun moteur de formule** : ObsiGate lit et écrit les formules telles
qu'Excel les a enregistrées, sans jamais les recalculer. Une saisie
commençant par `=` ou `@` est stockée comme **texte** (garde anti-DDE)
tant que le bouton `f(x)` n'est pas activé ; l'enregistrement vous le
signale par un message. Excel reste la référence pour les valeurs calculées.
- L'**annulation** couvre l'édition, l'effacement, le tri/filtre et les
actions de structure ; en revanche une **suppression** (feuille, ligne,
colonne) n'est pas annulable, faute d'inverse.
---
+8
View File
@@ -203,6 +203,10 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-093* | Le job CI `security` échoue : `pip-audit` bloque sur deux DoS de ressources dans `pypdf` 6.16.0 (PYSEC-2026-3910, PYSEC-2026-3911) — et le plancher `pypdf>=4.0` ne les corrigeait pas, car l'image Act du runner embarque 6.16.0 *préinstallé* dans sa toolcache Python (`Requirement already satisfied` ⇒ jamais mis à niveau) | 🟢 corrigé | P0 | CI / sécurité | IA | `backend/requirements.txt`, `.gitea/workflows/ci.yml`, `tests/test_ci_workflow.py` | Run Gitea #1660, job `security` : `Found 2 known vulnerabilities, ignored 2 in 1 package` → `pypdf 6.16.0 PYSEC-2026-3910 6.16.1` / `PYSEC-2026-3911 6.16.1` | Plancher `pypdf>=6.16.1` (correctif des deux advisories), commenté pour expliquer la contrainte de la toolcache. Ajout de `tests/test_ci_workflow.py::TestDependencySecurityFloors`, qui verrouille les planchers de sécurité (`pypdf`, `pyjwt`) et interdit qu'ils retombent sous le correctif | Les deux advisories sont des **consommations de ressources non contrôlées** (PDF à outlines multiples ou à nombreux XForm réutilisés) et sont donc **atteignables** par ObsiGate, dont `backend/pdf_reader.py` extrait le texte et parcourt les outlines de PDF fournis par l'utilisateur. Contre-preuve : plancher remis à `>=4.0` → le garde-fou échoue. pip-audit local : 6.16.1, 6.16.2 et 6.19.0 sans vulnérabilité connue. Correction découverte en lisant le log du job (`/actions/runs/1660/jobs/5541/logs`, accessible sans token) — le log de l'étape Semgrep collé précédemment datait d'un run antérieur |
| *BUG-094* | Feuille `.xlsx` vide ou nouvellement ajoutée : impossible d'y saisir une valeur et d'y insérer une ligne/colonne — la feuille s'affiche « Feuille vide » sans aucune cellule | 🟢 corrigé | P1 | tableur Excel / UX | IA | `backend/xlsx_reader.py::render_sheets`, `frontend/js/viewer.js::renderXlsxViewer` | Ajouter une feuille (`PUT …/xlsx/structure` `sheet_add`) puis tenter de saisir A1 ou d'insérer une ligne/colonne | `render_sheets()` remplace une grille vide par un quadrillage vierge 20×8 (constantes `EMPTY_SHEET_ROWS`/`EMPTY_SHEET_COLS`) aux vraies coordonnées A1 ; la visionneuse retombe sur `parseRef(activeRef) || {row:1,col:1}` pour que le menu Structure propose toujours insérer/supprimer ligne et colonne. Contre-preuve : `TestXlsxDisplay::test_empty_sheet_renders_an_editable_blank_grid` (sans le correctif : « Feuille vide » sans `data-cell`) | Le classeur n'était pas en cause : seule la **représentation HTML** était vide, donc aucun `td` à sélectionner → aucune cellule active → aucune action de structure possible. Vérifié : `test_xlsx_viewer.py` 59 passed, `xlsx-viewer.test.mjs` 52/52 |
| *BUG-095* | Le job CI `security` échoue : `pip-audit` bloque sur CVE-2026-102274 dans `pyjwt` 2.13.0 (correctif 2.14.0) — le plancher `>=2.13.0` (BUG-091) est désormais sous le dernier correctif | 🟢 corrigé | P1 | CI / sécurité | IA | `backend/requirements.txt`, `.gitea/workflows/ci.yml`, `tests/test_ci_workflow.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | Run Gitea du push v2.44.0, job `security` : `Found 1 known vulnerability, ignored 2 in 1 package` → `pyjwt 2.13.0 CVE-2026-102274 2.14.0` | Plancher `pyjwt[crypto]>=2.14.0` (au-dessus de la version préinstallée de la toolcache du runner, sinon `pip` répond « already satisfied »). Garde-fou `TestDependencySecurityFloors` : `FLOORS["pyjwt"]` porté à `(2, 14, 0)` — contre-preuve : plancher remis à `2.13.0` → test rouge. Commentaire de la job `security` mis à jour | Le plancher de BUG-091 (2.13.0) corrigeait PYSEC-2026-178 mais est lui-même vulnérable depuis. Même mécanisme que BUG-093 (pypdf) : un plancher de sécurité doit rester au-dessus du dernier correctif. Vérifié : `test_ci_workflow.py` 9 passed ; pip-audit local OK (pyjwt 2.15.1 ≥ 2.14.0) |
| *BUG-096* | [🟡 IMPORTANT] Enregistrer un `.csv` depuis la visionneuse échoue : `TypeError` sur `sheets[…].name` (aucun `PUT …/csv/save` émis) | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/viewer.js` (`renderXlsxViewer`, construction des jobs de sauvegarde) | Ouvrir un `.csv` dans ObsiGate, modifier une cellule, cliquer **Enregistrer** | `frontend/js/viewer.js` : le job de sauvegarde ne lit plus le nom de feuille dans `xlsx_sheets` (absent du payload CSV — l'origine du `TypeError`) — repli feuille → `data.title`/`data.path`. Test JSDOM « saving an edited csv PUTs /csv/save (payload without xlsx_sheets) ». Vérifié : suite 1490 passed / 6 skipped, `xlsx-viewer.test.mjs` 61/61 | Défaut #156 A1. En mode CSV la réponse de lecture n'a pas de `xlsx_sheets` (`backend/routers/files_read.py`) : `sheets` est vide et `sheets[Number(panel.dataset.sheet)].name` lève un `TypeError` **avant** le `try`, donc aucun `PUT /api/file/{vault}/csv/save` n'est émis (le service `save_csv_cells()` et l'endpoint sont, eux, corrects). **Reproduit en JSDOM le 2026-09-29** (harnais de `tests/frontend/xlsx-viewer.test.mjs`) : `Cannot read properties of undefined (reading 'name')`, 0 requête d'API enregistrée. Analyse et critères : `docs/features/xlsx-editor-completeness.md` |
| *BUG-097* | [🟡 IMPORTANT] Feuille `.xlsm` tronquée : « Charger la suite » échoue en **415** (`GET …/xlsx/sheet` n'accepte que `.xlsx`) alors que le `.xlsm` est éditable | 🟢 corrigé | P1 | ⚙️ backend + 🔌 api | IA | `backend/routers/files_read.py` (`api_file_xlsx_sheet`), `frontend/js/viewer.js` (`wireLazyRows`) | Ouvrir un `.xlsm` de plus de 500 lignes puis cliquer le pied « Charger la suite » (ou approcher du bas du tableau) | `backend/routers/files_read.py` : `GET …/xlsx/sheet` accepte `.xlsx` **et** `.xlsm` (autres formats → 415 inchangé). Tests `TestXlsmEditable::test_sheet_window_is_served_for_xlsm` + `test_sheet_window_still_refuses_other_formats` | Défaut #156 A2. `.xlsm` est servi éditable (`files_read.py:491-512`, pas de `xlsx_readonly`) donc la visionneuse câble le chargement paresseux, mais `api_file_xlsx_sheet` refuse tout ce qui n'est pas `.xlsx` (`files_read.py:272`). Analyse et critères : `docs/features/xlsx-editor-completeness.md` |
| *BUG-098* | [🟡 IMPORTANT] Délimiteur CSV figé `,` : un `.csv` français (`;`) s'affiche en une seule colonne et se réécrit dans un autre format | 🟢 corrigé | P2 | ⚙️ backend | IA | `backend/xlsx_reader.py` (`render_csv_table`, `delimiter=","` par défaut), `backend/routers/files_read.py:551`, `backend/services/mutations.py` (`save_csv_cells`) | Ouvrir un CSV `;` (export Excel FR) dans ObsiGate : une seule colonne ; éditer une cellule puis enregistrer : le fichier est réécrit en `,` | `backend/xlsx_reader.py` (`sniff_csv_delimiter`) + `backend/services/mutations.py::save_csv_cells` : délimiteur détecté et **réutilisé**. Tests `TestCsvDelimiter` + lecture/écriture/guillemets point-virgule | Défaut #156 A3. `render_csv_table(raw)` est appelé sans délimiteur et `save_csv_cells()` re-parse en `,`, alors que l'export CSV de la visionneuse écrit en `;`. Traitement prévu : détection `;`/`,`/tab partagée lecture/écriture/export. Analyse : `docs/features/xlsx-editor-completeness.md` |
| *BUG-099* | [🔵 MINEUR] Sonde de perte plafonnée (8 Mo, budget global) : risque de perte **silencieuse** des valeurs calculées en cache au-delà du budget | 🟢 corrigé | P2 | ⚙️ backend | IA | `backend/xlsx_reader.py` (`_MAX_PROBE_BYTES`, `_has_cached_formulas`, `inspect_workbook`) | Constituer un classeur dont le XML de feuille dépasse 8 Mo **avant** la première formule cachée, puis l'éditer : la garde 409 `xlsx_lossy_content` ne se déclenche pas | `backend/xlsx_reader.py` : budget **par feuille** (4 Mo) + plafond global (32 Mo) ; `_scan_cached_formulas()` renvoie `(found, unverified)` et `inspect_workbook()` ajoute `cached_values_unverified` (libellé i18n FR/EN). Contre-preuve : budget épuisé → signalé au lieu de `[]`. Tests `TestXlsxCachedValueProbe` (3) | Défaut #156 A4, **analyse statique (non reproduit)**. Le budget est partagé entre toutes les feuilles : au-delà, `cached_values` n'est pas détecté et l'écriture détruit ces valeurs sans avertissement (risque n°1 de #153). Traitement prévu : budget par feuille + signal d'incertitude pour que la garde reste prudente. Analyse : `docs/features/xlsx-editor-completeness.md` |
| | | | | | | | | | |
### TODOs techniques (améliorations / nouvelles tâches)
@@ -301,6 +305,10 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-09-27 | #87 T6, T7, T8 | Sécurité (fin #87) | `backend/requirements.txt`, `backend/{render,export}.py`, `backend/tools/documents.py`, `backend/auth/router.py`, `backend/main.py`, `semgrep-rules/` (nouveau), `.gitea/workflows/ci.yml`, `tests/test_i18n_parity.py` (nouveau), `tests/test_auth_api.py`, `tests/test_security_headers.py`, `docker-compose.yml`, `.env.example`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/ISSUES_TODOLIST.md` | **T6** : dépendances qualifiées (mistune 3.3.3, multipart 0.0.31, weasyprint 70, mcp 1.28.1, fastapi 0.141.1 + starlette 1.7.0, setuptools 84 ; `cast` mistune 3 sites) — suite 1359 passed, ruff/mypy 0, **`pip-audit` bloquant 0 vuln** (exception ecdsa/Minerva documentée : sans fix, HS256 only). **T7** : **semgrep bloquant** local 8 règles, 0 finding (trivy écarté : réseau). **T8** : Secure auto + `X-Forwarded-Proto` (`TRUST_PROXY`), warning affiné, CORS same-origin explicite, `style-src` résiduel assumé (189+343 sites) ; TODO exemple purgé, locales FR/EN 2213 parité testée, `npm audit` 0. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-29 | BUG-094 | Correction | `backend/xlsx_reader.py`, `frontend/js/viewer.js`, `tests/test_xlsx_viewer.py`, `tests/frontend/xlsx-viewer.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-094 — une feuille vide ou nouvellement ajoutée devient éditable et manipulable.** `render_sheets()` substitue une grille vierge 20×8 (`EMPTY_SHEET_ROWS`/`EMPTY_SHEET_COLS`) quand la feuille ne porte aucune cellule, avec de vraies coordonnées A1 ; la visionneuse retombe sur `parseRef(activeRef) || {row:1,col:1}` pour que le menu Structure propose toujours insérer/supprimer ligne et colonne. Contre-preuve : `TestXlsxDisplay::test_empty_sheet_renders_an_editable_blank_grid` (sans le correctif : « Feuille vide » sans `data-cell`). Vérifié : `test_xlsx_viewer.py` 59 passed, `xlsx-viewer.test.mjs` 52/52. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-29 | BUG-095 | Correction CI / sécurité | `backend/requirements.txt`, `.gitea/workflows/ci.yml`, `tests/test_ci_workflow.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **Le job `security` repasse au vert** : `pyjwt` 2.13.0 est vulnérable (CVE-2026-102274, correctif 2.14.0) et le plancher de BUG-091 (`>=2.13.0`) était donc sous le dernier correctif. Plancher porté à `pyjwt[crypto]>=2.14.0` (au-dessus de la toolcache du runner, sinon `pip` répond « already satisfied »), garde-fou `TestDependencySecurityFloors` mis à jour (contre-preuve : plancher remis à 2.13.0 → test rouge), commentaire de la job `security` aligné. Vérifié : `test_ci_workflow.py` 9 passed, pip-audit local OK (pyjwt 2.15.1). | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-29 | BUG-096 → BUG-099 (#156 A1-A4) | Correction | `frontend/js/viewer.js`, `frontend/locales/{fr,en}.json`, `backend/xlsx_reader.py`, `backend/services/mutations.py`, `backend/routers/files_read.py`, `tests/frontend/xlsx-viewer.test.mjs`, `tests/test_xlsx_viewer.py`, `tests/test_xlsx_formats.py`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/features/xlsx-editor-completeness.md`, `docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md`, `docs/ISSUES_TODOLIST.md` | **Les 4 défauts P0 du tableur sont corrigés.** (BUG-096) l'enregistrement d'un `.csv` depuis la visionneuse ne lit plus le nom de feuille dans `xlsx_sheets` (absent du payload CSV) : le `TypeError` partait **avant** tout appel réseau, aucun `PUT …/csv/save` n'était émis. (BUG-097) `GET …/xlsx/sheet` accepte `.xlsx` **et** `.xlsm` — le chargement des lignes au-delà du plafond fonctionne pour les classeurs macro. (BUG-098) `sniff_csv_delimiter()` détecte `;`/`,`/tabulation et `save_csv_cells()` réutilise le délimiteur : un CSV français s'affiche en colonnes distinctes et **le reste** après édition. (BUG-099) sonde de perte **par feuille** (4 Mo) + plafond global (32 Mo) et clé `cached_values_unverified` quand le budget est épuisé — plus de perte silencieuse possible des valeurs calculées. Tests : `TestXlsxCachedValueProbe` (3, contre-preuve : budget épuisé signalé au lieu de `[]`), `TestCsvDelimiter` + 3 tests CSV, fenêtre `.xlsm` (+ refus `.csv`), 1 test JSDOM CSV. Vérifié : suite **1490 passed / 6 skipped**, ruff 0, mypy 0 (102 fichiers), `xlsx-viewer.test.mjs` 61/61, validate-imports 40 modules, unit 12/12, i18n parity. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-29 | #156 A5-A7 (P1) | Fonctionnalité (sans nouveau défaut) | `frontend/js/viewer.js`, `frontend/js/xlsx/command-bar.js`, `frontend/locales/{fr,en}.json`, `tests/frontend/xlsx-viewer.test.mjs`, `tests/e2e/xlsx-viewer.spec.js`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/features/xlsx-editor-completeness.md`, `docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md` | **P1 livré** — (A5) presse-papiers de plage : copier/couper/coller un bloc TSV au clavier et au menu contextuel, presse-papiers interne + miroir système, remplissage multi-cellules, insertion **texte** échappée, débordement signalé, annulable ; (A6) clavier complet (`Ctrl+S`/`Ctrl+A`/`Suppr`/`F2`/`Ctrl+Home|End`/`Home|End`/`PgUp|PgDn`/`Ctrl+flèches`/`Maj+Entrée`) ; (A7) zone Nom éditable (« Atteindre ») + liste de fonctions. Vérifié : JSDOM `xlsx-viewer.test.mjs` 84/84 (20 nouveaux), E2E Playwright. | ✅ livré |
| 2026-09-30 | #156 A8-A14 (P2 + P3) — clôture du backlog | Fonctionnalité (sans nouveau défaut) | `backend/services/mutations.py`, `backend/routers/files_read.py`, `backend/routers/files_write.py`, `backend/schemas.py`, `backend/xlsx_reader.py`, `backend/openapi_docs.py`, `backend/tools/{spreadsheets,schemas,labels}.py`, `frontend/js/viewer.js`, `frontend/js/xlsx/command-bar.js`, `frontend/locales/{fr,en}.json`, `frontend/style.css`, `tests/test_xlsx_styles.py`, `tests/test_xlsx_viewer.py`, `tests/test_spreadsheet_tools.py`, `tests/test_xlsx_formats.py`, `tests/frontend/xlsx-viewer.test.mjs`, `tests/e2e/xlsx-viewer.spec.js`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/features/xlsx-editor-completeness.md`, `docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md`, `README.md`, `README.fr.md` | **P2 + P3 livrés, backlog #156 clôturé** — (A8) **mise en forme en écriture** : bouton Mise en forme (gras/italique/souligné, alignements, couleurs via sélecteur natif, formats de nombre, fusion/défusion, volets figés, largeur/hauteur) et nouvelle route `PUT …/xlsx/style` (`mutate_xlsx_style` : verrou, backup, swap atomique, garde de perte, `If-Match`) ; (A9) décision « pas de moteur de formule » **annoncée dans l'UI** ; (A10) undo/redo unifié avec piles par fichier conservées au re-rendu ; (A11) export de la sélection + Markdown + HTML + impression et recherche sur **toutes** les feuilles (`n/m · k feuilles`) ; (A12) concurrence optimiste `If-Match` → **409** `conflict`/`stale_revision`, retry qui relit ; (A13) cache de `read_workbook_meta()` par `(chemin, mtime_ns, taille)` (LRU 8) ; (A14) outils IA `.xlsm`/`.csv` + `search_workbook`, `analyze_range`, `edit_xlsx_structure` (confirmation conservée). Vérifié : suite **1525 passed / 6 skipped**, ruff 0, mypy 0 (102 fichiers), JSDOM `xlsx-viewer.test.mjs` 107/107, validate-imports + unit, i18n parity 2355/2355, E2E `xlsx` 10/10 + `Split View` 37 + `XSS` 2. | ✅ livré |
| 2026-09-29 | #156 (audit), BUG-096 → BUG-099 | Enregistrement (audit statique + 1 repro JSDOM) | `docs/ROADMAP.md`, `docs/features/xlsx-editor-completeness.md` (nouveau), `docs/ISSUES_TODOLIST.md`, `CHANGELOG.md` | **Audit de complétude de l'éditeur Excel → ouverture de l'item #156** (P0 défauts · P1 presse-papiers/clavier · P2 mise en forme/calcul/undo · P3 sortie/robustesse/perf) avec fiche dédiée. **4 défauts** enregistrés : BUG-096 (enregistrement `.csv` en `TypeError`, **reproduit en JSDOM** — `Cannot read properties of undefined (reading 'name')`, aucun `PUT …/csv/save`), BUG-097 (lazy-load `.xlsm` → 415), BUG-098 (délimiteur CSV `,` figé vs export `;`), BUG-099 (sonde de perte plafonnée à 8 Mo, risque théorique). Manques fonctionnels recensés : presse-papiers de plage, clavier complet, zone Nom éditable, mise en forme en écriture, calcul, undo/redo unifié, export/impression, concurrence optimiste, cache des métadonnées, outils IA `.xlsm`/`.csv`. **Aucun code modifié** (documentation seule). | 🔴 ouvert (à traiter) |
---
+55 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.44.1 | **Dernière mise à jour :** 2026-09-29
> **Version :** 2.45.0 | **Dernière mise à jour :** 2026-09-30
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
> vers les fonctionnalités livrées.
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
@@ -137,6 +137,55 @@
---
## ✅ Terminé — Tableur Excel (complétude)
### 156. Éditeur Excel — complétude fonctionnelle (presse-papiers, mise en forme, calcul, robustesse)
- **Effort :** 15-22 jours (P0 2-3 j ✅ · P1 4-6 j ✅ · P2 5-7 j ✅ · P3 4-6 j ✅) | **Impact :** 🟡
- **Statut :** ✅ **livré le 2026-09-30** — **P0** (4 défauts **BUG-096 → BUG-099** corrigés),
**P1** (**A5-A7** : presse-papiers de plage, clavier complet, zone Nom éditable), **P2**
(**A8** mise en forme en écriture, **A9** décision « pas de moteur de formule, annoncée dans
l'UI », **A10** undo/redo unifié conservé au re-rendu) et **P3** (**A11** export sélection/
Markdown/HTML/impression + recherche multi-feuilles, **A12** concurrence optimiste `If-Match`,
**A13** cache des métadonnées, **A14** outils IA `.xlsm`/`.csv` + recherche/analyse/structure)
- **Analyse, défauts, risques et critères d'acceptation :** [features/xlsx-editor-completeness.md](./features/xlsx-editor-completeness.md)
- **Description :** #153 a rendu l'éditeur **correct** sur la grille de valeurs, #154/#155 l'ont
rendu **convivial** (ruban, inspecteur, undo/redo des cellules, sélection et menu contextuel
type Excel). Il reste l'écart avec un vrai éditeur tableur : coller une **plage**, écrire la
**mise en forme**, **calculer**, **sortir** le résultat, et détecter un écrivain concurrent.
La fiche porte l'audit ; les cases ci-dessous sont le **suivi de référence**.
- **Défauts corrigés le 2026-09-29 (cf. [ISSUES_TODOLIST.md](./ISSUES_TODOLIST.md)) :**
- ✅ BUG-096 — enregistrer un `.csv` depuis la visionneuse levait un `TypeError`
(`sheet: sheets[…].name` alors qu'un CSV n'a pas de `xlsx_sheets`) : aucun `PUT …/csv/save`
n'était émis (reproduit en JSDOM, corrigé + test) ;
- ✅ BUG-097 — feuille `.xlsm` tronquée : « Charger la suite » échouait en **415** —
`GET …/xlsx/sheet` accepte désormais `.xlsx` **et** `.xlsm` ;
- ✅ BUG-098 — délimiteur CSV détecté (`;`/`,`/tabulation) et réutilisé à l'écriture :
un CSV français s'affiche en colonnes et le reste après édition ;
- ✅ BUG-099 — sonde de perte **par feuille** + signal `cached_values_unverified` quand le
budget est épuisé : plus de perte silencieuse possible des valeurs calculées.
- **Sous-tâches :**
- **P0 — défauts (🔴, 2-3 j) — ✅ livré le 2026-09-29**
- [x] **A1** Sauvegarde `.csv` depuis la visionneuse — BUG-096 (test JSDOM : `PUT …/csv/save` émis)
- [x] **A2** Chargement paresseux des `.xlsm` — BUG-097 (`GET …/xlsx/sheet` accepte `.xlsx`/`.xlsm`)
- [x] **A3** Délimiteur CSV détecté et réutilisé à l'écriture — BUG-098 (`sniff_csv_delimiter`)
- [x] **A4** Sonde de perte par feuille + signal `cached_values_unverified` — BUG-099
- **P1 — presse-papiers & clavier (🟡, 4-6 j) — ✅ livré le 2026-09-29**
- [x] **A5** Presse-papiers de plage — copier/couper/coller un bloc TSV (`Ctrl+C`/`Ctrl+X`/`Ctrl+V`, menu contextuel), presse-papiers interne + système, remplissage multi-cellules (tests JSDOM : `A1:B2` → `D5:E6`)
- [x] **A6** Clavier complet — `Ctrl+S`, `Ctrl+A`, `Suppr`, `F2`, `Ctrl+Home/End`, `Home`/`End`, `PgUp/PgDn`, `Ctrl+flèches`, `Maj+Entrée` (saut de ligne en cellule)
- [x] **A7** Zone Nom éditable (« Atteindre » : `B12`, `A1:B3`, `Feuille!A1`) + aide à la saisie des fonctions
- **P2 — mise en forme, calcul, undo (🟡, 5-7 j) — ✅ livré le 2026-09-30**
- [x] **A8** Mise en forme en écriture — bouton **Mise en forme** (gras/italique/souligné, alignements, couleurs, formats de nombre, fusions, volets figés, largeur/hauteur) via `PUT …/xlsx/style` (verrou, backup, garde de perte, `If-Match`)
- [x] **A9** Calcul — **décision documentée** : pas de moteur de formule, l'enregistrement annonce « enregistrée comme texte » (la garde anti-DDE reste)
- [x] **A10** Undo/redo unifié (cellules, effacement, tri/filtre, structure) et conservé au re-rendu (`_xlsxHistory` par fichier)
- **P3 — sortie, robustesse, performances (🟢, 4-6 j) — ✅ livré le 2026-09-30**
- [x] **A11** Sortie & recherche : export de la sélection / Markdown / HTML / impression, recherche sur toutes les feuilles (compteur `n/m · k feuilles`)
- [x] **A12** Concurrence optimiste inter-processus (`ETag`/`If-Match`, **409** réparable, bouton « Réessayer » qui relit)
- [x] **A13** Performances : cache des métadonnées par `(chemin, mtime, taille)` (LRU 8), invalidé à chaque écriture (axe des colonnes > `MAX_COLS` : hors périmètre)
- [x] **A14** Outils IA étendus — `.xlsm`/`.csv` acceptés, `search_workbook`, `analyze_range`, `edit_xlsx_structure`
---
## ⚪ Backlog — Priorité 4 (P4)
### 73. Synchronisation multi-appareils — Obsidian Sync compatible
@@ -187,6 +236,7 @@
| 152 | Viewer XLSX — affichage multi-feuilles, édition des cellules, téléchargement | 2.27.0 | [archive](./archive/COMPLETED_v1-v2.md) |
| 154 | Tableur — Refonte UI/UX (ruban groupé, onglets permanents, badges d'état, inspecteur droit, undo/redo) | 2.40.0→2.43.1 | [features/xlsx-ui-redesign.md](./features/xlsx-ui-redesign.md) |
| 155 | Tableur — Menu contextuel (clic droit / appui long) & sélection type Excel | 2.44.0 | [features/xlsx-context-menu.md](./features/xlsx-context-menu.md) |
| 156 | Tableur — Complétude éditeur : presse-papiers/clavier (A5-A7), mise en forme (A8), décision calcul (A9), undo unifié (A10), export + recherche multi-feuilles (A11), concurrence optimiste (A12), cache méta (A13), outils IA `.xlsm`/`.csv` (A14) + BUG-096 → BUG-099 | 2.45.0 | [features/xlsx-editor-completeness.md](./features/xlsx-editor-completeness.md) |
| BUG-047 | Versionnage — source unique `VERSION` + bump SemVer automatique au commit (hooks + tag) | 2.3.0 | [DEVELOPMENT_AND_RELEASES.md](./DEVELOPMENT_AND_RELEASES.md) |
| 90 | Barre d'actions du document — regroupement fonctionnel + spacers | 2.3.0 | [archive](./archive/COMPLETED_v1-v2.md) |
| 89 | Drag & drop complet de fichiers/dossiers & intégration Assistant IA | 2.3.0 | [features/drag-and-drop-ai.md](./features/drag-and-drop-ai.md) |
@@ -268,6 +318,7 @@
| ✅ Terminé | #153 Visionneuse & édition XLSX — complétude (A1-A17 **toutes livrées**, v2.27.0 → v2.39.0) | 0 jour restant |
| ✅ Terminé | #154 Refonte UI/UX tableur (A1-A5 **toutes livrées**, v2.40.0 → v2.43.1) | 0 jour restant |
| ✅ Terminé | #155 Ergonomie tableur — menu contextuel & sélection type Excel | 0 jour restant |
| ✅ Terminé | #156 Éditeur Excel — complétude — **P0-P3 ✅ livrés le 2026-09-30** (BUG-096 → BUG-099, A5-A14) | 0 jour restant |
| **Total chemin critique** | **#77 fin + #87** | **~4-6 jours** |
---
@@ -278,6 +329,9 @@
- **Ajout 2026-09-27 :** #153 ouvert à la suite de l'audit de la visionneuse XLSX (limitations, risques de perte de données, périmètre IA/recherche) — détail et critères dans [features/xlsx-viewer.md](./features/xlsx-viewer.md).
- **Ajout 2026-09-29 :** #154 ouvert — refonte UI/UX de la visionneuse/éditeur XLSX (audit UX, architecture cible, plan par lots) dans [features/xlsx-ui-redesign.md](./features/xlsx-ui-redesign.md) ; **livré en 5 lots** (ruban groupé, onglets permanents + « + », badges d'état, tokens de grille, dialogues thémés, inspecteur droit, undo/redo, extraction `frontend/js/xlsx/*`).
- **Ajout 2026-09-29 :** #155 livré — menu contextuel (clic droit / appui long) et sélection type Excel sur la grille ([features/xlsx-context-menu.md](./features/xlsx-context-menu.md)) ; au passage **BUG-094** corrigé (feuille vide/nouvelle désormais éditable, quadrillage vierge 20×8).
- **Clôture 2026-09-30 :** #156 **livré (P0 → P3)** — **A8** mise en forme en écriture (bouton **Mise en forme** : gras/italique/souligné, alignements, couleurs, formats de nombre, fusions, volets figés, largeur/hauteur — via `PUT …/xlsx/style`), **A9** décision « pas de moteur de formule, annoncée dans l'UI », **A10** undo/redo unifié conservé au re-rendu, **A11** export de la sélection / Markdown / HTML / impression + recherche sur toutes les feuilles, **A12** concurrence optimiste (`If-Match`, **409** réparable), **A13** cache des métadonnées par `mtime`, **A14** outils IA `.xlsm`/`.csv` + `search_workbook`/`analyze_range`/`edit_xlsx_structure` — détail dans [features/xlsx-editor-completeness.md](./features/xlsx-editor-completeness.md).
- **Ajout 2026-09-29 :** #156 **P1 livré** — **A5** presse-papiers de plage (copier/couper/coller un bloc, presse-papiers interne + système, entrées du menu contextuel, remplissage multi-cellules), **A6** clavier complet (`Ctrl+S`/`Ctrl+A`/`Suppr`/`F2`/`Ctrl+Home|End`/`PgUp|PgDn`/`Ctrl+flèches`/`Maj+Entrée`) et **A7** zone Nom éditable + aide à la saisie ; restent P2 (mise en forme, calcul, undo unifié) et P3 (sortie, concurrence optimiste, performances, outils IA).
- **Ajout 2026-09-29 :** #156 ouvert — audit de complétude de l'éditeur Excel : **4 défauts recensés** (BUG-096 enregistrement `.csv`, BUG-097 lazy-load `.xlsm`, BUG-098 délimiteur CSV, BUG-099 sonde de perte), **corrigés le jour même (P0 ✅)** avec tests de non-régression ; restent P1-P3 (presse-papiers, clavier, mise en forme, calcul, export, concurrence optimiste) puis presse-papiers de plage, clavier complet, mise en forme en écriture, calcul, undo/redo unifié, export/impression, concurrence optimiste — détail dans [features/xlsx-editor-completeness.md](./features/xlsx-editor-completeness.md).
- **Clôture #85 (v2.27.13) :** monolithe découpé (T1→T9), stores verrouillés + rate-limit SQLite (T10), fiche `docs/features/archi-refonte-85.md`.
- Les items P3/P4 ne sont pas ordonnés par priorité interne — à raffiner selon les retours utilisateurs.
- L'effort inclut le développement + tests unitaires + intégration CI, mais pas la documentation utilisateur.
+322
View File
@@ -0,0 +1,322 @@
# #156 — Éditeur Excel : complétude fonctionnelle (presse-papiers, mise en forme, calcul, robustesse)
> **Item de roadmap :** [#156 — Éditeur Excel — complétude fonctionnelle](../ROADMAP.md)
> **Origine :** audit de complétude demandé le 2026-09-29, après #153 (backlog tableur),
> #154 (refonte UI/UX) et #155 (menu contextuel & sélection)
> **Statut :** ✅ **livré le 2026-09-30** — P0 ✅ (BUG-096 → BUG-099), P1 ✅ (A5, A6, A7),
> P2 ✅ (A8, A9, A10) et P3 ✅ (A11, A12, A13, A14) ; ouverture 2026-09-29 (audit statique,
> A1 reproduit en JSDOM)
> **Effort estimé :** 15-22 jours (P0 2-3 j · P1 4-6 j · P2 5-7 j · P3 4-6 j) — **réalisé dans
> l'enveloppe annoncée**
> **Règle de maintenance :** la Roadmap porte les cases à cocher (suivi), cette fiche porte
> l'analyse, les défauts, les risques et les critères d'acceptation. **Ne pas dupliquer le détail.**
---
## 1. Objectif
#153 a rendu l'éditeur XLSX **correct sur le périmètre « grille de valeurs »** (lecture/écriture
gardées, styles en lecture, structure, formats `.xlsm`/`.xls`/`.ods`/`.csv`, indexation, outils IA,
tableau de bord). #154/#155 l'ont rendu **convivial** (ruban, inspecteur, undo/redo des cellules,
sélection et menu contextuel type Excel).
Reste l'écart avec ce qu'un utilisateur attend d'un **éditeur tableur** : coller une plage,
penser la mise en forme, calculer, sortir le résultat, et se protéger d'un autre écrivain.
Cet item recense **4 défauts** (A1 reproduit, A4 théorique) et les **manques fonctionnels**
correspondants.
## 2. Périmètre (couches touchées)
| Couche | Fichier | Rôle dans cet item |
|---|---|---|
| Lecture XLSX | `backend/xlsx_reader.py` | Fenêtres de lignes, délimiteur CSV, sonde de perte, métadonnées |
| Écriture XLSX | `backend/services/mutations.py` | Cellules, structure, **style (A8)**, coercion, `revision` (A12) |
| Endpoints | `backend/routers/files_read.py`, `files_write.py` | `xlsx/sheet`, `xlsx/save`, `xlsx/structure`, **`xlsx/style` (A8)**, `csv/save` |
| Outils IA | `backend/tools/spreadsheets.py` | Lecture/édition par l'assistant (`.xlsx`/`.xlsm`/`.csv`, recherche, analyse, structure — A14) |
| Visionneuse | `frontend/js/viewer.js` + `frontend/js/xlsx/*` | Presse-papiers, clavier, barre de formule, undo/redo, export |
| Styles | `frontend/style.css` | Affordances presse-papiers/mise en forme (variables CSS) |
| i18n | `frontend/locales/{fr,en}.json` | Tout libellé nouveau, FR **et** EN |
| Tests | `tests/test_xlsx_*.py`, `tests/frontend/xlsx-viewer.test.mjs`, `tests/e2e/xlsx-viewer.spec.js` | Non-régression par sous-tâche |
## 3. Audit
### 3.1 Défauts recensés (P0) — ✅ corrigés le 2026-09-29
#### ✅ A1 → BUG-096 — Enregistrer un `.csv` depuis la visionneuse échoue
`frontend/js/viewer.js` construit l'unique job de sauvegarde ainsi :
```js
const jobs = panelEls.map((panel) => {
const cells = {};
panel.querySelectorAll("td.xlsx-dirty").forEach((td) => { cells[td.dataset.cell] = td.textContent; });
return { sheet: sheets[Number(panel.dataset.sheet)].name, cells }; // ← ligne 1757
}).filter((job) => Object.keys(job.cells).length);
```
En mode CSV, la réponse de lecture (`backend/routers/files_read.py:548-556`) **ne contient pas
`xlsx_sheets`** : `sheets` est donc vide et `sheets[...].name` lève un `TypeError`. Vérifié le
2026-09-29 avec le harnais JSDOM (`tests/frontend/xlsx-viewer.test.mjs`) : après édition d'une
cellule CSV et clic sur **Enregistrer**, `TypeError: Cannot read properties of undefined
(reading 'name')` et **aucun** `PUT /api/file/{vault}/csv/save` n'est émis. Le service backend
`save_csv_cells()` et l'endpoint sont pourtant corrects : le défaut est purement côté visionneuse.
*Cause racine :* le mode CSV réutilise un panneau unique (`data-sheet="0"`) sans feuille
correspondante dans `sheets` ; le chemin d'enregistrement n'a pas été adapté comme l'ont été les
onglets, la structure et le tableau de bord (qui, eux, testent `isCsv`).
**Correctif (2026-09-29) :** le nom de feuille est résolu avec repli
(`sheets[idx]?.name || sheets[0]?.name || data.title || data.path`). Test JSDOM : un CSV monté
**sans** `xlsx_sheets` émet un unique `PUT …/csv/save`.
#### ✅ A2 → BUG-097 — `.xlsm` éditable : « Charger la suite » renvoie 415 au-delà de 500 lignes
Un `.xlsm` est servi **éditable** (`backend/routers/files_read.py:491-512`, pas de
`xlsx_readonly`) : la visionneuse câble donc le chargement paresseux. Mais
`GET /api/file/{vault}/xlsx/sheet` refuse **tout ce qui n'est pas `.xlsx`** (`files_read.py:272`,
**415**). Une feuille `.xlsm` de plus de 500 lignes affiche donc le pied « Charger la suite » dont
chaque clic échoue (toast d'erreur), alors que la limitation n'est pas annoncée comme telle pour
ce format.
**Correctif (2026-09-29) :** `GET …/xlsx/sheet` accepte `.xlsx` **et** `.xlsm` (les autres
formats restent en 415). Tests : fenêtre servie pour un `.xlsm`, refus maintenu pour un `.csv`.
#### ✅ A3 → BUG-098 — Délimiteur CSV figé : un `.csv` français s'ouvre en une seule colonne
`render_csv_table(raw)` utilise le délimiteur par défaut `,` (`backend/xlsx_reader.py:784`),
appelé sans argument (`backend/routers/files_read.py:551`) — tandis que l'**export** CSV de la
visionneuse écrit du `;` et un BOM UTF-8. Un CSV produit par Excel/Sheets en locale française
(`;`) s'affiche donc en une colonne. Le même `,` est utilisé par `save_csv_cells()` pour re-parser
et réécrire le fichier : l'édition peut re-sérialiser dans un format différent de la source.
**Correctif (2026-09-29) :** `sniff_csv_delimiter()` (Sniffer, repli par décompte sur la
première ligne, puis `,`) est partagé par le rendu et la réécriture — `save_csv_cells()`
réutilise le délimiteur détecté. Tests : lecture, écriture et champs entre guillemets contenant
le séparateur.
#### ✅ A4 → BUG-099 — Sonde de perte plafonnée à 8 Mo : perte silencieuse possible
`inspect_workbook()` détecte les valeurs calculées en cache via un budget global
`_MAX_PROBE_BYTES = 8_000_000` (`backend/xlsx_reader.py:69`) partagé entre toutes les feuilles
(`_has_cached_formulas`). Sur un classeur dont le XML des feuilles dépasse ce budget **avant** la
première formule cachée, `xlsx_lossy_features` ne contient pas `cached_values` : le `409`
`xlsx_lossy_content` n'est pas déclenché et l'enregistrement détruit ces valeurs **sans
avertissement** — exactement le risque n°1 de #153. À ce stade, risque **théorique** (non
reproduit).
**Correctif (2026-09-29) :** budget **par feuille** (4 Mo) + plafond global (32 Mo) ;
`_scan_cached_formulas()` renvoie `(found, unverified)` et `inspect_workbook()` ajoute
`cached_values_unverified` (libellé i18n FR/EN) : la sauvegarde demande une confirmation au lieu
de passer en silence. Trucs de test : budgets monkeypatchés. Contre-preuve : budget épuisé →
signalé au lieu de `[]` (comportement antérieur).
### 3.2 Fonctions manquantes (écart vs Excel / Google Sheets)
**Presse-papiers et sélection** — ✅ traité en P1 (A5)
- ✅ Coller une **plage** : le handler forcait tout sur une ligne
(`viewer.js` : `replace(/\r?\n/g, " ")`) — coller un bloc TSV/CSV depuis Excel écrasait une seule
cellule. Le collage découpe désormais le bloc (`parseMatrix`) et remplit la plage à partir de la
cellule active (`fillFrom`), 1×1 restant un remplacement de cellule.
- ✅ Copier / Couper / Coller de plage côté application : `Ctrl+C`/`Ctrl+X`/`Ctrl+V` (événements
`copy`/`cut`/`paste`) **et** entrées du menu contextuel ; une sélection de texte dans la cellule
éditée garde le comportement natif.
- Toujours absents : poignée de recopie (fill), multi-sélection `Ctrl+clic`, glisser-déposer de
lignes/colonnes, copie d'une image/du HTML.
**Clavier / navigation** — ✅ traité en P1 (A6, A7)
- ✅ `Ctrl+S`, `Suppr`, `F2`, `Ctrl+Home/End`, `Home`/`End`, `PgUp/PgDn`, `Ctrl+flèches`, `Ctrl+A`
sont câblés dans un handler unique de la grille ; une cellule reste en mode « navigation »
jusqu'à la saisie (ou `F2`), sinon `Suppr`/`Home`/`End` voleraient une touche du curseur.
- ✅ `Maj+Entrée` insère un saut de ligne **dans** la cellule (texte uniquement).
- ✅ La **zone Nom** est un champ éditable (« Atteindre ») : `B12`, `A1:B3`, `Feuille!A1`.
- ✅ Aide à la saisie : la barre de formule propose les noms de fonctions courants (`<datalist>`,
liste localisée FR/EN). L'autocomplétion contextuelle reste à faire si un moteur de formules
arrive (A9).
**Mise en forme en écriture** — ✅ traitée (A8, 2026-09-30)
- ✅ Un bouton **Mise en forme** (`#xlsx-format-btn`, classeurs éditables non-CSV) agit sur la
**sélection** via `PUT …/xlsx/style` : gras / italique / souligné / effacer, alignement
gauche-centré-droite, couleur de police et de fond (`<input type="color">`, donc aucune
palette codée en dur), formats de nombre (général, nombre, pourcentage, devise, date, texte),
fusion/défusion, volets figés/libérés, largeur de colonne et hauteur de ligne. La fusion exige
une vraie plage (`xlsx.format_merge_needs_range`).
- Écriture sur le modèle de `xlsx/save` / `xlsx/structure` : verrou par fichier, backup, écriture
atomique (`.tmp` + `os.replace`), garde de perte (refus sans `force`), `If-Match` (A12), garde
anti-formule de BUG-088, plafond `MAX_STYLE_CELLS = 10 000`, invalidation du cache méta (A13).
- Toujours **hors périmètre** : commentaires, liens hypertexte, validation de données, mise en
forme conditionnelle, protection de cellules (lecture seule), bordures et palette Excel.
**Calcul** — ✅ décision documentée (A9, 2026-09-30)
- Aucun moteur de formule n'est livré : ObsiGate lit et écrit les formules telles qu'Excel les a
enregistrées, sans recalcul (pas de références inter-feuilles, pas d'agrégats). Le choix est
**annoncé à l'utilisateur** : un enregistrement contenant une valeur 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.
**Sortie et recherche** — ✅ traité (A11, 2026-09-30)
- ✅ Export de la **sélection** (CSV, nom `Feuille-A1B2.csv`) quand une plage de plusieurs cellules
est active, sinon la feuille entière ; l'écart doc/code de #153-A13 est tranché dans ce sens.
- ✅ Menu **Exporter** : Markdown (tableau), HTML (document autonome) et **impression** (iframe
hors écran, cadre précédent détruit). Les quatre formats suivent le **contenu affiché**.
- ✅ La recherche (`Ctrl+F`) parcourt **toutes les feuilles** : compteur `n/m · k feuilles` et
activation automatique de l'onglet contenant la correspondance.
**Robustesse / multi-postes** — ✅ traitée (A10, A12, 2026-09-30)
- ✅ Concurrence optimiste : `revision` (`mtime_ns` + taille) renvoyée à la lecture et acceptée en
`If-Match` sur `xlsx/save`, `xlsx/structure`, `xlsx/style` et `csv/save` ; un écrivain externe
détecté donne un **409** `conflict` (`details.reason = "stale_revision"`) au lieu d'un
écrasement, et le bandeau « Réessayer » **relit** le fichier avant de rejouer.
- ✅ Piles undo/redo **unifiées et conservées au re-rendu** (`_xlsxHistory` par fichier) : édition,
effacement, tri/filtre et structure ; action destructive (suppression) non annulable faute
d'inverse, et lecture du filtre **coalescée** (une entrée par session de frappe).
- Le tri/filtre restent **d'affichage** : après un tri, un enregistrement ne persiste pas l'ordre
(seules les cellules sales sont écrites) — le libellé `xlsx.sort_applied` le dit.
**Outils IA** — ✅ traités (A14, 2026-09-30)
- ✅ `_spreadsheet_path()` accepte `.xlsx`, `.xlsm` (macros préservées) et `.csv` (délimiteur
conservé) ; `update_xlsx_cells` délègue à `save_csv_cells` pour un CSV.
- ✅ Nouveaux outils : `search_workbook` (recherche multi-feuilles, comptée par feuille),
`analyze_range` (nombre, somme, moyenne, min, max d'une plage A1), `edit_xlsx_structure`
(feuilles, lignes, colonnes — risque WRITE, **confirmation conservée**).
- Bornes explicites : `MAX_SCAN_ROWS/MAX_SCAN_COLS`, `MAX_SEARCH_RESULTS = 100`,
`MAX_RANGE_VALUES = 200`, `MAX_RANGE_CELLS = 10 000`.
### 3.3 Limites techniques relevées
- `MAX_COLS = 40` sans pagination de colonnes : le chargement paresseux ne concerne que les
lignes (`read_sheet_window`), l'axe des colonnes reste tronqué.
- Coût de lecture : `render_sheets()` charge le classeur pour les formules, une seconde fois si
des valeurs cachées existent, puis `read_workbook_meta()` (troisième chargement, scan
`500 × 40` par feuille). ✅ **A13** : `read_workbook_meta()` est mémoïsé par
`(chemin, mtime_ns, taille)` (LRU 8) et invalidé à chaque écriture — le reload complet à chaque
fenêtre demandée a disparu ; `render_sheets()` reste hors cache (il doit relire les valeurs).
- `allow_formula` (bouton `f(x)`) reste actif pour toute la session une fois activé : rien ne le
remet à zéro après un enregistrement.
- `role="grid"` posé (#154-A4) mais sans `aria-rowcount`/`aria-colcount`/`aria-selected`/
`aria-activedescendant` ni annonce des changements de cellule active.
## 4. Backlog #156 — sous-tâches
Légende : 🔴 P0 (défaut) · 🟡 P1 (productivité immédiate) · 🟢 P2 (fidélité/finitions) ·
effort en jours-homme (développement + tests).
### P0 — défauts (2-3 j)
- [x] **A1 — Sauvegarde `.csv` depuis la visionneuse (BUG-096).** Nom de feuille résolu avec
repli (feuille → `data.title`/`data.path`), le chemin d'enregistrement ne dépend plus de la
structure du classeur. *Vérifié :* test JSDOM « saving an edited csv PUTs /csv/save
(payload without xlsx_sheets) » (contre-preuve : échouait avec le `TypeError`).
- [x] **A2 — Chargement paresseux des `.xlsm` (BUG-097).** `GET …/xlsx/sheet` accepte `.xlsx`
**et** `.xlsm` ; les autres formats restent refusés (415). *Vérifié :*
`test_sheet_window_is_served_for_xlsm` + `test_sheet_window_still_refuses_other_formats`.
- [x] **A3 — Détection du délimiteur CSV (BUG-098).** `sniff_csv_delimiter()` partagé par la
lecture et la réécriture, qui conserve le délimiteur détecté. *Vérifié :* `TestCsvDelimiter`
(3 séparateurs + repli) et 3 tests client (lecture, écriture, guillemets).
- [x] **A4 — Sonde de perte par feuille (BUG-099).** Budget par feuille + plafond global ;
budget épuisé → clé `cached_values_unverified` (i18n FR/EN) au lieu d'un silence. *Vérifié :*
`TestXlsxCachedValueProbe` (3), dont la contre-preuve du silence antérieur.
### P1 — presse-papiers & clavier (4-6 j)- [x] **A5 — Presse-papiers de plage (livré le 2026-09-29).** Copier / couper / coller un bloc
(presse-papiers interne TSV, miroir système best-effort) via `Ctrl+C`/`Ctrl+X`/`Ctrl+V` et les
entrées du menu contextuel ; remplissage multi-cellules depuis la cellule active, bloc laissé
sélectionné, collage **texte** uniquement (R4) et annulable cellule par cellule ; un bloc plus
grand que la grille rendue est tronqué et signalé (`xlsx.paste_out_of_grid`). Couper efface la
source au collage, sauf en collage sur place.
*Vérifié :* JSDOM — `A1:B2` copié puis collé en `D5` donne `D5:E6`, bloc TSV Excel + retour
ligne final, 1×1 qui remplace la cellule, coupe → collage, menu contextuel, débordement
signalé, `E2E` (Playwright : copie/collage par le menu contextuel sur la fixture 520 lignes).
- [x] **A6 — Clavier complet (livré le 2026-09-29).** `Ctrl+S`, `Ctrl+A`, `Suppr`, `F2`,
`Ctrl+Home/End`, `Home`/`End`, `PgUp/PgDn`, `Ctrl+flèches`, `Maj+Entrée` ; undo/redo déplacés
dans le même handler priorisé et `Suppr` passe par `clearRange()` (donc **annulable**).
*Vérifié :* JSDOM — un test par raccourci, dont la contre-épreuve « `Suppr` pendant la saisie
supprime un caractère, pas la sélection » ; `E2E` (Ctrl+A, Ctrl+Home/End, Suppr).
- [x] **A7 — Zone Nom éditable & aide à la saisie (livré le 2026-09-29).** La zone Nom est un
`<input>` (`#xlsx-active-cell`) : `B12`, `A1:B3`, `$A$1`, `Feuille!A1` (changement d'onglet) ;
référence inconnue → message `xlsx.name_box_invalid` et adresse précédente restaurée. Le champ
de formule porte la liste des fonctions (`<datalist id="xlsx-function-list">`, `xlsx.fn_suggestions`).
*Vérifié :* JSDOM (atteindre, plage normalisée, refus d'une référence inconnue, bascule
d'onglet, liste de fonctions) ; `E2E` (zone Nom → `B12`).
### P2 — mise en forme, calcul, undo (5-7 j)
- [x] **A8 — Mise en forme en écriture (livré le 2026-09-30).** Bouton **Mise en forme** :
gras/italique/souligné/effacer, alignements, couleurs de police et de fond (sélecteur natif),
formats de nombre (général/nombre/pourcentage/devise/date/texte), fusion/défusion, volets
figés/libérés, largeur de colonne et hauteur de ligne, sur la sélection courante. Nouvelle route
`PUT …/xlsx/style` (`mutate_xlsx_style`) sur le modèle de `PUT …/xlsx/structure` : verrou,
`.tmp`/`os.replace`, backup, garde de perte (`force`), `If-Match`, garde anti-formule BUG-088,
plafond `MAX_STYLE_CELLS`. i18n FR/EN. *Vérifié :* `tests/test_xlsx_styles.py` (17 : rechargement,
fusions/tailles/volets, effacement, entrées invalides, CSV refusé, `if_match`, backup) et JSDOM
(application d'un format, refus d'un CSV, garde de perte).
- [x] **A9 — Calcul : décision documentée (livré le 2026-09-30).** Pas de moteur de formule ; le
gap est **annoncé dans l'UI** (toast « enregistrée comme texte » à l'enregistrement si une valeur
commence par `=` ou `@` sans le bouton `f(x)`), la garde anti-DDE reste. *Vérifié :* JSDOM
(l'avertissement apparaît, disparaît une fois `f(x)` activé).
- [x] **A10 — Undo/redo unifié (livré le 2026-09-30).** Entrées typées `cell|order|hidden|structure`,
piles **par fichier** (`_xlsxHistory`, 8 fichiers) conservées au re-rendu, filtre coalescé,
rejeu de structure silencieux, action destructive non annulable. *Vérifié :* JSDOM (édition →
effacement → tri annulés successivement, insertion de ligne rejouée, filtre annulé en un `Ctrl+Z`).
### P3 — sortie, robustesse, performances (4-6 j)
- [x] **A11 — Sortie & recherche (livré le 2026-09-30).** Bouton **Exporter** : CSV de la
**sélection** (sinon de la feuille, nom `Feuille-A1B2.csv`), Markdown, HTML et impression
(iframe hors écran) ; écart #153-A13 tranché. `runFind` parcourt **toutes** les feuilles avec
compteur `n/m · k feuilles` et activation de l'onglet correspondant. *Vérifié :* JSDOM
(sélection multi-cellules, feuille entière, 4 formats, compteur multi-feuilles) et E2E.
- [x] **A12 — Concurrence optimiste (livré le 2026-09-30).** `revision` (`mtime_ns` + taille)
renvoyée à la lecture (`xlsx_revision`) et acceptée en `If-Match`/`if_match` sur `xlsx/save`,
`xlsx/structure`, `xlsx/style` et `csv/save` ; unconflict produit un **409** `conflict`
(`details.reason = "stale_revision"`) et le bandeau « Réessayer » relit avant de rejouer.
*Vérifié :* `TestOptimisticConcurrency` (6) + JSDOM (version rafraîchie par la réponse, relecture
avant le retry).
- [x] **A13 — Performances & plafonds (livré le 2026-09-30).** Cache de `read_workbook_meta()` par
`(chemin, mtime_ns, taille)` (LRU 8), invalidé à chaque écriture : charger une fenêtre ne
rescanne plus le classeur. *Hors périmètre :* la pagination de l'axe des colonnes au-delà de
`MAX_COLS = 40` (documenté dans les limites du guide). *Vérifié :* `TestMetaCache` (4).
- [x] **A14 — Outils IA étendus (livré le 2026-09-30).** `SPREADSHEET_EXTENSIONS` =
`.xlsx`/`.xlsm`/`.csv` (`_spreadsheet_path`, `_read_grid`), `update_xlsx_cells`/`append_xlsx_rows`
adaptés, nouveaux outils `search_workbook` (READ), `analyze_range` (READ) et `edit_xlsx_structure`
(WRITE, **confirmation conservée**, refus d'un CSV). *Vérifié :*
`tests/test_spreadsheet_tools.py` (34, dont `TestXlsmSupport`, `TestCsvSupport`,
`TestSearchWorkbook`, `TestAnalyzeRange`, `TestEditXlsxStructure`).
## 5. Risques et sécurité
| # | Risque | Où | Traitement |
|---|---|---|---|
| R1 | Perte silencieuse de valeurs calculées (sonde plafonnée) | `xlsx_reader.inspect_workbook` | ✅ A4 — budget par feuille + clé `cached_values_unverified` (BUG-099) |
| R2 | Écrasement par un écrivain externe | `mutations`, routes write | ✅ A12 — `If-Match` + **409** `conflict`, retry qui relit |
| R3 | Réécriture CSV dans un délimiteur différent de la source | `xlsx_reader`, `mutations.save_csv_cells` | ✅ A3 — `sniff_csv_delimiter` partagé (BUG-098) |
| R4 | Collage d'un bloc : ne jamais injecter de HTML/`contenteditable` brut | `viewer.js` | ✅ A5 — insertion **texte** uniquement, échappée |
| R5 | Mise en forme en écriture : garder la garde anti-formule (BUG-088) et la garde de perte (BUG-085) | `mutations` | ✅ A8 — `mutate_xlsx_style` réutilise verrou, backup, swap atomique, garde de perte et `If-Match` |
## 6. Règles de livraison (rappel `AGENTS.md` / `DELIVERY_WORKFLOW.md`)
- Chaque sous-tâche démarre par son **ID stable** (`#156-A<n>`) ; un **défaut** est ouvert comme
`BUG-NNN` dans `docs/ISSUES_TODOLIST.md` (A1→BUG-096, A2→BUG-097, A3→BUG-098, A4→BUG-099).
- Tout correctif arrive avec son **test de non-régression** (contre-preuve quand c'est possible).
- Frontend : `vanilla JS`, **zéro build**, `safeCreateIcons()`, **variables CSS**, **i18n FR+EN**
(`test_i18n_parity.py` vert).
- Backend : docstrings, `response_model` pour tout endpoint ajouté, exemple dans
`backend/openapi_docs.py`, chemin utilisateur via `resolve_safe_path()`.
- Documentation : `CHANGELOG.md` `[Unreleased]`, Roadmap (case cochée + index), cette fiche,
guide utilisateur i18n + README si impact utilisateur.
## 7. Historique
| Date | Événement |
|---|---|
| 2026-09-29 | Audit de complétude de l'éditeur Excel → ouverture de #156 et de **BUG-096 → BUG-099** ; défaut A1 reproduit en JSDOM (`TypeError` sur l'enregistrement d'un CSV, aucun `PUT …/csv/save` émis) |
| 2026-09-29 | **P1 livré** — A5 (presse-papiers de plage), A6 (clavier complet) et A7 (zone Nom éditable + aide à la saisie) : presse-papiers interne TSV (miroir système best-effort), collage texte/annulable, `Suppr`/`F2`/`Ctrl+flèches`/`PgUp-PgDn`/`Ctrl+A`/`Maj+Entrée`, zone Nom `<input>` et liste de fonctions i18n FR/EN. Tests : JSDOM `xlsx-viewer.test.mjs` 84/84 (20 nouveaux) + E2E Playwright |
| 2026-09-30 | **P2 livré** — A8 (mise en forme en écriture : bouton Mise en forme, `PUT …/xlsx/style`, verrou/backup/garde de perte/`If-Match`), A9 (décision « pas de moteur de formule », annoncée dans l'UI), A10 (undo/redo unifié, piles conservées au re-rendu). Tests : `test_xlsx_styles.py` 17 + JSDOM `xlsx-viewer.test.mjs` |
| 2026-09-30 | **P3 livré** — A11 (export sélection / Markdown / HTML / impression, recherche multi-feuilles), A12 (`If-Match` + 409 réparable), A13 (cache des métadonnées par `mtime`), A14 (outils IA `.xlsm`/`.csv`, recherche, analyse de plage, structure). Tests : `test_spreadsheet_tools.py` 34, `test_xlsx_viewer.py` (`TestOptimisticConcurrency`, `TestMetaCache`), JSDOM |
| 2026-09-30 | **Backlog #156 clôturé** — P0 → P3 livrés (A1 → A14, dont 4 défauts **BUG-096 → BUG-099**). Suite complète : `pytest tests/` 1525 passed / 6 skipped, ruff/mypy 0, JSDOM `xlsx-viewer.test.mjs` 107/107, E2E `xlsx` 10/10 |
| 2026-09-29 | **P0 livré** — A1-A4 corrigés et testés (BUG-096 → BUG-099) : sauvegarde `.csv` depuis la visionneuse, fenêtres `.xlsm`, délimiteur CSV détecté/réutilisé, sonde de perte par feuille + signal `cached_values_unverified`. Suite complète : 1490 passed / 6 skipped, ruff/mypy 0, JSDOM `xlsx-viewer.test.mjs` 61/61 |