package pages import ( "context" "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"` } // ── Page data structs ──────────────────────── // AdminPageData is what the admin surface templates receive. type AdminPageData struct { Section string `json:"section"` Category string `json:"category"` ConfigSections []ConfigSectionEntry `json:"config_sections,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"` } // ── Loader registration ────────────────────── // TeamAdminPageData is what the team-admin surface receives. type TeamAdminPageData struct { Section string `json:"section"` ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` } 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, _ store.Stores) (any, error) { section := c.Param("section") if section == "" { section = "users" } return &AdminPageData{ Section: section, Category: sectionCategory(section), }, nil } // ── 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 "workflows": return "workflows" case "settings", "storage", "packages", "broadcast", "backup": return "system" case "usage", "audit", "stats": return "monitoring" default: return "system" } } // ── Settings loader ────────────────────────── // 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 }