chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:21 +02:00
parent 670f0d9b6f
commit 5413ddfc32
22 changed files with 2732 additions and 27 deletions

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/
.venv/
build/
dist/
node_modules/
webui/node_modules/
webui/dist/
*.tsbuildinfo
.component-test-build/
.module-test-build/
.policy-test-build/
.template-preview-test-build/
.import-test-build/
webui/.component-test-build/
webui/.module-test-build/
webui/.policy-test-build/
webui/.template-preview-test-build/
webui/.import-test-build/

View File

@@ -42,3 +42,9 @@ Frontend package:
``` ```
Platform module manifests, configuration packages, release catalogs, and governance rules are documented in `govoplan-core/docs/`. Platform module manifests, configuration packages, release catalogs, and governance rules are documented in `govoplan-core/docs/`.
## Concept documents
- `docs/DOCUMENTATION_LAYER_CONCEPT.md` defines the configured, available, and evidence documentation model.
- `docs/INSTANCE_AWARE_DOCUMENTATION.md` defines the runtime context and condition model.
- `docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md` defines the workflow, structure/reference, and design-pattern documentation experience.

View File

@@ -1,27 +0,0 @@
# Docs Backlog
## P1 Immediate
- [ ] Define the `docs` backend manifest, permissions, module metadata, and route contribution.
- [ ] Define documentation source DTOs for manifests, routes, capabilities, policies, configuration packages, and wiki-backed sources.
- [ ] Implement configured, available, and evidence classification for documentation entries.
- [ ] Add API routes for current-context documentation lookup and source inspection.
- [ ] Create focused tests for role-aware and configuration-aware documentation filtering.
## P2 Module Integration
- [ ] Define module manifest fields for documentation contributions.
- [ ] Define capability metadata fields that the docs module can render without importing provider internals.
- [ ] Integrate release catalog and configuration package descriptions as documentation sources.
- [ ] Link repository docs and synced wiki pages as durable evidence sources.
## P2 WebUI
- [ ] Build the documentation browser route contribution and navigation metadata.
- [ ] Build configured-system, available-feature, and evidence views.
- [ ] Build route-aware help entry points from the shared WebUI shell.
## P3 Governance
- [ ] Document ownership rules for runtime docs, repository docs, Gitea wiki pages, and active Gitea issues.
- [ ] Document privacy and permission boundaries for role-aware documentation.

View File

@@ -0,0 +1,293 @@
# Documentation Experience Concept
GovOPlaN Docs should support three ways of understanding the configured
platform:
- outcome-oriented workflow guidance
- structure-oriented reference documentation
- shared design-pattern explanations
These are different reading modes over the same configured-system knowledge.
They should link to each other, respect user and admin perspectives, and keep
the current installation as the primary source of truth.
## Goals
The docs experience should answer these questions first:
- What can I do in this system?
- Which steps do I take to finish this task?
- What does this screen or field mean?
- Why does the interface behave this way?
- For admins: which API, permission, policy, or configuration source backs this
screen?
It should not start from a full product manual. The current tenant, installed
modules, enabled routes, permissions, and configuration decide what is shown as
the default path.
Tracking issue: `add-ideas/govoplan-docs#15`.
## Editorial Pillars
### Workflow Guidance
Workflow docs are written from the reader's intent:
- "I want to create an API key."
- "I want to add a user to a group."
- "I want to configure a reusable mail sender."
- "I want to understand why an option is disabled."
Each workflow topic should have:
- outcome
- audience, usually user or admin
- prerequisites and blockers
- steps using visible interface labels
- expected result
- verification or review step when relevant
- related structure topics
- related design patterns
User workflow topics avoid API paths, internal module ids, raw permission names,
and server configuration unless the user must act on them. They should explain
who can help when a feature is not available.
Admin workflow topics can include technical context: API endpoints, required
permissions, policy sources, configuration keys, migration or audit effects, and
operator escalation paths.
### Structure Reference
Structure docs describe what is visible on a screen and how it maps to platform
contracts.
For users, structure reference is a field glossary:
- screen section
- visible label
- short plain-language meaning
- allowed values or validation
- where the value appears later
- common mistakes or limits
For admins, structure reference adds technical columns:
- route and module
- API method and path
- request or response field
- configuration key or policy field
- permission or scope
- provenance or inheritance rule
- audit/event implication
- safety or approval requirement
Structure docs should be linked from workflow steps and from inline field help.
They should also link back to the workflows where the field is commonly used.
### Design Patterns
Pattern docs explain repeated platform behavior once, then link to examples.
They are not implementation tutorials; they explain what a reader should expect
when they see a familiar affordance.
Initial pattern topics should include:
- field help marker next to labels
- disabled action and blocker explanation
- advanced options panel
- guided wizard and review step
- policy provenance and effective value rows
- status badges and problem lists
- in-app help menu and route-specific help context
- evidence, available, and configured documentation layers
The field help marker is the immediate example. In the app, `FieldLabel` can
render `InlineHelp` beside form labels, and `helpForFieldLabel` currently maps
known labels to short help text. The docs layer should treat this as a pattern:
brief local explanation beside the field, with a link to the fuller structure or
workflow topic when more detail is needed.
## Audience Model
The docs source model may be shared, but presentation must be audience-specific.
### User View
User docs should be calm, task-focused, and plain-language:
- start from "I want to..."
- use current interface labels
- explain limits and unavailable options without exposing internals
- show who can change configuration or permissions
- link to short field explanations and general patterns
User docs should not require knowledge of APIs, scopes, module ids, deployment
configuration, or policy source chains.
### Admin View
Admin docs should still be workflow-centered, but they may be technical:
- start from tenant, module, access, policy, and operator tasks
- expose API endpoints, DTO fields, permissions, module ids, capabilities, and
configuration keys
- show route and field mappings
- explain provenance, inheritance, audit, and approval behavior
- include evidence for installed-but-disabled and optional capabilities
Admin docs are the right place to map an interface field to the API field and
explain operational consequences.
## Presentation Shape
The docs module should remain part of the platform because it needs the current
configuration, registry, permissions, modules, routes, and policy state. The
presentation should still feel like a documentation interface, not like another
business module.
The recommended shape is:
- keep the `docs` backend module and configured-system API
- expose a full-page Help Center route served by the same server
- use lighter documentation chrome instead of normal module-workspace density
- keep route-aware entry points from the app shell, help menu, field help, and
admin screens
- allow direct links such as `/docs?type=user&topic=...` or
`/docs?type=admin&route=...`
- label the content as documentation for "this system" rather than as a generic
marketing or product manual
This preserves third-person reference behavior while keeping the runtime
benefits of a docs module. The server can still filter and explain content based
on the actual installation.
## Navigation Model
The Help Center should have primary modes:
- Workflows
- Reference
- Patterns
- This system
`This system` is the configured/available/evidence view that already exists in
the first docs module slice. It should remain useful for admins and diagnostics,
but normal users should mostly enter through Workflows or contextual help.
Each topic should support stable anchors and related-topic links:
- workflow topics link to fields and patterns
- field/reference topics link to workflows and API details when the reader is an
admin
- pattern topics link to examples and the components that implement them
- unavailable topics link to the blocker and the actor who can resolve it
## Source Contract Direction
The current `DocumentationTopic` contract can carry the first version through
`metadata`, `documentation_types`, `conditions`, `links`, `related_modules`, and
`unlocks`. As the docs experience matures, typed source DTOs should be added for
clarity.
Recommended topic kinds:
- `workflow`
- `reference`
- `pattern`
- `system`
Recommended workflow metadata:
- `outcome`
- `steps`
- `prerequisites`
- `result`
- `verification`
- `related_field_ids`
- `related_pattern_ids`
Recommended reference metadata:
- `route`
- `screen`
- `section`
- `fields`
- `api`
- `permissions`
- `configuration_keys`
- `policy_sources`
Recommended field metadata:
- `field_id`
- `label`
- `user_description`
- `admin_description`
- `api_field`
- `api_path`
- `configuration_key`
- `permission_scope`
- `validation`
- `provenance`
- `audit_event`
Recommended pattern metadata:
- `pattern_id`
- `purpose`
- `when_used`
- `user_explanation`
- `admin_explanation`
- `component_refs`
- `related_topics`
Feature modules should contribute this data through manifests, runtime providers,
route metadata, generated docs, or typed DTOs. The docs module renders and
links the material; it should not import feature-module internals.
## Authoring Rules
Workflow topics:
- use visible UI labels
- keep each step actionable
- mention blockers where the user would encounter them
- link to field/reference topics instead of repeating field tables
Reference topics:
- keep user columns short
- put API, permission, and configuration mapping in the admin view
- identify generated or runtime-derived values
- avoid exposing secrets or raw policy payloads
Pattern topics:
- explain the pattern once
- link to current examples
- distinguish user meaning from admin/operator meaning
- stay stable even when individual screens change
## Implementation Path
1. Keep the existing context API and add topic kind metadata.
2. Add docs-topic grouping for workflows, reference, patterns, and system
diagnostics.
3. Add backend DTOs for field/reference metadata after the metadata shape is
proven with one or two modules.
4. Rework the WebUI into a Help Center route with audience mode, search,
contextual anchors, and the four navigation modes.
5. Connect in-app help entry points to docs topics: help menu, route context,
inline field help, disabled-action blockers, and admin reference tables.
6. Seed access-module content first because users, groups, roles, sessions, and
API keys have clear user/admin splits and concrete API mappings.
7. Add pattern topics for existing core UI behavior, starting with field help,
blockers, advanced panels, wizards, and policy provenance.
8. Add tests for classification, audience filtering, topic linking, and route or
field anchor generation.
The first shippable milestone should be small: one workflow, one structure
reference, and one design-pattern topic for access administration, all filtered
by the current docs context and visible in the Help Center.

View File

@@ -6,6 +6,11 @@ GovOPlaN Docs provides a documentation layer that starts with the configured sys
Only after that should the documentation provide evidence about what else might be possible: installed-but-disabled features, optional modules, extension points, product capabilities, or examples from other configurations. Only after that should the documentation provide evidence about what else might be possible: installed-but-disabled features, optional modules, extension points, product capabilities, or examples from other configurations.
The editorial experience is defined in
`docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md`: workflow guidance, structure
reference, and shared design-pattern explanations are separate reading modes
over this configured-system model.
## Problem ## Problem
Traditional product documentation describes the full system. That creates avoidable complexity in configurable public-sector platforms because most users only need a slice of the system: Traditional product documentation describes the full system. That creates avoidable complexity in configurable public-sector platforms because most users only need a slice of the system:
@@ -71,6 +76,7 @@ Evidence should be discoverable, but it should not be the primary reading path f
The docs module should consume documentation sources through explicit contracts: The docs module should consume documentation sources through explicit contracts:
- module manifest documentation metadata - module manifest documentation metadata
- module manifest runtime documentation providers
- route metadata and page-level help references - route metadata and page-level help references
- capability metadata - capability metadata
- configuration-package descriptions - configuration-package descriptions
@@ -80,6 +86,113 @@ The docs module should consume documentation sources through explicit contracts:
Feature modules should contribute documentation metadata without the docs module importing their internals. Feature modules should contribute documentation metadata without the docs module importing their internals.
### Module-Owned Documentation
Documentation is distributed over the modules that own the behavior. A module can
register durable topics directly in its `ModuleManifest.documentation` tuple.
Use this for stable explanations such as the module purpose, common workflows,
policy hierarchy, route meaning, and links to public docs or repository docs.
When the text depends on active configuration, a module registers a provider in
`ModuleManifest.documentation_providers`. The provider receives a
`DocumentationContext` with the active registry, principal, settings, and a
database session when available. The module can then describe the effective
state without the docs module importing feature internals.
For example, `govoplan-mail` contributes static documentation for reusable mail
profiles and a runtime tenant policy topic. If the effective tenant mail policy
limits sending to approved profile ids and disables user, group, and
campaign-local profiles, the runtime docs state that users can choose approved
profiles but cannot bring arbitrary SMTP or IMAP servers at lower scopes. If the
policy allows lower scopes, the same topic explains which scopes may define
profiles and whether credential inheritance is locked.
Runtime providers must avoid leaking secrets. They should summarize posture,
counts, source provenance, and enabling conditions rather than exposing
credentials, hostnames, profile ids, or raw policy payloads unless the route is
explicitly intended for that level of detail.
### Conditions And Unlocks
Documentation topics can declare conditions:
- required modules
- one-of module alternatives
- modules that must be absent
- required capabilities
- required scopes or one-of scope alternatives
- configuration keys that influence the topic
The docs API classifies satisfied topics into their requested layer. Unsatisfied
topics remain visible as available or evidence documentation when safe, with a
reason such as a missing module, capability, or scope.
Topics can also declare related modules and unlock notes. This lets a module
state that additional behavior becomes available when another module is
installed. Runtime docs should link to the configured local behavior first and
then to public module documentation for broader examples.
### Admin And User Documentation
GovOPlaN has two documentation presentations over the same source model.
Admin documentation is allowed to be technical. It can expose installed module
ids, route contributions, API paths, permissions, capability names, server
configuration keys, policy source chains, migration notes, and operator
evidence. This is the right place to explain exactly why a setting is available,
which API applies it, and which configuration key or policy source controls it.
User documentation should be written for people without technical background. It
should explain what the user can do, what they cannot do, and who to ask when a
needed option is missing. It should avoid route names, API calls, raw policy
payloads, internal module ids, and server configuration unless that information
is genuinely meaningful to the user.
Each `DocumentationTopic` declares `documentation_types`, currently `admin`,
`user`, or both. Runtime providers receive the requested type in
`DocumentationContext.documentation_type`, so they can generate different text
from the same effective configuration. For example, mail admin docs can mention
mail profile policy sources and API endpoints, while user docs can simply say
whether the user may choose an approved mail profile or add a local mail server.
The first presentation rule is:
- admin docs show technical context and evidence tables
- user docs show plain-language topics, examples, limits, and escalation paths
- both presentations are filtered by installed modules, active configuration,
permissions, and safe disclosure rules
### Localization
Localization should be prepared before substantial documentation text is added.
Every durable topic should have a stable `i18n_key`. The English source text can
remain in the manifest or provider, but topics may include locale-specific
`translations` for `title`, `summary`, and `body`.
The docs API accepts a requested locale and falls back in this order:
- exact locale, such as `de-AT`
- base language, such as `de`
- English translation if present
- source text
This keeps the first implementation simple while avoiding a later rewrite of
every module documentation contribution. A later dedicated translation catalog
can still use the same `i18n_key` values.
### Public Documentation For Missing Modules
The configured-system docs should not explain every uninstalled module in full.
General documentation for the product and modules not installed in the current
system belongs on `https://govplan.add-ideas.de/`, served by `govoplan-web`.
Runtime documentation should link to the public page when:
- an optional module is not installed
- a feature is unlocked by adding another module
- an example uses modules that are not active locally
- a user needs conceptual background beyond the configured system
## Rendering Model ## Rendering Model
The runtime docs UI should be context-aware: The runtime docs UI should be context-aware:
@@ -111,6 +224,7 @@ The first implementation should define:
- a configured/available/evidence classifier - a configured/available/evidence classifier
- a minimal API for current-context documentation - a minimal API for current-context documentation
- a WebUI route contribution for the documentation browser - a WebUI route contribution for the documentation browser
- a provider contract for configuration-derived runtime topics
The first UI can be simple: The first UI can be simple:
@@ -119,5 +233,6 @@ The first UI can be simple:
- active policies - active policies
- available-but-disabled evidence - available-but-disabled evidence
- source links to wiki or repository docs - source links to wiki or repository docs
- module documentation topics with links and unlock notes
Once the source model is stable, feature modules can add richer documentation contributions through their manifests and capabilities. Once the source model is stable, feature modules can add richer documentation contributions through their manifests and capabilities.

