diff --git a/CHANGELOG.md b/CHANGELOG.md index 220da28..1f7360c 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.27.1**. +> [Unreleased](#unreleased). La dernière version livrée est **2.27.2**. --- @@ -14,6 +14,18 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.27.2] — 2026-09-26 + +### Modifié + +- **#85 (T1) — extraction du domaine `health` hors du monolithe `backend/main.py`.** + `GET /api/health` et `GET /api/health/detailed` (admin) sont servis par le + nouveau `backend/routers/health.py` (monté dans `main.py`) et le modèle + `HealthResponse` déménage dans `backend/schemas.py` — chemins, réponses, + tags OpenAPI et authentification inchangés (aucun impact utilisateur). + +--- + ## [2.27.1] — 2026-09-26 ### Modifié diff --git a/README.fr.md b/README.fr.md index 98cd017..635cf5f 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.27.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.27.2-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/) @@ -976,8 +976,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.27.1). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.27.2). --- -*Projet : ObsiGate | Version : 2.27.1 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.27.2 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index fdb2ecb..f7b2521 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.27.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.27.2-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/) @@ -1151,8 +1151,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.27.1). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.27.2). --- -*Project: ObsiGate | Version: 2.27.1 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.27.2 | Last updated: September 2026* diff --git a/VERSION b/VERSION index f046523..05abc55 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.27.1 +2.27.2 diff --git a/backend/main.py b/backend/main.py index a40dc06..b063989 100644 --- a/backend/main.py +++ b/backend/main.py @@ -398,19 +398,6 @@ class ReloadResponse(BaseModel): vaults: dict[str, Any] = Field(description="Per-vault file counts after reload") -class HealthResponse(BaseModel): - """Application health status.""" - status: str = Field(description="Health status ('ok' or 'error')") - version: str = Field(description="Application version (x.y.z — latest release tag)") - vaults: int = Field(description="Number of configured vaults") - total_files: int = Field(description="Total indexed files across all vaults") - total_tokens: int = Field(description="Total indexed tokens (approx.) across all vaults", default=0) - last_full_index_ts: str = Field(description="ISO timestamp of last full index rebuild", default="") - uptime_seconds: int = Field(description="Server uptime in seconds", default=0) - git_describe: str = Field(default="", description="Full git describe string (commits beyond tag), empty if no git") - git_commit: str = Field(default="", description="Short HEAD commit hash, empty if no git") - - class DirectoryCreateRequest(BaseModel): """Request to create a new directory.""" path: str = Field(description="Relative path of the new directory") @@ -867,7 +854,7 @@ async def lifespan(app: FastAPI): _search_executor = None -from backend.version import get_git_commit, get_git_describe, get_version +from backend.version import get_version app = FastAPI( title="ObsiGate API", @@ -959,6 +946,7 @@ except Exception: # pragma: no cover - WeasyPrint/GTK missing from backend.ai_routes import router as ai_router from backend.bookslm_routes import router as bookslm_router from backend.export import ExportError, export_epub, export_html, export_md_bundle +from backend.routers.health import router as health_router from backend.saved_searches import delete_saved, get_saved, save_search from backend.share import ( create_share, @@ -981,6 +969,7 @@ app.include_router(auth_router) app.include_router(ai_router) app.include_router(bookslm_router) app.include_router(skills_router) +app.include_router(health_router) # ROADMAP #85 T1 — System / health # Admin Dashboard endpoints (system stats, audit logs, backups, stream) try: @@ -1306,125 +1295,9 @@ def _render_markdown(raw_md: str, vault_name: str, current_file_path: Path | Non # --------------------------------------------------------------------------- -# API Endpoints +# API Endpoints — System / health : voir backend.routers.health (#85 T1) # --------------------------------------------------------------------------- -@app.get("/api/health", response_model=HealthResponse) -async def api_health(): - """Health check endpoint for Docker and monitoring. - - Returns: - Application status, version, vault count and total file count. - """ - total_files = sum(len(v["files"]) for v in index.values()) - total_tokens = sum(len(v.get("files", [])) * 1000 for v in index.values()) # rough approx - import time - - from backend.indexer import _last_full_index_ts - uptime = int(time.time() - _SERVER_START_TIME) if '_SERVER_START_TIME' in globals() else 0 - return { - "status": "ok", - "version": app.version, - "vaults": len(index), - "total_files": total_files, - "total_tokens": total_tokens, - "last_full_index_ts": _last_full_index_ts, - "uptime_seconds": uptime, - "git_describe": get_git_describe(), - "git_commit": get_git_commit(), - } - - -@app.get("/api/health/detailed", response_model=HealthResponse) -async def api_health_detailed(current_user=Depends(require_admin)): - """Detailed health check — admin only. - - Returns enriched metrics including memory, disk, SSE connections, and backup stats. - """ - - import psutil - - from backend.admin import _count_active_sessions, _get_disk_stats - from backend.indexer import _last_full_index_ts, index - - total_files = sum(len(v["files"]) for v in index.values()) - total_tokens = sum(len(v.get("files", [])) * 1000 for v in index.values()) - import time - uptime = int(time.time() - _SERVER_START_TIME) if '_SERVER_START_TIME' in globals() else 0 - - # Memory - vm = psutil.virtual_memory() - mem_used_mb = round(vm.used / (1024 ** 2), 1) - mem_total_mb = round(vm.total / (1024 ** 2), 1) - mem_pct = round(vm.percent, 1) - - # CPU - cpu_pct = psutil.cpu_percent(interval=None) - - # Disk - disk_used_gb, disk_total_gb = _get_disk_stats() - disk_free_gb = round(disk_total_gb - disk_used_gb, 2) - disk_pct = round((disk_used_gb / disk_total_gb * 100) if disk_total_gb > 0 else 0, 1) - - # SSE connections (approximation) - active_sessions = _count_active_sessions() - - # Backups - from backend.admin import _scan_backups - backup_rows = _scan_backups() - total_backups = len(backup_rows) - total_backup_size_mb = round(sum(r["size"] for r in backup_rows) / (1024 ** 2), 2) - oldest_backup_age_days = 0.0 - if backup_rows: - now_ts = int(time.time()) - oldest_ts = min(r["timestamp"] for r in backup_rows) - oldest_backup_age_days = round((now_ts - oldest_ts) / 86400, 2) - - # Index details - index_detail = {} - for name, data in index.items(): - index_detail[name] = { - "file_count": len(data["files"]), - "tag_count": len(data["tags"]), - "token_count_approx": len(data.get("files", [])) * 1000, - } - - return { - "status": "ok", - "version": app.version, - "vaults": len(index), - "total_files": total_files, - "total_tokens": total_tokens, - "last_full_index_ts": _last_full_index_ts, - "uptime_seconds": uptime, - "git_describe": get_git_describe(), - "git_commit": get_git_commit(), - # Enriched fields - "memory": { - "used_mb": mem_used_mb, - "total_mb": mem_total_mb, - "percent": mem_pct, - }, - "cpu": { - "percent": cpu_pct, - }, - "disk": { - "used_gb": disk_used_gb, - "total_gb": disk_total_gb, - "free_gb": disk_free_gb, - "percent": disk_pct, - }, - "connections": { - "active_sse": active_sessions, - }, - "backups": { - "total_count": total_backups, - "total_size_mb": total_backup_size_mb, - "oldest_age_days": oldest_backup_age_days, - }, - "index": index_detail, - } - @app.get("/api/vaults", response_model=list[VaultInfo]) async def api_vaults(current_user=Depends(require_auth)): diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py new file mode 100644 index 0000000..f1a4614 --- /dev/null +++ b/backend/routers/__init__.py @@ -0,0 +1,7 @@ +"""ObsiGate — routers FastAPI par domaine (ROADMAP #85). + +Découpage progressif du monolithe ``backend/main.py`` : chaque module de ce +paquet expose un ``APIRouter`` monté par ``main.py``. Les handlers sont +déplacés sans changement de comportement (mêmes chemins, mêmes modèles de +réponse, mêmes dépendances d'authentification). +""" diff --git a/backend/routers/health.py b/backend/routers/health.py new file mode 100644 index 0000000..d5a5779 --- /dev/null +++ b/backend/routers/health.py @@ -0,0 +1,143 @@ +"""System health endpoints (ROADMAP #85, tranche 1). + +Handlers déplacés depuis :mod:`backend.main` sans changement de +comportement : mêmes chemins (``/api/health``, ``/api/health/detailed``), +même ``response_model`` (:class:`backend.schemas.HealthResponse`), même +dépendance admin. Seule différence : la version est lue via +:func:`backend.version.get_version` au lieu de ``app.version`` (valeur +identique, figée au démarrage depuis le fichier ``VERSION``). + +Note : ``uptime_seconds`` reprend l'expression d'origine +(``'_SERVER_START_TIME' in globals()``), qui vaut toujours 0 — le global +n'est défini nulle part dans ``backend.main`` (voir ``backend.admin`` qui +possède son propre compteur). Ce comportement est préservé tel quel ; le +corriger fera l'objet d'une tranche ultérieure avec test dédié. +""" + +from fastapi import APIRouter, Depends + +from backend.auth.middleware import require_admin +from backend.indexer import index +from backend.schemas import HealthResponse +from backend.version import get_git_commit, get_git_describe, get_version + +router = APIRouter(tags=["System"]) + + +@router.get("/api/health", response_model=HealthResponse) +async def api_health(): + """Health check endpoint for Docker and monitoring. + + Returns: + Application status, version, vault count and total file count. + """ + total_files = sum(len(v["files"]) for v in index.values()) + total_tokens = sum(len(v.get("files", [])) * 1000 for v in index.values()) # rough approx + import time + + from backend.indexer import _last_full_index_ts + # `_SERVER_START_TIME` n'existe dans aucun module (comportement d'origine + # préservé : uptime toujours 0 — voir docstring du module). + uptime = int(time.time() - _SERVER_START_TIME) if '_SERVER_START_TIME' in globals() else 0 # noqa: F821 + return { + "status": "ok", + "version": get_version(), + "vaults": len(index), + "total_files": total_files, + "total_tokens": total_tokens, + "last_full_index_ts": _last_full_index_ts, + "uptime_seconds": uptime, + "git_describe": get_git_describe(), + "git_commit": get_git_commit(), + } + + +@router.get("/api/health/detailed", response_model=HealthResponse) +async def api_health_detailed(current_user=Depends(require_admin)): + """Detailed health check — admin only. + + Returns enriched metrics including memory, disk, SSE connections, and backup stats. + """ + + import psutil + + from backend.admin import _count_active_sessions, _get_disk_stats + from backend.indexer import _last_full_index_ts, index + + total_files = sum(len(v["files"]) for v in index.values()) + total_tokens = sum(len(v.get("files", [])) * 1000 for v in index.values()) + import time + uptime = int(time.time() - _SERVER_START_TIME) if '_SERVER_START_TIME' in globals() else 0 # noqa: F821 — voir ci-dessus + + # Memory + vm = psutil.virtual_memory() + mem_used_mb = round(vm.used / (1024 ** 2), 1) + mem_total_mb = round(vm.total / (1024 ** 2), 1) + mem_pct = round(vm.percent, 1) + + # CPU + cpu_pct = psutil.cpu_percent(interval=None) + + # Disk + disk_used_gb, disk_total_gb = _get_disk_stats() + disk_free_gb = round(disk_total_gb - disk_used_gb, 2) + disk_pct = round((disk_used_gb / disk_total_gb * 100) if disk_total_gb > 0 else 0, 1) + + # SSE connections (approximation) + active_sessions = _count_active_sessions() + + # Backups + from backend.admin import _scan_backups + backup_rows = _scan_backups() + total_backups = len(backup_rows) + total_backup_size_mb = round(sum(r["size"] for r in backup_rows) / (1024 ** 2), 2) + oldest_backup_age_days = 0.0 + if backup_rows: + now_ts = int(time.time()) + oldest_ts = min(r["timestamp"] for r in backup_rows) + oldest_backup_age_days = round((now_ts - oldest_ts) / 86400, 2) + + # Index details + index_detail = {} + for name, data in index.items(): + index_detail[name] = { + "file_count": len(data["files"]), + "tag_count": len(data.get("tags", [])), + "token_count_approx": len(data.get("files", [])) * 1000, + } + + return { + "status": "ok", + "version": get_version(), + "vaults": len(index), + "total_files": total_files, + "total_tokens": total_tokens, + "last_full_index_ts": _last_full_index_ts, + "uptime_seconds": uptime, + "git_describe": get_git_describe(), + "git_commit": get_git_commit(), + # Enriched fields + "memory": { + "used_mb": mem_used_mb, + "total_mb": mem_total_mb, + "percent": mem_pct, + }, + "cpu": { + "percent": cpu_pct, + }, + "disk": { + "used_gb": disk_used_gb, + "total_gb": disk_total_gb, + "free_gb": disk_free_gb, + "percent": disk_pct, + }, + "connections": { + "active_sse": active_sessions, + }, + "backups": { + "total_count": total_backups, + "total_size_mb": total_backup_size_mb, + "oldest_age_days": oldest_backup_age_days, + }, + "index": index_detail, + } diff --git a/backend/schemas.py b/backend/schemas.py index 07341ed..0961381 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -408,6 +408,31 @@ class DashboardResponse(BaseModel): total_images: int = 0 +# --------------------------------------------------------------------------- +# System / health (#85 — extrait de backend.main, comportement inchangé) +# --------------------------------------------------------------------------- + + +class HealthResponse(BaseModel): + """Application health status. + + Déplacé depuis :mod:`backend.main` sans modification : pas de + ``extra="allow"`` ici, pour préserver la validation actuelle des + réponses (les champs enrichis de ``/api/health/detailed`` restent + filtrés comme avant). + """ + + status: str = Field(description="Health status ('ok' or 'error')") + version: str = Field(description="Application version (x.y.z — latest release tag)") + vaults: int = Field(description="Number of configured vaults") + total_files: int = Field(description="Total indexed files across all vaults") + total_tokens: int = Field(description="Total indexed tokens (approx.) across all vaults", default=0) + last_full_index_ts: str = Field(description="ISO timestamp of last full index rebuild", default="") + uptime_seconds: int = Field(description="Server uptime in seconds", default=0) + git_describe: str = Field(default="", description="Full git describe string (commits beyond tag), empty if no git") + git_commit: str = Field(default="", description="Short HEAD commit hash, empty if no git") + + # --------------------------------------------------------------------------- # Webhooks, sharing & conflicts # --------------------------------------------------------------------------- diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 4cd164c..f373c73 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.27.1" +version = "2.27.2" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index eb8c9ad..244891c 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.27.1" +version = "2.27.2" 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 d20e750..d61ffa1 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.27.1", + "version": "2.27.2", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8e0a40a..1221319 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.27.1 | **Dernière mise à jour :** 2026-09-26 +> **Version :** 2.27.2 | **Dernière mise à jour :** 2026-09-26 > **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)** @@ -67,10 +67,11 @@ - **Effort :** 8-12 jours | **Impact :** 🟡 | **Zone :** backend - **Décision 2026-09-26 : prioritaire (axe Dette & sécurité).** +- **Statut :** 🔵 en cours depuis 2026-09-26 — découpe par tranches à impact minimal (comportement inchangé, un domaine par commit). **T1 livrée (v2.27.2) :** `health` (`/api/health`, `/api/health/detailed` → `backend/routers/health.py`, `HealthResponse` → `schemas.py`). - **Description :** extraire le monolithe `backend/main.py` (~4 827 lignes au 2026-09-26, ~17 % du backend) en routers FastAPI par domaine et rendre persistant l'état qui ne l'est pas (index de recherche, JTI révoqués, compteurs de rate-limit) pour préparer le multi-nœuds. L'état mémoire actuel (index, inverted index, vecteurs sémantiques, `SSEManager`, collab) rend le multi-workers unsafe. - **Sous-tâches :** - - [ ] Routers par domaine : files, search, share, webhooks, plugins, collab, admin, ai — `main.py` conservé comme assemblage (< 500 lignes) ; dédupliquer les modèles Pydantic vers `schemas.py` - - [ ] Créer `tools/registry.py` (inexistant) : contrat central des outils IA (permissions, quotas, redaction) + - [ ] Routers par domaine : files, search, share, webhooks, plugins, collab, admin, ai — `main.py` conservé comme assemblage (< 500 lignes) ; dédupliquer les modèles Pydantic vers `schemas.py`. **Avancement :** `health` ✅ (T1, `backend/routers/health.py`) ; `tools/registry.py` existe déjà (permissions/quotas/redaction — à compléter, pas à créer) + - [ ] Compléter `tools/registry.py` (existant : permissions/quotas/redaction) comme contrat central des outils IA si des manques sont constatés - [ ] Persister index, JTI révoqués et compteurs de rate-limit (SQLite par défaut, Redis en option multi-nœuds ; le rate-limit actuel est in-memory mono-process) - [ ] Verrous asyncio autour de l'index global et des stores JSON ; auditer les `except Exception` larges (> 100 occurrences) : best-effort (backup/audit) vs masquage d'erreur (erreurs typées 4xx/5xx + test) - [ ] Extraire le service de partage public (expiration, révocation, quotas) diff --git a/package.json b/package.json index 6460a24..873eed1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.27.1", + "version": "2.27.2", "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": {