diff --git a/CHANGELOG.md b/CHANGELOG.md index df3a87a..8288bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,47 @@ All notable changes to Chat Switchboard. +## [0.22.5] — 2026-03-03 + +### Added +- **Go template engine.** Server-side HTML rendering via `html/template` with `//go:embed` for compiled-in templates. Custom `FuncMap` with helpers (`roleFilterType`, `toJSON`, `hasPrefix`, `dict`). CSP nonce generation per request. Replaces monolithic `index.html` + client-side DOM construction with composable, server-rendered surfaces. +- **Surface architecture.** Each page route renders a dedicated surface template that owns its full layout below the classification banner. Surfaces compose from reusable components (`model-select`, `team-select`, `file-upload`) without sharing CSS layout rules. Banner height handled via CSS custom properties (`--banner-top-h`, `--banner-bot-h`, `--surface-h`). +- **Chat surface.** Server-rendered shell at `/` and `/chat/:chatID`. Go template renders banner + layout + script tags; existing JS builds DOM inside the container. Bridge pattern preserves all current chat functionality. +- **Admin surface.** Full admin panel at `/admin/:section` with 5 category tabs (People, AI, Routing, System, Monitoring) and section sidebar. Server-rendered pages for providers, models, teams, users, and settings. Hybrid fallback for JS-loaded sections (groups, presets, knowledge, memory, health, capabilities, extensions, storage, usage, audit, stats). +- **Editor surface.** Server-rendered layout shell at `/editor/:wsId` with proper CSS height ownership. `mountServerRendered()` method on `EditorMode` bypasses the Surfaces registry for Go template mode. Dedicated CSS owns full viewport below banners — no layout collision with other surfaces. **(Fixes bugs #4, #5: editor layout collapse)** +- **Notes surface.** Server-rendered layout at `/notes/:noteId` with notes-main + notes-assist split. Boot script calls `openNotes()` in standalone mode. Responsive: assist pane hidden on mobile. +- **Settings surface.** Full-page settings at `/settings/:section` replacing modal-based settings. Left nav with all sections (general, appearance, providers, models, presets, roles, knowledge, memory, notifications, usage). General and Appearance sections server-rendered with form controls; dynamic sections populated by existing JS. +- **Login page.** Standalone Go template at `/login` with cookie-based auth. Sets `redirect_after_login` cookie for post-login navigation. +- **`AuthOrRedirect` middleware.** Cookie-based JWT validation for page routes. Reads `sb_token` cookie, redirects to `/login` on missing/invalid token (instead of 401 JSON). `RequireAdminPage()` helper aborts with 403 for non-admin users. Skips auth entirely in unmanaged mode. +- **Cookie-based auth sync.** `api.js` `saveTokens()` now sets `sb_token` cookie alongside localStorage on every token save/refresh. `clearTokens()` clears the cookie. Bridges API auth (Bearer header) with page auth (cookie). +- **Reusable `model-select` component.** Go template partial renders ` + + + + + {{if .Data.Models}} + + + + + + + + + + + + {{range .Data.Models}} + + + + + + + + {{end}} + +
ModelProviderTypeCapabilities
+ {{.DisplayName}} + {{if ne .DisplayName .ModelID}}
{{.ModelID}}{{end}} +
{{.ProviderName}}{{.ModelType}}
+ {{else}} +
No models in catalog. Sync a provider to populate.
+ {{end}} + +{{end}} diff --git a/server/pages/templates/admin/overview.html b/server/pages/templates/admin/overview.html new file mode 100644 index 0000000..b7f05d2 --- /dev/null +++ b/server/pages/templates/admin/overview.html @@ -0,0 +1,6 @@ +{{define "admin-overview"}} +
+

Admin Overview

+

System administration. Select a section from the navigation.

+
+{{end}} diff --git a/server/pages/templates/admin/providers.html b/server/pages/templates/admin/providers.html new file mode 100644 index 0000000..ae3e08a --- /dev/null +++ b/server/pages/templates/admin/providers.html @@ -0,0 +1,97 @@ +{{/* Admin providers — server-rendered provider list with status. */}} + +{{define "admin-providers"}} +
+

Providers

+

AI provider configurations. Each provider connects to an API endpoint.

+ +
+ +
+ + + + {{if .Data.Providers}} + + + + + + + + + + + + + {{range .Data.ProviderDetails}} + + + + + + + + + {{end}} + +
NameTypeScopeModelsEndpoint
{{.Name}}{{.Provider}}{{.Scope}}{{.ModelCount}}{{.Endpoint}} + + +
+ {{else}} +
No providers configured. Add one to get started.
+ {{end}} +
+{{end}} + +{{define "admin-provider-form"}} +
+

