docs: restructurer le suivi et unifier la methode de livraison
CI / lint (push) Successful in 57s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m13s
CI / build (push) Successful in 34s
CI / e2e (push) Successful in 10m33s

- ROADMAP: ne garde que le travail a venir + index compact du complete (995 -> ~155 lignes); detail deplace vers docs/features/ et docs/archive/

- docs/features/: fiches detaillees #74, #75, #76, #77, #78, #79

- docs/archive/COMPLETED_v1-v2.md: detail des items courts livres

- CHANGELOG: alignement sur les tags (2.0.0 date, 2.2.0/2.2.1 ajoutes, Unreleased = travail #79 post-2.2.1)

- AGENTS.md + docs/DELIVERY_WORKFLOW.md: methode de livraison unique (Definition of Done) referencee par ROADMAP, CONTRIBUTING, ISSUES_TODOLIST
This commit is contained in:
2026-09-11 14:07:56 -04:00
parent 55696bfb31
commit ce23ab38f7
14 changed files with 1251 additions and 883 deletions
+61
View File
@@ -0,0 +1,61 @@
# #79 — Assistant IA — Outils (function calling) & serveur MCP
> **Statut :** 🔵 En cours — Phase 0 + B1/B2/B3/B7 + G livrés (2026-09-11)
> **Effort :** 10-15 jours | **Impact :** 🟡
> **Références :** [Roadmap](../ROADMAP.md) · [Guide d'architecture IA](../AI_ARCHITECTURE_GUIDE.md)
- **Description :** Transformer l'assistant BooksLM (protocole texte `obsigate-action` limité à `create_file`/`create_directory`) en un agent capable de lire, chercher, lister, ouvrir et modifier, via **function calling natif**, puis exposer ObsiGate à des **clients MCP externes** (Claude Desktop, Cursor…). Les deux fronts consomment une **couche d'outils partagée** — source unique de vérité.
- **Documentation :** [AI_ARCHITECTURE_GUIDE.md](../AI_ARCHITECTURE_GUIDE.md) (architecture, catalogue d'outils, sécurité, phases).
## A. Fondations — couche d'outils partagée (2-3 jours) — ✅ livré (2026-09-11)
- [x] **A1.** Créer `backend/tools/` : `context.py` (`ToolContext` : user, allowed_vaults, mode, confirmed), `registry.py` (décorateur `@tool` + schéma JSON), `schemas.py`, `service.py`, `audit.py`
- [ ] **A2.** Extraire la logique métier des routes de `backend/main.py` en fonctions de service réutilisables (les routes deviennent des wrappers) — *partiel : services lecture/recherche livrés (`list_vaults`, `list_directory`, `read_file`, `search_fulltext`, `list_tags`) ; refactor des routes `main.py` à suivre*
- [x] **A3.** Contexte de permissions : chaque outil applique `check_vault_access` + `resolve_safe_path`
- [x] **A4.** Audit : journalisation JSONL de chaque appel d'outil (qui, quoi, vault, résultat) — action `ai_tool_call`, arguments sensibles résumés
- [x] **A5.** Tests unitaires du registry + services (sans IA) — `tests/test_tools.py` (30 tests)
## B. Function calling in-app (3-4 jours) — 🔵 partiel (B1/B2/B3/B7 livrés 2026-09-11)
- [x] **B1.** Abstraction tool-calling provider-agnostique : `backend/ai_chat.py` (`chat_completion`, `ToolCall`, `LLMResponse`) — OpenAI-compat (`tools`/`tool_choice`, parsing `tool_calls`) + Gemini (`functionDeclarations`/`functionCall`)
- [x] **B2.** Agent loop `backend/agent/loop.py` : boucle tool→résultat→tool, limite d'itérations (10), truncation des résultats ; endpoint opt-in `POST /api/ai/bookslm/agent` (events SSE `tool`/`message`/`confirmation`)
- [x] **B3.** Fallback : retry sans `tools` si le provider rejette les tools (400/404/422) → chat simple ; protocole texte `obsigate-action` conservé côté frontend
- [ ] **B4.** SSE réellement streaming (corriger `bookslm_routes.py` — le message final reste envoyé en un seul événement)
- [ ] **B5.** Confirmations UI : outils `read` auto, outils `write` via carte Apply (`bookslm.js:515`), aperçu diff pour `edit_file`
- [ ] **B6.** Outils de navigation in-app : `open_file`, `reveal_in_tree` (événement `obsigate:open-file`)
- [x] **B7.** Tests : agent loop LLM mocké (`tests/test_agent_loop.py`), providers (`tests/test_ai_chat.py`), endpoint (`tests/test_bookslm.py`)
## C. Catalogue d'outils — lecture & recherche (1-2 jours)
- [ ] **C1.** Vaults/navigation : `list_vaults`, `list_directory`, `list_all_files`
- [ ] **C2.** Lecture : `read_file`, `read_file_raw`, `get_backlinks`, `list_backups`, `diff_backup`, `get_graph`
- [ ] **C3.** Recherche : `search_fulltext`, `search_advanced`, `search_paths`, `list_tags`, `suggest_tags`, `list_recent`
## D. Catalogue d'outils — mutations (2 jours)
- [ ] **D1.** `create_file`, `create_directory` (migration du protocole texte existant)
- [ ] **D2.** `edit_file`, `append_to_file`, `rename_file`, `rename_directory`, `move_path`
- [ ] **D3.** `replace_in_files` (⚠️ confirmation two-step + backup auto)
- [ ] **D4.** `delete_file`, `delete_directory`, `restore_backup` (⚠️ confirmation two-step + backup auto, toggle par vault)
## E. Serveur MCP (2-3 jours)
- [ ] **E1.** `backend/mcp/server.py` (SDK MCP Python) enregistrant les outils depuis le registry
- [ ] **E2.** Mapping des primitives : Tools (mutations + recherche), Resources (vaults/fichiers en lecture `vault://`), Prompts (templates)
- [ ] **E3.** Transport **Streamable HTTP** : endpoint `/mcp` dans FastAPI, auth `Authorization: Bearer <JWT>` → `get_current_user` (décision 2026-09-11). `stdio` optionnel plus tard
- [ ] **E4.** Confirmations : **two-step `propose_*`/`apply_*`** avec token signé (usage unique, TTL) ; élicitation optionnelle si le client l'annonce (décision 2026-09-11)
- [ ] **E5.** Toggle par vault pour désactiver les outils destructifs (défaut : activés)
- [ ] **E6.** Tests : handshake MCP, permissions par vault, mapping des ressources, anti-rejeu du token
## F. Durcissement & documentation (1 jour)
- [ ] **F1.** Rate limiting par token/outil + quotas (`BOOKSLM_MAX_*`)
- [ ] **F2.** Redaction des secrets avant retour au LLM
- [ ] **F3.** Documentation OpenAPI (`backend/openapi_docs.py`) + guide MCP
- [ ] **F4.** Tests E2E de bout en bout
## G. Sélection fournisseur/modèle par défaut — ✅ livré
- [x] **G1.** Persistance `ai_default_provider` + `ai_default_models` dans `data/config.json` (`_DEFAULT_CONFIG`)
- [x] **G2.** Lecture + rechargement à chaud dans `backend/ai.py` (`get_default_provider`, `reload_ai_config`)
- [x] **G3.** UI : sélecteurs « Fournisseur par défaut » + « Modèle par défaut » dans `#cfg-ai` (`frontend/index.html`, `frontend/js/config.js`)
- [x] **G4.** i18n FR/EN
## Décisions
- ✅ Transport MCP : **Streamable HTTP** (2026-09-11)
- ✅ Confirmation MCP : **two-step `propose`/`apply`** (2026-09-11)
- ✅ Périmètre des mutations externes : **toutes autorisées** (create/edit/rename/move/delete) — encadrées par confirmation + backup auto + audit + toggle par vault (2026-09-11)
- Détail et justification dans le [guide §8](../AI_ARCHITECTURE_GUIDE.md).
+94
View File
@@ -0,0 +1,94 @@
# #76 — BooksLM — Console AI contextuelle par répertoire (style NotebookLM)
> **Statut :** ✅ Terminé
> **Effort :** 5-6 jours (réalisé) | **Impact :** 🟡
> **Références :** [Roadmap](../ROADMAP.md) · [Changelog — 2.2.0](../../CHANGELOG.md)
> **Extension (2026-09-11) :** l'assistant AI est désormais **multi-contexte** — Répertoire
> (mode historique), Documents (documents ouverts, via le bouton flottant) et Général (aucun
> document ouvert : aide sur l'app + création de fichiers/dossiers par cartes d'action). Header
> refondu (sélecteur fournisseur/modèle sur une ligne dédiée), `Entrée` envoie / `Ctrl+Entrée`
> insère un saut de ligne. Voir CHANGELOG.
- **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 :**
- L'utilisateur fait un clic-droit sur un répertoire dans l'arborescence → option « BooksLM »
- Un panneau latéral (450-500px) s'ouvre à droite, poussant le viewer existant
- Le backend collecte tous les fichiers `.md` (et `.pdf` si #74 est complété) récursivement
- Le contenu est assemblé en un contexte système pour le LLM (fenêtre de contexte optimisée)
- L'utilisateur peut chatter avec l'AI qui a une connaissance complète du répertoire
- 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) — ✅ 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.
- [ ] **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) — ✅ 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) — ✅ 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
- [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) — ✅ 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) — ✅ 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) — ✅ 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 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 (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) (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é
## H. Points d'attention / Risques
- **Taille du contexte** : Un répertoire avec 500 fichiers markdown peut facilement dépasser 1M de caractères → essentiel de tronquer intelligemment (préférer les fichiers modifiés récemment, ou prioriser selon la structure : README.md, index.md en premier).
- **Coût API** : Chaque message envoie le contexte complet au LLM → potentiellement coûteux en tokens. Ajouter un avertissement si le contexte dépasse 100K tokens estimés.
- **Performance** : La collecte récursive de 200+ fichiers peut prendre plusieurs secondes → l'UI doit montrer la progression (streaming SSE) et le backend doit être non-bloquant (background task).
- **Sécurité** : Ne pas envoyer les fichiers ignorés (`.env`, `.git/`, `.hermes/`, tokens, secrets) dans le contexte. Le `secret_redactor.py` existant (#16) doit être appliqué au contenu avant envoi au LLM.
- **Privacy** : Si le provider AI est externe (OpenRouter, DeepSeek, Gemini), le contenu des fichiers est envoyé à un tiers → warning dans l'UI avec option d'activer/désactiver par vault.
- **Scope répertoire** : Ne PAS inclure les fichiers hors du répertoire (pas de remontée au parent ou de traversée de vault). Le scope est strictement le répertoire + sous-répertoires.
+87
View File
@@ -0,0 +1,87 @@
# #77 — Application Desktop native — Tauri (Windows / Linux / macOS)
> **Statut :** 🔵 En cours — livré : A/B/C/D/E/F ; reste signature de code (optionnel), wizard 1er lancement (optionnel), 6 tests E2E manuels
> **Effort :** 8-12 jours | **Impact :** 🟡 | **Framework :** Tauri v2 (Rust + Webview)
> **Références :** [Roadmap](../ROADMAP.md) · [Guide de build & releases](../DEVELOPMENT_AND_RELEASES.md)
- **Description :** Packager ObsiGate en application desktop native autonome. L'utilisateur télécharge un `.exe` (Windows) ou un `.AppImage` (Linux), l'installe, et lance ObsiGate comme n'importe quelle app — sans Docker, sans terminal, sans navigateur. Le backend Python est embarqué, le frontend s'affiche dans une webview native. L'expérience est identique à l'application web, avec des capacités supplémentaires (accès fichiers natif, notifications OS, tray icon).
- **Architecture :**
```
ObsiGate.exe (Tauri shell ~5 Mo)
├── python-embed/ ← Python 3.11 embarqué (~30 Mo)
│ ├── backend/ ← Code FastAPI existant
│ └── site-packages/ ← Dépendances gelées
├── frontend/ ← HTML/CSS/JS (identique au web)
└── obsigate-desktop ← Binaire Rust (lance Python + ouvre webview)
```
- **Pourquoi Tauri plutôt qu'Electron ?**
- Binaire de ~35-40 Mo contre ~180 Mo pour Electron (pas de Chromium embarqué)
- RAM idle ~50 Mo contre ~200 Mo — la webview utilise le moteur du navigateur système
- Rust gère le cycle de vie du backend Python (spawn, health check, kill propre)
- Signature de code native Windows/macOS pour éviter les faux positifs antivirus
- Auto-update natif via le mécanisme de Tauri (vérifie un endpoint JSON)
- **Sous-tâches :**
## 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) — ✅ 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
- [x] **Thème système** : `get_system_theme` lit le thème OS → appliqué automatiquement
- [x] **Notifications natives** : `tauri-plugin-notification` intégré — remplace le service worker Push API
- [x] **Associations de fichiers** : `.md` → « Ouvrir avec ObsiGate » dans `tauri.conf.json`
- [x] **Menu natif** : Fichier (Nouvelle fenêtre, Fermer, Quitter) / Édition (Annuler, Rétablir, Couper, Copier, Coller, Tout sélectionner) / Aide (À propos)
- [x] **Raccourcis clavier** : `Ctrl+N`, `Ctrl+W`, `Ctrl+Q`, `Ctrl+Z`, `Ctrl+Shift+Z`, `Ctrl+X/C/V/A`
- [x] **Tray icon** : menu contextuel (Ouvrir, À propos, Quitter) + toggle fenêtre au clic gauche
- [x] **Single instance** : `tauri-plugin-single-instance` — deuxième lancement focus la fenêtre existante
- [x] **Auto-update** : `tauri-plugin-updater` configuré → vérifie les releases Gitea
- [x] **Pas de terminal visible** : `#![windows_subsystem = "windows"]` + `CREATE_NO_WINDOW` sur le processus Python
- [x] **Persistance fenêtre** : position/taille sauvegardée dans `config.json`
## D. Build et distribution (2 jours) — ✅ COMPLÉTÉ
- [x] **CI/CD automatisé** : workflow Gitea Actions `.gitea/workflows/desktop-build.yml` — build Windows + Linux à chaque push sur `main` (si `desktop/` modifié), upload des artefacts `.msi`/`.AppImage`/`.deb` en release
- [x] **Build Windows local** : `cargo build --release` vérifié (rustc 1.94.1) — `cargo tauri build --bundles msi` prêt
- [x] **Build Linux local** : workflow CI couvre `.deb`, `.rpm`, `.AppImage`
- [x] **Auto-update** : `tauri-plugin-updater` configuré → vérifie `https://git.dracodev.net/api/v1/repos/Projets/ObsiGate/releases/latest`
- [ ] **Signature de code** : configurer le certificat (optionnel mais recommandé pour Windows)
- [x] **Page de release** : README desktop existe (`desktop/README.md`)
## E. Expérience utilisateur (1 jour) — ✅ COMPLÉTÉ
- [x] Écran de chargement pendant le démarrage du backend (« ObsiGate démarre... » avec spinner) — splash inline `#boot-splash` dans `index.html`, retiré quand `app.js` signale le boot ; statut mis à jour depuis Rust
- [x] Gestion des erreurs : backend crash → `showBackendCrashBanner()` appelé par le monitor loop toutes les 5s
- [x] Sauvegarde des préférences desktop : position/taille fenêtre sauvées dans `%APPDATA%/ObsiGate/config.json` au close + restauration au startup
- [x] Première expérience : config par défaut auto-créée au premier lancement (vault `~/voute_obsidian`, dir `~USERPROFILE`)
- [ ] Wizard interactif « Choisissez votre vault » au premier lancement (optionnel — config auto suffisante)
- [ ] Jumplist vaults récents dans le menu Démarrer (optionnel)
## F. Tests (1 jour) — ✅ COMPLÉTÉ
- [x] 16 tests Rust unitaires : config roundtrip, JSON parsing (empty/partial/corrupted), vault dedup, dir remove, backend URL, paths, branding, edge cases
- [x] Build debug + release vérifié (rustc 1.94.1, tauri-cli 2.11.4)
- [x] CI desktop workflow existant (desktop-build.yml)
- [ ] Test E2E : installation → premier lancement → wizard vault → ouverture fichier (manuel)
- [ ] Test E2E : tray icon → réduire → restaurer (manuel)
- [ ] Test E2E : notifications natives → fichier modifié → popup OS (manuel)
- [ ] Test E2E : association `.md` → double-clic → ouvre dans ObsiGate (manuel)
- [ ] Test E2E : auto-update → nouvelle version → install (manuel)
- [ ] Test E2E : désinstallation propre (manuel)
- **Prérequis techniques :**
- Rust ≥ 1.75 (stable) — installé via `rustup`
- Tauri CLI ≥ 2.0 — `cargo install tauri-cli`
- Python 3.11 embed — téléchargé depuis python.org
- NSIS (Windows) — pour le générateur d'installateur `.exe`
- AppImageKit (Linux) — pour le packaging portable
+152
View File
@@ -0,0 +1,152 @@
# #78 — Éditeur Excalidraw — Ouverture et édition de fichiers .excalidraw
> **Statut :** ✅ Terminé (2026-09-10 — éditeur iframe complet, détection, création, autosave, support `.excalidraw.md`, B5 extraction texte pour la recherche, C8 création via menu contextuel, F3 E2E `tests/e2e/excalidraw.spec.js`, doc H1-H3. F2 non retenu. BUG-002 corrigé)
> **Effort :** 3-4 jours | **Impact :** 🟡
> **Références :** [Roadmap](../ROADMAP.md) · [Changelog — 2.2.0](../../CHANGELOG.md)
- **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.
- **Pourquoi c'est important :** Excalidraw est devenu le standard de fait pour les diagrammes et croquis dans l'écosystème Obsidian (plugin communautaire avec 1M+ téléchargements). Les utilisateurs créent des `.excalidraw` dans leur vault et s'attendent à pouvoir les visualiser et éditer. Actuellement ObsiGate traite ces fichiers comme du JSON brut — illisible. Avec l'éditeur intégré, ObsiGate devient un viewer/éditeur Excalidraw à part entière, supprimant le besoin d'ouvrir Obsidian Desktop ou l'app web Excalidraw séparément.
- **Fonctionnement général :**
- L'utilisateur clique sur un fichier `.excalidraw` dans l'arborescence → ObsiGate détecte l'extension et le type de contenu (`type: "excalidraw"` dans le JSON)
- Au lieu du viewer markdown ou JSON brut, une **iframe sandbox** charge l'éditeur Excalidraw avec les données du fichier
- L'utilisateur peut dessiner, ajouter des formes, du texte, des flèches, des images — l'expérience Excalidraw complète
- Les modifications sont sauvegardées automatiquement (Ctrl+S ou auto-save) via `postMessage` → le parent écrit dans le fichier via l'API ObsiGate
- L'éditeur respecte le thème sombre/clair d'ObsiGate
- **Architecture :**
```
ObsiGate SPA (content-area)
└── <iframe sandbox="allow-scripts allow-same-origin">
└── /frontend/excalidraw-editor.html
├── import * as ExcalidrawLib from "esm.sh/@excalidraw/excalidraw"
├── React + ReactDOM (fournis par Excalidraw)
├── Écoute postMessage("init", {data, theme})
└── Poste postMessage("save", {data}) au parent
```
- **Choix technique — Pourquoi une iframe plutôt qu'une intégration directe ?**
- **Isolation** : Excalidraw est un composant React avec son propre DOM virtuel, ses propres polices, et des styles CSS globaux. L'iframe empêche les conflits CSS avec ObsiGate (variables CSS, polices, z-index des modales).
- **Sandbox** : L'iframe isole le code d'Excalidraw — une erreur dans l'éditeur ne crashe pas l'application principale.
- **Chargement lazy** : Excalidraw pèse ~2 Mo minifié + React ~40 Ko. L'iframe n'est chargée QUE quand l'utilisateur ouvre un fichier `.excalidraw`. Pas d'impact sur le temps de chargement initial.
- **Communication standard** : `postMessage` est une API web native, simple et sécurisée. L'iframe n'a pas accès au DOM parent, seulement au canal de messages.
- **CSS indépendant** : Le thème sombre/clair est passé comme paramètre → l'iframe applique son propre thème sans toucher aux variables CSS d'ObsiGate.
- **Sous-tâches :**
## A. Fichier `frontend/excalidraw-editor.html` — Éditeur autonome (1.5 jour)
- [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]";
window.ExcalidrawLib = ExcalidrawLib;
</script>
```
Version épinglée (`@0.18.0`) pour la stabilité. Mise à jour manuelle testée.
- [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).
- [x] **A4. Initialisation React** (import map esm.sh → React 18.3.1 épinglé) : Excalidraw nécessite React + ReactDOM. Les importer depuis esm.sh également :
```html
<script type="module">
import React from "https://esm.sh/react@18";
import ReactDOM from "https://esm.sh/react-dom@18";
window.React = React;
window.ReactDOM = ReactDOM;
</script>
```
- [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)
- [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.
- [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)
- [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
- [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.
- [x] **B5. Indexation du contenu texte** (FAIT 2026-09) : `extract_excalidraw_indexable()` extrait `element.text` des éléments (JSON pur et `.excalidraw.md` compressé) → recherche TF-IDF fonctionnelle.
- [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)
- [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
- Envoie `postMessage({type: "init", data: {elements, appState, files}, theme})` à l'iframe
- É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
- [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
- [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`
- [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)
- [x] **C8. Création via le menu contextuel** (FAIT 2026-09 — `frontend/js/ui.js` `_createExcalidraw()`, option « Nouveau diagramme Excalidraw » sur les répertoires, E2E couvert) : ouvre la modale avec `.excalidraw` pré-sélectionné.
## D. CSS & Design (0.5 jour)
- [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.
- [x] **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.
- [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)
- [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: []`.
- [x] **E4. Fichier corrompu** (fallback viewer JSON + tests `test_invalid_json_excalidraw_fallback`/`test_json_without_excalidraw_type`) : 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 ».
- [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 ✅ (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** — ⚪ NON RETENU :
- Chargement de l'iframe avec des données de test
- Communication postMessage (init → ready → save)
- Changement de thème propagé à l'iframe
- [x] **F3. Tests E2E (Playwright)** (FAIT 2026-09 — `tests/e2e/excalidraw.spec.js`) :
- 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
## G. Points d'attention / Risques
- **Taille du bundle** : React + ReactDOM + Excalidraw ≈ 2.5 Mo minifié. Chargé depuis `esm.sh` (CDN global, cache HTTP). L'impact n'est perceptible qu'à la première ouverture d'un `.excalidraw`. Solution : précharger l'iframe en arrière-plan (`<link rel="prefetch">`) après le chargement de l'app.
- **Performance React dans iframe** : React dans une iframe fonctionne parfaitement — c'est un contexte JavaScript indépendant. Testé sur Chrome, Firefox, Safari, Edge.
- **CORS et esm.sh** : Les modules ESM depuis `esm.sh` sont servis avec les headers CORS appropriés. L'iframe est same-origin (`/frontend/excalidraw-editor.html`) donc pas de problème.
- **Mises à jour d'Excalidraw** : La version est épinglée (`@0.18.0`). Pour mettre à jour, changer le numéro dans le HTML + tester. Le format de données `.excalidraw` est stable (v2 depuis 2021).
- **Sécurité postMessage** : Vérifier `event.origin` dans les deux sens. L'iframe n'accepte que les messages de `window.parent`. Le parent n'accepte que les messages de l'iframe connue. Pas de `"*"` en production.
- **Tauri Desktop (#77)** : L'iframe se charge depuis le filesystem local (`tauri://localhost/frontend/excalidraw-editor.html`). Les imports ESM depuis `esm.sh` fonctionnent si le réseau est disponible. Pour le mode offline, bundler Excalidraw dans l'app desktop (à traiter dans #77, pas ici).
- **Pas d'édition collaborative** : Cette implémentation est mono-utilisateur. La collaboration temps réel (#62) pourra être étendue aux fichiers `.excalidraw` ultérieurement via le même mécanisme Yjs.
## H. Documentation utilisateur
- [x] **H1.** Mettre à jour README : ajouter `.excalidraw` dans les formats supportés (FR + EN)
- [x] **H2.** Ajouter dans le guide d'utilisation (Quick Help) : section « Diagrammes Excalidraw » (i18n FR/EN)
- [x] **H3.** Note : « Les fichiers .excalidraw créés avec le plugin Obsidian Excalidraw sont compatibles »
+104
View File
@@ -0,0 +1,104 @@
# #74 — Support complet des documents PDF
> **Statut :** ✅ Terminé (2026-09-07 — C3 + Range 206 + config G3 + indexation incrémentale, commit 7042307. Optionnels D2/E4/H2/I2 non retenus)
> **Effort :** 4-5 jours | **Impact :** 🟡
> **Références :** [Roadmap](../ROADMAP.md) · [Changelog — 2.1.0](../../CHANGELOG.md#210--2026-09-08)
- **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 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` (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)
- [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`).
- [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)
- [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)
- [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
- [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)
- [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.
- [ ] **D2. Distinction visuelle** — ⚪ NON RETENU : 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)
- [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)
- [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 »
- [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** — ⚪ 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)
- [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)
- [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)
- [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`
- `test_pdf_api.py` : endpoint `view` retourne `is_pdf: true`, endpoint `pdf/stream` retourne `application/pdf`, endpoint `pdf/info` retourne les métadonnées
- [ ] **H2. Tests frontend** — ⚪ NON RETENU :
- 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
- [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)
- [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 ? » — ⚪ NON RETENU (README + guide couvrent déjà le support 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.
- **Sécurité** : Les PDFs malveillants (injections JS, liens externes) ne sont pas exécutés dans l'iframe par défaut (sandbox du navigateur). Ajouter `sandbox="allow-same-origin"` sur l'iframe pour renforcer.
- **Mémoire** : pymupdf charge le PDF entier en mémoire. Pour les très gros PDFs (>200 Mo), utiliser le streaming ou `pypdf` qui supporte la lecture paresseuse.
- **Compatibilité navigateurs** : Le rendu PDF natif fonctionne sur Chrome, Firefox, Edge, Safari. Safari iOS a des limitations sur les iframes PDF. Prévoir le fallback « ouvrir dans un nouvel onglet » pour ces cas.
- **PDFs dans les vaults Obsidian** : Obsidian Desktop ne gère pas nativement les PDFs (affichage via iframe système). ObsiGate apporte une valeur ajoutée en offrant la visualisation + recherche.
+93
View File
@@ -0,0 +1,93 @@
# #75 — Éditeur multi-panneaux (Split View)
> **Statut :** ✅ Terminé — 100% implémenté + 37 tests E2E (16 split-view.spec.js + 21 split-view-matrix.spec.js, 2026-09-08)
> **Effort :** 5-7 jours (réalisé) | **Impact :** 🟡
> **Références :** [Roadmap](../ROADMAP.md) · [Changelog — 2.1.0](../../CHANGELOG.md#210--2026-09-08)
- **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)
- **Description :** Système de panneaux divisés permettant d'afficher plusieurs documents côte à côte ou superposés, à la manière d'un éditeur de code (VS Code). Drag & drop des onglets entre les panneaux, drag-to-split, menu contextuel, raccourcis clavier, palette de commandes.
- **Architecture :**
- **PaneManager** — orchestre la grille de panneaux (1-4), crée/détruit les instances PaneTabManager
- **PaneTabManager (factory `createPaneTabManager(paneId)`)** — chaque panneau a son instance indépendante avec ses propres onglets, cache, état
- **Singleton TabManager** — délègue automatiquement vers le PaneTabManager actif quand en mode split ; gère les onglets en single-pane
- **Migration automatique** — les onglets migrent singleton→PaneTabManager au premier split, et inversement au collapse
- **getContentArea()** — helper global dans viewer.js, priorise `window._activePaneContentArea` (set par PaneTabManager)
- **Implémenté :**
## A. PaneManager — Gestionnaire de panneaux
- [x] **A1.** Module `PaneManager` + factory `createPaneTabManager` — état indépendant par panneau
- [x] **A2.** Création dynamique du DOM — `.pane-grid` avec `.pane-container`, IDs suffixés `-N`
- [x] **A3.** Redimensionnement — drag handles, double-clic reset 50/50, contrainte 20-80%, localStorage
- [x] **A4.** Indépendance des panneaux — chaque panneau a ses propres onglets, scroll, cache, source view
## B. Refactoring du TabManager
- [x] **B1.** Découplage du DOM — `init(tabBar, tabList, contentArea)` par paramètre
- [x] **B2.** Factory `createPaneTabManager(paneId)` — instances indépendantes
- [x] **B3.** Compatibilité ascendante — pane 0 réutilise les IDs existants, panes 1+ suffixés `-N`
- [x] **B4.** Synchronisation state global — `setActivePane()` met à jour `currentVault`/`currentPath`
## C. Actions de division
- [x] **C1.** Menu contextuel — « Diviser à droite », « Diviser en bas », « Fermer le panneau »
- [x] **C1b.** Sous-menu « Déplacer vers... » — flyout hover listant les autres panneaux
- [x] **C2.** Raccourcis clavier — `Ctrl+Alt+\` (split R), `Ctrl+Alt+Shift+\` (split D), `Ctrl+Alt+W` (close pane), `Ctrl+Shift+Alt+W` (close others), `Ctrl+Alt+←/→/↑/↓` (navigate)
- [x] **C3.** Boutons dans la barre d'onglets — ⊞→ et ⊞↓ visibles au survol
- [x] **C4.** Fermeture panneau — retour single-pane si dernier, focus voisin, onglets perdus (pas déplacés)
## D. Drag & Drop
- [x] **D1.** Drag cross-pane — `text/plain` JSON `{paneId, tabId, index}`, indicateurs visuels
- [x] **D2.** Drag depuis l'arborescence — format `tree:{vault,path}`, drop sur panneau spécifique
- [x] **D3.** Drag-to-split — zone droite 28% → split right, zone basse 28% → split down, overlay bleu
## E. Rendu et performance
- [x] **E1.** Rendu indépendant — `window._activePaneContentArea` override pour `renderFile()`
- [x] **E2.** Cache de contenu — chaque PaneTabManager a son propre `_tabCache`, pas de refetch
- [x] **E3.** Rendu paresseux — contenu masqué via `display:none` sur panneaux inactifs
- [x] **E4.** Verrouillage éditeur — même fichier déjà ouvert ailleurs → focus le panneau existant
## F. CSS & Design
- [x] **F1-F5.** Grid layout, resize handles avec hover accent, barre onglets par panneau, variables CSS (dark/light), responsive <768px
## G. Persistance et restauration
- [x] **G1.** localStorage `obsigate-panes` — layout, panes[{id, width, activeTab, tabs[]}]
- [x] **G2.** Restauration au chargement — reconstruction grille + onglets + activation
- [x] **G3.** Reset dans la palette de commandes — « 🔄 Réinitialiser les panneaux »
## H. Compatibilité
- [x] **H1.** Palette de commandes — « Diviser à droite », « Diviser en bas », « Fermer le panneau », « Panneau suivant/précédent » (catégorie Panneaux)
- [x] **H2.** Pop-out inchangé
- [x] **H3.** Mermaid scoped au content-area
- [x] **H4.** AI Editor en modale (inchangé)
- [x] **H5.** Ctrl+S par panneau
## I. Tests
- [x] **I1.** 22 tests statiques (structure, CSS, brace balance, régression backend)
- [x] **I4.** 307 tests de régression passent
- [x] **I2.** Tests d'intégration frontend (JSDOM) — `tests/frontend/pane-manager.test.mjs` (9 tests)
- [x] **I3.** Tests E2E Playwright (#58) — 2026-09 : `split-view.spec.js` (16) + `split-view-matrix.spec.js` (21) = 37 tests verts en local + CI
## Reste à faire
- [x] **C1b.** Sous-menu « Déplacer vers... » dans le menu contextuel des onglets
- [x] **D2.** Drag & drop depuis l'arborescence vers un panneau spécifique
- [x] **E3.** Rendu paresseux (mémoire) — contenu masqué via `display:none` sur panneaux inactifs
- [x] **E4.** Verrouillage éditeur multi-panneau — même fichier ouvert dans 2 panneaux → focus le panneau existant
- [x] **G3.** Bouton reset dans la palette de commandes (🔄 Réinitialiser les panneaux)
- [x] **I2.** Tests d'intégration frontend (JSDOM) — `tests/frontend/pane-manager.test.mjs` (9 tests, 100% verts)
- [x] **I3.** Tests E2E Playwright — matrice complète ouverture/fermeture couverte (voir split-view-matrix.spec.js)
## Matrice de couverture ouverture/fermeture (validée 2026-09-08, 37/37 verts)
| Cas | Surface testée | Spec |
|---|---|---|
| Ouverture fichier → onglet | clic arbre, double-clic preview vs persistant | split-view |
| Split droit | raccourci Ctrl+Alt+\ · bouton ⊞→ · menu contextuel · palette · drag-to-split bord droit | les 5 |
| Split bas | raccourci Ctrl+Alt+Shift+\ · bouton ⊞↓ · menu contextuel · drag-to-split bord bas | les 4 |
| Déplacer onglet | drag cross-pane · flyout « Déplacer vers... » · drag depuis arbre vers panneau | les 3 |
| Navigation | Ctrl+Alt+←/→ (horizontal) · Ctrl+Alt+↑/↓ (vertical) · palette suivant/précédent | les 3 |
| Fermeture onglet | bouton X · double-clic · clic molette · Ctrl+W (single) | les 4 |
| Fermeture menu ctx | Fermer · Fermer les autres · Fermer à droite · Fermer tout → dashboard | les 4 |
| Fermeture panneau | Ctrl+Alt+W (clavier) · menu « Fermer le panneau » · Ctrl+Shift+Alt+W (autres) · auto-collapse panneau vidé (C4) · fermeture pane 0 → voisin survit · onglets du panneau fermé perdus (documenté) | les 5 |
| Persistance | layout restauré au reload · reset palette vide localStorage | les 2 |
| Edge | plafond 4 panneaux (PANE_MAX) · pas de doublon à la réouverture (E4) | les 2 |
Non couvert par E2E (hors périmètre ouverture/fermeture) : poignée de redimensionnement drag/dblclick-reset (A3) — logique vérifiée par les 22 tests statiques + JSDOM.