diff --git a/CHANGELOG.md b/CHANGELOG.md index c67b705..a95bfee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ ## Changelog +### v0.28.3 — Catch-All Page, Deploy Controls, Dashboard Open (2026-02-23) + +#### Added +- **Catch-all page for stopped/undeployed apps** — When a user visits a stopped app's subdomain (e.g., `travel.demo-felhom.eu`), they now see a branded felhom page with the app name and status ("Az alkalmazás jelenleg le van állítva") instead of Traefik's raw 404. Implemented via a low-priority (1) Traefik catch-all router on the controller container + `CatchAllMiddleware` in `server.go` that intercepts non-controller hosts and renders standalone `catchall.html` without auth. +- **Start/Stop/Restart buttons on deploy settings page** — Deployed apps now show Indítás/Leállítás/Újraindítás buttons in the page header, plus a "Megnyitás ↗" link to the app's subdomain (visible when running). Previously the deploy page had no state controls. +- **"Megnyitás ↗" button on Vezérlőpult** — Running apps on the dashboard now show an open button that launches the app in a new tab. Uses the `Subdomains` map built from `app.yaml` SUBDOMAIN env with metadata fallback. +- **`findStackBySubdomain()`** helper in `server.go` — looks up stacks by subdomain, checking deployed `app.yaml` env first, then `.felhom.yml` metadata. + +#### Changed +- **Subdomain links on Alkalmazások page** — Links now only shown for deployed apps (previously shown for all apps including non-deployed ones where the subdomain isn't final yet). +- **`docker-compose.yml`** — Added 6 catch-all Traefik router labels (`traefik.http.routers.catchall.*`) with `priority=1` and `certresolver=letsencrypt`. + ### v0.28.2 — Async Deploy & AdventureLog Fix (2026-02-23) #### Changed diff --git a/controller/README.md b/controller/README.md index 9b912cf..6fcd7a6 100644 --- a/controller/README.md +++ b/controller/README.md @@ -145,6 +145,21 @@ The app catalog lives in a separate Git repository. The controller: 5. Pre-generated secret values are submitted as hidden form inputs so the **same values** the user saw are saved to `app.yaml` (no silent re-generation on submit). Controller saves `app.yaml`, sets in-memory `Deployed` + `Deploying` flags, then runs `docker compose up -d` **asynchronously** in a goroutine — API returns immediately so the UI switches to the progress panel without waiting for image pulls. On failure the goroutine reverts both disk and in-memory state and sets `DeployError`. 6. 3-step progress panel polls `GET /api/stacks/{name}` every 3s: config saved → `deploying` (pulling images) → containers starting → health check passed. New `StateDeploying` state shown while compose-up is in progress (no containers yet). 7. Post-deploy: locked fields (DB_PASSWORD, etc.) become read-only; the "Automatikusan generált értékek" section continues to show the saved values on the settings page +8. The deploy/settings page includes **start/stop/restart** buttons for deployed apps, plus a "Megnyitás ↗" link to the app's subdomain URL (only visible when running) + +#### Catch-All Page for Stopped Apps + +When a user visits a stopped or undeployed app's subdomain (e.g., `travel.demo-felhom.eu`), the controller serves a branded error page instead of Traefik's raw 404: + +- **Traefik catch-all router**: The controller's `docker-compose.yml` registers a second router (`catchall`) with `priority=1` (lowest) and `HostRegexp(.+)`. Running apps always win; only requests with no matching container reach the controller. +- **`CatchAllMiddleware`** in `server.go` intercepts requests where `Host` ≠ `felhom.DOMAIN`, serves the catch-all page **without auth** (user has no session on the app subdomain). +- **`findStackBySubdomain()`** identifies the app by matching the subdomain against deployed `app.yaml` `SUBDOMAIN` env or metadata fallback. +- **`catchall.html`** — standalone template (no layout, inline CSS) showing the app name, status ("leállítva" / "nincs telepítve" / "nem található"), and links to the controller dashboard or the app's detail page. +- **Subdomain links** on the Alkalmazások page are only shown for deployed apps (non-deployed apps have no guaranteed subdomain yet). + +#### Dashboard "Megnyitás" Button + +Running apps on the Vezérlőpult now show a "Megnyitás ↗" button that opens the app's subdomain in a new tab. The `Subdomains` map is built in `dashboardHandler` from `app.yaml` env or metadata fallback. #### App Info Pages @@ -1132,7 +1147,7 @@ controller/ │ │ └── templates/ # 7 wizard HTML templates (Hungarian) │ ├── recovery/info.go # Recovery info file generator (recovery-info.txt) │ └── web/ -│ ├── server.go # HTTP server, routing, static files, executeTemplate wrapper +│ ├── server.go # HTTP server, routing, static files, catch-all middleware, executeTemplate wrapper │ ├── auth.go # Session auth + per-session CSRF token, login/logout, session cleanup │ ├── csrf.go # CsrfProtect middleware, csrfToken/csrfField helpers │ ├── handlers.go # Page handlers (dashboard, stacks, deploy, backups, etc.) @@ -1143,7 +1158,7 @@ controller/ │ ├── alerts.go # State-based alert generation │ ├── funcmap.go # Template functions (state colors, Hungarian formatting) │ ├── embed.go # go:embed for templates + Chart.js -│ └── templates/ # 14 HTML files + style.css (Hungarian UI, incl. debug.html) +│ └── templates/ # 15 HTML files + style.css (Hungarian UI, incl. debug.html, catchall.html) ├── configs/ │ ├── controller.yaml.example # Full config reference │ └── example-felhom-metadata.yml # .felhom.yml format reference diff --git a/controller/cmd/controller/main.go b/controller/cmd/controller/main.go index 9d8999c..50ab82d 100644 --- a/controller/cmd/controller/main.go +++ b/controller/cmd/controller/main.go @@ -720,7 +720,7 @@ func main() { // --- Start HTTP server --- server := &http.Server{ Addr: cfg.Web.Listen, - Handler: mux, + Handler: webServer.CatchAllMiddleware(mux), ReadTimeout: 30 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, diff --git a/controller/docker-compose.yml b/controller/docker-compose.yml index 5a01b7f..c363d05 100644 --- a/controller/docker-compose.yml +++ b/controller/docker-compose.yml @@ -53,6 +53,13 @@ services: - "traefik.http.routers.controller.tls=true" - "traefik.http.services.controller.loadbalancer.server.port=8080" - "traefik.docker.network=traefik-public" + # Catch-all: branded error page for stopped/undeployed app subdomains + - "traefik.http.routers.catchall.rule=HostRegexp(`.+`)" + - "traefik.http.routers.catchall.priority=1" + - "traefik.http.routers.catchall.entrypoints=websecure" + - "traefik.http.routers.catchall.tls=true" + - "traefik.http.routers.catchall.tls.certresolver=letsencrypt" + - "traefik.http.routers.catchall.service=controller" # Health check labels for monitoring - "felhom.managed=true" - "felhom.component=controller" diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index 2cf6f90..1b98a5b 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -171,6 +171,21 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) { data["CrossDriveFailed"] = crossDriveFailed } + // Build subdomain map for "Megnyitás" buttons + subdomains := make(map[string]string) + for _, stack := range deployedStacks { + 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 + } + } + } + subdomains[stack.Name] = stack.Meta.Subdomain + } + data["Subdomains"] = subdomains + if s.alertManager != nil { data["DiskWarnings"] = s.alertManager.GetInlineAlerts("dashboard") } @@ -303,6 +318,13 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri } data["StoragePaths"] = deployPaths + // Effective subdomain for "Megnyitás" button + if alreadyDeployed && appCfg != nil { + if sd, ok := appCfg.Env["SUBDOMAIN"]; ok && sd != "" { + data["EffectiveSubdomain"] = sd + } + } + // Storage info for already-deployed apps with HDD data if alreadyDeployed { storageInfo := s.storageInfoForStack(name) diff --git a/controller/internal/web/server.go b/controller/internal/web/server.go index 9f016ed..a8380fe 100644 --- a/controller/internal/web/server.go +++ b/controller/internal/web/server.go @@ -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) diff --git a/controller/internal/web/templates/catchall.html b/controller/internal/web/templates/catchall.html new file mode 100644 index 0000000..e44c72b --- /dev/null +++ b/controller/internal/web/templates/catchall.html @@ -0,0 +1,47 @@ +{{define "catchall"}} + + + + + +{{if .AppName}}{{.AppName}} — {{end}}felhom.eu + + + +
+ +
+ {{if eq .Status "stopped"}}⏸{{else if eq .Status "not_deployed"}}📦{{else}}🔍{{end}} +
+ {{if .AppName}}
{{.AppName}}
{{end}} +
{{.StatusText}}
+
+ {{if .AppSlug}} + Alkalmazás kezelése + {{else}} + Alkalmazások + {{end}} + Vezérlőpult +
+
{{.Host}}
+
+ + +{{end}} diff --git a/controller/internal/web/templates/dashboard.html b/controller/internal/web/templates/dashboard.html index 667fcb9..2670732 100644 --- a/controller/internal/web/templates/dashboard.html +++ b/controller/internal/web/templates/dashboard.html @@ -174,6 +174,8 @@ Telepítés {{else}} {{if isOperational .State}} + {{$subdomain := index $.Subdomains .Name}} + {{if $subdomain}}Megnyitás ↗{{end}} {{else}} diff --git a/controller/internal/web/templates/deploy.html b/controller/internal/web/templates/deploy.html index 8a3fa26..6b20344 100644 --- a/controller/internal/web/templates/deploy.html +++ b/controller/internal/web/templates/deploy.html @@ -6,7 +6,20 @@ ← Vissza

{{.Meta.DisplayName}} — {{if .AlreadyDeployed}}Beállítások{{else}}Telepítés{{end}}

- ℹ️ Részletek +
+ {{if .AlreadyDeployed}} + {{if isOperational .Stack.State}} + + + {{else}} + + {{end}} + {{if and (isOperational .Stack.State) .EffectiveSubdomain}} + Megnyitás ↗ + {{end}} + {{end}} + ℹ️ Részletek +
diff --git a/controller/internal/web/templates/stacks.html b/controller/internal/web/templates/stacks.html index bbf9b67..4c2f5cf 100644 --- a/controller/internal/web/templates/stacks.html +++ b/controller/internal/web/templates/stacks.html @@ -25,7 +25,7 @@

{{.Meta.DisplayName}}

{{$subdomain := index $.Subdomains .Name}} - {{if $subdomain}} + {{if and $subdomain .Deployed}} {{$subdomain}}.{{$.Domain}} ↗