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.
Navigation/ergonomie:
- barre sticky de pilules (Stats, Audit, Backups, Utilisateurs) sous le header
- scrollspy (IntersectionObserver) + scroll fluide au clic dans admin.js
- ancres + scroll-margin-top sur chaque section, layout responsive <600px
Themes:
- admin.html importe et appelle initThemes() (themes.js) au boot: la page
reprend le theme/mode sauvegarde dans l'app (localStorage meme-origine)
- script anti-FOUC en <head> applique vite le mode avant le CSS
admin.html charge /static/style.css dont body{overflow:hidden;height:100vh}
(congu pour la SPA avec scroll interne). Sur la page admin autonome, ce bloc
desactivait le scroll de la page.
- frontend/admin.html: body.admin-page force overflow:auto + height:auto pour
surcharger le CSS global (specificite body.admin-page > body).
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
- search.js : suppression par flag (jusqu a la prochaine frappe) au lieu d un timeout 500ms qui expirait avant le clic sur le bouton Forge
- split-view.spec.js : le test reset panes attend l apparition de la liste de commandes (debounce 150ms du palette) avant Entree
- la copie de l onglet actif etait dedupe (fichier deja ouvert dans le pane source) et le transfert clavier (200ms) vidait le pane source -> auto-collapse -> grid disparu
- comportement coherent avec le menu contextuel : pane vide, l utilisateur y glisse ses fichiers
- search.js : le dropdown d autocomplete ne se rouvre plus 500ms apres un hide (la recherche le rouvrait et bloquait les clics toolbar - test Forge)
- ui.js + pane-manager.js : le split via menu contextuel ne transfert plus l onglet en synchrone (vidait le pane source -> auto-collapse -> grid disparu)
- split-view.spec.js : navigation clavier attend la copie asynchrone (setTimeout 150ms qui reactivait pane 0)
- split-view.spec.js : drag depuis l arbre via dragTo (drag natif HTML5)
- obsigate.spec.ts : searchFor appuie sur Entree (la recherche se declenche au keydown Enter, l autocomplete s affiche pendant la frappe)
- test_vault : ajout note1.md et note2.md (requis par les tests split-view)
- viewer.js : initialise la barre de filtres saved searches meme quand la liste est vide (sinon clics sans effet)
- frontend/index.html : splash #boot-splash 100% inline (CSS+JS), scripts CDN en defer pour ne pas bloquer le premier paint
- frontend/js/app.js : signale le boot terminé via __OBSIGATE_BOOTED (try/finally)
- desktop/src/main.rs : met à jour le statut du splash avant la redirection vers le backend
- ci: stage backend/ + frontend/ sous desktop/ avant tauri build (resources backend/** et frontend/**) dans desktop-build.yml et build-linux.sh
- desktop/gen/schemas/windows-schema.json : régénéré (permissions store/updater)
- docs: README, README.fr, ROADMAP
- 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.
Root cause: 'import ReactDOM from react-dom' resolves to the root
react-dom package which does NOT export createRoot in React 18.
createRoot lives in react-dom/client.
Changes:
- Changed import to 'react-dom/client' (line 109)
- Added error handling around the render call so loading spinner
shows an error message instead of spinning forever on failures
Three fixes for drag-and-drop in split view (#75):
1. Tree drag-to-split: edge zone detection now runs BEFORE the tree/tab
dispatch, so dragging a sidebar file to the right/bottom edge of a
single pane correctly creates a split and opens the file in the new
pane (was: tree check returned early before edge zone logic).
2. Cross-pane transfer consistency: transferTab() now tracks pane count
before the transfer — if closePane() fires inside transferTabTo()
(source pane becomes empty), the DOM is rebuilt and the original
targetTm reference is stale. After close, re-lookup the tab manager
that actually holds the transferred tab before _renderTabs()+activate.
3. dragdrop.js flicker: onDragLeave() now checks relatedTarget instead
of unconditionally clearing highlights on every child boundary.
- _hasAnyTab(): empêche split si aucun onglet ouvert (tous les points d'entrée)
- _copyActiveTabToNewPane(): copie l'onglet actif dans le nouveau panneau
- Menu contextuel: l'onglet cliqué-droit est transféré dans le nouveau panneau
- Raccourci clavier Ctrl+Alt+\: transfert l'onglet actif dans le nouveau panneau
- Boutons split: vérifient _hasAnyTab() avant d'agir
- Singleton ui.js: même comportement (split + transfert de l'onglet)
Refonte complète du layout grid:
- _computePanePositions: positions explicites (row/col/span) selon n et layout
- _createPaneAt: grid-row/grid-column explicites sur chaque .pane-container
- _addAllResizeHandles: handles placés aux bonnes positions grid
- _rebuildResizeHandles: nettoie et recrée les handles après add/remove
- closePane: repositionne les panneaux restants + rebuild handles
- 3 panneaux horizontal: 2 en haut, 1 pleine largeur en bas
- 3 panneaux vertical: 1 pleine hauteur à gauche, 2 à droite
- 4 panneaux: grille 2×2
Fix aussi: _addPane avec positions explicites + rebuild handles
Problème: le lazy rendering masquait le contenu du panneau non-sélectionné
→ split view devenait inutile (un seul panneau visible à la fois)
→ Solution: supprimé display:none dans setActivePane, tous les panneaux visibles
Test mis à jour: test_panes_remain_visible_when_inactive
C1b: Sous-menu « Déplacer vers... » (flyout hover) — liste les autres panneaux
D2: Drag depuis l'arborescence vers panneau — format 'tree:{vault,path}'
E3: Rendu paresseux — display:none sur contenu des panneaux inactifs
E4: Verrouillage éditeur — même fichier déjà ouvert → focus le panneau existant
G3: Bouton reset (🔄 Réinitialiser les panneaux) dans la palette
ROADMAP: section 75 marquée ✅ Complété (reste I2/I3 tests)
Ajouts:
- C3: Boutons Split ⊞→ ⊞↓ dans la barre d'onglets (visibles au survol)
- C2: Ctrl+Alt+←/→/↑/↓ navigation entre panneaux
- C2: Ctrl+Shift+Alt+W fermer tous les autres panneaux
- H1: 5 commandes palette (Split R/D, Close Pane, Focus Next/Prev)
- ROADMAP.md: section 75 mise à jour avec statut 80% complété
CSS: .tab-split-buttons, .tab-split-btn avec opacity hover
_buildGrid: quand on passe de single-pane à multi-pane, migre les onglets
du TabManager singleton vers le PaneTabManager du pane 0.
Sans ça, transferTab() ne trouvait pas l'onglet (il était resté dans le singleton).
_collapseToSingle: migration inverse (PaneTabManager → singleton)
quand on ferme le dernier split.
- dragstart: setData 'text/plain' (universel, pas de custom MIME)
- drop container + tab: getData('text/plain')
- console.log dans le drop handler pour diagnostic
- Suppression application/obsigate-tab (non fiable cross-browser)
Bug 1 — bookmarks/récents sans tab:
dashboard.js:135: openFile() → TabManager.openPreview()
_createCard() est partagé par DashboardRecentWidget ET DashboardBookmarkWidget
Bug 2 — drag-to-split bloqué en single-pane:
La condition 'if (dragData.paneId === paneId) return' empêchait le split
car en single-pane, le drag part du pane 0 et drop sur le pane 0.
→ Supprimé. Edge detection vérifiée avant, cross-pane vérifié après.
Même pane + pas edge → laissé au tab handler (reorder).
Cause: _setupPaneDropZone était uniquement appelé sur .pane-container (créé par split).
En mode single-pane, le DOM est .content-wrapper > .tab-bar + .content-area sans wrapper.
→ Ajout de _setupPaneDropZone sur .content-wrapper dans PaneManager.init()
→ CSS: position:relative sur .content-wrapper pour les indicateurs absolute
Changements:
- pane-manager.js: init() ajoute drop zone sur .content-wrapper (single-pane)
- style.css: position:relative sur .content-wrapper
- dragstart: effectAllowed='move' + double setData (obsigate-tab + text/plain)
- dragenter/dragover: acceptent TOUS les drags (plus de filtre types.includes cassant)
- drop: fallback text/plain si application/obsigate-tab indisponible
- console.debug au lieu de silent catch pour diagnostiquer les erreurs restantes
1. types.includes → Array.from(types).includes (DOMStringList n'a pas .includes)
2. dragleave: compteur _dragCounter empêche clear sur child enter/leave
3. drag-to-split: zone recalculée dans drop handler (plus de closure corrompue)
4. drop: stopPropagation + skip same-paneId dans container handler
5. dragenter: appel preventDefault pour accepter le drop dès l'entrée
- _setupPaneDropZone: dragover/drop sur .pane-container (fonctionne même si tab bar vide)
- Drag-to-split: déposer un onglet dans les 25% droit → split right, 25% bas → split down
- Indicateurs visuels: zone bleue semi-transparente au drop, tab-bar dashed outline
- stopPropagation dans le drop handler des tabs pour éviter double-fire
- CSS: .pane-drop-zone-indicator, .tab-bar.drop-target, position:relative sur pane-container
- ui.js: window.TabManager et window._singletonTabManager exposés dans init()
- pane-manager.js: référence corrigée vers window.TabManager (n'était pas en scope)
- PaneTabManager.close/transferTabTo: ferme automatiquement le panneau quand vide (sauf dernier)
- Déployé sur 192.168.30.101:2020, 307 tests verts