From fa32cca03f1a3b8e41e92f5a12b08d0335c380e9 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 3 Aug 2026 10:16:26 +0200 Subject: [PATCH] Complete guided Core configuration patterns --- docs/DOCUMENTATION_MAP.md | 1 + docs/INTERFACE_PATTERN_MIGRATION.md | 28 +++++++ docs/UI_UX_DECISION_LEDGER.md | 8 +- webui/package.json | 3 +- .../scripts/test-core-interface-patterns.mjs | 27 +++++++ .../components/CredentialEnvelopeManager.tsx | 78 ++++++++++++++++-- .../privacy/RetentionPolicyManagement.tsx | 81 ++++++++++++++++++- webui/src/features/settings/SettingsPage.tsx | 43 +++++++++- 8 files changed, 252 insertions(+), 17 deletions(-) create mode 100644 docs/INTERFACE_PATTERN_MIGRATION.md create mode 100644 webui/scripts/test-core-interface-patterns.mjs diff --git a/docs/DOCUMENTATION_MAP.md b/docs/DOCUMENTATION_MAP.md index 4cc739e..0f6a66a 100644 --- a/docs/DOCUMENTATION_MAP.md +++ b/docs/DOCUMENTATION_MAP.md @@ -39,6 +39,7 @@ operator, and roadmap pages. | Product roadmap and module routing | `GOVOPLAN_MASTER_ROADMAP.md` | Product-level sequencing, implementation gates, issue routing, and missing-module decisions. | | Institutional governance target | `govoplan/docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md` | Cross-product semantic layers, source-authority modes, candidate Mandates/Services/Parties/Decisions boundaries, and migration sequence. | | UI/UX decisions | `UI_UX_DECISION_LEDGER.md` | Binding guided-UI decisions, open decisions, impact index, and review checklist. | +| Core interface migration | `INTERFACE_PATTERN_MIGRATION.md` | Core-owned settings, credential, retention, lifecycle, and shared-component evidence for the product pattern language. | | Interface ethics and design doctrine | `INTERFACE_ETHICS_AND_DESIGN_DOCTRINE.md` | Product-level doctrine for context, decision, consequence, contestability, responsibility, and traceability. | | Public-sector integration posture | `PUBLIC_SECTOR_INTEGRATION_STRATEGY.md` | Strategy index; executable target inventory lives in `govoplan-connectors`. | | Configuration packages | `CONFIGURATION_PACKAGES.md` | Package model, provider contract, import/export flow, and tracking slices. | diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md new file mode 100644 index 0000000..b803a21 --- /dev/null +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -0,0 +1,28 @@ +# Core Interface Pattern Migration + +This document records the Core-owned part of the product-wide interface +pattern-language rollout. The normative product grammar and complete route +inventory live in the `govoplan` meta repository. Core owns reusable behavior; +domain modules own their compositions. + +## Core Surfaces + +| Surface | Pattern | Consequence and provenance contract | Evidence | +| --- | --- | --- | --- | +| User settings | Two-zone settings workspace with typed controls and unsaved-change protection | Save actions distinguish busy, unchanged, and test-in-progress states; contextual help resolves through Docs or the hosted fallback | `SettingsPage.tsx`, `test-core-interface-patterns.mjs` | +| Reusable credentials | Repeated administration with an adaptive create/edit dialog and destructive confirmation | Secret values are write-only; scope/permission blockers name the required action, responsible actor, and destination; unavailable row actions remain keyboard-explainable | `CredentialEnvelopeManager.tsx`, shared `ActionBlockerHint`, `Button`, `TableActionGroup`, and `ConfirmDialog` | +| Retention policy | Effective-policy editor with inherited source paths and typed, narrowing-only controls | Parent locks and missing write authority are explicit; the save action distinguishes locks, missing target, loading, clean draft, and active save | `RetentionPolicyManagement.tsx`, policy logic tests, `test-core-interface-patterns.mjs` | +| Module lifecycle | Guided operator projection over durable installer-queue evidence | Preflight, handoff, progress, stale evidence, recovery, and rollback consequences remain visible | Admin module lifecycle tests and the Core installer-queue contract | +| Shared configuration primitives | Cross-module component contract | Dialog focus, blocker structure, disabled-action focus, contextual help, unsaved changes, confirmation, loading, alerts, problem lists, and policy provenance are centralized | Core component tests and module-permutation build | + +## Boundary + +Files and Mail are the first two external consumers of the layered +server/credential/policy pattern. Their own repositories retain provider +discovery, transport behavior, authorization, and migration evidence. Remaining +module surfaces are tracked by bounded module-owned issues under GovOPlaN #11; +they are not reasons to add sibling-private behavior to Core. + +Raw JSON remains permitted only for diagnostics, expert inspection, +interchange, or conflict evidence. It is not a primary Core configuration +editor. diff --git a/docs/UI_UX_DECISION_LEDGER.md b/docs/UI_UX_DECISION_LEDGER.md index 60f67bc..5ca5dfd 100644 --- a/docs/UI_UX_DECISION_LEDGER.md +++ b/docs/UI_UX_DECISION_LEDGER.md @@ -307,14 +307,14 @@ converted or reviewed. | Surface | Repository | UX State | Next Action | | --- | --- | --- | --- | -| File connector settings | `govoplan-files` | First adaptive modal slice started: connections and credentials now use full-state create/edit forms with conditional fields, advanced panels, and blocker primitives. Wizard shell is retained for later assisted setup. Central policy card still needs a layered editor. | Finish provider discovery/test-in-flow, then convert policy editing. | -| Mail server settings | `govoplan-mail` / `govoplan-core` | Uses the shared server/credential model visually, but create/edit still needs the same adaptive pattern as files. | Migrate to adaptive server/credential/policy dialogs, with optional assisted wizard later. | +| File connector settings | `govoplan-files` | Migrated to the shared adaptive server/credential/policy pattern with provider discovery, typed controls, actionable blockers, consequence-aware removal, and module-owned verification evidence. | Continue only through bounded Files-owned follow-ups. | +| Mail server settings | `govoplan-mail` / `govoplan-core` | Migrated to the same layered profile/server/credential/policy pattern, including focused connection tests, unsaved-state handling, contextual help, and permission/target blockers. | Continue only through bounded Mail-owned follow-ups. | | Connector policy/effective rows | `govoplan-core`, module UIs | Effective-policy direction exists, but many editors still expose broad option sets. | Put effective value first, move overrides into modal, and explain blocked edits. | | Admin module management | `govoplan-admin` | Has preflight concepts, but operational choices are still technical and dense. | Convert install/uninstall/package changes to operator wizards. | | Configuration packages | `govoplan-admin` | Catalog/import work exists, but package editing can still drift toward technical fields. | Add guided import/review/problem-list flow. | -| Retention and privacy | `govoplan-core` | Functional editor exists; consequence language and provenance can be stronger. | Layer advanced retention options and add review for broad changes. | +| Retention and privacy | `govoplan-core` | Typed effective-policy editor exposes source paths, narrowing semantics, platform locks, permission/target blockers, and explicit clean/loading/save states. | Broader governed-change review remains module-owned where a policy change requires approval. | | API keys | `govoplan-access` / admin UI | Security-sensitive creation needs least-privilege guidance. | Add scoped creation wizard with expiry/owner review. | -| User settings | `govoplan-core` | Preferences persistence exists; interface navigation issue was fixed earlier, but the surface still needs UX review. | Keep simple sections, remove double-click traps, and add quiet explanations. | +| User settings | `govoplan-core` | Simple typed sections use unsaved-change guards, quiet result feedback, contextual help, and explicit busy/clean disabled-action reasons. | Keep bounded; new contributed sections must satisfy the checklist. | ## Impact Index diff --git a/webui/package.json b/webui/package.json index 79702a9..0124dbf 100644 --- a/webui/package.json +++ b/webui/package.json @@ -26,13 +26,14 @@ }, "scripts": { "dev": "vite --host 127.0.0.1 --port 5173", - "prebuild": "npm run audit:i18n-structural && npm run test:theme-contract", + "prebuild": "npm run audit:i18n-structural && npm run test:theme-contract && npm run test:core-interface-patterns", "build": "tsc && vite build && node scripts/check-bundle-budget.mjs", "check:bundle-budget": "node scripts/check-bundle-budget.mjs", "preview": "vite preview --host 127.0.0.1 --port 4173", "audit:i18n-structural": "node scripts/audit-i18n-structural.mjs", "test:i18n-catalog": "node --test tests/i18n-catalog-validation.test.mjs", "test:theme-contract": "node scripts/test-theme-contract.mjs", + "test:core-interface-patterns": "node scripts/test-core-interface-patterns.mjs", "test:file-drop-zone": "rm -rf .file-drop-test-build && mkdir -p .file-drop-test-build && printf '{\"type\":\"commonjs\"}\\n' > .file-drop-test-build/package.json && tsc -p tsconfig.file-drop-tests.json && node .file-drop-test-build/tests/file-drop-resolver.test.js && node scripts/test-file-drop-zone-structure.mjs", "test:data-grid-actions": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/data-grid-actions.test.js && node .component-test-build/tests/data-grid-sizing.test.js", "test:dialog-focus": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/dialog-focus.test.js && node scripts/test-dialog-focus-structure.mjs", diff --git a/webui/scripts/test-core-interface-patterns.mjs b/webui/scripts/test-core-interface-patterns.mjs new file mode 100644 index 0000000..50ffab0 --- /dev/null +++ b/webui/scripts/test-core-interface-patterns.mjs @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const webuiRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); +const read = (path) => readFileSync(resolve(webuiRoot, path), "utf8"); + +const settings = read("src/features/settings/SettingsPage.tsx"); +const retention = read("src/features/privacy/RetentionPolicyManagement.tsx"); +const credentials = read("src/components/CredentialEnvelopeManager.tsx"); + +assert.match(settings, /contextId: "core\.settings"/, "settings expose stable contextual documentation"); +assert.match(settings, /There are no unsaved profile changes\./, "profile save explains its clean state"); +assert.match(settings, /There are no unsaved interface changes\./, "preference save explains its clean state"); + +assert.match(retention, / @@ -397,16 +425,24 @@ export default function CredentialEnvelopeManager({ )} {error && {error}} {notice && !error && {notice}} + {managerBlocker && ( + + )} + @@ -414,7 +450,8 @@ export default function CredentialEnvelopeManager({ type="button" variant="primary" onClick={openCreate} - disabled={!canWrite || !scopeReady || loading} + disabled={Boolean(writeDisabledReason) || loading} + disabledReason={loading ? "Wait until credentials have loaded." : writeDisabledReason} > Add credential @@ -435,7 +472,8 @@ export default function CredentialEnvelopeManager({ label: `Edit ${credential.name}`, icon: , onClick: () => openEdit(credential), - disabled: !canWrite || saving + disabled: !canWrite || saving, + disabledReason: writeDisabledReason }, { id: "delete", @@ -443,6 +481,7 @@ export default function CredentialEnvelopeManager({ icon: , onClick: () => setDeleting(credential), disabled: !canWrite || saving, + disabledReason: writeDisabledReason, variant: "danger" } ]} @@ -461,8 +500,8 @@ export default function CredentialEnvelopeManager({ footerClassName="button-row compact-actions" footer={ <> - - + @@ -555,6 +594,35 @@ export default function CredentialEnvelopeManager({ ); } +function credentialManagerBlocker({ + canWrite, + scopeReady, + scopeType +}: { + canWrite: boolean; + scopeReady: boolean; + scopeType: CredentialEnvelopeManagerProps["scopeType"]; +}): ActionBlockerReason | null { + if (!scopeReady) { + return { + summary: "A credential owner is required.", + details: `No ${scopeType} target is currently selected.`, + requiredAction: "Select the user or group that will own the reusable credential.", + actor: "An administrator with access to this settings surface", + target: "The target selector above" + }; + } + if (!canWrite) { + return { + summary: "You can inspect reusable credentials but cannot change them.", + requiredAction: "Ask for credential-management authority if a change is required.", + actor: "A scope or access administrator", + target: "Access and role administration" + }; + } + return null; +} + function credentialDraft(credential: CredentialEnvelopeSummary): CredentialDraft { return { ...EMPTY_DRAFT, diff --git a/webui/src/features/privacy/RetentionPolicyManagement.tsx b/webui/src/features/privacy/RetentionPolicyManagement.tsx index 0277c9c..4a0abc5 100644 --- a/webui/src/features/privacy/RetentionPolicyManagement.tsx +++ b/webui/src/features/privacy/RetentionPolicyManagement.tsx @@ -21,6 +21,8 @@ import type { PolicySourcePathItem } from "../../components/PolicySourcePath"; import FormField from "../../components/FormField"; import ToggleSwitch from "../../components/ToggleSwitch"; import { useUnsavedDraftGuard } from "../../components/UnsavedChangesGuard"; +import ActionBlockerHint, { type ActionBlockerReason } from "../../components/ActionBlockerHint"; +import DocumentationHelpLink from "../../components/help/DocumentationHelpLink"; import { privacyRetentionAuditDetailOptionDisabled, privacyRetentionLocalAllowsLowerLevel, @@ -72,6 +74,11 @@ type FieldDefinition = { type RawJsonValue = "inherit" | "keep" | "disable"; type AuditDetailValue = "inherit" | PrivacyRetentionPolicy["audit_detail_level"]; +const RETENTION_DOCUMENTATION = { + contextId: "privacy.retention", + documentationType: "admin" as const +}; + const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = { store_raw_campaign_json: true, raw_campaign_json_retention_days: true, @@ -139,7 +146,10 @@ export function RetentionPolicyScopeManager({ return (
{targetSelectionRequired && - + } + >
- +
{profileResult && {profileResult}}
@@ -401,7 +415,14 @@ export default function SettingsPage({ onChange={setReduceMotion} />
- +
{uiResult && {uiResult}}
@@ -457,7 +478,14 @@ export default function SettingsPage({ onChange={() => undefined} />
- +
{uiResult && {uiResult}} @@ -488,7 +516,14 @@ export default function SettingsPage({
- +
{testResult && {testResult}}