From ed424c729cd0c1c7a1cbec387a793db0819c91fe Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Sat, 22 Aug 2026 18:04:34 +0200 Subject: [PATCH] Release Admin v0.1.19 guided configuration packages --- package.json | 4 +- pyproject.toml | 7 +- src/govoplan_admin/backend/manifest.py | 34 +++- .../test_interface_documentation_contract.py | 24 +++ webui/package.json | 4 +- .../test-interface-pattern-language.mjs | 5 + webui/src/api/admin.ts | 22 +++ .../admin/ConfigurationPackagesPanel.tsx | 148 ++++++++++++++++-- webui/src/i18n/generatedTranslations.ts | 20 +++ 9 files changed, 248 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index a48607c..0cbdb66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/admin-webui", - "version": "0.1.18", + "version": "0.1.19", "private": true, "type": "module", "main": "webui/src/index.ts", @@ -18,7 +18,7 @@ "LICENSE" ], "peerDependencies": { - "@govoplan/core-webui": "^0.1.18", + "@govoplan/core-webui": "^0.1.35", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", diff --git a/pyproject.toml b/pyproject.toml index a50d06e..681c29e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-admin" -version = "0.1.18" +version = "0.1.19" description = "GovOPlaN generic administration module." readme = "README.md" requires-python = ">=3.12" authors = [{ name = "GovOPlaN" }] dependencies = [ - "govoplan-core>=0.1.18", - "govoplan-access>=0.1.18", + "govoplan-core>=0.1.35", + "govoplan-access>=0.1.20", ] [tool.setuptools.packages.find] @@ -22,4 +22,3 @@ govoplan_admin = ["py.typed"] [project.entry-points."govoplan.modules"] admin = "govoplan_admin.backend.manifest:get_manifest" - diff --git a/src/govoplan_admin/backend/manifest.py b/src/govoplan_admin/backend/manifest.py index 4b1e358..89e62cf 100644 --- a/src/govoplan_admin/backend/manifest.py +++ b/src/govoplan_admin/backend/manifest.py @@ -78,7 +78,7 @@ ADMIN_ROLE_TEMPLATES = ( manifest = ModuleManifest( id="admin", name="Admin", - version="0.1.18", + version="0.1.19", permissions=ADMIN_PERMISSIONS, role_templates=ADMIN_ROLE_TEMPLATES, required_capabilities=( @@ -168,11 +168,28 @@ manifest = ModuleManifest( title="Govern configuration and module lifecycle", summary="Admin owns reusable governance templates, configuration packages, and the operator-facing module lifecycle queue.", body=( - "Configuration packages import or export module-owned configuration; they do not install software. Module catalog actions create reviewed install, update, activation, deactivation, or retirement requests for the trusted installer process. Governance templates materialize approved role and group structures through the owning Access contracts. Template assignment validation bulk-loads the selected tenants before mutation, and synchronization delegates one bounded versioned batch to Access instead of issuing per-tenant calls. Operators may preview or apply synchronization for up to 100 selected templates and 500 assignments. Every assignment returns an explicit outcome and provenance; missing tenants, protected memberships, role mappings, and module vetoes remain visible as blockers rather than being skipped. Retries are idempotent and preview/application runs are audited." + "Configuration packages import or export module-owned configuration; they do not install software. The operator selects a package and tenant, runs preflight, and supplies only the deployment values declared by that package through generated fields. Editing the package, tenant, or supplied data makes the previous preflight stale and disables Apply until a new blocker-free preflight succeeds. Provider applies may commit independently, so the result states whether nothing changed, a retained database snapshot is the rollback path, or a partial apply requires recovery; the screen never promises atomic cross-module undo. Export selects the providers named by the package fragments, redacts secret requirements, and records source version, module versions, exporter, scope, and timestamp as provenance. Module catalog actions create reviewed install, update, activation, deactivation, or retirement requests for the trusted installer process. Governance templates materialize approved role and group structures through the owning Access contracts. Template assignment validation bulk-loads the selected tenants before mutation, and synchronization delegates one bounded versioned batch to Access instead of issuing per-tenant calls. Operators may preview or apply synchronization for up to 100 selected templates and 500 assignments. Every assignment returns an explicit outcome and provenance; missing tenants, protected memberships, role mappings, and module vetoes remain visible as blockers rather than being skipped. Retries are idempotent and preview/application runs are audited." ), documentation_types=("admin",), audience=("system_admin", "operator", "module_admin"), related_modules=("access", "audit", "ops"), + translations={ + "de": { + "title": "Konfiguration und Modul-Lebenszyklus steuern", + "summary": "Admin stellt wiederverwendbare Governance-Vorlagen, Konfigurationspakete und die Bedienoberfläche für den Modul-Lebenszyklus bereit.", + "body": ( + "Konfigurationspakete importieren oder exportieren modul-eigene Konfiguration; sie installieren keine Software. " + "Die ausführende Person wählt Paket und Mandant, startet die Vorprüfung und erfasst ausschließlich die vom Paket deklarierten Einsatzwerte in erzeugten Feldern. " + "Eine Änderung an Paket, Mandant oder Eingabedaten macht die vorherige Vorprüfung ungültig und sperrt Anwenden, bis eine neue Vorprüfung ohne Blocker erfolgreich ist. " + "Provider können ihre Änderungen unabhängig festschreiben. Das Ergebnis weist deshalb ausdrücklich aus, ob nichts geändert wurde, ob der aufbewahrte Datenbank-Snapshot der Rücksetzweg ist oder ob eine Teilanwendung Wiederherstellung verlangt; eine atomare modulübergreifende Rücknahme wird nicht zugesagt. " + "Beim Export werden die in den Fragmenten genannten Provider ausgewählt, geheime Anforderungen geschwärzt und Quellversion, Modulversionen, exportierende Identität, Umfang und Zeitpunkt als Herkunftsnachweis festgehalten. " + "Aktionen im Modulkatalog erzeugen geprüfte Installations-, Aktualisierungs-, Aktivierungs-, Deaktivierungs- oder Stilllegungsaufträge für den vertrauenswürdigen Installer-Prozess. " + "Governance-Vorlagen materialisieren freigegebene Rollen- und Gruppenstrukturen über die zuständigen Access-Verträge. " + "Die Validierung lädt ausgewählte Mandanten gesammelt; die Synchronisierung übergibt einen begrenzten, versionierten Stapel an Access. Bis zu 100 Vorlagen und 500 Zuordnungen werden je Lauf verarbeitet. " + "Jede Zuordnung liefert Ergebnis und Herkunft; fehlende Mandanten, geschützte Mitgliedschaften, Rollenabbildungen und Modul-Vetos bleiben als Blocker sichtbar. Wiederholungen sind idempotent, Vorschau und Anwendung werden protokolliert." + ), + } + }, metadata={ "kind": "reference", "help_contexts": [ @@ -181,7 +198,20 @@ manifest = ModuleManifest( "admin.configuration-packages", "admin.governance-templates", ], + "limitations": [ + "Configuration packages do not install modules or grant permissions.", + "Generic rollback is snapshot recovery; providers may expose narrower compensation separately.", + "Secret values are never included in export provenance or portable fragments.", + ], + "operational_consequences": [ + "Changing package inputs invalidates the previous preflight and requires another review.", + "A partial apply stops before later fragments and must be recovered before retrying.", + "Retain the pre-apply database snapshot until post-apply verification is complete.", + ], "api_paths": [ + "/api/v1/admin/configuration-packages/dry-run", + "/api/v1/admin/configuration-packages/apply", + "/api/v1/admin/configuration-packages/export", "/api/v1/admin/system/governance-templates", "/api/v1/admin/system/governance-templates/synchronize", ], diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py index 0db2aaa..8f5a40e 100644 --- a/tests/test_interface_documentation_contract.py +++ b/tests/test_interface_documentation_contract.py @@ -60,6 +60,30 @@ class InterfaceDocumentationContractTests(unittest.TestCase): }, ) + def test_configuration_package_help_explains_safety_boundaries(self) -> None: + topic = next( + item + for item in manifest.documentation + if item.id == "admin.governance-and-module-lifecycle" + ) + + self.assertIn("previous preflight stale", topic.body) + self.assertIn("partial apply requires recovery", topic.body) + self.assertIn("provenance", topic.body) + german = (topic.translations or {}).get("de", {}) + self.assertIn("vorherige Vorprüfung ungültig", german.get("body", "")) + self.assertIn("Teilanwendung Wiederherstellung", german.get("body", "")) + + metadata = topic.metadata or {} + self.assertIn( + "Generic rollback is snapshot recovery; providers may expose narrower compensation separately.", + metadata.get("limitations", ()), + ) + self.assertIn( + "Changing package inputs invalidates the previous preflight and requires another review.", + metadata.get("operational_consequences", ()), + ) + def test_module_administrator_has_only_tenant_module_permissions(self) -> None: permissions = {item.scope for item in manifest.permissions} template = next(item for item in manifest.role_templates if item.slug == "module_admin") diff --git a/webui/package.json b/webui/package.json index 29e7fa5..d5372c8 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/admin-webui", - "version": "0.1.18", + "version": "0.1.19", "private": true, "type": "module", "main": "src/index.ts", @@ -17,7 +17,7 @@ "test:interface-patterns": "node scripts/test-interface-pattern-language.mjs" }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.18", + "@govoplan/core-webui": "^0.1.35", "lucide-react": "^1.23.0", "react": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20", diff --git a/webui/scripts/test-interface-pattern-language.mjs b/webui/scripts/test-interface-pattern-language.mjs index b7f0fa9..0ac1d06 100644 --- a/webui/scripts/test-interface-pattern-language.mjs +++ b/webui/scripts/test-interface-pattern-language.mjs @@ -27,6 +27,11 @@ for (const source of [changes, packages, templates, modules]) { assert.match(changes, /DataGrid/); assert.match(packages, /ReferenceSelect/); assert.match(packages, /manualReferences/); +assert.match(packages, /reviewedFingerprint === currentFingerprint/); +assert.match(packages, /RequiredDataInputs/); +assert.match(packages, /type=\{item\.secret \? "password"/); +assert.match(packages, /ConfigurationRollbackState/); +assert.match(packages, /result\.provenance/); assert.match(templates, /TableActionGroup/); assert.match(modules, /StageRail/); assert.match(modules, /ActionBlockerHint/); diff --git a/webui/src/api/admin.ts b/webui/src/api/admin.ts index f021d36..91c14cf 100644 --- a/webui/src/api/admin.ts +++ b/webui/src/api/admin.ts @@ -129,6 +129,26 @@ export type ConfigurationPackageFragment = { payload: Record; }; +export type ConfigurationPackageRollback = { + status: "blocked_before_apply" | "not_required" | "database_restore_required" | "partial_apply_requires_recovery"; + summary: string; + recovery_action?: string | null; +}; + +export type ConfigurationPackageExportProvenance = { + exported_at: string; + source_core_version: string; + module_versions: Record; + tenant_id?: string | null; + exporter_id?: string | null; + selection: { + scopes: string[]; + module_ids: string[]; + object_refs: string[]; + }; + redacted_secret_keys: string[]; +}; + export type ConfigurationPackageRunPayload = { package: Record; tenant_id?: string | null; @@ -963,6 +983,7 @@ export function applyConfigurationPackage(settings: ApiSettings, payload: Config diagnostics: ConfigurationPackageDiagnostic[]; created_refs: Record; updated_refs: Record; + rollback?: ConfigurationPackageRollback | null; }> { return apiFetch(settings, "/api/v1/admin/configuration-packages/apply", { method: "POST", @@ -979,6 +1000,7 @@ export function exportConfigurationPackage(settings: ApiSettings, payload: { fragments: ConfigurationPackageFragment[]; data_requirements: ConfigurationPackageRequiredData[]; diagnostics: ConfigurationPackageDiagnostic[]; + provenance?: ConfigurationPackageExportProvenance | null; }> { return apiFetch(settings, "/api/v1/admin/configuration-packages/export", { method: "POST", diff --git a/webui/src/features/admin/ConfigurationPackagesPanel.tsx b/webui/src/features/admin/ConfigurationPackagesPanel.tsx index f043ae5..f775110 100644 --- a/webui/src/features/admin/ConfigurationPackagesPanel.tsx +++ b/webui/src/features/admin/ConfigurationPackagesPanel.tsx @@ -11,6 +11,7 @@ import { type ConfigurationChangeRequest, type ConfigurationPackageDiagnostic, type ConfigurationPackagePlanItem, + type ConfigurationPackageRollback, type ConfigurationPackageRequiredData } from "../../api/admin"; import { @@ -63,6 +64,7 @@ type ApplyResult = { diagnostics: ConfigurationPackageDiagnostic[]; created_refs: Record; updated_refs: Record; + rollback?: ConfigurationPackageRollback | null; }; export default function ConfigurationPackagesPanel({ settings, auth, canWrite }: {settings: ApiSettings;auth: AuthInfo;canWrite: boolean;}) { @@ -74,6 +76,7 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }: const [changeRequestId, setChangeRequestId] = useState(""); const [manualReferences, setManualReferences] = useState(false); const [dryRun, setDryRun] = useState(null); + const [reviewedFingerprint, setReviewedFingerprint] = useState(""); const [applyResult, setApplyResult] = useState(null); const [exportText, setExportText] = useState(""); const [lastRequest, setLastRequest] = useState(null); @@ -86,6 +89,22 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }: const parsedPackage = useMemo(() => parseObject(packageText), [packageText]); const parsedSuppliedData = useMemo(() => parseObject(suppliedDataText), [suppliedDataText]); const canRun = Boolean(parsedPackage.value && parsedSuppliedData.value); + const currentFingerprint = useMemo( + () => packageRunFingerprint(parsedPackage.value, parsedSuppliedData.value, tenantId), + [parsedPackage.value, parsedSuppliedData.value, tenantId] + ); + const dryRunReady = Boolean( + dryRun && + reviewedFingerprint === currentFingerprint && + !dryRun.diagnostics.some((item) => item.severity === "blocker") + ); + const applyDisabledReason = !dryRun + ? "i18n:govoplan-admin.run_preflight_before_apply.c214a009" + : reviewedFingerprint !== currentFingerprint + ? "i18n:govoplan-admin.preflight_is_stale.c214a010" + : dryRun.diagnostics.some((item) => item.severity === "blocker") + ? "i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b" + : undefined; const tenantProvider = useMemo( () => createTenantReferenceProvider(settings), [settings.accessToken, settings.apiBaseUrl, settings.apiKey] @@ -132,6 +151,7 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }: try { const result = await dryRunConfigurationPackage(settings, runPayload(manifest, suppliedData)); setDryRun(result); + setReviewedFingerprint(packageRunFingerprint(manifest, suppliedData, tenantId)); setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b" : "i18n:govoplan-admin.preflight_passed.c0c99055"); } catch (err) { setError(adminErrorMessage(err)); @@ -182,27 +202,31 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }: } } - async function exportAccessPackage() { + async function exportPackage() { + const manifest = requireParsedPackage(); + if (!manifest) return; + const moduleIds = packageProviderModuleIds(manifest); setBusy("export"); setError(""); setMessage(""); try { const result = await exportConfigurationPackage(settings, { tenant_id: tenantId.trim() || null, - module_ids: ["access"], + module_ids: moduleIds, scopes: tenantId.trim() ? ["tenant"] : ["system"] }); const exported = { - package_id: tenantId.trim() ? "govoplan.export.access-tenant" : "govoplan.export.access-system", - name: tenantId.trim() ? "i18n:govoplan-admin.exported_tenant_access_configuration.9122c85c" : "i18n:govoplan-admin.exported_system_access_configuration.2e1c6f4c", + package_id: `${String(manifest.package_id || "govoplan.configuration")}.export`, + name: String(manifest.name || "i18n:govoplan-admin.export.f3e4fadb"), version: "0.1.0", - required_modules: [{ module_id: "access" }], - required_capabilities: ["configuration.provider", "access.configuration"], + required_modules: moduleIds.map((module_id) => ({ module_id })), + required_capabilities: ["configuration.provider", ...moduleIds.map((module_id) => `${module_id}.configuration`)], fragments: result.fragments, - data_requirements: result.data_requirements + data_requirements: result.data_requirements, + provenance: result.provenance }; setExportText(JSON.stringify(exported, null, 2)); - setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.export_finished_with_blockers.e2f70611" : "i18n:govoplan-admin.access_configuration_exported.098f200d"); + setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.export_finished_with_blockers.e2f70611" : "i18n:govoplan-admin.export.f3e4fadb"); } catch (err) { setError(adminErrorMessage(err)); } finally { @@ -210,6 +234,23 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }: } } + function setSuppliedDataValue(item: ConfigurationPackageRequiredData, rawValue: string | boolean) { + const current = parsedSuppliedData.value ?? {}; + const next = { ...current }; + if (rawValue === "") { + delete next[item.key]; + } else if (item.data_type === "boolean") { + next[item.key] = Boolean(rawValue); + } else if (item.data_type === "integer") { + next[item.key] = Number.parseInt(String(rawValue), 10); + } else if (item.data_type === "number") { + next[item.key] = Number.parseFloat(String(rawValue)); + } else { + next[item.key] = rawValue; + } + setSuppliedDataText(JSON.stringify(next, null, 2)); + } + function runPayload(manifest: Record, suppliedData: Record, requestId?: string | null) { return { package: manifest, @@ -306,14 +347,19 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
- - + +
{lastRequest &&

i18n:govoplan-admin.last_request.4508ef35 {lastRequest.id} ({lastRequest.status})

} {dryRun && + } @@ -322,6 +368,7 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }: + {applyResult.rollback && } } {exportText && @@ -354,6 +401,87 @@ function parseObject(text: string): {value: Record | null;error } } +function packageRunFingerprint(manifest: Record | null, suppliedData: Record | null, tenantId: string): string { + if (!manifest || !suppliedData) return ""; + return JSON.stringify({ manifest, suppliedData, tenantId: tenantId.trim() || null }); +} + +function packageProviderModuleIds(manifest: Record): string[] { + const fragments = Array.isArray(manifest.fragments) ? manifest.fragments : []; + const moduleIds = fragments.flatMap((item) => { + if (!item || typeof item !== "object" || Array.isArray(item)) return []; + const moduleId = (item as Record).module_id; + return typeof moduleId === "string" && moduleId.trim() ? [moduleId.trim()] : []; + }); + return Array.from(new Set(moduleIds.length > 0 ? moduleIds : ["access"])).sort(); +} + +function RequiredDataInputs({ items, values, onChange }: { + items: ConfigurationPackageRequiredData[]; + values: Record; + onChange: (item: ConfigurationPackageRequiredData, value: string | boolean) => void; +}) { + if (items.length === 0) return null; + return ( +
+ {items.map((item) => { + const value = values[item.key]; + return ( + + {item.data_type === "boolean" ? ( + onChange(item, checked)} + label={item.label} + help={item.description || undefined} + /> + ) : ( + <> + onChange(item, event.target.value)} + /> + {item.description ? {item.description} : null} + + )} + + ); + })} +
+ ); +} + +function ConfigurationRollbackState({ rollback }: { rollback: ConfigurationPackageRollback }) { + const status = rollback.status === "not_required" ? "success" : rollback.status === "blocked_before_apply" ? "inactive" : "warning"; + const copy = { + blocked_before_apply: { + label: "i18n:govoplan-admin.package_no_changes_applied.c214a001", + summary: "i18n:govoplan-admin.package_blocked_before_apply.c214a002" + }, + not_required: { + label: "i18n:govoplan-admin.package_no_rollback_needed.c214a003", + summary: "i18n:govoplan-admin.package_all_fragments_noop.c214a004" + }, + database_restore_required: { + label: "i18n:govoplan-admin.package_keep_database_snapshot.c214a005", + summary: "i18n:govoplan-admin.package_snapshot_is_rollback.c214a006" + }, + partial_apply_requires_recovery: { + label: "i18n:govoplan-admin.package_partial_apply.c214a007", + summary: "i18n:govoplan-admin.package_recover_before_retry.c214a008" + } + }[rollback.status]; + return ( +
+ +

{copy.summary}

+
+ ); +} + function PackageDiagnostics({ diagnostics }: {diagnostics: ConfigurationPackageDiagnostic[];}) { if (diagnostics.length === 0) return

i18n:govoplan-admin.no_diagnostics.2b6e2630

; const columns: DataGridColumn[] = [ diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index 0652b66..d53495a 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -2,6 +2,16 @@ import type { PlatformTranslations } from "@govoplan/core-webui"; export const generatedTranslations: PlatformTranslations = { "en": { + "i18n:govoplan-admin.package_no_changes_applied.c214a001": "No changes applied", + "i18n:govoplan-admin.package_blocked_before_apply.c214a002": "Preflight or the first provider blocked before configuration changed.", + "i18n:govoplan-admin.package_no_rollback_needed.c214a003": "No rollback needed", + "i18n:govoplan-admin.package_all_fragments_noop.c214a004": "All package fragments were already current and produced no changes.", + "i18n:govoplan-admin.package_keep_database_snapshot.c214a005": "Keep the database snapshot", + "i18n:govoplan-admin.package_snapshot_is_rollback.c214a006": "Provider-owned configuration changed. Retain the reviewed pre-apply database snapshot until verification is complete.", + "i18n:govoplan-admin.package_partial_apply.c214a007": "Partial apply—recovery required", + "i18n:govoplan-admin.package_recover_before_retry.c214a008": "A provider blocked after earlier changes committed. Restore the snapshot or complete an explicitly supported compensation before retrying.", + "i18n:govoplan-admin.run_preflight_before_apply.c214a009": "Run preflight before applying this package.", + "i18n:govoplan-admin.preflight_is_stale.c214a010": "Package, deployment data, or tenant changed. Run preflight again before applying.", "i18n:govoplan-admin.appearance_defaults": "Appearance defaults", "i18n:govoplan-admin.system_palette_default": "System palette default", "i18n:govoplan-admin.system_palette_default_help": "Validated default inherited by tenants and users without an explicit child choice.", @@ -539,6 +549,16 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.working.049ac820": "Working..." }, "de": { + "i18n:govoplan-admin.package_no_changes_applied.c214a001": "Keine Änderungen angewendet", + "i18n:govoplan-admin.package_blocked_before_apply.c214a002": "Die Vorprüfung oder der erste Provider hat blockiert, bevor Konfiguration geändert wurde.", + "i18n:govoplan-admin.package_no_rollback_needed.c214a003": "Keine Rücksetzung erforderlich", + "i18n:govoplan-admin.package_all_fragments_noop.c214a004": "Alle Paketfragmente waren bereits aktuell und haben nichts geändert.", + "i18n:govoplan-admin.package_keep_database_snapshot.c214a005": "Datenbank-Snapshot aufbewahren", + "i18n:govoplan-admin.package_snapshot_is_rollback.c214a006": "Modul-eigene Konfiguration wurde geändert. Der geprüfte Datenbank-Snapshot vor der Anwendung muss bis zum Abschluss der Verifikation erhalten bleiben.", + "i18n:govoplan-admin.package_partial_apply.c214a007": "Teilanwendung – Wiederherstellung erforderlich", + "i18n:govoplan-admin.package_recover_before_retry.c214a008": "Ein Provider hat nach bereits festgeschriebenen Änderungen blockiert. Vor einem neuen Versuch muss der Snapshot wiederhergestellt oder eine ausdrücklich unterstützte Kompensation abgeschlossen werden.", + "i18n:govoplan-admin.run_preflight_before_apply.c214a009": "Vor dem Anwenden dieses Pakets muss die Vorprüfung ausgeführt werden.", + "i18n:govoplan-admin.preflight_is_stale.c214a010": "Paket, Einsatzdaten oder Mandant wurden geändert. Vor dem Anwenden muss die Vorprüfung erneut ausgeführt werden.", "i18n:govoplan-admin.appearance_defaults": "Darstellungsstandards", "i18n:govoplan-admin.system_palette_default": "Systemstandard für die Farbpalette", "i18n:govoplan-admin.system_palette_default_help": "Geprüfter Standard für Mandanten und Benutzer ohne eigene Auswahl.",