Add Provider

+ +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + {{template "team-select" (dict "FieldName" "providerFormTeamId" "Label" "" "Teams" .Data.Teams "Selected" "" "AllowEmpty" false)}} +
+
+
+ + +
+
+{{end}} diff --git a/server/pages/templates/admin/roles.html b/server/pages/templates/admin/roles.html new file mode 100644 index 0000000..e536764 --- /dev/null +++ b/server/pages/templates/admin/roles.html @@ -0,0 +1,94 @@ +{{/* + admin/roles.html — Model Roles configuration page. + Fixes bug #1: embedding models now appear via model_type filtering. +*/}} +{{define "admin-roles"}} +
+

Model Roles

+

Configure which models serve background tasks. Each role has a primary and optional fallback.

+ + {{$providers := .Data.Providers}} + {{$models := .Data.Models}} + + {{range .Data.Roles}} +
+

{{.Name}} Role

+

+ {{if eq .Name "utility"}}Summarization, classification, routing — typically a fast, cheap chat model.{{end}} + {{if eq .Name "embedding"}}Vector generation for knowledge base search and semantic note search.{{end}} +

+ + {{$roleName := .Name}} + {{$filterType := roleFilterType .Name}} + {{$primaryProvID := ""}} + {{$primaryModelID := ""}} + {{$fallbackProvID := ""}} + {{$fallbackModelID := ""}} + {{if .Primary}} + {{$primaryProvID = .Primary.ProviderID}} + {{$primaryModelID = .Primary.ModelID}} + {{end}} + {{if .Fallback}} + {{$fallbackProvID = .Fallback.ProviderID}} + {{$fallbackModelID = .Fallback.ModelID}} + {{end}} + +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+ + +
+
+ {{end}} +
+{{end}} diff --git a/server/pages/templates/admin/routing.html b/server/pages/templates/admin/routing.html new file mode 100644 index 0000000..2939217 --- /dev/null +++ b/server/pages/templates/admin/routing.html @@ -0,0 +1,80 @@ +{{/* + admin/routing.html — Routing Policy management. + Fixes bug #2: team scope uses a proper dropdown, not raw text. +*/}} +{{define "admin-routing"}} +
+

Routing Policies

+

Define rules for model selection, cost limits, and team-specific routing.

+ +
+ +
+ + + +
+ {{if not .Data.Policies}} +
No routing policies configured.
+ {{end}} +
+
+{{end}} diff --git a/server/pages/templates/admin/settings.html b/server/pages/templates/admin/settings.html new file mode 100644 index 0000000..a849503 --- /dev/null +++ b/server/pages/templates/admin/settings.html @@ -0,0 +1,83 @@ +{{/* Admin settings — core system configuration. */}} + +{{define "admin-settings"}} +
+

Settings

+

Core system configuration. Changes take effect immediately.

+ + {{/* ── Registration ───────────────────── */}} +
+

Registration

+
+ +
+
+ + +
+
+ + {{/* ── User Permissions ─────────────────── */}} +
+

User Permissions

+
+ +
+
+ +
+
+ +
+
+ + {{/* ── Banner ─────────────────────────── */}} +
+

Environment Banner

+
+ +
+ +
+ + {{/* ── System Prompt ──────────────────── */}} +
+

Default System Prompt

+
+ +
+
+ +
+ +
+
+{{end}} diff --git a/server/pages/templates/admin/teams.html b/server/pages/templates/admin/teams.html new file mode 100644 index 0000000..21e0f25 --- /dev/null +++ b/server/pages/templates/admin/teams.html @@ -0,0 +1,62 @@ +{{/* Admin teams — team list with member counts. */}} + +{{define "admin-teams"}} +
+

Teams

+

Organize users into teams for scoped access to providers, presets, and policies.

+ +
+ +
+ + + + {{if .Data.Teams}} + + + + + + + + + + + + {{range .Data.Teams}} + + + + + + + + {{end}} + +
NameDescriptionMembersStatus
{{.Name}}{{.Description}}{{.MemberCount}}{{if .IsActive}}active{{else}}inactive{{end}} + +
+ {{else}} +
No teams created yet.
+ {{end}} +
+{{end}} diff --git a/server/pages/templates/admin/users.html b/server/pages/templates/admin/users.html new file mode 100644 index 0000000..fa3b960 --- /dev/null +++ b/server/pages/templates/admin/users.html @@ -0,0 +1,48 @@ +{{/* Admin users — user management table. */}} + +{{define "admin-users"}} +
+

