"""History endpoints — recent, bookmarks, saved searches (ROADMAP #85, tranche 8). Handlers déplacés depuis :mod:`backend.main` sans changement de comportement : mêmes chemins, mêmes modèles (``BookmarkToggleRequest`` déménagé dans :mod:`backend.schemas`), mêmes dépendances d'authentification. Adaptations strictement équivalentes : - ``_resolve_safe_path`` / ``_backup_file`` → :mod:`backend.services.paths` et :mod:`backend.services.backups` (pass-through). - ``_load_config`` vient de :mod:`backend.routers.config`. """ import logging from pathlib import Path import frontmatter from fastapi import APIRouter, Body, Depends, HTTPException, Query from backend.auth.middleware import check_vault_access, require_auth from backend.history import get_bookmarks, toggle_bookmark from backend.indexer import find_file_in_index, get_vault_data, update_single_file from backend.routers.config import _load_config from backend.saved_searches import delete_saved, get_saved, save_search from backend.schemas import ( BookmarksResponse, BookmarkToggleRequest, BookmarkToggleResponse, RecentResponse, SavedSearch, StatusResponse, ) from backend.services.backups import create_backup from backend.services.paths import resolve_safe_path from backend.services.recent import humanize_mtime, list_recent logger = logging.getLogger("obsigate") router = APIRouter(tags=["Bookmarks"]) @router.get("/api/recent", response_model=RecentResponse) async def api_recent(limit: int | None = Query(None), vault: str | None = Query(None), mode: str | None = Query("opened"), current_user=Depends(require_auth)): config = _load_config() actual_limit = limit if limit is not None else config.get("recent_files_limit", 20) username = current_user.get("username") user_vaults = current_user.get("_token_vaults") or current_user.get("vaults", []) return list_recent( username, user_vaults, vault=vault, limit=actual_limit, mode=mode or "opened", ) @router.get("/api/bookmarks", response_model=BookmarksResponse) async def api_bookmarks(vault: str | None = Query(None), current_user=Depends(require_auth)): username = current_user.get("username") if not username: return {"files": []} history = get_bookmarks(username, vault_filter=vault) files_resp = [] for item in history: v_name = item["vault"] # #194 : check_vault_access — "*" n'inclut pas les dossiers persos. if not check_vault_access(v_name, current_user): continue # Find in index to get metadata f_idx = find_file_in_index(item["path"], v_name) if f_idx: files_resp.append({ "path": f_idx["path"], "title": f_idx.get("title") or item["path"].split("/")[-1], "vault": v_name, "mtime": item["bookmarked_at"], "mtime_human": humanize_mtime(item["bookmarked_at"]), "size_bytes": f_idx.get("size", 0), "tags": [f"#{t}" for t in f_idx.get("tags", [])][:5], "bookmarked": True }) else: files_resp.append({ "path": item["path"], "title": item.get("title") or item["path"].split("/")[-1], "vault": v_name, "mtime": item["bookmarked_at"], "mtime_human": humanize_mtime(item["bookmarked_at"]), "tags": [], "bookmarked": True }) return { "files": files_resp, "total": len(files_resp) } @router.post("/api/bookmarks/toggle", response_model=BookmarkToggleResponse) async def api_toggle_bookmark(req: BookmarkToggleRequest, current_user=Depends(require_auth)): username = current_user.get("username") if not username: raise HTTPException(status_code=401, detail="Not authenticated") # Check vault access if not check_vault_access(req.vault, current_user): raise HTTPException(status_code=403, detail="Access denied to vault") is_now_bookmarked = toggle_bookmark(username, req.vault, req.path, req.title or "") # Update the file's YAML frontmatter: favoris: true/false vault_data = get_vault_data(req.vault) if vault_data: file_path = resolve_safe_path(Path(vault_data["path"]), req.path) if file_path.exists() and file_path.suffix == ".md": try: raw = file_path.read_text(encoding="utf-8", errors="replace") post = frontmatter.loads(raw) if is_now_bookmarked: post.metadata["favoris"] = True elif "favoris" in post.metadata: del post.metadata["favoris"] new_raw = frontmatter.dumps(post) create_backup(file_path, req.vault, req.path) file_path.write_text(new_raw, encoding="utf-8") await update_single_file(req.vault, str(file_path)) except Exception as e: logger.warning(f"Failed to update favoris metadata on {req.vault}/{req.path}: {e}") return {"bookmarked": is_now_bookmarked} @router.get("/api/saved-searches", response_model=list[SavedSearch]) async def api_saved_searches(current_user=Depends(require_auth)): username = current_user.get("username") if not username: raise HTTPException(401) return get_saved(username) @router.post("/api/saved-searches", response_model=SavedSearch) async def api_save_search(body: dict = Body(...), current_user=Depends(require_auth)): username = current_user.get("username") if not username: raise HTTPException(401) return save_search(username, body) @router.delete("/api/saved-searches/{search_id}", response_model=StatusResponse) async def api_delete_saved_search(search_id: str, current_user=Depends(require_auth)): username = current_user.get("username") if not username: raise HTTPException(401) if not delete_saved(username, search_id): raise HTTPException(404, "Not found") return {"status": "deleted"}