Feat default surface routing + fix startup hang (#4)
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 4s
CI/CD / test-go-pg (pull_request) Failing after 2m18s
CI/CD / test-sqlite (pull_request) Successful in 2m40s
CI/CD / build-and-deploy (pull_request) Has been skipped
Some checks failed
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Successful in 4s
CI/CD / test-go-pg (pull_request) Failing after 2m18s
CI/CD / test-sqlite (pull_request) Successful in 2m40s
CI/CD / build-and-deploy (pull_request) Has been skipped
Default surface routing (v0.2.1): - `/` redirects to configurable default surface with fallback chain: configured → first enabled extension surface → /admin - Auto-sets default_surface on first extension surface install - Admin settings UI: Default Surface dropdown - disabledRedirect() and extension surface disabled redirect now go to /admin instead of / to prevent redirect loops Fix Gin route param conflict causing backend startup hang: - Team package settings routes used `:pkgId` while sibling routes used `:id` — Gin's radix tree entered infinite loop on conflicting param names. Unified to `:id`. Docker entrypoint hardening: - Health check timeout 10s → 60s - Stale backend process cleanup on restart - Crash detection (exit early if backend dies) Roadmap direction updates: - v0.3.0: Notes surface (Obsidian-style) replaces Editor surface - v0.4.0: MVP (was v0.5.0) — chat moved to post-MVP - No built-in auto-install — explicit install only Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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" {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user