Changeset 0.7.3 (#41)

This commit is contained in:
2026-02-21 21:59:38 +00:00
parent 416e5439ea
commit 1ec392879b
26 changed files with 1084 additions and 319 deletions

309
BRANDING.md Normal file
View 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 |