Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0841778b99 | ||
|
|
788d84a2bf | ||
|
|
168261964e | ||
|
|
2f129c3f23 |
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](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*
|
||||
|
||||
@@ -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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](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*
|
||||
|
||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.3.0"
|
||||
version = "2.3.4"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"env_logger",
|
||||
|
||||
+1
-1
@@ -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,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",
|
||||
|
||||
@@ -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` :
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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}")
|
||||
@@ -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 \{([^}]*)\}/);
|
||||
|
||||
Reference in New Issue
Block a user