Files
govoplan-dashboard/src/govoplan_dashboard/backend/manifest.py
T

272 lines
12 KiB
Python

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