Files
felhom-controller/controller/internal/web/templates/settings_system.html
T
admin 201f936e9a
gates / gates (push) Successful in 48s
v0.310.0: „A gép kikapcsolása" — the household turns the whole machine off (R-998); honest restart text (R-999)
POST /api/server/poweroff (dashboard login, POST only, the typed word KIKAPCSOLÁS / TURN OFF)
→ agent POST /host/poweroff (MinAgent 0.155.0; hidden for an older agent). The controller refuses
during its own backup/restore/app update; the agent's reasons are shown as household sentences.
„Kiszolgáló újraindítása" becomes „A Felhom újraindítása" and says it restarts the Felhom part;
/api/server/reboot is POST only (R-983). Tests red-proofed; parity fixtures regenerated on purpose.
2026-10-13 20:17:30 +02:00

487 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{define "settings_system"}}
{{template "layout_start" .}}
<div class="page-header">
<h2>{{T "settings_system.beallitasok_rendszer"}}</h2>
</div>
<!-- Section A: System Configuration (read-only) -->
<div class="settings-card">
<h3>{{T "settings_system.rendszer_konfiguracio"}}</h3>
<p class="settings-card-desc">{{T "settings_system.az_uzemelteto_altal_beallitott_ertekek"}}</p>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">{{T "common.ugyfel_azonosito"}}</span>
<span class="settings-value mono">{{.CustomerID}}</span>
</div>
<div class="settings-row">
<span class="settings-label">{{T "settings_system.ugyfel_neve"}}</span>
<span class="settings-value">{{.CustomerName}}</span>
</div>
<div class="settings-row">
<span class="settings-label">Domain</span>
<span class="settings-value mono">{{.CustomerDomain}}</span>
</div>
{{if .GitRepoURL}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.alkalmazas_sablon_forras"}}</span>
<span class="settings-value mono settings-value-truncate">{{.GitRepoURL}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.sablon_szinkronizalas"}}</span>
<span class="settings-value mono">{{.GitSyncInterval}}</span>
</div>
<div class="settings-row">
<span class="settings-label">{{T "settings_system.biztonsagi_mentes"}}</span>
<span class="settings-value">{{if .BackupEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.aktiv"}}</span>{{else}}<span class="state-text-neutral">{{T "settings_system.inaktiv"}}</span>{{end}}</span>
</div>
{{if .BackupEnabled}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.mentes_utemezes"}}</span>
<span class="settings-value mono">{{.DBDumpSchedule}} / {{.ResticSchedule}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Monitoring</span>
<span class="settings-value">{{if .MonitoringEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.aktiv"}}</span>{{else}}<span class="state-text-neutral">{{T "settings_system.inaktiv"}}</span>{{end}}</span>
</div>
{{if .MonitoringEnabled}}
<div class="settings-row">
<span class="settings-label">Healthchecks URL</span>
<span class="settings-value mono settings-value-truncate">{{if .HealthchecksBase}}{{.HealthchecksBase}}{{else}}–{{end}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.hub_jelentes"}}</span>
<span class="settings-value">{{if .HubEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.aktiv"}}</span>{{else}}–{{end}}</span>
</div>
</div>
</div>
<!-- Section: Version & Update -->
<div class="settings-card">
<h3>{{T "settings_system.verzio_es_frissites"}}</h3>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">{{T "settings_system.jelenlegi_verzio"}}</span>
<span class="settings-value mono">{{.Version}}</span>
</div>
{{if .SelfUpdateEnabled}}
{{if .LatestVersion}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.legujabb_verzio"}}</span>
<span class="settings-value mono">
{{.LatestVersion}}
{{if .UpdateAvailable}}
<span class="state-text-run" style="margin-left:0.5em;">{{T "settings_system.frissites_elerheto"}}</span>
{{else}}
<span style="margin-left:0.5em; color:var(--text-3);">{{T "settings_system.naprakesz"}}</span>
{{end}}
</span>
</div>
{{end}}
{{if .LastCheckTime}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.utolso_ellenorzes"}}</span>
<span class="settings-value mono">{{.LastCheckTime}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Registry</span>
<span class="settings-value">{{if .RegistryAnonymous}}{{T "settings_system.nyilvanos_hitelesites_nelkul"}}{{else}}{{T "settings_system.hitelesitett"}}{{end}}</span>
</div>
{{if .LastCheckError}}
<div class="settings-row">
<span class="settings-label">{{T "common.hiba"}}</span>
<span class="settings-value state-text-crit">{{.LastCheckError}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.automatikus_frissites"}}</span>
<span class="settings-value">
{{if .AutoUpdateEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.aktiv"}}</span> <span class="mono">({{.AutoUpdateTime}})</span>{{else}}–{{end}}
</span>
</div>
{{if .ControllerFloor}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.minimalis_verzio_uzemelteto"}}</span>
<span class="settings-value mono">
{{.ControllerFloor}}
<span style="margin-left:0.5em; color:#888;">{{T "settings_system.a_rendszer_automatikusan_erre_a"}}</span>
</span>
</div>
{{end}}
{{if .UpdateRunning}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.allapot"}}</span>
<span class="settings-value state-text-progress" id="auto-update-status"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> {{T "settings_system.frissites_folyamatban_a_vezerlo_hamarosa"}}</span>
</div>
{{end}}
{{with .LastUpdateState}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.utolso_frissites"}}</span>
<span class="settings-value">
{{if eq .Status "success"}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> {{T "settings_system.sikeres"}}</span> ({{.PreviousVersion}} → {{.TargetVersion}})
{{else if eq .Status "failed"}}<span class="state-text-crit"><svg class="ico ico-sm"><use href="#i-x"/></svg> {{T "settings_system.sikertelen"}}</span> — {{.Error}}
{{else if eq .Status "pending"}}<span class="state-text-progress"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> {{T "settings_system.folyamatban"}}</span>
{{end}}
</span>
</div>
{{end}}
<div class="settings-row" style="padding-top: 0.5em;">
<span class="settings-label"></span>
<span class="settings-value">
<button class="btn btn-secondary btn-sm" id="btn-check-update" onclick="checkUpdate()">{{T "settings_system.frissites_keresese"}}</button>
{{if .UpdateAvailable}}
<button class="btn btn-primary btn-sm" id="btn-trigger-update" onclick="triggerUpdate()" style="margin-left:0.5em;">{{T "settings_system.frissites_telepitese"}}</button>
{{end}}
<span id="update-status-msg" style="margin-left:0.5em; display:none;"></span>
</span>
</div>
{{end}}
</div>
</div>
<!-- Section: Automatic app updates (v0.271.0, `09` §3 decision 12) — the per-box switch, ON by default. -->
<div class="settings-card" id="app-update">
<h3>{{T "settings_system.app_update_title"}}</h3>
<p class="settings-card-desc">{{T "settings_system.app_update_desc"}}</p>
{{if .AppUpdateSuccess}}<div class="alert alert-info">{{.AppUpdateSuccess}}</div>{{end}}
{{if .AppUpdateError}}<div class="alert alert-error">{{.AppUpdateError}}</div>{{end}}
<form method="POST" action="/settings/app-update">
{{.CSRFField}}
<div class="form-group">
<label style="display:flex;align-items:center;gap:.5rem">
<input type="checkbox" name="app_update_unattended" value="on" {{if .AppUpdateUnattended}}checked{{end}} data-app-update-unattended="{{.AppUpdateUnattended}}">
{{T "settings_system.app_update_toggle"}}
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">{{T "settings_system.app_update_save"}}</button>
</div>
</form>
</div>
<!-- Section: Network (R-66) — the box's own address. Every value is live-computed per render and
stored NOWHERE (S-5: a DHCP lease persisted anywhere eventually misdirects people); a missing
value renders „—" because an address-less row beats a wrong address. -->
<div class="settings-card">
<h3>{{T "settings_system.halozat"}}</h3>
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">{{T "settings_system.helyi_cim_lan"}}</span>
<span class="settings-value mono">{{if .NetLANAddress}}{{.NetLANAddress}}{{else}}— <span style="color:var(--text-3);">{{T "settings_system.nem_allapithato_meg"}}</span>{{end}}</span>
</div>
{{if .NetSMBName}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.halozati_nev"}}</span>
<span class="settings-value mono">\\{{.NetSMBName}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">{{T "settings_system.atjaro"}}</span>
<span class="settings-value mono">{{if .NetGateway}}{{.NetGateway}}{{else}}— <span style="color:var(--text-3);">{{T "settings_system.nem_allapithato_meg"}}</span>{{end}}</span>
</div>
</div>
<p class="settings-card-desc" style="margin-top:0.5em;">{{T "settings_system.ezek_az_ertekek_a_helyi"}}</p>
</div>
<div class="settings-card">
<h3>{{T "settings_system.szerver_memoria_ram"}}</h3>
{{if .MemorySupported}}
{{if .MemoryReachable}}
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">{{T "settings_system.jelenlegi_memoria"}}</span>
<span class="settings-value mono">{{.MemoryAllocatedMB}} MB</span>
</div>
<div class="settings-row">
<span class="settings-label">{{T "settings_system.felhasznalt"}}</span>
<span class="settings-value mono">{{.MemoryUsageMB}} MB</span>
</div>
<div class="settings-row">
<span class="settings-label">{{T "settings_system.megengedett_tartomany"}}</span>
<span class="settings-value mono">{{.MemoryMinMB}} MB – {{.MemoryMaxMB}} MB</span>
</div>
<div class="settings-row" style="padding-top: 0.5em;">
<span class="settings-label">{{T "settings_system.uj_meret_mb"}}</span>
<span class="settings-value">
<input type="number" id="mem-input" class="input mono" style="width:8em;"
value="{{.MemoryAllocatedMB}}" min="{{.MemoryMinMB}}" max="{{.MemoryMaxMB}}" step="256"
data-current="{{.MemoryAllocatedMB}}">
<button class="btn btn-primary btn-sm" id="btn-mem-resize" onclick="resizeMemory()" style="margin-left:0.5em;">{{T "settings_system.atmeretezes"}}</button>
<span id="mem-status-msg" style="margin-left:0.5em; display:none;"></span>
</span>
</div>
</div>
{{else}}
<div class="settings-grid">
<div class="settings-row">
<span class="settings-label">{{T "settings_system.jelenlegi_memoria"}}</span>
<span class="settings-value mono">{{if .MemoryProcMB}}{{.MemoryProcMB}} MB{{else}}n/a{{end}}</span>
</div>
</div>
<p class="settings-card-desc">{{T "settings_system.a_kiszolgalo_ugynok_jelenleg_nem"}}</p>
{{end}}
{{else}}
<p class="settings-card-desc">{{T "settings_system.a_memoria_atmeretezesehez_a_kiszolgalo"}}</p>
{{end}}
</div>
<div id="dialog-root"></div>
<script>
// Light overlay dialog (D1) — replaces the native blocking browser dialogs (same texts).
function escText(s){var d=document.createElement('div');d.textContent=String(s==null?'':s);return d.innerHTML;}
function closeDialog(){ var r=document.getElementById('dialog-root'); if(r) r.innerHTML=''; }
function openDialog(opts){
var root=document.getElementById('dialog-root');
if(!root) return;
root.innerHTML='<div class="confirm-overlay" onclick="if(event.target===this)document.getElementById(\'dialog-cancel\').click()"><div class="confirm-box">'
+'<h3>'+escText(opts.title||'{{T "settings_system.megerosites"}}')+'</h3>'
+'<p style="white-space:pre-line">'+escText(opts.message||'')+'</p>'
// R-998: typeToConfirm — the button stays off until the household types the word (the storage page's pattern).
+(opts.typeToConfirm?'<div class="confirm-input"><label>{{T "settings_system.megerositeshez_ird_be"}} <strong class="mono">'+escText(opts.typeToConfirm)+'</strong></label>'
+'<input type="text" id="dialog-type" class="form-control" autocomplete="off" placeholder="'+escText(opts.typeToConfirm)+'"></div>':'')
+'<div class="form-actions"><button id="dialog-go" class="btn '+(opts.typeToConfirm?'btn-danger-outline':'btn-primary')+'"'+(opts.typeToConfirm?' disabled':'')+'>'+escText(opts.confirmLabel||'{{T "settings_system.megerosites"}}')+'</button>'
+'<button type="button" class="btn btn-outline" id="dialog-cancel">{{T "common.megsem"}}</button></div>'
+'</div></div>';
var typed=document.getElementById('dialog-type');
if(typed){ typed.oninput=function(){ document.getElementById('dialog-go').disabled=(this.value.trim().toUpperCase()!==opts.typeToConfirm.toUpperCase()); }; }
document.getElementById('dialog-go').onclick=function(){ var v=typed?typed.value:''; closeDialog(); if(opts.onConfirm) opts.onConfirm(v); };
document.getElementById('dialog-cancel').onclick=function(){ closeDialog(); if(opts.onCancel) opts.onCancel(); };
}
function checkUpdate() {
var btn = document.getElementById('btn-check-update');
var msg = document.getElementById('update-status-msg');
btn.disabled = true;
btn.textContent = '{{T "settings_system.ellenorzes"}}';
msg.style.display = 'none';
fetch('/api/selfupdate/check', {method:'POST', headers: csrfHeaders()})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
location.reload();
} else {
msg.textContent = data.error || '{{T "common.hiba_tortent"}}';
msg.style.display = 'inline';
btn.disabled = false;
btn.textContent = '{{T "settings_system.frissites_keresese"}}';
}
})
.catch(function() {
msg.textContent = '{{T "settings_system.kapcsolodasi_hiba"}}';
msg.style.display = 'inline';
btn.disabled = false;
btn.textContent = '{{T "settings_system.frissites_keresese"}}';
});
}
function triggerUpdate() {
openDialog({title:'{{T "settings_system.vezerlo_frissitese"}}', confirmLabel:'{{T "settings_system.frissites"}}',
message:'{{T "settings_system.biztosan_frissiti_a_controllert_n"}}',
onConfirm:doTriggerUpdate});
}
function doTriggerUpdate() {
var btn = document.getElementById('btn-trigger-update');
var checkBtn = document.getElementById('btn-check-update');
var msg = document.getElementById('update-status-msg');
btn.disabled = true;
btn.textContent = '{{T "settings_system.frissites_2"}}';
if (checkBtn) checkBtn.disabled = true;
msg.textContent = '{{T "settings_system.frissites_folyamatban"}}';
msg.style.display = 'inline';
fetch('/api/selfupdate/update', {method:'POST', headers: csrfHeaders()})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
msg.textContent = '{{T "settings_system.ujraindulas"}}';
pollUntilBack();
} else {
msg.textContent = data.error || '{{T "common.hiba_tortent"}}';
btn.disabled = false;
btn.textContent = '{{T "settings_system.frissites_telepitese"}}';
if (checkBtn) checkBtn.disabled = false;
}
})
.catch(function() {
msg.textContent = '{{T "settings_system.kapcsolodasi_hiba"}}';
pollUntilBack();
});
}
function resizeMemory() {
var input = document.getElementById('mem-input');
var target = parseInt(input.value, 10);
var current = parseInt(input.getAttribute('data-current'), 10);
if (isNaN(target) || target <= 0) {
showMemMsg('{{T "settings_system.ervenytelen_memoria_ertek"}}', true);
return;
}
if (target < current) {
openDialog({title:'{{T "settings_system.memoria_csokkentese"}}', confirmLabel:'{{T "settings_system.csokkentes"}}',
message:'{{T "settings_system.a_memoria_csokkentese_hatassal_lehet"}}',
onConfirm:function(){ doResizeMemory(target); }});
return;
}
doResizeMemory(target);
}
function doResizeMemory(target) {
var btn = document.getElementById('btn-mem-resize');
btn.disabled = true;
btn.textContent = '{{T "settings_system.atmeretezes_2"}}';
fetch('/api/system/memory/resize', {
method:'POST', headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
body: JSON.stringify({memory_mb: target})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
showMemMsg(data.error || data.message || '{{T "settings_system.kesz"}}', false);
setTimeout(function(){ location.reload(); }, 1200);
} else {
showMemMsg(data.error || '{{T "settings_system.a_muvelet_nem_sikerult"}}', true);
btn.disabled = false;
btn.textContent = '{{T "settings_system.atmeretezes"}}';
}
})
.catch(function() {
showMemMsg('{{T "settings_system.kapcsolodasi_hiba"}}', true);
btn.disabled = false;
btn.textContent = '{{T "settings_system.atmeretezes"}}';
});
}
function showMemMsg(text, isErr) {
var msg = document.getElementById('mem-status-msg');
if (!msg) return;
msg.textContent = text;
msg.style.color = isErr ? 'var(--danger, #c0392b)' : 'var(--text-2)';
msg.style.display = 'inline';
}
function pollUntilBack() {
var iv = setInterval(function() {
fetch('/api/health')
.then(function(r) {
if (r.ok) {
clearInterval(iv);
location.reload();
}
})
.catch(function() {});
}, 3000);
}
// If an update is already in flight when the page loads (e.g. a floor-driven AUTO-update kicked off by
// the hub, not a button click), surface the same restart-poll panel so the page recovers itself.
{{if .UpdateRunning}}
pollUntilBack();
{{end}}
</script>
<!-- Section: Controller restart (self-serve) — always available (not gated on RetrievalPassword) -->
<div class="settings-card">
<h3>{{T "settings_system.vezerlo_ujrainditasa"}}</h3>
<p class="settings-card-desc">
{{T "settings_system.ha_a_vezerlo_hibasan_mukodik"}}
</p>
<div id="restart-status"></div>
<button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">{{T "settings_system.vezerlo_ujrainditasa"}}</button>
</div>
<!-- Section: Full server (guest) restart — a deliberate maintenance affordance, sibling to the controller restart -->
<div class="settings-card">
<h3>{{T "settings_system.kiszolgalo_ujrainditasa"}}</h3>
<p class="settings-card-desc">
{{T "settings_system.az_egesz_kiszolgalo_szerver_ujrainditasa"}}
</p>
<div id="server-restart-status"></div>
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">{{T "settings_system.kiszolgalo_ujrainditasa"}}</button>
</div>
<!-- Section: R-998 — „A gép kikapcsolása": the whole machine off (agent POST /host/poweroff, doc 14). Dashboard login
only; the household types the word; the agent may still refuse (its reason is shown). Hidden for an old agent. -->
<div class="settings-card" id="poweroff-card">
<h3>{{T "settings_system.a_gep_kikapcsolasa"}}</h3>
{{if .PowerOffSupported}}
<p class="settings-card-desc">{{T "settings_system.a_gep_teljesen_kikapcsol"}}</p>
<p class="settings-card-desc"><strong>{{T "settings_system.ujra_bekapcsolni_csak_otthon"}}</strong></p>
<div id="poweroff-status"></div>
<button type="button" class="btn btn-danger-outline" id="btn-poweroff" onclick="powerOffMachine()">{{T "settings_system.a_gep_kikapcsolasa"}}</button>
{{else}}
<p class="settings-card-desc" data-poweroff-unavailable>{{T "settings_system.kikapcsolas_frissites_utan"}}</p>
{{end}}
</div>
<script>
function restartController() {
openDialog({title:'{{T "settings_system.vezerlo_ujrainditasa"}}', confirmLabel:'{{T "common.ujrainditas"}}',
message:'{{T "settings_system.biztosan_ujrainditja_a_vezerlot_a"}}',
onConfirm:doRestartController});
}
function doRestartController() {
var btn = document.getElementById('btn-restart-controller');
var status = document.getElementById('restart-status');
btn.disabled = true;
status.innerHTML = '<div class="alert alert-info">{{T "settings_system.ujrainditas_folyamatban_ujracsatlakozas"}}</div>';
fetch('/api/selfrestart', { method: 'POST', headers: csrfHeaders() })
.then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless
}
function restartServer() {
openDialog({title:'{{T "settings_system.kiszolgalo_ujrainditasa"}}', confirmLabel:'{{T "common.ujrainditas"}}',
message:'{{T "settings_system.biztosan_ujrainditja_a_kiszolgalot_az"}}',
onConfirm:doRestartServer});
}
function doRestartServer() {
var btn = document.getElementById('btn-restart-server');
var status = document.getElementById('server-restart-status');
if (btn) btn.disabled = true;
if (status) status.innerHTML = '<div class="alert alert-info">{{T "settings_system.ujrainditas_folyamatban_a_vezerlopult_ne"}}</div>';
fetch('/api/server/reboot', { method: 'POST', headers: csrfHeaders() })
.then(function(){ pollRestart(0); })
.catch(function(){ pollRestart(0); }); // the guest reboot drops the connection — poll regardless
}
function powerOffMachine() {
var word = '{{T "settings_system.kikapcsolas_szo"}}';
openDialog({title:'{{T "settings_system.a_gep_kikapcsolasa"}}', confirmLabel:'{{T "settings_system.kikapcsolas"}}',
message:'{{T "settings_system.biztosan_kikapcsolod_a_gepet"}}', typeToConfirm: word,
onConfirm:doPowerOffMachine});
}
function doPowerOffMachine(typed) {
var btn = document.getElementById('btn-poweroff');
var status = document.getElementById('poweroff-status');
if (btn) btn.disabled = true;
var h = csrfHeaders(); h['Content-Type'] = 'application/json';
fetch('/api/server/poweroff', { method: 'POST', headers: h, body: JSON.stringify({confirm: typed}) })
.then(function(r){ return r.json().catch(function(){ return {ok:false}; }); })
.then(function(d){
if (d && d.ok) {
status.innerHTML = '<div class="alert alert-info">' + escText('{{T "settings_system.a_gep_kikapcsol_folyamatban"}}') + '</div>';
return; // the box goes down — nothing to poll
}
status.innerHTML = '<div class="alert alert-error">' + escText((d && d.error) || '{{T "common.hiba_tortent"}}') + '</div>';
if (btn) btn.disabled = false;
})
.catch(function(){
status.innerHTML = '<div class="alert alert-error">' + escText('{{T "settings_system.kapcsolodasi_hiba"}}') + '</div>';
if (btn) btn.disabled = false;
});
}
function pollRestart(attempt) {
if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently
document.getElementById('restart-status').innerHTML =
'<div class="alert alert-error">{{T "settings_system.az_ujrainditas_a_vartnal_tovabb"}}</div>';
return;
}
setTimeout(function(){
fetch('/', { method: 'GET', cache: 'no-store' })
.then(function(r){ if (r.ok) { window.location.reload(); } else { pollRestart(attempt + 1); } })
.catch(function(){ pollRestart(attempt + 1); });
}, 2000);
}
</script>
{{template "layout_end" .}}
{{end}}