ADR-010: Platform-controlled per-Träger allowlist for the Erscheinungsbild (Appearance/Theme) section
Status: Proposed — 2026-07-06 (this session); pending Frank's confirmation of the data model, then implementation. · Datum: 2026-07-06
- Status: Proposed — 2026-07-06 (this session); pending Frank's confirmation of the data model, then implementation.
- Date: 2026-07-06
- Deciders: Frank (product) + AI (engineering)
- Related: THB theme-builder work (PRs #123/#124/#125 closed, #153 phone-preview merged 06-17, #126 THB-06 merged 07-02);
ProtectedPageLayoutWrapper.tsx/settingsTabs.tsvisibility gating; ORISO-AdminGlobalSettings(SuperAdmin-only page, login + smtp tabs); TenantServiceTenantAdminControlsService(platform-global controls singleton); memoryoriso-design-rule-disable-not-hide(this ADR is a deliberate, scoped exception — see Decision §3).
Context
The "Erscheinungsbild" (Appearance/Theme) section was reported lost after the React 19 / antd 5 modernization. Verification (2026-07-06) showed it is not lost: the THB theme builder (src/pages/Tenants/Edit/ThemeSettings → components/Tenants/GeneralSettings → ThemeBuilder, with the phone preview iphone-14-pro.png + MiniChatPreview) is intact on dev, byte-identical across the modernization. It is a subsection inside the tenant edit view, gated by role + multitenancy mode via shouldShowThemeSettings (ProtectedPageLayoutWrapper.tsx:82, App.tsx:77, settingsTabs.ts:63).
Two facts drive this ADR:
-
There is already an
appearancepermission toggle —settings.tenantAdminControls.allowedPermissionToggles.appearance— read inGeneralSettings/index.tsx(appearanceEditable), but it only makes the ThemeBuilder read-only, and it is platform-global, not per-tenant:TenantAdminControlsServiceloads a single row (findTopByOrderByIdAsc) and enriches every tenant's settings payload with the same controls object (comments state "platform-global admin controls"). So today it is all-tenants-or-none, and only read-only. -
The requirement is per-Träger. A platform admin (SuperAdmin) must decide, via a multi-select of Träger, which tenants may edit the Erscheinungsbild. Allowed → section visible and editable. Not allowed → the Erscheinungsbild subsection is completely hidden for that Träger.
SuperAdmin identity: useUserRoles.hook.ts:39 — AgencyAdmin && TenantAdmin && tenantId === 0.
Decision (proposed)
-
Introduce a platform-global allowlist of Träger IDs —
appearanceAllowedTenantIds: number[]— inside the existingtenant_admin_controlsJSON blob. No schema migration:controlsisLONGTEXTJSON, so the field is additive. This is the single authoritative source, edited only by SuperAdmin. -
Compute the per-tenant boolean server-side. When TenantService enriches a specific tenant's settings (
enrichSettingsWithTenantAdminControls), setallowedPermissionToggles.appearance = appearanceAllowedTenantIds.contains(tenantId)for the tenant being served. This reuses the existing boolean the frontend already reads, and keeps the raw allowlist server-side (a tenant-admin's client only learns its own allowed/not, never the full list). -
Frontend: hide, do not read-only. Change the Erscheinungsbild subsection from
readOnlyto completely hidden whenappearance !== trueand the user is not SuperAdmin — hide the subsection card and the tab/nav entry. This is a deliberate exception to the panel-wide "disable, don't hide" rule (oriso-design-rule-disable-not-hide): the rule governs settings a role is entitled to see but not change; here appearance editing is a platform-granted capability a non-allowlisted Träger has no entitlement to and should not be aware of. -
New SuperAdmin-only multi-select in
GlobalSettingsas a new tab (/admin/global-settings/appearance), listing all Träger (searchTenantData, labelname/ valueid), reusingSelectFormFieldisMulti. Selection =appearanceAllowedTenantIds, saved via the existing controls update endpoint (same path as global SMTP settings). Visible/editable only for SuperAdmin. -
SuperAdmin always sees and edits appearance (bypass the allowlist), so the platform can configure any Träger's theme.
Considered options
- Per-tenant boolean column on each tenant (extend
tenant_admin_controlswith atenant_id, or a new per-tenant flag). Rejected: requires a schema migration + Liquibase changeset + per-tenant writes, for no gain over an additive JSON array. - Keep the single global
appearanceboolean. Rejected: cannot express "these Träger yes, those no." - Read-only instead of hidden. Rejected by product: completely hide for non-allowlisted Träger.
- Ship the full allowlist to every tenant client and filter in the browser. Rejected: needlessly exposes the platform's full Träger allowlist to every tenant-admin; compute the per-tenant boolean on the server instead (§2).
Consequences
Positive: small, mostly-additive change — no DB migration (JSON field), reuses the existing global-controls service/endpoint and the existing appearance boolean the FE already consumes; the FE change is essentially readOnly → hidden plus one new SuperAdmin page; the section "returns" for allowlisted Träger. Negative / cost: the meaning of the existing global appearance boolean changes from "global on/off" to "computed per served tenant from the allowlist" — any current consumer expecting the global semantics must be checked; SuperAdmin must curate the list; a tenant newly removed from the list loses the section on next load.
Testing (what this needs)
Unit (ORISO-Admin, vitest):
- A pure gating helper
isAppearanceEditingAllowed({ appearanceToggle, isSuperAdmin })(or equivalent): allowed boolean true → true; false/undefined → false; SuperAdmin → true regardless. - Visibility: given
appearance !== trueand non-SuperAdmin, the Erscheinungsbild subsection and its tab/nav entry are not rendered (assert absence in the DOM, not merely areadOnlyprop). - The SuperAdmin multi-select page renders only for SuperAdmin; a non-SuperAdmin never reaches it.
Unit / slice (ORISO-TenantService):
TenantAdminControlsServiceround-tripsappearanceAllowedTenantIdsthrough the JSONcontrols(serialize/deserialize; default empty list; backward-compatible with existing rows that lack the field).- Enrichment computes
allowedPermissionToggles.appearance = allowlist.contains(servedTenantId)— true for a listed tenant, false for an unlisted one; the raw list is not exposed in the enriched per-tenant payload.
Integration / E2E (Admin gate, Playwright/Cypress):
- SuperAdmin: open Global Settings → Appearance tab → multi-select lists Träger → enable a Träger → save → that Träger's admin sees and can edit the Erscheinungsbild section; disable → the section disappears for that Träger.
- Non-SuperAdmin tenant-admin: never sees the multi-select; sees the section iff their tenant is allowlisted.
Open (pending Frank)
- Confirm the data model (platform-global allowlist array vs. a real per-tenant flag). Recommendation: the array in the existing controls JSON (§1).
- Any existing consumer of the global
appearanceboolean that relies on global semantics (to be grepped before flipping to computed-per-tenant). - i18n keys + exact German labels for the new tab ("Erscheinungsbild") and the multi-select ("Welche Träger dürfen das Erscheinungsbild ändern?").
- Whether SingleTenantAdmin (single-domain mode) is ever a platform-level curator, or strictly SuperAdmin.
ADR-009: Global Topic/Category ownership and AI-assisted translation infrastructure
Status: Accepted — Frank, 2026-07-01 (grill-with-docs session). Design decided now ("set the points"); implementation deliberately deferred, see Rollout below. · Datum: 2026-07-01
ADR-011: Helm-only deployment; single-domain path-based routing supersedes per-service subdomains
Status: Accepted — already implemented and live on Pre-Dev. · Datum: 2026-07-07