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