Changeset 0.37.14 (#226)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">×</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}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user