fix(docs): bind user guidance to visible routes

This commit is contained in:
2026-07-21 19:14:58 +02:00
parent e9f3106657
commit 7cdd1e1c78
3 changed files with 57 additions and 10 deletions

View File

@@ -43,16 +43,17 @@ def docs_context(
registry = _registry(request)
resolved_locale = _preferred_locale(request, locale)
route_items = _route_items(registry.manifests(), principal)
visible_routes = [item for item in route_items if item["visible"]]
visible_route_items = [item for item in route_items if item["visible"]]
documentation_layers = _documentation_layers(request, registry, principal, documentation_type=documentation_type, locale=resolved_locale)
if documentation_type == "admin":
visible_routes = visible_route_items
modules = [_module_payload(manifest, technical=True) for manifest in registry.manifests()]
permissions = [_permission_payload(permission, principal) for permission in registry.permissions()]
available_routes = [item for item in route_items if not item["visible"]]
optional_modules = _optional_module_evidence(registry.manifests())
else:
visible_module_ids = {
*(str(item["module_id"]) for item in visible_routes),
*(str(item["module_id"]) for item in visible_route_items),
*(str(topic["source_module_id"]) for topic in _all_documentation_topics(documentation_layers)),
}
modules = [
@@ -61,7 +62,7 @@ def docs_context(
if manifest.id in visible_module_ids
]
permissions = []
visible_routes = [_user_route_payload(item) for item in visible_routes]
visible_routes = _user_route_payloads(visible_route_items)
available_routes = []
optional_modules = []
documentation_count = sum(len(layer) for layer in documentation_layers.values())
@@ -166,6 +167,18 @@ def _user_route_payload(item: Mapping[str, Any]) -> dict[str, Any]:
}
def _user_route_payloads(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
seen: set[tuple[str, str]] = set()
for item in items:
key = (str(item["module_id"]), str(item["path"]))
if key in seen:
continue
seen.add(key)
result.append(_user_route_payload(item))
return result
def _permission_payload(permission: PermissionDefinition, principal: ApiPrincipal) -> dict[str, Any]:
return {
"scope": permission.scope,
@@ -305,6 +318,14 @@ def _classify_documentation(
) -> 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()}
visible_runtime_paths = frozenset([
"/settings", # Authenticated shell route, not contributed by a module manifest.
*(
str(item["path"])
for item in _route_items(registry.manifests(), principal)
if item["visible"]
),
]) if documentation_type == "user" else frozenset()
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):
@@ -324,6 +345,7 @@ def _classify_documentation(
blockers=blockers,
locale=locale,
documentation_type=documentation_type,
visible_runtime_paths=visible_runtime_paths,
))
return layers
@@ -539,6 +561,7 @@ def _documentation_topic_payload(
blockers: dict[str, list[str]],
locale: str,
documentation_type: DocumentationType,
visible_runtime_paths: frozenset[str],
) -> dict[str, Any]:
module_id = topic.source_module_id or source_module_id
translation_locale, translation = _translation_for_locale(topic, locale)
@@ -598,7 +621,7 @@ def _documentation_topic_payload(
"links": [
_documentation_link_payload(link)
for link in topic.links
if _user_link_allowed(link)
if _user_link_allowed(link, visible_runtime_paths=visible_runtime_paths)
],
"related_modules": [],
"unlocks": list(topic.unlocks),
@@ -680,16 +703,22 @@ def _bounded_string_list(value: object, *, maximum_items: int, maximum_length: i
]
def _user_link_allowed(link: DocumentationLink) -> bool:
def _user_link_allowed(link: DocumentationLink, *, visible_runtime_paths: frozenset[str]) -> bool:
href = link.href.strip()
if link.kind == "runtime":
if not href.startswith("/") or href.startswith("//") or "\\" in href:
return False
parsed = urlsplit(href)
return not parsed.scheme and not parsed.netloc
try:
parsed = urlsplit(href)
except ValueError:
return False
return not parsed.scheme and not parsed.netloc and parsed.path in visible_runtime_paths
if link.kind != "public":
return False
parsed = urlsplit(href)
try:
parsed = urlsplit(href)
except ValueError:
return False
return parsed.scheme == "https" and bool(parsed.netloc) and not parsed.username and not parsed.password

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
from govoplan_core.core.modules import (
DocumentationCondition,
DocumentationLink,
DocumentationTopic,
FrontendModule,
@@ -168,6 +169,11 @@ manifest = ModuleManifest(
audience=("tenant_admin", "access_admin", "operator", "user"),
related_modules=("organizations", "identity", "idm", "access"),
order=21,
conditions=(
DocumentationCondition(
required_modules=("organizations", "identity", "idm", "access"),
),
),
links=(
DocumentationLink(label="Organizations", href="/organizations", kind="runtime"),
DocumentationLink(label="IDM assignments", href="/idm", kind="runtime"),