From 23a3c147cd487a8be73923d822e908b920294efe Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 17 Sep 2026 08:00:10 -0400 Subject: [PATCH] fix(editor): le bouton Sauvegarder ne reste plus bloque sur le spinner (BUG-054) --- CHANGELOG.md | 19 ++++++++++++++++++- README.fr.md | 6 +++--- README.md | 6 +++--- VERSION | 2 +- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ISSUES_TODOLIST.md | 4 +++- docs/ROADMAP.md | 2 +- frontend/js/utils.js | 25 +++++++++++++++++++++++++ package.json | 2 +- tests/frontend/editor-inline.test.mjs | 25 +++++++++++++++++++++++++ 12 files changed, 83 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72753d2..ab9bc3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.7.4**. +> [Unreleased](#unreleased). La dernière version livrée est **2.7.5**. --- @@ -14,6 +14,23 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [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é diff --git a/README.fr.md b/README.fr.md index 8225693..96de4e6 100644 --- a/README.fr.md +++ b/README.fr.md @@ -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.7.4-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.7.5-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/) @@ -916,8 +916,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.7.4). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.7.5). --- -*Projet : ObsiGate | Version : 2.7.4 | Dernière mise à jour : Juin 2026* +*Projet : ObsiGate | Version : 2.7.5 | Dernière mise à jour : Juin 2026* diff --git a/README.md b/README.md index ddf63bc..a533704 100644 --- a/README.md +++ b/README.md @@ -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.7.4-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.7.5-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/) @@ -1085,8 +1085,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.7.4). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.7.5). --- -*Project: ObsiGate | Version: 2.7.4 | Last updated: May 2026* +*Project: ObsiGate | Version: 2.7.5 | Last updated: May 2026* diff --git a/VERSION b/VERSION index a4dd9db..a603bb5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.4 +2.7.5 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index f7c2152..f81583f 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.7.4" +version = "2.7.5" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index e4f5090..608fb41 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.7.4" +version = "2.7.5" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index d3987c5..6ef476a 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.7.4", + "version": "2.7.5", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index 636d923..3e8be55 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -14,7 +14,7 @@ - **Projet** : ObsiGate — Porte d'entrée web pour vaults Obsidian - **Stack** : Python 3.11+ (backend FastAPI) · JavaScript/Vanilla (frontend) · Tauri/Rust (desktop) -- **Dernière mise à jour** : 2026-09-16 +- **Dernière mise à jour** : 2026-09-17 --- @@ -163,6 +163,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | *BUG-051* | [🟡 IMPORTANT] Assistant IA : la recherche web répond toujours « je ne peux pas accéder à internet » (mode agent ou non) | 🟢 corrigé | P1 | ⚙️ backend + 🤖 ia | IA | `backend/tools/web.py`, `tests/test_web_tools.py` | Assistant : « fais une recherche sur l'horaire du Canadien de Montréal 2026-2027 » | `backend/tools/web.py` : chaîne de repli sans clé — SearXNG puis DuckDuckGo (HTML sans JS) puis Bing (HTML), premier fournisseur non vide retenu (`provider`), replis désactivables via `OBSIGATE_WEB_FALLBACK=0` | Cause : l'instance SearXNG par défaut (`search.dracodev.net`) remonte 0 résultat (moteurs amont suspendus/CAPTCHA) → le modèle en déduisait une absence d'accès réseau. Tests : `tests/test_web_tools.py` (+4) | | *BUG-052* | [🟡 IMPORTANT] Assistant IA : recherche web sans réponse finale (10 étapes + sources affichées, aucun texte dans la conversation) | 🟢 corrigé | P1 | ⚙️ backend + 🤖 ia | IA | `backend/agent/loop.py`, `tests/test_agent_loop.py` | Assistant (mode agent) : recherche web qui enchaîne 10 étapes puis n'affiche aucune réponse | `backend/agent/loop.py` : `_finalize_answer` — dernier appel LLM sans outil (instruction de synthèse) quand le budget d'itérations/quota est épuisé, repli déterministe `_fallback_summary` (liste des sources), résultats `deferred` pour les appels non atteints du lot en quota | Cause : `content=""` renvoyé sur `STOP_MAX_ITERATIONS`/`STOP_QUOTA_EXCEEDED` alors que le modèle appelait encore des outils. Tests : `tests/test_agent_loop.py` (+2) | | *BUG-053* | [🟡 IMPORTANT] Assistant IA (mode agent) : le fichier demandé n'est pas créé — le modèle émet un bloc texte `obsigate-action` au lieu d'appeler l'outil `create_file` | 🟢 corrigé | P1 | ⚙️ backend + 🤖 ia | IA | `backend/bookslm.py`, `backend/bookslm_routes.py`, `tests/test_bookslm.py` | Mode agent, contexte Général (ou dossier vide) : « créer le fichier TestVault/sport/… avec le tableau des 84 matchs » → réponse avec un bloc ```obsigate-action``` tronqué, aucun fichier | `backend/bookslm.py` : protocole d'action scindé — `GENERAL_ACTION_TOOL_PROTOCOL` (outils natifs, interdiction des blocs `obsigate-action`) utilisé quand `agent=True`, protocole texte conservé pour le chat classique ; `backend/bookslm_routes.py` : `_resolve_system_prompt(..., agent=True)` depuis l'endpoint agent + règle « Mode agent » pour les prompts dossier/documents, `max_tokens` agent 4096 → 8192 (contenu de fichier complet) | Cause : le prompt Général enseignait encore le protocole texte alors que l'agent dispose du function calling. Tests : `tests/test_bookslm.py` (+3) | +| *BUG-054* | [🟡 IMPORTANT] Éditeur « Editer » : le bouton Sauvegarder reste bloqué sur le spinner de chargement (retour au crochet uniquement après un refresh complet) | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/utils.js` | Ouvrir un fichier → Editer → cliquer Sauvegarder (ou Ctrl+S) ; rouvrir l'éditeur : le bouton reste un spinner désactivé | Nouveau helper `resetSaveButton()` (crochet `✓` + `disabled=false` + styles en ligne nettoyés) appelé à l'ouverture (`openEditor`), à la fermeture (`closeEditor`) et en cas d'échec (`saveFile`). Tests : `tests/frontend/editor-inline.test.mjs` (+4) | Le nœud `#editor-save` est partagé entre sessions : l'état « spinner + désactivé » posé par une sauvegarde manuelle n'était jamais remis à zéro (succès → fermeture puis réouverture, Forge, ou échec réseau dans le `catch`). Seul un rechargement de `index.html` restaurait le crochet | | | | | | | | | | | | | ### TODOs techniques (améliorations / nouvelles tâches) @@ -217,6 +218,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | 2026-09-16 | BUG-051 (complément) | Correction | `backend/tools/web.py`, `CHANGELOG.md` | **BUG-051** suite : un `User-Agent` navigateur seul ne suffit pas — Bing renvoie une SERP factice (résultats sans rapport, ex. « highest paying jobs » / « Sam Reid ») aux requêtes sans en-têtes de navigation. Ajout de `BROWSER_HEADERS` (`Accept-Language`, `Sec-Fetch-*`, `Upgrade-Insecure-Requests`) pour DuckDuckGo et Bing. Vérifié **en conteneur** (`obsigate-test`, v2.7.1) : `web_search('Canadien de Montreal horaire matchs 2026 2027')` → `provider: bing`, 5 résultats pertinents (nhl.com/fr/canadiens, rds.ca, fr.wikipedia.org). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-052 | Correction | `backend/agent/loop.py`, `tests/test_agent_loop.py`, `CHANGELOG.md` | **BUG-052** : la boucle d'agent ne rendait plus jamais de réponse vide. `_finalize_answer` : à l'épuisement du budget d'itérations (`STOP_MAX_ITERATIONS`) ou du quota d'appels (`STOP_QUOTA_EXCEEDED`), un dernier appel LLM **sans outil** reçoit une instruction de synthèse (« N'appelle plus aucun outil. Réponds maintenant… ») et son texte devient la réponse ; si l'appel échoue ou reste vide, `_fallback_summary` compose une liste déterministe des sources (`web_search`/`fetch_url`) pour ne jamais renvoyer un tour vide. Les `tool_calls` non atteints lors d'un arrêt sur quota reçoivent un résultat `deferred` (conversation valide pour la synthèse). Vérifié : `tests/test_agent_loop.py` 16/16 (+2 : synthèse finale, repli sources), suite complète 1084 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (`backend/agent/loop.py`). | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-16 | BUG-053 | Correction | `backend/bookslm.py`, `backend/bookslm_routes.py`, `tests/test_bookslm.py`, `CHANGELOG.md` | **BUG-053** : en mode agent, le prompt Général (et dossier vide) enseignait le protocole texte `obsigate-action` ; le modèle décrivait donc l'action au lieu d'appeler l'outil natif `create_file` (bloc volumineux de surcroît tronqué avant fermeture → aucun fichier créé). Le prompt est scindé : `GENERAL_ACTION_TOOL_PROTOCOL` (appel direct des outils natifs, interdiction explicite des blocs `obsigate-action`) pour `build_general_system_prompt(agent=True)`, le protocole texte restant utilisé par le chat classique ; `_resolve_system_prompt` propage `agent` et ajoute une règle « Mode agent » aux prompts dossier/documents ; `max_tokens` de l'agent porté à 8192 pour un contenu de fichier complet. Vérifié : `tests/test_bookslm.py` 68/68 (+3 : prompt agent sans protocole texte, prompt classique inchangé, prompt système de l'endpoint agent), suite complète 1087 passed / 6 skipped (14 erreurs MCP préexistantes), ruff 0 (backend), mypy 0 (`backend/bookslm.py`, `backend/bookslm_routes.py`). | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-09-17 | BUG-054 | Correction | `frontend/js/utils.js`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-054** : le bouton `#editor-save` (nœud partagé entre toutes les sessions d'édition) restait bloqué sur le spinner de chargement et désactivé — une sauvegarde manuelle (clic ou Ctrl+S) remplaçait le crochet par le loader et ne le restaurait jamais : succès (l'éditeur se ferme, la réouverture réaffichait le spinner), sauvegarde Forge, ou échec réseau (le `catch` ne restaurait ni l'icône ni l'état). Nouveau helper `resetSaveButton()` (crochet `✓`, `disabled=false`, styles en ligne nettoyés) appelé à l'ouverture (`openEditor`), à la fermeture (`closeEditor`) et en cas d'échec (`saveFile`). Vérifié : `tests/frontend/editor-inline.test.mjs` 29/29 (+4), validate-imports 38 modules / 0 erreur. | 🟢 corrigé (en attente vérif utilisateur) | --- diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e4ee5bf..1a778ac 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.7.4 | **Dernière mise à jour :** 2026-09-17 +> **Version :** 2.7.5 | **Dernière mise à jour :** 2026-09-17 > **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)** diff --git a/frontend/js/utils.js b/frontend/js/utils.js index a41b6d8..7b11b8c 100644 --- a/frontend/js/utils.js +++ b/frontend/js/utils.js @@ -365,6 +365,9 @@ function escapeHtml(str) { async function openEditor(vaultName, filePath) { state.editorVault = vaultName; state.editorPath = filePath; + // A previous session may have left the shared save button in its spinner + // state (manual save, Forge, failed request). BUG-054. + resetSaveButton(); const modal = document.getElementById("editor-modal"); const titleInput = document.getElementById("editor-title-input"); @@ -582,6 +585,7 @@ function closeEditor() { const modal = document.getElementById("editor-modal"); if (!modal) return; modal.classList.remove("active"); + resetSaveButton(); stopCollab(); if (state.editorView) { state.editorView.destroy(); @@ -683,6 +687,26 @@ async function reloadExternalWrite(vault, path) { } } +/** + * Restore the save button to its idle state (checkmark, enabled). + * + * The button (`#editor-save`) is a single shared DOM node reused across every + * edition session, including inline mode where it travels with the editor + * container. A manual save swaps its content for a spinner and disables it; if + * that state is not cleared on success/failure, the spinner leaks into the next + * session and only a full page reload (which re-parses index.html) brings the + * checkmark back. BUG-054. + */ +function resetSaveButton() { + const saveBtn = document.getElementById("editor-save"); + if (!saveBtn) return; + saveBtn.disabled = false; + saveBtn.innerHTML = '✓'; + saveBtn.style.background = ''; + saveBtn.style.color = ''; + saveBtn.style.borderColor = ''; +} + async function saveFile(silent = false) { // If Forge is open, delegate save to the iframe var forgeFrame = document.getElementById("forge-iframe"); @@ -750,6 +774,7 @@ async function saveFile(silent = false) { } } catch (err) { console.error("Save error:", err); + resetSaveButton(); if (saveDot) { saveDot.className = 'editor-save-dot err'; } if (saveLabel) saveLabel.textContent = 'Erreur'; // If offline, queue the save for later sync diff --git a/package.json b/package.json index 29d6d0b..c04f517 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.7.4", + "version": "2.7.5", "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": { diff --git a/tests/frontend/editor-inline.test.mjs b/tests/frontend/editor-inline.test.mjs index b86b09d..857ed3b 100644 --- a/tests/frontend/editor-inline.test.mjs +++ b/tests/frontend/editor-inline.test.mjs @@ -356,6 +356,31 @@ test("style.css: #editor-body scrolls through the CodeMirror scroller only", () "global .cm-scroller min-height override reintroduces the double scrollbar"); }); +// ── BUG-054: the shared save button must not stay stuck on the spinner ── +test("utils.js resetSaveButton restores the checkmark and re-enables the button", () => { + const fn = utilsSrc.match(/function resetSaveButton\(\) \{([\s\S]*?)\n\}/); + assert.ok(fn, "resetSaveButton not found"); + assert.match(fn[1], /saveBtn\.disabled = false;/); + assert.match(fn[1], /saveBtn\.innerHTML = '✓'/); + assert.match(fn[1], /saveBtn\.style\.background = '';/); +}); + +test("utils.js openEditor resets the save button before each session", () => { + const fn = utilsSrc.match(/async function openEditor\(vaultName, filePath\) \{([\s\S]*?)\n\}/); + assert.ok(fn, "openEditor not found"); + assert.match(fn[1], /resetSaveButton\(\);/); +}); + +test("utils.js closeEditor resets the save button on success/cancel/delete", () => { + const fn = utilsSrc.match(/function closeEditor\(\) \{([\s\S]*?)\n\}/); + assert.ok(fn, "closeEditor not found"); + assert.match(fn[1], /resetSaveButton\(\);/); +}); + +test("utils.js saveFile restores the save button when the request fails", () => { + assert.match(utilsSrc, /console\.error\("Save error:", err\);\s*\n\s*resetSaveButton\(\);/); +}); + console.log(`\n${testCount - failCount}/${testCount} tests passed`); if (failCount > 0) { console.error(`${failCount} test(s) failed`);