Add saved searches with CRUD API and UI sidebar
Add extension field to search results and display it Add active filter badges and save button to search header
This commit is contained in:
@@ -182,6 +182,7 @@ class AdvancedSearchResultItem(BaseModel):
|
||||
score: float = Field(description="TF-IDF relevance score")
|
||||
snippet: str = Field(description="Content excerpt with <mark> highlights")
|
||||
modified: str = Field(description="ISO 8601 modification timestamp")
|
||||
extension: str = Field(default="", description="File extension")
|
||||
|
||||
|
||||
class SearchFacets(BaseModel):
|
||||
@@ -561,6 +562,7 @@ from backend.secret_redactor import redact_file_content
|
||||
from backend.pdf_export import generate_pdf, build_pdf_html
|
||||
from backend.share import create_share, get_share_by_token, record_access, revoke_share, list_shares
|
||||
from backend.webhooks import get_webhooks, create_webhook, update_webhook, delete_webhook, dispatch_webhooks
|
||||
from backend.saved_searches import get_saved, save_search, delete_saved
|
||||
|
||||
app.include_router(auth_router)
|
||||
|
||||
@@ -1017,6 +1019,32 @@ async def api_toggle_bookmark(req: BookmarkToggleRequest, current_user=Depends(r
|
||||
return {"bookmarked": is_now_bookmarked}
|
||||
|
||||
|
||||
@app.get("/api/saved-searches")
|
||||
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)
|
||||
|
||||
|
||||
@app.post("/api/saved-searches")
|
||||
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)
|
||||
|
||||
|
||||
@app.delete("/api/saved-searches/{search_id}")
|
||||
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"}
|
||||
|
||||
|
||||
@app.get("/api/browse/{vault_name}", response_model=BrowseResponse)
|
||||
async def api_browse(vault_name: str, path: str = "", current_user=Depends(require_auth)):
|
||||
"""Browse directories and files in a vault at a given path level.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Saved search definitions — persisted search queries with their active filters.
|
||||
|
||||
Stored in data/saved_searches.json per user.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger("obsigate.saved_searches")
|
||||
|
||||
DATA_DIR = Path("data")
|
||||
|
||||
|
||||
def _get_file(username: str) -> Path:
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return DATA_DIR / f"{username}_saved_searches.json"
|
||||
|
||||
|
||||
def _read(file: Path) -> List[Dict[str, Any]]:
|
||||
if not file.exists():
|
||||
return []
|
||||
try:
|
||||
return json.loads(file.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _write(file: Path, data: List[Dict[str, Any]]):
|
||||
tmp = file.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
shutil.move(str(tmp), str(file))
|
||||
|
||||
|
||||
def get_saved(username: str) -> List[Dict[str, Any]]:
|
||||
return _read(_get_file(username))
|
||||
|
||||
|
||||
def save_search(username: str, definition: dict) -> dict:
|
||||
"""Save a search definition. Returns the saved entry."""
|
||||
searches = _read(_get_file(username))
|
||||
|
||||
entry = {
|
||||
"id": str(int(time.time() * 1000)),
|
||||
"query": definition.get("query", ""),
|
||||
"vault": definition.get("vault", "all"),
|
||||
"case_sensitive": definition.get("case_sensitive", False),
|
||||
"whole_word": definition.get("whole_word", False),
|
||||
"regex": definition.get("regex", False),
|
||||
"include_paths": definition.get("include_paths", ""),
|
||||
"exclude_paths": definition.get("exclude_paths", ""),
|
||||
"created_at": time.time(),
|
||||
}
|
||||
searches.insert(0, entry)
|
||||
# Keep last 50
|
||||
searches = searches[:50]
|
||||
_write(_get_file(username), searches)
|
||||
return entry
|
||||
|
||||
|
||||
def delete_saved(username: str, search_id: str) -> bool:
|
||||
searches = _read(_get_file(username))
|
||||
new_list = [s for s in searches if s["id"] != search_id]
|
||||
if len(new_list) == len(searches):
|
||||
return False
|
||||
_write(_get_file(username), new_list)
|
||||
return True
|
||||
@@ -1084,6 +1084,7 @@ def advanced_search(
|
||||
"score": round(score, 4),
|
||||
"snippet": snippet,
|
||||
"modified": file_info.get("modified", ""),
|
||||
"extension": file_info.get("extension", file_info.get("path", "").rsplit(".", 1)[-1] if "." in file_info.get("path", "") else ""),
|
||||
}
|
||||
scored_results.append((score, result))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user