4f06149abc
- 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)
154 lines
9.0 KiB
HTML
154 lines
9.0 KiB
HTML
{{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}}
|
||
{{$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}}
|
||
<a href="/stacks/{{.Name}}/deploy" class="btn btn-primary" onclick="return checkBeforeDeploy(event, '{{.Name}}')">Telepítés</a>
|
||
<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}}
|