feat(branding): customize application identity

This commit is contained in:
courtmanr@gmail.com
2026-07-30 16:25:36 +01:00
parent 647d062531
commit 672db9113f
27 changed files with 855 additions and 35 deletions
+7 -1
View File
@@ -236,7 +236,13 @@ environment (`PULSE_REPORT_PROVIDER_BRAND_DISPLAY_NAME`,
model each client runtime has its own settings, so the override is
per-client; in shared-process mode the settings override applies
instance-wide, so all organizations share one brand (usually yours). Branding
requires the `white_label` entitlement on the licence.
requires the `white_label` entitlement on the licence. Entitled administrators
can edit the settings-based display name and bounded inline PNG, JPEG, or GIF
under **Settings → System → General → Appearance**. That override is used by
both generated reports and the authenticated application header; the browser
title follows the configured display name. Without the entitlement, the
runtime returns and renders the built-in Pulse identity even if branding
settings remain stored.
Scheduled reports are tenant-local. In provider-hosted MSP, each client
runtime stores its own schedules in `report_schedules.json`, writes generated
@@ -8711,3 +8711,22 @@ to configure a public URL. `TestContract_SSOProviderResponseBaseURLNeverGuessesL
in `internal/api/contract_test.go` pins the precedence, the request fallback,
the trusted-proxy gate on forwarded headers, and the empty-rather-than-wrong
behavior.
### Runtime branding is a narrow presentation contract
`GET /api/runtime/branding` is the authenticated `monitoring:read` boundary
for application-shell branding. It returns only `enabled`, `displayName`, and
the validated inline `logoDataUrl`; it never returns the system settings
document, filesystem logo paths, provider credentials, or licence material.
The handler returns the canonical disabled/empty shape unless the request's
active licence service grants `white_label`, even when `reportBranding`
remains persisted.
The settings write contract remains `reportBranding` on
`POST /api/system/settings/update`, protected by `settings:write` and the
existing bounded display-name, base64, and PNG/JPEG/GIF validation. The
runtime read normalizes valid image bytes to a canonical data URL and omits an
invalid or unknown image without discarding a valid display name.
`internal/api/runtime_branding_test.go` and
`internal/api/route_inventory_test.go` pin the entitlement, shape, image, and
route contracts.
@@ -5502,3 +5502,22 @@ as the card. `settingsArchitecture.test.ts` pins the guidance owner, the absent
localhost literal, and the corrected post-save copy; the rendered fallbacks and
copy affordances are covered by
`frontend-modern/src/components/Settings/__tests__/SSOProvidersPanel.test.tsx`.
### Entitled application branding stays app-shell-owned
`frontend-modern/src/stores/systemSettings.ts` owns the narrow reactive
runtime-brand payload loaded during authenticated bootstrap. The shared
`frontend-modern/src/AppLayout.tsx` shell is the only owner of applying that
payload to the centered header lockup and route-aware browser title. A custom
bounded banner logo replaces the built-in mark; a non-empty display name
replaces the Pulse wordmark, while a logo with an empty display name may stand
alone. Kiosk mode keeps its existing hidden-header behavior.
The Appearance surface edits the already-canonical `reportBranding` object
through `BrandingSettingsCard`; it accepts PNG, JPEG, or GIF files no larger
than the persisted inline-logo boundary, previews the exact saved material,
marks the shared settings form dirty, and provides an explicit remove action.
It must not create a page-local branding cache or render configured values
when `white_label` is unavailable. Focused proofs live in
`BrandingSettingsCard.test.tsx`, `AppLayout.test.tsx`, and
`stores/__tests__/systemSettings.test.ts`.
@@ -1789,3 +1789,22 @@ already enforces scopes server-side and no backend behaviour changed.
Regression coverage: the `Issue1650` cases in
`frontend-modern/src/utils/__tests__/securityScorePresentation.test.ts` and
`frontend-modern/src/components/__tests__/SecurityWarning.test.tsx`.
### Runtime branding reveals presentation material only after entitlement
The authenticated application header cannot depend on the admin-only full
system-settings response, so `/api/runtime/branding` is deliberately readable
with `monitoring:read`. That wider read authority is safe only because the
payload is a strict allowlist of three presentation fields and the server
fails closed: without the active `white_label` entitlement it returns
`enabled: false` with empty name and logo values. It never exposes
`logoPath`, other settings, environment values, licence records, or storage
locations.
Brand mutations remain behind `settings:write` and the existing report-brand
validation rejects unsupported keys, newlines, oversized base64, malformed
base64, and formats outside PNG/JPEG/GIF. Browser rendering consumes only the
server-filtered runtime payload; hiding controls or checking a client-side
capability is not treated as the authorization boundary.
`internal/api/runtime_branding_test.go` proves the no-entitlement non-leakage
and image normalization.
@@ -4818,3 +4818,12 @@ only for a session that can reach settings, using the single
handoff, demo organization suppression, and platform-page route carriage in
`frontend-modern/src/App.tsx` are unchanged; nothing about backup, restore, or
recovery routing keys off banner visibility.
The persisted `reportBranding` object remains adjacent tenant/runtime
configuration rather than storage inventory or recovery state. The
application-header extension reuses that same object and does not create a
second logo artifact, database table, or backup surface. Existing system
configuration export/restore therefore carries the display name and bounded
inline logo exactly as it already carries report branding. Runtime reads
through `/api/runtime/branding` are read-only and entitlement-filtered; they do
not mutate, relocate, or synthesize persisted branding state.