From e2bf104c53dc158cdead2dfba2936d364635c4be Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Fri, 31 Jul 2026 04:21:34 +0200 Subject: [PATCH] feat: expose capability and catalog documentation --- README.md | 24 +++ src/govoplan_docs/backend/manifest.py | 17 -- src/govoplan_docs/backend/sources.py | 236 +++++++++++++++++++++++- tests/test_docs_context.py | 112 ++++++++++- webui/src/api/docs.ts | 29 +++ webui/src/features/docs/DocsPage.tsx | 169 +++++++++++++++-- webui/src/i18n/generatedTranslations.ts | 12 ++ 7 files changed, 555 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 6babf9f..76bfb74 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,30 @@ Frontend package: Platform module manifests, configuration packages, release catalogs, and governance rules are documented in `govoplan-core/docs/`. +Capabilities can provide generic documentation without exposing their runtime +provider implementation: + +```python +ModuleManifest( + capability_factories={"example.lookup": build_lookup}, + capability_documentation={ + "example.lookup": CapabilityDocumentation( + label="Example lookup", + summary="Resolves records through the versioned lookup contract.", + contract_version="2", + stability="stable", + audience=("module_admin",), + ), + }, +) +``` + +Docs also projects the configured module release catalog and configuration +package catalog through the public Core catalog contracts. Catalog trust, +freshness, provenance, descriptions, and package requirements remain visible +as typed evidence; provider objects, credentials, and secret configuration are +never imported into the Docs WebUI. + ## Concept documents - `docs/DOCUMENTATION_LAYER_CONCEPT.md` defines the configured, available, and evidence documentation model. diff --git a/src/govoplan_docs/backend/manifest.py b/src/govoplan_docs/backend/manifest.py index e2c69fe..62af601 100644 --- a/src/govoplan_docs/backend/manifest.py +++ b/src/govoplan_docs/backend/manifest.py @@ -195,23 +195,6 @@ manifest = ModuleManifest( ), ), documentation_sources=( - DocumentationSourceDefinition( - id="docs.configuration.packages", - kind="configuration_package", - label="Configuration package catalog", - condition=DocumentationCondition( - any_scopes=("system:settings:read", "admin:settings:read"), - ), - provenance={ - "source": "core_configuration_package_contract", - "version": "1", - }, - inspection={ - "package_id": "govoplan.configuration-packages", - "schema_version": "1", - "description": "Signed, preflighted platform configuration packages.", - }, - ), DocumentationSourceDefinition( id="docs.project.wiki", kind="wiki", diff --git a/src/govoplan_docs/backend/sources.py b/src/govoplan_docs/backend/sources.py index ff4a3a1..669aac2 100644 --- a/src/govoplan_docs/backend/sources.py +++ b/src/govoplan_docs/backend/sources.py @@ -58,18 +58,58 @@ class RouteInspection(_SourceModel): class CapabilityInspection(_SourceModel): kind: Literal["capability"] = "capability" capability: str + label: str | None = None + summary: str | None = None + contract_version: str | None = None + stability: Literal["experimental", "stable", "deprecated"] | None = None + audience: list[str] = Field(default_factory=list) class PolicyInspection(_SourceModel): kind: Literal["policy"] = "policy" capability: str + label: str | None = None + summary: str | None = None + contract_version: str | None = None + stability: Literal["experimental", "stable", "deprecated"] | None = None + audience: list[str] = Field(default_factory=list) + + +class ReleaseCatalogEntryInspection(_SourceModel): + id: str + name: str + version: str | None = None + description: str | None = None + action: str | None = None + tags: list[str] = Field(default_factory=list) + + +class ReleaseCatalogInspection(_SourceModel): + kind: Literal["release_catalog"] = "release_catalog" + catalog_type: Literal["modules", "configuration_packages"] + channel: str | None = None + sequence: int | None = None + generated_at: str | None = None + entry_count: int + signed: bool + trusted: bool + cache_used: bool + warnings: list[str] = Field(default_factory=list) + entries: list[ReleaseCatalogEntryInspection] = Field(default_factory=list) class ConfigurationPackageInspection(_SourceModel): kind: Literal["configuration_package"] = "configuration_package" package_id: str + name: str | None = None + version: str | None = None schema_version: str | None = None description: str | None = None + publisher: str | None = None + category: str | None = None + tags: list[str] = Field(default_factory=list) + required_modules: list[str] = Field(default_factory=list) + required_capabilities: list[str] = Field(default_factory=list) class WikiInspection(_SourceModel): @@ -90,6 +130,7 @@ DocumentationSourceInspection = Annotated[ | RouteInspection | CapabilityInspection | PolicyInspection + | ReleaseCatalogInspection | ConfigurationPackageInspection | WikiInspection | RepositoryInspection, @@ -142,6 +183,8 @@ class DocumentationSourceRegistry: def build_documentation_source_registry( manifests: tuple[ModuleManifest, ...], + *, + include_runtime_catalogs: bool = True, ) -> DocumentationSourceRegistry: registry = DocumentationSourceRegistry() for manifest in manifests: @@ -155,6 +198,9 @@ def build_documentation_source_registry( for source in _linked_sources(manifest): if registry.get(source.item.id) is None: registry.register(source) + if include_runtime_catalogs: + for source in _catalog_sources(): + registry.register(source) return registry @@ -203,21 +249,37 @@ def _route_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSou def _capability_sources(manifest: ModuleManifest) -> tuple[RegisteredDocumentationSource, ...]: sources: list[RegisteredDocumentationSource] = [] for capability in sorted(manifest.capability_factories): + metadata = manifest.capability_documentation.get(capability) is_policy = capability.startswith("policy.") kind = "policy" if is_policy else "capability" inspection: DocumentationSourceInspection = ( - PolicyInspection(capability=capability) + PolicyInspection( + capability=capability, + label=metadata.label if metadata else None, + summary=metadata.summary if metadata else None, + contract_version=metadata.contract_version if metadata else None, + stability=metadata.stability if metadata else None, + audience=list(metadata.audience) if metadata else [], + ) if is_policy - else CapabilityInspection(capability=capability) + else CapabilityInspection( + capability=capability, + label=metadata.label if metadata else None, + summary=metadata.summary if metadata else None, + contract_version=metadata.contract_version if metadata else None, + stability=metadata.stability if metadata else None, + audience=list(metadata.audience) if metadata else [], + ) ) sources.append(_registered_source( source_id=_derived_source_id(manifest.id, kind, capability), kind=kind, owner_module_id=manifest.id, - label=f"{manifest.name} {kind} {capability}", + label=metadata.label if metadata else f"{manifest.name} {kind} {capability}", provenance={"source": "module_manifest", "version": manifest.version}, inspection=inspection, condition=DocumentationCondition(required_modules=(manifest.id,)), + documentation_types=metadata.documentation_types if metadata else ("admin",), )) return tuple(sources) @@ -240,6 +302,7 @@ def _defined_source( condition=definition.condition, documentation_types=definition.documentation_types, state=definition.state, + state_reason=definition.state_reason, configuration_key=definition.configuration_key, ) @@ -249,8 +312,15 @@ def _defined_inspection(definition: DocumentationSourceDefinition) -> Documentat if definition.kind == "configuration_package": return ConfigurationPackageInspection( package_id=str(safe.get("package_id") or definition.id), + name=_optional_text(safe.get("name")), + version=_optional_text(safe.get("version")), schema_version=_optional_text(safe.get("schema_version")), description=_optional_text(safe.get("description")), + publisher=_optional_text(safe.get("publisher")), + category=_optional_text(safe.get("category")), + tags=_string_list(safe.get("tags")), + required_modules=_string_list(safe.get("required_modules")), + required_capabilities=_string_list(safe.get("required_capabilities")), ) href = definition.link.href if definition.link else str(safe.get("href") or "") if definition.kind == "wiki": @@ -271,9 +341,23 @@ def _defined_inspection(definition: DocumentationSourceDefinition) -> Documentat order=int(safe.get("order") or 0), ) if definition.kind == "policy": - return PolicyInspection(capability=str(safe.get("capability") or definition.id)) + return PolicyInspection( + capability=str(safe.get("capability") or definition.id), + label=_optional_text(safe.get("label")), + summary=_optional_text(safe.get("summary")), + contract_version=_optional_text(safe.get("contract_version")), + stability=_capability_stability(safe.get("stability")), + audience=_string_list(safe.get("audience")), + ) if definition.kind == "capability": - return CapabilityInspection(capability=str(safe.get("capability") or definition.id)) + return CapabilityInspection( + capability=str(safe.get("capability") or definition.id), + label=_optional_text(safe.get("label")), + summary=_optional_text(safe.get("summary")), + contract_version=_optional_text(safe.get("contract_version")), + stability=_capability_stability(safe.get("stability")), + audience=_string_list(safe.get("audience")), + ) return ManifestInspection( module_id=str(safe.get("module_id") or definition.id.split(".", 1)[0]), name=str(safe.get("name") or definition.label), @@ -319,6 +403,7 @@ def _registered_source( condition: DocumentationCondition | None = None, documentation_types: tuple[DocumentationType, ...] = ("admin",), state: Literal["configured", "disabled", "unavailable"] = "configured", + state_reason: str | None = None, configuration_key: str | None = None, ) -> RegisteredDocumentationSource: clean_provenance = _safe_source_fields(provenance) @@ -329,6 +414,7 @@ def _registered_source( owner_module_id=owner_module_id, label=label, state=state, + state_reason=state_reason, inspection_url=f"/api/v1/docs/sources/{source_id}", provenance=DocumentationSourceProvenance( source=str(clean_provenance.get("source") or "unknown"), @@ -346,6 +432,130 @@ def _registered_source( ) +def _catalog_sources() -> tuple[RegisteredDocumentationSource, ...]: + from govoplan_core.core.configuration_packages import validate_configuration_package_catalog + from govoplan_core.core.module_package_catalog import validate_module_package_catalog + + return ( + *_release_catalog_sources( + "modules", + "Module release catalog", + validate_module_package_catalog(), + ), + *_release_catalog_sources( + "configuration_packages", + "Configuration package catalog", + validate_configuration_package_catalog(), + ), + ) + + +def _release_catalog_sources( + catalog_type: Literal["modules", "configuration_packages"], + label: str, + validation: Mapping[str, object], +) -> tuple[RegisteredDocumentationSource, ...]: + entry_key = "modules" if catalog_type == "modules" else "packages" + raw_entries = validation.get(entry_key) + entries = [item for item in raw_entries if isinstance(item, Mapping)] if isinstance(raw_entries, list) else [] + configured = bool(validation.get("configured")) + valid = bool(validation.get("valid")) + state: Literal["configured", "disabled", "unavailable"] = ( + "configured" if configured and valid else "unavailable" if configured else "disabled" + ) + reason = _optional_text(validation.get("error")) + if state == "disabled": + reason = "No catalog source is configured." + catalog_id = f"docs.release-catalog.{catalog_type.replace('_', '-')}" + catalog_source = _registered_source( + source_id=catalog_id, + kind="release_catalog", + owner_module_id="docs", + label=label, + provenance={ + "source": "core_catalog_contract", + "published_at": validation.get("generated_at"), + }, + inspection=ReleaseCatalogInspection( + catalog_type=catalog_type, + channel=_optional_text(validation.get("channel")), + sequence=_optional_int(validation.get("sequence")), + generated_at=_optional_text(validation.get("generated_at")), + entry_count=len(entries), + signed=bool(validation.get("signed")), + trusted=bool(validation.get("trusted")), + cache_used=bool(validation.get("cache_used")), + warnings=_string_list(validation.get("warnings")), + entries=[_release_catalog_entry(item, catalog_type) for item in entries], + ), + state=state, + state_reason=reason, + ) + if catalog_type == "modules" or not valid: + return (catalog_source,) + return ( + catalog_source, + *( + _configuration_package_source(item, validation) + for item in entries + ), + ) + + +def _release_catalog_entry( + item: Mapping[str, object], + catalog_type: Literal["modules", "configuration_packages"], +) -> ReleaseCatalogEntryInspection: + item_id = ( + _optional_text(item.get("module_id")) + if catalog_type == "modules" + else _optional_text(item.get("package_id")) + ) + return ReleaseCatalogEntryInspection( + id=item_id or "unknown", + name=_optional_text(item.get("name")) or item_id or "Unknown", + version=_optional_text(item.get("version")), + description=_optional_text(item.get("description")), + action=_optional_text(item.get("action")), + tags=_string_list(item.get("tags")), + ) + + +def _configuration_package_source( + item: Mapping[str, object], + validation: Mapping[str, object], +) -> RegisteredDocumentationSource: + package_id = _optional_text(item.get("package_id")) or "unknown" + required_modules = [ + str(requirement.get("module_id")) + for requirement in item.get("required_modules", ()) + if isinstance(requirement, Mapping) and requirement.get("module_id") + ] if isinstance(item.get("required_modules"), (list, tuple)) else [] + return _registered_source( + source_id=_derived_source_id("docs", "configuration-package", package_id), + kind="configuration_package", + owner_module_id="docs", + label=_optional_text(item.get("name")) or package_id, + provenance={ + "source": "configuration_package_catalog", + "version": item.get("version"), + "published_at": validation.get("generated_at"), + }, + inspection=ConfigurationPackageInspection( + package_id=package_id, + name=_optional_text(item.get("name")), + version=_optional_text(item.get("version")), + schema_version=_optional_text(item.get("schema_version")), + description=_optional_text(item.get("description")), + publisher=_optional_text(item.get("publisher")), + category=_optional_text(item.get("category")), + tags=_string_list(item.get("tags")), + required_modules=required_modules, + required_capabilities=_string_list(item.get("required_capabilities")), + ), + ) + + def _visibility_payload( condition: DocumentationCondition, documentation_types: tuple[DocumentationType, ...], @@ -385,6 +595,22 @@ def _optional_text(value: object) -> str | None: return text[:2_000] if text else None +def _optional_int(value: object) -> int | None: + if value is None: + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + +def _capability_stability( + value: object, +) -> Literal["experimental", "stable", "deprecated"] | None: + text = _optional_text(value) + return text if text in {"experimental", "stable", "deprecated"} else None + + def _string_list(value: object) -> list[str]: if not isinstance(value, (list, tuple)): return [] diff --git a/tests/test_docs_context.py b/tests/test_docs_context.py index b8d19c9..a3a4779 100644 --- a/tests/test_docs_context.py +++ b/tests/test_docs_context.py @@ -10,6 +10,7 @@ from fastapi import HTTPException from govoplan_access.backend.manifest import get_manifest as get_access_manifest from govoplan_core.core.modules import DocumentationCondition, DocumentationLink, DocumentationTopic, ModuleManifest, NavItem from govoplan_core.core.modules import ( + CapabilityDocumentation, DocumentationConfigurationDecision, DocumentationConfigurationProviderRegistration, DocumentationSourceDefinition, @@ -27,7 +28,10 @@ from govoplan_docs.backend.api.v1.routes import ( docs_context, ) from govoplan_docs.backend.manifest import get_manifest as get_docs_manifest -from govoplan_docs.backend.sources import build_documentation_source_registry +from govoplan_docs.backend.sources import ( + _release_catalog_sources, + build_documentation_source_registry, +) class FakePrincipal: @@ -446,6 +450,14 @@ class DocsContextTests(unittest.TestCase): "example.lookup": lambda _context: object(), "policy.example": lambda _context: object(), }, + capability_documentation={ + "example.lookup": CapabilityDocumentation( + label="Example lookup", + summary="Resolves example records through a stable provider contract.", + contract_version="2", + audience=("module_admin",), + ), + }, frontend=FrontendModule( module_id="example", routes=( @@ -490,7 +502,10 @@ class DocsContextTests(unittest.TestCase): ), ) - sources = build_documentation_source_registry((manifest,)).sources() + sources = build_documentation_source_registry( + (manifest,), + include_runtime_catalogs=False, + ).sources() self.assertEqual( {source.item.kind for source in sources}, @@ -505,6 +520,14 @@ class DocsContextTests(unittest.TestCase): }, ) self.assertNotIn("must-not-leak", repr(sources)) + capability_source = next( + source for source in sources + if source.item.kind == "capability" + ) + self.assertEqual(capability_source.item.label, "Example lookup") + self.assertEqual(capability_source.item.inspection.summary, "Resolves example records through a stable provider contract.") + self.assertEqual(capability_source.item.inspection.contract_version, "2") + self.assertEqual(capability_source.item.inspection.audience, ["module_admin"]) for source in sources: self.assertTrue(source.item.id.startswith("example.")) self.assertEqual("example", source.item.owner_module_id) @@ -513,6 +536,86 @@ class DocsContextTests(unittest.TestCase): source.item.inspection_url, ) + def test_release_and_configuration_catalogs_become_typed_sources(self) -> None: + release_sources = _release_catalog_sources( + "modules", + "Module release catalog", + { + "configured": True, + "valid": True, + "channel": "stable", + "sequence": 12, + "generated_at": "2026-07-31T10:00:00Z", + "signed": True, + "trusted": True, + "cache_used": False, + "warnings": [], + "modules": [{ + "module_id": "files", + "name": "Files", + "version": "0.1.10", + "description": "Managed files.", + "action": "install", + "tags": ["official"], + }], + }, + ) + self.assertEqual(len(release_sources), 1) + release = release_sources[0].item + self.assertEqual(release.kind, "release_catalog") + self.assertEqual(release.state, "configured") + self.assertEqual(release.inspection.entry_count, 1) + self.assertEqual(release.inspection.entries[0].description, "Managed files.") + + configuration_sources = _release_catalog_sources( + "configuration_packages", + "Configuration package catalog", + { + "configured": True, + "valid": True, + "channel": "stable", + "signed": True, + "trusted": True, + "packages": [{ + "package_id": "public-service.base", + "name": "Public service base", + "version": "3", + "description": "Baseline configuration for a public service.", + "required_modules": [{"module_id": "access"}], + "required_capabilities": ["policy.retention"], + "tags": ["reference"], + }], + }, + ) + self.assertEqual(len(configuration_sources), 2) + package = next( + source.item for source in configuration_sources + if source.item.kind == "configuration_package" + ) + self.assertEqual(package.inspection.package_id, "public-service.base") + self.assertEqual(package.inspection.required_modules, ["access"]) + self.assertEqual(package.inspection.required_capabilities, ["policy.retention"]) + + def test_docs_manifest_links_repository_and_wiki_sources(self) -> None: + sources = build_documentation_source_registry( + (get_docs_manifest(),), + include_runtime_catalogs=False, + ).sources() + + source_kinds = {source.item.kind for source in sources} + self.assertIn("repository", source_kinds) + self.assertIn("wiki", source_kinds) + self.assertTrue(any( + source.item.inspection.href.endswith("/govoplan-docs/wiki") + for source in sources + if source.item.kind == "wiki" + )) + self.assertTrue(any( + "DOCUMENTATION_LAYER_CONCEPT.md" in source.item.inspection.href + for source in sources + if source.item.kind == "repository" + )) + def test_source_visibility_hides_unauthorized_ids(self) -> None: manifest = ModuleManifest( id="example", @@ -536,7 +639,10 @@ class DocsContextTests(unittest.TestCase): ) registry = PlatformRegistry() registry.register(manifest) - sources = build_documentation_source_registry((manifest,)).sources() + sources = build_documentation_source_registry( + (manifest,), + include_runtime_catalogs=False, + ).sources() denied = _visible_documentation_sources( sources, diff --git a/webui/src/api/docs.ts b/webui/src/api/docs.ts index 2fd84e1..69e5431 100644 --- a/webui/src/api/docs.ts +++ b/webui/src/api/docs.ts @@ -70,6 +70,20 @@ export type DocsSource = { }; }; +export type DocsSourceDetail = DocsSource & { + visibility: { + documentation_types: string[]; + required_modules: string[]; + any_modules: string[]; + missing_modules: string[]; + required_capabilities: string[]; + required_scopes: string[]; + any_scopes: string[]; + configuration_keys: string[]; + }; + inspection: Record; +}; + export type DocsDocumentationCondition = { required_modules: string[]; any_modules: string[]; @@ -186,3 +200,18 @@ export function fetchDocsContext(settings: ApiSettings, options: { documentation const query = params.toString(); return apiFetch(settings, `/api/v1/docs/context${query ? `?${query}` : ""}`); } + +export function fetchDocsSource( + settings: ApiSettings, + sourceId: string, + options: { documentationType?: "admin" | "user"; locale?: string } = {} +): Promise { + const params = new URLSearchParams(); + if (options.documentationType) params.set("type", options.documentationType); + if (options.locale) params.set("locale", options.locale); + const query = params.toString(); + return apiFetch( + settings, + `/api/v1/docs/sources/${encodeURIComponent(sourceId)}${query ? `?${query}` : ""}` + ); +} diff --git a/webui/src/features/docs/DocsPage.tsx b/webui/src/features/docs/DocsPage.tsx index 3a9a405..d87b446 100644 --- a/webui/src/features/docs/DocsPage.tsx +++ b/webui/src/features/docs/DocsPage.tsx @@ -1,9 +1,10 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { Link, useLocation } from "react-router"; -import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react"; +import { ChevronDown, ChevronRight, Eye, RefreshCw } from "lucide-react"; import { Button, DataGrid, + Dialog, DismissibleAlert, ExplorerTree, LoadingFrame, @@ -18,12 +19,14 @@ import { } from "@govoplan/core-webui"; import { fetchDocsContext, + fetchDocsSource, type DocsContext, type DocsDocumentationTopic, type DocsModule, type DocsOptionalModuleEvidence, type DocsRoute, - type DocsSource + type DocsSource, + type DocsSourceDetail } from "../../api/docs"; type DocumentationType = "admin" | "user"; @@ -189,6 +192,8 @@ export default function DocsPage({ settings }: { settings: ApiSettings }) { grantedPermissions={grantedPermissions} evidenceModules={context?.layers.evidence.optional_modules ?? []} evidenceSources={context?.layers.evidence.sources ?? []} + settings={settings} + locale={locale} /> @@ -269,7 +274,9 @@ function SelectedPageContent({ availableRoutes, grantedPermissions, evidenceModules, - evidenceSources + evidenceSources, + settings, + locale }: { page: DocsPageNode | null; adminDocs: boolean; @@ -281,6 +288,8 @@ function SelectedPageContent({ grantedPermissions: Array<{ scope: string; label: string; category: string }>; evidenceModules: DocsOptionalModuleEvidence[]; evidenceSources: DocsSource[]; + settings: ApiSettings; + locale: string; }) { if (!page) { return ( @@ -310,7 +319,13 @@ function SelectedPageContent({