View File

@@ -0,0 +1,70 @@
# Instance-Aware Documentation
The docs module renders documentation for the current GovOPlaN instance instead
of showing a static product manual.
## Inputs
The documentation context is built from:
- enabled module manifests
- frontend routes and navigation metadata
- module permissions and the current principal's granted scopes
- documentation topics contributed by manifests
- runtime documentation providers contributed by modules
- optional dependencies and declared capabilities
## Layers
| Layer | Meaning |
| --- | --- |
| `always` | Generic platform concepts that are useful in every deployment. |
| `configured` | Documentation for installed modules/features visible in this instance. |
| `available` | Installed features hidden by missing permissions or unmet conditions. |
| `evidence` | Hints for unavailable optional modules, missing capabilities, or external evidence sources. |
Normal user documentation should emphasize `always` and `configured` content.
Admin documentation may show all layers plus route, permission, module, and
capability diagnostics.
## Conditions
Documentation topics can declare:
- required modules
- any-of module sets
- modules that must be absent
- required capabilities
- required scopes
- any-of scope sets
- related configuration keys
The API returns both a human-readable `reason` and structured `blockers`:
```json
{
"reason": "missing modules: mail; missing scopes: mail:profile:read",
"blockers": {
"modules": ["mail"],
"capabilities": [],
"scopes": ["mail:profile:read"]
}
}
```
The WebUI must display the reason and, when present, the blocker lists. This
prevents dead-end instructions: users see whether a feature is unavailable
because a module is missing, a capability is not registered, or their current
permissions do not expose it.
## Module Guidance
Module docs should not hard-code assumptions about sibling modules. A module
should contribute conditional topics such as:
- "Send selected file by mail" requiring `files` and `mail`
- "Campaign attachments from storage" requiring `campaigns` and `files`
- "Mailbox diagnostics" requiring `mail.devMailbox`
The docs module remains the renderer. Feature modules own their subject matter
and describe unlocks through manifest metadata.

35
package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "@govoplan/docs-webui",
"version": "0.1.6",
"private": true,
"type": "module",
"main": "webui/src/index.ts",
"module": "webui/src/index.ts",
"types": "webui/src/index.ts",
"exports": {
".": {
"types": "./webui/src/index.ts",
"import": "./webui/src/index.ts"
}
},
"files": [
"webui/src",
"README.md"
],
"peerDependencies": {
"@govoplan/core-webui": "^0.1.6",
"lucide-react": "^1.23.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.6"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
}
}

24
pyproject.toml Normal file
View File

@@ -0,0 +1,24 @@
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "govoplan-docs"
version = "0.1.6"
description = "GovOPlaN documentation module for configured-system, available, and evidence documentation."
readme = "README.md"
requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.6",
"govoplan-access>=0.1.6",
]
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
govoplan_docs = ["py.typed"]
[project.entry-points."govoplan.modules"]
docs = "govoplan_docs.backend.manifest:get_manifest"

View File

@@ -0,0 +1,5 @@
"""GovOPlaN configured-system documentation module."""
__all__ = ["__version__"]
__version__ = "0.1.6"

View File

@@ -0,0 +1 @@
"""Backend integration for the GovOPlaN docs module."""

View File

@@ -0,0 +1 @@
"""Docs API package."""

View File

@@ -0,0 +1 @@
"""Docs API v1 package."""

View File

