package pages import ( "context" "log" "net/http" "github.com/gin-gonic/gin" ) // SeedSurfaces writes core surface manifests to the registry table. // Called once at startup. Does NOT overwrite the enabled flag — admin // toggles survive restarts. func (e *Engine) SeedSurfaces() { if e.stores.Packages == nil { log.Printf("[pages] Surface registry not available — skipping seed") return } ctx := context.Background() for _, s := range e.surfaces { manifest := map[string]any{ "route": s.Route, "alt_routes": s.AltRoutes, "template": s.Template, "components": s.Components, "data_requires": s.DataRequires, "auth": s.Auth, "layout": s.Layout, } if err := e.stores.Packages.Seed(ctx, s.ID, s.Title, s.Source, manifest); err != nil { log.Printf("[pages] Failed to seed surface %q: %v", s.ID, err) } } log.Printf("[pages] Seeded %d core surfaces into registry", len(e.surfaces)) // v0.31.0: Clean up old "editor" core surface row. // Editor is now an installable package — the old seed row would show a // broken nav link until the package .pkg is uploaded via admin UI. if sr, err := e.stores.Packages.Get(ctx, "editor"); err == nil && sr != nil && sr.Source == "extension" { if sr.Type == "" || sr.Type == "surface" { if delErr := e.stores.Packages.Delete(ctx, "editor"); delErr != nil { log.Printf("[pages] Failed to clean up old editor surface: %v", delErr) } else { log.Printf("[pages] Cleaned up old editor core surface — install editor .pkg via admin UI") } } } } // IsSurfaceEnabled checks if a surface is enabled in the registry. // 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 { if surfaceID == "admin" { return true } if e.stores.Packages == nil { return true // No registry = all surfaces enabled (backward compat) } ctx := context.Background() sr, err := e.stores.Packages.Get(ctx, surfaceID) if err != nil || sr == nil { return true // Not in registry = assume enabled (backward compat) } return sr.Enabled } // EnabledSurfaceIDs returns a list of enabled surface IDs for nav rendering. func (e *Engine) EnabledSurfaceIDs() []string { if e.stores.Packages == nil { // No registry — return all core surface IDs var all []string for _, s := range e.surfaces { all = append(all, s.ID) } return all } ctx := context.Background() ids, err := e.stores.Packages.ListEnabled(ctx) if err != nil { log.Printf("[pages] Failed to load enabled surfaces: %v", err) // Fallback: return all core surface IDs var all []string for _, s := range e.surfaces { all = append(all, s.ID) } return all } return ids } // DefaultSurfaceRedirect returns a handler for GET / that redirects to the // configured default surface, falling back to the first enabled extension // surface, then /admin. func (e *Engine) DefaultSurfaceRedirect() gin.HandlerFunc { return func(c *gin.Context) { target := e.resolveDefaultSurface(c.Request.Context()) 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 { if e.stores.GlobalConfig == nil || e.stores.Packages == nil { return "/admin" } // 1. Check configured 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 != "" { return path } // Configured surface is missing or disabled — fall through } } // 2. First enabled extension surface surfaces, err := e.stores.Packages.ListEnabledByType(ctx, "surface") if err == nil { for _, s := range surfaces { if s.Source == "core" { continue } if s.Enabled { return "/s/" + s.ID } } } // 3. Fallback return "/admin" } // surfacePath returns the URL path for a surface ID, or "" if the surface // doesn't exist or is disabled. func (e *Engine) surfacePath(ctx context.Context, id string) string { pkg, err := e.stores.Packages.Get(ctx, id) if err != nil || pkg == nil || !pkg.Enabled { return "" } // Core surfaces live at /{id}, extension surfaces at /s/{slug} if pkg.Source == "core" { // Look up the core surface's base route if sm := e.GetSurface(id); sm != nil { // Use the first alt route (without params) or strip params from Route if len(sm.AltRoutes) > 0 { return sm.AltRoutes[0] } } return "/" + id } 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. func (e *Engine) disabledRedirect() gin.HandlerFunc { return func(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/admin") } }