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
23 KiB
#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énementscopy/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+Asont câblés dans un handler unique de la grille ; une cellule reste en mode « navigation » jusqu'à la saisie (ouF2), sinonSuppr/Home/Endvoleraient une touche du curseur. - ✅
Maj+Entréeinsè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 viaPUT …/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 sansforce),If-Match(A12), garde anti-formule de BUG-088, plafondMAX_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 boutonf(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 : compteurn/m · k feuilleset 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 enIf-Matchsurxlsx/save,xlsx/structure,xlsx/styleetcsv/save; un écrivain externe détecté donne un 409conflict(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 (
_xlsxHistorypar 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_appliedle 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_cellsdélègue àsave_csv_cellspour 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 = 40sans 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, puisread_workbook_meta()(troisième chargement, scan500 × 40par 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(boutonf(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 sansaria-rowcount/aria-colcount/aria-selected/aria-activedescendantni 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
.csvdepuis 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 leTypeError). - A2 — Chargement paresseux des
.xlsm(BUG-097).GET …/xlsx/sheetaccepte.xlsxet.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é etSupprpasse parclearRange()(donc annulable). Vérifié : JSDOM — un test par raccourci, dont la contre-épreuve «Supprpendant 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 → messagexlsx.name_box_invalidet 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 dePUT …/xlsx/structure: verrou,.tmp/os.replace, backup, garde de perte (force),If-Match, garde anti-formule BUG-088, plafondMAX_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 boutonf(x)), la garde anti-DDE reste. Vérifié : JSDOM (l'avertissement apparaît, disparaît une foisf(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 unCtrl+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é.runFindparcourt toutes les feuilles avec compteurn/m · k feuilleset 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 enIf-Match/if_matchsurxlsx/save,xlsx/structure,xlsx/styleetcsv/save; unconflict produit un 409conflict(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à deMAX_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_rowsadaptés, nouveaux outilssearch_workbook(READ),analyze_range(READ) etedit_xlsx_structure(WRITE, confirmation conservée, refus d'un CSV). Vérifié :tests/test_spreadsheet_tools.py(34, dontTestXlsmSupport,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 commeBUG-NNNdansdocs/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.pyvert). - Backend : docstrings,
response_modelpour tout endpoint ajouté, exemple dansbackend/openapi_docs.py, chemin utilisateur viaresolve_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 |