Files
felhom-controller/controller/internal/web/templates/stacks.html
T
admin 2c80868c63 Launcher polish: monogram reveal-on-failure + app placeholder on every icon surface (v0.163.1)
(A) The launcher monogram bled through every transparent white logo — it rendered
unconditionally under the img. Now hidden by default (.launch-mono display:none),
revealed only when the img chain fails (onerror adds .launch-tile--noimg).

(B) The /static/app-placeholder.svg default reached only app_list_row. The four
other sanctioned app-logo onerror chains now match the canonical SVG->PNG->placeholder
grammar: backups_apps (aligned row), stacks (infra->infra-logo else placeholder),
app_info hero (screenshots still vanish), deploy (keeps .LogoURL/.LogoPNGURL).

Template/CSS only; no handler/funcmap change. 5 tests + 2 red-proofs.
2026-07-24 10:14:25 +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="" data-fallback="{{if $im}}/static/infra-logo.svg{{else}}/static/app-placeholder.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}}