Changeset 0.27.0 (#166)
This commit is contained in:
@@ -91,6 +91,9 @@ type PageData struct {
|
||||
// v0.25.0: Enabled surface IDs for conditional nav rendering.
|
||||
EnabledSurfaces []string `json:"-"`
|
||||
|
||||
// v0.27.0: Extension surfaces for sidebar nav rendering.
|
||||
ExtensionSurfaces []ExtensionNavItem `json:"-"`
|
||||
|
||||
// v0.22.7: Login/splash page fields
|
||||
InstanceName string // branding: instance display name
|
||||
LogoURL string // branding: custom logo URL
|
||||
@@ -110,6 +113,52 @@ func (pd PageData) SurfaceEnabled(id string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ExtensionNavItem holds the minimal info needed to render an extension
|
||||
// surface link in the sidebar nav.
|
||||
type ExtensionNavItem struct {
|
||||
ID string
|
||||
Title string
|
||||
Route string
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if e.stores.Surfaces == nil {
|
||||
return nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
surfaces, err := e.stores.Surfaces.List(ctx)
|
||||
if err != nil {
|
||||
log.Printf("[pages] Failed to load extension surfaces for nav: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var items []ExtensionNavItem
|
||||
for _, sr := range surfaces {
|
||||
if sr.Source == "core" || !sr.Enabled {
|
||||
continue
|
||||
}
|
||||
// Editor is Source="extension" but hardcoded as core — skip from dynamic nav
|
||||
if sr.ID == "editor" {
|
||||
continue
|
||||
}
|
||||
route := ""
|
||||
if sr.Manifest != nil {
|
||||
route, _ = sr.Manifest["route"].(string)
|
||||
}
|
||||
if route == "" {
|
||||
route = "/s/" + sr.ID
|
||||
}
|
||||
items = append(items, ExtensionNavItem{
|
||||
ID: sr.ID,
|
||||
Title: sr.Title,
|
||||
Route: route,
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// UserContext is the authenticated user's info available to templates.
|
||||
type UserContext struct {
|
||||
ID string `json:"id"`
|
||||
@@ -297,12 +346,74 @@ func (e *Engine) RenderSurface(surfaceID string) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
e.Render(c, "base.html", PageData{
|
||||
Surface: surfaceID,
|
||||
Section: section,
|
||||
User: user,
|
||||
Data: data,
|
||||
Manifest: e.GetSurface(surfaceID),
|
||||
EnabledSurfaces: e.EnabledSurfaceIDs(),
|
||||
Surface: surfaceID,
|
||||
Section: section,
|
||||
User: user,
|
||||
Data: data,
|
||||
Manifest: e.GetSurface(surfaceID),
|
||||
EnabledSurfaces: e.EnabledSurfaceIDs(),
|
||||
ExtensionSurfaces: e.extensionNavItems(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RenderExtensionSurface handles /s/:slug — dynamic DB lookup for extension surfaces.
|
||||
// No restart required after installing a surface via admin API.
|
||||
func (e *Engine) RenderExtensionSurface() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
slug := c.Param("slug")
|
||||
if slug == "" {
|
||||
c.String(http.StatusNotFound, "Surface not found")
|
||||
return
|
||||
}
|
||||
|
||||
if e.stores.Surfaces == nil {
|
||||
c.String(http.StatusNotFound, "Surface registry not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Look up by ID (slug == surface ID)
|
||||
sr, err := e.stores.Surfaces.Get(c.Request.Context(), slug)
|
||||
if err != nil || sr == nil {
|
||||
c.String(http.StatusNotFound, "Surface not found: "+slug)
|
||||
return
|
||||
}
|
||||
if !sr.Enabled {
|
||||
c.Redirect(http.StatusTemporaryRedirect, e.cfg.BasePath+"/")
|
||||
return
|
||||
}
|
||||
if sr.Source == "core" {
|
||||
// Core surfaces have their own routes — don't serve them here
|
||||
c.String(http.StatusNotFound, "Surface not found: "+slug)
|
||||
return
|
||||
}
|
||||
|
||||
user := e.getUserContext(c)
|
||||
|
||||
// Build manifest from DB record
|
||||
route, _ := sr.Manifest["route"].(string)
|
||||
if route == "" {
|
||||
route = "/s/" + sr.ID
|
||||
}
|
||||
layout, _ := sr.Manifest["layout"].(string)
|
||||
if layout == "" {
|
||||
layout = "single"
|
||||
}
|
||||
manifest := &SurfaceManifest{
|
||||
ID: sr.ID,
|
||||
Route: route,
|
||||
Title: sr.Title,
|
||||
Template: "surface-extension",
|
||||
Layout: layout,
|
||||
Source: "extension",
|
||||
}
|
||||
|
||||
e.Render(c, "base.html", PageData{
|
||||
Surface: sr.ID,
|
||||
User: user,
|
||||
Manifest: manifest,
|
||||
EnabledSurfaces: e.EnabledSurfaceIDs(),
|
||||
ExtensionSurfaces: e.extensionNavItems(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -341,6 +452,10 @@ func (e *Engine) RegisterPageRoutes(base *gin.RouterGroup, mw PageRouteMiddlewar
|
||||
handler := e.RenderSurface(s.ID)
|
||||
registerRoutes(group, s, handler)
|
||||
}
|
||||
|
||||
// v0.27.0: Extension surface catch-all — DB lookup at request time.
|
||||
// No restart needed after installing new surfaces via admin API.
|
||||
group.GET("/s/:slug", e.RenderExtensionSurface())
|
||||
}
|
||||
|
||||
// Admin surfaces — auth + admin role middleware
|
||||
|
||||
Reference in New Issue
Block a user