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 ( CapabilityDocumentation, DocumentationTopic, FrontendModule, FrontendRoute, MigrationSpec, ModuleContext, ModuleInterfaceProvider, 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 from govoplan_dashboard.backend.dsar_provider import ( DASHBOARD_DSAR_CAPABILITY, DashboardDsarProvider, ) def _dashboard_router(_context): from govoplan_dashboard.backend.router import router return router def _dsar_provider(_context: ModuleContext) -> DashboardDsarProvider: return DashboardDsarProvider() 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.18", required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR), optional_dependencies=("ops", "campaigns", "files", "mail", "tasks", "notifications", "reporting"), provides_interfaces=( ModuleInterfaceProvider(name=DASHBOARD_DSAR_CAPABILITY, version="0.1.0"), ), route_factory=_dashboard_router, capability_factories={DASHBOARD_DSAR_CAPABILITY: _dsar_provider}, capability_documentation={ DASHBOARD_DSAR_CAPABILITY: CapabilityDocumentation( label="Dashboard data-subject request provider", summary=( "Exports and deletes exact account-owned Dashboard layout " "preferences without traversing widget data." ), contract_version="0.1.0", ), }, 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="Active interface modules widget", parent_id="dashboard.grid", order=10, ), ), ), documentation=( DocumentationTopic( id="dashboard.data-subject-requests", title="Dashboard data-subject requests", summary=( "Export or delete personal Dashboard layouts without changing " "the data displayed by their widgets." ), body=( "Dashboard contributes layouts only when the request contains one " "exact, corroborated account identifier in the active tenant. A " "layout or View reference can narrow that result. The access package " "contains layout version, revision, View, widget identities, sizes, " "and positions. It deliberately omits arbitrary widget configuration " "values and never follows a widget into its owning module. Erasure " "deletes the selected account-owned layout after verifying its owner " "and revision; a repeated execution is unchanged. The operation does " "not delete widgets, reports, files, tasks, or any other domain data." ), layer="configured", documentation_types=("admin", "user"), audience=("user", "tenant_admin", "operator", "auditor"), related_modules=("core", "views"), order=19, metadata={ "help_contexts": [ "dashboard.page", "dashboard.action.reset", "privacy.data-subject-requests", ], "consequence_classes": { "export_layout": ( "Returns minimized layout structure, never widget-domain data." ), "delete_layout": ( "Irreversibly removes the selected personal layout only." ), }, }, ), 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. The active interface-module count includes only " "tenant-enabled modules whose WebUI loaded in the current browser session. It intentionally differs from the " "Administration package catalogue, which includes discovered backend-only and headless module manifests." ), 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. The page action bar always reports whether " "the draft is saved, unsaved, or currently saving; Save and Cancel remain visible in stable " "positions and are disabled with an explanation when no draft change exists. Leaving while " "dirty invokes the shared save-or-discard guard. 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