5dc277f2b1
- style.css: navy token palette (--bg-0/1/2, --line, --text-1/2/3, --blue, --warn, --crit), single 2px radius, all box-shadows and the bg grid overlay removed, fonts via --font-ui/--font-data. - Components: .meter (3px hairline track, blue nominal fill, neutral 70/85 ticks, warn/crit flag), .tag (square 2px state chip + dot, pulse on progress, reduced-motion respected), .metarow (icon + text, no container), .panel/.list/.section-h primitives, boxless .stats with hairline dividers, buttons 2px (danger = crit outline). - funcmap: stateColor -> run/progress/warn/neutral/off (stopped is neutral, NOT red — operator-approved exception-color change), usageColor/tempColor -> nominal/warn/crit (thresholds unchanged); stateLabel Hungarian copy untouched (guarded by test). - layout.html: sprite nav icons (layout-grid/cloud/shield/cpu/wrench/ settings), alert banner emoji -> triangle-alert/info icons. - dashboard.html: meters with disk warn/crit flags (Fogyóban a hely / Kritikusan kevés hely), boxless stats (Leállítva 0 muted, >0 amber), single-panel stack list with 2px state edges, tags instead of badges, icon action buttons. - stacks.html: state tag + metarow rows; catalog keeps its grid. - setup minimalCSS retokened to v2 (drops GitHub-dark hexes). - Tests: §8 truth tables for stateColor/usageColor/tempColor + stateLabel byte-identity guard (red-proven vs pre-change funcmap: stopped->red and 0->green failed as required).
150 lines
8.3 KiB
HTML
150 lines
8.3 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}}
|
||
<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>
|
||
{{$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}}
|
||
{{$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 .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}}
|