3 Commits
Author SHA1 Message Date
zemion 34211e848b fix(ui): align contextual documentation with headings
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:46 +02:00
zemion 1236b861b8 refactor(forms-runtime): centralize owner-specific email normalization
Module Package Release / publish-packages (push) Successful in 14s
Release v0.1.22. Coordinated integrity review: GovOPlaN/govoplan-core#298.
2026-09-08 12:19:39 +02:00
zemion cd78800a65 fix(packaging): expose immutable WebUI Git package for v0.1.21
Module Package Release / publish-packages (push) Successful in 11s
2026-09-08 02:06:09 +02:00
12 changed files with 124 additions and 40 deletions
+18
View File
@@ -104,3 +104,21 @@ From the core checkout, labels can be synced once a local `GITEA_TOKEN` is avail
cd /mnt/DATA/git/govoplan-core
/mnt/DATA/git/govoplan/tools/gitea/gitea-sync-labels.py --root /mnt/DATA/git/govoplan-forms-runtime --apply
```
## Git-source WebUI package
The repository root exposes `@govoplan/forms-runtime-webui` for Git-tagged release
dependencies. It mirrors the owning `webui/package.json` version, public
TypeScript/CSS exports and peer requirements, with entry paths under
`webui/src`. Consumers provide the shared Core/React peers; the facade runs no
development or install scripts. The source archive contains `webui/src`, this
README and any repository license file. Run module development checks from `webui/`; Python
installation remains governed by `pyproject.toml`.
Das Repository stellt `@govoplan/forms-runtime-webui` am Wurzelpfad für versionierte
Git-Abhängigkeiten bereit. Version, öffentliche TypeScript-/CSS-Exporte und
Peer-Anforderungen entsprechen `webui/package.json`; die Einstiegspfade liegen
unter `webui/src`. Gemeinsame Core-/React-Peers stellt die einbindende Anwendung
bereit. Die Fassade führt keine Entwicklungs- oder Installationsskripte aus.
Entwicklungsprüfungen bleiben in `webui/`, die Python-Installation weiterhin in
`pyproject.toml` definiert.
+29 -3
View File
@@ -1,8 +1,34 @@
{
"name": "@govoplan/forms-runtime",
"version": "0.1.20",
"name": "@govoplan/forms-runtime-webui",
"version": "0.1.22",
"private": true,
"description": "Definition-aware form submissions and service launch for GovOPlaN.",
"type": "module",
"peerDependencies": {}
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20",
"react-router": ">=8.3.0 <9"
},
"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"
},
"./styles/forms-runtime.css": "./webui/src/styles/forms-runtime.css"
},
"peerDependenciesMeta": {
"@govoplan/core-webui": {
"optional": true
}
},
"files": [
"webui/src",
"README.md",
"LICENSE"
]
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-forms-runtime"
version = "0.1.20"
version = "0.1.22"
description = "Definition-aware form submissions and service launch for GovOPlaN."
readme = "README.md"
requires-python = ">=3.12"
@@ -15,6 +15,7 @@ from govoplan_core.core.dsar import (
DsarSubjectRef,
dsar_capability_name,
)
from govoplan_forms_runtime.backend.email_normalization import normalize_status_email as _normalized_email
from govoplan_forms_runtime.backend.db.models import (
FormAcknowledgement,
FormAssistedConfirmation,
@@ -1116,21 +1117,6 @@ def _session(value: object) -> Session:
return value
def _normalized_email(value: object) -> str | None:
candidate = str(value or "").strip().casefold()
if (
not candidate
or len(candidate) > 320
or candidate.count("@") != 1
or any(character.isspace() for character in candidate)
):
return None
local, domain = candidate.rsplit("@", 1)
if not local or "." not in domain or domain.startswith(".") or domain.endswith("."):
return None
return candidate
def _normalized_id(value: object) -> str | None:
if value is None:
return None
+20
View File
@@ -0,0 +1,20 @@
"""Existing Forms Runtime email-selector policy, shared by status access and DSAR.
This is selector canonicalization, not a transformation of submitted form values
or a general-purpose email-address validator.
"""
def normalize_status_email(value: object) -> str | None:
candidate = str(value or "").strip().casefold()
if (
not candidate
or len(candidate) > 320
or candidate.count("@") != 1
or any(character.isspace() for character in candidate)
):
return None
local, domain = candidate.rsplit("@", 1)
if not local or "." not in domain or domain.startswith(".") or domain.endswith("."):
return None
return candidate
@@ -65,7 +65,7 @@ from govoplan_forms_runtime.backend.status_access import FormStatusAccessService
MODULE_ID = "forms_runtime"
MODULE_NAME = "Forms Runtime"
MODULE_VERSION = "0.1.20"
MODULE_VERSION = "0.1.22"
PARTICIPATE_SCOPE = "forms_runtime:submission:participate"
ASSIST_SCOPE = "forms_runtime:submission:assist"
READ_SCOPE = "forms_runtime:workspace:read"
@@ -568,6 +568,8 @@ manifest = ModuleManifest(
title="Complete and manage Forms",
summary="Save permitted drafts, submit validated values, and retain exact definition and handoff evidence.",
body=(
"Documentation books sit beside Forms runtime or Form instance. Editing guidance is beside "
"the displayed form title while the instance is editable; field help stays with its label. "
"Every instance resolves one immutable published Form revision. Draft and final values are validated on the server; final submission also enforces attachment, signature, and policy requirements. "
"Service launches retain the exact Service and binding. Native Case and Workflow handoffs persist intent before execution, use owner capabilities with stable provider keys, and require reconciliation after unknown outcomes. History, receipts, and handoffs are replay-safe and optimistic-concurrency guarded."
" Invitation and explicitly enabled anonymous intake use hash-only expiring tokens, bounded rate limits, and isolated synthetic actors. Authenticated assisted sessions retain channel, affected and represented parties, authority, purpose, notice, responsible function, language, accessibility needs, and field provenance without bypassing the exact Form rules. Submission requires immutable read-back evidence bound to the current revision, values, attachments, and signatures; any later draft edit invalidates it. Administrators can configure applicant status per exact Form revision as authenticated-only, a short-lived link sent to a matching Form email, or a non-expiring public link. Status projections expose only a bounded lifecycle timeline and receipt reference, never Form values, actors, evidence, internal notes, or handoff details. Files-backed attachments use one-time purpose-bound grants, while authenticated acknowledgements bind an exact actor and payload digest without claiming advanced or qualified signature assurance. When Search is enabled, Forms Runtime contributes a rebuildable metadata-only projection; submitted values and evidence content are excluded, and every candidate receives a current workspace or participant access check."
@@ -625,6 +627,9 @@ manifest = ModuleManifest(
"Zulässige Entwürfe speichern, validierte Werte einreichen und exakte Definitions- und Übergabenachweise bewahren."
),
"body": (
"Dokumentationsbücher stehen neben Formularlaufzeit oder Formularinstanz. Solange die Instanz "
"bearbeitbar ist, steht die Bearbeitungshilfe neben dem angezeigten Formulartitel; Feldhilfe "
"bleibt bei der Feldbezeichnung. "
"Jede Instanz löst genau eine unveränderliche veröffentlichte Formularrevision auf. Entwurfs- und Endwerte "
"werden auf dem Server validiert; die endgültige Einreichung erzwingt zusätzlich Anforderungen an Anlagen, "
"Signaturen und Regeln. Service-Aufrufe bewahren den exakten Service und seine Bindung. Native Übergaben an "
@@ -27,6 +27,7 @@ from govoplan_forms_runtime.backend.db.models import (
FormStatusAccessToken,
)
from govoplan_forms_runtime.backend.domain import FormInstance
from govoplan_forms_runtime.backend.email_normalization import normalize_status_email as _normalize_email
STATUS_ACCESS_MODES = frozenset(
@@ -616,21 +617,6 @@ def _consume_request_limit(grant: FormStatusAccessGrant, *, now: datetime) -> bo
return True
def _normalize_email(value: object) -> str | None:
candidate = str(value or "").strip().casefold()
if (
not candidate
or len(candidate) > 320
or candidate.count("@") != 1
or any(character.isspace() for character in candidate)
):
return None
local, domain = candidate.rsplit("@", 1)
if not local or "." not in domain or domain.startswith(".") or domain.endswith("."):
return None
return candidate
def _email_digest(grant_id: str, email: str) -> str:
return hashlib.sha256(f"{grant_id}\0{email}".encode("utf-8")).hexdigest()
+37
View File
@@ -0,0 +1,37 @@
import unittest
from govoplan_forms_runtime.backend.dsar_provider import _normalized_email
from govoplan_forms_runtime.backend.email_normalization import normalize_status_email
from govoplan_forms_runtime.backend.status_access import _normalize_email
class EmailNormalizationTests(unittest.TestCase):
def test_status_access_and_dsar_share_the_same_selector_policy(self) -> None:
self.assertIs(normalize_status_email, _normalize_email)
self.assertIs(normalize_status_email, _normalized_email)
def test_existing_selector_results_remain_exact_at_policy_boundaries(self) -> None:
boundary = "a" * 308 + "@example.org"
values = (
(None, None), (False, None), (0, None), ("", None), (" ", None),
(" Subject@Example.ORG ", "subject@example.org"),
("\u00a0Straße@BÜRO.Example\u00a0", "strasse@büro.example"),
("a+b@sub.example.org", "a+b@sub.example.org"),
("a..b@example..org", "a..b@example..org"), # Existing policy, not a new RFC validator.
("a b@example.org", None), ("a@exa\tmple.org", None), ("a@exa\u00a0mple.org", None),
("a@@example.org", None), ("@example.org", None), ("a@example", None),
("a@.example.org", None), ("a@example.org.", None),
(boundary, boundary), ("a" + boundary, None),
)
for value, expected in values:
with self.subTest(value=value):
self.assertEqual(expected, normalize_status_email(value))
def test_selector_canonicalization_does_not_mutate_submitted_values(self) -> None:
values = {"email": " Subject@Example.ORG ", "other": ["001", " ", None]}
self.assertEqual("subject@example.org", normalize_status_email(values["email"]))
self.assertEqual({"email": " Subject@Example.ORG ", "other": ["001", " ", None]}, values)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/forms-runtime-webui",
"version": "0.1.20",
"version": "0.1.22",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -8,6 +8,7 @@ import { ActionBlockerHint,
DescriptionList,
Dialog,
DocumentationHelpLink,
TextWithHelp,
DismissibleAlert,
FileDropZone,
FormField as CoreFormField,
@@ -397,6 +398,8 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
<main className="forms-runtime-page">
<WorkspaceFrame className="form-instance-shell" label="Form instance" interfaceId="forms-runtime.instance" helpContextId="forms-runtime.page.instance" helpModuleId="forms-runtime">
<WorkspaceActionBar
title="Form instance"
titleHelp={<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />}
scope="workspace"
variant="detail"
refreshable
@@ -416,7 +419,6 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
File in eAkte
</Button>
: undefined}
helpAction={<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />}
/>
<PageScrollViewport className="form-instance-viewport">
{error &&
@@ -446,7 +448,7 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
<div className="form-instance-content">
<section className="form-instance-main">
<header>
<h1>{localized.title}</h1>
<TextWithHelp as="div" help={editable ? <DocumentationHelpLink reference={FORMS_RUNTIME_FIELD_DOCUMENTATION} /> : undefined}><h1>{localized.title}</h1></TextWithHelp>
{localized.description && <p>{localized.description}</p>}
</header>
{assisted &&
@@ -553,7 +555,6 @@ export default function FormInstancePage({ settings, auth }: PlatformRouteContex
}
{editable &&
<div className="form-instance-actions">
<DocumentationHelpLink reference={FORMS_RUNTIME_FIELD_DOCUMENTATION} />
{canSave &&
<label className="form-change-reason">
<span>Change reason</span>
@@ -70,6 +70,8 @@ export default function FormsRuntimePage({ settings, auth }: PlatformRouteContex
<main className="forms-runtime-page">
<WorkspaceFrame className="forms-runtime-shell" label="Forms runtime" interfaceId="forms-runtime.workspace" helpContextId="forms-runtime.page.workspace" helpModuleId="forms-runtime">
<WorkspaceActionBar
title="Forms runtime"
titleHelp={<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />}
scope="workspace"
variant="collection"
refreshable
@@ -110,7 +112,6 @@ export default function FormsRuntimePage({ settings, auth }: PlatformRouteContex
Assisted intake
</Button>
: undefined}
helpAction={<DocumentationHelpLink reference={FORMS_RUNTIME_DOCUMENTATION} />}
/>
<PageScrollViewport className="forms-runtime-list-viewport">
{error &&
+4
View File
@@ -1,6 +1,8 @@
import type { PlatformTranslations } from "@govoplan/core-webui";
const en = {
"Form instance": "Form instance",
"Forms runtime": "Forms runtime",
"i18n:govoplan-forms-runtime.forms": "Forms",
"i18n:govoplan-forms-runtime.loading_reason": "Form data is still loading.",
"i18n:govoplan-forms-runtime.saving_reason": "A Form operation is still running.",
@@ -202,6 +204,8 @@ const en = {
} as const;
const de: Record<keyof typeof en, string> = {
"Form instance": "Formularinstanz",
"Forms runtime": "Formularlaufzeit",
"i18n:govoplan-forms-runtime.forms": "Formulare",
"i18n:govoplan-forms-runtime.loading_reason": "Formulardaten werden noch geladen.",
"i18n:govoplan-forms-runtime.saving_reason": "Eine Formularaktion läuft noch.",