Restaure et termine le lot tableur resté non committé. Il n'existait que dans un `git stash` (24 fichiers suivis) et en fichiers non suivis (menus.js, formula.js, color-picker.js, tests, fiches) : la 2.53.3 livrée ne le contenait donc pas. Le stash, créé avant le commit BUG-108, n'a jamais été restauré. #180 Redimensionnement au curseur (en-tête de bord miroité + classe de repaint), grille thémée sur les 15 thèmes et les modes contraste élevé / sépia, menu Fichier au niveau des onglets, barre épinglée pleine largeur. #181 Modèle de saisie Google Sheets : sélection ≠ édition (caret masqué sans quitter contenteditable, double-clic / F2 / première frappe qui remplace, Entrée contextuelle, Échap qui restaure) + inventaire priorisé des écarts. #182 Désélection fiable (clic simple sans dépendre du focus), contours de plage non empilés (sélecteur de classes sans point), couleurs texte/fond sur une plage, clic droit qui préserve la sélection multiple. #183 Panneau de couleurs façon Google Sheets : palette 8 × 10, STANDARD, PERSONNALISÉ, coche selon la luminance, sortie `#rrggbb` (une valeur HSL était rejetée par normHex). #184 Peinture de format complète (toutes propriétés, source sans format = réinitialisation), sélection multi-lignes/colonnes depuis les marges, grille étendue : colonnes A → Z d'emblée, lignes ajoutées PAR BLOCS DE 100 au défilement jusqu'à 1000 — matérialiser 1000 lignes d'un coup = ~26 000 cellules câblées par feuille, ce qui épuisait le tas de la suite JSDOM ; index de cellules `ref → td`, court-circuits formule/styles, marqueur data-wired. #185 Barre de menus : les 10 menus Google Sheets (161 entrées, 125 câblées, 36 annoncées indisponibles), ruban façon Sheets, grille unie 1 px dérivée du thème. #186 « Créer un fichier » propose .xlsx et construit un vrai classeur OPC (openpyxl) au lieu d'une charge utile texte illisible. Corrections trouvées en restaurant et en exerçant le lot : - fuite mémoire : écouteur `click` anonyme posé sur #content-area à chaque rendu, jamais retiré — sa fermeture retenait la grille précédente en entier ; - sorties Markdown / HTML / Imprimer de la barre de menus inertes (`data-xlsx-export` jamais réparti, seul `data-xlsx-action` l'était) ; - collision de classe `.xlsx-structure-menu` entre la barre de menus et le menu Structure de la barre d'outils (toute requête tombait sur un nœud masqué) ; - curseur col/row-resize absent quand le pointeur est sur la table elle-même ; - l'export emportait les lignes et colonnes vides du quadrillage (CSV, Markdown, HTML, impression) ; - clic extérieur avalé par la grâce de 250 ms du menu contextuel (destinée au seul appui long tactile) ; - une entrée indisponible laissait la barre de menus ouverte. Tests : pytest 1595 passés / 2 ignorés ; ruff et mypy 0 erreur ; bandit 0 ; 33 suites frontend vertes (xlsx-viewer 163/163, xlsx-menus 19/19, xlsx-formula 14/14) ; E2E complet 132 passés / 12 ignorés ; E2E xlsx-viewer 19/19.
4.9 KiB
#186 — Création d'un fichier Excel depuis « Créer un fichier »
Statut : ✅ livré le 2026-10-05 | Effort : 0,5 jour | Impact : 🟢 Références : Roadmap · Changelog · Guide Recherche, PDF, Excel & Excalidraw · #153 — Visionneuse XLSX
1. Constat
ObsiGate sait lire et éditer les classeurs depuis #152/#153, et l'assistant IA sait
en produire un (create_xlsx, #92). Mais la modale « Créer un fichier » — celle
du menu contextuel d'un vault / d'un dossier et de la palette de commandes —
ne proposait que des formats texte :
.md, .txt, .py, .js, .json, .yaml, .sh, .ps1 (+ Excalidraw).
Démarrer un classeur depuis l'UI imposait donc de passer par l'IA (avec confirmation) ou de déposer un fichier existant dans le vault. Le point de départ naturel manquait alors que l'éditeur complet était disponible.
2. Livré (A1-A3)
A1 — Option Excel dans la liste des types
frontend/js/ui.js, FileOperations.showCreateFileModal() : ajout de
<option value=".xlsx"> dans le <select id="file-ext-select">, après .json
(ordre : formats texte, puis formats enrichis, comme .excalidraw).
Au passage, les dix libellés du sélecteur passent en i18n
(modal.ext_md, modal.ext_txt, …, modal.ext_xlsx) : ils étaient des chaînes
françaises codées en dur, ce qui exposait un texte non traduit en mode EN et
violait la règle « tout texte d'interface existe en FR et en EN ». Les deux
locales sont alignées (tests/test_i18n_parity.py).
Aucun changement pour les autres formats : le nom saisi sans extension reçoit
celle choisie (name.endsWith(ext) ? name : name + ext), donc un nom déjà
suffiché n'est jamais doublé.
A2 — Un vrai classeur côté backend
Le point non évident : POST /api/file/{vault} écrit du texte
(file_path.write_text(content)), et la modale poste content: "". Un .xlsx
n'est pas un fichier texte mais une archive OPC (un ZIP) : écrire la charge
utile vide produisait un fichier de 0 octet que ni openpyxl, ni la visionneuse,
ni Excel n'ouvrent — l'utilisateur aurait vu une erreur à l'ouverture, ou pire,
un fichier corrompu.
backend/services/mutations.py :
_empty_xlsx_bytes()construit l'archive avec openpyxl (une feuilleFeuille1, le titre par défaut déjà utilisé par l'outil IAcreate_xlsx) — import paresseux, comme le reste du module qui garde openpyxl optionnel à l'import ;create_file()écrit ces octets (write_bytes) pour cette extension et ignorecontent, qui n'a pas de sens pour un classeur ;sizerenvoie le nombre d'octets réellement écrits (et nonlen(content), qui aurait valu 0) ;- même garde-fou que
.excalidraw: le squelette est injecté par le service, donc la palette, le glisser-déposer, l'IA et l'API en bénéficient, pas seulement la modale.
openpyxl reste importé à la demande : un vault sans tableur ne paie pas l'import.
A3 — Tests
tests/test_api_main.py::TestFileCRUD—test_create_xlsx_empty_workbook: le fichier créé est relu paropenpyxl(sheetnames == ["Feuille1"]) et servi parGET /api/file/{vault}avecis_xlsx: true(donc lisible par la visionneuse) ;test_create_xlsx_in_new_subdircouvre la création dans un dossier inexistant (auto-créé). Non-régression : sans A2, l'archive serait vide et les deux tests échoueraient.tests/e2e/xlsx-viewer.spec.js— bloc « création depuis l'UI » : l'option.xlsxexiste et son libellé vient des locales, la création passe par le menu contextuel d'un dossier, et le classeur s'ouvre dans la grille (.xlsx-table+ ongletFeuille1) ; l'API est interrogée pour confirmeris_xlsx. Le fichier est supprimé en fin de test (le run ne doit pas salirtest_vault/).
3. Limites assumées
- Le nom de feuille est
Feuille1, en dur. Un vrai nouveau classeur de Google Sheets le demande ; ici le classeur s'ouvre sur sa seule feuille et l'utilisateur la renomme via le menu Structure. La modale reste celle d'un éditeur de texte, volontairement simple. .csvreste absent de la liste, alors que le backend l'accepte (SUPPORTED_EXTENSIONS) et que la visionneuse sait l'éditer comme tableur (#156-A14). L'écart est réel mais hors périmètre de #186 ; le même correctif s'appliquerait (une ligne dans le<select>).frontend/app.js(monolithe obsolète, non chargé parindex.html) duplique l'ancienne liste sans.xlsx. Le code vivant est dansfrontend/js/; ce fichier n'a pas été touché.- L'outil IA
create_xlsxreste le chemin pour un classeur rempli : la modale crée une grille vierge, comme un.mdvierge.
4. Historique
| Date | Événement |
|---|---|
| 2026-10-05 | #186 ouvert puis livré dans la foulée : option .xlsx + squelette openpyxl dans create_file() |