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
323 lines
23 KiB
Markdown
323 lines
23 KiB
Markdown
# #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 |
|