catalog: the lifecycle implementation itself (fixes the previous commit)

The previous commit landed only the new test/badge files: a 'git stash' used
to compare REUSE.md ref-check output silently dropped the staged index, so
every modification to an existing file was left behind and that commit does
not build. This adds the metadata field, the predicates, the fail-closed
deploy gate, the catalog filter, the funcmap entries, the template edits and
the docs that those tests exercise.
This commit is contained in:
2026-07-21 16:20:24 +02:00
parent ea0d3f1764
commit 5fdd2039fd
11 changed files with 213 additions and 5 deletions
+8
View File
@@ -412,6 +412,14 @@ func (s *Server) templateFuncMap() template.FuncMap {
}
return s.cfg.AppPageURL(slug)
},
// lifecycleBadge returns the catalog-metadata pill for an app's lifecycle, or nil when
// there is nothing to say. Pair it with the `meta_badge` partial, which no-ops on nil.
// R-56's difficulty badge is meant to be a sibling entry returning the same *MetaBadge.
"lifecycleBadge": lifecycleBadge,
// canInstall reports whether a catalog template may be OFFERED for a new install. The
// server-side deploy gate uses the same stacks.Metadata.CanInstall, so the button and the
// endpoint can never disagree.
"canInstall": func(m stacks.Metadata) bool { return m.CanInstall() },
// infraMeta resolves a protected infra stack's curated Hungarian identity
// (inframeta.go); nil for regular apps — templates branch on it.
"infraMeta": infraMetaFor,
+22 -1
View File
@@ -211,9 +211,30 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
s.executeTemplate(w, r, "dashboard", data)
}
// visibleCatalogStacks drops templates that are no longer OFFERED for new installs (lifecycle
// `hidden` or `abandoned`) AND are not deployed on this box.
//
// The `Deployed || Protected` half is the load-bearing part: a customer already running an app must
// keep seeing and managing it, whatever the catalog now says about offering it to new customers.
// Withdrawing an app must never take a working app away from someone — that is precisely the failure
// the short-lived `retired/` directory move would have caused, and why lifecycle is a metadata field
// rather than a deletion.
//
// Filtered here, in the handler, rather than in the template: the template already carries five
// conditional badges per card, and a visibility rule buried among them is a rule nobody can test.
func visibleCatalogStacks(in []stacks.Stack) []stacks.Stack {
out := make([]stacks.Stack, 0, len(in))
for _, st := range in {
if st.Deployed || st.Protected || st.Meta.CanInstall() {
out = append(out, st)
}
}
return out
}
func (s *Server) stacksHandler(w http.ResponseWriter, r *http.Request) {
data := s.baseData("stacks", "Alkalmazások")
allStacks := s.stackMgr.GetStacks()
allStacks := visibleCatalogStacks(s.stackMgr.GetStacks())
data["Stacks"] = allStacks
data["MissingStorage"] = s.missingStorageMap(allStacks)
nw, ns := s.networkStorageWarnings(allStacks) // NAS unreachable (recoverable) / guest-side stub (defect)
@@ -10,6 +10,7 @@
{{if .Stack.Deployed}}
<span class="stack-state-badge state-{{stateColor .Stack.State}}">{{stateLabel .Stack.State}}</span>
{{if .Stack.Orphaned}}<span class="badge badge-orphaned">Elavult</span>{{end}}
{{template "meta_badge" (lifecycleBadge .Meta)}}
{{if .EffectiveSubdomain}}<a href="https://{{.EffectiveSubdomain}}.{{.Domain}}{{.Meta.OpenPath}}" target="_blank" class="btn btn-sm btn-outline">Megnyitás ↗</a>{{end}}
<a href="/stacks/{{.Stack.Name}}/logs" class="btn btn-sm btn-outline">Napló</a>
{{if .Stack.Orphaned}}
@@ -19,7 +20,9 @@
<a href="/stacks/{{.Stack.Name}}/deploy" class="btn btn-sm btn-outline">Beállítások</a>
{{end}}
{{else}}
<a href="/stacks/{{.Stack.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Stack.Name}}')">Telepítés</a>
{{/* Server-side the deploy endpoint refuses a non-installable template; hide the button
so the page never offers an action that would be rejected. */}}
{{if canInstall .Meta}}<a href="/stacks/{{.Stack.Name}}/deploy" class="btn btn-sm btn-primary" onclick="return checkBeforeDeploy(event, '{{.Stack.Name}}')">Telepítés</a>{{end}}
{{end}}
</div>
</div>
@@ -49,6 +52,12 @@
{{if .Meta.Resources.PiCompatible}}<span class="meta-badge meta-badge-ok">Pi kompatibilis</span>{{else}}<span class="meta-badge meta-badge-warn">Csak x86</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="meta-badge meta-badge-ok">Magyar felület</span>{{end}}
</div>
{{if .Meta.IsAbandoned}}
<div class="alert alert-warning" style="margin-top:.75rem">
Az alkalmazás fejlesztője felhagyott a fejlesztéssel. A telepített verzió továbbra is használható,
de frissítések és biztonsági javítások már nem érkeznek hozzá.
</div>
{{end}}
</div>
</div>
@@ -154,6 +154,7 @@
{{end}}
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="tag tag-warn">Elavult</span>{{end}}
{{template "meta_badge" (lifecycleBadge .Meta)}}
{{$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}}
{{$ns := index $.NetworkStubs .Name}}{{if $ns}}<span class="tag tag-warn" title="Az alkalmazás környezetében a hálózati tárhely helyén üres helyi könyvtár van — az adatok nem a NAS-ra kerülnek. Jelezze az üzemeltetőnek."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely hibás — az alkalmazás nem a NAS-t látja</span>{{end}}
{{$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}}
@@ -39,6 +39,7 @@
</div>
<span class="tag tag-{{stateColor .State}}"><span class="dot"></span>{{stateLabel .State}}</span>
{{if .Orphaned}}<span class="tag tag-warn">Elavult</span>{{end}}
{{template "meta_badge" (lifecycleBadge .Meta)}}
{{$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."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hiányzó tárhely: {{$ms}}</span>{{end}}
{{$ns := index $.NetworkStubs .Name}}{{if $ns}}<span class="tag tag-warn" title="Az alkalmazás környezetében a hálózati tárhely helyén üres helyi könyvtár van — az adatok nem a NAS-ra kerülnek. Jelezze az üzemeltetőnek."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely hibás — az alkalmazás nem a NAS-t látja</span>{{end}}
{{$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}}
@@ -79,7 +80,9 @@
<a href="/apps/{{.Meta.Slug}}" class="btn btn-outline">Részletek</a>
{{end}}
{{else if not .Deployed}}
<a href="/stacks/{{.Name}}/deploy" class="btn btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">Telepítés</a>
{{/* The endpoint refuses a non-installable template server-side; hiding the button
here keeps the two consistent rather than offering an action that will fail. */}}
{{if canInstall .Meta}}<a href="/stacks/{{.Name}}/deploy" class="btn btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">Telepítés</a>{{end}}
<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">Részletek</a>
{{else}}
{{if isOperational .State}}