4319 lines
240 KiB
Markdown
4319 lines
240 KiB
Markdown
# Changelog — ObsiGate
|
||
|
||
Toutes les modifications notables d'ObsiGate sont documentées dans ce fichier.
|
||
|
||
Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||
|
||
> **En cours de développement** : les changements à venir sont listés dans la section
|
||
> [Unreleased](#unreleased). La dernière version livrée est **2.65.2**.
|
||
|
||
---
|
||
|
||
## [Unreleased]
|
||
|
||
---
|
||
|
||
## [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](docs/features/xlsx-sheet-input-181.md).
|
||
- 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](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](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](./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](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](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](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](./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](./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](./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](./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](./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](./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](./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](./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](./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](./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)
|
||
+ extension `tests/test_ai_models.py` et `tests/frontend/ai.test.mjs` (+9). Détail :
|
||
[docs/features/ai-assistant-commands.md](./docs/features/ai-assistant-commands.md).
|
||
|
||
- **#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](./docs/DEVELOPMENT_AND_RELEASES.md#2bis-signature-des-mises-à-jour-updater-tauri).
|
||
|
||
- **#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`](./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](./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](./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](./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`](./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](./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](./docs/ROADMAP.md) 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`](./docs/DELIVERY_WORKFLOW.md)
|
||
(Definition of Done : tests, documentation, commit, push, CI) et [`AGENTS.md`](./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 (``)
|
||
- 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
|