feat: catch-all page for stopped apps, deploy controls, dashboard open button
Stopped/undeployed app subdomains now show a branded page instead of Traefik 404. Deploy settings page gains start/stop/restart controls. Dashboard shows "Megnyitás" button for running apps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -285,6 +285,88 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// CatchAllMiddleware intercepts requests to non-controller hosts and serves
|
||||
// a branded error page (for stopped/undeployed app subdomains). Requests to
|
||||
// the controller host (felhom.DOMAIN) pass through normally.
|
||||
func (s *Server) CatchAllMiddleware(next http.Handler) http.Handler {
|
||||
controllerHost := "felhom." + s.cfg.Customer.Domain
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.Host
|
||||
if idx := strings.LastIndex(host, ":"); idx != -1 {
|
||||
host = host[:idx]
|
||||
}
|
||||
if strings.EqualFold(host, controllerHost) || host == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
s.serveCatchAll(w, r, host)
|
||||
})
|
||||
}
|
||||
|
||||
// serveCatchAll renders a branded page for requests reaching a stopped/undeployed
|
||||
// app subdomain. Served without auth since the user has no session on this host.
|
||||
func (s *Server) serveCatchAll(w http.ResponseWriter, r *http.Request, host string) {
|
||||
domain := s.cfg.Customer.Domain
|
||||
subdomain := ""
|
||||
suffix := "." + domain
|
||||
if strings.HasSuffix(host, suffix) {
|
||||
subdomain = strings.TrimSuffix(host, suffix)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Domain": domain,
|
||||
"ControllerURL": "https://felhom." + domain,
|
||||
"Host": host,
|
||||
}
|
||||
|
||||
if subdomain != "" {
|
||||
if stack, ok := s.findStackBySubdomain(subdomain); ok {
|
||||
data["AppName"] = stack.Meta.DisplayName
|
||||
data["AppSlug"] = stack.Meta.Slug
|
||||
data["AppLogoURL"] = s.cfg.AppLogoURL(stack.Meta.Slug)
|
||||
if stack.Deployed {
|
||||
data["Status"] = "stopped"
|
||||
data["StatusText"] = "Az alkalmazás jelenleg le van állítva"
|
||||
} else {
|
||||
data["Status"] = "not_deployed"
|
||||
data["StatusText"] = "Az alkalmazás nincs telepítve"
|
||||
}
|
||||
} else {
|
||||
data["Status"] = "unknown"
|
||||
data["StatusText"] = "Ez az oldal nem található"
|
||||
}
|
||||
} else {
|
||||
data["Status"] = "unknown"
|
||||
data["StatusText"] = "Ez az oldal nem található"
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
if err := s.tmpl.ExecuteTemplate(w, "catchall", data); err != nil {
|
||||
s.logger.Printf("[ERROR] Catch-all template error: %v", err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// findStackBySubdomain looks up the stack that owns the given subdomain.
|
||||
func (s *Server) findStackBySubdomain(subdomain string) (*stacks.Stack, bool) {
|
||||
for _, stack := range s.stackMgr.GetStacks() {
|
||||
// Check deployed app.yaml SUBDOMAIN env first
|
||||
if stack.Deployed {
|
||||
if appCfg := s.stackMgr.LoadAppConfigByName(stack.Name); appCfg != nil {
|
||||
if sd, ok := appCfg.Env["SUBDOMAIN"]; ok && sd == subdomain {
|
||||
return &stack, true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to metadata subdomain
|
||||
if stack.Meta.Subdomain == subdomain {
|
||||
return &stack, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ServeStorageAPI handles /api/storage/* routes (JSON API for disk operations).
|
||||
func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) {
|
||||
s.storageAPIHandler(w, r)
|
||||
|
||||
Reference in New Issue
Block a user