@@ -0,0 +1,556 @@
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from govoplan_access.backend.auth.dependencies import ApiPrincipal, has_scope, require_any_scope
from govoplan_core.core.modules import (
DocumentationCondition,
DocumentationContext,
DocumentationLink,
DocumentationTopic,
DocumentationType,
FrontendRoute,
ModuleManifest,
NavItem,
PermissionDefinition,
)
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.db.session import get_database
from govoplan_docs.backend.manifest import DOCS_READ_SCOPES
router = APIRouter(prefix="/docs", tags=["docs"])
TOPIC_KINDS = ("workflow", "reference", "pattern", "system")
@router.get("/context")
def docs_context(
request: Request,
documentation_type: DocumentationType = Query(default="admin", alias="type", pattern="^(admin|user)$"),
locale: str | None = Query(default=None, min_length=2, max_length=20),
principal: ApiPrincipal = Depends(require_any_scope(*DOCS_READ_SCOPES)),
) -> dict[str, Any]:
registry = _registry(request)
resolved_locale = _preferred_locale(request, locale)
modules = [_module_payload(manifest) for manifest in registry.manifests()]
permissions = [_permission_payload(permission, principal) for permission in registry.permissions()]
route_items = _route_items(registry.manifests(), principal)
visible_routes = [item for item in route_items if item["visible"]]
available_routes = [item for item in route_items if not item["visible"]]
optional_modules = _optional_module_evidence(registry.manifests())
documentation_layers = _documentation_layers(request, registry, principal, documentation_type=documentation_type, locale=resolved_locale)
documentation_count = sum(len(layer) for layer in documentation_layers.values())
topic_groups = _documentation_topic_groups(documentation_layers)
return {
"actor": {
"tenant_id": principal.tenant_id,
"user_id": principal.user.id,
"scope_count": len(principal.scopes),
"documentation_type": documentation_type,
"locale": resolved_locale,
},
"summary": {
"module_count": len(modules),
"visible_route_count": len(visible_routes),
"available_route_count": len(available_routes),
"permission_count": len(permissions),
"granted_permission_count": sum(1 for item in permissions if item["granted"]),
"optional_module_count": len(optional_modules),
"documentation_topic_count": documentation_count,
"configured_documentation_topic_count": len(documentation_layers["configured"]),
"workflow_topic_count": len(topic_groups["workflow"]),
"reference_topic_count": len(topic_groups["reference"]),
"pattern_topic_count": len(topic_groups["pattern"]),
"system_topic_count": len(topic_groups["system"]),
},
"topic_groups": topic_groups,
"layers": {
"always": {
"documentation": documentation_layers["always"],
},
"configured": {
"modules": modules,
"routes": visible_routes,
"permissions": permissions,
"documentation": documentation_layers["configured"],
},
"available": {
"routes": available_routes,
"permissions": [item for item in permissions if not item["granted"]],
"documentation": documentation_layers["available"],
},
"evidence": {
"optional_modules": optional_modules,
"sources": _evidence_sources(),
"documentation": documentation_layers["evidence"],
},
},
}
def _registry(request: Request) -> PlatformRegistry:
registry = getattr(request.app.state, "govoplan_registry", None)
if not isinstance(registry, PlatformRegistry):
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="GovOPlaN module registry is not configured")
return registry
def _module_payload(manifest: ModuleManifest) -> dict[str, Any]:
frontend = manifest.frontend
migration = manifest.migration_spec
return {
"id": manifest.id,
"name": manifest.name,
"version": manifest.version,
"dependencies": list(manifest.dependencies),
"optional_dependencies": list(manifest.optional_dependencies),
"permission_count": len(manifest.permissions),
"role_template_count": len(manifest.role_templates),
"nav_count": len(manifest.nav_items) + (len(frontend.nav_items) if frontend else 0),
"route_count": (1 if manifest.route_factory else 0) + (len(frontend.routes) if frontend else 0),
"frontend_package": frontend.package_name if frontend else None,
"backend_route_contributed": manifest.route_factory is not None,
"migration_module_id": migration.module_id if migration else None,
"capabilities": sorted(manifest.capability_factories),
"documentation_count": len(manifest.documentation),
"documentation_provider_count": len(manifest.documentation_providers),
}
def _permission_payload(permission: PermissionDefinition, principal: ApiPrincipal) -> dict[str, Any]:
return {
"scope": permission.scope,
"label": permission.label,
"description": permission.description,
"category": permission.category,
"level": permission.level,
"module_id": permission.module_id,
"resource": permission.resource,
"action": permission.action,
"deprecated": permission.deprecated,
"granted": has_scope(principal, permission.scope),
}
def _route_items(manifests: tuple[ModuleManifest, ...], principal: ApiPrincipal) -> list[dict[str, Any]]:
items: list[dict[str, Any]] = []
for manifest in manifests:
seen: set[tuple[str, str]] = set()
for item in manifest.nav_items:
key = ("nav", item.path)
if key not in seen:
seen.add(key)
items.append(_nav_item_payload(manifest.id, item, principal, source="nav"))
if manifest.frontend is None:
continue
nav_by_path = {item.path: item for item in manifest.frontend.nav_items}
for item in manifest.frontend.nav_items:
key = ("frontend_nav", item.path)
if key not in seen:
seen.add(key)
items.append(_nav_item_payload(manifest.id, item, principal, source="frontend_nav"))
for route in manifest.frontend.routes:
key = ("frontend_route", route.path)
if key in seen:
continue
seen.add(key)
items.append(_frontend_route_payload(manifest.id, route, nav_by_path.get(route.path), principal))
return sorted(items, key=lambda item: (int(item["order"]), str(item["path"]), str(item["source"])))
def _nav_item_payload(module_id: str, item: NavItem, principal: ApiPrincipal, *, source: str) -> dict[str, Any]:
visible, reason = _visibility(item.required_all, item.required_any, principal)
return {
"module_id": module_id,
"path": item.path,
"label": item.label,
"icon": item.icon,
"section": item.section,
"source": source,
"component": None,
"required_all": list(item.required_all),
"required_any": list(item.required_any),
"order": item.order,
"visible": visible,
"reason": reason,
}
def _frontend_route_payload(module_id: str, route: FrontendRoute, nav_item: NavItem | None, principal: ApiPrincipal) -> dict[str, Any]:
visible, reason = _visibility(route.required_all, route.required_any, principal)
return {
"module_id": module_id,
"path": route.path,
"label": nav_item.label if nav_item else route.component,
"icon": nav_item.icon if nav_item else None,
"section": nav_item.section if nav_item else None,
"source": "frontend_route",
"component": route.component,
"required_all": list(route.required_all),
"required_any": list(route.required_any),
"order": route.order,
"visible": visible,
"reason": reason,
}
def _visibility(required_all: tuple[str, ...], required_any: tuple[str, ...], principal: ApiPrincipal) -> tuple[bool, str]:
missing_all = [scope for scope in required_all if not has_scope(principal, scope)]
any_satisfied = not required_any or any(has_scope(principal, scope) for scope in required_any)
if not missing_all and any_satisfied:
return True, "visible"
reasons: list[str] = []
if missing_all:
reasons.append("missing " + ", ".join(missing_all))
if not any_satisfied:
reasons.append("requires one of " + ", ".join(required_any))
return False, "; ".join(reasons)
def _optional_module_evidence(manifests: tuple[ModuleManifest, ...]) -> list[dict[str, str]]:
installed = {manifest.id for manifest in manifests}
evidence: list[dict[str, str]] = []
for manifest in manifests:
for module_id in manifest.optional_dependencies:
evidence.append({
"module_id": module_id,
"source_module_id": manifest.id,
"status": "installed" if module_id in installed else "not_installed",
"reason": f"{manifest.name} declares optional integration with {module_id}.",
})
return sorted(evidence, key=lambda item: (item["status"], item["module_id"], item["source_module_id"]))
def _documentation_layers(
request: Request,
registry: PlatformRegistry,
principal: ApiPrincipal,
*,
documentation_type: DocumentationType,
locale: str,
) -> dict[str, list[dict[str, Any]]]:
settings = _settings(request)
try:
with get_database().SessionLocal() as session:
return _classify_documentation(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale)
except RuntimeError:
return _classify_documentation(registry, principal, settings=settings, session=None, documentation_type=documentation_type, locale=locale)
def _settings(request: Request) -> object | None:
direct_settings = getattr(request.app.state, "govoplan_settings", None)
if direct_settings is not None:
return direct_settings
lifecycle = getattr(request.app.state, "govoplan_lifecycle", None)
return getattr(lifecycle, "settings", None)
def _classify_documentation(
registry: PlatformRegistry,
principal: ApiPrincipal,
*,
settings: object | None,
session: object | None,
documentation_type: DocumentationType,
locale: str,
) -> dict[str, list[dict[str, Any]]]:
layers: dict[str, list[dict[str, Any]]] = {"always": [], "configured": [], "available": [], "evidence": []}
installed = {manifest.id for manifest in registry.manifests()}
topics = _collect_documentation_topics(registry, principal, settings=settings, session=session, documentation_type=documentation_type, locale=locale)
for source_module_id, topic in sorted(topics, key=lambda item: (item[1].order, item[0], item[1].id)):
if not _topic_matches_documentation_type(topic, documentation_type):
continue
active, reason, blockers = _documentation_visibility(topic, installed, registry, principal)
target_layer = _documentation_target_layer(topic, active, blockers)
if target_layer not in layers:
target_layer = "evidence"
layers[target_layer].append(_documentation_topic_payload(
source_module_id,
topic,
active=active,
reason=reason,
target_layer=target_layer,
blockers=blockers,
locale=locale,
))
return layers
def _collect_documentation_topics(
registry: PlatformRegistry,
principal: ApiPrincipal,
*,
settings: object | None,
session: object | None,
documentation_type: DocumentationType,
locale: str,
) -> list[tuple[str, DocumentationTopic]]:
topics: list[tuple[str, DocumentationTopic]] = []
for manifest in registry.manifests():
for topic in manifest.documentation:
topics.append((manifest.id, topic))
if not manifest.documentation_providers:
continue
context = DocumentationContext(
registry=registry,
principal=principal,
settings=settings,
session=session,
documentation_type=documentation_type,
locale=locale,
data={"source_module_id": manifest.id},
)
for provider in manifest.documentation_providers:
try:
provided_topics = tuple(provider(context))
except Exception as exc:
provided_topics = (
DocumentationTopic(
id=f"{manifest.id}.runtime-documentation-unavailable",
title=f"{manifest.name} runtime documentation unavailable",
summary="The module registered runtime documentation, but it could not be evaluated for this request.",
body="The static module documentation remains available. Check the module API and logs if this persists.",
layer="evidence",
documentation_types=(documentation_type,),
source_module_id=manifest.id,
metadata={"error_type": type(exc).__name__},
),
)
for topic in provided_topics:
topics.append((manifest.id, topic))
return topics
def _topic_matches_documentation_type(topic: DocumentationTopic, documentation_type: DocumentationType) -> bool:
return documentation_type in (topic.documentation_types or ("admin",))
def _documentation_visibility(
topic: DocumentationTopic,
installed: set[str],
registry: PlatformRegistry,
principal: ApiPrincipal,
) -> tuple[bool, str, dict[str, list[str]]]:
if not topic.conditions:
return True, "documented", {"modules": [], "capabilities": [], "scopes": []}
reasons: list[str] = []
blockers = {"modules": [], "capabilities": [], "scopes": []}
for condition in topic.conditions:
active, reason, condition_blockers = _condition_visibility(condition, installed, registry, principal)
if active:
return True, reason, {"modules": [], "capabilities": [], "scopes": []}
reasons.append(reason)
for key, values in condition_blockers.items():
blockers[key].extend(value for value in values if value not in blockers[key])
return False, "; ".join(reason for reason in reasons if reason) or "documentation conditions are not satisfied", blockers
def _condition_visibility(
condition: DocumentationCondition,
installed: set[str],
registry: PlatformRegistry,
principal: ApiPrincipal,
) -> tuple[bool, str, dict[str, list[str]]]:
blockers = {"modules": [], "capabilities": [], "scopes": []}
missing_required_modules = [module_id for module_id in condition.required_modules if module_id not in installed]
if missing_required_modules:
blockers["modules"].extend(missing_required_modules)
if condition.any_modules and not any(module_id in installed for module_id in condition.any_modules):
blockers["modules"].extend(module_id for module_id in condition.any_modules if module_id not in blockers["modules"])
conflicting_modules = [module_id for module_id in condition.missing_modules if module_id in installed]
if conflicting_modules:
blockers["modules"].extend(conflicting_modules)
missing_capabilities = [name for name in condition.required_capabilities if not registry.has_capability(name)]
if missing_capabilities:
blockers["capabilities"].extend(missing_capabilities)
missing_scopes = [scope for scope in condition.required_scopes if not has_scope(principal, scope)]
missing_any_scopes = list(condition.any_scopes) if condition.any_scopes and not any(has_scope(principal, scope) for scope in condition.any_scopes) else []
if missing_scopes:
blockers["scopes"].extend(missing_scopes)
if missing_any_scopes:
blockers["scopes"].extend(scope for scope in missing_any_scopes if scope not in blockers["scopes"])
if not blockers["modules"] and not blockers["capabilities"] and not blockers["scopes"]:
return True, "conditions satisfied", blockers
parts: list[str] = []
if missing_required_modules:
parts.append("missing modules: " + ", ".join(missing_required_modules))
if condition.any_modules and not any(module_id in installed for module_id in condition.any_modules):
parts.append("requires one installed module from: " + ", ".join(condition.any_modules))
if conflicting_modules:
parts.append("not active when installed: " + ", ".join(conflicting_modules))
if missing_capabilities:
parts.append("missing capabilities: " + ", ".join(missing_capabilities))
if missing_scopes:
parts.append("missing scopes: " + ", ".join(missing_scopes))
if missing_any_scopes:
parts.append("requires one scope from: " + ", ".join(missing_any_scopes))
return False, "; ".join(parts), blockers
def _documentation_target_layer(topic: DocumentationTopic, active: bool, blockers: dict[str, list[str]]) -> str:
if topic.layer == "always":
return "always"
if active:
return topic.layer
if blockers["modules"] or blockers["capabilities"]:
return "evidence"
return "available"
def _documentation_topic_payload(
source_module_id: str,
topic: DocumentationTopic,
*,
active: bool,
reason: str,
target_layer: str,
blockers: dict[str, list[str]],
locale: str,
) -> dict[str, Any]:
module_id = topic.source_module_id or source_module_id
translation_locale, translation = _translation_for_locale(topic, locale)
kind = _documentation_topic_kind(topic)
return {
"id": topic.id,
"source_module_id": module_id,
"kind": kind,
"anchor_id": _documentation_topic_anchor(module_id, topic.id),
"title": str(translation.get("title") or topic.title),
"summary": str(translation.get("summary") or topic.summary),
"body": str(translation.get("body") or topic.body),
"layer": topic.layer,
"target_layer": target_layer,
"documentation_types": list(topic.documentation_types),
"active": active,
"reason": reason,
"blockers": {
"modules": sorted(dict.fromkeys(blockers.get("modules", ()))),
"capabilities": sorted(dict.fromkeys(blockers.get("capabilities", ()))),
"scopes": sorted(dict.fromkeys(blockers.get("scopes", ()))),
},
"audience": list(topic.audience),
"order": topic.order,
"i18n_key": topic.i18n_key or topic.id,
"locale": locale,
"translation_locale": translation_locale,
"conditions": [_documentation_condition_payload(condition) for condition in topic.conditions],
"links": [_documentation_link_payload(link) for link in topic.links],
"related_modules": list(topic.related_modules),
"unlocks": list(topic.unlocks),
"configuration_keys": sorted({*topic.configuration_keys, *(key for condition in topic.conditions for key in condition.configuration_keys)}),
"metadata": dict(topic.metadata),
}
def _documentation_topic_kind(topic: DocumentationTopic) -> str:
raw_kind = topic.metadata.get("kind")
if isinstance(raw_kind, str):
normalized = raw_kind.strip().lower().replace("_", "-")
if normalized in TOPIC_KINDS:
return normalized
return "system"
def _documentation_topic_anchor(source_module_id: str, topic_id: str) -> str:
raw = f"docs-topic-{source_module_id}-{topic_id}"
cleaned = "".join(char if char.isalnum() or char in {"_", "-"} else "-" for char in raw)
while "--" in cleaned:
cleaned = cleaned.replace("--", "-")
return cleaned.strip("-").lower()
def _documentation_topic_groups(documentation_layers: dict[str, list[dict[str, Any]]]) -> dict[str, list[dict[str, Any]]]:
groups: dict[str, list[dict[str, Any]]] = {kind: [] for kind in TOPIC_KINDS}
for topic in _all_documentation_topics(documentation_layers):
kind = str(topic.get("kind") or "system")
groups.setdefault(kind, []).append(topic)
return groups
def _all_documentation_topics(documentation_layers: dict[str, list[dict[str, Any]]]) -> list[dict[str, Any]]:
topics: list[dict[str, Any]] = []
for layer in ("always", "configured", "available", "evidence"):
topics.extend(documentation_layers.get(layer, ()))
return topics
def _translation_for_locale(topic: DocumentationTopic, locale: str) -> tuple[str, Mapping[str, str]]:
translations = topic.translations or {}
for candidate in _locale_candidates(locale):
translation = translations.get(candidate)
if translation:
return candidate, translation
return "source", {}
def _locale_candidates(locale: str) -> tuple[str, ...]:
normalized = _normalize_locale(locale)
base = normalized.split("-", 1)[0]
candidates = [normalized]
if base and base != normalized:
candidates.append(base)
if "en" not in candidates:
candidates.append("en")
return tuple(candidates)
def _preferred_locale(request: Request, explicit_locale: str | None) -> str:
if explicit_locale:
return _normalize_locale(explicit_locale)
accept_language = request.headers.get("accept-language", "")
first = accept_language.split(",", 1)[0].split(";", 1)[0].strip()
return _normalize_locale(first or "en")
def _normalize_locale(value: str) -> str:
clean = "".join(char for char in value.strip().replace("_", "-") if char.isalnum() or char == "-")
if not clean:
return "en"
parts = [part for part in clean.split("-") if part]
if not parts:
return "en"
head = parts[0].lower()
tail = [part.upper() if len(part) == 2 else part for part in parts[1:]]
return "-".join([head, *tail])[:20]
def _documentation_condition_payload(condition: DocumentationCondition) -> dict[str, list[str]]:
return {
"required_modules": list(condition.required_modules),
"any_modules": list(condition.any_modules),
"missing_modules": list(condition.missing_modules),
"required_capabilities": list(condition.required_capabilities),
"required_scopes": list(condition.required_scopes),
"any_scopes": list(condition.any_scopes),
"configuration_keys": list(condition.configuration_keys),
}
def _documentation_link_payload(link: DocumentationLink) -> dict[str, str]:
return {
"label": link.label,
"href": link.href,
"kind": link.kind,
}
def _evidence_sources() -> list[dict[str, str]]:
return [
{
"label": "Documentation layer concept",
"source": "govoplan-docs/docs/DOCUMENTATION_LAYER_CONCEPT.md",
"layer": "evidence",
},
{
"label": "Master roadmap",
"source": "govoplan-core/docs/GOVOPLAN_MASTER_ROADMAP.md",
"layer": "evidence",
},
]

View File

