Indítópult launcher page + universal app placeholder icon (v0.163.0)
New /launcher page: a grid of large tappable tiles, one per openable deployed app (subdomain presence is the single openability criterion, shared with the Megnyitás button via the extracted Server.subdomainMap helper). Colored tiles (deterministic slug color or .felhom.yml brand_color), white glyph/monogram fallback, target=_blank links for operational apps, greyed unclickable tiles for stopped ones. First sidebar item; / stays the Vezérlőpult. Universal app placeholder: new AppPlaceholderSVG served at /static/app-placeholder.svg, now the default FallbackIcon on app_list_row so a logo-less app shows a placeholder instead of visibility:hidden. Brand mark is never an app placeholder. New Metadata.BrandColor; new funcmap tileColor/initial. 10 new test functions + 4 red-proofs. No agent coupling; MinAgent unchanged.
This commit is contained in:
@@ -187,8 +187,22 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Build subdomain map for "Megnyitás" buttons
|
||||
data["Subdomains"] = s.subdomainMap(deployedStacks)
|
||||
|
||||
if s.alertManager != nil {
|
||||
data["DiskWarnings"] = s.alertManager.GetInlineAlerts("dashboard")
|
||||
}
|
||||
|
||||
s.executeTemplate(w, r, "dashboard", data)
|
||||
}
|
||||
|
||||
// subdomainMap builds the stack-name → subdomain lookup used by the "Megnyitás" open links (dashboard
|
||||
// + Alkalmazások) and the Indítópult launcher. Priority — unchanged from the two inline copies it
|
||||
// replaces: the deployed app.yaml SUBDOMAIN env wins, then the .felhom.yml Meta.Subdomain, then the
|
||||
// well-known protectedStackSubdomains fallback. A stack with no source of a subdomain gets no entry.
|
||||
func (s *Server) subdomainMap(stackList []stacks.Stack) map[string]string {
|
||||
subdomains := make(map[string]string)
|
||||
for _, stack := range deployedStacks {
|
||||
for _, stack := range stackList {
|
||||
if stack.Deployed {
|
||||
if appCfg := s.stackMgr.LoadAppConfigByName(stack.Name); appCfg != nil {
|
||||
if sd, ok := appCfg.Env["SUBDOMAIN"]; ok && sd != "" {
|
||||
@@ -203,13 +217,74 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
subdomains[stack.Name] = sd
|
||||
}
|
||||
}
|
||||
data["Subdomains"] = subdomains
|
||||
return subdomains
|
||||
}
|
||||
|
||||
if s.alertManager != nil {
|
||||
data["DiskWarnings"] = s.alertManager.GetInlineAlerts("dashboard")
|
||||
// controllerStackName is the controller's own stack — it IS this dashboard, so it never appears as a
|
||||
// launchable app even if a subdomain mapping somehow exists for it.
|
||||
const controllerStackName = "felhom-controller"
|
||||
|
||||
// LauncherApp is one openable app tile for the Indítópult (launcher) page.
|
||||
type LauncherApp struct {
|
||||
Name string
|
||||
DisplayName string
|
||||
Slug string
|
||||
State stacks.ContainerState
|
||||
Subdomain string
|
||||
OpenPath string
|
||||
BrandColor string
|
||||
}
|
||||
|
||||
// buildLauncherApps builds the sorted launcher tile list from the deployed/protected stacks and the
|
||||
// subdomain lookup. A tile exists exactly when a "Megnyitás" button would — i.e. the stack has a
|
||||
// subdomain entry — minus the controller itself. Sorted by DisplayName (tie-broken by Name) so the
|
||||
// grid order is stable regardless of Go's map iteration order.
|
||||
func buildLauncherApps(stackList []stacks.Stack, subdomains map[string]string) []LauncherApp {
|
||||
var apps []LauncherApp
|
||||
for _, st := range stackList {
|
||||
if st.Name == controllerStackName {
|
||||
continue
|
||||
}
|
||||
sd, ok := subdomains[st.Name]
|
||||
if !ok || sd == "" {
|
||||
continue
|
||||
}
|
||||
dn := st.Meta.DisplayName
|
||||
if dn == "" {
|
||||
dn = st.Name
|
||||
}
|
||||
apps = append(apps, LauncherApp{
|
||||
Name: st.Name,
|
||||
DisplayName: dn,
|
||||
Slug: st.Meta.Slug,
|
||||
State: st.State,
|
||||
Subdomain: sd,
|
||||
OpenPath: st.Meta.OpenPath,
|
||||
BrandColor: st.Meta.BrandColor,
|
||||
})
|
||||
}
|
||||
sort.Slice(apps, func(i, j int) bool {
|
||||
if apps[i].DisplayName == apps[j].DisplayName {
|
||||
return apps[i].Name < apps[j].Name
|
||||
}
|
||||
return apps[i].DisplayName < apps[j].DisplayName
|
||||
})
|
||||
return apps
|
||||
}
|
||||
|
||||
s.executeTemplate(w, r, "dashboard", data)
|
||||
// launcherHandler renders the Indítópult: a grid of large tappable tiles, one per openable deployed
|
||||
// app (subdomain presence is the single openability criterion — see buildLauncherApps). Behind
|
||||
// RequireAuth like every page; the "/" landing page stays the Vezérlőpult.
|
||||
func (s *Server) launcherHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var eligible []stacks.Stack
|
||||
for _, st := range s.stackMgr.GetStacks() {
|
||||
if st.Deployed || st.Protected {
|
||||
eligible = append(eligible, st)
|
||||
}
|
||||
}
|
||||
data := s.baseData("launcher", "Indítópult")
|
||||
data["Apps"] = buildLauncherApps(eligible, s.subdomainMap(eligible))
|
||||
s.executeTemplate(w, r, "launcher", data)
|
||||
}
|
||||
|
||||
// visibleCatalogStacks drops templates that are no longer OFFERED for new installs (lifecycle
|
||||
@@ -263,23 +338,7 @@ func (s *Server) stacksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data["StorageLabels"] = storageLabels
|
||||
|
||||
// Build effective subdomain lookup (stored env > metadata > well-known fallback)
|
||||
subdomains := make(map[string]string)
|
||||
for _, stack := range s.stackMgr.GetStacks() {
|
||||
if stack.Deployed {
|
||||
if appCfg := s.stackMgr.LoadAppConfigByName(stack.Name); appCfg != nil {
|
||||
if sd, ok := appCfg.Env["SUBDOMAIN"]; ok && sd != "" {
|
||||
subdomains[stack.Name] = sd
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if stack.Meta.Subdomain != "" {
|
||||
subdomains[stack.Name] = stack.Meta.Subdomain
|
||||
} else if sd, ok := protectedStackSubdomains[stack.Name]; ok {
|
||||
subdomains[stack.Name] = sd
|
||||
}
|
||||
}
|
||||
data["Subdomains"] = subdomains
|
||||
data["Subdomains"] = s.subdomainMap(s.stackMgr.GetStacks())
|
||||
|
||||
s.executeTemplate(w, r, "stacks", data)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user