Compare commits

...
4 Commits
Author SHA1 Message Date
bruno 0841778b99 docs(agents): versionnage automatique documente dans AGENTS.md + menage des fichiers temporaires
CI / lint (push) Successful in 1m26s
CI / security (push) Successful in 57s
CI / test (push) Successful in 2m16s
CI / build (push) Successful in 53s
CI / e2e (push) Successful in 10m40s
AGENTS.md : la checklist de fin de tache rappelle que VERSION est la source unique de verite, incrementee a chaque commit par le hook, avec resynchronisation des derives et publication du tag au push ; ligne ajoutee a la cartographie documentaire. Suppression des captures de diagnostic (diag-*.png, ogdiag.png, state.png) et du script jetable tmp-verify-inline.cjs.
2026-09-16 12:12:06 -04:00
bruno 788d84a2bf fix(editeur): #93 garde-fous de session d'edition inline (onglets, ecriture externe) + hooks de versionnage documentes
CI / lint (push) Successful in 1m27s
CI / security (push) Successful in 57s
CI / test (push) Successful in 2m52s
CI / build (push) Successful in 53s
CI / e2e (push) Successful in 10m45s
Une session d'edition inline (Editer/Forge) qui possede la zone de contenu etait detruite par deux chemins qui vident cette zone : activation d'onglet (TabManager.activate, PaneTabManager.activate) -> detachInlineEditor() avant le placeholder ; evenement SSE index_updated -> reloadExternalWrite() recharge le tampon de l'editeur au lieu de re-rendre la vue lecture. Nouveau helper queryEditor() ; closeEditor() remet le conteneur dans la modale avant de restaurer en-tete/pied/marque. docs/CONTRIBUTING.md documente scripts/install-hooks.sh.
2026-09-16 11:43:41 -04:00
bruno 168261964e docs(version): preciser le SHA reecrit par l'amend du hook post-commit
CI / lint (push) Successful in 1m27s
CI / security (push) Successful in 57s
CI / test (push) Successful in 3m27s
CI / build (push) Successful in 53s
CI / e2e (push) Successful in 10m59s
2026-09-16 11:20:29 -04:00
bruno 2f129c3f23 chore(scripts): outil de controle du CI Gitea (runs et jobs par head_sha)
scripts/check_ci.py : interroge l'API Gitea (jeton via git credential fill, User-Agent navigateur) et rapporte l'etat des jobs du run correspondant au HEAD local. Bump automatique de version = demonstration du hook prepare-commit-msg.
2026-09-16 11:20:13 -04:00
23 changed files with 274 additions and 38 deletions
+9
View File
@@ -23,6 +23,14 @@ Aucune tâche n'est terminée avant que sa checklist soit complète **et le CI v
(statut + index), fiche `docs/features/` **ou** `docs/archive/`, `docs/ISSUES_TODOLIST.md`,
guide utilisateur i18n FR/EN + README si impact utilisateur.
- **Commit** conventionnel référençant l'ID, puis **push**.
- Version : le fichier VERSION (racine du dépôt) est la **source unique de
vérité (MAJEUR.MINEUR.CORRECTIF), incrémenté automatiquement à chaque commit** par le hook
.githooks/prepare-commit-msg — feat → mineur, !: / BREAKING CHANGE → majeur, sinon
correctif. Le même commit resynchronise package.json, le desktop Tauri, README.md/
README.fr.md, docs/ROADMAP.md et publie la section [Unreleased] du CHANGELOG.md en
[X.Y.Z] — date ; le tag vX.Y.Z est créé au commit et publié au push (push.followTags).
Hooks à installer une fois par clone : scripts/install-hooks.sh. Garde-fou :
tests/test_version.py (détail : docs/DELIVERY_WORKFLOW.md §7).
- Vérifier le **CI Gitea vert** (jobs `lint`, `test`, `security`, `build`, `e2e`).
## Cartographie documentaire
@@ -30,6 +38,7 @@ Aucune tâche n'est terminée avant que sa checklist soit complète **et le CI v
| Sujet | Fichier |
|---|---|
| Méthode de livraison / DoD | `docs/DELIVERY_WORKFLOW.md` |
| Version livrée (source unique) | VERSION + scripts/bump_version.py |
| Travail à venir + index | `docs/ROADMAP.md` |
| Historique des versions | `CHANGELOG.md` |
| Conception par feature | `docs/features/<slug>.md` |
+47 -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.3.0**.
> [Unreleased](#unreleased). La dernière version livrée est **2.3.4**.
---
@@ -14,6 +14,52 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [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é
+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.3.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.3.4-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.3.0).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.3.4).
---
*Projet : ObsiGate | Version : 2.3.0 | Dernière mise à jour : Juin 2026*
*Projet : ObsiGate | Version : 2.3.4 | Dernière mise à jour : Juin 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.3.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.3.4-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.3.0).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.3.4).
---
*Project: ObsiGate | Version: 2.3.0 | Last updated: May 2026*
*Project: ObsiGate | Version: 2.3.4 | Last updated: May 2026*
+1 -1
View File
@@ -1 +1 @@
2.3.0
2.3.4
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.3.0"
version = "2.3.4"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.3.0"
version = "2.3.4"
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.3.0",
"version": "2.3.4",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+9
View File
@@ -30,8 +30,17 @@ source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
pip install -r backend/requirements.txt
# Activer les hooks git versionnés (.githooks) : incrément automatique de la
# version livrée (VERSION) à chaque commit + publication du tag vX.Y.Z au push.
scripts/install-hooks.sh
```
> **Hooks obligatoires** : `scripts/install-hooks.sh` pose `core.hooksPath=.githooks` et
> `push.followTags=true`. Sans eux, la version (`VERSION`) ne suit plus les livraisons et le
> CI reste sur un numéro obsolète. Détail : [DEVELOPMENT_AND_RELEASES.md](./DEVELOPMENT_AND_RELEASES.md) §3.
> Contournement ponctuel d'un commit : `SKIP_VERSION_BUMP=1 git commit …`.
### 2. Configurer les vaults de test
Créez un dossier `test_vault/` (ignoré par `.gitignore`) avec quelques fichiers `.md` :
+3
View File
@@ -134,6 +134,9 @@ npx playwright test
diverge de `VERSION` (numéro codé en dur, CHANGELOG sans section, README non resynchronisé).
- Bump manuel si besoin : `scripts/bump_version.py --dry-run|--minor|--set X.Y.Z|--push`.
Commit sans incrément (cas exceptionnel) : `SKIP_VERSION_BUMP=1 git commit …`.
Le rattachement des fichiers au commit se fait par un `--amend` immédiat (commit non encore
poussé) : le SHA affiché par `git commit` est donc remplacé par celui de l'amend — `git log`,
`HEAD` et le tag `vX.Y.Z` restent, eux, alignés sur la version livrée.
- **Ne jamais** réécrire une version déjà publiée dans le CHANGELOG.
---
+5
View File
@@ -210,6 +210,11 @@ git push origin main # → branche + tag publiés
Contournement ponctuel (commit sans incrément) : `SKIP_VERSION_BUMP=1 git commit …`.
> **SHA affiché vs SHA réel** : l'incrément et les fichiers dérivés sont rattachés au
> commit par un `--amend` immédiat (le commit n'est pas encore poussé), donc le SHA
> affiché par `git commit` est remplacé par celui de l'amend — `git log`, `HEAD` et le
> tag `vX.Y.Z` sont, eux, alignés sur ce dernier. `git push` envoie bien la version finale.
### Bump manuel / outillage
```bash
+2 -1
View File
@@ -200,7 +200,8 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-09-14 | BUG-043, #82 | Correction | `frontend/js/ai.js`, `frontend/js/bookslm.js`, `frontend/js/config.js`, `tests/frontend/ai.test.mjs`, `docs/features/ai-provider-picker.md` | La liste des fournisseurs de la barre latérale de l'assistant suit désormais la configuration du projet : nouveau `refreshAIPickers()` (`ai.js`) qui reconstruit chaque picker monté dans son emplacement `.ai-picker-slot` (host conservé dans la barre de l'assistant, montage par `replaceChildren`), appelé après `saveAIKeys()` et `deleteAIKey()` (`config.js`) ; slot préservé même sans fournisseur configuré (le premier fournisseur ajouté s'y monte) ; sélection persistée d'un fournisseur non configuré purgée de `obsigate_ai_picker` (retour au défaut, modèle effacé). Vérifié : tests frontend 66/66 (IA) + 9 suites JSDOM vertes, validate-imports 36 modules, pytest 963 passed / 6 skipped, ruff OK, et vérification en navigateur (Playwright) sur l'instance de test : ajout de `nvidia` → fournisseur visible sans rechargement, retrait → disparition + sélection réinitialisée (`{"provider":null,"model":null}`), un seul montage du picker dans son host. CI Gitea verte (lint, test, security, build, e2e). |
| 2026-09-15 | BUG-044 | Correction | `backend/provider_capabilities.py` (nouveau), `backend/model_capabilities.py`, `backend/main.py`, `backend/ai_routes.py`, `tests/test_provider_capabilities.py` (nouveau), `tests/test_model_capabilities.py`, `tests/test_ai_models.py`, `docs/features/ai-provider-picker.md`, `CHANGELOG.md` | BUG-044 : les capacités des modèles IA sont désormais lues chez le fournisseur quand il les publie (Mistral `capabilities`, OpenRouter `architecture`, détection par forme du payload), mises en cache par `GET /api/config/ai-models` (aucune requête supplémentaire) et prioritaires sur la table statique qui ne comble plus que les drapeaux non déclarés (repli hors ligne / sans clé). Table statique corrigée : familles vision Mistral (`ministral`, `magistral`, `mistral-small`, `mistral-medium`, `mistral-vibe-cli`, `labs-leanstral`), `mistral-ocr` = vision sans chat, défaut fournisseur Mistral sans `embeddings` (mistral-large / codestral n'étaient plus des « embedders »). Vérifié : diagnostic live avant/après sur l'API Mistral (28 modèles vision déclarés, **0** détectés avant → **28** après, 0 écart dans les deux sens), pytest 1007 passed / 6 skipped, ruff 0 (backend), mypy 0 (68 fichiers), tests frontend (unit 9/9, IA 66/66, validate-imports 37 modules), et vérification sur l'instance de test reconstruite (`obsigate-test`, http://localhost:2020) via les deux endpoints du panneau : `mistral-small/medium-latest`, `ministral-8b-latest`, `magistral-small-latest` → `chat,vision` ; `mistral-ocr-latest` → `vision` seul ; `mistral-large-latest`/`codestral-latest` → `chat` ; `mistral-embed` → `embeddings` seul ; `deepseek-chat` (fournisseur muet) inchangé. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-16 | BUG-045, BUG-046 | Correction | `frontend/style.css`, `frontend/js/bookslm.js`, `backend/bookslm.py`, `backend/bookslm_routes.py`, `frontend/sw.js`, `tests/test_bookslm.py`, `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs`, `CHANGELOG.md` | BUG-045 : fin de la double barre de défilement de l'éditeur « Editer » — `#editor-body` en flex column `overflow:hidden`, `.cm-editor` `flex:1 1 auto`, seul le scroller CodeMirror défile ; suppression de l'override global `.cm-scroller` (min-height/overflow-y !important). BUG-046 : « Échec de l'action : [object Object] » à l'application d'un ajout de texte au document courant — (1) `_responseError()` aplatit le `detail` 422 (tableau d'objets FastAPI) en message lisible, (2) la continuation de confirmation hérite du payload d'origine (le second « Appliquer » ne POSTe plus sans `message`), (3) le prompt documents/dossier nomme le vault et enjoint les outils d'écriture d'utiliser ce nom exact (le modèle inventait `"vault":"test"` → échec silencieux). `SW_VERSION` v20. Vérifié : pytest 1038 passed / 6 skipped, ruff 0 (backend), tests frontend IA 84/84, editor-inline 25/25, validate-imports 38 modules ; reproduction et validation Playwright sur l'instance de test (un seul conteneur scrollable après correctif). | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-16 | BUG-047 | Correction + build | `VERSION` (nouveau), `scripts/bump_version.py` (nouveau), `.githooks/prepare-commit-msg` + `.githooks/post-commit` (nouveaux), `scripts/install-hooks.sh` (nouveau), `scripts/bump_version.sh`, `backend/version.py`, `Dockerfile`, `docker-compose.yml`, `build.sh`, `.gitea/workflows/ci.yml`, `desktop/build.rs`, `package.json`, `README.md`, `README.fr.md`, `docs/ROADMAP.md`, `docs/DELIVERY_WORKFLOW.md`, `docs/DEVELOPMENT_AND_RELEASES.md`, `tests/test_version.py` (nouveau) | **Version alignée de bout en bout.** Cause : la version affichée venait du dernier **tag git** et aucun tag n'était créé aux livraisons → 66 commits livrés mais UI/API figées sur `2.2.1` ; quatre autres numéros codés en dur dérivaient (`package.json` 1.0.0, desktop 2.0.0, `Dockerfile` 2.2.1, README 1.7.0). Nouveau modèle : **`VERSION` (racine) = source unique de vérité** (`MAJEUR.MINEUR.CORRECTIF`), incrémentée **automatiquement au commit** par le hook versionné `prepare-commit-msg` (SemVer depuis le message : `!:`/`BREAKING CHANGE` → MAJEUR, `feat` → MINEUR, sinon CORRECTIF ; aucun bump pour merge/revert/`chore(release)`/amend) qui resynchronise `package.json`, desktop Tauri, ROADMAP, READMEs et publie la section `[Unreleased]` du CHANGELOG en `[X.Y.Z] — date` ; `post-commit` rattache ces fichiers au commit qui vient d'être créé (amend immédiat, le commit n'étant pas encore poussé) puis crée le tag `vX.Y.Z`, publié au push (`push.followTags`). `bump_version.py` (avec `--dry-run`, `--set`, `--major|--minor|--patch`, `--print-version`) reste utilisable à la main ; `scripts/install-hooks.sh` installe les hooks. Côté build : Docker `COPY VERSION` (plus d'`ARG VERSION` ni d'env compose codés en dur), `build.sh` et CI lisent `VERSION`, `desktop/build.rs` aussi. Vérifié : `tests/test_version.py` (46 tests, dont le garde-fou `TestRepoVersionAlignment` : VERSION ↔ package.json ↔ desktop ↔ CHANGELOG ↔ ROADMAP ↔ READMEs ↔ pipeline sans numéro codé en dur), pytest complet, ruff/mypy 0, `/api/health` → `2.3.0` sur l'instance de test reconstruite. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-16 | #93 (complément) | Correction | `frontend/js/editor-inline.js`, `frontend/js/utils.js`, `frontend/js/viewer.js`, `frontend/js/ui.js`, `frontend/js/pane-manager.js`, `frontend/js/sync.js`, `tests/frontend/editor-inline.test.mjs`, `docs/features/editeur-inline.md`, `docs/CONTRIBUTING.md`, `CHANGELOG.md` | Garde-fous de **session d'édition inline** (#93) : l'activation d'un onglet (`TabManager.activate`, `PaneTabManager.activate`) appelle `detachInlineEditor()` avant de vider la zone de contenu, et l'événement SSE `index_updated` sur le fichier affiché recharge le **tampon de l'éditeur** (`reloadExternalWrite()`) au lieu de re-rendre la vue lecture — ces deux chemins détruisaient la session CodeMirror/Forge en cours. Nouveau helper `queryEditor()` (l'en-tête/pied/marque voyagent avec le conteneur en mode inline, `modal.querySelector()` les manquait) ; `closeEditor()` remet le conteneur dans la modale avant de restaurer en-tête/pied/marque. `docs/CONTRIBUTING.md` documente `scripts/install-hooks.sh` (hooks de versionnage) dans la mise en place d'un clone. Vérifié : validate-imports 38 modules, unit 9/9, suites JSDOM (editor-inline 25/25, pane-manager, mobile-editor 35/35, toolbar-order, ai 84/84, sw 8/8, collab 10/10, semantic-search 4/4, desktop 23, plugins 21, excalidraw-viewer) toutes vertes, pytest 1082 passed / 6 skipped. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-16 | BUG-047 | Correction + build | `VERSION` (nouveau), `scripts/bump_version.py` (nouveau), `.githooks/prepare-commit-msg` + `.githooks/post-commit` (nouveaux), `scripts/install-hooks.sh` (nouveau), `scripts/bump_version.sh`, `backend/version.py`, `Dockerfile`, `docker-compose.yml`, `build.sh`, `.gitea/workflows/ci.yml`, `desktop/build.rs`, `package.json`, `README.md`, `README.fr.md`, `docs/ROADMAP.md`, `docs/DELIVERY_WORKFLOW.md`, `docs/DEVELOPMENT_AND_RELEASES.md`, `tests/test_version.py` (nouveau) | **Version alignée de bout en bout.** Cause : la version affichée venait du dernier **tag git** et aucun tag n'était créé aux livraisons → 66 commits livrés mais UI/API figées sur `2.2.1` ; quatre autres numéros codés en dur dérivaient (`package.json` 1.0.0, desktop 2.0.0, `Dockerfile` 2.2.1, README 1.7.0). Nouveau modèle : **`VERSION` (racine) = source unique de vérité** (`MAJEUR.MINEUR.CORRECTIF`), incrémentée **automatiquement au commit** par le hook versionné `prepare-commit-msg` (SemVer depuis le message : `!:`/`BREAKING CHANGE` → MAJEUR, `feat` → MINEUR, sinon CORRECTIF ; aucun bump pour merge/revert/`chore(release)`/amend) qui resynchronise `package.json`, desktop Tauri, ROADMAP, READMEs et publie la section `[Unreleased]` du CHANGELOG en `[X.Y.Z] — date` ; `post-commit` rattache ces fichiers au commit qui vient d'être créé (amend immédiat, le commit n'étant pas encore poussé) puis crée le tag `vX.Y.Z`, publié au push (`push.followTags`). `bump_version.py` (avec `--dry-run`, `--set`, `--major\|--minor\|--patch`, `--print-version`) reste utilisable à la main ; `scripts/install-hooks.sh` installe les hooks. Côté build : Docker `COPY VERSION` (plus d'`ARG VERSION` ni d'env compose codés en dur), `build.sh` et CI lisent `VERSION`, `desktop/build.rs` aussi. Vérifié : `tests/test_version.py` (46 tests, dont le garde-fou `TestRepoVersionAlignment` : VERSION ↔ package.json ↔ desktop ↔ CHANGELOG ↔ ROADMAP ↔ READMEs ↔ pipeline sans numéro codé en dur), pytest complet, ruff/mypy 0, `/api/health` → `2.3.0` sur l'instance de test reconstruite. | 🟢 corrigé (en attente vérif utilisateur) |
---
+1 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.3.0 | **Dernière mise à jour :** 2026-09-16
> **Version :** 2.3.4 | **Dernière mise à jour :** 2026-09-16
> **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)**
+2
View File
@@ -59,6 +59,8 @@ d'édition mobile `.me-ribbon`, ancré à la modale, conserve `pointer-events: a
| ✓ (Sauvegarder) | sauvegarde puis `closeEditor()` → retour à la lecture (contenu relu depuis le disque) |
| ✕ (Annuler) / **Échap** | `closeEditor()` → retour à la lecture (les modifications non sauvegardées sont abandonnées, comme avant) |
| Ouverture d'un autre fichier (`renderFile()`) | garde-fou : `detachInlineEditor()` libère la session (destroy CodeMirror/Yjs, retrait de l'iframe Forge) avant le rendu de la lecture |
| Activation d'un onglet (`TabManager.activate`, `PaneTabManager.activate`) | même garde-fou **avant** le placeholder de chargement : ces fonctions vident la zone de contenu, ce qui détruirait le DOM de l'éditeur |
| Événement SSE `index_updated` sur le fichier affiché | `reloadExternalWrite()` recharge le **tampon de l'éditeur** depuis le disque au lieu de re-rendre la vue lecture (sans quoi la modification externe détruisait la session) |
| `forge-close` (message de l'iframe) | route vers `closeEditor()` — plus de manipulation manuelle de la modale dans `sync.js` |
**Repli overlay** : si la cible n'est pas le document affiché (fichier venant d'être créé via la
+11
View File
@@ -69,6 +69,17 @@ export function isInlineEditorActive() {
return !!(container && container.classList.contains(INLINE_CONTAINER_CLASS));
}
/**
* Query an element inside the editor container, wherever it currently lives
* (modal or content area). Use it instead of `modal.querySelector(...)` for the
* header/footer/brand, which move with the container in inline mode (#93).
*/
export function queryEditor(selector) {
const container = getEditorContainer();
const scope = container || getEditorModal();
return scope ? scope.querySelector(selector) : null;
}
/**
* Move the editor container into `area`, replacing the rendered document.
* @returns {boolean} true when the container was mounted inline.
+4 -1
View File
@@ -10,7 +10,7 @@
*/
import { state } from './state.js';
import { api } from './auth.js';
import { escapeHtml, getFileIcon, safeCreateIcons } from './utils.js';
import { escapeHtml, getFileIcon, safeCreateIcons, detachInlineEditor } from './utils.js';
const PANE_MAX = 4;
@@ -154,6 +154,9 @@ function createPaneTabManager(paneId) {
async activate(tabId) {
if (this._activeTabId === tabId && this._tabs.length > 0) return;
// #93 — an inline edition session owns this pane's content area: release
// it before the load placeholder / read view wipes it off the DOM.
detachInlineEditor();
if (this._activeTabId && this._tabCache[this._activeTabId])
this._saveCurrentTabState();
+6 -2
View File
@@ -6,7 +6,7 @@ import { api, AuthManager } from './auth.js';
import { loadVaults, loadTags, refreshTagsForContext, refreshSidebarTreePreservingState } from './sidebar.js';
import { loadRecentFiles, initAboutModal } from './config.js';
import { openFile } from './viewer.js';
import { closeEditor } from './utils.js';
import { closeEditor, reloadExternalWrite } from './utils.js';
import { initOffline, syncFileIndexFromServer } from './offline.js';
import { initThemes } from './themes.js';
@@ -166,7 +166,11 @@ export const IndexUpdateManager = (() => {
if (state.currentVault && state.currentPath) {
const changed = (data.changes || []).some((c) => c.vault === state.currentVault && c.path === state.currentPath);
if (changed) {
openFile(state.currentVault, state.currentPath);
// #93 — an inline edition session (or Forge) owns the content area:
// re-render the read view below it would destroy the editor mid-
// session. Reload the editor buffer from disk instead (same path as
// an external AI write).
reloadExternalWrite(state.currentVault, state.currentPath);
}
}
} catch (err) {
+6 -2
View File
@@ -2,7 +2,7 @@
import { api, AuthManager, initLoginForm } from './auth.js';
import { state } from './state.js';
import { openFile, showWelcome, renderFile, el } from './viewer.js';
import { safeCreateIcons, getFileIcon, escapeHtml } from './utils.js';
import { safeCreateIcons, getFileIcon, escapeHtml, detachInlineEditor } from './utils.js';
import { syncActiveFileTreeItem, refreshSidebarTreePreservingState } from './sidebar.js';
import { GraphViewManager } from './graph.js';
import { DashboardConflictsWidget } from './dashboard.js';
@@ -2014,7 +2014,11 @@ export const TabManager = {
/** Activate a specific tab */
async activate(tabId) {
if (this._activeTabId === tabId && this._tabs.length > 0) return;
// #93 — an inline edition session owns this content area: release it before
// the load placeholder / read view wipes it off the DOM.
detachInlineEditor();
// Save current tab state
if (this._activeTabId && this._tabCache[this._activeTabId]) {
this._saveCurrentTabState();
+14 -13
View File
@@ -6,9 +6,11 @@ import { createAIToolbar } from './ai.js';
import { startCollab, stopCollab } from './collab.js';
import {
getInlineHostArea,
getEditorContainer,
mountEditorInline,
unmountEditorInline,
isInlineEditorActive,
queryEditor,
} from './editor-inline.js';
// ---------------------------------------------------------------------------
@@ -372,9 +374,9 @@ async function openEditor(vaultName, filePath) {
const bodyEl = document.getElementById("editor-body");
// Restore header/footer (may be hidden by Forge)
var headerEl = modal.querySelector(".editor-header");
var footerEl = modal.querySelector(".editor-footer");
var container = modal.querySelector(".editor-container");
var headerEl = queryEditor(".editor-header");
var footerEl = queryEditor(".editor-footer");
var container = getEditorContainer();
if (headerEl) headerEl.style.display = "";
if (footerEl) footerEl.style.display = "";
if (container) container.style.maxHeight = "";
@@ -580,14 +582,6 @@ function closeEditor() {
const modal = document.getElementById("editor-modal");
if (!modal) return;
modal.classList.remove("active");
// Restore brand text
var brand = modal.querySelector('.editor-brand');
if (brand) brand.textContent = 'ObsiGate';
// Restore header/footer (may be hidden by Forge)
var headerEl = modal.querySelector(".editor-header");
var footerEl = modal.querySelector(".editor-footer");
if (headerEl) headerEl.style.display = "";
if (footerEl) footerEl.style.display = "";
stopCollab();
if (state.editorView) {
state.editorView.destroy();
@@ -598,9 +592,16 @@ function closeEditor() {
// Remove forge iframe if present
var forgeFrame = document.getElementById("forge-iframe");
if (forgeFrame) forgeFrame.remove();
// #93 — give the editor container back to the modal; an inline session also
// releases the content area, whose read view must be re-rendered.
// #93 — hand the editor container back to the modal BEFORE restoring the
// header/footer/brand: they live inside the container and therefore travel
// with it in inline mode (a `modal.querySelector` would miss them).
const host = unmountEditorInline();
var brand = queryEditor('.editor-brand');
if (brand) brand.textContent = 'ObsiGate';
var headerEl = queryEditor(".editor-header");
var footerEl = queryEditor(".editor-footer");
if (headerEl) headerEl.style.display = "";
if (footerEl) footerEl.style.display = "";
_invalidateActiveTabCache(vault, path);
state.editorVault = null;
state.editorPath = null;
+4 -3
View File
@@ -2,7 +2,7 @@
import { api, AuthManager } from './auth.js';
import { state } from './state.js';
import { escapeHtml, safeCreateIcons, safeHighlight, getFileIcon, openEditor, copyToClipboard, activateInlineEditor, detachInlineEditor } from './utils.js';
import { isInlineEditorActive } from './editor-inline.js';
import { isInlineEditorActive, queryEditor } from './editor-inline.js';
import { TabManager, closeMobileSidebar, ContextMenuManager, RightSidebarManager, showToast, buildFrontmatterCard } from './ui.js';
import { syncActiveFileTreeItem, searchByTag, TagFilterService, refreshSidebarTreePreservingState, focusPathInSidebar } from './sidebar.js';
import { AutocompleteDropdown, performAdvancedSearch } from './search.js';
@@ -768,8 +768,9 @@ export function renderFile(data) {
state.editorVault = data.vault;
state.editorPath = data.path;
}
// Update header: show "Forge" brand + filename
var brand = modal.querySelector(".editor-brand");
// Update header: show "Forge" brand + filename (the header travels with the
// container in inline mode — query it wherever it lives, #93)
var brand = queryEditor(".editor-brand");
var titleInp = document.getElementById("editor-title-input");
var fileInp = document.getElementById("editor-filename-input");
if (brand) brand.textContent = t("viewer.forge_brand");
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.3.0",
"version": "2.3.4",
"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": {
+79
View File
@@ -0,0 +1,79 @@
"""Vérifie l'état du dernier run CI Gitea d'ObsiGate (lecture seule)."""
import json
import subprocess
import sys
import urllib.request
REPO = "Projets/ObsiGate"
BASE = f"https://git.dracodev.net/api/v1/repos/{REPO}"
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36"
cred = subprocess.run(
["git", "credential", "fill"],
input="protocol=https\nhost=git.dracodev.net\n\n",
capture_output=True,
text=True,
cwd=r"C:\dev\git\python\ObsiGate",
)
token = ""
for line in cred.stdout.splitlines():
if line.startswith("password"):
token = line.split("=", 1)[1]
if not token:
raise SystemExit("pas de jeton git disponible")
sha = subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
text=True,
cwd=r"C:\dev\git\python\ObsiGate",
).stdout.strip()
def get(path: str):
req = urllib.request.Request(
f"{BASE}{path}",
headers={
"Authorization": f"token {token}",
"Accept": "application/json",
"User-Agent": UA,
},
)
with urllib.request.urlopen(req, timeout=25) as resp:
return json.load(resp)
runs = get("/actions/runs?limit=10").get("workflow_runs", [])
print(f"HEAD local : {sha[:8]} — {len(runs)} run(s) récent(s)")
target = None
for run in runs:
head = str(run.get("head_sha") or "")
title = run.get("display_title") or run.get("name") or "?"
flag = " <== HEAD" if head.startswith(sha[:8]) else ""
print(f" #{run.get('id')} {title[:60]} | {run.get('status')}/{run.get('conclusion')} | {head[:8]}{flag}")
if head.startswith(sha[:8]) and target is None:
target = run
if target is None:
print("Aucun run pour le HEAD courant (encore en file d'attente ?)")
sys.exit(0)
jobs_resp = get(f"/actions/runs/{target['id']}/jobs")
print(f"run #{target['id']} : status={target.get('status')} conclusion={target.get('conclusion')} "
f"started={target.get('run_started_at')} updated={target.get('updated_at')}")
print(f"clés jobs: {list(jobs_resp)[:6]}")
for job in jobs_resp.get("workflow_jobs", jobs_resp.get("jobs", [])) or []:
print(f" - {job.get('name')}: {job.get('status')}/{job.get('conclusion')}")
if job.get("status") != "completed":
for step in job.get("steps", []) or []:
print(f" · {step.get('name')}: {step.get('status')}/{step.get('conclusion')}")
try:
runners = get("/actions/runners")
entries = runners.get("runners", runners) if isinstance(runners, dict) else runners
print(f"runners: {len(entries) if isinstance(entries, list) else entries}")
if isinstance(entries, list):
for runner in entries:
print(f" - {runner.get('name')} status={runner.get('status')} labels={runner.get('labels')}")
except Exception as exc: # noqa: BLE001
print(f"runners indisponibles: {exc}")
+61 -3
View File
@@ -67,6 +67,7 @@ const {
isInlineEditorActive,
mountEditorInline,
unmountEditorInline,
queryEditor,
} = mod;
let testCount = 0;
@@ -161,6 +162,17 @@ test("unmount without an inline session is a no-op", () => {
assert.equal(container().parentElement, modal());
});
test("queryEditor follows the container (modal → content area)", () => {
reset();
assert.equal(queryEditor(".editor-brand").textContent, "ObsiGate");
mountEditorInline(area());
assert.ok(queryEditor(".editor-brand"), "brand reachable while mounted inline");
assert.equal(document.getElementById("editor-modal").querySelector(".editor-brand"), null, "brand really left the overlay");
unmountEditorInline();
assert.ok(document.getElementById("editor-modal").querySelector(".editor-brand"), "brand back in the overlay");
reset();
});
test("host area follows the active pane in split view", () => {
reset();
const paneArea = document.createElement("div");
@@ -193,12 +205,37 @@ const forgeSrc = read("frontend", "editor-poc.html");
console.log("Inline edition (#93) — wiring");
test("utils.js / viewer.js import every editor-inline helper they call", () => {
const helpers = ["getEditorContainer", "getInlineHostArea", "isInlineEditorActive", "mountEditorInline", "unmountEditorInline", "queryEditor"];
const strip = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
for (const [name, raw] of [["utils.js", utilsSrc], ["viewer.js", viewerSrc]]) {
const src = strip(raw);
const marker = "from './editor-inline.js';";
const end = raw.indexOf(marker);
const start = end === -1 ? -1 : raw.lastIndexOf("import {", end);
const imported =
end === -1 || start === -1
? []
: raw
.slice(start + "import {".length, end)
.replace(/\}/g, "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
for (const helper of helpers) {
if (new RegExp(`\\b${helper}\\(`).test(src)) {
assert.ok(imported.includes(helper), `${name} calls ${helper}() without importing it`);
}
}
}
});
test("index.html gives the editor container a stable id", () => {
assert.match(indexSrc, /<div class="editor-container" id="editor-container">/);
});
test("viewer.js mounts the editor inline when rendering is taken over", () => {
assert.match(viewerSrc, /import \{ isInlineEditorActive \} from '\.\/editor-inline\.js';/);
assert.ok(viewerSrc.includes("import { isInlineEditorActive, queryEditor } from './editor-inline.js';"));
const guard = viewerSrc.match(/export function renderFile\(data\) \{([\s\S]*?)\n const area = getContentArea\(\);/);
assert.ok(guard, "renderFile head not found");
assert.match(guard[1], /if \(isInlineEditorActive\(\)\) detachInlineEditor\(\);/);
@@ -206,6 +243,8 @@ test("viewer.js mounts the editor inline when rendering is taken over", () => {
test("viewer.js Forge button mounts inline and sizes the iframe accordingly", () => {
assert.match(viewerSrc, /var inline = activateInlineEditor\(data\.vault, data\.path\);/);
assert.ok(viewerSrc.includes('var brand = queryEditor(".editor-brand");'), "header query must follow the container");
assert.equal(viewerSrc.includes('modal.querySelector(".editor-brand")'), false, "modal-scoped brand query would break inline mode");
assert.match(
viewerSrc,
/iframe\.style\.cssText = "width:100%;height:" \+ \(inline \? "100%" : "82vh"\) \+ ";border:none;display:block;";/,
@@ -245,12 +284,32 @@ test("utils.js reloads the displayed document after an AI write", () => {
});
test("sync.js forge-close goes through the shared close path", () => {
assert.match(syncSrc, /import \{ closeEditor \} from '\.\/utils\.js';/);
assert.ok(syncSrc.includes("import { closeEditor, reloadExternalWrite } from './utils.js';"), "utils import missing");
const handler = syncSrc.match(/if \(e\.data\.type === 'forge-close'\) \{([\s\S]*?)\n \}/);
assert.ok(handler, "forge-close handler not found");
assert.match(handler[1], /closeEditor\(\);/);
});
test("sync.js keeps an open edition session alive on external file changes", () => {
const sse = syncSrc.match(/const changed = \(data\.changes \|\| \[\]\)([\s\S]*?)\n \}/);
assert.ok(sse, "SSE index_updated refresh block not found");
assert.ok(sse[1].includes("reloadExternalWrite(state.currentVault, state.currentPath);"), "SSE refresh must not re-render the read view");
assert.ok(!/^\s*openFile\(state\.currentVault/m.test(sse[1]), "openFile must not be called directly here");
});
test("tab activation releases an inline edition session first", () => {
const uiActivate = read("frontend", "js", "ui.js").match(/async activate\(tabId\) \{([\s\S]*?)\n \},/);
assert.ok(uiActivate, "TabManager.activate not found");
assert.match(uiActivate[1], /detachInlineEditor\(\);/);
});
test("pane activation releases an inline edition session first", () => {
const paneSrc = read("frontend", "js", "pane-manager.js");
const paneActivate = paneSrc.match(/async activate\(tabId\) \{([\s\S]*?)\n const cache/);
assert.ok(paneActivate, "PaneTabManager.activate not found");
assert.match(paneActivate[1], /detachInlineEditor\(\);/);
});
test("bookslm.js reports the edited document and the assistant writes", () => {
assert.match(bookslmSrc, /const editing = this\._editingDocument\(\);/);
assert.match(bookslmSrc, /if \(editing\) ctx\.editing = editing;/);
@@ -281,7 +340,6 @@ test("style.css lets the inline editor fill the content area", () => {
assert.match(fill[1], /height: auto;/, "must win over the mobile 100vh rule");
assert.match(fill[1], /max-width: none;/);
});
// ── BUG-045: one scroll surface only in the CodeMirror editor ──
test("style.css: #editor-body scrolls through the CodeMirror scroller only", () => {
const body = cssSrc.match(/\.editor-body\.editor-body-cm \{([^}]*)\}/);