Feat default surface routing (#4)
Some checks failed
CI/CD / detect-changes (push) Successful in 3s
CI/CD / test-frontend (push) Successful in 5s
CI/CD / test-go-pg (push) Failing after 2m17s
CI/CD / test-sqlite (push) Successful in 2m44s
CI/CD / build-and-deploy (push) Has been skipped

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #4.
This commit is contained in:
2026-03-26 19:56:14 +00:00
committed by xcaliber
parent a36be83aaf
commit f4512071db
9 changed files with 169 additions and 40 deletions

View File

@@ -391,7 +391,7 @@ func (e *Engine) RenderExtensionSurface() gin.HandlerFunc {
return
}
if !sr.Enabled {
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/")
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/admin")
return
}
if sr.Source == "core" {

View File

@@ -90,9 +90,77 @@ func (e *Engine) EnabledSurfaceIDs() []string {
return ids
}
// disabledRedirect returns a handler that redirects to the root.
func (e *Engine) disabledRedirect() gin.HandlerFunc {
// 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) {
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/")
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")
}
}