V0.38.3 extension config sections (#236)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-25 16:38:38 +00:00
committed by xcaliber
parent 2136535176
commit fe5894b6e2
11 changed files with 338 additions and 12 deletions

View File

@@ -95,6 +95,7 @@ type AdminPageData struct {
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.
@@ -110,14 +111,16 @@ type NotesPageData struct {
// 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"`
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"`
Section string `json:"section"`
ConfigSections []ConfigSectionEntry `json:"config_sections,omitempty"` // v0.38.3
}
// ProjectsPageData is what the projects surface receives.
@@ -246,6 +249,9 @@ func (e *Engine) adminLoader(c *gin.Context, s store.Stores) (any, error) {
data.Users = e.loadUsers(ctx, s)
}
// v0.38.3: Discover extension config sections for admin surface
data.ConfigSections = configSectionsForSurface(ctx, s, "admin")
return data, nil
}
@@ -409,7 +415,10 @@ func (e *Engine) teamAdminLoader(c *gin.Context, s store.Stores) (any, error) {
if section == "" {
section = "members"
}
return &TeamAdminPageData{Section: section}, nil
return &TeamAdminPageData{
Section: section,
ConfigSections: configSectionsForSurface(context.Background(), s, "team-admin"),
}, nil
}
func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
@@ -418,10 +427,84 @@ func (e *Engine) settingsLoader(c *gin.Context, s store.Stores) (any, error) {
section = "general"
}
return &SettingsPageData{Section: section}, nil
return &SettingsPageData{
Section: section,
ConfigSections: configSectionsForSurface(context.Background(), s, "settings"),
}, nil
}
func (e *Engine) projectsLoader(c *gin.Context, s store.Stores) (any, error) {
projectID := c.Param("id")
return &ProjectsPageData{ProjectID: projectID}, 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
}

View File

@@ -135,6 +135,17 @@ type ExtensionNavItem struct {
Route string
}
// ConfigSectionEntry describes a package-declared config section for
// lazy-loading in Settings, Admin, or Team Admin surfaces.
// v0.38.3: manifest-driven discovery — no new tables or endpoints.
type ConfigSectionEntry struct {
PackageID string `json:"package_id"` // section key & asset path prefix
Label string `json:"label"` // nav link text
Icon string `json:"icon"` // optional SVG path data (admin CatIcon format)
Component string `json:"component"` // relative asset path, e.g. "js/config.js"
Category string `json:"category"` // admin only: category tab (default "system")
}
// extensionNavItems returns enabled extension surfaces for sidebar rendering.
// Queries the DB at render time so newly installed surfaces appear immediately.
func (e *Engine) extensionNavItems() []ExtensionNavItem {

View File

@@ -21,6 +21,7 @@
{{/* Inject section name for Preact routing */}}
<script nonce="{{.CSPNonce}}">
window.__SECTION__ = '{{.Section}}';
window.__CONFIG_SECTIONS__ = {{.Data.ConfigSections | toJSON}};
</script>
{{/* Vendor: Preact + htm → globals, then SDK boot, then admin surface */}}

View File

@@ -18,6 +18,7 @@
{{/* Inject section name for Preact routing */}}
<script nonce="{{.CSPNonce}}">
window.__SECTION__ = '{{.Section}}';
window.__CONFIG_SECTIONS__ = {{.Data.ConfigSections | toJSON}};
</script>
{{/* Bridge section scripts removed in v0.37.7 — all sections are native Preact */}}

View File

@@ -15,6 +15,7 @@
{{define "scripts-team-admin"}}
<script nonce="{{.CSPNonce}}">
window.__SECTION__ = '{{.Section}}';
window.__CONFIG_SECTIONS__ = {{.Data.ConfigSections | toJSON}};
</script>
<script type="module" nonce="{{.CSPNonce}}">