""" Geschützter App-Bereich: Dashboard-Lab Layout (kein Produktiv-Dashboard). /api/app/dashboard-layout — nur mit Session + aktivem Profil (X-Profile-Id). """ from typing import Any, Optional from fastapi import APIRouter, Depends, Header, HTTPException from psycopg2.extras import Json from auth import require_auth from dashboard_layout_schema import DashboardLayoutPayload, coalesce_effective_layout, default_layout_dict from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload from db import get_cursor, get_db from routers.profiles import get_pid router = APIRouter(prefix="/api/app", tags=["app-dashboard-lab"]) @router.get("/widgets/catalog") def get_widgets_catalog( x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth), ) -> dict[str, Any]: """Katalog inkl. allowed pro Widget (Feature / Subscription, effektiver Tier).""" _ = session pid = get_pid(x_profile_id) with get_db() as conn: return widgets_catalog_payload(pid, conn) @router.get("/dashboard-layout") def get_dashboard_layout( x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth), ) -> dict[str, Any]: _ = session pid = get_pid(x_profile_id) with get_db() as conn: cur = get_cursor(conn) cur.execute( "SELECT dashboard_layout FROM profiles WHERE id = %s", (pid,), ) row = cur.fetchone() raw = row["dashboard_layout"] if row else None custom, effective = coalesce_effective_layout(raw) with get_db() as conn: effective = apply_entitlements_to_layout_dict(effective, pid, conn) default_adj = apply_entitlements_to_layout_dict(default_layout_dict(), pid, conn) return { "custom": custom, "layout": effective, "default_layout": default_adj, } @router.put("/dashboard-layout") def put_dashboard_layout( body: dict[str, Any], x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth), ) -> dict[str, Any]: _ = session pid = get_pid(x_profile_id) try: payload = DashboardLayoutPayload.model_validate(body) except Exception as e: raise HTTPException(422, str(e)) from e with get_db() as conn: adjusted = apply_entitlements_to_layout_dict(payload.to_stored_dict(), pid, conn) try: payload = DashboardLayoutPayload.model_validate(adjusted) except Exception as e: raise HTTPException(422, str(e)) from e stored = payload.to_stored_dict() with get_db() as conn: cur = get_cursor(conn) cur.execute( "UPDATE profiles SET dashboard_layout = %s WHERE id = %s", (Json(stored), pid), ) if cur.rowcount == 0: raise HTTPException(404, "Profil nicht gefunden") return {"ok": True, "layout": stored} @router.post("/dashboard-layout/reset") def reset_dashboard_layout( x_profile_id: Optional[str] = Header(default=None), session: dict = Depends(require_auth), ) -> dict[str, Any]: _ = session pid = get_pid(x_profile_id) with get_db() as conn: cur = get_cursor(conn) cur.execute( "UPDATE profiles SET dashboard_layout = NULL WHERE id = %s", (pid,), ) if cur.rowcount == 0: raise HTTPException(404, "Profil nicht gefunden") return {"ok": True, "layout": default_layout_dict()}