This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/pages/loaders.go
Jeffrey Smith 7b6e54d5b7 step 5 (complete): docs purge, fresh ARCHITECTURE + ROADMAP + CHANGELOG
Purged 29,966 lines of stale chat-switchboard documentation:
  - 41 docs/ files (ICD specs, design docs, archive, workflow docs)
  - 5 root MD files (CHANGESET, TURNOVER, ICD-DRIFT-AUDIT, etc.)

New documentation:
  - docs/ARCHITECTURE.md — kernel components, design principles, data layer
  - ROADMAP.md — v0.1.0 through v0.5.0 MVP with decision log
  - CHANGELOG.md — fresh, starting from v0.1.0 fork
  - README.md — rewritten for switchboard-core

Also in this commit:
  - config.go: stripped 7 dropped fields, DB default → switchboard_core
  - pages/loaders.go: stripped provider/model/notes/projects loaders
  - pages/pages.go: stripped persona store lookup
  - handlers/workflows.go: stripped persona tool grants
  - main.go: stripped team provider routes, avatar routes

Production code: zero references to deleted packages, stores, or models.
-29,966/+352 lines across 73 files.
2026-03-26 05:10:40 -04:00

297 lines
8.4 KiB
Go

package pages
import (
"context"
"fmt"
"log"
"github.com/gin-gonic/gin"
"switchboard-core/store"
)
// TeamOption for template dropdowns.
type TeamOption struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
MemberCount int `json:"member_count,omitempty"`
IsActive bool `json:"is_active"`
}
// ProviderTypeOption for the provider type dropdown.
type ProviderTypeOption struct {
ID string `json:"id"`
Name string `json:"name"`
}
// UserRow is a user for the admin users table.
type UserRow struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
DisplayName string `json:"display_name"`
Role string `json:"role"`
IsActive bool `json:"is_active"`
}
// RoleConfig holds a role's current settings.
type RoleConfig struct {
Name string `json:"name"`
Primary *RoleSelection `json:"primary"`
Fallback *RoleSelection `json:"fallback"`
}
// RoleSelection is provider + model pair for a role slot.
type RoleSelection struct {
ProviderID string `json:"provider_config_id"`
ModelID string `json:"model_id"`
}
// ── Page data structs ────────────────────────
//
// v0.25.0: Each loader function is a "data provider" keyed by surface ID.
// The surface manifest's DataRequires field references these keys.
// Currently 1:1 (one loader per surface). Future: composite loaders
// that assemble data from multiple providers for dashboard-style surfaces.
// AdminPageData is what the admin surface templates receive.
type AdminPageData struct {
Section string `json:"section"`
Category string `json:"category"`
ProviderTypes []ProviderTypeOption `json:"provider_types,omitempty"`
Teams []TeamOption `json:"teams"`
Users []UserRow `json:"users,omitempty"`
Roles []RoleConfig `json:"roles"`
Policies any `json:"policies,omitempty"`
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
}
// ChatPageData is what the chat surface templates receive.
type ChatPageData struct {
ChatID string `json:"chat_id,omitempty"`
}
// SettingsPageData is what the settings surface templates receive.
// Feature gates (BYOK, personas) moved to sw.auth.policies in v0.37.19.
type SettingsPageData struct {
Section string `json:"section"`
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
}
// ── Loader registration ──────────────────────
// TeamAdminPageData is what the team-admin surface receives.
type TeamAdminPageData struct {
Section string `json:"section"`
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
}
func (e *Engine) registerLoaders() {
e.RegisterLoader("admin", e.adminLoader)
e.RegisterLoader("settings", e.settingsLoader)
e.RegisterLoader("team-admin", e.teamAdminLoader)
}
// Used for manifest validation — DataRequires entries must match a key here.
func (e *Engine) ListDataProviders() []string {
keys := make([]string, 0, len(e.loaders))
for k := range e.loaders {
keys = append(keys, k)
}
return keys
}
// ── Admin loader ─────────────────────────────
// Pre-loads ALL dropdown data. Fixes bugs #1 (model roles) and #2 (team scope).
func (e *Engine) adminLoader(c *gin.Context, s store.Stores) (any, error) {
ctx := context.Background()
section := c.Param("section")
if section == "" {
section = "users"
}
data := &AdminPageData{
Section: section,
Category: sectionCategory(section),
}
rolesMap, ok := raw["roles"].(map[string]any)
if !ok {
rolesMap = raw
}
for _, name := range roleNames {
rc := RoleConfig{Name: name}
if roleData, ok := rolesMap[name].(map[string]any); ok {
if primary, ok := roleData["primary"].(map[string]any); ok {
rc.Primary = &RoleSelection{
ProviderID: fmt.Sprintf("%v", primary["provider_config_id"]),
ModelID: fmt.Sprintf("%v", primary["model_id"]),
}
}
if fallback, ok := roleData["fallback"].(map[string]any); ok {
rc.Fallback = &RoleSelection{
ProviderID: fmt.Sprintf("%v", fallback["provider_config_id"]),
ModelID: fmt.Sprintf("%v", fallback["model_id"]),
}
}
}
roles = append(roles, rc)
}
return roles
}
// ── Admin helpers ────────────────────────────
// sectionCategory maps an admin section to its category tab.
func sectionCategory(section string) string {
switch section {
case "users", "teams", "groups":
return "people"
case "providers", "models", "personas", "roles", "knowledgeBases", "memory":
return "ai"
case "health", "routing", "capabilities":
return "routing"
case "settings", "storage", "packages", "channels", "broadcast":
return "system"
case "usage", "audit", "stats":
return "monitoring"
default:
return "ai" // default landing
}
}
// loadProviderTypes returns the registered provider type metadata.
func loadProviderTypes() []ProviderTypeOption {
out := make([]ProviderTypeOption, 0, len(types))
for _, t := range types {
out = append(out, ProviderTypeOption{ID: t.ID, Name: t.Name})
}
return out
}
// loadUsers returns the user list for the admin users table.
func (e *Engine) loadUsers(ctx context.Context, s store.Stores) []UserRow {
if s.Users == nil {
return nil
}
users, _, err := s.Users.List(ctx, store.ListOptions{Limit: 500, Sort: "username", Order: "asc"})
if err != nil {
log.Printf("[pages/admin] Failed to list users: %v", err)
return nil
}
out := make([]UserRow, 0, len(users))
for _, u := range users {
out = append(out, UserRow{
ID: u.ID,
Username: u.Username,
Email: u.Email,
DisplayName: u.DisplayName,
Role: u.Role,
IsActive: u.IsActive,
})
}
return out
}
// ── Settings loader ──────────────────────────
// v0.22.7: Reads feature gates from GlobalConfig to control
// which nav links/tabs are visible (BYOK, User Personas).
func (e *Engine) teamAdminLoader(c *gin.Context, s store.Stores) (any, error) {
section := c.Param("section")
if section == "" {
section = "members"
}
return &TeamAdminPageData{
Section: section,
ConfigSections: configSectionsForSurface(context.Background(), s, "team-admin"),
}, nil
}
func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
section := c.Param("section")
if section == "" {
section = "general"
}
return &SettingsPageData{
Section: section,
ConfigSections: configSectionsForSurface(context.Background(), s, "settings"),
}, nil
}
// ── Config section discovery (v0.38.3) ───────
//
// Packages declare a config_section in their manifest. This helper scans
// enabled packages and returns entries whose surfaces array includes the
// target surface. No new tables or endpoints — purely manifest-driven.
func configSectionsForSurface(ctx context.Context, s store.Stores, surfaceID string) []ConfigSectionEntry {
if s.Packages == nil {
return nil
}
pkgs, err := s.Packages.List(ctx)
if err != nil {
log.Printf("[pages] config sections: failed to list packages: %v", err)
return nil
}
var sections []ConfigSectionEntry
for _, pkg := range pkgs {
if !pkg.Enabled || pkg.Manifest == nil {
continue
}
csRaw, ok := pkg.Manifest["config_section"]
if !ok {
continue
}
cs, ok := csRaw.(map[string]any)
if !ok {
continue
}
// Check if this section targets the requested surface
surfacesRaw, _ := cs["surfaces"].([]any)
if len(surfacesRaw) == 0 {
continue
}
found := false
for _, v := range surfacesRaw {
if s, ok := v.(string); ok && s == surfaceID {
found = true
break
}
}
if !found {
continue
}
label, _ := cs["label"].(string)
if label == "" {
label = pkg.Title
}
icon, _ := cs["icon"].(string)
component, _ := cs["component"].(string)
if component == "" {
component = "js/config.js"
}
category, _ := cs["category"].(string)
if category == "" {
category = "system"
}
sections = append(sections, ConfigSectionEntry{
PackageID: pkg.ID,
Label: label,
Icon: icon,
Component: component,
Category: category,
})
}
return sections
}