3c6b49b31c
gates / gates (push) Successful in 27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
172 lines
11 KiB
HTML
172 lines
11 KiB
HTML
{{define "stacks"}}
|
|
{{template "layout_start" .}}
|
|
|
|
<div class="page-header">
|
|
<h2>{{T "stacks.alkalmazasok"}}</h2>
|
|
<span class="domain-badge">{{.Domain}}</span>
|
|
<a href="/import" class="btn btn-sm btn-outline" title="{{T "stacks.alkalmazas_visszaallitasa_exportalt_csom"}}">{{T "stacks.importalas"}}</a>
|
|
<button class="btn btn-sm btn-outline" id="sync-btn" onclick="syncTemplates()" title="{{T "stacks.sablonok_frissitese_a_kozponti_katalogus"}}">{{T "stacks.sablonok_frissitese"}}</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">{{T "stacks.mind"}} <span class="filter-count" id="count-all"></span></button>
|
|
<button class="filter-btn" data-filter="running">{{T "stacks.futo"}} <span class="filter-count" id="count-running"></span></button>
|
|
<button class="filter-btn" data-filter="stopped">{{T "stacks.leallitva"}} <span class="filter-count" id="count-stopped"></span></button>
|
|
<button class="filter-btn" data-filter="available">{{T "stacks.telepitheto"}} <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="{{T "stacks.a_proxy_traefik_csak_egeszseges"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> {{T "stacks.url_nem_elerheto_utvonal_nincs"}}</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">{{T "common.elavult"}}</span>{{end}}
|
|
{{template "meta_badge" (lifecycleBadge .Meta)}}
|
|
{{template "meta_badge" (updateBadge .)}}
|
|
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="{{T "stacks.az_alkalmazas_adattaroloja_nem_elerheto"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "stacks.hianyzo_tarhely"}}</span>{{end}}
|
|
{{$ns := index $.NetworkStubs .Name}}{{if $ns}}<span class="tag tag-warn" title="{{T "stacks.az_alkalmazas_kornyezeteben_a_halozati"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "stacks.halozati_tarhely_hibas_az_alkalmazas"}}</span>{{end}}
|
|
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="{{T "stacks.a_halozati_tarhely_nas_jelenleg"}}"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>{{T "stacks.halozati_tarhely_nem_elerheto"}}</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>{{T "common.pi_kompatibilis"}}</span>{{end}}
|
|
{{if .Meta.Resources.NeedsHDD}}<span class="metarow"><svg class="ico"><use href="#i-hard-drive"/></svg>{{T "common.hdd_szukseges"}}</span>{{end}}
|
|
{{if .Meta.Resources.HungarianUI}}<span class="metarow"><svg class="ico"><use href="#i-check"/></svg>{{T "common.magyar_felulet"}}</span>{{end}}
|
|
{{if and .Deployed (index $.StorageLabels .Name)}}<span class="metarow" title="{{T "stacks.adattarolo"}}"><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>{{T "stacks.vedett_rendszerkomponens"}}</span>
|
|
{{if isOperational .State}}
|
|
<button class="btn btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">{{T "common.ujrainditas"}}</button>
|
|
{{end}}
|
|
{{if .Meta.Slug}}
|
|
<a href="/apps/{{.Meta.Slug}}" class="btn btn-outline">{{T "stacks.reszletek"}}</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}}')">{{T "stacks.telepites"}}</a>{{end}}
|
|
<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">{{T "stacks.reszletek"}}</a>
|
|
{{else}}
|
|
{{/* Slice 4 (v0.238.0): the UPDATING and HELD checks come BEFORE isOperational. That
|
|
predicate counts `restarting` as operational, which is how the 2026-09-01 spike saw
|
|
a green Frissítés beside a crash loop. An app being updated offers no lifecycle
|
|
button; a held app offers none that would start it, only the way back. */}}
|
|
{{if .Updating}}
|
|
<span class="tag tag-progress" data-update-phase="{{.UpdatePhase}}"><span class="dot"></span>{{updatePhaseText .}}</span>
|
|
{{else if and .HoldReason (eq .HoldKind "unhealthy_stop")}}
|
|
<div class="alert alert-warning" data-unhealthy-stop="true">{{holdText .}}</div>
|
|
{{if not .Orphaned}}<button class="btn btn-success" onclick="stackAction(event, '{{.Name}}', 'start')">{{T "stacks.inditas"}}</button>{{end}}
|
|
{{else if .HoldReason}}
|
|
<div class="alert alert-error" data-held="true">{{holdText .}}{{if not .HoldNoWholeCopy}} <a href="/backups/apps" class="btn btn-sm btn-outline">{{T "stacks.mentesek"}}</a>{{end}}</div>
|
|
{{if not .Orphaned}}<button class="btn btn-danger" onclick="removeStack('{{.Name}}')">{{T "common.eltavolitas"}}</button>{{end}}
|
|
{{else if isOperational .State}}
|
|
{{if .UpdateError}}<div class="alert alert-warning" data-update-error="true">{{updateErrorText .}}</div>{{end}}
|
|
{{if not .Orphaned}}<button class="btn btn-success" onclick="stackAction(event, '{{.Name}}', 'update')">{{T "stacks.frissites"}}</button>{{end}}
|
|
<button class="btn btn-warning" onclick="stackAction(event, '{{.Name}}', 'restart')">{{T "common.ujrainditas"}}</button>
|
|
<button class="btn btn-danger" onclick="stackAction(event, '{{.Name}}', 'stop')">{{T "stacks.leallitas"}}</button>
|
|
{{else}}
|
|
{{if .UpdateError}}<div class="alert alert-warning" data-update-error="true">{{updateErrorText .}}</div>{{end}}
|
|
<button class="btn btn-success" onclick="stackAction(event, '{{.Name}}', 'start')">{{T "stacks.inditas"}}</button>
|
|
{{if not .Orphaned}}<button class="btn btn-danger" onclick="removeStack('{{.Name}}')">{{T "common.eltavolitas"}}</button>{{end}}
|
|
{{end}}
|
|
<a href="/stacks/{{.Name}}/logs" class="btn btn-outline">{{T "stacks.naplok"}}</a>
|
|
{{if not .Orphaned}}<a href="{{appPageURL .Meta.Slug}}" class="btn btn-outline">{{T "stacks.reszletek"}}</a>{{end}}
|
|
{{if .Orphaned}}<button class="btn btn-danger" onclick="deleteOrphanStack('{{.Name}}')">{{T "common.torles"}}</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}}
|