Files
felhom-controller/controller/internal/web/templates/layout.html
T
admin 89712563a0
gates / gates (push) Successful in 10s
R-302: the abandon banner promises only what the box can still see is true
The retrieval clause rendered unconditionally on every page and is false on a
reachable state - the same screen where the orphan card says we cannot tell.

The condition is a fingerprint PINNED at the decision, not a comparison against
the current key. The obvious proxy asks about the wrong key: the set-aside
copies were written under an older key the box no longer has, so on a
twice-rebuilt box the proxy promises about copies nothing can open. Demonstrated
- under the proxy, the replaced-package and legacy cases both flip back to
promising.

The pin is a recorded assumption and says so: nothing on the box records which
key wrote those copies. Empty is not a match. A countdown started before this
carries no pin and takes the cautious branch, not a backfill.

A sweep of all 36 templates found a fourth instance (backups page, same
condition applied) and a fifth (the confirmation screen, correctly left alone -
true at the moment of the decision).

New retrieval_promise_gate registers each claim with a reason rather than
banning a verb: a string ban failed twice, and the honest replacement copy
contains the stem.
2026-08-12 15:27:29 +02:00

492 lines
30 KiB
HTML

{{define "layout_start"}}
<!DOCTYPE html>
<html lang="hu" class="no-js">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} — Felhom.eu</title>
{{/* ?v= cache-bust: Cloudflare edge-caches /static/* for 4h — an unversioned URL keeps
serving the PREVIOUS release's asset after a deploy (0.126.1 live QA hit this on CSS;
the logo/favicon share the failure mode). The controller version busts it every release. */}}
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg?v={{.Version}}">
<link rel="stylesheet" href="/static/style.css?v={{.Version}}">
<meta name="csrf-token" content="{{.CSRFToken}}">
<script>
// Progressive-enhancement hook (v0.166.0): flip no-js → js before any CSS-dependent paint so the
// mobile off-canvas drawer engages only when the drawer JS below can actually open it.
document.documentElement.className=document.documentElement.className.replace('no-js','js');
function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{};}
// Inline two-step confirm (drill F-11): the trigger swaps IN PLACE to "<question> Igen/Mégse".
// Never native confirm() — it is an OS-modal that blocks browser automation — and no overlay:
// this is the LIGHT pattern for one-click consequential actions. Form buttons opt in with
// data-confirm="…" (submitted via requestSubmit so formaction/name-value are honored);
// JS flows call felhomConfirm(el, question, onYes) directly.
function felhomConfirm(el,question,onYes){
if(!el||el.dataset.fcOpen)return;
el.dataset.fcOpen='1';
var wrap=document.createElement('span');wrap.className='inline-confirm';
var q=document.createElement('span');q.className='inline-confirm-q';q.textContent=question;
var yes=document.createElement('button');yes.type='button';yes.className='btn btn-xs btn-danger';yes.textContent='Igen';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='Mégse';
wrap.appendChild(q);wrap.appendChild(yes);wrap.appendChild(no);
el.style.display='none';el.parentNode.insertBefore(wrap,el.nextSibling);
function close(){wrap.remove();el.style.display='';delete el.dataset.fcOpen;}
no.addEventListener('click',close);
yes.addEventListener('click',function(){close();onYes();});
}
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('[data-confirm]'):null;
if(!btn)return;
e.preventDefault();
felhomConfirm(btn,btn.getAttribute('data-confirm'),function(){
var form=btn.closest('form');
if(form){if(form.requestSubmit)form.requestSubmit(btn);else form.submit();}
});
});
/* Collapsible sidebar groups (v0.146.0). Progressive enhancement: the server already
renders the group containing the active page with .is-open, so the correct group is
open before this runs and stays open if JS never does. Exactly one group open at a
time; clicking the open one closes it. */
document.addEventListener('click',function(e){
var btn=e.target.closest?e.target.closest('.nav-group-toggle'):null;
if(!btn)return;
var group=btn.closest('.nav-group');
if(!group)return;
var willOpen=!group.classList.contains('is-open');
document.querySelectorAll('.nav-group.is-open').forEach(function(g){
g.classList.remove('is-open');
var t=g.querySelector('.nav-group-toggle');
if(t)t.setAttribute('aria-expanded','false');
});
if(willOpen){group.classList.add('is-open');btn.setAttribute('aria-expanded','true');}
});
function showAlert(msg){var o=document.createElement('div');o.className='modal-overlay';o.id='alert-modal';o.addEventListener('click',function(e){if(e.target===o)o.remove();});var c=document.createElement('div');c.className='modal-card';c.innerHTML='<h3>Üzenet</h3><pre style="white-space:pre-wrap;word-break:break-word;background:var(--bg-2);padding:.75rem;border-radius:.375rem;font-size:.85rem;max-height:60vh;overflow-y:auto;user-select:text;cursor:text">'+msg.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</pre><div class="modal-actions"><button class="btn btn-primary" onclick="document.getElementById(\'alert-modal\').remove()">OK</button></div>';o.appendChild(c);document.body.appendChild(o);}
</script>
</head>
<body>
{{template "icon_sprite"}}
<header class="mobile-topbar">
<a href="/" class="mobile-topbar-logo"><img src="/static/felhom-logo.svg?v={{.Version}}" alt="Felhom.eu"></a>
<button type="button" class="nav-burger" aria-expanded="false" aria-controls="sidebar" aria-label="Menü">
<svg class="ico"><use href="#i-menu"/></svg>
</button>
</header>
<div class="nav-backdrop" hidden></div>
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="/static/felhom-logo.svg?v={{.Version}}" alt="Felhom.eu" class="sidebar-logo">
</div>
<ul class="nav-links">
<li><a href="/launcher" class="{{if eq .Page "launcher"}}active{{end}}"><svg class="ico"><use href="#i-rocket"/></svg>Indítópult</a></li>
<li><a href="/dashboard" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
{{$storageOpen := or (eq .Page "storage") (eq .Page "storage-network")}}
<li class="nav-group{{if $storageOpen}} is-open{{end}}">
<button type="button" class="nav-group-toggle{{if $storageOpen}} active{{end}}" aria-expanded="{{if $storageOpen}}true{{else}}false{{end}}" aria-controls="nav-group-storage"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-storage" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/storage" class="{{if eq .Page "storage"}}active{{end}}">Meghajtók</a></li>
<li><a href="/storage/network" class="{{if eq .Page "storage-network"}}active{{end}}">Hálózati tárhely</a></li>
</ul>
</li>
{{$backupsOpen := or (eq .Page "backups") (eq .Page "backups-remote") (eq .Page "backups-apps") (eq .Page "backups-restore")}}
<li class="nav-group{{if $backupsOpen}} is-open{{end}}">
<button type="button" class="nav-group-toggle{{if $backupsOpen}} active{{end}}" aria-expanded="{{if $backupsOpen}}true{{else}}false{{end}}" aria-controls="nav-group-backups"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-backups" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}">Áttekintés</a></li>
<li><a href="/backups/remote" class="{{if eq .Page "backups-remote"}}active{{end}}">Távoli mentés</a></li>
<li><a href="/backups/apps" class="{{if eq .Page "backups-apps"}}active{{end}}">Alkalmazások</a></li>
<li><a href="/backups/restore" class="{{if eq .Page "backups-restore"}}active{{end}}">Visszaállítás</a></li>
</ul>
</li>
{{$sharingOpen := eq .Page "sharing"}}
<li class="nav-group{{if $sharingOpen}} is-open{{end}}">
<button type="button" class="nav-group-toggle{{if $sharingOpen}} active{{end}}" aria-expanded="{{if $sharingOpen}}true{{else}}false{{end}}" aria-controls="nav-group-sharing"><svg class="ico"><use href="#i-share"/></svg>Megosztás<svg class="ico nav-chevron"><use href="#i-chevron-down"/></svg></button>
<ul id="nav-group-sharing" class="nav-links nav-links-sub nav-links-nested">
<li><a href="/sharing" class="{{if eq .Page "sharing"}}active{{end}}">Hálózati megosztás</a></li>
</ul>
</li>
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}"><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
<li><a href="/debug" class="{{if eq .Page "debug"}}active{{end}}"><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>
</ul>
<div class="sidebar-bottom">
<div class="nav-group-label">Beállítások</div>
<ul class="nav-links nav-links-sub">
<li><a href="/settings" class="{{if eq .Page "settings"}}active{{end}}"><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
<li><a href="/settings/notifications" class="{{if eq .Page "settings-notifications"}}active{{end}}"><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
<li><a href="/settings/security" class="{{if eq .Page "settings-security"}}active{{end}}"><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">{{.Version}}</span>
{{if .AuthEnabled}}<a href="/logout" class="logout-link">Kijelentkezés ↗</a>{{end}}
</div>
</div>
</nav>
<main class="content">
{{if .ClaimLegacyOpen}}
<div class="alerts-container">
<div class="alert-banner alert-banner-error">
<span class="alert-icon"><svg class="ico"><use href="#i-triangle-alert"/></svg></span>
<span class="alert-message">A vezérlőpult még nincs jelszóval védve — a beállító kódot hamarosan e-mailben küldjük.</span>
</div>
</div>
{{end}}
{{/* R-241 (v0.206.0) — the recovery reminder bar. Rendered while the situation holds, the customer
has not opted out of reminders for THIS situation, and they have not clicked it away this visit.
It NEVER replaces the entry point on the Távoli mentés page — silencing a reminder is not the
same as removing the route. */}}
{{if .RecoveryBanner}}
<div class="alerts-container">
<div class="alert-banner alert-banner-warning">
<span class="alert-icon"><svg class="ico"><use href="#i-triangle-alert"/></svg></span>
<span class="alert-message">
{{if .RecoveryAbandonDays}}
{{/* R-302: the DELETION and its date are certain and always render. The RETRIEVAL clause is
conditional on the hub still holding the same sealed package it held when the customer
decided — pinned then, compared now. It rendered unconditionally, and was false on a
reachable state: a fresh escrow ceremony during the window replaces the package, which
is the exact act that cost both demo boxes their history on 2026-08-04. A countdown
started before this shipped carries no pin and takes the cautious branch. */}}
A korábbi távoli mentéseidet <strong>{{.RecoveryAbandonDays}} nap múlva</strong> ({{.RecoveryAbandonDate}}) véglegesen töröljük, a kérésed szerint.
{{if .RecoveryAbandonRetrievalOffered}}Addig még visszaszerezheted őket a helyreállítási kóddal.{{else}}Hogy ezek még visszaszerezhetők-e a helyreállítási kóddal, azt innen nem tudjuk megállapítani — ha vissza szeretnéd kapni őket, <strong>írj nekünk a törlés előtt</strong>.{{end}}
{{else if ge .RecoveryReminderTier 14}}
<strong>Két hete</strong> várnak rád a korábbi távoli mentéseid, és még nem adtad meg a helyreállítási kódodat. Amíg nem teszed, ezekhez a mentésekhez nem férsz hozzá.
{{else if ge .RecoveryReminderTier 7}}
Már <strong>egy hete</strong> megvannak a korábbi távoli mentéseid, de a helyreállítási kódod nélkül nem tudjuk megnyitni őket.
{{else if ge .RecoveryReminderTier 3}}
A korábbi távoli mentéseid megvannak — a megnyitásukhoz a helyreállítási kódod szükséges.
{{else}}
A korábbi távoli mentéseid megvannak, de ehhez a géphez a helyreállítási kódod szükséges.
{{end}}
<a href="/recovery">Megnézem</a>
</span>
<span class="alert-actions">
<form method="POST" action="/recovery/banner/dismiss" style="display:inline">
{{.CSRFField}}<input type="hidden" name="back" value="{{.RecoveryBannerBack}}">
<button type="submit" class="btn btn-sm btn-outline">Most nem</button>
</form>
{{if not .RecoveryAbandonDays}}
<form method="POST" action="/recovery/remind-optout" style="display:inline">
{{.CSRFField}}<input type="hidden" name="back" value="{{.RecoveryBannerBack}}">
<button type="submit" class="btn btn-sm btn-outline">Ne emlékeztessen újra</button>
</form>
{{end}}
</span>
</div>
</div>
{{end}}
{{if .Alerts}}
<div class="alerts-container">
{{range .Alerts}}
{{if and (not .Inline) (or (not .PageOnly) (pageMatch .PageOnly $.Page))}}
<div class="alert-banner alert-banner-{{.Level}}">
<span class="alert-icon"><svg class="ico">{{if eq .Level "error"}}<use href="#i-triangle-alert"/>{{else if eq .Level "warning"}}<use href="#i-triangle-alert"/>{{else}}<use href="#i-info"/>{{end}}</svg></span>
<span class="alert-message">{{.Message}}</span>
{{if .Link}}<a href="{{.Link}}" class="alert-link">{{.LinkText}} →</a>{{end}}
</div>
{{end}}
{{end}}
</div>
{{end}}
{{end}}
{{define "layout_end"}}
</main>
<script>
// Mobile off-canvas drawer (v0.166.0). Reuses the existing sidebar as the drawer; the accordion
// handler in the head script is untouched and keeps working inside it. Only wires up on ≤768px
// chrome — on desktop the burger/backdrop are display:none so these listeners never fire.
(function(){
var burger=document.querySelector('.nav-burger');
var sidebar=document.getElementById('sidebar');
var backdrop=document.querySelector('.nav-backdrop');
if(!burger||!sidebar||!backdrop)return;
function setOpen(open){
sidebar.classList.toggle('is-open',open);
document.body.classList.toggle('nav-open',open);
backdrop.hidden=!open;
burger.setAttribute('aria-expanded',open?'true':'false');
}
burger.addEventListener('click',function(){setOpen(!sidebar.classList.contains('is-open'));});
backdrop.addEventListener('click',function(){setOpen(false);});
document.addEventListener('keydown',function(e){
if(e.key==='Escape'&&sidebar.classList.contains('is-open'))setOpen(false);
});
})();
document.addEventListener('click', function(e) {
if (e.target.closest('a, button, .btn, input, select, textarea, .app-row-actions, .stack-detail-actions')) return;
var card = e.target.closest('[data-href]');
if (card) window.location.href = card.dataset.href;
});
async function checkBeforeDeploy(e, name) {
try {
var resp = await fetch('/api/stacks/' + name);
var data = await resp.json();
if (data.ok && data.data && data.data.deployed) {
e.preventDefault();
showAlert('Ez az alkalmazás már telepítve van.');
window.location.reload();
return false;
}
} catch(err) {}
return true;
}
async function syncTemplates() {
const btn = document.getElementById('sync-btn');
const toast = document.getElementById('sync-toast');
if (!btn) return;
const origText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '↻ Frissítés...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/sync', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (toast) {
toast.textContent = data.ok ? (data.message || 'Sablonok frissítve') : ('Hiba: ' + (data.error || 'Ismeretlen hiba'));
toast.className = 'sync-toast ' + (data.ok ? 'sync-toast-ok' : 'sync-toast-err');
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
if (data.ok && data.data && (data.data.new_apps && data.data.new_apps.length > 0 || data.data.updated && data.data.updated.length > 0)) {
setTimeout(function() { window.location.reload(); }, 1500);
}
} catch (err) {
if (toast) {
toast.textContent = 'Hálózati hiba: ' + err.message;
toast.className = 'sync-toast sync-toast-err';
toast.style.display = 'block';
setTimeout(function() { toast.style.display = 'none'; }, 5000);
}
}
btn.innerHTML = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
async function stackAction(event, name, action) {
const btn = event.currentTarget;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Folyamatban...';
btn.classList.add('loading');
try {
const resp = await fetch('/api/stacks/' + name + '/' + action, {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders())
});
const data = await resp.json();
if (!data.ok) {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
window.location.reload();
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeDeleteModal(); });
document.body.appendChild(modal);
try {
var resp = await fetch('/api/stacks/' + name + '/hdd-data');
var data = await resp.json();
var hddInfo = '';
var checkboxHTML = '';
if (data.ok && data.data && data.data.has_hdd_data) {
hddInfo = '<div class="modal-hdd-info"><strong>Felhasználói adatok a merevlemezen:</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> Felhasználói adatok törlése a merevlemezről</label>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás törlése: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Ez a művelet eltávolítja a konténereket, a köteteket és a konfigurációs fájlokat.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">Törlés</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">Bezárás</button></div>';
}
}
function closeDeleteModal() {
var modal = document.getElementById('delete-modal');
if (modal) modal.remove();
}
async function confirmDelete(name) {
var btn = document.getElementById('confirm-delete-btn');
var checkbox = document.getElementById('delete-hdd-check');
var removeHDD = checkbox ? checkbox.checked : false;
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name, {
method: 'DELETE',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('delete-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen törölve!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') törölve lett.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Törlés';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Törlés';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>Betöltés...</h3></div>';
modal.addEventListener('click', function(e) { if (e.target === modal) closeRemoveModal(); });
document.body.appendChild(modal);
try {
var [hddResp, backupResp] = await Promise.all([
fetch('/api/stacks/' + name + '/hdd-data').then(function(r) { return r.json(); }),
fetch('/api/stacks/' + name + '/backup-data').then(function(r) { return r.json(); })
]);
var sections = '';
// Section 1: Always removed
sections += '<div class="modal-section">' +
'<strong>Mindig törlődik:</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>Docker kötetek (adatbázis, alkalmazás konfiguráció)</li>' +
'<li>Telepítési konfiguráció (app.yaml)</li>' +
'<li>Másodlagos mentés ütemezése</li>' +
'</ul></div>';
// Section 2: HDD data
var hddCheckbox = '';
if (hddResp.ok && hddResp.data && hddResp.data.has_hdd_data) {
var hddPaths = '';
hddResp.data.hdd_paths.forEach(function(p) {
hddPaths += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : 'nem létezik') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>Felhasználói adatok a merevlemezen:</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> Felhasználói adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'Ha újratelepíti az alkalmazást, az adatokat újra importálnia kell, mivel az adatbázis törlődik. A megtartott adatok a továbbiakban NEM lesznek automatikusan mentve.' +
'</div></div>';
hddCheckbox = '<script>document.getElementById("remove-hdd-check").addEventListener("change",function(){document.getElementById("remove-hdd-keep-warning").style.display=this.checked?"none":"";});<\/script>';
}
// Section 3: Backup data
var backupCheckbox = '';
if (backupResp.ok && backupResp.data && backupResp.data.has_backups) {
var bkPaths = '';
backupResp.data.backup_paths.forEach(function(p) {
if (p.exists) bkPaths += '<div class="modal-hdd-path">' + p.path + ' (' + p.size_human + ')</div>';
});
if (bkPaths) {
sections += '<div class="modal-section">' +
'<strong>Mentési adatok:</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> Mentési adatok törlése</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'Az éjszakai restic pillanatképek nem törölhetők egyenként — a megőrzési szabályok szerint automatikusan elavulnak.' +
'</div></div>';
}
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Alkalmazás eltávolítása: ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">Az alkalmazás visszaáll "Nincs telepítve" állapotba. A sablon megmarad, újratelepíthető.</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">Ez a művelet nem visszavonható!</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">Mégsem</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">Eltávolítás</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>Hiba</h3><p style="color:var(--text-2)">Nem sikerült lekérni az adatokat: ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">Bezárás</button></div>';
}
}
function closeRemoveModal() {
var modal = document.getElementById('remove-modal');
if (modal) modal.remove();
}
async function confirmRemoveStack(name) {
var btn = document.getElementById('confirm-remove-btn');
var hddCheck = document.getElementById('remove-hdd-check');
var backupCheck = document.getElementById('remove-backup-check');
var removeHDD = hddCheck ? hddCheck.checked : false;
var removeBackups = backupCheck ? backupCheck.checked : false;
btn.disabled = true;
btn.textContent = 'Eltávolítás folyamatban...';
try {
var resp = await fetch('/api/stacks/' + name + '/remove', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({remove_hdd_data: removeHDD, remove_backups: removeBackups})
});
var data = await resp.json();
if (data.ok) {
var modal = document.getElementById('remove-modal');
var removedInfo = '';
if (data.data && data.data.hdd_paths_removed && data.data.hdd_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt adatok: ' + data.data.hdd_paths_removed.join(', ') + '</p>';
}
if (data.data && data.data.backup_paths_removed && data.data.backup_paths_removed.length > 0) {
removedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Törölt mentések: ' + data.data.backup_paths_removed.join(', ') + '</p>';
}
var preservedInfo = '';
if (data.data && data.data.hdd_paths_preserved && data.data.hdd_paths_preserved.length > 0) {
preservedInfo = '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">Megőrzött adatok: ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>Sikeresen eltávolítva!</h3>' +
'<p style="color:var(--text-2)">Az alkalmazás (' + name + ') eltávolítva. Újratelepíthető a Telepítés gombbal.</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">Bezárás</button></div>';
} else {
showAlert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
} catch (err) {
showAlert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Eltávolítás';
}
}
</script>
</body>
</html>
{{end}}