A14 : service mutate_xlsx_structure (batch ordonné d'actions
sheet_add/sheet_rename/sheet_delete/sheet_duplicate et
row/col_insert/delete, une seule réécriture verrouillée et atomique,
409 lossy sans force, backup, dernier sheet protégé) ; endpoint
PUT /api/file/{vault}/xlsx/structure (1-50 actions/requête) ;
menu « structure » dans la visionneuse : ajout/renommage/duplication/
suppression de feuilles et insertion/suppression de ligne/colonne à
partir de la cellule active, confirmations explicites pour les
destructions, re-rendu serveur après succès, reprise force après
confirmation 409.
Vérifié : test_xlsx_structure.py 11 (ops, gardes, atomicité du batch,
409 lossy + force) + contre-preuve (garde dernier sheet neutralisée ->
1 échec), xlsx-viewer.test.mjs 33/33 (3 nouveaux), ruff 0, mypy 0,
i18n parity, validate-imports.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
A13 (affichage seul, le classeur n'est jamais réécrit) : clic sur un
en-tête pour trier la colonne (asc/desc, numérique si toutes les valeurs
le sont, localeCompare fr sinon — les cellules modifiées suivent leur
ligne) ; l'unique champ de recherche filtre les lignes ET surligne les
occurrences (marque <mark>, navigation ↑/↓/Entrée, compteur i18n,
insensible à la casse par défaut, bouton Aa) ; bouton « Réinitialiser »
qui re-rend le fichier (vérité serveur) ; export CSV de la feuille
visible (BOM UTF-8, échappement ;/" et retrait des ombres de valeurs
calculées).
Vérifié : xlsx-viewer.test.mjs 30/30 (5 nouveaux) + contre-preuves
(tri neutralisé -> 1 échec, filtre neutralisé -> 1 échec), validate-imports,
i18n parity (xlsx.find_*, xlsx.sort_*, xlsx.csv_export).
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
A7 : barre de formule sous la barre d'outils (nom de cellule active A1 +
miroir du contenu, édition live, Entrée valide, Échap annule puis
refocalise) ; flèches et Tab déplacent la cellule active dans les 4
directions (Shift inverse Tab), outline persistant sur la cellule active
quand le focus passe à la barre ; Maj+Entrée ne valide plus (réservé au
multiligne A7+). td.tabIndex=0 pour la focalisation clavier.
Vérifié : xlsx-viewer.test.mjs 25/25 (6 nouveaux), E2E 9/9 (barre +
flèches + Échap sur fixture réelle), validate-imports, i18n parity
(xlsx.formula_bar_placeholder, xlsx.active_cell).
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
A9bis : sous une feuille tronquée, un pied de page « N lignes affichées
sur M · Charger la suite » fetch la fenêtre suivante (limit=500) au clic
ou à l'approche du bas du tableau (sentinelle de défilement, marge 120px).
Les lignes ajoutées passent par le même pipeline d'édition que le rendu
initial (setupCell factorisé) : éditables et sauvegardables immédiatement.
Fetch échoué → bouton restauré (retry) + toast ; feuille complète → pied
de page masqué (class done).
Vérifié : xlsx-viewer.test.mjs 19/19 (5 nouveaux, contre-preuve
wireLazyRows désactivé → 5 échecs), E2E 8/8 (A520 visible et éditable
après clic), validate-imports 40 modules, unit.test.mjs 12/12, i18n parity.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
- BUG-090 (#153 A8) : render_sheets() expose total_rows/total_cols,
max_rows/max_cols et truncated ; la visionneuse affiche un bandeau
« Feuille tronquée » (i18n FR/EN) au lieu de couper en silence, et la
ligne d'en-têtes devient sticky (top:auto sur les numéros de ligne).
- #153 A9 : GET /api/file/{vault}/xlsx/sheet?sheet&offset&limit sert une
fenêtre de 1 à 1000 lignes avec les vraies coordonnées A1, has_more de
pagination et valeurs calculées A12 ; 404 feuille inconnue, 415 non-xlsx.
- Tests : TestXlsxTruncationNotice (4) + TestXlsxSheetWindow (11) avec
contre-preuves, xlsx-viewer.test.mjs 14/14, E2E 7/7 (fixture
sample-xlsx-large.xlsx 520 lignes), suite 1417 passed / 6 skipped,
ruff/mypy 0, i18n parity.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
L'édition d'un .xlsx pouvait détruire une partie du classeur, le
concurrencer en silence, ou diffuser une injection de formule.
- BUG-085 : inspect_workbook() détecte ce qu'un round-trip openpyxl perd
(valeurs calculées en cache, slicers, contrôles, connexions, custom
XML, signature, commentaires enrichis, macros) → xlsx_lossy_features
exposé en lecture, bandeau FR/EN, et 409 xlsx_lossy_content sans
`force` (confirmation explicite puis reprise). Périmètre réel
revalidé : graphiques, images et TCD survivent au round-trip.
- BUG-086 : écriture atomique (fichier .tmp + os.replace) : un plantage
ne peut plus tronquer le classeur, le backup reste intact.
- BUG-087 : verrou par fichier autour du read-modify-write (timeout 15 s,
409 conflict) ; endpoint xlsx/save devenu synchrone pour que
l'attente s'exécute dans le threadpool.
- BUG-088 : une saisie en '=' ou '@' est stockée en texte, sauf opt-in
`allow_formula` ou le bouton f(x) de la visionneuse. Le handler
ServiceError expose désormais code + details, que api() propage.
- BUG-084 : la suppression d'une vault purge enfin l'index inversé
(documents fantômes qui continuaient de matcher) et is_stale() devient
is_ready(), le nom étant trompeur (la staleness n'existe plus).
Tests : 1390 pytest, 10 JSDOM (xlsx-viewer.test.mjs, branché au CI),
3 E2E Playwright, suite E2E complète verte, ruff/mypy 0.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <[email protected]>
L'assistant IA peut desormais inserer sa reponse dans l'editeur Forge (postMessage parent-insert) en plus d'Editer, et chaque bloc de code propose un bouton « Ajouter la section » pour n'inserer que ce bloc.
Une session d'edition inline (Editer/Forge) qui possede la zone de contenu etait detruite par deux chemins qui vident cette zone : activation d'onglet (TabManager.activate, PaneTabManager.activate) -> detachInlineEditor() avant le placeholder ; evenement SSE index_updated -> reloadExternalWrite() recharge le tampon de l'editeur au lieu de re-rendre la vue lecture. Nouveau helper queryEditor() ; closeEditor() remet le conteneur dans la modale avant de restaurer en-tete/pied/marque. docs/CONTRIBUTING.md documente scripts/install-hooks.sh.
BUG-046: 'Ajouter du texte au document courant' échouait avec 'Échec de
l'action: [object Object]'. Trois causes corrigées:
- continuation de confirmation sans payload -> second Appliquer POSTait
sans 'message' (422); le payload d'origine est désormais transmis
- detail 422 FastAPI (tableau d'objets) stringifié tel quel -> nouvelle
_responseError() qui aplatit en message lisible
- prompt documents/dossier muet sur le vault -> le modèle inventait
'vault: test' et append_to_file échouait en silence; build_system_prompt
nomme le vault et enjoint les outils d'écriture de l'utiliser exactement
BUG-045: double barre de défilement dans l'éditeur Editer (fichiers
longs): #editor-body overflow:auto + cm-editor height:100% sous la barre
IA -> corps et scroller CodeMirror défilant tous les deux. Flex column,
seul le scroller défile; override global .cm-scroller retiré. SW v20.
Vérifié: pytest 1038, ruff backend 0, tests frontend AI 84/84,
editor-inline 25/25, validate-imports 38 modules; Playwright sur
l'instance de test (un seul conteneur scrollable) et flux agent complet
append_to_file ok=true sur fichier réel
Le conteneur d'edition (#editor-container, CodeMirror ou iframe Forge) est deplace dans la zone de contenu du document (#content-area ou pane active) au lieu de l'overlay plein ecran : le mode edition remplace la vue lecture. L'overlay reste monte (transparent, pointer-events: none) car le ruban d'edition mobile y est ancre ; il ne sert plus que de repli quand la cible n'est pas le document affiche. Garde-fou dans renderFile() pour liberer proprement la session (destroy CodeMirror/Yjs, retrait de l'iframe Forge) ; forge-close passe par closeEditor() ; cache d'onglet invalide au retour en lecture.
Assistant IA : app_context.editing annonce le document en cours d'edition (frontend bookslm.js + backend bookslm.py), et chaque ecriture d'outil (edit_file, append_to_file, create_file, restore_backup) recharge le document affiche (obsigate:file-written) — tampon CodeMirror remplace avec auto-save neutralisee, parent-reload pour l'iframe Forge, re-rendu de la vue lecture sinon.
Tests : tests/frontend/editor-inline.test.mjs (19), tests/test_bookslm.py::TestGeneralPrompt (3 nouveaux). Fiche : docs/features/editeur-inline.md.