612c417024
gates / gates (push) Successful in 19s
Message bundles (internal/i18n) expanded into templates before parsing, one template set per language. Launcher, /backups, /apps/<slug> and the layout converted; household language setting, POST /settings/language, ?lang= override, report field. Parity test against fixtures captured from unconverted templates; copy gates read templates expanded; new i18n_missing_gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
541 lines
34 KiB
HTML
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,'&').replace(/</g,'<').replace(/>/g,'>')+'</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 "layout.hiba"}} ' + (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 "layout.hiba"}} ' + (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 "layout.megsem"}}</button>' +
|
|
'<button class="btn btn-danger" id="confirm-delete-btn" onclick="confirmDelete(\'' + name + '\')">{{T "layout.torles"}}</button>' +
|
|
'</div>';
|
|
} catch (err) {
|
|
modal.querySelector('.modal-card').innerHTML =
|
|
'<h3>{{T "layout.hiba_2"}}</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 "layout.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 "layout.bezaras"}}</button></div>';
|
|
} else {
|
|
showAlert('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
|
|
btn.disabled = false;
|
|
btn.textContent = '{{T "layout.torles"}}';
|
|
}
|
|
} catch (err) {
|
|
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
|
|
btn.disabled = false;
|
|
btn.textContent = '{{T "layout.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 "layout.megsem"}}</button>' +
|
|
'<button class="btn btn-danger" id="confirm-remove-btn" onclick="confirmRemoveStack(\'' + name + '\')">{{T "layout.eltavolitas"}}</button>' +
|
|
'</div>' + hddCheckbox;
|
|
} catch (err) {
|
|
modal.querySelector('.modal-card').innerHTML =
|
|
'<h3>{{T "layout.hiba_2"}}</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 "layout.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 "layout.bezaras"}}</button></div>';
|
|
} else {
|
|
showAlert('{{T "layout.hiba"}} ' + (data.error || '{{T "layout.ismeretlen_hiba"}}'));
|
|
btn.disabled = false;
|
|
btn.textContent = '{{T "layout.eltavolitas"}}';
|
|
}
|
|
} catch (err) {
|
|
showAlert('{{T "layout.halozati_hiba"}} ' + err.message);
|
|
btn.disabled = false;
|
|
btn.textContent = '{{T "layout.eltavolitas"}}';
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|