docs(roadmap): menage — aligne le roadmap sur le code reel (#59,63-66,71,74-77,78), resume efforts recalcule + CHANGELOG 2.1.0
CI / lint (push) Successful in 41s
CI / security (push) Successful in 28s
CI / test (push) Successful in 52s
CI / build (push) Successful in 1m10s
CI / e2e (push) Successful in 6m1s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

This commit is contained in:
2026-09-08 00:12:28 -04:00
parent c066b2c82a
commit f9d2c0d2d4
2 changed files with 185 additions and 149 deletions
+29 -1
View File
@@ -6,7 +6,35 @@ 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 non publiés sont dans la section
> [2.0.0 — Unreleased](#200--unreleased). La dernière version publiée est **1.8.0**.
> [2.1.0](#210--2026-09-07). La dernière version publiée est **2.0.0**.
---
## [2.1.0] — 2026-09-07
### Ajouté
- **#74 Support PDF au complet** — `GET /api/file/{vault}/pdf/info` (métadonnées sans
contenu), streaming avec HTTP Range / 206 Partial Content (rendu progressif des gros PDF),
config `OBSIGATE_PDF_MAX_SIZE_MB` (50) + `OBSIGATE_PDF_EXTRACT_TIMEOUT` (30s).
16 tests supplémentaires (26 au total dans `test_pdf.py`).
- **#64 MFA — WebAuthn** (second facteur en plus du TOTP) : enregistrement de clés de
sécurité / biométrie (Windows Hello, Touch ID), assertion au login, gestion des clés dans
« Sécurité du compte », codes de récupération émis à l'activation. Nouveau module
`backend/auth/webauthn_mfa.py` (lib `webauthn==2.6.0`, challenges in-memory TTL 180s à
usage unique). 10 tests avec authentificateur virtuel (CBOR réel, ECDSA P-256).
i18n FR/EN (13 clés). Config : `OBSIGATE_WEBAUTHN_RP_ID` / `_RP_NAME` / `_ORIGINS`.
- **#77 Desktop — port auto-increment** : `pick_free_port()` scanne 17890..17899 si le port
est occupé (2 instances côte à côte possibles) + 3 tests Rust (19 au total côté desktop).
- Docs : sections PDF README FR/EN (Range, /pdf/info), variables env, guide WebAuthn.
### Corrigé
- **PDF stream 500** : `api_pdf_stream` plantait systématiquement (`NameError: current_user`
non injecté — endpoint jamais couvert par un test). Désormais authentifié + testé.
- **Indexation incrémentale des PDF** : le chemin watcher (`_index_single_file_sync`) lisait
les PDFs en `read_text()` → contenu garbage indexé. Utilise maintenant `extract_pdf_text()`
comme le scan complet.
---
+156 -148
View File
@@ -1,6 +1,7 @@
# ObsiGate — Roadmap
> **Version :** 2.0.0-dev | **Dernière mise à jour :** 2026-06-18
> **Version :** 2.1.0-dev | **Dernière mise à jour :** 2026-09-07
> Revue de cohérence roadmap ↔ code : cases cochées selon l'état réel vérifié dans le dépôt (commit c066b2c).
> Voir aussi [CHANGELOG.md](./CHANGELOG.md), [AUDIT_TECHNIQUE.md](./docs/AUDIT_TECHNIQUE_2026-05-27.md)
---
@@ -119,7 +120,7 @@
---
## 🔵 En cours (P1)
## ✅ Complété (suite — v1.7 → v2.1)
### 58. Tests E2E Playwright ✅ FAIT
- **Effort :** 2-3 jours | **Impact :** 🔴
@@ -146,7 +147,7 @@
## ⚪ Backlog — Priorité 3 (P3)
### 59. Mode hors-ligne PWA complet ✅ FAIT
### 59. Mode hors-ligne PWA complet — ✅ TERMINÉ
- **Effort :** 3-4 jours | **Impact :** 🟡
- **Description :** Service worker avancé avec IndexedDB pour permettre la navigation et la recherche en mode hors-ligne, avec file de synchronisation au retour réseau.
- **Implémentation :** `offline-db.js` (377 lignes) + `offline.js` (209 lignes). IndexedDB 3 stores (files, content, pending). Badge hors-ligne dans le header. Modale résolution de conflits.
@@ -190,7 +191,7 @@
- [ ] Gestion des déconnexions : reconnexion automatique, merge state au retour
- [ ] Tests de charge : 5+ utilisateurs simultanés sur le même fichier
### 63. Internationalisation (i18n) — Multilingue
### 63. Internationalisation (i18n) — Multilingue — ✅ TERMINÉ
- **Effort :** 2-3 jours | **Impact :** 🟡 | **Statut :** ✅ Terminé
- **Description :** Support de l'anglais et du français via un système de clés de traduction.
- **Sous-tâches :**
@@ -207,7 +208,7 @@
- [x] Thèmes, palette de commandes, raccourcis, webhooks → EN/FR complet
- [x] Messages système : toasts, statuts, événements → EN/FR complet
### 64. MFA — Authentification multi-facteurs ✅ FAIT
### 64. MFA — Authentification multi-facteurs — ✅ TERMINÉ (TOTP + WebAuthn + recovery codes)
- **Effort :** 2 jours (réalisé) | **Impact :** 🟡
- **Description :** Ajout d'un second facteur d'authentification obligatoire pour les comptes administrateur. Deux méthodes sont proposées :
- **TOTP** (Time-based One-Time Password) : l'utilisateur scanne un QR code avec son app d'authentification (Google Authenticator, Authy, Bitwarden) qui génère un code à 6 chiffres renouvelé toutes les 30 secondes. Au login, après avoir saisi son mot de passe, l'utilisateur doit entrer le code affiché sur son téléphone. Même si le mot de passe est volé, le compte reste protégé car l'attaquant n'a pas le téléphone.
@@ -215,14 +216,15 @@
- **Codes de secours** : 8 codes à usage unique imprimables, à conserver en lieu sûr, qui permettent de se connecter même si on perd son téléphone ou sa clé. Chaque code ne fonctionne qu'une seule fois.
- **Pourquoi c'est important :** Le vol de mot de passe est la cause #1 de brèches de sécurité. Avec un vault Obsidian contenant des notes personnelles, projets sensibles, secrets et tokens API, l'authentification par simple mot de passe n'est plus suffisante. Le MFA empêche 99.9% des attaques de prise de compte automatisées (source : Microsoft Security).
- **Sous-tâches :**
- [ ] TOTP : génération de secret, QR code, vérification code 6 chiffres
- [ ] WebAuthn : enregistrement de clé, assertion, attestation
- [ ] UI : page « Sécurité du compte » avec activation/désactivation MFA
- [x] Flow login : mot de passe → challenge TOTP si activé
- [ ] Recovery codes : 8 codes de backup à usage unique
- [x] Stockage : totp_secret + `webauthn_credential_id` dans `users.json`
- [x] TOTP : génération de secret, QR code, vérification code 6 chiffres
- [x] WebAuthn : enregistrement de clé, assertion, attestation (`backend/auth/webauthn_mfa.py`, lib `webauthn==2.6.0`, challenges in-memory TTL 180s à usage unique) — FAIT en 2026-09 (commit ab795ec)
- [x] UI : page « Sécurité du compte » avec activation/désactivation MFA + gestion des clés WebAuthn (liste, ajout, retrait)
- [x] Flow login : mot de passe → challenge TOTP OU WebAuthn selon `mfa_method` retourné par /login
- [x] Recovery codes : 8 codes de backup à usage unique (générés à l'activation, hachés SHA-256)
- [x] Stockage : `mfa_secret` + `webauthn_credentials[]` dans `users.json`
- [x] Tests : `tests/test_mfa.py` (29) + `tests/test_webauthn.py` (10, authentificateur virtuel CBOR/EC P-256)
### 65. Thèmes personnalisés — CSS variables ✅ FAIT
### 65. Thèmes personnalisés — CSS variables — ✅ TERMINÉ
- **Effort :** 1-2 jours (réalisé) | **Impact :** 🟢
- **Description :** Exposition de variables CSS pour permettre aux utilisateurs de créer des thèmes personnalisés. Presets inclus : light, dark, high-contrast, sepia.
- **Sous-tâches :**
@@ -232,7 +234,7 @@
- [x] Import/export de thème personnalisé (JSON)
- [x] Application dynamique via document.documentElement.style.setProperty
### 66. Export multi-formats ✅ FAIT
### 66. Export multi-formats — ✅ TERMINÉ
- **Effort :** 1-2 jours (réalisé) | **Impact :** 🟢
- **Description :** Export de notes individuelles ou de vaults entiers en HTML standalone, bundle Markdown (.zip), et ePub pour liseuses.
- **Sous-tâches :**
@@ -242,79 +244,83 @@
- [x] UI : dropdown Export dans toolbar viewer (HTML / MD bundle / ePub)
- [x] Endpoints : `GET /api/export/html`, `GET /api/export/md-bundle`, `GET /api/export/epub`
### 74. Support complet des documents PDF — ✅ Pratiquement complet
- **Effort :** 4-5 jours | **Impact :** 🟡 | **Statut :** ✅ FAIT (sauf C3 pdf/info endpoint)
### 74. Support complet des documents PDF — ✅ TERMINÉ
- **Effort :** 4-5 jours | **Impact :** 🟡 | **Statut :** ✅ COMPLET (2026-09-07 — C3 + Range 206 + config G3 + indexation incrémentale, commit 7042307)
- **Description :** Prise en charge native des fichiers PDF dans ObsiGate avec parité fonctionnelle complète avec les documents Markdown : apparition dans l'arborescence, indexation full-text, visualisation inline dans le navigateur, recherche TF-IDF, et téléchargement.
- **Implémentation réelle (vérifiée) :**
- **Implémentation réelle (vérifiée 2026-09-07) :**
- **Bugs corrigés (2026-09) :** `api_pdf_stream` crashait en 500 (`NameError: current_user` jamais injecté) ; l'indexation incrémentale du watcher faisait `read_text()` sur les PDFs (garbage) ; Range/206 et `pdf/info` absents malgré le texte ci-dessous.
- `GET /api/file/{vault}/pdf/info` — métadonnées seules sans transférer le document (C3)
- Stream avec `Accept-Ranges` + 206 Partial Content (single range, suffix-range, 416) (C2)
- `OBSIGATE_PDF_MAX_SIZE_MB` (50) + `OBSIGATE_PDF_EXTRACT_TIMEOUT` (30s via thread-pool) (B4/G3)
- Backend `backend/pdf_reader.py` (existant) — extraction pypdf + pymupdf (fallback), métadonnées, TOC
- `backend/indexer.py` — `.pdf` dans SUPPORTED_EXTENSIONS, extraction dans `index_document()`
- `backend/main.py` — flag `is_pdf: True` retourné par `api_file_view`, endpoint `GET /api/file/{vault}/pdf/stream` avec support Range/206
- `backend/search.py` — filtre `ext:pdf` (déjà implémenté avant cette PR)
- `frontend/js/viewer.js:451-480` — branche `if (data.is_pdf)` + iframe + toolbar + TOC + bouton download
- **Tests :** `tests/test_pdf.py` (13 tests, 100% verts) — text/metadata/TOC + edge cases + indexation + filtre
- **Tests :** `tests/test_pdf.py` (26 tests verts) — text/metadata/TOC + indexation scan/incrémentale + filtre ext + stream 200/206/416 + /pdf/info + limite de taille
- **Bug fixé dans cette PR :** `PdfReader` NameError dans `pdf_reader.py` quand pymupdf est installé (la variable `PdfReader` n'était déclarée que dans la branche `except ImportError`)
- `backend/requirements-test.txt` (nouveau) — `reportlab` pour générer des PDFs de test
- **Sous-tâches :**
##### A. Backend — Extraction de texte PDF (1-1.5 jour)
- [ ] **A1. Dépendance** : Ajouter `pymupdf` (PyMuPDF/fitz) à `requirements.txt` — bibliothèque C performante avec extraction texte + métadonnées, déjà compatible avec l'image Docker (libs système GTK/Pango déjà présentes pour WeasyPrint). Alternative légère : `pypdf` (pure Python, pas de deps système) si pymupdf pose problème.
- [ ] **A2. Module `backend/pdf_reader.py`** : Créer un module dédié avec les fonctions :
- [x] **A1. Dépendance** : `pypdf>=4.0` retenu dans requirements (pure Python, simplicité Docker) ; PyMuPDF (`fitz`) utilisé automatiquement en priorité s'il est importable — l'inverse du plan initial, fonctionnellement équivalent.
- [x] **A2. Module `backend/pdf_reader.py`** : Créer un module dédié avec les fonctions :
- `extract_pdf_text(file_path: Path) -> str` : extrait tout le texte du PDF, page par page, avec séparateur `\f` entre pages. Gère les PDF encodés, protégés par mot de passe (retourne erreur explicite), et corrompus.
- `extract_pdf_metadata(file_path: Path) -> dict` : extrait titre, auteur, sujet, nombre de pages, taille.
- `extract_pdf_preview(file_path: Path, max_chars: int = 100000) -> str` : extrait les N premiers caractères pour l'indexation (limité par `SEARCH_CONTENT_LIMIT`).
- [ ] **A3. Fallback pypdf** : Si pymupdf non disponible (exception d'import), fallback automatique sur `pypdf` avec un log warning. Code structuré avec une interface abstraite (`PdfReader` protocol) pour swap transparent.
- [x] **A3. Fallback pypdf** : Si pymupdf non disponible (exception d'import), fallback automatique sur `pypdf` avec un log warning. Code structuré avec une interface abstraite (`PdfReader` protocol) pour swap transparent.
##### B. Backend — Indexation des PDF (1 jour)
- [ ] **B1. Ajout à `SUPPORTED_EXTENSIONS`** : Ajouter `.pdf` au set dans `backend/indexer.py:56`. Déclencher un rebuild complet de l'index (incrémental via le file watcher pour les nouveaux PDFs).
- [ ] **B2. Modification de `index_document()`** (`backend/indexer.py:524`) : Dans la fonction d'indexation, détecter l'extension `.pdf` et appeler `extract_pdf_text()` au lieu de `read_text()`. Le texte extrait alimente le pipeline TF-IDF existant — aucun changement nécessaire dans `search.py`.
- [ ] **B3. Métadonnées PDF dans le document info** : Enrichir la structure de retour de `index_document()` avec les champs spécifiques PDF : `page_count`, `pdf_title` (titre extrait des métadonnées, prioritaire sur le nom de fichier), `pdf_author`.
- [ ] **B4. Gestion d'erreur robuste** : PDF corrompu → log warning + skip (ne pas bloquer l'indexation). PDF volumineux (>50 Mo) → log info + extraction tronquée à `SEARCH_CONTENT_LIMIT`. Timeout d'extraction configurable (30s par défaut).
- [x] **B1. Ajout à `SUPPORTED_EXTENSIONS`** : Ajouter `.pdf` au set dans `backend/indexer.py:56`. Déclencher un rebuild complet de l'index (incrémental via le file watcher pour les nouveaux PDFs).
- [x] **B2. Lecture PDF dans les DEUX chemins d'indexation** (`_scan_vault` + `_index_single_file_sync`, utilisé par le watcher) : détection `.pdf` → `extract_pdf_text()`. Fix 2026-09 : seul le scan complet gérait les PDFs, l'incrémental indexait du garbage.
- [x] **B3. Métadonnées PDF (adapté)** : titre PDF prioritaire sur le nom de fichier dans l'index ; `pages`/`author` exposés via `api_file_view` + `/pdf/info` (non stockés dans l'entrée d'index).
- [x] **B4. Gestion d'erreur robuste** : PDF corrompu → log warning + skip (ne pas bloquer l'indexation). PDF volumineux (>50 Mo) → log info + extraction tronquée à `SEARCH_CONTENT_LIMIT`. Timeout d'extraction configurable (30s par défaut).
##### C. Backend — API endpoints PDF (0.5 jour)
- [ ] **C1. Modification de `api_file_view()`** (`backend/main.py:2270`) : Avant la tentative de `read_text()`, détecter `.pdf` par extension. Pour les PDF :
- [x] **C1. Modification de `api_file_view()`** (`backend/main.py:2270`) : Avant la tentative de `read_text()`, détecter `.pdf` par extension. Pour les PDF :
- Extraire le texte avec `extract_pdf_text()`
- Extraire les métadonnées (pages, auteur)
- Retourner une réponse structurée : `is_pdf: true`, `page_count`, `pdf_metadata`, `html` (aperçu texte formaté), `raw_length`
- Le champ `html` contient un rendu texte simple (pas de markdown) : texte paginé ou première page formatée
- [ ] **C2. Nouvel endpoint `GET /api/file/{vault}/pdf/stream`** : Sert le fichier PDF brut avec `Content-Type: application/pdf` et `Content-Disposition: inline` pour visualisation dans le navigateur. Supporte le `Range` header (HTTP 206 Partial Content) pour le streaming progressif des gros PDFs — essentiel pour la performance sur des documents volumineux.
- [ ] **C3. Nouvel endpoint `GET /api/file/{vault}/pdf/info`** : Retourne les métadonnées seules (pages, titre, auteur) sans le contenu — permet à l'UI d'afficher les infos avant de charger le PDF lourd.
- [ ] **C4. Endpoint download** : Déjà fonctionnel (`/api/file/{vault}/download`) — aucun changement nécessaire.
- [x] **C2. Nouvel endpoint `GET /api/file/{vault}/pdf/stream`** : Sert le fichier PDF brut avec `Content-Type: application/pdf` et `Content-Disposition: inline` pour visualisation dans le navigateur. Supporte le `Range` header (HTTP 206 Partial Content) pour le streaming progressif des gros PDFs — essentiel pour la performance sur des documents volumineux.
- [x] **C3. Nouvel endpoint `GET /api/file/{vault}/pdf/info`** : Retourne les métadonnées seules (pages, titre, auteur) sans le contenu — permet à l'UI d'afficher les infos avant de charger le PDF lourd.
- [x] **C4. Endpoint download** : Déjà fonctionnel (`/api/file/{vault}/download`) — aucun changement nécessaire.
##### D. Frontend — Arborescence de fichiers (0.5 jour)
- [ ] **D1. Icône et filtre** : L'icône PDF (`file-text` de Lucide) est déjà mappée dans `EXT_ICONS` (`frontend/js/utils.js:129`). Une fois `.pdf` dans `SUPPORTED_EXTENSIONS`, les PDFs apparaissent automatiquement dans l'arborescence via l'API `list_directory`. Aucun changement UI nécessaire.
- [ ] **D2. Distinction visuelle** (optionnel) : Sous-titre léger sous le nom du fichier dans l'arborescence indiquant le nombre de pages (ex: « 12 pages ») pour différencier rapidement les PDF des MD. Donnée disponible via l'API `pdf/info`.
- [ ] **D3. Drag & drop et upload** : Le mécanisme d'upload existant (`POST /api/file/{vault}/upload`) fonctionne déjà pour tout type de fichier. Vérifier que le MIME type `application/pdf` est correctement détecté et que le watcher réindexe automatiquement.
- [x] **D1. Icône et filtre** : L'icône PDF (`file-text` de Lucide) est déjà mappée dans `EXT_ICONS` (`frontend/js/utils.js:129`). Une fois `.pdf` dans `SUPPORTED_EXTENSIONS`, les PDFs apparaissent automatiquement dans l'arborescence via l'API `list_directory`. Aucun changement UI nécessaire.
- [x] **D2. Distinction visuelle** (optionnel) : Sous-titre léger sous le nom du fichier dans l'arborescence indiquant le nombre de pages (ex: « 12 pages ») pour différencier rapidement les PDF des MD. Donnée disponible via l'API `pdf/info`.
- [x] **D3. Drag & drop et upload** : Le mécanisme d'upload existant (`POST /api/file/{vault}/upload`) fonctionne déjà pour tout type de fichier. Vérifier que le MIME type `application/pdf` est correctement détecté et que le watcher réindexe automatiquement.
##### E. Frontend — Viewer PDF (1 jour)
- [ ] **E1. Rendu inline natif** : Utiliser le visualiseur PDF intégré du navigateur via `<iframe>` pointant sur `/api/file/{vault}/pdf/stream?path=...`. Approche optimale :
- [x] **E1. Rendu inline natif** : Utiliser le visualiseur PDF intégré du navigateur via `<iframe>` pointant sur `/api/file/{vault}/pdf/stream?path=...`. Approche optimale :
- Zéro dépendance JS supplémentaire
- Rendu identique à Chrome/Firefox/Safari natif
- Support natif du zoom, recherche dans le document, navigation par pages, rotation
- L'iframe s'adapte en hauteur (`height: 100%` du content-area)
- [ ] **E2. Détection dans le viewer** : Dans `frontend/js/viewer.js`, fonction `renderFileContent()` — ajouter une branche après la détection `data.unsupported` :
- [x] **E2. Détection dans le viewer** : Dans `frontend/js/viewer.js`, fonction `renderFileContent()` — ajouter une branche après la détection `data.unsupported` :
- Si `data.is_pdf === true` → render l'iframe PDF au lieu du viewer markdown
- Si le navigateur ne supporte pas le rendu PDF inline → fallback sur l'UI « binaire » avec bouton download + bouton « Ouvrir dans un nouvel onglet »
- [ ] **E3. Barre d'outils PDF** : Dans la barre d'outils du viewer (celle qui a déjà les boutons Copier, Source, .md, PDF, Éditer, pop-out), pour les fichiers PDF :
- [x] **E3. Barre d'outils PDF** : Dans la barre d'outils du viewer (celle qui a déjà les boutons Copier, Source, .md, PDF, Éditer, pop-out), pour les fichiers PDF :
- Remplacer « Copier » / « Source » / « Éditer » par des actions spécifiques PDF
- Bouton « Télécharger » (.pdf) — déjà existant, fonctionne
- Bouton « Plein écran » — ouvre le PDF dans un nouvel onglet en plein écran
- Badge « N pages » indiquant le nombre de pages
- Bouton « pop-out » — gardé, ouvre le viewer PDF dans une popup séparée
- [ ] **E4. Thème** : L'iframe PDF est en dehors du DOM applicatif donc pas affecté par le thème dark/light. Ajouter un message discret « Le PDF s'affiche avec le thème de votre navigateur » si `_currentTheme === 'dark'` (les PDFs en fond blanc dans un thème sombre peuvent surprendre).
- [ ] **E5. Responsive** : L'iframe s'adapte à la largeur du content-area. En mode mobile, hauteur ajustée à la viewport. La toolbar mobile existante fonctionne avec les actions PDF.
- [x] **E4. Thème (optionnel, non retenu)** : L'iframe PDF est en dehors du DOM applicatif donc pas affecté par le thème dark/light. Ajouter un message discret « Le PDF s'affiche avec le thème de votre navigateur » si `_currentTheme === 'dark'` (les PDFs en fond blanc dans un thème sombre peuvent surprendre).
- [x] **E5. Responsive** : L'iframe s'adapte à la largeur du content-area. En mode mobile, hauteur ajustée à la viewport. La toolbar mobile existante fonctionne avec les actions PDF.
##### F. Frontend — Recherche (0.5 jour)
- [ ] **F1. Résultats de recherche** : Les PDFs apparaissent dans les résultats via le TF-IDF existant (le texte extrait est indexé). Ajouter un badge visuel « PDF » à côté du titre dans les résultats de recherche pour distinguer les PDFs des MD — utiliser l'icône `file-text`.
- [ ] **F2. Snippets de recherche** : Les extraits de contexte montrent le texte extrait du PDF avec surlignage des termes recherchés — fonctionnement identique aux MD via le mécanisme de snippet existant dans `search.py`.
- [ ] **F3. Filtres de recherche avancés** : Ajouter `ext:pdf` comme filtre pour limiter la recherche aux PDFs uniquement (complément aux filtres `created:`, `modified:`, `size:` déjà prévus #34).
- [x] **F1. Résultats de recherche** : Les PDFs apparaissent dans les résultats via le TF-IDF existant (le texte extrait est indexé). Ajouter un badge visuel « PDF » à côté du titre dans les résultats de recherche pour distinguer les PDFs des MD — utiliser l'icône `file-text`.
- [x] **F2. Snippets de recherche** : Les extraits de contexte montrent le texte extrait du PDF avec surlignage des termes recherchés — fonctionnement identique aux MD via le mécanisme de snippet existant dans `search.py`.
- [x] **F3. Filtres de recherche avancés** : Ajouter `ext:pdf` comme filtre pour limiter la recherche aux PDFs uniquement (complément aux filtres `created:`, `modified:`, `size:` déjà prévus #34).
##### G. Docker & Dépendances (0.5 jour)
- [ ] **G1. requirements.txt** : Ajouter `pymupdf>=1.24.0` (sinon `pypdf>=4.0` en fallback).
- [ ] **G2. Dockerfile** : Vérifier que l'image `python:3.11-slim` dispose des libs système nécessaires pour pymupdf. Si besoin, ajouter `libmupdf-dev` ou utiliser `pypdf` (pure Python) pour éviter la complexité. Recommandation : pypdf pour la simplicité Docker, pymupdf en option pour la performance.
- [ ] **G3. Configuration** : Ajouter `OBSIGATE_PDF_MAX_SIZE_MB` (défaut 50) pour limiter la taille des PDFs indexés et `OBSIGATE_PDF_EXTRACT_TIMEOUT` (défaut 30s).
- [x] **G1. requirements.txt** : `pypdf>=4.0` retenu (pymupdf optionnel, utilisé s'il est importable).
- [x] **G2. Dockerfile** : Vérifier que l'image `python:3.11-slim` dispose des libs système nécessaires pour pymupdf. Si besoin, ajouter `libmupdf-dev` ou utiliser `pypdf` (pure Python) pour éviter la complexité. Recommandation : pypdf pour la simplicité Docker, pymupdf en option pour la performance.
- [x] **G3. Configuration** : `OBSIGATE_PDF_MAX_SIZE_MB` (50) + `OBSIGATE_PDF_EXTRACT_TIMEOUT` (30s) — documentés dans `.env.example` et README FR/EN.
##### H. Tests (1 jour)
- [ ] **H1. Tests unitaires backend** :
- [x] **H1. Tests unitaires backend** :
- `test_pdf_reader.py` : extraction texte PDF simple, PDF vide, PDF avec uniquement des images (OCR non requis — retourne chaîne vide), PDF protégé par mot de passe, PDF corrompu, extraction métadonnées
- Fixtures : créer un PDF de test minimal (2 pages, texte simple) via `reportlab` dans les fixtures de test
- `test_pdf_indexing.py` : vérifier qu'un PDF dans un vault est correctement indexé, que le texte est recherchable, que `index_document()` gère l'extension `.pdf`
@@ -323,12 +329,12 @@
- Test d'intégration : naviguer vers un fichier PDF → l'iframe est rendue
- Test : fichier PDF dans les résultats de recherche
- Test : téléchargement de PDF fonctionnel
- [ ] **H3. CI** : Ajouter la fixture PDF de test dans les artefacts de CI. Les tests PDF sont sautés si pymupdf/pypdf n'est pas disponible.
- [x] **H3. CI** : Ajouter la fixture PDF de test dans les artefacts de CI. Les tests PDF sont sautés si pymupdf/pypdf n'est pas disponible.
##### I. Documentation utilisateur (inclus dans l'effort)
- [ ] **I1.** Mettre à jour README.md : mentionner le support PDF dans les formats supportés
- [x] **I1.** Mettre à jour README.md : mentionner le support PDF dans les formats supportés
- [ ] **I2.** Ajouter une note dans la FAQ : « Comment visualiser un PDF dans ObsiGate ? »
- [ ] **I3.** Documenter les limitations : pas d'OCR (PDFs scannés non recherchables), pas d'annotation PDF, pas d'édition de PDF
- [x] **I3.** Documenter les limitations : pas d'OCR (PDFs scannés non recherchables), pas d'annotation PDF, pas d'édition de PDF
##### J. Points d'attention / Risques
- **Performance** : Un PDF de 500 pages peut générer beaucoup de texte → `SEARCH_CONTENT_LIMIT` (100 Ko) limite l'indexation au début du document. Pour les PDFs volumineux, envisager une extraction paginée avec `SEARCH_CONTENT_LIMIT` réparti sur les N premières pages.
@@ -339,7 +345,7 @@
---
### 75. Éditeur multi-panneaux (Split View) ✅ Complété
### 75. Éditeur multi-panneaux (Split View) — ✅ TERMINÉ (reste I3 : tests E2E split view)
- **Effort :** 5-7 jours (réalisé) | **Impact :** 🟡
- **Statut :** Fonctionnel — toutes les sous-tâches implémentées (reste tests I2/I3)
- **Fichiers clés :** `frontend/js/pane-manager.js` (1082 loc), `frontend/js/viewer.js` (modifié), `frontend/js/ui.js` (modifié), `frontend/js/dashboard.js` (modifié), `frontend/js/palette.js` (modifié), `frontend/style.css` (modifié), `tests/test_pane_manager.py` (22 tests)
@@ -414,7 +420,7 @@
---
### 76. BooksLM — Console AI contextuelle par répertoire (style NotebookLM) ✅ FAIT
### 76. BooksLM — Console AI contextuelle par répertoire (style NotebookLM) — ✅ TERMINÉ
- **Effort :** 5-6 jours (réalisé) | **Impact :** 🟡
- **Description :** Console de chat AI contextuelle accessible via le menu contextuel des répertoires dans l'arborescence. Au clic sur « BooksLM », un panneau de chat s'ouvre à droite du viewer et indexe automatiquement toutes les ressources markdown (et PDF via #74) du répertoire courant et de ses sous-répertoires récursivement comme contexte pour un assistant AI. L'assistant peut répondre à des questions, résumer, synthétiser, et croiser l'information à travers tous les documents du scope — exactement comme NotebookLM de Google, mais pour n'importe quel répertoire de votre vault Obsidian.
- **Fonctionnement général :**
@@ -426,65 +432,65 @@
- L'historique de chat est optionnellement sauvegardé (localStorage ou fichier `.books-lm.json` dans le répertoire)
- **Sous-tâches :**
##### A. Backend — Collecte et préparation du contexte (1.5-2 jours)
- [ ] **A1. Nouvel endpoint `POST /api/ai/bookslm/context`** : Reçoit `{vault, directory}` → parcourt récursivement le répertoire → lit tous les fichiers supportés → retourne un objet `{files: [{path, title, content, type: "md"|"pdf"}], total_chars, file_count, directory_tree}`
- [ ] **A2. Limites configurables** : `BOOKSLM_MAX_FILES` (défaut 200), `BOOKSLM_MAX_TOTAL_CHARS` (défaut 200 000), `BOOKSLM_MAX_FILE_CHARS` (défaut 30 000 par fichier). Les fichiers au-delà sont tronqués avec un message `[... continue dans le fichier]`.
- [ ] **A3. Filtrage intelligent** : Ignorer les fichiers cachés (`.` préfixe), les dossiers `_attachments/`, les fichiers binaires non-supportés. Respecter `.gitignore` ou `.obsigate-ignore` si présent.
- [ ] **A4. Streaming du contexte** : Pour les très gros répertoires, l'endpoint supporte le streaming SSE pour informer l'UI de la progression (« Indexation de 45/127 fichiers... »).
##### A. Backend — Collecte et préparation du contexte (1.5-2 jours) — ✅ livré (backend/bookslm.py, bookslm_routes.py)
- [x] **A1. Nouvel endpoint `POST /api/ai/bookslm/context`** : Reçoit `{vault, directory}` → parcourt récursivement le répertoire → lit tous les fichiers supportés → retourne un objet `{files: [{path, title, content, type: "md"|"pdf"}], total_chars, file_count, directory_tree}`
- [x] **A2. Limites configurables** : `BOOKSLM_MAX_FILES` (défaut 200), `BOOKSLM_MAX_TOTAL_CHARS` (défaut 200 000), `BOOKSLM_MAX_FILE_CHARS` (défaut 30 000 par fichier). Les fichiers au-delà sont tronqués avec un message `[... continue dans le fichier]`.
- [x] **A3. Filtrage intelligent** : Ignorer les fichiers cachés (`.` préfixe), les dossiers `_attachments/`, les fichiers binaires non-supportés. Respecter `.gitignore` ou `.obsigate-ignore` si présent.
- [x] **A4. Streaming du contexte (non retenu — collecte rapide, indicateur simple côté UI)** : Pour les très gros répertoires, l'endpoint supporte le streaming SSE pour informer l'UI de la progression (« Indexation de 45/127 fichiers... »).
##### B. Backend — Endpoint chat BooksLM (1 jour)
- [ ] **B1. Endpoint `POST /api/ai/bookslm/chat`** : Reçoit `{vault, directory, message, conversation_history: [{role, content}]}` → construit le contexte système à partir des fichiers du répertoire → appelle le provider AI configuré → stream la réponse via SSE.
- [ ] **B2. Prompt système** : Template par défaut optimisé : « Tu es un assistant de recherche qui aide à comprendre et analyser les documents d'un répertoire. Voici le contenu de tous les documents disponibles. Réponds en te basant UNIQUEMENT sur ces documents. Cite tes sources avec le nom du fichier. Si l'information n'est pas dans les documents, dis-le clairement. »
- [ ] **B3. Mode « Sources »** : Chaque réponse inclut les fichiers référencés (détectés via mention de titre ou contenu). L'UI affiche des badges de source cliquables.
- [ ] **B4. Mise en cache du contexte** : Le contexte du répertoire est caché en mémoire (hash du contenu) pour éviter de re-parser tous les fichiers à chaque message. Invalidé si un fichier est modifié (watcher).
- [ ] **B5. Provider** : Utilise la même abstraction provider que l'AI Editor (#27) — DeepSeek, OpenRouter, Gemini. Ajouter `BOOKSLM_DEFAULT_MODEL` dans `.env` (défaut : `DEEPSEEK_MODEL`).
##### B. Backend — Endpoint chat BooksLM (1 jour) — ✅ livré
- [x] **B1. Endpoint `POST /api/ai/bookslm/chat`** : Reçoit `{vault, directory, message, conversation_history: [{role, content}]}` → construit le contexte système à partir des fichiers du répertoire → appelle le provider AI configuré → stream la réponse via SSE.
- [x] **B2. Prompt système** : Template par défaut optimisé : « Tu es un assistant de recherche qui aide à comprendre et analyser les documents d'un répertoire. Voici le contenu de tous les documents disponibles. Réponds en te basant UNIQUEMENT sur ces documents. Cite tes sources avec le nom du fichier. Si l'information n'est pas dans les documents, dis-le clairement. »
- [x] **B3. Mode « Sources »** : Chaque réponse inclut les fichiers référencés (détectés via mention de titre ou contenu). L'UI affiche des badges de source cliquables.
- [x] **B4. Mise en cache du contexte** : Le contexte du répertoire est caché en mémoire (hash du contenu) pour éviter de re-parser tous les fichiers à chaque message. Invalidé si un fichier est modifié (watcher).
- [x] **B5. Provider** : Utilise la même abstraction provider que l'AI Editor (#27) — DeepSeek, OpenRouter, Gemini. Ajouter `BOOKSLM_DEFAULT_MODEL` dans `.env` (défaut : `DEEPSEEK_MODEL`).
##### C. Frontend — Panneau de chat BooksLM (2 jours)
- [ ] **C1. Module `frontend/js/bookslm.js`** : Nouveau module ES avec la classe `BooksLM` :
##### C. Frontend — Panneau de chat BooksLM (2 jours) — ✅ livré (frontend/js/bookslm.js)
- [x] **C1. Module `frontend/js/bookslm.js`** : Nouveau module ES avec la classe `BooksLM` :
- Gère l'état : `_isOpen`, `_currentDirectory`, `_messages[]`, `_contextFiles[]`, `_isLoading`
- Crée le DOM du panneau : conteneur latéral `.bookslm-panel` (450px, redimensionnable via poignée)
- Header : titre « BooksLM », nom du répertoire courant, bouton fermer, bouton « Nouvelle conversation »
- Zone de messages : scrollable, bulles utilisateur (droite) et assistant (gauche) avec Markdown rendu
- Zone d'entrée : `textarea` avec Ctrl+Enter pour envoyer, bouton envoyer
- Barre d'état : nombre de fichiers indexés, nombre total de caractères
- [ ] **C2. Intégration au menu contextuel** : Dans `frontend/js/context-menu.js`, ajouter l'option « 🧠 BooksLM » pour les nœuds de type `directory` dans l'arborescence. Visible seulement si le vault est accessible.
- [ ] **C3. Rendu Markdown dans le chat** : Utiliser le renderer Markdown existant (ou un sous-ensemble simplifié) pour afficher les réponses de l'AI avec support du **gras**, *italique*, `code`, listes, et tableaux.
- [ ] **C4. Streaming des réponses** : Connexion SSE pour afficher la réponse de l'AI token par token (effet « typing » naturel).
- [ ] **C5. Badges de sources** : Après chaque réponse, afficher les fichiers sources mentionnés sous forme de badges cliquables qui ouvrent le fichier dans le viewer principal.
- [ ] **C6. Mode plein écran** : Bouton pour basculer en mode plein écran (cache la sidebar, le panneau prend tout l'espace). Utile pour les sessions de recherche intense.
- [x] **C2. Intégration au menu contextuel** : Dans `frontend/js/context-menu.js`, ajouter l'option « 🧠 BooksLM » pour les nœuds de type `directory` dans l'arborescence. Visible seulement si le vault est accessible.
- [x] **C3. Rendu Markdown dans le chat** : Utiliser le renderer Markdown existant (ou un sous-ensemble simplifié) pour afficher les réponses de l'AI avec support du **gras**, *italique*, `code`, listes, et tableaux.
- [x] **C4. Streaming des réponses** : Connexion SSE pour afficher la réponse de l'AI token par token (effet « typing » naturel).
- [x] **C5. Badges de sources** : Après chaque réponse, afficher les fichiers sources mentionnés sous forme de badges cliquables qui ouvrent le fichier dans le viewer principal.
- [x] **C6. Mode plein écran** : Bouton pour basculer en mode plein écran (cache la sidebar, le panneau prend tout l'espace). Utile pour les sessions de recherche intense.
##### D. Frontend — Actions et UX (0.5-1 jour)
- [ ] **D1. Copier la réponse** : Bouton copie sur chaque message assistant.
- [ ] **D2. Régénérer** : Bouton pour régénérer la dernière réponse (utile si la réponse est hors-sujet).
- [ ] **D3. Exporter la conversation** : Bouton pour exporter l'historique en Markdown → sauvegarder comme note dans le répertoire courant.
- [ ] **D4. Historique des conversations** : Stockage dans `localStorage` par clé `bookslm-history-{vault}-{directory}`. Liste déroulante dans le header pour charger une conversation précédente.
- [ ] **D5. Indicateur de contexte** : Barre de progression montrant l'utilisation du contexte (% de la limite `BOOKSLM_MAX_TOTAL_CHARS`). Si le répertoire est trop gros, suggérer de réduire le scope.
- [ ] **D6. Suggestions de questions** : Après l'indexation, afficher 3 questions suggérées basées sur les titres et métadonnées des fichiers (« Résume ce répertoire », « Quels sont les thèmes principaux ? », « Y a-t-il des contradictions entre ces documents ? »).
##### D. Frontend — Actions et UX (0.5-1 jour) — ✅ livré (D5 partiel)
- [x] **D1. Copier la réponse** : Bouton copie sur chaque message assistant.
- [x] **D2. Régénérer** : Bouton pour régénérer la dernière réponse (utile si la réponse est hors-sujet).
- [x] **D3. Exporter la conversation** : Bouton pour exporter l'historique en Markdown → sauvegarder comme note dans le répertoire courant.
- [x] **D4. Historique des conversations** : Stockage dans `localStorage` par clé `bookslm-history-{vault}-{directory}`. Liste déroulante dans le header pour charger une conversation précédente.
- [x] **D5. Indicateur de contexte (barre de progression %) — non retenu, compteur fichiers/caractères affiché)** : Barre de progression montrant l'utilisation du contexte (% de la limite `BOOKSLM_MAX_TOTAL_CHARS`). Si le répertoire est trop gros, suggérer de réduire le scope.
- [x] **D6. Suggestions de questions** : Après l'indexation, afficher 3 questions suggérées basées sur les titres et métadonnées des fichiers (« Résume ce répertoire », « Quels sont les thèmes principaux ? », « Y a-t-il des contradictions entre ces documents ? »).
##### E. CSS & Design (0.5 jour)
- [ ] **E1. Panneau latéral** : Animation slide-in depuis la droite (300ms ease-out). Ombre portée pour séparation visuelle.
- [ ] **E2. Poignée de redimensionnement** : Similaire à `.sidebar-resize-handle`, curseur `col-resize`, largeur min 350px, max 800px. Persistance dans localStorage.
- [ ] **E3. Bulles de chat** : Style cohérent avec le thème actuel. Messages utilisateur avec accent-color, messages assistant avec fond `var(--surface2)`.
- [ ] **E4. Responsive** : Sur mobile (<768px), le panneau passe en plein écran (pas de split view). Navigation par swipe pour revenir au viewer.
- [ ] **E5. Thème sombre/clair** : Toutes les variables CSS utilisent les customs properties existantes → compatibilité automatique.
##### E. CSS & Design (0.5 jour) — ✅ livré
- [x] **E1. Panneau latéral** : Animation slide-in depuis la droite (300ms ease-out). Ombre portée pour séparation visuelle.
- [x] **E2. Poignée de redimensionnement** : Similaire à `.sidebar-resize-handle`, curseur `col-resize`, largeur min 350px, max 800px. Persistance dans localStorage.
- [x] **E3. Bulles de chat** : Style cohérent avec le thème actuel. Messages utilisateur avec accent-color, messages assistant avec fond `var(--surface2)`.
- [x] **E4. Responsive** : Sur mobile (<768px), le panneau passe en plein écran (pas de split view). Navigation par swipe pour revenir au viewer.
- [x] **E5. Thème sombre/clair** : Toutes les variables CSS utilisent les customs properties existantes → compatibilité automatique.
##### F. Intégration et compatibilité (0.5 jour)
- [ ] **F1. Compatibilité Split View (#75)** : Si le split view est actif, BooksLM s'ouvre en remplacement du panneau le plus à droite (ou en 3e colonne). Le panneau BooksLM est traité comme un type spécial de pane dans le PaneManager.
- [ ] **F2. Compatibilité AI Editor (#26-29)** : BooksLM utilise le même système de provider AI. Les clés API configurées pour l'AI Editor fonctionnent pour BooksLM.
- [ ] **F3. Compatibilité PDF (#74)** : Si le support PDF est implémenté, les PDFs dans le répertoire sont inclus dans le contexte (texte extrait).
- [ ] **F4. Palette de commandes (#31)** : Ajouter les commandes « BooksLM: Ouvrir pour le répertoire courant » et « BooksLM: Nouvelle conversation ».
##### F. Intégration et compatibilité (0.5 jour) — ✅ livré (F1 adapté : panneau dédié, pas un pane du PaneManager)
- [ ] **F1. Compatibilité Split View (#75) (adapté : panneau latéral indépendant, cohabite avec le split view)** : Si le split view est actif, BooksLM s'ouvre en remplacement du panneau le plus à droite (ou en 3e colonne). Le panneau BooksLM est traité comme un type spécial de pane dans le PaneManager.
- [x] **F2. Compatibilité AI Editor (#26-29)** : BooksLM utilise le même système de provider AI. Les clés API configurées pour l'AI Editor fonctionnent pour BooksLM.
- [x] **F3. Compatibilité PDF (#74)** : Si le support PDF est implémenté, les PDFs dans le répertoire sont inclus dans le contexte (texte extrait).
- [x] **F4. Palette de commandes (#31)** : Ajouter les commandes « BooksLM: Ouvrir pour le répertoire courant » et « BooksLM: Nouvelle conversation ».
##### G. Tests (1 jour)
- [ ] **G1. Tests unitaires backend** :
##### G. Tests (1 jour) — ✅ G1 livré (28 tests), G2/G3 non retenus
- [x] **G1. Tests unitaires backend** :
- `test_bookslm_context.py` : collecte récursive, respect des limites, filtrage fichiers cachés, streaming SSE
- `test_bookslm_chat.py` : construction du prompt, caching du contexte, invalidation après modification
- [ ] **G2. Tests d'intégration frontend** :
- [ ] **G2. Tests d'intégration frontend (non retenus)** :
- Ouverture du panneau BooksLM depuis le menu contextuel
- Envoi d'un message et affichage de la réponse
- Badges de sources cliquables
- Export de conversation
- Redimensionnement du panneau
- [ ] **G3. Tests E2E (Playwright, #58)** :
- [ ] **G3. Tests E2E (Playwright) (non retenus à ce jour)** :
- Test : clic-droit sur répertoire → BooksLM → panneau visible
- Test : chat fonctionnel → message envoyé → réponse reçue
- Test : fermeture et réouverture → historique restauré
@@ -501,7 +507,7 @@
### 78. Éditeur Excalidraw — Ouverture et édition de fichiers .excalidraw
- **Effort :** 3-4 jours | **Impact :** 🟡 | **Statut :** ⚪ Prévu
- **Effort :** 3-4 jours | **Impact :** 🟡 | **Statut :** 🟡 ~90% livré (2026-09 — éditeur iframe complet, détection, création, autosave, support `.excalidraw.md`. Reste : B5 extraction texte pour recherche, C8 menu contextuel, F2/F3 tests, vérif BUG-002)
- **Description :** Prise en charge native des fichiers `.excalidraw` dans ObsiGate avec un éditeur visuel complet intégré. L'utilisateur peut ouvrir un fichier `.excalidraw` depuis l'arborescence et obtenir l'éditeur de diagrammes Excalidraw directement dans ObsiGate — dessiner, modifier, sauvegarder, comme dans l'app Excalidraw standalone, mais intégré au flux de travail du vault Obsidian.
@@ -535,8 +541,8 @@
- **Sous-tâches :**
##### A. Fichier `frontend/excalidraw-editor.html` — Éditeur autonome (1.5 jour)
- [ ] **A1. Structure HTML** : Page minimale avec un `<div id="excalidraw-container">` en plein écran. Pas de header ObsiGate — tout l'espace est pour le canvas.
- [ ] **A2. Import Excalidraw** :
- [x] **A1. Structure HTML** : Page minimale avec un `<div id="excalidraw-container">` en plein écran. Pas de header ObsiGate — tout l'espace est pour le canvas.
- [x] **A2. Import Excalidraw** :
```html
<script type="module">
import * as ExcalidrawLib from "https://esm.sh/@excalidraw/[email protected]";
@@ -544,7 +550,7 @@
</script>
```
Version épinglée (`@0.18.0`) pour la stabilité. Mise à jour manuelle testée.
- [ ] **A3. Configuration du chemin d'assets** : Définir `window.EXCALIDRAW_ASSET_PATH` pour pointer vers le CDN des fonts/polices d'Excalidraw (nécessaire pour le rendu des polices handwriting).
- [x] **A3. Configuration du chemin d'assets** : Définir `window.EXCALIDRAW_ASSET_PATH` pour pointer vers le CDN des fonts/polices d'Excalidraw (nécessaire pour le rendu des polices handwriting).
- [ ] **A4. Initialisation React** : Excalidraw nécessite React + ReactDOM. Les importer depuis esm.sh également :
```html
<script type="module">
@@ -554,37 +560,37 @@
window.ReactDOM = ReactDOM;
</script>
```
- [ ] **A5. Rendu du composant** : Monter `<ExcalidrawLib.Excalidraw>` dans le conteneur avec les `initialData` reçues. Configurer les callbacks `onChange` pour détecter les modifications.
- [ ] **A6. Barre d'outils minimaliste** (dans l'iframe, superposée en haut à droite) :
- [x] **A5. Rendu du composant** : Monter `<ExcalidrawLib.Excalidraw>` dans le conteneur avec les `initialData` reçues. Configurer les callbacks `onChange` pour détecter les modifications.
- [x] **A6. Barre d'outils minimaliste** (dans l'iframe, superposée en haut à droite) :
- Bouton « 💾 Sauvegarder » → envoie les données au parent
- Badge « Modifié » (disparaît après sauvegarde)
- Indicateur de thème 🌙/☀️
- Optionnel : bouton « Export PNG » et « Export SVG » (natif Excalidraw)
- [ ] **A7. Communication postMessage** :
- [x] **A7. Communication postMessage** :
- Réception : écouter `message` → si `type === "init"`, charger `data.elements` + `data.appState` + `data.files` dans l'état Excalidraw. Si `type === "theme"`, basculer `theme` (dark/light).
- Émission : `postMessage({type: "save", data: {elements, appState, files}}, "*")` quand l'utilisateur sauvegarde.
- Émission : `postMessage({type: "ready"}, "*")` au chargement pour signaler que l'iframe est prête.
- Émission : `postMessage({type: "modified", dirty: true/false}, "*")` pour l'indicateur de modification.
- [ ] **A8. Gestion des erreurs** : Si les données sont invalides (JSON corrompu, pas un fichier Excalidraw), afficher un message d'erreur stylisé dans l'iframe.
- [x] **A8. Gestion des erreurs** : Si les données sont invalides (JSON corrompu, pas un fichier Excalidraw), afficher un message d'erreur stylisé dans l'iframe.
##### B. Backend — Détection et API (0.5 jour)
- [ ] **B1. Ajout à `SUPPORTED_EXTENSIONS`** : Ajouter `.excalidraw` dans `backend/indexer.py:56` pour que les fichiers apparaissent dans l'arborescence et soient indexés.
- [ ] **B2. Icône** : Ajouter `.excalidraw` dans `EXT_ICONS` (`frontend/js/utils.js`) → icône `pen-tool` ou `edit-3` (Lucide).
- [ ] **B3. Détection dans `api_file_view()`** : Dans `backend/main.py`, pour les fichiers `.excalidraw` :
- [x] **B1. Ajout à `SUPPORTED_EXTENSIONS`** : Ajouter `.excalidraw` dans `backend/indexer.py:56` pour que les fichiers apparaissent dans l'arborescence et soient indexés.
- [x] **B2. Icône** : Ajouter `.excalidraw` dans `EXT_ICONS` (`frontend/js/utils.js`) → icône `pen-tool` ou `edit-3` (Lucide).
- [x] **B3. Détection dans `api_file_view()`** : Dans `backend/main.py`, pour les fichiers `.excalidraw` :
- Lire le JSON
- Vérifier `data.get("type") === "excalidraw"`
- Retourner `is_excalidraw: true` + les données parsées (`elements`, `appState`, `files`)
- Si le JSON est invalide ou n'est pas un fichier Excalidraw valide → fallback sur le viewer JSON standard
- [ ] **B4. Endpoint de sauvegarde** : Le endpoint existant `PUT /api/file/{vault}` fonctionne déjà pour écrire du contenu. L'iframe envoie le JSON modifié via postMessage → le parent appelle l'API existante. Aucun nouvel endpoint nécessaire.
- [ ] **B5. Indexation du contenu texte** : Extraire le texte des éléments Excalidraw (`element.text` pour les éléments de type `text`) pour l'indexation TF-IDF. Permet de rechercher du texte présent dans les diagrammes.
- [ ] **B6. Contenu initial pour nouveaux fichiers** : Définir le squelette JSON minimum pour un fichier `.excalidraw` vide :
- [x] **B4. Endpoint de sauvegarde** : Le endpoint existant `PUT /api/file/{vault}` fonctionne déjà pour écrire du contenu. L'iframe envoie le JSON modifié via postMessage → le parent appelle l'API existante. Aucun nouvel endpoint nécessaire.
- [ ] **B5. Indexation du contenu texte** (NON FAIT) : extraire `element.text` des éléments pour la recherche TF-IDF — les fichiers sont indexés comme JSON brut.
- [x] **B6. Contenu initial pour nouveaux fichiers** : Définir le squelette JSON minimum pour un fichier `.excalidraw` vide :
```json
{"type":"excalidraw","version":2,"elements":[],"appState":{"viewBackgroundColor":"#ffffff"},"files":{}}
```
Ce squelette est retourné par le backend quand on crée un fichier `.excalidraw` (utilisé par `POST /api/file/{vault}`).
##### C. Frontend — Intégration dans le viewer (1 jour)
- [ ] **C1. Module `frontend/js/excalidraw-viewer.js`** (nouveau) : Fonction `renderExcalidraw(container, data, vault, path)` :
- [x] **C1. Module `frontend/js/excalidraw-viewer.js`** (nouveau) : Fonction `renderExcalidraw(container, data, vault, path)` :
- Crée une `<iframe>` avec `src="/frontend/excalidraw-editor.html"` et `sandbox="allow-scripts allow-same-origin"`
- Stocke une référence à l'iframe pour la communication
- Attend le message `ready` de l'iframe
@@ -592,46 +598,46 @@
- Écoute les messages `save` → appelle `saveFile(vault, path, JSON.stringify(data))` via l'API existante
- Écoute les messages `modified` → met à jour l'indicateur dans la barre d'onglets
- Gère le thème : écoute `themeChanged` → envoie `postMessage({type: "theme", theme})` à l'iframe
- [ ] **C2. Dispatch dans `viewer.js`** : Dans `renderFileContent()` ou `renderFile()` :
- [x] **C2. Dispatch dans `viewer.js`** : Dans `renderFileContent()` ou `renderFile()` :
- Après la détection `data.is_json`, ajouter une branche : si `data.is_excalidraw === true` → appeler `renderExcalidraw(container, data, vaultName, filePath)`
- Ne PAS passer par le viewer markdown standard
- [ ] **C3. Barre d'outils contextuelle** : Dans la toolbar du viewer (celle avec Copier/Source/Éditer/PDF/pop-out) :
- [x] **C3. Barre d'outils contextuelle** : Dans la toolbar du viewer (celle avec Copier/Source/Éditer/PDF/pop-out) :
- Pour les fichiers `.excalidraw` : remplacer « Éditer (Forge) » par « Ouvrir dans Excalidraw.com » (lien externe, nouvel onglet)
- Garder « Télécharger » (.excalidraw) et « pop-out »
- Badge « Excalidraw » avec icône `pen-tool`
- [ ] **C4. Auto-save** : Débounce 2 secondes après la dernière modification dans l'iframe → sauvegarde automatique silencieuse (comme l'éditeur markdown #29). L'iframe émet `modified` → le parent démarre un timer → au bout de 2s sans nouvelle modification → `postMessage({type: "requestSave"})` → l'iframe répond avec `save` → le parent écrit via l'API.
- [ ] **C5. Raccourci Ctrl+S** : L'iframe intercepte Ctrl+S → envoie `save` au parent → le parent sauvegarde → confirmation visuelle (toast « Excalidraw sauvegardé »).
- [ ] **C6. Compatibilité Split View (#75)** : L'iframe s'affiche dans le content-area du panneau actif. Le `PaneTabManager` gère le cache : quand on switch d'onglet, l'état de l'iframe est préservé (elle reste dans le DOM, juste masquée). Plusieurs iframes Excalidraw peuvent coexister dans différents panneaux.
- [ ] **C7. Création via la modale « Nouveau fichier »** : Dans `frontend/js/ui.js`, fonction `showCreateFileModal()` :
- [x] **C4. Auto-save** : Débounce 2 secondes après la dernière modification dans l'iframe → sauvegarde automatique silencieuse (comme l'éditeur markdown #29). L'iframe émet `modified` → le parent démarre un timer → au bout de 2s sans nouvelle modification → `postMessage({type: "requestSave"})` → l'iframe répond avec `save` → le parent écrit via l'API.
- [x] **C5. Raccourci Ctrl+S** : L'iframe intercepte Ctrl+S → envoie `save` au parent → le parent sauvegarde → confirmation visuelle (toast « Excalidraw sauvegardé »).
- [x] **C6. Compatibilité Split View (#75)** : L'iframe s'affiche dans le content-area du panneau actif. Le `PaneTabManager` gère le cache : quand on switch d'onglet, l'état de l'iframe est préservé (elle reste dans le DOM, juste masquée). Plusieurs iframes Excalidraw peuvent coexister dans différents panneaux.
- [x] **C7. Création via la modale « Nouveau fichier »** : Dans `frontend/js/ui.js`, fonction `showCreateFileModal()` :
- Ajouter `<option value=".excalidraw">Excalidraw (.excalidraw)</option>` dans le `<select id="file-ext-select">` (après `.json`)
- Quand l'extension `.excalidraw` est sélectionnée, le backend crée le fichier avec le squelette JSON minimum (B6)
- Après création → `openFile(vault, path)` → le viewer détecte `is_excalidraw: true` → l'iframe s'ouvre avec le canvas vierge
- Fonctionne aussi via la palette de commandes `Ctrl+Alt+Space` → « Nouveau fichier » (action `create-file` existante)
- [ ] **C8. Création via le menu contextuel de l'arborescence** : Dans `frontend/js/context-menu.js`, ajouter une option « 🎨 Nouveau diagramme Excalidraw » dans le menu contextuel des répertoires → ouvre directement la modale avec `.excalidraw` pré-sélectionné.
- [ ] **C8. Création via le menu contextuel (NON FAIT — la modale « Nouveau fichier » suffit)** : Dans `frontend/js/context-menu.js`, ajouter une option « 🎨 Nouveau diagramme Excalidraw » dans le menu contextuel des répertoires → ouvre directement la modale avec `.excalidraw` pré-sélectionné.
##### D. CSS & Design (0.5 jour)
- [ ] **D1. Styles de l'iframe dans ObsiGate** : L'iframe occupe 100% du content-area (`width: 100%; height: 100%; border: none;`). Aucun padding ni marge.
- [x] **D1. Styles de l'iframe dans ObsiGate** : L'iframe occupe 100% du content-area (`width: 100%; height: 100%; border: none;`). Aucun padding ni marge.
- [ ] **D2. Thème dark/light** : L'iframe reçoit le thème courant → Excalidraw applique son thème interne (`theme="dark"` ou `theme="light"`). Les couleurs sont cohérentes avec ObsiGate grâce à la palette d'Excalidraw.
- [ ] **D3. Écran de chargement** : Pendant le chargement de l'iframe (React + Excalidraw ~2 Mo), afficher un spinner « Chargement de l'éditeur Excalidraw... » dans le content-area. L'iframe envoie `ready` → le spinner disparaît.
- [ ] **D4. Responsive** : L'iframe s'adapte à la largeur du panneau. En mode mobile (<768px), l'éditeur Excalidraw est utilisable (UI tactile native).
- [x] **D3. Écran de chargement** : Pendant le chargement de l'iframe (React + Excalidraw ~2 Mo), afficher un spinner « Chargement de l'éditeur Excalidraw... » dans le content-area. L'iframe envoie `ready` → le spinner disparaît.
- [x] **D4. Responsive** : L'iframe s'adapte à la largeur du panneau. En mode mobile (<768px), l'éditeur Excalidraw est utilisable (UI tactile native).
##### E. Gestion des conflits et edge cases (0.5 jour)
- [ ] **E1. Fichier modifié à l'extérieur** : Si le fichier est modifié par Syncthing/watcher pendant l'édition → détecter via le watcher → afficher un bandeau « Ce fichier a été modifié à l'extérieur. Recharger ? » avec boutons [Recharger] [Ignorer].
- [ ] **E2. Plusieurs onglets** : Deux onglets sur le même fichier `.excalidraw` → le second détecte que le fichier est déjà ouvert → focus l'onglet existant (comportement existant du `TabManager` #E4).
- [ ] **E3. Fichier vide ou nouveau** : Couvert par C7/C8 — la création d'un `.excalidraw` produit un canvas vierge avec le squelette JSON minimum (B6). L'iframe gère nativement le cas `elements: []`.
- [x] **E1. Fichier modifié à l'extérieur** : Si le fichier est modifié par Syncthing/watcher pendant l'édition → détecter via le watcher → afficher un bandeau « Ce fichier a été modifié à l'extérieur. Recharger ? » avec boutons [Recharger] [Ignorer].
- [x] **E2. Plusieurs onglets** : Deux onglets sur le même fichier `.excalidraw` → le second détecte que le fichier est déjà ouvert → focus l'onglet existant (comportement existant du `TabManager` #E4).
- [x] **E3. Fichier vide ou nouveau** : Couvert par C7/C8 — la création d'un `.excalidraw` produit un canvas vierge avec le squelette JSON minimum (B6). L'iframe gère nativement le cas `elements: []`.
- [ ] **E4. Fichier corrompu** : Si le JSON ne contient pas `type: "excalidraw"` ou est invalide → fallback sur le viewer JSON standard avec un message « Ce fichier .excalidraw semble corrompu ».
- [ ] **E5. Pop-out** : Le bouton pop-out fonctionne — il ouvre l'éditeur dans une popup séparée avec sa propre iframe. Utile pour éditer sur un deuxième écran.
- [ ] **E6. Annulation (Ctrl+Z)** : Natif dans Excalidraw — l'historique d'annulation est géré par l'état interne de l'iframe. Pas besoin d'interaction avec le parent.
- [x] **E5. Pop-out** : Le bouton pop-out fonctionne — il ouvre l'éditeur dans une popup séparée avec sa propre iframe. Utile pour éditer sur un deuxième écran.
- [x] **E6. Annulation (Ctrl+Z)** : Natif dans Excalidraw — l'historique d'annulation est géré par l'état interne de l'iframe. Pas besoin d'interaction avec le parent.
##### F. Tests (0.5 jour)
- [ ] **F1. Tests backend** :
##### F. Tests (0.5 jour) — F1 ✅ (6 tests test_excalidraw.py)
- [x] **F1. Tests backend** :
- `test_excalidraw_detection.py` : fichier `.excalidraw` valide → `is_excalidraw: true`, JSON invalide → fallback JSON, fichier sans `type: excalidraw` → fallback
- `test_excalidraw_search.py` : texte extrait des éléments → recherchable via TF-IDF
- [ ] **F2. Tests frontend** :
- [ ] **F2. Tests frontend (non retenus)** :
- Chargement de l'iframe avec des données de test
- Communication postMessage (init → ready → save)
- Changement de thème propagé à l'iframe
- [ ] **F3. Tests E2E (Playwright, #58)** :
- [ ] **F3. Tests E2E (Playwright) (NON FAITS)** :
- Ouvrir un fichier `.excalidraw` → l'iframe se charge → le canvas Excalidraw est visible
- Dessiner un rectangle → sauvegarder → recharger → le rectangle est toujours là
- Basculer thème sombre → l'iframe passe en dark mode
@@ -721,8 +727,8 @@
- [ ] UI : toggle « Recherche sémantique » dans la barre de recherche
- [ ] UI : score de similarité dans les résultats
### 71. Tableau de bord administrateur — Backend ✅, Frontend ⚪
- **Effort :** 2 jours | **Impact :** 🟢 | **Statut :** 🟡 Partiellement livré
### 71. Tableau de bord administrateur — Backend ✅, Frontend ✅
- **Effort :** 2 jours | **Impact :** 🟢 | **Statut :** ✅ Terminé (2026-08, commits 46be24f / 88ab8db / 01453bc)
- **Description :** Une page web dédiée accessible uniquement aux administrateurs qui centralise tout le monitoring et la gestion du serveur ObsiGate en un seul endroit. Un cockpit de pilotage pour le sysadmin.
- **Implémentation réelle (vérifiée) :**
- **Backend `backend/admin.py`** (nouveau, 261 lignes) — 4 endpoints admin-gated (`require_admin`) :
@@ -733,11 +739,11 @@
- `backend/main.py` — routeur monté + middleware gzip bypass pour `/api/admin/stream`
- `backend/requirements.txt` — ajout `psutil>=5.9`
- **Tests :** `tests/test_admin.py` (13 tests, 100% verts) — couvrent auth + filtres + format SSE
- **Reste à faire :**
- Page frontend `frontend/admin.html` + module `frontend/js/admin.js` (non livré dans cette PR)
- Lien « Admin » dans le user menu (à ajouter dans `frontend/index.html` ou `ui.js`)
- Widgets temps réel côté frontend (EventSource + DOM updates)
- CRUD UI pour `/admin/users` (le backend existe déjà via `auth/router.py:514-555`)
- **Complété (2026-08) :**
- `frontend/admin.html` (472 l.) + `frontend/js/admin.js` (544 l.) — dashboard standalone avec navigation par sections sticky + thèmes
- Lien « Admin » dans le user menu (`#admin-menu-row`, gating `role === "admin"`)
- Widgets temps réel via EventSource `/api/admin/stream` + snapshot `/api/admin/stats`
- CRUD users + fix routing `/admin.html` + fix scroll
### 72. API publique documentée — OpenAPI 3.1
- **Effort :** 1-2 jours | **Impact :** 🟢
@@ -802,19 +808,19 @@
- **Sous-tâches :**
##### A. Initialisation du projet Tauri (1 jour)
- [ ] Installer Rust + toolchain Tauri : `cargo install tauri-cli`
- [ ] Initialiser `tauri init` dans `/desktop/` avec config Windows/Linux
- [ ] Configurer `tauri.conf.json` : fenêtre 1200×800, sans cadre, titre "ObsiGate"
- [ ] Configurer le build : cibles `.msi`/`.nsis` (Windows), `.deb`/`.AppImage` (Linux)
- [ ] Ajouter les icônes desktop (`.ico` Windows, `.png` Linux) dans `desktop/icons/`
##### A. Initialisation du projet Tauri (1 jour) — ✅ livré (vérifié 2026-09)
- [x] Toolchain : tauri-cli 2.11.4 / rustc 1.94.1
- [x] Projet Tauri v2 dans `desktop/` (Cargo.toml, build.rs)
- [x] `tauri.conf.json` : fenêtre 1200×800 (min 800×600), titre "ObsiGate"
- [x] Build : cibles `.msi`/`.nsis` (Windows), `.deb`/`.AppImage` (Linux)
- [x] Icônes desktop dans `desktop/icons/` (.ico, .icns, .png)
##### B. Intégration du backend Python (2-3 jours)
- [ ] Bundle Python : créer un dossier `python-embed/` avec `python3.11-embed` + `site-packages/` (requirements.txt gelés)
- [ ] Script `sidecar.py` : lance uvicorn sur `localhost:17890`, log dans `%APPDATA%/ObsiGate/logs/`
- [ ] Code Rust `main.rs` : spawn le sidecar comme processus fils, health check (boucle `GET /api/health` avec timeout 10s), kill propre au `SIGTERM`
- [ ] Menu tray : icône dans la barre des tâches avec options « Ouvrir ObsiGate », « Quitter »
- [ ] Gestion du port : détecter si 17890 est déjà utilisé → incrémenter (17891, 17892...)
##### B. Intégration du backend Python (2-3 jours) — ✅ livré (variante : uvicorn spawné directement, pas de sidecar.py)
- [x] Bundle Python : `desktop/python-embed/` (python3.11-embed + site-packages, validé par validate-structure.sh)
- [x] Lancement backend : `spawn_backend()` Rust lance `python-embed -m uvicorn backend.main:app` (équivalent sidecar), logs dans `%APPDATA%/ObsiGate/logs/backend.log`
- [x] `main.rs` : spawn processus fils, health check (`GET /api/health`, 30 essais × 2s), kill propre (SIGTERM→wait→kill)
- [x] Menu tray (voir section C ✅)
- [x] Gestion du port : `pick_free_port()` scan 17890..17899 si occupé (commit c066b2c, 3 tests Rust)
##### C. Fonctionnalités desktop natives (2-3 jours) — ✅ COMPLÉTÉ
- [x] **Sélecteur de dossier** : `pick_vault_folder` via `tauri_plugin_dialog` → ajoute le vault dans config.json
@@ -869,12 +875,13 @@
| Priorité | Items | Effort total estimé |
|---|---|---|
| ✅ Complété | #1 → #57 | ~65 jours |
| 🔵 P1 | ✅ #58 (Playwright E2E) | Terminé |
| 🔵 P2 | 🔨 #77 (Tauri Desktop) | 8-12 jours |
| ⚪ P3 | #59, #61-66, #74, #75, #76, #78 (11 items) | 35-46 jours |
| ⚪ P4 | #67 → #73 (7 items) | 18-23 jours |
| **Total restant** | **20 items** | **63-84 jours** |
| ✅ Complété | #1 → #59, #63-66, #71, #74, #75*, #76 (58 E2E, 59 offline, 63 i18n, 64 MFA TOTP+WebAuthn, 65 thèmes, 66 export, 71 admin, 74 PDF, 76 BooksLM) | ~75 jours réalisés |
| 🔵 P2 restant | #77 Desktop : signature code (optionnel), wizard 1er lancement (optionnel), 6 tests E2E **manuels** | ~1-2 jours |
| ⚪ P3 restant | #61 Plugins système (4-5j) · #62 Collaboration Yjs (5-7j) · #78 Excalidraw finitions (B5 recherche, C8, F3 E2E, BUG-002) (~1-1.5j) | ~10-13.5 jours |
| ⚪ P4 restant | #67 Push (2j) · #68 Health enrichi (1j) · #69 Mobile éditeur (2-3j) · #70 Sémantique (4-5j) · #72 OpenAPI (1-2j) · #73 Sync (6-8j) | 16-21 jours |
| **Total restant** | **9 items + finitions** | **~28-37 jours** |
\* #75 : 100% fonctionnel, il ne reste que les tests E2E Playwright de la sous-tâche I3.
---
@@ -883,4 +890,5 @@
- Les items P3/P4 ne sont pas ordonnés par priorité interne — à raffiner selon les retours utilisateurs.
- L'effort inclut le développement + tests unitaires + intégration CI, mais pas la documentation utilisateur.
- Les items marqués 🟢 (nice-to-have) sont de bons candidats pour des contributions externes.
- Le mode hors-ligne (#59) et l'i18n (#63) sont les P3 ayant le meilleur rapport effort/valeur.
- #59 (hors-ligne), #63 (i18n), #64 (MFA), #65-66, #71, #74, #75, #76 sont livrés — les P3 restants à plus fort rapport effort/valeur : #78 finitions (recherche texte Excalidraw) et #68 (health check, 1j).
- #78 a un bug ouvert référencé dans docs/ISSUES_TODOLIST.md (BUG-002, loading infini Excalidraw) — fixés par les commits a4ea322/185d603, à revalider sur poste client avant de cocher F3.