# ObsiGate — Roadmap > **Version :** 2.1.0-dev | **Dernière mise à jour :** 2026-09-10 > Revue de cohérence roadmap ↔ code : cases cochées selon l'état réel vérifié dans le dépôt (#78 docs H1-H3, #67 push, #68 health, #74/#61 optionnels non retenus). > Voir aussi [CHANGELOG.md](./CHANGELOG.md), [AUDIT_TECHNIQUE.md](./docs/AUDIT_TECHNIQUE_2026-05-27.md) --- ## Légende | Icône | Signification | |---|---| | ✅ | Terminé | | 🔵 | En cours | | ⚪ | Prévu / Backlog | | 🔴 Impact | Critique / Bloquant | | 🟡 Impact | Utile / Attendu | | 🟢 Impact | Nice-to-have / Confort | --- ## ✅ Complété (v1.0.0 → v2.0.0-dev) ### Fondations (v1.0.0 → v1.4.0) | # | Feature | Effort | Impact | |---|---|---|---| | 1 | FastAPI backend — CRUD fichiers, vaults, recherche full-text | 5j | 🔴 | | 2 | Moteur TF-IDF + stemming français (`snowballstemmer`) | 2j | 🔴 | | 3 | Watchdog — indexation temps réel avec debounce | 1j | 🟡 | | 4 | Interface SPA vanilla JS — sidebar, viewer, éditeur CodeMirror | 8j | 🔴 | | 5 | Sécurité : JWT + Argon2id, rate limiting, audit log, CSP headers | 3j | 🔴 | | 6 | Protection path traversal, utilisateur non-root Docker | 0.5j | 🔴 | | 7 | Compression GZip SSE-safe, Cache-Control immutable | 0.5j | 🟢 | | 8 | PWA : manifest, service worker, mode standalone | 1j | 🟡 | ### UX & Productivité (v1.5.0 → v1.6.0) | # | Feature | Effort | Impact | |---|---|---|---| | 9 | Publication publique de documents (lien partageable, token) | 1j | 🟡 | | 10 | Webhooks HTTP avec signature HMAC-SHA256 | 1j | 🟡 | | 11 | Dashboard statistiques (fichiers, tags, taille, vaults) | 0.5j | 🟡 | | 12 | Gestion des conflits Syncthing | 0.5j | 🟢 | | 13 | Index inversé incrémental (hook pattern) | 1j | 🟡 | | 14 | Backlinks panel dans le viewer | 0.5j | 🟡 | | 15 | Fichiers non-supportés → UI download | 0.3j | 🟢 | | 16 | Redaction de secrets (`secret_redactor.py`) | 0.5j | 🟡 | | 17 | Backup automatique avant écriture, restauration | 1j | 🟡 | | 18 | Vue graphe — Barnes-Hut, focus, plein écran, export PNG | 3j | 🟡 | | 19 | Header flat design, sticky panels, navigation historique ← → ↑ | 1j | 🟢 | | 20 | Ctrl+survol → aperçu contenu formaté | 0.5j | 🟢 | ### Architecture (v1.5.1) | # | Feature | Effort | Impact | |---|---|---|---| | 21 | Split `app.js` (8 875 lignes) → 16 modules ES | 3j | 🔴 | | 22 | Validateur imports/exports CI + tests unitaires frontend Node.js | 0.5j | 🟡 | ### CI/CD & Qualité (v1.6.0) | # | Feature | Effort | Impact | |---|---|---|---| | 23 | Pipeline Gitea Actions : lint → test → security → build | 1j | 🔴 | | 24 | Ruff (0 erreur) + Mypy (0 erreur) + Bandit SAST + Pip-audit | 0.5j | 🟡 | | 25 | Pytest : 285 tests, 63% coverage | 3j | 🔴 | ### AI Editor (v1.7.0) | # | Feature | Effort | Impact | |---|---|---|---| | 26 | Toolbar : Edit, Tone, Translate, Generate, Rewrite, Toolbox | 2j | 🟡 | | 27 | Multi-provider : DeepSeek, OpenRouter, Gemini | 1j | 🟡 | | 28 | 16 endpoints REST `/api/ai/{action}` + backend `ai.py` / `ai_routes.py` | 2j | 🟡 | | 29 | Auto-save silencieux (2s debounce), loading toasts | 0.5j | 🟢 | ### Fonctionnalités avancées (v1.8.0 → v1.9.0) | # | Feature | Effort | Impact | |---|---|---|---| | 30 | Export PDF via WeasyPrint (endpoint API + lien share public) | 1j | 🟡 | | 31 | Palette de commandes `Ctrl+Alt+Space` | 1j | 🟡 | | 32 | Barre d'outils mobile + palette fichiers/commandes 📱 | 1j | 🟡 | | 33 | Drag & drop de fichiers | 1j | 🟡 | | 34 | Filtres recherche avancés : `created:`, `modified:`, `size:` | 0.5j | 🟡 | | 35 | Fichiers récents par vault | 0.5j | 🟡 | | 36 | Indicateur AI Actif (header + toast) | 0.3j | 🟢 | | 37 | Page d'accueil de vault (liste récursive par date, style recherche) | 0.5j | 🟡 | | 38 | Indexation non-bloquante (background thread) | 0.5j | 🟡 | | 39 | Git tags semver (v1.8.0, v1.9.0) | 0.2j | 🟢 | ### Gestion des Backups (v1.9.0) | # | Feature | Effort | Impact | |---|---|---|---| | 40 | Diff viewer : unifié + côte à côte, restauration depuis backup | 1j | 🟡 | | 41 | Gestionnaire de backups : page complète, filtre, suppression, purge | 1.5j | 🟡 | | 42 | Purge par vault avec confirmation, preview contenu (100 Ko) | 0.5j | 🟡 | | 43 | Compression gzip des vieux backups (niveau 6) | 0.5j | 🟢 | | 44 | Backup automatique périodique (`POST /api/backups/auto`) | 1j | 🟡 | | 45 | Restauration depuis le gestionnaire (extraction timestamp, confirmation) | 0.5j | 🟡 | | 46 | Auto-nettoyage : `max_backups_per_file` (défaut 10) | 0.5j | 🟡 | ### Mermaid.js (v2.0.0-dev) | # | Feature | Effort | Impact | |---|---|---|---| | 47 | CDN mermaid@11, `securityLevel: strict`, `startOnLoad: false` | 0.5j | 🟡 | | 48 | `renderMermaidBlocks()` — parse, rendu SVG, bloc d'erreur stylisé | 1j | 🟡 | | 49 | 22 templates (flowchart → sankey-beta) | 0.5j | 🟡 | | 50 | Live preview dans l'éditeur (debounce 500ms, panneau `#mermaid-live-preview`) | 1j | 🟡 | | 51 | Thème dark/light synchronisé, export SVG + PNG | 1j | 🟡 | | 52 | Rendu inline dans le viewer Markdown | 0.5j | 🟡 | | 53 | Zoom molette + drag + boutons +/- | 0.5j | 🟢 | | 54 | Mode plein écran avec header bar (icônes zoom, copy, download, close) | 0.5j | 🟢 | | 55 | Focus mode — clic diagramme → panneau latéral 480px | 0.3j | 🟡 | | 56 | Pré-processeur Obsidian (`[[liens]]`, `![[img]]`, `==highlight==`) | 0.3j | 🟡 | | 57 | Header bar : type de diagramme + toggle Code/Preview + copy SVG + download PNG | 1j | 🟡 | --- ## ✅ Complété (suite — v1.7 → v2.1) ### 58. Tests E2E Playwright ✅ FAIT - **Effort :** 2-3 jours | **Impact :** 🔴 - **Description :** Tests navigateur automatisés pour les flows critiques : login, navigation vault, recherche full-text, ouverture/édition/sauvegarde de fichier, rendu Mermaid, export PDF. - **Implémentation :** 44 tests Playwright (chromium-desktop) + 12 tests mobile dans `tests/e2e/obsigate.spec.ts`. Intégré au CI Gitea (job `e2e` après `build`). - **Sous-tâches :** - [x] Installation Playwright + config (`playwright.config.ts`) - [x] Fixtures : vault de test (utilise les vaults Docker existants) - [x] Test : dashboard → stats, tabs, Quick Help, sidebar - [x] Test : recherche full-text → résultats, snippets, tri pertinence/date - [x] Test : ouverture fichier → viewer Markdown, métadonnées - [x] Test : éditeur → Forge, basic modal, Ctrl+S - [x] Test : rendu Mermaid dans le viewer + preview Forge - [x] Test : export PDF → bouton présent - [x] Test : mode sombre → toggle, persistence localStorage - [x] Test : responsive mobile → layout, recherche, barre flottante - [x] Test : barre flottante résultats → compteur, nav, toggles Aa/wd - [x] Test : sauvegardes → filtres Tous/Recherches/Répertoires - [x] Test : raccourcis clavier → Ctrl+K, /, Escape - [x] Test : menu contextuel répertoire - [x] Intégration CI : job `e2e` dans `.gitea/workflows/ci.yml` --- ## ⚪ Backlog — Priorité 3 (P3) ### 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. - **Sous-tâches :** - [x] IndexedDB : stockage local de l'index des fichiers (paths, titles, tags) - [x] Moteur de recherche offline via IndexedDB (cursor + filtre) - [x] Cache des fichiers markdown récemment ouverts (derniers 50, prune) - [x] Stratégie de cache : Network First avec fallback IndexedDB - [x] File de synchronisation : modifications offline → appliquées au retour réseau - [x] UI indicateur : badge « Hors-ligne » + compteur de modifications en attente - [x] Gestion des conflits : détection et résolution manuelle (choix version locale vs serveur) ### 61. Plugins système — Extensions utilisateur ✅ - **Effort :** 4-5 jours | **Impact :** 🟢 | **Statut :** ✅ Livré (backend + frontend + tests + docs) - **Description :** Système de plugins permettant aux utilisateurs d'étendre ObsiGate avec des renderers personnalisés, des opérateurs de recherche, et des hooks d'UI. Inspiré du modèle de plugins Obsidian. - **Sous-tâches :** - [x] Spécification du format de plugin : `plugin.json` (name, version, hooks, permissions) - [x] API de hooks : `onFileRender`, `onSearchFilter`, `onEditorAction`, `onSidebarItem`, `onFileCreate`, `onFileDelete`, `onVaultMount` - [x] Sandbox d'exécution : Web Worker isolé pour le code plugin (blob URL, postMessage structuré, CSP sans importScripts) - [x] UI : page « Plugins » dans les paramètres (installer, activer/désactiver, désinstaller, template, viewer) - [ ] Distribution : dépôt de plugins communautaire (fichier JSON index) — ⚪ NON RETENU (backlog) - [x] Hot-reload : activation/désactivation sans rechargement de page (marker `.disabled`) - [x] Sécurité : manifest de permissions, validation path-traversal, CSP restrictif - **Livré :** - Backend `backend/plugins.py` — validation manifest (name regex, semver, hooks/permissions autorisés), stockage par vault `/.obsigate-plugins/`, lifecycle complet, validation ZIP (path traversal, limite 100 fichiers, 500KB/fichier), 9 endpoints `/api/plugins/*` (admin-gated pour install/uninstall/enable/disable), template API. - Frontend `frontend/js/plugins.js` — PluginManager, sandbox Web Worker (code via blob URL, protocole postMessage structuré), UI Settings > Plugins, hooks dispatch (`executeHook`/`onFileRender`/`onSearchFilter`/…). - Tests : `tests/test_plugins.py` (44) + `tests/frontend/plugins.test.mjs` (21) — validation, lifecycle, ZIP/dir sécurité, protocole sandbox, isolation DOM/CSP. - Docs : `docs/PLUGINS.md`. - **En backlog (non retenu) :** dépôt communautaire (index JSON), signature de code des plugins. ### 62. Collaboration temps réel — Édition simultanée - **Effort :** 5-7 jours | **Impact :** 🟢 - **Description :** Permettre à plusieurs utilisateurs d'éditer le même document markdown en même temps, comme Google Docs. Chaque personne voit en temps réel ce que les autres tapent, avec leur curseur affiché en couleur. - **WebSocket** : connexion persistante bidirectionnelle entre le navigateur et le serveur. Contrairement à HTTP où le client doit constamment demander « y a-t-il du nouveau ? » (polling), le WebSocket permet au serveur de pousser les changements instantanément. Une room WebSocket est créée par fichier ouvert — tous les utilisateurs qui éditent le même fichier rejoignent la même room. - **Yjs + CRDT** : Yjs est une bibliothèque qui implémente un algorithme CRDT (Conflict-free Replicated Data Type). Imagine deux personnes qui tapent en même temps au même endroit — sans CRDT, on aurait un conflit et du texte perdu. Avec CRDT, les deux modifications sont fusionnées mathématiquement sans perte. Chaque caractère reçoit un identifiant unique, et l'ordre final est déterministe même si les opérations arrivent dans le désordre. Pas besoin de verrouiller le fichier ni de résoudre des conflits manuellement. - **Awareness** : chaque utilisateur voit le curseur des autres (position, sélection) représenté par un nom et une couleur. Un indicateur dans la barre d'outils montre qui est connecté. - **Persistance** : le serveur sauvegarde périodiquement le document (debounce 2s après la dernière modification) pour que les changements survivent à une déconnexion. - **Pourquoi c'est important :** Permet le travail d'équipe sur la documentation, les notes de réunion, les spécifications techniques, les brainstorms. C'est le passage d'ObsiGate de « outil personnel » à « outil d'équipe ». - **Sous-tâches :** - [ ] Serveur WebSocket : endpoint `/ws/collab/{vault}/{path}` avec gestion des rooms - [ ] Intégration Yjs : `Y.Doc` partagé, `Y.Text` pour le contenu markdown - [ ] Awareness : curseurs colorés par utilisateur, sélections visibles - [ ] Synchro backend : persistance périodique du document (debounce 2s) - [ ] Gestion des droits : vérification `check_vault_access` par connexion WS - [ ] UI : indicateur de présence (avatars dans la barre d'outils éditeur) - [ ] UI : curseurs distants dans CodeMirror (extension collaborative) - [ ] 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 — ✅ 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 :** - [x] Extraction des chaînes : ~1200 clés UI extraites - [x] Format : JSON `fr.json` + `en.json` dans `frontend/locales/` → 1206 clés parfaitement synchronisées - [x] Fonction `t(key)` → `frontend/js/i18n.js` avec `_applyDOM()`, `data-i18n`, `data-i18n-attr`, `data-i18n-placeholder`, `data-i18n-html`, support des templates `{var}` - [x] Sélecteur de langue dans les paramètres (persisté localStorage `obsigate-lang`) - [x] Traduction des messages backend → les toast/showToast sont maintenant i18n dans tous les fichiers JS - [x] Documentation multilingue → `README.md` + `README.fr.md` - [x] Nettoyage des clés inutilisées → locales nettoyées - [x] Tous les fichiers JS utilisent `t()` → plus de texte FR en dur (ai.js, sync.js, graph.js, autocomplete.js) - [x] Interface principale : dashboard, sidebar, editor, search, settings → EN/FR complet - [x] Guide d'utilisation : 18 sections (Intro → Astuces) → tous les paragraphes traduits - [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 — ✅ 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. - **WebAuthn** (clés de sécurité physiques) : l'utilisateur enregistre une clé USB (YubiKey, SoloKey) ou utilise la biométrie de son appareil (empreinte digitale, Face ID, Windows Hello). Au login, le navigateur demande de toucher la clé physique ou de scanner le doigt. C'est le niveau de sécurité le plus élevé — résistant au phishing car la clé vérifie le domaine du site avant de répondre. - **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 :** - [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 — ✅ 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 :** - [x] Audit des variables CSS existantes → 40+ variables - [x] Presets: light, dark, high-contrast, sepia (générés dynamiquement) - [x] UI : sélecteur de thème dans les paramètres (swatches grid) - [x] Import/export de thème personnalisé (JSON) - [x] Application dynamique via document.documentElement.style.setProperty ### 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 :** - [x] Export HTML standalone : CSS inliné, images en base64, navigation inter-fichiers - [x] Export MD bundle : ZIP du vault avec structure préservée - [x] Export ePub : conversion markdown → ePub (zipfile + mistune, 0 nouvelle dep) - [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 — ✅ TERMINÉ - **Effort :** 4-5 jours | **Impact :** 🟡 | **Statut :** ✅ COMPLET (2026-09-07 — C3 + Range 206 + config G3 + indexation incrémentale, commit 7042307. Optionnels D2/E4/H2/I2 non retenus) - **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 `