Release Admin v0.1.19 guided configuration packages
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-22 18:04:34 +02:00
parent c22da17c64
commit ed424c729c
9 changed files with 248 additions and 20 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/admin-webui", "name": "@govoplan/admin-webui",
"version": "0.1.18", "version": "0.1.19",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "webui/src/index.ts", "main": "webui/src/index.ts",
@@ -18,7 +18,7 @@
"LICENSE" "LICENSE"
], ],
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.35",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": ">=19.2.7 <20", "react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20",
+3 -4
View File
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-admin" name = "govoplan-admin"
version = "0.1.18" version = "0.1.19"
description = "GovOPlaN generic administration module." description = "GovOPlaN generic administration module."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }] authors = [{ name = "GovOPlaN" }]
dependencies = [ dependencies = [
"govoplan-core>=0.1.18", "govoplan-core>=0.1.35",
"govoplan-access>=0.1.18", "govoplan-access>=0.1.20",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
@@ -22,4 +22,3 @@ govoplan_admin = ["py.typed"]
[project.entry-points."govoplan.modules"] [project.entry-points."govoplan.modules"]
admin = "govoplan_admin.backend.manifest:get_manifest" admin = "govoplan_admin.backend.manifest:get_manifest"
+32 -2
View File
@@ -78,7 +78,7 @@ ADMIN_ROLE_TEMPLATES = (
manifest = ModuleManifest( manifest = ModuleManifest(
id="admin", id="admin",
name="Admin", name="Admin",
version="0.1.18", version="0.1.19",
permissions=ADMIN_PERMISSIONS, permissions=ADMIN_PERMISSIONS,
role_templates=ADMIN_ROLE_TEMPLATES, role_templates=ADMIN_ROLE_TEMPLATES,
required_capabilities=( required_capabilities=(
@@ -168,11 +168,28 @@ manifest = ModuleManifest(
title="Govern configuration and module lifecycle", title="Govern configuration and module lifecycle",
summary="Admin owns reusable governance templates, configuration packages, and the operator-facing module lifecycle queue.", summary="Admin owns reusable governance templates, configuration packages, and the operator-facing module lifecycle queue.",
body=( 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",), documentation_types=("admin",),
audience=("system_admin", "operator", "module_admin"), audience=("system_admin", "operator", "module_admin"),
related_modules=("access", "audit", "ops"), 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={ metadata={
"kind": "reference", "kind": "reference",
"help_contexts": [ "help_contexts": [
@@ -181,7 +198,20 @@ manifest = ModuleManifest(
"admin.configuration-packages", "admin.configuration-packages",
"admin.governance-templates", "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_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",
"/api/v1/admin/system/governance-templates/synchronize", "/api/v1/admin/system/governance-templates/synchronize",
], ],
@@ -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: def test_module_administrator_has_only_tenant_module_permissions(self) -> None:
permissions = {item.scope for item in manifest.permissions} permissions = {item.scope for item in manifest.permissions}
template = next(item for item in manifest.role_templates if item.slug == "module_admin") template = next(item for item in manifest.role_templates if item.slug == "module_admin")
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/admin-webui", "name": "@govoplan/admin-webui",
"version": "0.1.18", "version": "0.1.19",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -17,7 +17,7 @@
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs" "test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
}, },
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.35",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": ">=19.2.7 <20", "react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20", "react-dom": ">=19.2.7 <20",
@@ -27,6 +27,11 @@ for (const source of [changes, packages, templates, modules]) {
assert.match(changes, /DataGrid/); assert.match(changes, /DataGrid/);
assert.match(packages, /ReferenceSelect/); assert.match(packages, /ReferenceSelect/);
assert.match(packages, /manualReferences/); 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(templates, /TableActionGroup/);
assert.match(modules, /StageRail/); assert.match(modules, /StageRail/);
assert.match(modules, /ActionBlockerHint/); assert.match(modules, /ActionBlockerHint/);
+22
View File
@@ -129,6 +129,26 @@ export type ConfigurationPackageFragment = {
payload: Record<string, unknown>; payload: Record<string, unknown>;
}; };
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<string, string>;
tenant_id?: string | null;
exporter_id?: string | null;
selection: {
scopes: string[];
module_ids: string[];
object_refs: string[];
};
redacted_secret_keys: string[];
};
export type ConfigurationPackageRunPayload = { export type ConfigurationPackageRunPayload = {
package: Record<string, unknown>; package: Record<string, unknown>;
tenant_id?: string | null; tenant_id?: string | null;
@@ -963,6 +983,7 @@ export function applyConfigurationPackage(settings: ApiSettings, payload: Config
diagnostics: ConfigurationPackageDiagnostic[]; diagnostics: ConfigurationPackageDiagnostic[];
created_refs: Record<string, string>; created_refs: Record<string, string>;
updated_refs: Record<string, string>; updated_refs: Record<string, string>;
rollback?: ConfigurationPackageRollback | null;
}> { }> {
return apiFetch(settings, "/api/v1/admin/configuration-packages/apply", { return apiFetch(settings, "/api/v1/admin/configuration-packages/apply", {
method: "POST", method: "POST",
@@ -979,6 +1000,7 @@ export function exportConfigurationPackage(settings: ApiSettings, payload: {
fragments: ConfigurationPackageFragment[]; fragments: ConfigurationPackageFragment[];
data_requirements: ConfigurationPackageRequiredData[]; data_requirements: ConfigurationPackageRequiredData[];
diagnostics: ConfigurationPackageDiagnostic[]; diagnostics: ConfigurationPackageDiagnostic[];
provenance?: ConfigurationPackageExportProvenance | null;
}> { }> {
return apiFetch(settings, "/api/v1/admin/configuration-packages/export", { return apiFetch(settings, "/api/v1/admin/configuration-packages/export", {
method: "POST", method: "POST",
@@ -11,6 +11,7 @@ import {
type ConfigurationChangeRequest, type ConfigurationChangeRequest,
type ConfigurationPackageDiagnostic, type ConfigurationPackageDiagnostic,
type ConfigurationPackagePlanItem, type ConfigurationPackagePlanItem,
type ConfigurationPackageRollback,
type ConfigurationPackageRequiredData } from type ConfigurationPackageRequiredData } from
"../../api/admin"; "../../api/admin";
import { import {
@@ -63,6 +64,7 @@ type ApplyResult = {
diagnostics: ConfigurationPackageDiagnostic[]; diagnostics: ConfigurationPackageDiagnostic[];
created_refs: Record<string, string>; created_refs: Record<string, string>;
updated_refs: Record<string, string>; updated_refs: Record<string, string>;
rollback?: ConfigurationPackageRollback | null;
}; };
export default function ConfigurationPackagesPanel({ settings, auth, canWrite }: {settings: ApiSettings;auth: AuthInfo;canWrite: boolean;}) { 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 [changeRequestId, setChangeRequestId] = useState("");
const [manualReferences, setManualReferences] = useState(false); const [manualReferences, setManualReferences] = useState(false);
const [dryRun, setDryRun] = useState<DryRunResult | null>(null); const [dryRun, setDryRun] = useState<DryRunResult | null>(null);
const [reviewedFingerprint, setReviewedFingerprint] = useState("");
const [applyResult, setApplyResult] = useState<ApplyResult | null>(null); const [applyResult, setApplyResult] = useState<ApplyResult | null>(null);
const [exportText, setExportText] = useState(""); const [exportText, setExportText] = useState("");
const [lastRequest, setLastRequest] = useState<ConfigurationChangeRequest | null>(null); const [lastRequest, setLastRequest] = useState<ConfigurationChangeRequest | null>(null);
@@ -86,6 +89,22 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
const parsedPackage = useMemo(() => parseObject(packageText), [packageText]); const parsedPackage = useMemo(() => parseObject(packageText), [packageText]);
const parsedSuppliedData = useMemo(() => parseObject(suppliedDataText), [suppliedDataText]); const parsedSuppliedData = useMemo(() => parseObject(suppliedDataText), [suppliedDataText]);
const canRun = Boolean(parsedPackage.value && parsedSuppliedData.value); 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( const tenantProvider = useMemo(
() => createTenantReferenceProvider(settings), () => createTenantReferenceProvider(settings),
[settings.accessToken, settings.apiBaseUrl, settings.apiKey] [settings.accessToken, settings.apiBaseUrl, settings.apiKey]
@@ -132,6 +151,7 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
try { try {
const result = await dryRunConfigurationPackage(settings, runPayload(manifest, suppliedData)); const result = await dryRunConfigurationPackage(settings, runPayload(manifest, suppliedData));
setDryRun(result); 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"); setMessage(result.diagnostics.some((item) => item.severity === "blocker") ? "i18n:govoplan-admin.preflight_finished_with_blockers.7e2ca12b" : "i18n:govoplan-admin.preflight_passed.c0c99055");
} catch (err) { } catch (err) {
setError(adminErrorMessage(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"); setBusy("export");
setError(""); setError("");
setMessage(""); setMessage("");
try { try {
const result = await exportConfigurationPackage(settings, { const result = await exportConfigurationPackage(settings, {
tenant_id: tenantId.trim() || null, tenant_id: tenantId.trim() || null,
module_ids: ["access"], module_ids: moduleIds,
scopes: tenantId.trim() ? ["tenant"] : ["system"] scopes: tenantId.trim() ? ["tenant"] : ["system"]
}); });
const exported = { const exported = {
package_id: tenantId.trim() ? "govoplan.export.access-tenant" : "govoplan.export.access-system", package_id: `${String(manifest.package_id || "govoplan.configuration")}.export`,
name: tenantId.trim() ? "i18n:govoplan-admin.exported_tenant_access_configuration.9122c85c" : "i18n:govoplan-admin.exported_system_access_configuration.2e1c6f4c", name: String(manifest.name || "i18n:govoplan-admin.export.f3e4fadb"),
version: "0.1.0", version: "0.1.0",
required_modules: [{ module_id: "access" }], required_modules: moduleIds.map((module_id) => ({ module_id })),
required_capabilities: ["configuration.provider", "access.configuration"], required_capabilities: ["configuration.provider", ...moduleIds.map((module_id) => `${module_id}.configuration`)],
fragments: result.fragments, fragments: result.fragments,
data_requirements: result.data_requirements data_requirements: result.data_requirements,
provenance: result.provenance
}; };
setExportText(JSON.stringify(exported, null, 2)); 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) { } catch (err) {
setError(adminErrorMessage(err)); setError(adminErrorMessage(err));
} finally { } 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<string, unknown>, suppliedData: Record<string, unknown>, requestId?: string | null) { function runPayload(manifest: Record<string, unknown>, suppliedData: Record<string, unknown>, requestId?: string | null) {
return { return {
package: manifest, package: manifest,
@@ -306,14 +347,19 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
<div className="button-row compact-actions"> <div className="button-row compact-actions">
<Button onClick={() => void runDryRun()} disabled={!canRun || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canRun ? ADMIN_INTERFACE_I18N.validJsonRequired : undefined}><Play size={16} /> i18n:govoplan-admin.dry_run.3d14659c</Button> <Button onClick={() => void runDryRun()} disabled={!canRun || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canRun ? ADMIN_INTERFACE_I18N.validJsonRequired : undefined}><Play size={16} /> i18n:govoplan-admin.dry_run.3d14659c</Button>
<Button onClick={() => void requestApproval()} disabled={!canWrite || !parsedPackage.value || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !parsedPackage.value ? ADMIN_INTERFACE_I18N.packageRequired : undefined}><Check size={16} /> i18n:govoplan-admin.request_approval.6245aea1</Button> <Button onClick={() => void requestApproval()} disabled={!canWrite || !parsedPackage.value || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !parsedPackage.value ? ADMIN_INTERFACE_I18N.packageRequired : undefined}><Check size={16} /> i18n:govoplan-admin.request_approval.6245aea1</Button>
<Button variant="primary" onClick={() => setConfirmApply(true)} disabled={!canWrite || !canRun || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !canRun ? ADMIN_INTERFACE_I18N.validJsonRequired : undefined}><Check size={16} /> i18n:govoplan-admin.apply.cfea419c</Button> <Button variant="primary" onClick={() => setConfirmApply(true)} disabled={!canWrite || !canRun || !dryRunReady || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !canWrite ? ADMIN_INTERFACE_I18N.writeRequired : !canRun ? ADMIN_INTERFACE_I18N.validJsonRequired : applyDisabledReason}><Check size={16} /> i18n:govoplan-admin.apply.cfea419c</Button>
<Button onClick={() => void exportAccessPackage()} disabled={Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : undefined}><Download size={16} /> i18n:govoplan-admin.export_access.9a2eb91e</Button> <Button onClick={() => void exportPackage()} disabled={!parsedPackage.value || Boolean(busy)} disabledReason={busy ? ADMIN_INTERFACE_I18N.busy : !parsedPackage.value ? ADMIN_INTERFACE_I18N.packageRequired : undefined}><Download size={16} /> i18n:govoplan-admin.export.f3e4fadb</Button>
</div> </div>
{lastRequest && <p className="muted small-note">i18n:govoplan-admin.last_request.4508ef35 {lastRequest.id} ({lastRequest.status})</p>} {lastRequest && <p className="muted small-note">i18n:govoplan-admin.last_request.4508ef35 {lastRequest.id} ({lastRequest.status})</p>}
</Card> </Card>
{dryRun && <Card title="i18n:govoplan-admin.preflight.8016a487"> {dryRun && <Card title="i18n:govoplan-admin.preflight.8016a487">
<PackageDiagnostics diagnostics={dryRun.diagnostics} /> <PackageDiagnostics diagnostics={dryRun.diagnostics} />
<RequiredDataInputs
items={dryRun.required_data}
values={parsedSuppliedData.value ?? {}}
onChange={setSuppliedDataValue}
/>
<RequiredData items={dryRun.required_data} /> <RequiredData items={dryRun.required_data} />
<PackagePlan items={dryRun.plan} /> <PackagePlan items={dryRun.plan} />
</Card>} </Card>}
@@ -322,6 +368,7 @@ export default function ConfigurationPackagesPanel({ settings, auth, canWrite }:
<PackageDiagnostics diagnostics={applyResult.diagnostics} /> <PackageDiagnostics diagnostics={applyResult.diagnostics} />
<ReferenceMap title="i18n:govoplan-admin.created.accf40c8" refs={applyResult.created_refs} /> <ReferenceMap title="i18n:govoplan-admin.created.accf40c8" refs={applyResult.created_refs} />
<ReferenceMap title="i18n:govoplan-admin.updated.f2f8570d" refs={applyResult.updated_refs} /> <ReferenceMap title="i18n:govoplan-admin.updated.f2f8570d" refs={applyResult.updated_refs} />
{applyResult.rollback && <ConfigurationRollbackState rollback={applyResult.rollback} />}
</Card>} </Card>}
{exportText && <Card title="i18n:govoplan-admin.export.f3e4fadb"> {exportText && <Card title="i18n:govoplan-admin.export.f3e4fadb">
@@ -354,6 +401,87 @@ function parseObject(text: string): {value: Record<string, unknown> | null;error
} }
} }
function packageRunFingerprint(manifest: Record<string, unknown> | null, suppliedData: Record<string, unknown> | null, tenantId: string): string {
if (!manifest || !suppliedData) return "";
return JSON.stringify({ manifest, suppliedData, tenantId: tenantId.trim() || null });
}
function packageProviderModuleIds(manifest: Record<string, unknown>): 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<string, unknown>).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<string, unknown>;
onChange: (item: ConfigurationPackageRequiredData, value: string | boolean) => void;
}) {
if (items.length === 0) return null;
return (
<div className="module-installer-request-grid">
{items.map((item) => {
const value = values[item.key];
return (
<FormField key={item.key} label={item.label} documentation={ADMIN_GOVERNANCE_DOCUMENTATION}>
{item.data_type === "boolean" ? (
<ToggleSwitch
checked={Boolean(value)}
onChange={(checked) => onChange(item, checked)}
label={item.label}
help={item.description || undefined}
/>
) : (
<>
<input
type={item.secret ? "password" : item.data_type === "integer" || item.data_type === "number" ? "number" : "text"}
value={value === undefined || value === null ? "" : String(value)}
required={item.required}
autoComplete={item.secret ? "off" : undefined}
onChange={(event) => onChange(item, event.target.value)}
/>
{item.description ? <span className="muted small-note">{item.description}</span> : null}
</>
)}
</FormField>
);
})}
</div>
);
}
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 (
<div className="stack compact-stack">
<StatusBadge status={status} label={copy.label} />
<p>{copy.summary}</p>
</div>
);
}
function PackageDiagnostics({ diagnostics }: {diagnostics: ConfigurationPackageDiagnostic[];}) { function PackageDiagnostics({ diagnostics }: {diagnostics: ConfigurationPackageDiagnostic[];}) {
if (diagnostics.length === 0) return <p className="muted">i18n:govoplan-admin.no_diagnostics.2b6e2630</p>; if (diagnostics.length === 0) return <p className="muted">i18n:govoplan-admin.no_diagnostics.2b6e2630</p>;
const columns: DataGridColumn<ConfigurationPackageDiagnostic>[] = [ const columns: DataGridColumn<ConfigurationPackageDiagnostic>[] = [
+20
View File
@@ -2,6 +2,16 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = { export const generatedTranslations: PlatformTranslations = {
"en": { "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.appearance_defaults": "Appearance defaults",
"i18n:govoplan-admin.system_palette_default": "System palette default", "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.", "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..." "i18n:govoplan-admin.working.049ac820": "Working..."
}, },
"de": { "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.appearance_defaults": "Darstellungsstandards",
"i18n:govoplan-admin.system_palette_default": "Systemstandard für die Farbpalette", "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.", "i18n:govoplan-admin.system_palette_default_help": "Geprüfter Standard für Mandanten und Benutzer ohne eigene Auswahl.",