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:
@@ -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 {
|
||||
|
||||
@@ -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>`
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user