Changeset 0.25.0 (#160)

This commit is contained in:
2026-03-08 16:54:17 +00:00
parent 937be26578
commit 2b01d540d6
63 changed files with 6942 additions and 2773 deletions

350
docs/SURFACES.md Normal file
View File

@@ -0,0 +1,350 @@
# Surface Development Guide
**Version:** 0.25.0
**Audience:** Anyone with admin access to a Chat Switchboard instance
---
## Overview
A surface is a full-screen application mode in Chat Switchboard. The platform ships with core surfaces (Chat, Notes, Settings, Admin, Workflow) that cannot be uninstalled. Extension surfaces are uploaded as `.surface` archives via the Admin panel and can be enabled, disabled, and uninstalled at runtime.
This guide covers everything needed to build, package, install, and manage surfaces using only a running instance — no access to the Go codebase required.
---
## Quick Start
Create a minimal surface in under 5 minutes:
### 1. Create the files
**manifest.json:**
```json
{
"id": "hello",
"version": "1.0.0",
"title": "Hello World",
"description": "Minimal surface example",
"route": "/hello",
"auth": "authenticated",
"scripts": ["js/hello.js"],
"styles": ["css/hello.css"],
"source": "extension"
}
```
**js/hello.js:**
```javascript
(function() {
document.addEventListener('DOMContentLoaded', function() {
if (window.__SURFACE__ !== 'hello') return;
var root = document.getElementById('surfaceRoot');
if (!root) return;
root.innerHTML =
'<div style="display:flex;flex-direction:column;height:100%;background:var(--bg);color:var(--text);font-family:inherit;">' +
'<div style="display:flex;align-items:center;gap:8px;padding:0 12px;height:40px;background:var(--bg-secondary);border-bottom:1px solid var(--border);position:relative;z-index:20;">' +
'<a href="' + (window.__BASE__ || '') + '/" style="color:var(--text-3);text-decoration:none;font-size:12px;">← Back</a>' +
'<span style="font-size:13px;font-weight:600;">Hello World</span>' +
'</div>' +
'<div style="flex:1;display:flex;align-items:center;justify-content:center;">' +
'<div style="text-align:center;">' +
'<div style="font-size:48px;margin-bottom:16px;">👋</div>' +
'<h1 style="font-size:24px;font-weight:600;margin:0 0 8px;">Hello from a Surface</h1>' +
'<p style="color:var(--text-3);margin:0;" id="helloStatus">Waiting for app init…</p>' +
'</div>' +
'</div>' +
'</div>';
document.addEventListener('sb:ready', function() {
var el = document.getElementById('helloStatus');
if (el) {
el.textContent = 'Logged in as ' + (API.user?.username || 'unknown') +
' | ' + (App.models?.length || 0) + ' models loaded';
}
}, { once: true });
});
})();
```
**css/hello.css:**
```css
/* Empty for minimal example. Real surfaces use CSS classes with var(--*) tokens. */
```
### 2. Package
```bash
zip -r hello.surface manifest.json js/ css/
```
### 3. Install
**Admin → System → Surfaces → + Install Surface**. Upload `hello.surface`.
### 4. Visit
Navigate to `https://your-instance/hello`.
---
## How Surface Loading Works
```
Browser requests /hello
→ Router matches route from surface_registry
→ Auth middleware runs (based on manifest "auth" field)
→ base.html renders with:
- Common CSS (variables, layout, components)
- Common JS (api.js, events.js, ui-core.js, all component factories)
- Your surface CSS and JS from manifest
- app.js (boot script)
→ DOMContentLoaded fires:
1. Your surface JS runs — build DOM, create components
2. app.js init() runs — auth, settings, models, WebSocket
3. 'sb:ready' CustomEvent fires when init is complete
```
### Critical: Boot Timing
Your JS runs BEFORE app init completes.
| Available at DOMContentLoaded | Available at sb:ready |
|---|---|
| `window.__SURFACE__`, `window.__BASE__` | `App.models`, `App.settings` |
| All component factories | `API.user`, `API.isAdmin` |
| DOM ready | `Events` WebSocket connected |
**Rule:** Anything needing models, user info, or settings goes in `sb:ready`:
```javascript
document.addEventListener('sb:ready', function() {
// Safe to use App.models, API.user, App.settings
}, { once: true });
```
---
## manifest.json Reference
| Field | Required | Description |
|---|---|---|
| `id` | Yes | Unique identifier. Lowercase, no spaces. |
| `title` | Yes | Display name in admin panel and nav. |
| `version` | No | Semver for display. |
| `description` | No | Short description for admin. |
| `route` | No | URL pattern. Gin params: `/thing/:id`. Defaults to `/{id}`. |
| `alt_routes` | No | Additional URL patterns. |
| `auth` | No | `"authenticated"` (default), `"admin"`, `"session"`, `"public"`. |
| `scripts` | No | JS files relative to archive root. |
| `styles` | No | CSS files relative to archive root. |
| `components` | No | Component IDs used (documentation only). |
| `source` | No | Always `"extension"` for uploaded surfaces. |
---
## .surface Archive Format
```
my-surface.surface (ZIP file)
├── manifest.json ← Required
├── js/
│ └── my-surface.js ← Boot script(s)
├── css/
│ └── my-surface.css ← Styles
└── assets/ ← Optional: images, fonts
```
Standard ZIP. `.surface` extension is conventional; `.zip` also accepted.
---
## CSS Theme Tokens
Use these instead of hardcoded colors:
```css
/* Backgrounds */
var(--bg) var(--bg-secondary) var(--bg-tertiary) var(--bg-raised) var(--bg-hover)
/* Text */
var(--text) var(--text-2) var(--text-3)
/* Accent */
var(--accent) var(--accent-dim) var(--accent-hover)
/* Borders */
var(--border) var(--border-light)
/* Semantic */
var(--success) var(--warning) var(--danger) var(--purple)
var(--success-dim) var(--warning-dim) var(--danger-dim) var(--purple-dim)
/* Type & Layout */
var(--mono) var(--msg-font) var(--radius) var(--radius-lg) var(--transition)
```
---
## Reusable Components
All loaded by `base.html` on every surface. No imports needed.
### UserMenu
```javascript
// Build DOM into a container
container.innerHTML =
'<div class="user-menu-wrap" id="userMenuWrap">' +
'<button id="userMenuBtn" class="user-btn">' +
'<div id="userAvatar" class="user-avatar"><span id="avatarLetter">?</span></div>' +
'<span id="userName" class="sb-label">User</span>' +
'</button>' +
'<div id="userFlyout" class="user-flyout">' +
'<button id="menuSettings" class="flyout-item">Settings</button>' +
'<button id="menuAdmin" class="flyout-item" style="display:none">Admin</button>' +
'<button id="menuDebug" class="flyout-item" style="display:none">Debug</button>' +
'<hr class="flyout-divider">' +
'<button id="menuSignout" class="flyout-item flyout-danger">Sign Out</button>' +
'</div>' +
'</div>';
// Wire after auth (sb:ready)
var menu = UserMenu.create({ id: '' });
menu.setUser(API.user);
menu.bind({
onSettings: function() { location.href = (window.__BASE__ || '') + '/settings'; },
onAdmin: function() { location.href = (window.__BASE__ || '') + '/admin'; },
onSignout: function() { if (typeof handleLogout === 'function') handleLogout(); },
});
menu.showAdmin(API.isAdmin);
```
### ChatPane
```javascript
// Build DOM
slot.innerHTML =
'<div class="chat-pane" id="xChatPane">' +
'<div class="chat-pane-messages" id="xChatMessages"></div>' +
'<div class="chat-pane-input-bar">' +
'<div class="chat-pane-input-wrap">' +
'<div class="chat-pane-input" id="xChatInput"></div>' +
'<button class="chat-pane-send" id="xSendBtn">Send</button>' +
'</div>' +
'</div>' +
'</div>';
// Create instance
var pane = ChatPane.create({
id: 'x',
messagesEl: document.getElementById('xChatMessages'),
inputEl: document.getElementById('xChatInput'),
sendBtnEl: document.getElementById('xSendBtn'),
standalone: true,
});
pane.showWelcome();
```
### PaneContainer
```javascript
PaneContainer.registerPreset('my-layout', {
type: 'split', direction: 'horizontal',
sizes: [250, null, 350],
children: [
{ type: 'leaf', id: 'nav', size: 250, minSize: 150 },
{ type: 'leaf', id: 'main' }, // flex pane (no size)
{ type: 'tabbed', id: 'assist', size: 350, minSize: 200,
tabs: [{ id: 'chat', label: 'Chat' }, { id: 'tools', label: 'Tools' }]
},
],
});
var layout = PaneContainer.mount(document.getElementById('body'), 'my-layout');
var mainEl = layout._panes.get('main').el;
var chatTab = layout._panes.get('assist').getTabPanel('chat');
```
**Rules:** One child per split must omit `size` (flex pane). Drag handles cap at 60%. Sizes persist in localStorage.
---
## Making API Calls
```javascript
// CRUD
var data = await API._get('/api/v1/channels');
var resp = await API._post('/api/v1/channels', { title: 'New', type: 'direct' });
await API._put('/api/v1/channels/' + id, { title: 'Updated' });
await API._del('/api/v1/channels/' + id);
// Streaming (SSE)
var resp = await API.streamCompletion(channelId, content, model, abortSignal);
var reader = resp.body.getReader();
```
Auth tokens and base path are handled automatically.
---
## Admin: Surface Management
**Location:** Admin → System → Surfaces
| Action | Description |
|---|---|
| **+ Install Surface** | Upload `.surface` or `.zip` archive |
| **Toggle** | Enable/disable. Disabled surfaces redirect to Chat. |
| **Uninstall** | Remove extension surface (files + DB entry). |
**Protected:** Chat and Admin toggles are locked — cannot be disabled.
### API
```bash
# List
curl -H "$AUTH" "$BASE/api/v1/admin/surfaces"
# Install
curl -H "$AUTH" -F "file=@hello.surface" "$BASE/api/v1/admin/surfaces/install"
# Disable / Enable
curl -X PUT -H "$AUTH" "$BASE/api/v1/admin/surfaces/editor/disable"
curl -X PUT -H "$AUTH" "$BASE/api/v1/admin/surfaces/editor/enable"
# Uninstall
curl -X DELETE -H "$AUTH" "$BASE/api/v1/admin/surfaces/hello"
# Public: list enabled (for nav)
curl -H "$AUTH" "$BASE/api/v1/surfaces"
```
---
## Checklist
- [ ] `manifest.json` has `id` and `title`
- [ ] Boot script checks `window.__SURFACE__` guard
- [ ] DOM built in JS (no Go template dependency)
- [ ] Deferred init via `sb:ready`
- [ ] Topbar: `position: relative; z-index: 20`
- [ ] All colors use `var(--*)` tokens
- [ ] Back link: `window.__BASE__ + '/'`
- [ ] Graceful with 0 models
- [ ] Archive: only `manifest.json`, `js/`, `css/`, `assets/`
---
## Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Surface doesn't appear after install | Routes registered at startup | Restart server or toggle disable/enable |
| "Cannot overwrite core surface" | ID conflicts with chat/notes/etc. | Choose different ID |
| `API is undefined` | Code runs before app init | Move to `sb:ready` listener |
| No styles applied | CSS path mismatch | Check manifest `styles` matches archive structure |
| Flyout hidden behind content | Missing stacking context | Add `z-index: 20` to topbar |
| Drag handle snaps | v0.25.0 known issue | Sub-pixel rounding; partially mitigated |