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:
@@ -1,5 +1,17 @@
|
|||||||
## Changelog
|
## 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)
|
### v0.28.2 — Async Deploy & AdventureLog Fix (2026-02-23)
|
||||||
|
|
||||||
#### Changed
|
#### Changed
|
||||||
|
|||||||
+17
-2
@@ -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`.
|
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).
|
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
|
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
|
#### App Info Pages
|
||||||
|
|
||||||
@@ -1132,7 +1147,7 @@ controller/
|
|||||||
│ │ └── templates/ # 7 wizard HTML templates (Hungarian)
|
│ │ └── templates/ # 7 wizard HTML templates (Hungarian)
|
||||||
│ ├── recovery/info.go # Recovery info file generator (recovery-info.txt)
|
│ ├── recovery/info.go # Recovery info file generator (recovery-info.txt)
|
||||||
│ └── web/
|
│ └── 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
|
│ ├── auth.go # Session auth + per-session CSRF token, login/logout, session cleanup
|
||||||
│ ├── csrf.go # CsrfProtect middleware, csrfToken/csrfField helpers
|
│ ├── csrf.go # CsrfProtect middleware, csrfToken/csrfField helpers
|
||||||
│ ├── handlers.go # Page handlers (dashboard, stacks, deploy, backups, etc.)
|
│ ├── handlers.go # Page handlers (dashboard, stacks, deploy, backups, etc.)
|
||||||
@@ -1143,7 +1158,7 @@ controller/
|
|||||||
│ ├── alerts.go # State-based alert generation
|
│ ├── alerts.go # State-based alert generation
|
||||||
│ ├── funcmap.go # Template functions (state colors, Hungarian formatting)
|
│ ├── funcmap.go # Template functions (state colors, Hungarian formatting)
|
||||||
│ ├── embed.go # go:embed for templates + Chart.js
|
│ ├── 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/
|
├── configs/
|
||||||
│ ├── controller.yaml.example # Full config reference
|
│ ├── controller.yaml.example # Full config reference
|
||||||
│ └── example-felhom-metadata.yml # .felhom.yml format reference
|
│ └── example-felhom-metadata.yml # .felhom.yml format reference
|
||||||
|
|||||||
@@ -720,7 +720,7 @@ func main() {
|
|||||||
// --- Start HTTP server ---
|
// --- Start HTTP server ---
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: cfg.Web.Listen,
|
Addr: cfg.Web.Listen,
|
||||||
Handler: mux,
|
Handler: webServer.CatchAllMiddleware(mux),
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadTimeout: 30 * time.Second,
|
||||||
WriteTimeout: 60 * time.Second,
|
WriteTimeout: 60 * time.Second,
|
||||||
IdleTimeout: 120 * time.Second,
|
IdleTimeout: 120 * time.Second,
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ services:
|
|||||||
- "traefik.http.routers.controller.tls=true"
|
- "traefik.http.routers.controller.tls=true"
|
||||||
- "traefik.http.services.controller.loadbalancer.server.port=8080"
|
- "traefik.http.services.controller.loadbalancer.server.port=8080"
|
||||||
- "traefik.docker.network=traefik-public"
|
- "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
|
# Health check labels for monitoring
|
||||||
- "felhom.managed=true"
|
- "felhom.managed=true"
|
||||||
- "felhom.component=controller"
|
- "felhom.component=controller"
|
||||||
|
|||||||
@@ -171,6 +171,21 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
data["CrossDriveFailed"] = crossDriveFailed
|
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 {
|
if s.alertManager != nil {
|
||||||
data["DiskWarnings"] = s.alertManager.GetInlineAlerts("dashboard")
|
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
|
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
|
// Storage info for already-deployed apps with HDD data
|
||||||
if alreadyDeployed {
|
if alreadyDeployed {
|
||||||
storageInfo := s.storageInfoForStack(name)
|
storageInfo := s.storageInfoForStack(name)
|
||||||
|
|||||||
@@ -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).
|
// ServeStorageAPI handles /api/storage/* routes (JSON API for disk operations).
|
||||||
func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
s.storageAPIHandler(w, r)
|
s.storageAPIHandler(w, r)
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{{define "catchall"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="hu">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{if .AppName}}{{.AppName}} — {{end}}felhom.eu</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{background:#0d1117;color:#e6edf3;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
||||||
|
.container{text-align:center;max-width:480px;padding:2rem}
|
||||||
|
.logo{width:80px;height:auto;margin-bottom:1.5rem;opacity:.85}
|
||||||
|
.app-name{font-size:1.5rem;font-weight:600;margin-bottom:.5rem}
|
||||||
|
.status-text{color:#8b949e;font-size:1.1rem;margin-bottom:2rem;line-height:1.5}
|
||||||
|
.status-icon{font-size:2.5rem;margin-bottom:1rem}
|
||||||
|
.status-stopped .status-icon{color:#da3633}
|
||||||
|
.status-not_deployed .status-icon{color:#8b949e}
|
||||||
|
.status-unknown .status-icon{color:#8b949e}
|
||||||
|
.btn{display:inline-block;padding:.6rem 1.2rem;border-radius:6px;text-decoration:none;font-size:.9rem;font-weight:500;transition:all .2s}
|
||||||
|
.btn-primary{background:#0088cc;color:#fff}
|
||||||
|
.btn-primary:hover{background:#00aaff}
|
||||||
|
.btn-outline{border:1px solid #30363d;color:#8b949e;margin-left:.5rem}
|
||||||
|
.btn-outline:hover{border-color:#8b949e;color:#e6edf3}
|
||||||
|
.host-label{color:#6e7681;font-size:.85rem;margin-top:2rem}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container status-{{.Status}}">
|
||||||
|
<img class="logo" src="{{.ControllerURL}}/static/felhom-logo.svg" alt="felhom.eu">
|
||||||
|
<div class="status-icon">
|
||||||
|
{{if eq .Status "stopped"}}⏸{{else if eq .Status "not_deployed"}}📦{{else}}🔍{{end}}
|
||||||
|
</div>
|
||||||
|
{{if .AppName}}<div class="app-name">{{.AppName}}</div>{{end}}
|
||||||
|
<div class="status-text">{{.StatusText}}</div>
|
||||||
|
<div>
|
||||||
|
{{if .AppSlug}}
|
||||||
|
<a class="btn btn-primary" href="{{.ControllerURL}}/apps/{{.AppSlug}}">Alkalmazás kezelése</a>
|
||||||
|
{{else}}
|
||||||
|
<a class="btn btn-primary" href="{{.ControllerURL}}/stacks">Alkalmazások</a>
|
||||||
|
{{end}}
|
||||||
|
<a class="btn btn-outline" href="{{.ControllerURL}}">Vezérlőpult</a>
|
||||||
|
</div>
|
||||||
|
<div class="host-label">{{.Host}}</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
@@ -174,6 +174,8 @@
|
|||||||
<a href="/stacks/{{.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">Telepítés</a>
|
<a href="/stacks/{{.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">Telepítés</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if isOperational .State}}
|
{{if isOperational .State}}
|
||||||
|
{{$subdomain := index $.Subdomains .Name}}
|
||||||
|
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">Megnyitás ↗</a>{{end}}
|
||||||
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">↻</button>
|
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">↻</button>
|
||||||
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')">■</button>
|
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')">■</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@@ -6,7 +6,20 @@
|
|||||||
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
|
<a href="/stacks" class="btn btn-sm btn-outline">← Vissza</a>
|
||||||
<h2>{{.Meta.DisplayName}} — {{if .AlreadyDeployed}}Beállítások{{else}}Telepítés{{end}}</h2>
|
<h2>{{.Meta.DisplayName}} — {{if .AlreadyDeployed}}Beállítások{{else}}Telepítés{{end}}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:.5rem">
|
||||||
|
{{if .AlreadyDeployed}}
|
||||||
|
{{if isOperational .Stack.State}}
|
||||||
|
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Stack.Name}}', 'restart')">Újraindítás</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="stackAction(event, '{{.Stack.Name}}', 'stop')">Leállítás</button>
|
||||||
|
{{else}}
|
||||||
|
<button class="btn btn-sm btn-success" onclick="stackAction(event, '{{.Stack.Name}}', 'start')">Indítás</button>
|
||||||
|
{{end}}
|
||||||
|
{{if and (isOperational .Stack.State) .EffectiveSubdomain}}
|
||||||
|
<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}" target="_blank" class="btn btn-sm btn-outline">Megnyitás ↗</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline">ℹ️ Részletek</a>
|
<a href="/apps/{{.Meta.Slug}}" class="btn btn-sm btn-outline">ℹ️ Részletek</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="deploy-container">
|
<div class="deploy-container">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3>{{.Meta.DisplayName}}</h3>
|
<h3>{{.Meta.DisplayName}}</h3>
|
||||||
{{$subdomain := index $.Subdomains .Name}}
|
{{$subdomain := index $.Subdomains .Name}}
|
||||||
{{if $subdomain}}
|
{{if and $subdomain .Deployed}}
|
||||||
<a class="subdomain-link" href="https://{{$subdomain}}.{{$.Domain}}" target="_blank">
|
<a class="subdomain-link" href="https://{{$subdomain}}.{{$.Domain}}" target="_blank">
|
||||||
{{$subdomain}}.{{$.Domain}} ↗
|
{{$subdomain}}.{{$.Domain}} ↗
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user