Users

+

Manage user accounts, roles, and access.

+ +
+ +
+ + {{if .Data.Users}} + + + + + + + + + + + + + {{range .Data.Users}} + + + + + + + + + {{end}} + +
UsernameEmailDisplay NameRoleStatus
{{.Username}}{{.Email}}{{.DisplayName}}{{.Role}}{{if .IsActive}}active{{else}}inactive{{end}} + + {{if .IsActive}} + + {{else}} + + {{end}} +
+ {{else}} +
No users found.
+ {{end}} +
+{{end}} diff --git a/server/pages/templates/base.html b/server/pages/templates/base.html new file mode 100644 index 0000000..20f8381 --- /dev/null +++ b/server/pages/templates/base.html @@ -0,0 +1,72 @@ +{{define "base.html"}} + + + + + + {{block "title" .}}Chat Switchboard{{end}} + + + + {{if eq .Surface "chat"}}{{template "css-chat" .}}{{end}} + {{if eq .Surface "admin"}}{{template "css-admin" .}}{{end}} + {{if eq .Surface "editor"}}{{template "css-editor" .}}{{end}} + {{if eq .Surface "notes"}}{{template "css-notes" .}}{{end}} + {{if eq .Surface "settings"}}{{template "css-settings" .}}{{end}} + + + + + {{if .Banner.Visible}} + + {{end}} + +
+ {{if eq .Surface "chat"}}{{template "surface-chat" .}} + {{else if eq .Surface "admin"}}{{template "surface-admin" .}} + {{else if eq .Surface "editor"}}{{template "surface-editor" .}} + {{else if eq .Surface "notes"}}{{template "surface-notes" .}} + {{else if eq .Surface "settings"}}{{template "surface-settings" .}} + {{else}}
Unknown surface: {{.Surface}}
+ {{end}} +
+ + {{if .Banner.Visible}} + + {{end}} + + + + + + + + + {{if eq .Surface "chat"}}{{template "scripts-chat" .}}{{end}} + {{if eq .Surface "admin"}}{{template "scripts-admin" .}}{{end}} + {{if eq .Surface "editor"}}{{template "scripts-editor" .}}{{end}} + {{if eq .Surface "notes"}}{{template "scripts-notes" .}}{{end}} + {{if eq .Surface "settings"}}{{template "scripts-settings" .}}{{end}} + + +{{end}} diff --git a/server/pages/templates/components/file-upload.html b/server/pages/templates/components/file-upload.html new file mode 100644 index 0000000..61ab8af --- /dev/null +++ b/server/pages/templates/components/file-upload.html @@ -0,0 +1,29 @@ +{{/* + Reusable file upload component (v0.23.0). + Renders a drop zone + file picker button. + + Expects dict with: + FieldName — unique ID for the input (e.g. "workspace-upload") + Label — optional label text + Multiple — bool, allow multiple files + Accept — optional file type filter (e.g. ".md,.txt,.go") + DropZone — bool, show drag-and-drop zone +*/}} +{{define "file-upload"}} +
+ {{if .Label}}{{end}} + + + {{if .DropZone}} +
+ Drop files here or click above +
+ {{end}} +
+{{end}} diff --git a/server/pages/templates/components/model-select.html b/server/pages/templates/components/model-select.html new file mode 100644 index 0000000..da8e9d7 --- /dev/null +++ b/server/pages/templates/components/model-select.html @@ -0,0 +1,36 @@ +{{/* + model-select: Reusable provider + model dropdown pair. + Used in admin roles page with FilterType for model_type filtering. + When provider changes, Pages.roleProviderChanged() filters models + from __PAGE_DATA__.Models on the client side. +*/}} +{{define "model-select"}} +
+ {{if index . "Label"}}{{end}} +
+ + +
+
+{{end}} diff --git a/server/pages/templates/components/team-select.html b/server/pages/templates/components/team-select.html new file mode 100644 index 0000000..a4121b1 --- /dev/null +++ b/server/pages/templates/components/team-select.html @@ -0,0 +1,18 @@ +{{/* + team-select: Reusable team dropdown. + Pre-populated from server data — no client-side fetch needed. +*/}} +{{define "team-select"}} +
+ {{if index . "Label"}}{{end}} + +
+{{end}} diff --git a/server/pages/templates/login.html b/server/pages/templates/login.html new file mode 100644 index 0000000..714eded --- /dev/null +++ b/server/pages/templates/login.html @@ -0,0 +1,72 @@ +{{define "login.html"}} + + + + + + Login — Chat Switchboard + + + + + + {{if .Banner.Visible}} + + {{end}} + +
+
+
+ +

