from __future__ import annotations from pathlib import Path from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER from govoplan_core.core.module_guards import ( drop_table_retirement_provider, persistent_table_uninstall_guard, ) from govoplan_core.core.modules import ( DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleManifest, NavItem, ) from govoplan_core.core.provider_governance import declared_module_architecture from govoplan_core.core.views import ViewSurface from govoplan_core.db.base import Base from govoplan_dashboard.backend.db.models import DashboardLayout def _dashboard_router(_context): from govoplan_dashboard.backend.router import router return router def _tenant_summary(session, tenant_id: str) -> dict[str, int]: return { "dashboard_layouts": ( session.query(DashboardLayout) .filter(DashboardLayout.tenant_id == tenant_id) .count() ) } manifest = ModuleManifest( id="dashboard", name="Dashboard", version="0.1.15", required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), optional_dependencies=("ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"), route_factory=_dashboard_router, tenant_summary_providers=(_tenant_summary,), migration_spec=MigrationSpec( module_id="dashboard", metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=drop_table_retirement_provider( DashboardLayout, label="Dashboard", ), retirement_notes=( "Destructive retirement removes personal Dashboard layouts after " "the installer captures a database snapshot." ), ), uninstall_guard_providers=( persistent_table_uninstall_guard( DashboardLayout, label="Dashboard", ), ), nav_items=(NavItem(path="/dashboard", label="Dashboard", icon="dashboard", order=10),), frontend=FrontendModule( module_id="dashboard", package_name="@govoplan/dashboard-webui", routes=(FrontendRoute(path="/dashboard", component="DashboardPage", order=10),), nav_items=(NavItem(path="/dashboard", label="Dashboard", icon="dashboard", order=10),), view_surfaces=( ViewSurface( id="dashboard.page", module_id="dashboard", kind="route", label="Dashboard", order=10, ), ViewSurface( id="dashboard.summary", module_id="dashboard", kind="section", label="Dashboard summary", parent_id="dashboard.page", order=10, ), ViewSurface( id="dashboard.library", module_id="dashboard", kind="section", label="Widget library", parent_id="dashboard.page", order=20, ), ViewSurface( id="dashboard.grid", module_id="dashboard", kind="section", label="Dashboard grid", parent_id="dashboard.page", order=30, ), ViewSurface( id="dashboard.widget-settings", module_id="dashboard", kind="action", label="Widget settings", parent_id="dashboard.grid", order=40, ), ViewSurface( id="dashboard.widget.installed-modules", module_id="dashboard", kind="section", label="Installed modules widget", parent_id="dashboard.grid", order=10, ), ), ), documentation=( DocumentationTopic( id="dashboard.configurable-home", title="Configurable user dashboard", summary="The dashboard module owns the configurable home surface. Feature modules expose widgets through a narrow dashboard.widgets capability.", body=( "Core only provides a minimal fallback home when the dashboard module is absent. " "Dashboard widgets must be contributed through core contracts, not by importing sibling module components directly. " "Personal layouts are stored per tenant, account, and active View." ), layer="configured", documentation_types=("admin", "user"), audience=("user", "tenant_admin", "operator"), related_modules=("core", "ops"), order=20, metadata={ "help_contexts": [ "dashboard.page", "dashboard.summary", "dashboard.library", "dashboard.grid", "dashboard.state.browser-fallback", "dashboard.state.view-specific", ], }, ), DocumentationTopic( id="dashboard.reference.layout-and-widgets", title="Dashboard layouts and widget consequences", summary="Per-user, per-tenant, and per-View widget placement, sizing, configuration, availability, and fallback semantics.", body=( "A Dashboard layout belongs to the active tenant, account, and focused View. " "Configuring changes only a local draft until Save layout is selected; Cancel or " "Discard restores the last saved arrangement. Widget removal removes only the placement, " "not the module data represented by the widget. Reset restores the defaults announced by " "currently active modules and remains reversible until save. A widget is offered only when " "its module, focused View surface, and permission contract are available. Widgets never grant " "access and providers must authorize every data request. If the server layout is unreachable, " "the browser or module default can be displayed but is identified as a fallback until saved. " "Concurrent saves use the layout revision and reject stale updates instead of overwriting them." ), layer="available", documentation_types=("admin", "user"), audience=("user", "tenant_admin", "operator", "module_admin"), related_modules=("core", "views", "access", "ops"), order=21, metadata={ "help_contexts": [ "dashboard.action.configure", "dashboard.action.save", "dashboard.action.reset", "dashboard.action.remove-widget", "dashboard.field.widget-size", "dashboard.field.widget-configuration", ], "consequence_classes": { "save_layout": "Persists the complete layout for the active tenant, account, and View revision context.", "reset_layout": "Replaces the draft with current module defaults and remains reversible until save.", "remove_widget": "Removes only the placement from the draft; provider data is unchanged.", "configure_widget": "Changes presentation and query preferences for one widget placement.", }, }, ), ), architecture=declared_module_architecture( layer="data_reporting_integration", kind="presentation", maturity="vertical_slice", documentation_ref="README.md", test_ref="tests/test_dashboard_layouts.py", known_limits=("Widget configuration depth and target accessibility evidence vary by contributing module.",), owned_concepts=("dashboard layout", "widget placement", "view-specific dashboard"), non_owned_concepts=("widget domain data", "report definition", "view policy"), ), ) def get_manifest() -> ModuleManifest: return manifest