Files
ObsiGate/CHANGELOG.md
T
bruno 2be0b15bcf
CI / lint (push) Successful in 3m0s
CI / security (push) Failing after 1m38s
CI / test (push) Successful in 4m58s
CI / build (push) Successful in 1m36s
CI / e2e (push) Successful in 19m55s
fix: partage dirigé — ACL court-circuitée après résolution Partage/ + chemin canonique au token #196
- le partage EST l'autorisation: plus de 403 'Accès refusé à la vault' pour un
  destinataire sans accès au vault émetteur (api_file, raw, download)
- chemin virtuel canonique Partage/<token>/<nom> — désambiguïse les homonymes,
  fallback par nom conservé
- 'reçu' = partagé_avec contient l'utilisateur: un admin ne voit plus les
  partages des autres comme reçus (fix File not found: Partage/…)
- +3 tests (14 total)
2026-10-10 23:11:12 -04:00

243 KiB
Raw Blame History

Changelog — ObsiGate

Toutes les modifications notables d'ObsiGate sont documentées dans ce fichier.

Format basé sur Keep a Changelog, et Semantic Versioning.

En cours de développement : les changements à venir sont listés dans la section Unreleased. La dernière version livrée est 2.67.2.


[Unreleased]


[2.67.2] — 2026-10-10


[2.67.1] — 2026-10-10


[2.67.0] — 2026-10-10

Ajouté

  • #196 — Partage dirigé (itération 2 : visibilité & recherche)
    • Icône « partagé » (share-2 bleu) dans l'arborescence sur tout fichier porteur d'un partage actif (créé ou reçu).
    • Dossier virtuel Partage à la racine du dossier personnel du destinataire : documents reçus listés avec l'icône de partage et « Partagé par X » en infobulle ; ouverture en lecture seule via la page de partage.
    • Les documents reçus sont cherchables : la recherche plein texte couvre les partages dirigés reçus (résultats home-<user>/Partage/…, clic → page de partage, jamais l'éditeur du vault d'autrui).
    • Cache d'icônes partagé invalidé à la création/révocation.
    • Itération 3 : correction « Directory not found: Partage » (le dossier virtuel ne déclenche plus la navigation) ; un fichier reçu s'ouvre dans l'application en onglet — arbre, recherche et dashboard — via résolution serveur home-<user>/Partage/<fichier> vers le fichier source (/api/file, raw, download) ; un path vide/null est rejeté (400) à la création d'un partage.
    • Itération 4 : correctifs d'ouverture — « Accès refusé à la vault … » (le partage dirigé est l'autorisation, l'ACL du vault source est court-circuitée après résolution Partage/…), « File not found: Partage/… » (les partages non adressés à l'utilisateur ne s'affichent plus comme reçus ; chemin canonique Partage/<token>/<nom> pour désambiguïser les fichiers homonymes).

[2.66.0] — 2026-10-10

Ajouté

  • #196 — Partage dirigé entre utilisateurs
    • Un utilisateur peut partager un document à un ou plusieurs comptes précis (champ destinataires dans le dialogue de partage, noms séparés par des virgules) : les pages /s/{token} (+ PDF, téléchargement brut) exigent alors une session et n'acceptent que le créateur, les admins et les destinataires — 404 opaque sinon (l'existence d'un token dirigé n'est pas confirmable de l'extérieur).
    • Dashboard : les partages reçus s'affichent avec « Partagé par X » et s'ouvrent en lecture seule via la page de partage ; pas de bouton révoquer pour un destinataire.
    • Rétrocompatible : les partages publics existants (sans destinataires) restent accessibles anonymement.

Sécurité

  • #196 — GET /api/shares scopé : un utilisateur non-admin ne voit plus TOUS les partages de l'instance, mais uniquement ceux qu'il a créés ou reçus (fuite préexistante, intenable avec le partage user-to-user). La révocation d'un partage est réservée à son créateur ou à un admin.
    • i18n FR/EN (7 nouvelles clés), tests tests/test_directed_shares.py (9), fiche docs/features/directed-shares-196.md.

[2.65.2] — 2026-10-10

