feat(M2): Persönliche Dashboards — Tabelle, CRUD, Lazy-Seed, RLS (#360)
Check Cross-Plugin Imports / check (push) Has been cancelled

- dashboards-Tabelle (Layout JSONB, Tabs, is_default, partial unique name index)
- 6 CRUD-Endpoints /api/v1/dashboards, Owner-only (saved_views-Präzedenz), Audit
- Lazy Default-Seed aus MiniApp-Registry (permission-gefiltert, 12-Spalten-Flow)
- CORE_PERMISSIONS dashboard:read/write (fixt Phantom-Permission in dashboard.py)
- Migration 0144: RLS crm_api+crm_worker + konvergenter Fix der 3 Phase-L-Policies
- Tests: test_dashboards_backend.py 23/23 (TDD rot->grün); Regression 162/163
This commit is contained in:
Agent Zero
2026-08-30 16:21:34 +02:00
parent 7a755d32e6
commit b3e259fc25
14 changed files with 1155 additions and 17 deletions
+59
View File
@@ -0,0 +1,59 @@
"""Schemas for personal dashboards (Phase M2).
Layout is validated structurally (12-column grid bounds) before it is
persisted; MiniApp existence and permissions are enforced at seed time and
when the frontend resolves widget components via /api/v1/miniapps.
"""
from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
class DashboardWidget(BaseModel):
"""A MiniApp instance placed on a dashboard tab."""
app_id: str = Field(..., min_length=1, max_length=80)
settings: dict[str, Any] = Field(default_factory=dict)
col: int = Field(default=0, ge=0)
row: int = Field(default=0, ge=0)
col_span: int = Field(default=1, ge=1, le=12)
row_span: int = Field(default=1, ge=1, le=12)
class DashboardTab(BaseModel):
"""A tab of a dashboard holding widget placements."""
id: str = Field(..., min_length=1, max_length=80)
name: str = Field(..., min_length=1, max_length=100)
widgets: list[DashboardWidget] = Field(default_factory=list)
class DashboardLayout(BaseModel):
"""Full persisted layout of a personal dashboard."""
version: int = Field(default=1, ge=1)
tabs: list[DashboardTab] = Field(default_factory=list)
class DashboardCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
class DashboardUpdate(BaseModel):
name: str | None = Field(default=None, min_length=1, max_length=100)
layout: DashboardLayout | None = None
class DashboardResponse(BaseModel):
id: str
name: str
layout: dict[str, Any]
is_default: bool
user_id: str
created_at: str | None = None
updated_at: str | None = None
model_config = {"from_attributes": True}