Changeset 0.37.14 (#226)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-23 16:47:48 +00:00
committed by xcaliber
parent fcb998bff9
commit b7746c3004
164 changed files with 6972 additions and 3527 deletions

View File

@@ -149,7 +149,7 @@ func (e *Engine) adminLoader(c *gin.Context, s store.Stores) (any, error) {
ctx := context.Background()
section := c.Param("section")
if section == "" {
section = "overview"
section = "users"
}
data := &AdminPageData{

View File

@@ -68,9 +68,24 @@ type BannerConfig struct {
Visible bool `json:"visible"`
}
// MessageConfig holds dismissible message bar settings.
type MessageConfig struct {
Text string `json:"text"`
Variant string `json:"variant"` // info, warn, error, success
Visible bool `json:"visible"`
}
// FooterConfig holds optional footer bar settings.
type FooterConfig struct {
Text string `json:"text"`
Visible bool `json:"visible"`
}
// PageData is passed to every template render.
type PageData struct {
Banner BannerConfig
Message MessageConfig
Footer FooterConfig
Surface string // active surface ID
Section string // sub-section (for admin pages)
CSPNonce string
@@ -300,6 +315,8 @@ func (e *Engine) Render(c *gin.Context, name string, data PageData) {
data.Environment = e.cfg.Environment
data.CSPNonce = generateNonce()
data.Banner = e.loadBanner()
data.Message = e.loadMessage()
data.Footer = e.loadFooter()
// v0.22.7: Default theme if not set
if data.Theme == "" {
@@ -336,7 +353,7 @@ func (e *Engine) RenderSurface(surfaceID string) gin.HandlerFunc {
section := c.Param("section")
if section == "" && surfaceID == "admin" {
section = "overview"
section = "users"
}
if section == "" && surfaceID == "settings" {
section = "general"
@@ -837,6 +854,50 @@ func (e *Engine) loadBanner() BannerConfig {
return b
}
// loadMessage reads dismissible message bar config from global settings.
func (e *Engine) loadMessage() MessageConfig {
if e.stores.GlobalConfig == nil {
return MessageConfig{}
}
raw, err := e.stores.GlobalConfig.Get(context.Background(), "message")
if err != nil || raw == nil {
return MessageConfig{}
}
m := MessageConfig{}
if v, ok := raw["enabled"].(bool); ok {
m.Visible = v
}
if v, ok := raw["text"].(string); ok {
m.Text = v
}
if v, ok := raw["variant"].(string); ok {
m.Variant = v
}
if m.Variant == "" {
m.Variant = "info"
}
return m
}
// loadFooter reads optional footer bar config from global settings.
func (e *Engine) loadFooter() FooterConfig {
if e.stores.GlobalConfig == nil {
return FooterConfig{}
}
raw, err := e.stores.GlobalConfig.Get(context.Background(), "footer")
if err != nil || raw == nil {
return FooterConfig{}
}
f := FooterConfig{}
if v, ok := raw["enabled"].(bool); ok {
f.Visible = v
}
if v, ok := raw["text"].(string); ok {
f.Text = v
}
return f
}
// loadBranding reads instance branding from global settings.
func (e *Engine) loadBranding() (name, logoURL, tagline string) {
name = "Chat Switchboard" // default

View File

@@ -3,7 +3,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content">
<title>{{block "title" .}}Chat Switchboard{{end}}</title>
<link rel="icon" type="image/svg+xml" href="{{.BasePath}}/favicon.svg?v={{.Version}}">
<link rel="icon" type="image/x-icon" href="{{.BasePath}}/favicon.ico?v={{.Version}}">
@@ -24,6 +24,7 @@
<link rel="stylesheet" href="{{.BasePath}}/css/workflow.css?v={{.Version}}">
<link rel="stylesheet" href="{{.BasePath}}/css/admin-surfaces.css?v={{.Version}}">
<link rel="stylesheet" href="{{.BasePath}}/css/extension-surface.css?v={{.Version}}">
<link rel="stylesheet" href="{{.BasePath}}/css/sw-shell.css?v={{.Version}}">
{{if eq .Surface "chat"}}{{template "css-chat" .}}{{end}}
{{if eq .Surface "notes"}}{{template "css-notes" .}}{{end}}
{{/* v0.27.0: Extension surface CSS — loaded from /surfaces/{id}/css/main.css */}}
@@ -51,6 +52,23 @@
document.documentElement.setAttribute('data-theme', resolved);
var meta = document.getElementById('metaThemeColor');
if (meta) meta.content = resolved === 'light' ? '#f7f7fa' : '#0e0e10';
var fav = document.querySelector('link[rel="icon"][type="image/svg+xml"]');
if (fav) fav.href = fav.href.replace(/favicon(-light)?\.svg/, resolved === 'light' ? 'favicon-light.svg' : 'favicon.svg');
} catch(e) {}
})();
// Early appearance — apply scale + msg font from localStorage on all surfaces.
// Scale uses transform on .surface-inner so shell stays fixed, content fills viewport.
(function() {
try {
var p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
if (p.scale && p.scale !== 100) {
var s = p.scale / 100;
document.addEventListener('DOMContentLoaded', function() {
var el = document.getElementById('surfaceInner');
if (el) { el.style.transform = 'scale('+s+')'; el.style.transformOrigin = 'top left'; el.style.width = (100/s)+'%'; el.style.height = (100/s)+'%'; }
});
}
if (p.msgFont && p.msgFont !== 14) document.documentElement.style.setProperty('--msg-font', p.msgFont + 'px');
} catch(e) {}
})();
</script>
@@ -62,6 +80,15 @@
</div>
{{end}}
{{if .Message.Visible}}
<div class="sw-shell__announcement" id="shellMessage">
<div class="sw-shell__announcement-inner sw-shell__announcement--{{.Message.Variant}}">
<span class="sw-shell__announcement-text">{{.Message.Text}}</span>
<button class="sw-shell__banner-close" onclick="this.closest('.sw-shell__announcement').remove()" aria-label="Dismiss">&times;</button>
</div>
</div>
{{end}}
<div class="surface" id="surface">
<div class="surface-inner" id="surfaceInner">
{{if eq .Surface "chat"}}{{template "surface-chat" .}}
@@ -75,6 +102,10 @@
</div>
</div>
{{if .Footer.Visible}}
<div class="sw-shell__footer">{{.Footer.Text}}</div>
{{end}}
{{if .Banner.Visible}}
<div class="banner banner-bottom active" style="background:{{.Banner.Background}};color:{{.Banner.Color}};height:var(--banner-h);line-height:var(--banner-h);text-align:center;font-size:12px;font-weight:600;overflow:hidden;">
{{.Banner.Text}}
@@ -92,32 +123,9 @@
{{if .Manifest}}window.__MANIFEST__ = {{.Manifest | toJSON}};{{end}}
</script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/sb.js?v={{.Version}}"></script>
{{/* v0.37.13 Scorched Earth III: app-state.js, ui-primitives.js,
user-menu.js, file-tree.js, code-editor.js removed.
Earlier: api.js, ui-core.js, pages.js (v0.37.10),
ui-format.js, virtual-scroll.js, model-selector.js, drag-resize.js (v0.37.12),
chat-pane.js, pane-container.js (v0.37.10). */}}
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/events.js?v={{.Version}}"></script>
{{/* v0.28.5: SDK — composition layer over globals. */}}
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/switchboard-sdk.js?v={{.Version}}"></script>
{{/* ── Universal init: SDK boot for ALL surfaces ── */}}
<script type="module" nonce="{{.CSPNonce}}">
// v0.28.5: SDK handles theme + appearance. Idempotent.
Switchboard.init();
// Universal logout — available on every surface.
// Preact surfaces override with richer versions.
function handleLogout() {
if (!confirm('Sign out?')) return;
if (typeof Events !== 'undefined') { Events.disconnect(); Events.clear(); }
if (typeof API !== 'undefined' && API.logout) API.logout();
location.reload();
}
// Register fallback — app.js re-registers with a richer version.
sb.register('handleLogout', handleLogout);
</script>
{{/* v0.37.14 Scorched Earth IV: sb.js, events.js, switchboard-sdk.js,
workflow-surfaces.js removed. All surfaces use Preact SDK boot().
Survivors: debug.js, repl.js (standalone, no old-layer deps). */}}
{{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}}
{{if eq .Surface "admin"}}{{template "scripts-admin" .}}{{end}}
@@ -136,13 +144,13 @@
<div class="modal modal-wide">
<div class="modal-header">
<h2>Debug Log</h2>
<button class="modal-close" onclick="sb.call('closeModal','debugModal')"></button>
<button class="modal-close" onclick="closeModal('debugModal')"></button>
</div>
<div class="modal-tabs">
<button class="debug-tab active" data-tab="console" onclick="sb.call('switchDebugTab','console')">Console <span id="debugConsoleCount" class="badge">0</span></button>
<button class="debug-tab" data-tab="network" onclick="sb.call('switchDebugTab','network')">Network <span id="debugNetworkCount" class="badge">0</span></button>
<button class="debug-tab" data-tab="state" onclick="sb.call('switchDebugTab','state')">State</button>
<button class="debug-tab" data-tab="repl" onclick="sb.call('switchDebugTab','repl')">REPL</button>
<button class="debug-tab active" data-tab="console" onclick="switchDebugTab('console')">Console <span id="debugConsoleCount" class="badge">0</span></button>
<button class="debug-tab" data-tab="network" onclick="switchDebugTab('network')">Network <span id="debugNetworkCount" class="badge">0</span></button>
<button class="debug-tab" data-tab="state" onclick="switchDebugTab('state')">State</button>
<button class="debug-tab" data-tab="repl" onclick="switchDebugTab('repl')">REPL</button>
</div>
<div class="modal-body" style="padding:0;overflow:hidden;display:flex;flex-direction:column;min-height:0;">
<div id="debugConsoleTab" class="debug-tab-content" style="display:flex;flex-direction:column;flex:1;min-height:0;">
@@ -167,20 +175,19 @@
</div>
<div class="modal-footer" style="display:flex;justify-content:space-between;align-items:center;">
<div style="display:flex;gap:8px;">
<button class="btn-danger btn-small" onclick="sb.call('runDebugDiagnostics')">Diagnostics</button>
<button class="btn-danger btn-small" onclick="sb.call('purgeCache')">Purge Cache</button>
<button class="btn-danger btn-small" onclick="runDebugDiagnostics()">Diagnostics</button>
<button class="btn-danger btn-small" onclick="purgeCache()">Purge Cache</button>
</div>
<div style="display:flex;gap:8px;">
<button class="btn-secondary btn-small" onclick="sb.call('clearDebugLog')">Clear</button>
<button class="btn-secondary btn-small" onclick="sb.call('copyDebugLog')">Copy</button>
<button class="btn-secondary btn-small" onclick="sb.call('exportDebugLog')">Export</button>
<button class="btn-secondary btn-small" onclick="clearDebugLog()">Clear</button>
<button class="btn-secondary btn-small" onclick="copyDebugLog()">Copy</button>
<button class="btn-secondary btn-small" onclick="exportDebugLog()">Export</button>
</div>
</div>
</div>
</div>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/debug.js?v={{.Version}}"></script>
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/js/repl.js?v={{.Version}}"></script>
<div id="toastContainer" class="toast-container"></div>
</body>
</html>
{{end}}

View File

@@ -47,6 +47,7 @@ window.addEventListener('unhandledrejection', function(e) {
{{define "scripts-chat"}}
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/marked.min.js"></script>
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/purify.min.js"></script>
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/codemirror/codemirror.bundle.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}">
// v0.37.10: Preact boot — same pattern as settings/admin/team-admin surfaces.
// Vendor modules: no ?v= query — hooks.module.js does a bare

View File

@@ -13,9 +13,6 @@
{{define "surface-extension"}}
<div id="extension-surface" class="extension-surface"
data-surface-id="{{.Surface}}">
{{/* User menu — standard empty prefix, hydrated by base.html universal init */}}
{{template "user-menu" dict "ID" ""}}
<div id="extension-mount" class="extension-mount"></div>
</div>
{{end}}

View File

@@ -47,6 +47,7 @@ window.addEventListener('unhandledrejection', function(e) {
{{define "scripts-notes"}}
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/marked.min.js"></script>
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/purify.min.js"></script>
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/codemirror/codemirror.bundle.js?v={{$.Version}}"></script>
<script type="module" nonce="{{$.CSPNonce}}">
// v0.37.11: Preact boot — same pattern as chat surface.
const { h, render } = await import('{{$.BasePath}}/js/sw/vendor/preact.module.js');

View File

@@ -593,32 +593,12 @@
});
}
// ── Custom surface (v0.30.2) ──────────
// ── Custom surface (v0.37.14: legacy registry removed) ──────────
if (SURFACE_PKG_ID) {
(async function() {
var mount = document.getElementById('customSurfaceMount');
if (!mount) return;
mount.innerHTML = '<div style="padding:24px;text-align:center;color:var(--text-3)">Loading surface\u2026</div>';
try {
// Load workflow-surfaces registry + dependencies
await loadScript(BASE + '/js/ui-primitives.js');
await loadScript(BASE + '/js/workflow-surfaces.js');
// Load the package's surface JS (registers via WorkflowSurfaces.register())
await loadScript(BASE + '/surfaces/' + SURFACE_PKG_ID + '/js/main.js');
// Mount the custom surface
var ctx = { channelId: CHAN_ID, sessionId: SESSION_ID, basePath: BASE,
stageMode: STAGE_MODE, formTemplate: FORM_TPL,
totalStages: TOTAL_STAGES, currentStage: CURRENT_STAGE };
mount.innerHTML = '';
if (typeof WorkflowSurfaces !== 'undefined') {
WorkflowSurfaces.mount(mount, SURFACE_PKG_ID, ctx);
} else {
mount.innerHTML = '<div style="padding:24px;color:var(--danger)">Failed to load surface registry.</div>';
}
} catch(e) {
mount.innerHTML = '<div style="padding:24px;color:var(--danger)">Failed to load surface: ' + escHtml(e.message) + '</div>';
}
})();
var mount = document.getElementById('customSurfaceMount');
if (mount) {
mount.innerHTML = '<div style="padding:24px;color:var(--danger)">Custom workflow surfaces require an updated package format. The legacy surface registry has been removed.</div>';
}
}
function loadScript(src) {