"""Admin Trash: view, restore, and permanently delete soft-deleted items.""" from fastapi import APIRouter, Request, Depends, Form from fastapi.templating import Jinja2Templates from fastapi.responses import RedirectResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import text from typing import Optional from core.database import get_db from core.base_repository import BaseRepository from core.sidebar import get_sidebar_data router = APIRouter(prefix="/admin/trash", tags=["admin"]) templates = Jinja2Templates(directory="templates") # Entity configs for trash view: (table, display_name, name_column, detail_url_pattern) TRASH_ENTITIES = [ {"table": "tasks", "label": "Tasks", "name_col": "title", "url": "/tasks/{id}"}, {"table": "projects", "label": "Projects", "name_col": "name", "url": "/projects/{id}"}, {"table": "notes", "label": "Notes", "name_col": "title", "url": "/notes/{id}"}, {"table": "links", "label": "Links", "name_col": "label", "url": "/links"}, {"table": "contacts", "label": "Contacts", "name_col": "first_name", "url": "/contacts/{id}"}, {"table": "domains", "label": "Domains", "name_col": "name", "url": "/domains"}, {"table": "areas", "label": "Areas", "name_col": "name", "url": "/areas"}, {"table": "lists", "label": "Lists", "name_col": "name", "url": "/lists/{id}"}, {"table": "meetings", "label": "Meetings", "name_col": "title", "url": "/meetings/{id}"}, {"table": "decisions", "label": "Decisions", "name_col": "title", "url": "/decisions/{id}"}, {"table": "files", "label": "Files", "name_col": "original_filename", "url": "/files"}, {"table": "weblinks", "label": "Weblinks", "name_col": "label", "url": "/weblinks"}, {"table": "weblink_folders", "label": "Weblink Folders", "name_col": "name", "url": "/weblinks"}, {"table": "appointments", "label": "Appointments", "name_col": "title", "url": "/appointments/{id}"}, {"table": "capture", "label": "Capture", "name_col": "raw_text", "url": "/capture"}, {"table": "daily_focus", "label": "Focus Items", "name_col": "id", "url": "/focus"}, {"table": "processes", "label": "Processes", "name_col": "name", "url": "/processes/{id}"}, {"table": "process_runs", "label": "Process Runs", "name_col": "title", "url": "/processes/runs/{id}"}, {"table": "time_budgets", "label": "Time Budgets", "name_col": "id", "url": "/time-budgets"}, ] @router.get("/") async def trash_view( request: Request, entity_type: Optional[str] = None, db: AsyncSession = Depends(get_db), ): sidebar = await get_sidebar_data(db) deleted_items = [] entity_counts = {} for entity in TRASH_ENTITIES: try: # Count deleted items per type result = await db.execute(text( f"SELECT count(*) FROM {entity['table']} WHERE is_deleted = true" )) count = result.scalar() or 0 entity_counts[entity["table"]] = count # Load items for selected type (or all if none selected) if count > 0 and (entity_type is None or entity_type == entity["table"]): result = await db.execute(text(f""" SELECT id, {entity['name_col']} as display_name, deleted_at, created_at FROM {entity['table']} WHERE is_deleted = true ORDER BY deleted_at DESC LIMIT 50 """)) rows = [dict(r._mapping) for r in result] for row in rows: deleted_items.append({ "id": str(row["id"]), "name": str(row.get("display_name") or row["id"])[:100], "table": entity["table"], "type_label": entity["label"], "deleted_at": row.get("deleted_at"), "created_at": row.get("created_at"), }) except Exception: continue total_deleted = sum(entity_counts.values()) return templates.TemplateResponse("trash.html", { "request": request, "sidebar": sidebar, "deleted_items": deleted_items, "entity_counts": entity_counts, "trash_entities": TRASH_ENTITIES, "current_type": entity_type or "", "total_deleted": total_deleted, "page_title": "Trash", "active_nav": "trash", }) @router.post("/{table}/{item_id}/restore") async def restore_item(table: str, item_id: str, request: Request, db: AsyncSession = Depends(get_db)): # Validate table name against known entities valid_tables = {e["table"] for e in TRASH_ENTITIES} if table not in valid_tables: return RedirectResponse(url="/admin/trash", status_code=303) repo = BaseRepository(table, db) await repo.restore(item_id) referer = request.headers.get("referer", "/admin/trash") return RedirectResponse(url=referer, status_code=303) @router.post("/{table}/{item_id}/permanent-delete") async def permanent_delete_item(table: str, item_id: str, request: Request, db: AsyncSession = Depends(get_db)): valid_tables = {e["table"] for e in TRASH_ENTITIES} if table not in valid_tables: return RedirectResponse(url="/admin/trash", status_code=303) repo = BaseRepository(table, db) await repo.permanent_delete(item_id) referer = request.headers.get("referer", "/admin/trash") return RedirectResponse(url=referer, status_code=303) @router.post("/empty") async def empty_trash(request: Request, db: AsyncSession = Depends(get_db)): """Permanently delete ALL soft-deleted items across all tables.""" for entity in TRASH_ENTITIES: try: await db.execute(text( f"DELETE FROM {entity['table']} WHERE is_deleted = true" )) except Exception: continue return RedirectResponse(url="/admin/trash", status_code=303)