@@ -0,0 +1,146 @@
from __future__ import annotations
from govoplan_core.core.modules import (
DocumentationLink,
DocumentationTopic,
FrontendModule,
FrontendRoute,
ModuleContext,
ModuleManifest,
NavItem,
PermissionDefinition,
RoleTemplate,
)
DOCS_READ_SCOPE = "docs:documentation:read"
DOCS_READ_SCOPES = (DOCS_READ_SCOPE, "system:settings:read", "admin:settings:read")
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
module_id, resource, action = scope.split(":", 2)
return PermissionDefinition(
scope=scope,
label=label,
description=description,
category="Documentation",
level="tenant",
module_id=module_id,
resource=resource,
action=action,
)
def _route_factory(context: ModuleContext):
del context
from govoplan_docs.backend.api.v1.routes import router
return router
manifest = ModuleManifest(
id="docs",
name="Docs",
version="0.1.6",
dependencies=("access",),
optional_dependencies=("policy", "audit", "ops", "workflow", "search"),
permissions=(
_permission(
DOCS_READ_SCOPE,
"View configured documentation",
"Read documentation generated from installed modules, visible routes, permissions, and evidence sources.",
),
),
role_templates=(
RoleTemplate(
slug="docs_reader",
name="Documentation reader",
description="Read the configured-system documentation browser.",
permissions=(DOCS_READ_SCOPE,),
),
),
route_factory=_route_factory,
nav_items=(NavItem(path="/docs", label="Docs", icon="reports", required_any=DOCS_READ_SCOPES, order=880),),
frontend=FrontendModule(
module_id="docs",
package_name="@govoplan/docs-webui",
routes=(FrontendRoute(path="/docs", component="DocsPage", required_any=DOCS_READ_SCOPES, order=880),),
nav_items=(NavItem(path="/docs", label="Docs", icon="reports", required_any=DOCS_READ_SCOPES, order=880),),
),
documentation=(
DocumentationTopic(
id="docs.configured-system-documentation",
title="Configured system documentation",
summary="This documentation page starts with installed modules, active configuration, visible routes, and permissions for the current actor.",
body="Module manifests can contribute durable documentation topics. Modules can also register runtime documentation providers when content depends on tenant policy, installed integrations, or operational settings.",
layer="always",
documentation_types=("admin", "user"),
audience=("tenant_admin", "operator", "module_admin"),
order=10,
i18n_key="docs.topic.configured_system_documentation",
translations={
"de": {
"title": "Dokumentation dieses Systems",
"summary": "Diese Dokumentation beginnt mit den installierten Modulen, der aktiven Konfiguration und den Funktionen, die fuer diese Rolle sichtbar sind.",
"body": "Module koennen feste Dokumentationsabschnitte beitragen. Wenn Inhalte von Tenant-Regeln, installierten Integrationen oder Betriebsoptionen abhaengen, kann ein Modul laufzeitbasierte Dokumentation registrieren.",
},
},
links=(
DocumentationLink(
label="Public GovOPlaN module documentation",
href="https://govplan.add-ideas.de/",
kind="public",
),
DocumentationLink(
label="Documentation layer concept",
href="govoplan-docs/docs/DOCUMENTATION_LAYER_CONCEPT.md",
kind="repository",
),
DocumentationLink(
label="Instance-aware documentation contract",
href="govoplan-docs/docs/INSTANCE_AWARE_DOCUMENTATION.md",
kind="repository",
),
),
metadata={"kind": "system"},
),
DocumentationTopic(
id="docs.pattern.field-help",
title="Field help marker",
summary="A small help marker next to a label gives local context without turning dense forms into manuals.",
body="Use the marker for short explanations of a field, option, or compact term. Link to a workflow or reference topic when the reader needs steps, API mapping, policy provenance, or operational detail.",
layer="always",
documentation_types=("admin", "user"),
audience=("user", "tenant_admin", "operator", "module_admin"),
order=20,
i18n_key="docs.topic.pattern.field_help",
links=(
DocumentationLink(
label="Documentation experience concept",
href="govoplan-docs/docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md",
kind="repository",
),
),
metadata={
"kind": "pattern",
"pattern_id": "field-help-marker",
"purpose": "Keep labels scannable while making short explanations available on demand.",
"when_used": "Form labels, toggle labels, effective-value rows, and compact admin terms.",
"user_explanation": "Open the marker when a label is unclear. It should explain the local choice in one or two sentences.",
"admin_explanation": "Field help stays local. Longer procedural, API, or policy explanations belong in linked workflow or reference topics.",
"component_refs": [
"govoplan-core/webui/src/components/help/FieldLabel.tsx",
"govoplan-core/webui/src/components/help/InlineHelp.tsx",
"govoplan-core/webui/src/utils/fieldHelp.ts",
],
"related_topic_ids": [
"access.reference.admin-access-fields",
"access.workflow.grant-user-access",
],
},
),
),
)
def get_manifest() -> ModuleManifest:
return manifest

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,91 @@
from __future__ import annotations
import unittest
from types import SimpleNamespace
from govoplan_access.backend.manifest import get_manifest as get_access_manifest
from govoplan_core.core.registry import PlatformRegistry
from govoplan_tenancy.backend.manifest import get_manifest as get_tenancy_manifest
from govoplan_docs.backend.api.v1.routes import (
_classify_documentation,
_documentation_topic_anchor,
_documentation_topic_groups,
)
from govoplan_docs.backend.manifest import get_manifest as get_docs_manifest
class FakePrincipal:
tenant_id = "tenant-1"
user = SimpleNamespace(id="user-1")
def __init__(self, scopes: set[str]) -> None:
self.scopes = frozenset(scopes)
def has(self, required_scope: str) -> bool:
return required_scope in self.scopes
class DocsContextTests(unittest.TestCase):
def test_topic_groups_preserve_layer_classification(self) -> None:
registry = PlatformRegistry()
registry.register(get_tenancy_manifest())
registry.register(get_access_manifest())
registry.register(get_docs_manifest())
principal = FakePrincipal({"admin:users:read", "admin:groups:read", "docs:documentation:read"})
layers = _classify_documentation(
registry,
principal,
settings=None,
session=None,
documentation_type="admin",
locale="en",
)
groups = _documentation_topic_groups(layers)
workflow_ids = {topic["id"] for topic in groups["workflow"]}
reference_ids = {topic["id"] for topic in groups["reference"]}
pattern_ids = {topic["id"] for topic in groups["pattern"]}
system_ids = {topic["id"] for topic in groups["system"]}
self.assertIn("access.workflow.grant-user-access", workflow_ids)
self.assertIn("access.reference.admin-access-fields", reference_ids)
self.assertIn("docs.pattern.field-help", pattern_ids)
self.assertIn("docs.configured-system-documentation", system_ids)
configured_ids = {topic["id"] for topic in layers["configured"]}
always_ids = {topic["id"] for topic in layers["always"]}
self.assertIn("access.workflow.grant-user-access", configured_ids)
self.assertIn("docs.pattern.field-help", always_ids)
def test_unavailable_topic_still_appears_in_kind_group(self) -> None:
registry = PlatformRegistry()
registry.register(get_tenancy_manifest())
registry.register(get_access_manifest())
registry.register(get_docs_manifest())
principal = FakePrincipal({"docs:documentation:read"})
layers = _classify_documentation(
registry,
principal,
settings=None,
session=None,
documentation_type="user",
locale="en",
)
groups = _documentation_topic_groups(layers)
workflows = {topic["id"]: topic for topic in groups["workflow"]}
self.assertIn("access.workflow.grant-user-access", workflows)
self.assertFalse(workflows["access.workflow.grant-user-access"]["active"])
self.assertIn("access.workflow.grant-user-access", {topic["id"] for topic in layers["available"]})
def test_topic_anchor_is_stable(self) -> None:
self.assertEqual(
_documentation_topic_anchor("access", "access.workflow.grant-user-access"),
"docs-topic-access-access-workflow-grant-user-access",
)
if __name__ == "__main__":
unittest.main()

30
webui/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "@govoplan/docs-webui",
"version": "0.1.6",
"private": true,
"type": "module",
"main": "src/index.ts",
"module": "src/index.ts",
"types": "src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
}
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.6",
"lucide-react": "^1.23.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.6"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
}
}

169
webui/src/api/docs.ts Normal file
View File

