69698a89e8
- Health severity fix: mount-point check downgraded from issue (FAIL) to warning (WARN) - All storage health messages translated to Hungarian - Success flash messages for all storage operations - Edit storage path labels (inline edit UI + backend) - App details per storage path on settings page (expandable list with names + sizes) - Storage badge on stacks page showing which storage each app uses - Deploy dropdown with free space display and low-space warning (<20%) - Filesystem & disk info on settings page (ext4/btrfs, device, model via findmnt) - Backup page storage context with per-app storage label badges Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
141 lines
6.5 KiB
HTML
141 lines
6.5 KiB
HTML
{{define "stacks"}}
|
|
{{template "layout_start" .}}
|
|
|
|
<div class="page-header">
|
|
<h2>Alkalmazások</h2>
|
|
<span class="domain-badge">{{.Domain}}</span>
|
|
<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}}
|
|
<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="" onerror="this.onerror=function(){this.style.display='none'};this.src='{{logoPNGURL .Meta.Slug}}'">
|
|
<div>
|
|
<h3>{{.Meta.DisplayName}}</h3>
|
|
{{if .Meta.Subdomain}}
|
|
<a class="subdomain-link" href="https://{{.Meta.Subdomain}}.{{$.Domain}}" target="_blank">
|
|
{{.Meta.Subdomain}}.{{$.Domain}} ↗
|
|
</a>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
<span class="stack-state-badge state-{{stateColor .State}}">{{stateLabel .State}}</span>
|
|
{{if .Orphaned}}<span class="badge badge-orphaned">Elavult</span>{{end}}
|
|
</div>
|
|
|
|
{{if .Meta.Description}}
|
|
<p class="stack-detail-desc">{{.Meta.Description}}</p>
|
|
{{end}}
|
|
|
|
<div class="stack-meta-badges">
|
|
{{if .Meta.Resources.MemRequest}}<span class="meta-badge">~{{.Meta.Resources.MemRequest}}</span>{{end}}
|
|
{{if .Meta.Resources.PiCompatible}}<span class="meta-badge meta-badge-ok">Pi kompatibilis</span>{{end}}
|
|
{{if .Meta.Resources.NeedsHDD}}<span class="meta-badge">HDD szükséges</span>{{end}}
|
|
{{if and .Deployed (index $.StorageLabels .Name)}}<span class="meta-badge meta-badge-storage" title="Adattároló: {{index $.StorageLabels .Name}}">💾 {{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="badge badge-protected">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>
|
|
{{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}}
|