diff --git a/CHANGELOG.md b/CHANGELOG.md index 822b28c..22e7657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,61 @@ All notable changes to Switchboard Core are documented here. +## [Unreleased] — v0.2.5 + +### Added + +- **Welcome surface**: New core surface shown as fallback. Detects whether + extensions exist (shows "Set Default Surface") vs truly empty install + (shows "Go to Packages"). Topbar + UserMenu included. +- **User default surface**: Users can set a personal landing page in + Settings > General, overriding the admin-configured global default. +- **UserMenu in admin topbar**: Replaced Back button with UserMenu, + providing consistent surface navigation and eliminating infinite loops. +- **Package manifest icons**: Added emoji icons to hello-dashboard, + icd-test-runner, sdk-test-runner, and team-activity-log manifests. +- **PoC documentation**: README.md for tasks and schedules packages + documenting Proof of Concept status and graduation criteria. + +### Changed + +- **Default surface resolution**: Priority chain is now + user preference → global config → first extension → `/welcome`. + User preference read from JWT cookie on unauthenticated `/` route. + Correctly resolves `type: "full"` extension packages (not just `type: "surface"`). +- **User settings General section**: Replaced dead chat fields (Default + Model, System Prompt, Max Tokens, Temperature, Show Thinking) with + a Default Surface dropdown. +- **Admin settings**: Removed dead chat sections (System Prompt, Default + Model, Policies, Web Search, Auto-Compaction, Memory Extraction). +- **Roadmap restructured**: Workflows → v0.3.x (includes team roles), + Notes → v0.4.0, MVP → v0.5.0. Added settings audit milestones + (v0.2.7 admin, v0.2.8 user, v0.2.9 team-admin pass 1). +- Updated bus doc examples and test labels from `chat.*` to `workflow.*`. +- Renamed `channel-` prefixed test paths to `test-` in storage tests. +- Updated doc comments throughout to remove references to gutted surfaces. + +### Removed + +- **~500 lines of dead CSS**: Orphaned classes for gutted chat, channel, + project, notes, editor-chat, sidebar, and router-picker features from + `layout.css` and `surfaces.css`. +- **Dead Go types**: `Grant` struct (persona-era), `CompositeModelKey` func, + comment-only stubs for NoteGraph, ProjectChannel, etc. +- **Dead event code**: `chat.typing.*` / `channel.typing.*` condition in WS + subscriber, empty Chat/Channel event route table sections. +- **Dead test helpers**: `seed_helpers.go` (SeedTestMessage, SeedTestMessages, + SeedTestCursor — all callerless). +- **Stale template refs**: CSS link tags for non-existent `sw-chat-pane.css`, + `sw-notes-pane.css`, `chat.css`. Orphaned `chat-pane.html` component. +- **Vestigial guards**: `"chat"` surface checks in `IsSurfaceEnabled()` and + `DisablePackage()` (chat is no longer a surface). +- **Dead settings UI**: Chat defaults from user settings, System Prompt / + Default Model / Policies / Web Search / Compaction / Memory from admin. +- Unused `fmt.Sprintf` references in team stores. + +--- + ## [Unreleased] — v0.2.4 ### Added diff --git a/ROADMAP.md b/ROADMAP.md index bca20f7..77707d8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -81,20 +81,56 @@ SDK stabilization, and the first rebuilt extension (tasks). | Step | Status | Description | |------|--------|-------------| -| UI bug pass | ⬚ | Systematic review of all surfaces (admin, settings, dashboard, git-board, tasks, schedules). Fix broken CSS, missing states, responsive issues, dark/light theme gaps. | -| Dead code sweep | ⬚ | Audit for Phase 0 remnants: orphaned CSS classes, stale imports, references to gutted chat/notes/projects in templates and route registrations, unused Go handlers/stores. | -| Template cleanup | ⬚ | Remove stale CSS link tags (sw-chat-pane.css, sw-notes-pane.css etc.) from HTML templates. Clean up base.html surface wiring. | -| Package proof-of-concept status | ⬚ | Mark tasks + schedules packages as PoC (not permanent). Document what "permanent" means and the graduation criteria for extensions shipping with core. | +| UI bug pass | ✅ | Reviewed admin, settings, login, welcome surfaces in light/dark themes. Fixed settings crash (models API undefined). Removed dead chat settings from both user and admin settings. | +| Dead code sweep | ✅ | Removed ~700 lines: orphaned CSS, dead Go types, stale event code, unused test helpers, dead settings UI (chat defaults, system prompt, default model, policies, web search, compaction, memory). | +| Template cleanup | ✅ | Removed stale CSS link tags and orphaned chat-pane.html. Updated doc comments throughout. | +| Package proof-of-concept status | ✅ | Created README.md for tasks + schedules with graduation criteria. Added missing manifest icons. | +| Welcome surface | ✅ | New fallback surface when no extensions installed. Topbar + welcome card with admin link. Replaces `/admin` as final redirect target. | +| Default surface routing | ✅ | Resolution chain: user preference → global config → first extension → `/welcome`. Users can set personal default in Settings > General. Admin sets global default in Admin > Settings. | +| Admin navigation | ✅ | Replaced Back button with UserMenu in admin topbar. Eliminates back-button infinite loop. | -### v0.2.6 — Workflow Architecture +### v0.2.6 — Extension Lifecycle | Step | Status | Description | |------|--------|-------------| -| Workflow design session | ⬚ | Define what "workflow" means in the extension-first model. Determine if the existing `workflows` table/handler survives, gets rebuilt, or gets removed. Document the Starlark contract for multi-step automation. | -| Trigger composition model | ⬚ | How do triggers, schedules, and workflows compose? Event chains, conditional branching, error handling. Design doc before code. | | Extension lifecycle | ⬚ | Define permanent vs PoC extensions. Package graduation criteria. Dependency policy. What ships with core vs what's installed separately. | -## v0.3.0 — Notes Surface +### v0.2.7 — Admin Settings Audit + +| Step | Status | Description | +|------|--------|-------------| +| Admin settings E2E | ⬚ | Verify all surviving admin settings work end-to-end: default surface, registration, banner, message bar, footer, vault, email. Remove any handler/store vestiges of deleted settings (search, compaction, memory). | +| Packages surface | ⬚ | Verify packages page correctly shows installed vs available. Enable/disable works. Broken post-fork surfaces identified and either fixed or marked. | + +### v0.2.8 — User Settings Audit + +| Step | Status | Description | +|------|--------|-------------| +| User settings E2E | ⬚ | Verify all user settings sections: General (default surface), Appearance (theme, scale, font), Profile (display name, avatar, handle), Connections, Notifications. Remove dead features, fix broken states. | +| Visibility gating | ⬚ | Ensure settings sections only show features that are actually available. Hide empty sections. Respect `user_overridable` flag from extension manifests. | + +### v0.2.9 — Team Admin Settings Audit (Pass 1) + +| Step | Status | Description | +|------|--------|-------------| +| Team admin E2E | ⬚ | Verify team member management, team settings cascade, role assignment (admin/member). Audit for dead code from pre-fork team features. First pass — validates current functionality before v0.3.x adds team roles. | +| User settings team tab | ⬚ | Verify the Teams section in user settings — team list, join/leave, team-scoped settings. | + +## v0.3.x — Workflow Architecture + +Workflows are the core platform capability. This series implements the +full multi-step automation system with team role integration. + +### v0.3.0 — Workflow Design + Schema + +| Step | Status | Description | +|------|--------|-------------| +| Workflow design session | ⬚ | Define what "workflow" means in the extension-first model. Determine if the existing `workflows` table/handler survives, gets rebuilt, or gets removed. Document the Starlark contract for multi-step automation. See `docs/DESIGN-WORKFLOW-REDESIGN-0.2.6.md`. | +| Team roles | ⬚ | Different roles per team responsible for different workflow stages. Role-based stage assignment, multi-party validation (2-party sign-off at stage boundaries). | +| Trigger composition model | ⬚ | How do triggers, schedules, and workflows compose? Event chains, conditional branching, error handling. Design doc before code. | +| Settings audit pass 2 | ⬚ | Focused audit of team admin + user settings for workflow/team-role changes applied in this series. Validates new team role UI, stage assignment settings, workflow preferences. | + +## v0.4.0 — Notes Surface Obsidian-style rich-text notes rebuilt as an installable surface package. Zero platform special-casing. Proves the full extension stack E2E. @@ -104,7 +140,7 @@ Zero platform special-casing. Proves the full extension stack E2E. - Folder tree, backlinks, tags — all extension-provided - Markdown import/export -## v0.4.0 — MVP +## v0.5.0 — MVP Extension and operations tracks converge. First externally usable release. diff --git a/nginx.conf.template b/nginx.conf.template index af89c8b..de564e7 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -65,6 +65,7 @@ server { location ${BASE_PATH}/admin { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location ${BASE_PATH}/team-admin { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location ${BASE_PATH}/settings { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } + location ${BASE_PATH}/welcome { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location ${BASE_PATH}/w/ { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # Extension surface page routes → backend diff --git a/packages/hello-dashboard/manifest.json b/packages/hello-dashboard/manifest.json index 43caa73..a39ef64 100644 --- a/packages/hello-dashboard/manifest.json +++ b/packages/hello-dashboard/manifest.json @@ -1,5 +1,6 @@ { "id": "hello-dashboard", + "icon": "👋", "title": "Hello Dashboard", "route": "/s/hello-dashboard", "auth": "authenticated", diff --git a/packages/icd-test-runner/manifest.json b/packages/icd-test-runner/manifest.json index 8bb3fb0..0c2db45 100644 --- a/packages/icd-test-runner/manifest.json +++ b/packages/icd-test-runner/manifest.json @@ -1,5 +1,6 @@ { "id": "icd-test-runner", + "icon": "🧪", "title": "ICD Test Runner", "route": "/s/icd-test-runner", "auth": "authenticated", diff --git a/packages/schedules/README.md b/packages/schedules/README.md new file mode 100644 index 0000000..1b0d9d4 --- /dev/null +++ b/packages/schedules/README.md @@ -0,0 +1,22 @@ +# Schedules + +**Status: Proof of Concept** + +Schedule management surface wrapping the kernel cron API. Ships with +core for SDK validation purposes, not as a permanent commitment. + +## Purpose + +Provides a management UI for user-created scheduled tasks (cron). Proves +that kernel APIs can be wrapped by extension surfaces without +special-casing. + +## Graduation Criteria + +To become a permanent core package, schedules must meet: + +1. **Cron expression builder** — visual UI for building cron expressions +2. **Execution history** — log viewer with output and error details +3. **Error alerting** — notification on failed runs +4. **Test coverage** — API contract tests in the ICD test runner +5. **Timezone-aware display** — show next run in user's local timezone diff --git a/packages/sdk-test-runner/manifest.json b/packages/sdk-test-runner/manifest.json index 717d5b1..eae9218 100644 --- a/packages/sdk-test-runner/manifest.json +++ b/packages/sdk-test-runner/manifest.json @@ -1,5 +1,6 @@ { "id": "sdk-test-runner", + "icon": "🔬", "title": "SDK Test Runner", "route": "/s/sdk-test-runner", "auth": "authenticated", diff --git a/packages/tasks/README.md b/packages/tasks/README.md new file mode 100644 index 0000000..0524cb9 --- /dev/null +++ b/packages/tasks/README.md @@ -0,0 +1,22 @@ +# Tasks + +**Status: Proof of Concept** + +Task management surface rebuilt as a Starlark extension. Ships with core +for SDK validation purposes, not as a permanent commitment. + +## Purpose + +Validates all three trigger primitives (event bus, webhook, cron) and +proves the full extension surface stack: Starlark API routes, ext_data +tables, SDK UI components, and notifications. + +## Graduation Criteria + +To become a permanent core package, tasks must meet: + +1. **Feature completeness** — assignees, comments, attachments, subtasks +2. **Test coverage** — API contract tests in the ICD test runner +3. **Performance** — tested at 1000+ tasks with acceptable load times +4. **Design review** — UI/UX matches platform design language +5. **No kernel special-casing** — runs purely through extension APIs diff --git a/packages/team-activity-log/manifest.json b/packages/team-activity-log/manifest.json index ea64f0d..b68a799 100644 --- a/packages/team-activity-log/manifest.json +++ b/packages/team-activity-log/manifest.json @@ -1,5 +1,6 @@ { "id": "team-activity-log", + "icon": "📋", "title": "Team Activity Log", "type": "full", "tier": "starlark", diff --git a/server/database/seed_helpers.go b/server/database/seed_helpers.go deleted file mode 100644 index 599f105..0000000 --- a/server/database/seed_helpers.go +++ /dev/null @@ -1,62 +0,0 @@ -package database - -import ( - "strings" - "testing" -) - -// ── Additional Test Seed Helpers (v0.15.0) ── - -// SeedTestMessage creates a single message in a channel and returns the message ID. -func SeedTestMessage(t *testing.T, channelID, parentID, role, content string) string { - t.Helper() - var id string - var parentPtr *string - if parentID != "" { - parentPtr = &parentID - } - err := DB.QueryRow(` - INSERT INTO messages (channel_id, parent_id, role, content, sibling_index) - VALUES ($1, $2, $3, $4, 0) - RETURNING id - `, channelID, parentPtr, role, content).Scan(&id) - if err != nil { - t.Fatalf("SeedTestMessage: %v", err) - } - return id -} - -// SeedTestMessages creates a linear chain of alternating user/assistant messages. -// Returns all message IDs in order. The first message has no parent. -func SeedTestMessages(t *testing.T, channelID string, count int, contentSize int) []string { - t.Helper() - content := strings.Repeat("x", contentSize) - ids := make([]string, 0, count) - - parentID := "" - for i := 0; i < count; i++ { - role := "user" - if i%2 == 1 { - role = "assistant" - } - id := SeedTestMessage(t, channelID, parentID, role, content) - ids = append(ids, id) - parentID = id - } - - return ids -} - -// SeedTestCursor sets the active leaf for a user in a channel. -func SeedTestCursor(t *testing.T, channelID, userID, leafID string) { - t.Helper() - _, err := DB.Exec(` - INSERT INTO channel_cursors (channel_id, user_id, active_leaf_id) - VALUES ($1, $2, $3) - ON CONFLICT (channel_id, user_id) - DO UPDATE SET active_leaf_id = $3, updated_at = NOW() - `, channelID, userID, leafID) - if err != nil { - t.Fatalf("SeedTestCursor: %v", err) - } -} diff --git a/server/events/bus.go b/server/events/bus.go index d4180b6..bb47ffa 100644 --- a/server/events/bus.go +++ b/server/events/bus.go @@ -43,10 +43,10 @@ func (b *Bus) SetBroadcastHook(fn func(Event)) { // // Patterns: // -// "chat.message.abc123" — exact match -// "chat.message.*" — wildcard: matches chat.message.{anything} -// "chat.*" — wildcard: matches chat.{anything} -// "*" — matches all events +// "workflow.assigned.abc123" — exact match +// "workflow.assigned.*" — wildcard: matches workflow.assigned.{anything} +// "workflow.*" — wildcard: matches workflow.{anything} +// "*" — matches all events func (b *Bus) Subscribe(pattern string, handler Handler) func() { b.mu.Lock() b.seq++ diff --git a/server/events/bus_test.go b/server/events/bus_test.go index 6577ada..aa0ee29 100644 --- a/server/events/bus_test.go +++ b/server/events/bus_test.go @@ -12,13 +12,13 @@ func TestMatch(t *testing.T) { label, pattern string want bool }{ - {"chat.message.abc", "chat.message.abc", true}, - {"chat.message.abc", "chat.message.*", true}, - {"chat.message.abc", "chat.*", true}, - {"chat.message.abc", "*", true}, - {"chat.message.abc", "chat.message.xyz", false}, - {"chat.message.abc", "channel.message.*", false}, - {"chat.message.abc", "chat.message", false}, + {"workflow.assigned.abc", "workflow.assigned.abc", true}, + {"workflow.assigned.abc", "workflow.assigned.*", true}, + {"workflow.assigned.abc", "workflow.*", true}, + {"workflow.assigned.abc", "*", true}, + {"workflow.assigned.abc", "workflow.assigned.xyz", false}, + {"workflow.assigned.abc", "notification.new.*", false}, + {"workflow.assigned.abc", "workflow.assigned", false}, {"ping", "ping", true}, {"ping", "pong", false}, {"plugin.hook.pre_completion", "plugin.hook.*", true}, @@ -37,12 +37,12 @@ func TestBusPublishExact(t *testing.T) { bus := NewBus() var count int32 - bus.Subscribe("chat.message.abc", func(e Event) { + bus.Subscribe("workflow.assigned.abc", func(e Event) { atomic.AddInt32(&count, 1) }) - bus.Publish(Event{Label: "chat.message.abc", Payload: json.RawMessage(`{}`)}) - bus.Publish(Event{Label: "chat.message.xyz", Payload: json.RawMessage(`{}`)}) + bus.Publish(Event{Label: "workflow.assigned.abc", Payload: json.RawMessage(`{}`)}) + bus.Publish(Event{Label: "workflow.assigned.xyz", Payload: json.RawMessage(`{}`)}) if atomic.LoadInt32(&count) != 1 { t.Errorf("expected 1 dispatch, got %d", count) @@ -53,13 +53,13 @@ func TestBusPublishWildcard(t *testing.T) { bus := NewBus() var count int32 - bus.Subscribe("chat.message.*", func(e Event) { + bus.Subscribe("workflow.assigned.*", func(e Event) { atomic.AddInt32(&count, 1) }) - bus.Publish(Event{Label: "chat.message.abc", Payload: json.RawMessage(`{}`)}) - bus.Publish(Event{Label: "chat.message.xyz", Payload: json.RawMessage(`{}`)}) - bus.Publish(Event{Label: "chat.typing.abc", Payload: json.RawMessage(`{}`)}) + bus.Publish(Event{Label: "workflow.assigned.abc", Payload: json.RawMessage(`{}`)}) + bus.Publish(Event{Label: "workflow.assigned.xyz", Payload: json.RawMessage(`{}`)}) + bus.Publish(Event{Label: "workflow.claimed.abc", Payload: json.RawMessage(`{}`)}) if atomic.LoadInt32(&count) != 2 { t.Errorf("expected 2 dispatches, got %d", count) @@ -91,7 +91,7 @@ func TestBusGlobalWildcard(t *testing.T) { atomic.AddInt32(&count, 1) }) - bus.Publish(Event{Label: "chat.message.abc", Payload: json.RawMessage(`{}`)}) + bus.Publish(Event{Label: "workflow.assigned.abc", Payload: json.RawMessage(`{}`)}) bus.Publish(Event{Label: "system.notify", Payload: json.RawMessage(`{}`)}) bus.Publish(Event{Label: "plugin.hook.pre_completion", Payload: json.RawMessage(`{}`)}) @@ -105,8 +105,6 @@ func TestRouteFor(t *testing.T) { label string want Direction }{ - {"chat.message.abc", DirLocal}, // chat routes removed in v0.1.0 - {"chat.typing.abc", DirLocal}, // chat routes removed in v0.1.0 {"system.notify", DirToClient}, {"plugin.hook.pre_completion", DirLocal}, {"internal.db.write", DirLocal}, diff --git a/server/events/types.go b/server/events/types.go index 3f43852..ec0ef91 100644 --- a/server/events/types.go +++ b/server/events/types.go @@ -35,10 +35,6 @@ const ( // routeTable defines the default routing for known event prefixes. // Events not listed default to DirLocal (server-only). var routeTable = map[string]Direction{ - // Chat events - - // Channel events - // User/presence "user.presence": DirToClient, "user.status": DirToClient, diff --git a/server/events/ws.go b/server/events/ws.go index e5700d6..4a49375 100644 --- a/server/events/ws.go +++ b/server/events/ws.go @@ -250,13 +250,6 @@ func (c *Conn) subscribeToBus() { return } - // Don't echo typing events back to the sender - if strings.HasPrefix(e.Label, "chat.typing.") || strings.HasPrefix(e.Label, "channel.typing.") { - if e.SenderID == c.userID && e.ConnID == c.id { - return - } - } - // Room filtering: if event has a room, only send if conn is in that room if e.Room != "" && !c.rooms[e.Room] { return diff --git a/server/handlers/packages.go b/server/handlers/packages.go index daf4675..c884bf7 100644 --- a/server/handlers/packages.go +++ b/server/handlers/packages.go @@ -101,13 +101,13 @@ func (h *PackageHandler) EnablePackage(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"id": id, "enabled": true}) } -// DisablePackage disables a package. Chat and Admin cannot be disabled. +// DisablePackage disables a package. Admin cannot be disabled. // PUT /api/v1/admin/packages/:id/disable // PUT /api/v1/admin/surfaces/:id/disable (alias) func (h *PackageHandler) DisablePackage(c *gin.Context) { id := c.Param("id") - if id == "chat" || id == "admin" { + if id == "admin" { c.JSON(http.StatusBadRequest, gin.H{"error": id + " cannot be disabled"}) return } diff --git a/server/handlers/workflows.go b/server/handlers/workflows.go index 71d093e..46432b7 100644 --- a/server/handlers/workflows.go +++ b/server/handlers/workflows.go @@ -285,7 +285,7 @@ func (h *WorkflowHandler) Publish(c *gin.Context) { stages = []models.WorkflowStage{} } - // Snapshot includes persona tool grants at publish time (frozen for running instances) + // Snapshot stages at publish time (frozen for running instances) type stageSnapshot struct { models.WorkflowStage ToolGrants []string `json:"tool_grants,omitempty"` diff --git a/server/middleware/auth.go b/server/middleware/auth.go index a29b354..2512a2c 100644 --- a/server/middleware/auth.go +++ b/server/middleware/auth.go @@ -142,6 +142,21 @@ func parseAndValidateJWT(tokenString string, jwtSecret string) (*Claims, bool) { return claims, true } +// UserIDFromCookie extracts the user ID from the sb_token cookie without +// requiring authentication. Returns "" if no valid token is found. +// Used by unauthenticated routes that want optional user context. +func UserIDFromCookie(c *gin.Context, jwtSecret string) string { + cookie, err := c.Cookie("sb_token") + if err != nil || cookie == "" { + return "" + } + claims, ok := parseAndValidateJWT(cookie, jwtSecret) + if !ok { + return "" + } + return claims.UserID +} + // ─── Auth middleware ───────────────────────────────────────── // Auth returns a Gin middleware that validates JWT bearer tokens and diff --git a/server/models/models.go b/server/models/models.go index 87bee9b..8ff386c 100644 --- a/server/models/models.go +++ b/server/models/models.go @@ -81,19 +81,6 @@ type TeamMember struct { UserRole string `json:"user_role,omitempty"` } -// ========================================= -// GRANTS -// ========================================= - -type Grant struct { - ID string `json:"id" db:"id"` - PersonaID string `json:"persona_id" db:"persona_id"` - GrantType string `json:"grant_type" db:"grant_type"` - GrantRef string `json:"grant_ref" db:"grant_ref"` - Config JSONMap `json:"config,omitempty" db:"config"` - CreatedAt time.Time `json:"created_at" db:"created_at"` -} - // PLATFORM POLICIES var PolicyDefaults = map[string]string{ @@ -106,26 +93,6 @@ var PolicyDefaults = map[string]string{ "default_model": "", } -// USER MODEL SETTINGS - -// HiddenEntry identifies a model+provider pair for bulk visibility operations. -// CompositeModelKey builds the composite key used for per-provider model preferences. -func CompositeModelKey(providerConfigID, modelID string) string { - return providerConfigID + ":" + modelID -} - -// CHANNELS - -// SESSION PARTICIPANTS (v0.24.3) - -// SessionParticipant is an ephemeral identity for anonymous workflow -// channel visitors. Scoped to a single channel, no users row required. -// MESSAGES - -// CHANNEL PARTICIPANTS, MODELS, CURSORS - -// PERSONA GROUPS (v0.23.0) - // HandleFromName generates a URL-safe @mention handle from a display name. // "Veronica Sharpe" → "veronica-sharpe" func HandleFromName(name string) string { @@ -150,26 +117,6 @@ func HandleFromName(name string) string { return h } -// ORGANIZATION - -// ProjectPatch holds optional fields for updating a project. -// ProjectChannel represents a channel's membership in a project. -// ProjectKB represents a KB's association with a project. -// ProjectNote represents a note's association with a project. -// NOTES - -// NoteLink represents a directed link extracted from [[wikilink]] syntax. -// ExportNoteLink is a note_link with source_note_id included (for export). -// NoteLinkResult represents a backlink — a note that links to a given note. -// NoteGraphNode is a lightweight note representation for graph display. -// NoteGraphEdge is a resolved link between two notes. -// NoteGraphDangling is an unresolved [[link]] reference. -// NoteGraph is the full graph topology for a user's notes. -// ATTACHMENTS - -// FILES - -// FileOrigin constants // ========================================= // AUDIT LOG // ========================================= @@ -187,14 +134,6 @@ type AuditEntry struct { CreatedAt time.Time `json:"created_at" db:"created_at"` } -// USAGE TRACKING - -// MODEL PRICING - -// VIEW MODELS (computed, not stored) - -// UserModel is the view model returned by the capability resolver. -// Combines catalog entries + Personas for the frontend. // ========================================= // JSON HELPERS // ========================================= diff --git a/server/pages/pages.go b/server/pages/pages.go index 19dde05..c44ef5b 100644 --- a/server/pages/pages.go +++ b/server/pages/pages.go @@ -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 } diff --git a/server/pages/pages_surfaces.go b/server/pages/pages_surfaces.go index b7214cf..2a027dd 100644 --- a/server/pages/pages_surfaces.go +++ b/server/pages/pages_surfaces.go @@ -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") diff --git a/server/pages/templates/base.html b/server/pages/templates/base.html index ea7081b..59e77bc 100644 --- a/server/pages/templates/base.html +++ b/server/pages/templates/base.html @@ -12,13 +12,8 @@ - {{/* 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 */}} - {{/* splash.css removed in v0.37.12 — login is Preact with sw-login.css */}} - - @@ -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}}
Unknown surface: {{.Surface}}
{{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")}} diff --git a/server/pages/templates/components/chat-pane.html b/server/pages/templates/components/chat-pane.html deleted file mode 100644 index 26881dc..0000000 --- a/server/pages/templates/components/chat-pane.html +++ /dev/null @@ -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"}} -
- -
-
-
-
- -
-
-
-
-{{end}} diff --git a/server/pages/templates/surfaces/welcome.html b/server/pages/templates/surfaces/welcome.html new file mode 100644 index 0000000..a77f023 --- /dev/null +++ b/server/pages/templates/surfaces/welcome.html @@ -0,0 +1,25 @@ +{{/* + Welcome surface — shown when no extension surfaces are installed. + Renders topbar + welcome card with getting-started info. +*/}} + +{{define "surface-welcome"}} +
+{{end}} + +{{define "scripts-welcome"}} + +{{end}} diff --git a/server/pages/templates/workflow.html b/server/pages/templates/workflow.html index 61503bd..b51a15c 100644 --- a/server/pages/templates/workflow.html +++ b/server/pages/templates/workflow.html @@ -9,7 +9,6 @@ -