Files
felhom-controller/controller/internal/web/templates/layout.html
T
admin ff68b0b433
gates / gates (push) Successful in 20s
v0.248.0: i18n slice 1 release A — apps and settings in English, Hungarian byte-identical (R-556)
Ten pages converted against fixtures captured from unconverted templates (6be55a0); marker
coverage test; English page mask narrowed; English retrieval-promise stems; common keys;
title keys and infraMeta from the bundle; old wording tests read the Hungarian expansion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-17 17:36:08 +02:00

541 lines
34 KiB
HTML

{{define "layout_start"}}
<!DOCTYPE html>
<html lang="{{T "layout.html_lang"}}" 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='{{T "layout.igen"}}';
var no=document.createElement('button');no.type='button';no.className='btn btn-xs btn-outline';no.textContent='{{T "layout.megse"}}';
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>{{T "layout.uzenet"}}</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="{{T "layout.menu"}}">
<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>{{T "layout.inditopult"}}</a></li>
<li><a href="/dashboard" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>{{T "layout.vezerlopult"}}</a></li>
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>{{T "layout.alkalmazasok"}}</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 "layout.tarhely"}}<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}}">{{T "layout.meghajtok"}}</a></li>
<li><a href="/storage/network" class="{{if eq .Page "storage-network"}}active{{end}}">{{T "layout.halozati_tarhely"}}</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>{{T "layout.biztonsagi_mentes"}}<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}}">{{T "layout.attekintes"}}</a></li>
<li><a href="/backups/remote" class="{{if eq .Page "backups-remote"}}active{{end}}">{{T "layout.tavoli_mentes"}}</a></li>
<li><a href="/backups/apps" class="{{if eq .Page "backups-apps"}}active{{end}}">{{T "layout.alkalmazasok"}}</a></li>
<li><a href="/backups/restore" class="{{if eq .Page "backups-restore"}}active{{end}}">{{T "layout.visszaallitas"}}</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>{{T "layout.megosztas"}}<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}}">{{T "layout.halozati_megosztas"}}</a></li>
</ul>
</li>
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}"><svg class="ico"><use href="#i-cpu"/></svg>{{T "layout.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">{{T "layout.beallitasok"}}</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>{{T "layout.rendszer"}}</a></li>
<li><a href="/settings/notifications" class="{{if eq .Page "settings-notifications"}}active{{end}}"><svg class="ico"><use href="#i-bell"/></svg>{{T "layout.ertesitesek"}}</a></li>
<li><a href="/settings/security" class="{{if eq .Page "settings-security"}}active{{end}}"><svg class="ico"><use href="#i-lock"/></svg>{{T "layout.biztonsag_es_hozzaferes"}}</a></li>
</ul>
<div class="sidebar-footer">
<span class="version">{{.Version}}</span>{{if .LangSwitch}}<form method="POST" action="/settings/language" class="lang-switch">{{.CSRFField}}<input type="hidden" name="back" value="{{.LangSwitchBack}}"><button type="submit" name="lang" value="hu" class="lang-switch-btn{{if eq .Lang "hu"}} active{{end}}" lang="hu">Magyar</button><button type="submit" name="lang" value="en" class="lang-switch-btn{{if eq .Lang "en"}} active{{end}}" lang="en">English</button></form>{{end}}
{{if .AuthEnabled}}<a href="/logout" class="logout-link">{{T "layout.kijelentkezes"}}</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">{{T "layout.a_vezerlopult_meg_nincs_jelszoval"}}</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. */}}
{{T "layout.a_korabbi_tavoli_menteseidet_nap"}}
{{if .RecoveryAbandonRetrievalOffered}}{{T "layout.addig_meg_visszaszerezheted_oket_a"}}{{else}}{{T "layout.hogy_ezek_meg_visszaszerezhetok_e"}}{{end}}
{{else if ge .RecoveryReminderTier 14}}
{{T "layout.ket_hete_varnak_rad_a"}}
{{else if ge .RecoveryReminderTier 7}}
{{T "layout.mar_egy_hete_megvannak_a"}}
{{else if ge .RecoveryReminderTier 3}}
{{T "layout.a_korabbi_tavoli_menteseid_megvannak"}}
{{else}}
{{T "layout.a_korabbi_tavoli_menteseid_megvannak_2"}}
{{end}}
<a href="/recovery">{{T "layout.megnezem"}}</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">{{T "layout.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">{{T "layout.ne_emlekeztessen_ujra"}}</button>
</form>
{{end}}
</span>
</div>
</div>
{{end}}
{{/* R-543 (v0.245.0) — the escrow reminder bar. The off-site tier is ON by default and does NOT RUN
until the household creates its recovery code (zero-knowledge escrow: their code is the only key).
The pause is the design; what was missing was ASKING. Same mechanism as the R-241 bar above —
dismissable for the visit, back at the next one, gone for good when the escrow completes. */}}
{{if .EscrowBanner}}
<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">
{{T "layout.a_tavoli_mentes_szunetel_amig"}}
</span>
<span class="alert-actions">
<form method="POST" action="/backup/escrow/banner/dismiss" style="display:inline">
{{.CSRFField}}<input type="hidden" name="back" value="{{.EscrowBannerBack}}">
<button type="submit" class="btn btn-sm btn-outline">{{T "layout.most_nem"}}</button>
</form>
</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('{{T "layout.ez_az_alkalmazas_mar_telepitve"}}');
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 = '{{T "layout.frissites"}}';
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 || '{{T "layout.sablonok_frissitve"}}') : ('{{T "common.hiba_kettospont"}} ' + (data.error || '{{T "layout.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 = '{{T "layout.halozati_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 = '{{T "layout.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('{{T "common.hiba_kettospont"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
return;
}
// Slice 4: an update is ACCEPTED, not finished (HTTP 202). Follow its phases on this
// button until the box says it is done or failed — never reload into a page that still
// shows the old state as if nothing were happening.
if (action === 'update') {
followUpdate(name, btn);
return;
}
window.location.reload();
} catch (err) {
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
btn.textContent = origText;
btn.disabled = false;
btn.classList.remove('loading');
}
}
function followUpdate(name, btn) {
var started = Date.now();
var timer = setInterval(async function() {
if (Date.now() - started > 30 * 60 * 1000) { clearInterval(timer); window.location.reload(); return; }
try {
var r = await fetch('/api/stacks/' + name);
var d = await r.json();
if (!d.ok || !d.data) return;
if (d.data.update_phase_label) btn.textContent = d.data.update_phase_label;
if (!d.data.updating) { clearInterval(timer); window.location.reload(); }
} catch (e) {}
}, 3000);
}
async function deleteOrphanStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'delete-modal';
modal.innerHTML = '<div class="modal-card"><h3>{{T "layout.betoltes"}}</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>{{T "layout.felhasznaloi_adatok_a_merevlemezen"}}</strong>';
data.data.hdd_paths.forEach(function(p) {
hddInfo += '<div class="modal-hdd-path">' + p.path + ' (' + (p.exists ? p.size_human : '{{T "layout.nem_letezik"}}') + ')</div>';
});
hddInfo += '</div>';
checkboxHTML = '<label class="modal-checkbox"><input type="checkbox" id="delete-hdd-check"> {{T "layout.felhasznaloi_adatok_torlese_a_merevlemez"}}</label>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>{{T "layout.alkalmazas_torlese"}} ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">{{T "layout.ez_a_muvelet_eltavolitja_a"}}</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">{{T "layout.ez_a_muvelet_nem_visszavonhato"}}</div>' +
hddInfo + checkboxHTML +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeDeleteModal()">{{T "common.megsem"}}</button>' +
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">{{T "common.torles"}}</button>' +
'</div>';
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>{{T "common.hiba"}}</h3><p style="color:var(--text-2)">{{T "layout.nem_sikerult_lekerni_az_adatokat"}} ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeDeleteModal()">{{T "common.bezaras"}}</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 "layout.torles_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 "layout.torolt_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">{{T "layout.megorzott_adatok"}} ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>{{T "layout.sikeresen_torolve"}}</h3>' +
'<p style="color:var(--text-2)">{{T "layout.az_alkalmazas"}}' + name + '{{T "layout.torolve_lett"}}</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">{{T "common.bezaras"}}</button></div>';
} else {
showAlert('{{T "common.hiba_kettospont"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
btn.disabled = false;
btn.textContent = '{{T "common.torles"}}';
}
} catch (err) {
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
btn.disabled = false;
btn.textContent = '{{T "common.torles"}}';
}
}
async function removeStack(name) {
var modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.id = 'remove-modal';
modal.innerHTML = '<div class="modal-card"><h3>{{T "layout.betoltes"}}</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>{{T "layout.mindig_torlodik"}}</strong>' +
'<ul style="margin:.25rem 0;padding-left:1.2rem;color:var(--text-2);font-size:.85rem">' +
'<li>{{T "layout.docker_kotetek_adatbazis_alkalmazas_konf"}}</li>' +
'<li>{{T "layout.telepitesi_konfiguracio_app_yaml"}}</li>' +
'<li>{{T "layout.masodlagos_mentes_utemezese"}}</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 : '{{T "layout.nem_letezik"}}') + ')</div>';
});
sections += '<div class="modal-section">' +
'<strong>{{T "layout.felhasznaloi_adatok_a_merevlemezen"}}</strong>' + hddPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-hdd-check"> {{T "layout.felhasznaloi_adatok_torlese"}}</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem" id="remove-hdd-keep-warning">' +
'{{T "layout.ha_ujratelepiti_az_alkalmazast_az"}}' +
'</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>{{T "layout.mentesi_adatok"}}</strong>' + bkPaths +
'<label class="modal-checkbox"><input type="checkbox" id="remove-backup-check"> {{T "layout.mentesi_adatok_torlese"}}</label>' +
'<div class="alert alert-info" style="margin-top:.5rem;font-size:.8rem">' +
'{{T "layout.az_ejszakai_restic_pillanatkepek_nem"}}' +
'</div></div>';
}
}
modal.querySelector('.modal-card').innerHTML =
'<h3>{{T "layout.alkalmazas_eltavolitasa"}} ' + name + '</h3>' +
'<p style="color:var(--text-2);font-size:.9rem;margin-bottom:.75rem">{{T "layout.az_alkalmazas_visszaall_nincs_telepitve"}}</p>' +
'<div class="alert alert-warning" style="margin-bottom:.75rem">{{T "layout.ez_a_muvelet_nem_visszavonhato"}}</div>' +
sections +
'<div class="modal-actions">' +
'<button class="btn btn-outline" onclick="closeRemoveModal()">{{T "common.megsem"}}</button>' +
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">{{T "common.eltavolitas"}}</button>' +
'</div>' + hddCheckbox;
} catch (err) {
modal.querySelector('.modal-card').innerHTML =
'<h3>{{T "common.hiba"}}</h3><p style="color:var(--text-2)">{{T "layout.nem_sikerult_lekerni_az_adatokat"}} ' + err.message + '</p>' +
'<div class="modal-actions"><button class="btn btn-outline" onclick="closeRemoveModal()">{{T "common.bezaras"}}</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 = '{{T "layout.eltavolitas_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 "layout.torolt_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 "layout.torolt_mentesek"}} ' + 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">{{T "layout.megorzott_adatok"}} ' + data.data.hdd_paths_preserved.join(', ') + '</p>';
}
if (data.data && data.data.hdd_note) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">' + data.data.hdd_note + '</p>';
}
if (data.data && data.data.backup_paths_refused && data.data.backup_paths_refused.length > 0) {
preservedInfo += '<p style="color:var(--text-2);font-size:.85rem;margin-top:.5rem">{{T "layout.nem_torolt_mentesek"}} ' + data.data.backup_paths_refused.join('; ') + '</p>';
}
modal.querySelector('.modal-card').innerHTML =
'<h3>{{T "layout.sikeresen_eltavolitva"}}</h3>' +
'<p style="color:var(--text-2)">{{T "layout.az_alkalmazas"}}' + name + '{{T "layout.eltavolitva_ujratelepitheto_a_telepites"}}</p>' +
removedInfo + preservedInfo +
'<div class="modal-actions"><button class="btn btn-primary" onclick="window.location.href=\'/stacks\'">{{T "common.bezaras"}}</button></div>';
} else {
showAlert('{{T "common.hiba_kettospont"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
btn.disabled = false;
btn.textContent = '{{T "common.eltavolitas"}}';
}
} catch (err) {
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
btn.disabled = false;
btn.textContent = '{{T "common.eltavolitas"}}';
}
}
</script>
</body>
</html>
{{end}}