From 4f06149abc014589fec970c2ede71249bc7e74e7 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 13 Jul 2026 11:31:38 +0200 Subject: [PATCH] =?UTF-8?q?Part=20B:=20infra=20stacks=20carry=20identity?= =?UTF-8?q?=20=E2=80=94=20curated=20Hungarian=20metadata=20for=20cloudflar?= =?UTF-8?q?ed/traefik/filebrowser=20(v0.126.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - inframeta.go: static display-only map (name/description/Linked) + infraMeta template func; filebrowser is the ONLY Linked stack (files. Megnyitás on the dashboard row; the stacks card already links via Subdomains) - generic infra icon: embedded /static/infra-logo.svg (Lucide-style server, monochrome) wired through the app-row FallbackIcon slot + the stacks-card onerror chain - dashboard rows + app cards: infra name + description + existing Védett chip - guarded WRONG outcome: no customer link for cloudflared/traefik — render tests count exactly ONE https:// link; red-proven (Linked:true on cloudflared → test FAILS) --- controller/internal/web/funcmap.go | 3 + controller/internal/web/inframeta.go | 48 ++++++++ controller/internal/web/inframeta_test.go | 103 ++++++++++++++++++ controller/internal/web/server.go | 4 + .../internal/web/templates/dashboard.html | 11 ++ controller/internal/web/templates/stacks.html | 11 +- 6 files changed, 176 insertions(+), 4 deletions(-) create mode 100644 controller/internal/web/inframeta.go create mode 100644 controller/internal/web/inframeta_test.go diff --git a/controller/internal/web/funcmap.go b/controller/internal/web/funcmap.go index e4f35a8..3b3677a 100644 --- a/controller/internal/web/funcmap.go +++ b/controller/internal/web/funcmap.go @@ -403,6 +403,9 @@ func (s *Server) templateFuncMap() template.FuncMap { } return s.cfg.AppPageURL(slug) }, + // infraMeta resolves a protected infra stack's curated Hungarian identity + // (inframeta.go); nil for regular apps — templates branch on it. + "infraMeta": infraMetaFor, // pageMatch returns true if currentPage is in the pages slice. // Used to filter page-specific alerts in layout.html. "pageMatch": func(pages []string, currentPage string) bool { diff --git a/controller/internal/web/inframeta.go b/controller/internal/web/inframeta.go new file mode 100644 index 0000000..1e17082 --- /dev/null +++ b/controller/internal/web/inframeta.go @@ -0,0 +1,48 @@ +package web + +// Infra-stack display metadata (v0.126.0 Part B). The protected base stacks (infra.go: +// traefik, cloudflared, filebrowser) carry no catalog metadata, so they rendered as bare +// directory names on the dashboard and app cards. This map gives them curated Hungarian +// identity — DISPLAY ONLY, keyed by stack name; it never affects protection, deploy or +// backup logic. +// +// Linked marks the ONLY infra stack with a customer-facing UI (filebrowser → +// files.). cloudflared and traefik have no customer UI — rendering an open link +// for them is the guarded-wrong outcome (render test asserts absence). + +// InfraMeta is one protected infra stack's customer-facing identity. +type InfraMeta struct { + DisplayName string + Description string + Linked bool // has a customer-facing URL (subdomain via protectedStackSubdomains) +} + +var infraMetaMap = map[string]InfraMeta{ + "cloudflared": { + DisplayName: "Cloudflare Tunnel", + Description: "Biztonságos internetkapcsolat — a szerver portnyitás nélkül érhető el kívülről.", + }, + "traefik": { + DisplayName: "Traefik", + Description: "Forgalomirányító (reverse proxy) — a kéréseket a megfelelő alkalmazáshoz irányítja.", + }, + "filebrowser": { + DisplayName: "FileBrowser", + Description: "Fájlkezelő — a tárhely fájljainak böngészése a böngészőből.", + Linked: true, + }, +} + +// infraMetaFor returns the curated metadata for a protected infra stack, nil for +// everything else (templates branch on the nil). +func infraMetaFor(stackName string) *InfraMeta { + if m, ok := infraMetaMap[stackName]; ok { + return &m + } + return nil +} + +// InfraLogoSVG is the shared generic infra icon (Lucide-style "server", monochrome, +// --text-2 stroke) — the last-resort icon for infra stacks whose logo asset is not +// synced. Served at /static/infra-logo.svg; consumed via the app-row FallbackIcon slot. +const InfraLogoSVG = `` diff --git a/controller/internal/web/inframeta_test.go b/controller/internal/web/inframeta_test.go new file mode 100644 index 0000000..9fe4026 --- /dev/null +++ b/controller/internal/web/inframeta_test.go @@ -0,0 +1,103 @@ +package web + +import ( + "strings" + "testing" + + "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" + "gitea.dooplex.hu/admin/felhom-controller/internal/system" +) + +// v0.126.0 Part B — infra stacks carry curated identity (name + description + Védett chip), +// and the guarded-wrong outcome: NO customer link for cloudflared/traefik (filebrowser is +// the ONLY Linked infra stack). +// COMPANION red-proof (recorded in REPORT): set Linked:true on cloudflared → the +// single-Megnyitás assertions below FAIL. + +func infraStacksTestSet() []stacks.Stack { + return []stacks.Stack{ + {Name: "cloudflared", Protected: true, State: stacks.StateRunning, + Meta: stacks.Metadata{Slug: "cloudflared", DisplayName: "Cloudflared"}}, + {Name: "traefik", Protected: true, State: stacks.StateRunning, + Meta: stacks.Metadata{Slug: "traefik", DisplayName: "Traefik"}}, + {Name: "filebrowser", Protected: true, State: stacks.StateRunning, + Meta: stacks.Metadata{Slug: "filebrowser", DisplayName: "Filebrowser"}}, + } +} + +func infraPageData(page string) map[string]interface{} { + return map[string]interface{}{ + "Page": page, "Title": "t", + "Stacks": infraStacksTestSet(), + "MissingStorage": map[string]string{}, + "NetworkWarnings": map[string]string{}, + "NetworkStubs": map[string]string{}, + "StorageLabels": map[string]string{}, + "Subdomains": map[string]string{"filebrowser": "files"}, + "RunningCount": 3, "StoppedCount": 0, "TotalCount": 3, + "SystemInfo": system.SystemInfo{}, + "BackupEnabled": false, + "Domain": "demo-felhom.eu", + } +} + +func assertInfraIdentity(t *testing.T, surface, html string) { + t.Helper() + for _, want := range []string{ + "Cloudflare Tunnel", + "Biztonságos internetkapcsolat — a szerver portnyitás nélkül érhető el kívülről.", + "Traefik", + "Forgalomirányító (reverse proxy) — a kéréseket a megfelelő alkalmazáshoz irányítja.", + "FileBrowser", + "Fájlkezelő — a tárhely fájljainak böngészése a böngészőből.", + } { + if !strings.Contains(html, want) { + t.Errorf("%s: infra identity %q missing", surface, want) + } + } + if !strings.Contains(html, "Védett") { + t.Errorf("%s: the Védett chip is missing", surface) + } + if !strings.Contains(html, `https://files.demo-felhom.eu`) { + t.Errorf("%s: filebrowser must link files.", surface) + } + // The guarded WRONG outcome: exactly ONE customer link (filebrowser) — a second + // https:// link would mean cloudflared/traefik got one. + if n := strings.Count(html, `https://`); n != 1 { + t.Errorf("%s: expected exactly 1 customer link (filebrowser), found %d https:// occurrences", surface, n) + } +} + +func TestInfraMeta_DashboardRows(t *testing.T) { + html := renderBackupPage(t, "dashboard", infraPageData("dashboard")) + assertInfraIdentity(t, "dashboard", html) + if n := strings.Count(html, "Megnyitás"); n != 1 { + t.Errorf("dashboard: expected exactly 1 Megnyitás (filebrowser), found %d", n) + } + if n := strings.Count(html, `data-fallback="/static/infra-logo.svg"`); n != 3 { + t.Errorf("dashboard: all 3 infra rows must carry the generic-icon fallback, found %d", n) + } +} + +func TestInfraMeta_AppCards(t *testing.T) { + html := renderBackupPage(t, "stacks", infraPageData("stacks")) + assertInfraIdentity(t, "stacks", html) + if !strings.Contains(html, "Védett rendszerkomponens") { + t.Error("stacks: protected card chip missing") + } + if n := strings.Count(html, `data-fallback="/static/infra-logo.svg"`); n != 3 { + t.Errorf("stacks: all 3 infra cards must carry the generic-icon fallback, found %d", n) + } +} + +func TestInfraMetaFor_RegularAppIsNil(t *testing.T) { + if infraMetaFor("radarr") != nil { + t.Error("regular apps must have no infra metadata") + } + if m := infraMetaFor("cloudflared"); m == nil || m.Linked { + t.Error("cloudflared must exist and must NOT be Linked (no customer UI)") + } + if m := infraMetaFor("filebrowser"); m == nil || !m.Linked { + t.Error("filebrowser must be the Linked infra stack") + } +} diff --git a/controller/internal/web/server.go b/controller/internal/web/server.go index baf8d48..fb70409 100644 --- a/controller/internal/web/server.go +++ b/controller/internal/web/server.go @@ -398,6 +398,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.serveLogoHandler(w, r) case path == "/static/favicon.svg": s.serveFaviconHandler(w, r) + case path == "/static/infra-logo.svg": + w.Header().Set("Content-Type", "image/svg+xml") + w.Header().Set("Cache-Control", "public, max-age=86400") + fmt.Fprint(w, InfraLogoSVG) case strings.HasPrefix(path, "/static/assets/"): s.serveAsset(w, r, strings.TrimPrefix(path, "/static/assets/")) case strings.HasPrefix(path, "/apps/"): diff --git a/controller/internal/web/templates/dashboard.html b/controller/internal/web/templates/dashboard.html index c78b3ce..91bedb7 100644 --- a/controller/internal/web/templates/dashboard.html +++ b/controller/internal/web/templates/dashboard.html @@ -146,7 +146,12 @@
{{range .Stacks}} + {{$im := infraMeta .Name}} + {{if $im}} + {{template "app_list_row" dict "Slug" .Meta.Slug "Name" $im.DisplayName "Secondary" $im.Description "RowClass" (printf "stack-state-%s" (stateColor .State)) "Href" (appHref .Meta.Slug) "FallbackIcon" "/static/infra-logo.svg"}} + {{else}} {{template "app_list_row" dict "Slug" .Meta.Slug "Name" .Meta.DisplayName "Secondary" .Meta.Description "RowClass" (printf "stack-state-%s" (stateColor .State)) "Href" (appHref .Meta.Slug)}} + {{end}} {{stateLabel .State}} {{if .Orphaned}}Elavult{{end}} {{$ms := index $.MissingStorage .Name}}{{if $ms}}Hiányzó tárhely: {{$ms}}{{end}} @@ -156,6 +161,12 @@ {{if .Protected}} Védett + {{/* Customer link ONLY for Linked infra (filebrowser → files.) — + cloudflared/traefik have no customer UI (guarded by render test). */}} + {{if and $im $im.Linked}} + {{$subdomain := index $.Subdomains .Name}} + {{if $subdomain}}Megnyitás {{end}} + {{end}} {{if isOperational .State}} {{end}} diff --git a/controller/internal/web/templates/stacks.html b/controller/internal/web/templates/stacks.html index 707d1cb..9bd75d9 100644 --- a/controller/internal/web/templates/stacks.html +++ b/controller/internal/web/templates/stacks.html @@ -18,13 +18,14 @@
{{range .Stacks}} + {{$im := infraMeta .Name}}
- +
-

{{.Meta.DisplayName}}

+

{{if $im}}{{$im.DisplayName}}{{else}}{{.Meta.DisplayName}}{{end}}

{{$subdomain := index $.Subdomains .Name}} {{if and $subdomain (or .Deployed .Protected)}} @@ -43,7 +44,9 @@ {{$nw := index $.NetworkWarnings .Name}}{{if $nw}}Hálózati tárhely nem elérhető: {{$nw}}{{end}}
- {{if .Meta.Description}} + {{if $im}} +

{{$im.Description}}

+ {{else if .Meta.Description}}

{{.Meta.Description}}

{{end}}