feat: govern tenant appearance overrides

This commit is contained in:
2026-08-20 10:50:55 +02:00
parent c5119ab868
commit c558621550
6 changed files with 103 additions and 9 deletions
+40 -4
View File
@@ -18,7 +18,14 @@ from govoplan_core.core.access import (
TenantContextSwitcher, TenantContextSwitcher,
) )
from govoplan_core.core.change_sequence import ChangeSequenceEntry, decode_sequence_watermark, encode_sequence_watermark, record_change, sequence_watermark_is_expired from govoplan_core.core.change_sequence import ChangeSequenceEntry, decode_sequence_watermark, encode_sequence_watermark, record_change, sequence_watermark_is_expired
from govoplan_core.core.appearance import APPEARANCE_SETTINGS_KEY, appearance_settings, resolve_effective_appearance, update_appearance_settings from govoplan_core.core.appearance import (
APPEARANCE_SETTINGS_KEY,
appearance_custom_overrides_policy,
appearance_settings,
resolve_effective_appearance,
update_appearance_custom_overrides_policy,
update_appearance_settings,
)
from govoplan_core.core.module_entitlements import MODULE_ENTITLEMENTS_KEY from govoplan_core.core.module_entitlements import MODULE_ENTITLEMENTS_KEY
from govoplan_core.core.navigation import ( from govoplan_core.core.navigation import (
navigation_preferences_from_settings, navigation_preferences_from_settings,
@@ -259,6 +266,8 @@ def _tenant_settings_item(session: Session, tenant: Tenant) -> TenantSettingsIte
navigation = navigation_preferences_from_settings(tenant.settings) navigation = navigation_preferences_from_settings(tenant.settings)
system_palette, system_locked = appearance_settings(system_settings.settings) system_palette, system_locked = appearance_settings(system_settings.settings)
tenant_palette, tenant_locked = appearance_settings(tenant.settings) tenant_palette, tenant_locked = appearance_settings(tenant.settings)
system_custom_overrides_allowed = appearance_custom_overrides_policy(system_settings.settings) is True
tenant_custom_overrides_allowed = appearance_custom_overrides_policy(tenant.settings)
effective_appearance = resolve_effective_appearance( effective_appearance = resolve_effective_appearance(
system_settings=system_settings.settings, system_settings=system_settings.settings,
tenant_settings=tenant.settings, tenant_settings=tenant.settings,
@@ -279,6 +288,9 @@ def _tenant_settings_item(session: Session, tenant: Tenant) -> TenantSettingsIte
system_appearance_palette_locked=system_locked, system_appearance_palette_locked=system_locked,
effective_appearance_palette=effective_appearance.palette, effective_appearance_palette=effective_appearance.palette,
effective_appearance_source=effective_appearance.source, effective_appearance_source=effective_appearance.source,
appearance_custom_overrides_allowed=tenant_custom_overrides_allowed,
system_appearance_custom_overrides_allowed=system_custom_overrides_allowed,
effective_appearance_custom_overrides_allowed=effective_appearance.custom_overrides_allowed,
settings=tenant.settings or {}, settings=tenant.settings or {},
) )
@@ -301,6 +313,9 @@ def _tenant_settings_sections(item: TenantSettingsItem) -> dict[str, Any]:
"system_appearance_palette_locked": payload["system_appearance_palette_locked"], "system_appearance_palette_locked": payload["system_appearance_palette_locked"],
"effective_appearance_palette": payload["effective_appearance_palette"], "effective_appearance_palette": payload["effective_appearance_palette"],
"effective_appearance_source": payload["effective_appearance_source"], "effective_appearance_source": payload["effective_appearance_source"],
"appearance_custom_overrides_allowed": payload["appearance_custom_overrides_allowed"],
"system_appearance_custom_overrides_allowed": payload["system_appearance_custom_overrides_allowed"],
"effective_appearance_custom_overrides_allowed": payload["effective_appearance_custom_overrides_allowed"],
}, },
"settings": payload["settings"], "settings": payload["settings"],
} }
@@ -997,6 +1012,8 @@ def update_tenant_settings(
system_settings = get_system_settings(session) system_settings = get_system_settings(session)
tenant_palette, tenant_locked = appearance_settings(tenant.settings) tenant_palette, tenant_locked = appearance_settings(tenant.settings)
system_palette, system_locked = appearance_settings(system_settings.settings) system_palette, system_locked = appearance_settings(system_settings.settings)
system_custom_overrides_allowed = appearance_custom_overrides_policy(system_settings.settings) is True
tenant_custom_overrides_allowed = appearance_custom_overrides_policy(tenant.settings)
appearance_palette_changed = ( appearance_palette_changed = (
"appearance_palette" in payload.model_fields_set "appearance_palette" in payload.model_fields_set
and payload.appearance_palette != tenant_palette and payload.appearance_palette != tenant_palette
@@ -1005,17 +1022,26 @@ def update_tenant_settings(
"appearance_palette_locked" in payload.model_fields_set "appearance_palette_locked" in payload.model_fields_set
and payload.appearance_palette_locked != tenant_locked and payload.appearance_palette_locked != tenant_locked
) )
custom_overrides_policy_changed = (
"appearance_custom_overrides_allowed" in payload.model_fields_set
and payload.appearance_custom_overrides_allowed != tenant_custom_overrides_allowed
)
if payload.appearance_custom_overrides_allowed is True and not system_custom_overrides_allowed:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail="The system appearance policy does not allow personal custom overrides.",
)
if system_locked and (appearance_palette_changed or appearance_lock_changed): if system_locked and (appearance_palette_changed or appearance_lock_changed):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"The system appearance policy locks palette {system_palette or 'default'}.", detail=f"The system appearance policy locks palette {system_palette or 'default'}.",
) )
if ( if (
appearance_lock_changed or (tenant_locked and appearance_palette_changed) appearance_lock_changed or (tenant_locked and appearance_palette_changed) or custom_overrides_policy_changed
) and not has_scope(principal, "admin:policies:write"): ) and not has_scope(principal, "admin:policies:write"):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail="Changing the tenant appearance lock or its locked value requires admin:policies:write.", detail="Changing the tenant appearance policy requires admin:policies:write.",
) )
system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale) system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale)
current_i18n = i18n_settings(tenant.settings) current_i18n = i18n_settings(tenant.settings)
@@ -1035,6 +1061,11 @@ def update_tenant_settings(
default_palette=payload.appearance_palette if "appearance_palette" in payload.model_fields_set else current_palette, default_palette=payload.appearance_palette if "appearance_palette" in payload.model_fields_set else current_palette,
palette_locked=payload.appearance_palette_locked if payload.appearance_palette_locked is not None else current_locked, 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:
tenant.settings = update_appearance_custom_overrides_policy(
tenant.settings,
allowed=payload.appearance_custom_overrides_allowed,
)
if "navigation" in payload.model_fields_set: if "navigation" in payload.model_fields_set:
tenant.settings = update_navigation_preferences( tenant.settings = update_navigation_preferences(
tenant.settings, tenant.settings,
@@ -1052,9 +1083,14 @@ def update_tenant_settings(
"default_locale": tenant.default_locale, "default_locale": tenant.default_locale,
"enabled_language_codes": enabled, "enabled_language_codes": enabled,
"navigation_updated": "navigation" in payload.model_fields_set, "navigation_updated": "navigation" in payload.model_fields_set,
"appearance_updated": bool({"appearance_palette", "appearance_palette_locked"}.intersection(payload.model_fields_set)), "appearance_updated": bool(
{"appearance_palette", "appearance_palette_locked", "appearance_custom_overrides_allowed"}.intersection(
payload.model_fields_set
)
),
"appearance_palette": appearance_settings(tenant.settings)[0], "appearance_palette": appearance_settings(tenant.settings)[0],
"appearance_palette_locked": appearance_settings(tenant.settings)[1], "appearance_palette_locked": appearance_settings(tenant.settings)[1],
"appearance_custom_overrides_allowed": appearance_custom_overrides_policy(tenant.settings),
}, },
) )
after_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant)) after_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
@@ -144,6 +144,9 @@ class TenantSettingsItem(BaseModel):
system_appearance_palette_locked: bool = False system_appearance_palette_locked: bool = False
effective_appearance_palette: Literal["default", "civic_blue", "forest", "plum"] = "default" effective_appearance_palette: Literal["default", "civic_blue", "forest", "plum"] = "default"
effective_appearance_source: Literal["tenant", "system", "tenant_lock", "system_lock"] = "system" effective_appearance_source: Literal["tenant", "system", "tenant_lock", "system_lock"] = "system"
appearance_custom_overrides_allowed: bool | None = None
system_appearance_custom_overrides_allowed: bool = False
effective_appearance_custom_overrides_allowed: bool = False
settings: dict[str, Any] = Field(default_factory=dict) settings: dict[str, Any] = Field(default_factory=dict)
@@ -165,3 +168,4 @@ class TenantSettingsUpdateRequest(BaseModel):
navigation: NavigationPreferencesPayload | None = None navigation: NavigationPreferencesPayload | None = None
appearance_palette: Literal["default", "civic_blue", "forest", "plum"] | None = None appearance_palette: Literal["default", "civic_blue", "forest", "plum"] | None = None
appearance_palette_locked: bool | None = None appearance_palette_locked: bool | None = None
appearance_custom_overrides_allowed: bool | None = None
+1 -1
View File
@@ -60,7 +60,7 @@ manifest = ModuleManifest(
id="tenancy.lifecycle-and-settings", id="tenancy.lifecycle-and-settings",
title="Administer tenant lifecycle and settings", title="Administer tenant lifecycle and settings",
summary="Tenancy adds explicit tenant creation, activation, context resolution, and tenant-owned settings over Core's shared scope storage.", summary="Tenancy adds explicit tenant creation, activation, context resolution, and tenant-owned settings over Core's shared scope storage.",
body="A tenant is a concrete administrative and data boundary. Tenant lifecycle changes must preserve ownership and recovery guarantees for module-owned records. New tenants default to the German reference language unless the administrator selects another enabled system language; existing tenant and user preferences remain unchanged. Tenant administrators can inherit or override the system side-rail order and visibility and can lock entries visible for users; system locks remain effective. Personal navigation preferences still take precedence except that they cannot hide locked entries. Tenant appearance likewise inherits the system palette until explicitly selected; an unlocked tenant default permits a personal palette, while a policy-authorized tenant lock suppresses it and a system lock always wins. Resetting the tenant palette restores inheritance rather than copying the current system value. Navigation and appearance changes never grant module entitlement, View visibility, or permissions. Tenancy contributes system tenant management and tenant settings to the shared administration workspace; without this module, the Core and Access baseline can operate in single-scope compatibility mode. Core-reserved module entitlement settings are managed only through the Admin module's tenant-module policy endpoints and are preserved when generic tenant settings are replaced.", body="A tenant is a concrete administrative and data boundary. Tenant lifecycle changes must preserve ownership and recovery guarantees for module-owned records. New tenants default to the German reference language unless the administrator selects another enabled system language; existing tenant and user preferences remain unchanged. Tenant administrators can inherit or override the system side-rail order and visibility and can lock entries visible for users; system locks remain effective. Personal navigation preferences still take precedence except that they cannot hide locked entries. Tenant appearance likewise inherits the system palette until explicitly selected; an unlocked tenant default permits a personal palette, while a policy-authorized tenant lock suppresses it and a system lock always wins. Resetting the tenant palette restores inheritance rather than copying the current system value. When the system permits advanced personal color overrides, a policy-authorized tenant administrator may inherit, allow, or block them; the tenant cannot enable a system-denied policy, and palette locks still suppress the editor. Advanced documents cover both light and dark modes and are validated atomically by Core. Navigation and appearance changes never grant module entitlement, View visibility, or permissions. Tenancy contributes system tenant management and tenant settings to the shared administration workspace; without this module, the Core and Access baseline can operate in single-scope compatibility mode. Core-reserved module entitlement settings are managed only through the Admin module's tenant-module policy endpoints and are preserved when generic tenant settings are replaced.",
documentation_types=("admin",), documentation_types=("admin",),
audience=("system_admin", "tenant_admin", "operator"), audience=("system_admin", "tenant_admin", "operator"),
related_modules=("access", "admin", "audit"), related_modules=("access", "admin", "audit"),
+5 -1
View File
@@ -50,6 +50,9 @@ export type TenantSettingsItem = {
system_appearance_palette_locked: boolean; system_appearance_palette_locked: boolean;
effective_appearance_palette: UserUiPalette; effective_appearance_palette: UserUiPalette;
effective_appearance_source: "tenant" | "system" | "tenant_lock" | "system_lock"; effective_appearance_source: "tenant" | "system" | "tenant_lock" | "system_lock";
appearance_custom_overrides_allowed: boolean | null;
system_appearance_custom_overrides_allowed: boolean;
effective_appearance_custom_overrides_allowed: boolean;
settings: Record<string, unknown>; settings: Record<string, unknown>;
}; };
@@ -61,7 +64,7 @@ export type TenantSettingsDeltaSections = Partial<{
"available_languages" | "system_enabled_language_codes" | "enabled_language_codes" "available_languages" | "system_enabled_language_codes" | "enabled_language_codes"
>; >;
navigation: TenantSettingsItem["navigation"]; navigation: TenantSettingsItem["navigation"];
appearance: Pick<TenantSettingsItem, "appearance_palette" | "appearance_palette_locked" | "system_appearance_palette" | "system_appearance_palette_locked" | "effective_appearance_palette" | "effective_appearance_source">; appearance: Pick<TenantSettingsItem, "appearance_palette" | "appearance_palette_locked" | "system_appearance_palette" | "system_appearance_palette_locked" | "effective_appearance_palette" | "effective_appearance_source" | "appearance_custom_overrides_allowed" | "system_appearance_custom_overrides_allowed" | "effective_appearance_custom_overrides_allowed">;
settings: TenantSettingsItem["settings"]; settings: TenantSettingsItem["settings"];
}>; }>;
@@ -161,6 +164,7 @@ export function updateTenantSettings(
navigation?: NavigationPreferences | null; navigation?: NavigationPreferences | null;
appearance_palette?: UserUiPalette | null; appearance_palette?: UserUiPalette | null;
appearance_palette_locked?: boolean; appearance_palette_locked?: boolean;
appearance_custom_overrides_allowed?: boolean | null;
} }
): Promise<TenantSettingsItem> { ): Promise<TenantSettingsItem> {
return apiFetch(settings, "/api/v1/admin/tenant/settings", { return apiFetch(settings, "/api/v1/admin/tenant/settings", {
@@ -51,7 +51,10 @@ const fallback: TenantSettingsItem = {
system_appearance_palette: "default", system_appearance_palette: "default",
system_appearance_palette_locked: false, system_appearance_palette_locked: false,
effective_appearance_palette: "default", effective_appearance_palette: "default",
effective_appearance_source: "system" effective_appearance_source: "system",
appearance_custom_overrides_allowed: null,
system_appearance_custom_overrides_allowed: false,
effective_appearance_custom_overrides_allowed: false
}; };
export default function TenantSettingsPanel({ export default function TenantSettingsPanel({
@@ -76,6 +79,11 @@ export default function TenantSettingsPanel({
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes); const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes);
const dirty = tenantSettingsDraftKey(draft) !== tenantSettingsDraftKey(savedDraft); const dirty = tenantSettingsDraftKey(draft) !== tenantSettingsDraftKey(savedDraft);
const customOverridesEffectivelyAllowed =
draft.system_appearance_custom_overrides_allowed
&& draft.appearance_custom_overrides_allowed !== false
&& !draft.system_appearance_palette_locked
&& !draft.appearance_palette_locked;
const saveDisabledReason = tenantMutationDisabledReason({ const saveDisabledReason = tenantMutationDisabledReason({
busy, busy,
permitted: canWrite, permitted: canWrite,
@@ -127,7 +135,8 @@ export default function TenantSettingsPanel({
enabled_language_codes: draft.enabled_language_codes, enabled_language_codes: draft.enabled_language_codes,
navigation: draft.navigation, navigation: draft.navigation,
appearance_palette: draft.appearance_palette, 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); setDraft(saved);
setSavedDraft(saved); setSavedDraft(saved);
@@ -230,10 +239,36 @@ export default function TenantSettingsPanel({
help={!canWritePolicy ? "i18n:govoplan-tenancy.appearance_lock_policy_permission" : draft.system_appearance_palette_locked ? "i18n:govoplan-tenancy.system_palette_is_locked" : undefined} help={!canWritePolicy ? "i18n:govoplan-tenancy.appearance_lock_policy_permission" : draft.system_appearance_palette_locked ? "i18n:govoplan-tenancy.system_palette_is_locked" : undefined}
label="i18n:govoplan-tenancy.lock_tenant_palette" label="i18n:govoplan-tenancy.lock_tenant_palette"
/> />
<FormField
label="i18n:govoplan-tenancy.custom_overrides_policy"
help={!canWritePolicy ? "i18n:govoplan-tenancy.appearance_policy_permission" : "i18n:govoplan-tenancy.custom_overrides_policy_help"}
>
<select
value={draft.appearance_custom_overrides_allowed === null ? "inherit" : draft.appearance_custom_overrides_allowed ? "allow" : "block"}
disabled={!canWrite || !canWritePolicy || busy}
onChange={(event) => {
const appearance_custom_overrides_allowed = event.target.value === "inherit" ? null : event.target.value === "allow";
setDraft({
...draft,
appearance_custom_overrides_allowed,
effective_appearance_custom_overrides_allowed:
draft.system_appearance_custom_overrides_allowed
&& appearance_custom_overrides_allowed !== false
&& !draft.system_appearance_palette_locked
&& !draft.appearance_palette_locked
});
}}
>
<option value="inherit">i18n:govoplan-tenancy.inherit_system_policy</option>
<option value="allow" disabled={!draft.system_appearance_custom_overrides_allowed}>i18n:govoplan-tenancy.allow_for_tenant_users</option>
<option value="block">i18n:govoplan-tenancy.block_for_tenant_users</option>
</select>
</FormField>
<AppearancePalettePreview palette={draft.system_appearance_palette_locked ? draft.system_appearance_palette : draft.appearance_palette ?? draft.system_appearance_palette} /> <AppearancePalettePreview palette={draft.system_appearance_palette_locked ? draft.system_appearance_palette : draft.appearance_palette ?? draft.system_appearance_palette} />
<DescriptionList variant="inline"> <DescriptionList variant="inline">
<div><dt>i18n:govoplan-tenancy.effective_source</dt><dd>{draft.system_appearance_palette_locked ? "i18n:govoplan-tenancy.system_lock" : draft.appearance_palette ? "i18n:govoplan-tenancy.tenant_default" : "i18n:govoplan-tenancy.system_default"}</dd></div> <div><dt>i18n:govoplan-tenancy.effective_source</dt><dd>{draft.system_appearance_palette_locked ? "i18n:govoplan-tenancy.system_lock" : draft.appearance_palette ? "i18n:govoplan-tenancy.tenant_default" : "i18n:govoplan-tenancy.system_default"}</dd></div>
<div><dt>i18n:govoplan-tenancy.user_override</dt><dd>{draft.system_appearance_palette_locked || draft.appearance_palette_locked ? "i18n:govoplan-tenancy.blocked_by_policy" : "i18n:govoplan-tenancy.allowed"}</dd></div> <div><dt>i18n:govoplan-tenancy.user_override</dt><dd>{draft.system_appearance_palette_locked || draft.appearance_palette_locked ? "i18n:govoplan-tenancy.blocked_by_policy" : "i18n:govoplan-tenancy.allowed"}</dd></div>
<div><dt>i18n:govoplan-tenancy.advanced_color_overrides</dt><dd>{customOverridesEffectivelyAllowed ? "i18n:govoplan-tenancy.allowed" : "i18n:govoplan-tenancy.blocked_by_policy"}</dd></div>
</DescriptionList> </DescriptionList>
</Card> </Card>
</div> </div>
@@ -255,7 +290,8 @@ function tenantSettingsDraftKey(item: TenantSettingsItem): string {
enabled_language_codes: item.enabled_language_codes, enabled_language_codes: item.enabled_language_codes,
navigation: item.navigation, navigation: item.navigation,
appearance_palette: item.appearance_palette, 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
}); });
} }
+14
View File
@@ -6,6 +6,13 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-tenancy.tenant_palette_default": "Tenant palette default", "i18n:govoplan-tenancy.tenant_palette_default": "Tenant palette default",
"i18n:govoplan-tenancy.tenant_palette_default_help": "Inherit the system palette or select the default for this tenant.", "i18n:govoplan-tenancy.tenant_palette_default_help": "Inherit the system palette or select the default for this tenant.",
"i18n:govoplan-tenancy.appearance_lock_policy_permission": "Policy-write permission is required to change this lock.", "i18n:govoplan-tenancy.appearance_lock_policy_permission": "Policy-write permission is required to change this lock.",
"i18n:govoplan-tenancy.custom_overrides_policy": "Personal color override policy",
"i18n:govoplan-tenancy.custom_overrides_policy_help": "Inherit the system decision, or explicitly allow or block the validated advanced editor for this tenant.",
"i18n:govoplan-tenancy.appearance_policy_permission": "Policy-write permission is required to change this appearance policy.",
"i18n:govoplan-tenancy.inherit_system_policy": "Inherit system policy",
"i18n:govoplan-tenancy.allow_for_tenant_users": "Allow for tenant users",
"i18n:govoplan-tenancy.block_for_tenant_users": "Block for tenant users",
"i18n:govoplan-tenancy.advanced_color_overrides": "Advanced color overrides",
"i18n:govoplan-tenancy.system_palette_is_locked": "The system palette is locked and takes precedence.", "i18n:govoplan-tenancy.system_palette_is_locked": "The system palette is locked and takes precedence.",
"i18n:govoplan-tenancy.lock_tenant_palette": "Lock the tenant palette", "i18n:govoplan-tenancy.lock_tenant_palette": "Lock the tenant palette",
"i18n:govoplan-tenancy.effective_source": "Effective source", "i18n:govoplan-tenancy.effective_source": "Effective source",
@@ -118,6 +125,13 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-tenancy.tenant_palette_default": "Mandantenstandard für die Farbpalette", "i18n:govoplan-tenancy.tenant_palette_default": "Mandantenstandard für die Farbpalette",
"i18n:govoplan-tenancy.tenant_palette_default_help": "Systempalette übernehmen oder einen Standard für diesen Mandanten auswählen.", "i18n:govoplan-tenancy.tenant_palette_default_help": "Systempalette übernehmen oder einen Standard für diesen Mandanten auswählen.",
"i18n:govoplan-tenancy.appearance_lock_policy_permission": "Zum Ändern dieser Sperre ist das Recht zum Bearbeiten von Richtlinien erforderlich.", "i18n:govoplan-tenancy.appearance_lock_policy_permission": "Zum Ändern dieser Sperre ist das Recht zum Bearbeiten von Richtlinien erforderlich.",
"i18n:govoplan-tenancy.custom_overrides_policy": "Richtlinie für persönliche Farbanpassungen",
"i18n:govoplan-tenancy.custom_overrides_policy_help": "Die Systementscheidung übernehmen oder den geprüften erweiterten Editor für diesen Mandanten ausdrücklich zulassen oder sperren.",
"i18n:govoplan-tenancy.appearance_policy_permission": "Zum Ändern dieser Darstellungsrichtlinie ist das Recht zum Bearbeiten von Richtlinien erforderlich.",
"i18n:govoplan-tenancy.inherit_system_policy": "Systemrichtlinie übernehmen",
"i18n:govoplan-tenancy.allow_for_tenant_users": "Für Mandantenbenutzer zulassen",
"i18n:govoplan-tenancy.block_for_tenant_users": "Für Mandantenbenutzer sperren",
"i18n:govoplan-tenancy.advanced_color_overrides": "Erweiterte Farbanpassungen",
"i18n:govoplan-tenancy.system_palette_is_locked": "Die Systempalette ist verbindlich und hat Vorrang.", "i18n:govoplan-tenancy.system_palette_is_locked": "Die Systempalette ist verbindlich und hat Vorrang.",
"i18n:govoplan-tenancy.lock_tenant_palette": "Mandantenpalette verbindlich festlegen", "i18n:govoplan-tenancy.lock_tenant_palette": "Mandantenpalette verbindlich festlegen",
"i18n:govoplan-tenancy.effective_source": "Wirksame Quelle", "i18n:govoplan-tenancy.effective_source": "Wirksame Quelle",