This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/BRANDING.md
2026-02-21 21:59:38 +00:00

310 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 |