Compare commits

...
8 Commits
Author SHA1 Message Date
zemion 5de46e9c0e chore(webui): load Files folder sync release
Module Package Release / publish-packages (push) Successful in 12s
2026-08-21 22:50:48 +02:00
zemion 1d9b677c1b chore(webui): load IDM relationship administration release
Module Package Release / publish-packages (push) Successful in 12s
2026-08-21 22:06:29 +02:00
zemion 54178ee56c chore(webui): load Access credential help release
Module Package Release / publish-packages (push) Successful in 12s
2026-08-21 21:15:31 +02:00
zemion 10e7597612 feat(core): define datasource visibility policy contract
Module Package Release / publish-packages (push) Failing after 6s
2026-08-21 20:31:51 +02:00
zemion 142ccbc587 feat(core): define managed tabular file contract
Module Package Release / publish-packages (push) Successful in 12s
2026-08-21 19:29:48 +02:00
zemion f75ad48d78 test(core): isolate entitlement navigation fixture 2026-08-21 19:26:00 +02:00
zemion 5a9e8f79f9 test(webui): cover contextual help in the browser 2026-08-21 17:36:06 +02:00
zemion fbea74a74b feat(core): define durable datasource artifact contract 2026-08-21 17:36:05 +02:00
17 changed files with 534 additions and 38 deletions
+21
View File
@@ -48,6 +48,12 @@ module and server restrictions, lower-scope visibility, activation, save, and
irreversible deletion. This ensures F1 explains secret custody and the effect on
dependent connections from system, tenant, group, user, and personal surfaces.
The source inventory treats literal `helpContextId` and
`data-help-context-id` declarations as authored help associations, including a
native control nested in `FormField`. Dynamic context expressions remain
separate evidence and generic derived fallbacks remain in the richer-help
candidate queue.
The generated `help_review_candidates` list is therefore a content-depth queue,
not a list of controls on which F1 cannot work. It should prioritize:
@@ -57,6 +63,14 @@ not a list of controls on which F1 cannot work. It should prioritize:
4. provider authority, synchronization, conflict, and outcome unknown;
5. fields whose consequences are not evident from their label.
The shared browser conformance journey mounts the production Help menu and
resolver. It proves that F1 uses the focused control rather than only the page,
maps an exact retention action to Policy-owned administrator documentation,
retains the page context as fallback for derived actions, exposes an accessible
modal at narrow widths, closes with Escape, and restores focus to the triggering
control. Module journeys should add their own exact high-risk mappings; they do
not need to reimplement the keyboard or dialog mechanics.
## Verification
```bash
@@ -74,3 +88,10 @@ The check must report:
- no duplicate stable IDs;
- no undeclared public WebUI surface;
- no stale runtime route or endpoint declaration.
Browser acceptance is part of the focused workspace gate and can be run alone:
```bash
cd /mnt/DATA/git/govoplan-core/webui
npm run test:conformance
```
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-core"
version = "0.1.18"
version = "0.1.23"
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
readme = "README.md"
requires-python = ">=3.12"
+151 -2
View File
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal, Protocol, runtime_checkable
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.external_references import (
SOURCE_AUTHORITY_MODES,
SourceAuthorityMode,
@@ -23,6 +24,8 @@ CAPABILITY_DATASOURCE_CATALOGUE = "datasources.catalogue"
CAPABILITY_DATASOURCE_LIFECYCLE = "datasources.lifecycle"
CAPABILITY_DATASOURCE_PUBLICATION = "datasources.publication"
CAPABILITY_DATASOURCE_ORIGINS = "connectors.datasourceOrigins"
CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS = "datasources.artifactBackends"
CAPABILITY_POLICY_DATASOURCE_VISIBILITY = "policy.datasourceVisibility"
DatasourceMode = Literal["live", "cached", "static"]
DatasourceKind = Literal[
@@ -37,6 +40,12 @@ DatasourceKind = Literal[
]
DatasourceShape = Literal["tabular", "document", "binary", "directory", "stream"]
DatasourceConsistency = Literal["current", "live", "frozen"]
DatasourceVisibilityAction = Literal["discover", "read"]
DatasourcePublicationStatus = Literal[
"published",
"published_with_warnings",
"review_required",
]
class DatasourceError(ValueError):
@@ -64,6 +73,7 @@ class DatasourceField:
name: str
data_type: str
nullable: bool = True
classification: str = "internal"
@dataclass(frozen=True, slots=True)
@@ -84,6 +94,8 @@ class DatasourceGovernance:
classification: str = "internal"
privacy_profile_ref: str | None = None
retention_policy_ref: str | None = None
access_policy_ref: str | None = None
visibility_policy: Mapping[str, object] = field(default_factory=dict)
hold_refs: tuple[str, ...] = ()
publication_state: str = "draft"
transfer_agreement_ref: str | None = None
@@ -154,6 +166,12 @@ class DatasourceGovernance:
retention_policy_ref=_optional_governance_text(
source.get("retention_policy_ref")
),
access_policy_ref=_optional_governance_text(
source.get("access_policy_ref")
),
visibility_policy=_governance_mapping(
source.get("visibility_policy")
),
hold_refs=_governance_texts(source.get("hold_refs")),
publication_state=str(source.get("publication_state") or "draft"),
transfer_agreement_ref=_optional_governance_text(
@@ -185,6 +203,8 @@ class DatasourceGovernance:
"classification": self.classification,
"privacy_profile_ref": self.privacy_profile_ref,
"retention_policy_ref": self.retention_policy_ref,
"access_policy_ref": self.access_policy_ref,
"visibility_policy": dict(self.visibility_policy),
"hold_refs": list(self.hold_refs),
"publication_state": self.publication_state,
"transfer_agreement_ref": self.transfer_agreement_ref,
@@ -197,6 +217,39 @@ class DatasourceGovernance:
}
@dataclass(frozen=True, slots=True)
class DatasourceVisibilityPolicyRequest:
tenant_id: str
datasource_ref: str
principal: PrincipalRef
action: DatasourceVisibilityAction
classification: str = "internal"
policy_ref: str | None = None
consistency: DatasourceConsistency = "current"
materialization_ref: str | None = None
@dataclass(frozen=True, slots=True)
class DatasourceVisibilityPolicyDecision:
allowed: bool
reason: str | None = None
policies: tuple[Mapping[str, object], ...] = ()
decision_ref: str | None = None
provenance: Mapping[str, object] = field(default_factory=dict)
@runtime_checkable
class DatasourceVisibilityPolicyProvider(Protocol):
"""Optionally tighten Datasources-owned local visibility policy."""
def decide_datasource_visibility(
self,
session: object,
*,
request: DatasourceVisibilityPolicyRequest,
) -> DatasourceVisibilityPolicyDecision: ...
@dataclass(frozen=True, slots=True)
class DatasourceDescriptor:
ref: str
@@ -309,12 +362,73 @@ class DatasourceStageInput:
governance: DatasourceGovernance | None = None
@dataclass(frozen=True, slots=True)
class DatasourceArtifactReference:
"""Immutable provider-neutral reference to a durable tabular payload.
The producer owns creation of the payload. Datasources pins its locator,
checksum and declared shape without importing the artifact-owning module;
a configured payload backend verifies integrity and provides bounded reads.
"""
backend: str
locator: str
checksum: str
row_count: int
byte_count: int
schema: tuple[DatasourceField, ...]
fingerprint: str
media_type: str = "application/x-ndjson"
checkpoint: Mapping[str, object] = field(default_factory=dict)
metadata: Mapping[str, object] = field(default_factory=dict)
validation: Mapping[str, object] = field(default_factory=dict)
@runtime_checkable
class DatasourceArtifactBackend(Protocol):
"""Storage-module boundary for immutable artifact-backed tabular data."""
backend: str
def verify(
self,
session: object,
*,
tenant_id: str,
artifact: DatasourceArtifactReference,
) -> None: ...
def read_rows(
self,
session: object,
*,
tenant_id: str,
artifact: DatasourceArtifactReference,
offset: int,
limit: int,
) -> Sequence[Mapping[str, object]]: ...
def delete(
self,
session: object,
*,
tenant_id: str,
artifact: DatasourceArtifactReference,
) -> None: ...
@runtime_checkable
class DatasourceArtifactBackendProvider(Protocol):
def artifact_backends(self) -> Sequence[DatasourceArtifactBackend]: ...
@dataclass(frozen=True, slots=True)
class DatasourcePublicationRequest:
producer_module: str
producer_run_ref: str
idempotency_key: str
rows: tuple[Mapping[str, object], ...]
rows: tuple[Mapping[str, object], ...] | None = None
artifact: DatasourceArtifactReference | None = None
target_datasource_ref: str | None = None
name: str | None = None
source_name: str | None = None
@@ -331,7 +445,7 @@ class DatasourcePublicationRequest:
@dataclass(frozen=True, slots=True)
class DatasourcePublicationResult:
ref: str
status: str
status: DatasourcePublicationStatus
datasource: DatasourceDescriptor
materialization: DatasourceMaterialization
replayed: bool = False
@@ -564,6 +678,17 @@ def datasource_catalogue(registry: object | None) -> DatasourceCatalogueProvider
return capability if isinstance(capability, DatasourceCatalogueProvider) else None
def datasource_artifact_backend_provider(
registry: object | None,
) -> DatasourceArtifactBackendProvider | None:
capability = _capability(registry, CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS)
return (
capability
if isinstance(capability, DatasourceArtifactBackendProvider)
else None
)
def datasource_lifecycle(registry: object | None) -> DatasourceLifecycleProvider | None:
capability = _capability(registry, CAPABILITY_DATASOURCE_LIFECYCLE)
return capability if isinstance(capability, DatasourceLifecycleProvider) else None
@@ -581,6 +706,13 @@ def datasource_origins(registry: object | None) -> DatasourceOriginProvider | No
return capability if isinstance(capability, DatasourceOriginProvider) else None
def datasource_visibility_policy_provider(
registry: object | None,
) -> DatasourceVisibilityPolicyProvider | None:
capability = _capability(registry, CAPABILITY_POLICY_DATASOURCE_VISIBILITY)
return capability if isinstance(capability, DatasourceVisibilityPolicyProvider) else None
def _capability(registry: object | None, name: str) -> object | None:
if (
registry is None
@@ -613,9 +745,15 @@ def _governance_mapping(value: object) -> Mapping[str, object]:
__all__ = [
"CAPABILITY_DATASOURCE_CATALOGUE",
"CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS",
"CAPABILITY_DATASOURCE_LIFECYCLE",
"CAPABILITY_DATASOURCE_ORIGINS",
"CAPABILITY_DATASOURCE_PUBLICATION",
"CAPABILITY_POLICY_DATASOURCE_VISIBILITY",
"DatasourceAccessError",
"DatasourceArtifactReference",
"DatasourceArtifactBackend",
"DatasourceArtifactBackendProvider",
"DatasourceCatalogueProvider",
"DatasourceConsistency",
"DatasourceDescriptor",
@@ -631,6 +769,10 @@ __all__ = [
"DatasourceOriginProvider",
"DatasourceOriginReadRequest",
"DatasourceOriginReadResult",
"DatasourcePublicationProvider",
"DatasourcePublicationRequest",
"DatasourcePublicationResult",
"DatasourcePublicationStatus",
"DatasourceReadRequest",
"DatasourceReadResult",
"DatasourceShape",
@@ -638,7 +780,14 @@ __all__ = [
"DatasourceStageInput",
"DatasourceUnavailableError",
"DatasourceValidationError",
"DatasourceVisibilityAction",
"DatasourceVisibilityPolicyDecision",
"DatasourceVisibilityPolicyProvider",
"DatasourceVisibilityPolicyRequest",
"datasource_catalogue",
"datasource_artifact_backend_provider",
"datasource_lifecycle",
"datasource_origins",
"datasource_publication",
"datasource_visibility_policy_provider",
]
+119
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass, field
from datetime import datetime
from typing import Protocol, runtime_checkable
from govoplan_core.core.access import ResourceAccessExplanationProvider
@@ -10,6 +11,48 @@ from govoplan_core.core.access import ResourceAccessExplanationProvider
CAPABILITY_FILES_ACCESS = "files.access"
CAPABILITY_FILES_ARTIFACT_STORE = "files.artifact_store"
CAPABILITY_FILES_POSTBOX_REFERENCES = "files.postbox_references"
CAPABILITY_FILES_TABULAR_CONTENT = "files.tabular_content"
class ManagedTabularFileError(ValueError):
"""Stable base error for exact-version managed tabular file access."""
class ManagedTabularFileNotFoundError(ManagedTabularFileError):
pass
class ManagedTabularFileAccessError(ManagedTabularFileError):
pass
class ManagedTabularFileUnavailableError(ManagedTabularFileError):
pass
class ManagedTabularFileValidationError(ManagedTabularFileError):
pass
@dataclass(frozen=True, slots=True)
class ManagedTabularFile:
"""Authorized metadata for one immutable managed file version."""
file_asset_id: str
file_version_id: str
filename: str
display_path: str
content_type: str | None
size_bytes: int
sha256: str
updated_at: datetime | None = None
current_version: bool = True
@dataclass(frozen=True, slots=True)
class ManagedTabularFileContent:
file: ManagedTabularFile
payload: bytes
@dataclass(frozen=True, slots=True)
@@ -91,6 +134,39 @@ class PostboxFileReferenceProvider(Protocol):
) -> tuple[PostboxFileReferenceRef, ...]: ...
@runtime_checkable
class ManagedTabularFileProvider(Protocol):
"""List and open authorized CSV/XLSX content without exposing Files internals."""
def list_tabular_files(
self,
session: object,
principal: object,
*,
query: str = "",
limit: int = 100,
) -> tuple[ManagedTabularFile, ...]: ...
def get_tabular_file(
self,
session: object,
principal: object,
*,
file_asset_id: str,
file_version_id: str | None = None,
) -> ManagedTabularFile | None: ...
def read_tabular_file(
self,
session: object,
principal: object,
*,
file_asset_id: str,
file_version_id: str,
max_bytes: int,
) -> ManagedTabularFileContent: ...
def postbox_file_reference_provider(
registry: object | None,
) -> PostboxFileReferenceProvider | None:
@@ -107,3 +183,46 @@ def postbox_file_reference_provider(
"PostboxFileReferenceProvider"
)
return provider
def managed_tabular_file_provider(
registry: object | None,
) -> ManagedTabularFileProvider | None:
if (
registry is None
or not hasattr(registry, "has_capability")
or not registry.has_capability(CAPABILITY_FILES_TABULAR_CONTENT)
):
return None
provider = registry.require_capability(CAPABILITY_FILES_TABULAR_CONTENT)
if not isinstance(provider, ManagedTabularFileProvider):
raise TypeError(
"files.tabular_content provider does not implement "
"ManagedTabularFileProvider"
)
return provider
__all__ = [
"CAPABILITY_FILES_ACCESS",
"CAPABILITY_FILES_ARTIFACT_STORE",
"CAPABILITY_FILES_POSTBOX_REFERENCES",
"CAPABILITY_FILES_TABULAR_CONTENT",
"FileAccessProvider",
"ManagedArtifactRef",
"ManagedArtifactStore",
"ManagedArtifactWriteRequest",
"ManagedTabularFile",
"ManagedTabularFileAccessError",
"ManagedTabularFileContent",
"ManagedTabularFileError",
"ManagedTabularFileNotFoundError",
"ManagedTabularFileProvider",
"ManagedTabularFileUnavailableError",
"ManagedTabularFileValidationError",
"PostboxFileReferenceProvider",
"PostboxFileReferenceRef",
"PostboxFileReferenceRequest",
"managed_tabular_file_provider",
"postbox_file_reference_provider",
]
+20
View File
@@ -3,11 +3,14 @@ from __future__ import annotations
import unittest
from govoplan_core.core.datasources import (
CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS,
CAPABILITY_DATASOURCE_CATALOGUE,
CAPABILITY_DATASOURCE_LIFECYCLE,
CAPABILITY_DATASOURCE_ORIGINS,
CAPABILITY_DATASOURCE_PUBLICATION,
CAPABILITY_POLICY_DATASOURCE_VISIBILITY,
DatasourceCatalogueProvider,
DatasourceArtifactBackendProvider,
DatasourceDescriptor,
DatasourceField,
DatasourceLifecycleProvider,
@@ -19,10 +22,14 @@ from govoplan_core.core.datasources import (
DatasourcePublicationResult,
DatasourceReadResult,
DatasourceStage,
DatasourceVisibilityPolicyDecision,
DatasourceVisibilityPolicyProvider,
datasource_catalogue,
datasource_artifact_backend_provider,
datasource_lifecycle,
datasource_origins,
datasource_publication,
datasource_visibility_policy_provider,
)
from govoplan_core.core.modules import ModuleContext, ModuleManifest
from govoplan_core.core.registry import PlatformRegistry
@@ -155,6 +162,13 @@ class _Provider:
truncated=False,
)
def artifact_backends(self):
return ()
def decide_datasource_visibility(self, session, *, request):
del session, request
return DatasourceVisibilityPolicyDecision(allowed=True)
class DatasourceContractTests(unittest.TestCase):
def test_capabilities_are_runtime_checkable_and_resolved_without_modules(self) -> None:
@@ -163,6 +177,8 @@ class DatasourceContractTests(unittest.TestCase):
self.assertIsInstance(provider, DatasourceLifecycleProvider)
self.assertIsInstance(provider, DatasourcePublicationProvider)
self.assertIsInstance(provider, DatasourceOriginProvider)
self.assertIsInstance(provider, DatasourceArtifactBackendProvider)
self.assertIsInstance(provider, DatasourceVisibilityPolicyProvider)
registry = PlatformRegistry()
registry.register(
ModuleManifest(
@@ -174,6 +190,8 @@ class DatasourceContractTests(unittest.TestCase):
CAPABILITY_DATASOURCE_LIFECYCLE: lambda context: provider,
CAPABILITY_DATASOURCE_PUBLICATION: lambda context: provider,
CAPABILITY_DATASOURCE_ORIGINS: lambda context: provider,
CAPABILITY_DATASOURCE_ARTIFACT_BACKENDS: lambda context: provider,
CAPABILITY_POLICY_DATASOURCE_VISIBILITY: lambda context: provider,
},
)
)
@@ -183,6 +201,8 @@ class DatasourceContractTests(unittest.TestCase):
self.assertIs(provider, datasource_lifecycle(registry))
self.assertIs(provider, datasource_publication(registry))
self.assertIs(provider, datasource_origins(registry))
self.assertIs(provider, datasource_artifact_backend_provider(registry))
self.assertIs(provider, datasource_visibility_policy_provider(registry))
self.assertIsNone(datasource_catalogue(PlatformRegistry()))
def test_descriptor_distinguishes_mode_kind_shape_and_materialization(self) -> None:
+90
View File
@@ -0,0 +1,90 @@
from __future__ import annotations
import unittest
from datetime import UTC, datetime
from govoplan_core.core.files import (
CAPABILITY_FILES_TABULAR_CONTENT,
ManagedTabularFile,
ManagedTabularFileContent,
ManagedTabularFileProvider,
managed_tabular_file_provider,
)
class _Provider:
def list_tabular_files(self, session, principal, *, query="", limit=100):
del session, principal, query, limit
return ()
def get_tabular_file(
self,
session,
principal,
*,
file_asset_id,
file_version_id=None,
):
del session, principal, file_asset_id, file_version_id
return None
def read_tabular_file(
self,
session,
principal,
*,
file_asset_id,
file_version_id,
max_bytes,
):
del session, principal, file_asset_id, file_version_id, max_bytes
file = ManagedTabularFile(
file_asset_id="asset-1",
file_version_id="version-1",
filename="source.csv",
display_path="Imports/source.csv",
content_type="text/csv",
size_bytes=8,
sha256="a" * 64,
updated_at=datetime(2026, 8, 21, tzinfo=UTC),
)
return ManagedTabularFileContent(file=file, payload=b"id\n1\n")
class _Registry:
def __init__(self, provider):
self.provider = provider
def has_capability(self, name):
return name == CAPABILITY_FILES_TABULAR_CONTENT
def require_capability(self, name):
if not self.has_capability(name):
raise KeyError(name)
return self.provider
class ManagedTabularFileContractTests(unittest.TestCase):
def test_runtime_protocol_and_registry_resolution(self) -> None:
provider = _Provider()
self.assertIsInstance(provider, ManagedTabularFileProvider)
self.assertIs(provider, managed_tabular_file_provider(_Registry(provider)))
self.assertIsNone(managed_tabular_file_provider(None))
def test_exact_version_content_retains_safe_metadata(self) -> None:
result = _Provider().read_tabular_file(
object(),
object(),
file_asset_id="asset-1",
file_version_id="version-1",
max_bytes=100,
)
self.assertEqual("version-1", result.file.file_version_id)
self.assertEqual("a" * 64, result.file.sha256)
self.assertEqual(b"id\n1\n", result.payload)
if __name__ == "__main__":
unittest.main()
+6
View File
@@ -10,6 +10,7 @@ from fastapi import APIRouter, Depends, FastAPI
from fastapi.testclient import TestClient
from govoplan_core.auth import ApiPrincipal, get_api_principal
from govoplan_core.admin.models import SystemSettings
from govoplan_core.celery_app import _run_tenant_worker_batches
from govoplan_core.core.access import PrincipalRef
from govoplan_core.core.lifecycle import require_module_active
@@ -26,6 +27,7 @@ from govoplan_core.core.module_entitlements import (
from govoplan_core.core.modules import ModuleContext, ModuleManifest
from govoplan_core.core.registry import PlatformRegistry
from govoplan_core.db.session import configure_database, get_database
from govoplan_core.db.base import Base
from govoplan_core.server.platform import create_platform_router
from govoplan_core.tenancy.scope import Tenant, create_scope_tables
@@ -257,6 +259,10 @@ class TenantModuleEntitlementRouteTests(unittest.TestCase):
root = Path(tempfile.mkdtemp(prefix="govoplan-entitlement-test-"))
configure_database(f"sqlite:///{root / 'test.db'}")
create_scope_tables(get_database().engine)
Base.metadata.create_all(
bind=get_database().engine,
tables=[SystemSettings.__table__],
)
self.manifests = (
ModuleManifest(id="access", name="Access", version="test"),
ModuleManifest(
+28
View File
@@ -26,6 +26,7 @@ import WorkspaceFrame from "../src/components/WorkspaceFrame";
import WorkspaceLayout from "../src/components/WorkspaceLayout";
import WorkspaceActionBar from "../src/components/WorkspaceActionBar";
import BreadcrumbBar from "../src/layout/BreadcrumbBar";
import HelpMenu from "../src/layout/HelpMenu";
import { useGuardedNavigate } from "../src/components/UnsavedChangesGuard";
import {
createQuickAccessLaunchContext,
@@ -149,6 +150,7 @@ export default function ConformanceApp() {
</section>
<LaunchContextScenario />
{new URLSearchParams(location.search).has("help") ? <HelpConformanceScenario /> : null}
</PageLayout>
<Dialog
@@ -170,6 +172,32 @@ export default function ConformanceApp() {
);
}
function HelpConformanceScenario() {
return (
<section className="conformance-section" aria-labelledby="help-heading">
<h2 id="help-heading">Kontextsensitive Hilfe</h2>
<p>F1 löst den Hilfekontext des fokussierten Bedienelements auf und kehrt nach dem Schließen dorthin zurück.</p>
<ActionToolbar surface="subtle">
<Button
variant="danger"
data-testid="f1-retention-action"
helpContextId="policy.retention.action.apply"
helpModuleId="policy"
>
Aufbewahrungsregeln anwenden
</Button>
<Button
data-testid="f1-fallback-action"
interfaceId="core.conformance.action.review"
>
Allgemeine Aktion prüfen
</Button>
<HelpMenu auth={null} />
</ActionToolbar>
</section>
);
}
function QuickAccessScenario() {
const location = useLocation();
const mode = new URLSearchParams(location.search).get("quick-access");
+3
View File
@@ -0,0 +1,3 @@
const installedModules: Array<never> = [];
export default installedModules;
+19
View File
@@ -3,6 +3,9 @@ import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router";
import ConformanceApp from "./ConformanceApp";
import { UnsavedChangesProvider } from "../src/components/UnsavedChangesGuard";
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
import { PlatformModulesProvider } from "../src/platform/ModuleContext";
import type { PlatformWebModule } from "../src/types";
import "../src/styles/tokens.css";
import "../src/styles/layout.css";
import "../src/styles/forms.css";
@@ -16,12 +19,28 @@ import "./conformance.css";
const theme = new URLSearchParams(window.location.search).get("theme");
if (theme === "dark" || theme === "light") document.documentElement.dataset.theme = theme;
const CONFORMANCE_MODULES: PlatformWebModule[] = [{
id: "policy",
label: "Richtlinien",
version: "1",
helpContexts: [{
id: "policy.retention.action.apply",
topic_id: "policy.admin.retention-and-disposition",
title: "Aufbewahrungsregeln sicher anwenden",
documentation_types: ["admin"]
}]
}];
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<PlatformModulesProvider modules={CONFORMANCE_MODULES}>
<PlatformLanguageProvider preferredLanguageCode="de">
<UnsavedChangesProvider>
<ConformanceApp />
</UnsavedChangesProvider>
</PlatformLanguageProvider>
</PlatformModulesProvider>
</BrowserRouter>
</React.StrictMode>
);
@@ -110,6 +110,44 @@ test("Quick Access preserves focus and adapts to a narrow viewport", async ({ pa
await expect(filesTrigger).toBeFocused();
});
test("F1 resolves focused high-risk help and restores focus accessibly", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/?theme=light&help=1");
const retentionAction = page.getByTestId("f1-retention-action");
await retentionAction.focus();
await page.keyboard.press("F1");
const dialog = page.getByRole("dialog", { name: "Kontexthilfe" });
await expect(dialog).toBeVisible();
await expect(dialog.locator("[data-help-context]"))
.toHaveAttribute("data-help-context", "policy.retention.action.apply");
await expect(dialog.getByRole("heading", { level: 3 }))
.toHaveText("Aufbewahrungsregeln anwenden");
await expect(dialog).toContainText("topic=policy.admin.retention-and-disposition");
await expect(dialog.getByRole("button", { name: "Administrator-Dokumentation öffnen" }))
.toBeVisible();
await expectNoAccessibilityViolations(page);
const bounds = await dialog.boundingBox();
expect(bounds?.x).toBeGreaterThanOrEqual(0);
expect((bounds?.x ?? 0) + (bounds?.width ?? 0)).toBeLessThanOrEqual(390);
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
await expect(retentionAction).toBeFocused();
const fallbackAction = page.getByTestId("f1-fallback-action");
await fallbackAction.focus();
await page.keyboard.press("F1");
const fallbackDialog = page.getByRole("dialog", { name: "Kontexthilfe" });
await expect(fallbackDialog.locator("[data-help-context]"))
.toHaveAttribute("data-help-context", "core.conformance.action.review");
await expect(fallbackDialog).toContainText("fallback_context=campaigns.list");
await page.keyboard.press("Escape");
await expect(fallbackAction).toBeFocused();
});
test("View focus has a deliberate permission-derived all-tools escape", async ({ page }) => {
await page.route("**/api/v1/quick-access/effective*", async (route) => {
await route.fulfill({
+7 -7
View File
@@ -1,12 +1,12 @@
{
"name": "@govoplan/core-webui",
"version": "0.1.18",
"version": "0.1.23",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@govoplan/core-webui",
"version": "0.1.18",
"version": "0.1.23",
"dependencies": {
"@govoplan/access-webui": "file:../../govoplan-access/webui",
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
@@ -82,7 +82,7 @@
},
"../../govoplan-access/webui": {
"name": "@govoplan/access-webui",
"version": "0.1.18",
"version": "0.1.19",
"devDependencies": {
"typescript": "^5.7.2"
},
@@ -288,7 +288,7 @@
},
"../../govoplan-datasources/webui": {
"name": "@govoplan/datasources-webui",
"version": "0.1.18",
"version": "0.1.20",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0",
@@ -356,7 +356,7 @@
},
"../../govoplan-files/webui": {
"name": "@govoplan/files-webui",
"version": "0.1.18",
"version": "0.1.20",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@vitejs/plugin-react": "^5.2.0",
@@ -436,7 +436,7 @@
},
"../../govoplan-idm/webui": {
"name": "@govoplan/idm-webui",
"version": "0.1.18",
"version": "0.1.19",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@vitejs/plugin-react": "^5.2.0",
@@ -547,7 +547,7 @@
},
"../../govoplan-policy/webui": {
"name": "@govoplan/policy-webui",
"version": "0.1.18",
"version": "0.1.19",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0",
+11 -11
View File
@@ -1,22 +1,22 @@
{
"name": "@govoplan/core-webui",
"version": "0.1.18",
"version": "0.1.23",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@govoplan/core-webui",
"version": "0.1.18",
"version": "0.1.23",
"dependencies": {
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.18",
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.19",
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18",
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18",
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.18",
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.18",
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.18",
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.18",
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.18",
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.20",
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.19",
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.18",
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18",
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18",
@@ -753,8 +753,8 @@
"optional": true
},
"node_modules/@govoplan/access-webui": {
"version": "0.1.18",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#44799b15e56cb1ace679689783be51aac7bee4c3",
"version": "0.1.19",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#2d1b1e356ecb8726219d4a502db78e61f435a88f",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0",
@@ -874,8 +874,8 @@
}
},
"node_modules/@govoplan/files-webui": {
"version": "0.1.18",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#3a3360650f9f12238852de5ffc1ee35c24077ddd",
"version": "0.1.20",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#378f4d6ac5525d0f07f38eefce893bfa2924f2b9",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@vitejs/plugin-react": "^5.2.0",
@@ -893,8 +893,8 @@
}
},
"node_modules/@govoplan/idm-webui": {
"version": "0.1.18",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#9bcd2de587fbce31e43221bfc4d4492597823dc5",
"version": "0.1.19",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#b0eda351957526bc7e59270c7a42016aa12bc1a3",
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@vitejs/plugin-react": "^5.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/core-webui",
"version": "0.1.18",
"version": "0.1.23",
"private": true,
"type": "module",
"main": "src/index.ts",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/core-webui",
"version": "0.1.18",
"version": "0.1.23",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -26,14 +26,14 @@
"preview": "vite preview --host 127.0.0.1 --port 4173"
},
"dependencies": {
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.18",
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.19",
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18",
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18",
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.18",
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.18",
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.18",
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.18",
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.20",
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.19",
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.18",
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.18",
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18",
+11 -9
View File
@@ -26,6 +26,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.restore_safe_defaults": "Restore safe defaults",
"i18n:govoplan-core.import_overrides": "Import JSON",
"i18n:govoplan-core.export_overrides": "Export JSON",
"i18n:govoplan-core.explanation_subject.6ff46d22": "Explanation subject",
"i18n:govoplan-core.remove_overrides": "Remove overrides",
"i18n:govoplan-core.appearance_override_not_configured_help": "No personal token overrides are stored. Preset and policy inheritance remain active.",
"i18n:govoplan-core.appearance_override_policy_disabled": "The governing appearance policy currently blocks creating or editing personal overrides. Existing overrides may still be exported or removed.",
@@ -761,6 +762,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.restore_safe_defaults": "Sichere Standardwerte wiederherstellen",
"i18n:govoplan-core.import_overrides": "JSON importieren",
"i18n:govoplan-core.export_overrides": "JSON exportieren",
"i18n:govoplan-core.explanation_subject.6ff46d22": "Person für die Zugriffserklärung",
"i18n:govoplan-core.remove_overrides": "Anpassungen entfernen",
"i18n:govoplan-core.appearance_override_not_configured_help": "Es sind keine persönlichen Token-Anpassungen gespeichert. Vorgaben und Richtlinienvererbung bleiben aktiv.",
"i18n:govoplan-core.appearance_override_policy_disabled": "Die geltende Darstellungsrichtlinie sperrt derzeit das Anlegen oder Bearbeiten persönlicher Anpassungen. Bestehende Anpassungen können weiterhin exportiert oder entfernt werden.",
@@ -936,7 +938,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.choose_date.e7877fd9": "Choose date",
"i18n:govoplan-core.clear_filter.b667d6f9": "Clear filter",
"i18n:govoplan-core.close_filter.3a281c3f": "Close filter",
"i18n:govoplan-core.close.bbfa773e": "Close",
"i18n:govoplan-core.close.bbfa773e": "Schließen",
"i18n:govoplan-core.collapse.9cf188d3": "Collapse",
"i18n:govoplan-core.color_picker.91dee962": "Color picker",
"i18n:govoplan-core.comfortable.2313707a": "Comfortable",
@@ -950,7 +952,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.connection_security_mode_expected_by_the_server_.4348e12e": "Connection security mode expected by the server: plain, TLS or STARTTLS.",
"i18n:govoplan-core.connection_successful_the_backend_health_endpoin.14eb9295": "Connection successful. The backend health endpoint responded.",
"i18n:govoplan-core.contains.33e15d00": "Contains",
"i18n:govoplan-core.context_help.7fc44ea1": "Context help",
"i18n:govoplan-core.context_help.7fc44ea1": "Kontexthilfe",
"i18n:govoplan-core.controls_contextual_ui_help_markers_once_persist.0eaef9c4": "Controls contextual UI help markers once persisted user preferences are available.",
"i18n:govoplan-core.core_only.d46ce7d9": "Core only",
"i18n:govoplan-core.core_shell_only.4949b5d5": "Core shell only",
@@ -1047,9 +1049,9 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.group.171a0606": "Group",
"i18n:govoplan-core.groups": "Gruppen",
"i18n:govoplan-core.headers.520de744": "Headers",
"i18n:govoplan-core.help_context.61aed3b9": "Help context:",
"i18n:govoplan-core.help_context.61aed3b9": "Hilfekontext:",
"i18n:govoplan-core.help_f1": "Hilfe (F1)",
"i18n:govoplan-core.help.c47ae153": "Help",
"i18n:govoplan-core.help.c47ae153": "Hilfe",
"i18n:govoplan-core.hh_mm.a4c7ee9b": "HH:MM",
"i18n:govoplan-core.hide_password.e40123b4": "Hide password",
"i18n:govoplan-core.high_level_campaign_pattern_used_by_guided_setup.c5ec6652": "High-level campaign pattern used by guided setup to prefill sensible defaults.",
@@ -1174,9 +1176,9 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.on.e0049a66": "On",
"i18n:govoplan-core.only_users_with_maintenance_access_can_use_the_s.5566264f": "Only users with maintenance access can use the system right now.",
"i18n:govoplan-core.open_address_form.f8ee560f": "Open address form",
"i18n:govoplan-core.open_admin_documentation.6adbdae3": "Open admin documentation",
"i18n:govoplan-core.open_admin_documentation.6adbdae3": "Administrator-Dokumentation öffnen",
"i18n:govoplan-core.open_maintenance_mode_settings.99b41249": "Open maintenance mode settings",
"i18n:govoplan-core.open_user_documentation.084af515": "Open user documentation",
"i18n:govoplan-core.open_user_documentation.084af515": "Benutzerdokumentation öffnen",
"i18n:govoplan-core.optional_backend_url_leave_empty_to_use_the_curr.52bd2bb4": "Optional backend URL. Leave empty to use the current origin and the configured Vite/backend proxy.",
"i18n:govoplan-core.optional_html_version_of_the_email_body_for_rich.3f01db97": "Optional HTML version of the email body for richer formatting.",
"i18n:govoplan-core.optional_local_alias_for_the_active_tenant_it_do.edbb7b14": "Optional local alias for the active tenant. It does not replace the global account name in the title bar.",
@@ -1184,7 +1186,7 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.owner.89ff3122": "Eigentümer",
"i18n:govoplan-core.owner_policy.1e8df143": "Owner policy",
"i18n:govoplan-core.p_no_html_body_content_p.c305bfc6": "<p>No HTML body content.</p>",
"i18n:govoplan-core.page.fb06270f": "Page",
"i18n:govoplan-core.page.fb06270f": "Seite",
"i18n:govoplan-core.password_for_the_selected_account_or_mail_server.d9681f16": "Password for the selected account or mail server. Stored and transmitted according to the configured backend flow.",
"i18n:govoplan-core.password_protected.09d9e174": "Password protected",
"i18n:govoplan-core.password.8be3c943": "Passwort",
@@ -1346,12 +1348,12 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.theme_and_language.60a8cf59": "Design und Sprache",
"i18n:govoplan-core.theme.a797e309": "Theme",
"i18n:govoplan-core.this_address_is_already_listed.0f6d0ff2": "This address is already listed.",
"i18n:govoplan-core.this_area_is_prepared_for_context_sensitive_help.57665877": "This area is prepared for context-sensitive help. Future help content can use this context identifier, or the equivalent help query parameter",
"i18n:govoplan-core.this_area_is_prepared_for_context_sensitive_help.57665877": "Das fokussierte Bedienelement verweist auf den folgenden dokumentierten Hilfekontext:",
"i18n:govoplan-core.this_global_name_is_shown_in_the_title_bar_and_f.1d46240e": "This global name is shown in the title bar and follows you across tenant memberships.",
"i18n:govoplan-core.this_module_is_prepared_but_not_implemented_yet.6e6449ee": "Dieses Modul ist vorbereitet, aber noch nicht umgesetzt.",
"i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b": "This page has unsaved changes. Save them before leaving, or discard the changes and continue.",
"i18n:govoplan-core.timeout_seconds.0bfc6553": "Timeout seconds",
"i18n:govoplan-core.to_open_the_right_page_or_section.5ecf4fd2": "to open the right page or section.",
"i18n:govoplan-core.to_open_the_right_page_or_section.5ecf4fd2": "Öffnen Sie die verknüpfte Dokumentation für Berechtigungen, Folgen und Hinweise zur Wiederherstellung.",
"i18n:govoplan-core.to.ae79ea1e": "To",
"i18n:govoplan-core.tu.b44892b7": "Tu",
"i18n:govoplan-core.type_a_name_and_email_address_then_press_enter.7c8d43f0": "Type a name and email address, then press Enter",
+1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
resolve: {
alias: {
"@govoplan/core-webui": resolve(import.meta.dirname, "conformance/QuickAccessCoreFacade.ts"),
"virtual:govoplan-installed-modules": resolve(import.meta.dirname, "conformance/installedModules.ts"),
react: resolve(import.meta.dirname, "node_modules/react"),
"react-router": resolve(import.meta.dirname, "node_modules/react-router"),
"lucide-react": resolve(import.meta.dirname, "node_modules/lucide-react")