"""Vault management endpoints (ROADMAP #85, tranche 8). Handlers déplacés depuis :mod:`backend.main` sans changement de comportement : mêmes chemins (``/api/vaults*``), mêmes modèles de réponse (``VaultInfo`` déménagé dans :mod:`backend.schemas`), mêmes dépendances d'authentification. Le handle du file-watcher vit désormais dans :mod:`backend.watcher_state` (partagé avec le lifespan de ``main``) au lieu du global de ``main``. """ from pathlib import Path from fastapi import APIRouter, Body, Depends, HTTPException from backend.auth.middleware import require_admin, require_auth from backend.indexer import add_vault_to_index, index, remove_vault_from_index from backend.schemas import VaultActionResponse, VaultInfo, VaultsStatusResponse, VaultStatsResponse from backend.services.vaults import list_accessible_vaults from backend.sse import sse_manager from backend.watcher_state import get_watcher router = APIRouter(tags=["vaults"]) @router.get("/api/vaults", response_model=list[VaultInfo]) async def api_vaults(current_user=Depends(require_auth)): """List configured vaults the user has access to. Returns: List of vault summary objects filtered by user permissions. """ return list_accessible_vaults(current_user) @router.post("/api/vaults/add", response_model=VaultStatsResponse) async def api_add_vault(body: dict = Body(...), current_user=Depends(require_admin)): """Add a new vault dynamically without restarting. Body: name: Display name for the vault. path: Absolute filesystem path to the vault directory. """ name = body.get("name", "").strip() vault_path = body.get("path", "").strip() if not name or not vault_path: raise HTTPException(status_code=400, detail="Both 'name' and 'path' are required") if name in index: raise HTTPException(status_code=409, detail=f"Vault '{name}' already exists") if not Path(vault_path).exists(): raise HTTPException(status_code=400, detail=f"Path does not exist: {vault_path}") stats = await add_vault_to_index(name, vault_path) # #194 : persister, sinon ce vault disparaît au prochain rebuild/redémarrage. from backend.indexer import persist_vault persist_vault(name) # Start watching the new vault watcher = get_watcher() if watcher: await watcher.add_vault(name, vault_path) await sse_manager.broadcast("vault_added", {"vault": name, "stats": stats}) return {"status": "ok", "vault": name, "stats": stats} @router.delete("/api/vaults/{vault_name}", response_model=VaultActionResponse) async def api_remove_vault(vault_name: str, current_user=Depends(require_admin)): """Remove a vault from the index and stop watching it. Args: vault_name: Name of the vault to remove. """ if vault_name not in index: raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found") # Stop watching watcher = get_watcher() if watcher: await watcher.remove_vault(vault_name) await remove_vault_from_index(vault_name) # #194 : plus de trace au redémarrage (les vaults d'env, eux, reviennent). from backend.indexer import unpersist_vault unpersist_vault(vault_name) await sse_manager.broadcast("vault_removed", {"vault": vault_name}) return {"status": "ok", "vault": vault_name} @router.get("/api/vaults/status", response_model=VaultsStatusResponse) async def api_vaults_status(current_user=Depends(require_auth)): """Detailed status of all vaults including watcher state. Returns per-vault: file count, tag count, watching status, vault path. """ watcher = get_watcher() statuses = {} for vname, vdata in index.items(): watching = watcher is not None and vname in watcher.observers statuses[vname] = { "file_count": len(vdata.get("files", [])), "tag_count": len(vdata.get("tags", {})), "path": vdata.get("path", ""), "watching": watching, } return { "vaults": statuses, "watcher_active": watcher is not None, "sse_clients": sse_manager.client_count, }