diff --git a/CHANGELOG.md b/CHANGELOG.md index 52de57a..d30c18a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4f3217e..7cb56fd 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 `