diff --git a/nginx.conf.template b/nginx.conf.template index af89c8b..de564e7 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -65,6 +65,7 @@ server { location ${BASE_PATH}/admin { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location ${BASE_PATH}/team-admin { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location ${BASE_PATH}/settings { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } + location ${BASE_PATH}/welcome { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location ${BASE_PATH}/w/ { proxy_pass $backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # Extension surface page routes → backend diff --git a/server/pages/pages.go b/server/pages/pages.go index 4e51acd..c44ef5b 100644 --- a/server/pages/pages.go +++ b/server/pages/pages.go @@ -244,6 +244,11 @@ func (e *Engine) registerCoreSurfaces() { Title: "Workflow Landing", Template: "workflow-landing", Auth: "public", Layout: "single", Source: "core", }, + { + ID: "welcome", Route: "/welcome", + Title: "Welcome", Template: "surface-welcome", Auth: "authenticated", + Layout: "single", Source: "core", + }, } } diff --git a/server/pages/pages_surfaces.go b/server/pages/pages_surfaces.go index 3370e7c..68f8655 100644 --- a/server/pages/pages_surfaces.go +++ b/server/pages/pages_surfaces.go @@ -91,22 +91,35 @@ func (e *Engine) EnabledSurfaceIDs() []string { // DefaultSurfaceRedirect returns a handler for GET / that redirects to the // configured default surface, falling back to the first enabled extension -// surface, then /admin. +// surface, then /welcome. func (e *Engine) DefaultSurfaceRedirect() gin.HandlerFunc { return func(c *gin.Context) { - target := e.resolveDefaultSurface(c.Request.Context()) + userID := c.GetString("user_id") + target := e.resolveDefaultSurface(c.Request.Context(), userID) 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 { +// Priority: user preference → global default_surface → first enabled extension → /welcome. +func (e *Engine) resolveDefaultSurface(ctx context.Context, userID string) string { if e.stores.GlobalConfig == nil || e.stores.Packages == nil { - return "/admin" + return "/welcome" } - // 1. Check configured default_surface + // 1. Check user preference (default_surface in user settings) + if userID != "" && e.stores.Users != nil { + if user, err := e.stores.Users.GetByID(ctx, userID); err == nil && user != nil { + if id, _ := user.Settings["default_surface"].(string); id != "" { + if path := e.surfacePath(ctx, id); path != "" { + return path + } + // User's chosen surface is missing or disabled — fall through + } + } + } + + // 2. Check admin-configured global 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 != "" { @@ -116,7 +129,7 @@ func (e *Engine) resolveDefaultSurface(ctx context.Context) string { } } - // 2. First enabled extension surface + // 3. First enabled extension surface surfaces, err := e.stores.Packages.ListEnabledByType(ctx, "surface") if err == nil { for _, s := range surfaces { @@ -129,8 +142,8 @@ func (e *Engine) resolveDefaultSurface(ctx context.Context) string { } } - // 3. Fallback - return "/admin" + // 4. Fallback — welcome surface (no extensions installed) + return "/welcome" } // surfacePath returns the URL path for a surface ID, or "" if the surface @@ -155,9 +168,8 @@ func (e *Engine) surfacePath(ctx context.Context, id string) string { 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. +// disabledRedirect returns a handler that redirects to /. +// The DefaultSurfaceRedirect handler will resolve to the appropriate surface. func (e *Engine) disabledRedirect() gin.HandlerFunc { return func(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/admin") diff --git a/server/pages/templates/base.html b/server/pages/templates/base.html index 5750e37..59e77bc 100644 --- a/server/pages/templates/base.html +++ b/server/pages/templates/base.html @@ -89,6 +89,7 @@ {{if eq .Surface "admin"}}{{template "surface-admin" .}} {{else if eq .Surface "team-admin"}}{{template "surface-team-admin" .}} {{else if eq .Surface "settings"}}{{template "surface-settings" .}} + {{else if eq .Surface "welcome"}}{{template "surface-welcome" .}} {{else if and .Manifest (eq .Manifest.Source "extension")}}{{template "surface-extension" .}} {{else}}
+ No surfaces are installed yet. Install an extension surface + to get started. +
+ ${isAdmin && html` + + Go to Packages + + `} + ${!isAdmin && html` ++ Ask your administrator to install a surface package. +
+ `} +