diff --git a/README.md b/README.md index 5a48385..18ee62b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ This repository owns: - backend module manifest `ops` - operator-facing status APIs +- governance inventory for module-declared permissions, roles, capabilities, + policies, documentation, access-control hooks, search providers, and + migration ownership - deployment profile and sizing assumption summaries - WebUI route contribution `@govoplan/ops-webui` - future operational runbooks that describe the configured platform rather than @@ -21,6 +24,10 @@ This repository owns: Core owns lifecycle management, module discovery, maintenance mode, package installation safety, and shared WebUI shell behavior. +Core exposes the registry contract but does not own an operations dashboard. +Ops projects the provider-neutral registry metadata and runtime checks into the +operator-facing governance surface. + ## Runbooks - `docs/SCALABILITY_PROFILES.md` explains how to use the Ops page with the diff --git a/src/govoplan_ops/backend/api/v1/routes.py b/src/govoplan_ops/backend/api/v1/routes.py index 2699a2a..9c23ef6 100644 --- a/src/govoplan_ops/backend/api/v1/routes.py +++ b/src/govoplan_ops/backend/api/v1/routes.py @@ -58,6 +58,7 @@ def _ops_status_payload(request: Request) -> dict[str, Any]: _deployment_security_check(current_profile), ] readiness = _readiness(checks, maintenance_mode) + governance = _governance_inventory(registry) return { "summary": { "app_env": core_settings.app_env, @@ -72,6 +73,7 @@ def _ops_status_payload(request: Request) -> dict[str, Any]: }, "readiness": readiness, "checks": checks, + "governance": governance, "deployment_profiles": _deployment_profiles(current_profile), "sizing": _sizing_assumptions(), } @@ -84,6 +86,49 @@ def _registry(request: Request) -> PlatformRegistry: return registry +def _governance_inventory(registry: PlatformRegistry) -> dict[str, Any]: + modules: list[dict[str, Any]] = [] + for manifest in registry.manifests(): + capability_names = tuple(sorted(manifest.capability_factories)) + access_control_count = ( + len(manifest.resource_acl_providers) + + len(manifest.ownership_providers) + + sum(len(providers) for providers in manifest.delete_veto_providers.values()) + + len(manifest.uninstall_guard_providers) + ) + modules.append({ + "module_id": manifest.id, + "name": manifest.name, + "version": manifest.version, + "permission_count": len(manifest.permissions), + "role_template_count": len(manifest.role_templates), + "capability_count": len(capability_names), + "policy_count": sum(name.startswith("policy.") for name in capability_names), + "documentation_count": len(manifest.documentation), + "documentation_provider_count": len(manifest.documentation_providers), + "access_control_count": access_control_count, + "search_provider_count": len(manifest.search_providers) + len(manifest.search_sources), + "migration_managed": manifest.migration_spec is not None, + }) + return { + "summary": { + "module_count": len(modules), + "permission_count": sum(item["permission_count"] for item in modules), + "role_template_count": sum(item["role_template_count"] for item in modules), + "capability_count": sum(item["capability_count"] for item in modules), + "policy_count": sum(item["policy_count"] for item in modules), + "documented_module_count": sum( + bool(item["documentation_count"] or item["documentation_provider_count"]) + for item in modules + ), + "access_control_count": sum(item["access_control_count"] for item in modules), + "search_provider_count": sum(item["search_provider_count"] for item in modules), + "migration_module_count": sum(bool(item["migration_managed"]) for item in modules), + }, + "modules": modules, + } + + def _database_status() -> dict[str, Any]: try: with get_database().session() as session: diff --git a/tests/test_governance_inventory.py b/tests/test_governance_inventory.py new file mode 100644 index 0000000..5626659 --- /dev/null +++ b/tests/test_governance_inventory.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import unittest + +from govoplan_core.core.modules import ( + CapabilityDocumentation, + DocumentationTopic, + MigrationSpec, + ModuleManifest, + PermissionDefinition, + RoleTemplate, +) +from govoplan_core.core.registry import PlatformRegistry + +from govoplan_ops.backend.api.v1.routes import _governance_inventory + + +class GovernanceInventoryTests(unittest.TestCase): + def test_inventory_projects_manifest_governance_without_provider_internals(self) -> None: + registry = PlatformRegistry() + registry.register(ModuleManifest( + id="example", + name="Example", + version="1.2.3", + permissions=( + PermissionDefinition( + scope="example:item:read", + label="Read examples", + description="Read example records.", + category="Examples", + level="tenant", + module_id="example", + resource="item", + action="read", + ), + ), + role_templates=( + RoleTemplate( + slug="example_reader", + name="Example reader", + description="Reads examples.", + permissions=("example:item:read",), + ), + ), + capability_factories={ + "example.lookup": lambda _context: object(), + "policy.example": lambda _context: object(), + }, + capability_documentation={ + "example.lookup": CapabilityDocumentation( + label="Example lookup", + summary="Resolves examples.", + ), + }, + documentation=( + DocumentationTopic( + id="example.reference", + title="Example reference", + summary="Documents the example module.", + ), + ), + migration_spec=MigrationSpec(module_id="example"), + )) + + payload = _governance_inventory(registry) + + self.assertEqual(payload["summary"]["module_count"], 1) + self.assertEqual(payload["summary"]["permission_count"], 1) + self.assertEqual(payload["summary"]["role_template_count"], 1) + self.assertEqual(payload["summary"]["capability_count"], 2) + self.assertEqual(payload["summary"]["policy_count"], 1) + self.assertEqual(payload["summary"]["documented_module_count"], 1) + self.assertEqual(payload["summary"]["migration_module_count"], 1) + self.assertEqual(payload["modules"][0]["module_id"], "example") + self.assertNotIn("capability_factories", payload["modules"][0]) + self.assertFalse(any( + callable(value) + for value in payload["modules"][0].values() + )) + + +if __name__ == "__main__": + unittest.main() diff --git a/webui/src/api/ops.ts b/webui/src/api/ops.ts index e687d7d..90af109 100644 --- a/webui/src/api/ops.ts +++ b/webui/src/api/ops.ts @@ -23,6 +23,21 @@ export type OpsSizingAssumption = { operator_note: string; }; +export type OpsGovernanceModule = { + module_id: string; + name: string; + version: string; + permission_count: number; + role_template_count: number; + capability_count: number; + policy_count: number; + documentation_count: number; + documentation_provider_count: number; + access_control_count: number; + search_provider_count: number; + migration_managed: boolean; +}; + export type OpsStatus = { summary: { app_env: string; @@ -49,6 +64,20 @@ export type OpsStatus = { }>; }; checks: OpsCheck[]; + governance: { + summary: { + module_count: number; + permission_count: number; + role_template_count: number; + capability_count: number; + policy_count: number; + documented_module_count: number; + access_control_count: number; + search_provider_count: number; + migration_module_count: number; + }; + modules: OpsGovernanceModule[]; + }; deployment_profiles: OpsDeploymentProfile[]; sizing: OpsSizingAssumption[]; }; diff --git a/webui/src/features/ops/OpsPage.tsx b/webui/src/features/ops/OpsPage.tsx index 472575b..29c2030 100644 --- a/webui/src/features/ops/OpsPage.tsx +++ b/webui/src/features/ops/OpsPage.tsx @@ -14,7 +14,14 @@ import { type ApiSettings, type DataGridColumn } from "@govoplan/core-webui"; -import { fetchOpsStatus, type OpsCheck, type OpsDeploymentProfile, type OpsSizingAssumption, type OpsStatus } from "../../api/ops"; +import { + fetchOpsStatus, + type OpsCheck, + type OpsDeploymentProfile, + type OpsGovernanceModule, + type OpsSizingAssumption, + type OpsStatus +} from "../../api/ops"; export default function OpsPage({ settings }: {settings: ApiSettings;}) { const [status, setStatus] = useState(null); @@ -60,6 +67,8 @@ export default function OpsPage({ settings }: {settings: ApiSettings;}) { + + @@ -70,6 +79,10 @@ export default function OpsPage({ settings }: {settings: ApiSettings;}) { + + + + @@ -84,6 +97,72 @@ export default function OpsPage({ settings }: {settings: ApiSettings;}) { } +function GovernanceTable({ modules }: { modules: OpsGovernanceModule[] }) { + const columns: DataGridColumn[] = [ + { + id: "module", + header: "i18n:govoplan-ops.module.b8ff0289", + width: "minmax(200px, 1fr)", + minWidth: 180, + resizable: true, + sortable: true, + filterable: true, + value: (module) => `${module.name} ${module.module_id} ${module.version}`, + render: (module) =>
{module.name}{module.module_id} · {module.version}
+ }, + { + id: "authority", + header: "i18n:govoplan-ops.authority.8802e425", + width: "minmax(190px, .8fr)", + minWidth: 170, + resizable: true, + value: (module) => `${module.permission_count} ${module.role_template_count}`, + render: (module) => `${module.permission_count} permissions · ${module.role_template_count} roles` + }, + { + id: "contracts", + header: "i18n:govoplan-ops.contracts.57d80902", + width: "minmax(190px, .8fr)", + minWidth: 170, + resizable: true, + value: (module) => `${module.capability_count} ${module.policy_count}`, + render: (module) => `${module.capability_count} capabilities · ${module.policy_count} policies` + }, + { + id: "controls", + header: "i18n:govoplan-ops.controls.0cdb80fb", + width: "minmax(190px, .8fr)", + minWidth: 170, + resizable: true, + value: (module) => `${module.access_control_count} ${module.search_provider_count}`, + render: (module) => `${module.access_control_count} access · ${module.search_provider_count} search` + }, + { + id: "evidence", + header: "i18n:govoplan-ops.evidence.7ea014de", + width: "minmax(190px, .8fr)", + minWidth: 170, + resizable: true, + value: (module) => `${module.documentation_count} ${module.documentation_provider_count} ${module.migration_managed}`, + render: (module) => ( +
+ {module.documentation_count + module.documentation_provider_count} docs + {module.migration_managed ? "migration managed" : "no module migrations"} +
+ ) + } + ]; + return ( + module.module_id} + emptyText="i18n:govoplan-ops.no_modules_reported.847f06d9" + /> + ); +} + function CheckList({ checks }: {checks: OpsCheck[];}) { if (!checks.length) return

i18n:govoplan-ops.no_health_checks_reported.03c067c4

; return ( diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index 477d367..94480b8 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -4,25 +4,36 @@ export const generatedTranslations: PlatformTranslations = { "en": { "i18n:govoplan-ops.area.2745deba": "Area", "i18n:govoplan-ops.baseline.e6ab7982": "Baseline", + "i18n:govoplan-ops.authority.8802e425": "Authority", "i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting", "i18n:govoplan-ops.components.9289473e": "Components", "i18n:govoplan-ops.current_health_checks.7830bccf": "Current health checks", + "i18n:govoplan-ops.contracts.57d80902": "Contracts", + "i18n:govoplan-ops.controls.0cdb80fb": "Controls", + "i18n:govoplan-ops.declared_governance_permissions.d08d3bf1": "Declared governance permissions", "i18n:govoplan-ops.deployment_profiles.b0caa179": "Deployment Profiles", "i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d": "Enabled in the runtime registry", + "i18n:govoplan-ops.evidence.7ea014de": "Evidence", "i18n:govoplan-ops.fit.dab564d8": "Fit", "i18n:govoplan-ops.health_checks.201c869f": "Health Checks", + "i18n:govoplan-ops.governance_inventory.835d8e57": "Governance Inventory", "i18n:govoplan-ops.loading_operations_status.6890fe6e": "Loading operations status...", "i18n:govoplan-ops.modules.04e9462c": "Modules", + "i18n:govoplan-ops.module.b8ff0289": "Module", "i18n:govoplan-ops.no_database_url.51a2db0c": "No database URL", "i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db": "No deployment profiles reported.", "i18n:govoplan-ops.no_health_checks_reported.03c067c4": "No health checks reported.", + "i18n:govoplan-ops.no_modules_reported.847f06d9": "No modules reported.", "i18n:govoplan-ops.no_sizing_assumptions_reported.17515959": "No sizing assumptions reported.", "i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note", "i18n:govoplan-ops.ops.907a54c2": "Ops", + "i18n:govoplan-ops.permissions.842c35eb": "Permissions", + "i18n:govoplan-ops.policies.e7800f56": "Policies", "i18n:govoplan-ops.profile.ff4fc027": "Profile", "i18n:govoplan-ops.readiness.1db9d6fb": "Readiness", "i18n:govoplan-ops.reload.cce71553": "Reload", "i18n:govoplan-ops.redis.5eaa1f2f": "Redis", + "i18n:govoplan-ops.registered_policy_capabilities.112a2b64": "Registered policy capabilities", "i18n:govoplan-ops.no_readiness_blockers.0df259bd": "No readiness blockers", "i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156": "Runtime health, deployment profile, worker split, and sizing assumptions.", "i18n:govoplan-ops.scale_trigger.1c85e10e": "Scale trigger", @@ -34,25 +45,36 @@ export const generatedTranslations: PlatformTranslations = { "de": { "i18n:govoplan-ops.area.2745deba": "Area", "i18n:govoplan-ops.baseline.e6ab7982": "Baseline", + "i18n:govoplan-ops.authority.8802e425": "Berechtigungen", "i18n:govoplan-ops.celery_worker_setting.323d7737": "Celery worker setting", "i18n:govoplan-ops.components.9289473e": "Components", "i18n:govoplan-ops.current_health_checks.7830bccf": "Current health checks", + "i18n:govoplan-ops.contracts.57d80902": "Verträge", + "i18n:govoplan-ops.controls.0cdb80fb": "Kontrollen", + "i18n:govoplan-ops.declared_governance_permissions.d08d3bf1": "Deklarierte Governance-Berechtigungen", "i18n:govoplan-ops.deployment_profiles.b0caa179": "Deployment Profiles", "i18n:govoplan-ops.enabled_in_the_runtime_registry.d2c6142d": "Enabled in the runtime registry", + "i18n:govoplan-ops.evidence.7ea014de": "Nachweise", "i18n:govoplan-ops.fit.dab564d8": "Fit", "i18n:govoplan-ops.health_checks.201c869f": "Health Checks", + "i18n:govoplan-ops.governance_inventory.835d8e57": "Governance-Inventar", "i18n:govoplan-ops.loading_operations_status.6890fe6e": "Loading operations status...", "i18n:govoplan-ops.modules.04e9462c": "Module", + "i18n:govoplan-ops.module.b8ff0289": "Modul", "i18n:govoplan-ops.no_database_url.51a2db0c": "No database URL", "i18n:govoplan-ops.no_deployment_profiles_reported.7c3af1db": "No deployment profiles reported.", "i18n:govoplan-ops.no_health_checks_reported.03c067c4": "No health checks reported.", + "i18n:govoplan-ops.no_modules_reported.847f06d9": "Keine Module gemeldet.", "i18n:govoplan-ops.no_sizing_assumptions_reported.17515959": "No sizing assumptions reported.", "i18n:govoplan-ops.operator_note.1dc58f7b": "Operator note", "i18n:govoplan-ops.ops.907a54c2": "Betrieb", + "i18n:govoplan-ops.permissions.842c35eb": "Berechtigungen", + "i18n:govoplan-ops.policies.e7800f56": "Richtlinien", "i18n:govoplan-ops.profile.ff4fc027": "Profil", "i18n:govoplan-ops.readiness.1db9d6fb": "Bereitschaft", "i18n:govoplan-ops.reload.cce71553": "Neu laden", "i18n:govoplan-ops.redis.5eaa1f2f": "Redis", + "i18n:govoplan-ops.registered_policy_capabilities.112a2b64": "Registrierte Richtlinien-Fähigkeiten", "i18n:govoplan-ops.no_readiness_blockers.0df259bd": "Keine Bereitschaftsblocker", "i18n:govoplan-ops.runtime_health_deployment_profile_worker_split_a.55340156": "Runtime health, deployment profile, worker split, and sizing assumptions.", "i18n:govoplan-ops.scale_trigger.1c85e10e": "Scale trigger",