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
+3 -1
View File
@@ -17,12 +17,13 @@ from app.models.contact_folder import ContactFolder
from app.models.contact_merge import ContactMergeHistory
from app.models.currency import Currency
from app.models.custom_field_definition import CustomFieldDefinition
from app.models.dashboard import Dashboard
from app.models.entity_history import EntityHistory
from app.models.entity_permission import EntityPermission
from app.models.entity_policy import EntityPolicy
from app.models.group import Group, UserGroup
from app.models.notification import Notification, NotificationPreference, NotificationType
from app.models.outbox import EventOutbox, OutboxDelivery
from app.models.outbox import EventOutbox, OutboxDelivery # noqa: F401
from app.models.owned_mixin import OwnedMixin
from app.models.permission_delegation import PermissionDelegation
from app.models.permission_template import PermissionTemplate
@@ -80,6 +81,7 @@ __all__ = [
"WorkflowInstance",
"WorkflowStepHistory",
"SavedView",
"Dashboard",
]
from app.models.entity_attachment import EntityAttachment # noqa: F401
from app.models.workspace import ( # noqa: F401
+52
View File
@@ -0,0 +1,52 @@
"""Dashboard model — personal per-user dashboards (Phase M2).
Dashboards are personal (user-owned) layouts of MiniApp instances: tabs,
widget placements and per-instance settings, stored as JSONB. Access is
owner-only (saved_views precedent) — the active workspace limits only the
available widget types (Phase N), never this personal layout.
"""
from __future__ import annotations
import uuid
from typing import Any
from sqlalchemy import Boolean, ForeignKey, Index, String, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
class Dashboard(Base, TenantMixin):
"""Personal dashboard — per-user layout of MiniApp instances."""
__tablename__ = "dashboards"
__table_args__ = (
# Partial unique: soft-deleted dashboards free their name (unlike the
# saved_views plain constraint, which keeps names occupied forever).
Index(
"uq_dashboards_tenant_user_name",
"tenant_id",
"user_id",
"name",
unique=True,
postgresql_where=text("deleted_at IS NULL"),
),
Index("ix_dashboards_tenant_user", "tenant_id", "user_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(100), nullable=False)
# Layout JSONB (validated by app.schemas.dashboard.DashboardLayout):
# {version, tabs: [{id, name, widgets: [{app_id, settings, col, row, spans}]}]}
layout: Mapped[dict[str, Any]] = mapped_column(
JSONB, nullable=False, default=dict
)
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
user_id: Mapped[uuid.UUID] = mapped_column(
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)