Files
ObsiGate/docs/features/xlsx-editor-completeness.md
T
bruno c5c225a68e
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
feat: completude de l'editeur Excel - mise en forme, sortie, concurrence, cache, outils IA #156
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
2026-09-30 07:04:47 -04:00

23 KiB
Raw Blame History

#156 — Éditeur Excel : complétude fonctionnelle (presse-papiers, mise en forme, calcul, robustesse)

Item de roadmap : #156 — Éditeur Excel — complétude fonctionnelle 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 :

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)

  • 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).
  • 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.
  • 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).
  • 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).

  • 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).
  • 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)

  • 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).
  • 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é).
  • 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)

  • 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.
  • 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).
  • 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).
  • 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