@@ -0,0 +1,169 @@
import { apiFetch, type ApiSettings } from "@govoplan/core-webui";
export type DocsModule = {
id: string;
name: string;
version: string;
dependencies: string[];
optional_dependencies: string[];
permission_count: number;
role_template_count: number;
nav_count: number;
route_count: number;
frontend_package?: string | null;
backend_route_contributed: boolean;
migration_module_id?: string | null;
capabilities: string[];
documentation_count: number;
documentation_provider_count: number;
};
export type DocsRoute = {
module_id: string;
path: string;
label: string;
icon?: string | null;
section?: string | null;
source: string;
component?: string | null;
required_all: string[];
required_any: string[];
order: number;
visible: boolean;
reason: string;
};
export type DocsPermission = {
scope: string;
label: string;
description: string;
category: string;
level: "system" | "tenant";
module_id: string;
resource: string;
action: string;
deprecated: boolean;
granted: boolean;
};
export type DocsOptionalModuleEvidence = {
module_id: string;
source_module_id: string;
status: "installed" | "not_installed" | string;
reason: string;
};
export type DocsSource = {
label: string;
source: string;
layer: string;
};
export type DocsDocumentationCondition = {
required_modules: string[];
any_modules: string[];
missing_modules: string[];
required_capabilities: string[];
required_scopes: string[];
any_scopes: string[];
configuration_keys: string[];
};
export type DocsDocumentationLink = {
label: string;
href: string;
kind: string;
};
export type DocsTopicKind = "workflow" | "reference" | "pattern" | "system" | string;
export type DocsDocumentationTopic = {
id: string;
source_module_id: string;
kind: DocsTopicKind;
anchor_id: string;
title: string;
summary: string;
body: string;
layer: "always" | "configured" | "available" | "evidence" | string;
target_layer: string;
documentation_types: Array<"admin" | "user" | string>;
active: boolean;
reason: string;
blockers: {
modules: string[];
capabilities: string[];
scopes: string[];
};
audience: string[];
order: number;
i18n_key: string;
locale: string;
translation_locale: string;
conditions: DocsDocumentationCondition[];
links: DocsDocumentationLink[];
related_modules: string[];
unlocks: string[];
configuration_keys: string[];
metadata: Record<string, unknown>;
};
export type DocsContext = {
actor: {
tenant_id: string;
user_id: string;
scope_count: number;
documentation_type: "admin" | "user";
locale: string;
};
summary: {
module_count: number;
visible_route_count: number;
available_route_count: number;
permission_count: number;
granted_permission_count: number;
optional_module_count: number;
documentation_topic_count: number;
configured_documentation_topic_count: number;
workflow_topic_count: number;
reference_topic_count: number;
pattern_topic_count: number;
system_topic_count: number;
};
topic_groups: {
workflow: DocsDocumentationTopic[];
reference: DocsDocumentationTopic[];
pattern: DocsDocumentationTopic[];
system: DocsDocumentationTopic[];
[kind: string]: DocsDocumentationTopic[];
};
layers: {
always: {
documentation: DocsDocumentationTopic[];
};
configured: {
modules: DocsModule[];
routes: DocsRoute[];
permissions: DocsPermission[];
documentation: DocsDocumentationTopic[];
};
available: {
routes: DocsRoute[];
permissions: DocsPermission[];
documentation: DocsDocumentationTopic[];
};
evidence: {
optional_modules: DocsOptionalModuleEvidence[];
sources: DocsSource[];
documentation: DocsDocumentationTopic[];
};
};
};
export function fetchDocsContext(settings: ApiSettings, options: { documentationType?: "admin" | "user"; locale?: string } = {}): Promise<DocsContext> {
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/context${query ? `?${query}` : ""}`);
}

View File

@@ -0,0 +1,950 @@
import { useEffect, useMemo, useState } from "react";
import { Link, useLocation } from "react-router-dom";
import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react";
import {
Button,
DismissibleAlert,
ExplorerTree,
LoadingFrame,
PageTitle,
SegmentedControl,
StatusBadge,
adminErrorMessage,
useGuardedNavigate,
usePlatformLanguage,
type ApiSettings
} from "@govoplan/core-webui";
import {
fetchDocsContext,
type DocsContext,
type DocsDocumentationTopic,
type DocsModule,
type DocsOptionalModuleEvidence,
type DocsRoute,
type DocsSource
} from "../../api/docs";
type DocumentationType = "admin" | "user";
type DocsPageNode =
| {
id: string;
title: string;
kind: "topic";
topic: DocsDocumentationTopic;
}
| {
id: string;
title: string;
kind: "topic-list";
topics: DocsDocumentationTopic[];
emptyText: string;
}
| {
id: string;
title: string;
kind: "admin-reference";
};
type DocsTreeNode = {
id: string;
title: string;
page: DocsPageNode;
children: DocsTreeNode[];
};
type OutlineItem = {
id: string;
label: string;
};
export default function DocsPage({ settings }: { settings: ApiSettings }) {
const location = useLocation();
const navigate = useGuardedNavigate();
const { language } = usePlatformLanguage();
const [context, setContext] = useState<DocsContext | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [documentationType, setDocumentationType] = useState<DocumentationType>(() => documentationTypeFromSearch(location.search));
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(() => new Set());
const locale = localeFromSearch(location.search) ?? language;
const adminDocs = documentationType === "admin";
async function load() {
setLoading(true);
setError("");
try {
setContext(await fetchDocsContext(settings, { documentationType, locale }));
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setLoading(false);
}
}
useEffect(() => {
const nextType = documentationTypeFromSearch(location.search);
setDocumentationType((current) => current === nextType ? current : nextType);
}, [location.search]);
useEffect(() => { void load(); }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken, documentationType, locale]);
const treeNodes = useMemo(() => docsTreeNodes(context, adminDocs), [context, adminDocs]);
const pages = useMemo(() => flattenTreePages(treeNodes), [treeNodes]);
const selectedPage = selectedPageFromSearch(location.search, pages);
const topicById = useMemo(() => topicIndex(context), [context]);
const visibleRoutes = context?.layers.configured.routes ?? [];
const availableRoutes = context?.layers.available.routes ?? [];
const grantedPermissions = context?.layers.configured.permissions.filter((item) => item.granted) ?? [];
const moduleRows = useMemo(() => context?.layers.configured.modules ?? [], [context]);
const outlineItems = outlineForPage(selectedPage, adminDocs);
useEffect(() => {
setExpandedNodes(new Set(defaultExpandedNodeIds(treeNodes)));
}, [treeNodes]);
useEffect(() => {
if (!selectedPage) return;
const ancestorIds = ancestorNodeIdsForPage(treeNodes, selectedPage.id);
if (!ancestorIds.length) return;
setExpandedNodes((current) => {
const next = new Set(current);
for (const nodeId of ancestorIds) {
next.add(nodeId);
}
return next;
});
}, [treeNodes, selectedPage?.id]);
return (
<div className="workspace module-workspace docs-workspace">
<aside className="section-sidebar docs-outline" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
<div className="docs-sidebar-header">
<div className="section-title">i18n:govoplan-docs.help_center.f3f3a34b</div>
<AudienceToggle selected={documentationType} onSelect={selectDocumentationType} />
</div>
<nav className="docs-tree" aria-label="i18n:govoplan-docs.documentation_outline.6f836b99">
<ExplorerTree
nodes={treeNodes}
getNodeId={(node) => node.id}
getNodeLabel={(node) => node.title}
getNodeChildren={(node) => node.children}
activeId={activeNodeIdForPage(treeNodes, selectedPage?.id ?? "")}
expandedIds={expandedNodes}
depth={0}
childrenBaseClassName="docs-tree-children"
nodeContainerClassName="docs-tree-node"
nodeWrapBaseClassName="docs-tree-row"
toggleBaseClassName="docs-tree-toggle"
nodeButtonBaseClassName="docs-tree-page"
getNodeWrapStyle={(_node, context) => ({ paddingLeft: `${context.depth * 14}px` })}
getNodeButtonClassName={(node) => selectedPage?.id === node.page.id ? "is-active" : ""}
renderToggleIcon={(_node, context) => context.hasChildren ? context.expanded ? <ChevronDown size={15} /> : <ChevronRight size={15} /> : <span className="docs-tree-toggle-placeholder" />}
renderNodeContent={(node) => node.title}
onToggle={(node) => toggleNode(node.id)}
onOpen={(node) => selectPage(node.page)}
/>
</nav>
</aside>
<section className="workspace-content docs-workspace-content">
<div className="content-pad workspace-data-page docs-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>i18n:govoplan-docs.help_center.f3f3a34b</PageTitle>
<p>{adminDocs ? "i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e" : "i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad"}</p>
</div>
<div className="button-row compact-actions">
<Button onClick={() => void load()} disabled={loading}><RefreshCw size={16} /> i18n:govoplan-docs.reload.cce71553</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
<LoadingFrame loading={loading} label="i18n:govoplan-docs.loading_documentation_context.1c091645">
<div className="docs-content">
<main className="docs-page-main">
<SelectedPageContent
page={selectedPage}
adminDocs={adminDocs}
documentationType={documentationType}
topicById={topicById}
moduleRows={moduleRows}
visibleRoutes={visibleRoutes}
availableRoutes={availableRoutes}
grantedPermissions={grantedPermissions}
evidenceModules={context?.layers.evidence.optional_modules ?? []}
evidenceSources={context?.layers.evidence.sources ?? []}
/>
</main>
<PageOutline items={outlineItems} />
</div>
</LoadingFrame>
</div>
</section>
</div>
);
function selectDocumentationType(type: DocumentationType) {
const params = new URLSearchParams(location.search);
params.set("type", type);
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
}
function selectPage(page: DocsPageNode) {
const params = new URLSearchParams(location.search);
params.set("topic", page.id);
navigate(`${location.pathname}?${params.toString()}`, { replace: true });
}
function toggleNode(nodeId: string) {
setExpandedNodes((current) => {
const next = new Set(current);
if (next.has(nodeId)) {
next.delete(nodeId);
} else {
next.add(nodeId);
}
return next;
});
}
}
function AudienceToggle({ selected, onSelect }: { selected: DocumentationType; onSelect: (type: DocumentationType) => void }) {
return (
<SegmentedControl
className="docs-audience-toggle"
role="group"
size="equal"
width="fill"
ariaLabel="i18n:govoplan-docs.documentation_type.5a66690c"
value={selected}
onChange={onSelect}
options={[
{ id: "admin", label: "i18n:govoplan-docs.admin_docs.bf504a56" },
{ id: "user", label: "i18n:govoplan-docs.user_docs.1e38e8d3" }
]}
/>
);
}
function PageOutline({ items }: { items: OutlineItem[] }) {
return (
<aside className="docs-page-outline-box" aria-label="i18n:govoplan-docs.page_outline.e5940949">
<h2>i18n:govoplan-docs.page_outline.e5940949</h2>
{items.length ?
<nav>
{items.map((item) => <a key={item.id} href={`#${item.id}`}>{item.label}</a>)}
</nav> :
<p className="muted">i18n:govoplan-docs.no_outline_items.3ea2842e</p>
}
</aside>
);
}
function SelectedPageContent({
page,
adminDocs,
documentationType,
topicById,
moduleRows,
visibleRoutes,
availableRoutes,
grantedPermissions,
evidenceModules,
evidenceSources
}: {
page: DocsPageNode | null;
adminDocs: boolean;
documentationType: DocumentationType;
topicById: Map<string, DocsDocumentationTopic>;
moduleRows: DocsModule[];
visibleRoutes: DocsRoute[];
availableRoutes: DocsRoute[];
grantedPermissions: Array<{ scope: string; label: string; category: string }>;
evidenceModules: DocsOptionalModuleEvidence[];
evidenceSources: DocsSource[];
}) {
if (!page) {
return (
<section className="docs-section">
<h2>i18n:govoplan-docs.help_center.f3f3a34b</h2>
<p className="muted">i18n:govoplan-docs.no_system_topics_found.d01a8068</p>
</section>
);
}
if (page.kind === "topic") {
return <TopicPage topic={page.topic} showTechnical={adminDocs} documentationType={documentationType} topicById={topicById} />;
}
if (page.kind === "admin-reference") {
return (
<section className="docs-section" id={page.id}>
<h2>{page.title}</h2>
<section id="docs-admin-modules" className="docs-reference-block">
<h3>i18n:govoplan-docs.modules.04e9462c</h3>
<ModuleTable modules={moduleRows} />
</section>
<section id="docs-admin-routes" className="docs-reference-block">
<h3>i18n:govoplan-docs.visible_routes.6c9b1605</h3>
<RouteTable routes={visibleRoutes} emptyText="i18n:govoplan-docs.no_visible_routes_found_for_this_actor.3feeaf0b" />
<h3>i18n:govoplan-docs.available_routes.c2635868</h3>
<RouteTable routes={availableRoutes} emptyText="i18n:govoplan-docs.no_hidden_installed_routes_found.22f62845" />
</section>
<section id="docs-admin-permissions" className="docs-reference-block">
<h3>i18n:govoplan-docs.granted_permissions.0a232e78</h3>
<PermissionList permissions={grantedPermissions} />
<EvidenceList modules={evidenceModules} sources={evidenceSources} />
</section>
</section>
);
}
return (
<section className="docs-section" id={page.id}>
<h2>{page.title}</h2>
<DocumentationTopicList topics={page.topics} emptyText={page.emptyText} showTechnical={adminDocs} documentationType={documentationType} topicById={topicById} />
</section>
);
}
function TopicPage({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
return (
<article className={`docs-topic docs-topic-page docs-topic-${topic.kind}`} id={topicAnchorId(topic)}>
<div className="docs-topic-meta">
<StatusBadge status={topic.active ? "success" : "warning"} label={topic.active ? topic.kind : topic.target_layer} />
<span className="muted">{topic.source_module_id}</span>
{showTechnical && <span className="muted">{topic.i18n_key} · {topic.translation_locale}</span>}
</div>
<h2>{topic.title}</h2>
{topic.summary && <p id={`${topicAnchorId(topic)}-summary`} className="docs-topic-summary">{topic.summary}</p>}
<div id={`${topicAnchorId(topic)}-details`}>
<BodyText value={topic.body} />
<TopicMetadata topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />
{!topic.active && <UnavailableDocumentationReason topic={topic} />}
</div>
{!!topic.unlocks.length && <p className="muted">i18n:govoplan-docs.unlocks.67d569db {topic.unlocks.join(" ")}</p>}
{!!topic.links.length && <TopicLinks topic={topic} />}
</article>
);
}
function DocumentationTopicList({ topics, emptyText, showTechnical = false, documentationType, topicById }: { topics: DocsDocumentationTopic[]; emptyText: string; showTechnical?: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
if (!topics.length) return <p className="muted">{emptyText}</p>;
return (
<div className="docs-topic-list">
{topics.map((topic) =>
<article className={`docs-topic docs-topic-${topic.kind}`} id={topicAnchorId(topic)} key={`${topic.source_module_id}-${topic.id}`}>
<div className="docs-topic-meta">
<StatusBadge status={topic.active ? "success" : "warning"} label={topic.active ? topic.kind : topic.target_layer} />
<span className="muted">{topic.source_module_id}</span>
{showTechnical && <span className="muted">{topic.i18n_key} · {topic.translation_locale}</span>}
</div>
<h3>{topic.title}</h3>
{topic.summary && <p className="docs-topic-summary">{topic.summary}</p>}
<BodyText value={topic.body} />
<TopicMetadata topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />
{!topic.active && <UnavailableDocumentationReason topic={topic} />}
{!!topic.unlocks.length && <p className="muted">i18n:govoplan-docs.unlocks.67d569db {topic.unlocks.join(" ")}</p>}
{!!topic.links.length && <TopicLinks topic={topic} />}
</article>
)}
</div>
);
}
function TopicMetadata({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
if (topic.kind === "workflow") return <WorkflowDetails topic={topic} documentationType={documentationType} topicById={topicById} />;
if (topic.kind === "reference") return <ReferenceDetails topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />;
if (topic.kind === "pattern") return <PatternDetails topic={topic} showTechnical={showTechnical} documentationType={documentationType} topicById={topicById} />;
return <RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />;
}
function WorkflowDetails({ topic, documentationType, topicById }: { topic: DocsDocumentationTopic; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
const prerequisites = metadataList(topic.metadata, "prerequisites");
const steps = metadataList(topic.metadata, "steps");
const outcome = metadataString(topic.metadata, "outcome");
const result = metadataString(topic.metadata, "result");
const verification = metadataString(topic.metadata, "verification");
const prefix = topicAnchorId(topic);
return (
<div className="docs-topic-details">
{outcome && <DetailBlock id={`${prefix}-outcome`} title="i18n:govoplan-docs.outcome.10172bd3" value={outcome} />}
{!!prerequisites.length && <DetailList id={`${prefix}-prerequisites`} title="i18n:govoplan-docs.prerequisites.fdf2407f" items={prerequisites} />}
{!!steps.length &&
<div className="docs-detail-block" id={`${prefix}-steps`}>
<h4>i18n:govoplan-docs.steps.6041435e</h4>
<ol>
{steps.map((step) => <li key={step}>{step}</li>)}
</ol>
</div>
}
{result && <DetailBlock id={`${prefix}-result`} title="i18n:govoplan-docs.result.1f4fbf43" value={result} />}
{verification && <DetailBlock id={`${prefix}-verification`} title="i18n:govoplan-docs.verification.4b5f10dd" value={verification} />}
<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />
</div>
);
}
function ReferenceDetails({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
const route = metadataString(topic.metadata, "route");
const screen = metadataString(topic.metadata, "screen");
const section = metadataString(topic.metadata, "section");
const fields = metadataRecords(topic.metadata, "fields");
return (
<div className="docs-topic-details">
{(route || screen || section) &&
<dl className="detail-list compact">
{screen && <div><dt>i18n:govoplan-docs.screen.c4878ec4</dt><dd>{screen}</dd></div>}
{section && <div><dt>i18n:govoplan-docs.section.5e498158</dt><dd>{section}</dd></div>}
{showTechnical && route && <div><dt>i18n:govoplan-docs.route.4999528e</dt><dd>{route}</dd></div>}
</dl>
}
{!!fields.length && <ReferenceFieldTable id={`${topicAnchorId(topic)}-fields`} fields={fields} showTechnical={showTechnical} />}
<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />
</div>
);
}
function ReferenceFieldTable({ id, fields, showTechnical }: { id: string; fields: Record<string, unknown>[]; showTechnical: boolean }) {
return (
<div className="admin-table-wrap" id={id}>
<table className="admin-table">
<thead>
<tr>
<th>i18n:govoplan-docs.field.7558c082</th>
<th>i18n:govoplan-docs.meaning.584d8aa0</th>
{showTechnical && <th>i18n:govoplan-docs.api_mapping.e969f6f7</th>}
{showTechnical && <th>i18n:govoplan-docs.permission.d71c9448</th>}
<th>i18n:govoplan-docs.validation.ef7f6d9c</th>
</tr>
</thead>
<tbody>
{fields.map((field) =>
<tr key={metadataString(field, "field_id") || metadataString(field, "label")}>
<td><strong>{metadataString(field, "label")}</strong></td>
<td>
{metadataString(field, "user_description")}
{showTechnical && metadataString(field, "admin_description") && <span className="muted block">{metadataString(field, "admin_description")}</span>}
{showTechnical && metadataString(field, "provenance") && <span className="muted block">{metadataString(field, "provenance")}</span>}
</td>
{showTechnical && <td>{metadataString(field, "api_path")}<span className="muted block">{metadataString(field, "api_field")}</span></td>}
{showTechnical && <td>{metadataString(field, "permission_scope") || "-"}</td>}
<td>{metadataString(field, "validation") || "-"}</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
function PatternDetails({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
const purpose = metadataString(topic.metadata, "purpose");
const whenUsed = metadataString(topic.metadata, "when_used");
const explanation = showTechnical ? metadataString(topic.metadata, "admin_explanation") : metadataString(topic.metadata, "user_explanation");
const componentRefs = metadataList(topic.metadata, "component_refs");
const prefix = topicAnchorId(topic);
return (
<div className="docs-topic-details">
{purpose && <DetailBlock id={`${prefix}-purpose`} title="i18n:govoplan-docs.purpose.4af40581" value={purpose} />}
{whenUsed && <DetailBlock id={`${prefix}-when-used`} title="i18n:govoplan-docs.when_used.b870b361" value={whenUsed} />}
{explanation && <DetailBlock id={`${prefix}-details`} title="i18n:govoplan-docs.details.a6b3c45f" value={explanation} />}
{showTechnical && !!componentRefs.length && <DetailList id={`${prefix}-components`} title="i18n:govoplan-docs.components.e81e57d8" items={componentRefs} />}
<RelatedTopics topic={topic} documentationType={documentationType} topicById={topicById} />
</div>
);
}
function DetailBlock({ id, title, value }: { id: string; title: string; value: string }) {
return (
<div className="docs-detail-block" id={id}>
<h4>{title}</h4>
<p>{value}</p>
</div>
);
}
function DetailList({ id, title, items }: { id: string; title: string; items: string[] }) {
return (
<div className="docs-detail-block" id={id}>
<h4>{title}</h4>
<ul>
{items.map((item) => <li key={item}>{item}</li>)}
</ul>
</div>
);
}
function RelatedTopics({ topic, documentationType, topicById }: { topic: DocsDocumentationTopic; documentationType: DocumentationType; topicById: Map<string, DocsDocumentationTopic> }) {
const related = metadataList(topic.metadata, "related_topic_ids")
.map((id) => topicById.get(id))
.filter((item): item is DocsDocumentationTopic => Boolean(item));
if (!related.length) return null;
return (
<p className="docs-topic-links">
<span>i18n:govoplan-docs.related_topics.d5eb8b74 </span>
{related.map((item, index) =>
<span key={item.id}>
{index > 0 && " | "}
<Link to={topicPageHref(item, documentationType)}>{item.title}</Link>
</span>
)}
</p>
);
}
function TopicLinks({ topic }: { topic: DocsDocumentationTopic }) {
return (
<p className="docs-topic-links">
{topic.links.map((link, index) =>
<span key={link.href}>
{index > 0 && " | "}
<a href={link.href} target={link.kind === "public" ? "_blank" : undefined} rel={link.kind === "public" ? "noreferrer" : undefined}>{link.label}</a>
</span>
)}
</p>
);
}
function BodyText({ value }: { value: string }) {
if (!value) return null;
return (
<>
{value.split("\n").filter(Boolean).map((line) => <p className="docs-topic-body" key={line}>{line}</p>)}
</>
);
}
function UnavailableDocumentationReason({ topic }: { topic: DocsDocumentationTopic }) {
const rows = [
["i18n:govoplan-docs.modules.04e9462c", topic.blockers.modules],
["i18n:govoplan-docs.capabilities.ca09c54b", topic.blockers.capabilities],
["i18n:govoplan-docs.permissions.d06d5557", topic.blockers.scopes]
].filter(([, values]) => Array.isArray(values) && values.length);
return (
<div className="docs-unavailable-reason">
<p className="muted">{topic.reason}</p>
{!!rows.length &&
<dl className="detail-list compact">
{rows.map(([label, values]) =>
<div key={String(label)}>
<dt>{label}</dt>
<dd>{(values as string[]).join(", ")}</dd>
</div>
)}
</dl>
}
</div>
);
}
function docsTreeNodes(context: DocsContext | null, adminDocs: boolean): DocsTreeNode[] {
const allTopics = allDocumentationTopics(context);
const systemTopics = context?.topic_groups.system ?? [];
const workflowTopics = context?.topic_groups.workflow ?? [];
const referenceTopics = context?.topic_groups.reference ?? [];
const patternTopics = context?.topic_groups.pattern ?? [];
const systemTopicIds = new Set(systemTopics.map((topic) => topic.id));
const moduleTopics = allTopics.filter((topic) => !systemTopicIds.has(topic.id));
const workingTopics = uniqueTopics([...patternTopics, ...workflowTopics]);
const troubleshootingTopics = allTopics.filter((topic) => !topic.active || topic.layer === "evidence");
const basicsNode = categoryTreeNode(
"basics",
"i18n:govoplan-docs.basics.5fcebeef",
patternTopics,
"i18n:govoplan-docs.no_basic_topics_found.ee5ec3c6"
);
const commonTasksNode = categoryTreeNode(
"common-tasks",
"i18n:govoplan-docs.common_tasks.9f825c48",
workflowTopics,
"i18n:govoplan-docs.no_common_task_topics_found.c17814cf"
);
const nodes: DocsTreeNode[] = [
categoryTreeNode(
"about-govoplan",
"i18n:govoplan-docs.about_govoplan.6b2d7127",
systemTopics,
"i18n:govoplan-docs.no_about_topics_found.7cd8af6d"
),
{
id: "tree:working-with-govoplan",
title: "i18n:govoplan-docs.working_with_govoplan.81908ef4",
page: {
id: "working-with-govoplan:overview",
title: "i18n:govoplan-docs.working_with_govoplan.81908ef4",
kind: "topic-list",
topics: workingTopics,
emptyText: "i18n:govoplan-docs.no_working_with_govoplan_topics_found.b6db8384"
},
children: [basicsNode, commonTasksNode]
},
categoryTreeNode(
"advanced",
"i18n:govoplan-docs.advanced.203a8d6b",
referenceTopics,
"i18n:govoplan-docs.no_advanced_topics_found.271166c9"
),
{
id: "tree:modules",
title: "i18n:govoplan-docs.modules.04e9462c",
page: {
id: "modules:overview",
title: "i18n:govoplan-docs.modules.04e9462c",
kind: "topic-list",
topics: moduleTopics,
emptyText: "i18n:govoplan-docs.no_module_topics_found.0cbbdc9b"
},
children: moduleTreeNodes(moduleTopics, context)
}
];
if (adminDocs) {
nodes.push({
id: "tree:administration",
title: "i18n:govoplan-docs.administration.101aedaf",
page: {
id: "administration:overview",
title: "i18n:govoplan-docs.administration.101aedaf",
kind: "topic-list",
topics: referenceTopics,
emptyText: "i18n:govoplan-docs.no_administration_topics_found.6213dff2"
},
children: [
...referenceTopics.map(topicTreeNode),
{
id: "tree:administration:technical-reference",
title: "i18n:govoplan-docs.technical_reference.f271430d",
page: {
id: "system:technical-reference",
title: "i18n:govoplan-docs.technical_reference.f271430d",
kind: "admin-reference"
},
children: []
}
]
});
}
if (troubleshootingTopics.length) {
nodes.push(categoryTreeNode(
"troubleshooting",
"i18n:govoplan-docs.troubleshooting_and_support.3d42f434",
troubleshootingTopics,
"i18n:govoplan-docs.no_troubleshooting_topics_found.8c275468"
));
}
return nodes;
}
function categoryTreeNode(id: string, title: string, topics: DocsDocumentationTopic[], emptyText: string): DocsTreeNode {
const sortedTopics = [...topics].sort((left, right) => left.order - right.order || left.title.localeCompare(right.title));
return {
id: `tree:${id}`,
title,
page: {
id: `${id}:overview`,
title,
kind: "topic-list",
topics: sortedTopics,
emptyText
},
children: nestedTopicNodes(sortedTopics)
};
}
function topicPage(topic: DocsDocumentationTopic): DocsPageNode {
return {
id: topic.id,
title: topic.title,
kind: "topic",
topic
};
}
function topicTreeNode(topic: DocsDocumentationTopic): DocsTreeNode {
return {
id: `tree:topic:${topic.id}`,
title: topic.title,
page: topicPage(topic),
children: []
};
}
function moduleTreeNodes(topics: DocsDocumentationTopic[], context: DocsContext | null): DocsTreeNode[] {
const topicsByModule = new Map<string, DocsDocumentationTopic[]>();
for (const topic of topics) {
const moduleTopics = topicsByModule.get(topic.source_module_id) ?? [];
moduleTopics.push(topic);
topicsByModule.set(topic.source_module_id, moduleTopics);
}
const configuredModules = context?.layers.configured.modules ?? [];
const moduleOrder = new Map(configuredModules.map((module, index) => [module.id, index]));
return Array.from(topicsByModule.entries())
.sort(([left], [right]) => (moduleOrder.get(left) ?? 999) - (moduleOrder.get(right) ?? 999) || moduleTitle(left, context).localeCompare(moduleTitle(right, context)))
.map(([moduleId, moduleTopics]) => {
const sortedTopics = [...moduleTopics].sort((left, right) => left.order - right.order || left.title.localeCompare(right.title));
const title = moduleTitle(moduleId, context);
return {
id: `tree:module:${moduleId}`,
title,
page: {
id: `module:${moduleId}`,
title,
kind: "topic-list",
topics: sortedTopics,
emptyText: "i18n:govoplan-docs.no_system_topics_found.d01a8068"
},
children: nestedTopicNodes(sortedTopics)
};
});
}
function nestedTopicNodes(topics: DocsDocumentationTopic[]): DocsTreeNode[] {
const nodes = new Map(topics.map((topic) => [topic.id, topicTreeNode(topic)]));
const roots: DocsTreeNode[] = [];
for (const topic of topics) {
const node = nodes.get(topic.id);
if (!node) continue;
const parentId = topicParentId(topic);
const parent = parentId ? nodes.get(parentId) : undefined;
if (parent && parentId !== topic.id) {
parent.children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
function topicParentId(topic: DocsDocumentationTopic): string {
return metadataString(topic.metadata, "tree_parent_id") || metadataString(topic.metadata, "parent_topic_id") || metadataString(topic.metadata, "parent_id");
}
function moduleTitle(moduleId: string, context: DocsContext | null): string {
const module = context?.layers.configured.modules.find((item) => item.id === moduleId);
if (module) return module.name;
return moduleId
.replace(/^govoplan[-_]/, "")
.split(/[-_]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ") || moduleId;
}
function flattenTreePages(nodes: DocsTreeNode[]): DocsPageNode[] {
return nodes.flatMap((node) => [node.page, ...flattenTreePages(node.children)]);
}
function defaultExpandedNodeIds(nodes: DocsTreeNode[]): string[] {
return nodes.filter((node) => node.children.length).map((node) => node.id);
}
function ancestorNodeIdsForPage(nodes: DocsTreeNode[], pageId: string, ancestors: string[] = []): string[] {
for (const node of nodes) {
if (node.page.id === pageId) return ancestors;
const match = ancestorNodeIdsForPage(node.children, pageId, [...ancestors, node.id]);
if (match.length) return match;
}
return [];
}
function activeNodeIdForPage(nodes: DocsTreeNode[], pageId: string): string {
if (!pageId) return "";
for (const node of nodes) {
if (node.page.id === pageId) return node.id;
const childId = activeNodeIdForPage(node.children, pageId);
if (childId) return childId;
}
return "";
}
function allDocumentationTopics(context: DocsContext | null): DocsDocumentationTopic[] {
const topics = Object.values(context?.topic_groups ?? {}).flat();
return uniqueTopics(topics).sort((left, right) => left.order - right.order || left.title.localeCompare(right.title));
}
function uniqueTopics(topics: DocsDocumentationTopic[]): DocsDocumentationTopic[] {
const seen = new Set<string>();
return topics.filter((topic) => {
if (seen.has(topic.id)) return false;
seen.add(topic.id);
return true;
});
}
function selectedPageFromSearch(search: string, pages: DocsPageNode[]): DocsPageNode | null {
if (!pages.length) return null;
const requested = new URLSearchParams(search).get("topic") || "";
return pages.find((page) => page.id === requested) ?? pages[0];
}
function outlineForPage(page: DocsPageNode | null, showTechnical: boolean): OutlineItem[] {
if (!page) return [];
if (page.kind === "admin-reference") {
return [
{ id: page.id, label: page.title },
{ id: "docs-admin-modules", label: "i18n:govoplan-docs.modules.04e9462c" },
{ id: "docs-admin-routes", label: "i18n:govoplan-docs.routes.03730e58" },
{ id: "docs-admin-permissions", label: "i18n:govoplan-docs.permissions.d06d5557" }
];
}
if (page.kind === "topic-list") {
return [
{ id: page.id, label: page.title },
...page.topics.map((topic) => ({ id: topicAnchorId(topic), label: topic.title }))
];
}
const topic = page.topic;
const prefix = topicAnchorId(topic);
const items: OutlineItem[] = [{ id: prefix, label: topic.title }];
if (topic.summary) items.push({ id: `${prefix}-summary`, label: "i18n:govoplan-docs.summary.d6b9936d" });
if (topic.kind === "workflow") {
if (metadataString(topic.metadata, "outcome")) items.push({ id: `${prefix}-outcome`, label: "i18n:govoplan-docs.outcome.10172bd3" });
if (metadataList(topic.metadata, "prerequisites").length) items.push({ id: `${prefix}-prerequisites`, label: "i18n:govoplan-docs.prerequisites.fdf2407f" });
if (metadataList(topic.metadata, "steps").length) items.push({ id: `${prefix}-steps`, label: "i18n:govoplan-docs.steps.6041435e" });
if (metadataString(topic.metadata, "result")) items.push({ id: `${prefix}-result`, label: "i18n:govoplan-docs.result.1f4fbf43" });
if (metadataString(topic.metadata, "verification")) items.push({ id: `${prefix}-verification`, label: "i18n:govoplan-docs.verification.4b5f10dd" });
}
if (topic.kind === "reference" && metadataRecords(topic.metadata, "fields").length) {
items.push({ id: `${prefix}-fields`, label: "i18n:govoplan-docs.field.7558c082" });
}
if (topic.kind === "pattern") {
if (metadataString(topic.metadata, "purpose")) items.push({ id: `${prefix}-purpose`, label: "i18n:govoplan-docs.purpose.4af40581" });
if (metadataString(topic.metadata, "when_used")) items.push({ id: `${prefix}-when-used`, label: "i18n:govoplan-docs.when_used.b870b361" });
if (showTechnical && metadataList(topic.metadata, "component_refs").length) items.push({ id: `${prefix}-components`, label: "i18n:govoplan-docs.components.e81e57d8" });
}
return items;
}
function documentationTypeFromSearch(search: string): DocumentationType {
return new URLSearchParams(search).get("type") === "user" ? "user" : "admin";
}
function localeFromSearch(search: string): string | null {
const value = new URLSearchParams(search).get("locale");
if (!value) return null;
return value.toLowerCase().startsWith("de") ? "de" : "en";
}
function topicAnchorId(topic: DocsDocumentationTopic): string {
return topic.anchor_id || `docs-topic-${topic.source_module_id}-${topic.id}`.replace(/[^a-z0-9_-]+/gi, "-").toLowerCase();
}
function topicPageHref(topic: DocsDocumentationTopic, documentationType: DocumentationType): string {
const params = new URLSearchParams();
params.set("type", documentationType);
params.set("topic", topic.id);
return `?${params.toString()}#${topicAnchorId(topic)}`;
}
function topicIndex(context: DocsContext | null): Map<string, DocsDocumentationTopic> {
const index = new Map<string, DocsDocumentationTopic>();
for (const topic of allDocumentationTopics(context)) {
index.set(topic.id, topic);
}
return index;
}
function metadataString(metadata: Record<string, unknown>, key: string): string {
const value = metadata[key];
return typeof value === "string" ? value : "";
}
function metadataList(metadata: Record<string, unknown>, key: string): string[] {
const value = metadata[key];
if (!Array.isArray(value)) return [];
return value.filter((item): item is string => typeof item === "string");
}
function metadataRecords(metadata: Record<string, unknown>, key: string): Record<string, unknown>[] {
const value = metadata[key];
if (!Array.isArray(value)) return [];
return value.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object" && !Array.isArray(item));
}
function ModuleTable({ modules }: { modules: DocsModule[] }) {
if (!modules.length) return <p className="muted">i18n:govoplan-docs.no_configured_modules_found.f6f9ce24</p>;
return (
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr><th>i18n:govoplan-docs.module.b8ff0289</th><th>i18n:govoplan-docs.routes.03730e58</th><th>i18n:govoplan-docs.permissions.d06d5557</th><th>i18n:govoplan-docs.frontend.152d1cf2</th><th>i18n:govoplan-docs.capabilities.ca09c54b</th></tr>
</thead>
<tbody>
{modules.map((module) =>
<tr key={module.id}>
<td><strong>{module.name}</strong><span className="muted block">{module.id} {module.version}</span></td>
<td>{module.route_count} i18n:govoplan-docs.route.200e2a66 {module.nav_count} nav</td>
<td>{module.permission_count}</td>
<td>{module.frontend_package || "-"}</td>
<td>{module.capabilities.length ? module.capabilities.join(", ") : "-"}{module.documentation_count || module.documentation_provider_count ? <span className="muted block">{module.documentation_count} i18n:govoplan-docs.docs.5dc1e9b8 {module.documentation_provider_count} provider</span> : null}</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
function RouteTable({ routes, emptyText }: { routes: DocsRoute[]; emptyText: string }) {
if (!routes.length) return <p className="muted">{emptyText}</p>;
return (
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr><th>i18n:govoplan-docs.route.4999528e</th><th>i18n:govoplan-docs.module.b8ff0289</th><th>i18n:govoplan-docs.source.6da13add</th><th>i18n:govoplan-docs.requirements.09a428f9</th><th>i18n:govoplan-docs.status.bae7d5be</th></tr>
</thead>
<tbody>
{routes.map((route) =>
<tr key={`${route.source}-${route.module_id}-${route.path}`}>
<td><strong>{route.label}</strong><span className="muted block">{route.path}</span></td>
<td>{route.module_id}</td>
<td>{route.source}</td>
<td>{routeRequirements(route)}</td>
<td><StatusBadge status={route.visible ? "success" : "warning"} label={route.visible ? "visible" : route.reason} /></td>
</tr>
)}
</tbody>
</table>
</div>
);
}
function PermissionList({ permissions }: { permissions: Array<{ scope: string; label: string; category: string }> }) {
if (!permissions.length) return <p className="muted">i18n:govoplan-docs.no_granted_platform_permissions_found.36010898</p>;
return (
<dl className="detail-list">
{permissions.slice(0, 24).map((permission) =>
<div key={permission.scope}>
<dt>{permission.category}</dt>
<dd><strong>{permission.label}</strong><span className="muted"> · {permission.scope}</span></dd>
</div>
)}
{permissions.length > 24 && <div><dt>i18n:govoplan-docs.more.4bab2d8f</dt><dd>{permissions.length - 24} i18n:govoplan-docs.additional_permissions.8042cb01</dd></div>}
</dl>
);
}
function EvidenceList({ modules, sources }: { modules: DocsOptionalModuleEvidence[]; sources: DocsSource[] }) {
if (!modules.length && !sources.length) return <p className="muted">i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6</p>;
return (
<dl className="detail-list">
{modules.map((item) =>
<div key={`${item.source_module_id}-${item.module_id}`}>
<dt><StatusBadge status={item.status === "installed" ? "success" : "inactive"} label={item.status} /></dt>
<dd><strong>{item.module_id}</strong><span className="muted"> · {item.reason}</span></dd>
</div>
)}
{sources.map((item) =>
<div key={item.source}>
<dt>{item.layer}</dt>
<dd><strong>{item.label}</strong><span className="muted"> · {item.source}</span></dd>
</div>
)}
</dl>
);
}
function routeRequirements(route: DocsRoute): string {
const parts = [];
if (route.required_all.length) parts.push(`all: ${route.required_all.join(", ")}`);
if (route.required_any.length) parts.push(`any: ${route.required_any.join(", ")}`);
return parts.join(" | ") || "-";
}

View File

@@ -0,0 +1,186 @@
import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
"en": {
"i18n:govoplan-docs.additional_permissions.8042cb01": "additional permissions",
"i18n:govoplan-docs.about_govoplan.6b2d7127": "About GovOPlaN",
"i18n:govoplan-docs.admin_docs.bf504a56": "Admin docs",
"i18n:govoplan-docs.administration.101aedaf": "Administration",
"i18n:govoplan-docs.advanced.203a8d6b": "Advanced",
"i18n:govoplan-docs.available_documentation.db8bcc42": "Available documentation",
"i18n:govoplan-docs.available_routes.c2635868": "Available routes",
"i18n:govoplan-docs.basics.5fcebeef": "Basics",
"i18n:govoplan-docs.capabilities.ca09c54b": "Capabilities",
"i18n:govoplan-docs.common_tasks.9f825c48": "Common tasks",
"i18n:govoplan-docs.configured_documentation.fb6eaaad": "Configured documentation",
"i18n:govoplan-docs.docs.5dc1e9b8": "docs,",
"i18n:govoplan-docs.docs.68a41942": "Docs",
"i18n:govoplan-docs.docs.e6706405": "DOCS",
"i18n:govoplan-docs.api_mapping.e969f6f7": "API mapping",
"i18n:govoplan-docs.components.e81e57d8": "Components",
"i18n:govoplan-docs.details.a6b3c45f": "Details",
"i18n:govoplan-docs.documentation_mode.35f872f3": "Documentation mode",
"i18n:govoplan-docs.documentation_outline.6f836b99": "Documentation outline",
"i18n:govoplan-docs.documentation_type.5a66690c": "Documentation type",
"i18n:govoplan-docs.evidence.7ea014de": "Evidence",
"i18n:govoplan-docs.field.7558c082": "Field",
"i18n:govoplan-docs.frontend.152d1cf2": "Frontend",
"i18n:govoplan-docs.granted_permissions.0a232e78": "Granted permissions",
"i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad": "Guidance for the functions available in this installation.",
"i18n:govoplan-docs.help_center.f3f3a34b": "Help Center",
"i18n:govoplan-docs.loading_documentation_context.1c091645": "Loading documentation context...",
"i18n:govoplan-docs.meaning.584d8aa0": "Meaning",
"i18n:govoplan-docs.module.b8ff0289": "Module",
"i18n:govoplan-docs.modules.04e9462c": "Modules",
"i18n:govoplan-docs.more_examples.383f0e0e": "More examples",
"i18n:govoplan-docs.more.4bab2d8f": "More",
"i18n:govoplan-docs.no_about_topics_found.7cd8af6d": "No GovOPlaN overview topics found.",
"i18n:govoplan-docs.no_administration_topics_found.6213dff2": "No administration topics found.",
"i18n:govoplan-docs.no_advanced_topics_found.271166c9": "No advanced topics found.",
"i18n:govoplan-docs.no_always_present_documentation_sections_found.3b89b31e": "No always-present documentation sections found.",
"i18n:govoplan-docs.no_available_documentation_topics_found.38b84a6d": "No available documentation topics found.",
"i18n:govoplan-docs.no_basic_topics_found.ee5ec3c6": "No basic topics found.",
"i18n:govoplan-docs.no_common_task_topics_found.c17814cf": "No common task topics found.",
"i18n:govoplan-docs.no_configured_documentation_topics_found.04dcfb92": "No configured documentation topics found.",
"i18n:govoplan-docs.no_configured_modules_found.f6f9ce24": "No configured modules found.",
"i18n:govoplan-docs.no_evidence_documentation_topics_found.c59bd849": "No evidence documentation topics found.",
"i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6": "No evidence sources found.",
"i18n:govoplan-docs.no_granted_platform_permissions_found.36010898": "No granted platform permissions found.",
"i18n:govoplan-docs.no_hidden_installed_routes_found.22f62845": "No hidden installed routes found.",
"i18n:govoplan-docs.no_module_topics_found.0cbbdc9b": "No module topics found.",
"i18n:govoplan-docs.no_outline_items.3ea2842e": "No outline items for this page.",
"i18n:govoplan-docs.no_pattern_topics_found.35b70eee": "No design-pattern topics found.",
"i18n:govoplan-docs.no_reference_topics_found.724d5a68": "No reference topics found.",
"i18n:govoplan-docs.no_system_topics_found.d01a8068": "No system documentation topics found.",
"i18n:govoplan-docs.no_visible_routes_found_for_this_actor.3feeaf0b": "No visible routes found for this actor.",
"i18n:govoplan-docs.no_workflow_topics_found.a82b52da": "No workflow topics found.",
"i18n:govoplan-docs.no_troubleshooting_topics_found.8c275468": "No troubleshooting topics found.",
"i18n:govoplan-docs.no_working_with_govoplan_topics_found.b6db8384": "No working-with-GovOPlaN topics found.",
"i18n:govoplan-docs.outcome.10172bd3": "Outcome",
"i18n:govoplan-docs.page_outline.e5940949": "On this page",
"i18n:govoplan-docs.patterns.a2bc4d8f": "Patterns",
"i18n:govoplan-docs.permission.d71c9448": "Permission",
"i18n:govoplan-docs.permissions.d06d5557": "Permissions",
"i18n:govoplan-docs.prerequisites.fdf2407f": "Prerequisites",
"i18n:govoplan-docs.purpose.4af40581": "Purpose",
"i18n:govoplan-docs.reference.1fd9d50a": "Reference",
"i18n:govoplan-docs.related_topics.d5eb8b74": "Related topics:",
"i18n:govoplan-docs.reload.cce71553": "Reload",
"i18n:govoplan-docs.requirements.09a428f9": "Requirements",
"i18n:govoplan-docs.result.1f4fbf43": "Result",
"i18n:govoplan-docs.route.200e2a66": "route,",
"i18n:govoplan-docs.route.4999528e": "Route",
"i18n:govoplan-docs.routes.03730e58": "Routes",
"i18n:govoplan-docs.screen.c4878ec4": "Screen",
"i18n:govoplan-docs.section.5e498158": "Section",
"i18n:govoplan-docs.source.6da13add": "Source",
"i18n:govoplan-docs.status.bae7d5be": "Status",
"i18n:govoplan-docs.steps.6041435e": "Steps",
"i18n:govoplan-docs.summary.d6b9936d": "Summary",
"i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e": "Technical documentation for the modules and configuration active in this installation.",
"i18n:govoplan-docs.technical_reference.f271430d": "Technical reference",
"i18n:govoplan-docs.this_system.b13a51ad": "This system",
"i18n:govoplan-docs.toggle_tree_node.4070b59f": "Expand or collapse",
"i18n:govoplan-docs.troubleshooting_and_support.3d42f434": "Troubleshooting and support",
"i18n:govoplan-docs.unlocks.67d569db": "Unlocks:",
"i18n:govoplan-docs.user_docs.1e38e8d3": "User docs",
"i18n:govoplan-docs.validation.ef7f6d9c": "Validation",
"i18n:govoplan-docs.verification.4b5f10dd": "Verification",
"i18n:govoplan-docs.visible_routes.6c9b1605": "Visible routes",
"i18n:govoplan-docs.when_used.b870b361": "When used",
"i18n:govoplan-docs.working_with_govoplan.81908ef4": "Working with GovOPlaN",
"i18n:govoplan-docs.workflows.4f4a16d1": "Workflows",
"i18n:govoplan-docs.your_documentation.8a4cd9a3": "Your documentation"
},
"de": {
"i18n:govoplan-docs.additional_permissions.8042cb01": "additional permissions",
"i18n:govoplan-docs.about_govoplan.6b2d7127": "Über GovOPlaN",
"i18n:govoplan-docs.admin_docs.bf504a56": "Administrationsdokumentation",
"i18n:govoplan-docs.administration.101aedaf": "Administration",
"i18n:govoplan-docs.advanced.203a8d6b": "Fortgeschritten",
"i18n:govoplan-docs.available_documentation.db8bcc42": "Verfügbare Dokumentation",
"i18n:govoplan-docs.available_routes.c2635868": "Verfügbare Routen",
"i18n:govoplan-docs.basics.5fcebeef": "Grundlagen",
"i18n:govoplan-docs.capabilities.ca09c54b": "Fähigkeiten",
"i18n:govoplan-docs.common_tasks.9f825c48": "Häufige Aufgaben",
"i18n:govoplan-docs.configured_documentation.fb6eaaad": "Configured documentation",
"i18n:govoplan-docs.docs.5dc1e9b8": "docs,",
"i18n:govoplan-docs.docs.68a41942": "Dokumentation",
"i18n:govoplan-docs.docs.e6706405": "DOCS",
"i18n:govoplan-docs.api_mapping.e969f6f7": "API-Zuordnung",
"i18n:govoplan-docs.components.e81e57d8": "Komponenten",
"i18n:govoplan-docs.details.a6b3c45f": "Details",
"i18n:govoplan-docs.documentation_mode.35f872f3": "Dokumentationsmodus",
"i18n:govoplan-docs.documentation_outline.6f836b99": "Dokumentationsübersicht",
"i18n:govoplan-docs.documentation_type.5a66690c": "Dokumentationstyp",
"i18n:govoplan-docs.evidence.7ea014de": "Evidence",
"i18n:govoplan-docs.field.7558c082": "Feld",
"i18n:govoplan-docs.frontend.152d1cf2": "Frontend",
"i18n:govoplan-docs.granted_permissions.0a232e78": "Gewährte Berechtigungen",
"i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad": "Anleitung für die in dieser Installation verfügbaren Funktionen.",
"i18n:govoplan-docs.help_center.f3f3a34b": "Hilfezentrum",
"i18n:govoplan-docs.loading_documentation_context.1c091645": "Dokumentationskontext wird geladen...",
"i18n:govoplan-docs.meaning.584d8aa0": "Bedeutung",
"i18n:govoplan-docs.module.b8ff0289": "Modul",
"i18n:govoplan-docs.modules.04e9462c": "Module",
"i18n:govoplan-docs.more_examples.383f0e0e": "More examples",
"i18n:govoplan-docs.more.4bab2d8f": "Weitere",
"i18n:govoplan-docs.no_about_topics_found.7cd8af6d": "Keine GovOPlaN-Übersichtsthemen gefunden.",
"i18n:govoplan-docs.no_administration_topics_found.6213dff2": "Keine Administrationsthemen gefunden.",
"i18n:govoplan-docs.no_advanced_topics_found.271166c9": "Keine fortgeschrittenen Themen gefunden.",
"i18n:govoplan-docs.no_always_present_documentation_sections_found.3b89b31e": "Keine immer vorhandenen Dokumentationsabschnitte gefunden.",
"i18n:govoplan-docs.no_available_documentation_topics_found.38b84a6d": "Keine verfügbaren Dokumentationsthemen gefunden.",
"i18n:govoplan-docs.no_basic_topics_found.ee5ec3c6": "Keine Grundlagenthemen gefunden.",
"i18n:govoplan-docs.no_common_task_topics_found.c17814cf": "Keine Themen zu häufigen Aufgaben gefunden.",
"i18n:govoplan-docs.no_configured_documentation_topics_found.04dcfb92": "Keine konfigurierten Dokumentationsthemen gefunden.",
"i18n:govoplan-docs.no_configured_modules_found.f6f9ce24": "Keine konfigurierten Module gefunden.",
"i18n:govoplan-docs.no_evidence_documentation_topics_found.c59bd849": "Keine Nachweis-Dokumentationsthemen gefunden.",
"i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6": "Keine Nachweisquellen gefunden.",
"i18n:govoplan-docs.no_granted_platform_permissions_found.36010898": "Keine gewährten Plattformberechtigungen gefunden.",
"i18n:govoplan-docs.no_hidden_installed_routes_found.22f62845": "Keine ausgeblendeten installierten Routen gefunden.",
"i18n:govoplan-docs.no_module_topics_found.0cbbdc9b": "Keine Modulthemen gefunden.",
"i18n:govoplan-docs.no_outline_items.3ea2842e": "Keine Gliederungspunkte für diese Seite.",
"i18n:govoplan-docs.no_pattern_topics_found.35b70eee": "Keine Themen zu Entwurfsmustern gefunden.",
"i18n:govoplan-docs.no_reference_topics_found.724d5a68": "Keine Referenzthemen gefunden.",
"i18n:govoplan-docs.no_system_topics_found.d01a8068": "Keine Systemdokumentation gefunden.",
"i18n:govoplan-docs.no_visible_routes_found_for_this_actor.3feeaf0b": "Keine sichtbaren Routen für diesen Akteur gefunden.",
"i18n:govoplan-docs.no_workflow_topics_found.a82b52da": "Keine Ablaufanleitungen gefunden.",
"i18n:govoplan-docs.no_troubleshooting_topics_found.8c275468": "Keine Themen zu Fehlerbehebung gefunden.",
"i18n:govoplan-docs.no_working_with_govoplan_topics_found.b6db8384": "Keine Themen zum Arbeiten mit GovOPlaN gefunden.",
"i18n:govoplan-docs.outcome.10172bd3": "Ziel",
"i18n:govoplan-docs.page_outline.e5940949": "Auf dieser Seite",
"i18n:govoplan-docs.patterns.a2bc4d8f": "Muster",
"i18n:govoplan-docs.permission.d71c9448": "Berechtigung",
"i18n:govoplan-docs.permissions.d06d5557": "Berechtigungen",
"i18n:govoplan-docs.prerequisites.fdf2407f": "Voraussetzungen",
"i18n:govoplan-docs.purpose.4af40581": "Zweck",
"i18n:govoplan-docs.reference.1fd9d50a": "Referenz",
"i18n:govoplan-docs.related_topics.d5eb8b74": "Verwandte Themen:",
"i18n:govoplan-docs.reload.cce71553": "Neu laden",
"i18n:govoplan-docs.requirements.09a428f9": "Anforderungen",
"i18n:govoplan-docs.result.1f4fbf43": "Ergebnis",
"i18n:govoplan-docs.route.200e2a66": "route,",
"i18n:govoplan-docs.route.4999528e": "Route",
"i18n:govoplan-docs.routes.03730e58": "Routen",
"i18n:govoplan-docs.screen.c4878ec4": "Ansicht",
"i18n:govoplan-docs.section.5e498158": "Bereich",
"i18n:govoplan-docs.source.6da13add": "Quelle",
"i18n:govoplan-docs.status.bae7d5be": "Status",
"i18n:govoplan-docs.steps.6041435e": "Schritte",
"i18n:govoplan-docs.summary.d6b9936d": "Zusammenfassung",
"i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e": "Technische Dokumentation für die in dieser Installation aktiven Module und Konfiguration.",
"i18n:govoplan-docs.technical_reference.f271430d": "Technische Referenz",
"i18n:govoplan-docs.this_system.b13a51ad": "Dieses System",
"i18n:govoplan-docs.toggle_tree_node.4070b59f": "Auf- oder zuklappen",
"i18n:govoplan-docs.troubleshooting_and_support.3d42f434": "Fehlerbehebung und Support",
"i18n:govoplan-docs.unlocks.67d569db": "Schaltet frei:",
"i18n:govoplan-docs.user_docs.1e38e8d3": "Benutzerdokumentation",
"i18n:govoplan-docs.validation.ef7f6d9c": "Prüfung",
"i18n:govoplan-docs.verification.4b5f10dd": "Kontrolle",
"i18n:govoplan-docs.visible_routes.6c9b1605": "Sichtbare Routen",
"i18n:govoplan-docs.when_used.b870b361": "Wann verwendet",
"i18n:govoplan-docs.working_with_govoplan.81908ef4": "Arbeiten mit GovOPlaN",
"i18n:govoplan-docs.workflows.4f4a16d1": "Abläufe",
"i18n:govoplan-docs.your_documentation.8a4cd9a3": "Your documentation"
}
};

2
webui/src/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { docsModule as default, docsModule } from "./module";
export { default as DocsPage } from "./features/docs/DocsPage";

27
webui/src/module.ts Normal file
View File

@@ -0,0 +1,27 @@
import { createElement, lazy } from "react";
import type { PlatformWebModule } from "@govoplan/core-webui";
import { generatedTranslations } from "./i18n/generatedTranslations";
const DocsPage = lazy(() => import("./features/docs/DocsPage"));
const docsReadScopes = ["docs:documentation:read", "system:settings:read", "admin:settings:read"];
const translations = {
en: generatedTranslations.en,
de: generatedTranslations.de
};
export const docsModule: PlatformWebModule = {
id: "docs",
label: "i18n:govoplan-docs.docs.68a41942",
version: "1.0.0",
dependencies: ["access"],
optionalDependencies: ["policy", "audit", "ops", "workflow", "search"],
translations,
navItems: [{ to: "/docs", label: "i18n:govoplan-docs.docs.68a41942", iconName: "reports", anyOf: docsReadScopes, order: 880 }],
routes: [
{ path: "/docs", anyOf: docsReadScopes, order: 880, render: ({ settings }) => createElement(DocsPage, { settings }) }]
};
export default docsModule;