Changeset 0.7.3 (#41)
This commit is contained in:
309
BRANDING.md
Normal file
309
BRANDING.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user