Files
felhom-controller/controller/internal/web/templates/stacks.html
T
admin 5fdd2039fd 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.
2026-07-21 16:20:24 +02:00

157 lines
9.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "stacks"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>Alkalmazások</h2>
<span class="domain-badge">{{.Domain}}</span>
<a href="/import" class="btn btn-sm btn-outline" title="Alkalmazás visszaállítása exportált csomagból">Importálás</a>
<button class="btn btn-sm btn-outline" id="sync-btn" onclick="syncTemplates()" title="Sablonok frissítése a központi katalógusból">↻ Sablonok frissítése</button>
</div>
<div id="sync-toast" class="sync-toast" style="display:none"></div>
<div class="filter-bar" id="filter-bar">
<button class="filter-btn active" data-filter="all">Mind <span class="filter-count" id="count-all"></span></button>
<button class="filter-btn" data-filter="running">Futó <span class="filter-count" id="count-running"></span></button>
<button class="filter-btn" data-filter="stopped">Leállítva <span class="filter-count" id="count-stopped"></span></button>
<button class="filter-btn" data-filter="available">Telepíthető <span class="filter-count" id="count-available"></span></button>
</div>
<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=""{{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>{{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">
{{$subdomain}}.{{$.Domain}} ↗
</a>
{{if and .Deployed (routeUnpublished .State)}}
<span class="route-unpublished" title="A proxy (Traefik) csak egészséges konténerhez publikál nyilvános útvonalat. Amíg az alkalmazás nem egészséges, az URL nem érhető el (404), pedig a konténer fut."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> URL nem elérhető útvonal nincs publikálva</span>
{{end}}
{{end}}
</div>
</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}}
</div>
{{if $im}}
<p class="stack-detail-desc">{{$im.Description}}</p>
{{else if .Meta.Description}}
<p class="stack-detail-desc">{{.Meta.Description}}</p>
{{end}}
<div class="metarows">
{{if .Meta.Resources.MemRequest}}<span class="metarow"><svg class="ico"><use href="#i-memory-stick"/></svg><span class="mono">~{{.Meta.Resources.MemRequest}}</span></span>{{end}}
{{if .Meta.Resources.PiCompatible}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Pi kompatibilis</span>{{end}}
{{if .Meta.Resources.NeedsHDD}}<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>HDD szükséges</span>{{end}}
{{if .Meta.Resources.HungarianUI}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>Magyar felület</span>{{end}}
{{if and .Deployed (index $.StorageLabels .Name)}}<span class="metarow" title="Adattároló: {{index $.StorageLabels .Name}}"><svg class="ico"><use href="#i-hard-drive"/></svg>{{index $.StorageLabels .Name}}</span>{{end}}
</div>
{{if .Containers}}
<div class="container-list">
{{range .Containers}}
<div class="container-row">
<span class="container-name">{{.Name}}</span>
<span class="container-status state-text-{{stateColor .State}}">{{.Status}}</span>
</div>
{{end}}
</div>
{{end}}
<div class="stack-detail-actions">
{{if .Protected}}
<span class="tag"><svg class="ico ico-sm"><use href="#i-lock"/></svg>Védett rendszerkomponens</span>
{{if isOperational .State}}
<button class="btn btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">Újraindítás</button>
{{end}}
{{if .Meta.Slug}}
<a href="/apps/{{.Meta.Slug}}" class="btn btn-outline">Részletek</a>
{{end}}
{{else if not .Deployed}}
{{/* 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}}
{{if not .Orphaned}}<button class="btn btn-success" onclick="stackAction(event, '{{.Name}}', 'update')">Frissítés</button>{{end}}
<button class="btn btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">Újraindítás</button>
<button class="btn btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')">Leállítás</button>
{{else}}
<button class="btn btn-success" onclick="stackAction(event, '{{.Name}}', 'start')">Indítás</button>
{{if not .Orphaned}}<button class="btn btn-danger" onclick="removeStack('{{.Name}}')">Eltávolítás</button>{{end}}
{{end}}
<a href="/stacks/{{.Name}}/logs" class="btn btn-outline">Naplók</a>
{{if not .Orphaned}}<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">Részletek</a>{{end}}
{{if .Orphaned}}<button class="btn btn-danger" onclick="deleteOrphanStack('{{.Name}}')">Törlés</button>{{end}}
{{end}}
</div>
</div>
{{end}}
</div>
<script>
(function() {
var cards = document.querySelectorAll('.stack-detail-card[data-filter-state]');
var counts = {all: cards.length, running: 0, stopped: 0, available: 0};
cards.forEach(function(card) {
var cat = card.getAttribute('data-filter-state');
if (counts[cat] !== undefined) counts[cat]++;
});
['all', 'running', 'stopped', 'available'].forEach(function(f) {
var el = document.getElementById('count-' + f);
if (el) el.textContent = '(' + counts[f] + ')';
});
function applyFilter(filter) {
cards.forEach(function(card) {
if (filter === 'all' || card.getAttribute('data-filter-state') === filter) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
document.querySelectorAll('.filter-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-filter') === filter);
});
var url = new URL(window.location);
if (filter === 'all') {
url.searchParams.delete('filter');
} else {
url.searchParams.set('filter', filter);
}
history.replaceState(null, '', url);
}
document.querySelectorAll('.filter-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
applyFilter(this.getAttribute('data-filter'));
});
});
var urlFilter = new URLSearchParams(window.location.search).get('filter');
if (urlFilter && ['running', 'stopped', 'available'].indexOf(urlFilter) !== -1) {
applyFilter(urlFilter);
}
})();
</script>
{{template "layout_end" .}}
{{end}}