Files
ObsiGate/docs/ISSUES_TODOLIST.md
T
bruno d0e10d4cc6
CI / lint (push) Successful in 2m47s
CI / security (push) Successful in 1m34s
CI / test (push) Successful in 4m24s
CI / build (push) Successful in 1m27s
CI / e2e (push) Successful in 17m37s
docs: journal des interventions AI #189/#169 + versions d'index resynchronisées
- `docs/ISSUES_TODOLIST.md` : deux lignes ajoutées au journal (features
  #189 mobile tableur et #169 chat par fichier, avec vérifications et
  résultats CI), exigées par le DoD pour tout passage d'IA modifiant du code.
- `docs/ROADMAP.md` : colonne version de l'index corrigée — #189 → 2.56.0,
  #169 → 2.57.0 (le hook de bump ne resynchronise pas ces rangées).
2026-10-08 18:42:31 -04:00

191 KiB
Raw Blame History

IMPORTANT — Lire d'abord Ce document utilise un format tabulaire simple et rigoureux conçu pour être maintenu à la fois par un humain (éditeur texte) et par un agent IA. Les règles exactes sont définies dans la section « Comment fonctionne ce document ». La procédure de livraison obligatoire (tests, docs, commit, push, CI) est définie dans DELIVERY_WORKFLOW.md.

🐛 ObsiGate — Suivi des Bugs / TODO de Correction

Rôle du document : registre unique et vivant des bugs découverts et des corrections à apporter au projet ObsiGate (application Web + desktop Tauri). C'est l'outil de communication commun entre l'utilisateur humain et l'IA qui effectue les corrections dans le code.

  • Projet : ObsiGate — Porte d'entrée web pour vaults Obsidian
  • Stack : Python 3.11+ (backend FastAPI) · JavaScript/Vanilla (frontend) · Tauri/Rust (desktop)
  • Dernière mise à jour : 2026-09-29

⚙️ Comment fonctionne ce document (À LIRE ABSOLUMENT)

Ce document est un contrat de travail partagé. Suivez les règles ci-dessous pour qu'un humain OU un agent IA puisse le lire, le modifier et l'exploiter sans ambiguïté.

1. Une table = un état (workflow par rangée)

Chaque bug/TODO est une rangée dans la section 📋 Registre des bugs / TODOs. Une rangée ne change jamais de table au fil de sa vie ; c'est la colonne « Statut » qui encode son avancement :

Statut (colonne) Signification Qui peut le poser
🔴 ouvert Bug confirmé / tâche à faire, en attente de traitement Utilisateur ou IA
🟠 en cours Un agent IA a commencé à corriger IA uniquement
🟢 corrigé La correction est écrite et vérifiée (tests OK) IA uniquement
✅ vérifié L'utilisateur (ou les tests) a validé le correctif Utilisateur uniquement
⚪ abandonné Décision de ne pas corriger (documentée dans notes) Utilisateur ou IA

2. Cycle de vie d'un bug (workflow)

