Skip to content

feat(branding): admin-configurable app name, logo, favicon & primary colour - #214

Merged
antosubash merged 10 commits into
mainfrom
feat/branding
Jun 18, 2026
Merged

feat(branding): admin-configurable app name, logo, favicon & primary colour#214
antosubash merged 10 commits into
mainfrom
feat/branding

Conversation

@antosubash

@antosubashantosubash commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a branding module letting an admin customise the application's identity from the admin UI (/branding), applied everywhere the framework hard-coded "SimpleModule":

  • App name, logo, favicon, primary colour — managed at /branding (gated by a new branding.manage permission), with image upload + colour picker + live preview.
  • No new table — values persist via the existing settings store (SYSTEM scope), hydrate at boot, and hot-swap on save. Logo/favicon images reuse the file_storage module (referenced by UUID).
  • New generic framework hookregister_inertia_shared_provider(app, fn) lets any module contribute layout-wide Inertia shared props without the framework importing the plugin (mirrors the principal_serializer precedent, keeps SM009 intact). A provider that raises is skipped; it can't clobber framework-owned keys.
  • Frontend — typed branding shared prop; SidebarLayout/PublicLayout render the configured name + logo (fallback to defaults); BrandingHead (in the layouts) sets the favicon and overrides the --primary CSS vars reactively.

Scope notes: branding is SYSTEM-scoped for now (the settings store already supports tenant scope, leaving room for per-tenant branding later). Branding the browser-tab <title> is a deferred follow-up. The numeric primary-* colour scale isn't regenerated from the hex, so a few scale-based accents (e.g. the sidebar-active highlight) keep the default tint — the semantic primary token (buttons, swatches) does update.

Screenshots

The admin page at /branding, the same page rebranded as "Acme Analytics" (custom logo + name + #7c3aed), and the branded sidebar across the app:

Admin page (default)Branding appliedAcross the app
admin defaultbranding appliedbranded dashboard

Verification

  • Code review (high effort, multi-angle) — caught and fixed one critical bug: BrandingHead was first rendered as a sibling of <App>, outside Inertia's PageContext/HeadContext, which would have thrown on every page; now rendered inside the layouts. Plus hardening: blank app_name → 422 (not 500), defensive provider merge, reuse of file_storage route constants.
  • Browser QA (Playwright, real server with SQLite + seeded admin) — landing, login, dashboard, and /branding all render with no React/console errors; uploading a logo + setting app name "Acme Analytics" + colour #7c3aed applied live to the sidebar logo/wordmark and --primary CSS var (screenshots above).
  • Local CImake lint ✓ · Python 1365 passed (incl. 21 new branding/framework tests + an end-to-end Inertia shared-props integration test) · JS 22 passed · production build ✓ · make doctor reports zero branding diagnostics.

Test plan

  • Reviewer opens /branding as an admin, sets a name/colour and uploads a logo, and confirms the sidebar + favicon + accent colour update.
  • CI is green.

Modules can register a Callable[[Request], dict] via
register_inertia_shared_provider(app, fn); InertiaLayoutDataMiddleware
merges each provider's output into the per-page shared props after the
built-in auth/menus/i18n blocks. A provider that raises is skipped and
logged, never failing the request. Mirrors the principal_serializer
precedent so the framework never imports plugin modules (SM009).
…d-props)
New branding module: app name, logo, favicon and primary colour, persisted
via the settings store (no own table), surfaced to every page through a
registered Inertia shared-props provider. JSON API (PUT text fields, logo/
favicon upload+clear) gated by branding.manage; Inertia view at /branding
gated by branding.view; sidebar entry + permissions registered.
Also fixes two make-new-module scaffold bugs hit here: host/pyproject used
the bare package name instead of simple_module_<name>, and the module
pyproject was missing [tool.hatch.build.targets.wheel] packages.
…min page
- Type the branding shared prop; SidebarLayout renders the configured app
name + logo (fallback to the SM badge); PublicLayout wordmark too.
- BrandingHead (rendered globally in app.tsx Root) sets the favicon and
overrides the --primary CSS vars from the configured colour, reactively.
- Document <title> suffix uses the configured app name.
- Branding/Manage admin page: app name, colour picker, logo + favicon
upload with live preview; saves via fetch then router.reload().
- Regenerated module pages manifest + i18n keys.
- Use _MODULE_*/_PAGE_* constants for depends_on and the Inertia page id
(keeping the render literal inline for SM003/SM004 AST pairing) + a
drift-guard test.
- Add required pyproject metadata (readme, license, keywords, repo URL).
- Add modules/branding/README.md with Install/Usage sections.
Proves PUT /api/branding -> persist + hot-swap -> shared-props provider ->
InertiaLayoutDataMiddleware -> branding block in a real Inertia page's props.
- CRITICAL: render BrandingHead inside Inertia context (SidebarLayout,
PublicLayout, AuthCardShell) instead of as a sibling of <App> in Root,
where usePage()/<Head> threw and the ErrorBoundary replaced every page.
- Blank/whitespace app_name now returns 422 (validated in BrandingUpdate)
instead of 500 from BrandingSettings re-validation.
- Harden the shared-prop provider merge: a provider can no longer clobber
framework-owned keys (auth/menus/i18n) or an earlier provider's key.
- Reuse file_storage's route constants for the download URL; centralize the
hex-colour regex; drop the duplicate usePage() in Manage.tsx.
- Add regression tests for the 422 and the provider-clobber guard.
Browser QA showed the document <title> never reflects the configured app
name (this app's title updates are page-driven and Inertia's title callback
can't read live page props), so the brandAppName machinery was dead code with
a misleading comment. Revert to the original static suffix; the in-app
branding (sidebar name/logo, favicon, primary colour) works and is verified.
Branding the browser-tab title is a documented follow-up.
…hared
Moves the provider-merge loop out of InertiaLayoutDataMiddleware into
merge_shared_prop_providers() so middleware.py stays under the 300-line cap and
the merge logic is unit-testable in isolation. Behaviour unchanged.
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jun 17, 2026

Copy link
Copy Markdown

Deploying simple-module-python with Cloudflare Pages Cloudflare Pages

Latest commit:5c37b48
Status: ✅ Deploy successful!
Preview URL:https://6f924362.simple-module-python.pages.dev
Branch Preview URL:https://feat-branding.simple-module-python.pages.dev

View logs

@antosubash
antosubash merged commit e6d3c1a into mainJun 18, 2026
12 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@antosubash