From 935cee8ccd7fc26146e6fbca526d34fd45640243 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Thu, 20 Aug 2026 10:50:55 +0200 Subject: [PATCH] feat: manage custom appearance policy --- src/govoplan_admin/backend/api/v1/routes.py | 26 +++++++++++++++++-- src/govoplan_admin/backend/api/v1/schemas.py | 2 ++ src/govoplan_admin/backend/manifest.py | 2 +- webui/src/api/admin.ts | 4 ++- .../features/admin/SystemSettingsPanel.tsx | 13 ++++++++-- .../src/features/admin/systemSettingsModel.ts | 3 ++- webui/src/i18n/generatedTranslations.ts | 6 +++++ 7 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/govoplan_admin/backend/api/v1/routes.py b/src/govoplan_admin/backend/api/v1/routes.py index 8513d93..c70dcac 100644 --- a/src/govoplan_admin/backend/api/v1/routes.py +++ b/src/govoplan_admin/backend/api/v1/routes.py @@ -15,7 +15,12 @@ from govoplan_core.audit.logging import audit_from_principal, audit_operation_co from govoplan_admin.backend.db.models import GovernanceTemplate, GovernanceTemplateAssignment from govoplan_core.admin.common import AdminConflictError, AdminValidationError from govoplan_core.core.access import CAPABILITY_ACCESS_ADMINISTRATION, AccessAdministration -from govoplan_core.core.appearance import appearance_settings, update_appearance_settings +from govoplan_core.core.appearance import ( + appearance_custom_overrides_policy, + appearance_settings, + update_appearance_custom_overrides_policy, + update_appearance_settings, +) from govoplan_core.core.change_sequence import ( decode_sequence_watermark, encode_sequence_watermark, @@ -311,6 +316,7 @@ def _system_settings_item(session: Session) -> SystemSettingsItem: navigation=navigation.as_dict() if navigation is not None else None, appearance_palette=appearance_palette or "default", appearance_palette_locked=appearance_palette_locked, + appearance_custom_overrides_allowed=appearance_custom_overrides_policy(item.settings) is True, ) @@ -333,6 +339,7 @@ def _system_settings_sections(item: SystemSettingsItem) -> dict[str, Any]: "appearance": { "appearance_palette": payload["appearance_palette"], "appearance_palette_locked": payload["appearance_palette_locked"], + "appearance_custom_overrides_allowed": payload["appearance_custom_overrides_allowed"], }, "settings": payload["settings"], } @@ -1793,21 +1800,28 @@ def write_system_settings( before_privacy = privacy_policy_from_settings(item).model_dump(mode="json") before_maintenance = saved_maintenance_mode(session).as_dict() before_appearance_palette, before_appearance_locked = appearance_settings(item.settings) + before_custom_overrides_allowed = appearance_custom_overrides_policy(item.settings) is True appearance_palette_changed = ( "appearance_palette" in payload.model_fields_set and payload.appearance_palette is not None and payload.appearance_palette != (before_appearance_palette or "default") ) + custom_overrides_policy_changed = ( + "appearance_custom_overrides_allowed" in payload.model_fields_set + and payload.appearance_custom_overrides_allowed is not None + and payload.appearance_custom_overrides_allowed != before_custom_overrides_allowed + ) if ( ( "appearance_palette_locked" in payload.model_fields_set and payload.appearance_palette_locked != before_appearance_locked ) or (before_appearance_locked and appearance_palette_changed) + or custom_overrides_policy_changed ) and not has_scope(principal, "admin:policies:write"): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, - detail="Changing the system appearance lock or its locked value requires admin:policies:write.", + detail="Changing the system appearance policy requires admin:policies:write.", ) if payload.privacy_retention_policy is not None: privacy_value = payload.privacy_retention_policy.model_dump(mode="json") @@ -1852,6 +1866,14 @@ def write_system_settings( default_palette=payload.appearance_palette or current_palette or "default", palette_locked=payload.appearance_palette_locked if payload.appearance_palette_locked is not None else current_locked, ) + if ( + "appearance_custom_overrides_allowed" in payload.model_fields_set + and payload.appearance_custom_overrides_allowed is not None + ): + item.settings = update_appearance_custom_overrides_policy( + item.settings, + allowed=payload.appearance_custom_overrides_allowed, + ) if "navigation" in payload.model_fields_set: item.settings = update_navigation_preferences( item.settings, diff --git a/src/govoplan_admin/backend/api/v1/schemas.py b/src/govoplan_admin/backend/api/v1/schemas.py index 6143211..c7352eb 100644 --- a/src/govoplan_admin/backend/api/v1/schemas.py +++ b/src/govoplan_admin/backend/api/v1/schemas.py @@ -53,6 +53,7 @@ class SystemSettingsItem(BaseModel): navigation: NavigationPreferencesPayload | None = None appearance_palette: Literal["default", "civic_blue", "forest", "plum"] = "default" appearance_palette_locked: bool = False + appearance_custom_overrides_allowed: bool = False class SystemSettingsDeltaResponse(BaseModel): @@ -79,6 +80,7 @@ class SystemSettingsUpdateRequest(BaseModel): navigation: NavigationPreferencesPayload | None = None appearance_palette: Literal["default", "civic_blue", "forest", "plum"] | None = None appearance_palette_locked: bool | None = None + appearance_custom_overrides_allowed: bool | None = None change_request_id: str | None = None diff --git a/src/govoplan_admin/backend/manifest.py b/src/govoplan_admin/backend/manifest.py index 9dea12e..ccb8ab8 100644 --- a/src/govoplan_admin/backend/manifest.py +++ b/src/govoplan_admin/backend/manifest.py @@ -75,7 +75,7 @@ manifest = ModuleManifest( id="admin.workspace", title="Use the administration workspace", summary="The administration workspace shows only the sections supplied by enabled modules and allowed by the current account's permissions.", - body="System and tenant administration share one workspace. Available sections can include settings, configuration changes and packages, governance templates, groups, and module lifecycle controls. A missing section normally means that its owning module is disabled or the current account lacks the required authority. System appearance settings select a validated palette default. Changing the separate palette lock additionally requires policy-write authority; a system lock suppresses tenant and personal palette choices, while an unlocked default remains inheritable and overridable.", + body="System and tenant administration share one workspace. Available sections can include settings, configuration changes and packages, governance templates, groups, and module lifecycle controls. A missing section normally means that its owning module is disabled or the current account lacks the required authority. System appearance settings select a validated palette default. Changing the separate palette lock additionally requires policy-write authority; a system lock suppresses tenant and personal palette choices, while an unlocked default remains inheritable and overridable. The separate advanced-override policy is disabled by default and also requires policy-write authority. Enabling it permits tenants to inherit or narrow access to Core's versioned, accessibility-validated personal accent, surface, and status editor; palette locks continue to win.", documentation_types=("admin", "user"), audience=("tenant_admin", "system_admin", "operator"), metadata={ diff --git a/webui/src/api/admin.ts b/webui/src/api/admin.ts index e9d67b9..60e6d27 100644 --- a/webui/src/api/admin.ts +++ b/webui/src/api/admin.ts @@ -36,6 +36,7 @@ export type SystemSettingsItem = { navigation: NavigationPreferences | null; appearance_palette: UserUiPalette; appearance_palette_locked: boolean; + appearance_custom_overrides_allowed: boolean; }; export type SystemSettingsDeltaSections = Partial<{ @@ -46,7 +47,7 @@ export type SystemSettingsDeltaSections = Partial<{ maintenance_mode: SystemSettingsItem["maintenance_mode"]; settings: SystemSettingsItem["settings"]; navigation: SystemSettingsItem["navigation"]; - appearance: Pick; + appearance: Pick; }>; export type SystemSettingsUpdatePayload = { @@ -61,6 +62,7 @@ export type SystemSettingsUpdatePayload = { navigation?: NavigationPreferences | null; appearance_palette?: UserUiPalette; appearance_palette_locked?: boolean; + appearance_custom_overrides_allowed?: boolean; change_request_id?: string | null; }; diff --git a/webui/src/features/admin/SystemSettingsPanel.tsx b/webui/src/features/admin/SystemSettingsPanel.tsx index b993146..0e51c0c 100644 --- a/webui/src/features/admin/SystemSettingsPanel.tsx +++ b/webui/src/features/admin/SystemSettingsPanel.tsx @@ -66,7 +66,8 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint maintenance_mode: draft.maintenance_mode, navigation: draft.navigation, appearance_palette: draft.appearance_palette, - appearance_palette_locked: draft.appearance_palette_locked + appearance_palette_locked: draft.appearance_palette_locked, + appearance_custom_overrides_allowed: draft.appearance_custom_overrides_allowed }); setDraft(saved); setSavedDraft(saved); @@ -120,6 +121,13 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint help={!canWritePolicy ? "i18n:govoplan-admin.appearance_lock_policy_permission" : undefined} label="i18n:govoplan-admin.lock_system_palette" /> + setDraft({ ...draft, appearance_custom_overrides_allowed })} + disabled={!canWrite || !canWritePolicy || busy} + help={!canWritePolicy ? "i18n:govoplan-admin.appearance_policy_permission" : "i18n:govoplan-admin.custom_overrides_policy_help"} + label="i18n:govoplan-admin.allow_custom_appearance_overrides" + />

i18n:govoplan-admin.system_palette_precedence_help

@@ -156,6 +164,7 @@ function systemSettingsDraftKey(item: SystemSettingsItem): string { maintenance_mode: item.maintenance_mode, navigation: item.navigation, appearance_palette: item.appearance_palette, - appearance_palette_locked: item.appearance_palette_locked + appearance_palette_locked: item.appearance_palette_locked, + appearance_custom_overrides_allowed: item.appearance_custom_overrides_allowed }); } diff --git a/webui/src/features/admin/systemSettingsModel.ts b/webui/src/features/admin/systemSettingsModel.ts index 43cc291..800ad66 100644 --- a/webui/src/features/admin/systemSettingsModel.ts +++ b/webui/src/features/admin/systemSettingsModel.ts @@ -41,7 +41,8 @@ export const SYSTEM_SETTINGS_FALLBACK: SystemSettingsItem = { settings: {}, navigation: null, appearance_palette: "default", - appearance_palette_locked: false + appearance_palette_locked: false, + appearance_custom_overrides_allowed: false }; export function applySystemSettingsSections( diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index 4308b03..f479d75 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -8,6 +8,9 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.appearance_lock_policy_permission": "Policy-write permission is required to change this lock.", "i18n:govoplan-admin.lock_system_palette": "Lock the system palette", "i18n:govoplan-admin.system_palette_precedence_help": "A system lock suppresses tenant and personal palette choices. Without a lock, explicit child choices take precedence.", + "i18n:govoplan-admin.allow_custom_appearance_overrides": "Allow governed personal color overrides", + "i18n:govoplan-admin.custom_overrides_policy_help": "Allows tenants to inherit or restrict access to the validated advanced editor. Palette locks still suppress overrides.", + "i18n:govoplan-admin.appearance_policy_permission": "Policy-write permission is required to change this appearance policy.", "i18n:govoplan-admin.data_subject_requests.ds001": "Data-subject requests", "i18n:govoplan-admin.data_subject_requests.ds002": "Search, export, and govern erasure requests with explicit provider coverage and retained evidence.", "i18n:govoplan-admin.data_subject_requests.ds003": "Create request", @@ -541,6 +544,9 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.appearance_lock_policy_permission": "Zum Ändern dieser Sperre ist das Recht zum Bearbeiten von Richtlinien erforderlich.", "i18n:govoplan-admin.lock_system_palette": "Systempalette verbindlich festlegen", "i18n:govoplan-admin.system_palette_precedence_help": "Eine Systemsperre unterdrückt Mandanten- und Benutzerauswahlen. Ohne Sperre haben ausdrückliche untergeordnete Auswahlen Vorrang.", + "i18n:govoplan-admin.allow_custom_appearance_overrides": "Gesteuerte persönliche Farbanpassungen zulassen", + "i18n:govoplan-admin.custom_overrides_policy_help": "Mandanten können den Zugang zum geprüften erweiterten Editor erben oder einschränken. Palettensperren unterdrücken Anpassungen weiterhin.", + "i18n:govoplan-admin.appearance_policy_permission": "Zum Ändern dieser Darstellungsrichtlinie ist das Recht zum Bearbeiten von Richtlinien erforderlich.", "i18n:govoplan-admin.data_subject_requests.ds001": "Betroffenenanfragen", "i18n:govoplan-admin.data_subject_requests.ds002": "Auskunfts- und Löschanfragen mit expliziter Anbieterabdeckung und Aufbewahrungsnachweisen suchen, exportieren und steuern.", "i18n:govoplan-admin.data_subject_requests.ds003": "Anfrage anlegen",