diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5fd4061 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 0063dff..02cc131 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,9 @@ Frontend package: ``` 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. diff --git a/docs/DOCS_BACKLOG.md b/docs/DOCS_BACKLOG.md deleted file mode 100644 index 11b95f9..0000000 --- a/docs/DOCS_BACKLOG.md +++ /dev/null @@ -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. diff --git a/docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md b/docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md new file mode 100644 index 0000000..a6f5b34 --- /dev/null +++ b/docs/DOCUMENTATION_EXPERIENCE_CONCEPT.md @@ -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. diff --git a/docs/DOCUMENTATION_LAYER_CONCEPT.md b/docs/DOCUMENTATION_LAYER_CONCEPT.md index 4ee72b8..1c742e0 100644 --- a/docs/DOCUMENTATION_LAYER_CONCEPT.md +++ b/docs/DOCUMENTATION_LAYER_CONCEPT.md @@ -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. +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 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: - module manifest documentation metadata +- module manifest runtime documentation providers - route metadata and page-level help references - capability metadata - 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. +### 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 The runtime docs UI should be context-aware: @@ -111,6 +224,7 @@ The first implementation should define: - a configured/available/evidence classifier - a minimal API for current-context documentation - a WebUI route contribution for the documentation browser +- a provider contract for configuration-derived runtime topics The first UI can be simple: @@ -119,5 +233,6 @@ The first UI can be simple: - active policies - available-but-disabled evidence - 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. diff --git a/docs/INSTANCE_AWARE_DOCUMENTATION.md b/docs/INSTANCE_AWARE_DOCUMENTATION.md new file mode 100644 index 0000000..fdaab82 --- /dev/null +++ b/docs/INSTANCE_AWARE_DOCUMENTATION.md @@ -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. diff --git a/package.json b/package.json new file mode 100644 index 0000000..65fab60 --- /dev/null +++ b/package.json @@ -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 + } + } +} + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5c026d0 --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/src/govoplan_docs/__init__.py b/src/govoplan_docs/__init__.py new file mode 100644 index 0000000..247fcd4 --- /dev/null +++ b/src/govoplan_docs/__init__.py @@ -0,0 +1,5 @@ +"""GovOPlaN configured-system documentation module.""" + +__all__ = ["__version__"] + +__version__ = "0.1.6" diff --git a/src/govoplan_docs/backend/__init__.py b/src/govoplan_docs/backend/__init__.py new file mode 100644 index 0000000..436b315 --- /dev/null +++ b/src/govoplan_docs/backend/__init__.py @@ -0,0 +1 @@ +"""Backend integration for the GovOPlaN docs module.""" diff --git a/src/govoplan_docs/backend/api/__init__.py b/src/govoplan_docs/backend/api/__init__.py new file mode 100644 index 0000000..74bc064 --- /dev/null +++ b/src/govoplan_docs/backend/api/__init__.py @@ -0,0 +1 @@ +"""Docs API package.""" diff --git a/src/govoplan_docs/backend/api/v1/__init__.py b/src/govoplan_docs/backend/api/v1/__init__.py new file mode 100644 index 0000000..a1478a3 --- /dev/null +++ b/src/govoplan_docs/backend/api/v1/__init__.py @@ -0,0 +1 @@ +"""Docs API v1 package.""" diff --git a/src/govoplan_docs/backend/api/v1/routes.py b/src/govoplan_docs/backend/api/v1/routes.py new file mode 100644 index 0000000..5d91a77 --- /dev/null +++ b/src/govoplan_docs/backend/api/v1/routes.py @@ -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", + }, + ] diff --git a/src/govoplan_docs/backend/manifest.py b/src/govoplan_docs/backend/manifest.py new file mode 100644 index 0000000..dcea706 --- /dev/null +++ b/src/govoplan_docs/backend/manifest.py @@ -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 diff --git a/src/govoplan_docs/py.typed b/src/govoplan_docs/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/govoplan_docs/py.typed @@ -0,0 +1 @@ + diff --git a/tests/test_docs_context.py b/tests/test_docs_context.py new file mode 100644 index 0000000..4799fc7 --- /dev/null +++ b/tests/test_docs_context.py @@ -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() diff --git a/webui/package.json b/webui/package.json new file mode 100644 index 0000000..b51e5a4 --- /dev/null +++ b/webui/package.json @@ -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 + } + } +} diff --git a/webui/src/api/docs.ts b/webui/src/api/docs.ts new file mode 100644 index 0000000..693e84a --- /dev/null +++ b/webui/src/api/docs.ts @@ -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; +}; + +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 { + 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}` : ""}`); +} diff --git a/webui/src/features/docs/DocsPage.tsx b/webui/src/features/docs/DocsPage.tsx new file mode 100644 index 0000000..d7a6090 --- /dev/null +++ b/webui/src/features/docs/DocsPage.tsx @@ -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(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [documentationType, setDocumentationType] = useState(() => documentationTypeFromSearch(location.search)); + const [expandedNodes, setExpandedNodes] = useState>(() => 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 ( +
+ +
+
+
+
+ i18n:govoplan-docs.help_center.f3f3a34b +

{adminDocs ? "i18n:govoplan-docs.technical_documentation_for_the_modules_and_conf.267e739e" : "i18n:govoplan-docs.guidance_for_the_functions_available_in_this_ins.723b7cad"}

+
+
+ +
+
+ + {error && {error}} + + +
+
+ +
+ +
+
+
+
+
+ ); + + 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 ( + + ); +} + +function PageOutline({ items }: { items: OutlineItem[] }) { + return ( + + ); +} + +function SelectedPageContent({ + page, + adminDocs, + documentationType, + topicById, + moduleRows, + visibleRoutes, + availableRoutes, + grantedPermissions, + evidenceModules, + evidenceSources +}: { + page: DocsPageNode | null; + adminDocs: boolean; + documentationType: DocumentationType; + topicById: Map; + moduleRows: DocsModule[]; + visibleRoutes: DocsRoute[]; + availableRoutes: DocsRoute[]; + grantedPermissions: Array<{ scope: string; label: string; category: string }>; + evidenceModules: DocsOptionalModuleEvidence[]; + evidenceSources: DocsSource[]; +}) { + if (!page) { + return ( +
+

i18n:govoplan-docs.help_center.f3f3a34b

+

i18n:govoplan-docs.no_system_topics_found.d01a8068

+
+ ); + } + if (page.kind === "topic") { + return ; + } + if (page.kind === "admin-reference") { + return ( +
+

{page.title}

+
+

i18n:govoplan-docs.modules.04e9462c

+ +
+
+

i18n:govoplan-docs.visible_routes.6c9b1605

+ +

i18n:govoplan-docs.available_routes.c2635868

+ +
+
+

i18n:govoplan-docs.granted_permissions.0a232e78

+ + +
+
+ ); + } + return ( +
+

{page.title}

+ +
+ ); +} + +function TopicPage({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map }) { + return ( +
+
+ + {topic.source_module_id} + {showTechnical && {topic.i18n_key} · {topic.translation_locale}} +
+

{topic.title}

+ {topic.summary &&

{topic.summary}

} +
+ + + {!topic.active && } +
+ {!!topic.unlocks.length &&

i18n:govoplan-docs.unlocks.67d569db {topic.unlocks.join(" ")}

} + {!!topic.links.length && } +
+ ); +} + +function DocumentationTopicList({ topics, emptyText, showTechnical = false, documentationType, topicById }: { topics: DocsDocumentationTopic[]; emptyText: string; showTechnical?: boolean; documentationType: DocumentationType; topicById: Map }) { + if (!topics.length) return

{emptyText}

; + return ( +
+ {topics.map((topic) => +
+
+ + {topic.source_module_id} + {showTechnical && {topic.i18n_key} · {topic.translation_locale}} +
+

{topic.title}

+ {topic.summary &&

{topic.summary}

} + + + {!topic.active && } + {!!topic.unlocks.length &&

i18n:govoplan-docs.unlocks.67d569db {topic.unlocks.join(" ")}

} + {!!topic.links.length && } +
+ )} +
+ ); +} + +function TopicMetadata({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map }) { + if (topic.kind === "workflow") return ; + if (topic.kind === "reference") return ; + if (topic.kind === "pattern") return ; + return ; +} + +function WorkflowDetails({ topic, documentationType, topicById }: { topic: DocsDocumentationTopic; documentationType: DocumentationType; topicById: Map }) { + 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 ( +
+ {outcome && } + {!!prerequisites.length && } + {!!steps.length && +
+

i18n:govoplan-docs.steps.6041435e

+
    + {steps.map((step) =>
  1. {step}
  2. )} +
+
+ } + {result && } + {verification && } + +
+ ); +} + +function ReferenceDetails({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map }) { + 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 ( +
+ {(route || screen || section) && +
+ {screen &&
i18n:govoplan-docs.screen.c4878ec4
{screen}
} + {section &&
i18n:govoplan-docs.section.5e498158
{section}
} + {showTechnical && route &&
i18n:govoplan-docs.route.4999528e
{route}
} +
+ } + {!!fields.length && } + +
+ ); +} + +function ReferenceFieldTable({ id, fields, showTechnical }: { id: string; fields: Record[]; showTechnical: boolean }) { + return ( +
+ + + + + + {showTechnical && } + {showTechnical && } + + + + + {fields.map((field) => + + + + {showTechnical && } + {showTechnical && } + + + )} + +
i18n:govoplan-docs.field.7558c082i18n:govoplan-docs.meaning.584d8aa0i18n:govoplan-docs.api_mapping.e969f6f7i18n:govoplan-docs.permission.d71c9448i18n:govoplan-docs.validation.ef7f6d9c
{metadataString(field, "label")} + {metadataString(field, "user_description")} + {showTechnical && metadataString(field, "admin_description") && {metadataString(field, "admin_description")}} + {showTechnical && metadataString(field, "provenance") && {metadataString(field, "provenance")}} + {metadataString(field, "api_path")}{metadataString(field, "api_field")}{metadataString(field, "permission_scope") || "-"}{metadataString(field, "validation") || "-"}
+
+ ); +} + +function PatternDetails({ topic, showTechnical, documentationType, topicById }: { topic: DocsDocumentationTopic; showTechnical: boolean; documentationType: DocumentationType; topicById: Map }) { + 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 ( +
+ {purpose && } + {whenUsed && } + {explanation && } + {showTechnical && !!componentRefs.length && } + +
+ ); +} + +function DetailBlock({ id, title, value }: { id: string; title: string; value: string }) { + return ( +
+

{title}

+

{value}

+
+ ); +} + +function DetailList({ id, title, items }: { id: string; title: string; items: string[] }) { + return ( +
+

{title}

+
    + {items.map((item) =>
  • {item}
  • )} +
+
+ ); +} + +function RelatedTopics({ topic, documentationType, topicById }: { topic: DocsDocumentationTopic; documentationType: DocumentationType; topicById: Map }) { + 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 ( +

+ i18n:govoplan-docs.related_topics.d5eb8b74 + {related.map((item, index) => + + {index > 0 && " | "} + {item.title} + + )} +

+ ); +} + +function TopicLinks({ topic }: { topic: DocsDocumentationTopic }) { + return ( +

+ {topic.links.map((link, index) => + + {index > 0 && " | "} + {link.label} + + )} +

+ ); +} + +function BodyText({ value }: { value: string }) { + if (!value) return null; + return ( + <> + {value.split("\n").filter(Boolean).map((line) =>

{line}

)} + + ); +} + +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 ( +
+

{topic.reason}

+ {!!rows.length && +
+ {rows.map(([label, values]) => +
+
{label}
+
{(values as string[]).join(", ")}
+
+ )} +
+ } +
+ ); +} + +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(); + 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(); + 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 { + const index = new Map(); + for (const topic of allDocumentationTopics(context)) { + index.set(topic.id, topic); + } + return index; +} + +function metadataString(metadata: Record, key: string): string { + const value = metadata[key]; + return typeof value === "string" ? value : ""; +} + +function metadataList(metadata: Record, 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, key: string): Record[] { + const value = metadata[key]; + if (!Array.isArray(value)) return []; + return value.filter((item): item is Record => Boolean(item) && typeof item === "object" && !Array.isArray(item)); +} + +function ModuleTable({ modules }: { modules: DocsModule[] }) { + if (!modules.length) return

i18n:govoplan-docs.no_configured_modules_found.f6f9ce24

; + return ( +
+ + + + + + {modules.map((module) => + + + + + + + + )} + +
i18n:govoplan-docs.module.b8ff0289i18n:govoplan-docs.routes.03730e58i18n:govoplan-docs.permissions.d06d5557i18n:govoplan-docs.frontend.152d1cf2i18n:govoplan-docs.capabilities.ca09c54b
{module.name}{module.id} {module.version}{module.route_count} i18n:govoplan-docs.route.200e2a66 {module.nav_count} nav{module.permission_count}{module.frontend_package || "-"}{module.capabilities.length ? module.capabilities.join(", ") : "-"}{module.documentation_count || module.documentation_provider_count ? {module.documentation_count} i18n:govoplan-docs.docs.5dc1e9b8 {module.documentation_provider_count} provider : null}
+
+ ); +} + +function RouteTable({ routes, emptyText }: { routes: DocsRoute[]; emptyText: string }) { + if (!routes.length) return

{emptyText}

; + return ( +
+ + + + + + {routes.map((route) => + + + + + + + + )} + +
i18n:govoplan-docs.route.4999528ei18n:govoplan-docs.module.b8ff0289i18n:govoplan-docs.source.6da13addi18n:govoplan-docs.requirements.09a428f9i18n:govoplan-docs.status.bae7d5be
{route.label}{route.path}{route.module_id}{route.source}{routeRequirements(route)}
+
+ ); +} + +function PermissionList({ permissions }: { permissions: Array<{ scope: string; label: string; category: string }> }) { + if (!permissions.length) return

i18n:govoplan-docs.no_granted_platform_permissions_found.36010898

; + return ( +
+ {permissions.slice(0, 24).map((permission) => +
+
{permission.category}
+
{permission.label} · {permission.scope}
+
+ )} + {permissions.length > 24 &&
i18n:govoplan-docs.more.4bab2d8f
{permissions.length - 24} i18n:govoplan-docs.additional_permissions.8042cb01
} +
+ ); +} + +function EvidenceList({ modules, sources }: { modules: DocsOptionalModuleEvidence[]; sources: DocsSource[] }) { + if (!modules.length && !sources.length) return

i18n:govoplan-docs.no_evidence_sources_found.be3bb2f6

; + return ( +
+ {modules.map((item) => +
+
+
{item.module_id} · {item.reason}
+
+ )} + {sources.map((item) => +
+
{item.layer}
+
{item.label} · {item.source}
+
+ )} +
+ ); +} + +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(" | ") || "-"; +} diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts new file mode 100644 index 0000000..34c98dd --- /dev/null +++ b/webui/src/i18n/generatedTranslations.ts @@ -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" + } +}; diff --git a/webui/src/index.ts b/webui/src/index.ts new file mode 100644 index 0000000..5ce6a32 --- /dev/null +++ b/webui/src/index.ts @@ -0,0 +1,2 @@ +export { docsModule as default, docsModule } from "./module"; +export { default as DocsPage } from "./features/docs/DocsPage"; diff --git a/webui/src/module.ts b/webui/src/module.ts new file mode 100644 index 0000000..4fd6464 --- /dev/null +++ b/webui/src/module.ts @@ -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; \ No newline at end of file