flowchart LR
    A[🔴 ouvert<br/>Découverte d'un bug] --> B[🟠 en cours<br/>IA travaille dessus]
    B --> C[🟢 corrigé<br/>Correctif écrit + tests OK]
    C --> D[✅ vérifié<br/>Re-validation par l'utilisateur]
    C --> E[⚪ abandonné<br/>Refus / hors périmètre]
    D --> F[✅ Fermé : lignée conservée en historique]
  1. Un bug est signalé → posé en 🔴 ouvert par l'utilisateur ou l'IA (à l'issue d'une investigation ou d'une session de test).
  2. L'IA prend en charge → passe le statut en 🟠 en cours avant de commencer à coder, et crée une entrée dans 🛠️ Journal des interventions AI.
  3. La correction est terminée → l'IA exécute les tests pertinents, passe le statut en 🟢 corrigé et remplit la colonne « Correctif / Commit ».
  4. Validation finale → c'est l'utilisateur qui passe en ✅ vérifié une fois le correctif contrôlé. Les rangées ✅ vérifié sont déplacées dans l'historique périodiquement.
  5. Abandon → justifié obligatoirement dans la colonne « Notes / Scope ».

⚠️ Ne JAMAIS supprimer une rangée terminée : déplacez-la dans la section 📜 Historique des bugs résolus à la place.

3. Ordre de lecture pour un agent IA

Avant de corriger quoi que ce soit, un agent IA doit :

  1. Lire ce fichier en entier (en particulier ce guide et le registre).
  2. Identifier les rangées de statut 🔴 ouvert avec un scope IA et une priorité élevée qui lui sont attribuées (colonne « Assigné »).
  3. Poser le statut 🟠 en cours sur la rangée choisie avant toute modification de code.
  4. Corriger en respectant la checklist de vérification et les conventions du projet.
  5. Revenir mettre le statut à 🟢 corrigé, documenter le correctif, et journaliser son passage.
  6. Ne pas marquer ✅ vérifié lui-même (c'est un droit utilisateur, sauf mention contraire explicite).

4. Identifiants uniques (ID)

  • Chaque bug porte un ID stable BUG-### ou TODO-### (voir le format dans le registre).
  • L'ID ne change jamais, même après résolution. Il permet de retrouver le bug dans l'historique et dans le journal d'interventions.
  • Le format du numéro est ### = 3 chiffres incrémental (001, 002, 003…).
  • Si vous (IA) créez un nouveau bug, reprenez le plus grand numéro existant + 1 dans le registre, quel que soit l'ordre des lignes.

5. Comment mettre à jour ce document (bonnes pratiques)

  • Formatage : garder les colonnes alignées par des espaces pour la lisibilité brute. Une rangée = une seule ligne de tableau Markdown.
  • Copier-coller : pour ajouter un bug, dupliquez une ligne existante puis modifiez les champs.
  • Ne pas casser : conservez le séparateur d'en-tête |---|---|…| et les champs fixes (#, Titre, Statut, Priorité, Scope, Assigné, Zone, Cmd de repro, Correctif/Commit, Notes).
  • Frontière des rôles : un agent IA ne doit jamais se passer à lui-même une rangée en ✅ vérifié ; il s'arrête à 🟢 corrigé (sauf si l'utilisateur l'y autorise explicitement).

📋 Registre des bugs / TODOs

Légende colonnes :

  • Statut : 🔴 ouvert | 🟠 en cours | 🟢 corrigé | ✅ vérifié | ⚪ abandonné
  • Priorité : P0 (critique/bloquant) · P1 (élevée) · P2 (normale) · P3 (basse/cosmétique)
  • Scope : 📱 frontend · ⚙️ backend · 🔌 api · 🧩 tests · 🖥️ desktop · 📦 build · 📄 docs · 🤖 ia · ❓ inconnu (À affiner par l'IA)
  • Assigné : IA (à traiter par un agent) · USER (à traiter par l'utilisateur) · — (non attribué)
  • Zone : chemin/nom de fichier concerné (ex. frontend/app.js, backend/search.py)
  • Cmd de repro : commande ou scénario permettant de reproduire / vérifier (vide si N/C)

Bugs ouverts (à traiter)

# Titre Statut Priorité Scope Assigné Zone (fichier) Cmd de repro Correctif / Commit Notes
BUG-001 L'ouverture des fichier PDF ne fonctionne pas et donne l'erreur Internal Server Error 🟢 corrigé P1 fichier PDF IA backend/main.py GET /api/file/{vault}/pdf/stream?path=…(pdf à nom accentué) backend/main.py : Content-Disposition encodé RFC 5987 (helper _content_disposition) 500 car nom Unicode brut dans l'en-tête → header invalide. Vérifié: stream 200 / Range 206 + test tests/test_pdf_stream.py
BUG-002 l'ouverture d'un fichier .excalidraw ne fonctionne pas et affiche toujours Loading Excalidraw… 🟢 corrigé P1 fichier .excalidraw IA frontend/excalidraw-editor.html Ouvrir un fichier .excalidraw frontend/excalidraw-editor.html : alias esm.sh supprimé (408 jotai) + React 19 cohérent + prop excalidrawAPI 2 causes: 408 esm.sh sur ?alias + prop legacy excalidrawRef inopérante en 0.18. Vérifié navigateur: Loading masqué + cycle save OK
BUG-003 mypy : 33 erreurs de typage (étape CI en mode advisory → bloquante) 🟢 corrigé P2 ⚙️ backend IA backend/{main,indexer,export,pdf_reader,bookslm_routes}.py, backend/auth/router.py, .gitea/workflows/ci.yml mypy backend/ --ignore-missing-imports Annotations de types, gardes None (auth/router), import PROVIDERS manquant (bug latent main.py:4523), PdfReader: Any ; CI mypy rendu bloquant 0 erreur après correction. PROVIDERS non importé → NameError avalé par le except (le modèle par défaut n'était jamais prépendé). Tests : 728 passed
BUG-004 Lien cassé vers CONTRIBUTING.md dans README.md 🟢 corrigé P3 📄 docs IA README.md Cliquer le lien « Contributing » README.md : lien → docs/CONTRIBUTING.md (+ DELIVERY_WORKFLOW.md) ; arbre du projet corrigé README.fr.md pointait déjà correctement vers ./docs/CONTRIBUTING.md
BUG-005 Sur mobile via Cloudflare (og.dracodev.net), le site ne charge pas complètement même après purge des caches (téléphone + Cloudflare) 🟢 corrigé P1 📱 frontend + ⚙️ backend IA frontend/sw.js, frontend/index.html, frontend/js/sync.js, backend/main.py Ouvrir og.dracodev.net sur mobile (PWA installée), purger les caches, recharger SW network-first pour le code + caches versionnés (SW_VERSION) ; précache corrigé (/static/js/…) ; migration ponctuelle localStorage ; Cache-Control: no-cache (le middleware renvoyait immutable 1 an sur /static) ; tests tests/frontend/sw.test.mjs + TestStaticCaching Cause : SW cache-first à nom fixe + en-têtes immutable non fingerprinted → ancien build servi indéfiniment (Cache Storage ≠ cache navigateur/CF). Vérifié : 754 tests backend, frontend OK
BUG-006 Clé API DeepSeek réelle commitée en clair dans .env.example 🟢 corrigé P0 📄 docs + 🔐 sécurité IA .env.example git grep "sk-" .env.example .env.example : clé remplacée par un placeholder (sk-xxx…) + modèle deepseek-chat ⚠️ Rotation de la clé requise : elle reste dans l'historique Git → révoquer/régénérer côté DeepSeek. Le fichier .env (réel) est bien gitignoré
BUG-007 Assistant IA : menus / et @ — navigation clavier ↑/↓ inopérante et filtrage des skills cassé par les accents 🟢 corrigé P1 📱 frontend IA frontend/js/bookslm.js Taper /résumé ou @café, puis ↑/↓ bookslm.js : motifs Unicode \p{L} (détection + sélection), recherche normalisée NFD (insensible aux accents), jeton _menuSeq (rendus async obsolètes), scrollIntoView de l'élément actif Le menu se fermait dès la saisie d'un accent ; la frappe rapide pouvait écraser le menu avec un résultat obsolète. Tests : tests/frontend/ai.test.mjs (+3)
BUG-008 Assistant IA : commande @ — chemins accentués + contexte ad-hoc ignoré en mode Général 🟢 corrigé P1 📱 frontend + ⚙️ backend IA frontend/js/bookslm.js, backend/bookslm_routes.py @fichier-accentué puis envoyer en mode Général Frontend : détection Unicode + repli vault=all sans vault courant. Backend : _resolve_system_prompt résout le vault optionnel dès qu'un contexte @ est présent (mode Général) Le backend ignorait extra_files/extra_directories en mode Général (vault_path is None). Tests : tests/test_bookslm.py (+2) et tests/frontend/ai.test.mjs
BUG-009 Assistant IA : liste de modèles corrompue (art ASCII) et clés i18n brutes (ai.model_search) sous OpenRouter 🟢 corrigé P1 📱 frontend IA frontend/js/ai.js, frontend/js/i18n.js, frontend/sw.js, frontend/style.css Ouvrir le menu modèle sous « openrouter » Locale non content-hashée en cache HTTP → cache:'no-store' + bump SW_VERSION v4 ; styles critiques du picker appliqués en ligne (popover absolu, liste en colonne, options display:block) ; rendu plafonné à 200 modèles + indicateur « … N autres » OpenRouter expose plusieurs centaines de modèles ; si style.css est en cache, la liste s'affichait en bloc inline (art ASCII) et les clés i18n brutes provenaient d'un fr.json obsolète. Tests : tests/frontend/ai.test.mjs (+1)
BUG-010 Assistant IA : la commande @ n'ajoute pas le contexte à la requête en mode Général 🟢 corrigé P1 📱 frontend IA frontend/js/bookslm.js Assistant en mode Général, taper @fichier, sélectionner, envoyer Capture du vault renvoyé par /api/tree-search dans la sélection @ ; _contextVault() propage ce vault aux requêtes /context et /chat ; recherches @ suivantes limitées à ce vault En mode Général, _vault est nul : la requête partait sans vault et le backend ignorait extra_files. Tests : tests/frontend/ai.test.mjs (+1)
BUG-011 Assistant IA : noms de modèles illisibles dans la liste déroulante 🟢 corrigé P2 📱 frontend IA frontend/js/ai.js, frontend/style.css Ouvrir le menu modèle dans la barre latérale de l'assistant Popover aligné à droite du déclencheur (right:0), largeur 340 px bornée à 100vw - 24px, noms sur plusieurs lignes (overflow-wrap:anywhere, police 0,78 rem) + title La liste s'ouvrait vers la droite et dépassait le bord de la sidebar ancrée à droite ; noms tronqués. Tests : tests/frontend/ai.test.mjs
BUG-012 Assistant IA : la commande @ n'affiche aucun menu de sélection en mode Général 🟢 corrigé P1 📱 frontend IA frontend/js/bookslm.js Assistant en mode Général (aucun vault), taper @ _showMentionMenu rend toujours le menu (même vide) et _activeVault() retombe sur state.selectedContextVault puis le premier state.allVaults, avant vault=all pour les requêtes Sans vault résolu, l'ancien code masquait le menu et le backend ignorait le contexte. Tests : tests/frontend/ai.test.mjs (+1)
BUG-013 Assistant IA : les liens fichiers/répertoires des réponses affichent « Aucun vault actif pour ouvrir ce lien » 🟢 corrigé P1 📱 frontend IA frontend/js/bookslm.js En mode Général, cliquer un lien de fichier/dossier dans une réponse _openFileLink() et _revealPath() utilisent _activeVault() (vault de contexte → vault sélectionné → premier vault) au lieu de _resolveVault() seul _resolveVault() est nul sans document ouvert ; les liens échouaient en mode Général. Tests : tests/frontend/ai.test.mjs (+1)
BUG-014 Assistant IA : les flèches ↑/↓ ne naviguent pas dans les menus / et @ 🟢 corrigé P1 📱 frontend IA frontend/js/bookslm.js Ouvrir /, puis ↑/↓ (y compris après un clic hors zone de texte) Navigation gérée au niveau du panneau en phase de capture (panel.addEventListener('keydown', ..., true)), donc indépendante du focus Le keydown n'était écouté que sur la zone de texte ; dès que le focus changeait, les flèches étaient ignorées. Tests : tests/frontend/ai.test.mjs (+1)
BUG-015 Assistant IA : la ligne sélectionnée des menus / et @ est invisible au clavier 🟢 corrigé P2 📱 frontend IA frontend/style.css Ouvrir / ou @, naviguer avec ↑/↓ État .active en --bg-hover + barre d'accent à gauche (inset 3px 0 0 var(--accent)) au lieu de --surface2 --surface2 est identique à --bg-primary (fond du menu) en thème sombre : la sélection ne se voyait pas. Idem pour la liste de modèles
BUG-016 Mobile : le bouton « mode lecture » flottant recouvre le bouton d'envoi de l'assistant IA 🟢 corrigé P2 📱 frontend IA frontend/js/mobile-editor.js, frontend/style.css Mobile, sans fichier ouvert : ouvrir l'assistant puis constater que 📖 masque ✈️ updateReadingButtonVisibility() n'affiche #me-reading-btn que si state.currentPath est défini et que le panneau assistant est fermé ; resynchronisé à chaque mutation de #content-area et sur bookslm:opened/bookslm:closed ; règle .me-reading-btn[hidden]{display:none} Le bouton flottant (z-index 890) passait au-dessus du panneau plein écran mobile (z-index 100). Tests : tests/frontend/mobile-editor.test.mjs (+2)
BUG-017 Accueil mobile : les tuiles de « Favoris » et « Récents » ne s'affichent pas correctement en largeur (l'onglet « Partagés » s'affiche correctement) 🟢 corrigé P2 📱 frontend IA frontend/style.css Mobile : page d'accueil → onglets Favoris / Récents frontend/style.css : min-width:0 + overflow:hidden sur .dashboard-card (et header/footer) → la grille 1 colonne ne déborde plus Cause : titre/chemin nowrap forçaient la largeur mini de la carte
BUG-018 Éditeur mobile : mise en page inadaptée — boutons « Annuler » / « Sauvegarder » inaccessibles et barre d'outils IA non optimisée 🟢 corrigé P1 📱 frontend IA frontend/style.css Mobile : ouvrir un fichier → barre d'outils de l'éditeur puis panneau assistant frontend/style.css : en-tête mobile compact (marque/séparateur/espaceur/présence masqués, champs compressibles, libellé « Saved » masqué) + cibles tactiles .ai-toolbar-btn agrandies Les 3 boutons d'action restent accessibles ; voir aussi #83 pour le ruban
BUG-019 Éditeur mobile : la barre d'outils du bas n'est pas toujours visible au-dessus du clavier quand le curseur n'est pas en bas de l'éditeur 🟢 corrigé P1 📱 frontend IA frontend/js/mobile-editor.js, frontend/style.css Mobile : ouvrir un fichier, placer le curseur au milieu du document, afficher le clavier initKeyboardAnchor() utilise window.visualViewport (--kb-offset) pour ancrer le ruban au-dessus du clavier ; padding-bottom sur .cm-scroller Fonctionne quel que soit le défilement / la position du curseur
BUG-020 Éditeur mobile : deux barres de défilement verticales à droite de la page en mode édition 🟢 corrigé P3 📱 frontend IA frontend/style.css Mobile : ouvrir l'éditeur et observer le bord droit frontend/style.css : .editor-body en overflow:hidden + colonne flex ; seul .cm-scroller défile Une seule barre de défilement
BUG-021 [🔴 CRITIQUE] XSS stocké via le rendu markdown (escape=False) 🟢 corrigé P0 🔐 sécurité IA backend/services/sanitizer.py, backend/main.py Ouvrir une note contenant <img src=x onerror=alert(1)> backend/services/sanitizer.py (whitelist stdlib) appliqué après _add_heading_ids dans _render_markdown ; tests tests/test_security_hardening.py HTML brut d'un vault injecté dans le DOM → JS dans le navigateur de chaque utilisateur. DOMPurify client non ajouté (défense serveur suffisante)
BUG-022 [🔴 CRITIQUE] XSS stocké sur la page publique /s/{token} (title + frontmatter non échappés) 🟢 corrigé P0 🔐 sécurité IA backend/main.py Partager une note dont le frontmatter contient un title avec <script> backend/main.py : html.escape() sur title/frontmatter + JSON échappé (\u003c) pour le bloc <script> et a.download a.download="{title}.md" se trouvait dans un bloc script → breakout JS. Test d'intégration dans tests/test_security_hardening.py
BUG-023 [🔴 CRITIQUE] Brute-force MFA sans rate-limit ni verrouillage 🟢 corrigé P0 🔐 sécurité IA backend/auth/router.py POST /api/auth/mfa/totp/verify en boucle avec des codes aléatoires _enforce_mfa_rate_limit + _record_mfa_failure sur totp/verify, recovery, webauthn/verify (IP + compte + lockout) TOTP 6 chiffres brute-forceable ; le login était protégé, pas le second facteur
BUG-024 [🔴 CRITIQUE] Traversal inter-vaults : startswith() sans séparateur dans resolve_safe_path 🟢 corrigé P0 🔐 sécurité IA backend/services/paths.py Avec les vaults vault et vault-evil, lire un fichier de vault-evil via vault _is_within() par segments (relative_to + repli casse-insensible) ; tests tests/test_security_hardening.py::TestPathIsolation Rupture d'isolation entre vaults (lecture / écriture / suppression)
BUG-025 [🟡 IMPORTANT] ReDoS : regex utilisateur appliquée au contenu en masse 🟢 corrigé P1 🔐 sécurité IA backend/services/regex_safety.py, backend/search.py, backend/services/mutations.py Recherche avancée avec ^(a+)+$ sur un gros document Validation de pattern (longueur ≤500, rejet quantificateurs imbriqués/backrefs), contenu tronqué (200k) et matchs plafonnés ; 400 sur pattern refusé Le dry-run replace_in_files déclenchait la charge ; la validation est faite avant compilation
BUG-026 [🟡 IMPORTANT] Webhooks : SSRF (URL non validée) + secret en clair 🟢 corrigé P1 🔐 sécurité IA backend/webhooks.py, data/webhook_secrets.json Configurer une URL vers http://169.254.169.254 puis déclencher le webhook Validation d'URL (HTTPS par défaut, IP privées/boucle bloquées), résolution DNS au dispatch, redirections non suivies ; secret dans webhook_secrets.json (0600) ou env OBSIGATE_WEBHOOK_SECRET_<ID> get_webhooks() n'expose plus le secret (has_secret) ; options OBSIGATE_WEBHOOK_ALLOW_HTTP/_ALLOW_PRIVATE
BUG-027 [🟡 IMPORTANT] Sessions : refresh non rotatif, access token non révoqué au logout 🟢 corrigé P1 🔐 sécurité IA backend/auth/jwt_handler.py, backend/auth/router.py, backend/auth/middleware.py Logout puis réutilisation de l'ancien access token Rotation du refresh token (create_refresh_token(remember=…)) + révocation de l'ancien JTI ; access token révoqué au logout et vérifié dans le middleware JTI persistés dans data/revoked_tokens.json
BUG-028 [🟡 IMPORTANT] Politique de mot de passe incohérente + sessions non invalidées au changement 🟢 corrigé P1 🔐 sécurité IA backend/auth/password.py, backend/auth/router.py, backend/auth/user_store.py, backend/auth/middleware.py Créer un utilisateur avec password:"" (admin) ; changer son mot de passe puis réutiliser l'ancien token validate_password_strength (8–128) sur création/modif admin/changement ; password_changed_at invalide les jetons émis avant le changement ; change-password réémet une paire Tests TestPasswordPolicy + TestTokenInvalidation
BUG-029 [🟡 IMPORTANT] Race read-modify-write sur users.json (perte de mises à jour) 🟢 corrigé P1 ⚙️ backend IA backend/auth/user_store.py Activer MFA et changer son mot de passe en parallèle threading.RLock (_users_lock) autour de create_user, update_user, delete_user, record_login_failure RLock réentrant car record_login_failure appelle update_user
BUG-030 [🟡 IMPORTANT] Adresse IP jamais consignée dans les audits (_request_ip toujours « unknown ») 🟢 corrigé P1 ⚙️ backend IA backend/services/net.py, backend/auth/middleware.py Lire le journal d'audit après un save/delete get_client_ip() (request.client.host, X-Forwarded-For si OBSIGATE_TRUST_PROXY=true) injecté dans current_user["_request_ip"] Test TestClientIp
BUG-031 [🟡 IMPORTANT] Rate-limiter en mémoire, mono-process, budget exclusivement IP 🟢 corrigé P1 ⚙️ backend IA backend/ratelimit.py, backend/auth/router.py Lancer plusieurs workers et observer le partage du compteur Budget par compte (is_account_rate_limited/record_account_*) en plus de l'IP, intégré login + MFA ; limite mono-process documentée Rotation d'IP neutralisée ; stockage partagé (Redis) hors périmètre
BUG-032 [🟡 IMPORTANT] Indexation : symlinks suivis (contenu hors vault indexé) + scan initial coûteux 🟢 corrigé P1 ⚙️ backend IA backend/indexer.py Placer un symlink dans le vault vers un dossier externe puis relancer l'index _scan_vault réécrit avec os.walk(followlinks=False) + refus des symlinks sortant de la racine ; test TestSymlinkIndexing Scan incrémental/index persistant : voir #86 (phase 3)
BUG-033 [🟡 IMPORTANT] Recherche classique et tool IA search_fulltext en O(N) sans inverted index 🟢 corrigé P1 ⚙️ backend IA backend/search.py, backend/tools/service.py GET /api/search sur un vault de 50 000 fichiers search() récupère les candidats via l'inverted index (intersection des termes + expansion de préfixes), repli sur le scan pendant la construction search_fulltext en bénéficie automatiquement
BUG-034 [🟡 IMPORTANT] CSP affaiblie ('unsafe-inline' + CDN distants) et token d'accès en sessionStorage 🟢 corrigé P1 🔐 sécurité IA backend/main.py, frontend/js/auth.js, frontend/js/admin.js, frontend/js/sync.js Inspecter les en-têtes CSP ; lire sessionStorage en console Token en mémoire + cookie HttpOnly (plus de sessionStorage) ; CSP durcie (object-src 'none', base-uri, form-action, frame-ancestors). Reste : migration nonce 'unsafe-inline' conservé tant que les gestionnaires inline n'ont pas été convertis (résidu documenté)
BUG-035 [🔵 MINEUR] secret_redactor : faux positifs sur les hashs hex (git, SHA) 🟢 corrigé P2 ⚙️ backend IA backend/secret_redactor.py Lire une note contenant un commit git (40 caractères hexadécimaux) Masquage hex conditionné au contexte (_redact_bare_hex_secrets) : secret exigé dans les 60 caractères précédents, exemption explicite pour commit/sha*/hash/checksum/git/etag. Tests : tests/test_api_main.py::TestSecretRedactor (+4) Contenus mutilés dans les lectures et réponses IA
BUG-036 [🔵 MINEUR] Collab WebSocket : token en query string 🟢 corrigé P2 ⚙️ backend IA backend/collab.py Observer l'URL du websocket dans le trafic réseau authenticate_websocket ne lit plus ?token= : cookie HttpOnly access_token uniquement ; rejet des trames > MAX_MESSAGE_CHARS (16 Mio) avant analyse. Tests : tests/test_collab.py (+3) Jeton visible dans les logs/proxys
BUG-037 [🔵 MINEUR] Compte « anonymous » administrateur si auth désactivée 🟢 corrigé P2 🔐 sécurité IA backend/auth/middleware.py, backend/main.py Démarrer avec l'authentification désactivée _guard_insecure_auth() : avertissement explicite + refus de démarrage sur bind non-loopback sans OBSIGATE_ALLOW_INSECURE=true. Tests : tests/test_auth.py::TestInsecureAuthGuard (+6) Comportement par conception mais risqué si mal configuré
BUG-038 [🔵 MINEUR] Argon2 à 64 MB par vérification : risque d'épuisement mémoire 🟢 corrigé P2 🔐 sécurité IA backend/auth/password.py Lancer de nombreux POST /api/auth/login simultanés Recalibré à m=19456 Kio (19 Mio), t=2, p=1 (OWASP) ; anciens hachages valides + rehash auto. Test : tests/test_auth.py::TestPasswordHashing::test_argon2_memory_recalibrated DoS mémoire possible sur les petites instances
BUG-039 [🔵 MINEUR] Enumération de comptes : 429 (verrouillé) vs 401 (inconnu) 🟢 corrigé P3 🔐 sécurité IA backend/auth/router.py Tenter un login sur un compte verrouillé puis un nom inconnu Login uniforme : inconnu / désactivé / verrouillé / rate-limit par compte → 401 Identifiants invalides + hachage factice (timing équivalent) ; seul le rate-limit IP reste 429. Tests : tests/test_auth_api.py (+3) Le statut HTTP distinguait l'existence d'un compte
BUG-040 [🔵 MINEUR] Extraction PDF intégrale (100 ko) au scan de démarrage 🟢 corrigé P2 ⚙️ backend IA backend/indexer.py, backend/main.py Démarrer sur un vault contenant de nombreux PDF _scan_vault ne lit que les métadonnées ; enrich_pdf_texts() extrait le texte après l'index (démarrage) et après chaque réindexation. Tests : tests/test_pdf.py (+3) Ralentit fortement le démarrage et le rebuild d'index
BUG-041 [🟡 IMPORTANT] Assistant IA : échec sur un répertoire vide (« Aucun fichier markdown trouvé dans ce dossier ») au lieu de répondre 🟢 corrigé P1 📱 frontend + ⚙️ backend IA backend/bookslm_routes.py, backend/bookslm.py, frontend/js/bookslm.js Ouvrir l'assistant sur un dossier vide puis envoyer une question _resolve_system_prompt dégrade vers le prompt Général + bloc « Dossier vide » (plus de 404) ; contexte applicatif app_context enrichi (documents ouverts, répertoire, recherche, fichiers récents) Le 404 bloquait toute la requête. Feature #88, fiche docs/features/ai-app-context.md. Tests : tests/test_bookslm.py (+3), tests/frontend/ai.test.mjs
BUG-042 [🟡 IMPORTANT] Assistant IA : liens de fichiers non fiables (« File not found: ») — pas de règle déterministe nom / dossier / chemin 🟢 corrigé P1 📱 frontend IA frontend/js/bookslm.js Cliquer les liens de fichiers/dossiers dans une réponse de l'assistant (noms avec espaces et/ou accents, chemin préfixé par le nom du vault) _classifyPath distingue name (copie presse-papiers) / dir (révélation arborescence) / file (ouverture) ; _activatePath() résout le chemin contre l'index du vault (exact → suffixe → basename unique) avant d'agir ; espaces + accents pris en charge (classes Unicode \p{L}\p{N}\p{M}, comparaison normalisée NFC, markdown <…>/%20, code inline, mentions brutes confirmées par l'index) ; _splitVaultPrefix retire un préfixe Vault/… et ouvre dans ce vault (_fetchPathsForVault) Les liens morts ouvraient un fichier inexistant. Feature #88. Tests : tests/frontend/ai.test.mjs (+11)
BUG-043 [🟡 IMPORTANT] Assistant IA : la liste des fournisseurs de la barre latérale ne suit pas les ajouts/retraits de clés API dans la configuration du projet 🟢 corrigé P1 📱 frontend IA frontend/js/ai.js, frontend/js/bookslm.js, frontend/js/config.js Ajouter (ou supprimer) une clé de fournisseur AI dans la configuration puis observer le menu Fournisseur de l'assistant sans recharger la page Le picker lit /api/ai/status une seule fois, à sa construction, et le panneau de l'assistant est un singleton monté pour toute la session → liste figée. Nouveau refreshAIPickers() (exporté par ai.js) qui reconstruit chaque picker monté dans son emplacement .ai-picker-slot (conservé même sans fournisseur configuré, donc un premier fournisseur s'y monte aussi) ; appelé après saveAIKeys() et deleteAIKey() (config.js) ; une sélection dont le fournisseur n'est plus configuré est purgée de obsigate_ai_picker (retour au défaut + modèle effacé au lieu d'un nom fantôme) Il fallait recharger la page pour voir un nouveau fournisseur (ou en voir disparaître un). Feature #82. Tests : tests/frontend/ai.test.mjs (+4)
BUG-044 [🟡 IMPORTANT] Capacités des modèles IA erronées : aucun modèle Mistral n'est détecté « Vision capable » (et des modèles texte sont annoncés « Embeddings ») 🟢 corrigé P1 ⚙️ backend + 🔌 api IA backend/model_capabilities.py, backend/provider_capabilities.py, backend/main.py, backend/ai_routes.py curl -H "Authorization: Bearer $MISTRAL_API_KEY" https://api.mistral.ai/v1/models puis GET /api/ai/model-capabilities?provider=mistral&model=mistral-small-latest Nouveau calque déclaratif (backend/provider_capabilities.py) : les capacités publiées par le fournisseur (Mistral capabilities, OpenRouter architecture) sont lues et mises en cache par GET /api/config/ai-models ; elles priment sur la table statique, qui ne comble plus que les drapeaux non déclarés (et sert de repli hors ligne / sans clé). Table statique corrigée : familles vision Mistral (ministral, magistral, mistral-small, mistral-medium, mistral-vibe-cli, labs-leanstral), mistral-ocr = vision sans chat, défaut fournisseur Mistral sans embeddings. Tests : tests/test_provider_capabilities.py, tests/test_model_capabilities.py::TestMistralFamilies, tests/test_ai_models.py::TestDeclaredCapabilities Le panneau de modèle par défaut et la bulle ⓘ n'affichaient aucun Mistral vision alors que l'API en déclare 28 ; seul le motif pixtral (retiré de l'API) matchait. Effet secondaire corrigé : mistral-large-latest / codestral-latest étaient annoncés « Embeddings » (défaut fournisseur). La porte vision de l'assistant (bookslm_routes.py) accepte désormais les images avec un modèle Mistral vision
BUG-045 [🟡 IMPORTANT] Éditeur « Editer » : deux barres de défilement superposées sur les documents longs 🟢 corrigé P1 📱 frontend Éditeur frontend/style.css Ouvrir un fichier long (ex. IT/Docker Guide.md), cliquer Editer, mesurer #editor-body et .cm-scroller .editor-body-cm gardait overflow:auto et un .cm-editor{height:100%} sous la rangée barre d'outils IA → le corps (toolbar+éditeur) ET le scroller CodeMirror débordaient simultanément. L'override global legacy .cm-scroller{min-height:100%;overflow-y:auto!important} aggravait. Passé en flex column : corps overflow:hidden, toolbar flex:0 0 auto, .cm-editor flex:1 1 auto; height:auto, seul le scroller défile ; override legacy retiré. Test : tests/frontend/editor-inline.test.mjs (+1) ; vérifié Playwright sur l'instance de test (un seul conteneur scrollable).
BUG-046 [🔴 BLOQUANT] Assistant IA : « Échec de l'action : [object Object] » à l'application d'un ajout de texte au document courant 🟢 corrigé P0 📱 frontend + ⚙️ backend IA frontend/js/bookslm.js, backend/bookslm.py, backend/bookslm_routes.py Mode agent : demander d'ajouter du texte au document ouvert puis cliquer « Appliquer » Trois causes : (1) continuation de confirmation avec payload:null → second « Appliquer » sans message → 422 ; (2) new Error(detail) sur un detail tableau d'objets FastAPI → « [object Object] » ; (3) prompt documents/directory sans nom de vault → le modèle inventait "vault":"test" → échec silencieux de l'outil. Nouveau _responseError() (aplatit tableau/objet), payload porté à la continuation, bloc « Ces documents appartiennent au vault « X » » + consigne outils d'écriture (build_system_prompt(vault_name=...)). SW_VERSION v20. Tests : tests/test_bookslm.py::test_vault_name_guidance, tests/frontend/ai.test.mjs (+2).
BUG-047 [🔴 BLOQUANT] La version affichée par l'application ne suit pas les livraisons : 66 commits livrés depuis v2.2.1 et l'UI/API restent bloquées sur 2.2.1 (et les numéros codés en dur divergent : package.json 1.0.0, desktop Tauri 2.0.0, Dockerfile 2.2.1, README 1.7.0) 🟢 corrigé P0 ⚙️ build + 📄 docs IA VERSION (nouveau), scripts/bump_version.py (nouveau), .githooks/prepare-commit-msg + .githooks/post-commit (nouveaux), scripts/install-hooks.sh (nouveau), backend/version.py, Dockerfile, docker-compose.yml, build.sh, .gitea/workflows/ci.yml, desktop/build.rs, tests/test_version.py (nouveau) git tag -l | tail -1 puis python scripts/bump_version.py --print-version ; curl -s http://localhost:2020/api/health | jq .version Le numéro provenait du dernier tag git et aucun tag n'était créé aux livraisons (bump_version.sh jamais appelé) → version figée, plus quatre numéros codés en dur ailleurs. Corrigé : VERSION (racine) = source unique de vérité, incrémentée automatiquement à chaque commit par le hook versionné prepare-commit-msg (SemVer : !:/BREAKING CHANGE → MAJEUR, feat → MINEUR, sinon CORRECTIF), tag vX.Y.Z créé par post-commit et publié au push (push.followTags) ; bump_version.py resynchronise package.json, desktop Tauri, ROADMAP, READMEs et fait la rotation du CHANGELOG dans le même commit ; backend, image Docker (COPY VERSION) et desktop lisent ce fichier. Contournement ponctuel : SKIP_VERSION_BUMP=1. Garde-fou : tests/test_version.py::TestRepoVersionAlignment échoue dès qu'un dérivé diverge de VERSION. Vérifié : pytest complet vert, ruff/mypy 0, /api/health → 2.3.0 sur l'instance de test.
BUG-048 [🟡 IMPORTANT] Assistant IA : les entrées « Contextes » et « Skills » du menu « + » n'ouvraient pas leur menu (@ / /) 🟢 corrigé P2 📱 frontend IA frontend/js/bookslm.js Menu « + » de l'assistant → cliquer « Contextes » ou « Skills » e.stopPropagation() sur les entrées du panneau .bookslm-ext-menu (le clic remontait au gestionnaire du panneau qui annulait le rendu asynchrone) Journal 2026-09-16. Tests : tests/frontend/ai.test.mjs (+3)
BUG-049 [🔵 MINEUR] Assistant IA : icône du bouton « + » invisible (largeur SVG nulle) 🟢 corrigé P3 📱 frontend IA frontend/style.css Ouvrir l'assistant et observer le bouton « + » Sélecteur porté à .bookslm-input-area button.bookslm-btn-plus (la règle générique padding: 8px 16px sur un bouton 32 px annulait la largeur de contenu) Vérifié navigateur : SVG 0 px → 18 px. Journal 2026-09-16
BUG-050 [🟡 IMPORTANT] Assistant IA : échec de la création d'un sous-dossier contenant un fichier (appels d'outils parallèles + confirmation) 🟢 corrigé P1 ⚙️ backend + 🤖 ia IA backend/agent/loop.py, backend/services/mutations.py, backend/tools/service.py, backend/bookslm.py Mode Agent : « crée le dossier X et un fichier Y dedans » puis Appliquer backend/agent/loop.py : résultats « deferred » (_deferred_tool_message) pour les tool_calls non atteints lors d'une pause de confirmation ; backend/services/mutations.py : create_directory(..., exist_ok=True) ; backend/tools/service.py + backend/bookslm.py : consignes create_file (parents auto-créés, chemin imbriqué unique) Cause : le message assistant listait plusieurs tool_calls mais la pause n'ajoutait le résultat que du seul appel confirmé → conversation invalide (tool_call_id sans réponse) au resume. Tests : tests/test_agent_loop.py (+1), tests/test_tools_mutations.py (+1), tests/test_api_main.py (+1)
BUG-051 [🟡 IMPORTANT] Assistant IA : la recherche web répond toujours « je ne peux pas accéder à internet » (mode agent ou non) 🟢 corrigé P1 ⚙️ backend + 🤖 ia IA backend/tools/web.py, tests/test_web_tools.py Assistant : « fais une recherche sur l'horaire du Canadien de Montréal 2026-2027 » backend/tools/web.py : chaîne de repli sans clé — SearXNG puis DuckDuckGo (HTML sans JS) puis Bing (HTML), premier fournisseur non vide retenu (provider), replis désactivables via OBSIGATE_WEB_FALLBACK=0 Cause : l'instance SearXNG par défaut (search.dracodev.net) remonte 0 résultat (moteurs amont suspendus/CAPTCHA) → le modèle en déduisait une absence d'accès réseau. Tests : tests/test_web_tools.py (+4)
BUG-052 [🟡 IMPORTANT] Assistant IA : recherche web sans réponse finale (10 étapes + sources affichées, aucun texte dans la conversation) 🟢 corrigé P1 ⚙️ backend + 🤖 ia IA backend/agent/loop.py, tests/test_agent_loop.py Assistant (mode agent) : recherche web qui enchaîne 10 étapes puis n'affiche aucune réponse backend/agent/loop.py : _finalize_answer — dernier appel LLM sans outil (instruction de synthèse) quand le budget d'itérations/quota est épuisé, repli déterministe _fallback_summary (liste des sources), résultats deferred pour les appels non atteints du lot en quota Cause : content="" renvoyé sur STOP_MAX_ITERATIONS/STOP_QUOTA_EXCEEDED alors que le modèle appelait encore des outils. Tests : tests/test_agent_loop.py (+2)
BUG-053 [🟡 IMPORTANT] Assistant IA (mode agent) : le fichier demandé n'est pas créé — le modèle émet un bloc texte obsigate-action au lieu d'appeler l'outil create_file 🟢 corrigé P1 ⚙️ backend + 🤖 ia IA backend/bookslm.py, backend/bookslm_routes.py, tests/test_bookslm.py Mode agent, contexte Général (ou dossier vide) : « créer le fichier TestVault/sport/… avec le tableau des 84 matchs » → réponse avec un bloc obsigate-action tronqué, aucun fichier backend/bookslm.py : protocole d'action scindé — GENERAL_ACTION_TOOL_PROTOCOL (outils natifs, interdiction des blocs obsigate-action) utilisé quand agent=True, protocole texte conservé pour le chat classique ; backend/bookslm_routes.py : _resolve_system_prompt(..., agent=True) depuis l'endpoint agent + règle « Mode agent » pour les prompts dossier/documents, max_tokens agent 4096 → 8192 (contenu de fichier complet) Cause : le prompt Général enseignait encore le protocole texte alors que l'agent dispose du function calling. Tests : tests/test_bookslm.py (+3)
BUG-054 [🟡 IMPORTANT] Éditeur « Editer » : le bouton Sauvegarder reste bloqué sur le spinner de chargement (retour au crochet uniquement après un refresh complet) 🟢 corrigé P1 📱 frontend IA frontend/js/utils.js Ouvrir un fichier → Editer → cliquer Sauvegarder (ou Ctrl+S) ; rouvrir l'éditeur : le bouton reste un spinner désactivé Nouveau helper resetSaveButton() (crochet &#10003; + disabled=false + styles en ligne nettoyés) appelé à l'ouverture (openEditor), à la fermeture (closeEditor) et en cas d'échec (saveFile). Tests : tests/frontend/editor-inline.test.mjs (+4) Le nœud #editor-save est partagé entre sessions : l'état « spinner + désactivé » posé par une sauvegarde manuelle n'était jamais remis à zéro (succès → fermeture puis réouverture, Forge, ou échec réseau dans le catch). Seul un rechargement de index.html restaurait le crochet
BUG-055 [🟡 IMPORTANT] Éditeur Forge : l'autocomplétion (Tab) ajoute des espaces parasites, l'effacement détruit le mot complété et la complétion fantôme est illisible 🟢 corrigé P1 📱 frontend + ⚙️ backend IA frontend/editor-poc.html, frontend/js/autocomplete.js, backend/ai.py, .gitea/workflows/ci.yml, tests/frontend/forge-completion.test.mjs (nouveau) Forge : taper un mot, puis Tab pour compléter ; un espace (voire deux) s'insère avant le mot complété, et le retour arrière efface l'ajout. La prédiction IA s'affichait décalée (texte miroir du document entier) Cause : trois gestionnaires keydown Tab indépendants s'exécutaient tous — l'indentation (insertAtCursor(' ')) s'ajoutait à la complétion de mot et à l'acceptation du ghost. Correctif : gestion unifiée de Tab (liste ouverte > ghost > mot du document > indentation, une seule action), helpers purs partagés (getWordFragment, findWordCompletions, normalizeGhost, chooseTabAction) dans autocomplete.js, liste déroulante si plusieurs candidats, dropdown positionné au curseur, ghost positionné au curseur (fini le miroir du document, nettoyé au déplacement/scroll), complétion de mot sans espace garanti (normalizeGhost tronque au premier espace) et prompt /api/ai/inline-complete simplifié. Tests : tests/frontend/forge-completion.test.mjs (28). Cause du bug : l'indentation Tab n'était pas conditionnée à l'absence de suggestion. Le ghost re-rendait tout le texte transparent + prédiction, d'où l'impression d'espaces et les erreurs d'effacement
BUG-056 [🟡 IMPORTANT] Éditeur Forge en plein écran : l'Assistant IA s'ouvre en arrière-plan et reste invisible 🟢 corrigé P1 📱 frontend IA frontend/editor-poc.html, frontend/js/sync.js, tests/frontend/forge-completion.test.mjs, tests/frontend/editor-inline.test.mjs Forge : passer en plein écran puis cliquer le bouton « Assistant IA » (ou Ctrl+J) — le panneau s'ouvre dans le document parent, masqué par l'iframe plein écran Sortie du plein écran avant d'ouvrir le panneau, des deux côtés : côté iframe (openAssistant → document.exitFullscreen() puis postMessage à la résolution) et côté parent (sync.js sur forge-open-ai → document.exitFullscreen() puis openForCurrentContext()), car le plein écran peut être détenu par le document parent et non par l'iframe (dans ce cas document.fullscreenElement est nul dans l'iframe et sa sortie échoue). Tests : forge-completion.test.mjs (+1), editor-inline.test.mjs (+1) Le panneau assistant est monté dans document.body du parent : l'API Fullscreen ne rend que l'élément plein écran et ses descendants, donc il ne peut pas s'afficher au-dessus de l'iframe Forge en plein écran. La sortie côté iframe seule ne suffisait pas quand le parent détient le plein écran
BUG-057 [🟡 IMPORTANT] Assistant IA : le bouton « Ajouter » est inopérant dans l'éditeur Forge (fonctionne seulement dans « Editer ») 🟢 corrigé P1 📱 frontend IA frontend/js/bookslm.js, frontend/editor-poc.html Ouvrir un document dans Forge, demander une réponse à l'assistant puis cliquer « Ajouter » _insertIntoEditor() cible Forge (#forge-iframe) : postMessage({ type: 'parent-insert', text }) ; editor-poc.html insère au curseur (insertAtCursor) et marque le tampon modifié. Repli textarea inclus. Tests : tests/frontend/ai.test.mjs (+3), tests/frontend/editor-inline.test.mjs (+1) state.editorView (CodeMirror) est nul en Forge : le clic affichait « Aucun document ouvert dans l'éditeur »
BUG-058 [🔵 MINEUR] Éditeur « Editer » : la barre de numérotation de ligne ne suit pas la couleur du thème (gutter clair #f5f5f5 en thème sombre) 🟢 corrigé P2 📱 frontend IA frontend/style.css Ouvrir un document → Editer en thème sombre : la colonne des numéros de ligne reste gris clair alors que le fond de l'éditeur est sombre Thème du gutter CodeMirror via les variables CSS (color-mix(var(--text-primary) …) pour le fond, --text-secondary pour les numéros, --border pour la séparation, --text-primary pour la ligne active) au lieu des valeurs codées en dur de CodeMirror ; test de non-régression dans tests/frontend/editor-inline.test.mjs. Vérifié Playwright (instance de test) : sombre color(srgb 0.90 0.93 0.95 / 0.05) + bordure #21262d, clair color(srgb 0.12 0.14 0.16 / 0.05) + bordure #d0d7de CodeMirror applique background:#f5f5f5 par défaut, indépendamment du thème ObsiGate ; en mode sombre le fond de l'éditeur suit --bg-secondary mais pas le gutter
BUG-060 [🟡 IMPORTANT] Viewer PDF : l'affichage des pages ne fonctionne pas — seule la barre d'outils « PDF — N pages » s'affiche, le contenu reste vide 🟢 corrigé P1 📱 frontend IA frontend/js/viewer.js, tests/frontend/pdf-viewer.test.mjs (nouveau), tests/e2e/pdf-viewer.spec.js (nouveau) Cliquer un fichier .pdf dans l'arborescence frontend/js/viewer.js : le rendu PDF passe de <embed type="application/pdf"> à <iframe> (autorisée par frame-src 'self', le stream étant same-origin). Tests : tests/frontend/pdf-viewer.test.mjs (+6) et tests/e2e/pdf-viewer.spec.js (fixture test_vault/sample-pdf.pdf) Cause : la CSP durcie en BUG-034 pose object-src 'none', directive qui gouverne <embed>/<object> → le lecteur PDF natif était bloqué (barre d'outils rendue, corps vide). Le test E2E échoue bien avec l'ancien <embed>. object-src 'none' conservé (le correctif ne désarme pas la CSP)
BUG-061 [🟡 IMPORTANT] Assistant IA : le bouton « Plein écran » n'agrandit plus le panneau 🟢 corrigé P2 📱 frontend IA frontend/style.css, tests/frontend/ai.test.mjs Ouvrir l'assistant, redimensionner le panneau, puis cliquer « Plein écran » La largeur du panneau est écrite en ligne par la poignée de redimensionnement / la largeur persistée (localStorage) ; l'inline l'emportait sur .bookslm-panel.fullscreen { width: 100vw }. Ajout de !important sur la règle plein écran. Tests : ai.test.mjs (+1 : classe basculée + règle CSS). Vérifié Playwright : 640 px → 1400 px (viewport)
BUG-062 [🟡 IMPORTANT] Viewer PDF : le document ne prend pas toute la largeur quand la navigation est masquée 🟢 corrigé P2 📱 frontend IA frontend/style.css, tests/frontend/pdf-viewer.test.mjs, tests/e2e/pdf-viewer.spec.js Ouvrir un PDF puis masquer la barre de navigation gauche La règle .sidebar.hidden ~ .content-wrapper .content-area { max-width: 1200px } (colonne de lecture centrée) s'appliquait aussi aux viewers plein cadre. Ajout de .content-area:has(.pdf-viewer-container) (et .image-viewer-container) avec max-width: none; margin: 0. Test E2E : max-width calculé = none, conteneur = largeur du contenu
BUG-063 [🟡 IMPORTANT] Viewer PDF : la table des matières s'affiche mais ne navigue pas 🟢 corrigé P1 📱 frontend IA frontend/js/viewer.js, tests/frontend/pdf-viewer.test.mjs, tests/e2e/pdf-viewer.spec.js (fixture test_vault/sample-pdf-toc.pdf) Ouvrir un PDF avec signets, puis cliquer une entrée de la TOC Deux causes : (1) contentWindow.location.hash='page=N' n'atteint pas le document (lecteur PDF natif dans une fenêtre about:blank) ; (2) un simple changement de fragment sur iframe.src est une navigation same-document ignorée par le lecteur natif. navigatePdfToPage() (liens data-page + listeners, plus d'onclick inline) recharge réellement l'iframe via un paramètre de query qui change (&_pdfpage=<ts>#page=N). Test E2E : src finit par &_pdfpage=<n>#page=3. Vérifié en Chrome headful : page 1 → page 8 → page 1 (captures identiques au retour) Le fragment seul ne suffisait pas : Chrome applique #page=N au chargement, pas lors d'un changement de fragment
BUG-064 [🟡 IMPORTANT] Éditeur Excalidraw : le diagramme ne s'affiche jamais (canvas vide), pour tout fichier .excalidraw / .excalidraw.md 🟢 corrigé P1 📱 frontend + ⚙️ backend IA frontend/excalidraw-editor.html, backend/main.py, tests/frontend/excalidraw-viewer.test.mjs, tests/test_security_hardening.py, tests/e2e/excalidraw.spec.js, test_vault/diagram-app-export.excalidraw Ouvrir un .excalidraw (ou .excalidraw.md) dans ObsiGate Deux causes : (1) la feuille de style d'Excalidraw n'était jamais chargée → éditeur non stylisé + .excalidraw sans hauteur fixe → boucle de resize jusqu'au plafond 2^25 (33 554 432 px) → scène blanche. Correctif : <link> CSS depuis esm.sh + style-src CSP autorisant https://esm.sh. (2) appState.collaborators objet JSON → collaborators.forEach is not a function ; sanitizeAppState() reconvertit en Map et écarte width/height/offsetLeft/offsetTop. Vérifié navigateur : hauteur canvas 525 px (avant 33 554 432), dessin affiché, UI stylisée, 0 erreur. E2E + tests statiques CSP/CSS ajoutés.
BUG-065 [🟡 IMPORTANT] Éditeur Excalidraw : l'auto-save recharge la page en pleine édition 🟢 corrigé P1 📱 frontend IA frontend/js/excalidraw-viewer.js, frontend/js/utils.js, frontend/excalidraw-editor.html, tests/frontend/excalidraw-viewer.test.mjs Ouvrir un .excalidraw puis modifier un élément : au bout de 2 s la vue se recharge Chaque modification déclenchait un PUT save 2 s plus tard → SSE index_updated → reloadExternalWrite → openFile → recréation de l'iframe (refresh visible). Auto-save supprimée : sauvegarde explicite (bouton 💾 / Ctrl+S). reloadExternalWrite ignore le fichier si un iframe Excalidraw est ouvert (iframe[data-excalidraw-vault/path]). Le badge « Modified » ne réagit plus aux changements d'appState (resize/zoom) mais à la signature des éléments. Vérifié Playwright : plus de refresh, badge stable après bascule plein écran. Test statique (absence de requestSave/saveTimer).
BUG-066 [🔵 MINEUR] Configuration : icônes manquantes dans la table des matières (« Fichiers cachés », « Partages publics ») 🟢 corrigé P3 📱 frontend IA frontend/locales/{fr,en}.json Ouvrir Configuration → observer le sommaire : les entrées « Fichiers cachés » et « Partages publics » n'ont pas d'icône config.section_hidden → « 🗂️ Fichiers cachés » / « 🗂️ Hidden files », config.section_shares → « 📤 Partages publics » (EN avait déjà l'icône). Test : tests/frontend/unit.test.mjs (+1 : toutes les entrées du sommaire portent une icône FR/EN) Les libellés du sommaire utilisent des clés i18n distinctes des titres de section (auto.f8ba6127, config.section_partages-publics) qui, elles, avaient l'icône
BUG-067 [🔵 MINEUR] Guide d'utilisation : l'entrée « 📱 Mobile » du sommaire ne fait rien (section absente) 🟢 corrigé P3 📱 frontend IA frontend/index.html Ouvrir le Guide → cliquer « 📱 Mobile » dans le sommaire : rien ne se passe L'ancre #help-mobile-editor était présente dans la TOC mais aucune section id="help-mobile-editor" n'existait (l'édition mobile n'était qu'un h3 de help-edition). Fix #105 : section dédiée créée avec ancre + entrée de nav cohérente. Vérifié par test statique tests/test_guide.py::test_nav_anchors_resolve
BUG-068 Configuration — section « 🔒 Sécurité du compte » inachevée : boutons hors thème, QR code invisible, fiabilité des fonctions à valider 🟢 corrigé P1 📱 frontend + ⚙️ backend IA frontend/js/auth.js, frontend/style.css, backend/auth/router.py Configuration → 🔒 Sécurité du compte frontend/style.css (+config-btn-primary/danger thème), backend/auth/router.py (qr_data_url segno local), frontend/js/auth.js (QR local + fallback, recovery WebAuthn, carte mot de passe, escapeHtml labels), locales FR/EN, backend/requirements.txt (+segno) ; tests tests/test_mfa.py (+1) + tests/frontend/mfa-settings.test.mjs (nouveau, 9) pytest 1241 passed / 6 skipped, ruff 0, mypy 0, frontend unit + validate-imports verts
BUG-069 Login 2FA bloqué sans erreur : après user+pwd corrects, la page de login reste affichée et le challenge MFA n'apparaît jamais 🟢 corrigé P0 📱 frontend IA frontend/js/auth.js, frontend/index.html Activer 2FA → logout → login (bon user+pwd) frontend/js/auth.js (showMfaChallenge → .login-card + erreur mfa.challenge_unavailable si montage impossible), locales FR/EN ; tests tests/frontend/mfa-settings.test.mjs (+2) Reproduit au navigateur avant correctif (challenge jamais affiché), vérifié après : challenge affiché, code erroné → erreur, code valide (200) → app ; frontend mfa-settings 11/11, unit + validate-imports verts
BUG-070 Activation clé physique WebAuthn impossible : « Validation du credential WebAuthn échouée » à chaque tentative 🟢 corrigé P0 ⚙️ backend IA backend/auth/webauthn_mfa.py, backend/auth/router.py Config → Sécurité → Ajouter une clé → cérémonie navigateur → 400 resolve_relying_party() (rp_id/origines dérivés de la requête, config explicite prioritaire, forwarded si TRUST_PROXY) sur les 4 endpoints ; challenges multiples (5 derniers) acceptés ; .env.example ; tests tests/test_webauthn.py (+8) Logs : origin http://localhost:2020 rejetée + challenge mismatch au retry. Vérifié navigateur (authentificateur virtuel CDP) : register 200 + clé listée, clé de test retirée (admin de nouveau TOTP seul) ; pytest 1249 passed, ruff/mypy 0
BUG-071 Configuration « Configurations » inutilisable en mode mobile : sommaire masqué sans bouton d'accès, navigation par ancre sans JS, grilles 2 colonnes et rangées d'ajout qui débordent (≤768px) 🟢 corrigé P1 📱 frontend IA frontend/index.html, frontend/js/config.js, frontend/js/i18n.js, frontend/style.css, frontend/locales/{fr,en}.json Mobile (≤768px) : ouvrir Configurations → aucun sommaire ni moyen d'atteindre une section ; champs « Clés IA » / jetons / webhooks débordent index.html (+#config-hamburger .help-hamburger, config.toc_toggle FR/EN) ; config.js (toggle, scroll doux + actif + repli auto mobile, reset à l'ouverture) ; i18n.js (data-i18n-attr multi-paires ;) ; style.css (bloc mobile #config-modal : sommaire haut 46vh, grilles 1fr, add-rows wrap + !important, items wrap, 44px) ; tests tests/frontend/config-mobile.test.mjs (nouveau, 11) + CI ; E2E tests/e2e/config-mobile.spec.js (nouveau, 3/3 projet chromium-mobile, ignoré en desktop) pytest 1249 passed / 6 skipped, ruff 0, mypy 0, validate-imports 39 modules, unit 10/10, JSDOM ai 93/93 + sidebar 6/6 + mobile 35/35 + ai-keys 7/7
BUG-072 Visionneuse d'images : le plein écran et le panneau « Métadonnées » ne sont pas conservés lors de la navigation ←/→, et le panneau s'affiche sous la pellicule au lieu d'une barre latérale 🟢 corrigé P2 📱 frontend IA frontend/js/viewer.js, frontend/style.css Ouvrir une image, activer le plein écran (ou Métadonnées), puis naviguer avec les flèches précédent/suivant État persistant _imageViewerState { lightbox, meta } + drapeau _imageViewerNavPending posé par go()/pellicule : renderFile ne réinitialise que hors navigation image→image. Panneau reconstruit dans .image-viewer-body (sidebar droite, border-left, width:280px; max-width:40%) ; la règle lightbox ne masque plus que la pellicule. Boutons image-btn-lightbox/image-btn-metadata (+ aria-pressed), Escape resynchronisé. Tests : tests/frontend/image-viewer.test.mjs (+2), E2E tests/e2e/image-viewer.spec.js (+1). Navigation → openFile → renderImageViewer recréait le conteneur : les états lightbox/metaPanel étaient perdus. Le panneau était rendu en bas (colonne) au lieu d'une sidebar droite
BUG-073 Mobile : la barre de navigation fixe du bas masque le bas de tous les documents et pages affichés (les dernières lignes restent définitivement sous la barre) 🟢 corrigé P1 📱 frontend IA frontend/style.css Mobile (≤768px) : ouvrir un document long, défiler jusqu'au bas → la fin du contenu passe sous la barre #mobile-toolbar et n'est jamais atteignable Clearance mobile retargetée de .main-layout (sélecteur mort, absent de index.html) vers .main-body (calc(64px + env(safe-area-inset-bottom, 0))) ; règle sœur morte .editor-modal.active ~ .main-layout supprimée ; reset body.reading-mode .main-body { padding-bottom: 0 } (barre masquée en mode lecture) ; body.np-active .content-area ramené à 76px (dégagement dock seul, géométrie totale inchangée). Tests : tests/frontend/mobile-toolbar.test.mjs (7, au CI), E2E tests/e2e/mobile-toolbar.spec.js (2, chromium-mobile, skip desktop) La règle de clearance du bloc mobile cible .main-layout, classe absente de index.html (vrai conteneur : .main-body) → sélecteur mort, aucun dégagement réservé
BUG-074 [🟡 IMPORTANT] Assistant IA : le bloc d'étapes affiche « 1 step » sans titre alors que l'agent réalise plusieurs actions (compteur toujours à 1) 🟢 corrigé P1 📱 frontend + ⚙️ backend IA frontend/js/bookslm.js, backend/agent/loop.py Mode agent : demander une création multi-fichiers/dossiers → chaque message ne montre qu'« 1 étape ▶ » sans détail backend/agent/loop.py + frontend/js/bookslm.js : compteur = actions (hors réflexions) + titre = 1re action dans le <summary> ; reprise de confirmation diffusée dans le même message (fusion des étapes). Tests : tests/frontend/ai.test.mjs (+3) Le résumé <summary> ne porte aucun titre ; chaque reprise de confirmation crée un nouveau message assistant qui ne contient qu'une action ; les réflexions gonflent le compteur
BUG-075 [🟡 IMPORTANT] Assistant IA : chaque action mutatrice demande son propre « Appliquer » — aucun résumé des actions en attente ni approbation globale 🟢 corrigé P1 ⚙️ backend + 📱 frontend IA backend/agent/loop.py, backend/bookslm_routes.py, frontend/js/bookslm.js Mode agent : demander une structure de répertoires multi-fichiers → valider une action après l'autre backend/agent/loop.py (pending.actions, lot exécuté au resume) ; backend/bookslm_routes.py (confirm_all → ctx.confirmed) ; frontend/js/bookslm.js (carte multi-actions + « Tout approuver (N) »). Tests : tests/test_agent_loop.py, tests/test_bookslm.py, tests/frontend/ai.test.mjs La pause de confirmation ne capture que le premier appel mutateur du lot (les suivants sont deferred) ; carte unique sans liste ; nouveau confirm_all à ajouter pour autoriser la suite de l'exécution en une approbation
BUG-076 [🟡 IMPORTANT] Assistant IA : après une action de l'agent, l'arborescence et le document ouvert ne sont pas rafraîchis dynamiquement 🟢 corrigé P1 📱 frontend IA frontend/js/bookslm.js Mode agent : créer/supprimer un fichier ou dossier, modifier le document ouvert → l'UI ne bouge pas frontend/js/bookslm.js : MUTATING_TOOLS/FILE_WRITE_TOOLS, refresh d'arborescence débouncé sur event tool, _notifyFileWritten étendu (xlsx/docx/csv/pdf). Tests : tests/frontend/ai.test.mjs, tests/frontend/editor-inline.test.mjs Aucun refresh explicite sur les événements tool mutateurs (repose uniquement sur le watcher SSE) ; _notifyFileWritten ignore les créations de documents (xlsx/docx/csv/pdf)
BUG-077 [🟡 IMPORTANT] Assistant IA : aucun bouton « Stop » pour arrêter l'exécution de l'agent à tout moment 🟢 corrigé P1 📱 frontend IA frontend/js/bookslm.js Mode agent : lancer une longue tâche → le bouton Envoyer est désactivé, impossible d'arrêter (seule la fermeture du panneau abort) frontend/js/bookslm.js + frontend/style.css : bouton d'envoi → Stop (_syncSendButton/_stopGeneration/_markStopped), i18n ai.stop/ai.stopped. Tests : tests/frontend/ai.test.mjs (+2) _abortCtrl n'est déclenché que par close() ; aucun signal d'arrêt côté client pendant le stream
BUG-078 [🟡 IMPORTANT] Fichiers de code : la coloration syntaxique (highlight.js) disparaît — les feuilles de thème sont basculées à partir de la clé de thème au lieu du mode 🟢 corrigé P1 📱 frontend IA frontend/js/themes.js, frontend/js/ui.js, tests/frontend/unit.test.mjs Ouvrir un fichier .py/.sh/.ps1/.yml : le code s'affiche en texte brut, sans couleurs frontend/js/themes.js : applyTheme bascule hljs-theme-dark/hljs-theme-light selon le mode (isDark). frontend/js/ui.js : initTheme/applyTheme résolvent le mode persisté (obsigate-theme-mode) au lieu de traiter la clé (defaut-obsigate) comme un mode. Test : unit.test.mjs (+1). Les deux feuilles étaient désactivées car defaut-obsigate !== "dark" et !== "light" ; résultat non déterministe selon l'ordre UI.initTheme() (clé) / Sync.init() → themes.initThemes() (mode). Vérifié Playwright : 5/5 chargements colorés (.py), sépia/contraste élevé sur la palette claire
BUG-081 GET /api/auth/mfa/status → 500 quand l'auth est désactivée (user None, AttributeError sur user.get) 🟢 corrigé P1 ⚙️ backend IA backend/auth/router.py::mfa_status, tests/test_mfa.py Auth désactivée : curl http://127.0.0.1:2029/api/auth/mfa/status → 500 (reproduit live 2026-09-27) Garde user is None → payload MFA désactivé (mfa_enabled: false, totp_enabled: false, webauthn_credentials: 0) ; test TestMfaStatusAuthDisabled (échoue en 500 sans le correctif). Vérifié : test_mfa.py 32 passed, ruff/mypy 0 require_auth laisse passer le pseudo-user anonymous, get_user(username) → None non gardé. Trouvé via les logs E2E pendant BUG-080
BUG-079 GET /api/diagnostics → 500 « dictionary changed size during iteration » (stats d'index) 🟢 corrigé P1 ⚙️ backend IA backend/main.py Charger la page de diagnostic pendant une indexation : GET /api/diagnostics → 500 backend/main.py (api_diagnostics) : snapshot avant itération — list(index.items()) et inv.word_index.copy() (copie C atomique sous le GIL) ; test de non-régression tests/test_api_main.py::TestConfig::test_diagnostics_concurrent_index_writes Le handler itérait les dicts en direct alors que l'indexeur les modifiait depuis un autre thread (rebuild initial dans _search_executor, hooks incrémentaux add_document/remove_document) → RuntimeError dans le générateur → 500. Test déterministe (RaceDict fait grossir le dict en cours d'itération) : échoue sans le correctif, passe avec. Vérifié : pytest 1305 passed / 6 skipped, ruff 0, mypy 0
BUG-084 Index inversé : la suppression d'une vault y laisse des documents fantômes (résultats pour une vault inexistante) 🟢 corrigé P1 ⚙️ backend IA backend/indexer.py::remove_vault_from_index, backend/search.py::_remove_doc_internals Supprimer une vault configurée, puis chercher un terme contenu dans ses fichiers → les résultats la concernent encore remove_vault_from_index() déclenche _on_index_change('remove', …) pour chaque fichier de la vault ; _remove_doc_internals() supprime la clé vault_docs dont le set devient vide (defaultdict : une lecture la recréait). Test tests/test_search_advanced.py::TestVaultRemovalPurgesInvertedIndex (contre-preuve : échoue sans le correctif) Trouvé pendant la relecture de plan.md (étape 6 déjà livrée). Mesuré : 8 documents fantômes sur 8 après suppression de la vault de test (postings, doc_info, doc_vault, vault_docs) ; seul un reindex manuel les effaçait. Vérifié : test_search_advanced.py 27 passed, ruff/mypy 0, suite complète 1374 passed / 6 skipped
BUG-085 Édition d'un .xlsx : les valeurs calculées en cache disparaissent du classeur (et tout lecteur data_only=True voit None) 🟢 corrigé P1 tableur Excel IA backend/xlsx_reader.py::inspect_workbook, backend/services/mutations.py::edit_xlsx_cells, backend/routers/files_read.py, backend/routers/files_write.py, frontend/js/viewer.js::renderXlsxViewer Ouvrir un classeur contenant =B1*2 (avec sa valeur calculée) → éditer une cellule → le <v> disparaît du XML de la feuille LOSSY_PARTS + sonde <f>…</f><v>[^<] ; la lecture renvoie xlsx_lossy_features ; PUT xlsx/save refuse sans force (409 xlsx_lossy_content) ; bandeau + confirmation UI puis reprise force: true. Tests : TestXlsxLossyGuard (5) + xlsx-viewer.test.mjs (10) + tests/e2e/xlsx-viewer.spec.js (3) #153 A1. Périmètre réel vérifié sur openpyxl 3.1.5 : graphiques, images, dessins et TCD survivent au round-trip ; les pertes sont valeurs en cache, slicers/chronologies, contrôles de formulaire, connexions/requêtes, custom XML, signature, commentaires enrichis, macros. Vérifié : test_xlsx_viewer.py 31 passed, suite 1390 passed / 6 skipped, ruff/mypy 0, E2E 3/3
BUG-086 Édition d'un .xlsx : wb.save() écrit en place, un plantage laisse un classeur corrompu 🟢 corrigé P1 tableur Excel IA backend/services/mutations.py::edit_xlsx_cells Simuler un OSError pendant Workbook.save → le fichier d'origine est tronqué Écriture atomique : wb.save(<nom>.<pid>.tmp) puis os.replace() ; .tmp supprimé sur échec ; le backup .bak reste inchangé. Test : TestXlsxAtomicWrite::test_failed_save_keeps_the_original (octets identiques après échec) + test_no_tmp_left_after_a_successful_save #153 A2. Le fichier temporaire a un suffixe .tmp → ignoré par le watcher (_is_relevant ne retient que les extensions supportées). Vérifié : cf. BUG-085
BUG-087 Édition d'un .xlsx concurrente (deux onglets, agent IA + viewer) : read-modify-write sans verrou, le dernier écrivain gagne silencieusement 🟢 corrigé P1 tableur Excel IA backend/services/mutations.py::_xlsx_write_lock Deux PUT xlsx/save simultanés sur le même fichier → une écriture est écrasée sans trace Verrou par chemin (registre + garde, timeout 15 s) autour du cycle load → edit → os.replace ; attente dépassée → 409 conflict. L'endpoint est devenu def (sync) pour que l'attente s'exécute dans le threadpool et ne bloque pas la boucle d'événements. Test : TestXlsxWriteLock (2) #153 A3. Verrou en mémoire, par processus : protège les cas d'un même serveur (le cas desktop/Tauri). Vérifié : cf. BUG-085
BUG-088 Injection de formule dans un .xlsx : une saisie =cmd|'/c calc'!A1 est stockée comme formule et s'exécute à l'ouverture dans Excel (DDE) 🟢 corrigé P0 tableur Excel / sécurité IA backend/services/mutations.py::_write_cell, backend/routers/files_write.py, frontend/js/viewer.js::renderXlsxViewer PUT /api/file/V/xlsx/save avec {"sheet": "S", "cells": {"A1": "=1+1"}} → la cellule sort en data_type == "f" cell.data_type = "s" après affectation : le texte est stocké comme chaîne, aucun <f> n'est écrit. Opt-in via allow_formula: true (endpoint) et le bouton f(x) de la visionneuse (session, jamais persisté). Test : TestXlsxFormulaGuard (4) + xlsx-viewer.test.mjs (toggle) #153 A4. +/- ne sont pas neutralisés : ils sont déjà convertis en nombre par _coerce_xlsx_value. Le handler global ServiceError expose désormais code + details (le client en a besoin pour le 409), et api() (frontend) les propage sur l'Error. Vérifié : cf. BUG-085
BUG-089 Un reindex manuel ne reconstruisait pas l'index inversé : la recherche TF-IDF continuait de servir un index périmé 🟢 corrigé P1 ⚙️ backend / recherche IA backend/indexer.py::reload_index, backend/indexer.py::reload_single_vault, backend/search.py Modifier le contenu d'un fichier, puis GET /api/index/reload → la recherche renvoie encore l'ancien contenu (ou rien pour un fichier nouveau) reload_index() / reload_single_vault() appellent init_inverted_index() après le rebuild (le remplacement wholesale d'une entrée de vault n'émet pas les notifications incrémentales). En prime, backend/search.py lisait l'index via from backend.indexer import index (liaison par valeur du dict) : un importlib.reload(backend.indexer) recréait le dict côté indexer tandis que la recherche écrivait encore dans l'ancien — l'index inversé n'indexait alors plus rien. Tous les accès passent désormais par _indexer.index. Contre-preuve : TestXlsxSearchable::test_search_finds_a_word_stored_in_a_cell échoue sans le correctif #153 A5. Trouvé en écrivant le test de recherche d'A5 : il passait isolément et échouait en suite complète selon l'ordre. Le reload incrémental par fichier (watcher, edition) n'est pas concerné : il passe par le hook _on_index_change. Vérifié : suite 1402 passed / 6 skipped, ruff/mypy 0
BUG-090 Troncature silencieuse d'une feuille .xlsx au-delà de 500 lignes × 40 colonnes : l'utilisateur voit une table courte sans aucun indice que la suite existe 🟢 corrigé P1 tableur Excel / UX IA backend/xlsx_reader.py::render_sheets, backend/routers/files_read.py, frontend/js/viewer.js::renderXlsxViewer, frontend/style.css Ouvrir test_vault/sample-xlsx-large.xlsx (520 lignes) → la feuille s'arrête à la ligne 500 sans aucun message render_sheets() renvoie désormais total_rows/total_cols (dimensions déclarées par la feuille), max_rows/max_cols (plafonds du moteur) et truncated ; la visionneuse affiche un bandeau « Feuille tronquée — 500 lignes affichées sur 520 » (i18n xlsx.truncated_* FR/EN, axe des colonnes inclus). Contre-preuve : neutraliser truncated → TestXlsxTruncationNotice (2 tests) échoue #153 A8/R5. La ligne d'en-têtes est aussi sticky au défilement vertical (thead th { top: 0 } + top: auto sur les numéros de ligne pour éviter l'empilement en haut à gauche). L'endpoint GET …/xlsx/sheet (#153 A9) sert les fenêtres au-delà du plafond, mais le chargement paresseux complet (défilement virtuel, « charger tout ») reste à faire — le bandeau dit la vérité en attendant. Vérifié : test_xlsx_viewer.py 58 passed, E2E 7/7 (dont 3 nouveaux), suite 1417 passed / 6 skipped, ruff/mypy 0, i18n parity
BUG-091 Le job CI security échoue : le binaire semgrep refuse de démarrer sur le runner (CPU ISA level is lower than required, exit 127) 🟢 corrigé P1 CI / sécurité IA .gitea/workflows/ci.yml (job security), backend/requirements.txt Run Gitea #1641 : étape « Semgrep » → libs/libresolv.so.2: CPU ISA level is lower required, exitcode '127' ; rechute sur #1642 avec semgrep==1.174.0, puis sur #1654 avec 1.157.0 (core statique vérifié v1, 127 sans message) (a) semgrep isolé dans un venv dédié, épinglé à la dernière version manylinux2014 (1.157.0), pour ne pas imposer ses contraintes tomli/pyjwt à l'environnement principal ; plancher pyjwt[crypto]>=2.13.0 dans requirements.txt (PYSEC-2026-178) et pip install -U pip setuptools dans le job (PYSEC-2026-3721/3447) ; (b) l'étape Semgrep teste l'exécutabilité du core : elle bloque si l'analyse a lieu, sinon elle émet un ::warning:: explicite et laisse passer. Bandit et pip-audit restent bloquants #153. security échouait déjà avant ce push (v2.31.0/v2.32.0 rouges) ; les commits de features v2.33.0→v2.39.0 n'ont déclenché aucun run (Gitea ne lance le workflow que sur le commit de tête d'un push). Deux hypothèses infirmées en route : « série 1.175+ incompatible » (1.157.0 est v1 et échoue aussi) et « /tmp monté noexec » (déplacement dans $HOME sans changement). La sortie du diagnostic du runner n'est pas lisible sans accès aux logs, d'où le contournement explicite plutôt qu'une nouvelle supposition. À reprendre sur un runner x86-64-v2, où semgrep redeviendra bloquant sans modification

| BUG-092 | Les tests réseau dépendent du DNS réel du runner : test_worker_failure_maps_to_tool_error échoue en dns_error au lieu d'atteindre le worker Playwright mocké, et le job CI test rougit de façon intermittente | 🟢 corrigé | P1 | CI / tests | IA | tests/test_webrender.py, tests/test_web_tools.py | Sur un runner au DNS instable : pytest tests/test_webrender.py -k test_worker_failure_maps_to_tool_error → assert 'dns_error' == 'render_unavailable' | Fixture no_dns mockant les deux références du garde SSRF _assert_public_http_url (celle de backend/tools/web.py et celle importée dans le namespace de backend/tools/webrender.py, ligne 30 — la seconde avait d'abord échappé au correctif). Les tests de garde SSRF n'utilisent pas la fixture et continuent de traverser le vrai garde | Le garde est appelé par fetch_url avant le traitement ; seule la couche httpx était mockée. Contre-preuve : DNS coupé globalement (socket.getaddrinfo → gaierror) → avant 1 échec, après 1474 passed / 6 skipped | | BUG-093 | Le job CI security échoue : pip-audit bloque sur deux DoS de ressources dans pypdf 6.16.0 (PYSEC-2026-3910, PYSEC-2026-3911) — et le plancher pypdf>=4.0 ne les corrigeait pas, car l'image Act du runner embarque 6.16.0 préinstallé dans sa toolcache Python (Requirement already satisfied ⇒ jamais mis à niveau) | 🟢 corrigé | P0 | CI / sécurité | IA | backend/requirements.txt, .gitea/workflows/ci.yml, tests/test_ci_workflow.py | Run Gitea #1660, job security : Found 2 known vulnerabilities, ignored 2 in 1 package → pypdf 6.16.0 PYSEC-2026-3910 6.16.1 / PYSEC-2026-3911 6.16.1 | Plancher pypdf>=6.16.1 (correctif des deux advisories), commenté pour expliquer la contrainte de la toolcache. Ajout de tests/test_ci_workflow.py::TestDependencySecurityFloors, qui verrouille les planchers de sécurité (pypdf, pyjwt) et interdit qu'ils retombent sous le correctif | Les deux advisories sont des consommations de ressources non contrôlées (PDF à outlines multiples ou à nombreux XForm réutilisés) et sont donc atteignables par ObsiGate, dont backend/pdf_reader.py extrait le texte et parcourt les outlines de PDF fournis par l'utilisateur. Contre-preuve : plancher remis à >=4.0 → le garde-fou échoue. pip-audit local : 6.16.1, 6.16.2 et 6.19.0 sans vulnérabilité connue. Correction découverte en lisant le log du job (/actions/runs/1660/jobs/5541/logs, accessible sans token) — le log de l'étape Semgrep collé précédemment datait d'un run antérieur | | BUG-094 | Feuille .xlsx vide ou nouvellement ajoutée : impossible d'y saisir une valeur et d'y insérer une ligne/colonne — la feuille s'affiche « Feuille vide » sans aucune cellule | 🟢 corrigé | P1 | tableur Excel / UX | IA | backend/xlsx_reader.py::render_sheets, frontend/js/viewer.js::renderXlsxViewer | Ajouter une feuille (PUT …/xlsx/structure sheet_add) puis tenter de saisir A1 ou d'insérer une ligne/colonne | render_sheets() remplace une grille vide par un quadrillage vierge 20×8 (constantes EMPTY_SHEET_ROWS/EMPTY_SHEET_COLS) aux vraies coordonnées A1 ; la visionneuse retombe sur parseRef(activeRef) || {row:1,col:1} pour que le menu Structure propose toujours insérer/supprimer ligne et colonne. Contre-preuve : TestXlsxDisplay::test_empty_sheet_renders_an_editable_blank_grid (sans le correctif : « Feuille vide » sans data-cell) | Le classeur n'était pas en cause : seule la représentation HTML était vide, donc aucun td à sélectionner → aucune cellule active → aucune action de structure possible. Vérifié : test_xlsx_viewer.py 59 passed, xlsx-viewer.test.mjs 52/52 | | BUG-095 | Le job CI security échoue : pip-audit bloque sur CVE-2026-102274 dans pyjwt 2.13.0 (correctif 2.14.0) — le plancher >=2.13.0 (BUG-091) est désormais sous le dernier correctif | 🟢 corrigé | P1 | CI / sécurité | IA | backend/requirements.txt, .gitea/workflows/ci.yml, tests/test_ci_workflow.py, CHANGELOG.md, docs/ISSUES_TODOLIST.md | Run Gitea du push v2.44.0, job security : Found 1 known vulnerability, ignored 2 in 1 package → pyjwt 2.13.0 CVE-2026-102274 2.14.0 | Plancher pyjwt[crypto]>=2.14.0 (au-dessus de la version préinstallée de la toolcache du runner, sinon pip répond « already satisfied »). Garde-fou TestDependencySecurityFloors : FLOORS["pyjwt"] porté à (2, 14, 0) — contre-preuve : plancher remis à 2.13.0 → test rouge. Commentaire de la job security mis à jour | Le plancher de BUG-091 (2.13.0) corrigeait PYSEC-2026-178 mais est lui-même vulnérable depuis. Même mécanisme que BUG-093 (pypdf) : un plancher de sécurité doit rester au-dessus du dernier correctif. Vérifié : test_ci_workflow.py 9 passed ; pip-audit local OK (pyjwt 2.15.1 ≥ 2.14.0) | | BUG-096 | [🟡 IMPORTANT] Enregistrer un .csv depuis la visionneuse échoue : TypeError sur sheets[…].name (aucun PUT …/csv/save émis) | 🟢 corrigé | P1 | 📱 frontend | IA | frontend/js/viewer.js (renderXlsxViewer, construction des jobs de sauvegarde) | Ouvrir un .csv dans ObsiGate, modifier une cellule, cliquer Enregistrer | frontend/js/viewer.js : le job de sauvegarde ne lit plus le nom de feuille dans xlsx_sheets (absent du payload CSV — l'origine du TypeError) — repli feuille → data.title/data.path. Test JSDOM « saving an edited csv PUTs /csv/save (payload without xlsx_sheets) ». Vérifié : suite 1490 passed / 6 skipped, xlsx-viewer.test.mjs 61/61 | Défaut #156 A1. En mode CSV la réponse de lecture n'a pas de xlsx_sheets (backend/routers/files_read.py) : sheets est vide et sheets[Number(panel.dataset.sheet)].name lève un TypeError avant le try, donc aucun PUT /api/file/{vault}/csv/save n'est émis (le service save_csv_cells() et l'endpoint sont, eux, corrects). Reproduit en JSDOM le 2026-09-29 (harnais de tests/frontend/xlsx-viewer.test.mjs) : Cannot read properties of undefined (reading 'name'), 0 requête d'API enregistrée. Analyse et critères : docs/features/xlsx-editor-completeness.md | | BUG-097 | [🟡 IMPORTANT] Feuille .xlsm tronquée : « Charger la suite » échoue en 415 (GET …/xlsx/sheet n'accepte que .xlsx) alors que le .xlsm est éditable | 🟢 corrigé | P1 | ⚙️ backend + 🔌 api | IA | backend/routers/files_read.py (api_file_xlsx_sheet), frontend/js/viewer.js (wireLazyRows) | Ouvrir un .xlsm de plus de 500 lignes puis cliquer le pied « Charger la suite » (ou approcher du bas du tableau) | backend/routers/files_read.py : GET …/xlsx/sheet accepte .xlsx et .xlsm (autres formats → 415 inchangé). Tests TestXlsmEditable::test_sheet_window_is_served_for_xlsm + test_sheet_window_still_refuses_other_formats | Défaut #156 A2. .xlsm est servi éditable (files_read.py:491-512, pas de xlsx_readonly) donc la visionneuse câble le chargement paresseux, mais api_file_xlsx_sheet refuse tout ce qui n'est pas .xlsx (files_read.py:272). Analyse et critères : docs/features/xlsx-editor-completeness.md | | BUG-098 | [🟡 IMPORTANT] Délimiteur CSV figé , : un .csv français (;) s'affiche en une seule colonne et se réécrit dans un autre format | 🟢 corrigé | P2 | ⚙️ backend | IA | backend/xlsx_reader.py (render_csv_table, delimiter="," par défaut), backend/routers/files_read.py:551, backend/services/mutations.py (save_csv_cells) | Ouvrir un CSV ; (export Excel FR) dans ObsiGate : une seule colonne ; éditer une cellule puis enregistrer : le fichier est réécrit en , | backend/xlsx_reader.py (sniff_csv_delimiter) + backend/services/mutations.py::save_csv_cells : délimiteur détecté et réutilisé. Tests TestCsvDelimiter + lecture/écriture/guillemets point-virgule | Défaut #156 A3. render_csv_table(raw) est appelé sans délimiteur et save_csv_cells() re-parse en ,, alors que l'export CSV de la visionneuse écrit en ;. Traitement prévu : détection ;/,/tab partagée lecture/écriture/export. Analyse : docs/features/xlsx-editor-completeness.md | | BUG-099 | [🔵 MINEUR] Sonde de perte plafonnée (8 Mo, budget global) : risque de perte silencieuse des valeurs calculées en cache au-delà du budget | 🟢 corrigé | P2 | ⚙️ backend | IA | backend/xlsx_reader.py (_MAX_PROBE_BYTES, _has_cached_formulas, inspect_workbook) | Constituer un classeur dont le XML de feuille dépasse 8 Mo avant la première formule cachée, puis l'éditer : la garde 409 xlsx_lossy_content ne se déclenche pas | backend/xlsx_reader.py : budget par feuille (4 Mo) + plafond global (32 Mo) ; _scan_cached_formulas() renvoie (found, unverified) et inspect_workbook() ajoute cached_values_unverified (libellé i18n FR/EN). Contre-preuve : budget épuisé → signalé au lieu de []. Tests TestXlsxCachedValueProbe (3) | Défaut #156 A4, analyse statique (non reproduit). Le budget est partagé entre toutes les feuilles : au-delà, cached_values n'est pas détecté et l'écriture détruit ces valeurs sans avertissement (risque n°1 de #153). Traitement prévu : budget par feuille + signal d'incertitude pour que la garde reste prudente. Analyse : docs/features/xlsx-editor-completeness.md | | BUG-100 | Retour à l'accueil incomplet : le clic sur le titre perd la section « Raccourcis & Astuces » et laisse la recherche globale ainsi que le filtre de la sidebar actifs | 🟢 corrigé | P1 | 📱 frontend | IA | frontend/js/viewer.js (showWelcome), frontend/js/legacy.js (goHome) | Ouvrir un fichier (le dashboard est détruit), puis cliquer le titre ObsiGate | viewer.js : #quick-help capturé au chargement du module et réinjecté dans le template reconstruit de showWelcome() ; legacy.js::goHome clique #search-clear-btn (chips + barre de résultats) et #sidebar-filter-clear-btn avant d'afficher l'accueil | Cause racine : showWelcome() reconstruit #dashboard-home depuis un template qui n'embarque pas le bloc #quick-help présent dans index.html — il ne disparaissait qu'après la première reconstruction (ouverture de fichier, puis retour). Livré avec #158 A3 | | BUG-101 | Raccourcis d'onglets déclarés deux fois : un seul Ctrl+W fermait deux onglets (le second fermait celui ré-activé par la fermeture précédente) et Ctrl+Tab sautait un onglet | 🟢 corrigé | P1 | 📱 frontend | IA | frontend/js/ui.js (bloc « Keyboard shortcuts for tabs » dupliqué en fin de fichier) | Ouvrir deux onglets puis Ctrl+W une fois | Suppression du second bloc document.addEventListener("keydown", …) dupliqué (fin de ui.js) — un seul listener reste | Exposé par #158 : le second onglet n'était plus jamais vide auparavant (la 2ᵉ fermeture tombait sur _activeTabId === null et devenait sans effet). Vérifié : repro Playwright (closes: ["fichier", "nav"] avant → ["fichier"] après) + suite E2E | | BUG-102 | Menu contextuel qui se referme ~10 ms après son ouverture : la reflow des icônes (remplacement <i> → <svg> par lucide) déclenche un événement scroll capturé par ContextMenuManager qui referme le menu | 🟢 corrigé | P2 | 📱 frontend | IA | frontend/js/ui.js (ContextMenuManager.init/show) | Clic droit sur un fichier de la page de navigation (ou de l'arbre) juste après un rendu d'icônes | show() mémorise _shownAt ; le listener scroll (capture) ignore un déclenchement dans les 250 ms suivant l'ouverture | Exposé par #158 A5 (test E2E nav-tab.spec.js : menu résolu « hidden » alors que 6 items étaient construits). Vérifié : instrumentation Playwright (display block conservé après le scroll) + suite E2E | | BUG-103 | Mobile : le bas du sidebar de navigation est masqué par la barre d'outils du bas (z-index 200 < 900) | 🟢 corrigé | P2 | 📱 frontend | IA | frontend/style.css (règle .sidebar du bloc @media (max-width: 768px)) | Vue mobile (≤768 px), sidebar ouvert depuis « Explorateur » | Passé à z-index: 950 (au-dessus de .mobile-toolbar, 900) | Exposé après #158 (retour utilisateur). Vérifié : elementFromPoint Playwright (393×851) + test source mobile sidebar over toolbar (unit.test.mjs) | | BUG-104 | Desktop : toutes les commandes Tauri invoquées depuis la page backend (http://127.0.0.1) rejetées « not allowed by ACL » — le bouton « Choisir mon dossier » du wizard ne fait rien | 🟢 corrigé | P1 | 🖥️ desktop | IA | desktop/permissions/commands.toml (nouveau), desktop/capabilities/default.json, desktop/build.rs | Installeur 2.49.2 : clic sur « Choisir mon dossier » → aucune fenêtre ; console [desktop] invoke(pick_vault_folder) failed: Command … not allowed by ACL (capturée via CDP, WebView2 --remote-debugging-port) | Manifeste de permissions applicatives créé (allow-app-commands, 19 commandes) et référencé par la capability : Tauri v2 ACL gate toute commande invoquée depuis une origine remote, y compris celles de l'invoke_handler | cargo test 25 passed (nouveau garde-fou test_frontend_invokes_are_acl_allowed) + vérif live CDP : get_version→2.49.2, clic → fenêtre native « Select Folder », parcours complet ajout de vault terminé | | BUG-105 | Desktop : crash 2-15 s après l'ouverture de la fenêtre (APPCRASH c0000374, heap corruption détectée dans ntdll) — absent de v2.0.0, apparu en 2.49.2 | 🟢 corrigé | P1 | 🖥️ desktop | IA | desktop/src/jumplist.rs (set_link_title) | Installeur 2.49.2 : fenêtre visible quelques secondes puis fermeture (3/3), Event Log Windows APPCRASH obsigate-desktop.exe … c0000374, rien dans stderr | set_link_title (#77) construisait un PROPVARIANT VT_LPWSTR pointant un buffer heap Rust et le confiait au property store du shell, qui le libère avec l'allocateur Windows → corruption du tas ; fonction supprimée (SetDescription fournit déjà le libellé) | Bisect 3 états (jumplist off = stable, titre off = stable, d'origine = crash) ; run 9 min sans crash + sortie propre EXIT=0, zéro événement WER (avant : 3 crashes ≤20 s) | | BUG-106 | Page blanche sur /docs (Swagger UI) : la CSP script-src sans unsafe-inline (#87 T5c) refuse le script inline d'init de FastAPI, jamais noncé | 🟢 corrigé | P1 | ⚙️ backend | IA | backend/main.py (SecurityHeadersMiddleware) | Ouvrir https://og.dracodev.net/docs : HTML servi (200) mais aucune UI, console Refused to execute inline script … Content Security Policy | Injection du nonce dans le HTML de /docs et /redoc depuis le middleware, via le helper existant inject_csp_nonce ; corps décompressé/recompressé (GZipMiddleware est plus proche de la route) | pytest tests/test_csp_nonce.py (3 tests ajoutés : nonce sur /docs, /redoc sans injection, regex sur bundle externe) + 45 passed sur les 3 suites sécurité ; ruff/mypy 0 | | BUG-107 | Drag & drop de fichiers depuis l'Explorateur inutilisable sur desktop : Tauri/wry pose son propre IDropTarget par-dessus de celui du WebView2 et aucun événement natif n'est écouté — #89 ne fonctionnait que sur le web | 🟢 corrigé | P1 | 🖥️ desktop | IA | desktop/tauri.conf.json (create: false), desktop/src/main.rs (WebviewWindowBuilder::…disable_drag_drop_handler) | App desktop : glisser un fichier de l'Explorateur sur la fenêtre → aucun survol, aucun dépôt — les gestionnaires HTML5 de dragdrop.js ne sont jamais déclenchés | Création manuelle de la fenêtre avec disable_drag_drop_handler() (doc Tauri : « required to use HTML5 drag and drop APIs on the frontend on Windows ») ; create: false évite la double création par la boucle Tauri | cargo test 26 passed (nouveau garde-fou test_window_created_without_tauri_drag_drop_handler : les deux marqueurs obligatoires) + lancement OK (fenêtre unique, boot normal) ; dépôt réel à valider par l'utilisateur | | BUG-108 | Tableur : le « Coller » du menu contextuel ne fait rien (copier-coller par menu muet) | 🟢 corrigé | P1 | 📱 frontend | IA | frontend/js/xlsx/context-menu.js (closeContextMenu) | Ouvrir un .xlsx, clic droit « Copier » sur une plage, clic droit cible puis « Coller » → rien n'est collé (le menu se ferme sans action) | closeContextMenu() retirait le nœud du DOM avant de remettre _menu à null : retirer le menu qui tient le focus émet focusout en synchrone, qui rappelle closeContextMenu() → second remove() sur un nœud déjà démonté → NotFoundError qui avorte le handler de l'action « Coller » du viewer. Introduit par #179 (fermeture au focus). Correctif : _menu = null avant menu.remove() | Reproduit en local sur HEAD (e2e « coller une plage » : attendu Date, obtenu la valeur datée) puis vert après fix : E2E -g "coller une plage" 1/1, xlsx-menus 11/11, ai-quick-actions 13/13 | | | | | | | | | | | |

TODOs techniques (améliorations / nouvelles tâches)

# Titre Statut Priorité Scope Assigné Zone (fichier) Cmd de repro Correctif / Commit Notes
(À remplir)

🛠️ Journal des interventions AI

Chaque passage d'un agent IA qui modifie du code ou l'état du registre est journalisé. Règle : l'IA ajoute une ligne à la fin de ce tableau à chaque session de correction. Colonnes : Date (YYYY-MM-DD) · ID(s) traité(s) · Action · Fichiers modifiés · Résumé · Statut après.

Date ID(s) traité(s) Action Fichiers modifiés Résumé Statut après
2026-09-28 BUG-090 (#153 A8 + A9) Correction + feature backend/xlsx_reader.py, backend/routers/files_read.py, backend/schemas.py, backend/openapi_docs.py, frontend/js/viewer.js, frontend/style.css, frontend/locales/{fr,en}.json, tests/test_xlsx_viewer.py, tests/frontend/xlsx-viewer.test.mjs, tests/e2e/xlsx-viewer.spec.js, test_vault/sample-xlsx-large.xlsx La troncature d'une feuille est annoncée et les lignes cachées restent accessibles : (BUG-090/A8) render_sheets() renvoie total_rows/total_cols/max_rows/max_cols/truncated, la visionneuse affiche un bandeau « Feuille tronquée » (i18n FR/EN, axes lignes et colonnes) et la ligne d'en-têtes devient sticky (top: auto sur les numéros de ligne pour éviter l'empilement) ; (A9) GET /api/file/{vault}/xlsx/sheet?sheet=&offset=&limit= (XlsxSheetWindowResponse, plafond 1 000 lignes/requête, 404 feuille inconnue, 415 non-xlsx) sert une fenêtre avec les vraies coordonnées A1 et le has_more de pagination. Contre-preuves : neutraliser truncated → 2 tests échouent ; neutraliser l'offset → 3 tests échouent. Vérifié : test_xlsx_viewer.py 58 passed, xlsx-viewer.test.mjs 14/14, E2E 7/7 (3 nouveaux + fixture sample-xlsx-large.xlsx 520 lignes), suite 1417 passed / 6 skipped, ruff 0, mypy 0, i18n parity, validate-imports 40 modules 🟢 corrigé (en attente vérif utilisateur)
2026-09-29 BUG-091 (suite — désactivation semgrep en CI) Correction CI .gitea/workflows/ci.yml, CHANGELOG.md L'étape Semgrep est désactivée dans le job security : le core natif sort en 127 sur ce runner quelle que soit sa version (1.178 = message ISA explicite ; 1.157.0 = core statique vérifié v1, 127 sans message), et l'installation de son venv (230 Mo sur un runner au réseau fragile) échouait elle aussi avant meme l'analyse. Trois hypothèses ont été testées puis infirmées — « releases 1.175+ incompilables » (1.157.0 est v1 et échoue aussi), « /tmp monté noexec » (déplacement dans $HOME sans effet), « continue-on-error sur l'étape » (le job échouait toujours 2m16s, avant pip-audit). Faute d'accès aux logs du runner pour lire la sortie du diagnostic, la SAST semgrep est retirée du CI : bandit et pip-audit restent bloquants, les 8 règles locales restent applicables en local (semgrep --config semgrep-rules/ backend/) et l'étape est réactivable telle quelle sur un runner x86-64-v2 🟢 corrigé (en attente vérif utilisateur)
2026-09-29 BUG-092 (job CI test, #153) Correction tests tests/test_webrender.py, tests/test_web_tools.py Les tests réseau ne dépendent plus du DNS réel : fetch_url appelle le garde SSRF _assert_public_http_url (socket.getaddrinfo) avant le traitement, et seule la couche httpx était mockée. Sur le runner au DNS instable, tests/test_webrender.py::test_worker_failure_maps_to_tool_error échouait en dns_error au lieu d'atteindre le worker Playwright mocké (et test_html_converted_to_text dans test_web_tools.py de la même façon). Correctif : fixture no_dns mockant les deux références du garde (web._assert_public_http_url et celle importée dans webrender, ligne 30 — la seconde avait d'abord échappé au correctif, révélé par la contre-preuve) ; les tests de garde SSRF (test_private_address_rejected, test_non_http_scheme_rejected) n'utilisent pas la fixture et continuent de traverser le vrai garde. Contre-preuve : DNS cassé globalement (socket.getaddrinfo → gaierror) → avant 1 échec, après 1474 passed / 6 skipped 🟢 corrigé (en attente vérif utilisateur)
2026-09-29 BUG-093 (job CI security, run #1660) Sécurité / Correction CI backend/requirements.txt, .gitea/workflows/ci.yml, tests/test_ci_workflow.py Le job security est enfin vert : la désactivation de semgrep (v2.39.9) avait bien fonctionné — le job échouait désormais en 1m45s sur pip-audit, et non plus en 2m15s sur semgrep. Cause : deux DoS de ressources publiés sur pypdf 6.16.0 (PYSEC-2026-3910 outlines, PYSEC-2026-3911 XForm, correctif 6.16.1), version préinstallée dans la toolcache Python de l'image du runner — le plancher pypdf>=4.0 était donc satisfait et l'image n'était jamais mise à niveau. Correctif : plancher pypdf>=6.16.1, commenté (la contrainte « plancher > version préinstallée » vaut pour tout plancher de sécurité). Garde-fou tests/test_ci_workflow.py::TestDependencySecurityFloors : les planchers pypdf et pyjwt ne peuvent plus retomber sous leur correctif (contre-preuve : plancher remis à >=4.0 → test rouge). Au passage, tests/test_ci_workflow.py::TestSemgrepStep était en régression depuis v2.39.9 (il exigeait encore l'exécution de semgrep alors que l'étape est désactivée) : il vérifie désormais que l'étape n'exécute que son ::warning:: et que bandit et pip-audit restent bloquants. Cause trouvée en lisant le log brut du job (/actions/runs/1660/jobs/5541/logs, accessible sans token) — le log d'étape Semgrep collé précédemment datait d'un run antérieur 🟢 corrigé (en attente vérif utilisateur)
2026-09-29 BUG-091 (#153, runs CI #1641-#1642) Correction CI .gitea/workflows/ci.yml, backend/requirements.txt, docs/ISSUES_TODOLIST.md, CHANGELOG.md Le job security est réparé définitivement : (1) le binaire semgrep non épinglé exige depuis 1.158.0 un CPU x86-64-v2 que le runner Gitea ne fournit pas (libs/libresolv.so.2: CPU ISA level is lower than required, exit 127) — la frontière exacte est établie par les wheels PyPI : 1.157.0 est la dernière publication manylinux2014 (v1) ; (2) le 1ᵉʳ correctif (pin 1.174.0, v2.39.2) échouait car cette version ne publie qu'en manylinux_2_34 ; (3) semgrep vit désormais dans un venv isolé du job (/tmp/semgrep-venv, pin 1.157.0) car ses dépendances contredisent l'env principal (tomli~=2.0.1 vs pip-audit ≥ 2.10, pyjwt~=2.12.0 vs PYSEC-2026-178) ; (4) plancher pyjwt[crypto]>=2.13.0 dans requirements.txt (transitif de mcp) et pip install -U pip setuptools dans le job (nouveaux advisories pip PYSEC-2026-3721, setuptools PYSEC-2026-3447). Validation : environnement frais reconstitué en local → résolution sans conflit (pyjwt 2.15.1), pip-audit exit 0, semgrep 1.157.0 exit 0 sur semgrep-rules/. Au passage documenté : security échouait déjà avant ce push (v2.31.0/v2.32.0 rouges) et les commits de features n'ont déclenché aucun run (Gitea : commit de tête uniquement) 🟢 corrigé (en attente vérif utilisateur)
2026-09-28 #153 A6 → A17 (v2.33.0 → v2.39.0) Feature + clôture documentaire (aucun bug nouveau) CHANGELOG.md, docs/features/xlsx-viewer.md, docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md, README.md, README.fr.md Clôture du backlog #153 : entrées CHANGELOG des 7 sous-tâches, fiche features/xlsx-viewer.md (statut terminé, cases A6-A17 cochées, historique), section 6 du guide utilisateur étendue (barre de formule, navigation clavier, tri/filtre/recherche/export CSV, structure, styles, formats .xlsm/.xls/.ods/.csv, tableau de bord) et bullets README FR/EN. Code livré : v2.33.0 A6 (outils IA backend/tools/spreadsheets.py), v2.34.0 A7 (clavier + barre de formule), v2.35.0 A13 (tri/filtre/recherche/export), v2.36.0 A14 (structure PUT …/xlsx/structure), v2.37.0 A15 (styles/fusions/volets figés), v2.38.0 A16 (.xlsm éditable, .xls/.ods lecture seule, .csv RFC 4180), v2.39.0 A17 (dashboard GET …/xlsx/dashboard). Vérifié : suite xlsx 116 passed, xlsx-viewer.test.mjs 35/35, ruff/mypy 0, i18n parity, validate-imports 40 modules ✅ livré (en attente vérif utilisateur)
2026-09-28 BUG-089 (#153 A5, A10, A12) Correction backend/xlsx_reader.py, backend/indexer.py, backend/search.py, backend/services/mutations.py, frontend/js/viewer.js, frontend/style.css, frontend/locales/{fr,en}.json, tests/test_xlsx_viewer.py Les tableurs deviennent visibles ettypés : (A5) extract_indexable_text() indexe noms de feuilles + 20 premières lignes (plafond 5 k caractères) dans le TF-IDF et la recherche sémantique — un mot tapé dans une cellule rend le fichier trouvable ; (A10) _coerce_xlsx_value() reconnaît désormais les booléens (TRUE/FAUX/OUI/NON) et les dates FR JJ/MM/AAAA (jour-first : 01/02/2026 = 1er février), symétrique avec l'affichage ; (A12) la valeur calculée en cache s'affiche sous la formule (<span class="xlsx-cached">, 2ᵉ lecture data_only=True uniquement si l'archive contient un <v>), info-bulle traduite via xlsx.cached_value_title FR/EN. (BUG-089) un reindex manuel reconstruisait mal l'index inversé et backend/search.py lisait l'index par valeur. Contre-preuves vérifiées pour A5, A10 et A12. Vérifié : test_xlsx_viewer.py 43 passed, suite 1402 passed / 6 skipped, ruff 0, mypy 0, i18n parity, validate-imports 40 modules, xlsx-viewer.test.mjs 10/10 🟢 corrigé (en attente vérif utilisateur)
2026-09-27 BUG-085 → BUG-088 (#153 A1-A4) Correction backend/xlsx_reader.py, backend/services/mutations.py, backend/routers/files_read.py, backend/routers/files_write.py, backend/schemas.py, backend/main.py, frontend/js/viewer.js, frontend/js/auth.js, frontend/style.css, frontend/locales/{fr,en}.json, frontend/sw.js, tests/test_xlsx_viewer.py, tests/frontend/xlsx-viewer.test.mjs, tests/e2e/xlsx-viewer.spec.js, test_vault/sample-xlsx-lossy.xlsx, .gitea/workflows/ci.yml Garde-fous d'écriture des classeurs Excel : (BUG-085) inspect_workbook() détecte ce qu'un round-trip openpyxl perd (valeurs calculées, slicers, contrôles, connexions, custom XML, signature) → la lecture expose xlsx_lossy_features, la visionneuse affiche une bannière et PUT xlsx/save refuse sans force (409 xlsx_lossy_content, confirmation explicite puis reprise) ; (BUG-086) écriture atomique .tmp + os.replace ; (BUG-087) verrou par fichier (409 conflict, endpoint sync pour le threadpool) ; (BUG-088) une saisie =/@ est stockée en texte (data_type = "s"), sauf opt-in allow_formula / bouton f(x). Le handler ServiceError expose désormais code + details et api() les propage. Périmètre de perte revalidé empiriquement sur openpyxl 3.1.5 (graphiques, images et TCD sont préservés). Vérifié : test_xlsx_viewer.py 31 passed, suite 1390 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules, xlsx-viewer.test.mjs 10/10, E2E 3/3 🟢 corrigé (en attente vérif utilisateur)
(exemple) 2026-06-15 BUG-001 Correction frontend/app.js Réécriture de renderFile() pour préserver le DOM dashboard 🟢 corrigé (en attente vérif)
2026-09-09 BUG-001, BUG-002 Correction backend/main.py, frontend/excalidraw-editor.html, tests/test_pdf_stream.py BUG-001: Content-Disposition RFC 5987 (nom PDF accentué ne casse plus l'en-tête → plus de 500). BUG-002: suppression alias esm.sh (408 jotai) + React 19 cohérent + prop excalidrawAPI → Loading masqué, save OK. Vérifié: 534 tests backend verts + E2E navigateur. 🟢 corrigé (en attente vérif utilisateur)
2026-09-11 BUG-003, BUG-004 Correction backend/{main,indexer,export,pdf_reader,bookslm_routes}.py, backend/auth/router.py, .gitea/workflows/ci.yml, README.md, README.fr.md BUG-003: 33 erreurs mypy corrigées (annotations, gardes None, import PROVIDERS manquant → bug latent) + étape CI mypy rendue bloquante. BUG-004: lien README.md → docs/CONTRIBUTING.md. Vérifié: mypy 0 erreur, ruff OK, pytest 728 passed, frontend OK. 🟢 corrigé (en attente vérif utilisateur)
2026-09-11 BUG-005 Correction frontend/sw.js, frontend/index.html, frontend/js/sync.js, backend/main.py, .gitea/workflows/ci.yml, tests/frontend/sw.test.mjs, tests/test_api_main.py BUG-005: chargement mobile incomplet via Cloudflare. SW réécrit en network-first pour HTML/JS/CSS + caches versionnés ; précache corrigé (/static/js/…) ; kill-switch de session remplacé par une migration localStorage ponctuelle ; en-têtes /static passés de immutable 1 an à no-cache ; index.html/manifest/SPA en no-cache ; reload unique sur controllerchange. Vérifié: 754 tests backend, frontend 9+28+8 OK, ruff/mypy OK. 🟢 corrigé (en attente vérif utilisateur)
2026-09-11 BUG-006 Correction sécurité .env.example Clé API DeepSeek réelle exposée dans .env.example → remplacée par un placeholder. Rotation de la clé à faire côté DeepSeek (présente dans l'historique Git). 🟢 corrigé (rotation à confirmer par l'utilisateur)
2026-09-12 BUG-007, BUG-008, #82 Correction + refonte UI frontend/js/bookslm.js, frontend/js/ai.js, frontend/style.css, frontend/locales/{fr,en}.json, backend/bookslm_routes.py, tests/frontend/ai.test.mjs, tests/test_bookslm.py BUG-007 : motifs Unicode + recherche NFD insensible aux accents + navigation ↑/↓ fiabilisée (jeton de séquence, scrollIntoView). BUG-008 : contexte ad-hoc @ pris en compte en mode Général (backend) + repli vault=all. #82 : section « Fournisseur & modèle » compacte (recherche modèle + bulle capacités ℹ️ au survol/clic/appui long). Vérifié : tests frontend 41/41, tests/test_bookslm.py 47 passed. 🟢 corrigé (en attente vérif utilisateur)
2026-09-12 BUG-009 Correction frontend/js/ai.js, frontend/js/i18n.js, frontend/sw.js, frontend/style.css, frontend/locales/{fr,en}.json, tests/frontend/ai.test.mjs BUG-009 : locale non content-hashée servie depuis le cache HTTP → clés i18n brutes ; passage en cache:'no-store' + SW_VERSION v4. Liste de modèles corrompue (OpenRouter, styles en cache) → styles critiques en ligne, options display:block, plafond 200 + indicateur. Vérifié : tests frontend 42/42. 🟢 corrigé (en attente vérif utilisateur)
2026-09-12 BUG-010, BUG-011 Correction frontend/js/bookslm.js, frontend/js/ai.js, frontend/style.css, tests/frontend/ai.test.mjs BUG-010 : capture du vault des résultats @ + _contextVault() propagé aux requêtes /context//chat (mode Général). BUG-011 : popover modèle aligné à droite, largeur 340 px, noms multi-lignes + title. Vérifié en navigateur (Playwright) et tests frontend 43/43. 🟢 corrigé (en attente vérif utilisateur)
2026-09-12 BUG-012, #82 Correction + UX frontend/js/bookslm.js, frontend/js/ai.js, frontend/style.css, frontend/sw.js, frontend/locales/{fr,en}.json, tests/frontend/ai.test.mjs BUG-012 : le menu @ est toujours rendu et _mentionVault() retombe sur le vault de contexte puis le premier vault disponible. #82 : suppression des intitulés « Fournisseur & modèle » / « Fournisseur : », description du contexte Général déplacée en info-bulle, placeholder retiré, bouton Envoyer en emoji ✈️, indicateur d'activité (envoi/réception/outils/confirmation/succès/échec), SW_VERSION v5. Vérifié Playwright + tests frontend 45/45. 🟢 corrigé (en attente vérif utilisateur)
2026-09-12 BUG-013, BUG-014 Correction frontend/js/bookslm.js, frontend/index.html, frontend/sw.js, tests/frontend/ai.test.mjs BUG-013 : liens fichiers/dossiers via _activeVault() (vault de contexte → sélection → premier vault). BUG-014 : navigation ↑/↓ gérée au niveau du panneau en phase de capture. Purge des caches élargie (SW_VERSION v6, migration obsigate-*). Vérifié : tests frontend 47/47, pytest 930. 🟢 corrigé (en attente vérif utilisateur)
2026-09-12 BUG-015, #82 Correction + UX frontend/js/bookslm.js, frontend/style.css, tests/frontend/ai.test.mjs BUG-015 : sélection des menus / et @ rendue visible (--bg-hover + barre d'accent à gauche) car --surface2 = --bg-primary. #82 : retrait de l'indice clavier sous la zone de saisie. Vérifié Playwright (contraste #1f2430 vs #0f1117 + barre accent) et tests frontend 47/47. 🟢 corrigé (en attente vérif utilisateur)
2026-09-12 #82 Amélioration perf + UX backend/services/search.py, backend/main.py, frontend/js/bookslm.js, frontend/js/ui.js, frontend/style.css, tests/test_api_main.py, tests/frontend/ai.test.mjs #82 : endpoint GET /api/vault/{vault}/paths + préchargement et filtrage client du menu @ (instantané) ; sélecteurs Fournisseur/Modèle agrandis (0,8 rem / 34 px) ; « 🧠 BooksLM » ajouté au menu contextuel de la racine des vaults. Vérifié : pytest 151 (ciblés) + ruff/mypy, tests frontend 49/49. ✅ livré (en attente vérif utilisateur)
2026-09-12 #82 UX frontend/js/ai.js, frontend/style.css, tests/frontend/ai.test.mjs Sélecteurs Fournisseur/Modèle alignés à droite dans la barre de l'assistant et réordonnés : capacité du modèle (ⓘ) → fournisseur → modèle. Bulle de capacités ouverte vers la droite (left: 0). Test JSDOM de l'ordre. Vérifié : tests frontend 49/49. ✅ livré (en attente vérif utilisateur)
2026-09-12 BUG-016 Correction frontend/js/mobile-editor.js, frontend/style.css, tests/frontend/mobile-editor.test.mjs, tests/e2e/mobile-editor.spec.js BUG-016 : le bouton « mode lecture » n'apparaît que si un fichier est ouvert et que l'assistant est fermé, sinon il recouvrait le bouton d'envoi sur mobile. Vérifié : tests frontend 24/24 (mobile) + suite JSDOM verte. 🟢 corrigé (en attente vérif utilisateur)
2026-09-13 BUG-017, BUG-018, BUG-019, BUG-020, #83 Enregistrement docs/ISSUES_TODOLIST.md, docs/ROADMAP.md Signalements mobile consignés : accueil (tuiles Favoris/Récents), éditeur (boutons Annuler/Sauvegarder inaccessibles + barre IA), visibilité de la barre d'outils au-dessus du clavier, double barre de défilement. Feature #83 ajoutée : barre d'outils d'édition mobile style Obsidian Android. 🔴 ouvert (à traiter)
2026-09-13 Revue sécurité statique (BUG-021 → BUG-040, roadmap #84 → #87) Enregistrement docs/ISSUES_TODOLIST.md, docs/ROADMAP.md Revue statique 2026-09-13 consignée : XSS markdown (escape=False) + page de partage publique, brute-force MFA, traversal par préfixe paths.py, ReDoS, SSRF webhooks, cycle de vie des sessions, politique de mot de passe, races users.json, audits IP, rate-limit, indexation symlinks, recherche O(N), CSP. BUG-021→BUG-040 ouverts au registre ; roadmap #84→#87 (consolidation/sécurité, refonte architecturale, performance, CI/CD). 🔴 ouvert (à traiter)
2026-09-13 BUG-021 → BUG-034 (#84 phase 1) Correction backend/services/sanitizer.py, backend/services/paths.py, backend/services/net.py, backend/services/regex_safety.py, backend/main.py, backend/search.py, backend/webhooks.py, backend/indexer.py, backend/ratelimit.py, backend/auth/{router,user_store,password,jwt_handler,middleware}.py, frontend/js/{auth,admin,sync}.js, tests/test_security_hardening.py, tests/test_auth_api.py Sanitizer XSS serveur (markdown + page de partage), rate-limit/lockout MFA, isolation vaults par segments, caps regex (ReDoS), SSRF webhooks + secrets externalisés, rotation/révocation des jetons, politique de mot de passe + invalidation des sessions, verrous users.json, IP réelle dans les audits, rate-limit par compte, symlinks d'index ignorés, recherche simple via inverted index, token en mémoire + cookie HttpOnly + CSP durcie. Vérifié : pytest 961 passed / 6 skipped, ruff 0, mypy 0, tests frontend 36 modules + 9 JSDOM suites verts. 🟢 corrigé (en attente vérif utilisateur)
2026-09-13 BUG-017 → BUG-020, #83 Correction + feature frontend/js/mobile-editor.js, frontend/style.css, frontend/index.html, frontend/locales/{fr,en}.json, tests/frontend/mobile-editor.test.mjs, tests/e2e/mobile-editor.spec.js BUG-017 : cartes de tableau de bord compressibles (min-width:0) ; BUG-018 : en-tête éditeur mobile compact + barre IA tactile ; BUG-019 : ancrage du ruban via visualViewport ; BUG-020 : suppression de la double scrollbar. #83 : ruban d'édition horizontal défilable style Obsidian (commandes étendues + personnalisation ⚙ persistée). Vérifié : tests frontend 35/35 (mobile) + 50/50 (IA) + 9 JSDOM suites. 🟢 corrigé (en attente vérif utilisateur)
2026-09-14 BUG-042 (complément) Correction frontend/js/bookslm.js, tests/frontend/ai.test.mjs, docs/features/ai-app-context.md Prise en charge des espaces dans les noms de fichiers et chemins des liens de l'assistant : cibles markdown avec espaces / <…> / %20 décodés, code inline reconnu (_looksLikePath élargi, rejette toujours les extraits de code), mentions brutes liées uniquement si présentes dans l'index du vault (_linkifySpacePaths + _confirmPathInCache, plus long suffixe aligné sur un mot pour ne pas avaler le mot de prose précédent). Vérifié : tests frontend 57/57 (IA) + 9 suites JSDOM, validate-imports 36 modules, pytest 963 passed / 6 skipped. 🟢 corrigé (en attente vérif utilisateur)
2026-09-14 BUG-041, BUG-042, #88 Correction + feature frontend/js/bookslm.js, backend/bookslm.py, backend/bookslm_routes.py, frontend/locales/{fr,en}.json, tests/test_bookslm.py, tests/frontend/ai.test.mjs, docs/features/ai-app-context.md BUG-041 : contexte de dossier vide → dégradation gracieuse vers le prompt Général + bloc « Dossier vide » (fin du 404). BUG-042 : liens de fichiers déterministes (nom → presse-papiers, dossier → arborescence, chemin → viewer) + résolution du chemin contre l'index du vault avant ouverture. #88 : app_context (documents ouverts, répertoire/vault courants, recherche + résultats affichés) et fichiers récemment modifiés injectés dans le prompt Général. Vérifié : pytest 963 passed / 6 skipped, ruff 0, mypy 0, tests frontend 52/52 + validate-imports 36 modules. 🟢 corrigé (en attente vérif utilisateur)
2026-09-14 BUG-042 (accents + préfixe vault) Correction frontend/js/bookslm.js, tests/frontend/ai.test.mjs, docs/features/ai-app-context.md Caractères accentués : classes de caractères Unicode (\p{L}\p{N}\p{M}) pour PATH_WITH_DIR_RE et PATH_NAME_RE (_looksLikePath), comparaison normalisée NFC via _normKey() → une mention décomposée (e + accent combinant, style macOS) correspond à une entrée d'index précomposée, et les liens sans espace mais accentués sont enfin produits. Préfixe de vault : _splitVaultPrefix() retire un premier segment égal à un vault connu (TestVault/Recettes/Pizza Maison.md) ; _fetchPathsForVault() interroge l'index de ce vault sans écraser le cache du vault actif ; _openFileLink/_revealPath reçoivent le vault cible ; repli « retirer le premier segment » si le préfixe est inconnu. Vérifié : tests frontend 62/62 (IA) + 9 suites JSDOM, validate-imports 36 modules, pytest 963 passed / 6 skipped, et vérification contre l'instance live (données réelles accentuées/espacées : Recettes/Préparation.md, Recettes/Pâtes carbonara.md, Recettes/Pizza Maison.md) — 8/8 contrôles. 🟢 corrigé (en attente vérif utilisateur)
2026-09-14 BUG-043, #82 Correction frontend/js/ai.js, frontend/js/bookslm.js, frontend/js/config.js, tests/frontend/ai.test.mjs, docs/features/ai-provider-picker.md La liste des fournisseurs de la barre latérale de l'assistant suit désormais la configuration du projet : nouveau refreshAIPickers() (ai.js) qui reconstruit chaque picker monté dans son emplacement .ai-picker-slot (host conservé dans la barre de l'assistant, montage par replaceChildren), appelé après saveAIKeys() et deleteAIKey() (config.js) ; slot préservé même sans fournisseur configuré (le premier fournisseur ajouté s'y monte) ; sélection persistée d'un fournisseur non configuré purgée de obsigate_ai_picker (retour au défaut, modèle effacé). Vérifié : tests frontend 66/66 (IA) + 9 suites JSDOM vertes, validate-imports 36 modules, pytest 963 passed / 6 skipped, ruff OK, et vérification en navigateur (Playwright) sur l'instance de test : ajout de nvidia → fournisseur visible sans rechargement, retrait → disparition + sélection réinitialisée ({"provider":null,"model":null}), un seul montage du picker dans son host. CI Gitea verte (lint, test, security, build, e2e).
2026-09-15 BUG-044 Correction backend/provider_capabilities.py (nouveau), backend/model_capabilities.py, backend/main.py, backend/ai_routes.py, tests/test_provider_capabilities.py (nouveau), tests/test_model_capabilities.py, tests/test_ai_models.py, docs/features/ai-provider-picker.md, CHANGELOG.md BUG-044 : les capacités des modèles IA sont désormais lues chez le fournisseur quand il les publie (Mistral capabilities, OpenRouter architecture, détection par forme du payload), mises en cache par GET /api/config/ai-models (aucune requête supplémentaire) et prioritaires sur la table statique qui ne comble plus que les drapeaux non déclarés (repli hors ligne / sans clé). Table statique corrigée : familles vision Mistral (ministral, magistral, mistral-small, mistral-medium, mistral-vibe-cli, labs-leanstral), mistral-ocr = vision sans chat, défaut fournisseur Mistral sans embeddings (mistral-large / codestral n'étaient plus des « embedders »). Vérifié : diagnostic live avant/après sur l'API Mistral (28 modèles vision déclarés, 0 détectés avant → 28 après, 0 écart dans les deux sens), pytest 1007 passed / 6 skipped, ruff 0 (backend), mypy 0 (68 fichiers), tests frontend (unit 9/9, IA 66/66, validate-imports 37 modules), et vérification sur l'instance de test reconstruite (obsigate-test, http://localhost:2020) via les deux endpoints du panneau : mistral-small/medium-latest, ministral-8b-latest, magistral-small-latest → chat,vision ; mistral-ocr-latest → vision seul ; mistral-large-latest/codestral-latest → chat ; mistral-embed → embeddings seul ; deepseek-chat (fournisseur muet) inchangé. 🟢 corrigé (en attente vérif utilisateur)
2026-09-16 BUG-045, BUG-046 Correction frontend/style.css, frontend/js/bookslm.js, backend/bookslm.py, backend/bookslm_routes.py, frontend/sw.js, tests/test_bookslm.py, tests/frontend/ai.test.mjs, tests/frontend/editor-inline.test.mjs, CHANGELOG.md BUG-045 : fin de la double barre de défilement de l'éditeur « Editer » — #editor-body en flex column overflow:hidden, .cm-editor flex:1 1 auto, seul le scroller CodeMirror défile ; suppression de l'override global .cm-scroller (min-height/overflow-y !important). BUG-046 : « Échec de l'action : [object Object] » à l'application d'un ajout de texte au document courant — (1) _responseError() aplatit le detail 422 (tableau d'objets FastAPI) en message lisible, (2) la continuation de confirmation hérite du payload d'origine (le second « Appliquer » ne POSTe plus sans message), (3) le prompt documents/dossier nomme le vault et enjoint les outils d'écriture d'utiliser ce nom exact (le modèle inventait "vault":"test" → échec silencieux). SW_VERSION v20. Vérifié : pytest 1038 passed / 6 skipped, ruff 0 (backend), tests frontend IA 84/84, editor-inline 25/25, validate-imports 38 modules ; reproduction et validation Playwright sur l'instance de test (un seul conteneur scrollable après correctif). 🟢 corrigé (en attente vérif utilisateur)
2026-09-16 #93 (complément) Correction frontend/js/editor-inline.js, frontend/js/utils.js, frontend/js/viewer.js, frontend/js/ui.js, frontend/js/pane-manager.js, frontend/js/sync.js, tests/frontend/editor-inline.test.mjs, docs/features/editeur-inline.md, docs/CONTRIBUTING.md, CHANGELOG.md Garde-fous de session d'édition inline (#93) : l'activation d'un onglet (TabManager.activate, PaneTabManager.activate) appelle detachInlineEditor() avant de vider la zone de contenu, et l'événement SSE index_updated sur le fichier affiché recharge le tampon de l'éditeur (reloadExternalWrite()) au lieu de re-rendre la vue lecture — ces deux chemins détruisaient la session CodeMirror/Forge en cours. Nouveau helper queryEditor() (l'en-tête/pied/marque voyagent avec le conteneur en mode inline, modal.querySelector() les manquait) ; closeEditor() remet le conteneur dans la modale avant de restaurer en-tête/pied/marque. docs/CONTRIBUTING.md documente scripts/install-hooks.sh (hooks de versionnage) dans la mise en place d'un clone. Vérifié : validate-imports 38 modules, unit 9/9, suites JSDOM (editor-inline 25/25, pane-manager, mobile-editor 35/35, toolbar-order, ai 84/84, sw 8/8, collab 10/10, semantic-search 4/4, desktop 23, plugins 21, excalidraw-viewer) toutes vertes, pytest 1082 passed / 6 skipped. 🟢 corrigé (en attente vérif utilisateur)
2026-09-16 BUG-047 Correction + build VERSION (nouveau), scripts/bump_version.py (nouveau), .githooks/prepare-commit-msg + .githooks/post-commit (nouveaux), scripts/install-hooks.sh (nouveau), scripts/bump_version.sh, backend/version.py, Dockerfile, docker-compose.yml, build.sh, .gitea/workflows/ci.yml, desktop/build.rs, package.json, README.md, README.fr.md, docs/ROADMAP.md, docs/DELIVERY_WORKFLOW.md, docs/DEVELOPMENT_AND_RELEASES.md, tests/test_version.py (nouveau) Version alignée de bout en bout. Cause : la version affichée venait du dernier tag git et aucun tag n'était créé aux livraisons → 66 commits livrés mais UI/API figées sur 2.2.1 ; quatre autres numéros codés en dur dérivaient (package.json 1.0.0, desktop 2.0.0, Dockerfile 2.2.1, README 1.7.0). Nouveau modèle : VERSION (racine) = source unique de vérité (MAJEUR.MINEUR.CORRECTIF), incrémentée automatiquement au commit par le hook versionné prepare-commit-msg (SemVer depuis le message : !:/BREAKING CHANGE → MAJEUR, feat → MINEUR, sinon CORRECTIF ; aucun bump pour merge/revert/chore(release)/amend) qui resynchronise package.json, desktop Tauri, ROADMAP, READMEs et publie la section [Unreleased] du CHANGELOG en [X.Y.Z] — date ; post-commit rattache ces fichiers au commit qui vient d'être créé (amend immédiat, le commit n'étant pas encore poussé) puis crée le tag vX.Y.Z, publié au push (push.followTags). bump_version.py (avec --dry-run, --set, --major|--minor|--patch, --print-version) reste utilisable à la main ; scripts/install-hooks.sh installe les hooks. Côté build : Docker COPY VERSION (plus d'ARG VERSION ni d'env compose codés en dur), build.sh et CI lisent VERSION, desktop/build.rs aussi. Vérifié : tests/test_version.py (46 tests, dont le garde-fou TestRepoVersionAlignment : VERSION ↔ package.json ↔ desktop ↔ CHANGELOG ↔ ROADMAP ↔ READMEs ↔ pipeline sans numéro codé en dur), pytest complet, ruff/mypy 0, /api/health → 2.3.0 sur l'instance de test reconstruite. 🟢 corrigé (en attente vérif utilisateur)

| 2026-09-16 | #94, #95, #96, #97 | Feature | backend/ai_history.py (nouveau), backend/bookslm_routes.py, frontend/js/bookslm.js, frontend/js/config.js, frontend/index.html, frontend/style.css, frontend/locales/{fr,en}.json, tests/test_bookslm.py, tests/frontend/ai.test.mjs, docs/features/ai-assistant-history.md (nouvelle), docs/ROADMAP.md, CHANGELOG.md | Assistant IA : #95 historique permanent (backend data/ai_history/{user}.json, cap 200, endpoints CRUD /api/ai/bookslm/history[…] résumés/full, sync frontend debounced 600 ms + repli localStorage + migration des clés legacy bookslm-sessions-*/bookslm-history-*, événement bookslm:history-updated) ; #96 onglet sidebar #sidebar-tab-ai (messages-square) + panneau #sidebar-panel-ai (liste chronologique, ouverture via openWithSession) ; #97 bouton « + » remplaçant « Attach an image » + panneau modulaire .bookslm-ext-menu (registre _extensions : Fichiers, Image, Contextes, Skills, Deep Research = mode agent + prompt, Recherche web & Canva en « Bientôt ») ; #94 bouton d'envoi circulaire + icône Lucide arrow-up. Vérifié : pytest 1088 passed / 6 skipped, ruff 0, mypy 0 (71 fichiers), tests frontend IA 84/84, unit 9/9, validate-imports 38 modules. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-048, #98 | Correction + feature | frontend/js/bookslm.js, frontend/js/config.js, frontend/js/sidebar.js, frontend/style.css, frontend/locales/{fr,en}.json, .gitea/workflows/ci.yml, tests/frontend/ai.test.mjs, tests/frontend/ai-sidebar.test.mjs (nouveau), docs/features/ai-assistant-history.md, docs/ROADMAP.md, CHANGELOG.md | BUG-048 : les entrées « Contextes » et « Skills » du menu « + » ouvraient bien leur menu (@ / /), mais le clic remontait au gestionnaire du panneau qui annulait le rendu asynchrone → menu jamais affiché ; correction par e.stopPropagation() sur les entrées du panneau .bookslm-ext-menu. #98 : la barre de filtrage de la sidebar agit désormais sur l'onglet « Historique IA » — filterAIHistory() (config.js) filtre par titre, aperçu, répertoire, contexte ou libellé de mode, insensible casse/accents (_aiNorm), cache sessions _aiSessionsCache, message « aucune correspondance » (bookslm.history_no_match) dans la liste et placeholder dédié (sidebar.filter_ai) ; initSidebarFilter (sidebar.js) route saisie/touche casse/bouton « × » vers filterAIHistory quand l'onglet IA est actif ; chaque entrée du panneau « + » porte l'icône Lucide plus. Vérifié : tests frontend IA 87/87 (+3), nouvelle suite ai-sidebar 6/6, unit 9/9, 9 suites JSDOM vertes, validate-imports 38 modules, pytest / ruff / mypy inchangés (aucune modification backend). | 🟢 corrigé (en attente vérif utilisateur) — CI Gitea verte (lint, test, security, build, e2e) pour v2.5.0 (run #1511) | | 2026-09-16 | BUG-049, #99, #100 | Correction + feature | frontend/style.css, frontend/js/config.js, frontend/js/viewer.js, frontend/js/sidebar.js, frontend/js/bookslm.js, frontend/locales/{fr,en}.json, .gitea/workflows/ci.yml, tests/frontend/ai.test.mjs, tests/frontend/sidebar-filters.test.mjs (nouveau), docs/features/sidebar-filters.md (nouvelle), docs/features/ai-assistant-history.md, docs/ROADMAP.md, CHANGELOG.md | BUG-049 : icône du bouton « + » de l'assistant invisible — la règle générique .bookslm-input-area button (spécificité supérieure) imposait padding: 8px 16px sur un bouton width: 32px ⇒ largeur de contenu nulle ⇒ SVG width: 0px ; sélecteur porté à .bookslm-input-area button.bookslm-btn-plus (+ :hover), vérifié en navigateur (Playwright : SVG 0 px → 18 px). #99 : la barre de filtrage de la sidebar agit désormais sur les vues Récents (filterRecentFiles, titre/chemin/vault/aperçu/tags) et Sauvegardes (filterSavedSearches, cumulable avec les pills type), insensible casse/accents (_sidebarNorm/_savedNorm), message d'absence de résultat (sidebar.no_results) et placeholders dédiés (sidebar.filter_recent, sidebar.filter_saved) ; initSidebarFilter refactoré en routeFilter/routeClear couvrant les 5 onglets. #100 : « Deep Research » ajoute une pastille .bookslm-chip-deep-research (au lieu d'injecter la directive dans le composeur), active le mode Agent et injecte la directive au moment de l'envoi. Vérifié : tests frontend IA 88/88 (+1), sidebar-filters 8/8 (nouveau), ai-sidebar 6/6, unit 9/9, 9 suites JSDOM vertes, validate-imports 38 modules, vérification navigateur du bouton « + » ; backend inchangé (pytest / ruff / mypy valides). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-050 | Correction | backend/agent/loop.py, backend/services/mutations.py, backend/tools/service.py, backend/bookslm.py, tests/test_agent_loop.py, tests/test_tools_mutations.py, tests/test_api_main.py, docs/features/ai-tools-mcp.md, CHANGELOG.md | BUG-050 : création d'un sous-dossier contenant un fichier en mode Agent. (1) La boucle d'agent renvoyait la conversation sans réponse pour les tool_calls non atteints lorsqu'un appel mutateur déclenchait une confirmation → le provider rejetait le tour de reprise (« tool_call_id » orphelin) ; les appels restants reçoivent désormais un résultat deferred explicite (_deferred_tool_message) que le modèle réémet après confirmation. (2) create_directory est idempotent côté outil IA (exist_ok=True, succès si le dossier existe), le REST restant strict (409). (3) Consignes renforcées : create_file crée les dossiers parents, un seul appel avec chemin imbriqué suffit (backend/bookslm.py, descriptions d'outils). Vérifié : pytest 1091 passed / 6 skipped, ruff 0 (backend), mypy 0 (71 fichiers), tests frontend validate-imports 38 modules + unit 9/9. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-051 | Correction | backend/tools/web.py, tests/test_web_tools.py, docs/features/ai-tools-roadmap.md, docs/features/ai-assistant-conversation-ux.md, docs/ROADMAP.md, CHANGELOG.md | BUG-051 : web_search ne dépend plus d'une seule instance SearXNG. Nouvelle chaîne de fournisseurs (_provider_chain) : SearXNG (auto-hébergé, JSON) → DuckDuckGo (html.duckduckgo.com/html/, extraction result__a/result__snippet, décodage du lien uddg=) → Bing (www.bing.com/search, extraction h2 > a + p.b_lineclamp*, décodage de la redirection u=a1<base64url>), UA navigateur, premier fournisseur non vide retenu et exposé (provider). Le champ warning final liste les fournisseurs essayés ; replis désactivables via OBSIGATE_WEB_FALLBACK=0 ; erreur web_search_unavailable uniquement si tous les fournisseurs sont injoignables. Vérifié : pytest 1082 passed / 6 skipped (14 erreurs MCP préexistantes, sans lien), ruff 0 (backend), mypy 0 (backend/tools/web.py), tests/test_web_tools.py 13/13, recherche live Bing (horaire Canadiens) sur l'hôte. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-051 (complément) | Correction | backend/tools/web.py, CHANGELOG.md | BUG-051 suite : un User-Agent navigateur seul ne suffit pas — Bing renvoie une SERP factice (résultats sans rapport, ex. « highest paying jobs » / « Sam Reid ») aux requêtes sans en-têtes de navigation. Ajout de BROWSER_HEADERS (Accept-Language, Sec-Fetch-*, Upgrade-Insecure-Requests) pour DuckDuckGo et Bing. Vérifié en conteneur (obsigate-test, v2.7.1) : web_search('Canadien de Montreal horaire matchs 2026 2027') → provider: bing, 5 résultats pertinents (nhl.com/fr/canadiens, rds.ca, fr.wikipedia.org). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-052 | Correction | backend/agent/loop.py, tests/test_agent_loop.py, CHANGELOG.md | BUG-052 : la boucle d'agent ne rendait plus jamais de réponse vide. _finalize_answer : à l'épuisement du budget d'itérations (STOP_MAX_ITERATIONS) ou du quota d'appels (STOP_QUOTA_EXCEEDED), un dernier appel LLM sans outil reçoit une instruction de synthèse (« N'appelle plus aucun outil. Réponds maintenant… ») et son texte devient la réponse ; si l'appel échoue ou reste vide, _fallback_summary compose une liste déterministe des sources (web_search/fetch_url) pour ne jamais renvoyer un tour vide. Les tool_calls non atteints lors d'un arrêt sur quota reçoivent un résultat deferred (conversation valide pour la synthèse). Vérifié : tests/test_agent_loop.py 16/16 (+2 : synthèse finale, repli sources), suite complète 1084 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (backend/agent/loop.py). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-053 | Correction | backend/bookslm.py, backend/bookslm_routes.py, tests/test_bookslm.py, CHANGELOG.md | BUG-053 : en mode agent, le prompt Général (et dossier vide) enseignait le protocole texte obsigate-action ; le modèle décrivait donc l'action au lieu d'appeler l'outil natif create_file (bloc volumineux de surcroît tronqué avant fermeture → aucun fichier créé). Le prompt est scindé : GENERAL_ACTION_TOOL_PROTOCOL (appel direct des outils natifs, interdiction explicite des blocs obsigate-action) pour build_general_system_prompt(agent=True), le protocole texte restant utilisé par le chat classique ; _resolve_system_prompt propage agent et ajoute une règle « Mode agent » aux prompts dossier/documents ; max_tokens de l'agent porté à 8192 pour un contenu de fichier complet. Vérifié : tests/test_bookslm.py 68/68 (+3 : prompt agent sans protocole texte, prompt classique inchangé, prompt système de l'endpoint agent), suite complète 1087 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (backend/bookslm.py, backend/bookslm_routes.py). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-054 | Correction | frontend/js/utils.js, tests/frontend/editor-inline.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-054 : le bouton #editor-save (nœud partagé entre toutes les sessions d'édition) restait bloqué sur le spinner de chargement et désactivé — une sauvegarde manuelle (clic ou Ctrl+S) remplaçait le crochet par le loader et ne le restaurait jamais : succès (l'éditeur se ferme, la réouverture réaffichait le spinner), sauvegarde Forge, ou échec réseau (le catch ne restaurait ni l'icône ni l'état). Nouveau helper resetSaveButton() (crochet &#10003;, disabled=false, styles en ligne nettoyés) appelé à l'ouverture (openEditor), à la fermeture (closeEditor) et en cas d'échec (saveFile). Vérifié : tests/frontend/editor-inline.test.mjs 29/29 (+4), validate-imports 38 modules / 0 erreur. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | #101 | Feature | frontend/editor-poc.html, frontend/js/sync.js, frontend/js/viewer.js, frontend/js/utils.js, frontend/index.html, frontend/style.css, frontend/locales/{fr,en}.json, tests/frontend/editor-inline.test.mjs, docs/features/forge-assistant.md (nouvelle), docs/ROADMAP.md, CHANGELOG.md | #101 : le bouton « AI Panel » de Forge ouvre désormais l'Assistant IA partagé (postMessage forge-open-ai → bookslm.openForCurrentContext()) au lieu du mini-chat isolé (supprimé) ; Forge lit localStorage['obsigate_ai_picker'] (aiPickerSelection()) pour ses appels /api/ai/* et sa complétion fantôme (repli ollama), endpoints corrigés (make-longer/make-shorter, target_lang) ; bouton plein écran natif ajouté à Forge (iframe allow="fullscreen") et à Editer (#editor-fullscreen, conteneur #editor-container, sortie à la fermeture, Échap laissé au navigateur) ; i18n editor.fullscreen/editor.exit_fullscreen. Vérifié : tests/frontend/editor-inline.test.mjs 40/40 (+10), unit 9/9, validate-imports 38 modules, 13 suites JSDOM vertes. | 🟢 livré (en attente vérif utilisateur) | | 2026-09-17 | BUG-055 | Correction | frontend/editor-poc.html, frontend/js/autocomplete.js, backend/ai.py, .gitea/workflows/ci.yml, tests/frontend/forge-completion.test.mjs (nouveau), CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-055 : trois gestionnaires keydown Tab indépendants s'exécutaient à chaque appui — l'indentation (insertAtCursor(' ')) s'ajoutait à la complétion de mot (insertAtCursor(suffixe)) et à l'acceptation du ghost text, d'où l'espace parasite avant le mot complété puis un effacement destructeur. Gestion unifiée de Tab (liste ouverte > ghost > mot du document > indentation), helpers purs partagés (getWordFragment/findWordCompletions/normalizeGhost/chooseTabAction) extraits dans autocomplete.js, liste déroulante au curseur quand plusieurs mots correspondent, ghost positionné au curseur (plus de miroir du document entier, nettoyé au déplacement/scroll), complétion de mot sans espace garantie et prompt /api/ai/inline-complete simplifié (128 tokens). Vérifié : tests/frontend/forge-completion.test.mjs 28/28 (nouveau), unit.test.mjs 9/9, editor-inline.test.mjs 40/40, ai.test.mjs 88/88, validate-imports 38 modules, pytest 1101 passed / 6 skipped, ruff 0, mypy 0. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-055 (complément) | Correction | frontend/editor-poc.html, frontend/js/utils.js, tests/frontend/forge-completion.test.mjs, tests/frontend/editor-inline.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-055 (complément) : une complétion acceptée au Tab disparaissait 1–2 s plus tard. Cause : l'auto-sauvegarde (2 s) déclenche un index_updated SSE sur le fichier affiché, et reloadExternalWrite rechargeait le tampon Forge depuis le disque, écrasant toute frappe postérieure à la sauvegarde. Le rechargement SSE est désormais ignoré si le tampon est modifié (parent-reload sans force + isDirty ; garde équivalente sur le point d'auto-sauvegarde CodeMirror) ; seul obsigate:file-written (assistant IA) passe force=true. L'auto-sauvegarde ne remet plus l'état « enregistré » si des modifications sont arrivées pendant la requête (Forge + CodeMirror), et acceptGhost() annule la requête de prédiction en attente. Vérifié : forge-completion.test.mjs 31/31 (+3), editor-inline.test.mjs 41/41 (+1), 14 suites frontend vertes, validate-imports 38 modules. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-056 | Correction | frontend/editor-poc.html, frontend/js/sync.js, tests/frontend/forge-completion.test.mjs, tests/frontend/editor-inline.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-056 : en plein écran Forge, l'Assistant IA s'ouvrait en arrière-plan. La sortie du plein écran est désormais faite côté iframe (openAssistant → document.exitFullscreen() puis postMessage à la résolution) et côté parent (sync.js sur forge-open-ai → document.exitFullscreen() puis openForCurrentContext()), car le plein écran peut appartenir au document parent (l'iframe voit alors fullscreenElement nul et sa sortie échoue — c'était le cas non couvert par le premier correctif). Vérifié : forge-completion.test.mjs 32/32 (+1), editor-inline.test.mjs 42/42 (+1), 14 suites frontend vertes, validate-imports 38 modules. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-057, #102 | Correction + feature | frontend/js/bookslm.js, frontend/editor-poc.html, frontend/style.css, frontend/locales/{fr,en}.json, tests/frontend/ai.test.mjs, tests/frontend/editor-inline.test.mjs, docs/archive/COMPLETED_v1-v2.md, docs/ROADMAP.md, CHANGELOG.md | BUG-057 : le bouton « Ajouter » de l'assistant ne ciblait que state.editorView (CodeMirror) ; en Forge il affichait « Aucun document ouvert dans l'éditeur ». _insertIntoEditor() gère désormais les trois surfaces : CodeMirror, l'iframe Forge (postMessage({ type: 'parent-insert', text }) → insertAtCursor dans editor-poc.html) et le textarea de repli. #102 : chaque bloc de code d'une réponse reçoit un bouton « Ajouter la section » (.bookslm-code-insert, révélé au survol) qui insère le contenu du bloc sans les délimiteurs ```. Vérifié : ai.test.mjs 91/91 (+3), editor-inline.test.mjs 43/43 (+1), forge-completion.test.mjs 32/32, unit 9/9, validate-imports 38 modules, pytest 1101 passed / 6 skipped, ruff 0, mypy 0. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-058 | Correction | frontend/style.css, tests/frontend/editor-inline.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-058 : la barre de numérotation de ligne de l'éditeur « Editer » ne suivait pas le thème — CodeMirror peint .cm-gutters avec des valeurs claires codées en dur (#f5f5f5, bordure #ddd), visibles en thème sombre. Correctif : le gutter dérive des variables CSS ObsiGate (background: color-mix(in srgb, var(--text-primary) 5%, transparent), color: var(--text-secondary), border-right: 1px solid var(--border), ligne active color-mix(… 10% …) / --text-primary), donc il suit les 15 thèmes et les 4 modes. Vérifié : editor-inline.test.mjs 44/44 (+1), unit 9/9, validate-imports 38 modules, pytest 1101 passed / 6 skipped, ruff 0, mypy 0, et Playwright sur l'instance de test (route style.css remplacée par le fichier local) — sombre color(srgb 0.90 0.93 0.95 / 0.05) + bordure #21262d, clair color(srgb 0.12 0.14 0.16 / 0.05) + bordure #d0d7de, plus de rgb(245,245,245). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-059 | Correction | frontend/js/bookslm.js, tests/frontend/ai.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-059 : dans une conversation ouverte (post ancré en haut), tout clic dans la fenêtre de messages — lien de fichier, étapes, sélection de texte — faisait sauter toute la conversation au bas de la fenêtre. Cause : le gestionnaire mousedown de dépintage (prévu pour la molette/tactile/poignée de scroll) se déclenchait aussi sur un simple clic, et le retrait du padding d'ancre (paddingBottom) bornait le scrollTop à la nouvelle hauteur max → saut au bas. Correctif : helper pur isScrollbarPress(target, clientX, container) — un appui ne dépine que s'il vise la poignée de scroll (cible = conteneur + zone de gouttière droite) ; molette et tactile conservent leur comportement. Vérifié : ai.test.mjs 92/92 (+1), unit 9/9, validate-imports 38 modules, pytest / ruff / mypy inchangés côté backend. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-060 | Correction | frontend/js/viewer.js, .gitea/workflows/ci.yml, tests/frontend/pdf-viewer.test.mjs (nouveau), tests/e2e/pdf-viewer.spec.js (nouveau), test_vault/sample-pdf.pdf (nouveau), CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-060 : l'ouverture d'un PDF n'affichait aucune page (barre d'outils « PDF — N pages » présente, corps vide). Cause : la CSP durcie en BUG-034 pose object-src 'none' — directive qui gouverne <embed>/<object> — alors que le viewer rendait le PDF via <embed type="application/pdf"> : le lecteur natif était bloqué. Correctif : rendu dans une <iframe> (autorisée par frame-src 'self', le stream /api/file/{vault}/pdf/stream étant same-origin) ; object-src 'none' conservé. Tests : pdf-viewer.test.mjs 6/6 (statique : pas d'<embed>, CSP frame-src 'self', iframe pleine hauteur), pdf-viewer.spec.js (E2E : iframe + stream application/pdf 200/206 + zéro violation CSP ; échoue bien avec l'ancien <embed>). Vérifié : pytest 1184 passed / 6 skipped, frontend 14 suites JSDOM vertes, validate-imports 38 modules, ruff/mypy 0. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-035, BUG-036, BUG-037, BUG-038, BUG-039, BUG-040 | Correction | backend/secret_redactor.py, backend/collab.py, backend/auth/{middleware,password,router}.py, backend/indexer.py, backend/main.py, tests/test_api_main.py, tests/test_auth.py, tests/test_auth_api.py, tests/test_collab.py, tests/test_pdf.py, CHANGELOG.md, docs/ISSUES_TODOLIST.md | Lot de 6 bugs mineurs (P2/P3) : BUG-035 masquage hex conditionné au contexte (git/SHA épargnés) ; BUG-036 jeton WebSocket cookie-only (plus de ?token=) + plafond de trame 16 Mio ; BUG-037 garde-fou au démarrage (refus d'un bind public sans auth sauf OBSIGATE_ALLOW_INSECURE=true) ; BUG-038 Argon2 recalibré 19 Mio/t=2/p=1 ; BUG-039 login uniforme 401 (fini 429/403 distinctifs) ; BUG-040 extraction PDF différée via enrich_pdf_texts(). Vérifié : pytest 1204 passed / 6 skipped, ruff 0, mypy 0 (77 fichiers), frontend validate-imports 38 modules + unit 9/9. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-061, BUG-062, BUG-063 | Correction | frontend/style.css, frontend/js/viewer.js, tests/frontend/ai.test.mjs, tests/frontend/pdf-viewer.test.mjs, tests/e2e/pdf-viewer.spec.js, test_vault/sample-pdf-toc.pdf (nouveau), CHANGELOG.md, docs/ISSUES_TODOLIST.md | Viewer PDF & assistant IA : BUG-061 le bouton plein écran du panneau assistant l'emportait mal sur la largeur inline (redimensionnement/persistée) → width: 100vw !important ; BUG-062 le plafond de lecture 1200 px s'appliquait au PDF quand la navigation était masquée → :has(.pdf-viewer-container) en max-width:none ; BUG-063 la TOC PDF ne naviguait pas (contentWindow = about:blank) → navigatePdfToPage() recharge l'iframe avec #page=N, liens data-page sans onclick inline. Vérifié : ai.test.mjs 93/93, pdf-viewer.test.mjs 8/8, validate-imports 38 modules (311 exports), unit 9/9, E2E pdf-viewer.spec.js 3/3, et Playwright sur l'instance de test (plein écran 640→1400 px, src → #page=3, max-width:none). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-063 (complément) | Correction | frontend/js/viewer.js, tests/frontend/pdf-viewer.test.mjs, tests/e2e/pdf-viewer.spec.js, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-063 non résolu au premier correctif : définir iframe.src = base + '#page=N' ne change que le fragment → navigation same-document que le lecteur PDF natif ignore. Diagnostic en Chrome headful (comparaison de captures) : fragment présent au chargement = OK ; changement de fragment après chargement = aucun effet ; changement de query + fragment = OK. navigatePdfToPage() ajoute donc un paramètre de query horodaté (&_pdfpage=<ts>#page=N) pour forcer un vrai rechargement. Vérifié via l'UI de l'app (Chrome headful) : page 1 → page 8 → retour page 1 (hash de capture identique au retour). Tests : pdf-viewer.test.mjs 8/8, E2E pdf-viewer.spec.js 3/3. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-064 | Correction | frontend/excalidraw-editor.html, backend/main.py, tests/frontend/excalidraw-viewer.test.mjs, tests/test_security_hardening.py, tests/e2e/excalidraw.spec.js, test_vault/diagram-app-export.excalidraw (nouveau), CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-064 : aucun diagramme Excalidraw ne s'affichait (canvas vide). Diagnostic navigateur : .excalidraw sans hauteur fixe → boucle de redimensionnement 525 → 56 181 → 33 554 432 px (2^25, plafond Excalidraw) ; canvas de 33 Mpx impossible à dessiner → scène blanche. Cause 1 : la feuille de style @excalidraw/excalidraw n'était jamais chargée (seuls 18 règles CSS présentes, toutes ObsiGate) — l'éditeur était non stylisé. Correctif : <link rel="stylesheet" href="https://esm.sh/@excalidraw/[email protected]/dist/prod/index.css"> + https://esm.sh ajouté à style-src de la CSP. Cause 2 : appState.collaborators (Map sérialisée en objet JSON par l'app/plugin) faisait planter Excalidraw 0.18 (collaborators.forEach is not a function) ; sanitizeAppState() reconvertit en Map et écarte la géométrie de viewport importée (width/height/offsetLeft/offsetTop). Vérifié Playwright sur l'instance de test (port 2020) : hauteur canvas 525 px, rectangle + losange affichés, UI stylisée, 0 pageerror. Tests : excalidraw-viewer.test.mjs 8/8 (dont 3 nouveaux), TestCspExcalidrawStylesheet (pytest), E2E (hauteur de canvas bornée). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-064 (complément) | Correction | frontend/style.css, tests/frontend/excalidraw-viewer.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-064 (complément) : quand la barre de navigation gauche est masquée, le viewer Excalidraw restait borné à la colonne de lecture centrée de 1200 px. La règle .sidebar.hidden ~ .content-wrapper .content-area { max-width: 1200px } s'appliquait au viewer comme aux notes. Ajout de .content-area:has(iframe[src*="excalidraw-editor.html"]) en max-width: none; margin: 0 (même traitement que les viewers PDF/image, BUG-062). Vérifié Playwright (viewport 1400 px) : contenu 1115 → 1400 px, iframe 1035 → 1320 px, max-width calculé none. Test statique ajouté (excalidraw-viewer.test.mjs 9/9). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | BUG-065, #78 (complément) | Correction + feature | frontend/js/excalidraw-viewer.js, frontend/js/utils.js, frontend/excalidraw-editor.html, tests/frontend/excalidraw-viewer.test.mjs, docs/features/excalidraw.md, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-065 : l'auto-save Excalidraw (débounce 2 s) déclenchait PUT save → SSE index_updated → reloadExternalWrite → openFile → recréation de l'iframe = refresh visible pendant le dessin. Auto-save retirée (excalidraw-viewer.js : plus de requestSave/saveTimer), sauvegarde explicite (bouton 💾 / Ctrl+S) ; reloadExternalWrite (utils.js) court-circuite le re-rendu si un iframe Excalidraw est ouvert sur ce fichier (attributs data-excalidraw-vault/data-excalidraw-path) ; le badge « Modified » suit désormais une signature des éléments (id:versionNonce) au lieu de tout onChange — resize/zoom/plein écran ne marquent plus le fichier modifié. #78 (complément) : bouton plein écran #btn-fullscreen dans la barre d'outils de l'éditeur (requestFullscreen sur le document de l'iframe) + iframe créée avec allow="fullscreen" allowfullscreen. Vérifié Playwright : bascule plein écran OK (document.fullscreenElement true→false), badge non modifié après bascule ; tests statiques excalidraw-viewer.test.mjs 12/12, validate-imports 38 modules, unit 9/9. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-17 | #78 (complément) | UI | frontend/excalidraw-editor.html, docs/features/excalidraw.md, CHANGELOG.md | #78 (complément) : la barre d'outils de l'éditeur Excalidraw passe en colonne d'icônes (34×34 px, SVG seuls), collée au bord droit (right: 0 ; top: 45% ; empilement vertical), avec title/aria-label. L'icône du bouton Save est remplacée par une coche pendant 1,2 s après une sauvegarde réussie. Badge « Modifié » réduit à une pastille. Vérifié Playwright : bord droit au bord de l'iframe, haut 45 %, 4 boutons empilés ; bascule plein écran OK, cycle d'icône Save + PUT save observés. | 🟢 livré (en attente vérif utilisateur) | | 2026-09-18 | BUG-066 | Correction | frontend/locales/fr.json, frontend/locales/en.json, tests/frontend/unit.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-066 : la table des matières de la page de configuration n'affichait aucune icône pour « Fichiers cachés » et « Partages publics ». Les libellés du sommaire proviennent de clés i18n (config.section_hidden, config.section_shares) distinctes des titres de section qui, eux, portaient déjà l'icône. Alignement : 🗂️ / 📤 en FR et EN. Test de non-régression : unit.test.mjs vérifie que toutes les entrées .help-nav-link du sommaire portent une icône dans les deux langues (17/17). Vérifié : unit.test.mjs 10/10, validate-imports 38 modules. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-18 | #105, BUG-067 | Documentation + correction | frontend/index.html, frontend/js/config.js, frontend/style.css, frontend/locales/{fr,en}.json, backend/guide_export.py, backend/main.py, tests/test_guide.py, docs/features/guide-coverage-105.md, CHANGELOG.md, docs/ROADMAP.md, docs/ISSUES_TODOLIST.md | #105 : audit complet de couverture du Guide d'utilisation — 8 nouvelles sections (Architecture + diagramme Mermaid, API & intégrations, Diagrammes Mermaid & Excalidraw, Hors-ligne & synchronisation, Collaboration temps réel, Application desktop, Bibliothèque & signets, Multilingue) et compléments (recherche sémantique, MFA/WebAuthn, notifications push, exports HTML/ePub/ZIP, PDF, vue multi-panneaux, admin). Téléchargement du guide en Markdown et PDF (GET /api/guide/download?format=md|pdf, FR/EN, rendu par le moteur d'export existant). Guide plus large en desktop. BUG-067 : ancre morte #help-mobile-editor → section dédiée créée. | 🟢 corrigé (en attente vérif utilisateur) | 2026-09-18 | #105 (ajustements) | Amélioration | frontend/index.html, frontend/js/config.js, frontend/sw.js, frontend/locales/{fr,en}.json, backend/guide_export.py, backend/pdf_export.py, Dockerfile, scripts/build_guide_diagrams.py, scripts/render_guide_diagram.mjs, scripts/guide_content.py, backend/assets/guide_diagrams/df7366a40db6a5a2.png, tests/test_guide.py, docs/features/guide-coverage-105.md, CHANGELOG.md | #105 (retour utilisateur) : 1) boutons de téléchargement du guide passés en icônes seules (tooltips i18n conservés) ; 2) le diagramme Mermaid de la section Architecture est désormais rendu en vraie image dans le PDF (pipeline de pré-rendu PNG Chromium+mermaid v11, PNG commité sous backend/assets/guide_diagrams/<sha1>.png, résolu par diagram_png_for() ; le Markdown garde le fenced mermaid) ; 3) emoji du PDF rendus en couleur au lieu de rectangles : fonts-noto-color-emoji ajouté au Dockerfile + "Noto Color Emoji" en fin de pile de polices PDF. Vérifié : pytest 1218 (test_guide ×13), ruff/mypy 0, validate-imports 38, unit 10/10 ; PDF live conteneur 2020 : 24 pages, 0 glyphes tofu, diagramme 3568x1174 embarqué. | 🟢 livré | 2026-09-22 | BUG-068 | Correction | backend/auth/router.py, backend/requirements.txt, frontend/js/auth.js, frontend/style.css, frontend/locales/{fr,en}.json, tests/test_mfa.py, tests/frontend/mfa-settings.test.mjs (nouveau), CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-068 : section « 🔒 Sécurité du compte » finalisée. (1) Boutons hors thème : config-btn-primary/config-btn-danger n'existaient pas en CSS → définis depuis les variables du thème (+ états disabled). (2) QR invisible : l'image tierce était bloquée par la CSP (img-src 'self' data: blob:) et exposait le secret TOTP → QR SVG data: généré en local par le backend (qr_data_url, segno) avec repli saisie manuelle. (3) Codes de récupération perdus à la 1re activation WebAuthn → _showRecoveryCodes(codes, targetId) avec repli webauthn-flow-area. (4) Carte « Mot de passe » ajoutée (endpoint change-password existant, jusque-là sans UI) + échappement des libellés de clés WebAuthn. Vérifié : pytest 1241 passed / 6 skipped, ruff 0, mypy 0 (78 fichiers), mfa-settings.test.mjs 9/9, unit 10/10, validate-imports 39 modules. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-23 | BUG-069 | Correction | frontend/js/auth.js, frontend/locales/{fr,en}.json, tests/frontend/mfa-settings.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-069 : login 2FA bloqué sans erreur — après user+pwd corrects, showMfaChallenge cherchait .login-box (inexistant dans index.html, marquage réel #login-screen > .login-card) et faisait un return silencieux : page de login figée, aucune erreur. Correctif : montage dans .login-card (repli #login-screen) + erreur visible mfa.challenge_unavailable (FR/EN) si le point de montage manque. Reproduit au navigateur (Playwright, instance Docker obsigate-test, compte jetable avec TOTP) : avant → challenge jamais affiché ; après → challenge affiché, code erroné → erreur, code valide (verify 200) → app. Tests : mfa-settings.test.mjs 11/11 (+2 ancrage DOM), unit 10/10, validate-imports 39 modules. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-23 | BUG-070 | Correction | backend/auth/webauthn_mfa.py, backend/auth/router.py, .env.example, tests/test_webauthn.py, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-070 : activation WebAuthn rejetée en 400. (1) Défauts localhost sans port → resolve_relying_party() dérive rp_id/origines de la requête (config explicite prioritaire, forwarded sous TRUST_PROXY), appliqué aux endpoints register + login. (2) Challenge single-use → 5 derniers conservés, vérification contre le challenge de la cérémonie en cours. Vérifié au navigateur (authentificateur virtuel CDP, instance Docker) : register 200, clé listée, clé de test retirée. Tests : test_webauthn.py 19/19 (+8), suite complète 1249 passed / 6 skipped, ruff/mypy 0. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-23 | BUG-071 | Correction | frontend/index.html, frontend/js/config.js, frontend/js/i18n.js, frontend/style.css, frontend/locales/{fr,en}.json, tests/frontend/config-mobile.test.mjs (nouveau), .gitea/workflows/ci.yml, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-071 : page « Configurations » inutilisable en mobile. (1) #config-nav masquée sous 768px sans toggle → hamburger #config-hamburger ajouté à l'en-tête (.help-hamburger, libellé config.toc_toggle FR/EN). (2) Ancres brutes sans JS → interception en config.js (scroll doux, lien actif, repli auto mobile, reset à l'ouverture). (3) Débordements 360px → bloc CSS mobile #config-modal (sommaire haut 46vh, grilles 1fr, add-rows wrap + largeurs inline neutralisées, items wrap, cibles 44px). data-i18n-attr multi-paires (;). Vérifié : config-mobile.test.mjs 11/11 (nouveau, au CI), pytest 1249 passed / 6 skipped, ruff/mypy 0, validate-imports 39 modules, unit 10/10, JSDOM ai 93/93 + ai-sidebar 6/6 + sidebar-filters 8/8 + mobile-editor 35/35 + config-ai-keys 7/7. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-23 | BUG-071 (complément E2E) | Test | tests/e2e/config-mobile.spec.js (nouveau), CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-071 (complément E2E) : spec Playwright mobile (convention mobile-editor.spec.js : test.skip hors viewport ≤768px, donc inactive sur le projet chromium-desktop du CI). Vérifié en local sur l'instance de test (port 2029, auth désactivée) : hamburger → sommaire, sélection → scroll + actif + repli, 0 débordement horizontal à 393px (3/3 chromium-mobile, 3 ignorés en desktop) ; suite mobile-editor.spec.js intacte (3/3). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-23 | BUG-072 | Correction | frontend/js/viewer.js, frontend/style.css, tests/frontend/image-viewer.test.mjs, tests/e2e/image-viewer.spec.js, scripts/run-e2e-local.ps1 (nouveau), package.json, AGENTS.md, README.md, README.fr.md, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-072 : dans la visionneuse d'images (#108-D), le plein écran (lightbox) et le panneau « Métadonnées » étaient perdus dès qu'on changeait d'image avec ←/→ (ou la pellicule), car openFile → renderFile recrée entièrement renderImageViewer. (1) Persistance : état module _imageViewerState { lightbox, meta } restauré à chaque rendu ; un drapeau _imageViewerNavPending posé par go() et le clic de vignette indique à renderFile que le rendu suivant est une navigation image→image (pas de réinitialisation) — toute autre ouverture repart à zéro. (2) Panneau latéral : .image-meta-panel déplacé dans un nouveau .image-viewer-body en flex row, à droite de .image-stage (border-left, width:280px; max-width:40%, défilement vertical) au lieu d'une bande sous la pellicule ; la règle lightbox ne masque plus que la pellicule. Boutons stables image-btn-lightbox/image-btn-metadata + aria-pressed, Escape resynchronise l'état. Tests statiques image-viewer.test.mjs (+2) et E2E Playwright (+1). Diagnostic E2E : npm run test:e2e bloquait car bash résout vers WSL (HS, Ubuntu Stopped, HCS_E_CONNECTION_TIMEOUT) et git-bash est bloqué par App Control → lanceur PowerShell ajouté. Vérifié : image-viewer.spec.js 4/4, suite chromium-desktop complète 103 passed / 6 skipped (10,3 min) via scripts/run-e2e-local.ps1, image-viewer.test.mjs 12/12, unit 10/10, validate-imports 40 modules. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-23 | BUG-073 | Correction | frontend/style.css, tests/frontend/mobile-toolbar.test.mjs (nouveau), tests/e2e/mobile-toolbar.spec.js (nouveau), .gitea/workflows/ci.yml, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-073 : en mobile (≤768px), la barre fixe #mobile-toolbar (64px + safe-area) recouvrait le bas de tous les documents/pages — fin de contenu inaccessible. (1) Cause : la règle de clearance du bloc @media (max-width: 768px) ciblait .main-layout, classe absente de index.html (vrai conteneur : .main-body) → sélecteur mort, zéro dégagement ; règle sœur morte .editor-modal.active ~ .main-layout { padding-bottom: 0 } supprimée (overlay plein écran / nécessaire en édition inline). (2) Correctif : .main-body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0)) }, reset body.reading-mode .main-body { padding-bottom: 0 } (barre masquée en mode lecture), body.np-active .content-area ramené de calc(64px+safe+76px) à 76px (dégagement dock seul — géométrie totale identique, pas de double comptage avec .main-body). Tests : mobile-toolbar.test.mjs (7 statiques, ajouté au CI), E2E mobile-toolbar.spec.js (géométrie + scroll fin de ANALYSE_REVIEW.md, skip hors viewport ≤768). Vérifié : mobile-toolbar 7/7, JSDOM 14 suites 0 échec, E2E chromium-mobile BUG-073 2/2 + régressions mobile-editor/config-mobile 6/6, suite chromium-desktop complète 106 passed / 9 skipped (11,4 min), pytest 1302 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules, unit 11/11. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-24 | #114 | Feature | frontend/index.html, frontend/js/config.js, frontend/style.css, frontend/locales/{fr,en}.json, tests/frontend/config-mobile.test.mjs, tests/e2e/config-mobile.spec.js, docs/features/settings-mobile-114.md (nouvelle), docs/ROADMAP.md, CHANGELOG.md, docs/ISSUES_TODOLIST.md | #114 — Configuration, refonte mobile-responsive (≤768px). (1) Drawer sommaire : #config-nav en panneau position: fixed (min(320px, 88vw), z-index 40) sous backdrop #config-modal.config-toc-open::before (z-index 35) ; bouton #config-toc-close ; backdrop/Échap ferment le drawer d'abord puis la modale ; _setConfigNav bascule la classe + conserve le display inline de reset. (2) Modale plein écran : 100dvh + padding: 0, border-radius: 0. (3) Tactile : boutons/liens ≥44px, inputs/selects 16px + min-height: 44px (anti-zoom iOS, scopé #config-modal), rangée .config-actions-row sticky column + safe-area, formulaires 1 colonne, MFA 1 colonne + code full-width, wrap webhook/token/share/diag/avatar/webauthn. (4) Dettes HTML/i18n : .config-actions-row replacée dans #cfg-backend-settings (</section> orphelin supprimé), id dupliqué cfg-partages-publics retiré du <h2>, #plugins-settings-container supprimé, doublons .config-btn-add + règle morte .mfa-recovery-input purgés, #mt-explorer → data-i18n="settings.explorer" ; i18n : clés mortes settings.{backend,backend_hint,restart_badge,save,plugins} supprimées, settings.explorer + config.toc_close ajoutées, settings.tabs FR = « Onglets ». Vérifié : config-mobile.test.mjs 27/27 (au CI), unit 11/11, validate-imports 40 modules, pytest 1302 passed / 6 skipped, ruff/mypy 0, E2E chromium-mobile 5/5. | ✅ livré (en attente vérif utilisateur) | | 2026-09-24 | BUG-074 → BUG-077 | Correction | backend/agent/loop.py, backend/bookslm_routes.py, frontend/js/bookslm.js, frontend/style.css, frontend/index.html, frontend/locales/{fr,en}.json, frontend/sw.js, tests/test_agent_loop.py, tests/test_bookslm.py, tests/frontend/ai.test.mjs, tests/frontend/editor-inline.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | Lot assistant IA (mode agent) : (BUG-075) confirmation par lot — pending.actions regroupe toutes les mutations d'un tour LLM, un unique bouton « Tout approuver (N) » envoie confirm_all (ToolContext.confirmed) et n'interrompt plus à chaque action ; les lectures du lot s'exécutent aussitôt. (BUG-074) résumé du bloc d'étapes avec titre de la 1re action + compteur limité aux actions, reprise diffusée dans le même message (fini le « 1 étape » fragmenté). (BUG-076) refresh de l'arborescence débouncé sur les events tool mutateurs + _notifyFileWritten étendu aux documents (xlsx/docx/csv/pdf). (BUG-077) le bouton d'envoi devient « Stop » pendant le stream (abort SSE, tâche serveur annulée à la déconnexion, marqueur « Exécution arrêtée. »). Guide/i18n FR/EN + ai.stop/ai.stopped/ai.confirm_actions/ai.action_apply_all ; SW_VERSION v25. Vérifié : pytest 1304 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules, unit 11/11, ai 100/100, editor-inline 44/44, mobile-editor 35/35, ai-sidebar 6/6, forge 32/32, pane-manager 9/9, sw 8/8. | 🟢 corrigé (en attente vérif utilisateur) |

| 2026-09-24 | #115, #117, BUG-078 | Feature + correction | frontend/js/themes.js, frontend/js/ui.js, frontend/js/viewer.js, frontend/js/config.js, frontend/index.html, frontend/style.css, frontend/popout.html, frontend/locales/{fr,en}.json, frontend/icons/avatar/* (nouveau), tests/frontend/unit.test.mjs, tests/frontend/toolbar-order.test.mjs, tests/frontend/settings-order-avatar.test.mjs, docs/features/viewer-toolbar-highlight-avatars.md (nouvelle), docs/ROADMAP.md, CHANGELOG.md | #115 barre d'outils de lecture épinglée : viewer.js/popout.html sortent .file-actions de .file-header dans un .file-toolbar enfant direct de .content-area (position: sticky; top: 0), masqué en mode lecture. BUG-078 coloration syntaxique : le basculement des feuilles highlight.js suit le mode (themes.applyTheme + ui.initTheme/applyTheme lisent obsigate-theme-mode) au lieu de la clé de thème qui désactivait les deux feuilles. #117 avatars prédéfinis : galerie de 12 images (frontend/icons/avatar/) dans #cfg-profile, clic → recadrage 256 px (pipeline import) + PATCH /api/auth/me, avatars actifs surlignés (obsigate-avatar-preset), import personnalisé et suppression conservés. Vérifié : Playwright (coloration 5/5 déterministe, toolbar épinglée à barTop constant au défilement), unit.test.mjs 12/12, toolbar-order 13/13, settings-order-avatar 12/12, JSDOM editor-inline/pane-manager/mobile-editor/image-viewer/pdf-viewer/config-mobile/media-viewer/excalidraw verts, pytest 1304 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-24 | BUG-079 | Correction | backend/main.py, tests/test_api_main.py, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-079 : GET /api/diagnostics renvoyait 500 « dictionary changed size during iteration ». Le handler itérait inv.word_index.values() et index.items() en direct alors que l'indexeur les modifiait depuis un autre thread (rebuild initial dans _search_executor, hooks incrémentaux add_document/remove_document) → RuntimeError dans le générateur. Correctif : snapshot avant itération (list(index.items()), inv.word_index.copy()) — copie C atomique sous le GIL, pas de verrou ajouté. Test de non-régression déterministe (RaceDict fait grossir le dict pendant l'itération ; échoue sans le correctif, passe avec). Vérifié : pytest 1305 passed / 6 skipped, ruff 0, mypy 0 (80 fichiers), validate-imports 40 modules, unit 12/12. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-27 | BUG-080, BUG-081 | Correction + enregistrement | scripts/run-e2e-local.ps1, scripts/run-e2e-local.sh, scripts/e2e-server.ps1, playwright.config.ts, tests/test_e2e_harness.py (nouveau), CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-080 : run E2E local pendu toute la nuit → harnais anti-blocage : npx --yes (plus de prompt interactif), install Chromium sautée si présent (E2E_INSTALL_BROWSERS=1), timeouts E2E_TIMEOUT_SEC (900)/E2E_BROWSER_INSTALL_TIMEOUT_SEC (600, exit 124), globalTimeout Playwright (15 min local / 30 min CI, E2E_GLOBAL_TIMEOUT_MS), pidfile resynchronisé sur le vrai owner du port + stop qui tue l'arbre complet (orphelins 81180/81936 nettoyés, port 2029 libéré). Diagnostic : double processus systématique (parent .venv parqué + enfant qui sert — environnemental, aussi sur flowdeck/3.13). BUG-081 (ouvert, non traité) : GET /api/auth/mfa/status → 500 auth désactivée (user None, router.py:827, reproduit live). Vérifié : test_e2e_harness.py 8/8, cycle start/stop live (pidfile cohérent, port libéré). | 🟢 corrigé (en attente vérif utilisateur) ; BUG-081 🔴 ouvert | | 2026-09-27 | BUG-082 | Correction CI | .gitea/workflows/ci.yml, tests/test_ci_workflow.py, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-082 : lint rouge (ERR_MODULE_NOT_FOUND: jsdom, rouge depuis 7bee4a2) — les fichiers de l'étape frontend racine à import statique jsdom (upload.test.mjs, puis config-ai-keys.test.mjs révélé par le CI après le 1er fix), alors que jsdom n'est installé que dans tests/frontend/node_modules (étape JSDOM). Les deux déplacés dans l'étape JSDOM (les deux branches) ; garde-fou test_ci_workflow.py généralisé (aucun fichier racine à import statique jsdom + suites verrouillées en JSDOM, contre-preuve OK). Vérifié : étape racine verte (11 suites) + upload et config-ai-keys verts depuis tests/frontend/. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-27 | BUG-083 | Correction CI | .gitea/workflows/ci.yml, tests/test_ci_workflow.py (nouveau), CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-083 : job security rouge — le runner Gitea Act tronque naïvement au premier # (même entre guillemets) : echo "... see #87)" devenait une citation non fermée (unexpected EOF while looking for matching '"'", /var/run/act/workflow/4 ligne 2). Seul run: du workflow avec un # (les # des noms d'étapes Bandit/Npm audit sont inoffensifs, ces étapes passent). Correctif : echo sans # (réf #87 en commentaire YAML). Garde-fou test_ci_workflow.py (aucun # dans le code des run:, upload.test.mjs verrouillé en étape JSDOM — BUG-082) + contre-preuve sur l'ancien ci.yml. Vérifié : 56 passed. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-27 | BUG-081 | Correction | backend/auth/router.py, tests/test_mfa.py, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-081 : GET /api/auth/mfa/status répondait 500 quand l'auth est désactivée — le pseudo-user anonymous n'a aucune entrée en store (get_user → None, AttributeError sur user.get). Garde user is None → payload « MFA désactivé ». Test TestMfaStatusAuthDisabled (échoue en 500 sans le correctif). Vérifié : test_mfa.py 32 passed, ruff/mypy 0. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-27 | #87 T6, T7, T8 | Sécurité (fin #87) | backend/requirements.txt, backend/{render,export}.py, backend/tools/documents.py, backend/auth/router.py, backend/main.py, semgrep-rules/ (nouveau), .gitea/workflows/ci.yml, tests/test_i18n_parity.py (nouveau), tests/test_auth_api.py, tests/test_security_headers.py, docker-compose.yml, .env.example, CHANGELOG.md, docs/ROADMAP.md, docs/ISSUES_TODOLIST.md | T6 : dépendances qualifiées (mistune 3.3.3, multipart 0.0.31, weasyprint 70, mcp 1.28.1, fastapi 0.141.1 + starlette 1.7.0, setuptools 84 ; cast mistune 3 sites) — suite 1359 passed, ruff/mypy 0, pip-audit bloquant 0 vuln (exception ecdsa/Minerva documentée : sans fix, HS256 only). T7 : semgrep bloquant local 8 règles, 0 finding (trivy écarté : réseau). T8 : Secure auto + X-Forwarded-Proto (TRUST_PROXY), warning affiné, CORS same-origin explicite, style-src résiduel assumé (189+343 sites) ; TODO exemple purgé, locales FR/EN 2213 parité testée, npm audit 0. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-29 | BUG-094 | Correction | backend/xlsx_reader.py, frontend/js/viewer.js, tests/test_xlsx_viewer.py, tests/frontend/xlsx-viewer.test.mjs, CHANGELOG.md, docs/ISSUES_TODOLIST.md | BUG-094 — une feuille vide ou nouvellement ajoutée devient éditable et manipulable. render_sheets() substitue une grille vierge 20×8 (EMPTY_SHEET_ROWS/EMPTY_SHEET_COLS) quand la feuille ne porte aucune cellule, avec de vraies coordonnées A1 ; la visionneuse retombe sur parseRef(activeRef) || {row:1,col:1} pour que le menu Structure propose toujours insérer/supprimer ligne et colonne. Contre-preuve : TestXlsxDisplay::test_empty_sheet_renders_an_editable_blank_grid (sans le correctif : « Feuille vide » sans data-cell). Vérifié : test_xlsx_viewer.py 59 passed, xlsx-viewer.test.mjs 52/52. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-29 | BUG-095 | Correction CI / sécurité | backend/requirements.txt, .gitea/workflows/ci.yml, tests/test_ci_workflow.py, CHANGELOG.md, docs/ISSUES_TODOLIST.md | Le job security repasse au vert : pyjwt 2.13.0 est vulnérable (CVE-2026-102274, correctif 2.14.0) et le plancher de BUG-091 (>=2.13.0) était donc sous le dernier correctif. Plancher porté à pyjwt[crypto]>=2.14.0 (au-dessus de la toolcache du runner, sinon pip répond « already satisfied »), garde-fou TestDependencySecurityFloors mis à jour (contre-preuve : plancher remis à 2.13.0 → test rouge), commentaire de la job security aligné. Vérifié : test_ci_workflow.py 9 passed, pip-audit local OK (pyjwt 2.15.1). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-29 | BUG-096 → BUG-099 (#156 A1-A4) | Correction | frontend/js/viewer.js, frontend/locales/{fr,en}.json, backend/xlsx_reader.py, backend/services/mutations.py, backend/routers/files_read.py, tests/frontend/xlsx-viewer.test.mjs, tests/test_xlsx_viewer.py, tests/test_xlsx_formats.py, CHANGELOG.md, docs/ROADMAP.md, docs/features/xlsx-editor-completeness.md, docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md, docs/ISSUES_TODOLIST.md | Les 4 défauts P0 du tableur sont corrigés. (BUG-096) l'enregistrement d'un .csv depuis la visionneuse ne lit plus le nom de feuille dans xlsx_sheets (absent du payload CSV) : le TypeError partait avant tout appel réseau, aucun PUT …/csv/save n'était émis. (BUG-097) GET …/xlsx/sheet accepte .xlsx et .xlsm — le chargement des lignes au-delà du plafond fonctionne pour les classeurs macro. (BUG-098) sniff_csv_delimiter() détecte ;/,/tabulation et save_csv_cells() réutilise le délimiteur : un CSV français s'affiche en colonnes distinctes et le reste après édition. (BUG-099) sonde de perte par feuille (4 Mo) + plafond global (32 Mo) et clé cached_values_unverified quand le budget est épuisé — plus de perte silencieuse possible des valeurs calculées. Tests : TestXlsxCachedValueProbe (3, contre-preuve : budget épuisé signalé au lieu de []), TestCsvDelimiter + 3 tests CSV, fenêtre .xlsm (+ refus .csv), 1 test JSDOM CSV. Vérifié : suite 1490 passed / 6 skipped, ruff 0, mypy 0 (102 fichiers), xlsx-viewer.test.mjs 61/61, validate-imports 40 modules, unit 12/12, i18n parity. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-29 | #156 A5-A7 (P1) | Fonctionnalité (sans nouveau défaut) | frontend/js/viewer.js, frontend/js/xlsx/command-bar.js, frontend/locales/{fr,en}.json, tests/frontend/xlsx-viewer.test.mjs, tests/e2e/xlsx-viewer.spec.js, CHANGELOG.md, docs/ROADMAP.md, docs/features/xlsx-editor-completeness.md, docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md | P1 livré — (A5) presse-papiers de plage : copier/couper/coller un bloc TSV au clavier et au menu contextuel, presse-papiers interne + miroir système, remplissage multi-cellules, insertion texte échappée, débordement signalé, annulable ; (A6) clavier complet (Ctrl+S/Ctrl+A/Suppr/F2/Ctrl+Home|End/Home|End/PgUp|PgDn/Ctrl+flèches/Maj+Entrée) ; (A7) zone Nom éditable (« Atteindre ») + liste de fonctions. Vérifié : JSDOM xlsx-viewer.test.mjs 84/84 (20 nouveaux), E2E Playwright. | ✅ livré | | 2026-09-30 | #156 A8-A14 (P2 + P3) — clôture du backlog | Fonctionnalité (sans nouveau défaut) | backend/services/mutations.py, backend/routers/files_read.py, backend/routers/files_write.py, backend/schemas.py, backend/xlsx_reader.py, backend/openapi_docs.py, backend/tools/{spreadsheets,schemas,labels}.py, frontend/js/viewer.js, frontend/js/xlsx/command-bar.js, frontend/locales/{fr,en}.json, frontend/style.css, tests/test_xlsx_styles.py, tests/test_xlsx_viewer.py, tests/test_spreadsheet_tools.py, tests/test_xlsx_formats.py, tests/frontend/xlsx-viewer.test.mjs, tests/e2e/xlsx-viewer.spec.js, CHANGELOG.md, docs/ROADMAP.md, docs/features/xlsx-editor-completeness.md, docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md, README.md, README.fr.md | P2 + P3 livrés, backlog #156 clôturé — (A8) mise en forme en écriture : bouton Mise en forme (gras/italique/souligné, alignements, couleurs via sélecteur natif, formats de nombre, fusion/défusion, volets figés, largeur/hauteur) et nouvelle route PUT …/xlsx/style (mutate_xlsx_style : verrou, backup, swap atomique, garde de perte, If-Match) ; (A9) décision « pas de moteur de formule » annoncée dans l'UI ; (A10) undo/redo unifié avec piles par fichier conservées au re-rendu ; (A11) export de la sélection + Markdown + HTML + impression et recherche sur toutes les feuilles (n/m · k feuilles) ; (A12) concurrence optimiste If-Match → 409 conflict/stale_revision, retry qui relit ; (A13) cache de read_workbook_meta() par (chemin, mtime_ns, taille) (LRU 8) ; (A14) outils IA .xlsm/.csv + search_workbook, analyze_range, edit_xlsx_structure (confirmation conservée). Vérifié : suite 1525 passed / 6 skipped, ruff 0, mypy 0 (102 fichiers), JSDOM xlsx-viewer.test.mjs 107/107, validate-imports + unit, i18n parity 2355/2355, E2E xlsx 10/10 + Split View 37 + XSS 2. | ✅ livré | | 2026-09-29 | #156 (audit), BUG-096 → BUG-099 | Enregistrement (audit statique + 1 repro JSDOM) | docs/ROADMAP.md, docs/features/xlsx-editor-completeness.md (nouveau), docs/ISSUES_TODOLIST.md, CHANGELOG.md | Audit de complétude de l'éditeur Excel → ouverture de l'item #156 (P0 défauts · P1 presse-papiers/clavier · P2 mise en forme/calcul/undo · P3 sortie/robustesse/perf) avec fiche dédiée. 4 défauts enregistrés : BUG-096 (enregistrement .csv en TypeError, reproduit en JSDOM — Cannot read properties of undefined (reading 'name'), aucun PUT …/csv/save), BUG-097 (lazy-load .xlsm → 415), BUG-098 (délimiteur CSV , figé vs export ;), BUG-099 (sonde de perte plafonnée à 8 Mo, risque théorique). Manques fonctionnels recensés : presse-papiers de plage, clavier complet, zone Nom éditable, mise en forme en écriture, calcul, undo/redo unifié, export/impression, concurrence optimiste, cache des métadonnées, outils IA .xlsm/.csv. Aucun code modifié (documentation seule). | 🔴 ouvert (à traiter) | | 2026-10-02 | #158 (A1-A3), BUG-100 | Fonctionnalité + correction | frontend/js/navfacets.js (nouveau), frontend/js/vaulthome.js, frontend/js/ui.js, frontend/js/sidebar.js, frontend/js/viewer.js, frontend/js/legacy.js, frontend/style.css, frontend/locales/{fr,en}.json, backend/services/vaults.py, backend/schemas.py, tests/test_nav_files.py (nouveau), tests/frontend/navfacets.test.mjs (nouveau), tests/frontend/unit.test.mjs, CHANGELOG.md, docs/ROADMAP.md, docs/ISSUES_TODOLIST.md | A1 : TabManager.openNav(vault, dir) — un onglet de navigation par vault, ouvert à chaque clic répertoire de l'arbre (mode focus et mode All) sans fermer les onglets de fichiers ; la ligne vault reste un expandeur. A2 : la vue liste les sous-répertoires cliquables et embarque le panneau de facettes Vaults · Tags · Extensions (mêmes classes que la page de recherche), les tris Pertinence / Date et le bouton Sauver ; GET /api/vault/{v}/files expose désormais les tags indexés (facettes calculées côté client). A3 / BUG-100 : showWelcome() réinjecte #quick-help et goHome() efface recherche globale + filtre sidebar. Tests : test_nav_files.py 3 passed, navfacets.test.mjs 10/10, unit.test.mjs 12/12, validate-imports 41 modules, ruff/mypy 0. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-10-02 | #158 (A4-A6) | Fonctionnalité | frontend/js/vaulthome.js, frontend/js/ui.js, frontend/js/pane-manager.js, frontend/js/legacy.js, frontend/js/sidebar.js, frontend/style.css, frontend/locales/{fr,en}.json, tests/e2e/nav-tab.spec.js (nouveau), CHANGELOG.md, docs/ROADMAP.md, docs/features/navigation-tab-158.md | A4 : TabManager.openHome() — le clic sur le titre ouvre la page d'accueil dans un onglet Accueil (icône Maison, common.home FR/EN) remonté en position 0 à chaque fois ; deactivate() conserve cet onglet actif, pane-manager.js a son propre openHome pour le split. A5 : menus contextuels répertoires et fichiers de la page de navigation via ContextMenuManager.show() (avec stopPropagation(), le handler global referme sinon le menu) — listes vérifiées identiques à la sidebar ; la ligne d'un vault rouvre la page de navigation. A6 : icône des répertoires à var(--accent) (couleur de l'arbre). Correctif annexé : l'effacement du filtre sidebar au retour à l'accueil n'est déclenché que s'il était actif (sinon restoreSidebarTree() repliait l'arbre). Vérifié : Playwright jetable 16/16, nav-tab.spec.js 4 tests, validate-imports/unit/i18n parity verts. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-10-02 | BUG-101 | Correction | frontend/js/ui.js | BUG-101 — le bloc « Keyboard shortcuts for tabs » existait en double dans ui.js (fin de fichier) : un seul Ctrl+W déclenchait deux close(), le second refermant l'onglet ré-activé par le premier — observé avec #158 quand un onglet navigation survit à la fermeture du fichier ; Ctrl+Tab sautait également un onglet. Bloc dupliqué supprimé (un seul listener). Vérifié : repro Playwright (close() appelé 2× puis 1×) et suite E2E. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-10-02 | BUG-102 | Correction | frontend/js/ui.js | BUG-102 — menu contextuel refermé par sa propre ouverture : la reflow des icônes lucide à l'affichage déclenche un scroll capturé qui refermait le menu ~10 ms après son apparition (constaté sur les fichiers de la page de navigation, #158 A5). show() mémorise _shownAt et le listener scroll ignore un déclenchement dans les 250 ms. Vérifié : instrumentation Playwright + suite E2E (nav-tab.spec.js). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-10-02 | BUG-103 | Correction | frontend/style.css | BUG-103 — sidebar mobile sous la barre d'outils du bas : en mobile le .sidebar (z-index: 200) passait sous .mobile-toolbar (z-index: 900, 64 px de haut), masquant les dernières entrées de l'arbre ; passé à 950. Vérifié : elementFromPoint Playwright (393×851) + test source mobile sidebar over toolbar (unit.test.mjs). | 🟢 corrigé | | 2026-10-02 | BUG-104 | Correction | desktop/permissions/commands.toml (nouveau), desktop/capabilities/default.json, desktop/build.rs | BUG-104 — commandes Tauri rejetées par l'ACL depuis la page backend : la fenêtre redirige vers http://127.0.0.1:<port>, origine remote pour Tauri v2, et aucune commande applicative n'était déclarée → Command … not allowed by ACL sur pick_vault_folder, get_wizard_state, etc. ; les erreurs sont avalées par desktop.js:invoke() → bouton du wizard silencieusement inopérant depuis #77. Correctif : manifeste desktop/permissions/commands.toml (allow-app-commands, 19 commandes) référencé par la capability + rerun-if-changed=permissions dans build.rs. Vérifié : cargo test 25 passed (nouveau garde-fou ACL), CDP live : get_version OK, clic → fenêtre native « Select Folder », ajout de vault complété de bout en bout. | 🟢 corrigé | | 2026-10-02 | BUG-105 | Correction | desktop/src/jumplist.rs | BUG-105 — crash heap (c0000374) à l'ouverture du desktop : set_link_title (#77) passait un PROPVARIANT VT_LPWSTR pointant un buffer heap Rust au property store du shell, qui le libère avec l'allocateur Windows → STATUS_HEAP_CORRUPTION détectée dans ntdll ≤20 s après le lancement (3/3, Event Log WER, même empreinte de pile). Fonction supprimée : le libellé des entrées vient de SetDescription. Vérifié : bisect sur 3 états de build, run 9 min EXIT=0, zéro événement WER. | 🟢 corrigé | | 2026-10-02 | BUG-106 | Correction | backend/main.py, tests/test_csp_nonce.py | BUG-106 — page blanche sur /docs : la CSP #87 T5c (script-src sans unsafe-inline, nonce frais par réponse) s'applique aussi aux pages générées par FastAPI (/docs, /redoc) dont le balisage n'est jamais passé par inject_csp_nonce → le navigateur refusait le script inline SwaggerUIBundle(…), la div restait vide. SecurityHeadersMiddleware injecte désormais le nonce dans ce HTML avec le helper existant ; le corps est décompressé/recompressé car SSESafeGZipMiddleware est plus proche de la route (les réponses arrivent gunzippées). /redoc n'a aucun script inline (bundle jsdelivr seul, couvert par script-src) — vérifié tel quel. Vérifié : test_docs_inline_script_nonced, test_redoc_served_with_csp, test_docs_external_bundle_not_nonced + 45 passed sur les 3 suites sécurité, ruff/mypy 0. | 🟢 corrigé | | 2026-10-02 | BUG-107 | Correction | desktop/tauri.conf.json, desktop/src/main.rs | BUG-107 — drag & drop de fichiers inutilisable sur desktop : Tauri/wry installe son propre IDropTarget par-dessus de celui du WebView2 (source wry : « Enumerate child windows to find the WebView2 window and override! ») et aucun événement natif n'était écouté → les glisser-déposer depuis l'Explorateur n'atteignaient jamais les gestionnaires HTML5 de dragdrop.js (#89 marchait donc uniquement sur le web). Correctif : la doc Tauri est explicite — disable_drag_drop_handler() « is required to use HTML5 drag and drop APIs on the frontend on Windows » ; la fenêtre est créée en code (create: false dans tauri.conf.json, boucle Tauri qui saute les fenêtres non auto-créées) via WebviewWindowBuilder::from_config(...).disable_drag_drop_handler(). Vérifié : cargo test 26 passed (nouveau garde-fou test_window_created_without_tauri_drag_drop_handler : les deux marqueurs obligatoires), lancement de l'app OK (fenêtre unique, boot log normal) ; glisser-déposer réel à valider par l'utilisateur. | 🟢 corrigé | | 2026-10-07 | BUG-108 | Correction | frontend/js/xlsx/context-menu.js | BUG-108 — « Coller » du menu contextuel du tableur muet : closeContextMenu() faisait menu.remove() avant _menu = null ; retirer le menu portant le focus émet focusout synchrone qui rappelle closeContextMenu(), le second remove() sur un nœud démonté lève NotFoundError et avorte le handler de l'action avant son exécution (introduit par la fermeture au focus #179). Reproduit par l'E2E xlsx-viewer.spec.js « coller une plage » (CI #850→#854). Correctif : déférencer avant de retirer. | 🟢 corrigé | | 2026-10-08 | #189 | Fonctionnalité | frontend/js/viewer.js, frontend/style.css, frontend/locales/{fr,en}.json, tests/frontend/xlsx-viewer.test.mjs, tests/e2e/xlsx-viewer.spec.js, docs/ROADMAP.md, docs/features/xlsx-mobile-layout-189.md, docs/GUIDES/RECHERCHE_PDF_EXCALIDRAW.md, README.md, README.fr.md, CHANGELOG.md | #189 — mise en page mobile du tableur : sur ≤ 768 px les barres menus/ruban/Recherche sont repliées par défaut (la barre des feuilles + barre formule fx restent visibles, la grille récupère la hauteur perdue), bascule unique #xlsx-tools-toggle (☰, aria-expanded), cibles tactiles ≥ 44 px, groupes du ruban qui s'enroulent, onglets de feuilles défilants, inputs 16 px (anti-zoom iOS). E2E mobile ajouté + helper openXlsx() (déplie le ruban). 10 échecs E2E mobile préexistants prouvés par git stash (hors périmètre, CI ne lance que desktop). Vérifié : xlsx-viewer.test.mjs 165/165, E2E desktop 19 passed/1 skipped, suite 1628 passed, ruff/mypy 0, CI run #1937 success | ✅ livré (en attente vérif utilisateur) | | 2026-10-08 | #169 | Fonctionnalité | backend/file_chat.py (nouveau), backend/routers/file_chat.py (nouveau), backend/schemas.py, backend/main.py, frontend/js/filechat.js (nouveau), frontend/js/viewer.js, frontend/js/sync.js, frontend/style.css, frontend/locales/{fr,en}.json, tests/test_file_chat.py (nouveau), tests/frontend/filechat.test.mjs (nouveau), tests/frontend/toolbar-order.test.mjs, .gitea/workflows/ci.yml, docs/ROADMAP.md, docs/features/file-chat-169.md (nouveau), docs/GUIDES/PRISE_EN_MAIN.md, CHANGELOG.md | #169 — chat intégré par fichier : store JSON par (vault, path) sous data/chats/ (nom SHA-256 → traversal impossible, plafond 500 msgs, texte 4000 car., écriture atomique, fichier corrompu → vide) ; GET/POST /api/file/{vault}/chat (auth + check_vault_access + resolve_safe_path, 400/404/403, response_model) + broadcast SSE chat_message sur le transport #62 (pas de 2ᵉ WebSocket) ; panneau latéral filechat.js (bouton 💬 toolbar, rendu chronologique textContent, envoi optimiste + dédup par id, toast si panneau fermé/autre fichier, plein écran ≤ 768 px) ; relais SSE en import dynamique dans sync.js ; i18n FR/EN 10 clés chat.*. En route : tag OpenAPI maison file-chat non déclaré → test_used_tags_are_declared rouge (retiré, dérivé « Files »), regex navBtns de toolbar-order.test.mjs (échappements → includes), ruff UP012/TRY004/I001. Vérifié : test_file_chat.py 15 passed, filechat.test.mjs 6/6, suite 1643 passed, ruff/mypy 0, validate-imports 42 modules, CI run #1938 5/5 success (v2.57.0) | ✅ livré (en attente vérif utilisateur) |


📜 Historique des bugs résolus

Les rangées ✅ vérifié (ou ⚪ abandonné) sont déplacées ici régulièrement pour garder le registre actuel court et lisible. Conservez l'ID d'origine pour la traçabilité.

# Titre Date résolution Résolu par Correctif / Commit Notes
BUG-083 Job CI security rouge : le runner Gitea Act tronque le script pip-audit au premier # (citation de l'echo non fermée → unexpected EOF while looking for matching '"') 2026-09-27 Utilisateur run: assaini (echo sans #, réf #87 en commentaire YAML) ; tests/test_ci_workflow.py (2 tests : aucun # dans le code des run:, upload.test.mjs verrouillé en étape JSDOM) ; vérifié : 56 passed (ci_workflow + e2e_harness + version), contre-preuve OK sur l'ancien ci.yml Seul run: du workflow contenant un # (see #87 dans l'echo). Les # des noms d'étapes (Bandit, Npm audit) sont inoffensifs (ces étapes passent). Correctif : echo sans #, réf #87 en commentaire YAML
BUG-082 CI lint rouge : suites frontend à import statique jsdom exécutées dans l'étape racine où jsdom n'est jamais installé 2026-09-27 Utilisateur upload.test.mjs + config-ai-keys.test.mjs déplacés dans l'étape JSDOM (les deux branches) ; garde-fou test_ci_workflow.py (aucun fichier racine à import statique jsdom + suites verrouillées en JSDOM) ; vérifié : étape racine verte + upload et config-ai-keys verts depuis tests/frontend/ jsdom ne vit que dans tests/frontend/node_modules (installé par l'étape JSDOM). Correctif : déplacer les suites concernées dans l'étape JSDOM
BUG-080 [🔴 BLOQUANT] E2E locaux bloqués toute la nuit : npm run test:e2e:ps ne termine jamais (serveurs orphelins sur le port 2029, npx playwright install sans --yes ni garde-fou, suite ~130 tests sans timeout global) 2026-09-27 Utilisateur run-e2e-local : npx --yes, skip install Chromium si présent (E2E_INSTALL_BROWSERS=1), timeouts E2E_TIMEOUT_SEC (900)/E2E_BROWSER_INSTALL_TIMEOUT_SEC (600, exit 124) ; playwright.config.ts : globalTimeout 15 min local / 30 min CI (E2E_GLOBAL_TIMEOUT_MS) ; e2e-server.ps1 : pidfile = vrai owner du port, stop tue l'arbre complet. Tests : tests/test_e2e_harness.py (8/8), cycle start/stop live (pidfile cohérent, port libéré) Constat 2026-09-27 : e2e-server.ps1 start OK (READY 12 s) mais run suivant pendu toute la nuit ; 2 python orphelins (PID 81180 parent + 81936 sur le port, pidfile périmé). Double processus systématique (parent .venv parqué + enfant qui sert — aussi sur flowdeck/3.13 : environnemental, sans impact après correctif). Trouvé au passage : BUG-081 (/api/auth/mfa/status → 500 auth désactivée)

🧪 Procédure de vérification d'une correction (pour l'IA)

Quand une correction est écrite, l'agent IA doit avant de passer en 🟢 corrigé :

  1. Exécuter les tests du projet :
    • Backend : python -m pytest tests/ -v (ou pytest)
    • S'il existe des tests ciblés sur la zone modifiée, les lancer en priorité.
  2. Vérifier le lancement du module concerné (import sans erreur, endpoint répond).
  3. Relire sa propre diff pour éviter les régressions.
  4. Mettre la colonne « Correctif / Commit » à jour puis le statut en 🟢 corrigé.
  5. Si un test échoue ou que la correction est incomplète, rester en 🟠 en cours et le noter.

✔️ Checklist de traitement d'un bug (rappel pour toute IA)

  • Bug identifié avec un ID et repris dans le registre
  • Statut posé en 🟠 en cours avant de coder
  • Correction conforme aux conventions du projet
  • Tests exécutés et vert (colonne « Correctif / Commit » renseignée)
  • Journal des interventions AI mis à jour
  • Statut passé en 🟢 corrigé (pas ✅ vérifié sauf autorisation)
  • (Après validation utilisateur) rangée déplacée dans l'historique

📚 Liens utiles