Corrigé

  • BUG-111 — Admin : icône de fermeture + navigation du tableau de bord
    • La modale « Administration — Utilisateurs » ouverte depuis la page /admin n'affichait pas l'icône du bouton fermer (lucide absent de admin.html) — même version du script UMD que index.html ajoutée.
    • Le tableau de bord admin affiche désormais une seule section à la fois (état initial : hash de l'URL ou « Statistiques temps réel ») au lieu des 4 sections empilées.

[2.65.1] — 2026-10-10


[2.65.0] — 2026-10-10

Ajouté

  • #194 (UI) — Dossier personnel visible dans l'arborescence
    • Les vaults home-<user> s'affichent en premier dans l'arborescence de la barre latérale (tri stable appliqué à la source, frontend/js/sidebar.js, donc aussi les sélecteurs de vault).
    • Icône maison lucide (data-lucide="home", même style/stroke: currentColor que les autres icônes) via getVaultIcon() — frontend/js/viewer.js.
    • docker-compose.test-win.yml : montage test_home:/vaults/Home + OBSIGATE_HOME_ROOT=/vaults/Home pour tester la feature en local (dossier test_home/ ignoré par git).
    • Tests : sidebar-filters.test.mjs +2 (icône maison pour home-*, gemme violette inchangée ailleurs).

Corrigé

  • #194 — Un dossier perso n'est plus visible par les autres comptes
    • check_vault_access (backend/auth/middleware.py) refuse désormais tout vault home-* qui n'est pas explicitement octroyé au compte : le vaults: ["*"] de l'admin ne couvre plus les dossiers persos (avant, l'admin voyait home-<user> de chaque utilisateur dans sa barre latérale).
    • Les filtres « * » écrits en dur dans les routes ont été remplacés par cet appel unique — dashboard, conflits, liens retour, favoris, abonnements push : partout la même règle.
    • /api/search filtre les résultats par vault accessible avant pagination (search_vaults(is_allowed=…)), sinon les notes personnelles d'un autre pouvait apparaître dans les résultats.
    • _grant octroie désormais le home aussi aux comptes « * » : l'admin voit son home-admin, réparé au démarrage pour les comptes existants.
    • Tests : test_user_home.py +2 (« * » sans homes, filtrage de la recherche), assertion API inversée dans test_auth_api.py.

[2.64.0] — 2026-10-10

Ajouté

  • #194 — Dossier personnel par utilisateur
    • Chaque utilisateur créé reçoit <OBSIGATE_HOME_ROOT>/<username> (ex. /NFS/OBSIDIAN_DOC/Home/<user> côté hôte) monté comme un vault propre home-<user> : l'isolation utilise l'ACL par vault existante (check_vault_access), sans nouvelle ACL par chemin.
    • backend/user_home.py : ensure_user_home() idempotent (mkdir + registre vault + watcher + octroi dans user.vaults), appelé à la création d'un compte (POST /api/auth/admin/users) et au démarrage (ensure_all_user_homes() : migration des users existants + réparation d'un dossier ou d'un registre perdu).
    • Registre persistant des vaults dynamiques data/vaults.json (load_vault_config() le fusionne avec les env VAULT_N_*) : auparavant, tout vault ajouté à la volée (/api/vaults/add) disparaissait au premier reindex complet et au redémarrage.
    • Suppression d'un compte → vault refermé (index/watcher/registre) mais dossier conservé (aucune perte de données).
    • Correctif : reload_single_vault remet la config du vault après remove_vault_from_index (sinon le vault disparaissait de vault_config jusqu'au rechargement complet).
    • Déploiement : montage unique /NFS/OBSIDIAN_DOC/Home:/vaults/Home + variable OBSIGATE_HOME_ROOT (fonctionnalité inactive sans cette variable — dev, tests, desktop inchangés).
    • Tests : tests/test_user_home.py (10) + 2 tests API dans test_auth_api.py (provisionnement, isolation, retrait).

[2.63.0] — 2026-10-09

Ajouté

  • #193 — Chat : un post s'affiche comme un document markdown
    • Le rendu vient du serveur : backend/file_chat.py ajoute un champ html à chaque message en réutilisant le pipeline des documents (backend/render.py::_render_markdown → mistune + wikilinks + masquage des secrets + sanitizer BUG-021). Pas de second moteur markdown côté client, donc pas de nouvelle surface XSS.
    • Titres, listes, tableaux, citations, notes, listes de tâches et blocs de code colorés : le front injecte le HTML dans un .md-content puis passe safeHighlight() (highlight.js + alias de langages) sur chaque pre code, exactement comme la visionneuse de documents.
    • Le html est calculé à la lecture (et à l'ajout, pour l'écho SSE) : rien n'est persisté dans data/chats/*.json, un message reste du texte.
    • Repli conservé : sans html, le texte brut + liens cliquables s'affichent comme avant ; un échec de rendu n'emporte jamais le message.
    • CSS : .file-chat-md annule le pre-wrap de la bulle (sinon les retours à la ligne du HTML source devenaient des lignes vides) et compacte .md-content (tailles, pre/table défilants) pour une bulle de chat.
    • Tests : pytest TestMarkdownRendering +7 (54 → 61 dans test_file_chat.py), JSDOM filechat.test.mjs +2 (25 → 27).

[2.62.0] — 2026-10-09

Modifié

  • BUG-110 — Chat : refonte visuelle des posts et des indicateurs
    • Pièces jointes image : boutons icônes seules (24 px, aria-label) rangés sous l'image, alignés à droite — télécharger, ouvrir.
    • Clic sur une image = onglet de l'app : le clic (ou le bouton « ouvrir ») affiche la pièce jointe dans un onglet, avec le même visualiseur que les images de vault (zoom/pan/plein écran/lightbox) — plus d'ouverture dans un onglet du navigateur. TabManager.openChatImage() + branche chatImage de activate() ; renderImageViewer() accepte une url directe (une pièce jointe de chat ne vit pas dans un vault).
    • Indicateur de non-lus : point de couleur pulsant sur l'icône « Chat » de la sidebar tant qu'il reste des messages non lus, en plus du badge chiffré.
    • Notification OS : quand l'app n'est pas au premier plan (document.hidden ou hors focus), un message entrant déclenche une notification navigateur (titre « Nouveau message de {user} », aperçu du texte, clic → retour sur l'app). Permission demandée au premier clic d'ouverture du chat.
    • Suppression réellement annulable : le DELETE est différé de 10 s ; la ligne passe à « Ce message a été supprimé » avec un bouton « Annuler » qui réarme le message, et un échec réseau le rend. (L'ancien « Annuler » restaurait une copie DOM d'un message déjà détruit côté serveur.)
    • i18n FR/EN : chat.deleted_message, chat.undo, chat.download_image, chat.view_image — clés mortes retirées (chat.undo_success, chat.undo_error, chat.new_messages_indicator).
    • Tests : tests/frontend/filechat.test.mjs 19 → 25 (6 tests #110).

[2.61.0] — 2026-10-08

Ajouté

  • #192 - Chat : boîte de saisie auto-agrandissante + accusé de réception
    • Saisie auto-agrandissante : la boîte d'envoi (panneau document + sidebar) est un textarea dont la hauteur suit le contenu (plafond 160 px puis défilement) — on relit son message entier avant l'envoi. Entrée envoie, Maj+Entrée saute une ligne, la hauteur repasse à une ligne après l'envoi.
    • Accusé de réception : chaque conversation stocke {utilisateur: dernier_ts_de_lecture} ; l'expéditeur voit ✓ envoyé puis ✓✓ lu sur ses propres messages (un message est lu dès qu'un autre participant ouvre la conversation). Le GET d'historique renvoie la carte de lecture, POST /api/chat/read la met à jour et la diffuse en SSE (chat_read) — les indicateurs basculent en direct.
    • Notification : quand un message passe à ✓✓, l'expéditeur reçoit le toast « {user} a lu votre message » (FR/EN) ; un message reçu pendant l'affichage du canal accuse réception aussitôt.
    • Verrou global sur le read-modify-write du store de chat (une lecture ne peut plus faire disparaître un message).
    • Tests : pytest TestReadReceipts (7) + JSDOM filechat.test.mjs +3 (19).

Corrigé

  • BUG-109 - Tuile de lien : l'image de la carte ne s'affichait jamais — la CSP (img-src 'self' data: blob:) bloque toute og:image distante et les images relatives étaient résolues contre l'origine d'ObsiGate (404). La vignette est désormais téléversée à l'envoi dans chat_uploads (/api/chat/attachment/<uuid>, même origine, garde SSRF + plafond 2 Mo + allow-list d'extensions) ; échec du téléchargement → carte conservée sans vignette. Les cartes déjà publiées gardent leur ancien lien (elles étaient déjà muettes).

[2.60.0] — 2026-10-08


[2.59.0] — 2026-10-08

Ajouté

  • #191 - Chat : suppression de post, messages privés, boîte compacte, link preview
    • Suppression d'un post : bouton 🗑 sur ses propres messages (et sur tout le monde pour un admin), confirmation, DELETE /api/chat/{id} et /api/chat/dm/{user}/{id}, retrait en direct chez tous les clients (SSE chat_deleted).
    • Messages privés 2 utilisateurs : rangée de canaux au-dessus du fil (« Général » + un bouton par utilisateur), conversations isolées (vault __dm__, chemin alice|bob trié), placeholder dédié, compteurs de non-lus par canal, GET /api/chat/users pour la liste des destinataires (sans hash/mot de passe, sans soi-même).
    • Boîte d'édition compacte : bouton d'envoi réduit à une icône (panneau document + sidebar), la saisie garde toute la largeur.
    • Link preview : métadonnées OpenGraph récupérées à l'envoi (titre, description, image, site), carte cliquable esthétique, garde SSRF réutilisée (host privé/loopback rejeté), cache borné 200 entrées, échec silencieux (URL morte → le message passe sans carte).
    • Tests : pytest +13 (45 au total file_chat), JSDOM filechat.test.mjs +5 (16).

[2.58.0] — 2026-10-08

Ajouté

  • #190 - Chat : suivi du document, chat général en onglet sidebar
    • Suivi du document : le panneau du chat affiche le document ciblé (titre + chemin au survol) et suit automatiquement le document rendu — changer de fichier re-cible le panneau ouvert.
    • Onglet « Chat » (dernier, après Historique IA) dans la sidebar : conversation générale non liée à un fichier, indicateur de messages non lus (pastille rouge sur l'onglet), pièces jointes image/vidéo (upload avec allow-list d'extensions, 25 MB max, nom UUID), URL cliquables dans les messages, date/heure d'envoi.
    • Ordre des onglets : la barre de filtre passe sous les onglets et sert de recherche dans le chat quand l'onglet Chat est actif (texte + auteur).
    • Backend : GET/POST /api/chat, POST /api/chat/upload, GET /api/chat/attachment/{name} (même store #169 via les sentinelles __global__/general), broadcast SSE chat_message réutilisé.
    • Tests : pytest +12 (27 au total), JSDOM filechat.test.mjs +5 (11).

[2.57.1] — 2026-10-08


[2.57.0] — 2026-10-08

Ajouté

  • #169 - Chat intégré par fichier
    • Bouton 💬 Chat dans la barre d'actions du document : ouvre un panneau latéral avec l'historique de la discussion et la saisie d'un message.
    • Persistance par fichier (data/chats/, plafond 500 messages, texte tronqué à 4000 caractères) ; routes GET/POST /api/file/{vault}/chat protégées (auth + accès vault + contrôle de traversal).
    • Temps réel via le transport SSE existant (événement chat_message) : le panneau ouvert s'incrémente en direct, toast de notification « Nouveau message de … » si le panneau est fermé ou affiche un autre fichier (pas de doublon côté émetteur).
    • Mobile : panneau plein écran ; i18n FR/EN.
    • Tests : pytest test_file_chat.py (15) + JSDOM filechat.test.mjs (6), ajoutés au pipeline CI.

[2.56.0] — 2026-10-08

Ajouté

  • #189 - Tableur : mise en page mobile (barres repliées, grille prioritaire)
    • Sur mobile (≤ 768 px), la barre de menus (Fichier, Modifier…), le ruban de formatage et le groupe Recherche sont repliés par défaut : la barre des feuilles et la barre de formule (fx) restent toujours visibles, et la grille récupère la hauteur d'écran perdue (l'ensemble dépassait 50 % de l'écran).
    • Un bouton unique (#xlsx-tools-toggle, icône ☰) dans la barre des feuilles déplie/replie le tout (aria-expanded, état actif) ; sur desktop il reste masqué et rien ne bouge.
    • Ergonomie tactile : cibles ≥ 44 px (boutons, menus, onglets), groupes du ruban qui s'enroulent au lieu de déborder à cette taille, onglets de feuilles sur une seule ligne qui défile horizontalement, champs de la barre de formule en 16 px (pas de zoom iOS au focus), barre de formule qui passe en 2 lignes quand les outils sont ouverts.
    • Tests : JSDOM xlsx-viewer.test.mjs (bascule + règles CSS, 165/165) et E2E mobile (repli par défaut, dépliage, ruban sans débordement, cibles 44 px).

[2.55.0] — 2026-10-08

Ajouté

  • #188 - Secrets masqués : couverture universelle + clic pour copier
    • Détection élargie (backend/secret_redactor.py) : les formats de clés les plus répandus sont masqués d'office — OpenAI/Anthropic/ OpenRouter (sk-), Stripe (sk_live_, whsec_), GitLab (glpat-), GitHub (ghp_, github_pat_), Google (AIza…, ya29.), AWS (AKIA/ASIA), Slack (xoxb-), SendGrid, Hugging Face, npm, Docker, Resend, Square, Atlassian, Discord, Telegram, jetons Bearer … — en plus des JWT, clés privées et connection strings déjà couverts ; le plancher des affectations api_key=/token=/secret= passe de 20 à 8 caractères.
    • Mots de passe masqués à leur tour : password=, "passwd": "…", db_password=, passphrase=, mot de passe=… toute longueur et toute quote, avec le libellé [MOT DE PASSE MASQUÉ] ; PWD= (dossier de travail shell) reste épargné.
    • Clic sur un masque = copie de la valeur : l'aperçu authentifié sert les masques en <span class="secret-mask" data-secret="…"> — la valeur n'apparaît jamais en clair et un clic la copie dans le presse-papiers (toast + infobulle FR/EN). Les masques passent par des placeholders qui survivent à la conversion markdown (blocs de code compris), où un <span> serait affiché en toutes lettres ; les blocs de code masqués ne sont plus re-colorés (highlight.js perdrait le badge).
    • Aucune fuite à l'extérieur : partages publics, exports PDF et contexte IA / MCP ne reçoivent que le libellé, jamais la valeur.
    • Guide in-app FR/EN + docs/GUIDES/AUTHENTIFICATION_SECURITE.md (nouvelle section « Secrets masqués dans les aperçus »), README FR/EN, tests TestSecretRedactor (+33).

[2.54.0] — 2026-10-08

Ajouté

  • #186 - Création d'un fichier Excel depuis « Créer un fichier »

    • Excel (.xlsx) fait partie des types proposés dans la modale « Créer un fichier » (menu contextuel d'un vault ou d'un dossier, palette de commandes) : le classeur s'ouvre directement dans la visionneuse, prêt à être saisi. Les 10 libellés du sélecteur passent en i18n FR/EN (modal.ext_*) — ils étaient codés en dur en français.
    • Un vrai classeur, pas un fichier vide. Un .xlsx est une archive OPC, pas du texte : create_file() construit désormais l'archive avec openpyxl (une feuille Feuille1) au lieu d'écrire la charge utile vide de la modale, qui aurait produit un fichier que ni la visionneuse ni Excel n'ouvrent. Le champ content est ignoré pour cette extension.
  • #183 — Panneau de sélection de couleurs façon Google Sheets (texte et fond)

    • Le ruban et le menu Mise en forme n'utilisent plus le sélecteur natif : ils ouvrent un panneau conforme à la description fournie — Réinitialiser en en-tête, palette 8 × 10 (ligne 1 = 8 nuances de gris du noir au blanc, lignes 2 à 10 = 8 teintes × 9 nuances du clair au foncé), section STANDARD (8 couleurs prédéfinies) avec crayon, section PERSONNALISÉ (pastille + bouton « + » + crayon), pied de page Mise en forme conditionnelle / Couleurs en alternance.
    • Un clic sur une pastille referme le panneau et applique la couleur à la cellule ou à la plage sélectionnée ; l'indicateur de sélection est une coche blanche ou noire selon la luminance de la pastille.
    • Correction d'un piège d'encodage : la palette est calculée en HSL pour la lisibilité mais sort en #rrggbb. Une valeur hsl(...) était silencieusement rejetée par normHex() — la couleur n'était donc jamais écrite sur la cellule.
    • Le chrome du panneau suit le thème actif (variables CSS uniquement) ; « + » et les crayons ouvrent le sélecteur natif pour une couleur arbitraire.
    • Les deux entrées de pied de page sont annoncées mais pas encore disponibles : elles affichent un message explicite plutôt qu'un bouton inerte.

Modifié

  • #185 - Tableur : barre de menus Google Sheets, boutons façon Sheets, grille unie

    • Les dix menus de Google Sheets sont là : Fichier, Modifier, Affichage, Insertion, Format, Données, Outils, Gemini, Extensions, Aide — dans l'ordre et avec les libellés exacts de Sheets (161 entrées). Le nouveau module frontend/js/xlsx/menus.js décrit l'arborescence, viewer.js ne fait que brancher chaque entrée sur un flux existant. Les sections imbriquées (Nombre, Thème, Alignement, Zoom, Nettoyage…) sont des accordéons, une seule ouverte à la fois, comme dans Sheets.
    • La majorité des fonctions sont opérationnelles : Partager, Télécharger (original / CSV / Markdown / HTML), Imprimer, Renommer, Déplacer vers la corbeille, Historique des versions, Détails ; Annuler, Rétablir, Couper, Copier, Coller, Collage spécial (valeurs uniquement), Supprimer (lignes / colonnes / contenu), Tout sélectionner, Rechercher et remplacer ; Figer (ici / ligne / colonne), Afficher → barre de formule · grille · en-têtes · tableau de bord, Zoom, Plein écran ; Lignes / Colonnes / Cellules, Lien, Commentaire, Fonction (curseur placé entre les parenthèses) ; Nombre, Thème, Gras / Italique / Souligné / Barré, Taille de police, Alignement, Retour à la ligne, Rotation, Couleurs alternées, Effacer la mise en forme ; Trier, Créer un filtre, Plages nommées, Nettoyage des données (doublons / espaces superflus), Diviser le texte en colonnes ; Saisie semi-automatique, Accessibilité ; Gemini (6 entrées vers l'assistant IA) ; Aide, Mises à jour, Raccourcis clavier, Signaler un problème (diagnostic copié dans le presse-papiers).
    • Les options qu'ObsiGate n'a pas le disent : les 36 entrées absentes (macros, Apps Script, tableaux croisés, graphiques, formulaires, modules, AppSheet, Looker Studio, formation…) restent listées pour tenir la parité demandée, mais un clic y répond « « X » n'est pas disponible dans ObsiGate » au lieu de ne rien faire.
    • Le menu Fichier a quitté la barre d'onglets pour la barre de menus, en conservant ses identifiants (#xlsx-file-btn, #xlsx-file-menu, #xlsx-download-btn, #xlsx-csv-btn) : seuls les raccourcis internes et les tests bougent. Seul Enregistrer reste épinglé à droite des onglets.
    • Ruban refait façon Google Sheets : boutons plats de 30 × 30, sans bordure ni fond au repos, survol discret, état actif teinté, groupes séparés par des filets verticaux. Enregistrer reste le seul pilier plein. Nouveau bouton grille, synchronisé avec Affichage ▸ Afficher ▸ Grille.
    • Grille unie et visible : chaque cellule est encadrée sur les quatre côtés par la même ligne de 1 px, en-têtes compris (ils se distinguent par leur fond, plus par une bordure plus lourde). --grid-border n'est plus --border, quasi identique au fond des cellules sur la plupart des thèmes : c'est désormais un color-mix du texte vers la surface, calculé par themes.js, donc une seule valeur pour toute la grille et lisible sur les 15 thèmes comme sur les modes contraste élevé et sépia.
  • #184 - Tableur : peinture de format complète, sélection de lignes/colonnes multiples, grille A → Z × 1000 lignes

    • Peindre le format ne ratait plus aucun cas. L'outil copiait un sous-ensemble de propriétés et refusait une cellule source sans mise en forme explicite — donc une cellule simple vers une cellule colorée et en grande taille ne produisait rien. La copie porte maintenant sur toute la mise en forme (gras, italique, souligné, barré, couleur du texte, couleur de fond, taille, alignement, valign, retour ligne, rotation, format de nombre, bordures) ; une source sans format réinitialise la cible au lieu de ne rien faire.
    • Fuite corrigée : annuler la peinture (Échap, second clic) laissait le gestionnaire « appliquer au prochain clic » en place — le clic suivant dans la grille repeignait une vieille mise en forme. Il est maintenant désinstallé.
    • Sélection de plusieurs lignes / colonnes depuis les marges, comme dans Sheets et Excel : glisser sur les numéros de ligne (ou Maj+clic) sélectionne une plage de lignes entières ; idem sur les lettres de colonne. Le redimensionnement par les bords d'en-tête reste prioritaire sur ce geste.
    • Grille étendue à A → Z, et jusqu'à 1000 lignes, les cellules vides étant de vraies cellules éditables. Les colonnes sont posées d'emblée ; les lignes s'ajoutent par blocs de 100 au défilement, et non d'un seul coup : matérialiser 1000 lignes revient à ~26 000 cellules câblées par feuille, ce qui épuisait le tas de la suite JSDOM. Une feuille tronquée garde son chargement serveur « Charger la suite » (les lignes manquantes sont des données, pas du remplissage).
    • Trois goulots d'étranglement levés, sinon la grille étendue rendait l'éditeur inutilisable : findTd faisait un querySelector par appel (sélectionner une colonne = 26 000 requêtes) → index de cellules avec auto-réparation ; le rafraîchissement des formules clonait 26 000 nœuds à chaque frappe → court-circuit sur les cellules vides ; l'application des styles touchait 26 000 cellules → court-circuit sur celles que le classeur ne style pas.
    • Marqueur data-wired explicite : data-orig valait "" pour une cellule vide, ce qui recâblait les cellules déjà câblées (double écouteur).
  • #181 — Éditeur tableur : modèle de saisie Google Sheets (sélection ≠ édition)

    • Deux états distincts. Une cellule est sélectionnée (clic, flèches) ou en cours d'édition, et plus les deux en même temps. Le curseur de texte n'apparaît plus en sélection (caret-color: transparent, sans quitter contenteditable) : on ne voit plus un curseur clignotant au clic ou aux flèches.
    • Entrées en édition, comme Google Sheets / Excel : double-clic (le curseur est placé sous le pointeur, l'édition se fait en place), première frappe — qui remplace le contenu au lieu de s'y ajouter —, ou F2. Alt+flèche insère un saut de ligne, Échap quitte l'édition et restaure la valeur.
    • Entrée contextuelle : en édition, elle valide et déplace le focus sur la cellule du dessous (au lieu de perdre le focus) ; en sélection, elle bascule la cellule courante en mode édition. Tab suit la même logique.
    • Indicateur de sélection renforcé : la plage est contourée comme dans Excel / Sheets (quatre classes de bord sur les côtés réels du bloc, pas un simple lavis), et les en-têtes couverts — lettres A, B… et numéros 1, 2… — sont reflétés.
    • Un clic simple après une édition abandonne l'état d'édition : plus de curseur résiduel.
    • Inventaire des écarts vs Google Sheets / Excel dressé et priorisé (poignée de recopie, remplacer, validation de données, filtre automatique, tri multi-critères, mise en forme conditionnelle, création de graphiques, protection…) : fiche #181.
    • Tests JSDOM : xlsx-viewer passe de 136 à 146/146 (10 cas #181).
  • #180 — Éditeur tableur : redimensionnement au curseur, grille thémée, menu Fichier & barre épinglée

    • Curseur de redimensionnement. Survoler le bord d'un en-tête de colonne (A, B, C…) ou de ligne (1, 2, 3…) affiche désormais l'icône col-resize / row-resize. Le curseur par défaut des cellules (cell) et des en-têtes (pointer) masquait auparavant l'indicateur, y compris pendant le glissement : les en-têtes concerned sont désormais miroirés en ligne et la table porte une classe de repaint (.xlsx-resize-col / .xlsx-resize-row).
    • Grille compatible tous les thèmes. Les jetons de grille (--grid-*) sont dérivés du thème actif (--surface, --bg-hover, --text-primary, --border, bandeau zébré en color-mix) : les 15 thèmes × 4 modes de l'application, y compris les modes générés contrasté élevé / sépia et les thèmes importés, sont désormais respectés, plus les couleurs figées du thème par défaut.
    • Menu Fichier. Télécharger, CSV, Markdown, HTML et Imprimer sont réunis dans un seul menu Fichier, placé à droite de la barre des onglets de feuilles, avec le bouton Enregistrer juste à côté. Le ruban conserve les commandes d'édition. Les identifiants des contrôles (#xlsx-download-btn, #xlsx-csv-btn, #xlsx-save-btn) sont inchangés.
    • Barre épinglée pleine largeur. La barre de boutons est épinglée en haut, au bord de la marge de la page, et l'éditeur occupe toute la largeur du contenu (comme les visionneuses PDF / images / Excalidraw).
    • Indicateur retiré. La pastille « Formules calculées localement » disparaît : le calcul local est le comportement normal, pas une limite worthy of a badge.

Corrigé

  • #182 — Éditeur tableur : les encadrés de la sélection s'empilaient et ne suivaient plus le focus

    • Cause racine. Le sélecteur CSS de nettoyage des contours de plage était écrit sans le point (querySelectorAll("xlsx-sel-top,…")). En CSS, un nom nu désigne une balise (<xlsx-sel-top>), pas une classe : la requête ne renvoyait rien et aucun contour n'était jamais effacé. Les encadrés restaient donc en place et s'accumulaient à chaque changement de cellule. Même famille de faute sur les en-têtes reflétés (classList.remove appelé avec un point).
    • Comportement corrigé : le contour suit désormais exactement la sélection — une cellule seule est encadrée, une plage est encadrée sur ses quatre bords réels, et rien ne reste sur les cellules quittées (navigation clavier, clic, changement de cellule, changement de feuille).
    • Écart Sheets corrigé au passage : pendant la saisie, les flèches valident et déplacent la sélection (auparavant elles déplaçaient le curseur dans le texte et la cellule semblait figée). Alt+flèche conserve l'insertion d'un saut de ligne.
    • Non-régression : xlsx-viewer passe de 149 à 152/152 (3 cas #182).
  • #182 — Éditeur tableur : la sélection multiple ne se désélectionnait plus, et les couleurs paraissaient inertes sur une plage

    • Cause racine de la sélection « collée ». La désélection reposait sur l'événement focus de la cellule. Or Maj+clic appelle preventDefault(), ce qui empêche le navigateur de déplacer le focus : activeTd et le focus réel divergeaient. Un clic suivant sur la cellule déjà focalisée ne déclenchait donc aucun focus et la plage restait affichée. Le clic simple efface désormais la plage explicitement dans mousedown, sans dépendre du focus ; après un Maj+clic le focus suit enfin la cellule cliquée (sous suppressSelectionReset, pour ne pas écraser la plage).
    • Zone Nom : après un glissement, elle affiche de nouveau la plage (A1:B2) au lieu de la seule cellule d'origine (A1).
    • Couleurs sur une plage : vérifié sur les trois chemins (menu Mise en forme, nuanciers du ruban, sélection par Maj+flèches ou par glissement) — la charge utile porte bien la plage (A1:B2) et les 4 cellules reçoivent color et background.
    • Retour visuel manquant : les deux nuanciers du ruban affichent désormais une pastille qui prend la couleur choisie, ce qui donnait l'impression que la couleur n'était pas appliquée.
    • Non-régression : xlsx-viewer passe de 146 à 149/149 (3 cas #182).
  • Visionneuse tableur / CSV : plus de ligne teintée au survol, la teinte de fond est réservée aux sélections de 2 cellules et plus (Google Sheets)

    • Le survol ne colore plus rien : les règles tr:hover de la grille XLSX (ligne entière en --bg-hover, y compris sur une cellule déjà sélectionnée) et du tableau CSV sont retirées.
    • Un clic simple ne change plus le fond de la cellule : elle ne garde que son cadre accent (les quatre bords xlsx-sel-* + le contour xlsx-active), comme dans Google Sheets.
    • La teinte .xlsx-selected est désormais conditionnée par la classe .xlsx-range-multi, posée/retirée sur .xlsx-viewer par applySelection() selon l'étendue de la sélection — elle n'apparaît donc qu'à partir de deux cellules. Les en-têtes de ligne / de colonne couverts restent reflétés, comme dans Sheets.
  • Visionneuse tableur : le clic droit écrasait la sélection multiple avant l'ouverture du menu contextuel

    • Cause. Le mousedown du bouton droit laissait le navigateur déplacer le focus sur la cellule ; le handler focus appelait setActiveCell() et réduisait la plage à cette cellule avant que le menu ne s'ouvre. Les entrées Copier / Coller / Effacer / Insérer / Supprimer ne s'appliquaient donc plus qu'à une cellule (Delete 1 row(s) au lieu de Delete 2 row(s)), et la plage disparaissait à l'écran.
    • Correctif. preventDefault() sur le mousedown de bouton droit dans la grille : le focus ne bouge plus, la sélection multiple survit jusqu'au choix d'une option. Un clic droit hors plage déplace toujours la sélection sur la cellule visée (comportement Sheets), via selectForTarget() ; le clic gauche, lui, écrase toujours la plage (#182).
    • Vérifié en conditions réelles sur l'instance Docker : la plage A2:C3 est conservée à l'ouverture du menu, le menu annonce 2 lignes / 3 colonnes, et Copier place bien les 2 lignes × 3 colonnes dans le presse-papiers.
  • #180 — 8 tests JSDOM réalignés sur la sémantique « écriture verrouillée puis application locale » des flux structure / mise en forme (le viewer n'est plus re-lu après chaque écriture) : le second appel PUT est absent, les assertions portent sur la grille peinte en place.

  • Tableur — les sorties du menu Fichier étaient inertes. Les entrées Markdown, HTML et Imprimer portaient data-xlsx-export mais aucun data-xlsx-action, et le répartiteur générique ne lit que ce dernier : le clic ne produisait rien (les identifiants #xlsx-download-btn et #xlsx-csv-btn, eux, fonctionnaient). Elles sont câblées sur export-md / export-html / print, donc sur les fonctions déjà en place.

  • Tableur — collision de classe sur .xlsx-structure-menu. Les menus déroulants de la nouvelle barre de menus portaient la même classe que le menu Structure de la barre d'outils : toute requête .xlsx-structure-menu tombait sur le premier nœud rencontré, donc sur un élément masqué. Les menus de la barre prennent .xlsx-menudrop ; l'aspect reste partagé par le CSS.

  • Tableur — curseur de redimensionnement absent sur la table. La sonde géométrique était conditionnée à la présence d'un en-tête sous le pointeur ; sur la table elle-même aucun en-tête n'est nommé et le curseur ne s'affichait pas. La table retombe désormais sur la sonde géométrique.

  • Tableur — l'export emportait le quadrillage vide. sheetMatrix() renvoyait la grille telle que mise en page : avec les colonnes étendues à A → Z, un classeur de deux colonnes exportait 26 colonnes de vide (CSV, Markdown, HTML, impression). Les lignes et colonnes vides de queue sont écartées — l'export porte le bloc utilisé, comme l'export d'un tableur.

  • Tableur — fuite mémoire à chaque rendu. renderXlsxViewer() ajoutait un écouteur click anonyme sur #content-area : l'élément survit au re-rendu et la fermeture de l'écouteur retenait la grille précédente en entier. Ouvrir ou enregistrer un classeur plusieurs fois faisait croître le tas sans borne. Gestionnaire nommé, démonté en tête de rendu.

  • Tableur — le clic extérieur laissait le menu contextuel ouvert. La fenêtre de grâce de 250 ms, destinée au click résiduel d'un appui long tactile, s'appliquait aussi au clic droit souris — qui n'en émet aucun : un clic délibéré fait juste après l'ouverture était avalé par la grâce. Elle ne couvre plus que l'ouverture tactile.

  • Tableur — une entrée indisponible ne refermait pas la barre de menus. Le répartiteur annonçait « X n'est pas disponible dans ObsiGate » puis sortait avant la fermeture : le menu restait ouvert. Tout choix referme désormais le menu, y compris une entrée qui ne fait que l'annoncer.


[2.53.3] — 2026-10-07

Corrigé

  • BUG-108 - Tableur : « Coller » du menu contextuel muet
    • closeContextMenu() retirait le nœud du menu avant de déférencer _menu : retirer le menu portant le focus émet un focusout synchrone qui rappelle la fonction, et le second remove() sur un nœud déjà démonté lève NotFoundError — le handler de l'action « Coller » du viewer est avorté avant son exécution (régression de la fermeture au focus #179 ; reproduite par l'E2E « coller une plage », CI #850-#854). Déférencement avant retrait ; l'E2E passe après correctif.

[2.53.2] — 2026-10-07

Modifié

  • #187 - Assistant IA : mode agent toujours actif
    • Le bouton « mode agent » du panneau a été supprimé : l'assistant utilise en permanence ses outils (lire, lister, chercher, modifier) ; la sécurité est inchangée — chaque modification demande une confirmation avec aperçu. Les images jointes passent toujours par l'endpoint multimodal classique.
    • Le provider réel de l'agent est désormais résolu comme pour le chat simple (l'étiquette « fournisseur · modèle » affichée est exacte), et les schémas d'outils sont mis en cache (≈ 27 ms économisées par requête).

[2.53.1] — 2026-10-04


[2.53.0] — 2026-10-04

Ajouté

  • #179 — Éditeur tableur : icônes des menus & boutons, fermeture au focus, polish mobile
    • Une icône Lucide par entrée (grille, Structure, Mise en forme, Export) et sur les boutons Enregistrer / « + » ; suppressions marquées danger, aria-haspopup/aria-expanded sur les boutons à menu.
    • Tout menu se ferme à la perte de focus (clic extérieur, focusout, Échap, défilement, redimensionnement) via trackDismissable() ; navigation clavier (↑/↓/Home/End) dans le menu grille.
    • Mobile : entrées ≥ 44 px (pointer: coarse), menus bornés au viewport, ruban sans débordement horizontal.

Corrigé

  • Build Docker : build.ps1 plaçait --no-cache avant build (unknown flag) — flag déplacé après docker compose -f … build, la procédure .\build.ps1 -Test refonctionne.

[2.52.0] — 2026-10-04

Ajouté

  • #166 — Assistant IA : détection & fusion de doublons
    • Score déterministe (Jaccard 70 % + titre 30 %, frontmatter exclu), scan borné (500 fichiers, truncated exposé).
    • Outils find_duplicates (READ) / merge_duplicate_notes (DANGEROUS, backup préalable) ; API GET /api/duplicates et POST /api/duplicates/merge (confirm: true obligatoire, stratégies append / prefer_target / prefer_source).
  • #168 — Assistant IA : notifications externes Discord / Telegram / SMTP / webhook
    • Canaux avec déclencheurs (manual, schedule_failure, schedule_success, duplicate_found), secrets hors config (notify_secrets.json 0600 ou OBSIGATE_NOTIFY_SECRET_<ID>), SSRF-safe.
    • CRUD admin /api/notify/channels, test /api/notify/test, outil notify_external (WRITE).
  • #170 — Assistant IA : tâches planifiées type cron
    • Actions create_file / append_to_file / notify (services existants), planifications interval_hours / daily_time / once_at, tick 60 s (OBSIGATE_SCHEDULER=0 pour désactiver), échec enregistré + notifié.
    • API /api/scheduler/tasks (CRUD + POST …/run), outils create/list/delete/run_scheduled_task*.

[2.51.0] — 2026-10-03

Ajouté

  • #160 — Premier lancement professionnel + section Configuration harmonisée
    • Premier lancement : %USERPROFILE%\ObsiGate est créé et monté comme vault de démarrage (nom « ObsiGate », remplace voute_obsidian), le contexte initial s'ouvre dessus, le dossier home reste une racine nommée d'après son chemin (fin du « bruno » codé en dur), et le document Prise en main.md (contenu embarqué) y est écrit une seule fois — jamais écrasé.
    • Section « 🖥️ Vaults & dossiers (Desktop) » refondue : lignes au motif des cartes de la configuration (icône, nom, chemin tronqué), boutons Retirer/Ajouter en .config-btn-sm secondaire/ primaire, zéro style inline, variables CSS uniquement, cibles tactiles 44px en mobile.

[2.50.2] — 2026-10-02


[2.50.1] — 2026-10-02

Corrigé

  • BUG-106 — page blanche sur /docs : la CSP script-src sans unsafe-inline (#87 T5c) refusait le script inline d'init de Swagger UI, généré par FastAPI et jamais noncé — SecurityHeadersMiddleware injecte désormais le nonce dans le HTML de /docs et /redoc (helper existant inject_csp_nonce, corps ré-encodé gzip), avec 3 tests de non-régression.
  • BUG-107 — drag & drop de fichiers inutilisable sur desktop : Tauri/wry remplaçait le gestionnaire de drag & drop du WebView2 par le sien (aucun événement natif écouté) — les dépôts depuis l'Explorateur n'atteignaient jamais la page. La fenêtre est désormais créée en code (create: false) avec disable_drag_drop_handler() : le HTML5 drag & drop (#89) fonctionne comme sur le web, avec un garde-fou de test.

[2.50.0] — 2026-10-02

Ajouté

  • #159 — Gestion desktop des vaults & dossiers : retrait d'un vault ou dossier racine par menu contextuel (« Retirer de l'application », desktop uniquement, confirmation — déregistration locale, aucun fichier supprimé) et nouvelle section Configuration « 🖥️ Vaults & dossiers (Desktop) » avec listes, retrait par ligne et ajout de vault / dossier racine (nouvelle commande pick_folder, sélecteur natif sans effet de bord). La jump list des raccourcis suit les ajouts et retraits de vaults.

[2.49.4] — 2026-10-02

Corrigé

  • BUG-104 — commandes desktop bloquées par l'ACL Tauri : la fenêtre desktop sert sa page depuis http://127.0.0.1 (origine remote) et aucune commande applicative n'était déclarée — toutes étaient rejetées « not allowed by ACL », erreurs avalées par desktop.js:invoke(). Nouveau manifeste desktop/permissions/commands.toml (allow-app-commands, 19 commandes) référencé par la capability, avec un test garde-fou (test_frontend_invokes_are_acl_allowed). Le bouton « Choisir mon dossier » du wizard et toute la gestion vaults/dossiers desktop fonctionnent.
  • BUG-105 — crash du desktop à l'ouverture (heap corruption c0000374) : le setter de titre du menu contextuel des raccourcis (#77) confiait un PROPVARIANT VT_LPWSTR sur un buffer heap Rust au property store du shell, qui le libérait avec l'allocateur Windows — l'application se fermait 2 à 15 s après le lancement. Fonction supprimée (libellé assuré par SetDescription).

[2.49.2] — 2026-10-02

Corrigé

  • Tests : mobileBlockContaining ignore les commentaires CSS — un sélecteur cité dans une annotation ne faisait plus trouver le bon bloc mobile (CI #842).

[2.49.1] — 2026-10-02

Corrigé

  • Mobile (BUG-103) : le bas du sidebar de navigation n'est plus masqué par la barre d'outils du bas — z-index du sidebar mobile 200 → 950 (au-dessus de .mobile-toolbar, 900).

[2.49.0] — 2026-10-02

Ajouté

  • #158 — onglet Accueil. Le clic sur le titre ouvre désormais la page d'accueil dans un onglet (icône Maison) systématiquement épinglé en tête de barre, réactivable d'un clic comme un onglet normal (le mode split a son propre onglet Accueil dans le panneau actif).
  • #158 — menus contextuels dans la page de navigation. Clic droit sur les répertoires et les fichiers de la vue : même menu que celui de la sidebar (ContextMenuManager), et la ligne d'un vault dans l'arbre ouvre à nouveau la page de navigation (racine du vault).
  • #158 — icône des répertoires alignée sur la sidebar. La couleur du dossier dans la section Répertoires est celle de l'arbre (var(--accent)).

Corrigé

  • BUG-102 — menu contextuel refermé par son propre affichage. L'auto-scroll déclenché par la reflow des icônes (lucide) à l'ouverture du menu arrivait dans le listener scroll (capture) et refermait le menu ~10 ms après son apparition : un décalage de 250 ms est ignoré, les vrais défilements ferment toujours le menu.
  • BUG-101 — raccourcis d'onglets déclarés deux fois. Le bloc « Keyboard shortcuts for tabs » existed en double dans ui.js (fin de fichier) : un seul Ctrl+W fermait deux onglets (le second refermait celui qui venait d'être ré-activé) et Ctrl+Tab sautait un onglet. Bloc dupliqué supprimé.
  • #158 — clic titre ne repliait plus l'arborescence. L'effacement du filtre de la sidebar n'est déclenché que s'il contenait quelque chose (sinon restoreSidebarTree() repliait tout l'arbre à chaque retour à l'accueil).

[2.48.0] — 2026-10-02

Ajouté

  • #158 — onglet de navigation. Chaque clic sur un répertoire de l'arbre (mode focus vault et mode All) ouvre un onglet de navigation dédié qui coexiste avec les onglets de fichiers : la vue chemin + Récents + fichiers n'est plus écrasée par un document ouvert, et change de répertoire en se mettant à jour dans son onglet. La ligne d'un vault reste un simple expandeur ; la racine du vault s'atteint depuis la vue (fil d'Ariane, facette Vaults) ou le sélecteur de vault.
  • #158 — contenu de la page de navigation. La vue liste désormais les sous-répertoires cliquables et embarque le mécanisme de la page de recherche : panneau de facettes Vaults · Tags · Extensions (repli partagé, filtre local avec pastille active), tri Pertinence / Date et bouton Sauver (enregistre le répertoire comme recherche sauvegardée). GET /api/vault/{vault}/files renvoie les tags indexés de chaque fichier (champ tags ajouté à VaultFileEntry) pour calculer les facettes.

Corrigé

  • BUG-100 — retour Home incomplet. Le clic sur le titre perdait la section Raccourcis & Astuces (le template de reconstruction de showWelcome() ne la contenait pas) et laissait la recherche globale ainsi que le filtre de la sidebar actifs : le bloc #quick-help est désormais capturé puis réinjecté, et goHome() efface la recherche (chips, barre de résultats) avant d'afficher l'accueil.

[2.47.1] — 2026-10-02


[2.47.0] — 2026-10-01

Ajouté

  • #157 — panneau de facettes repliable. Le bloc Vaults · Tags · Extensions des résultats de recherche se masque/restaure d'un clic sur un bouton discret (chevron), état mémorisé dans localStorage et conservé d'une recherche à l'autre.

Corrigé

  • #157 — facette « Extensions » absente de l'API. SearchFacets (backend/schemas.py) ne déclarait que tags et vaults : la clé extensions ajoutée par backend/search.py était supprimée par le response_model avant d'atteindre le navigateur. Champ ajouté, plus les tests d'advanced-search pointaient la route inexistante /api/advanced-search et sautaient en silence — ils ciblent désormais /api/search/advanced, avec une assertion stricte sur facets.extensions.
  • #157 — bouton de repli des facettes maladroit sur mobile. En dessous de 768 px, le panneau passe en colonne et le bouton s'étirait en pleine largeur d'écran, pivotait entièrement à la vertical au clic. Le bouton reste compact (align-self: flex-start) et seule l'icône chevron pivote (transition 120 ms), sur mobile comme sur desktop.

[2.46.0] — 2026-10-01

Ajouté

  • #157 — facette « Extensions » dans les facettes des résultats de recherche. Le panneau de facettes de la page de résultats (Vaults · Tags) gagne un troisième groupe qui liste les extensions de fichiers présents dans les résultats (.md, .xlsx, .pdf, …) avec leurs compteurs ; cliquer une extension ajoute l'opérateur ext: à la requête (en remplaçant un ext: déjà actif). Le backend calcule facets.extensions dans backend/search.py.

[2.45.2] — 2026-10-01


[2.45.1] — 2026-10-01


[2.45.0] — 2026-09-30

Ajouté

  • #156-A5 — presse-papiers de plage dans l'éditeur Excel. Copier, couper et coller un bloc de cellules (TSV), au clavier (Ctrl+C/Ctrl+X/Ctrl+V) comme par le menu contextuel ; le collage remplit la plage à partir de la cellule active et la laisse sélectionnée. La grille tient son propre presse-papiers (le presse-papiers système est synchronisé quand le navigateur l'autorise), le collage est toujours du texte — jamais de HTML — et il est annulable. Un bloc plus large que la grille affichée est tronqué et signalé.
  • #156-A6 — raccourcis clavier complets. Ctrl+S (enregistrer), Ctrl+A (sélectionner la feuille rendue), Suppr (vider la sélection), F2 (édition), Ctrl+Home/Ctrl+End (coins de la feuille), Home/End (bords de la ligne), PgUp/PgDn (un écran), Ctrl+flèches (bout de la plage de données) et Maj+Entrée (saut de ligne dans la cellule). Une cellule reste en mode « navigation » jusqu'à la saisie (ou F2) : Suppr et Home/End ne volent donc jamais une touche du curseur de texte.
  • #156-A7 — zone Nom éditable et aide à la saisie. La zone Nom devient un champ (« Atteindre ») : saisir B12, A1:B3 ou Feuille!A1 puis Entrée sélectionne la cellule ou la plage (avec changement d'onglet si la feuille est nommée) ; une référence inconnue est refusée avec un message et l'adresse précédente restaurée. La barre de formule propose les noms de fonctions courants (liste localisée FR/EN).
  • #156-A8 — mise en forme en écriture. Un bouton Mise en forme ouvre un menu qui agit sur la sélection courante (une cellule ou une plage) : gras, italique, souligné, effacement de la mise en forme, alignement gauche/centré/droite, couleur de police et de fond, format de nombre (général, nombre, pourcentage, devise, date, texte), fusion/défusion, volets figés/libérés et largeur de colonne / hauteur de ligne. L'écriture passe par une nouvelle route PUT /api/file/{vault}/xlsx/style, sur le même modèle que xlsx/save et xlsx/structure : verrou par fichier, écriture atomique (.tmp + os.replace), backup automatique, garde de perte (une opération qui effacerait des éléments non préservables est refusée sans force) et concurrence optimiste (If-Match, cf. A12). Les mises en forme ne sont pas proposées sur un .csv (le format n'en porte pas) ni sur une grille en lecture seule. Les couleurs viennent du sélecteur natif, jamais d'une palette codée en dur. Tests : tests/test_xlsx_styles.py (17) et JSDOM (application d'un format, refus d'un CSV, garde de perte).
  • #156-A9 — le calcul est explicitement hors périmètre, et annoncé. L'éditeur n'embarque aucun moteur de formule : ObsiGate affiche et écrit les formules telles qu'Excel les a enregistrées, les cellules dépendantes ne se recalculent pas à l'écran. Le choix est désormais dit à l'utilisateur : un enregistrement contenant des valeurs commençant par = ou @ affiche « enregistrée comme texte » tant que le bouton f(x) n'est pas activé (la garde anti-DDE de BUG-088 reste en place). La pastille « Formules non recalculées » et la fiche #156 portent la décision. Tests JSDOM : l'avertissement apparaît, et disparaît une fois f(x) activé.
  • #156-A10 — historique annuler/rétablir unifié, conservé au re-rendu. La pile ne couvrait que les éditions de cellules : « Effacer le contenu », le tri, le filtre et les actions de structure y entrent désormais, avec des entrées typées (cell, order, hidden, structure). Les piles sont conservées par fichier (8 fichiers, _xlsxHistory) au lieu d'être vidées par un re-rendu ; Suppr passe par le même chemin (donc annulable) ; le filtre est coalescé en une seule entrée par session de frappe ; un rejeu de structure appelle l'endpoint en mode silencieux (pas de doublon d'historique ni de bandeau parasite) et une action destructrice (suppression de ligne/colonne/feuille) n'est pas annulable, faute d'inverse. Tests JSDOM : édition → effacement → tri annulés successivement, insertion de ligne rejouée par l'endpoint, action destructive non annulable, filtre annulé par un seul Ctrl+Z.
  • #156-A11 — sortie (CSV, Markdown, HTML, impression) et recherche sur tout le classeur. L'export ne se limite plus à la feuille entière : le bouton CSV exporte la sélection quand une plage de plusieurs cellules est active (nom de fichier Feuille-A1B2.csv), sinon la feuille ; un menu Exporter ajoute le Markdown (tableau), le HTML (document autonome) et l'impression (la feuille seule, sans le ruban, via un cadre hors écran). Les quatre formats suivent le contenu affiché (la valeur calculée en cache reste une infobulle). L'écart doc/code de #153-A13 (« export de la sélection ») est tranché : la sélection est le défaut quand elle existe. La recherche (Ctrl+F) parcourt désormais toutes les feuilles : compteur « n/m · k feuilles » et activation automatique de l'onglet de la correspondance atteinte.
  • #156-A14 — l'assistant IA couvre les trois formats édités et gagne trois outils. Les outils existants acceptent .xlsm (écriture avec macros préservées) et .csv (lecture et écriture, délimiteur conservé) au lieu du seul .xlsx ; update_xlsx_cells écrit un .csv via save_csv_cells. Nouveaux outils : search_workbook (recherche dans toutes les feuilles, comptée par feuille), analyze_range (nombre, somme, moyenne, min, max d'une plage A1, avec échantillon borné) et edit_xlsx_structure (ajouter/renommer/dupliquer/supprimer une feuille, insérer/supprimer lignes et colonnes) — les deux premiers en risque READ, le dernier conservant la confirmation obligatoire des outils mutants. Tests tests/test_spreadsheet_tools.py (34).

Modifié

  • #156 — Éditeur Excel : audit de complétude, roadmap et 4 défauts enregistrés (documentation seule). Nouvelle fiche docs/features/xlsx-editor-completeness.md (audit par couche, manques fonctionnels, risques, critères d'acceptation) et item #156 dans docs/ROADMAP.md (P0 défauts · P1 presse-papiers/clavier · P2 mise en forme/calcul/undo · P3 sortie/robustesse/perf). BUG-096 → BUG-099 ouverts dans docs/ISSUES_TODOLIST.md : enregistrer un .csv depuis la visionneuse lève un TypeError (reproduit en JSDOM, aucun PUT …/csv/save émis) ; « Charger la suite » d'une feuille .xlsm tronquée répond 415 ; le délimiteur CSV est figé sur , alors que l'export écrit ; ; la sonde de perte plafonnée à 8 Mo peut laisser passer une écriture qui détruit les valeurs calculées en cache. L'audit lui-même ne changeait aucun code ; les correctifs correspondants suivent ci-dessous.
  • #156-A12 — concurrence optimiste entre processus (If-Match, 409 réparable). Chaque lecture renvoie la version du fichier (xlsx_revision : mtime_ns + taille) et les trois routes d'écriture (xlsx/save, xlsx/structure, csv/save) acceptent l'en-tête If-Match ou le champ if_match : si le fichier a changé depuis, l'écriture est refusée en 409 conflict (details.reason = "stale_revision", versions attendue et courante) au lieu d'écraser l'autre écrivain. Le bandeau « Réessayer » relit le fichier puis rejoue la sauvegarde avec la version courante ; les réponses de succès renvoient la nouvelle version, que le client enchaîne sur les écritures suivantes. Tests : TestOptimisticConcurrency (6) et JSDOM (version lue, version rafraîchie par la réponse, relecture avant le retry).
  • #156-A13 — cache des métadonnées par mtime. read_workbook_meta() rechargeait le classeur entier à chaque fenêtre servie ; les métadonnées (styles, fusions, volets, largeurs) sont désormais mémoïsées par (chemin, mtime_ns, taille) (LRU de 8) et invalidées à chaque écriture (cellules, structure, CSV). Charger une fenêtre ne rescanne plus le classeur. L'axe des colonnes au-delà de MAX_COLS reste hors périmètre (voir la fiche #156). Tests : TestMetaCache (4).

Corrigé

  • BUG-096 — enregistrer un .csv depuis la visionneuse échoue (TypeError, aucun PUT …/csv/save émis). Le job de sauvegarde lisait le nom de la feuille dans xlsx_sheets — absent du payload d'un CSV — donc sheets[0].name levait un TypeError avant toute requête : aucune modification de CSV ne pouvait être enregistrée. Le nom est désormais résolu avec repli (feuille → nom du fichier) ; l'endpoint csv/save est inchangé. Tests : JSDOM (un CSV monté sans xlsx_sheets émet un unique PUT …/csv/save).
  • BUG-097 — « Charger la suite » d'une feuille .xlsm renvoyait 415. Un .xlsm s'édite dans la même visionneuse que le .xlsx, mais GET …/xlsx/sheet refusait tout autre format : les lignes au-delà du plafond de 500 étaient inaccessibles. L'endpoint accepte désormais .xlsx et .xlsm (les autres formats restent en 415). Tests : test_xlsx_formats.py (fenêtre servie pour un .xlsm, refus maintenu pour un .csv).
  • BUG-098 — un CSV français (;) s'affichait en une seule colonne. Le délimiteur était figé à la virgule, en lecture comme en réécriture. sniff_csv_delimiter() détecte ;, , ou tabulation (repli : décompte sur la première ligne, puis ,) et save_csv_cells() réutilise le délimiteur détecté — un CSV point-virgule s'affiche en colonnes distinctes et le reste après édition. Tests : test_xlsx_formats.py (lecture, écriture, champs entre guillemets contenant le séparateur).
  • BUG-099 — perte silencieuse possible des valeurs calculées en cache (sonde plafonnée). La détection des <f>…</f><v>…</v> disposait d'un budget unique de 8 Mo partagé entre les feuilles : au-delà, un résultat négatif était pris pour une preuve et l'écriture détruisait les valeurs mises en cache sans avertissement. Le budget est désormais par feuille (4 Mo) avec un plafond global (32 Mo), et un budget épuisé est signalé (cached_values_unverified, libellé FR/EN) : la garde de sauvegarde demande une confirmation au lieu de laisser passer. Tests : TestXlsxCachedValueProbe (budget épuisé → signalé, feuille volumineuse → la suivante reste scannée, scan complet propre → aucun faux positif).

[2.44.1] — 2026-09-29

Sécurité

  • BUG-095 — le job CI security repasse au vert : pyjwt monté au-dessus du dernier correctif. pip-audit bloquait sur CVE-2026-102274 dans pyjwt 2.13.0 (correctif 2.14.0) : le plancher introduit par BUG-091 (>=2.13.0, qui corrigeait PYSEC-2026-178) était devenu insuffisant. Le plancher passe à pyjwt[crypto]>=2.14.0 — volontairement au-dessus de la version préinstallée dans la toolcache du runner (sinon pip répond « already satisfied » et n'aligne jamais la version). Le garde-fou tests/test_ci_workflow.py::TestDependencySecurityFloors verrouille ce nouveau plancher (contre-preuve : plancher remis à 2.13.0 → test rouge).

[2.44.0] — 2026-09-29

Ajouté

  • #155 — Ergonomie du tableur : menu contextuel (clic droit / appui long) et sélection type Excel. Un menu contextuel thémé (nouveau module frontend/js/xlsx/context-menu.js) s'ouvre au clic droit sur une cellule, un numéro de ligne ou un en-tête de colonne, et par appui long sur mobile : insérer une ligne au-dessus/en dessous, une colonne à gauche/à droite (avec count pour une plage), supprimer la/les ligne(s) ou colonne(s) après confirmation, trier A→Z / Z→A et effacer le contenu. La grille adopte la sélection d'Excel : plage par glisser, extension par Maj+clic et Maj+flèches, sélection de la ligne/colonne entière au clic sur son en-tête, curseur croix sur les cellules et zone Nom affichant la plage (A1:B3). Le tri par simple clic sur l'en-tête laisse place à la sélection (tri déplacé dans le menu contextuel). SW_VERSION passe à v29. Tests JSDOM tests/frontend/xlsx-viewer.test.mjs (60, dont 8 nouveaux) et E2E (9) verts.

[2.43.2] — 2026-09-29

Corrigé

  • BUG-094 — une feuille .xlsx vide ou nouvellement ajoutée est désormais éditable et manipulable. Elle s'affichait comme un simple « Feuille vide » sans aucune cellule : il était donc impossible d'y saisir une valeur ou d'y insérer une ligne/colonne (aucune cellule active → aucune action de structure). render_sheets() substitue maintenant un quadrillage vierge 20×8 aux vraies coordonnées A1 (constantes EMPTY_SHEET_ROWS/EMPTY_SHEET_COLS), et la visionneuse retombe sur {row:1,col:1} quand aucune cellule n'est active, de sorte que le menu Structure propose toujours insérer/supprimer ligne et colonne. Test de non-régression : TestXlsxDisplay::test_empty_sheet_renders_an_editable_blank_grid.

[2.43.1] — 2026-09-29

Ajouté

  • #154 — Refonte UI/UX de la visionneuse XLSX (Lot 5, clôture) : extraction modulaire et finitions. Les unités sans état de la visionneuse sont extraites dans frontend/js/xlsx/ : refs.js (références A1), command-bar.js (onglets + ruban + pastilles d'état) et dashboard.js (rendu du tableau de bord) — le noyau avec état reste dans viewer.js à comportement constant. Le tableau de bord devient interactif : cliquer une plage nommée sélectionne et révèle sa première cellule dans la grille (changement d'onglet si nécessaire). L'inspecteur est redimensionnable (largeur mémorisée par session). SW_VERSION passe à v28 et les nouveaux modules entrent dans le pré-cache. Tests JSDOM tests/frontend/xlsx-viewer.test.mjs (51) et E2E tests/e2e/xlsx-viewer.spec.js (9) verts. Aucun changement backend.

[2.43.0] — 2026-09-29

Ajouté

  • #154 — Refonte UI/UX de la visionneuse XLSX (Lot 4) : undo/redo, défilement et accessibilité. Les éditions de cellules peuvent être annulées / rétablies (boutons dans le ruban, raccourcis Ctrl+Z, Ctrl+Maj+Z, Ctrl+Y). Les fenêtres de lignes d'une feuille tronquée se chargent via IntersectionObserver (repli sur l'écouteur de défilement quand l'API est absente). La grille expose désormais une sémantique ARIA (role="grid" / row / gridcell / columnheader / rowheader). Tests JSDOM tests/frontend/xlsx-viewer.test.mjs (47) et E2E tests/e2e/xlsx-viewer.spec.js (9) verts. Aucun changement backend.

[2.42.0] — 2026-09-29

Ajouté

  • #154 — Refonte UI/UX de la visionneuse XLSX (Lot 3) : inspecteur droit. Le Tableau de bord quitte le flux de la grille pour un panneau latéral droit repliable (.xlsx-inspector) : la grille reste visible à côté. L'en-tête de l'inspecteur porte le titre, une entrée Assistant IA (ouvre le panneau latéral global existant) et un bouton de fermeture. Sous 900 px, l'inspecteur se place sous la grille. Tests JSDOM tests/frontend/xlsx-viewer.test.mjs (44) et E2E tests/e2e/xlsx-viewer.spec.js (9) verts. Aucun changement backend.

[2.41.0] — 2026-09-29

Ajouté

  • #154 — Refonte UI/UX de la visionneuse XLSX (Lot 2) : dialogues thémés et conflits non bloquants. Les confirm() / prompt() natifs sont remplacés par des dialogues intégrés au thème (showConfirm() / showPrompt() dans frontend/js/ui.js, promise-based, réutilisant .obsigate-modal-*) pour toutes les actions de structure du classeur (ajouter / renommer / dupliquer / supprimer une feuille, insérer / supprimer une ligne ou une colonne) et la confirmation de perte (409 xlsx_lossy_content). Un conflit d'écriture (409 conflict) n'interrompt plus l'utilisateur : un bandeau non bloquant propose de réessayer en conservant les modifications. Le bouton Enregistrer et l'onglet de la feuille concernée signalent les modifications non sauvegardées. Tests JSDOM tests/frontend/xlsx-viewer.test.mjs (42) et E2E tests/e2e/xlsx-viewer.spec.js (9) adaptés. Aucun changement backend.

[2.40.0] — 2026-09-29

Ajouté

  • #154 — Refonte UI/UX de la visionneuse XLSX (Lot 1) : ruban de commandes groupé, onglets de feuilles permanents avec bouton « + », badges d'état. La vue tableur gagne une barre de commandes segmentée (Formules · Insertion · Vue · Fichier) avec un bouton Enregistrer primaire. La barre d'onglets est désormais toujours affichée (même à une seule feuille) et un bouton « + » y ajoute une feuille (même pipeline PUT …/xlsx/structure, re-rendu depuis le serveur). Deux pastilles d'état annoncent les limites de la vue : lecture seule (.xls/.ods — plus de « + », ni de structure, ni de tableau de bord, ni d'édition) et formules non recalculées. Des tokens de grille dédiés (--grid-bg, --grid-header-bg, --grid-header-text, --grid-border, --grid-zebra, déclinés dark/light) rendent les en-têtes clairement distincts des cellules, avec zébrage, survol et cellule active renforcée. Audit UX, architecture cible et plan par lots : docs/features/xlsx-ui-redesign.md. Tests JSDOM tests/frontend/xlsx-viewer.test.mjs (41, dont 6 nouveaux). Aucun changement backend.

[2.39.10] — 2026-09-29

Sécurité

  • BUG-093 — deux DoS de ressources dans pypdf 6.16.0 (PYSEC-2026-3910, PYSEC-2026-3911) corrigés par le plancher pypdf>=6.16.1. Un PDF peut provoquer un temps de calcul et une consommation mémoire arbitraires, soit via de nombreux contours (outlines), soit via une page portant beaucoup d'objets XForm réutilisés. ObsiGate est directement exposé : backend/pdf_reader.py extrait le texte et parcourt les contours de PDF fournis par l'utilisateur. Le plancher pypdf>=4.0 ne protégeait rien en pratique — l'image du runner Act embarque 6.16.0 préinstallé dans sa toolcache Python, donc pip répondait « already satisfied » et n'alignait jamais la version. Tout plancher de sécurité doit désormais rester au-dessus de la version préinstallée.

Correction

  • Le job CI security n'est plus rouge : la désactivation de semgrep fonctionne, et pip-audit est désormais réparé pour de bon. Le garde-fou tests/test_ci_workflow.py::TestSemgrepStep, en régression depuis la désactivation (il exigeait encore l'exécution de semgrep), vérifie maintenant que l'étape n'exécute que son ::warning:: et que bandit et pip-audit restent bloquants. Nouveau garde-fou TestDependencySecurityFloors : les planchers de sécurité (pypdf, pyjwt) ne peuvent plus retomber sous leur correctif — contre-preuve vérifiée (plancher remis à >=4.0 → test rouge).

[2.39.9] — 2026-09-29

Correction

  • BUG-091 — l'étape Semgrep est désactivée dans le job CI security. Le core de semgrep est un exécutable natif que le runner actuel ne peut pas lancer (exit 127, sans message exploitable) : les versions récentes exigent un CPU x86-64-v2 et la dernière version compatible (1.157.0, core statique vérifié en baseline v1) échoue également. Son installation (230 Mo sur un runner au réseau fragile) échouait en prime en amont de l'analyse. Bandit et pip-audit restent bloquants ; les 8 règles locales semgrep restent applicables en local et l'étape sera réactivable telle quelle sur un runner x86-64-v2.

[2.39.8] — 2026-09-29


[2.39.7] — 2026-09-29

Correction

  • BUG-091 — l'étape Semgrep ne bloque plus la CI quand le runner ne peut pas exécuter le core. Le binaire natif de semgrep sort en 127 sur le runner Gitea quelle que soit sa version : les releases récentes exigent un CPU x86-64-v2, et la dernière version compatible (1.157.0, core statique vérifié en baseline v1) échoue également, sans message. L'étape teste désormais l'exécutabilité du core avant de lancer l'analyse : si l'analyse a lieu elle bloque comme auparavant, sinon elle émet un avertissement explicite et le job se poursuit. Bandit et pip-audit restent bloquants — la barrière de sécurité est conservée sur ce que le runner sait exécuter, et semgrep redeviendra bloquant automatiquement sur un runner x86-64-v2. Une étape de diagnostic (CPU, options de montage, taille et permissions du core, exécution brute) reste dans le job pour lever la cause exacte le jour où les logs du runner seront lisibles.

[2.39.6] — 2026-09-29

Correction

  • BUG-091 (suite) — semgrep-core s'exécutait depuis un venv sous /tmp. Le binaire natif de semgrep sortait en 127 sans message, alors que sa version était bien compatible avec le CPU du runner (core statique, baseline x86-64 v1) : le filesystem /tmp du runner est monté noexec et le noyau refuse l'exécution sans message exploitable. Le venv isolé est donc créé dans $HOME, et l'étape de diagnostic du job security trace désormais CPU, options de montage, taille/permissions du core et exécution brute.

[2.39.5] — 2026-09-29

Correction

  • BUG-092 — les tests réseau ne dépendaient plus du DNS réel. Trois tests de fetch_url mockaient httpx mais laissaient le garde SSRF résoudre example.com pour de vrai : sur un runner au DNS instable, le job CI test échouait en dns_error au lieu d'atteindre la couche testée. Les tests isolent désormais le garde — y compris la référence importée dans webrender, qui échappait au premier correctif — et les tests de garde SSRF continuent de traverser le vrai chemin. Contre-preuve : DNS coupé globalement, la suite passe (1474 tests).

[2.39.4] — 2026-09-29


[2.39.3] — 2026-09-28


[2.39.2] — 2026-09-28

Correction

  • BUG-091 — le job CI security refusait de démarrer semgrep, puis échouait à l'audit des dépendances. Depuis 1.158.0, semgrep ne publie plus que des wheels manylinux_2_34/2_35 dont les bibliothèques natives exigent un CPU x86-64-v2 : le runner Gitea les refuse (« CPU ISA level is lower than required », exit 127). semgrep est désormais isolé dans un venv jetable du job, épinglé à 1.157.0 (dernière publication manylinux2014, baseline v1) — un venv, aussi, parce que ses dépendances contredisent l'environnement principal (tomli~=2.0.1 vs pip-audit ≥ 2.10, pyjwt~=2.12.0 vulnérable). Dans la foulée : plancher pyjwt[crypto]>=2.13.0 dans backend/requirements.txt (PYSEC-2026-178, pyjwt est transitif de mcp) et mise à jour de pip/setuptools dans le job (PYSEC-2026-3721 / PYSEC-2026-3447, apparus récemment dans la base d'advisories). Validé en environnement frais : résolution sans conflit, pip-audit et semgrep verts.

[2.39.1] — 2026-09-28

Ajouté

  • #153 A6 — l'assistant IA sait lire et modifier les classeurs existants. Quatre nouveaux outils dans backend/tools/spreadsheets.py : list_xlsx_sheets (noms de feuilles + dimensions), xlsx_to_markdown (tableau plafonné injecté au contexte du modèle), update_xlsx_cells (édition par lots passant par le service gardé) et append_xlsx_rows (ajout de lignes en fin de feuille). Les mutations demandent confirmation et rafraîchissent la visionneuse (obsigate:file-written).
  • #153 A7 — navigation clavier et barre de formule dans la visionneuse. Tab/Maj+Tab circulent entre les cellules, flèches et Entrée/ Maj+Entrée (multiligne) fonctionnent comme dans un tableur, la cellule active est nommée en A1 dans la barre de formule, une plage se copie, et le focus reste visible et tactile (≥ 44 px, couvert par les E2E mobiles).
  • #153 A13 — tri, filtre, recherche et export CSV dans la feuille. Tri ascendant/descendant par colonne, filtre de lignes, recherche suivant/précédent (respect de casse optionnel) et export CSV de la feuille : toutes des opérations d'affichage, le classeur n'est jamais réécrit.
  • #153 A14 — structure du classeur éditable depuis la visionneuse. Ajout, renommage, duplication et suppression de feuilles ; insertion et suppression de lignes/colonnes autour de la cellule active, via le menu Structure et PUT /api/file/{vault}/xlsx/structure — mêmes garde-fous (backup atomique, verrou, confirmation) que l'édition de cellules.
  • #153 A15 — styles, fusions et volets figés affichés fidèlement. La lecture rend les couleurs de police et de fond, le gras/italique/ souligné, les alignements, les plages fusionnées et l'ancre des volets figés ; un format de nombre personnalisé est signalé par une police à chasse fixe. Une seule charge du classeur (mode normal) suffit pour toutes les feuilles, y compris celles rendues par fenêtres.
  • #153 A16 — formats tableur additionnels. .xlsm éditable avec macros préservées (keep_vba), .xls et .ods en lecture seule (xlrd / odfpy), .csv édité comme un tableur et réécrit au format RFC 4180 (PUT …/csv/save). Dépendances : xlrd==2.0.2, odfpy==1.4.1 dans backend/requirements.txt.
  • #153 A17 — tableau de bord du classeur. Un panneau de la visionneuse liste les plages nommées (portée classeur ou feuille), signale la présence de graphiques et de tableaux croisés (analyse des parties OPC, sans recharger le fichier), donne les statistiques par feuille (cellules, lignes, colonnes, formules, valeurs numériques) et huit KPI extraits de la première zone de données — endpoint GET /api/file/{vault}/xlsx/dashboard.

[2.39.0] — 2026-09-28


[2.38.0] — 2026-09-28


[2.37.0] — 2026-09-28


[2.36.0] — 2026-09-28


[2.35.0] — 2026-09-28


[2.34.0] — 2026-09-28


[2.33.0] — 2026-09-28


[2.32.0] — 2026-09-28


[2.31.0] — 2026-09-28

Correction

  • BUG-090 — troncature silencieuse d'une feuille .xlsx au-delà de 500 lignes × 40 colonnes. render_sheets() renvoie les dimensions déclarées par la feuille (total_rows/total_cols), les plafonds du moteur (max_rows/max_cols) et un flag truncated : la visionneuse affiche un bandeau « Feuille tronquée — 500 lignes affichées sur 520 » (i18n FR/EN) au lieu de présenter une table courte comme complète. La ligne d'en-têtes est désormais figée au défilement vertical (thead sticky, top: auto sur les numéros de ligne pour éviter leur empilement en haut à gauche). #153 A8/R5.

Ajouté

  • #153 A9 — chargement paresseux d'une feuille par fenêtres. GET /api/file/{vault}/xlsx/sheet?sheet=&offset=&limit= renvoie un bloc de lignes (XlsxSheetWindowResponse, plafond 1 000 lignes par requête, has_more de pagination) avec les vraies coordonnées A1 et numéros de ligne de la feuille — une fenêtre se comporte exactement comme le rendu complet. Erreurs typées : 404 feuille inconnue, 415 fichier non-.xlsx. La lecture des valeurs calculées en cache (#153 A12) s'applique aussi aux fenêtres.
  • #153 A9bis — « Charger la suite » sous une feuille tronquée. Un pied de page annonce la progression et fetch la fenêtre suivante au clic ou à l'approche du bas du tableau (sentinelle de défilement). Les lignes ajoutées passent par le même pipeline d'édition que le rendu initial : éditables et sauvegardables immédiatement. Un fetch échoué restore le bouton (retry possible) ; feuille complète → pied de page masqué.

[2.30.0] — 2026-09-27

Correction

  • BUG-089 — un reindex manuel ne reconstruisait pas l'index inversé. reload_index() / reload_single_vault() remplacent l'entrée de vault en bloc, ce qui n'émet pas les notifications incrémentales : la recherche TF-IDF continuait de servir un index périmé après un reindex. Les deux fonctions appellent désormais init_inverted_index(). Au passage, backend/search.py lisait l'index via from backend.indexer import index — une liaison par valeur du dict : un rechargement du module backend.indexer recréait le dict côté indexer alors que la recherche écrivait dans l'ancien, et l'index inversé n'indexait plus rien. Tous les accès passent par _indexer.index. Trouvé en écrivant le test de recherche d'A5 : il passait isolément et échouait en suite complète selon l'ordre.

Ajouté

  • #153 A5 — les tableurs sont indexés par leur contenu. extract_indexable_text() extrait les noms de feuilles et les 20 premières lignes (plafond 5 000 caractères, 20 feuilles) pour le TF-IDF et la recherche sémantique. Un mot tapé dans une cellule rend désormais le classeur trouvable ; la lecture binaire pour l'affichage est inchangée et un classeur chiffré/corrompu s'indexe par son seul nom.
  • #153 A10 — la saisie est typée comme dans Excel. Une valeur TRUE/FAUX/OUI/NON devient un booléen, une date JJ/MM/AAAA (avec HH:MM optionnel) devient une vraie date — et dans l'ordre français : 01/02/2026 est le 1ᵉʳ février. Une saisie ressemblant à une formule n'est jamais convertie.
  • #153 A12 — la valeur calculée s'affiche sous la formule. Quand une cellule porte encore le résultat de son dernier calcul Excel, celui-ci s'affiche dans une ligne discrète sous la formule. La seconde lecture data_only=True n'a lieu que si l'archive contient réellement une valeur en cache, et toute erreur retombe sur l'affichage formules seul. Info-bulle traduite FR/EN (xlsx.cached_value_title).

[2.29.0] — 2026-09-27

Correction

  • BUG-084 — l'index inversé conservait des documents fantômes après la suppression d'une vault. remove_vault_from_index() (backend/indexer.py) ne notifiait pas le hook incrémental : après suppression d'une vault, ses documents restaient dans l'index inversé (postings, doc_info, doc_vault, vault_docs) et continuaient de correspondre aux recherches pour une vault inexistante — seul un reindex manuel les effaçait. Le correctif déclenche _on_index_change('remove', …) pour chaque fichier de la vault, et _remove_doc_internals() supprime désormais la clé vault_docs dont le set devient vide (c'est un defaultdict : une lecture la recréait). Test : TestVaultRemovalPurgesInvertedIndex (contre-preuve : échoue sans le correctif).

Maintenance

  • Index inversé — is_stale() renommé is_ready(). La relecture de plan.md a établi que les étapes 6 et 7 (suppression du cooldown et du hack de coalescence) étaient déjà livrées : ni _last_rebuild, ni _rebuild_cooldown, ni _source_generation, ni _on_vault_change ne subsistent. is_stale() ne mesurait donc plus aucune staleness — il indiquait seulement si l'index initial était construit, sous un nom trompeur. Renommé is_ready(), cohérent avec le is_ready() de SemanticIndex ; l'alias is_stale() de SemanticIndex, sans appelant, est supprimé. /api/diagnostics expose désormais is_ready (libellé « Index prêt » côté frontend/js/config.js). Tests : test_is_ready_tracks_initial_build, test_is_ready_survives_incremental_updates.

  • plan.md recalibré. Le fichier est désormais marqué « livré » et suivi d'une section « État réel » : le code a divergé du plan sur quatre points (pas de repli _needs_rebuild, _ready au lieu de doc_count == 0, rebuild() conservé au démarrage, is_stale() repurposé). Les extraits de code du plan sont explicitement signalés comme ne décrivant pas le code actuel.

  • Fixtures de test locales exclues du suivi Git. test_dir/music/, test_dir/video/, test_vault/images/, test_vault/markdown/ et test_vault/budget.xlsx (~200 Mo) sont ajoutés au .gitignore : aucun test ni job CI ne les référence — les tests unitaires génèrent leurs fixtures dans tmp_path et l'E2E n'utilise que les fixtures committées (test_vault/sample-*.{mp3,png,svg,webm,pdf}, test_dir/*.md). Ils restaient non suivis et polluaient git status.

Sécurité

  • BUG-088 — plus d'injection de formule via la visionneuse Excel. Une saisie =cmd|'/c calc'!A1 (ou @…) était stockée comme formule par openpyxl, donc exécutée par Excel à la réouverture du fichier (DDE). edit_xlsx_cells force maintenant le type texte (cell.data_type = "s") pour toute valeur commençant par = ou @ ; l'API accepte allow_formula: true et la visionneuse expose un bouton f(x) (opt-in, état de session, jamais persisté). +/- restent des nombres.
  • BUG-087 — écriture concurrente d'un classeur. load_workbook() → save() n'était pas sérialisé : deux sauvegardes simultanées (deux onglets, l'agent IA et la visionneuse) faisaient gagner la dernière, en silence. Verrou par chemin (backend/services/mutations.py::_xlsx_write_lock, timeout 15 s) autour du cycle lecture → édition → remplacement ; attente dépassée → 409 conflict. L'endpoint PUT …/xlsx/save est devenu synchrone pour que l'attente s'exécute dans le threadpool.

Corrigé

  • BUG-085 — la perte de données à l'enregistrement d'un .xlsx est annoncée, plus silencieuse. GET /api/file/{vault} renvoie xlsx_lossy_features (éléments qu'un round-trip openpyxl perd) ; la visionneuse affiche un bandeau listant ces éléments et la première sauvegarde demande confirmation avant de renvoyer force: true. Sans force, l'API répond 409 xlsx_lossy_content avec details.features. Périmètre remesuré sur openpyxl 3.1.5 : graphiques, images, dessins et tableaux croisés sont bien préservés ; sont perdus les valeurs calculées en cache, slicers/chronologies, contrôles de formulaire, connexions/requêtes, custom XML, signature numérique, commentaires enrichis et macros.
  • BUG-086 — écriture atomique des classeurs. wb.save() écrivait en place sur le fichier du vault : un plantage laissait un .xlsx tronqué. L'écriture passe désormais par un .tmp puis os.replace() (le backup .bak est inchangé, le .tmp est ignoré par le watcher).
  • Le handler global ServiceError expose maintenant code et details dans la réponse JSON, et api() (frontend) les propage sur l'Error — nécessaire pour que le client distingue un 409 de confirmation d'une autre erreur.

Ajouté

  • #153 (P0) — tests de la visionneuse Excel. tests/frontend/xlsx-viewer.test.mjs (10 tests JSDOM : bannière, confirmation + reprise force, refus, toggle f(x), payload de sauvegarde) et tests/e2e/xlsx-viewer.spec.js (3 tests Playwright sur la fixture test_vault/sample-xlsx-lossy.xlsx) ; la suite JSDOM est branchée dans le CI.

Documentation

  • #153 — Visionneuse & édition XLSX : audit complet et backlog de complétude. La visionneuse .xlsx livrée par #152 a été auditée couche par couche (lecture backend/xlsx_reader.py, écriture backend/services.mutations.edit_xlsx_cells, UI renderXlsxViewer, indexation, outils IA, tests). Bilan : la grille de valeurs est éditée correctement (sécurité, backup, audit, échappement HTML), mais l'ensemble supporté est étroit, une partie du classeur est perdue à l'enregistrement, les tableurs sont invisibles pour la recherche et l'IA ne sait que les créer. Ouverture de l'item #153 dans docs/ROADMAP.md (17 sous-tâches suivies A1 → A17 ; P0 livré, reste P1 recherche/IA/UX puis P2 étendu) et création de la fiche docs/features/xlsx-viewer.md : cartographie du code, limites par couche, tableau des risques R1-R5 et critères d'acceptation par sous-tâche.

[2.28.16] — 2026-09-27


[2.28.15] — 2026-09-27

Sécurité

  • #87 T6 — dépendances qualifiées, pip-audit bloquant (0 vulnérabilité). mistune 3.0.2 → 3.3.3 (XSS/ReDoS/DoS dans le moteur de rendu), python-multipart 0.0.9 → 0.0.31, weasyprint 69 → 70, mcp 1.9.4 → 1.28.1, fastapi 0.110.3 → 0.141.1 + starlette 0.37.2 → 1.7.0, setuptools 84 ; cast(str, …) aux 3 sites d'appel mistune (typage 3.3 resserré). Suite complète 1359 passed, ruff/mypy 0. Seule exception : PYSEC-2026-1325 (ecdsa, Minerva) — aucun correctif upstream ET JWT exclusivement HS256 (backend/auth/jwt_handler.py), les chemins ECDSA P-256 ne s'exécutent jamais → --ignore-vuln documenté.

  • #87 T7 — semgrep SAST local bloquant (8 règles, 0 finding). Ruleset semgrep-rules/ (eval/exec, shell=True, os.system, pickle, yaml.load sans Loader, verify=False, Markup, mktemp) — 100 % local, aucun registre réseau (runner au réseau fragile). Trivy écarté : binaire + base de vulnérabilités à télécharger à chaque run, couche Python déjà couverte par pip-audit bloquant (image = slim + 4 libs).

  • #87 T8 — fin BUG-034 : cookies Secure auto, CORS same-origin explicite. OBSIGATE_SECURE_COOKIES=true|false|auto (défaut auto : Secure en https, sinon rien — logins http locaux préservés ; X-Forwarded-Proto honoré sous TRUST_PROXY, avertissement démarrage affiné, TRUST_PROXY=true dans le compose prod) ; CORSMiddleware same-origin explicite (sûr : web et desktop Tauri same-origin, API directe hors navigateur) ; style-src 'unsafe-inline' conservé et assumé (189 attributs style= + 343 el.style — suppression = réécriture complète, risque nul côté exécution une fois script-src verrouillé en T5c).


[2.28.14] — 2026-09-27


[2.28.13] — 2026-09-27

Modifié

  • #87 (T5c) — script-src sans 'unsafe-inline'. Seuls les scripts avec nonce frais (backend/csp.py, T5b) ou servis par 'self'/CDN listés s'exécutent ; style-src garde 'unsafe-inline' (chantier séparé). Vérifié : test_csp_nonce.py 5/5, 0 handler inline restant dans les pages HTML (propriétés onXxx = fn en JS non concernées par la CSP).

[2.28.12] — 2026-09-27

Corrigé

  • BUG-081 — GET /api/auth/mfa/status → 500 quand l'auth est désactivée. Le pseudo-user anonymous (auth désactivée, mode E2E/CI) n'a aucune entrée en store : get_user(...) → None puis AttributeError sur user.get. Garde None → payload « MFA désactivé » (mfa_enabled: false, totp_enabled: false, webauthn_credentials: 0). Test : tests/test_mfa.py (TestMfaStatusAuthDisabled, échoue en 500 sans le correctif).

[2.28.11] — 2026-09-27


[2.28.10] — 2026-09-27

Corrigé

  • BUG-083 — job CI security rouge : le runner tronquait le # du run: pip-audit. Le runner Gitea Act coupe naïvement au premier # (même entre guillemets) : echo "... see #87)" devenait une citation non fermée (unexpected EOF while looking for matching '"'"). Seul run: du workflow avec un # ; l'echo n'a plus de # (réf #87 en commentaire YAML, jamais vu par le shell). Garde-fou : tests/test_ci_workflow.py (aucun # dans le code des run:, upload.test.mjs verrouillé dans l'étape JSDOM — BUG-082).

[2.28.9] — 2026-09-27

Corrigé

  • BUG-082 — CI lint rouge : suites frontend exigeant jsdom. tests/frontend/upload.test.mjs puis config-ai-keys.test.mjs (imports statiques jsdom, introduits par #89) étaient exécutés dans l'étape frontend racine où jsdom n'est jamais installé (ERR_MODULE_NOT_FOUND, rouge depuis 7bee4a2). Déplacés dans l'étape JSDOM (les deux branches, après install si besoin) ; garde-fou tests/test_ci_workflow.py : aucun fichier de l'étape racine ne doit importer jsdom statiquement.

[2.28.8] — 2026-09-27

Corrigé

  • BUG-080 — harnais E2E local anti-blocage (plus de run pendu toute la nuit). run-e2e-local.ps1 : Playwright lancé via node direct sur la CLI locale (jamais de prompt interactif, Start-Process ne sachant pas exécuter npx ; paramètre $Arguments, $Args étant une variable automatique qui l'écraserait), installation Chromium sautée si déjà présent (E2E_INSTALL_BROWSERS=1 pour forcer), étapes install/test bornées (E2E_TIMEOUT_SEC, défaut 1800 s / 600 s, exit 124 au dépassement — au-delà du globalTimeout pour un abandon propre avec rapport) ; run-e2e-local.sh : npx --yes + mêmes bornes ; playwright.config.ts : globalTimeout (25 min en local, 30 min en CI, E2E_GLOBAL_TIMEOUT_MS pour surcharger) ; e2e-server.ps1 : pidfile resynchronisé sur le vrai propriétaire du port et stop qui tue l'arbre complet (fini les serveurs orphelins qui squattent le port 2029). Garde-fous : tests/test_e2e_harness.py (8 tests).

[2.28.7] — 2026-09-26

Ajouté

  • #87 (T5b) — nonces CSP prêts pour la bascule (sans changement). Nonce frais par réponse dans script-src (backend/csp.py), injecté dans les 6 pages HTML servies (dont la nouvelle route /excalidraw-editor.html, utilisée par l'iframe du viewer au lieu de /static/) et la page de partage ; tests/test_csp_nonce.py (unicité, concordance en-tête/HTML). unsafe-inline conservé jusqu'en T5c.

[2.28.6] — 2026-09-26

Modifié

  • #87 (T5a) — 16 handlers inline convertis en listeners (CSP inchangée). onclick/onerror de index.html et des vues JS (config, plugins, sync, viewer, auth) remplacés par addEventListener ; suites frontend vertes (unit, ai, config-mobile, pdf-viewer, mfa-settings, sidebar-filters).

[2.28.5] — 2026-09-26


[2.28.4] — 2026-09-26

Ajouté

  • #87 (T4) — E2E XSS et serveur E2E piloté. tests/e2e/xss.spec.js : page publique /s/{token} (titre/frontmatter hostile échappé, JSON neutralisé, aucun JS exécuté) et lecteur markdown (sanitizer, aucun on*/javascript: vivant) — 2/2 verts en local. scripts/e2e-server.ps1 : start|stop|status|logs avec progression visible et fichier PID (fini les serveurs orphelins sur le port 2029).

[2.28.3] — 2026-09-26

Ajouté

  • #87 (T3) — cookies Secure et CORS explicites. Helper is_secure_cookies() centralisé (défaut false conservé pour ne pas casser les logins HTTP locaux) + avertissement au démarrage sur bind non-loopback sans Secure ; tests/test_security_headers.py atteste l'absence de CORS permissif (same-origin par défaut du navigateur) et les en-têtes de durcissement.

[2.28.2] — 2026-09-26

Ajouté

  • #87 (T2) — tests de durcissement : concurrence et regex. tests/test_hardening_concurrency.py : créations/mises à jour/login failures concurrents sur users.json (zéro mise à jour perdue, JSON toujours récupérable) et budget temps de la politique ReDoS (motifs catastrophiques rejetés en < 1 s, motifs acceptés < 5 s sur 200 Ko).

[2.28.1] — 2026-09-26

Modifié

  • #87 (T1) — CI sécurité durcie. bandit passe en bloquant (# nosec justifiés : SHA1 non-crypto, subprocess git à argv fixe, saxutils.escape sans parsing ; B105 exclu comme pyproject.toml) ; npm audit --omit=dev bloquant (0 faille) ; les 5 suites frontend hors CI (upload, pretty, media-viewer, mfa-settings, config-ai-keys, vertes en local) rejoignent le job lint. pip-audit reste consultatif (upgrades starlette/weasyprint à qualifier, chantier dédié).

[2.28.0] — 2026-09-26

Modifié

  • #85 (T10) — persistance d'état et clôture de la refonte architecturale. Verrous RLock sur les stores JSON sans protection (revoked_tokens, shares, webhooks + secrets, clés d'outils) avec tests de concurrence (tests/test_store_locks.py — pertes prouvées sans verrou) ; rate-limit auth persisté en option (OBSIGATE_RATELIMIT_DB, SQLite WAL, sémantique identique, défaut mémoire inchangé, tests/test_ratelimit_store.py). Contrat tools/registry.py audité (permissions/quotas/redaction déjà câblés, rien à coder). Index non persisté : rebuild différentiel #86 suffisant (décision documentée). Fiche docs/features/archi-refonte-85.md, #85 sorti du backlog (index roadmap).

[2.27.12] — 2026-09-26

Modifié

  • #85 (T9) — extraction realtime + render hors du monolithe backend/main.py. Le stream SSE /api/events et le WebSocket /ws/collab/* sont servis par backend/routers/realtime.py, le pipeline markdown (mistune, wikilinks, slugs, sanitizer) par backend/render.py (imports directs, plus de couplage différé). main.py (4 827 → ~760 lignes) ne contient plus que l'assemblage : lifespan, middlewares, montage des 16 routers, racine /api, statique/SPA et cales de compatibilité testées.

[2.27.11] — 2026-09-26

Modifié

  • #85 (T8) — extraction vaults/history/conflicts hors du monolithe backend/main.py. 13 routes servies par backend/routers/vaults.py, history.py et conflicts.py ; VaultInfo/BookmarkToggleRequest dans schemas.py, handle watcher partagé dans backend/watcher_state.py. tests/test_api_main.py importe humanize_mtime depuis son module canonique (services.recent).

[2.27.10] — 2026-09-26

Modifié

  • #85 (T7) — extraction du domaine config hors du monolithe backend/main.py. /api/config, ai-keys (get/post/delete/test), tool-keys (×3), ai-models, diagnostics et dashboard sont servis par backend/routers/config.py (_FALLBACK_MODELS, store clés et config déplacés ; main réimporte _load_config pour son lifespan, les fixtures de tests inchangées). tests/test_ai_models.py patch désormais la référence du router.

[2.27.9] — 2026-09-26

Modifié

  • #85 (T6c) — extraction media/pdf/export/guide hors du monolithe backend/main.py. file/pdf, exports (html/md-bundle/epub), guide/download, pdf/stream|info, image, media+thumb, attachments (rescan/stats), vault settings (get/post/all) et vault files sont servis par backend/routers/files_media.py ; le helper Range partagé vit dans backend/routers/helpers.py (tags OpenAPI inchangés, tests statiques frontend media-viewer/image-viewer réalignés).

[2.27.8] — 2026-09-26

Modifié

  • #85 (T6b) — extraction mutations fichiers/dossiers hors du monolithe backend/main.py. PUT .../save|xlsx/save, DELETE/POST/PATCH /api/file, POST/PATCH/DELETE /api/directory, POST /api/move, POST .../batch-upload sont servis par le nouveau backend/routers/files_write.py (effets de bord inchangés : audit, index, SSE, webhooks, plugins, historique) ; 15 modèles dans schemas.py.

[2.27.7] — 2026-09-26

Modifié

  • #85 (T6a) — extraction lecture fichiers hors du monolithe backend/main.py. /api/browse/{vault}, /api/file/{vault}/raw|download|backlinks et GET /api/file/{vault} (vue rendue tous formats) sont servis par le nouveau backend/routers/files_read.py ; modèles dans schemas.py, _content_disposition/_media_max_inline_bytes dans backend/routers/helpers.py (partagés avec les tranches suivantes). Correctif au passage : décorateur orphelin /s/{token} resté en T3 et double-enregistrement de /api/conflicts supprimés.

[2.27.6] — 2026-09-26

Modifié

  • #85 (T5) — extraction du domaine search hors du monolithe backend/main.py. Les 11 routes (/api/search, /advanced, /replace, /tags, /tree-search, /vault/{vault}/paths, /suggest, /tags/suggest, /graph/{vault}, /index/reload, /index/reload/{vault}) sont servies par le nouveau backend/routers/search.py ; les modèles search dans schemas.py et le pool de threads dans backend/search_executor.py (même dimensionnement, même cycle de vie) — comportement inchangé.

[2.27.5] — 2026-09-26

Modifié

  • #85 (T4) — extraction du domaine backups hors du monolithe backend/main.py. Les 9 routes (/api/file/{vault}/backups|diff|restore, /api/backups, /delete, /purge, /content, /compress, /auto) sont servies par le nouveau backend/routers/backups.py ; Diff/Restore* déménagent dans schemas.py et le singleton SSE dans backend/sse.py (partagé avec main) — comportement inchangé, aucun impact utilisateur.

[2.27.4] — 2026-09-26

Modifié

  • #85 (T3) — extraction du domaine sharing hors du monolithe backend/main.py. POST /api/share/{vault}, GET /api/shares, DELETE /api/share/{share_id} et les pages publiques /s/{token}, /s/{token}/raw, /s/{token}/pdf sont servis par le nouveau backend/routers/sharing.py — chemins, réponses, tags OpenAPI et authentification inchangés (aucun impact utilisateur).

[2.27.3] — 2026-09-26

Modifié

  • #85 (T2) — extraction du domaine webhooks hors du monolithe backend/main.py. Le CRUD GET/POST/PATCH/DELETE /api/webhooks (admin) est servi par le nouveau backend/routers/webhooks.py — chemins, réponses, tags OpenAPI et authentification inchangés (aucun impact utilisateur).

[2.27.2] — 2026-09-26

Modifié

  • #85 (T1) — extraction du domaine health hors du monolithe backend/main.py. GET /api/health et GET /api/health/detailed (admin) sont servis par le nouveau backend/routers/health.py (monté dans main.py) et le modèle HealthResponse déménage dans backend/schemas.py — chemins, réponses, tags OpenAPI et authentification inchangés (aucun impact utilisateur).

[2.27.1] — 2026-09-26

Modifié

  • Roadmap — priorisation dette & sécurité (décisions 2026-09-26). Items #85 (refonte architecturale) et #87 (CI/CD) détaillés et marqués prioritaires : backend/main.py mesuré à ~4 827 lignes, tools/registry.py à créer, persistance SQLite/Redis, verrous asyncio, audit des except larges, CI sécurité bloquante (bandit/semgrep/trivy, audits pip/npm), finition CSP nonce (BUG-034), cookies Secure par défaut, rotation clé DeepSeek à confirmer (BUG-006). #73 Sync reporté (P4, hors chemin critique) ; desktop #77 confirmé non signé + doc SmartScreen, reste les 6 tests E2E manuels. Corrections : sections livrées #83/#84 retirées du backlog (détail dans l'archive, index inchangé), total restant recalculé (~12-18 jours chemin critique : #77 fin + #85 + #87).

[2.27.0] — 2026-09-25

Ajouté

  • #152 — Viewer XLSX : affichage des fichiers .xlsx en tableaux multi-feuilles (onglets, en-têtes A1), édition inline des cellules avec PUT /api/file/{vault}/xlsx/save (backup avant écriture, coercion numérique) et téléchargement du fichier d'origine.

[2.25.1] — 2026-09-24

Corrigé

  • /api/diagnostics — erreur 500 « dictionary changed size during iteration ». Le calcul des statistiques d'index itérait inv.word_index et index en direct, pendant que l'indexeur les modifiait depuis un autre thread (build au démarrage, hooks incrémentaux) : l'itérateur de dict levait RuntimeError et l'endpoint renvoyait 500. Les deux dicts sont désormais copiés avant itération (copie atomique sous le GIL), ce qui supprime la course.

[2.25.0] — 2026-09-24

Ajouté

  • #115 — barre d'outils de lecture toujours visible. La barre d'actions d'un document (pop-out, bookmark, Editer, Source, Copier, Partager…) est désormais épinglée en haut de la zone de lecture : elle reste accessible en permanence, même au bas d'un document long, au lieu de disparaître au défilement. Elle est rendue comme un enfant direct du conteneur de défilement (.file-toolbar), masquée en mode lecture, et alignée dans la fenêtre pop-out.
  • #117 — avatars prédéfinis dans le profil. La section Profil propose une galerie de 12 avatars fournis avec l'application (frontend/icons/avatar/) : un clic charge l'image, la recadre au format carré 256 px (même pipeline que l'import) et l'enregistre sur le compte. L'avatar actif est surligné ; l'import d'une photo personnalisée et la suppression restent disponibles.

Corrigé

  • BUG-078 — coloration syntaxique des fichiers de code perdue. Les feuilles de style highlight.js étaient basculées à partir de la clé de thème (defaut-obsigate, …) au lieu du mode (dark/light) : les deux feuilles se retrouvaient désactivées et les blocs de code (.py, .sh, .ps1, .yml, JSON, blocs Markdown…) s'affichaient en texte brut. Le basculement est désormais piloté par le mode, de façon déterministe, dans le moteur de thème (themes.js) comme au premier rendu (ui.js) ; sépia et contraste élevé réutilisent la palette claire.

[2.24.0] — 2026-09-24

Ajouté

  • BUG-077 — assistant IA : bouton « Stop ». Pendant qu'une réponse se diffuse, le bouton d'envoi du composeur devient un bouton Stop (icône carrée, couleur d'alerte) : un clic interrompt immédiatement l'exécution de l'agent (abort de la requête SSE, annulation de la tâche côté serveur à la déconnexion) et la réponse partielle est conservée avec un marqueur « ⏹ Exécution arrêtée. ». Le bouton reste actif (il n'est plus désactivé) tant que l'agent travaille, y compris pendant la reprise d'une confirmation.

Modifié

  • BUG-075 — assistant IA : approbation globale des actions. Une même réponse du modèle peut demander plusieurs mutations (créer un dossier et les fichiers qu'il contient…). Elles sont désormais regroupées en une seule confirmation (au lieu d'une action après l'autre) : la carte liste chaque action avec son libellé et son aperçu de diff, et un unique bouton « Tout approuver (N) » envoie confirm_all — les actions en attente sont appliquées d'un bloc, puis la suite de l'exécution est autorisée sans nouvelle carte. Les appels en lecture du même lot s'exécutent immédiatement (conversation valide). Côté backend, pending.actions remplace le report deferred des mutations d'un lot (backend/agent/loop.py) et confirm_all arme ToolContext.confirmed pour le reste du run (backend/bookslm_routes.py).

Corrigé

  • BUG-074 — assistant IA : bloc d'étapes sans titre et compteur figé à 1. Le résumé replié affiche désormais un titre (le libellé de la première action, « N étapes — Fichier créé : notes/a.md ▶ »), le compteur ne compte plus les réflexions (seules les actions) et la reprise d'une confirmation continue de diffuser dans le même message au lieu de créer un nouveau message « 1 étape » par approbation : le bloc d'étapes s'accumule sur tout l'échange.
  • BUG-076 — assistant IA : arborescence et document ouvert non rafraîchis. Après une action mutatrice de l'agent (création/suppression/renommage de fichier ou dossier, écriture), l'arborescence est rafraîchie immédiatement (rafraîchissement débouncé sur les événements tool mutateurs, sans attendre le watcher) et le document affiché est rechargé depuis le disque ; les outils de création de documents (xlsx/docx/csv/pdf) notifient désormais aussi la visionneuse.

[2.23.0] — 2026-09-24

Ajouté

  • #114 — Configuration, refonte mobile-responsive. La page « Configurations » est désormais pensée pour le tactile (≤ 768 px) : modale plein écran (100dvh, safe-area), sommaire en drawer coulissant gauche (position: fixed, largeur min(320px, 88vw)) avec fond assombri (#config-modal.config-toc-open::before) et bouton de fermeture #config-toc-close (tap sur le backdrop ou Échap ferme le drawer d'abord, puis la modale) ; formulaires sur une colonne ; tous les boutons (save/secondary/danger/add/sm/hamburger/fermer) et liens du sommaire en cibles ≥ 44 px ; champs et selects en 16 px + min-height 44 px (anti-zoom iOS) ; rangée « Sauvegarder / Réindexer / Réinitialiser » sticky en bas de sa section avec safe-area ; MFA (codes de secours en 1 colonne, champ de code full-width et wrap des rangées d'action) ; débordements webhook/token/share corrigés. Dettes HTML/i18n de l'audit incluses : .config-actions-row replacée dans #cfg-backend-settings (+ </section> orphelin supprimé), id dupliqué cfg-partages-publics retiré du <h2>, conteneur mort #plugins-settings-container supprimé, libellé #mt-explorer i18n (settings.explorer), doublons .config-btn-add et règle morte .mfa-recovery-input purgés ; i18n : clés mortes settings.backend, settings.backend_hint, settings.restart_badge, settings.save, settings.plugins supprimées, config.toc_close ajoutée, settings.tabs FR corrigé (« Onglets »). Tests : config-mobile.test.mjs étendu (27, au CI) + E2E config-mobile.spec.js (5, projet chromium-mobile). Fiche : docs/features/settings-mobile-114.md.

[2.22.1] — 2026-09-23

Corrigé

  • BUG-073 — mobile, fin de contenu masquée. La barre de navigation fixe du bas (#mobile-toolbar, 64 px + safe-area) recouvrait le bas de tous les documents et pages en vue mobile (≤ 768 px) : les dernières lignes restaient définitivement sous la barre. Cause : la règle de clearance du bloc mobile ciblait .main-layout, classe absente de index.html (le vrai conteneur est .main-body) — sélecteur mort, aucun dégagement réservé. Correctif : cible .main-body (padding-bottom: calc(64px + env(safe-area-inset-bottom, 0))), suppression de la règle sœur morte .editor-modal.active ~ .main-layout, reset du dégagement en mode lecture (barre masquée) et body.np-active .content-area ramené à 76 px (le dégagement dock, sans double comptage avec .main-body). Tests : tests/frontend/mobile-toolbar.test.mjs (7, au CI) et E2E tests/e2e/mobile-toolbar.spec.js (2, projet chromium-mobile).

[2.22.0] — 2026-09-23

Ajouté

  • #113 — avatar utilisateur. La section « Profil » des Configurations permet de choisir / importer une image (PNG, JPG, WEBP, 8 Mo max) : elle est recadrée en carré 256 px côté client, validée côté serveur (data-URL + octets magiques, sans SVG) et persistée sur le compte (avatar dans data/users.json, exposé par GET/PATCH /api/auth/me et la réponse de login). L'image s'affiche dans le cercle du profil en bas de la sidebar (initiales en repli) et peut être supprimée. UI : aperçu circulaire avec overlay caméra au survol, boutons Choisir / Supprimer, erreurs en ligne. Clés i18n FR/EN.

Modifié

  • #113 — ordre des sections Configurations. La TOC et la page sont réorganisées dans un ordre naturel et parfaitement synchronisées : Profil en premier, puis Sécurité, Thèmes, Recherche, Historique récent, Tags, Fichiers cachés, Synchronisation, Backend, Diagnostics, IA, Sources connectées, Clés API & MCP, Push, Webhooks, Partages publics, Plugins et À propos en dernier. Les ancres cfg-tags et cfg-partages-publics sont désormais portées par leur <section> (cible de défilement = haut de section). Garde-fous : test statique tests/frontend/settings-order-avatar.test.mjs (ordre TOC = page, pas d'ancre morte).

[2.21.0] — 2026-09-23

Ajouté

  • #112 — section compte en bas de la sidebar. L'identité (avatar avec initiales, nom, rôle) et la déconnexion sont regroupées dans un bloc épinglé au bas de la barre latérale, comme sur les sites professionnels ; un clic sur l'identité ouvre le Profil. Nouvelle clef i18n FR/EN pour le rôle.
  • #112 — pellicule d'images défilable. La molette de la souris fait défiler horizontalement la bande de miniatures et deux flèches translucides sur les côtés (révélées au survol) remplissent la même fonction.

Modifié

  • #112 — en-tête allégé. La version quitte le header pour le menu Options (ligne « Version ») ; le bouton de déconnexion et le nom de l'utilisateur sont retirés du header au profit de la section compte de la sidebar. Le header droit ne conserve que l'indicateur hors-ligne, le contexte vault et le bouton Options.

[2.20.0] — 2026-09-23

Ajouté

  • #111 — visionneuse d'images : flèches latérales translucides. Deux boutons superposés le long des bords du cadre passent à l'image précédente/suivante ; quasi invisibles au repos, ils se révèlent au survol (et restent visibles sur les appareils tactiles). Un compteur n / total complète la barre d'outils.

Modifié

  • #111 — navigation d'images fluide. Changer d'image se fait désormais en place (showSibling remplace le src du <img>) : plus de rechargement de la vue ni de nouvel appel /api/browse à chaque flèche, même dans un dossier contenant beaucoup d'images. La liste du dossier est mise en cache (TTL court) et les images voisines sont préchargées ; la pellicule de miniatures reste visible et la vignette active est simplement re-marquée.
  • #111 — image toujours ajustée au cadre. La visionneuse occupe tout l'espace disponible (zone de contenu en flex, sans défilement de page) et l'image est systématiquement redimensionnée dans son cadre, y compris panneau « Métadonnées » ouvert.

[2.19.2] — 2026-09-23

Ajouté

  • Lanceur E2E Windows/PowerShell : scripts/run-e2e-local.ps1 (+ script npm test:e2e:ps) reproduit localement le job CI e2e (uvicorn natif, auth désactivée, fixtures TestVault/TestDir, port 2029, projet chromium-desktop) sans dépendre de bash — indispensable sur les postes Windows où WSL ne démarre pas et où git-bash est bloqué par une politique de contrôle d'application. Le script bash reste la référence pour la CI/Linux.

Corrigé

  • BUG-072 — visionneuse d'images : le plein écran (lightbox) et le panneau « Métadonnées » sont désormais conservés lors de la navigation entre images (flèches ←/→ et clic sur la pellicule) ; auparavant chaque changement d'image recréait la visionneuse et perdait ces états. Le panneau de métadonnées s'affiche maintenant en barre latérale à droite de l'image (au lieu d'une bande sous la pellicule) et reste visible en plein écran.

[2.19.1] — 2026-09-23

Corrigé

  • #110 — lisibilité et positionnement du lecteur média : en mode mobile, la barre audio passe en grille (barre de progression pleine largeur sur sa propre ligne) et les actions secondaires (précédent/suivant/agrandir/volume) sont masquées pour éviter tout chevauchement ; le volume est aussi masqué sur les écrans intermédiaires en desktop, et overflow: hidden empêche le débordement hors de la pilule. La mini-fenêtre vidéo n'est plus ré-aimantée sur les bords : elle se déplace et se redimensionne librement (position absolue mémorisée, centre autorisé), le glisser fonctionnant désormais depuis n'importe quel point de la fenêtre (boutons/curseurs/poignée exclus, seuil de 4 px pour préserver les contrôles natifs de la vidéo).

[2.19.0] — 2026-09-23

Ajouté

  • #110 — Lecteur média persistant « Now Playing » : les fichiers audio et vidéo continuent de jouer pendant la navigation grâce à un contrôleur global frontend/js/now-playing.js qui possède un seul élément <audio>/<video> téléporté entre la vue inline (onglet/panneau) et un dock flottant (<body>), sans interruption de lecture. Dock desktop : pilule verre dépoli (artwork, titre, voûte, play/pause, précédent/suivant, barre de progression, volume, retour au média, agrandir, fermer). Mini-fenêtre vidéo flottante déplaçable/redimensionnable avec aimantation aux coins et bouton Picture-in-Picture natif. Mode mobile : mini-player fixé au-dessus de la barre d'outils (safe-area viewport-fit=cover). Intégration Media Session (écran verrouillé, touches matérielles, Windows SMTC), panneau étendu façon « Now Playing », lecture auto du média suivant/précédent du dossier, reprise après rechargement, notification quand l'onglet est fermé pendant la lecture. Fiche : docs/features/media-viewers-109.md (§ Now Playing, #110).

[2.18.0] — 2026-09-23

Ajouté

  • #109 — Support audio & vidéo (lecteurs HTML5 intégrés) : les fichiers audio (.mp3 .m4a .aac .wav .ogg .oga .opus .flac) et vidéo (.mp4 .webm .mov .m4v) apparaissent désormais dans l'arborescence et l'index (nom/taille/date uniquement, contenu jamais lu, intégrés à SUPPORTED_EXTENSIONS via media_types.py). Nouvel endpoint GET /api/media/{vault}?path=… avec support HTTP Range / 206 Partial Content (Content-Range, Accept-Ranges, 416 sur plage invalide, 413 au-delà de OBSIGATE_MEDIA_MAX_INLINE_MB, défaut 500 Mo) — le helper Range de pdf/stream a été extrait en _stream_file_with_range() et est partagé. api_file_view() expose is_audio/is_video/stream_url/media_mime avant toute lecture texte. Frontend : lecteur audio dédié (artwork, durée via loadedmetadata) et lecteur vidéo (playsinline, scène noire letterboxée), icônes Lucide audio-lines / video, pause à la réinitialisation de la vue, repli téléchargement si le codec n'est pas lisible ou le fichier trop volumineux. Les médias sont exclus du contexte textuel BooksLM et du cache hors-ligne du service worker. Fiche : docs/features/media-viewers-109.md.

[2.17.0] — 2026-09-23

Ajouté

  • #108 — Support complet des images (arborescence, visionneuse, indexation) : les images (.png .jpg .jpeg .gif .svg .webp .bmp .ico) apparaissent désormais dans l'arborescence et l'index comme les autres fichiers — nom/taille/date uniquement, jamais les octets (contenu indexé vide, TF-IDF préservé). Nouveau module partagé backend/media_types.py (extensions + MIME, socle réutilisé par #109). Visionneuse dédiée : zoom molette 0,1×–8×, pan au glisser, double-clic pour réinitialiser, boutons +/−/reset et badge de zoom, navigation ←/→ entre les images du dossier avec pellicule de miniatures, panneau métadonnées (dimensions, taille, type, chemin, date), lightbox plein écran, « Ouvrir l'original » et téléchargement. Endpoint GET /api/media/{vault}/thumb (miniature WebP 256 px, cache disque invalidé par mtime, repli sur l'original pour le SVG, pillow>=10.0). Filtre ext:png/ext:jpg opérationnel ; compteurs d'images séparés dans /api/dashboard (image_count/total_images). Fiche : docs/features/image-support.md.

Corrigé

  • #108-B1 — Affichage isolé d'une image : le <img> généré par api_file_view() pointait vers /api/file/{vault}/raw (qui renvoie du JSON) au lieu de /api/image/{vault} (octets + MIME correct). Corrigé côté backend et dans viewer.js (bouton Plein écran), avec encodage d'URL des chemins.
  • #108-B3 — XSS via SVG : /api/image (et le repli miniatures) pose Content-Security-Policy: sandbox sur les SVG ouverts directement, pour empêcher l'exécution du JavaScript embarqué ; le middleware n'écrase plus une politique stricte posée par une route.

[2.16.6] — 2026-09-22

Ajouté

  • Guides d'utilisation docs/GUIDES/ : nouvel index + 10 guides FR (prise en main, recherche/PDF/Excalidraw, assistant IA & Forge, collaboration temps réel, PWA & hors-ligne, API REST, serveur MCP, authentification & sécurité, déploiement Docker, desktop Tauri). Le guide MCP est déplacé dans docs/GUIDES/MCP.md ; docs/MCP_GUIDE.md devient une page de redirection.

Modifié

  • README.md / README.fr.md : capture d'écran réelle de l'application en tête (remplace l'illustration ASCII) ; un emoji sur chaque entrée de la table des matières ; nouvelle section « Guides » avec liens vers docs/GUIDES/ ; renvois vers les guides depuis les sections API, Recherche, Sécurité, Desktop et Collaboration.

[2.16.5] — 2026-09-22

Corrigé

  • BUG-071 (complément) - spec E2E mobile de la page Configurations : tests/e2e/config-mobile.spec.js (nouveau, projet chromium-mobile, ignoré en chromium-desktop comme mobile-editor.spec.js) : le hamburger révèle le sommaire, le choix d'une section y défile + lien actif + repli auto, aucun débordement horizontal à 393px. Vérifié en local contre l'instance de test (port 2029, auth désactivée) : 3/3.

[2.16.4] — 2026-09-22

Corrigé

  • BUG-071 - Page « Configurations » inutilisable en mode mobile : trois causes. (1) Le sommaire (#config-nav) partageait la règle .help-nav qui le masque sous 768px, mais — contrairement au Guide — la modale n'avait aucun bouton pour l'afficher : aucun moyen d'atteindre une section. Nouvel hamburger #config-hamburger dans l'en-tête (même traitement .help-hamburger que le Guide, libellé traduit config.toc_toggle FR/EN). (2) Les liens du sommaire étaient des ancres brutes sans JS : config.js les intercepte désormais (défilement doux vers la section dans la modale, lien actif, repli automatique du sommaire sur mobile, réinit à l'ouverture). (3) Les grilles 2 colonnes (fournisseur/modèle IA, clé/modèle par fournisseur), les rangées d'ajout à largeurs fixes (jetons, webhooks) et les lignes webhook/jeton/partage en flex une ligne débordaient en 360px : bloc CSS mobile scopé #config-modal (1 colonne, wrap, largeurs inline neutralisées, cibles tactiles 44px, sommaire plafonné à 46vh). data-i18n-attr accepte désormais plusieurs paires attr:clé séparées par ; (titre + aria-label traduits). Tests : tests/frontend/config-mobile.test.mjs (nouveau, 11 — hamburger, i18n, câblage JS, CSS mobile, garde-fou ancres mortes façon BUG-067), enregistré dans le CI.

[2.16.3] — 2026-09-22

Corrigé

  • BUG-070 - Activation clé physique WebAuthn impossible (« Validation du credential WebAuthn échouée ») : deux causes. (1) Les valeurs par défaut (rp_id localhost, origines http://localhost sans port) rejetaient toute URL réelle — logs : Unexpected client data origin "http://localhost:2020", expected one of ['http://localhost']. rp_id/origines sont désormais dérivés de la requête (hôte exact, port inclus ; X-Forwarded-Host/Proto si OBSIGATE_TRUST_PROXY=true), la config explicite restant prioritaire (backend/auth/webauthn_mfa.py::resolve_relying_party, appliqué aux 4 endpoints d'enregistrement et de login). (2) Challenge à usage unique fragile au double-clic/retry (challenge was not expected) : les 5 derniers challenges sont conservés et la vérification accepte le challenge correspondant à la cérémonie en cours. .env.example documente le nouveau comportement. Vérifié au navigateur avec authentificateur virtuel (Playwright CDP, instance Docker) : enregistrement 200 + clé listée, puis clé de test retirée. Tests : tests/test_webauthn.py (+8 : résolution RP, forwarded, retry, roundtrip sans config).

[2.16.2] — 2026-09-22

Corrigé

  • BUG-069 - Login 2FA bloqué sans erreur : après user+mot de passe corrects sur un compte avec 2FA, la page de login restait affichée sans erreur et le challenge MFA n'apparaissait jamais. Cause : showMfaChallenge (frontend/js/auth.js) montait le challenge dans .login-box, inexistant dans index.html (marquage réel : #login-screen > .login-card) → return silencieux. Correctif : montage dans .login-card (repli #login-screen) + erreur visible (mfa.challenge_unavailable, FR/EN) au lieu d'un retour silencieux si le point de montage manque. Vérifié de bout en bout au navigateur (Playwright, instance Docker) : challenge affiché, code erroné → erreur, code valide → connecté. Tests : tests/frontend/mfa-settings.test.mjs (+2 contrôles d'ancrage DOM).

[2.16.1] — 2026-09-22

Corrigé

  • BUG-068 - Configuration : section « 🔒 Sécurité du compte » inachevée : boutons config-btn-primary / config-btn-danger définis depuis les variables du thème (frontend/style.css) ; QR code TOTP généré en local par le backend (POST /api/auth/mfa/totp/setup → qr_data_url, SVG data: via segno, backend/requirements.txt) au lieu de l'image tierce bloquée par la CSP (img-src 'self' data: blob:, secret TOTP exposé) ; codes de récupération affichés aussi à la première activation WebAuthn ; carte « Mot de passe » (changement via POST /api/auth/change-password) et échappement des libellés de clés WebAuthn. Tests : tests/test_mfa.py::test_mfa_setup_returns_local_qr_data_url, tests/frontend/mfa-settings.test.mjs (nouveau, 9 contrôles).

[2.16.0] — 2026-09-22

Modifié

  • #86 - Optimisation globale des performances (phase 3) : ferme les deux derniers points de la phase 3 (recherche via inverted index, PDF lazy et caps regex déjà livrés via BUG-033/BUG-040/BUG-025). Scan différentiel : _scan_vault réutilise les entrées inchangées (size + modified) d'un snapshot précédent — seuls os.walk + stat tournent à chaque rebuild (build_index, reload_single_vault). Extraction excalidraw différée : le scan ne lit plus les .excalidraw / .excalidraw.md (flag excalidraw_text_pending), enrich_pdf_texts() extrait leur texte après index comme pour les PDF. Garde-fou MAX_REPLACE_FILE_BYTES (5 Mio) sur replace_in_files. Tests : tests/test_perf_phase3.py (9). Voir docs/features/perf-phase3-86.md.

[2.15.0] — 2026-09-22

Ajouté

  • #107 - Configuration : gestion des clés API & MCP — nouvelle section « 🔑 Clés API & MCP » dans le panneau de configuration : création, liste (créée / expire / dernière utilisation) et révocation de jetons longue durée utilisables aussi bien sur l'API REST que sur le serveur MCP (/mcp) — un seul et même jeton Bearer pour les deux. Choix d'expiration à la création : 1 jour, 1 mois, 6 mois, 1 an, sans fin. Le secret n'est affiché qu'une fois (jamais persisté en clair, data/api_tokens.json ne contient que les métadonnées) ; révocation immédiate des deux côtés, plafond 50 clés par utilisateur, isolation par utilisateur, audit config_change. Corrigé au passage : le store de révocation (revoked_tokens.json) bornait toute entrée à 7 jours — un jeton longue durée révoqué « reprenait vie » après purge ; il est désormais calé sur l'expiration réelle du jeton. Voir docs/features/api-mcp-tokens-107.md.

[2.14.1] — 2026-09-22


[2.14.0] — 2026-09-19

Ajouté

  • #106 - Assistant IA : actions instantanées contextuelles + catalogue de prompts : la zone d'accueil remplace les 3 suggestions statiques par des actions suggérées selon le contexte détecté (sélection dans l'éditeur → concis/corriger/expliquer ; fichier de code → expliquer/bugs/tests ; 2+ docs → fusionner/comparer/frictions ; 1 doc → résumé 3 points/checklist/frontmatter ; répertoire ou général sinon), avec badge de contexte dans l'en-tête. Un bouton « Toutes les actions » ouvre un tiroir catalogue (recherche instantanée, 6 catégories, 25 actions i18n FR/EN). Nouveau prompt « Générer le frontmatter YAML » au format complet du vault (titre, auteur, dates ISO-8601, tags, aliases, booléens, NomDeVoute, Description) et « Mettre à jour le frontmatter » (conserve les champs existants, actualise modification_date, recalcule tags/aliases/Description, complète les manquants) — tous deux basculent en mode agent et appliquent le résultat via la carte de confirmation. Détail : features/ai-quick-actions.md.

[2.13.1] — 2026-09-18


[2.13.0] — 2026-09-18

Ajouté

  • #105 - Guide d'utilisation : audit de couverture, téléchargement Markdown/PDF, section Architecture : le guide intégré est aligné sur l'application réelle après ~35 features livrées. Huit nouvelles sections — 🏗️ Architecture (diagramme Mermaid des grandes composantes : clients SPA/PWA/Tauri → serveur FastAPI REST, index de recherche, rendu markdown, IA, MCP, WebSocket, webhooks → vaults, data/*.json, backups), 📊 Diagrammes (Mermaid : zoom, plein écran, copie SVG/code, thèmes), ⭐ Bibliothèque (signets, recherches sauvegardées, backlinks & graphe, conflits Syncthing, pièces jointes), 📴 Hors-ligne (PWA, file d'attente IndexedDB, replay, watcher), 👥 Collaboration (Yjs/CRDT, curseurs awareness), 🖥️ Desktop (Tauri, updater signé, assistant premier lancement), 🔌 API (OpenAPI 3.1 : /docs, /redoc, /api, /openapi.json, auth Bearer/cookie, serveur MCP, automatisation) et 🌍 Multilingue — plus des compléments dans les sections existantes (recherche sémantique hybride, notifications web push, export PDF, export HTML/ePub/ZIP, anti-doublons d'upload, vue multi-panneaux, MFA TOTP/WebAuthn, tableau de bord admin). Nouveau GET /api/guide/download?format=md|pdf&lang=fr|en (tag OpenAPI « Guide ») : le document est généré depuis la modale d'aide réelle et les locales, il reflète donc exactement le guide affiché, dans la langue de l'utilisateur ; boutons Markdown et PDF ajoutés dans l'en-tête du guide. En desktop (Tauri, body.desktop-mode) et sur grand écran web, le guide s'élargit (conteneur jusqu'à 1760 px, contenu 1280–1440 px) pour une lecture confortable. Source de vérité du nouveau contenu : scripts/guide_content.py (locales générées, jamais éditées à la main). Tests : tests/test_guide.py (11).

Modifié (ajustements retour utilisateur sur #105)

  • Boutons de téléchargement du guide : icônes seules (plus de libellé « Markdown »/ « PDF »), tooltip i18n explicite sur chaque bouton.
  • PDF du guide : le bloc Mermaid de la section Architecture est converti en diagramme rendu (PNG pré-rendu commité via scripts/build_guide_diagrams.py + scripts/render_guide_diagram.mjs, inséré par diagram_png_for()), au lieu du code brut ; le Markdown garde le fenced ```mermaid (copiable).
  • PDF du guide : emoji rendus en couleur au lieu de rectangles — fonts-noto-color-emoji ajouté à l'image Docker et "Noto Color Emoji" en fin de pile de polices du PDF (la TOC était correcte car elle utilise DejaVu, qui n'a pas les emoji pleine chasse).

Corrigé

  • BUG-067 - Guide : entrée « 📱 Mobile » morte : le sommaire pointait vers #help-mobile-editor, section inexistante (l'édition mobile n'était qu'un <h3> de la section Édition). Le bloc est devenu une section dédiée ancrée ; l'ancre #help-ia (également morte) a été corrigée en #help-ai et un attribut data-i18n-placeholder dupliqué nettoyé. Garde-fou : test_guide_nav_has_no_dead_anchors.

[2.12.2] — 2026-09-18

Corrigé

  • BUG-066 — Configuration : icônes manquantes dans la table des matières : les entrées « Fichiers cachés » et « Partages publics » du sommaire de la page de configuration n'affichaient pas d'icône (les titres de section, eux, en avaient une). Les libellés i18n config.section_hidden (🗂️) et config.section_shares (📤) sont alignés sur leurs titres de section, en FR et EN. Test de non-régression ajouté dans tests/frontend/unit.test.mjs (toutes les entrées du sommaire doivent porter une icône dans les deux langues).

[2.12.1] — 2026-09-18


[2.12.0] — 2026-09-18

Ajouté

  • #104 - Configuration : redesign UI de la section « 🤖 Clés API Intelligence Artificielle » : la longue liste plate de champs devient une interface structurée et dépliable —
    1. En-tête de section : titre + sous-titre explicatifs et une barre de recherche (#cfg-ai-search) pour filtrer les fournisseurs (normalisée, insensible à la casse et aux accents), avec état vide « Aucun fournisseur ne correspond ».
    2. Carte « Configuration par défaut » visuellement distincte : grille 2 colonnes fournisseur / modèle par défaut, et capacités du modèle rendues en badges colorés (nouveau renderCapabilityBadges() dans frontend/js/ai.js, uniquement les capacités actives) au lieu des cases à cocher.
    3. Fournisseurs d'API en cartes dépliables (rendu dynamique depuis AI_PROVIDER_NAMES par _renderAIProviderCards(), frontend/js/config.js) : état replié = logo (initiale), nom, badge de statut (« Configuré » vert / « Non configuré » gris) et corbeille discrète pour supprimer la clé (visible uniquement si configurée, confirmation conservée) ; état déplié = libellés au-dessus des champs, API key à 60 % / modèle à 40 % (grille 3fr 2fr). Les boutons « Configuré/Supprimer » redondants à l'intérieur des champs disparaissent — le statut vit désormais uniquement dans l'en-tête de la carte.
    4. Barre d'actions : « Sauvegarder » (primaire) et « Tester » (outline) dans un footer sticky en bas de section, toujours accessible pendant le défilement, avec le statut de test à côté. Styles .ai-keys-* / .ai-provider-* en variables CSS (profondeur fond/carte, espacement généreux, chevron animé, focus visible, responsive 1 colonne < 600 px) ; textes i18n FR/EN (config.ai_*). Les ID d'éléments (cfg-<provider>-key/model/badge/delete) et le comportement de sauvegarde/test/suppression restent inchangés (rétrocompatible pickers IA). Tests : tests/frontend/config-ai-keys.test.mjs (7 — rendu, badges, bascule, filtre, save, suppression, badges de capacités).

[2.11.6] — 2026-09-18

Corrigé

  • BUG-064 - Éditeur Excalidraw : le diagramme ne s'affiche jamais (canvas vide) — deux causes cumulées :
    1. La feuille de style d'Excalidraw n'était jamais chargée (@excalidraw/excalidraw exige un import CSS explicite). Sans elle, l'éditeur est non stylisé et .excalidraw n'a aucune hauteur fixe : la boucle de redimensionnement d'Excalidraw fait grossir le canvas jusqu'au plafond codé en dur de 2^25 (33 554 432 px), que le navigateur ne peut pas dessiner → scène blanche. Correctifs : <link> vers …/@excalidraw/[email protected]/dist/prod/index.css dans frontend/excalidraw-editor.html et ajout de https://esm.sh à style-src de la CSP (backend/main.py).
    2. appState.collaborators est une Map sérialisée en objet JSON ({}) par l'app Excalidraw / le plugin Obsidian ; réinjectée via initialData, Excalidraw 0.18 appelait .forEach() dessus et plantait (e.appState.collaborators.forEach is not a function). sanitizeAppState() la reconvertit en Map et écarte la géométrie de viewport importée (width, height, offsetLeft, offsetTop) pour les deux formats (.excalidraw et .excalidraw.md). Tests de non-régression : tests/frontend/excalidraw-viewer.test.mjs (CSS lié + CSP), tests/test_security_hardening.py::TestCspExcalidrawStylesheet, tests/e2e/excalidraw.spec.js (hauteur de canvas bornée) + fixtures test_vault/diagram-app-export.excalidraw.
  • BUG-064 (complément) - Éditeur Excalidraw : pleine largeur quand la navigation est masquée : la règle .sidebar.hidden ~ .content-wrapper .content-area { max-width: 1200px } (colonne de lecture centrée) s'appliquait aussi au viewer Excalidraw. Ajout de .content-area:has(iframe[src*="excalidraw-editor.html"]) en max-width: none; margin: 0, comme pour les viewers PDF/image (BUG-062). Fichier : frontend/style.css.
  • BUG-065 - Excalidraw : l'auto-save rechargeait la page en pleine édition : chaque modification déclenchait, 2 s plus tard, un PUT /api/file/…/save ; l'écriture émettait index_updated (SSE) qui re-rendait la vue et recréait l'iframe — un refresh visible qui interrompait le dessin. L'auto-save est supprimée (frontend/js/excalidraw-viewer.js) : sauvegarde explicite par le bouton « 💾 Save » ou Ctrl+S. En complément, reloadExternalWrite ne re-rend plus la vue quand un iframe Excalidraw est déjà ouvert sur le fichier (iframe[data-excalidraw-*]), et le badge « Modified » ne se déclenche plus sur les changements d'appState (resize, zoom) mais uniquement sur le contenu (signature des éléments).
  • #78 (complément) - Bouton plein écran pour les diagrammes Excalidraw : nouveau bouton #btn-fullscreen dans la barre d'outils de l'éditeur (frontend/excalidraw-editor.html) qui bascule le mode plein écran natif ; l'iframe est créée avec allow="fullscreen" allowfullscreen (frontend/js/excalidraw-viewer.js). Fichiers : frontend/excalidraw-editor.html, frontend/js/excalidraw-viewer.js, tests/frontend/excalidraw-viewer.test.mjs.
  • #78 (complément) - Barre d'outils Excalidraw en icônes, verticale à droite : les boutons Save / PNG / SVG / plein écran passent en icônes seules (34×34 px) dans une colonne collée au bord droit (right: 0), débutant à 45% de la hauteur, empilée verticalement, avec infobulles et aria-label. L'icône du bouton Save devient une coche après une sauvegarde réussie. Fichier : frontend/excalidraw-editor.html.

[2.11.5] — 2026-09-17

Corrigé

  • BUG-063 (complément) — Viewer PDF : la table des matières ne déplaçait toujours pas la page : le premier correctif réassignait iframe.src avec le seul fragment #page=N, ce qui ne change que le fragment → navigation same-document que le lecteur PDF natif ignore (il n'applique #page=N qu'au chargement). Diagnostic en Chrome headful par comparaison de captures. navigatePdfToPage() ajoute désormais un paramètre de query horodaté (&_pdfpage=<ts>#page=N) pour forcer un vrai rechargement de l'iframe. Fichiers : frontend/js/viewer.js, tests/frontend/pdf-viewer.test.mjs, tests/e2e/pdf-viewer.spec.js.

[2.11.4] — 2026-09-17

Corrigé

  • BUG-061 — Assistant IA : le bouton « Plein écran » n'agrandissait plus le panneau : la largeur du panneau est écrite en style inline par la poignée de redimensionnement (et par la largeur persistée en localStorage) ; cet inline l'emportait sur la règle .bookslm-panel.fullscreen { width: 100vw }, donc le panneau restait à sa largeur courante. La règle plein écran est désormais prioritaire (!important). Fichier : frontend/style.css.
  • BUG-062 — Viewer PDF : largeur incomplète quand la navigation est masquée : la règle de colonne de lecture centrée (.sidebar.hidden … { max-width: 1200px }) s'appliquait aussi aux viewers plein cadre. Les conteneurs PDF et image sont maintenant exemptés (:has(.pdf-viewer-container) / :has(.image-viewer-container) → max-width: none). Fichier : frontend/style.css.
  • BUG-063 — Viewer PDF : la table des matières ne naviguait pas : les liens faisaient contentWindow.location.hash = 'page=N', mais le lecteur PDF natif vit dans une fenêtre about:blank et l'affectation n'atteignait jamais le document. Nouveau helper navigatePdfToPage() qui recharge l'iframe avec le fragment #page=N ; les entrées portent un data-page et sont câblées par des écouteurs (plus d'onclick inline). Fichiers : frontend/js/viewer.js, frontend/style.css.
  • Tests : tests/frontend/ai.test.mjs (+1), tests/frontend/pdf-viewer.test.mjs (TOC, plein largeur), tests/e2e/pdf-viewer.spec.js (TOC #page=N, largeur, fixture test_vault/sample-pdf-toc.pdf).

[2.11.3] — 2026-09-17

Corrigé

  • BUG-035 — secret_redactor : faux positifs sur les hashs hex : la règle qui masquait tout jeton hexadécimal de 40 à 64 caractères mutilait les hashs git/SHA légitimes des notes. Le masquage des chaînes hexadécimales n'a désormais lieu que si un mot-clé de secret (secret, token, key, password, bearer…) figure dans les 60 caractères précédents ; un contexte de hash (commit, sha256, hash, checksum, git, etag…) exempte explicitement la chaîne. Fichier : backend/secret_redactor.py.
  • BUG-036 — Collaboration WebSocket : jeton accepté en query string : le JWT n'est plus lu depuis ?token= (URLs journalisées par les proxies et l'historique navigateur). Le cookie HttpOnly access_token, envoyé automatiquement par le navigateur lors du handshake same-origin, est le seul transport supporté ; les trames brutes dépassant MAX_MESSAGE_CHARS (16 Mio) sont rejetées avant analyse. Fichier : backend/collab.py.
  • BUG-037 — Mode sans authentification : au démarrage, un avertissement explicite est journalisé quand OBSIGATE_AUTH_ENABLED=false. Le serveur refuse désormais de démarrer s'il est lié à une adresse non-loopback sans l'opt-in explicite OBSIGATE_ALLOW_INSECURE=true, pour empêcher l'exposition publique d'une instance sans authentification (admin anonyme). Fichiers : backend/auth/middleware.py, backend/main.py.
  • BUG-038 — Argon2 : coût mémoire recalibré : memory_cost passe de 64 Mio à 19 Mio (m=19456 Kio, t=2, p=1, recommandation OWASP actuelle) pour supprimer le risque d'épuisement mémoire sous connexions simultanées ; les anciens hachages restent valides et sont migrés automatiquement (needs_rehash). Fichier : backend/auth/password.py.
  • BUG-039 — Énumération de comptes au login : les comptes inconnus, désactivés, verrouillés et limités par le budget par compte répondent tous un 401 Identifiants invalides avec un temps équivalent (hachage factice), au lieu d'un 429/403 distinctif ; seul le rate-limit par IP (non lié à un compte) conserve le 429. Fichier : backend/auth/router.py.
  • BUG-040 — Extraction PDF différée au scan : _scan_vault ne lit plus que les métadonnées des PDF ; l'extraction de texte intégrale (100 kio) est déléguée à enrich_pdf_texts(), exécutée après la construction de l'index/inverted index (démarrage) et après chaque réindexation. Un vault contenant de nombreux/gros PDF démarre sans être bloqué ; le texte reste recherchable une fois l'enrichissement terminé. Fichiers : backend/indexer.py, backend/main.py.
  • Tests : tests/test_api_main.py (redactor hex), tests/test_auth.py (coût Argon2, garde-fou d'instance non authentifiée), tests/test_auth_api.py (login uniforme), tests/test_collab.py (jeton query rejeté, trame surdimensionnée), tests/test_pdf.py (scan différé + enrichissement).

[2.11.2] — 2026-09-17

Modifié

  • Tests E2E PDF (BUG-060) : le helper d'ouverture de fichier de tests/e2e/pdf-viewer.spec.js étend désormais le vault dans l'arborescence s'il est replié (cas d'une instance avec authentification activée) au lieu de supposer le vault déjà déplié.

[2.11.1] — 2026-09-17

Corrigé

  • BUG-060 — Viewer PDF : l'affichage des pages ne fonctionnait pas : au clic sur un fichier .pdf, seule la barre d'outils « PDF — N pages » s'affichait, le corps restant vide. 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é. Le rendu passe désormais par une <iframe> (autorisée par frame-src 'self', le stream /api/file/{vault}/pdf/stream étant same-origin) ; object-src 'none' est conservé. Fichiers : frontend/js/viewer.js, tests/frontend/pdf-viewer.test.mjs (nouveau), tests/e2e/pdf-viewer.spec.js (nouveau, fixture test_vault/sample-pdf.pdf).

[2.11.0] — 2026-09-17

Ajouté

  • #103 — Clés des sources connectées configurables depuis la page Configurations : nouvelle section « 🔗 Sources connectées & recherche » (admin) permettant de saisir ses propres jetons sans toucher aux variables d'environnement : clés de recherche web à clé (Tavily, Brave, SerpAPI, Exa), URL Gitea + token, token GitHub. Stockage dans data/api_keys.json (même fichier que les clés des fournisseurs IA) via les endpoints GET/POST/DELETE /api/config/tool-keys (GET masque les secrets, URLs en clair ; liste blanche stricte ; admin requis). Les outils lisent la valeur stockée en priorité puis l'environnement (Infisical) — backend/tools/secrets.py. Fichiers : backend/tools/{secrets,web,connected}.py, backend/main.py, frontend/index.html, frontend/js/config.js, frontend/locales/{fr,en}.json, tests/test_tool_keys.py (nouveau), tests/test_connected_tools.py.

[2.10.1] — 2026-09-17

Corrigé

  • BUG-059 — Assistant IA : un clic dans la conversation faisait sauter le texte au bas de la fenêtre : dans une conversation ouverte (post ancré en haut), tout clic — lien de fichier, étapes, sélection de texte — dépinait l'ancre via le gestionnaire mousedown et retirait le padding d'ancre, bornant le scroll à la nouvelle hauteur max (saut au bas). Le dépintage est désormais réservé aux vrais gestes de scroll : molette, tactile et poignée de scroll uniquement (isScrollbarPress). Fichiers : frontend/js/bookslm.js, tests/frontend/ai.test.mjs.
  • #92 — create_pdf de l'Assistant : tableaux mal formatés : l'outil create_pdf (génération PDF via l'assistant) utilisait un rendu reportlab simplifié sans support des tableaux. Il passe désormais par le même pipeline que le bouton « Télécharger PDF » de la page document (mistune + plugin table + WeasyPrint), avec repli automatique sur le rendu simple si WeasyPrint n'est pas disponible (GTK absent). Fichiers : backend/tools/documents.py, tests/test_document_tools.py.

[2.10.0] — 2026-09-17

Ajouté

  • #92 — Assistant IA : écosystème d'outils phase 2 (web étendu, sources connectées, documents) :
    • Recherche web à clé : fournisseurs optionnels essayés avant SearXNG — Tavily, Brave Search, SerpAPI (Google) et Exa (OBSIGATE_TAVILY_API_KEY, OBSIGATE_BRAVE_API_KEY, OBSIGATE_SERPAPI_API_KEY, OBSIGATE_EXA_API_KEY), avec ordre personnalisable via OBSIGATE_WEB_PROVIDERS.
    • Transverse : réessais réseau avec backoff maison (OBSIGATE_WEB_RETRY) et cache SQLite des résultats web avec TTL (OBSIGATE_WEB_CACHE_TTL, 900 s par défaut, OBSIGATE_WEB_CACHE_PATH).
    • Rendu dynamique : fetch_url(render=True) délègue les pages SPA à un worker Playwright isolé (dépendance optionnelle, dégradation propre si non installée).
    • Crawl de site : crawl_site (WRITE, confirmation) capture jusqu'à 20 pages d'un même hôte et enregistre un condensé Markdown dans un vault.
    • Sources connectées : Gitea (OBSIGATE_GITEA_URL/OBSIGATE_GITEA_TOKEN) et GitHub (OBSIGATE_GITHUB_TOKEN) — git_list_repos, git_search_issues, git_get_file (READ, rate-limités, audités). Les drives cloud (Google Drive / OneDrive) restent orientés serveur MCP externe (#79), conformément à la feuille de route.
    • Production de documents : create_xlsx (openpyxl), create_docx (python-docx), create_csv, create_pdf (reportlab) — outils WRITE avec confirmation et sauvegarde dans le vault (backup avant écrasement).
    • Chaque outil : libellé labels.py + clés i18n ai.step.* FR/EN + tests unitaires mockés (httpx). Dépendances ajoutées : openpyxl, python-docx, reportlab. Fichiers : backend/tools/{webcache,webrender,connected,crawler,documents,web,schemas,labels}.py, backend/services/mutations.py, tests/test_{web_cache,web_search_providers,webrender,connected_tools,document_tools,crawler}.py.

[2.9.1] — 2026-09-17

Corrigé

  • BUG-058 — Éditeur « Editer » : la barre de numérotation de ligne ne suit pas le thème : CodeMirror peint son gutter (colonne des numéros de ligne) avec des valeurs claires codées en dur (#f5f5f5, bordure #ddd), si bien qu'en thème sombre la barre restait gris clair alors que le fond de l'éditeur suivait le thème. Le gutter est désormais dérivé des variables CSS du thème ObsiGate (color-mix(in srgb, var(--text-primary) …) pour un fond subtil, --text-secondary pour les numéros, --border pour la séparation), ce qui le fait suivre tous les thèmes et modes (sombre, clair, contraste élevé, sépia). Fichiers : frontend/style.css. Tests : tests/frontend/editor-inline.test.mjs.

[2.9.0] — 2026-09-17

Ajouté

  • #102 — Assistant IA : bouton « Ajouter la section » par bloc de code : chaque bloc de code d'une réponse de l'assistant (par ex. une section markdown) affiche un bouton discret « Ajouter la section » qui insère uniquement ce bloc dans le document ouvert (sans les délimiteurs de code), au lieu de la réponse complète. Fichiers : frontend/js/bookslm.js, frontend/style.css, frontend/locales/{fr,en}.json. Tests : tests/frontend/ai.test.mjs.

Corrigé

  • BUG-057 — Assistant IA : bouton « Ajouter » inopérant dans l'éditeur Forge : le bouton ne ciblait que state.editorView (CodeMirror de « Editer ») et affichait « Aucun document ouvert dans l'éditeur » en Forge. _insertIntoEditor() prend désormais en charge les trois surfaces : CodeMirror, l'iframe Forge (délégation par postMessage({ type: 'parent-insert' }) → insertAtCursor dans editor-poc.html) et le textarea de repli. Fichiers : frontend/js/bookslm.js, frontend/editor-poc.html. Tests : tests/frontend/ai.test.mjs, tests/frontend/editor-inline.test.mjs.

[2.8.4] — 2026-09-17


[2.8.3] — 2026-09-17

Corrigé

  • BUG-056 - Éditeur Forge en plein écran : l'Assistant IA s'ouvre en arrière-plan : le panneau de l'assistant est monté dans le document parent, alors que le plein écran Forge porte sur l'iframe — l'API Fullscreen n'affichant que l'élément plein écran et ses descendants, le panneau restait invisible. Le plein écran est désormais quitté avant d'ouvrir le panneau : côté parent (sync.js, sur forge-open-ai — le plein écran peut appartenir au document parent et non à l'iframe) et côté iframe (editor-poc.html, openAssistant), la demande d'ouverture étant émise une fois la sortie effective. Fichiers : frontend/editor-poc.html, frontend/js/sync.js. Tests : tests/frontend/forge-completion.test.mjs (+1), tests/frontend/editor-inline.test.mjs (+1).

[2.8.2] — 2026-09-17

Corrigé

  • BUG-055 (complément) - Éditeur Forge : une complétion acceptée au Tab était supprimée 1–2 s plus tard : l'auto-sauvegarde (2 s) déclenche un événement SSE index_updated sur le fichier en cours, et le parent rechargeait alors le tampon de Forge depuis le disque — écrasant une complétion (ou toute frappe) faite après la sauvegarde. Le rechargement SSE est désormais ignoré tant que le tampon local est modifié (isDirty) ; seul un écrit externe (assistant IA) force le rechargement. L'auto-sauvegarde ne repasse plus l'état « enregistré » si des modifications sont arrivées pendant la requête (Forge et éditeur CodeMirror), et l'acceptation du ghost annule la requête de prédiction en attente. Fichiers : frontend/editor-poc.html, frontend/js/utils.js. Tests : tests/frontend/forge-completion.test.mjs (+3), tests/frontend/editor-inline.test.mjs (+1).

[2.8.1] — 2026-09-17

Corrigé

  • BUG-055 - Éditeur Forge : autocomplétion Tab naturelle et fiable : la touche Tab déclenchait simultanément trois actions indépendantes (indentation, complétion d'un mot du document, acceptation de la prédiction IA), ce qui insérait un ou deux espaces avant le mot complété — et le retour arrière effaçait alors l'ajout. La gestion de Tab est désormais unifiée avec une priorité claire (liste de suggestions ouverte → prédiction IA → complétion de mot du document → indentation), une seule action par appui. Les helpers de complétion sont extraits en fonctions pures partagées avec CodeMirror (getWordFragment, findWordCompletions, normalizeGhost, chooseTabAction) ; plusieurs candidats affichent une liste positionnée au curseur ; la complétion fantôme n'affiche plus un miroir transparent de tout le document (source du décalage visuel et des espaces fantômes) mais uniquement la prédiction, positionnée exactement au curseur et nettoyée dès que le curseur bouge ou que la vue défile ; une complétion de mot ne peut plus introduire d'espace. Le prompt /api/ai/inline-complete est simplifié et borné à 128 tokens pour des suggestions plus courtes et plus rapides. Fichiers : frontend/editor-poc.html, frontend/js/autocomplete.js, backend/ai.py. Tests : tests/frontend/forge-completion.test.mjs (nouveau, 28 tests).

[2.8.0] — 2026-09-17

Ajouté

  • #101 - Forge : Assistant IA partagé et plein écran (Forge & Editer) : le bouton « AI Panel » de Forge ouvre désormais le panneau Assistant IA existant (barre latérale) au lieu d'un mini-chat isolé — même contenu, même fournisseur/modèle, même historique, mêmes menus / et @, sans duplication. Forge lit la sélection du sélecteur de l'assistant (localStorage['obsigate_ai_picker'], même origine) et l'injecte dans ses appels /api/ai/* et sa complétion fantôme (repli ollama si aucun fournisseur choisi) ; au passage, les endpoints erronés sont corrigés (make-longer/make-shorter, target_lang). Un bouton plein écran natif est ajouté à Forge (iframe allow="fullscreen") et à l'éditeur Editer (plein écran sur le conteneur, fonctionne en modale comme en inline, sortie à la fermeture). Libellés i18n FR/EN. Fichiers : 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 : tests/frontend/editor-inline.test.mjs (+10).

[2.7.5] — 2026-09-17

Corrigé

  • BUG-054 - Éditeur « Editer » : le bouton Sauvegarder restait bloqué sur le spinner de chargement : le bouton #editor-save est un nœud DOM partagé entre toutes les sessions d'édition (y compris en mode en ligne). Une sauvegarde manuelle y remplaçait le crochet par un spinner et le désactivait, mais cet état n'était jamais remis à zéro : après une sauvegarde réussie (l'éditeur se ferme puis se rouvre), après une sauvegarde Forge, ou après un échec de requête (le catch ne restaurait ni l'icône ni l'état), le spinner persistait jusqu'à un rechargement complet de la page. Un helper resetSaveButton() restaure désormais le crochet et réactive le bouton à l'ouverture (openEditor), à la fermeture (closeEditor) et en cas d'échec (saveFile). Fichier : frontend/js/utils.js. Tests : tests/frontend/editor-inline.test.mjs (+4).

[2.7.4] — 2026-09-17

Corrigé

  • BUG-053 - Assistant IA (mode agent) : le fichier demandé n'est pas créé : le prompt système du mode Général (et du dossier vide) enseignait encore le protocole texte obsigate-action, si bien que le modèle décrivait l'action dans un bloc texte au lieu d'appeler l'outil natif create_file — rien n'était donc créé (et le bloc, volumineux, était tronqué avant sa fermeture). En mode agent, le prompt demande désormais d'appeler directement les outils natifs (create_file, create_directory, …) et interdit les blocs obsigate-action ; le chat classique conserve le protocole texte. La limite de sortie de l'agent passe à 8 192 jetons pour laisser place au contenu complet d'un fichier. Fichiers : backend/bookslm.py, backend/bookslm_routes.py. Tests : tests/test_bookslm.py (+3).

[2.7.3] — 2026-09-16

Corrigé

  • BUG-052 - Assistant IA : recherche web sans réponse finale (étapes et sources affichées, aucun texte) : quand le budget d'itérations (DEFAULT_MAX_ITERATIONS = 10) ou le quota d'appels d'outils était épuisé pendant que le modèle enchaînait encore des recherches/lectures, la boucle d'agent renvoyait un contenu vide → la conversation n'affichait que les étapes et les sources. La boucle effectue désormais un dernier appel sans outil qui demande au modèle de synthétiser les informations recueillies (_finalize_answer), avec un repli déterministe listant les sources si cet appel échoue ou reste vide. Les appels d'outils non atteints du lot en cours de quota reçoivent un résultat deferred pour garder la conversation valide. Fichier : backend/agent/loop.py. Tests : tests/test_agent_loop.py (+2).

[2.7.2] — 2026-09-16

Corrigé

  • BUG-051 (complément) - Repli Bing : en-têtes de navigation navigateur : un User-Agent navigateur seul ne suffit pas — Bing renvoie des résultats factices (SERP sans rapport avec la requête) aux appels dépourvus des en-têtes de navigation habituels. Les fournisseurs HTML (DuckDuckGo, Bing) envoient désormais Accept-Language, Sec-Fetch-* et Upgrade-Insecure-Requests (BROWSER_HEADERS). Vérifié en conteneur : recherche « Canadien de Montréal 2026-2027 » → résultats NHL / RDS / Wikipédia pertinents (provider: bing). Fichier : backend/tools/web.py.

[2.7.1] — 2026-09-16

Corrigé

  • BUG-051 - Assistant IA : « je ne peux pas accéder à internet » malgré la recherche web : lorsque l'instance SearXNG auto-hébergée ne remontait aucun résultat (moteurs amont suspendus/CAPTCHA), web_search renvoyait une liste vide et le modèle concluait à une absence d'accès réseau. L'outil essaie désormais une chaîne de repli sans clé — SearXNG, puis DuckDuckGo (endpoint HTML sans JS), puis Bing (page de résultats HTML) — et ne s'arrête qu'au premier fournisseur qui renvoie des résultats (provider dans le résultat, OBSIGATE_WEB_FALLBACK=0 pour désactiver les replis). Le message d'avertissement final nomme les fournisseurs essayés. Fichier : backend/tools/web.py. Tests : tests/test_web_tools.py (+4).

[2.7.0] — 2026-09-16

Ajouté

  • Assistant IA — 30 skills intégrés (au lieu de 11) et prompts enrichis : les skills déclenchés par / couvrent désormais l'extraction/structuration (/extract, /timeline, /glossary, /tag), la transformation (/translate, /adapt, /clean, /summary-progressive), l'analyse critique & décision (/critique, /compare, /prioritize, /swot, /debate), l'apprentissage (/quiz, /reading-note, /qa-generator) et la méta-gestion (/link, /anonymize, /estimate). Tous les prompts sont réécrits (rôle, structure de sortie Markdown, cas limites) et complétés par un bloc COMMON_RULES commun (français, notes traitées comme données, anti-hallucination, signalement des contradictions, conservation des noms/dates/chiffres, réponse « Aucune information exploitable fournie. » si les notes sont insuffisantes). Fichier : backend/skills.py. Tests : tests/test_skills.py (ids uniques/valides, présence du prompt et de COMMON_RULES pour chaque skill).

[2.6.2] — 2026-09-16

Corrigé

  • BUG-050 — Assistant IA : échec de la création d'un sous-dossier contenant un fichier : lors d'une pause de confirmation, la boucle d'agent ne renvoyait le résultat que du seul appel confirmé alors que le message assistant annonçait tous les appels d'outils du tour — la conversation devenait invalide (identifiant tool_call_id sans réponse) et la reprise échouait. Les appels non atteints reçoivent désormais un résultat deferred explicite (backend/agent/loop.py). En complément, create_directory est idempotent côté outil IA (succès si le dossier existe déjà, backend/services/mutations.py) et les consignes (create_file crée les dossiers parents) invitent le modèle à un seul appel avec un chemin imbriqué (backend/bookslm.py, backend/tools/service.py). Tests : tests/test_agent_loop.py (+1), tests/test_tools_mutations.py (+1), tests/test_api_main.py (+1).

[2.6.1] — 2026-09-16


[2.6.0] — 2026-09-16

Ajouté

  • Barre de filtrage de la sidebar sur « Récents » et « Sauvegardes » (#99) : la barre de recherche de la sidebar agit désormais sur les onglets Récents (filterRecentFiles, filtrage titre/chemin/vault/aperçu/tags) et Sauvegardes (filterSavedSearches, cumulable avec les pills Tous/Recherches/Répertoires) — insensible à la casse et aux accents, avec message d'absence de résultat et placeholders dédiés (sidebar.filter_recent, sidebar.filter_saved). Le routage de initSidebarFilter est unifié (routeFilter/routeClear) pour couvrir les cinq onglets. Fiche : docs/features/sidebar-filters.md.
  • Assistant IA — Deep Research en pastille (#100) : « Deep Research » ajoute désormais une pastille (comme les skills) au lieu d'écrire la directive dans la zone de saisie ; le mode Agent est activé et la directive est injectée au moment de l'envoi, sans polluer le message affiché.

Corrigé

  • BUG-049 — icône du bouton « + » de l'assistant invisible : la règle générique .bookslm-input-area button écrasait .bookslm-btn-plus (padding: 8px 16px sur une largeur de 32 px ⇒ largeur de contenu nulle ⇒ SVG à 0 px). Sélecteur porté à .bookslm-input-area button.bookslm-btn-plus, l'icône plus est de nouveau visible (vérifié en navigateur : SVG 0 px → 18 px). Tests : tests/frontend/ai.test.mjs (+1), tests/frontend/sidebar-filters.test.mjs (nouveau, 8 tests).

[2.5.2] — 2026-09-16


[2.5.1] — 2026-09-16


[2.5.0] — 2026-09-16

Ajouté

  • Assistant IA — filtre de recherche dans la sidebar « Historique IA » (#98) : la barre de filtrage de la sidebar agit désormais sur l'onglet Historique IA. filterAIHistory() (frontend/js/config.js) filtre les sessions par titre, aperçu, répertoire, contexte ou libellé de mode — insensible à la casse et aux accents (_aiNorm) — avec cache sessions (_aiSessionsCache), message « aucune conversation ne correspond à la recherche » (bookslm.history_no_match) et placeholder dédié sidebar.filter_ai. Le routage saisie / touche casse / bouton « × » vers ce filtre est géré dans initSidebarFilter (frontend/js/sidebar.js) quand l'onglet IA est actif.

Corrigé

  • BUG-048 — menu d'ajout « + » : « Contextes » et « Skills » ouvrent enfin leurs menus « @ » / « / » : le clic sur une entrée du panneau .bookslm-ext-menu remontait au gestionnaire du panneau qui annulait le rendu asynchrone du menu (jamais affiché) ; e.stopPropagation() sur les entrées corrige le comportement. Le bouton « + » porte l'icône Lucide plus. Tests : tests/frontend/ai.test.mjs (+3), nouvelle suite tests/frontend/ai-sidebar.test.mjs (6 tests).

[2.4.0] — 2026-09-16

Ajouté

  • Assistant IA — historique permanent des conversations (#95) : les échanges sont désormais persistés côté backend (backend/ai_history.py, data/ai_history/{user}.json, cap 200, écriture atomique) et plus seulement en localStorage. Nouveaux endpoints GET/PUT/DELETE /api/ai/bookslm/history[…] (liste résumée sans messages → full, upsert qui force l'id, suppression, isolation par utilisateur). Le panneau synchronise ses sessions au chargement (debounce 600 ms, repli hors-ligne sur le cache local), migre les anciennes clés bookslm-sessions-* / bookslm-history-* et notifie la sidebar via l'événement bookslm:history-updated.
  • Assistant IA — accès rapide à l'historique dans la sidebar (#96) : cinquième onglet de navigation #sidebar-tab-ai (icône messages-square) listant les conversations par ordre chronologique ; un clic ouvre la conversation dans le panneau Assistant IA (openWithSession).
  • Assistant IA — panneau « + » extensible (#97) : le bouton « Attach an image » est remplacé par un bouton « + » ouvrant un panneau modulaire (registre _extensions simple à étendre) : Fichiers, Image, Contextes, Skills, Deep Research (mode agent + prompt pré-rempli), Recherche web et Canva (les deux derniers en « Bientôt »).
  • Assistant IA — bouton de soumission arrondi (#94) : .bookslm-btn-send circulaire (40 px), icône Lucide arrow-up remplaçant l'avion ✈️.
  • Fiche feature : docs/features/ai-assistant-history.md. Tests : tests/test_bookslm.py (+11), tests/frontend/ai.test.mjs (+3).

[2.3.4] — 2026-09-16

Documentation

  • AGENTS.md — versionnage automatique : la méthode obligatoire de fin de tâche rappelle désormais que VERSION (racine) est la source unique de vérité (MAJEUR.MINEUR.CORRECTIF), incrémentée à chaque commit par le hook .githooks/prepare-commit-msg, que les dérivés (package.json, desktop Tauri, READMEs, docs/ROADMAP.md, CHANGELOG.md) sont resynchronisés dans le même commit, et que le tag vX.Y.Z est publié au push. Ligne ajoutée à la cartographie documentaire. Ménage : suppression des captures de diagnostic (diag-*.png, ogdiag.png, state.png) et du script jetable tmp-verify-inline.cjs restés à la racine du dépôt.

[2.3.3] — 2026-09-16

Corrigé

  • #93 (complément) — garde-fous de session d'édition inline : une session d'édition (« Editer » ou Forge) qui possède la zone de contenu était détruite par deux chemins qui vident cette zone sans la libérer d'abord — l'activation d'un onglet (TabManager.activate, PaneTabManager.activate) et l'événement SSE index_updated sur le fichier affiché (un fichier modifié hors de l'app relançait openFile() et écrasait le DOM de l'éditeur). L'activation d'onglet appelle désormais detachInlineEditor() avant le placeholder de chargement, et l'écriture externe passe par reloadExternalWrite() (le tampon de l'éditeur est rechargé depuis le disque au lieu d'un re-rendu de la vue lecture). Nouveau helper queryEditor() (editor-inline.js) : l'en-tête, le pied et la marque voyagent avec le conteneur en mode inline, un modal.querySelector() les manquait ; closeEditor() remet le conteneur dans la modale avant de restaurer en-tête/pied/marque. Fiche : docs/features/editeur-inline.md. Tests : tests/frontend/editor-inline.test.mjs (25).
  • Documentation — hooks git obligatoires à l'installation : docs/CONTRIBUTING.md documente l'appel à scripts/install-hooks.sh (versionnage automatique) dans les étapes de mise en place d'un clone.

[2.3.2] — 2026-09-16


[2.3.1] — 2026-09-16


[2.3.0] — 2026-09-16

Ajouté

  • #89 Drag & Drop complet de fichiers/dossiers & intégration Assistant IA — solution complète de glisser-déposer de fichiers individuels, multiples et dossiers récursifs depuis l'OS vers le vault (POST /api/vault/{vault}/batch-upload, FileSystem API), overlay plein écran à double cible (dépôt vault vs analyse IA), surbrillance des cibles dans l'arborescence, et dropzone BooksLM pour injection de contexte ou pièces jointes. Fiche : docs/features/drag-and-drop-ai.md.
  • #89 Pretty print fichiers texte — bouton « Pretty » dans le mode lecture des fichiers non-markdown (code, JSON, configs, logs...) : formatage JSON, numéros de ligne avec gouttière et coloration syntaxique highlight.js alignée sur le thème clair/sombre, avec colorateur générique de secours (chaînes, commentaires, nombres, niveaux de log) quand highlight.js ne reconnaît pas le langage (txt, log, plaintext).
  • #88 Assistant IA : contexte applicatif — l'assistant en mode Général reçoit l'état vivant de l'interface (app_context) et connaît désormais les documents ouverts, le répertoire et le vault courants, la recherche en cours (requête + premiers résultats affichés) et les fichiers récemment modifiés du utilisateur. Le prompt Général est enrichi d'un bloc « Contexte applicatif actuel » (build_general_system_prompt(vaults, app_context, recent_files)). Fiche : docs/features/ai-app-context.md.
  • #90 Barre d'actions du document — regroupement fonctionnel : les boutons en haut du document sont réordonnés en quatre groupes séparés par des barres verticales (span.action-sep) : [TOC] [pop-out] [Bookmark] | [Editer] [Source] [.md] [Forge] | [Copier] [PDF] [Export] | [Partager]. Le bouton « Pretty » (fichiers texte) rejoint le groupe édition, les boutons non applicables à un type de fichier sont omis et les spacers d'un groupe vide ne sont pas rendus. Fichiers texte non markdown : [pop-out] [Bookmark] | [Editer] [Source] [.ext] [Pretty] | [Copier] | [Partager] (pas de TOC, pas de PDF/Export). La fenêtre pop-out (popout.html) est alignée sur le même ordre markdown (groupes nav / édition / export / partage). Tests : tests/frontend/toolbar-order.test.mjs (9).
  • #91 Assistant IA — zone de discussion façon Notion : le fil de discussion est reformaté (messages chronologiques, post utilisateur en bulle alignée à droite, réponse assistant pleine largeur sans fond, appels d'outils agent repliés en bloc « N étapes ») ; après l'envoi, la question est ancrée en haut de la zone visible (scrollIntoView block:start + scroll-margin-top) et la réponse se diffuse en dessous — le streaming ne provoque plus de saut de scroll ; un libellé discret au-dessus de chaque réponse rapporte le fournisseur et le modèle réellement utilisés (SSE provider/model) ; une barre d'actions au survol sous chaque bloc : « Copier » (texte brut + toast) et, pour les réponses, « Ajouter » (insertion dans le document ouvert dans l'éditeur Forge). Complément : la section « N étapes » affiche désormais des libellés humains produits par le backend (backend/tools/labels.py, événements SSE en direct pendant l'exécution + ligne « Réflexion »), et deux nouveaux outils web principaux sont disponibles en mode agent : web_search (SearXNG auto-hébergé) et fetch_url (page publique → texte, garde SSRF). Le reste des catégories d'outils Notion est documenté pour le futur dans la fiche. Fiche : docs/features/ai-assistant-conversation-ux.md Tests : tests/frontend/ai.test.mjs (76), tests/test_tool_labels.py (8), tests/test_web_tools.py (8).
  • #91 Assistant IA — section d'étapes enrichie : l'en-tête affiche « N étapes » suivi d'un chevron ▶ / ▼ (fin du préfixe « > ») et, pendant l'exécution, un indicateur animé de trois points devant le libellé ; la barre de chargement au-dessus de la zone de saisie est supprimée (en chat simple, l'indicateur s'affiche dans la bulle de réponse en attente). Chaque note de raisonnement devient une sous-section « Réflexion ▶ / ▼ » contenant le texte du modèle, et les recherches web ajoutent une sous-section « Sources (N) » avec les liens cliquables (le backend émet sources [{title, url}] dans l'événement SSE tool, résultats web_search plafonnés à 8 + page fetch_url). Les états d'ouverture (étapes, réflexion, sources) sont mémorisés sur le message : un token re-rendu ne referme jamais ce que l'utilisateur a déplié. web_search signale désormais explicitement au modèle une instance SearXNG sans résultat (moteurs suspendus/CAPTCHA) au lieu de le laisser relancer la même recherche jusqu'au quota. Tests : tests/frontend/ai.test.mjs (82), tests/test_bookslm.py::TestToolEventSources (4), tests/test_web_tools.py (10).

Modifié

  • #93 Édition inline — « Editer » et « Forge » remplacent la vue lecture : le conteneur d'édition (#editor-container, CodeMirror ou iframe Forge) est déplacé dans la zone de contenu du document (#content-area, ou le panneau actif en vue fractionnée) au lieu d'être affiché dans l'overlay plein écran : le mode édition remplace le document en mode lecture (bandeau, métadonnées, tags et contenu). L'overlay reste monté mais neutralisé (transparent, sans capture des clics, z-index conservé) car le ruban d'édition mobile y est ancré. Retour à la lecture par ✓ (sauvegarde), ✕, Échap ou l'ouverture d'un autre fichier — trois garde-fous libèrent proprement la session (destruction CodeMirror/Yjs, retrait de l'iframe Forge) avant qu'un rendu concurrent n'efface son DOM : renderFile(), l'activation d'onglet (TabManager.activate, PaneTabManager.activate, qui vident la zone avant de rendre) et l'événement SSE index_updated (un fichier modifié hors de l'app recharge le tampon de l'éditeur au lieu de re-rendre la vue lecture). L'overlay n'est conservé qu'en repli, quand la cible n'est pas le document affiché (ex. fichier créé depuis la palette). Conséquence directe : l'éditeur ne recouvre plus le panneau de l'assistant IA, qui peut donc mettre le document à jour sous les yeux de l'utilisateur — app_context.editing annonce au modèle le document en cours d'édition (et sa surface), et toute écriture d'outil (edit_file, append_to_file, create_file, restore_backup) recharge le document affiché depuis le disque (obsigate:file-written) : le tampon de l'éditeur est rafraîchi au lieu d'écraser la modification de l'assistant par son auto-sauvegarde, et l'iframe Forge reçoit parent-reload. Aucun identifiant DOM n'a changé (autosave, collab Yjs, barre IA, aperçu Mermaid, Forge intacts). Fiche : docs/features/editeur-inline.md. Tests : tests/frontend/editor-inline.test.mjs (19), tests/test_bookslm.py::TestGeneralPrompt (3 nouveaux).

Corrigé

  • BUG-047 — La version affichée ne suivait pas les livraisons : la version provenait du dernier tag git, or aucun tag n'était créé lors des livraisons → l'application restait bloquée sur 2.2.1 alors que 66 commits avaient été livrés (et les numéros codés en dur divergeaient : package.json 1.0.0, desktop Tauri 2.0.0, Dockerfile 2.2.1, README 1.7.0). Le fichier VERSION (racine du dépôt) devient la source unique de vérité (MAJEUR.MINEUR.CORRECTIF), incrémenté automatiquement à chaque commit par le hook versionné .githooks/prepare-commit-msg (!:/BREAKING CHANGE → MAJEUR, feat → MINEUR, sinon CORRECTIF) ; .githooks/post-commit crée le tag vX.Y.Z et push.followTags le publie à chaque push. Tous les dérivés sont resynchronisés dans le même commit : package.json, desktop Tauri (tauri.conf.json, Cargo.toml, Cargo.lock), docs/ROADMAP.md, README.md/README.fr.md et rotation de CHANGELOG.md ([Unreleased] → [X.Y.Z] — date). Le backend (backend/version.py), l'image Docker (COPY VERSION) et le desktop Tauri lisent ce même fichier : plus aucun numéro codé en dur dans le pipeline. Outils : scripts/bump_version.py (+ scripts/bump_version.sh), scripts/install-hooks.sh. Tests : tests/test_version.py, dont un garde-fou de cohérence globale (VERSION ↔ package.json ↔ desktop ↔ CHANGELOG ↔ ROADMAP ↔ README). Vérifié : curl /api/health sur l'instance de test → 2.3.0.

  • BUG-045 — Double barre de défilement dans l'éditeur « Editer » : sur les documents longs, #editor-body (qui gardait overflow:auto) et le scroller CodeMirror débordaient simultanément — la barre d'outils IA empilée sous un .cm-editor en height:100% faisait déborder le corps, et un ancien override global .cm-scroller forçait une seconde zone défilante. La surface d'édition est passée en flex column : le corps ne défile plus (overflow: hidden), l'éditeur remplit l'espace restant et seul le scroller CodeMirror défile. Repli textarea (fallback) cohérent ; la règle mobile BUG-020 est préservée.

  • BUG-046 — « Échec de l'action : [object Object] » (assistant IA, ajout de texte au document courant) : trois causes corrigées ensemble. (1) La carte de confirmation « Appliquer » reprenait un payload dont le message était absent après une première application échouée (continuation payload:null) → réponse 422 du backend. (2) Cette réponse 422 porte un detail tableau d'objets (format validation FastAPI) que new Error(detail) réduisait en [object Object] dans le toast — nouvelle méthode _responseError() qui aplatit tableaux et objets en message lisible. (3) Le prompt des modes documents/dossier ne nommait pas le vault : le modèle inventait parfois "vault":"test" et l'outil d'écriture échouait silencieusement — le prompt indique désormais le vault exact et enjoint les outils edit_file / append_to_file / create_file de l'utiliser. Cache service worker SW_VERSION v20.

  • #91 — Bulle utilisateur trop étroite & ancre d'envoi retardée : le plafond de largeur était appliqué deux fois (wrapper 80 % × bulle 85 % ≈ 68 % du fil), d'où des lignes très courtes ; le plafond est désormais porté par la seule bulle (90 %), le wrapper s'étirant → ~81 % de la largeur du fil (≈48 caractères/ligne au lieu de ~28). Par ailleurs, le rendu du message assistant « placeholder » écrasait la position de défilement juste après l'envoi (l'ancre était annulée : la question restait en bas jusqu'au premier token). _isLoading est maintenant posé avant ce rendu et l'ancrage d'envoi est instantané — la question passe en haut du fil dès la soumission et y reste pendant tout le streaming.

  • #91 — Question complètement en haut : la marge scroll-margin-top (14 px) laissait visible la fin de la réponse précédente au-dessus du post, et une réponse courte ne remplissait pas la fenêtre — le navigateur bloquait alors le défilement et la question restait à mi-hauteur. La marge est supprimée et le padding-bottom du fil est augmenté de la place manquante tant que l'ancre est active (retiré au dé-épinglage, aucun remplissage si le fil tient dans la fenêtre). Le rendu final post-streaming conserve aussi l'ancre, et la libération se fait sur molette, toucher ou clic dans le fil. Enfin, overflow-anchor: none sur le fil neutralise le ré-ancrage automatique de Chrome (le fil est reconstruit à chaque token, ce qui décalait la vue de ~33 px) et une passe de correction recale le défilement d'après la géométrie mesurée.

  • #91 — Tag fournisseur/modèle incomplet : le backend renvoyait le modèle demandé par le client (req.model or ""), donc une valeur vide dès que le client s'en remet au défaut du fournisseur — le tag se réduisait à « openrouter ». _effective_model() résout désormais le modèle réellement utilisé dans les deux flux SSE (/chat et /agent) : le tag affiche « openrouter · openai/gpt-4o-mini ». Couvert par tests/test_bookslm.py::TestEffectiveModel et le test SSE de l'endpoint agent.

  • BUG-041 — Assistant IA bloqué sur un répertoire vide : l'assistant ne renvoie plus ⚠ Error: Aucun fichier markdown trouvé dans ce dossier (HTTP 404). Le contexte vide dégrade vers le prompt Général augmenté d'un bloc « Dossier vide », et la requête aboutit sans contexte documentaire.

  • BUG-042 — Liens de fichiers de l'assistant non fiables : les liens des réponses suivent désormais une règle déterministe — un simple nom de fichier copie le nom dans le presse-papiers, un chemin de dossier est révélé dans l'arborescence, un chemin de fichier l'ouvre. Le chemin est résolu contre l'index du vault (exact → suffixe → basename unique) avant d'agir ; les chemins non résolus copient le nom au lieu d'afficher File not found. Les noms/chemins contenant des espaces sont pris en charge (Mon dossier/Ma note.md) : liens markdown (y compris cibles <…> et %20), code inline, et mentions brutes liées uniquement si le chemin existe dans l'index du vault. Les caractères accentués sont reconnus (classes de caractères Unicode \p{L}\p{N}\p{M}, comparaison normalisée NFC : une mention décomposée e+accent correspond à une entrée d'index précomposée) et un chemin préfixé par le nom du vault (TestVault/Recettes/Pizza Maison.md) est résolu puis ouvert dans ce vault, le préfixe étant retiré.

  • BUG-043 — Liste des fournisseurs de l'assistant non synchronisée avec la configuration : ajouter ou supprimer une clé API dans la configuration du projet met désormais à jour immédiatement le menu Fournisseur de la barre latérale de l'assistant, sans recharger la page. Le picker ne lisait /api/ai/status qu'une fois, à sa construction, et le panneau de l'assistant est un singleton monté pour toute la session : la liste restait figée. Le nouveau refreshAIPickers() (frontend/js/ai.js) reconstruit chaque picker monté dans son emplacement .ai-picker-slot et est appelé après l'enregistrement (saveAIKeys) et la suppression (deleteAIKey) d'une clé (frontend/js/config.js). Un emplacement est conservé même sans fournisseur configuré, donc le premier fournisseur ajouté s'y monte aussi. Une sélection dont le fournisseur n'est plus configuré est purgée de obsigate_ai_picker (retour au défaut au lieu d'un modèle fantôme).

  • BUG-044 — Capacités des modèles erronées (aucun modèle Mistral vision) : la bulle ⓘ et le sélecteur de modèle par défaut n'affichaient aucun modèle Mistral « Vision capable », alors que GET https://api.mistral.ai/v1/models en déclare 28 (mistral-medium, mistral-small, ministral-*, magistral-*, mistral-ocr-*, mistral-vibe-cli-*). Cause : la table de capacités était entièrement statique et aucun de ses motifs ne correspondait aux familles Mistral actuelles (seul pixtral, retiré de l'API, les matchait). ObsiGate lit désormais les capacités déclarées par le fournisseur (nouveau backend/provider_capabilities.py, snapshot mis en cache par GET /api/config/ai-models) : Mistral (capabilities.completion_chat / vision / audio_transcription / audio_speech) et OpenRouter (architecture.input_modalities / output_modalities) sont pris en charge ; la table statique ne sert plus qu'à combler les drapeaux non déclarés et de repli hors ligne. La table est également corrigée (familles vision Mistral, mistral-ocr = vision sans chat) et le défaut du fournisseur Mistral ne prétend plus qu'un modèle non reconnu sait produire des embeddings (BUG-044bis : mistral-large-latest / codestral-latest étaient annoncés « Embeddings »). Conséquence : la porte vision de l'assistant (frontend/js/bookslm.js, backend/bookslm_routes.py) accepte enfin les images avec un modèle Mistral vision.

Sécurité

  • #84 Consolidation & sécurité — phase 1 (BUG-021 → BUG-034) — traitement des vulnérabilités de la revue statique du 2026-09-13 :
    • BUG-021/022 — XSS stocké : nouveau sanitizer serveur en liste blanche (backend/services/sanitizer.py, stdlib) appliqué au rendu markdown ; échappement systématique du title, du frontmatter et du JSON de la page publique /s/{token} (</script> neutralisé). Tests : tests/test_security_hardening.py (9).
    • BUG-023 — brute-force MFA : rate-limit IP + compte et verrouillage de compte sur mfa/totp/verify, mfa/recovery et mfa/webauthn/verify (_enforce_mfa_rate_limit).
    • BUG-024 — traversal inter-vaults : resolve_safe_path compare désormais les chemins par segment (Path.relative_to + repli casse-insensible), plus par préfixe de chaîne : vault ne peut plus lire vault-evil.
    • BUG-025 — ReDoS : validation des regex utilisateur (longueur max, rejet des quantificateurs imbriqués/backrefs), contenu tronqué et nombre de matchs plafonné (backend/services/regex_safety.py), appliqué à la recherche avancée et au find/replace.
    • BUG-026 — SSRF webhooks : validation d'URL (HTTPS par défaut, IP privées/boucle interdites, résolution DNS vérifiée au dispatch, redirections non suivies) et externalisation du secret dans data/webhook_secrets.json (0600) ou variable OBSIGATE_WEBHOOK_SECRET_<ID> (plus de secret en clair dans webhooks.json).
    • BUG-027 — sessions : rotation du refresh token à chaque usage, révocation du JTI de l'access token au logout, et vérification de la révocation dans le middleware.
    • BUG-028 — politique de mot de passe : validation centralisée (8–128 caractères) à la création, à la modification admin et au changement ; password_changed_at invalide tous les jetons émis avant un changement de mot de passe.
    • BUG-029 — race users.json : verrou threading.RLock autour des cycles lecture-modification-écriture.
    • BUG-030 — audits : l'adresse IP réelle du client (X-Forwarded-For si OBSIGATE_TRUST_PROXY=true) est injectée dans current_user et consignée dans les audits.
    • BUG-031 — rate-limiter : budget par compte en plus du budget par IP (rotation d'IP neutralisée) ; limite mono-process documentée.
    • BUG-032 — indexation : _scan_vault utilise os.walk(followlinks=False) et refuse tout symlink sortant de la racine du vault.
    • BUG-033 — recherche O(N) : la recherche simple et l'outil IA search_fulltext utilisent l'inverted index (repli sur le scan pendant la construction).
    • BUG-034 — CSP & jetons : le token d'accès n'est plus persisté dans sessionStorage (mémoire + cookie HttpOnly) ; directives CSP durcies (object-src, base-uri, form-action, frame-ancestors). Reste : migration CSP par nonce (exige la conversion des gestionnaires d'événements inline).

Ajouté

  • #83 Barre d'outils d'édition mobile — ruban style Obsidian Android — la barre de mise en forme flottante est remplacée par un ruban horizontal défilable ancré juste au-dessus du clavier virtuel : fond anthracite aux coins arrondis, insertion/enrobage au curseur ou sur la sélection. Commandes : annuler/refaire, [[ ]] (lien interne), fichiers/modèles, tag #, pièce jointe, H1–H6, gras, italique, barré (~~), surligné (==), code en ligne/bloc, citation, lien externe, listes à puces/numérotées, case à cocher, indenter/désindenter, coller, A−/A+. Une icône ⚙ ouvre un panneau de personnalisation (ajouter / retirer / réordonner, persistance localStorage). i18n FR/EN. Helpers purs exportés (formatChange, normalizeRibbonCommands, moveRibbonCommand). Tests JSDOM tests/frontend/mobile-editor.test.mjs (+13) et E2E mobile.

  • #82 Assistant IA — menu @ instantané — nouvel endpoint GET /api/vault/{vault}/paths (liste plate et plafonnée de l'index des chemins). Le frontend la récupère une fois par vault (préchargée à l'ouverture du panneau) puis filtre côté client pendant la frappe : l'affichage des fichiers/répertoires est immédiat, au lieu d'une requête réseau par caractère. Tests : tests/test_api_main.py (+2), tests/frontend/ai.test.mjs (+1).

  • #82 Assistant IA — Section « Fournisseur & modèle » compacte — la barre de sélection du modèle devient discrète : la liste complète des types d'endpoints (Chat, Embeddings, Vision…) est remplacée par un bouton d'information ⓘ qui affiche les capacités dans une bulle au survol, au clic ou par appui long (mobile). Le choix du modèle passe par une liste déroulante avec recherche (filtrage insensible aux accents) au lieu d'un <select> natif, tout en conservant un select masqué pour les commandes admin (/model). Le picker partagé (frontend/js/ai.js) est utilisé par l'assistant et la barre d'édition IA. Tests JSDOM (tests/frontend/ai.test.mjs +1).

  • #81 Assistant IA — Commandes @//, skills, images & capacités des modèles — l'assistant devient plus flexible et multimodal. Panneau redimensionnable à la souris (largeur 320–1000 px persistée). Commande @ pour attacher des contextes ad-hoc (fichiers, répertoires) au contexte courant. Commande / pour lancer des skills réutilisables (/research, /resume, /actions, /reformuler, /correction, /brainstorm, /plan, /ask, /meeting-note, /livrable) ou des commandes admin (/help, /providers, /provider, /model, /keys) ; /create-new-skill permet de créer des skills personnalisés persistés par utilisateur (data/skills.json). Analyse d'images : coller une image dans la zone de saisie ou référencer une image d'un répertoire (@image.png), avec garde-fou de compatibilité vision. Capacités des modèles (Chat, Embeddings, Rerank, Images, Video, Audio Speech, Audio Transcriptions, Vision) affichées à la sélection dans l'assistant et le panneau de configuration. Backend : table curée backend/model_capabilities.py, endpoints GET /api/ai/model-capabilities et GET /api/ai/skills, support multimodal dans backend/ai_chat.py et backend/bookslm_routes.py. Tests : tests/test_model_capabilities.py (16), tests/test_skills.py (16), tests/test_ai_vision.py (12)

  • #77 Desktop — Signature des mises à jour Tauri — la paire de clés minisign de l'updater est générée et sa clé publique est embarquée dans desktop/tauri.conf.json (plugins.updater.pubkey, remplace le placeholder) ; bundle.createUpdaterArtifacts: true produit les fichiers .sig par artefact. Le workflow .gitea/workflows/desktop-build.yml expose les secrets TAURI_SIGNING_PRIVATE_KEY / TAURI_SIGNING_PRIVATE_KEY_PASSWORD aux builds Windows et Linux, avec repli automatique en build non signé si le secret est absent (CI toujours verte). Procédure complète : docs/DEVELOPMENT_AND_RELEASES.md.

  • #77 Desktop — Manifeste de mise à jour latest.json — nouveau scripts/updater_manifest.py qui construit le document consommé par l'updater Tauri (version, notes, pub_date, platforms Windows/Linux avec signature .sig et URLs des assets). Il est généré automatiquement par scripts/publish_release.py (écrit desktop/latest.json, l'ajoute aux assets de la release, upload des .sig) et peut être lancé seul. L'endpoint de l'updater (desktop/tauri.conf.json) pointe désormais sur le manifeste versionné .../raw/branch/main/desktop/latest.json. Les builds locaux (build-windows.bat, build-linux.sh) détectent automatiquement desktop/obsigate-updater.key pour signer, ou désactivent les artefacts de mise à jour s'il est absent. Tests : tests/test_updater_manifest.py (8).

  • #77 Desktop — Protocole de tests E2E manuels — nouveau docs/DESKTOP_E2E_CHECKLIST.md : prérequis, étapes et résultat attendu pour les 6 scénarios OS non automatisables (installation, tray, notifications natives, association .md, auto-update, désinstallation), avec emplacements des logs/config et procédure de signature de l'updater Tauri.

  • #70 Recherche sémantique — Embeddings vectoriels — la recherche comprend désormais le sens de la requête en plus des mots-clés. Embeddings : chaque document est découpé en chunks de 512 mots (recouvrement 64) puis vectorisé (384 dim) via all-MiniLM-L6-v2 (sentence-transformers), un endpoint /embeddings compatible OpenAI, ou un provider de repli sans dépendance (hachage déterministe). Stockage vectoriel : numpy/faiss (IndexFlatIP) si disponibles, sinon cosinus pur Python. Recherche hybride : fusion du classement TF-IDF et du classement sémantique par RRF (Reciprocal Rank Fusion), avec semantic_score par résultat. Indexation incrémentale branchée sur le watcher (un fichier modifié régénère son embedding). UI : toggle « Recherche sémantique » (~, raccourci Alt+S) dans la barre de résultats + affichage du score de similarité, clés i18n FR/EN. Nouveau module backend/semantic_search.py, paramètre semantic sur /api/search/advanced, dépendances optionnelles dans backend/requirements-semantic.txt. Tests : tests/test_semantic_search.py (27) + tests/frontend/semantic-search.test.mjs (4). Détail : docs/features/semantic-search.md.

  • #69 Éditeur mobile natif — Interface tactile optimisée — refonte de l'expérience d'édition sur téléphone/tablette. Barre d'outils flottante dans l'éditeur (gras, italique, code, liste à puces, lien) opérant directement sur la sélection CodeMirror (ou le textarea de secours), avec toggle (re-appui pour dé-formater) ; bouton « Coller » persistant qui contourne la restriction presse-papiers d'iOS (repli sur un message d'aide si l'accès est refusé). Adaptation CodeMirror : zoom par pincement à deux doigts, boutons A−/A+ et hauteur ajustable via une poignée (valeurs persistées en localStorage). Côté lecture : raccourcis swipe (gauche → liens entrants, droite → table des matières) et mode lecture plein écran (masquage de l'en-tête, des barres latérales et de la barre mobile, API Fullscreen, swipe horizontal pour passer au fichier suivant/précédent du même dossier). Nouveau module frontend/js/mobile-editor.js, styles frontend/style.css, clés i18n FR/EN. Tests : tests/frontend/mobile-editor.test.mjs (22 tests). Détail : docs/features/mobile-editor.md.

  • #62 Collaboration temps réel — Édition simultanée — plusieurs utilisateurs peuvent éditer le même document markdown en même temps, façon Google Docs. WebSocket : nouvel endpoint /ws/collab/{vault}/{path} (une room par fichier) qui relaie les changements instantanément ; authentification manuelle (cookie access_token ou paramètre token) + check_vault_access et resolve_safe_path par connexion. Yjs/CRDT côté client : Y.Doc/Y.Text fusionnent les modifications concurrentes sans conflit ni perte. Awareness : curseurs distants colorés et sélections dans CodeMirror, indicateur de présence (avatars + statut) dans l'en-tête de l'éditeur. Persistance serveur : le dernier texte reçu est écrit sur disque avec un debounce de 2 s (backend/collab.py). Reconnexion automatique avec backoff exponentiel et merge de l'état au retour. Nouveau module frontend frontend/js/collab.js (provider + liaison Y.Text↔CodeMirror + curseurs distants) et backend/collab.py (CollabManager). Tests : tests/test_collab.py (17 tests, dont 5 clients simultanés) et tests/frontend/collab.test.mjs. Détail : docs/features/collaboration.md.

  • #79 Phase F — Durcissement & documentation (rate limiting, redaction, OpenAPI/MCP, E2E) — clôture de la feature #79. Rate limiting par jeton et par outil (backend/tools/ratelimit.py, fenêtre glissante ; identité = JTI du jeton sinon id/username ; OBSIGATE_TOOL_RATE_LIMIT, OBSIGATE_TOOL_RATE_LIMIT_PER_TOOL, OBSIGATE_TOOL_RATE_WINDOW ; erreur rate_limited). Quotas BOOKSLM_MAX_* : BOOKSLM_MAX_TOOL_CALLS plafonne les appels d'outils par run d'agent (stopped="quota_exceeded") et BOOKSLM_MAX_TOOL_READ_BYTES plafonne read_file. Redaction systématique de tout résultat d'outil avant retour au LLM (backend/tools/redaction.py, récursif : diffs, extraits de recherche, lectures). OpenAPI : tag MCP + injection du path /mcp (Streamable HTTP) dans le schéma ; nouveau docs/MCP_GUIDE.md (config Claude Desktop/Cursor, tools/resources/prompts, sécurité, variables, dépannage). Tests E2E : tests/test_ai_e2e.py (agent read→confirm→write, quota, rate limit, redaction, flux MCP read→propose→apply→resources/read).

  • #79 Phase E — Serveur MCP (Streamable HTTP) — nouvel endpoint /mcp exposant ObsiGate à des clients MCP externes (Claude Desktop, Cursor…) via le SDK Python mcp==1.9.4. Les deux fronts (assistant in-app et MCP) consomment la même couche d'outils. Tools : outils de lecture/recherche directs ; outils d'écriture/destructifs en two-step propose_<tool> / apply_<tool> avec jeton JWT signé, usage unique et TTL (OBSIGATE_MCP_CONFIRMATION_TTL, défaut 300 s) et blacklist de JTI persistée (data/mcp_used_tokens.json, anti-rejeu). Resources : vault://<name> et vault://<name>/<path> (lecture seule, secrets redactés). Prompts : summarize-directory, generate-note, find-related. Auth Authorization: Bearer <JWT> → get_current_user ; permissions et toggle destructif par vault (aiDestructiveTools) appliqués. Nouveaux modules backend/mcp/server.py et backend/mcp/confirmations.py ; dépendances mcp==1.9.4 + sse-starlette==2.1.3 (compatibles FastAPI 0.110 / starlette 0.37). Tests : tests/test_mcp.py (13 tests).

  • #79 Phase D — Catalogue d'outils mutations + confirmations two-step — 11 nouveaux outils IA destructifs ou d'écriture exposés par la couche partagée (backend/tools/service.py) : create_file, create_directory, edit_file, append_to_file, rename_file, rename_directory, move_path, replace_in_files, delete_file, delete_directory, restore_backup. Nouveau service réutilisable backend/services/mutations.py (anti path-traversal, garde lecture seule, backup automatique avant écriture/suppression/restauration) ; les routes /api/file/{vault} (POST/PATCH/DELETE), /api/file/{vault}/save, /api/directory/{vault} (POST/PATCH/DELETE), /api/move/{vault}, /api/file/{vault}/restore et /api/search/replace délèguent désormais à cette couche. backend/services/backups.py expose create_backup (source unique, réutilisée par main._backup_file). Confirmations two-step : tout outil WRITE/DANGEROUS déclenche ToolConfirmationRequired (carte Apply in-app, propose/apply côté MCP en phase E). Toggle par vault aiDestructiveTools (défaut : activés) pour désactiver delete/rename/move/replace. Tests : tests/test_tools_mutations.py (40 tests).

  • #79 Phase C — Catalogue d'outils lecture & recherche — 10 nouveaux outils IA exposés par la couche partagée (backend/tools/service.py) : list_all_files, read_file_raw, get_backlinks, list_backups, diff_backup, get_graph, search_advanced, search_paths, suggest_tags, list_recent. Nouveaux services réutilisables backend/services/backups.py, graph.py, recent.py et extensions de vaults.py (list_all_files) / search.py (advanced_search_vaults, search_paths). Les routes /api/recent, /api/vault/{vault}/files, /api/file/{vault}/backups, /api/file/{vault}/diff, /api/graph/{vault}, /api/tree-search et /api/search/advanced délèguent désormais à ces services (source unique de vérité). Filtrage systématique par permissions vault ; secrets redactés sur read_file/read_file_raw. Tests : tests/test_tools.py (+17 tests).

  • #79 Assistant IA — Outils (function calling) & serveur MCP — Phase 0 (couche d'outils partagée) — nouveau paquet backend/tools/ : context.py (ToolContext), registry.py (décorateur @tool

    • schéma JSON), schemas.py, service.py, audit.py (journalisation JSONL des appels), façade backend/tools/api.py. Services lecture/recherche livrés (list_vaults, list_directory, read_file, search_fulltext, list_tags) ; permissions check_vault_access + resolve_safe_path appliquées par outil. Tests : tests/test_tools.py (30 tests).
  • #79 Phase B (partielle) — function calling in-app — abstraction tool-calling provider-agnostique backend/ai_chat.py (chat_completion, ToolCall, LLMResponse, OpenAI-compat

    • Gemini) ; agent loop backend/agent/loop.py (boucle tool→résultat→tool, limite 10 itérations, truncation) ; endpoint opt-in POST /api/ai/bookslm/agent (events SSE tool/message/confirmation) ; fallback automatique en chat simple si le provider rejette les tools. Tests : tests/test_agent_loop.py, tests/test_ai_chat.py, tests/test_bookslm.py.
  • #79 A2/B4/B5 — services partagés, SSE streaming & confirmations UI — extraction de la logique lecture/recherche dans backend/services/ (errors.py, paths.py, vaults.py, files.py, search.py) : routes REST et outils IA consomment la même source ; ServiceError mappée vers HTTPException (routes) et ToolError (outils). /api/ai/bookslm/chat streame réellement les tokens (stream_completion, OpenAI-compat + Gemini ; GZip ignoré pour les SSE BooksLM). Mode agent côté UI (toggle, événements tool/confirmation, carte Apply + aperçu diff LCS) et reprise confirm/confirm_messages de l'agent loop (confirmation one-shot). Tests : tests/test_services.py, tests/test_ai_chat.py, tests/test_agent_loop.py, tests/test_bookslm.py.

  • #79 G — Sélection fournisseur/modèle par défaut — ai_default_provider + ai_default_models persistés dans data/config.json, rechargement à chaud dans backend/ai.py, sélecteurs « Fournisseur par défaut » / « Modèle par défaut » dans #cfg-ai, i18n FR/EN.

  • #80 Assistant IA — Rendu Markdown, liens fichiers/paths & sessions — réponses de l'assistant rendues en Markdown formaté (titres, listes, tableaux, citations, code, emphase) via un renderer auto-contenu ; les fichiers et chemins mentionnés deviennent des liens cliquables (clic fichier → ouverture dans le viewer, clic répertoire → révélation/surlignage dans l'arborescence, outils open_file/reveal_in_tree côté in-app) ; gestion des sessions par contexte (historique consultable, rechargeable, supprimable) avec migration de l'ancienne conversation unique. Tests : tests/frontend/ai.test.mjs.

Modifié

  • Barre inférieure mobile — couleurs harmonisées — les boutons « settings.search », « Recherche » et « Onglets » adoptent la couleur d'accent (--accent) de l'icône et du libellé du bouton « Commandes » (frontend/style.css), pour un rendu uniforme de la barre du bas.

  • #82 Assistant IA — sélecteurs Fournisseur/Modèle alignés à droite et réordonnés — dans la barre de l'assistant, le groupe de sélection est aligné sur le bord droit et l'ordre devient capacité du modèle (ⓘ) → fournisseur → modèle. La bulle de capacités s'ouvre désormais vers la droite (left: 0) pour ne pas déborder de la barre latérale. Tests JSDOM (tests/frontend/ai.test.mjs).

  • scripts/bump_version.sh — synchronisation de la version desktop — le script met désormais à jour desktop/tauri.conf.json, desktop/Cargo.toml et desktop/Cargo.lock (obsigate-desktop) avant de créer un commit de release chore(release): vX.Y.Z puis le tag. --push pousse la branche et le tag ; --no-files conserve l'ancien comportement (tag seul) et --dry-run prévisualise. Indispensable au bon fonctionnement de l'updater Tauri (dont le manifeste reprend la version de tauri.conf.json).

  • .gitea/workflows/desktop-build.yml — déclenchement manuel uniquement — le workflow Desktop Build ne s'exécute plus sur push (aucun runner self-hosted [windows/linux, desktop] enregistré) mais uniquement via workflow_dispatch. Le build Windows se fait en local (desktop/build-windows.bat) ; le workflow reste disponible pour un futur runner.

  • Assistant IA (frontend/js/bookslm.js) — l'ancien rendu Markdown minimal est remplacé par un renderer bloc/inline complet ; l'en-tête expose un bouton « Historique des sessions » ; les badges de sources ouvrent désormais réellement le fichier (événement obsigate:open-file).

  • Assistant IA (frontend/js/bookslm.js) — bouton « mode agent » dans l'en-tête (persisté en localStorage) : l'envoi bascule vers /api/ai/bookslm/agent, affiche la trace des appels d'outils et les cartes de confirmation (Apply + aperçu diff) ; /chat reste le défaut. i18n FR/EN.

Modifié

  • #82 Assistant IA — barre latérale épurée & suivi visuel des requêtes — la section « Fournisseur & modèle » perd ses intitulés (titre de section et libellé « Fournisseur : ») pour rester discrète ; la description du contexte Général n'est plus affichée (bandeau de statut masqué, description accessible au survol de l'en-tête) ; le placeholder « Posez une question sur ces documents… » est retiré ; l'indice clavier « Entrée pour envoyer · Ctrl+Entrée… » est retiré ; le bouton « Envoyer » devient un emoji compact (✈️). Les sélecteurs Fournisseur/Modèle adoptent la taille des autres contrôles du site (police 0,8 rem, hauteur 34 px, rayon 6 px) au lieu d'être plus fins. Un indicateur d'activité apparaît pendant le traitement et détaille le workflow : envoi, réception de la réponse, appels d'outils (Outil : X…), attente de confirmation, succès ou échec. Le menu contextuel de la racine d'une vault expose aussi « 🧠 BooksLM », comme les répertoires. i18n FR/EN (ai.activity_*, ai.composer_label). Tests JSDOM (tests/frontend/ai.test.mjs +2).

Corrigé

  • BUG-017 — Accueil mobile : tuiles « Favoris » / « Récents » mal dimensionnées — les cartes de tableau de bord débordaient horizontalement à cause d'un titre/chemin nowrap : ajout de min-width: 0 et overflow: hidden sur .dashboard-card (et ses en-têtes/pieds) pour que la grille mobile (1 colonne) respecte la largeur du viewport. Les tuiles « Partagés » étaient déjà conformes.

  • BUG-018 — Éditeur mobile : boutons « Annuler » / « Sauvegarder » inaccessibles — l'en-tête cumulait marque, titre, nom de fichier et actions, poussant les boutons hors écran. Sur mobile, les éléments secondaires (marque, séparateur, espaceur, présence) sont masqués, les champs titre/nom de fichier deviennent compressibles (flex, min-width: 0) et le libellé « Saved » est masqué (le point d'état reste). La barre d'outils IA reçoit des cibles tactiles plus grandes.

  • BUG-019 — Éditeur mobile : barre d'outils masquée par le clavier — le ruban est désormais ancré via window.visualViewport (--kb-offset) pour rester juste au-dessus du clavier, quel que soit le défilement, et le scroller CodeMirror reçoit un padding-bottom pour ne pas cacher le curseur.

  • BUG-020 — Éditeur mobile : double barre de défilement — .editor-body passe en overflow: hidden + colonne flex ; seul le scroller CodeMirror défile.

  • BUG-016 — Mobile : le bouton « mode lecture » recouvre le bouton d'envoi de l'assistant — le bouton flottant 📖 (#me-reading-btn, z-index: 890) passait au-dessus du panneau assistant plein écran sur mobile (z-index: 100) et masquait le bouton d'envoi ✈️. Il n'apparaît désormais que lorsqu'un fichier est ouvert (state.currentPath) et que le panneau assistant est fermé ; sa visibilité est resynchronisée à chaque mutation de #content-area et sur les événements bookslm:opened/bookslm:closed. Tests JSDOM (tests/frontend/mobile-editor.test.mjs +2) et E2E mis à jour.

  • BUG-015 — Assistant IA : sélection des menus / et @ invisible au clavier — l'élément actif utilisait background: var(--surface2), or --surface2 est identique à --bg-primary (fond du menu) dans le thème sombre par défaut : la ligne sélectionnée par les flèches ↑/↓ ne se distinguait pas. L'état actif (et le survol) utilise désormais --bg-hover avec une barre d'accent à gauche (box-shadow: inset 3px 0 0 var(--accent)), pour les menus de commandes/mentions et la liste de modèles. Test JSDOM mis à jour.

  • BUG-014 — Assistant IA : navigation clavier ↑/↓ des menus inopérante — la navigation était liée au keydown du champ de saisie : dès que le focus quittait la zone de texte (clic sur le menu, bouton Envoyer…), les flèches ne faisaient plus défiler les items. La gestion est déplacée au niveau du panneau en phase de capture, ce qui fonctionne quel que soit l'élément focalisé dans la barre latérale. Tests JSDOM (tests/frontend/ai.test.mjs +1).

  • BUG-013 — Assistant IA : « Aucun vault actif pour ouvrir ce lien » — les liens fichiers/répertoires des réponses utilisaient _resolveVault() seul, nul en mode Général sans document ouvert. _openFileLink() et _revealPath() utilisent désormais _activeVault() qui retombe sur le vault de contexte, le vault sélectionné dans la barre latérale, puis le premier vault disponible. Tests JSDOM (tests/frontend/ai.test.mjs +1).

  • Assistant IA — caches obsolètes — SW_VERSION porté à v6 et migration de purge élargie à tous les caches obsigate-* (clé obsigate-sw-migration → v3), afin que les clients bloqués sur une ancienne version récupèrent bien le menu @ et les correctifs.

  • BUG-012 — Assistant IA : la commande @ n'affichait aucun menu en mode Général — sans vault résolu, la détection @ ne listait rien (menu masqué) et le backend ignorait ensuite le contexte. Le menu est désormais toujours rendu et _mentionVault() retombe sur le vault de contexte de la barre latérale, puis sur le premier vault disponible, avant de basculer sur vault=all pour les recherches. Tests JSDOM (tests/frontend/ai.test.mjs +1).

  • BUG-011 — Assistant IA : noms de modèles illisibles dans la liste déroulante — la liste déroulante des modèles s'ouvrait alignée à gauche (left: 0) et pouvait dépasser le bord droit de la barre latérale (ancrée à droite), rendant les noms tronqués/illisibles ; elle est désormais alignée à droite du déclencheur, large de 340 px (bornée à 100vw - 24px), et les noms reviennent à la ligne (overflow-wrap: anywhere, police 0,78 rem) avec l'info-bulle title complète. Tests JSDOM (tests/frontend/ai.test.mjs).

  • BUG-010 — Assistant IA : la commande @ n'ajoutait pas le contexte en mode Général — les fichiers/répertoires choisis via @ étaient bien ajoutés sous forme de puces, mais le vault n'était pas transmis : en mode Général (_vault nul), la requête partait sans vault et le backend ignorait le contexte ad-hoc. La sélection capture maintenant le vault renvoyé par /api/tree-search, _contextVault() le propage aux requêtes /context et /chat, et les recherches suivantes restent dans ce vault. Tests JSDOM (tests/frontend/ai.test.mjs).

  • BUG-009 — Assistant IA : liste de modèles corrompue et clés i18n brutes — deux causes : (1) les fichiers de locale n'étant pas content-hashed, un fr.json en cache HTTP affichait les clés brutes (ai.model_search, mobile_editor.*, …) ; le chargement i18n utilise désormais cache: 'no-store' et SW_VERSION est incrémenté pour purger les anciens caches du service worker. (2) la liste de modèles (OpenRouter = plusieurs centaines d'entrées) pouvait se retrouver non stylée si la feuille de style était en cache ; le picker applique maintenant les styles critiques en ligne (popover, liste en colonne, options display:block), plafonne le rendu à 200 entrées avec un indicateur « … N autres — affinez la recherche », et la recherche filtre le reste. Tests JSDOM (tests/frontend/ai.test.mjs +1).

  • BUG-007 — Assistant IA : menus / et @ (navigation ↑/↓ et filtrage accentué) — la détection des commandes/mentions et le filtrage des skills utilisaient des motifs ASCII ([a-z0-9-], \w) : dès qu'un caractère accentué était saisi (/résumé, @café), le menu se fermait. Les motifs sont désormais Unicode (\p{L}) et la recherche est insensible aux accents (normalisation NFD). La navigation clavier est fiabilisée par un jeton de séquence qui ignore les rendus asynchrones obsolètes (frappe rapide) et fait défiler l'élément actif dans la vue (scrollIntoView). Tests JSDOM (tests/frontend/ai.test.mjs +3).

  • BUG-008 — Assistant IA : commande @ et contexte ad-hoc en mode Général — deux causes : (1) le backend ignorait les fichiers/répertoires ad-hoc en mode Général car _resolve_system_prompt ne résolvait pas de vault (backend/bookslm_routes.py) ; le vault optionnel est maintenant résolu dès que du contexte @ est présent. (2) sans vault courant, la recherche de mention échouait ; elle bascule désormais sur vault=all. Tests backend (tests/test_bookslm.py +2) et frontend.

  • #77 Desktop — bannière de premier lancement réaffichée à chaque démarrage — l'état du wizard « Choisissez votre vault » n'était mémorisé que dans le localStorage de la webview et le paramètre hasVaultPath n'était jamais renseigné. Ajout d'un booléen persistant wizard_done dans la config desktop (desktop/src/main.rs, commandes get_wizard_state / complete_wizard, rétro-compatible via #[serde(default)]) : la bannière ne réapparaît plus après un choix de dossier ou un clic « Plus tard », même si le stockage webview est vidé. Tests Rust (desktop/src/main.rs) et JSDOM (tests/frontend/desktop.test.mjs) ajoutés.

  • Build Docker — « No space left » lors de apt-get install — le stage builder installait inutilement libpango-1.0-0, libpangocairo-1.0-0 et shared-mime-info (libs runtime de WeasyPrint, sans usage à la compilation), dupliquant les téléchargements apt. Ces paquets sont retirés du builder et apt-get clean est ajouté dans les deux stages pour purger les .deb conservés par défaut sous Debian trixie, réduisant l'empreinte disque et le temps de build. En complément, purge du cache de build (docker builder prune) ayant libéré l'espace nécessaire.

  • BUG-005 — Chargement mobile incomplet derrière Cloudflare (og.dracodev.net) — le service worker utilisait une stratégie cache-first avec un nom de cache fixe (obsigate-v1) et un précache de chemins erronés (/frontend/js/…) : le Cache Storage du SW (distinct du cache navigateur/Cloudflare) servait un ancien build indéfiniment, même après purge. Le middleware renvoyait en plus Cache-Control: public, max-age=31536000, immutable sur des assets non fingerprintés. Correctifs : frontend/sw.js réécrit (network-first pour HTML/JS/CSS, caches versionnés SW_VERSION, précache corrigé /static/js/…, skipWaiting/clients.claim) ; migration ponctuelle localStorage (suppression des anciens caches) en remplacement du kill-switch de session ; en-têtes no-cache sur /static, index.html, manifest.json et pages HTML ; rechargement unique sur controllerchange. Tests : tests/frontend/sw.test.mjs + TestStaticCaching. Guide Cloudflare : docs/PWA_GUIDE.md.

  • Typage backend (mypy) — 33 erreurs corrigées (backend/main.py, indexer.py, auth/router.py, pdf_reader.py, export.py, bookslm_routes.py) : annotations de types, gardes None sur get_user(), PdfReader: Any et import PROVIDERS manquant dans main.py (bug latent : le modèle par défaut n'était jamais prépendé à la liste des modèles live). L'étape mypy du CI devient bloquante (elle était en mode advisory).

  • README — lien « Contributing » corrigé vers docs/CONTRIBUTING.md (était cassé vers la racine) ; arbre du projet mis à jour.

  • #79 façade backend/tools/api.py — gestion des namespace packages et du __init__ ignoré lors du chargement des modules d'outils.

Documentation

  • Guide d'architecture IA — docs/AI_ARCHITECTURE_GUIDE.md (architecture, catalogue d'outils, sécurité, phases).

  • Refonte de la documentation — la Roadmap ne contient plus que le travail à venir + un index compact ; les fonctionnalités livrées sont archivées dans docs/archive/COMPLETED_v1-v2.md et documentées par feature dans docs/features/.

  • Méthode de livraison unifiée — nouveau docs/DELIVERY_WORKFLOW.md (Definition of Done : tests, documentation, commit, push, CI) et AGENTS.md (instructions obligatoires lues à chaque session). Référencés depuis la Roadmap, CONTRIBUTING et ISSUES_TODOLIST.


[2.2.1] — 2026-09-11

Ajouté

  • Mobile — assistant AI central — bouton assistant dans la barre du bas (commit 50b823e).

Corrigé

  • Version affichée dans l'UI — affichait 2.1.0 au lieu de 2.2.0 (commit 84bda90).
  • Mobile — boîte d'édition de l'assistant relevée au-dessus de la barre du bas (commit f307ecb).

[2.2.0] — 2026-09-11

Ajouté

  • Assistant AI contextuel (sidebar) — l'assistant s'adapte désormais au contexte d'ouverture :

    • Répertoire (directory) : ouvert depuis le menu contextuel d'un dossier — le contexte est le dossier et son contenu (mode historique BooksLM).
    • Documents (documents) : ouvert via le bouton flottant quand un ou plusieurs documents sont ouverts — le contexte est la liste des fichiers ouverts (tous onglets/panneaux).
    • Général (general) : ouvert via le bouton flottant quand aucun document n'est ouvert — l'assistant répond sur l'application et peut proposer des actions (créer un fichier/dossier) via un bloc obsigate-action rendu en carte avec bouton « Appliquer » (aucune exécution automatique).
    • Le contexte est affiché dans le header de la sidebar (icône, titre, sous-titre) et les suggestions de questions s'adaptent au mode.
    • Backend : /api/ai/bookslm/context et /chat acceptent mode (directory/documents/ general) et context_files ; collect_files_context(), empty_context() et build_general_system_prompt() dans backend/bookslm.py ; le mode documents dégrade proprement vers le mode général si aucun fichier n'est lisible.
    • Tests : tests/test_bookslm.py (+13 tests modes/documents/général) et tests/frontend/ai.test.mjs (+8 tests détection de contexte, clavier, actions, header).
  • #72 API publique documentée — OpenAPI 3.1 enrichie — documentation interactive complète de l'API REST, générée automatiquement et enrichie.

    • Backend backend/openapi_docs.py : métadonnées de tags (18 catégories : System, Auth, Files, PDF, Vaults, Search, Bookmarks, Backups, Export, AI, BooksLM, Sharing, Webhooks, Conflicts, Admin, Plugins, Push, Frontend), assignation automatique des tags par préfixe de route (normalisation des tags des routeurs), schémas de sécurité (bearerAuth, cookieAuth), exemples de requête/réponse sur les endpoints clés, réponses d'erreur documentées (401/403/404/422/500), serveur et externalDocs.
    • backend/schemas.py : nouveaux response_model Pydantic pour les endpoints qui n'en avaient pas (recent, bookmarks, saved searches, backups, diff/restore/backlinks, pdf/info, replace, vaults, attachments, settings, config, AI keys/models, diagnostics, dashboard, webhooks, shares, conflicts, BooksLM context, AI status).
    • backend/main.py : FastAPI(...) enrichi (description Markdown, contact, licence, tags) et override app.openapi pour post-traiter le schéma ; page de documentation /api (HTML autonome listant les endpoints groupés par catégorie, liens vers /docs, /redoc, /openapi.json).
    • Frontend : entrée « API » dans le menu d'options (i18n FR/EN) ouvrant Swagger UI.
    • Tests : tests/test_openapi.py (58 tests — version 3.1, tags, sécurité, erreurs, exemples, schémas de réponse, pages /api, /docs, /redoc, /openapi.json).
  • #61 Plugins système au complet — système de plugins permettant d'étendre ObsiGate (renderers personnalisés, filtres de recherche, actions d'éditeur), sandboxé pour la sécurité.

    • Backend backend/plugins.py : validation du manifest plugin.json (name regex lowercase, semver, hooks/permissions autorisés), stockage par vault <vault>/.obsigate-plugins/, lifecycle complet (install/uninstall/enable/disable via marker .disabled), validation ZIP à l'upload (anti path-traversal, max 100 fichiers, 500KB/fichier, 5MB upload), 9 endpoints /api/plugins/* (list/get/hooks/code/template + install/uninstall/enable/disable admin-gated), template API, scan au démarrage par vault (get_plugin_registry().scan_vault).
    • Frontend frontend/js/plugins.js : PluginManager (install/uninstall/enable/disable/view-code), sandbox d'exécution via Web Worker (code chargé par blob URL, protocole postMessage structuré, isolation DOM/localStorage/network selon permissions), hooks dispatch (executeHook, onFileRender, onSearchFilter, onEditorAction, onSidebarItem, onFileCreate, onFileDelete, onVaultMount), UI Settings > Plugins.
    • Sécurité : manifest de permissions (read_files, write_files, network_request, … restreint), CSP stricte sans importScripts, limites de taille.
    • Tests : tests/test_plugins.py (44 tests — validation manifest, lifecycle manager, validation ZIP/directory, démarrage scan) + tests/frontend/plugins.test.mjs (21 tests JSDOM — protocole sandbox Worker, isolation DOM, lifecycle mirror).
    • CI : job lint ajoute node plugins.test.mjs aux tests JSDOM.
    • Docs : docs/PLUGINS.md (manifest, hooks, permissions, modèle de sécurité, API, guide).
  • #78 Excalidraw — finitions : décompression lz-string du format plugin Obsidian, création depuis la modale et le menu contextuel, extraction du texte pour la recherche (B5), support .excalidraw.md (commit ac16fc1 + correctifs associés).

  • #67 Notifications web (Push API) : backend/push.py, POST /api/push/subscribe, clés VAPID dans config.json, toggle par vault, payload (fichier/vault/action) et ouverture du fichier au clic (commit ac16fc1).

  • #68 Health check enrichi : GET /api/health/detailed (admin-gated) exposant index, mémoire (RSS/heap), uptime, connexions SSE, backups et espace disque (commit ac16fc1).

  • #77 Desktop — jumplist vaults récents dans le menu Démarrer (commit ac16fc1).

  • #75 Split View — matrice E2E complète (37 tests) : 16 tests split-view.spec.js (raccourcis, drag & drop, persistance, navigation clavier) + 21 tests split-view-matrix.spec.js couvrant 100% des cas d'ouverture/fermeture.

Documentation

  • #78 Excalidraw — doc utilisateur finalisée : .excalidraw / .excalidraw.md ajoutés aux formats supportés (README FR/EN), nouvelle section « Diagrammes Excalidraw » dans le guide intégré (i18n FR/EN) et note de compatibilité avec le plugin Obsidian Excalidraw.
  • Roadmap : #78 marqué terminé (B5 extraction texte, C8 menu contextuel, F3 E2E tests/e2e/excalidraw.spec.js, H1-H3 docs) ; F2 non retenu. Points optionnels #61 (dépôt communautaire de plugins) et #74 (D2/E4/H2/I2) explicitement marqués non retenus.

Corrigé

  • Assistant AI — sidebar qui ne s'ouvrait plus & erreur de contexte :

    • Le bouton flottant passait le chemin d'un fichier comme répertoire de contexte, d'où l'erreur « Aucun fichier markdown trouvé dans ce dossier » à l'ouverture sur un document. Le contexte est maintenant détecté automatiquement (documents ouverts → mode documents, sinon mode général).
    • Un panneau précédemment masqué (obsigate-bookslm-hidden=true persisté en localStorage) restait caché lors d'une ouverture explicite → l'ouverture force désormais l'affichage.
    • Ergonomie du header : le sélecteur fournisseur/modèle était sur la même ligne que les actions et masquait ces dernières ; il est déplacé sur une seconde ligne dédiée (toolbar) pour que « nouvelle conversation / export / plein écran / fermer » restent toujours visibles.
    • Clavier de la boîte de chat : Entrée envoie la requête, Ctrl+Entrée insère un saut de ligne (au lieu de l'inverse), avec un texte d'aide sous le champ.
  • Outil AI de l'UI — fiabilisation :

    • BooksLM (chat IA par répertoire) : les requêtes context/chat n'envoyaient pas le jeton d'authentification (échec 401 quand l'auth est activée) et la réponse SSE était mal parsée (le backend émet {token: ...}, le front lisait data.content) → les réponses n'apparaissaient jamais. Le message utilisateur envoyé était le placeholder assistant vide au lieu du texte saisi. Corrigé : requêtes authentifiées via AuthManager (retry 401), parsing SSE event:/data: avec support token/error, historique construit avant l'ajout du message courant, barre de progression basée sur max_total_chars.
    • AI Editor : le raccourci Ctrl/Cmd+J (complétion inline) était re-lié à chaque ouverture de l'éditeur (N listeners → N requêtes) ; un seul listener global est désormais posé. Les libellés de la barre d'outils AI étaient codés en dur (FR/EN mélangés) → i18n complet. La réécriture personnalisée utilisait window.prompt() → remplacée par une modale accessible (Échap annule, Ctrl/Cmd+Entrée valide).
  • Recherche cassée (régression plugins) : onSearchFilter() était appelée avec 2 arguments au lieu de 3 (frontend/js/search.js), renvoyait undefined, faisait échouer le rendu des résultats et basculait silencieusement sur la recherche hors-ligne (0 résultat). Corrigé → la recherche full-text et l'autocomplétion refonctionnent (20 tests E2E).

  • .excalidraw.md corrompu à l'ouverture : Excalidraw émet onChange au montage, ce qui déclenchait un auto-save 2 s plus tard réécrivant le fichier au format JSON brut et détruisant le format plugin Obsidian (frontend/excalidraw-editor.html + frontend/js/excalidraw-viewer.js). Les changements initiaux sont désormais ignorés et le format frontmatter + compressed-json est préservé à la sauvegarde.

  • Splash / tests E2E : window.__OBSIGATE_BOOTED était posé avant la fin de init() — le splash disparaissait trop tôt et les tests E2E interagissaient avant que les handlers soient liés. Le flag est maintenant posé après résolution de init() (frontend/js/app.js), et goHome()/login() attendent ce signal.

  • Tests E2E Excalidraw : réécriture de tests/e2e/excalidraw.spec.js avec les sélecteurs réels (modale .obsigate-modal + #file-ext-select/#file-name-input, menu contextuel, canvas.first() pour les deux couches canvas).


[2.1.0] — 2026-09-08

Ajouté

  • #74 Support PDF au complet — GET /api/file/{vault}/pdf/info (métadonnées sans contenu), streaming avec HTTP Range / 206 Partial Content (rendu progressif des gros PDF), config OBSIGATE_PDF_MAX_SIZE_MB (50) + OBSIGATE_PDF_EXTRACT_TIMEOUT (30s). 16 tests supplémentaires (26 au total dans test_pdf.py).
  • #64 MFA — WebAuthn (second facteur en plus du TOTP) : enregistrement de clés de sécurité / biométrie (Windows Hello, Touch ID), assertion au login, gestion des clés dans « Sécurité du compte », codes de récupération émis à l'activation. Nouveau module backend/auth/webauthn_mfa.py (lib webauthn==2.6.0, challenges in-memory TTL 180s à usage unique). 10 tests avec authentificateur virtuel (CBOR réel, ECDSA P-256). i18n FR/EN (13 clés). Config : OBSIGATE_WEBAUTHN_RP_ID / _RP_NAME / _ORIGINS.
  • #77 Desktop — port auto-increment : pick_free_port() scanne 17890..17899 si le port est occupé (2 instances côte à côte possibles) + 3 tests Rust (19 au total côté desktop).
  • Docs : sections PDF README FR/EN (Range, /pdf/info), variables env, guide WebAuthn.
  • #75 Split View — matrice E2E complète (37 tests) : 16 tests split-view.spec.js (raccourcis, drag & drop, persistance, navigation clavier) + 21 tests split-view-matrix.spec.js couvrant 100% des cas d'ouverture/fermeture : boutons ⊞→/⊞↓, menu contextuel (diviser bas, fermer, fermer les autres/à droite/tout, fermer le panneau, flyout « Déplacer vers… »), palette (diviser, focus suivant/précédent, reset), fermeture par X/double-clic/molette/Ctrl+W, auto-collapse d'un panneau vidé, plafond 4 panneaux, drag-to-split bas, pas de doublon à la réouverture.
  • Fix scripts/run-e2e-local.sh : chemins Windows via cygpath (les env vars MSYS n'étaient pas converties → vaults vides, les 16 tests split-view échouaient en local alors qu'ils passaient en CI).

Corrigé

  • PDF stream 500 : api_pdf_stream plantait systématiquement (NameError: current_user non injecté — endpoint jamais couvert par un test). Désormais authentifié + testé.
  • Indexation incrémentale des PDF : le chemin watcher (_index_single_file_sync) lisait les PDFs en read_text() → contenu garbage indexé. Utilise maintenant extract_pdf_text() comme le scan complet.

[2.0.0] — 2026-08-25

ObsiGate passe à 2.0.0 avec sa version bureau native et de nombreuses fonctionnalités de productivité (branch main, 341 commits après le tag v1.8.0).

Ajouté

  • Application bureau native (Tauri 2) — Porte d'entrée desktop pour vaults Obsidian (desktop/)
  • Dashboard administrateur (#71, backend) — 4 endpoints admin-gated pour monitoring serveur
    • GET /api/admin/stats — CPU/RAM/Disk/Uptime via psutil
    • GET /api/admin/audit — 500 dernières entrées d'audit avec filtres user/action/limit/offset
    • GET /api/admin/backup-stats — compte + taille + age par vault
    • GET /api/admin/stream — Server-Sent Events qui push les stats toutes les 5s
    • Nouveau module backend/admin.py + 13 tests pytest
  • Tests JSDOM PaneManager (#75, sous-tâche I2) — 9 tests d'intégration frontend via JSDOM 30
    • Couvre : factory, init, open/activate/close, isolation entre panes, drag/drop smoke
    • Wired dans le CI Gitea (.gitea/workflows/ci.yml)
    • Shell Rust + Tauri 2 (tray icon, notifications OS, auto-update depuis Gitea)
    • Backend Python embarqué (python-embed) avec health check, démarrage ~2s
    • Sélecteur de dossiers natif, config persistante des vaults (type VAULT vs DIR)
    • Fenêtre console masquée, arrêt propre via tray, associations de fichiers .md
    • Packages : Windows MSI + NSIS, Linux .deb + AppImage
    • Pipeline CI/CD Desktop (.gitea/workflows/desktop-build.yml)
    • Thème système auto (clair/sombre) + RAM réduite (~80 MB au repos)
    • Écran de chargement inline (staging backend/frontend pour le bundle desktop)
  • Internationalisation FR/EN complète — 1357 clés i18n, préférence serveur, guide entièrement traduit
  • Vue multi-panneaux (Split View #75) — PaneManager + TabManager par panneau
    • Split droite/bas, drag & drop cross-pane, drag-to-split (bord droit/bas)
    • Boutons de split, navigation clavier, fermeture auto des panneaux vides
  • Mermaid.js — Rendu de diagrammes (22 templates, thème dark synchronisé)
    • Header bar Code/Preview, zoom, export SVG + PNG, plein écran / focus 480px
    • Pré-processeur Obsidian ([[liens]], ![[img]], ==highlight==)
  • Gestionnaire de Backups complet — Page, filtres, purge, restauration
    • Diff viewer (unifié + côte à côte), preview 100 Ko, compression gzip
    • Backup automatique, auto-nettoyage (max_backups_per_file)
  • Support Excalidraw (#78) — Fichiers .excalidraw / .excalidraw.md
    • Décompression lz-string, création depuis modale/menu contextuel
  • Éditeur Forge + autocomplétion IA — Auto-height, Mermaid QI, ghost text
    • Autocomplétion Ollama (qwen2.5-coder), Alt+/, auto-continuation listes/checkboxes
  • Support PDF + multi-format — PDF avec TOC, images/CSV/JSON/TXT
  • 15 thèmes clair/sombre (#65) — Système de thèmes configurable
  • Mode hors-ligne PWA (#59) — Badge hors-ligne + gestion des conflits
  • Tests E2E Playwright (#58) — Suite desktop (44 tests) + CI

Modifié

  • desktop/ — Nouveau : application Tauri (Cargo.toml, build.rs, src/main.rs, scripts build)
  • frontend/ — i18n, split view, Mermaid, Excalidraw, Forge, thèmes
  • backend/ — Endpoints backups, Excalidraw, multi-format, versioning git
  • docs/ROADMAP.md — Feuille de route passée à 2.0.0-dev, items #75/#78 complétés

[1.8.0] — 2026-06-04

Ajouté

  • Palette de commandes — Navigation rapide fichiers + commandes
    • Raccourcis : Ctrl+P fichiers, Ctrl+Alt+Espace commandes, Ctrl+Shift+Espace
    • Navigateur de dossiers : créer/supprimer fichiers/dossiers par sélection
    • Opérateurs +include / -exclude dans la recherche globale
    • 6 nouvelles commandes : créer/supprimer fichiers/dossiers, éditer, pop-out
  • Barre d'outils mobile de bas de page — Type Obsidian 📱
    • Explorateur, recherche, palette, changement d'onglet
    • Indicateur de vault contextuel sous le header, pull-to-refresh
  • Diff viewer des backups — Comparaison unifiée + côte à côte, restauration
  • Indicateur AI Actif — Badge/statut IA dans le header (lecture runtime des clés)
  • Page d'accueil de vault — Liste récursive des fichiers par date
  • Drag & drop de fichiers dans l'arborescence
  • Guide d'aide enrichi — Recherche, scroll spy, slide-over mobile, section webhooks
  • Indexation inversée non-bloquante (run_in_executor)
  • Infrastructure de tests — Couverture pytest portée à 70% (+109 tests)

Modifié

  • frontend/ — Palette, mobile toolbar, aide, AI indicator, vault home
  • backend/ — Indexation non-bloquante, statut IA, diff/restauration backups
  • docs/ — Guide d'utilisation enrichi (navigation clavier), CHANGELOG/README/ROADMAP

Corrigé

  • Correctifs UI mobile, palette, aide et AI toolbar (thème clair, menu clipping)

[1.7.0] — 2026-05-30

Ajouté

  • AI Editor Toolbar — Barre d'outils IA intégrée à l'éditeur CodeMirror
    • Menus dropdown : Edit (improve, fix spelling, shorter, longer, simplify), Tone (professional, casual), Translate (multi-langue), Generate (explain, summarize, continue), Custom Rewrite, Toolbox (to list, to table, frontmatter, to canvas)
    • Multi-provider : DeepSeek / OpenRouter / Gemini, configurable via .env
    • 16 endpoints REST : POST /api/ai/{edit|tone|translate|generate|rewrite|toolbox}
    • Auto-save silencieux (2s debounce) avec feedback visuel toast
    • Cache toolbar si pas de clé API configurée
    • Messages d'erreur clairs (401→clé invalide, etc.)
  • Graph view — Améliorations majeures
    • Header flat design 2 lignes responsives (titre flexible + recherche dédiée + barre statut)
    • Navigation historique ← → + ↑ Parent
    • Sticky panels : tuile info (top-left/right) avec métadonnées, preview pleine hauteur (droite)
    • Ctrl+survol → aperçu contenu formaté (via /raw) dans tooltip
    • Filtre par tag dans la vue graphe
    • Boutons close ✕ sur les panneaux
    • Fond hover foncé unifié

Modifié

  • backends/ai.py — Nouveau module IA backend
  • backends/ai_routes.py — Routes API pour l'éditeur IA
  • frontend/js/ai.js — Module frontend de la toolbar IA (404 lignes)
  • frontend/js/graph.js — Refonte complète : sticky panels, nav historique, aperçu markdown

[1.6.0] — 2026-05-29

Ajouté

  • Vue graphe des wikilinks — Canvas force-directed
    • Phase 1 : Full-vault view, filtre par tag, backlinks entrants, métriques de nœud
    • Phase 2 : Recherche visuelle, tooltips survol, couleurs adaptatives, slider profondeur (0-3)
    • Phase 3 : Filtre par type (dossier/fichier/.md/autre), mode focus, plein écran, export PNG
    • Phase 4 : Barnes-Hut O(n log n), cache graphe
  • sortedcontainers → O(log n) insert/remove dans l'index inversé
  • CI/CD Pipeline complet — Gitea Actions
    • Ruff linting (0 erreur)
    • Mypy type checking (0 erreur)
    • Pytest — 175 tests, 49% coverage
    • Bandit SAST — scan de sécurité
    • Pip-audit — vulnérabilités dépendances
    • Docker build — vérification image
    • Coverage artifact — uploadé via actions/upload-artifact@v3
    • Runner Gitea auto-hébergé (gitea/act_runner:0.2.11 sur Alpine/Proxmox)

Modifié

  • backend/search.py — Remplacement bisect → SortedList pour le vocabulaire
  • .gitea/workflows/ci.yml — Pipeline CI/CD complet

Corrigé

  • Résolution de 28 erreurs mypy pré-existantes
  • Fix lint ruff + faux positifs bandit + pip-audit non-bloquant

[1.5.1] — 2026-05-28

Ajouté

  • Split app.js (8 875 lignes) en 16 modules ES
    • Modules : state.js, utils.js, auth.js, ui.js (76KB), viewer.js (47KB), sidebar.js, search.js (45KB), config.js, dashboard.js, graph.js (33KB), sync.js, legacy.js, ai.js
    • Import/export validator CI (tests/frontend/validate-imports.mjs)
    • Tests unitaires frontend (tests/frontend/unit.test.mjs)
  • Stemming français — snowballstemmer intégré au tokenizer
    • "manger" trouve "mangé", "mangeons", "mangeait"
    • Performance O(T) au lieu de O(S×T) — fix freeze 15min sur gros vaults
    • Crash guard : try/except snowballstemmer IndexError sur tokens exotiques

Modifié

  • frontend/js/app.js → entrypoint ES module important les 16 modules
  • backend/search.py — tokenizer avec FrenchStemmer

Corrigé

  • Nombreuses régressions post-split : imports manquants, exports oubliés, shadowing de variables
  • Réécriture state.js → mutable object (export const state = {...}) pour éviter les erreurs assignment to constant
  • Plus de 60 correctifs d'imports/exports validés par le validator CI
  • Tags de template {{...}} exclus du tag cloud via filtre configurable

[1.5.0] — 2026-05-26

Ajouté

  • Publication publique — Génération de lien partageable avec token unique (64-char hex)
    • Expiration configurable, lecture seule, sans authentification
    • backend/share.py + endpoints CRUD + vue publique /s/{token}
    • Export PDF via WeasyPrint (lazy import) dans la vue publique
    • Support fichiers non-Markdown dans la vue publique
  • Webhooks — Notifications HTTP POST vers services externes
    • Signature HMAC-SHA256 optionnelle
    • backend/webhooks.py + CRUD endpoints + UI dans Configuration
  • Dashboard statistiques — Métriques agrégées par vault
    • Widget DashboardStatsWidget avec 4 cartes (fichiers, tags, taille, vaults)
    • Widget "Derniers fichiers ouverts" avec timestamps
  • Documentation OpenAPI enrichie — Tous les modèles Pydantic documentés (Field(description=...))
    • Visible dans /docs (Swagger UI) et /redoc
  • Documentation complète — ANALYSE_REVIEW.md, AUDIT_TECHNIQUE.md, ROADMAP.md

Modifié

  • frontend/js/app.js — Nouveau système d'onglets (TabManager), dashboard
  • frontend/style.css — Dashboard, share view, webhooks UI
  • backend/main.py — Nouveaux endpoints share/webhooks/dashboard
  • backend/search.py — Index inversé incrémental (hook pattern)

[1.4.0] — 2026-05-25

Ajouté

  • Rate limiting — IP-based (10 tentatives/15min) + lockout par compte (5 tentatives)
    • backend/ratelimit.py — RateLimiter thread-safe avec nettoyage périodique
    • Intégré à backend/auth/router.py
  • Secret redactor — Masquage automatique des secrets dans les aperçus
    • backend/secret_redactor.py — JWT, API keys, tokens GitHub, clés privées, connection strings
  • Audit log — Traçage des écritures/suppressions/changements de config
    • backend/audit.py — JSON lines dans data/audit.log, rotation 10MB
  • Backup automatique avant écriture
    • Sauvegarde du contenu original dans .obsigate-backup/ avant PUT/DELETE
  • Backlinks panel — Fichiers avec wikilinks pointant vers le fichier courant
    • GET /api/file/{vault}/backlinks + index inversé dans indexer.py
  • Gestion des conflits Syncthing
    • Détection automatique des fichiers .sync-conflict-*
    • Résolution : garder local, garder conflit
    • GET /api/conflicts, POST /api/conflicts/resolve
  • Syncthing conflicts dashboard — Vue dédiée dans l'UI
  • IGNORED_DIRS configurable — OBSIGATE_IGNORED_DIRS (liste séparée par virgules)
  • Timeout de session configurable — OBSIGATE_ACCESS_TOKEN_TTL / OBSIGATE_REFRESH_TOKEN_TTL

Modifié

  • backend/main.py — +audit, +backup, +redaction, +backlinks endpoint, +conflicts endpoint
  • backend/indexer.py — +backlink index, +IGNORED_DIRS
  • backend/auth/router.py — +IP rate limiting
  • backend/auth/jwt_handler.py — +configurable TTL
  • backend/watcher.py — +configurable IGNORED_DIRS

[1.3.0] — 2026-05-23

Ajouté

  • Authentification complète — JWT + Argon2id
    • Login/logout, refresh tokens, cookies httpOnly
    • Contrôle d'accès par vault
    • Interface d'administration : création/modification/suppression d'utilisateurs
    • "Se souvenir de moi", sessions persistantes
    • backend/auth/ — 5 modules (~600 lignes)
  • CodeMirror 6 — Éditeur de fichiers intégré
    • Syntax highlighting multi-langage
    • Sauvegarde, annulation, fallback textarea si CM6 échoue
    • Auto-sync frontmatter YAML
    • Find-in-page (Ctrl+F) avec regex, barre de navigation
  • PWA complet — manifest.json, sw.js, icônes multi-tailles
    • Installation via bouton "Installer l'application"
    • Thème synchronisé entre fenêtres popout
    • Service Worker avec mise en cache
  • Synchronisation temps réel — Watchdog + SSE
    • Surveillance automatique des fichiers
    • Mise à jour incrémentale de l'index
    • SSE Manager avec reconnexion automatique
    • backend/watcher.py — watchdog observer avec debounce 2s
  • Drag & drop / rename — Menu contextuel arborescence
    • Création fichier/dossier, rename inline, suppression
    • Boutons d'action toujours visibles, support mobile long-press

Modifié

  • frontend/app.js — Refonte majeure : authentication flows, code editor, PWA, sync
  • frontend/style.css — CodeMirror themes, auth forms, PWA UI, responsive
  • docker-compose.yml — auth env vars, data volume, non-root user
  • backend/main.py — auth middleware, file CRUD, code editor API, SSE, PWA
  • requirements.txt — argon2-cffi, python-jose, python-multipart

[1.2.0] — 2026-05-18

Ajouté

  • Rendu d'images Obsidian — Support complet de toutes les syntaxes
    • Standard Markdown avec attributs HTML
    • Wiki-link embed (![[path/to/image.png]])
    • Markdown standard (![alt](path))
    • Résolution multi-stratégies (7 niveaux)
    • Indexation des attachements au démarrage
    • Cache de résolution pour performance
  • Système de fichiers cachés — Configuration par vault
    • includeHidden, hiddenWhitelist — UI + env vars
    • Filtrage côté client (hideHiddenFiles)

Modifié

  • backend/attachment_indexer.py — Scan et indexation d'images
  • backend/image_processor.py — Prétraitement Markdown pour les syntaxes d'images
  • GET /api/image/{vault}?path= — Serve images avec MIME types
  • POST /api/attachments/rescan/{vault} — Rescan manuel
  • HIDDEN_FILES_GUIDE.md, IMAGE_RENDERING_GUIDE.md

[1.1.0] — 2026-05-15

Ajouté

  • Recherche TF-IDF avancée — Moteur complet avec index inversé
    • Opérateurs : tag:, vault:, title:, path:, ext:
    • Autocomplétion intelligente (fichiers, tags, historique)
    • Facettes, pagination, tri par pertinence
    • Snippets surlignés <mark>, accent-insensitive
    • Search history (localStorage, max 50, LIFO, dédupliqué)
  • Recherche regex — extractRegexSnippet() avec surlignage
  • Recherches sauvegardées — CRUD API + sidebar UI
    • Sauvegarde/chargement/suppression de requêtes
  • Filtres de recherche — toggles (case, mot entier, regex), chemins
  • Onglets sidebar — vaults, tags, récents, saved searches
  • Auto-expand vault dans le filtre sidebar
  • Tree search — Filtrage temps réel de l'arborescence avec surlignage

Modifié

  • backend/search.py — Nouveau : InvertedIndex, TF-IDF, autocomplete
  • frontend/app.js — Refonte recherche : nouveau système de requêtes, facettes, pagination, autocomplete
  • frontend/style.css — Search results cards, autocomplete dropdown, tags

[1.0.0] — 2026-05-10

Ajouté

  • Première version stable — Porte d'entrée web pour vaults Obsidian
  • Multi-vault — Visualisation de plusieurs vaults simultanément
  • Arborescence — Navigation hiérarchique avec lazy-loading
  • Rendu Markdown — Conversion HTML via mistune (tables, task lists, footnotes, strikethrough)
  • Extraction YAML frontmatter — Tous les champs parsés avec carte pliable, badges, tags
  • Vue Source (raw) — Toggle markdown brut + frontmatter
  • Édition — PUT/DELETE fichiers via API
  • Breadcrumbs — Navigation avec auto-expand et highlight
  • TOC — Table des matières avec scroll spy et barre de progression
  • Thème clair/sombre — Toggle localStorage + synchronisé fenêtres popout
  • Popout — Fenêtre détachée pour affichage standalone
  • Configuration UI — Paramètres persistants par vault
  • Docker multi-stage — Image ~180MB, linux/amd64 + arm64 + arm/v7 + i386
  • Healthcheck — Endpoint /api/health
  • Headers sécurité — CSP, X-Frame-Options, XSS-Protection, Referrer-Policy
  • Path traversal protection — _resolve_safe_path() symlink-aware
  • Security hardening — Utilisateur non-root (UID 1000)
  • Compression GZip (SSE-safe), Cache-Control immutable
  • build.sh — Script de build automatisé

Architecture

  • Backend : FastAPI 0.110.3, uvicorn 0.30.0, Python 3.11+
  • Frontend : Vanilla JS SPA (0 dépendances npm), CSS custom properties
  • Indexation : In-memory, mise à jour incrémentale, watcher watchdog
  • Recherche : TF-IDF inversé, sans I/O disque