i18n:govoplan-docs.granted_permissions.0a232e78

- +
); @@ -965,26 +980,142 @@ function PermissionList({ permissions }: { permissions: Array<{ scope: string; l ); } -function EvidenceList({ modules, sources }: { modules: DocsOptionalModuleEvidence[]; sources: DocsSource[] }) { +function EvidenceList({ + modules, + sources, + settings, + documentationType, + locale +}: { + modules: DocsOptionalModuleEvidence[]; + sources: DocsSource[]; + settings: ApiSettings; + documentationType: DocumentationType; + locale: string; +}) { + const [selected, setSelected] = useState(null); + const [loadingSourceId, setLoadingSourceId] = useState(""); + const [sourceError, setSourceError] = useState(""); + if (!modules.length && !sources.length) return

i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6

; + + async function inspectSource(source: DocsSource) { + setLoadingSourceId(source.id); + setSourceError(""); + try { + setSelected(await fetchDocsSource(settings, source.id, { documentationType, locale })); + } catch (error) { + setSourceError(adminErrorMessage(error)); + } finally { + setLoadingSourceId(""); + } + } + return ( -
- {modules.map((item) => -
-
-
{item.module_id} · {item.reason}
-
- )} - {sources.map((item) => -
-
-
{item.label} · {item.owner_module_id} · {item.kind} · {item.provenance.source}
-
- )} -
+ <> + {sourceError && {sourceError}} +
+ {modules.map((item) => +
+
+
{item.module_id} · {item.reason}
+
+ )} + {sources.map((item) => +
+
+
+ {item.label} + · {item.owner_module_id} · {item.kind} · {item.provenance.source} + {item.state_reason && {item.state_reason}} +
+
+ +
+
+ )} +
+ setSelected(null)} + footer={} + > + {selected && } + + ); } +function SourceInspection({ source }: { source: DocsSourceDetail }) { + const provenance = compactSourceRecord(source.provenance); + const visibility = compactSourceRecord(source.visibility); + const inspection = compactSourceRecord(source.inspection); + return ( +
+
+ + {source.state_reason &&

{source.state_reason}

} +
+ + + +
+ ); +} + +function SourceInspectionGroup({ title, values }: { title: string; values: Array<[string, unknown]> }) { + if (!values.length) return null; + return ( +
+

{title}

+
+ {values.map(([key, value]) => +
+
{humanizeSourceKey(key)}
+
{formatSourceValue(value)}
+
+ )} +
+
+ ); +} + +function compactSourceRecord(value: object): Array<[string, unknown]> { + return Object.entries(value).filter(([, item]) => { + if (item === null || item === undefined || item === "") return false; + return !Array.isArray(item) || item.length > 0; + }); +} + +function humanizeSourceKey(value: string): string { + const words = value.replaceAll("_", " "); + return words.charAt(0).toUpperCase() + words.slice(1); +} + +function formatSourceValue(value: unknown): string { + if (Array.isArray(value)) { + if (value.every((item) => ["string", "number", "boolean"].includes(typeof item))) { + return value.join(", "); + } + return JSON.stringify(value, null, 2); + } + if (value && typeof value === "object") return JSON.stringify(value, null, 2); + return String(value); +} + function routeRequirements(route: DocsRoute): string { const parts = []; if (route.required_all.length) parts.push(`all: ${route.required_all.join(", ")}`); diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index 34c98dd..c16768c 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -74,6 +74,12 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-docs.screen.c4878ec4": "Screen", "i18n:govoplan-docs.section.5e498158": "Section", "i18n:govoplan-docs.source.6da13add": "Source", + "i18n:govoplan-docs.source_details.6dc79c75": "Source details", + "i18n:govoplan-docs.inspect_source.bcc1739d": "Inspect source", + "i18n:govoplan-docs.inspection.83dc17ca": "Inspection", + "i18n:govoplan-docs.provenance.73e80298": "Provenance", + "i18n:govoplan-docs.visibility.80ab5798": "Visibility", + "i18n:govoplan-docs.close.87b84f71": "Close", "i18n:govoplan-docs.status.bae7d5be": "Status", "i18n:govoplan-docs.steps.6041435e": "Steps", "i18n:govoplan-docs.summary.d6b9936d": "Summary", @@ -165,6 +171,12 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-docs.screen.c4878ec4": "Ansicht", "i18n:govoplan-docs.section.5e498158": "Bereich", "i18n:govoplan-docs.source.6da13add": "Quelle", + "i18n:govoplan-docs.source_details.6dc79c75": "Quelldetails", + "i18n:govoplan-docs.inspect_source.bcc1739d": "Quelle anzeigen", + "i18n:govoplan-docs.inspection.83dc17ca": "Prüfdaten", + "i18n:govoplan-docs.provenance.73e80298": "Herkunft", + "i18n:govoplan-docs.visibility.80ab5798": "Sichtbarkeit", + "i18n:govoplan-docs.close.87b84f71": "Schließen", "i18n:govoplan-docs.status.bae7d5be": "Status", "i18n:govoplan-docs.steps.6041435e": "Schritte", "i18n:govoplan-docs.summary.d6b9936d": "Zusammenfassung",