Files
ObsiGate/docs/features/xlsx-viewer.md
T
bruno 99779ecc08
CI / lint (push) Successful in 2m33s
CI / security (push) Failing after 1m41s
CI / test (push) Failing after 3h11m57s
CI / build (push) Skipped
CI / e2e (push) Skipped
docs: cloture documentaire #153 - changelog A6-A17, fiche, guide utilisateur et README
2026-09-28 17:48:43 -04:00

261 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# #153 — Visionneuse & édition XLSX — état des lieux et backlog
> **Item de roadmap :** [#153 — Visionneuse & édition XLSX — complétude](../ROADMAP.md)
> **Origine :** #152 (visionneuse XLSX, livrée en 2.27.0 — voir
> [archive/COMPLETED_v1-v2.md](../archive/COMPLETED_v1-v2.md))
> **Statut :** ✅ **Backlog terminé et livré le 2026-09-28** — P0 le 2026-09-27 (BUG-085 → BUG-088), A5/A10/A12 le 2026-09-28 (avec BUG-089), A8/A9/A9bis le 2026-09-28 (avec BUG-090), puis A6→A17 en v2.33.0 → v2.39.0 (A11 étant au CI depuis A5/A8)
> **Effort estimé :** 8-13 jours au total (P0 ✅ 2-3 j · P1 4-6 j · P2 2-4 j)
> **Règle de maintenance :** la Roadmap porte les cases à cocher (suivi), cette fiche porte
> l'analyse, les risques et les critères d'acceptation. **Ne pas dupliquer le détail.**
---
## 1. Périmètre et architecture
| Couche | Fichier | Rôle |
|---|---|---|
| Lecture | `backend/xlsx_reader.py` | `render_sheets()` → un tableau HTML par feuille (openpyxl `read_only=True`, `data_only=False`) |
| Endpoint lecture | `backend/routers/files_read.py:241-265` | `GET /api/file/{vault}?path=…` → `is_xlsx: true` + `xlsx_sheets: [{name, html, rows, cols, total_*, max_*, truncated}]` |
| Endpoint fenêtre | `backend/routers/files_read.py` | `GET /api/file/{vault}/xlsx/sheet?path=&sheet=&offset=&limit=` (#153 A9) — une fenêtre de lignes, vraies coordonnées A1 |
| Schéma API | `backend/schemas.py:286-290` | `is_xlsx`, `xlsx_sheets`, `XlsxSheetWindowResponse` |
| Écriture | `backend/services/mutations.py:227-320` | `edit_xlsx_cells()` (backup, refs A1 validées, coercion `str`→`int`/`float`) |
| Endpoint écriture | `backend/routers/files_write.py:116-148` | `PUT /api/file/{vault}/xlsx/save` (1 à 500 cellules / requête) |
| Documentation API | `backend/openapi_docs.py:184-187` | exemple d'appel `xlsx/save` |
| UI | `frontend/js/viewer.js:998-1100` | `renderXlsxViewer()` (onglets, cellules sales, Entrée/Échap, collage monoligne) |
| CSS | `frontend/style.css:10927-10988` | `.xlsx-*` (variables CSS, colonne A `sticky`) |
| Indexation | `backend/indexer.py:68, 563-568, 957-960` | `.xlsx` supporté, **métadonnées seules** (`content=""`) |
| Outils IA | `backend/tools/documents.py:66-89` + `schemas.py:296-305` | `create_xlsx` (WRITE + confirmation) — **création seule** |
| Tests | `tests/test_xlsx_viewer.py` (58) + `test_xlsx_styles.py` (9) + `test_xlsx_formats.py` (12) + `test_xlsx_dashboard.py` (8) + `test_xlsx_structure.py` (11) + `test_spreadsheet_tools.py` (17) · `tests/frontend/xlsx-viewer.test.mjs` (35) · `tests/e2e/xlsx-viewer.spec.js` (9) | Backend, JSDOM et E2E (chromium-desktop) |
## 2. Ce qui est supporté aujourd'hui (livré, non concerné par #153 sauf mention)
**Lecture** — multi-feuilles avec onglets ; en-têtes A1/A2/B1 et numéros de ligne ; valeurs
`_fmt()` (dates `YYYY-MM-DD` / `YYYY-MM-DD HH:MM`) ; lignes et colonnes de fin élaguées
(`_trim`) ; feuille vide affichée ; `html.escape()` sur chaque valeur.
**Édition** — `contentEditable` par `<td>`, classe `xlsx-dirty`, bouton Save actif seulement si
modification ; `Entrée` → blur, `Échap` → restauration, collage forcé en monoligne ; un `PUT` par
feuille sale ; coercion automatique des nombres (`"250"` → int `250`) ; chaîne vide → cellule
vidée ; backup `.bak` avant écriture ; garde-fou vault read-only (403) ; `resolve_safe_path()`
(anti path-traversal) ; `check_vault_access()` + `require_auth` ; journalisation d'audit
(`log_file_save`).
**Divers** — téléchargement de l'original ; refresh de l'arborescence via le watcher après
écriture ; rafraîchissement de la visionneuse après une action IA (`create_xlsx` →
`obsigate:file-written`, BUG-076).
## 3. Limites connues (par couche)
> **Note (2026-09-28)** : les limites ci-dessous décrivent l'état du jour de l'audit
> (2026-09-27). La quasi-totalité a été levée depuis par le backlog §5 (styles, navigation
> clavier, tri/filtre/recherche, structure, formats `.xlsm`/`.xls`/`.ods`/`.csv`, indexation,
> outils IA) — se reporter aux cases cochées et à l'historique §7 ; ne pas relire cette
> section comme l'état actuel.
### 3.1 Fidélité du round-trip — risque n°1
`load_workbook()` → `wb.save()` : ce qui est **réellement** perdu a été mesuré sur
openpyxl 3.1.5 (2026-09-27), et non repris de la documentation :
| Élément | Round-trip openpyxl 3.1.5 |
|---|---|
| Graphiques, images, dessins | ✅ **préservés** (mesuré : `xl/charts/`, `xl/drawings/`, `xl/media/` intacts) |
| Tableaux croisés (pivot) + caches | ✅ **préservés** (`reader/excel.py` relit les `TableDefinition`, `workbook/_writer.py` les réécrit) |
| Styles, formats, fusions, validation de données, mise en forme conditionnelle, commentaires | ✅ préservés |
| **Valeurs calculées en cache** (`<f>…</f><v>…</v>`) | ❌ **perdues** → tout lecteur `data_only=True` (pandas, script tiers, convertisseur) renvoie `None` tant qu'Excel n'a pas recalculé |
| Slicers / chronologies, contrôles de formulaire (`ctrlProps`/`activeX`), connexions & requêtes, custom XML, signature numérique, commentaires enrichis, macros | ❌ **perdus** (parties absentes de l'archive après écriture) |
La liste fait foi dans le code : [`LOSSY_PARTS`](../backend/xlsx_reader.py) + la sonde
`<f>…</f><v>[^<]` pour les valeurs en cache (openpyxl écrivant lui-même un `<v></v>` vide).
**Ce qui reste ouvert** (non mesuré, prudence) : types de graphiques exotiques (treemap,
sunburst, funnel…), `sparklines`, `xl/queryTables` en lecture Excel. Un classeur qui en contient
peut sortir dégradé, voire échouer au chargement — d'où le refus par défaut (A1).
### 3.2 Lecture
- Aucun style, format de nombre, devise, pourcentage, largeur de colonne, ligne figée, cellule
fusionnée, commentaire, lien hypertexte, validation de données, mise en forme conditionnelle.
- Plafonds durs `MAX_ROWS = 500`, `MAX_COLS = 40` par feuille, **sans indicateur dans l'UI** : au-delà,
contenu silencieusement tronqué et **non éditable**.
- Pas de pagination ni de chargement à la demande : toutes les feuilles sont rendues d'un bloc
dans le JSON (20 feuilles × 20 000 cellules = payload énorme, UI gelée).
- Formules affichées **en texte** (`=B1*2`), jamais recalculées ; après édition, les cellules
dépendantes ne se mettent pas à jour à l'écran.
### 3.3 UI (`viewer.js`)
Navigation clavier (Tab/flèches) absente ; pas de barre de formule, pas de nom de cellule actif,
pas d'undo/redo global, pas de recherche dans la feuille, pas de tri/filtre, pas d'export CSV,
pas d'ajout/renommage/suppression de feuille, pas d'insertion/suppression de ligne ou colonne,
pas de sélection de plage, pas de copie d'une plage, pas de retour ligne dans une cellule
(`Maj+Entrée`) ; seul le retour de l'API est signalé (plafond 500 cellules) ; seule la
**colonne A** est `sticky` (le `thead` ne l'est pas → les en-têtes de colonnes disparaissent au
défilement vertical). **Couverture de test** : `tests/frontend/xlsx-viewer.test.mjs` (10) et
`tests/e2e/xlsx-viewer.spec.js` (3) depuis #153 P0 — la navigation clavier et la barre de formule
restent à faire (A7).
### 3.4 Recherche, IA et knowledge base
- **Indexation** : `content=""` → un `.xlsx` est totalement **invisible** à la recherche TF-IDF, à
la recherche sémantique, au remplacement global, aux tags et aux statistiques de contenu.
- **Outils IA** : seul `create_xlsx` existe (crée un fichier neuf, une seule feuille,
`overwrite=True` par défaut) ; `read_file` fait un `read_text()` sur l'archive ZIP → **bruit
binaire** envoyé au LLM ; pas de `update_xlsx_cells` pourtant le service existe déjà, pas
d'ajout de lignes, pas de `xlsx → markdown` pour le contexte.
## 4. Risques de sécurité / robustesse
| # | Risque | Où | Traitement | État |
|---|---|---|---|---|
| R1 | Perte silencieuse (valeurs calculées, slicers, contrôles, connexions, custom XML, signature) | `mutations.edit_xlsx_cells` | **A1** — bandeau + **409** `xlsx_lossy_content` sans `force` | 🟢 livré (BUG-085) |
| R2 | Écriture non atomique (`wb.save()` en place) → classeur corrompu si crash | `mutations.edit_xlsx_cells` | **A2** — `.tmp` + `os.replace` | 🟢 livré (BUG-086) |
| R3 | Concurrence : deux éditions (onglets, watcher + IA) → dernier écrivain gagne | `mutations.edit_xlsx_cells` | **A3** — verrou par chemin, **409** `conflict` | 🟢 livré (BUG-087) |
| R4 | **Injection de formule** : une saisie `=cmd\|…`, `=HYPERLINK(…)` est stockée comme formule par openpyxl → DDE à l'ouverture dans Excel | `mutations._write_cell` | **A4** — forçage texte (`data_type="s"`), opt-in `allow_formula` | 🟢 livré (BUG-088) |
| R5 | Troncature silencieuse au-delà de 500×40 | `xlsx_reader.MAX_ROWS/MAX_COLS` | A8 / A9 | 🟢 bandeau + dimensions exposées (BUG-090) ; le chargement paresseux par fenêtres sert les lignes au-delà du plafond |
## 5. Backlog #153 — sous-tâches
Légende : 🔴 P0 (sécurité / perte de données) · 🟡 P1 (valeur immédiate) · 🟢 P2 (confort /
couverture) · effort en jours-homme de développement + tests.
### P0 — Garde-fous d'écriture (2-3 j) — 🟢 livré le 2026-09-27
- [x] **A1 — Alerte de fidélité avant écriture (R1).** `inspect_workbook()` liste ce qu'un
round-trip perd (`LOSSY_PARTS` + sonde valeurs en cache) ; la lecture renvoie
`xlsx_lossy_features` ; la visionneuse affiche un bandeau listant les éléments ; `PUT
…/xlsx/save` répond **409** `xlsx_lossy_content` (avec `details.features`) tant que `force` n'est
pas passé, le client demande confirmation puis réémet avec `force: true` (une seule fois par
session). *Vérifié :* `TestXlsxLossyGuard` (5), `xlsx-viewer.test.mjs` (10), E2E (3).
- [x] **A2 — Écriture atomique (R2).** `wb.save(<nom>.<pid>.tmp)` puis `os.replace()` ; `.tmp`
supprimé sur échec ; backup `.bak` inchangé. Le `.tmp` est ignoré par le watcher. *Vérifié :*
`TestXlsxAtomicWrite` (2) — les octets d'origine sont intacts après un `save` en échec.
- [x] **A3 — Verrou par fichier (R3).** Verrou `threading.Lock` par chemin (registre + garde,
timeout 15 s) autour du cycle load → edit → replace ; **409** `conflict` si le délai est dépassé.
L'endpoint est passé en `def` (sync) pour que l'attente s'exécute dans le threadpool. *Vérifié :*
`TestXlsxWriteLock` (2). *Limite :* verrou en mémoire, par processus (suffisant pour un serveur
ObsiGate, y compris desktop).
- [x] **A4 — Neutralisation de l'injection de formule (R4).** `cell.data_type = "s"` après
affectation : une saisie `=`/`@` est stockée en texte. Opt-in `allow_formula: true` côté API et
bouton `f(x)` dans la visionneuse (état de session, jamais persisté). `+`/`-` restent des
nombres. Au passage : le handler `ServiceError` expose `code` + `details` et `api()` les
propage sur l'Error. *Vérifié :* `TestXlsxFormulaGuard` (4) + test du toggle côté UI.
### P1 — Recherche, IA, UX (4-6 j) — 🟢 livré le 2026-09-28 (A5 → A12)
- [x] **A5 — Indexation du contenu des feuilles.** `extract_indexable_text()` (noms de feuilles +
20 premières lignes, `MAX_INDEX_CHARS = 5 000`, 20 feuilles max) alimente le TF-IDF et la
recherche sémantique ; la lecture binaire reste inchangée pour l'affichage. Un classeur
chiffré/corrompu s'indexe par son seul nom (jamais d'exception). Au passage : **BUG-089**,
un reindex manuel ne reconstruisait pas l'index inversé. *Vérifié :* `TestXlsxSearchable` (4)
+ `TestXlsxIndexing`, **contre-preuve** (neutraliser l'extraction → 3 tests échouent).
- [x] **A6 — Outils IA sur classeur.** `update_xlsx_cells` (enveloppe du service existant),
`append_xlsx_rows`, `xlsx_to_markdown` (contexte LLM, plafonné), `list_xlsx_sheets` — risque
WRITE + confirmation pour les mutations, libellés i18n dans `backend/tools/labels.py`,
refresh viewer via `obsigate:file-written`. *Livré (v2.33.0) :* `backend/tools/spreadsheets.py`.
*Vérifié :* `tests/test_spreadsheet_tools.py` (17).
- [x] **A7 — Navigation clavier & barre de formule.** `Tab`/`Maj+Tab`/`Entrée`/flèches, cellule
active affichée (nom A1), `Maj+Entrée` pour le multiligne, copier une plage, focus visible
et compatible mobile (≥ 44 px, `tests/e2e/mobile-editor.spec.js`). *Livré (v2.34.0).*
- [x] **A8 — `thead` sticky + indicateur de troncature (R5) — livré 2026-09-28 (BUG-090).**
Ligne d'en-têtes figlée au défilement vertical (`thead th { top: 0 }` ; `top: auto` sur les
numéros de ligne, sans quoi ils s'empilent en haut à gauche) ; `render_sheets()` expose
`total_rows`/`total_cols` (dimensions déclarées), `max_rows`/`max_cols` (plafonds) et
`truncated` — le bandeau « feuille tronquée » annonce le **plafond atteint** et non la
taille élaguée (une feuille creuse rend 1×1 tout en couvrant 500 lignes) ; libellés
`xlsx.truncated_*` FR/EN. *Vérifié :* `TestXlsxTruncationNotice` (4), `xlsx-viewer.test.mjs`
(4 nouveaux), E2E sur `test_vault/sample-xlsx-large.xlsx` (520 lignes).
- [x] **A9 — Chargement paresseux par feuille (côté API).** Endpoint
`GET /api/file/{vault}/xlsx/sheet?sheet=&offset=&limit=` (`XlsxSheetWindowResponse`,
exemple dans `backend/openapi_docs.py`) : une fenêtre de 1 à 1 000 lignes (plafond
`MAX_WINDOW_ROWS`, `limit>1000` → 422), `has_more` pour paginer, valeurs calculées A12
incluses. Les numéros de ligne et `data-cell` restent les coordonnées A1 réelles de la
feuille (`_table(..., row_offset=offset)`) : une fenêtre est indistinguishable d'un rendu
complet et une édition dans la fenêtre cible la bonne cellule. Erreurs : 404 feuille
inconnue / fichier absent, 415 non-`.xlsx`. *Vérifié :* `TestXlsxSheetWindow` (11),
**contre-preuve** (neutraliser l'offset → 3 tests échouent), E2E « l'endpoint de fenêtre
sert les lignes au-delà du plafond ».
- [x] **A9bis — Chargement à la demande côté UI.** Sous une feuille tronquée, un pied de page
« N lignes affichées sur M · Charger la suite » apparaît : cliquer — ou approcher du bas
du tableau (sentinelle de défilement, marge 120 px) — fetch la fenêtre suivante
(`limit=500`) et l'insère dans la table. Les lignes ajoutées passent par le **même**
pipeline d'édition que le rendu initial (`setupCell` factorisé : contenteditable, dirty,
Échap, collage monoligne, info-bulle valeurs calculées) et sont donc sauvegardables
immédiatement. Un fetch échoué restore le libellé du pied de page (retry possible) et
toast l'erreur ; feuille complète → pied de page masqué (`class="done"`).
*Vérifié :* `xlsx-viewer.test.mjs` 19/19 (5 nouveaux), **contre-preuve** (désactiver
`wireLazyRows` → 5 tests échouent), E2E « le bouton charger la suite ajoute les lignes
cachées » sur `sample-xlsx-large.xlsx` (A520 visible et éditable après clic).
- [x] **A10 — Types et formats de saisie.** `_coerce_xlsx_value()` reconnait les booléens
(`true`/`vrai`/`oui`/`yes` et leurs négatifs) et les dates FR `JJ/MM/AAAA` (+ `HH:MM`),
jour-first comme Excel en locale française : `01/02/2026` = 1ᵉʳ février. Une saisie
ressemblant à une formule n'est jamais convertie (BUG-088 préservé) ; un code postal
numérique ou une version restent ce qu'ils sont. *Vérifié :* `TestXlsxValueCoercion` (5),
**contre-preuve** (neutraliser la coercion → 2 tests échouent).
- [x] **A11 — Tests frontend + E2E.** `tests/frontend/xlsx-viewer.test.mjs` (dirty, Échap,
collage, 1 PUT par feuille, bouton désactivé) et `tests/e2e/xlsx-viewer.spec.js`
(ouverture, onglets, édition, sauvegarde, rechargement) ; intégration au CI. *Vérifié :*
35 tests JSDOM (le job CI `lint` lance `node xlsx-viewer.test.mjs`) et 9 E2E
chromium-desktop ; la couverture a grandi avec chaque sous-tâche (A5/A8 → P2).
- [x] **A12 — Valeurs calculées.** La valeur en cache s'affiche sous la formule dans un
`<span class="xlsx-cached">`. La 2ᵉ lecture `data_only=True` n'a lieu que si l'archive
contient réellement un `<f>…</f><v>…</v>` (sonde déjà présente pour A1) : le cas courant
reste à un seul chargement, et toute erreur retombe sur l'affichage formules seul.
L'info-bulle est traduite côté client (`xlsx.cached_value_title` FR/EN) — aucun texte
d'interface n'est émis par le backend. *Vérifié :* `TestXlsxCachedValues` (3),
**contre-preuve** (neutraliser la 2ᵉ lecture → 2 tests échouent).
### P2 — Étendu (2-4 j) — 🟢 livré le 2026-09-28
- [x] **A13 — Tri / filtre / recherche dans la feuille + export CSV de la sélection.**
*Livré (v2.35.0) :* tout en manipulation d'affichage, le classeur n'est jamais réécrit
(info-bulle `xlsx.sort_applied`).
- [x] **A14 — CRUD de feuilles et de lignes/colonnes** (renommer, insérer, supprimer, dupliquer).
*Livré (v2.36.0) :* `PUT …/xlsx/structure` + menu Structure, mêmes garde-fous que
l'édition de cellules. *Vérifié :* `tests/test_xlsx_structure.py` (11).
- [x] **A15 — Styles minimaux en écriture et lecture fidèle** (gras, fond, format
devise/pourcentage/date, cellules fusionnées, volets figés) ; conserver `csv-table` comme
socle de rendu. *Livré (v2.37.0) en lecture :* couleurs, gras/italique/souligné,
alignements, fusions, ancre de volets figés ; un format de nombre personnalisé est signalé
en police mono (pas de rendu devise/pourcentage). L'application de styles **depuis la
visionneuse** (écriture) reste hors périmètre. *Vérifié :* `tests/test_xlsx_styles.py` (9).
- [x] **A16 — Formats additionnels.** `.xlsm` (`keep_vba=True`), `.xls`, `.ods`, `.csv` éditable
comme tableur — dépendances à qualifier (`xlrd`/`odfpy`) ou conversion. *Livré (v2.38.0) :*
`.xlsm` éditable macros préservées, `.xls`/`.ods` lecture seule (xlrd/odfpy), `.csv`
éditable et réécrit RFC 4180. *Vérifié :* `tests/test_xlsx_formats.py` (12).
- [x] **A17 — Vue « tableau de bord ».** Détection des plages nommées, TCD et graphiques ; vue
résumée (KPI par feuille) et proposal d'actions IA sur ces plages. *Livré (v2.39.0) :*
panneau Tableau de bord (`GET …/xlsx/dashboard`) — plages nommées avec portée, comptage
graphiques/TCD par analyse des parties OPC, stats par feuille, 8 KPI ; le volet IA se
limite à un conseil contextuel (pas d'appel IA dédié sur les plages).
*Vérifié :* `tests/test_xlsx_dashboard.py` (8).
## 6. Règles de livraison (rappel `AGENTS.md` / `DELIVERY_WORKFLOW.md`)
- Chaque sous-tâche démarre par un **ID stable** : nouvelle feature = `#153-A<n>` dans la
Roadmap ; si la sous-tâche est un **défaut** (A1, A2, A3, A4, A8), l'ouvrir aussi comme
`BUG-NNN` dans `docs/ISSUES_TODOLIST.md` au moment du démarrage.
- Backend : docstrings, `response_model` pour tout endpoint ajouté, exemple dans
`backend/openapi_docs.py`, chemin utilisateur via `resolve_safe_path()`.
- Frontend : vanilla JS sans build, `safeCreateIcons()`, **variables CSS** (jamais de couleur
hardcodée), **i18n FR + EN** pour chaque nouveau texte (`test_i18n_parity.py` vert).
- Tests : `pytest tests/test_xlsx_viewer.py`, `ruff`, `mypy`, `validate-imports`, suite frontend
ciblée, E2E si l'UI change — puis CI verte.
- Documentation : `CHANGELOG.md` `[Unreleased]`, Roadmap (case cochée), cette fiche (résultat),
guide utilisateur i18n + README si impact utilisateur.
## 7. Historique
| Date | Événement |
|---|---|
| 2.27.0 | #152 livré : affichage multi-feuilles, édition des cellules, téléchargement (`docs/archive/COMPLETED_v1-v2.md`) |
| 2026-09-27 | Audit complet → création de #153 : limites, risques R1-R5, backlog A1-A17 |
| 2026-09-27 | Périmètre de perte **remesuré** sur openpyxl 3.1.5 : graphiques / images / TCD sont préservés, seules les valeurs en cache et quelques parties exotiques sont perdues |
| 2026-09-27 | **P0 livré** (BUG-085 → BUG-088) : `xlsx_lossy_features` + 409 `xlsx_lossy_content`, écriture atomique, verrou par fichier, formules stockées en texte par défaut |
| 2026-09-28 | **A5 + A10 + A12 livrés** : le contenu des cellules est indexé (recherche), la saisie est typée (booléens, dates FR), la valeur calculée s'affiche sous la formule. **BUG-089** corrigé au passage (reindex manuel ≠ reconstruction de l'index inversé ; `backend/search.py` lisait l'index par valeur) |
| 2026-09-28 | **A8 + A9 livrés** (BUG-090) : la troncature d'une feuille est annoncée (bandeau + dimensions dans la réponse de lecture), les en-têtes restent visibles au défilement, et `GET …/xlsx/sheet` sert une fenêtre de lignes avec les vraies coordonnées A1 — les lignes au-delà du plafond redeviennent accessibles aux clients API. Défilement virtuel côté UI à suivre |
| 2026-09-28 | **A9bis livré** : « Charger la suite » + sentinelle de défilement sous une feuille tronquée ; les lignes ajoutées sont éditables et sauvegardables immédiatement (même pipeline que le rendu initial) |
| 2026-09-28 | **A6 + A7 livrés** (v2.33.0, v2.34.0) : l'assistant IA lit et modifie les classeurs (`list_xlsx_sheets`, `xlsx_to_markdown`, `update_xlsx_cells`, `append_xlsx_rows`) et la visionneuse gagne navigation clavier complète + barre de formule |
| 2026-09-28 | **A13 + A14 livrés** (v2.35.0, v2.36.0) : tri, filtre, recherche et export CSV côté affichage ; structure du classeur éditable (feuilles, lignes, colonnes) via `PUT …/xlsx/structure` |
| 2026-09-28 | **A15 + A16 + A17 livrés** (v2.37.0 → v2.39.0) : styles/fusions/volets figés rendus, formats `.xlsm`/`.xls`/`.ods`/`.csv` gérés, panneau Tableau de bord (plages nommées, graphiques/TCD, stats, KPI) — **backlog #153 terminé** |