Feat v0.2.5 ui polish dead code (#9)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #9.
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
//
|
||||
// templates/base.html — outer shell (banner + surface block + scripts)
|
||||
// templates/login.html — standalone login page
|
||||
// templates/components/*.html — reusable partials (model-select, team-select, chat-pane, etc.)
|
||||
// templates/surfaces/*.html — one per surface (chat, editor, notes, admin, etc.)
|
||||
// templates/components/*.html — reusable partials (model-select, team-select, etc.)
|
||||
// templates/surfaces/*.html — one per surface (admin, settings, team-admin, extension)
|
||||
package pages
|
||||
|
||||
import (
|
||||
@@ -46,12 +46,12 @@ type Engine struct {
|
||||
// registered in Go at startup. Extension surfaces will be registered from
|
||||
// manifest files (future).
|
||||
type SurfaceManifest struct {
|
||||
ID string `json:"id"` // unique identifier: "chat", "editor", "my-dashboard"
|
||||
Route string `json:"route"` // primary URL pattern: "/", "/editor/:wsId"
|
||||
AltRoutes []string `json:"alt_routes"` // additional URL patterns: ["/chat/:chatID"]
|
||||
Title string `json:"title"` // human-readable: "Chat", "Editor"
|
||||
Template string `json:"template"` // Go template name: "surface-chat", "surface-editor"
|
||||
Components []string `json:"components"` // component IDs used: ["chat-pane", "file-tree"]
|
||||
ID string `json:"id"` // unique identifier: "admin", "settings", "my-dashboard"
|
||||
Route string `json:"route"` // primary URL pattern: "/", "/admin"
|
||||
AltRoutes []string `json:"alt_routes"` // additional URL patterns
|
||||
Title string `json:"title"` // human-readable: "Admin", "Settings"
|
||||
Template string `json:"template"` // Go template name: "surface-admin", "surface-extension"
|
||||
Components []string `json:"components"` // component IDs used: ["file-tree"]
|
||||
DataRequires []string `json:"data_requires"` // data loader keys: ["workspace", "models"]
|
||||
Scripts []string `json:"scripts"` // JS files (surface-specific, beyond base.html common)
|
||||
Styles []string `json:"styles"` // CSS files (surface-specific)
|
||||
@@ -244,6 +244,11 @@ func (e *Engine) registerCoreSurfaces() {
|
||||
Title: "Workflow Landing", Template: "workflow-landing", Auth: "public",
|
||||
Layout: "single", Source: "core",
|
||||
},
|
||||
{
|
||||
ID: "welcome", Route: "/welcome",
|
||||
Title: "Welcome", Template: "surface-welcome", Auth: "authenticated",
|
||||
Layout: "single", Source: "core",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,7 +438,7 @@ func (e *Engine) RenderExtensionSurface() gin.HandlerFunc {
|
||||
// PageRouteMiddleware holds middleware handlers for each auth level.
|
||||
// Passed to RegisterPageRoutes by main.go.
|
||||
type PageRouteMiddleware struct {
|
||||
Authenticated gin.HandlerFunc // AuthOrRedirect — for chat, editor, notes, settings
|
||||
Authenticated gin.HandlerFunc // AuthOrRedirect — for authenticated surfaces
|
||||
Admin []gin.HandlerFunc // AuthOrRedirect + RequireAdminPage
|
||||
Session gin.HandlerFunc // AuthOrSession — for workflow
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"switchboard-core/middleware"
|
||||
)
|
||||
|
||||
// SeedSurfaces writes core surface manifests to the registry table.
|
||||
@@ -48,11 +50,10 @@ func (e *Engine) SeedSurfaces() {
|
||||
}
|
||||
|
||||
// IsSurfaceEnabled checks if a surface is enabled in the registry.
|
||||
// Chat and Admin are always enabled (system-critical).
|
||||
// Admin is always enabled (system-critical).
|
||||
// Returns true if the surface is not found (fail-open for backward compat).
|
||||
func (e *Engine) IsSurfaceEnabled(surfaceID string) bool {
|
||||
// Chat and Admin cannot be disabled — they're system-critical
|
||||
if surfaceID == "chat" || surfaceID == "admin" {
|
||||
if surfaceID == "admin" {
|
||||
return true
|
||||
}
|
||||
if e.stores.Packages == nil {
|
||||
@@ -92,22 +93,39 @@ func (e *Engine) EnabledSurfaceIDs() []string {
|
||||
|
||||
// DefaultSurfaceRedirect returns a handler for GET / that redirects to the
|
||||
// configured default surface, falling back to the first enabled extension
|
||||
// surface, then /admin.
|
||||
// surface, then /welcome.
|
||||
//
|
||||
// This route has no auth middleware, so we opportunistically read the JWT
|
||||
// from the cookie to check user preferences. If the cookie is missing or
|
||||
// invalid, user preference is skipped (falls through to global default).
|
||||
func (e *Engine) DefaultSurfaceRedirect() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
target := e.resolveDefaultSurface(c.Request.Context())
|
||||
userID := middleware.UserIDFromCookie(c, e.cfg.JWTSecret)
|
||||
target := e.resolveDefaultSurface(c.Request.Context(), userID)
|
||||
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+target)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveDefaultSurface returns the path to redirect to (without BasePath).
|
||||
// Priority: configured default_surface → first enabled extension surface → /admin.
|
||||
func (e *Engine) resolveDefaultSurface(ctx context.Context) string {
|
||||
// Priority: user preference → global default_surface → first enabled extension → /welcome.
|
||||
func (e *Engine) resolveDefaultSurface(ctx context.Context, userID string) string {
|
||||
if e.stores.GlobalConfig == nil || e.stores.Packages == nil {
|
||||
return "/admin"
|
||||
return "/welcome"
|
||||
}
|
||||
|
||||
// 1. Check configured default_surface
|
||||
// 1. Check user preference (default_surface in user settings)
|
||||
if userID != "" && e.stores.Users != nil {
|
||||
if user, err := e.stores.Users.GetByID(ctx, userID); err == nil && user != nil {
|
||||
if id, _ := user.Settings["default_surface"].(string); id != "" {
|
||||
if path := e.surfacePath(ctx, id); path != "" {
|
||||
return path
|
||||
}
|
||||
// User's chosen surface is missing or disabled — fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check admin-configured global default_surface
|
||||
if raw, err := e.stores.GlobalConfig.Get(ctx, "default_surface"); err == nil && raw != nil {
|
||||
if id, ok := raw["id"].(string); ok && id != "" {
|
||||
if path := e.surfacePath(ctx, id); path != "" {
|
||||
@@ -117,21 +135,24 @@ func (e *Engine) resolveDefaultSurface(ctx context.Context) string {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. First enabled extension surface
|
||||
surfaces, err := e.stores.Packages.ListEnabledByType(ctx, "surface")
|
||||
// 3. First enabled extension surface (type "surface" or "full")
|
||||
allPkgs, err := e.stores.Packages.List(ctx)
|
||||
if err == nil {
|
||||
for _, s := range surfaces {
|
||||
if s.Source == "core" {
|
||||
for _, s := range allPkgs {
|
||||
if s.Source == "core" || s.Source == "builtin" {
|
||||
continue
|
||||
}
|
||||
if s.Enabled {
|
||||
if !s.Enabled {
|
||||
continue
|
||||
}
|
||||
if s.Type == "surface" || s.Type == "full" {
|
||||
return "/s/" + s.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback
|
||||
return "/admin"
|
||||
// 4. Fallback — welcome surface (no extensions installed)
|
||||
return "/welcome"
|
||||
}
|
||||
|
||||
// surfacePath returns the URL path for a surface ID, or "" if the surface
|
||||
@@ -156,9 +177,8 @@ func (e *Engine) surfacePath(ctx context.Context, id string) string {
|
||||
return "/s/" + id
|
||||
}
|
||||
|
||||
// disabledRedirect returns a handler that redirects to /admin.
|
||||
// Uses /admin (not /) to avoid a redirect loop when the default surface
|
||||
// is the one being disabled.
|
||||
// disabledRedirect returns a handler that redirects to /.
|
||||
// The DefaultSurfaceRedirect handler will resolve to the appropriate surface.
|
||||
func (e *Engine) disabledRedirect() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/admin")
|
||||
|
||||
@@ -12,13 +12,8 @@
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/layout.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/primitives.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/modals.css?v={{.Version}}">
|
||||
{{/* chat.css removed in v0.37.10 — replaced by sw-chat-surface.css + sw-chat-pane.css */}}
|
||||
{{/* panels.css, pane-container.css, chat-pane.css removed in v0.37.10 */}}
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/surfaces.css?v={{.Version}}">
|
||||
{{/* splash.css removed in v0.37.12 — login is Preact with sw-login.css */}}
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/sw-primitives.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/sw-chat-pane.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/sw-notes-pane.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/user-menu.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/tool-grants.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/workflow.css?v={{.Version}}">
|
||||
@@ -94,6 +89,7 @@
|
||||
{{if eq .Surface "admin"}}{{template "surface-admin" .}}
|
||||
{{else if eq .Surface "team-admin"}}{{template "surface-team-admin" .}}
|
||||
{{else if eq .Surface "settings"}}{{template "surface-settings" .}}
|
||||
{{else if eq .Surface "welcome"}}{{template "surface-welcome" .}}
|
||||
{{else if and .Manifest (eq .Manifest.Source "extension")}}{{template "surface-extension" .}}
|
||||
{{else}}<div style="padding:20px">Unknown surface: {{.Surface}}</div>
|
||||
{{end}}
|
||||
@@ -128,6 +124,7 @@
|
||||
{{if eq .Surface "admin"}}{{template "scripts-admin" .}}{{end}}
|
||||
{{if eq .Surface "team-admin"}}{{template "scripts-team-admin" .}}{{end}}
|
||||
{{if eq .Surface "settings"}}{{template "scripts-settings" .}}{{end}}
|
||||
{{if eq .Surface "welcome"}}{{template "scripts-welcome" .}}{{end}}
|
||||
{{/* v0.27.0: Extension surface JS — loaded from /surfaces/{id}/js/main.js */}}
|
||||
{{if and .Manifest (eq .Manifest.Source "extension")}}
|
||||
<script type="module" nonce="{{.CSPNonce}}" src="{{.BasePath}}/surfaces/{{.Surface}}/js/main.js?v={{.Version}}"></script>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{{/*
|
||||
Chat Pane Component - reusable chat scaffold.
|
||||
Usage: {{template "chat-pane" dict "ID" "main"}}
|
||||
Creates mount points: {ID}ChatMessages, {ID}ChatInput, {ID}SendBtn, {ID}ModelSel
|
||||
ChatPane.create() in chat-pane.js binds to these IDs.
|
||||
|
||||
The header bar ({ID}ChatHeader) is hidden by default.
|
||||
Standalone panes (editor assist) show it for chat switching + model selection.
|
||||
*/}}
|
||||
{{define "chat-pane"}}
|
||||
<div class="chat-pane" id="{{.ID}}ChatPane">
|
||||
<div class="chat-pane-header" id="{{.ID}}ChatHeader" style="display:none;">
|
||||
<div class="chat-pane-header-left">
|
||||
<select class="chat-pane-chat-select" id="{{.ID}}ChatSelect" title="Switch chat">
|
||||
<option value="">New conversation</option>
|
||||
</select>
|
||||
<button class="chat-pane-new-btn" id="{{.ID}}ChatNewBtn" title="New chat">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chat-pane-header-right">
|
||||
<div class="chat-pane-model-sel" id="{{.ID}}ModelSel"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-pane-messages" id="{{.ID}}ChatMessages"></div>
|
||||
<div class="chat-pane-input-bar">
|
||||
<div class="chat-pane-input-wrap">
|
||||
<div class="chat-pane-input" id="{{.ID}}ChatInput"></div>
|
||||
<button class="chat-pane-send" id="{{.ID}}SendBtn" title="Send">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chat-pane-toolbar" id="{{.ID}}Toolbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
25
server/pages/templates/surfaces/welcome.html
Normal file
25
server/pages/templates/surfaces/welcome.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{{/*
|
||||
Welcome surface — shown when no extension surfaces are installed.
|
||||
Renders topbar + welcome card with getting-started info.
|
||||
*/}}
|
||||
|
||||
{{define "surface-welcome"}}
|
||||
<div id="welcome-mount" style="display:flex;flex-direction:column;height:100%;"></div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts-welcome"}}
|
||||
<script type="module" nonce="{{.CSPNonce}}">
|
||||
const { h, render } = await import('{{.BasePath}}/js/sw/vendor/preact.module.js');
|
||||
const hooksModule = await import('{{.BasePath}}/js/sw/vendor/hooks.module.js');
|
||||
const { default: htm } = await import('{{.BasePath}}/js/sw/vendor/htm.module.js');
|
||||
const html = htm.bind(h);
|
||||
window.preact = window.preact || { h, render };
|
||||
window.hooks = window.hooks || hooksModule;
|
||||
window.html = window.html || html;
|
||||
|
||||
const { boot } = await import('{{.BasePath}}/js/sw/sdk/index.js?v={{.Version}}');
|
||||
await boot();
|
||||
|
||||
await import('{{.BasePath}}/js/sw/surfaces/welcome/index.js?v={{.Version}}');
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -9,7 +9,6 @@
|
||||
<link rel="icon" type="image/x-icon" href="{{.BasePath}}/favicon.ico?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/variables.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/primitives.css?v={{.Version}}">
|
||||
<link rel="stylesheet" href="{{.BasePath}}/css/chat.css?v={{.Version}}">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: var(--font); background: var(--bg); color: var(--text); }
|
||||
|
||||
Reference in New Issue
Block a user