Chat Switchboard

+
+ + + +
+ + +
+
+ + +
+ +
+
+ + {{if .Banner.Visible}} + + {{end}} + + + + + + +{{end}} diff --git a/server/pages/templates/surfaces/admin.html b/server/pages/templates/surfaces/admin.html new file mode 100644 index 0000000..4f2ec65 --- /dev/null +++ b/server/pages/templates/surfaces/admin.html @@ -0,0 +1,193 @@ +{{/* Admin surface — full admin panel with category/section navigation. + Categories and sections mirror the SPA's ADMIN_SECTIONS structure. + Server-rendered sections: roles, routing, providers, models, teams, users, settings. + Hybrid sections: container div + JS loader fills content. */}} + +{{define "surface-admin"}} +
+ {{/* ── Category bar (top) ───────────────── */}} +
+ {{$cat := .Data.Category}} + + + + + +
+ ← Chat +
+ +
+ {{/* ── Section sidebar ──────────────── */}} + + + {{/* ── Content area ─────────────────── */}} +
+ {{if eq .Section "roles"}}{{template "admin-roles" .}} + {{else if eq .Section "routing"}}{{template "admin-routing" .}} + {{else if eq .Section "providers"}}{{template "admin-providers" .}} + {{else if eq .Section "models"}}{{template "admin-models" .}} + {{else if eq .Section "teams"}}{{template "admin-teams" .}} + {{else if eq .Section "users"}}{{template "admin-users" .}} + {{else if eq .Section "settings"}}{{template "admin-settings" .}} + {{else}} + {{/* Hybrid: container for JS-loaded sections */}} +
+
Loading {{.Section}}…
+
+ {{end}} +
+
+
+{{end}} + +{{define "css-admin"}} + +{{end}} + +{{define "scripts-admin"}} + +{{end}} diff --git a/server/pages/templates/surfaces/chat.html b/server/pages/templates/surfaces/chat.html new file mode 100644 index 0000000..03e9519 --- /dev/null +++ b/server/pages/templates/surfaces/chat.html @@ -0,0 +1,57 @@ +{{/* + Chat surface (Phase 1 bridge). + Server renders the page shell. Existing SPA JS takes over the DOM. + All scripts from index.html are loaded here — minus the three + already in base.html (api.js, events.js, ui-primitives.js). +*/}} + +{{define "surface-chat"}} + +{{end}} + +{{define "css-chat"}} + + + + + + + +{{end}} + +{{define "scripts-chat"}} +{{/* Vendor libs */}} + + + +{{/* App JS — order matches index.html (minus api/events/ui-primitives already in base) */}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/server/pages/templates/surfaces/editor.html b/server/pages/templates/surfaces/editor.html new file mode 100644 index 0000000..0eb5c39 --- /dev/null +++ b/server/pages/templates/surfaces/editor.html @@ -0,0 +1,92 @@ +{{/* + Editor surface (Phase 2b). + Server renders the layout shell with proper CSS sizing. + editor-mode.js mounts CodeMirror, file tree, etc. into these containers. + Fixes bugs #4 and #5: layout collapse when switching surfaces. +*/}} + +{{define "surface-editor"}} +
+ {{/* Header — editor-mode.js populates this */}} +
+ +
+ {{/* Editor main area — left pane (tabs + code) + split handle + right pane (chat) */}} +
+
+
+{{end}} + +{{define "css-editor"}} + + + + + + + +{{end}} + +{{define "scripts-editor"}} +{{/* Chat rendering for the assist pane */}} + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/server/pages/templates/surfaces/notes.html b/server/pages/templates/surfaces/notes.html new file mode 100644 index 0000000..3fc3a9d --- /dev/null +++ b/server/pages/templates/surfaces/notes.html @@ -0,0 +1,93 @@ +{{/* + Notes surface (Phase 2b). + Server renders the layout shell. Existing notes.js + note-graph.js + handle CRUD and rendering inside the containers. +*/}} + +{{define "surface-notes"}} +
+
+ {{/* Notes list + editor */}} +
+ {{/* Chat assist pane (optional, can toggle) */}} +
+
+
+{{end}} + +{{define "css-notes"}} + + + + + + + +{{end}} + +{{define "scripts-notes"}} + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/server/pages/templates/surfaces/settings.html b/server/pages/templates/surfaces/settings.html new file mode 100644 index 0000000..62927db --- /dev/null +++ b/server/pages/templates/surfaces/settings.html @@ -0,0 +1,249 @@ +{{/* + Settings surface (Phase 2b). + Server renders a full-page settings layout (replaces the modal). + Reuses existing settings-handlers.js + ui-settings.js for interactions. +*/}} + +{{define "surface-settings"}} +
+
+
Settings
+ {{$section := .Section}} + General + Appearance + My Providers + My Models + My Presets + Model Roles + Knowledge Bases + Memory + Notifications + Usage +
+ ← Back to Chat +
+
+ {{/* Content rendered by section-specific JS or server template */}} +
+ {{if eq .Section "general"}}{{template "settings-general" .}} + {{else if eq .Section "appearance"}}{{template "settings-appearance" .}} + {{else}}
Loading…
+ {{end}} +
+
+
+{{end}} + +{{define "settings-general"}} +
+

Profile

+
+ + +
+
+ + +
+ +
+ +
+

Password

+
+ + +
+
+ + +
+
+ + +
+ +
+{{end}} + +{{define "settings-appearance"}} +
+

Theme

+
+ + +
+
+
+

Editor Keymap

+
+ + + +
+
+
+

UI Scale

+
+ + 100% +
+

Message Font Size

+
+ + 14px +
+
+{{end}} + +{{define "css-settings"}} + +{{end}} + +{{define "scripts-settings"}} + + + + + + + + + + + + +{{end}} diff --git a/src/css/editor-mode.css b/src/css/editor-mode.css index 1be7710..f5ba791 100644 --- a/src/css/editor-mode.css +++ b/src/css/editor-mode.css @@ -64,6 +64,7 @@ } .editor-tree-add:hover { background: var(--hover); color: var(--accent); } .editor-tree-content { flex: 1; overflow-y: auto; padding: 4px 0; } +.editor-file-tree.drag-over { outline: 2px dashed var(--accent, #5865f2); outline-offset: -2px; background: rgba(88,101,242,0.05); } .editor-tree-row { display: flex; align-items: center; gap: 4px; padding: 3px 8px; cursor: pointer; diff --git a/src/js/api.js b/src/js/api.js index 88b6664..46a2e23 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -39,6 +39,12 @@ const API = { refreshToken: this.refreshToken, user: this.user })); + // Sync to cookie for Go template page auth (v0.22.5) + if (this.accessToken) { + document.cookie = `sb_token=${this.accessToken}; path=/; max-age=900; SameSite=Strict`; + } else { + document.cookie = 'sb_token=; path=/; max-age=0'; + } }, clearTokens() { @@ -46,6 +52,7 @@ const API = { this.refreshToken = null; this.user = null; localStorage.removeItem(_storageKey); + document.cookie = 'sb_token=; path=/; max-age=0'; // clear page auth cookie if (this._refreshTimer) { clearTimeout(this._refreshTimer); this._refreshTimer = null; } }, @@ -255,6 +262,32 @@ const API = { mkdirWorkspace(wsId, path) { return this._post(`/api/v1/workspaces/${wsId}/files/mkdir`, { path }); }, + + // Upload a File object to a workspace (binary-safe, v0.23.0) + async uploadWorkspaceFile(wsId, file, destPath) { + const path = destPath || file.name; + const url = BASE + `/api/v1/workspaces/${wsId}/files/write?path=${encodeURIComponent(path)}`; + const doFetch = () => fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': file.type || 'application/octet-stream', + 'Content-Length': file.size.toString(), + 'Authorization': `Bearer ${this.accessToken}`, + }, + body: file, + }); + let resp = await doFetch(); + if (resp.status === 401 && this.refreshToken) { + await this._refreshAccessToken(); + resp = await doFetch(); + } + if (!resp.ok) { + const data = await resp.json().catch(() => ({})); + throw new Error(data.error || `HTTP ${resp.status}`); + } + return resp.json(); + }, + getWorkspaceGitStatus(wsId) { return this._get(`/api/v1/workspaces/${wsId}/git/status`); }, getWorkspaceGitBranches(wsId) { return this._get(`/api/v1/workspaces/${wsId}/git/branches`); }, listWorkspaces() { return this._get('/api/v1/workspaces'); }, diff --git a/src/js/editor-mode.js b/src/js/editor-mode.js index 6680acc..d984258 100644 --- a/src/js/editor-mode.js +++ b/src/js/editor-mode.js @@ -175,6 +175,43 @@ const EditorMode = { }).catch(() => {}); }, + /** + * Mount into server-rendered editor surface template containers. + * Called by the