""" 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, lab_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 from system_dashboard_product_default import get_product_default_base_dict 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: base_product = get_product_default_base_dict(conn) if not custom: effective = base_product effective = apply_entitlements_to_layout_dict(effective, pid, conn) product_adj = apply_entitlements_to_layout_dict(base_product, pid, conn) lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn) return { "custom": custom, "layout": effective, "product_default_layout": product_adj, "lab_default_layout": lab_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") base = get_product_default_base_dict(conn) cleared = apply_entitlements_to_layout_dict(base, pid, conn) return {"ok": True, "layout": cleared}