Part B: infra stacks carry identity — curated Hungarian metadata for cloudflared/traefik/filebrowser (v0.126.0)

- inframeta.go: static display-only map (name/description/Linked) + infraMeta template func;
  filebrowser is the ONLY Linked stack (files.<domain> 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)
This commit is contained in:
2026-07-13 11:31:38 +02:00
parent 3eacb6c326
commit 4f06149abc
6 changed files with 176 additions and 4 deletions
+3
View File
@@ -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 {
+48
View File
@@ -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.<domain>). 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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#94A6BF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>`
+103
View File
@@ -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.<domain>", 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")
}
}
+4
View File
@@ -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/"):
@@ -146,7 +146,12 @@
<div class="stack-list">
{{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}}
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="tag tag-warn">Elavult</span>{{end}}
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="Az alkalmazás adattárolója nem elérhető. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hiányzó tárhely: {{$ms}}</span>{{end}}
@@ -156,6 +161,12 @@
{{if .Protected}}
<span class="tag"><svg class="ico ico-sm"><use href="#i-lock"/></svg>Védett</span>
{{/* Customer link ONLY for Linked infra (filebrowser → files.<domain>) —
cloudflared/traefik have no customer UI (guarded by render test). */}}
{{if and $im $im.Linked}}
{{$subdomain := index $.Subdomains .Name}}
{{if $subdomain}}<a href="https://{{$subdomain}}.{{$.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline" onclick="event.stopPropagation()">Megnyitás <svg class="ico ico-sm"><use href="#i-external-link"/></svg></a>{{end}}
{{end}}
{{if isOperational .State}}
<button class="btn btn-sm btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')" title="Újraindítás"><svg class="ico ico-sm"><use href="#i-rotate-cw"/></svg></button>
{{end}}
@@ -18,13 +18,14 @@
<div class="stack-grid">
{{range .Stacks}}
{{$im := infraMeta .Name}}
<div class="stack-detail-card stack-state-{{stateColor .State}}" data-filter-state="{{filterCategory .State .Deployed}}"{{if .Meta.Slug}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
<div class="stack-detail-header">
<div class="stack-title-row">
<img class="stack-logo-lg" src="{{logoURL .Meta.Slug}}"
alt="" onerror="this.onerror=function(){this.style.display='none'};this.src='{{logoPNGURL .Meta.Slug}}'">
<img class="stack-logo-lg" src="{{logoURL .Meta.Slug}}" alt=""{{if $im}} data-fallback="/static/infra-logo.svg"{{end}}
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{logoPNGURL .Meta.Slug}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
<div>
<h3>{{.Meta.DisplayName}}</h3>
<h3>{{if $im}}{{$im.DisplayName}}{{else}}{{.Meta.DisplayName}}{{end}}</h3>
{{$subdomain := index $.Subdomains .Name}}
{{if and $subdomain (or .Deployed .Protected)}}
<a class="subdomain-link" href="https://{{$subdomain}}.{{$.Domain}}" target="_blank">
@@ -43,7 +44,9 @@
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="A hálózati tárhely (NAS) jelenleg nem érhető el. Az alkalmazás fut; az adatok elérése a NAS visszatértével helyreáll."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely nem elérhető: {{$nw}}</span>{{end}}
</div>
{{if .Meta.Description}}
{{if $im}}
<p class="stack-detail-desc">{{$im.Description}}</p>
{{else if .Meta.Description}}
<p class="stack-detail-desc">{{.Meta.Description}}</p>
{{end}}