BUG-003: annotations de types, gardes None sur get_user(), PdfReader: Any et import PROVIDERS manquant (bug latent main.py:4523). Etape mypy du CI rendue bloquante (etait advisory).
BUG-004: lien README.md -> docs/CONTRIBUTING.md corrige (+ DELIVERY_WORKFLOW.md), arbre projet mis a jour, parite README.fr.md.
Verifie: mypy 0 erreur, ruff OK, pytest 728 passed / 5 skipped, frontend OK, liens md OK.
Le .gitignore exclut _*.py, donc backend/tools/__init__.py n'etait pas versionne et
la CI echouait a l'import (unknown location). Remplacement par une facade api.py
qui enregistre les outils et reexporte l'API publique. Tests adaptes.
- Persistance ai_default_provider / ai_default_models dans data/config.json
- Lecture + rechargement a chaud dans backend/ai.py (get_default_provider, reload_ai_config)
- UI: selecteurs Fournisseur/Modele par defaut dans la section Cles API IA (i18n FR/EN)
- docs/AI_ARCHITECTURE_GUIDE.md: architecture cible, catalogue d'outils, decisions MCP
Contexte:
- Repertoire (mode historique BooksLM, via menu contextuel dossier)
- Documents (bouton flottant quand des documents sont ouverts)
- General (bouton flottant sans document: aide app + creation de fichiers)
- Le contexte est affiche dans le header; suggestions adaptees au mode
- Blocs obsigate-action rendus en carte avec bouton Appliquer (pas d'execution auto)
Backend:
- /api/ai/bookslm/context et /chat acceptent mode + context_files
- collect_files_context(), empty_context(), build_general_system_prompt()
- mode documents degrade vers general si aucun fichier lisible
Corrige:
- bouton flottant passait le chemin d'un fichier comme repertoire -> erreur
'Aucun fichier markdown trouve dans ce dossier'
- panneau precedent masque (localStorage) ne se rouvrait plus
- selecteur fournisseur/modele deplace sur une ligne dediee (header actions visibles)
- Entree envoie, Ctrl+Entree insere un saut de ligne
Tests: test_bookslm.py (+13) et tests/frontend/ai.test.mjs (+8)
BUG-001 (PDF stream 500) : le nom Unicode du fichier etait injecte brut
dans Content-Disposition -> en-tete HTTP invalide -> 500. Nouveau helper
_content_disposition (RFC 5987 : filename ASCII + filename*=UTF-8'').
Applique aux 2 branches /pdf/stream. Test: tests/test_pdf_stream.py.
BUG-002 (Excalidraw 'Loading...' infini) : deux causes.
1) L'import esm.sh avec ?alias=react:... etait propage a chaque dep
transitive; esm.sh renvoie 408 sur les builds a gamme semver (jotai@>=2.9.2)
-> import entier echoue. On retire l'alias et on utilise React 19 coherent
(spec a gamme + target identique a celle d'Excalidraw) -> plus de 408.
2) L'editeur passait la prop legacy 'excalidrawRef', inoperante en 0.18
(la prop reelle est 'excalidrawAPI') -> l'API n'etait jamais recue,
le 'Loading' ne se masquait pas, save/export morts.
Verifie : 534 tests backend verts (dont 3 nouveaux PDF) + E2E navigateur
(loading masque + cycle requestSave->save OK).
Corrige l'incoherence d'affichage (page principale 0.0.0-dev vs a propos
v2.0.0-dev) et unifie la gestion de version sur une seule source de verite.
Backend:
- version.py: get_version() retourne toujours le tag x.y.z propre du dernier
tag (plus de 0.0.0-dev / -N-gHASH dans l'UI). Ajout get_git_describe() et
get_git_commit() pour le detail.
- main.py: /api/health expose git_describe + git_commit (nouveaux champs).
Frontend:
- modale A propos (populateAbout): version + commit lus depuis /api/health,
suppression du hardcode v2.0.0-dev dans index.html.
- section config A propos (loadAbout): lit health.version, plus de state.
APP_VERSION obsolete.
- badge header: fetch /api/health (fallback neutre '...').
- state.js: suppression de APP_VERSION (1.5.0) devenu mort; imports nettoyes.
Script:
- scripts/bump_version.sh: incrementation SemVer selon les commits
(breaking->major, feat->minor, sinon patch), creer/pousser le tag vX.Y.Z.
Tests: 507 passed, frontend validators OK.
Le menu Admin redirigeait vers /admin.html mais aucune route backend ne le
servait : le catch-all SPA /{full_path:path} renvoyait index.html, d'où le
retour à la page principale.
- backend/main.py: route GET /admin.html (gate require_admin) déclarée avant
le catch-all SPA + correction des imports JS /frontend/js -> /static/js
- frontend/admin.html: chemins d'assets alignés sur le mount /static
- tests/test_admin.py: 3 tests de régression (page servie admin, refus 401/403)
- backend: ajout DELETE /api/config/ai-keys/{provider_env} pour supprimer une clé
- frontend: badge ✓ Configuré / Non configuré sur chaque ligne provider
- frontend: bouton × Supprimer avec confirmation sur les providers configurés
- testAIKeys met à jour les badges selon les résultats du test
1. **fix(xiaomi): la clé Xiaomi échouait avec 'Name or service not known'**
- URL précédente api.xiaomi.com n'existe pas (DNS fail)
- Vraie URL: https://api.xiaomimimo.com/v1/models
- Xiaomi MiMo utilise un header custom 'api-key:' (PAS 'Authorization: Bearer')
- Modèles par défaut mis à jour: mimo-v2.5-pro, mimo-v2.5, mimo-v2.5-asr, etc.
- Le chat dans ai.py supporte maintenant un header custom via auth_header_name
2. **fix(admin): EventSource 503 → fallback polling**
- Ajout d'un fallback: si EventSource ne reçoit jamais le premier event
(cookie expiré, réseau bloqué, proxy timeout), on bascule sur du polling
/api/admin/stats toutes les 5s. Le UI continue de se mettre à jour.
- Cleanup correct du timer dans disconnectSSE
3. **fix(test_ai_models)**: 2 nouveaux tests de régression
- test_xiaomi_uses_api_key_header_not_bearer: vérifie URL + header
- test_xiaomi_test_endpoint_uses_real_url: vérifie /api/config/ai-keys/test
- Patche backend.ai ET backend.main (import local)
- Header case-insensitive (urllib normalise à 'Api-key', HTTP est insensible)
Vérifié :
- pytest : 504 passed (502 + 2 nouveaux Xiaomi)
- frontend unit : 7 passed
- validate-imports : 30 modules / 204 exports
- pane-manager JSDOM : 9/9
- ruff check backend/ : All checks passed
Trois bugs corrigés + une amélioration demandée :
1. **fix(admin): /admin.html redirigeait toujours vers /**
- admin.js _gateAdmin() lisait /api/auth/status qui ne contient PAS le rôle user
- Remplacé par /api/auth/me (retourne username, role, vaults)
- Le code distingue maintenant le cas 'auth désactivé' (admin anonyme) du
cas 'auth requise non admin' (affiche écran forbidden)
2. **fix(ai): les modèles Nvidia/Xiaomi ne se chargeaient pas dans les dropdowns**
- Xiaomi : l'endpoint public /v1/models est instable, échec réseau fréquent
- Toutes les erreurs réseau/d'API renvoyaient models=[] → dropdown vide
- Ajout d'un fallback curé : _FALLBACK_MODELS dict avec 4-8 modèles populaires
par provider, TOUJOURS retourné si la clé manque OU si l'API distante échoue
- Le frontend voit désormais 'fallback' vs 'live' comme hint pour l'utilisateur
- Gemini parsing : strip du préfixe 'models/' retourné par l'API Gemini
- 7 nouveaux tests pytest pour _FALLBACK_MODELS + endpoint /api/config/ai-models
3. **feat(ai): picker provider/model dans la toolbar AI + BooksLM**
- Plusieurs providers peuvent maintenant être activés simultanément
- Sélection provider+model par section (Forge, BooksLM, etc.) via dropdown
- État persisté en localStorage (le choix suit l'utilisateur entre sections)
- Chaque appel AI passe maintenant {provider, model} au backend
- ai.js : aiAction() lit le picker et l'injecte dans le body
- bookslm.js : envoie provider+model à /api/ai/bookslm/chat
- ai_routes.py + bookslm_routes.py : AIRequest et BooksLMChatRequest
acceptent provider+model, avec save/restore du modèle original
pour ne pas affecter les autres requêtes concurrentes
- 7 nouvelles clés i18n (ai.provider, ai.model, ai.model_loading, etc.)
- 1 nouveau test bookslm vérifie que le schema accepte provider+model
- validate-imports.mjs : fix faux positif sur 'export { X as Y }'
Vérifié :
- pytest : 502 passed, 5 skipped (494 baseline + 7 AI models + 1 BooksLM)
- frontend unit : 7 passed
- validate-imports : 30 modules / 204 exports / 0 erreur
- pane-manager JSDOM : 9/9
- ruff check backend/ : All checks passed
Fix final pour atteindre 'All checks passed!' sur ruff check backend/.
Le commit précédent avait raté ce fichier (modifié par --unsafe-fixes
mais non inclus dans le commit).
- bookslm.py: PERF402 — list(_cache) au lieu de la boucle d'append
- bookslm_routes.py: SIM102 — combine les conditions imbriquées (via --unsafe-fixes)
- export.py: DTZ005 — datetime.now(tz=timezone.utc) au lieu de naive now()
- watcher.py: SIM102 — combine les conditions imbriquées
Résultat : ruff check backend/ → All checks passed!
Vérifié : pytest full suite reste 492 passed, 5 skipped, 0 failed.
Réduit les erreurs ruff de 11 à 5 (toutes pré-existantes non auto-fixables) :
- F401 imports inutilisés dans auth/mfa.py
- I001 blocs d'imports non triés dans auth/router.py + main.py + pdf_reader.py
Les 5 restantes sont dans bookslm/export/watcher (code pré-existant, hors scope).
Vérifié : pytest full suite reste 492 passed, 5 skipped, 0 failed.
- tests/test_pdf.py : 13 tests (100% verts) couvrant :
- extract_pdf_text/metadata/toc avec edge cases (corrupt, missing, truncation)
- .pdf dans SUPPORTED_EXTENSIONS
- _scan_vault() extrait le texte des PDFs (vérifié avec fixture reportlab)
- parseur du filtre ext:pdf
- backend/pdf_reader.py : fix NameError quand pymupdf est installé
(PdfReader n'était déclaré que dans la branche except ImportError)
- backend/requirements-test.txt : reportlab pour générer des PDFs de test (devDep only)
- README.md + README.fr.md : section 'PDF support' documentée
- docs/ROADMAP.md : #74 marqué 'pratiquement complet' avec détail honnête des items livrés vs non
- CHANGELOG.md : entrées pour #71 admin (déjà dans commit précédent), #75 I2 et #74
inotify ne voit pas les ecritures faites depuis d'autres clients NFS
(Obsidian sur Windows), donc index_updated n'etait jamais emis et la
section Recent du sidebar ne se rafraichissait pas. Detection du fstype
par vault via /proc/mounts -> PollingObserver (stat-based) sur mounts
reseau, Observer natif conserve sur les mounts locaux.
- Ajoute la reconnaissance de la syntaxe Obsidian [[#Titre|Texte]] pour
créer des liens d'ancre vers les titres du document courant.
- Corrige _heading_slugify pour ignorer les balises HTML et décoder les
entités HTML (ex. &) avant de générer l'ID, évitant ainsi la
pollution par les wikilinks placés dans les titres.
- Ajoute le style .wikilink-anchor et des tests couvrant ces cas.
Fixe le rendu du document TestDir/Agents IA - Panorama Complet 2026.md.
CSP directive 'font-src' only allowed self, data:, and fonts.gstatic.com.
Excalidraw loads its handwriting fonts (*.woff2) from esm.sh via
EXCALIDRAW_ASSET_PATH. All ~12 font files were blocked (blocked:csp)
→ Excalidraw fails to initialize → infinite 'Loading...' spinner.
Fix: added https://esm.sh to font-src directive.
Add _normalize_line_breaks() pre-processor that converts single \n to
two-spaces+\n (hard break) before mistune rendering, while protecting
fenced code blocks. This matches Obsidian's default 'Strict Line Breaks
= off' behavior where standalone lines render on separate rows instead
of merging into a single paragraph.
- Dockerfile: COPY .git/HEAD + .git/refs/ → génère backend/VERSION avec hash
- version.py: fallback lit backend/VERSION (à côté du module, pas racine repo)
- Format: 1.8.0-0-g<8-char-hash> ou 1.8.0-dev si indisponible
#74 — Support PDF complet:
- backend/pdf_reader.py — pymupdf/pypdf text extraction
- .pdf added to SUPPORTED_EXTENSIONS in indexer.py
- PDF-aware file view API endpoint (metadata, text preview)
- Streaming endpoint /api/file/{vault}/pdf/stream
- Frontend PDF viewer with iframe, toolbar, download
- CSS for PDF viewer container
#75 — Split View (PaneManager):
- New module frontend/js/pane-manager.js — ES module
- 1-4 pane CSS grid with resize handles
- Split right/down via keyboard (Ctrl+Alt+\) or PaneManager API
- Persistence in localStorage (obsigate-panes)
- Collapse to single pane, per-pane active state
285 tests passent