Compare commits

...
5 Commits
Author SHA1 Message Date
bruno ed9029d760 fix: éditeur Forge — textarea qui ne grandit pas après insertion programmatique (tableau/code) #BUG-116
CI / lint (push) Successful in 3m17s
CI / security (push) Successful in 2m0s
CI / test (push) Successful in 5m14s
CI / build (push) Successful in 1m49s
CI / e2e (push) Successful in 17m57s
2026-10-14 22:09:28 -04:00
bruno 1892062d88 fix: éditeur Forge — espace checkbox/texte aperçu, flash du preview, menus table/langue coupés #BUG-115
CI / lint (push) Successful in 3m11s
CI / security (push) Successful in 2m0s
CI / test (push) Successful in 5m16s
CI / build (push) Successful in 1m49s
CI / e2e (push) Successful in 17m47s
2026-10-14 21:56:37 -04:00
bruno cbe3d9bc75 fix: popup currentPath is not defined après suppression/renommage de répertoire #BUG-114
CI / lint (push) Successful in 3m9s
CI / security (push) Successful in 1m59s
CI / test (push) Successful in 5m12s
CI / build (push) Successful in 1m49s
CI / e2e (push) Successful in 17m57s
2026-10-14 21:20:08 -04:00
bruno 2ebfe74459 perf: suppression de répertoire incrémentale au lieu du re-scan complet #BUG-113
CI / lint (push) Successful in 3m9s
CI / security (push) Successful in 1m55s
CI / test (push) Successful in 5m25s
CI / build (push) Successful in 1m48s
CI / e2e (push) Successful in 17m56s
2026-10-14 16:56:56 -04:00
bruno 6257adb46b fix: CI security — bandit B112 sur search_shared_notes (nosec, continue délibéré)
CI / lint (push) Successful in 3m9s
CI / security (push) Successful in 1m56s
CI / test (push) Successful in 5m13s
CI / build (push) Successful in 1m47s
CI / e2e (push) Successful in 18m17s
2026-10-14 13:43:26 -04:00
16 changed files with 219 additions and 31 deletions
+79 -1
View File
@@ -6,7 +6,7 @@ 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.68.1**.
> [Unreleased](#unreleased). La dernière version livrée est **2.68.6**.
---
@@ -14,6 +14,84 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.68.6] — 2026-10-14
---
## [2.68.5] — 2026-10-14
---
## [2.68.4] — 2026-10-14
### Corrigé
- **BUG-116** — éditeur Forge : après insertion d'un tableau (ou bloc de code,
diagramme), la textarea restait d'une seule ligne — les mutations
programmatiques de `ta.value` ne déclenchent pas l'événement `input`, seul
chemin qui rappelait `autoHeight()`. `insertAtCursor` et `wrapSelection` (les
deux points de passage de toutes les insertions) appellent désormais
`autoHeight()` + `updateLineNumbers()` — le nombre de lignes du gutter suit.
Vérifié en Playwright : tableau 4 lignes sur document d'une ligne → hauteur
51 → 189 px, gutter cohérent.
- **BUG-115** — éditeur Forge (aperçu et menus) : trois défauts —
(1) collure case à cocher/texte dans l'aperçu des checklists : le HTML du
serveur ne sépare pas l'`<input>` du texte et `editor-poc.html` n'avait pas
la règle `.task-list-item` de `style.css` (ajoutée à son `<style>`) ;
(2) l'aperçu « flashait » : chaque frappe effaçait le rendu pour afficher
« Rendering... » — l'ancien contenu reste désormais affiché pendant le
rafraîchissement, et la dernière frappe est rejouée (flag `pending`) au lieu
d'être perdue ; (3) le menu « dimensions du tableau » (et le sélecteur de
langage) était posé en `position: absolute` avec des coordonnées viewport →
double décalage → menu coupé hors du champ sur un document d'une ligne ;
passés en `position: fixed` avec clamp viewport comme le menu slash, et
`stopPropagation` sur les clics des menus (la bulle fermait la forme
aussitôt ouverte). Vérifié en Playwright sur l'instance E2E : spacing 6 px,
aucun spinner pendant la frappe, forme entièrement visible + insertion OK.
- **BUG-114** — popup « `currentPath is not defined` » après suppression (ou
renommage) d'un répertoire : `frontend/js/ui.js` référençait `currentPath` nu
(non déclaré — la variable vit dans `state`) au lieu de `state.currentPath`
dans les branches répertoire de `confirmDeleteDirectory` et du renommage en
ligne. La `ReferenceError` était avalée par le `catch` et affichée en toast
d'erreur. Trois usages corrigés ; vérifié en Playwright (flux réel clic droit
→ Supprimer) : toast « Dossier supprimé », aucun fantôme, 0 erreur JS.
---
## [2.68.3] — 2026-10-14
### Amélioré
- **BUG-113 — Suppression de répertoire rapide** : `DELETE /api/directory/{vault}`
re-scannait la vault entière et reconstruisait l'index inversé global après le
`rmtree` (`reload_single_vault`) — le temps croissait avec toute la vault, pas
avec le dossier supprimé. Nouveau chemin incrémental
(`remove_directory_from_index`) qui retire uniquement les fichiers du
sous-arbre (index, paths, index inversé et sémantique, dossiers fantômes
purgés). Mesuré sur l'instance E2E : 60 fichiers supprimés en **63 ms** au
lieu de **668 ms** (~10×, écart qui grandit avec la taille du home-user).
Tests : `test_delete_directory_subtree_clears_index` (aucun fantôme dans
paths ni dans la recherche plein-texte après suppression).
---
## [2.68.2] — 2026-10-14
### Corrigé
- **CI security — bandit bloquant sur B112** : `backend/services/search.py`
(`search_shared_notes` : un partage illisible était ignoré via
`except Exception: continue`) déclenchait `B112:try_except_continue`, le job
`security` sortait en rouge. Le `continue` est **délibéré** (un partage
inaccessible ne doit pas faire échouer la recherche) — `# nosec B112` posé
sur la ligne, comme les `# nosec B110` existants (`watcher.py`, `B105`,
`B324`). Bandit re-vérifié localement : 0 issue ; l'étape reste bloquante
(garde-fou `test_bandit_and_pip_audit_stay_blocking` intact).
---
## [2.68.1] — 2026-10-14
### Corrigé
+3 -3
View File
@@ -4,7 +4,7 @@
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
[![Version](https://img.shields.io/badge/Version-2.68.1-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.68.6-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
## 📝 Changelog
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.68.1).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.68.6).
---
*Projet : ObsiGate | Version : 2.68.1 | Dernière mise à jour : Septembre 2026*
*Projet : ObsiGate | Version : 2.68.6 | Dernière mise à jour : Septembre 2026*
+3 -3
View File
@@ -2,7 +2,7 @@
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
[![Version](https://img.shields.io/badge/Version-2.68.1-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.68.6-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
## 📝 Changelog
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.68.1).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.68.6).
---
*Project: ObsiGate | Version: 2.68.1 | Last updated: September 2026*
*Project: ObsiGate | Version: 2.68.6 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.68.1
2.68.6
+46
View File
@@ -1312,6 +1312,52 @@ async def handle_file_move(vault_name: str, src_abs: str, dest_abs: str) -> dict
return await update_single_file(vault_name, dest_abs)
async def remove_directory_from_index(vault_name: str, dir_rel_path: str) -> int:
"""Remove a directory subtree from the index without a full vault re-scan.
Incremental equivalent of ``reload_single_vault`` for deletions : every
indexed file under *dir_rel_path* is dropped via
``_remove_file_from_structures`` (which notifies the inverted and semantic
indexes through the change hook), and the subtree's directory entries are
purged from ``path_index``. Returns the number of files removed.
Used by :func:`api_directory_delete` — previously a ``rmtree`` was followed
by a full vault re-scan + global inverted-index rebuild, which is linear in
the whole vault size on every single directory deletion.
Args:
vault_name: Name of the vault.
dir_rel_path: Directory path relative to the vault root.
Returns:
Number of indexed files removed.
"""
vault_data = index.get(vault_name)
if not vault_data:
return 0
prefix = dir_rel_path.rstrip("/") + "/"
targets = [f["path"] for f in vault_data.get("files", []) if f["path"].startswith(prefix)]
lock = _get_async_lock()
async with lock:
removed = 0
for rel_path in targets:
if _remove_file_from_structures(vault_name, rel_path):
removed += 1
# Purge the subtree's directory entries too, otherwise empty dirs stay
# in path_index as ghosts (file entries were removed per-file above).
if vault_name in path_index:
path_index[vault_name] = [
p for p in path_index[vault_name]
if not (p["path"] == dir_rel_path or p["path"].startswith(prefix))
]
if removed:
logger.info(f"Directory removed from index: {vault_name}/{dir_rel_path} ({removed} files)")
return removed
async def remove_vault_from_index(vault_name: str):
"""Remove an entire vault from the index.
+4 -3
View File
@@ -532,9 +532,10 @@ async def api_directory_delete(
result = service_delete_directory(vault_name, path, recursive=True)
file_count = result["deleted_count"]
# Update index
from backend.indexer import reload_single_vault
await reload_single_vault(vault_name)
# Update index — incremental subtree removal (no full vault re-scan, no
# global inverted-index rebuild).
from backend.indexer import remove_directory_from_index
await remove_directory_from_index(vault_name, result["path"])
# Broadcast SSE event
await sse_manager.broadcast("directory_deleted", {
+1 -1
View File
@@ -80,7 +80,7 @@ def _shared_results(username: str, q: str, vault: str) -> list[dict[str, Any]]:
if not fp.exists() or fp.suffix.lower() != ".md":
continue
raw = fp.read_text(encoding="utf-8", errors="replace")
except Exception:
except Exception: # nosec B112 — partage illisible : ignoré, on continue
continue
title = fp.stem
occurrences = raw.lower().count(q_lower)
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.68.1"
version = "2.68.6"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.68.1"
version = "2.68.6"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.68.1",
"version": "2.68.6",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+8 -2
View File
@@ -220,8 +220,10 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-111* | Admin : modale « Administration — Utilisateurs » sans icône de fermeture (lucide absent de admin.html) et tableau de bord admin affichant toutes les sections au lieu de la seule section sélectionnée | 2026-10-10 | Utilisateur | `frontend/admin.html` (ajout du script lucide UMD) + `frontend/js/admin.js` (`_wireSectionNav` : show/hide au lieu du scrollspy) ; suites frontend + backend vertes, vérifié sur instance locale 2020 | lucide n'était chargé que dans index.html ; le scrollspy laissait les 4 sections visibles |
| *BUG-110* | Chat : refonte visuelle des posts — boutons discrets sous l'image, image ouverte dans un onglet de l'app, point de non-lus sur l'icône chat, notification OS hors focus, annulation **réelle** d'une suppression | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/js/filechat.js`, `frontend/js/ui.js`, `frontend/js/viewer.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/filechat.test.mjs` | Clic sur une image du chat → ouverture dans un onglet du navigateur au lieu du visualiseur de l'app ; boutons de pièce jointe bavards (icône + libellé) ; aucun repère visuel persistant sur l'icône chat ; aucune alerte OS quand l'app est en arrière-plan ; le bouton « Annuler » d'une suppression ne restaurait rien (copie DOM d'un message déjà supprimé côté serveur) | `TabManager.openChatImage()` (+ branche `chatImage` de `activate()`, `renderImageViewer()` accepte une `url` directe) ouvre la pièce jointe dans un **onglet** avec le visualiseur des images de vault (zoom/pan/plein écran) ; boutons **icônes seules** (24 px, `aria-label`) sous l'image, alignés à droite ; **point rouge pulsant** (`.chat-has-unread`) tant qu'il reste des non-lus, en plus du badge chiffré ; `_notifyNewMessage()` — notification OS si `document.hidden`/hors focus, permission demandée à l'ouverture du chat ; suppression **différée de 10 s** (ligne « Ce message a été supprimé » + « Annuler », DELETE envoyé à l'échéance seulement, échec réseau → le message revient) | Vérifié : `filechat.test.mjs` 25/25 (dont 6 tests #110), `validate-imports` 42 modules / 372 exports 0 erreur, `unit.test.mjs` 13/13 |
| *BUG-112* | E2E local : `pwsh -File scripts/e2e-server.ps1 2>&1 \| head -n 20` ne se termine **jamais** — la commande semble bloquée des dizaines de minutes alors que le serveur est prêt en ~7 s | 🟢 corrigé | P1 | 🧪 E2E local / Windows | IA | `scripts/e2e-server.ps1` (`start`) | `pwsh -NoProfile -File scripts/e2e-server.ps1 start 2>&1 \| head -n 20` → pipeline ouvert ~500 s+ ; se termine en <2 s dès qu'uvicorn est tué (problème d'EOF du pipe, pas de lenteur du démarrage) | `Start-Process` laissait uvicorn hériter des handles stdout/stderr du pwsh parent → le pipe `\| head` reste ouvert tant que le serveur vit → `head` attend un EOF qui n'arrive jamais. Remplacé par `Win32_Process.Create` (WMI) : processus détaché sans handle hérité ; le processus WMI n'héritant pas de l'environnement, les `OBSIGATE_*` sont passés via `set "NOM=valeur"` dans `cmd /c` (les `""` doublés faisaient échouer le quote-stripping de cmd : forme sans guillemets extérieurs) | Vérifié : commande exacte de l'utilisateur → `real 0m6,883s`, `[OK] READY v2.67.2`, serveur toujours up (2 vaults, 137 fichiers indexés), `stop`/`status` OK, syntaxe PS validée, aucun orphelin |
| | | | | | | | | | |
### TODOs techniques (améliorations / nouvelles tâches)
| *BUG-113* | Perf : suppression d'un répertoire dans la vault home-user très lente — le temps croît avec la vault entière, pas avec le dossier supprimé | 🟢 corrigé | P1 | ⚙️ backend / perf | IA | `backend/routers/files_write.py` (`api_directory_delete`), `backend/indexer.py` (`remove_directory_from_index` nouveau) | Supprimer un gros répertoire du home-user → l'API met des secondes (re-scan complet de la vault + rebuild de l'index inversé global) | `DELETE /api/directory/{vault}` appelait `reload_single_vault` après le `rmtree` : re-scan de TOUTE la vault + `init_inverted_index()` global. Remplacé par `remove_directory_from_index` : retire du sous-arbre uniquement (fichiers + entrées dossiers de `path_index`, notification incrémentale de l'index inversé et sémantique via le hook existant) | Mesuré sur l'instance E2E : 60 fichiers supprimés → **63 ms** vs **668 ms** avant (~10×) ; aucun fantôme dans `/api/vault/{v}/paths` ni dans la recherche plein-texte après suppression (`test_delete_directory_subtree_clears_index`) ; 232 tests verts, ruff 0, mypy 0 |
| *BUG-114* | Popup « currentPath is not defined » après suppression (ou renommage) d'un répertoire | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/ui.js` (`confirmDeleteDirectory`, renommage inline) | Ouvrir un fichier d'un dossier, clic droit → Supprimer le dossier (ou Renommer) → toast d'erreur `currentPath is not defined` alors que l'opération réussit | Les branches répertoire testaient `currentPath` nu (non déclaré dans ui.js — la variable vit dans `state`) au lieu de `state.currentPath` : `ReferenceError` dans le `try`, avalée par le `catch` → toast. Trois usages corrigés (startsWith ×2, slice ×1) | Vérifié en Playwright (instance 2029, flux réel clic droit → Supprimer sur un dossier contenant le fichier ouvert) : toast « Dossier supprimé (1 fichiers) », 0 fantôme dans /api/vault/{v}/paths, 0 erreur JS ; validate-imports 42 modules, unit.test.mjs 13/13 |
| *BUG-115* | Éditeur Forge : cases à cocher collées au texte dans l'aperçu, aperçu qui flashe à la frappe, menu « dimensions du tableau » coupé hors écran sur un document court | 🟢 corrigé | P2 | 📱 frontend / éditeur | IA | `frontend/editor-poc.html` (CSS aperçu, `renderPreview`, `showTableForm`, `showLangPicker`) | Ouvrir un `.md` avec `- [ ]`, activer l'aperçu → texte collé au checkbox + flash « Rendering... » ; `/table` sur un document d'une ligne → forme partiellement visible | (1) règle `.task-list-item input[type=checkbox] { margin-right: 6px }` ajoutée au `<style>` de l'aperçu (miroir `style.css:2303`) ; (2) `renderPreview` ne remplace plus le contenu par le spinner (affiché seulement si vide) + flag `_previewDirty` pour rejouer la dernière frappe ; (3) `.inline-form`/`.lang-picker` en `position: fixed` (coordonnées viewport) avec clamp bas/droite comme le menu slash + `stopPropagation` sur les clics qi/slash (le listener document refermait la forme) | Vérifié en Playwright (instance E2E) : marginRight 6 px + `list-style: none`, MutationObserver : aucun « Rendering... » pendant 14 frappes et dernier état rendu, forme table `bottom 193 < vh 900` sur document 1 ligne + insertion OK |
| *BUG-116* | Éditeur Forge : après insertion d'un tableau (ex. 4 lignes), la textarea reste d'une seule ligne au lieu de s'agrandir | 🟢 corrigé | P2 | 📱 frontend / éditeur | IA | `frontend/editor-poc.html` (`insertAtCursor`, `wrapSelection`) | Insérer un tableau via le menu : le contenu contient bien les nouvelles lignes mais la hauteur de la textarea ne change pas | Les mutations programmatiques de `ta.value` ne déclenchent pas l'événement `input` — seul handler qui rappelait `autoHeight()`/`updateLineNumbers()` (les handlers clavier, eux, les appelaient à la main). Ajout des deux appels dans `insertAtCursor` et `wrapSelection`, les deux points de passage de toutes les insertions (tableau, code, mermaid, checklist, gras multi-ligne) | Vérifié en Playwright sur l'instance E2E (qi → Table, 4 lignes, sur doc d'une ligne) : hauteur 51 → 189 px, 8 lignes rendues, gutter aligné | ### TODOs techniques (améliorations / nouvelles tâches)
| # | Titre | Statut | Priorité | Scope | Assigné | Zone (fichier) | Cmd de repro | Correctif / Commit | Notes |
|---|---|---|---|---|---|---|---|---|---|
@@ -347,6 +349,10 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-10-09 | #194 | Fonctionnalité (UI) | `frontend/js/sidebar.js`, `frontend/js/viewer.js`, `docker-compose.test-win.yml`, `.gitignore`, `tests/frontend/sidebar-filters.test.mjs`, `docs/ROADMAP.md`, `docs/features/user-home-194.md`, `CHANGELOG.md` | **#194 (incrément UI)** : les vaults `home-<user>` s'affichent **en premier** dans l'arborescence de la sidebar — tri stable appliqué à la source (`_homeFirst` dans `loadVaults()` et dans le soft-refresh, donc `state.allVaults`, l'arborescence et les sélecteurs restent cohérents) — et portent l'icône **maison** lucide (`data-lucide="home"` via `getVaultIcon`, même `safeCreateIcons`/`stroke: currentColor` que les autres icônes, thème-aware). Compose de test : `test_home:/vaults/Home` + `OBSIGATE_HOME_ROOT=/vaults/Home` (dossier `test_home/` ignoré par git). Vérifié : `sidebar-filters.test.mjs` 8→**10/10**, `validate-imports` 42 modules / 372 exports, `unit.test.mjs` 13/13, suites voisines (ai-sidebar, toolbar-order, search-facets, pane-manager, editor-inline) vertes, **live Playwright sur l'instance 2020** : ordre `home-admin, home-bruno, TestDir, TestVault` + `svg.lucide-home` rendu (placeholder `<i>` remplacé) | ✅ livré (en attente vérif utilisateur) |
| 2026-10-14 | BUG-112 | Correction | `scripts/e2e-server.ps1`, `docs/ISSUES_TODOLIST.md`, `CHANGELOG.md` | **BUG-112 — `pwsh -File scripts/e2e-server.ps1 2>&1 \| head -n 20` ne se termine jamais.** Constat : le démarrage n'est pas lent (~7 s, READY, health 200, 137 fichiers indexés) — c'est le pipe qui ne reçoit jamais d'EOF. Preuve : le pipeline survit 494 s+ puis se termine en <2 s dès qu'uvicorn est tué. Cause : `Start-Process` laisse l'enfant hériter des handles stdout/stderr du pwsh parent ; le serveur (immortel par design) maintient le pipe ouvert → `head` attend un EOF qui n'arrivera jamais. Correctif : `Win32_Process.Create` (WMI) lance uvicorn détaché sans handle hérité ; le processus WMI n'héritant pas de l'environnement, les `OBSIGATE_*` sont passés via `set "NOM=valeur"` dans `cmd /c` (les `""` doublés faisaient échouer le quote-stripping de cmd : forme sans guillemets extérieurs). Vérifié : commande exacte de l'utilisateur → `real 0m6,883s`, `[OK] READY v2.67.2`, serveur toujours up (2 vaults, 137 fichiers), `stop`/`status` OK, syntaxe PS validée, aucun orphelin | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-10-14 | BUG-113 | Perf | `backend/indexer.py`, `backend/routers/files_write.py`, `tests/test_api_main.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-113 — suppression de répertoire ~10× plus rapide.** Constat : dans le home-user, supprimer un répertoire prenait des secondes — le temps croissait avec la **vault entière**, pas avec le dossier supprimé. Cause : `DELETE /api/directory/{vault}` appelait `reload_single_vault` après le `rmtree` : re-scan complet de la vault + rebuild de l'index inversé **global** (toutes les vaults). Correctif : `remove_directory_from_index` (indexer.py) retire uniquement les fichiers du sous-arbre (files, tags, `_file_lookup`, `path_index` avec purge des dossiers fantômes) et notifie l'index inversé + sémantique **incrémentalement** via le hook existant — le même chemin que le watcher pour une suppression de fichier. Mesuré sur l'instance E2E : 60 fichiers supprimés → **63 ms** vs **668 ms** avant. Tests : `test_delete_directory_subtree_clears_index` (aucun fantôme dans `/api/vault/{v}/paths` ni dans la recherche plein-texte après suppression). Vérifié : 232 passed / 1 skip (api_main + indexer_advanced + tools_mutations + search), ruff 0, mypy 0 | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-10-14 | BUG-114 | Correction | `frontend/js/ui.js`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-114 — popup « currentPath is not defined » après suppression/renommage de répertoire.** Les branches répertoire de `confirmDeleteDirectory` (ligne ~1054) et du renommage inline (lignes ~967-968) référençaient `currentPath` nu au lieu de `state.currentPath` (ui.js ne déclare pas `currentPath`, contrairement à app.js) : `ReferenceError` levée dans le `try`, avalée par le `catch`, affichée en toast d'erreur alors que l'opération (DELETE/PATCH) avait réussi. Trois usages corrigés. Vérifié en Playwright sur l'instance E2E (flux réel clic droit → Supprimer) : toast de succès, 0 fantôme dans paths, 0 erreur JS ; validate-imports 42 modules / 373 exports, unit.test.mjs 13/13 | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-10-14 | BUG-115 | Correction | `frontend/editor-poc.html`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-115 — trois ajustements de l'éditeur Forge (aperçu + menus).** (1) Cases à cocher collées au texte : le rendu serveur (mistune task_lists) ne met pas d'espace après l'`<input>` et l'`<style>` de `editor-poc.html` n'avait pas l'équivalent de la règle `.md-content .task-list-item` de `style.css` → deux règles ajoutées (margin-right 6 px, list-style none). (2) Flash de l'aperçu : `renderPreview` écrasait le contenu par « Rendering... » à chaque frappe (debounce 500 ms) — remplacé par un rafraîchissement silencieux (l'ancien rendu reste affiché, spinner seulement au premier rendu) + `_previewDirty` qui rejoue la dernière frappe tombée pendant un fetch (état fini correct, sans clignotement). (3) Menu « dimensions du tableau » coupé sur un document d'une ligne : `.inline-form`/`.lang-picker` en `position: absolute` recevaient des coordonnées **viewport** (`wr.left+pt.x`) → double décalage et clipping par `.editor-pane { overflow: hidden }` ; passés en `position: fixed` avec clamp viewport (convention du menu slash, déjà dans le fichier) ; au passage le clic sur un item qi/slash bullait jusqu'au listener document qui refermait la forme aussitôt ouverte → `stopPropagation`. Vérifié en Playwright sur l'instance E2E (serveur réel) : checkbox `marginRight: 6px`, aucun « Rendering... » observé pendant la frappe et dernier état rendu, forme table entièrement visible (`bottom 193 < vh 900`) et insertion OK. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-10-14 | BUG-116 | Correction | `frontend/editor-poc.html`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-116 — la textarea Forge ne grandissait pas après une insertion programmatique.** Après un tableau de 4 lignes inséré via le menu, l'éditeur restait d'une ligne : le contenu était bien multiligne mais `autoHeight()` n'était rappelé que par l'événement `input`, que `ta.value = …` ne déclenche pas. Les handlers clavier appelaient `autoHeight(); updateLineNumbers();` à la main ; `insertAtCursor` et `wrapSelection` — les deux fonctions par lesquelles passent toutes les insertions (tableau, bloc de code, templates mermaid, checklist, mise en forme) — oubliaient les deux appels. Corrigé à la source dans les deux fonctions plutôt que chez chaque appelant. Vérifié en Playwright (instance E2E, chemin réel qi → Table, 4 lignes sur document d'une ligne) : hauteur 51 → 189 px, gutter = nb de lignes | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-10-09 | #194 | Correction (isolation) | `backend/auth/middleware.py`, `backend/services/recent.py`, `backend/services/search.py`, `backend/routers/search.py`, `backend/routers/config.py`, `backend/routers/conflicts.py`, `backend/routers/files_read.py`, `backend/routers/history.py`, `backend/push.py`, `backend/user_home.py`, `tests/test_user_home.py`, `tests/test_auth_api.py`, `docs/features/user-home-194.md`, `CHANGELOG.md` | **#194 — l'admin voyait le home de chaque utilisateur** (retour utilisateur) : `vaults: ["*"]` ouvrait **tous** les vaults, donc `home-<user>` compris. Correctif à la racine : `check_vault_access` exige un octroi **explicite** pour tout vault `home-*` (nouveau `is_home_vault()`), et les filtres « * » écrits en dur dans les routes ont été remplacés par cet appel unique — dashboard (`routers/config.py`), conflits, liens retour, favoris, abonnements push (`/api/search/replace` utilisait déjà `is_vault_allowed`). `/api/search` filtre désormais les bruts **avant** pagination via `search_vaults(is_allowed=…)` (sinon notes personnelles d'un autre compte dans les résultats). `_grant` n'écarte plus les comptes « * » → l'admin reçoit son propre `home-admin` (auto-réparé au démarrage). Vérifié : `test_user_home.py` 12→**14/14** (dont « * » sans homes + filtrage recherche), `test_auth_api.py` 2/2 (admin ne voit **plus** `home-alice`, alice voit le sien), suite **1705 passed / 2 skipped**, ruff backend 0, mypy 114 fichiers 0, **live Playwright instance 2020** : arborescence admin = `home-admin, TestDir, TestVault` (plus de `home-bruno`), `svg.lucide-home` rendu. Reste ouvert (fuite pré-existante, hors périmètre) : `/api/search/advanced`, `/api/tags`, `/api/tags/suggest`, `/api/tree-search`, `/api/suggest` ne filtrent pas par vault accessible | ✅ livré (en attente vérif utilisateur) |
---
+1 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.68.1 | **Dernière mise à jour :** 2026-10-14
> **Version :** 2.68.6 | **Dernière mise à jour :** 2026-10-14
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
> vers les fonctionnalités livrées.
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
+30 -9
View File
@@ -88,6 +88,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
.preview-content p { margin: 6px 0; }
.preview-content ul, .preview-content ol { margin: 6px 0; padding-left: 24px; }
.preview-content li { margin: 2px 0; }
.preview-content .task-list-item { list-style: none; margin-left: -1.2em; }
.preview-content .task-list-item input[type="checkbox"] { margin-right: 6px; }
.preview-content code { background: var(--bg3); padding: 1px 5px; border-radius: 3px; font-family: var(--mono); font-size: 12px; color: var(--ai); }
.preview-content pre { background: var(--bg3); padding: 12px 16px; border-radius: var(--R); overflow-x: auto; margin: 10px 0; border: 1px solid var(--border); }
.preview-content pre code { background: none; padding: 0; color: var(--text); }
@@ -134,7 +136,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
.qi-item:hover { background: var(--bg-hover); color: var(--text); }
/* Inline form (table dims, code lang) */
.inline-form { position: absolute; z-index: 195; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; padding: 8px 10px; min-width: 200px; }
.inline-form { position: fixed; z-index: 195; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; padding: 8px 10px; min-width: 200px; }
.inline-form.on { display: block; animation: fadeIn 100ms; }
.inline-form-label { font-size: 10px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 6px; }
.inline-form-row { display: flex; gap: 6px; align-items: center; margin-bottom: 5px; }
@@ -148,7 +150,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
.inline-form-actions button.ok:hover { background: #5a8fc5; }
/* Lang picker (in QI menu style) */
.lang-picker { position: absolute; z-index: 195; width: 200px; max-height: 320px; overflow-y: auto; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; padding: 3px; }
.lang-picker { position: fixed; z-index: 195; width: 200px; max-height: 320px; overflow-y: auto; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; padding: 3px; }
.lang-picker.on { display: block; animation: fadeIn 100ms; }
.lang-item { display: flex; align-items: center; gap: 7px; padding: 6px 9px; font-size: 12px; color: var(--text2); cursor: pointer; border-radius: var(--r); font-family: var(--mono); }
.lang-item:hover, .lang-item.sel { background: var(--bg-hover); color: var(--text); }
@@ -498,6 +500,10 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
ta.value = val().substring(0, p.s) + text + val().substring(p.e);
var np = cursorOffset != null ? p.s + cursorOffset : p.s + text.length;
setPos(np);
// Mutation programmatique : pas d'evenement 'input' → sans autoHeight/updateLineNumbers,
// sinon la textarea reste d'une ligne quand on insère un tableau/code.
autoHeight();
updateLineNumbers();
markDirty();
}
@@ -507,6 +513,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
var sel = val().substring(p.s, p.e);
ta.value = val().substring(0, p.s) + before + sel + (after || before) + val().substring(p.e);
setPos(p.s + before.length + sel.length + (after || before).length);
autoHeight();
updateLineNumbers();
markDirty();
}
@@ -764,6 +772,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
// ---- Markdown Preview (server-side rendering via ObsiGate API) ----
var previewTimer = null;
var _previewLoading = false;
var _previewDirty = false;
function schedulePreview() {
if (!previewVisible) return;
clearTimeout(previewTimer);
@@ -772,7 +781,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
function renderPreview() {
if (!previewVisible) return;
if (_previewLoading) return;
if (_previewLoading) { _previewDirty = true; return; }
// Demo mode: fallback to client-side marked
if (!fileVault || !filePath) {
try {
@@ -784,7 +793,11 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
}
// Save first, then fetch server-rendered HTML
_previewLoading = true;
previewContent.innerHTML = '<div class="preview-loading"><span class="spin"></span> Rendering...</div>';
// Ne jamais effacer le rendu affiché : un update garde l'ancien contenu
// jusqu'à l'arrivée du nouveau (sinon l'aperçu flashe à chaque frappe).
if (!previewContent.firstChild) {
previewContent.innerHTML = '<div class="preview-loading"><span class="spin"></span> Rendering...</div>';
}
var content = val();
fetch('/api/file/' + encodeURIComponent(fileVault) + '/save?path=' + encodeURIComponent(filePath) + '&backup=false', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
@@ -799,6 +812,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
previewContent.innerHTML = data.html || '<div class="preview-loading">Empty document</div>';
processPreviewBlocks();
_previewLoading = false;
if (_previewDirty) { _previewDirty = false; schedulePreview(); }
}).catch(function(e) {
previewContent.innerHTML = '<div class="preview-loading">Erreur de rendu</div>';
_previewLoading = false;
@@ -1082,8 +1096,11 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
ifCols.value = '3'; ifRows.value = '3';
var pt = caretToPixel(null);
var wr = editorWrap.getBoundingClientRect();
inlineForm.style.left = (wr.left + pt.x) + 'px';
inlineForm.style.top = (wr.top + pt.y + 8) + 'px';
var _ftop = wr.top + pt.y + 8, _fleft = wr.left + pt.x;
if (_ftop + 130 > window.innerHeight) _ftop = window.innerHeight - 140;
if (_fleft + 210 > window.innerWidth) _fleft = window.innerWidth - 220;
inlineForm.style.left = _fleft + 'px';
inlineForm.style.top = _ftop + 'px';
inlineForm.classList.add('on');
ifCols.focus(); ifCols.select();
}
@@ -1130,8 +1147,11 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
langPicker.innerHTML = html;
var pt = caretToPixel(null);
var wr = editorWrap.getBoundingClientRect();
langPicker.style.left = (wr.left + pt.x) + 'px';
langPicker.style.top = (wr.top + pt.y + 8) + 'px';
var _ltop = wr.top + pt.y + 8, _lleft = wr.left + pt.x;
if (_ltop + 320 > window.innerHeight) _ltop = window.innerHeight - 330;
if (_lleft + 200 > window.innerWidth) _lleft = window.innerWidth - 210;
langPicker.style.left = _lleft + 'px';
langPicker.style.top = _ltop + 'px';
langPicker.classList.add('on');
// Click handler
langPicker.querySelectorAll('.lang-item').forEach(function(item) {
@@ -1428,7 +1448,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
slashSearch.addEventListener('input', function() { filterSlash(slashSearch.value); });
slashMenu.addEventListener('click', function(e) {
var item = e.target.closest('.slash-item');
if (item) selectSlashAction(item.getAttribute('data-action'));
if (item) { e.stopPropagation(); selectSlashAction(item.getAttribute('data-action')); }
});
function navSlash(dir) {
var vis = getVisibleSlash(); if (vis.length === 0) return;
@@ -1452,6 +1472,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
document.getElementById('qi-btn').addEventListener('click', function(e) { e.stopPropagation(); toggleQI(); });
qiMenu.addEventListener('click', function(e) {
var item = e.target.closest('.qi-item'); if (!item) return;
e.stopPropagation();
var action = item.getAttribute('data-qi');
qiMenu.classList.remove('on');
selectSlashAction(action);
+3 -3
View File
@@ -964,8 +964,8 @@ const FileOperations = {
if (type === 'file' && state.currentVault === vault && state.currentPath === path) {
await openFile(vault, nextPath);
} else if (type === 'directory' && state.currentVault === vault && state.currentPath && (state.currentPath === path || currentPath.startsWith(`${path}/`))) {
const suffix = state.currentPath === path ? '' : currentPath.slice(path.length);
} else if (type === 'directory' && state.currentVault === vault && state.currentPath && (state.currentPath === path || state.currentPath.startsWith(`${path}/`))) {
const suffix = state.currentPath === path ? '' : state.currentPath.slice(path.length);
state.currentPath = `${nextPath}${suffix}`;
await focusPathInSidebar(vault, state.currentPath, { alignToTop: false });
}
@@ -1051,7 +1051,7 @@ const FileOperations = {
this._closeModal(overlay);
await refreshSidebarTreePreservingState();
if (state.currentVault === vault && state.currentPath && currentPath.startsWith(path)) {
if (state.currentVault === vault && state.currentPath && state.currentPath.startsWith(path)) {
showWelcome();
}
} catch (err) {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.68.1",
"version": "2.68.6",
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
"main": "patch.js",
"directories": {
+36
View File
@@ -340,6 +340,42 @@ class TestDirectoryCRUD:
data = resp.json()
assert data["success"] is True
def test_delete_directory_subtree_clears_index(self, client):
"""BUG-113 : suppression incrémentale — aucun fantôme dans paths ni
dans l'index inversé, sans re-scan complet de la vault."""
# Create files inside the directory
for name in ("alpha.md", "nested/beta.md"):
r = client.post("/api/file/TestVault", json={
"path": f"Projets/{name}",
"content": f"contenu unique {name}",
})
assert r.status_code == 200
# Indexed before deletion
paths_before = client.get("/api/vault/TestVault/paths").json()["results"]
assert any(p["path"] == "Projets/alpha.md" for p in paths_before)
hit = client.get("/api/search", params={"q": "alpha", "vault": "TestVault"}).json()
results = hit.get("results", hit if isinstance(hit, list) else [])
assert any(r["path"] == "Projets/alpha.md" for r in results)
# Delete the whole directory
resp = client.delete("/api/directory/TestVault", params={"path": "Projets"})
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["deleted_count"] >= 2
# No ghost in flat paths
paths_after = client.get("/api/vault/TestVault/paths").json()["results"]
ghost_dirs = [p["path"] for p in paths_after if p["path"].startswith("Projets")]
assert ghost_dirs == [], f"fantômes dans paths : {ghost_dirs}"
# No ghost in inverted index (full-text search)
hit = client.get("/api/search", params={"q": "alpha", "vault": "TestVault"}).json()
results = hit.get("results", hit if isinstance(hit, list) else [])
assert all(r["path"] != "Projets/alpha.md" for r in results)
assert all(r["path"] != "Projets/nested/beta.md" for r in results)
# ═══════════════════════════════════════════════════════════════════
# Search & Tags (supplement existing tests)