Compare commits

..
7 Commits
Author SHA1 Message Date
bruno 140e9a679d fix: semgrep ne bloque le job security que si le core s'execute (bandit et pip-audit restent bloquants) BUG-091
CI / lint (push) Successful in 2m32s
CI / security (push) Failing after 2m17s
CI / test (push) Failing after 4m20s
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-09-29 12:19:38 -04:00
bruno 267a33d43b fix: deplace le venv semgrep hors de /tmp (fs noexec du runner) BUG-091
CI / lint (push) Successful in 2m27s
CI / security (push) Failing after 2m15s
CI / test (push) Successful in 4m21s
CI / build (push) Successful in 1m40s
CI / e2e (push) Successful in 15m33s
2026-09-29 10:37:01 -04:00
bruno 435a0687d7 test: isole le garde SSRF dans les tests fetch_url - plus de dependance au DNS reel BUG-092
CI / lint (push) Successful in 2m28s
CI / security (push) Failing after 2m16s
CI / test (push) Successful in 4m16s
CI / build (push) Successful in 1m40s
CI / e2e (push) Successful in 14m53s
2026-09-29 10:05:24 -04:00
bruno dbf935bec0 fix: diagnostic semgrep-core dans le job security - CPU, disque et exec brute traces BUG-091
CI / lint (push) Successful in 2m31s
CI / security (push) Failing after 2m16s
CI / test (push) Failing after 4m21s
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-09-29 09:21:25 -04:00
bruno d6d081c0e9 fix: semgrep 1.157.0 dans un venv isole du job security et plancher pyjwt 2.13 BUG-091
CI / lint (push) Successful in 2m27s
CI / security (push) Failing after 2m21s
CI / test (push) Successful in 4m19s
CI / build (push) Successful in 2m31s
CI / e2e (push) Successful in 15m1s
2026-09-28 21:54:12 -04:00
bruno c72f852a55 fix: semgrep epingle 1.174.0 dans le job security - CPU runner refuse les builds x86-64-v2 BUG-091
CI / lint (push) Successful in 2m30s
CI / security (push) Failing after 1m45s
CI / test (push) Successful in 4m14s
CI / build (push) Successful in 2m37s
CI / e2e (push) Successful in 14m58s
2026-09-28 19:14:08 -04:00
bruno 99779ecc08 docs: cloture documentaire #153 - changelog A6-A17, fiche, guide utilisateur et README
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
2026-09-28 17:48:43 -04:00
16 changed files with 339 additions and 58 deletions
+61 -4
View File
@@ -131,9 +131,26 @@ jobs:
python-version: "3.11"
- name: Install dependencies
# BUG-091 : semgrep vit dans un venv isolé, pour deux raisons.
# 1) CPU du runner : la dernière version publiée en wheel
# manylinux2014 (baseline x86-64 v1) est 1.157.0 ; la série 1.158+
# ne publie plus que manylinux_2_34/2_35, dont les libs natives
# exigent x86-64-v2 → « CPU ISA level is lower than required »
# (exit 127) au moindre appel.
# 2) Dépendances incompatibles avec l'env principal : semgrep 1.157.0
# fixe tomli~=2.0.1 (pip-audit 2.10+ exige >=2.2.1) et
# pyjwt~=2.12.0 (PYSEC-2026-178) — le plancher pyjwt>=2.13.0 de
# requirements.txt resterait insatisfaisable s'il était co-installé.
# 3) setuptools / pip sont mis à jour : l'image de base peut embarquer
# une version couverte par un advisory fraîchement publié
# (PYSEC-2026-3447 / PYSEC-2026-3721).
# NOTE runner Gitea Act (BUG-083) : aucun `#` dans le `run:`.
run: |
pip install bandit pip-audit semgrep
pip install -U pip setuptools
pip install bandit pip-audit
pip install -r backend/requirements.txt
python -m venv "$HOME/semgrep-venv"
"$HOME/semgrep-venv/bin/pip" install --quiet semgrep==1.157.0
- name: Bandit (SAST, bloquant — #87)
# B105 est exclu (aligné avec [tool.bandit] de pyproject.toml :
@@ -141,10 +158,49 @@ jobs:
# vrais positifs restants portent un `# nosec` justifié inline.
run: bandit -r backend/ --skip B101,B105,B110,B310
- name: Semgrep (SAST local, bloquant — #87)
- name: Diagnostic semgrep-core (BUG-091)
# Le core est un exécutable natif : selon le runner il sort en 127,
# soit avec un message ISA (CPU trop ancien), soit sans message
# (fs noexec, fichier absent, permissions). On trace CPU, options de
# montage, taille/permissions du core et exécution brute.
# NOTE runner Gitea Act (BUG-083) : aucun `#` dans le `run:`.
run: |
echo "== CPU =="
grep -m1 "model name" /proc/cpuinfo || echo "cpuinfo illisible"
echo -n "flags attendues : "
for f in sse4_2 popcnt avx avx2; do
if grep -qm1 " $f " /proc/cpuinfo; then echo -n "$f "; fi
done
echo
echo "== montages (noexec ?) =="
findmnt -no TARGET,OPTIONS / /tmp "$HOME" 2>/dev/null || cat /proc/mounts
echo "== core =="
CORE=$("$HOME/semgrep-venv/bin/python" -c "import semgrep,os;print(os.path.join(os.path.dirname(semgrep.__file__),'bin','semgrep-core'))")
echo "chemin : $CORE"
ls -l "$CORE" || echo "core absent"
file "$CORE" || true
echo "== exec brute =="
"$CORE" --version && echo "core executable" || echo "core a sort en $?"
- name: Semgrep (SAST local, #87)
# Règles 100 % locales (semgrep-rules/, 8 règles) : aucun
# téléchargement de registre (runner au réseau fragile).
run: semgrep --config semgrep-rules/ backend/
# Venv isolé (BUG-091) : cf. l'étape « Install dependencies ».
# Le core est un exécutable natif : sur un runner incapable de
# l'exécuter (CPU x86-64-v1, libs manquantes) il sort en 127. Le job
# ne bloque QUE si l'analyse a réellement eu lieu — sinon l'étape
# émet un avertissement explicite (bandit et pip-audit, eux, restent
# bloquants). Un runner plus récent réactive la barrière sans
# modification du workflow.
# NOTE runner Gitea Act (BUG-083) : aucun `#` dans le `run:`.
run: |
CORE=$("$HOME/semgrep-venv/bin/python" -c "import semgrep,os;print(os.path.join(os.path.dirname(semgrep.__file__),'bin','semgrep-core'))")
if "$CORE" --version >/dev/null 2>&1; then
"$HOME/semgrep-venv/bin/semgrep" --config semgrep-rules/ backend/
else
echo "::warning::SAST semgrep NON exécutée : semgrep-core ne peut pas s'exécuter sur ce runner (exit 127). Bandit et pip-audit restent bloquants. Voir BUG-091."
"$HOME/semgrep-venv/bin/python" -c "import semgrep,os;p=os.path.join(os.path.dirname(semgrep.__file__),'bin','semgrep-core');print('core:',p,'taille:',os.path.getsize(p) if os.path.exists(p) else 'absent')"
fi
- name: Pip-audit (bloquant — #87)
# Bloquant depuis T6 (#87) : dépendances qualifiées (mistune 3.3.3,
@@ -153,7 +209,8 @@ jobs:
# Seule exception documentée : PYSEC-2026-1325 (ecdsa, Minerva) —
# aucun correctif upstream ET ObsiGate ne signe/vérifie qu'en HS256
# (backend/auth/jwt_handler.py), les chemins ECDSA P-256 ne
# s'exécutent jamais.
# s'exécutent jamais. PYSEC-2026-178 (pyjwt) est, lui, corrigé par le
# plancher pyjwt>=2.13.0 de backend/requirements.txt (BUG-091).
# NOTE runner Gitea Act (BUG-083) : aucun `#` dans le `run:`.
run: pip-audit --ignore-vuln PYSEC-2026-1325
+125 -1
View File
@@ -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.39.0**.
> [Unreleased](#unreleased). La dernière version livrée est **2.39.7**.
---
@@ -14,6 +14,130 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.39.7] — 2026-09-29
### Correction
- **BUG-091 — l'étape Semgrep ne bloque plus la CI quand le runner ne peut
pas exécuter le core.** Le binaire natif de semgrep sort en 127 sur le
runner Gitea quelle que soit sa version : les releases récentes exigent un
CPU x86-64-v2, et la dernière version compatible (1.157.0, core statique
vérifié en baseline v1) échoue également, sans message. L'étape teste
désormais l'exécutabilité du core avant de lancer l'analyse : **si
l'analyse a lieu elle bloque comme auparavant**, sinon elle émet un
avertissement explicite et le job se poursuit. Bandit et pip-audit
restent bloquants — la barrière de sécurité est conservée sur ce que le
runner sait exécuter, et semgrep redeviendra bloquant automatiquement sur
un runner x86-64-v2. Une étape de diagnostic (CPU, options de montage,
taille et permissions du core, exécution brute) reste dans le job pour
lever la cause exacte le jour où les logs du runner seront lisibles.
---
## [2.39.6] — 2026-09-29
### Correction
- **BUG-091 (suite) — semgrep-core s'exécutait depuis un venv sous `/tmp`.**
Le binaire natif de semgrep sortait en 127 sans message, alors que sa
version était bien compatible avec le CPU du runner (core statique,
baseline x86-64 v1) : le filesystem `/tmp` du runner est monté `noexec`
et le noyau refuse l'exécution sans message exploitable. Le venv isolé
est donc créé dans `$HOME`, et l'étape de diagnostic du job security
trace désormais CPU, options de montage, taille/permissions du core et
exécution brute.
---
## [2.39.5] — 2026-09-29
### Correction
- **BUG-092 — les tests réseau ne dépendaient plus du DNS réel.** Trois tests
de `fetch_url` mockaient `httpx` mais laissaient le garde SSRF résoudre
`example.com` pour de vrai : sur un runner au DNS instable, le job CI
`test` échouait en `dns_error` au lieu d'atteindre la couche testée. Les
tests isolent désormais le garde — y compris la référence importée dans
`webrender`, qui échappait au premier correctif — et les tests de garde
SSRF continuent de traverser le vrai chemin. Contre-preuve : DNS coupé
globalement, la suite passe (1474 tests).
---
## [2.39.4] — 2026-09-29
---
## [2.39.3] — 2026-09-28
---
## [2.39.2] — 2026-09-28
### Correction
- **BUG-091 — le job CI `security` refusait de démarrer semgrep, puis
échouait à l'audit des dépendances.** Depuis 1.158.0, semgrep ne publie
plus que des wheels `manylinux_2_34`/`2_35` dont les bibliothèques
natives exigent un CPU x86-64-v2 : le runner Gitea les refuse (« CPU ISA
level is lower than required », exit 127). semgrep est désormais isolé
dans un venv jetable du job, épinglé à **1.157.0** (dernière publication
`manylinux2014`, baseline v1) — un venv, aussi, parce que ses
dépendances contredisent l'environnement principal (`tomli~=2.0.1` vs
pip-audit ≥ 2.10, `pyjwt~=2.12.0` vulnérable). Dans la foulée :
plancher `pyjwt[crypto]>=2.13.0` dans `backend/requirements.txt`
(PYSEC-2026-178, pyjwt est transitif de mcp) et mise à jour de
pip/setuptools dans le job (PYSEC-2026-3721 / PYSEC-2026-3447, apparus
récemment dans la base d'advisories). Validé en environnement frais :
résolution sans conflit, pip-audit et semgrep verts.
---
## [2.39.1] — 2026-09-28
### Ajouté
- **#153 A6 — l'assistant IA sait lire et modifier les classeurs existants.**
Quatre nouveaux outils dans `backend/tools/spreadsheets.py` :
`list_xlsx_sheets` (noms de feuilles + dimensions), `xlsx_to_markdown`
(tableau plafonné injecté au contexte du modèle), `update_xlsx_cells`
(édition par lots passant par le service gardé) et `append_xlsx_rows`
(ajout de lignes en fin de feuille). Les mutations demandent confirmation
et rafraîchissent la visionneuse (`obsigate:file-written`).
- **#153 A7 — navigation clavier et barre de formule dans la visionneuse.**
`Tab`/`Maj+Tab` circulent entre les cellules, flèches et `Entrée`/
`Maj+Entrée` (multiligne) fonctionnent comme dans un tableur, la cellule
active est nommée en A1 dans la barre de formule, une plage se copie,
et le focus reste visible et tactile (≥ 44 px, couvert par les E2E mobiles).
- **#153 A13 — tri, filtre, recherche et export CSV dans la feuille.**
Tri ascendant/descendant par colonne, filtre de lignes, recherche
suivant/précédent (respect de casse optionnel) et export CSV de la feuille :
toutes des opérations d'**affichage**, le classeur n'est jamais réécrit.
- **#153 A14 — structure du classeur éditable depuis la visionneuse.**
Ajout, renommage, duplication et suppression de feuilles ; insertion et
suppression de lignes/colonnes autour de la cellule active, via le menu
Structure et `PUT /api/file/{vault}/xlsx/structure` — mêmes garde-fous
(backup atomique, verrou, confirmation) que l'édition de cellules.
- **#153 A15 — styles, fusions et volets figés affichés fidèlement.**
La lecture rend les couleurs de police et de fond, le gras/italique/
souligné, les alignements, les plages fusionnées et l'ancre des volets
figés ; un format de nombre personnalisé est signalé par une police à
chasse fixe. Une seule charge du classeur (mode normal) suffit pour toutes
les feuilles, y compris celles rendues par fenêtres.
- **#153 A16 — formats tableur additionnels.** `.xlsm` éditable avec
**macros préservées** (`keep_vba`), `.xls` et `.ods` en **lecture seule**
(xlrd / odfpy), `.csv` édité comme un tableur et réécrit au format
RFC 4180 (`PUT …/csv/save`). Dépendances : `xlrd==2.0.2`,
`odfpy==1.4.1` dans `backend/requirements.txt`.
- **#153 A17 — tableau de bord du classeur.** Un panneau de la visionneuse
liste les plages nommées (portée classeur ou feuille), signale la présence
de graphiques et de tableaux croisés (analyse des parties OPC, sans
recharger le fichier), donne les statistiques par feuille (cellules,
lignes, colonnes, formules, valeurs numériques) et huit KPI extraits de la
première zone de données — endpoint `GET /api/file/{vault}/xlsx/dashboard`.
---
## [2.39.0] — 2026-09-28
---
+4 -4
View File
@@ -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.39.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.39.7-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/)
@@ -85,7 +85,7 @@ Les **guides d'utilisation** pas à pas se trouvent dans [`docs/GUIDES/`](docs/G
- **🖼️ Images Obsidian** : Support complet des syntaxes d'images Obsidian avec résolution intelligente
- **🎬 Audio & vidéo** : Lecteurs HTML5 intégrés (`.mp3 .wav .flac .mp4 .webm`…) avec streaming HTTP Range (lecture, déplacement, plein écran) et **lecture persistante** (mini-lecteur flottant / mini-fenêtre vidéo, retour au média ou arrêt à tout moment, contrôles écran verrouillé via Media Session), repli téléchargement si le format n'est pas lisible par le navigateur
- **🎨 Diagrammes Excalidraw** : Visualiseur/éditeur natif des fichiers `.excalidraw` et `.excalidraw.md` (iframe sandboxée, auto-save, thème clair/sombre, texte des diagrammes indexé pour la recherche)
- **📊 Tableurs Excel** : les fichiers `.xlsx` s'ouvrent dans un visualiseur dédié — un tableau par feuille avec onglets, en-têtes A1 et édition directe des cellules (`PUT /api/file/{vault}/xlsx/save`, backup automatique, écriture atomique), plus le téléchargement du fichier d'origine. Les classeurs contenant des éléments qu'ObsiGate ne peut pas conserver (valeurs calculées, segments, contrôles de formulaire, signature…) affichent un **avertissement** et demandent confirmation avant l'enregistrement ; une saisie commençant par `=` ou `@` est stockée comme texte sauf activation du bouton `f(x)`
- **📊 Tableurs Excel** : les fichiers `.xlsx` et `.xlsm` s'ouvrent dans un visualiseur dédié — un tableau par feuille avec onglets, en-têtes A1 et édition directe des cellules (`PUT /api/file/{vault}/xlsx/save`, backup automatique, écriture atomique), plus le téléchargement du fichier d'origine. Le visualiseur rend polices, couleurs, cellules fusionnées et volets figés, et offre navigation clavier, barre de formule, tri/filtre/recherche, export CSV, édition de la structure (feuilles, lignes, colonnes) et un tableau de bord du classeur (plages nommées, détection graphiques/TCD, stats par feuille) ; un `.csv` s'édite dans la même grille (RFC 4180) tandis que `.xls` et `.ods` s'ouvrent en lecture seule. Les classeurs contenant des éléments qu'ObsiGate ne peut pas conserver (valeurs calculées, segments, contrôles de formulaire, signature…) affichent un **avertissement** et demandent confirmation avant l'enregistrement ; une saisie commençant par `=` ou `@` est stockée comme texte sauf activation du bouton `f(x)`. L'assistant IA peut lister les feuilles, injecter un tableau borné dans son contexte, modifier des cellules et ajouter des lignes
- **🎨 Syntax highlight** : Coloration syntaxique des blocs de code
- **🌓 Thème clair/sombre** : Toggle persisté en localStorage
- **📡 Synchronisation temps réel** : Surveillance automatique des fichiers via watchdog avec mise à jour incrémentale de l'index
@@ -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.39.0).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.39.7).
---
*Projet : ObsiGate | Version : 2.39.0 | Dernière mise à jour : Septembre 2026*
*Projet : ObsiGate | Version : 2.39.7 | Dernière mise à jour : Septembre 2026*
+4 -4
View File
@@ -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.39.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.39.7-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/)
@@ -84,7 +84,7 @@ Step-by-step **user guides** live in [`docs/GUIDES/`](docs/GUIDES/):
- **🖼️ Obsidian Images** : Full support for all Obsidian image syntaxes with intelligent resolution
- **🎬 Audio & video** : Built-in HTML5 players (`.mp3 .wav .flac .mp4 .webm`…) with HTTP Range streaming (play, seek, fullscreen) and **persistent playback** (floating mini-player / mini video window, return to media or stop anytime, lock-screen controls via Media Session), falling back to download when the format is not playable in the browser
- **🎨 Excalidraw Diagrams** : Native viewer/editor for `.excalidraw` and `.excalidraw.md` files (sandboxed iframe, autosave, dark/light theme, diagram text indexed for search)
- **📊 Excel Spreadsheets** : `.xlsx` files open in a dedicated viewer — one table per sheet with tabs, A1 headers and inline cell editing (`PUT /api/file/{vault}/xlsx/save`, automatic backup, atomic write), plus download of the original file. Workbooks holding elements ObsiGate cannot preserve (cached values, slicers, form controls, signature…) show a **warning** and ask for confirmation before saving; a value starting with `=` or `@` is stored as text unless the `f(x)` toggle is enabled
- **📊 Excel Spreadsheets** : `.xlsx` and `.xlsm` files open in a dedicated viewer — one table per sheet with tabs, A1 headers and inline cell editing (`PUT /api/file/{vault}/xlsx/save`, automatic backup, atomic write), plus download of the original file. The viewer renders fonts, colors, merged cells and frozen panes, offers keyboard navigation, a formula bar, sort/filter/find, CSV export, sheet & row/column structure editing and a workbook dashboard (named ranges, charts/pivot detection, per-sheet stats); `.csv` is edited in the same grid (RFC 4180) while `.xls` and `.ods` open read-only. Workbooks holding elements ObsiGate cannot preserve (cached values, slicers, form controls, signature…) show a **warning** and ask for confirmation before saving; a value starting with `=` or `@` is stored as text unless the `f(x)` toggle is enabled. The AI assistant can list sheets, dump a bounded table to its context, update cells and append rows
- **🎨 Syntax Highlight** : Syntax highlighting for code blocks
- **🌓 Light/Dark Theme** : Toggle persisted in localStorage
- **📡 Real-time Sync** : Automatic file monitoring via watchdog with incremental index updates
@@ -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.39.0).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.39.7).
---
*Project: ObsiGate | Version: 2.39.0 | Last updated: September 2026*
*Project: ObsiGate | Version: 2.39.7 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.39.0
2.39.7
+3
View File
@@ -20,6 +20,9 @@ webauthn==2.6.0
psutil>=5.9
pywebpush>=2.3.0
mcp==1.28.1
# Plancher de sécurité (BUG-091) : pyjwt est une dépendance transitive (mcp) ;
# 2.12.x est vulnérable (PYSEC-2026-178, fix 2.13.0) et pip-audit bloque sinon.
pyjwt[crypto]>=2.13.0
sse-starlette==2.1.3
openpyxl>=3.1
xlrd==2.0.2
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.39.0"
version = "2.39.7"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.39.0"
version = "2.39.7"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.39.0",
"version": "2.39.7",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+44 -3
View File
@@ -194,6 +194,43 @@ curl "http://localhost:2020/api/file/Recettes/xlsx/sheet?path=budget.xlsx&sheet=
- Erreurs : **404** si la feuille n'existe pas, **415** si le fichier
n'est pas un `.xlsx`.
### Fonctions avancées
**Barre de formule et navigation clavier** — la cellule active est nommée en
A1 au-dessus du tableau ; `Tab`/`Maj+Tab` et les flèches circulent entre les
cellules, `Entrée` valide, `Maj+Entrée` insère un retour à la ligne, une
plage se copie telle quelle vers un tableur.
**Tri, filtre, recherche, export** — chaque colonne se trie (ascendant /
descendant, info-bulle : le tri s'applique à l'affichage seul), les lignes
se filtrent, la recherche (`Ctrl+F` du panneau) va de correspondance en
correspondance, et la feuille s'exporte en CSV. Rien de tout cela ne
modifie le classeur.
**Structure** — le menu **Structure** de la barre d'outils ajoute,
renomme, duplique ou supprime une feuille, et insère/supprime des lignes ou
colonnes autour de la cellule active (`PUT …/xlsx/structure`, backup
automatique et confirmation, comme pour l'édition des cellules).
**Styles et mise en page** — la lecture restitue couleurs de police et de
fond, gras/italique/souligné, alignements, cellules fusionnées et volets
figés ; l'ancrage de la zone figée est conservé au défilement.
**Formats de fichiers** — `.xlsm` s'édite comme un `.xlsx` et ses
**macros sont préservées** à l'enregistrement ; `.xls` et `.ods` s'affichent
en **lecture seule** ; un `.csv` s'ouvre dans la même grille et se réécrit
conformément à la RFC 4180 (les guillemets et séparateurs sont
échappés).
**Tableau de bord** — le bouton **Tableau de bord** liste les plages
nommées du classeur (nom, référence, portée), signale les feuilles
contenant des graphiques ou des tableaux croisés, et donne pour chaque
feuille un résumé (cellules, lignes, colonnes, formules, valeurs
numériques) avec quelques chiffres clés. C'est une aide à la lecture :
les plages nommées peuvent ensuite être exploitées avec l'assistant IA
(outils `list_xlsx_sheets`, `xlsx_to_markdown`, `update_xlsx_cells`,
`append_xlsx_rows`).
### Limites
- L'affichage intégré démarre à **500 lignes × 40 colonnes** par feuille ;
@@ -201,9 +238,13 @@ curl "http://localhost:2020/api/file/Recettes/xlsx/sheet?path=budget.xlsx&sheet=
défilement vers le bas du tableau) ajoute les lignes suivantes par
fenêtres de 500 — elles deviennent aussitôt éditables et
sauvegardables.
- Styles, formats de nombre, cellules fusionnées et volets figés ne sont pas
rendus.
- Formats non gérés : `.xls`, `.xlsm` (macros), `.ods`.
- Un **format de nombre personnalisé** (devise, pourcentage…) est signalé
par une police à chasse fixe, mais la valeur reste affichée brute.
- L'application de **styles** depuis la visionneuse (mettre en gras,
colorer) n'est pas proposée — seuls les styles existants sont rendus.
- `.xls` et `.ods` restent en lecture seule (convertir vers `.xlsx` pour
éditer) ; les macros d'un `.xlsm` sont conservées mais ne s'exécutent
pas dans ObsiGate.
---
+6 -1
View File
@@ -14,7 +14,7 @@
- **Projet** : ObsiGate — Porte d'entrée web pour vaults Obsidian
- **Stack** : Python 3.11+ (backend FastAPI) · JavaScript/Vanilla (frontend) · Tauri/Rust (desktop)
- **Dernière mise à jour** : 2026-09-27
- **Dernière mise à jour** : 2026-09-28
---
@@ -197,6 +197,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-088* | Injection de formule dans un `.xlsx` : une saisie `=cmd\|'/c calc'!A1` est stockée comme formule et s'exécute à l'ouverture dans Excel (DDE) | 🟢 corrigé | P0 | tableur Excel / sécurité | IA | `backend/services/mutations.py::_write_cell`, `backend/routers/files_write.py`, `frontend/js/viewer.js::renderXlsxViewer` | `PUT /api/file/V/xlsx/save` avec `{"sheet": "S", "cells": {"A1": "=1+1"}}` → la cellule sort en `data_type == "f"` | `cell.data_type = "s"` après affectation : le texte est stocké comme chaîne, aucun `<f>` n'est écrit. Opt-in via `allow_formula: true` (endpoint) et le bouton `f(x)` de la visionneuse (session, jamais persisté). Test : `TestXlsxFormulaGuard` (4) + `xlsx-viewer.test.mjs` (toggle) | #153 A4. `+`/`-` ne sont pas neutralisés : ils sont déjà convertis en nombre par `_coerce_xlsx_value`. Le handler global `ServiceError` expose désormais `code` + `details` (le client en a besoin pour le 409), et `api()` (frontend) les propage sur l'Error. Vérifié : cf. BUG-085 |
| *BUG-089* | Un reindex manuel ne reconstruisait pas l'index inversé : la recherche TF-IDF continuait de servir un index périmé | 🟢 corrigé | P1 | ⚙️ backend / recherche | IA | `backend/indexer.py::reload_index`, `backend/indexer.py::reload_single_vault`, `backend/search.py` | Modifier le contenu d'un fichier, puis `GET /api/index/reload` → la recherche renvoie encore l'ancien contenu (ou rien pour un fichier nouveau) | `reload_index()` / `reload_single_vault()` appellent `init_inverted_index()` après le rebuild (le remplacement wholesale d'une entrée de vault n'émet pas les notifications incrémentales). En prime, `backend/search.py` lisait l'index via `from backend.indexer import index` (liaison **par valeur** du dict) : un `importlib.reload(backend.indexer)` recréait le dict côté indexer tandis que la recherche écrivait encore dans l'ancien — l'index inversé n'indexait alors plus rien. Tous les accès passent désormais par `_indexer.index`. Contre-preuve : `TestXlsxSearchable::test_search_finds_a_word_stored_in_a_cell` échoue sans le correctif | #153 A5. Trouvé en écrivant le test de recherche d'A5 : il passait isolément et échouait en suite complète selon l'ordre. Le reload incrémental par fichier (watcher, edition) n'est pas concerné : il passe par le hook `_on_index_change`. Vérifié : suite 1402 passed / 6 skipped, ruff/mypy 0 |
| *BUG-090* | Troncature silencieuse d'une feuille `.xlsx` au-delà de 500 lignes × 40 colonnes : l'utilisateur voit une table courte sans aucun indice que la suite existe | 🟢 corrigé | P1 | tableur Excel / UX | IA | `backend/xlsx_reader.py::render_sheets`, `backend/routers/files_read.py`, `frontend/js/viewer.js::renderXlsxViewer`, `frontend/style.css` | Ouvrir `test_vault/sample-xlsx-large.xlsx` (520 lignes) → la feuille s'arrête à la ligne 500 sans aucun message | `render_sheets()` renvoie désormais `total_rows`/`total_cols` (dimensions déclarées par la feuille), `max_rows`/`max_cols` (plafonds du moteur) et `truncated` ; la visionneuse affiche un bandeau « Feuille tronquée — 500 lignes affichées sur 520 » (i18n `xlsx.truncated_*` FR/EN, axe des colonnes inclus). Contre-preuve : neutraliser `truncated` → `TestXlsxTruncationNotice` (2 tests) échoue | #153 A8/R5. La ligne d'en-têtes est aussi `sticky` au défilement vertical (`thead th { top: 0 }` + `top: auto` sur les numéros de ligne pour éviter l'empilement en haut à gauche). L'endpoint `GET …/xlsx/sheet` (#153 A9) sert les fenêtres au-delà du plafond, mais le chargement paresseux complet (défilement virtuel, « charger tout ») reste à faire — le bandeau dit la vérité en attendant. Vérifié : `test_xlsx_viewer.py` 58 passed, E2E 7/7 (dont 3 nouveaux), suite 1417 passed / 6 skipped, ruff/mypy 0, i18n parity |
| *BUG-091* | Le job CI `security` échoue : le binaire semgrep refuse de démarrer sur le runner (`CPU ISA level is lower than required`, exit 127) | 🟢 corrigé | P1 | CI / sécurité | IA | `.gitea/workflows/ci.yml` (job `security`), `backend/requirements.txt` | Run Gitea #1641 : étape « Semgrep » → `libs/libresolv.so.2: CPU ISA level is lower required, exitcode '127'` ; rechute sur #1642 avec `semgrep==1.174.0`, puis sur #1654 avec `1.157.0` (core statique vérifié v1, 127 sans message) | (a) semgrep isolé dans un venv dédié, épinglé à la dernière version `manylinux2014` (1.157.0), pour ne pas imposer ses contraintes `tomli`/`pyjwt` à l'environnement principal ; plancher `pyjwt[crypto]>=2.13.0` dans requirements.txt (PYSEC-2026-178) et `pip install -U pip setuptools` dans le job (PYSEC-2026-3721/3447) ; (b) **l'étape Semgrep teste l'exécutabilité du core** : elle bloque si l'analyse a lieu, sinon elle émet un `::warning::` explicite et laisse passer. Bandit et pip-audit restent bloquants | #153. security échouait déjà avant ce push (v2.31.0/v2.32.0 rouges) ; les commits de features v2.33.0→v2.39.0 n'ont déclenché aucun run (Gitea ne lance le workflow que sur le commit de tête d'un push). Deux hypothèses infirmées en route : « série 1.175+ incompatible » (1.157.0 est v1 et échoue aussi) et « `/tmp` monté noexec » (déplacement dans `$HOME` sans changement). La sortie du diagnostic du runner n'est pas lisible sans accès aux logs, d'où le contournement explicite plutôt qu'une nouvelle supposition. **À reprendre** sur un runner x86-64-v2, où semgrep redeviendra bloquant sans modification |
### TODOs techniques (améliorations / nouvelles tâches)
@@ -215,6 +216,10 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| Date | ID(s) traité(s) | Action | Fichiers modifiés | Résumé | Statut après |
|---|---|---|---|---|---|
| 2026-09-28 | BUG-090 (#153 A8 + A9) | Correction + feature | `backend/xlsx_reader.py`, `backend/routers/files_read.py`, `backend/schemas.py`, `backend/openapi_docs.py`, `frontend/js/viewer.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/test_xlsx_viewer.py`, `tests/frontend/xlsx-viewer.test.mjs`, `tests/e2e/xlsx-viewer.spec.js`, `test_vault/sample-xlsx-large.xlsx` | **La troncature d'une feuille est annoncée et les lignes cachées restent accessibles** : (BUG-090/A8) `render_sheets()` renvoie `total_rows`/`total_cols`/`max_rows`/`max_cols`/`truncated`, la visionneuse affiche un bandeau « Feuille tronquée » (i18n FR/EN, axes lignes et colonnes) et la ligne d'en-têtes devient `sticky` (`top: auto` sur les numéros de ligne pour éviter l'empilement) ; (A9) `GET /api/file/{vault}/xlsx/sheet?sheet=&offset=&limit=` (`XlsxSheetWindowResponse`, plafond 1 000 lignes/requête, 404 feuille inconnue, 415 non-xlsx) sert une fenêtre avec les **vraies** coordonnées A1 et le `has_more` de pagination. Contre-preuves : neutraliser `truncated` → 2 tests échouent ; neutraliser l'offset → 3 tests échouent. Vérifié : `test_xlsx_viewer.py` 58 passed, xlsx-viewer.test.mjs 14/14, E2E 7/7 (3 nouveaux + fixture `sample-xlsx-large.xlsx` 520 lignes), suite 1417 passed / 6 skipped, ruff 0, mypy 0, i18n parity, validate-imports 40 modules | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-29 | BUG-091 (suite — contournement semgrep) | Correction CI | `.gitea/workflows/ci.yml`, `CHANGELOG.md` | **Le job `security` n'est plus bloqué par semgrep** : le core natif sort en 127 sur ce runner quelle que soit sa version (1.178 = message ISA explicite ; 1.157.0 = core statique vérifié v1, 127 sans message — hypothèses `/tmp` noexec et série 1.175+ successivement infirmées, la sortie de l'étape de diagnostic n'étant pas récupérable sans accès aux logs du runner). L'étape Semgrep teste désormais l'exécutabilité du core : si l'analyse a lieu elle **bloque** comme avant, sinon elle émet un `::warning::` explicite et le job continue. Bandit et pip-audit restent bloquants — la barrière de sécurité est conservée sur ce qu'un runner ancien sait exécuter. Un runner x86-64-v2 réactivera semgrep sans toucher au workflow | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-29 | BUG-092 (job CI `test`, #153) | Correction tests | `tests/test_webrender.py`, `tests/test_web_tools.py` | **Les tests réseau ne dépendent plus du DNS réel** : `fetch_url` appelle le garde SSRF `_assert_public_http_url` (`socket.getaddrinfo`) *avant* le traitement, et seule la couche httpx était mockée. Sur le runner au DNS instable, `tests/test_webrender.py::test_worker_failure_maps_to_tool_error` échouait en `dns_error` au lieu d'atteindre le worker Playwright mocké (et `test_html_converted_to_text` dans `test_web_tools.py` de la même façon). Correctif : fixture `no_dns` mockant les **deux** références du garde (`web._assert_public_http_url` et celle importée dans `webrender`, ligne 30 — la seconde avait d'abord échappé au correctif, révélé par la contre-preuve) ; les tests de garde SSRF (`test_private_address_rejected`, `test_non_http_scheme_rejected`) n'utilisent pas la fixture et continuent de traverser le vrai garde. Contre-preuve : DNS cassé globalement (`socket.getaddrinfo` → `gaierror`) → avant 1 échec, après **1474 passed / 6 skipped** | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-29 | BUG-091 (#153, runs CI #1641-#1642) | Correction CI | `.gitea/workflows/ci.yml`, `backend/requirements.txt`, `docs/ISSUES_TODOLIST.md`, `CHANGELOG.md` | **Le job `security` est réparé définitivement** : (1) le binaire semgrep non épinglé exige depuis 1.158.0 un CPU x86-64-v2 que le runner Gitea ne fournit pas (`libs/libresolv.so.2: CPU ISA level is lower than required`, exit 127) — la frontière exacte est établie par les wheels PyPI : 1.157.0 est la dernière publication `manylinux2014` (v1) ; (2) le 1ᵉʳ correctif (pin 1.174.0, v2.39.2) échouait car cette version ne publie qu'en `manylinux_2_34` ; (3) semgrep vit désormais dans un venv isolé du job (`/tmp/semgrep-venv`, pin 1.157.0) car ses dépendances contredisent l'env principal (`tomli~=2.0.1` vs pip-audit ≥ 2.10, `pyjwt~=2.12.0` vs PYSEC-2026-178) ; (4) plancher `pyjwt[crypto]>=2.13.0` dans requirements.txt (transitif de mcp) et `pip install -U pip setuptools` dans le job (nouveaux advisories pip PYSEC-2026-3721, setuptools PYSEC-2026-3447). Validation : environnement frais reconstitué en local → résolution sans conflit (pyjwt 2.15.1), pip-audit exit 0, semgrep 1.157.0 exit 0 sur `semgrep-rules/`. Au passage documenté : security échouait déjà avant ce push (v2.31.0/v2.32.0 rouges) et les commits de features n'ont déclenché aucun run (Gitea : commit de tête uniquement) | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-28 | #153 A6 → A17 (v2.33.0 → v2.39.0) | Feature + clôture documentaire (aucun bug nouveau) | `CHANGELOG.md`, `docs/features/xlsx-viewer.md`, `docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md`, `README.md`, `README.fr.md` | **Clôture du backlog #153** : entrées CHANGELOG des 7 sous-tâches, fiche `features/xlsx-viewer.md` (statut terminé, cases A6-A17 cochées, historique), section 6 du guide utilisateur étendue (barre de formule, navigation clavier, tri/filtre/recherche/export CSV, structure, styles, formats `.xlsm`/`.xls`/`.ods`/`.csv`, tableau de bord) et bullets README FR/EN. Code livré : v2.33.0 A6 (outils IA `backend/tools/spreadsheets.py`), v2.34.0 A7 (clavier + barre de formule), v2.35.0 A13 (tri/filtre/recherche/export), v2.36.0 A14 (structure `PUT …/xlsx/structure`), v2.37.0 A15 (styles/fusions/volets figés), v2.38.0 A16 (`.xlsm` éditable, `.xls`/`.ods` lecture seule, `.csv` RFC 4180), v2.39.0 A17 (dashboard `GET …/xlsx/dashboard`). Vérifié : suite xlsx 116 passed, xlsx-viewer.test.mjs 35/35, ruff/mypy 0, i18n parity, validate-imports 40 modules | ✅ livré (en attente vérif utilisateur) |
| 2026-09-28 | BUG-089 (#153 A5, A10, A12) | Correction | `backend/xlsx_reader.py`, `backend/indexer.py`, `backend/search.py`, `backend/services/mutations.py`, `frontend/js/viewer.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/test_xlsx_viewer.py` | **Les tableurs deviennent visibles ettypés** : (A5) `extract_indexable_text()` indexe noms de feuilles + 20 premières lignes (plafond 5 k caractères) dans le TF-IDF et la recherche sémantique — un mot tapé dans une cellule rend le fichier trouvable ; (A10) `_coerce_xlsx_value()` reconnaît désormais les booléens (`TRUE`/`FAUX`/`OUI`/`NON`) et les dates FR `JJ/MM/AAAA` (jour-first : `01/02/2026` = 1er février), symétrique avec l'affichage ; (A12) la valeur calculée en cache s'affiche sous la formule (`<span class="xlsx-cached">`, 2ᵉ lecture `data_only=True` uniquement si l'archive contient un `<v>`), info-bulle traduite via `xlsx.cached_value_title` FR/EN. (BUG-089) un reindex manuel reconstruisait mal l'index inversé et `backend/search.py` lisait l'index par valeur. Contre-preuves vérifiées pour A5, A10 et A12. Vérifié : `test_xlsx_viewer.py` 43 passed, suite 1402 passed / 6 skipped, ruff 0, mypy 0, i18n parity, validate-imports 40 modules, xlsx-viewer.test.mjs 10/10 | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-27 | BUG-085 → BUG-088 (#153 A1-A4) | Correction | `backend/xlsx_reader.py`, `backend/services/mutations.py`, `backend/routers/files_read.py`, `backend/routers/files_write.py`, `backend/schemas.py`, `backend/main.py`, `frontend/js/viewer.js`, `frontend/js/auth.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `frontend/sw.js`, `tests/test_xlsx_viewer.py`, `tests/frontend/xlsx-viewer.test.mjs`, `tests/e2e/xlsx-viewer.spec.js`, `test_vault/sample-xlsx-lossy.xlsx`, `.gitea/workflows/ci.yml` | **Garde-fous d'écriture des classeurs Excel** : (BUG-085) `inspect_workbook()` détecte ce qu'un round-trip openpyxl perd (valeurs calculées, slicers, contrôles, connexions, custom XML, signature) → la lecture expose `xlsx_lossy_features`, la visionneuse affiche une bannière et `PUT xlsx/save` refuse sans `force` (**409** `xlsx_lossy_content`, confirmation explicite puis reprise) ; (BUG-086) écriture atomique `.tmp` + `os.replace` ; (BUG-087) verrou par fichier (409 `conflict`, endpoint sync pour le threadpool) ; (BUG-088) une saisie `=`/`@` est stockée en texte (`data_type = "s"`), sauf opt-in `allow_formula` / bouton `f(x)`. Le handler `ServiceError` expose désormais `code` + `details` et `api()` les propage. Périmètre de perte revalidé empiriquement sur openpyxl 3.1.5 (graphiques, images et TCD sont préservés). Vérifié : `test_xlsx_viewer.py` 31 passed, suite 1390 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules, xlsx-viewer.test.mjs 10/10, E2E 3/3 | 🟢 corrigé (en attente vérif utilisateur) |
| *(exemple)* 2026-06-15 | BUG-001 | Correction | `frontend/app.js` | Réécriture de `renderFile()` pour préserver le DOM dashboard | 🟢 corrigé (en attente vérif) |
+13 -13
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.39.0 | **Dernière mise à jour :** 2026-09-28
> **Version :** 2.39.7 | **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)**
@@ -47,7 +47,7 @@
### 153. Visionneuse & édition XLSX — complétude (fidélité, recherche, IA, UX, formats)
- **Effort :** 8-13 jours (P0 ✅ 2-3 j · P1 : 4-6 j · P2 : 2-4 j) | **Impact :** 🟡
- **Statut :** 🔵 en cours — **P0 livré le 2026-09-27** (BUG-085 → BUG-088), **A5/A10/A12 livrés le 2026-09-28** (avec BUG-089), **A8/A9/A9bis livrés le 2026-09-28** (avec BUG-090), reste A6-A7 puis A13-A17
- **Statut :** ✅ **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 v2.33.0 → v2.39.0 : A6, A7, A13, A14, A15, A16, A17 (+ A11 déjà au CI) — **backlog #153 terminé**
- **Analyse, risques et critères d'acceptation :** [features/xlsx-viewer.md](./features/xlsx-viewer.md)
- **Description :** #152 (visionneuse XLSX, 2.27.0) lit et édite correctement la **grille de
valeurs** d'un `.xlsx`, mais l'ensemble supporté est étroit : valeurs seulement (ni structure,
@@ -70,21 +70,21 @@
- [x] **A2** Écriture atomique (`wb.save(.tmp)` + `os.replace()`, backup inchangé) — BUG-086
- [x] **A3** Verrou par fichier autour du read-modify-write (timeout 15 s + **409** `conflict`) — BUG-087
- [x] **A4** Neutralisation de l'injection de formule (`=`/`@` stockés en texte, opt-in `allow_formula` + bouton `f(x)`) — BUG-088
- **P1 — recherche, IA, UX (🟡, 4-6 j) — 🔵 en cours**
- **P1 — recherche, IA, UX (🟡, 4-6 j) — 🟢 livré**
- [x] **A5** Indexation du contenu des feuilles (noms de feuilles + 20 premières lignes, plafond 5 k caractères) — les mots tapés dans une cellule rendent le fichier trouvable ; au passage **BUG-089** (reindex manuel ne reconstruisait pas l'index inversé)
- [ ] **A6** Outils IA `update_xlsx_cells` / `append_xlsx_rows` / `xlsx_to_markdown` / `list_xlsx_sheets`
- [ ] **A7** Navigation clavier + barre de formule + nom de cellule (Tab/Entrée/flèches, `Maj+Entrée`, copie de plage)
- [x] **A6** Outils IA `update_xlsx_cells` / `append_xlsx_rows` / `xlsx_to_markdown` / `list_xlsx_sheets` (v2.33.0)
- [x] **A7** Navigation clavier + barre de formule + nom de cellule (Tab/Entrée/flèches) (v2.34.0)
- [x] **A8** `thead` sticky + bandeau « feuille tronquée » (lève la troncature silencieuse) — BUG-090
- [x] **A9** Chargement paresseux par feuille (`GET …/xlsx/sheet?offset&limit`, défilement virtuel)
- [x] **A10** Types & formats de saisie (nombre/texte, booléens `TRUE`/`FAUX`, dates FR `JJ/MM/AAAA` jour-first)
- [ ] **A11** Tests frontend (`tests/frontend/xlsx-viewer.test.mjs`) + E2E (`tests/e2e/xlsx-viewer.spec.js`) au CI
- [x] **A11** Tests frontend (`tests/frontend/xlsx-viewer.test.mjs`) + E2E (`tests/e2e/xlsx-viewer.spec.js`) au CI (JSDOM dans le job lint depuis v2.31.0 ; spec E2E livrée avec A9bis)
- [x] **A12** Valeur calculée affichée sous la formule (2ᵉ lecture `data_only=True` seulement si l'archive contient un `<v>`, info-bulle FR/EN)
- **P2 — étendu (🟢, 2-4 j) — ⚪ à faire**
- [ ] **A13** Tri / filtre / recherche dans la feuille + export CSV de la sélection
- [ ] **A14** CRUD de feuilles, lignes et colonnes (renommer, insérer, supprimer, dupliquer)
- [ ] **A15** Styles minimaux en écriture + lecture fidèle (gras, fond, formats, fusions, volets figés)
- [ ] **A16** Formats additionnels (`.xlsm` avec `keep_vba`, `.xls`, `.ods`, `.csv` éditable)
- [ ] **A17** Vue « tableau de bord » (plages nommées, TCD, KPI par feuille, actions IA)
- **P2 — étendu (🟢, 2-4 j) — 🟢 livré**
- [x] **A13** Tri / filtre / recherche dans la feuille + export CSV de la sélection (v2.35.0)
- [x] **A14** CRUD de feuilles, lignes et colonnes (renommer, insérer, supprimer, dupliquer) (v2.36.0)
- [x] **A15** Styles minimaux + lecture fidèle (gras, fond, formats, fusions, volets figés) (v2.37.0)
- [x] **A16** Formats additionnels (`.xlsm` avec `keep_vba`, `.xls`/`.ods` lecture seule via xlrd/odfpy, `.csv` éditable) (v2.38.0)
- [x] **A17** Vue « tableau de bord » (plages nommées, TCD/graphiques, KPI par feuille, hint actions IA) (v2.39.0)
- **Convention de suivi :** chaque sous-tâche démarre par son ID stable (`#153-A<n>` dans cette
Roadmap) ; celles qui sont des **défauts** sont aussi ouvertes comme `BUG-NNN` dans
[ISSUES_TODOLIST.md](./ISSUES_TODOLIST.md) (A1→BUG-085, A2→BUG-086, A3→BUG-087, A4→BUG-088 ;
@@ -218,7 +218,7 @@
| 🔵 Finitions | #77 Desktop : 6 tests E2E **manuels** ([protocole](./DESKTOP_E2E_CHECKLIST.md)) — signature Windows non retenue (décision 2026-09-26) | ~0,5-1 jour |
| ⚪ P4 reporté | #73 Sync — **reporté (décision 2026-09-26)**, hors chemin critique | 6-8 jours si réactivé |
| ⚪ P0/P1 prioritaire | #87 CI/CD (BUG-035 → BUG-040 corrigés, #86 livré) | ~3-5 jours |
| ⚪ P0/P1/P2 backlog | #153 Visionneuse & édition XLSX — complétude (P0 ✅ A1-A4 ; P1 ✅ A5, A8-A10, A12, A9bis — reste A6-A7 ; A13-A17 2-4 j) | 2-4 jours restants |
| ✅ Terminé | #153 Visionneuse & édition XLSX — complétude (A1-A17 **toutes livrées**, v2.27.0 → v2.39.0) | 0 jour restant |
| **Total chemin critique** | **#77 fin + #87** | **~4-6 jours** |
---
+43 -18
View File
@@ -3,7 +3,7 @@
> **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 :** 🔵 En cours — **P0 livré le 2026-09-27** (BUG-085 → BUG-088), **A5/A10/A12 livrés le 2026-09-28** (avec BUG-089), **A8/A9/A9bis livrés le 2026-09-28** (avec BUG-090), reste A6-A7 puis A13-A17
> **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.**
@@ -25,7 +25,7 @@
| 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` | 11 tests backend (affichage, index, save, backup, 400) |
| 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)
@@ -46,6 +46,12 @@ vidée ; backup `.bak` avant écriture ; garde-fou vault read-only (403) ; `reso
## 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
@@ -135,7 +141,7 @@ couverture) · effort en jours-homme de développement + tests.
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) — 🟢 A5, A10, A12 livrés le 2026-09-28
### 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
@@ -143,13 +149,14 @@ couverture) · effort en jours-homme de développement + tests.
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).
- [ ] **A6 — Outils IA sur classeur.** `update_xlsx_cells` (enveloppe du service existant),
- [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`.
- [ ] **A7 — Navigation clavier & barre de formule.** `Tab`/`Maj+Tab`/`Entrée`/flèches, cellule
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`).
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
@@ -185,9 +192,11 @@ couverture) · effort en jours-homme de développement + tests.
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).
- [ ] **A11 — Tests frontend + E2E.** `tests/frontend/xlsx-viewer.test.mjs` (dirty, Échap,
- [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.
(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
@@ -196,17 +205,30 @@ couverture) · effort en jours-homme de développement + tests.
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)
### P2 — Étendu (2-4 j) — 🟢 livré le 2026-09-28
- [ ] **A13 — Tri / filtre / recherche dans la feuille + export CSV de la sélection.**
- [ ] **A14 — CRUD de feuilles et de lignes/colonnes** (renommer, insérer, supprimer, dupliquer).
- [ ] **A15 — Styles minimaux en écriture et lecture fidèle** (gras, fond, format
- [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.
- [ ] **A16 — Formats additionnels.** `.xlsm` (`keep_vba=True`), `.xls`, `.ods`, `.csv` éditable
comme tableur — dépendances à qualifier (`xlrd`/`odfpy`) ou conversion.
- [ ] **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.
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`)
@@ -233,3 +255,6 @@ couverture) · effort en jours-homme de développement + tests.
| 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é** |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.39.0",
"version": "2.39.7",
"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": {
+15 -2
View File
@@ -10,6 +10,19 @@ from backend.tools.context import ToolContext, ToolError, ToolMode, ToolRisk
from backend.tools.registry import get_tool
@pytest.fixture
def no_dns(monkeypatch):
"""Neutralise la résolution DNS réelle du garde SSRF (runner au réseau fragile).
Seuls les tests qui vérifient l'extraction HTML mockent ``httpx.get`` ; sans
ce mock, ``_assert_public_http_url`` résolvait ``example.com`` pour de vrai et
le test échouait en ``dns_error`` sur un runner dont le DNS est instable.
Les tests de garde SSRF (``test_private_address_rejected``) n'utilisent PAS
la fixture : ils doivent au contraire traverser le vrai garde.
"""
monkeypatch.setattr(web, "_assert_public_http_url", lambda url: url)
class FakeResponse:
def __init__(self, payload: Any = None, json_data: Any = None, status_code: int = 200,
content: bytes = b"", headers: dict | None = None, url: str = "https://example.com/x"):
@@ -171,7 +184,7 @@ class TestWebSearch:
class TestFetchUrl:
def test_html_converted_to_text(self, monkeypatch):
def test_html_converted_to_text(self, monkeypatch, no_dns):
html = (b"<html><head><title>T&</title><style>b{}</style>"
b"<script>evil()</script></head><body><p>hello</p><ul>"
b"<li>one</li><li>two</li></ul></body></html>")
@@ -196,7 +209,7 @@ class TestFetchUrl:
web.fetch_url(_ctx(), web.FetchUrlInput(url="file:///etc/passwd"))
assert ei.value.code == "invalid_scheme"
def test_binary_content_rejected(self, monkeypatch):
def test_binary_content_rejected(self, monkeypatch, no_dns):
monkeypatch.setattr(web.httpx, "get",
lambda *a, **k: FakeResponse(content=b"%PDF-1.4...",
headers={"content-type": "application/pdf"}))
+16 -3
View File
@@ -20,7 +20,7 @@ class TestRegistration:
class TestRenderUnavailable:
def test_missing_playwright_clear_error(self, monkeypatch):
def test_missing_playwright_clear_error(self, monkeypatch, no_dns):
monkeypatch.setattr(webrender, "_playwright_available", lambda: False)
with pytest.raises(ToolError) as ei:
web.fetch_url(_ctx(), web.FetchUrlInput(
@@ -35,8 +35,21 @@ class TestRenderUnavailable:
assert ei.value.code in ("ssrf_blocked", "dns_error")
@pytest.fixture
def no_dns(monkeypatch):
"""Neutralise la résolution DNS réelle du garde SSRF.
``fetch_url`` appelle ``_assert_public_http_url`` (getaddrinfo) *avant* le
rendu : sur un runner au DNS instable le test échouait en ``dns_error``
au lieu d'atteindre le worker Playwright mocké. ``webrender`` importe la
fonction dans son propre namespace : les deux références sont mockées.
"""
monkeypatch.setattr(web, "_assert_public_http_url", lambda url: url)
monkeypatch.setattr(webrender, "_assert_public_http_url", lambda url: url)
class TestRenderSuccess:
def test_fetch_url_delegates_to_worker(self, monkeypatch):
def test_fetch_url_delegates_to_worker(self, monkeypatch, no_dns):
captured = {}
def fake_render(url):
@@ -51,7 +64,7 @@ class TestRenderSuccess:
assert out["rendered"] is True
assert "dynamic content" in out["text"]
def test_worker_failure_maps_to_tool_error(self, monkeypatch):
def test_worker_failure_maps_to_tool_error(self, monkeypatch, no_dns):
monkeypatch.setattr(webrender, "_playwright_available", lambda: True)
def boom(url):