310 lines
10 KiB
Markdown
310 lines
10 KiB
Markdown
# Branding — Volume Mount Contract
|
||
|
||
**Version:** 0.7.3
|
||
**Status:** Spec
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
Chat Switchboard supports white-label branding through a volume mount at
|
||
`/branding/` on the frontend container. Deployers provide a ConfigMap (or
|
||
bind mount) with their identity assets. The app reads a JSON config on
|
||
startup, serves static assets at runtime, and falls back gracefully when
|
||
no branding is mounted.
|
||
|
||
Switchboard provides the hooks. Your brand lives in its own repo.
|
||
|
||
---
|
||
|
||
## Mount Path
|
||
|
||
```
|
||
/branding/ ← volume mount root (frontend container)
|
||
├── branding.json ← config seed (required if mount exists)
|
||
├── favicon.png ← tab/bookmark icon
|
||
├── logo.png ← splash page hero, optional sidebar
|
||
└── custom.css ← style overrides (power-user escape hatch)
|
||
```
|
||
|
||
All files are optional individually, but `branding.json` is expected if the
|
||
mount exists. Missing files degrade gracefully — no broken images, no JS errors.
|
||
|
||
---
|
||
|
||
## branding.json — Config Seed
|
||
|
||
```json
|
||
{
|
||
"org_name": "Gobha.ai",
|
||
"tagline": "Something clever here",
|
||
"accent_color": "#4a9eff",
|
||
"logo": "logo.png",
|
||
"favicon": "favicon.png"
|
||
}
|
||
```
|
||
|
||
**Fields:**
|
||
|
||
| Field | Type | Default | Description |
|
||
|----------------|--------|----------------------|---------------------------------------------|
|
||
| `org_name` | string | `"Chat Switchboard"` | Displayed in splash, header, auth card, `<title>` |
|
||
| `tagline` | string | `"Multi-Model AI Chat"` | Splash page subtitle, auth footer |
|
||
| `headline` | string | `null` | Splash hero headline (default preserved if null) |
|
||
| `accent_color` | string | `"#4a9eff"` | Primary UI accent (hex) |
|
||
| `logo` | string | `null` | Filename relative to `/branding/` |
|
||
| `favicon` | string | `null` | Filename relative to `/branding/` |
|
||
| `pills` | array | (Switchboard defaults) | Feature pills on splash. `[]` = hide. See below. |
|
||
|
||
**Pills format:**
|
||
|
||
```json
|
||
"pills": [
|
||
{ "icon": "⚡", "text": "Fast inference", "style": "accent" },
|
||
{ "icon": "🔒", "text": "Zero trust", "style": "purple" },
|
||
{ "icon": "🏢", "text": "Enterprise ready" }
|
||
]
|
||
```
|
||
|
||
`style` is optional: `"accent"` uses the accent color, `"purple"` uses purple,
|
||
omit for the default neutral pill. Set `"pills": []` to hide the section entirely.
|
||
Omit the field to keep the stock Switchboard pills.
|
||
|
||
**Lifecycle:**
|
||
|
||
1. Frontend entrypoint (`docker-entrypoint-fe.sh`) reads `/branding/branding.json` on container start
|
||
2. Values are injected into `index.html` as a `<script>` block: `window.__BRANDING__`
|
||
3. The `branding` key is also upserted into `global_settings` via the backend on startup
|
||
(admin panel override layer — if admins change values in the UI, those take precedence
|
||
until the next cold deploy with a branding mount)
|
||
4. Frontend `initBranding()` applies values from `window.__BRANDING__` (fast, no API call)
|
||
then overlays any DB overrides from `App.serverSettings.branding` (from public settings)
|
||
|
||
**Resolution order:** `branding.json` (fast init) → DB `global_settings.branding` (override layer)
|
||
|
||
---
|
||
|
||
## favicon.png — Static Asset
|
||
|
||
Served at `/branding/favicon.png` by nginx. The frontend `<link rel="icon">` is
|
||
set dynamically by `initBranding()`:
|
||
|
||
```js
|
||
// If branding favicon exists, use it; otherwise keep built-in default
|
||
document.querySelector('link[rel="icon"]').href = '/branding/favicon.png';
|
||
```
|
||
|
||
**Recommendations:**
|
||
- PNG format (modern browsers prefer it over ICO)
|
||
- 32×32 minimum, 192×192 recommended (covers PWA + high-DPI)
|
||
- Transparent background works best with dark themes
|
||
|
||
---
|
||
|
||
## logo.png — Static Asset
|
||
|
||
Served at `/branding/logo.png`. Used in:
|
||
- Splash page hero (replaces the 🔀 emoji)
|
||
- Sidebar header (optional, depends on size)
|
||
|
||
**Recommendations:**
|
||
- PNG with transparency
|
||
- Max 512×512 (larger files are wasteful; displayed at ~80px on splash)
|
||
- Aspect ratio: square or landscape (tall logos will be clamped)
|
||
|
||
---
|
||
|
||
## custom.css — Style Extension
|
||
|
||
Loaded *after* the main stylesheet:
|
||
|
||
```html
|
||
<link rel="stylesheet" href="/branding/custom.css" onerror="this.remove()">
|
||
```
|
||
|
||
The `onerror` handler silently removes the tag if the file doesn't exist (no 404
|
||
console noise in unbrandeded deployments).
|
||
|
||
**What you can do:**
|
||
- Override `--accent-color` and any other CSS custom property
|
||
- Change fonts (`@import` or `@font-face` with files in `/branding/`)
|
||
- Add background textures or patterns
|
||
- Hide elements you don't want (`display: none`)
|
||
- Override specific component styles
|
||
|
||
**What you shouldn't do:**
|
||
- Rely on internal class names that may change between versions
|
||
- Override layout properties (`flex`, `grid`) unless you're testing against the current release
|
||
- Import external resources (breaks airgapped deployments)
|
||
|
||
**Example:**
|
||
|
||
```css
|
||
:root {
|
||
--accent-color: #e74c3c;
|
||
--bg-primary: #1a1a2e;
|
||
}
|
||
|
||
.splash-logo img {
|
||
border-radius: 50%;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Frontend Touchpoints
|
||
|
||
These are the DOM elements and CSS properties that branding affects:
|
||
|
||
| Element / Property | Default Value | Branding Source |
|
||
|--------------------------|----------------------------------|-----------------------|
|
||
| `<title>` | `Chat Switchboard` | `org_name` |
|
||
| `.brand-text` | `Chat Switchboard` | `org_name` |
|
||
| `.hero-wordmark` | `Chat Switchboard` | `org_name` |
|
||
| `.hero-headline` | `One interface. Every AI model.` | `headline` |
|
||
| `.hero-sub` | (default description) | `tagline` |
|
||
| `.splash-logo` | SVG switchboard icon | `logo.png` → `<img>` |
|
||
| `link[rel="icon"]` | `favicon-32.png` | `favicon.png` |
|
||
| `--accent-color` | `#4a9eff` | `accent_color` |
|
||
| `.auth-card-header h2` | `Welcome back` | `Welcome to {org_name}` |
|
||
| `.auth-card-header p` | `Sign in to continue to your workspace` | `Sign in to continue` |
|
||
| `.auth-footer p` | `Self-hosted AI chat...` | `tagline` |
|
||
| `.hero-features` | Switchboard feature pills | `pills` array or `[]` to hide |
|
||
|
||
---
|
||
|
||
## Nginx Configuration
|
||
|
||
The frontend entrypoint adds a branding location block. For path-based deployments,
|
||
the block is under `BASE_PATH` so Traefik routes requests to the correct pod:
|
||
|
||
```nginx
|
||
# Root deployment (no BASE_PATH):
|
||
location /branding/ {
|
||
alias /branding/;
|
||
expires 1h;
|
||
add_header Cache-Control "public";
|
||
try_files $uri =404;
|
||
}
|
||
|
||
# Path-based deployment (e.g. /dev, /test):
|
||
location ${BASE_PATH}/branding/ {
|
||
alias /branding/;
|
||
expires 1h;
|
||
add_header Cache-Control "public";
|
||
try_files $uri =404;
|
||
}
|
||
```
|
||
|
||
Frontend JS resolves paths via `window.__BASE__ + '/branding/'` so URLs
|
||
automatically include the environment prefix.
|
||
|
||
Short cache (1h) so branding updates via ConfigMap rollout are picked up
|
||
without requiring users to hard-refresh.
|
||
|
||
---
|
||
|
||
## K8s Deployment
|
||
|
||
The frontend deployment mounts the branding ConfigMap:
|
||
|
||
```yaml
|
||
spec:
|
||
containers:
|
||
- name: frontend
|
||
volumeMounts:
|
||
- name: branding
|
||
mountPath: /branding
|
||
readOnly: true
|
||
volumes:
|
||
- name: branding
|
||
configMap:
|
||
name: switchboard-branding
|
||
optional: true # ← app works without it
|
||
```
|
||
|
||
The `optional: true` is critical — Switchboard deploys cleanly with zero
|
||
branding config. The `switchboard-gobha-ai` repo (or any deployer's
|
||
equivalent) creates this ConfigMap.
|
||
|
||
---
|
||
|
||
## Deployer Repo Structure (Example: switchboard-gobha-ai)
|
||
|
||
```
|
||
switchboard-gobha-ai/
|
||
├── branding/
|
||
│ ├── branding.json
|
||
│ ├── favicon.png
|
||
│ ├── logo.png
|
||
│ └── custom.css
|
||
├── k8s/
|
||
│ └── configmap.yaml
|
||
├── README.md
|
||
└── .gitea/
|
||
└── workflows/
|
||
└── deploy.yaml
|
||
```
|
||
|
||
**configmap.yaml:**
|
||
|
||
```yaml
|
||
apiVersion: v1
|
||
kind: ConfigMap
|
||
metadata:
|
||
name: switchboard-branding
|
||
namespace: ${NAMESPACE}
|
||
data:
|
||
branding.json: |
|
||
{
|
||
"org_name": "Gobha.ai",
|
||
"tagline": "Your tagline",
|
||
"accent_color": "#4a9eff"
|
||
}
|
||
binaryData:
|
||
favicon.png: <base64-encoded>
|
||
logo.png: <base64-encoded>
|
||
custom.css: <base64-encoded-or-use-data>
|
||
```
|
||
|
||
Note: For binary files in ConfigMaps, use `binaryData` with base64 encoding.
|
||
Alternatively, use a script that creates the ConfigMap from files:
|
||
|
||
```sh
|
||
kubectl create configmap switchboard-branding \
|
||
--from-file=branding/branding.json \
|
||
--from-file=branding/favicon.png \
|
||
--from-file=branding/logo.png \
|
||
--from-file=branding/custom.css \
|
||
--dry-run=client -o yaml | kubectl apply -f -
|
||
```
|
||
|
||
---
|
||
|
||
## Backend Integration
|
||
|
||
The backend participates in branding in two ways:
|
||
|
||
1. **Startup seed** (optional, future): If the backend also mounts `/branding/`,
|
||
it can read `branding.json` and upsert into `global_settings` alongside the
|
||
admin bootstrap. This enables admin-panel overrides without redeployment.
|
||
|
||
2. **Public settings**: The `branding` key is added to `publicSettingKeys`,
|
||
making it available to non-admin users via `GET /api/v1/settings/public`.
|
||
|
||
For 0.7.3, the frontend reads branding from the static mount at init time.
|
||
Backend DB seeding is a future enhancement for admin-panel editing.
|
||
|
||
---
|
||
|
||
## Graceful Degradation
|
||
|
||
| Condition | Behavior |
|
||
|----------------------------------|-----------------------------------------------|
|
||
| No `/branding/` mount | All defaults. App looks like stock Switchboard |
|
||
| Mount exists, no `branding.json` | Static assets served, no text/color overrides |
|
||
| `branding.json` missing fields | Each field falls back to its default |
|
||
| `logo` field set, file missing | `<img>` gets 404, `onerror` shows emoji fallback |
|
||
| `custom.css` missing | `<link>` tag self-removes via `onerror` |
|
||
| `favicon.png` missing | Built-in favicon remains |
|