feat(admin): branding - custom logo, colors, login text (LC-96) #158
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-96-branding"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements LC-96: per-scope branding (logo + primary/accent colors + login heading/body), global for standalone and per-enclave for SaaS.
chat/0033_branding.sql:branding(scope_kind, scope_id, logo_upload_id, primary_color, accent_color, login_heading, login_body, ...)with('global', 0)seeded. Per-enclave rows fall back to global at resolution.logo_upload_idis a soft reference (not a FK) so the trimmed-migration test pools don't all need0012_uploads; the orphan sweep exempts referenced logos via a subquery.routes::branding::inject_branding_css): resolves scope from the request path and stamps<style>:root{--brand-primary;--brand-accent}</style>before</head>of everytext/html2xx response. No per-page struct threading, no Tailwind rebuild.assets/main.csscarries the--brand-*defaults +.brand-bg/.brand-text/.brand-borderutilities./branding/logo(public, login is pre-auth) +/enclave/{id}/branding/logo(membership-gated), both falling back to global. Uploaded through the existingfile_uploadspipeline via a shared multipart parser.markdown::render_login_body, strips raw HTML + fenced code blocks so syntect never loads). Sign-in button usesbrand-bg./admin/branding(standalone, admin-gated) for global;/enclave/{id}/branding(manage-gated) for per-enclave, linked from enclave settings.<input type="color">pickers; both audit-logbranding_set.Test plan
just check(both feature builds, clippy -D warnings, fmt --check).just test+just test-saasgreen. Newroutes_branding.rs(9 cases): resolve fallback, per-enclave precedence, path-scope parse, public-logo 404, middleware injection (global + enclave-scope), admin gating, form persistence, color-validation rejection. The 3 admin-form tests are#[cfg(feature = "standalone")]./admin/branding, confirm login page + in-app colors change; set a different per-enclave color and confirm it wins under/enclave/{id}/.Out of scope (locked plan)
Favicon, custom CSS injection, i18n. In-app sidebar logo placement is a follow-up — login + the admin preview cover logo visibility for now, and colors already propagate app-wide through the middleware.
Per-scope branding for standalone (global) and SaaS (per-enclave). Schema: chat/0033 adds a `branding(scope_kind, scope_id, ...)` table. Global lives at `('global', 0)` (seeded by the migration); per-enclave rows at `('enclave', N)` fall back to global at resolution time. `logo_upload_id` is a soft reference into `file_uploads` (not a FK, so test pools with trimmed migration lists don't all need `0012_uploads` upstream); the orphan sweep exempts whatever a branding row points at. Colors propagate via a tower middleware (`routes::branding::inject_branding_css`) that resolves the scope from the request path and stamps `<style>:root{--brand-primary:...;--brand-accent:...}</style>` before `</head>` of every text/html response. No per-page struct threading and no Tailwind rebuild: `assets/main.css` defines the `--brand-*` defaults + `.brand-bg` / `.brand-text` / `.brand-border` utility classes that read the vars. Logo: served from `/branding/logo` (public; the login page is pre-auth) and `/enclave/{id}/branding/logo` (membership-gated). Uploaded through the existing `file_uploads` pipeline via a shared multipart parser. Login page: heading is plain text; body renders through a new `markdown::render_login_body` that strips raw HTML and fenced code blocks (so the login page never loads syntect). The sign-in button now uses `brand-bg`. Admin UI: `/admin/branding` (standalone, admin-gated) manages global; `/enclave/{id}/branding` (manage-gated) manages per-enclave, linked from enclave settings. Both use `<input type="color">` pickers and audit-log `branding_set`. Tests: 9-case `routes_branding.rs` (resolve fallback, per-enclave precedence, path-scope parse, public-logo 404, middleware injection global + enclave-scope, admin gating, form persistence, color-validation rejection). Migration 0033 backfilled into the 14 hand-rolled-migration test files. Out of scope per the locked plan: favicon, custom CSS, i18n. In-app sidebar logo placement is a follow-up (login + admin-preview cover visibility for now); colors already propagate app-wide via the middleware.