From 69927176df885b27b661de7dba14e27310297939 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Tue, 29 Sep 2026 16:34:30 -0400 Subject: [PATCH] fix: feuille xlsx vide editable avec quadrillage vierge BUG-094 --- CHANGELOG.md | 17 ++++++++++++++++- README.fr.md | 6 +++--- README.md | 6 +++--- VERSION | 2 +- backend/xlsx_reader.py | 11 +++++++++++ desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ISSUES_TODOLIST.md | 3 +++ docs/ROADMAP.md | 2 +- frontend/js/viewer.js | 2 +- package.json | 2 +- tests/frontend/xlsx-viewer.test.mjs | 10 ++++++++++ tests/test_xlsx_viewer.py | 23 +++++++++++++++++++++++ 14 files changed, 76 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 482dc7d..6752102 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.43.1**. +> [Unreleased](#unreleased). La dernière version livrée est **2.43.2**. --- @@ -14,6 +14,21 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.43.2] — 2026-09-29 + +### Corrigé + +- **BUG-094 — une feuille `.xlsx` vide ou nouvellement ajoutée est désormais éditable et + manipulable.** Elle s'affichait comme un simple « Feuille vide » sans aucune cellule : il était + donc impossible d'y saisir une valeur ou d'y insérer une ligne/colonne (aucune cellule active → + aucune action de structure). `render_sheets()` substitue maintenant un quadrillage vierge + **20×8** aux vraies coordonnées A1 (constantes `EMPTY_SHEET_ROWS`/`EMPTY_SHEET_COLS`), et la + visionneuse retombe sur `{row:1,col:1}` quand aucune cellule n'est active, de sorte que le menu + Structure propose toujours insérer/supprimer ligne et colonne. Test de non-régression : + `TestXlsxDisplay::test_empty_sheet_renders_an_editable_blank_grid`. + +--- + ## [2.43.1] — 2026-09-29 ### Ajouté diff --git a/README.fr.md b/README.fr.md index f712ccf..91da637 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.43.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.43.2-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -976,8 +976,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.43.1). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.43.2). --- -*Projet : ObsiGate | Version : 2.43.1 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.43.2 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 93bc123..1bb48f9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.43.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.43.2-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1151,8 +1151,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.43.1). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.43.2). --- -*Project: ObsiGate | Version: 2.43.1 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.43.2 | Last updated: September 2026* diff --git a/VERSION b/VERSION index b1d9893..29a3ac7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.43.1 +2.43.2 diff --git a/backend/xlsx_reader.py b/backend/xlsx_reader.py index 8d86fa5..5321c06 100644 --- a/backend/xlsx_reader.py +++ b/backend/xlsx_reader.py @@ -32,6 +32,13 @@ logger = logging.getLogger("obsigate.xlsx_reader") MAX_ROWS = 500 MAX_COLS = 40 +# BUG-094 — an empty sheet used to render as a bare "Feuille vide" paragraph +# with no cell at all, so a freshly added sheet had nothing to click and no way +# to insert a row/column. Render a small blank grid instead (Excel-like), with +# real A1 coordinates, so the cells are editable and the structure actions work. +EMPTY_SHEET_ROWS = 20 +EMPTY_SHEET_COLS = 8 + # #153 A9 — window size served by ``read_sheet_window()`` (lazy per-sheet # loading). The endpoint is bounded so a single request can never ask for the # whole workbook back in one JSON payload; the UI pages through the rest. @@ -424,6 +431,10 @@ def render_sheets(file_path: Path) -> list[dict[str, Any]]: sheets = [] for i, title in enumerate(titles): grid = _trim(formulas[i]) + # BUG-094 — a blank sheet still needs an editable grid (see constants): + # the viewer's cell editing and structure actions all hang off a cell. + if not grid: + grid = [[""] * EMPTY_SHEET_COLS for _ in range(EMPTY_SHEET_ROWS)] # The shadow grid is NOT trimmed independently: _trim drops the # trailing empty columns of each grid on its own width, which would # shift every cached value left of its formula. Indexing it diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 374625f..af90bf0 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.43.1" +version = "2.43.2" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 3b44f71..0bbe8e9 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.43.1" +version = "2.43.2" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 609b46c..1e52a7f 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.43.1", + "version": "2.43.2", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index 95e1b76..3d35682 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -201,6 +201,8 @@ Avant de corriger quoi que ce soit, un agent IA doit : | *BUG-092* | Les tests réseau dépendent du DNS réel du runner : `test_worker_failure_maps_to_tool_error` échoue en `dns_error` au lieu d'atteindre le worker Playwright mocké, et le job CI `test` rougit de façon intermittente | 🟢 corrigé | P1 | CI / tests | IA | `tests/test_webrender.py`, `tests/test_web_tools.py` | Sur un runner au DNS instable : `pytest tests/test_webrender.py -k test_worker_failure_maps_to_tool_error` → `assert 'dns_error' == 'render_unavailable'` | Fixture `no_dns` mockant les **deux** références du garde SSRF `_assert_public_http_url` (celle de `backend/tools/web.py` et celle importée dans le namespace de `backend/tools/webrender.py`, ligne 30 — la seconde avait d'abord échappé au correctif). Les tests de garde SSRF n'utilisent pas la fixture et continuent de traverser le vrai garde | Le garde est appelé par `fetch_url` **avant** le traitement ; seule la couche httpx était mockée. Contre-preuve : DNS coupé globalement (`socket.getaddrinfo` → `gaierror`) → avant 1 échec, après **1474 passed / 6 skipped** | | *BUG-093* | Le job CI `security` échoue : `pip-audit` bloque sur deux DoS de ressources dans `pypdf` 6.16.0 (PYSEC-2026-3910, PYSEC-2026-3911) — et le plancher `pypdf>=4.0` ne les corrigeait pas, car l'image Act du runner embarque 6.16.0 *préinstallé* dans sa toolcache Python (`Requirement already satisfied` ⇒ jamais mis à niveau) | 🟢 corrigé | P0 | CI / sécurité | IA | `backend/requirements.txt`, `.gitea/workflows/ci.yml`, `tests/test_ci_workflow.py` | Run Gitea #1660, job `security` : `Found 2 known vulnerabilities, ignored 2 in 1 package` → `pypdf 6.16.0 PYSEC-2026-3910 6.16.1` / `PYSEC-2026-3911 6.16.1` | Plancher `pypdf>=6.16.1` (correctif des deux advisories), commenté pour expliquer la contrainte de la toolcache. Ajout de `tests/test_ci_workflow.py::TestDependencySecurityFloors`, qui verrouille les planchers de sécurité (`pypdf`, `pyjwt`) et interdit qu'ils retombent sous le correctif | Les deux advisories sont des **consommations de ressources non contrôlées** (PDF à outlines multiples ou à nombreux XForm réutilisés) et sont donc **atteignables** par ObsiGate, dont `backend/pdf_reader.py` extrait le texte et parcourt les outlines de PDF fournis par l'utilisateur. Contre-preuve : plancher remis à `>=4.0` → le garde-fou échoue. pip-audit local : 6.16.1, 6.16.2 et 6.19.0 sans vulnérabilité connue. Correction découverte en lisant le log du job (`/actions/runs/1660/jobs/5541/logs`, accessible sans token) — le log de l'étape Semgrep collé précédemment datait d'un run antérieur | +| *BUG-094* | Feuille `.xlsx` vide ou nouvellement ajoutée : impossible d'y saisir une valeur et d'y insérer une ligne/colonne — la feuille s'affiche « Feuille vide » sans aucune cellule | 🟢 corrigé | P1 | tableur Excel / UX | IA | `backend/xlsx_reader.py::render_sheets`, `frontend/js/viewer.js::renderXlsxViewer` | Ajouter une feuille (`PUT …/xlsx/structure` `sheet_add`) puis tenter de saisir A1 ou d'insérer une ligne/colonne | `render_sheets()` remplace une grille vide par un quadrillage vierge 20×8 (constantes `EMPTY_SHEET_ROWS`/`EMPTY_SHEET_COLS`) aux vraies coordonnées A1 ; la visionneuse retombe sur `parseRef(activeRef) || {row:1,col:1}` pour que le menu Structure propose toujours insérer/supprimer ligne et colonne. Contre-preuve : `TestXlsxDisplay::test_empty_sheet_renders_an_editable_blank_grid` (sans le correctif : « Feuille vide » sans `data-cell`) | Le classeur n'était pas en cause : seule la **représentation HTML** était vide, donc aucun `td` à sélectionner → aucune cellule active → aucune action de structure possible. Vérifié : `test_xlsx_viewer.py` 59 passed, `xlsx-viewer.test.mjs` 52/52 | +| | | | | | | | | | | ### TODOs techniques (améliorations / nouvelles tâches) | # | Titre | Statut | Priorité | Scope | Assigné | Zone (fichier) | Cmd de repro | Correctif / Commit | Notes | @@ -296,6 +298,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | 2026-09-27 | BUG-083 | Correction CI | `.gitea/workflows/ci.yml`, `tests/test_ci_workflow.py` (nouveau), `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-083** : job `security` rouge — le runner Gitea Act tronque naïvement au premier `#` (même entre guillemets) : `echo "... see #87)"` devenait une citation non fermée (`unexpected EOF while looking for matching '"'"`, `/var/run/act/workflow/4` ligne 2). Seul `run:` du workflow avec un `#` (les `#` des noms d'étapes Bandit/Npm audit sont inoffensifs, ces étapes passent). Correctif : echo sans `#` (réf `#87` en commentaire YAML). Garde-fou `test_ci_workflow.py` (aucun `#` dans le code des `run:`, `upload.test.mjs` verrouillé en étape JSDOM — BUG-082) + contre-preuve sur l'ancien `ci.yml`. Vérifié : 56 passed. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-27 | BUG-081 | Correction | `backend/auth/router.py`, `tests/test_mfa.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-081** : `GET /api/auth/mfa/status` répondait 500 quand l'auth est désactivée — le pseudo-user `anonymous` n'a aucune entrée en store (`get_user` → `None`, `AttributeError` sur `user.get`). Garde `user is None` → payload « MFA désactivé ». Test `TestMfaStatusAuthDisabled` (échoue en 500 sans le correctif). Vérifié : `test_mfa.py` 32 passed, ruff/mypy 0. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-27 | #87 T6, T7, T8 | Sécurité (fin #87) | `backend/requirements.txt`, `backend/{render,export}.py`, `backend/tools/documents.py`, `backend/auth/router.py`, `backend/main.py`, `semgrep-rules/` (nouveau), `.gitea/workflows/ci.yml`, `tests/test_i18n_parity.py` (nouveau), `tests/test_auth_api.py`, `tests/test_security_headers.py`, `docker-compose.yml`, `.env.example`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/ISSUES_TODOLIST.md` | **T6** : dépendances qualifiées (mistune 3.3.3, multipart 0.0.31, weasyprint 70, mcp 1.28.1, fastapi 0.141.1 + starlette 1.7.0, setuptools 84 ; `cast` mistune 3 sites) — suite 1359 passed, ruff/mypy 0, **`pip-audit` bloquant 0 vuln** (exception ecdsa/Minerva documentée : sans fix, HS256 only). **T7** : **semgrep bloquant** local 8 règles, 0 finding (trivy écarté : réseau). **T8** : Secure auto + `X-Forwarded-Proto` (`TRUST_PROXY`), warning affiné, CORS same-origin explicite, `style-src` résiduel assumé (189+343 sites) ; TODO exemple purgé, locales FR/EN 2213 parité testée, `npm audit` 0. | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-09-29 | BUG-094 | Correction | `backend/xlsx_reader.py`, `frontend/js/viewer.js`, `tests/test_xlsx_viewer.py`, `tests/frontend/xlsx-viewer.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-094 — une feuille vide ou nouvellement ajoutée devient éditable et manipulable.** `render_sheets()` substitue une grille vierge 20×8 (`EMPTY_SHEET_ROWS`/`EMPTY_SHEET_COLS`) quand la feuille ne porte aucune cellule, avec de vraies coordonnées A1 ; la visionneuse retombe sur `parseRef(activeRef) || {row:1,col:1}` pour que le menu Structure propose toujours insérer/supprimer ligne et colonne. Contre-preuve : `TestXlsxDisplay::test_empty_sheet_renders_an_editable_blank_grid` (sans le correctif : « Feuille vide » sans `data-cell`). Vérifié : `test_xlsx_viewer.py` 59 passed, `xlsx-viewer.test.mjs` 52/52. | 🟢 corrigé (en attente vérif utilisateur) | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4293f2a..4032592 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.43.1 | **Dernière mise à jour :** 2026-09-29 +> **Version :** 2.43.2 | **Dernière mise à jour :** 2026-09-29 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** diff --git a/frontend/js/viewer.js b/frontend/js/viewer.js index aabfd7f..d01eae1 100644 --- a/frontend/js/viewer.js +++ b/frontend/js/viewer.js @@ -1940,7 +1940,7 @@ export function renderXlsxViewer(area, data) { const idx = visibleSheetIndex(); const sheetName = sheets[idx]?.name || ""; const activeRef = cellName(activeTd && activeTd.closest(".xlsx-panel") === visiblePanel() ? activeTd : null); - const parsed = parseRef(activeRef); + const parsed = parseRef(activeRef) || { row: 1, col: 1 }; const menu = document.createElement("div"); menu.className = "xlsx-structure-menu"; const item = (label, fn) => { diff --git a/package.json b/package.json index 2673c74..7441740 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.43.1", + "version": "2.43.2", "description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.", "main": "patch.js", "directories": { diff --git a/tests/frontend/xlsx-viewer.test.mjs b/tests/frontend/xlsx-viewer.test.mjs index ecde5ce..e451376 100644 --- a/tests/frontend/xlsx-viewer.test.mjs +++ b/tests/frontend/xlsx-viewer.test.mjs @@ -690,6 +690,16 @@ await test("the 409 lossy flow re-emits with force after confirmation", async () assert.equal(calls[1].body.force, true); }); +// ── BUG-094 — structure actions work on an empty/new sheet ───────────────── + +await test("the structure menu offers row/column actions without an active cell", () => { + const area = mount(); + area.querySelector("#xlsx-structure-btn").click(); + const labels = [...area.querySelectorAll(".xlsx-structure-item")].map((b) => b.textContent); + assert.ok(labels.includes(FR["xlsx.row_insert"]), "row insert available before any click"); + assert.ok(labels.includes(FR["xlsx.col_insert"]), "column insert available before any click"); +}); + // ── A17 — dashboard panel ───────────────────────────────────────────── await test("the dashboard button fetches the metadata and renders named ranges + KPIs", async () => { const area = mount(); diff --git a/tests/test_xlsx_viewer.py b/tests/test_xlsx_viewer.py index 28753aa..f17b005 100644 --- a/tests/test_xlsx_viewer.py +++ b/tests/test_xlsx_viewer.py @@ -96,6 +96,29 @@ class TestXlsxDisplay: resp = client.get(f"/api/file/{VAULT}", params={"path": "corrupt.xlsx"}) assert resp.status_code == 500 + def test_empty_sheet_renders_an_editable_blank_grid(self, client, test_vault_dir): + """BUG-094 — a blank sheet used to render as a bare « Feuille vide » + paragraph with no cell, so a freshly added sheet could not be filled + and had no way to insert a row/column. It now exposes a small editable + grid with real A1 coordinates.""" + from openpyxl import Workbook + + path = Path(test_vault_dir) / "blank.xlsx" + wb = Workbook() + wb.active.title = "Vide" + wb.create_sheet("Vide2") + wb.save(str(path)) + + resp = client.get(f"/api/file/{VAULT}", params={"path": "blank.xlsx"}) + assert resp.status_code == 200 + vide = next(s for s in resp.json()["xlsx_sheets"] if s["name"] == "Vide") + assert "Feuille vide" not in vide["html"] + assert 'data-cell="A1"' in vide["html"] + assert 'data-cell="H20"' in vide["html"] # last cell of the blank grid + assert vide["rows"] == 20 + assert vide["cols"] == 8 + assert vide["truncated"] is False + # ── Index parity (tree visibility) ────────────────────────────────────────