v0.39.1: 8C orphan-template cleanup (delete 5 dead templates)

Remove five orphaned HTML templates left behind when slice 8C retired the
disk/storage/restore web handlers (storage_handlers.go, handler_restore.go and
the /api/storage/* + /api/restore/* routes): storage_init, storage_attach,
migrate, migrate_drive, restore. Zero .go references, zero cross-template
references, no route, no nav entry; embed is a glob so deletion is safe (14
templates remain, build + tests green). No behaviour change; the deleted pages
were already unreachable.

Also ships the live demo validation (v0.39.0) writeup in REPORT.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:24:13 +02:00
parent d8d1e17758
commit 6e77bea4d3
7 changed files with 217 additions and 1809 deletions
@@ -1,264 +0,0 @@
{{define "migrate"}}
{{template "layout_start" .}}
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/stacks/{{.Meta.Slug}}/deploy" class="btn btn-sm btn-outline">← Vissza</a>
<h2>{{.Meta.DisplayName}} — Adatáthelyezés</h2>
</div>
</div>
<div class="settings-card" id="migrate-form-card">
<h3>Adatok áthelyezése másik tárolóra</h3>
<div class="settings-grid" style="margin-bottom:1.5rem">
<div class="settings-row">
<span class="settings-label">Jelenlegi tárhely</span>
<span class="settings-value mono">{{.CurrentLabel}} ({{.CurrentHDDPath}})</span>
</div>
{{if .DataSizeHuman}}
<div class="settings-row">
<span class="settings-label">Adatméret</span>
<span class="settings-value mono">{{.DataSizeHuman}}</span>
</div>
{{end}}
</div>
<div class="form-group">
<label for="target-path">Cél tárhely <span class="required">*</span></label>
<select id="target-path" class="form-control">
{{range .OtherPaths}}
<option value="{{.Path}}">{{.Label}} ({{.Path}}) — {{.FreeHuman}} szabad</option>
{{end}}
</select>
</div>
<div class="alert alert-warning" style="margin-bottom:1.5rem">
<strong>Figyelmeztetések:</strong>
<ul style="margin:.5rem 0 0 1rem;padding:0">
<li>Az alkalmazás a mozgatás idejére leáll</li>
<li>Nagy adatmennyiségnél ez percekig tarthat</li>
<li>DB mentés fájlok is átkerülnek</li>
<li>A migráció után azonnal lefut egy biztonsági mentés az új meghajtón</li>
</ul>
</div>
<div style="margin-bottom:1.5rem">
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
<input type="checkbox" id="auto-delete" checked>
<span>Régi adatok törlése a forrás meghajtóról</span>
</label>
<span class="form-hint" style="margin-left:1.5rem">Ha bekapcsolva, a forrás meghajtóról az alkalmazás adatai és DB mentései automatikusan törlődnek a sikeres áthelyezés után.</span>
</div>
<div id="migrate-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
<div class="form-actions" style="gap:.75rem">
<button class="btn btn-primary" onclick="startMigrate()">📦 Mozgatás indítása</button>
<a href="/stacks/{{.Meta.Slug}}/deploy" class="btn btn-outline">Mégsem</a>
</div>
</div>
<div class="settings-card" id="migrate-progress-card" style="display:none">
<h3>Adatok áthelyezése...</h3>
<div class="disk-progress-steps" id="mig-steps">
<div class="disk-step" id="mstep-stopping"><span class="disk-step-icon"></span> Alkalmazás leállítása</div>
<div class="disk-step" id="mstep-copying"><span class="disk-step-icon"></span> Adatok másolása</div>
<div class="disk-step" id="mstep-updating"><span class="disk-step-icon"></span> Konfiguráció frissítése</div>
<div class="disk-step" id="mstep-starting"><span class="disk-step-icon"></span> Alkalmazás indítása</div>
<div class="disk-step" id="mstep-cleaning"><span class="disk-step-icon"></span> Régi adatok törlése</div>
<div class="disk-step" id="mstep-backing_up"><span class="disk-step-icon"></span> Biztonsági mentés</div>
<div class="disk-step" id="mstep-done"><span class="disk-step-icon"></span> Kész</div>
</div>
<div class="disk-progress-bar-wrap" style="margin-top:1.5rem">
<div class="system-bar" style="height:12px;border-radius:6px">
<div class="system-bar-fill system-bar-green" id="mig-progress-bar" style="width:0%;transition:width .4s ease;height:12px;border-radius:6px"></div>
</div>
<span class="mono form-hint" id="mig-progress-pct">0%</span>
</div>
<div id="mig-progress-msg" class="form-hint" style="margin-top:.75rem"></div>
<div id="mig-elapsed" class="form-hint mono" style="margin-top:.25rem"></div>
<div id="mig-progress-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
</div>
<div class="settings-card" id="migrate-done-card" style="display:none">
<h3>✅ Adatáthelyezés kész!</h3>
<p id="done-msg" style="margin-top:.75rem;color:var(--text-secondary)">
Az alkalmazás az új tárolóról fut.
</p>
<div id="done-tier2-warning" class="alert alert-warning" style="display:none;margin-top:1rem">
A 2. szintű mentés törlésre került, mert a cél meghajtó megegyezett a mentési céllal.
<a href="/stacks/{{.Meta.Slug}}/deploy">Újrakonfigurálás →</a>
</div>
<div id="done-manual-steps" class="alert alert-warning" style="margin-top:1rem">
<strong>Javasolt lépések:</strong>
<ol style="margin:.5rem 0 0 1rem;padding:0">
<li>Ellenőrizd, hogy az alkalmazás megfelelően működik</li>
<li>Győződj meg róla, hogy minden adat megtalálható</li>
</ol>
</div>
<div style="margin-top:1.5rem;display:flex;gap:.75rem;flex-wrap:wrap">
<a href="/stacks/{{.Meta.Slug}}/deploy" class="btn btn-primary">Alkalmazások megtekintése</a>
<button id="migrate-delete-old-btn" class="btn btn-outline btn-danger" onclick="deleteOldMigrationData()" style="display:none">
🗑️ Korábbi adatok törlése
</button>
<a href="/settings" class="btn btn-outline">Beállítások</a>
</div>
</div>
<script>
var stackName = '{{.Stack.Name}}';
var migPollTimer = null;
function startMigrate() {
var targetPath = document.getElementById('target-path').value;
if (!targetPath) {
document.getElementById('migrate-error').textContent = 'Válasszon cél tárhelyet.';
document.getElementById('migrate-error').style.display = 'block';
return;
}
document.getElementById('migrate-form-card').style.display = 'none';
document.getElementById('migrate-progress-card').style.display = 'block';
var autoDelete = document.getElementById('auto-delete').checked;
fetch('/api/storage/migrate', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, target_path: targetPath, auto_delete_stale: autoDelete})
})
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
showMigError(data.error || 'Ismeretlen hiba');
return;
}
migPollTimer = setInterval(pollMigProgress, 2000);
})
.catch(function(e) {
showMigError('Hálózati hiba: ' + e.message);
});
}
var migStepOrder = ['stopping','copying','updating','starting','cleaning','backing_up','done'];
function pollMigProgress() {
fetch('/api/storage/migrate/status')
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) return;
updateMigUI(data);
if (data.done) {
clearInterval(migPollTimer);
if (data.step === 'done') {
showMigDone();
}
}
})
.catch(function(){});
}
function updateMigUI(data) {
var currentIdx = migStepOrder.indexOf(data.step);
if (currentIdx < 0 && data.step === 'rolling_back') {
currentIdx = 1; // show during copy step
}
migStepOrder.forEach(function(s, i) {
var el = document.getElementById('mstep-' + s);
if (!el) return;
var icon = el.querySelector('.disk-step-icon');
if (i < currentIdx) {
el.className = 'disk-step disk-step-done';
icon.textContent = '✅';
} else if (i === currentIdx) {
el.className = 'disk-step disk-step-active';
icon.textContent = (data.step === 'error' || data.step === 'rolling_back') ? '❌' : '⏳';
} else {
el.className = 'disk-step';
icon.textContent = '○';
}
});
var pct = data.pct || 0;
document.getElementById('mig-progress-bar').style.width = pct + '%';
document.getElementById('mig-progress-pct').textContent = pct + '%';
document.getElementById('mig-progress-msg').textContent = data.msg || '';
if (data.elapsed_sec) {
document.getElementById('mig-elapsed').textContent = data.elapsed_sec + ' másodperce fut';
}
if (data.step === 'error' || (data.error && data.error !== '')) {
showMigError(data.error || data.msg || 'Ismeretlen hiba');
}
}
function showMigError(msg) {
clearInterval(migPollTimer);
document.getElementById('mig-progress-error').textContent = 'Hiba: ' + msg;
document.getElementById('mig-progress-error').style.display = 'block';
document.getElementById('migrate-progress-card').querySelector('h3').textContent = 'Áthelyezés sikertelen';
}
function showMigDone() {
document.getElementById('migrate-progress-card').style.display = 'none';
document.getElementById('migrate-done-card').style.display = 'block';
document.getElementById('migrate-done-card').scrollIntoView({behavior:'smooth'});
var autoDeleteChecked = document.getElementById('auto-delete').checked;
if (autoDeleteChecked) {
document.getElementById('done-msg').textContent =
'Az alkalmazás az új tárolóról fut. A régi adatok automatikusan törölve lettek.';
} else {
document.getElementById('done-msg').innerHTML =
'Az alkalmazás az új tárolóról fut.<br>A régi adatok a korábbi helyen megmaradtak.';
document.getElementById('migrate-delete-old-btn').style.display = '';
}
}
function deleteOldMigrationData() {
var oldPath = '{{.CurrentHDDPath}}';
if (!confirm('Biztosan törölni szeretnéd a korábbi adatokat?\n\nTárhely: ' + oldPath + '\n\n⚠️ Ez a művelet visszavonhatatlan!\nElőtte győződj meg róla, hogy az alkalmazás az új tárolóról megfelelően működik.')) {
return;
}
if (!confirm('UTOLSÓ FIGYELMEZTETÉS!\n\nA törlés visszavonhatatlan. Biztosan folytatod?')) {
return;
}
var btn = document.getElementById('migrate-delete-old-btn');
btn.disabled = true;
btn.textContent = 'Törlés folyamatban...';
fetch('/api/storage/stale-cleanup', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({stack_name: stackName, stale_path: oldPath})
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.ok) {
alert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = '🗑️ Korábbi adatok törlése';
return;
}
btn.textContent = '✅ Korábbi adatok törölve (' + (data.freed_human || '') + ')';
btn.classList.remove('btn-danger');
btn.classList.add('btn-outline');
btn.onclick = null;
})
.catch(function(e) {
alert('Hálózati hiba: ' + e.message);
btn.disabled = false;
btn.textContent = '🗑️ Korábbi adatok törlése';
});
}
</script>
{{template "layout_end" .}}
{{end}}
@@ -1,218 +0,0 @@
{{define "migrate_drive"}}
{{template "layout_start" .}}
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/settings" class="btn btn-sm btn-outline">&larr; Vissza</a>
<h2>Meghajtó kiváltása</h2>
</div>
</div>
<div class="settings-card" id="drive-mig-form-card">
<h3>Összes adat átköltöztetése másik meghajtóra</h3>
<div class="settings-grid" style="margin-bottom:1.5rem">
<div class="settings-row">
<span class="settings-label">Forrás meghajtó</span>
<span class="settings-value mono">{{.SourceLabel}} ({{.SourcePath}})</span>
</div>
{{if .SourceDiskInfo}}
<div class="settings-row">
<span class="settings-label">Használat</span>
<span class="settings-value mono">{{.SourceDiskInfo.UsedHuman}} / {{.SourceDiskInfo.TotalHuman}}</span>
</div>
{{end}}
<div class="settings-row">
<span class="settings-label">Alkalmazások</span>
<span class="settings-value">{{range $i, $app := .AppsOnSource}}{{if $i}}, {{end}}{{$app.DisplayName}}{{end}}</span>
</div>
</div>
<div class="form-group">
<label for="dest-path">Cél meghajtó <span class="required">*</span></label>
<select id="dest-path" class="form-control">
{{range .DestPaths}}
<option value="{{.Path}}">{{.Label}} ({{.Path}}) &mdash; {{.FreeHuman}} szabad</option>
{{end}}
</select>
</div>
<div class="alert alert-warning" style="margin-bottom:1.5rem">
<strong>Figyelmeztetések:</strong>
<ul style="margin:.5rem 0 0 1rem;padding:0">
<li>Minden alkalmazás leáll a mozgatás idejére</li>
<li>Nagy adatmennyiségnél ez hosszabb ideig tarthat</li>
<li>A restic mentés repók NEM kerülnek átmásolásra (helyet spórolunk)</li>
<li>A forrás meghajtó "Kiváltva" állapotba kerül</li>
<li>A 2. szintű mentések automatikusan átirányításra kerülnek</li>
</ul>
</div>
{{if .Tier2Impact}}
<div class="alert alert-info" style="margin-bottom:1.5rem">
<strong>Mentési hatás:</strong>
<ul style="margin:.5rem 0 0 1rem;padding:0">
{{range .Tier2Impact}}
<li>{{.}}</li>
{{end}}
</ul>
</div>
{{end}}
<div id="drive-mig-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
<div class="form-actions" style="gap:.75rem">
<button class="btn btn-primary" onclick="startDriveMigrate()">📦 Meghajtó kiváltás indítása</button>
<a href="/settings" class="btn btn-outline">Mégsem</a>
</div>
</div>
<div class="settings-card" id="drive-mig-progress-card" style="display:none">
<h3>Meghajtó kiváltás folyamatban...</h3>
<div class="disk-progress-steps" id="dm-steps">
<div class="disk-step" id="dmstep-validating"><span class="disk-step-icon"></span> Ellenőrzés</div>
<div class="disk-step" id="dmstep-stopping"><span class="disk-step-icon"></span> Alkalmazások leállítása</div>
<div class="disk-step" id="dmstep-copying"><span class="disk-step-icon"></span> Adatok másolása</div>
<div class="disk-step" id="dmstep-verifying"><span class="disk-step-icon"></span> Ellenőrzés</div>
<div class="disk-step" id="dmstep-configuring"><span class="disk-step-icon"></span> Konfiguráció</div>
<div class="disk-step" id="dmstep-starting"><span class="disk-step-icon"></span> Alkalmazások indítása</div>
<div class="disk-step" id="dmstep-backup"><span class="disk-step-icon"></span> Biztonsági mentés</div>
<div class="disk-step" id="dmstep-done"><span class="disk-step-icon"></span> Kész</div>
</div>
<div class="disk-progress-bar-wrap" style="margin-top:1.5rem">
<div class="system-bar" style="height:12px;border-radius:6px">
<div class="system-bar-fill system-bar-green" id="dm-progress-bar" style="width:0%;transition:width .4s ease;height:12px;border-radius:6px"></div>
</div>
<span class="mono form-hint" id="dm-progress-pct">0%</span>
</div>
<div id="dm-progress-msg" class="form-hint" style="margin-top:.75rem"></div>
<div id="dm-progress-detail" class="form-hint mono" style="margin-top:.25rem;font-size:.85rem"></div>
<div id="dm-elapsed" class="form-hint mono" style="margin-top:.25rem"></div>
<div id="dm-progress-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
</div>
<div class="settings-card" id="drive-mig-done-card" style="display:none">
<h3>Meghajtó kiváltás kész!</h3>
<p id="dm-done-msg" style="margin-top:.75rem;color:var(--text-secondary)"></p>
<div class="alert alert-info" style="margin-top:1rem">
<strong>A forrás meghajtó biztonságosan eltávolítható.</strong>
Ha nem szándékozod újrafelhasználni, a Beállítások oldalon eltávolíthatod a rendszerből.
</div>
<div style="margin-top:1.5rem;display:flex;gap:.75rem;flex-wrap:wrap">
<a href="/settings" class="btn btn-primary">Beállítások</a>
<a href="/backups" class="btn btn-outline">Mentések</a>
</div>
</div>
<script>
var sourcePath = '{{.SourcePath}}';
var dmPollTimer = null;
function startDriveMigrate() {
var destPath = document.getElementById('dest-path').value;
if (!destPath) {
document.getElementById('drive-mig-error').textContent = 'Válasszon cél meghajtót.';
document.getElementById('drive-mig-error').style.display = 'block';
return;
}
if (!confirm('Biztosan ki szeretné váltani a forrás meghajtót?\n\nMinden alkalmazás leáll a migráció idejére.\nEz a művelet nem vonható vissza egyszerűen.')) {
return;
}
document.getElementById('drive-mig-form-card').style.display = 'none';
document.getElementById('drive-mig-progress-card').style.display = 'block';
fetch('/api/storage/migrate-drive', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({source_path: sourcePath, dest_path: destPath})
})
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
showDMError(data.error || 'Ismeretlen hiba');
return;
}
dmPollTimer = setInterval(pollDMProgress, 2000);
})
.catch(function(e) {
showDMError('Hálózati hiba: ' + e.message);
});
}
var dmStepOrder = ['validating','stopping','copying','verifying','configuring','starting','backup','done'];
function pollDMProgress() {
fetch('/api/storage/migrate-drive/status')
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) return;
updateDMUI(data);
if (data.done) {
clearInterval(dmPollTimer);
if (data.step === 'done') {
showDMDone(data.msg);
}
}
})
.catch(function(){});
}
function updateDMUI(data) {
var currentIdx = dmStepOrder.indexOf(data.step);
if (currentIdx < 0 && data.step === 'rolling_back') {
currentIdx = dmStepOrder.indexOf('copying');
}
dmStepOrder.forEach(function(s, i) {
var el = document.getElementById('dmstep-' + s);
if (!el) return;
var icon = el.querySelector('.disk-step-icon');
if (i < currentIdx) {
el.className = 'disk-step disk-step-done';
icon.textContent = '\u2705';
} else if (i === currentIdx) {
el.className = 'disk-step disk-step-active';
icon.textContent = (data.step === 'error' || data.step === 'rolling_back') ? '\u274C' : '\u23F3';
} else {
el.className = 'disk-step';
icon.textContent = '\u25CB';
}
});
var pct = data.pct || 0;
document.getElementById('dm-progress-bar').style.width = pct + '%';
document.getElementById('dm-progress-pct').textContent = pct + '%';
document.getElementById('dm-progress-msg').textContent = data.msg || '';
document.getElementById('dm-progress-detail').textContent = data.detail || '';
if (data.elapsed_sec) {
document.getElementById('dm-elapsed').textContent = data.elapsed_sec + ' másodperce fut';
}
if (data.step === 'error' || (data.error && data.error !== '')) {
showDMError(data.error || data.msg || 'Ismeretlen hiba');
}
}
function showDMError(msg) {
clearInterval(dmPollTimer);
document.getElementById('dm-progress-error').textContent = 'Hiba: ' + msg;
document.getElementById('dm-progress-error').style.display = 'block';
document.getElementById('drive-mig-progress-card').querySelector('h3').textContent = 'Meghajtó kiváltás sikertelen';
}
function showDMDone(msg) {
document.getElementById('drive-mig-progress-card').style.display = 'none';
document.getElementById('drive-mig-done-card').style.display = 'block';
document.getElementById('dm-done-msg').textContent = msg || 'A meghajtó sikeresen kiváltva.';
document.getElementById('drive-mig-done-card').scrollIntoView({behavior:'smooth'});
}
</script>
{{template "layout_end" .}}
{{end}}
@@ -1,348 +0,0 @@
{{define "restore"}}
<!DOCTYPE html>
<html lang="hu">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Katasztrófa utáni visszaállítás — Felhom</title>
<link rel="stylesheet" href="/static/style.css">
<meta name="csrf-token" content="{{.CSRFToken}}">
<script>function csrfHeaders(){var el=document.querySelector('meta[name="csrf-token"]');return el?{'X-CSRF-Token':el.content}:{}}</script>
<style>
body { background: var(--bg-darker, #0d1117); margin: 0; padding: 0; }
.dr-container { max-width: 900px; margin: 0 auto; padding: 2rem 1.5rem; }
.dr-header { text-align: center; margin-bottom: 2rem; }
.dr-header img { width: 48px; height: 48px; margin-bottom: 0.5rem; }
.dr-header h1 { color: var(--warning, #f0ad4e); font-size: 1.5rem; margin: 0.5rem 0; }
.dr-header p { color: var(--text-secondary, #8b949e); margin: 0.25rem 0; }
.dr-card { background: var(--card-bg, #161b22); border: 1px solid var(--border, #30363d); border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; }
.dr-card h3 { margin: 0 0 0.75rem 0; color: var(--text-primary, #e6edf3); font-size: 1rem; }
.dr-drives { display: flex; gap: 0.75rem; flex-wrap: wrap; }
.dr-drive { background: var(--bg-darker, #0d1117); border: 1px solid var(--border, #30363d); border-radius: 6px; padding: 0.75rem 1rem; flex: 1; min-width: 200px; }
.dr-drive-label { font-weight: 600; color: var(--text-primary, #e6edf3); }
.dr-drive-path { font-size: 0.85rem; color: var(--text-secondary, #8b949e); font-family: monospace; }
.dr-drive-status { font-size: 0.85rem; margin-top: 0.25rem; }
.dr-drive-ok { color: var(--success, #3fb950); }
.dr-drive-warn { color: var(--warning, #f0ad4e); }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; padding: 0.5rem 0.75rem; color: var(--text-secondary, #8b949e); font-size: 0.85rem; font-weight: 500; border-bottom: 1px solid var(--border, #30363d); }
td { padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--border, #30363d); color: var(--text-primary, #e6edf3); font-size: 0.9rem; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: 500; }
.badge-ok { background: rgba(63,185,80,0.15); color: var(--success, #3fb950); }
.badge-warn { background: rgba(240,173,78,0.15); color: var(--warning, #f0ad4e); }
.badge-none { background: rgba(139,148,158,0.15); color: var(--text-secondary, #8b949e); }
.status-pending { color: var(--text-secondary, #8b949e); }
.status-restoring { color: var(--info, #58a6ff); }
.status-done { color: var(--success, #3fb950); }
.status-failed { color: var(--danger, #f85149); }
.status-skipped { color: var(--text-secondary, #8b949e); }
.dr-actions { display: flex; gap: 0.75rem; justify-content: center; margin-top: 1.5rem; }
.btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.6rem 1.5rem; border-radius: 6px; border: 1px solid transparent; font-size: 0.9rem; font-weight: 500; cursor: pointer; text-decoration: none; transition: background 0.2s; }
.btn-primary { background: var(--accent, #238636); color: #fff; border-color: var(--accent, #238636); }
.btn-primary:hover { background: #2ea043; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-outline { background: transparent; color: var(--text-secondary, #8b949e); border-color: var(--border, #30363d); }
.btn-outline:hover { color: var(--text-primary, #e6edf3); border-color: var(--text-secondary, #8b949e); }
.btn-success { background: var(--accent, #238636); color: #fff; }
.progress-bar { height: 4px; background: var(--border, #30363d); border-radius: 2px; margin-top: 1rem; overflow: hidden; display: none; }
.progress-bar-inner { height: 100%; background: var(--accent, #238636); transition: width 0.5s; width: 0%; }
.dr-info { display: flex; gap: 2rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
.dr-info-item { font-size: 0.9rem; }
.dr-info-label { color: var(--text-secondary, #8b949e); }
.dr-info-value { color: var(--text-primary, #e6edf3); font-weight: 500; }
.spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid var(--border, #30363d); border-top-color: var(--info, #58a6ff); border-radius: 50%; animation: spin 0.8s linear infinite; vertical-align: middle; margin-right: 4px; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="dr-container">
<div class="dr-header">
<img src="/static/felhom-logo.svg" alt="Felhom">
<h1>Korábbi telepítés észlelve</h1>
<p>A rendszer biztonsági mentést talált a központi szerveren</p>
</div>
<!-- Info card -->
<div class="dr-card">
<h3>Rendszer információ</h3>
<div class="dr-info">
<div class="dr-info-item">
<span class="dr-info-label">Ügyfél: </span>
<span class="dr-info-value">{{.CustomerName}}</span>
</div>
<div class="dr-info-item">
<span class="dr-info-label">Domain: </span>
<span class="dr-info-value">{{.Domain}}</span>
</div>
<div class="dr-info-item">
<span class="dr-info-label">Mentés időpontja: </span>
<span class="dr-info-value">{{.Timestamp}}</span>
</div>
</div>
</div>
<!-- Drives card -->
<div class="dr-card">
<h3>Meghajtók</h3>
<div class="dr-drives">
{{range .Drives}}
<div class="dr-drive">
<div class="dr-drive-label">{{.Label}}</div>
<div class="dr-drive-path">{{.Path}}</div>
<div class="dr-drive-status">
{{if .Available}}
{{if .HasBackup}}
<span class="dr-drive-ok">Elérhető, mentés megtalálva</span>
{{else}}
<span class="dr-drive-ok">Elérhető</span>
{{end}}
{{else}}
<span class="dr-drive-warn">Nem elérhető</span>
{{end}}
</div>
</div>
{{end}}
{{if not .Drives}}
<p style="color:var(--text-secondary)">Nem találhatók csatlakoztatott meghajtók.</p>
{{end}}
</div>
</div>
<!-- Apps table card -->
<div class="dr-card">
<h3>Visszaállítható alkalmazások</h3>
{{if .Apps}}
<table>
<thead>
<tr>
<th>Alkalmazás</th>
<th>Konfiguráció</th>
<th>Adatok</th>
<th>DB mentés</th>
<th>Állapot</th>
</tr>
</thead>
<tbody id="app-table-body">
{{range .Apps}}
<tr data-app="{{.Name}}">
<td>
<strong>{{.DisplayName}}</strong>
<div style="font-size:.8rem;color:var(--text-secondary)">{{.Name}}</div>
</td>
<td>
{{if .HasConfig}}
<span class="badge badge-ok">Megtalálva</span>
{{else}}
<span class="badge badge-none">Hiányzik</span>
{{end}}
</td>
<td>
{{if .HasData}}
<span class="badge badge-ok">Elérhető</span>
{{else if .HasRsyncData}}
<span class="badge badge-warn">Mentésből</span>
{{else if not .NeedsHDD}}
<span class="badge badge-none">Nem szükséges</span>
{{else}}
<span class="badge badge-warn">Hiányzik</span>
{{end}}
</td>
<td>
{{if .HasDBDump}}
<span class="badge badge-ok">Van</span>
{{else}}
<span class="badge badge-none">Nincs</span>
{{end}}
</td>
<td class="app-status" data-app="{{.Name}}">
<span class="status-{{.Status}}">{{statusText .Status}}</span>
</td>
</tr>
{{end}}
</tbody>
</table>
<div class="progress-bar" id="progress-bar">
<div class="progress-bar-inner" id="progress-inner"></div>
</div>
{{else}}
<p style="color:var(--text-secondary)">Nem találhatók visszaállítható alkalmazások.</p>
{{end}}
</div>
<!-- Action buttons -->
<div class="dr-actions" id="dr-actions">
{{if eq .PlanStatus "pending"}}
{{if .Apps}}
<button class="btn btn-primary" id="btn-restore-all" onclick="startRestoreAll()">
Összes visszaállítása ({{len .Apps}} alkalmazás)
</button>
{{end}}
<button class="btn btn-outline" id="btn-skip" onclick="skipRestore()">
Kihagyás — tovább a vezérlőpulthoz
</button>
{{else if eq .PlanStatus "restoring"}}
<button class="btn btn-primary" disabled>
<span class="spinner"></span> Visszaállítás folyamatban...
</button>
{{else if eq .PlanStatus "done"}}
<a href="/" class="btn btn-success" id="btn-continue" onclick="finishRestore(event)">
Tovább a vezérlőpulthoz
</a>
{{end}}
</div>
</div>
<script>
var polling = null;
var planStatus = "{{.PlanStatus}}";
if (planStatus === "restoring") {
startPolling();
}
function startRestoreAll() {
var btn = document.getElementById('btn-restore-all');
var skipBtn = document.getElementById('btn-skip');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Visszaállítás indítása...';
if (skipBtn) skipBtn.style.display = 'none';
fetch('/api/restore/all', { method: 'POST', headers: csrfHeaders() })
.then(function(resp) { return resp.json(); })
.then(function(data) {
if (data.ok) {
planStatus = 'restoring';
document.getElementById('progress-bar').style.display = 'block';
startPolling();
} else {
alert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
btn.disabled = false;
btn.textContent = 'Összes visszaállítása';
if (skipBtn) skipBtn.style.display = '';
}
})
.catch(function(err) {
alert('Hálózati hiba: ' + err.message);
btn.disabled = false;
btn.textContent = 'Összes visszaállítása';
if (skipBtn) skipBtn.style.display = '';
});
}
function skipRestore() {
if (!confirm('Biztosan ki szeretné hagyni a visszaállítást? A vezérlőpult üres alkalmazáslistával fog elindulni.')) return;
fetch('/api/restore/skip', { method: 'POST', headers: csrfHeaders() })
.then(function(resp) { return resp.json(); })
.then(function(data) {
if (data.ok) {
window.location.href = '/';
} else {
alert('Hiba: ' + (data.error || 'Ismeretlen hiba'));
}
})
.catch(function(err) { alert('Hálózati hiba: ' + err.message); });
}
function finishRestore(e) {
e.preventDefault();
fetch('/api/restore/skip', { method: 'POST', headers: csrfHeaders() })
.then(function() { window.location.href = '/'; })
.catch(function() { window.location.href = '/'; });
}
function startPolling() {
if (polling) return;
document.getElementById('progress-bar').style.display = 'block';
polling = setInterval(pollStatus, 2000);
pollStatus();
}
var pollErrors = 0;
function pollStatus() {
fetch('/api/restore/status')
.then(function(resp) {
if (!resp.ok) throw new Error('HTTP ' + resp.status);
return resp.json();
})
.then(function(data) {
pollErrors = 0;
if (!data.ok) return;
updateTable(data.apps || []);
updateProgress(data.apps || []);
if (data.status === 'done') {
clearInterval(polling);
polling = null;
planStatus = 'done';
updateActions();
}
})
.catch(function(err) {
pollErrors++;
console.error('Poll error:', err);
if (pollErrors >= 10) {
clearInterval(polling);
polling = null;
var actions = document.getElementById('dr-actions');
if (actions) {
actions.innerHTML = '<p style="color:var(--danger)">Kapcsolat megszakadt. <a href="/restore">Oldal frissítése</a></p>';
}
}
});
}
function updateTable(apps) {
apps.forEach(function(app) {
var cells = document.querySelectorAll('.app-status[data-app="' + app.name + '"]');
cells.forEach(function(cell) {
var span = document.createElement('span');
span.className = 'status-' + app.status;
if (app.status === 'restoring') {
var spinner = document.createElement('span');
spinner.className = 'spinner';
span.appendChild(spinner);
span.appendChild(document.createTextNode(' '));
}
span.appendChild(document.createTextNode(statusText(app.status)));
if (app.error) {
var errSpan = document.createElement('span');
errSpan.style.cssText = 'font-size:.8rem;color:var(--danger)';
errSpan.textContent = ' (' + app.error.substring(0, 60) + ')';
span.appendChild(errSpan);
}
cell.innerHTML = '';
cell.appendChild(span);
});
});
}
function updateProgress(apps) {
var total = apps.length;
if (total === 0) return;
var done = 0;
apps.forEach(function(a) {
if (a.status === 'done' || a.status === 'failed' || a.status === 'skipped') done++;
});
var pct = Math.round((done / total) * 100);
document.getElementById('progress-inner').style.width = pct + '%';
}
function updateActions() {
var actions = document.getElementById('dr-actions');
actions.innerHTML = '<a href="/" class="btn btn-success" id="btn-continue" onclick="finishRestore(event)">Tovább a vezérlőpulthoz</a>';
}
function statusText(s) {
switch (s) {
case 'pending': return 'Várakozik';
case 'restoring': return 'Visszaállítás...';
case 'done': return 'Kész';
case 'failed': return 'Sikertelen';
case 'skipped': return 'Kihagyva';
default: return s;
}
}
</script>
</body>
</html>
{{end}}
@@ -1,582 +0,0 @@
{{define "storage_attach"}}
{{template "layout_start" .}}
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/settings" class="btn btn-sm btn-outline">← Vissza</a>
<h2>Meglévő meghajtó csatolása</h2>
</div>
</div>
<!-- Step 1: Scan -->
<div class="settings-card" id="wizard-scan">
<h3>1. Meghajtók keresése</h3>
<p class="settings-card-desc">Keresse meg a rendszerhez csatlakoztatott, meglévő fájlrendszerrel rendelkező meghajtókat.</p>
<button class="btn btn-primary" onclick="scanDisks()" id="scan-btn">🔍 Meghajtók keresése</button>
<div id="scan-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
<div id="scan-result" style="display:none;margin-top:1.5rem">
<div id="available-disks"></div>
<div id="system-disks-note" style="display:none;margin-top:1rem"></div>
</div>
</div>
<!-- Step 2: Browse -->
<div class="settings-card" id="wizard-browse" style="display:none">
<h3>2. Mappa kiválasztása</h3>
<p class="settings-card-desc">Válasszon ki egy mappát a meghajtón, amelyet a controller használni fog. Új mappát is létrehozhat.</p>
<div id="browse-info" class="settings-grid" style="margin-bottom:1rem">
<div class="settings-row">
<span class="settings-label">Partíció</span>
<span class="settings-value mono" id="browse-device"></span>
</div>
<div class="settings-row">
<span class="settings-label">Fájlrendszer</span>
<span class="settings-value mono" id="browse-fstype"></span>
</div>
</div>
<div id="browse-error" class="alert alert-error" style="display:none;margin-bottom:1rem"></div>
<div id="dir-browser" style="border:1px solid var(--border);border-radius:6px;padding:1rem;background:var(--card-bg);margin-bottom:1rem">
<div id="dir-breadcrumb" class="form-hint mono" style="margin-bottom:.75rem"></div>
<div id="dir-list" style="min-height:100px"></div>
</div>
<div style="display:flex;gap:.75rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:1rem">
<div class="form-group" style="margin-bottom:0">
<label for="new-dir-name">Új mappa neve</label>
<input type="text" id="new-dir-name" class="form-control" placeholder="felhom_data"
pattern="[a-zA-Z0-9_]+" style="max-width:200px">
</div>
<button class="btn btn-outline" onclick="createDir()" id="mkdir-btn">📁 Mappa létrehozása</button>
</div>
<div id="selected-dir-info" class="alert alert-info" style="display:none;margin-bottom:1rem">
Kiválasztott mappa: <strong id="selected-dir-display" class="mono"></strong>
</div>
<div class="form-actions" style="gap:.75rem">
<button class="btn btn-primary" onclick="goToConfigure()" id="browse-next-btn" disabled>Tovább →</button>
<button class="btn btn-outline" onclick="cancelAttach()">Mégsem</button>
</div>
</div>
<!-- Step 3: Configure -->
<div class="settings-card" id="wizard-configure" style="display:none">
<h3>3. Konfiguráció</h3>
<p class="settings-card-desc">Adja meg a csatolás paramétereit.</p>
<form id="attach-form">
<div class="form-group">
<label>Kiválasztott partíció</label>
<span class="settings-value mono" id="config-device-display"></span>
</div>
<div class="form-group">
<label>Kiválasztott mappa</label>
<span class="settings-value mono" id="config-subpath-display"></span>
</div>
<div class="form-group">
<label for="mount-name">Csatlakoztatási név <span class="required">*</span></label>
<div class="form-inline">
<span class="mono" style="opacity:.6">/mnt/</span>
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1"
pattern="[a-zA-Z0-9_]+" required style="max-width:160px">
</div>
<span class="form-hint">Pl. hdd_1 → a mappa a /mnt/hdd_1 útvonalra kerül</span>
</div>
<div class="form-group">
<label for="storage-label">Megnevezés</label>
<input type="text" id="storage-label" class="form-control" placeholder="Külső HDD 1TB" maxlength="50">
</div>
<label class="toggle" style="margin-bottom:1.5rem">
<input type="checkbox" id="set-default" checked>
<span class="toggle-label">Beállítás alapértelmezett adattárolóként új telepítéseknél</span>
</label>
<div class="alert alert-info" style="margin-bottom:1.5rem">
<strong>️ Megjegyzés:</strong> A meghajtón lévő adatok <strong>NEM</strong> törlődnek.
A controller csak a kiválasztott mappában dolgozik.<br>
<strong>⚠️ A csatlakozási pont (/mnt/&lt;név&gt;) a meghajtó lecsatolásáig nem módosítható.</strong>
</div>
<div class="form-actions" style="gap:.75rem">
<button type="submit" class="btn btn-primary" id="attach-btn">Csatolás</button>
<button type="button" class="btn btn-outline" onclick="backToBrowse()">← Vissza</button>
</div>
<div id="attach-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
</form>
</div>
<!-- Step 4: Progress -->
<div class="settings-card" id="wizard-progress" style="display:none">
<h3>4. Csatolás folyamatban...</h3>
<div class="disk-progress-steps" id="progress-steps">
<div class="disk-step" id="pstep-validating"><span class="disk-step-icon"></span> Ellenőrzés</div>
<div class="disk-step" id="pstep-mounting"><span class="disk-step-icon"></span> Csatlakoztatás</div>
<div class="disk-step" id="pstep-permissions"><span class="disk-step-icon"></span> Mappák és jogosultságok</div>
<div class="disk-step" id="pstep-done"><span class="disk-step-icon"></span> Regisztráció</div>
</div>
<div style="margin-top:1.5rem;display:flex;align-items:center;gap:1rem">
<div class="progress-bar-task" style="flex:1">
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
</div>
<span id="progress-percent" style="font-size:0.9rem;color:var(--text-muted);font-family:'JetBrains Mono',monospace;white-space:nowrap">0%</span>
</div>
<div id="progress-msg" class="form-hint" style="margin-top:.75rem"></div>
<div id="progress-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
</div>
<!-- Step 5: Done -->
<div class="settings-card" id="wizard-done" style="display:none">
<h3>✅ Meghajtó sikeresen csatolva!</h3>
<div id="done-info" class="settings-grid" style="margin-top:1rem">
<div class="settings-row">
<span class="settings-label">Útvonal</span>
<span class="settings-value mono" id="done-path"></span>
</div>
</div>
<a href="/settings" class="btn btn-primary" style="margin-top:1.5rem">← Vissza a Beállításokhoz</a>
</div>
<script>
var selectedDevice = null;
var selectedPartition = null;
var currentBrowsePath = '';
var rawMountPath = '';
var selectedSubPath = '';
var pollTimer = null;
// --- Step 1: Scan ---
function scanDisks() {
var btn = document.getElementById('scan-btn');
var errEl = document.getElementById('scan-error');
var resultEl = document.getElementById('scan-result');
btn.textContent = 'Keresés...';
btn.disabled = true;
errEl.style.display = 'none';
resultEl.style.display = 'none';
// Clean up any stale raw mounts from interrupted previous sessions first,
// so the device appears as available in the scan results.
fetch('/api/storage/attach/cancel', {method:'POST', headers: csrfHeaders()})
.catch(function(){}) // ignore cancel errors
.then(function() { return fetch('/api/storage/scan', {method:'POST', headers: csrfHeaders()}); })
.then(function(r){ return r.json(); })
.then(function(data) {
btn.textContent = '🔍 Meghajtók keresése';
btn.disabled = false;
if (!data.ok) {
errEl.textContent = data.error || 'Ismeretlen hiba';
errEl.style.display = 'block';
return;
}
renderScanResult(data);
resultEl.style.display = 'block';
})
.catch(function(e) {
btn.textContent = '🔍 Meghajtók keresése';
btn.disabled = false;
errEl.textContent = 'Hálózati hiba: ' + e.message;
errEl.style.display = 'block';
});
}
function renderScanResult(data) {
var availEl = document.getElementById('available-disks');
var sysEl = document.getElementById('system-disks-note');
// Filter: only show disks that have at least one partition with a filesystem
var disksWithFS = [];
if (data.available) {
data.available.forEach(function(disk) {
if (disk.Partitions) {
var fsPartitions = disk.Partitions.filter(function(p) { return p.FSType && p.FSType !== ''; });
if (fsPartitions.length > 0) {
disksWithFS.push({disk: disk, partitions: fsPartitions});
}
}
});
}
if (disksWithFS.length === 0) {
availEl.innerHTML = '<div class="empty-state" style="padding:1rem">Nem található meglévő fájlrendszerrel rendelkező meghajtó.<br>' +
'<span class="form-hint">Ha üres meghajtót szeretne inicializálni, használja az <a href="/settings/storage/init">inicializálás varázslót</a>.</span></div>';
return;
}
var html = '<h4 style="margin-bottom:.75rem">Talált meghajtók csatolható partíciókkal:</h4>';
disksWithFS.forEach(function(item) {
var disk = item.disk;
html += '<div style="margin-bottom:1rem">';
html += '<div class="form-hint" style="margin-bottom:.5rem">' + disk.Path + ' — ' + (disk.Size || '?') +
(disk.Model ? ' — ' + disk.Model : '') + '</div>';
item.partitions.forEach(function(part) {
var info = part.FSType;
if (part.Label) info += ', címke: ' + part.Label;
if (part.UUID) info += ', UUID: ' + part.UUID.substring(0, 8) + '...';
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent;margin-bottom:.5rem" ' +
'onclick="selectPartition(this, \'' + part.Path + '\', \'' + (part.FSType || '') + '\', \'' + (part.Label || '') + '\')" ' +
'data-path="' + part.Path + '">' +
'<div class="storage-path-header"><div class="storage-path-info">' +
'<span class="storage-path-label">○ ' + part.Path + ' — ' + (part.Size || '?') + '</span>' +
'<span class="form-hint">' + info + '</span>' +
'</div></div></div>';
});
html += '</div>';
});
availEl.innerHTML = html;
if (data.system && data.system.length > 0) {
var sysNames = data.system.map(function(d){ return d.Path + ' (' + (d.Size||'?') + ')'; }).join(', ');
sysEl.innerHTML = '<span class="form-hint">A rendszermeghajtó(k) nem választhatók: ' + sysNames + '</span>';
sysEl.style.display = 'block';
}
}
function selectPartition(el, path, fsType, label) {
// Deselect all
document.querySelectorAll('[data-path]').forEach(function(d) {
d.style.border = '2px solid transparent';
var lbl = d.querySelector('.storage-path-label');
if (lbl) lbl.textContent = lbl.textContent.replace('● ', '○ ');
});
// Select this
el.style.border = '2px solid var(--accent-blue)';
var lbl = el.querySelector('.storage-path-label');
if (lbl) lbl.textContent = lbl.textContent.replace('○ ', '● ');
selectedDevice = path;
selectedPartition = {path: path, fsType: fsType, label: label};
// Mount raw and go to browse
mountRawAndBrowse(path, fsType);
}
// --- Step 2: Browse ---
function mountRawAndBrowse(devicePath, fsType) {
var errEl = document.getElementById('scan-error');
errEl.style.display = 'none';
fetch('/api/storage/attach/mount-raw', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({device_path: devicePath})
}).then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
errEl.textContent = data.error || 'Raw mount sikertelen';
errEl.style.display = 'block';
return;
}
rawMountPath = data.raw_path;
// Show browse step
document.getElementById('browse-device').textContent = devicePath;
document.getElementById('browse-fstype').textContent = fsType;
document.getElementById('wizard-scan').style.display = 'none';
document.getElementById('wizard-browse').style.display = 'block';
document.getElementById('wizard-browse').scrollIntoView({behavior:'smooth'});
// Browse root
browseDirectory(rawMountPath);
})
.catch(function(e) {
errEl.textContent = 'Hálózati hiba: ' + e.message;
errEl.style.display = 'block';
});
}
function browseDirectory(path) {
currentBrowsePath = path;
var errEl = document.getElementById('browse-error');
errEl.style.display = 'none';
// Update breadcrumb
var rel = path.replace(rawMountPath, '') || '/';
document.getElementById('dir-breadcrumb').textContent = 'Aktuális mappa: ' + rel;
fetch('/api/storage/attach/browse?path=' + encodeURIComponent(path))
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
errEl.textContent = data.error || 'Hiba a mappák listázásakor';
errEl.style.display = 'block';
return;
}
renderDirList(data.dirs || [], path);
})
.catch(function(e) {
errEl.textContent = 'Hálózati hiba: ' + e.message;
errEl.style.display = 'block';
});
}
function renderDirList(dirs, basePath) {
var listEl = document.getElementById('dir-list');
var html = '';
// "Use this directory" option (select the current directory itself)
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent;margin-bottom:.5rem;padding:.5rem .75rem" ' +
'onclick="selectDir(this, \'' + escapeJS(basePath) + '\')" data-dirpath="' + escapeAttr(basePath) + '">' +
'<span class="storage-path-label">📂 . (ez a mappa)</span></div>';
// Parent directory (if not at root)
if (basePath !== rawMountPath) {
var parentPath = basePath.substring(0, basePath.lastIndexOf('/'));
if (parentPath.length < rawMountPath.length) parentPath = rawMountPath;
html += '<div style="padding:.3rem .75rem;cursor:pointer;opacity:.7" onclick="browseDirectory(\'' + escapeJS(parentPath) + '\')">' +
'📁 .. (szülő mappa)</div>';
}
if (dirs.length === 0) {
html += '<div class="form-hint" style="padding:.5rem .75rem">Üres mappa</div>';
} else {
dirs.forEach(function(dir) {
html += '<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.25rem">';
// Clickable to navigate into
if (dir.has_children) {
html += '<div style="padding:.3rem .75rem;cursor:pointer;flex:1" onclick="browseDirectory(\'' + escapeJS(dir.path) + '\')">' +
'📁 ' + dir.name + ' →</div>';
} else {
html += '<div style="padding:.3rem .75rem;flex:1">📁 ' + dir.name + '</div>';
}
// Select button
html += '<button class="btn btn-xs btn-outline" onclick="selectDir(null, \'' + escapeJS(dir.path) + '\')">Kiválasztás</button>';
html += '</div>';
});
}
listEl.innerHTML = html;
}
function selectDir(el, path) {
selectedSubPath = path;
document.getElementById('selected-dir-display').textContent = path.replace(rawMountPath, '') || '/';
document.getElementById('selected-dir-info').style.display = 'block';
document.getElementById('browse-next-btn').disabled = false;
// Highlight selected
document.querySelectorAll('[data-dirpath]').forEach(function(d) {
d.style.border = '2px solid transparent';
});
if (el) {
el.style.border = '2px solid var(--accent-blue)';
}
// Pre-fill mount name from partition label if available
var mountInput = document.getElementById('mount-name');
if (!mountInput.value && selectedPartition && selectedPartition.label) {
mountInput.value = selectedPartition.label.replace(/[^a-zA-Z0-9_]/g, '_');
}
}
function createDir() {
var nameInput = document.getElementById('new-dir-name');
var name = nameInput.value.trim();
if (!name) return;
var errEl = document.getElementById('browse-error');
errEl.style.display = 'none';
// Client-side validation
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
errEl.textContent = 'A mappanéven csak betűk, számok és alávonás megengedett.';
errEl.style.display = 'block';
return;
}
if (name.length > 32) {
errEl.textContent = 'A mappanév legfeljebb 32 karakter lehet.';
errEl.style.display = 'block';
return;
}
fetch('/api/storage/attach/mkdir', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify({path: currentBrowsePath, name: name})
}).then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
errEl.textContent = data.error || 'Mappa létrehozása sikertelen';
errEl.style.display = 'block';
return;
}
nameInput.value = '';
// Auto-select the created directory
selectedSubPath = data.created_path;
document.getElementById('selected-dir-display').textContent = data.created_path.replace(rawMountPath, '');
document.getElementById('selected-dir-info').style.display = 'block';
document.getElementById('browse-next-btn').disabled = false;
// Refresh directory listing
browseDirectory(currentBrowsePath);
})
.catch(function(e) {
errEl.textContent = 'Hálózati hiba: ' + e.message;
errEl.style.display = 'block';
});
}
function goToConfigure() {
if (!selectedSubPath) return;
document.getElementById('config-device-display').textContent = selectedDevice;
document.getElementById('config-subpath-display').textContent = selectedSubPath.replace(rawMountPath, '') || '/ (gyökérmappa)';
document.getElementById('wizard-browse').style.display = 'none';
document.getElementById('wizard-configure').style.display = 'block';
document.getElementById('wizard-configure').scrollIntoView({behavior:'smooth'});
}
function backToBrowse() {
document.getElementById('wizard-configure').style.display = 'none';
document.getElementById('wizard-browse').style.display = 'block';
}
function cancelAttach() {
// Cleanup raw mount
fetch('/api/storage/attach/cancel', {method:'POST', headers: csrfHeaders()}).catch(function(){});
window.location.href = '/settings';
}
// --- Step 3: Submit ---
document.getElementById('attach-form').addEventListener('submit', function(e) {
e.preventDefault();
var mountName = document.getElementById('mount-name').value.trim();
var label = document.getElementById('storage-label').value.trim();
var setDefault = document.getElementById('set-default').checked;
var errEl = document.getElementById('attach-error');
if (!mountName) {
errEl.textContent = 'A csatlakoztatási nevet meg kell adni.';
errEl.style.display = 'block';
return;
}
errEl.style.display = 'none';
document.getElementById('wizard-configure').style.display = 'none';
document.getElementById('wizard-progress').style.display = 'block';
document.getElementById('wizard-progress').scrollIntoView({behavior:'smooth'});
var body = {
device_path: selectedDevice,
mount_name: mountName,
sub_path: selectedSubPath,
label: label,
set_default: setDefault
};
fetch('/api/storage/attach', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify(body)
}).then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
showProgressError(data.error || 'Ismeretlen hiba');
return;
}
pollTimer = setInterval(pollProgress, 1500);
})
.catch(function(e) {
showProgressError('Hálózati hiba: ' + e.message);
});
});
// --- Step 4: Progress ---
var stepOrder = ['validating','mounting','permissions','done'];
function pollProgress() {
fetch('/api/storage/attach/status')
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) return;
updateProgressUI(data);
if (data.done) {
clearInterval(pollTimer);
if (data.step === 'done') {
showDone('/mnt/' + document.getElementById('mount-name').value.trim());
}
}
})
.catch(function(){});
}
function updateProgressUI(data) {
var currentIdx = stepOrder.indexOf(data.step);
stepOrder.forEach(function(s, i) {
var el = document.getElementById('pstep-' + s);
if (!el) return;
var icon = el.querySelector('.disk-step-icon');
if (i < currentIdx) {
el.className = 'disk-step disk-step-done';
icon.textContent = '✅';
} else if (i === currentIdx) {
el.className = 'disk-step disk-step-active';
icon.textContent = data.step === 'error' ? '❌' : '⏳';
} else {
el.className = 'disk-step';
icon.textContent = '○';
}
});
var pct = data.pct || 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-percent').textContent = pct + '%';
document.getElementById('progress-msg').textContent = data.msg || '';
if (data.step === 'error' || data.error) {
showProgressError(data.error || data.msg || 'Ismeretlen hiba');
}
}
function showProgressError(msg) {
clearInterval(pollTimer);
document.getElementById('progress-error').textContent = 'Hiba: ' + msg;
document.getElementById('progress-error').style.display = 'block';
document.getElementById('wizard-progress').querySelector('h3').textContent = 'Csatolás sikertelen';
}
function showDone(mountPath) {
document.getElementById('wizard-progress').style.display = 'none';
document.getElementById('wizard-done').style.display = 'block';
document.getElementById('done-path').textContent = mountPath;
document.getElementById('wizard-done').scrollIntoView({behavior:'smooth'});
}
// --- Helpers ---
function escapeJS(s) {
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
function escapeAttr(s) {
return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Cleanup on page unload (best-effort)
window.addEventListener('beforeunload', function() {
if (rawMountPath && !document.getElementById('wizard-done').style.display !== 'none') {
// Best-effort cleanup via fetch (sendBeacon can't send CSRF headers)
fetch('/api/storage/attach/cancel', {method:'POST', headers: csrfHeaders(), keepalive: true}).catch(function(){});
}
});
</script>
{{template "layout_end" .}}
{{end}}
@@ -1,365 +0,0 @@
{{define "storage_init"}}
{{template "layout_start" .}}
<div class="page-header">
<div style="display:flex;align-items:center;gap:.5rem">
<a href="/settings" class="btn btn-sm btn-outline">← Vissza</a>
<h2>Új meghajtó inicializálása</h2>
</div>
</div>
<div class="settings-card" id="wizard-scan">
<h3>1. Meghajtók keresése</h3>
<p class="settings-card-desc">Keresse meg a rendszerhez csatlakoztatott, még nem inicializált meghajtókat.</p>
<button class="btn btn-primary" onclick="scanDisks()" id="scan-btn">🔍 Meghajtók keresése</button>
<div id="scan-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
<div id="scan-result" style="display:none;margin-top:1.5rem">
<div id="available-disks"></div>
<div id="system-disks-note" style="display:none;margin-top:1rem"></div>
</div>
</div>
<div class="settings-card" id="wizard-configure" style="display:none">
<h3>2. Konfiguráció</h3>
<p class="settings-card-desc">Adja meg az inicializálás paramétereit.</p>
<form id="init-form">
<input type="hidden" id="selected-device" name="device_path">
<input type="hidden" id="create-partition" name="create_partition" value="true">
<div class="form-group">
<label>Kiválasztott eszköz</label>
<span class="settings-value mono" id="selected-device-display"></span>
</div>
<div class="form-group">
<label for="mount-name">Csatlakoztatási név <span class="required">*</span></label>
<div class="form-inline">
<span class="mono" style="opacity:.6">/mnt/</span>
<input type="text" id="mount-name" class="form-control" placeholder="hdd_1"
pattern="[a-zA-Z0-9_]+" required style="max-width:160px">
</div>
<span class="form-hint">Pl. hdd_1 → a meghajtó a /mnt/hdd_1 útvonalra kerül</span>
</div>
<div class="form-group">
<label for="storage-label">Megnevezés</label>
<input type="text" id="storage-label" class="form-control" placeholder="Külső HDD 1TB" maxlength="50">
</div>
<label class="toggle" style="margin-bottom:1.5rem">
<input type="checkbox" id="set-default" checked>
<span class="toggle-label">Beállítás alapértelmezett adattárolóként új telepítéseknél</span>
</label>
<div class="alert alert-error" style="margin-bottom:1.5rem">
<strong>⚠️ FIGYELEM:</strong> A meghajtó <strong>ÖSSZES</strong> adata törlődik!<br>
Ez a művelet <strong>NEM vonható vissza.</strong>
</div>
<div class="form-group">
<label for="confirm-input">A folytatáshoz írja be: <strong>FORMÁZÁS</strong></label>
<input type="text" id="confirm-input" class="form-control" placeholder="FORMÁZÁS"
autocomplete="off" style="max-width:200px">
</div>
<div class="form-actions" style="gap:.75rem">
<button type="submit" class="btn btn-danger-outline" id="init-btn" disabled>
Inicializálás indítása
</button>
<button type="button" class="btn btn-outline" onclick="resetWizard()">Mégsem</button>
</div>
<div id="init-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
</form>
</div>
<div class="settings-card" id="wizard-progress" style="display:none">
<h3>3. Inicializálás folyamatban...</h3>
<div class="disk-progress-steps" id="progress-steps">
<div class="disk-step" id="pstep-validating"><span class="disk-step-icon"></span> Eszköz ellenőrzése</div>
<div class="disk-step" id="pstep-partitioning"><span class="disk-step-icon"></span> Partíció létrehozása</div>
<div class="disk-step" id="pstep-formatting"><span class="disk-step-icon"></span> Fájlrendszer formázása (ext4)</div>
<div class="disk-step" id="pstep-mounting"><span class="disk-step-icon"></span> Csatlakoztatás</div>
<div class="disk-step" id="pstep-permissions"><span class="disk-step-icon"></span> Mappák és jogosultságok</div>
<div class="disk-step" id="pstep-done"><span class="disk-step-icon"></span> Regisztráció</div>
</div>
<div style="margin-top:1.5rem;display:flex;align-items:center;gap:1rem">
<div class="progress-bar-task" style="flex:1">
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
</div>
<span id="progress-percent" style="font-size:0.9rem;color:var(--text-muted);font-family:'JetBrains Mono',monospace;white-space:nowrap">0%</span>
</div>
<div id="progress-msg" class="form-hint" style="margin-top:.75rem"></div>
<div id="progress-error" class="alert alert-error" style="display:none;margin-top:1rem"></div>
</div>
<div class="settings-card" id="wizard-done" style="display:none">
<h3>✅ Meghajtó sikeresen inicializálva!</h3>
<div id="done-info" class="settings-grid" style="margin-top:1rem">
<div class="settings-row">
<span class="settings-label">Útvonal</span>
<span class="settings-value mono" id="done-path"></span>
</div>
</div>
<a href="/settings" class="btn btn-primary" style="margin-top:1.5rem">← Vissza a Beállításokhoz</a>
</div>
<script>
var selectedDevice = null;
var pollTimer = null;
function scanDisks() {
var btn = document.getElementById('scan-btn');
var errEl = document.getElementById('scan-error');
var resultEl = document.getElementById('scan-result');
btn.textContent = 'Keresés...';
btn.disabled = true;
errEl.style.display = 'none';
resultEl.style.display = 'none';
fetch('/api/storage/scan', {method:'POST', headers: csrfHeaders()})
.then(function(r){ return r.json(); })
.then(function(data) {
btn.textContent = '🔍 Meghajtók keresése';
btn.disabled = false;
if (!data.ok) {
errEl.textContent = data.error || 'Ismeretlen hiba';
errEl.style.display = 'block';
return;
}
renderScanResult(data);
resultEl.style.display = 'block';
})
.catch(function(e) {
btn.textContent = '🔍 Meghajtók keresése';
btn.disabled = false;
errEl.textContent = 'Hálózati hiba: ' + e.message;
errEl.style.display = 'block';
});
}
function renderScanResult(data) {
var availEl = document.getElementById('available-disks');
var sysEl = document.getElementById('system-disks-note');
var hasAvail = data.available && data.available.length > 0;
var hasFP = data.formatable_partitions && data.formatable_partitions.length > 0;
if (!hasAvail && !hasFP) {
availEl.innerHTML = '<div class="empty-state" style="padding:1rem">Nem található inicializálható meghajtó vagy partíció.</div>';
} else {
var html = '';
if (hasAvail) {
html += '<h4 style="margin-bottom:.75rem">Talált meghajtók (' + data.available.length + '):</h4>';
data.available.forEach(function(disk) {
var partInfo = '';
if (disk.Partitions && disk.Partitions.length > 0) {
partInfo = disk.Partitions.map(function(p) {
return p.Name + (p.FSType ? ' (' + p.FSType + ')' : ' (nincs fájlrendszer)') + (p.MountPoint ? ' → ' + p.MountPoint : '');
}).join(', ');
}
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent" ' +
'onclick="selectDisk(this, \'' + disk.Path + '\', true)" ' +
'data-path="' + disk.Path + '" id="disk-' + disk.Name + '">' +
'<div class="storage-path-header"><div class="storage-path-info">' +
'<span class="storage-path-label">○ ' + disk.Path + ' — ' + (disk.Size || '?') + '</span>' +
(disk.Model ? '<span class="storage-path-path">' + disk.Model + '</span>' : '') +
(partInfo ? '<span class="form-hint">' + partInfo + '</span>' : '<span class="form-hint">Nincs partíció</span>') +
'</div></div></div>';
});
}
if (hasFP) {
html += '<h4 style="margin-bottom:.75rem;margin-top:1.5rem">Formázható partíciók a rendszermeghajtón (' +
data.formatable_partitions.length + '):</h4>';
html += '<div class="alert alert-info" style="margin-bottom:.75rem;font-size:.85rem">' +
'Az alábbi partíciók a rendszermeghajtón találhatók, de nincsenek használatban. ' +
'Formázás után adattárolóként használhatók.</div>';
data.formatable_partitions.forEach(function(fp) {
var parentInfo = fp.ParentDiskPath + ' (' + fp.ParentDiskSize + ')';
if (fp.ParentDiskModel) parentInfo += ' — ' + fp.ParentDiskModel;
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent" ' +
'onclick="selectDisk(this, \'' + fp.Path + '\', false)" ' +
'data-path="' + fp.Path + '">' +
'<div class="storage-path-header"><div class="storage-path-info">' +
'<span class="storage-path-label">○ ' + fp.Path + ' — ' + (fp.Size || '?') + '</span>' +
'<span class="form-hint">Rendszermeghajtó partíciója: ' + parentInfo + '</span>' +
'<span class="form-hint">Nincs fájlrendszer — formázásra kész</span>' +
'</div></div></div>';
});
}
availEl.innerHTML = html;
}
if (data.system && data.system.length > 0) {
var sysNames = data.system.map(function(d){ return d.Path + ' (' + (d.Size||'?') + ')'; }).join(', ');
sysEl.innerHTML = '<span class="form-hint">A rendszermeghajtó(k) nem választhatók: ' + sysNames + '</span>';
sysEl.style.display = 'block';
}
}
function selectDisk(el, path, needsPartition) {
// Deselect all
document.querySelectorAll('[data-path]').forEach(function(d) {
d.style.border = '2px solid transparent';
d.querySelector('.storage-path-label').textContent = d.querySelector('.storage-path-label').textContent.replace('● ', '○ ');
});
// Select this
el.style.border = '2px solid var(--accent-blue)';
el.querySelector('.storage-path-label').textContent = el.querySelector('.storage-path-label').textContent.replace('○ ', '● ');
selectedDevice = path;
document.getElementById('selected-device').value = path;
document.getElementById('create-partition').value = needsPartition ? 'true' : 'false';
document.getElementById('selected-device-display').textContent = path;
// Update warning text based on whole-disk vs partition-only operation
var warningEl = document.querySelector('#wizard-configure .alert-error');
if (needsPartition) {
warningEl.innerHTML = '<strong>⚠️ FIGYELEM:</strong> A meghajtó <strong>ÖSSZES</strong> adata törlődik!<br>' +
'Ez a művelet <strong>NEM vonható vissza.</strong>';
} else {
warningEl.innerHTML = '<strong>⚠️ FIGYELEM:</strong> A partíció formázva lesz, a rajta lévő adatok törlődnek!<br>' +
'A rendszermeghajtó többi partíciója <strong>NEM</strong> érintett.';
}
// Show/hide the partitioning progress step
var partStep = document.getElementById('pstep-partitioning');
if (partStep) partStep.style.display = needsPartition ? '' : 'none';
// Show configure step
document.getElementById('wizard-configure').style.display = 'block';
document.getElementById('wizard-configure').scrollIntoView({behavior:'smooth'});
}
function resetWizard() {
selectedDevice = null;
document.getElementById('wizard-configure').style.display = 'none';
document.getElementById('init-error').style.display = 'none';
document.getElementById('confirm-input').value = '';
document.getElementById('init-btn').disabled = true;
}
// Enable init button only when confirmation is correct
document.getElementById('confirm-input').addEventListener('input', function() {
document.getElementById('init-btn').disabled = (this.value !== 'FORMÁZÁS');
});
document.getElementById('init-form').addEventListener('submit', function(e) {
e.preventDefault();
if (document.getElementById('confirm-input').value !== 'FORMÁZÁS') {
return;
}
var mountName = document.getElementById('mount-name').value.trim();
var label = document.getElementById('storage-label').value.trim();
var setDefault = document.getElementById('set-default').checked;
if (!mountName) {
document.getElementById('init-error').textContent = 'A csatlakoztatási nevet meg kell adni.';
document.getElementById('init-error').style.display = 'block';
return;
}
document.getElementById('wizard-scan').style.display = 'none';
document.getElementById('wizard-configure').style.display = 'none';
document.getElementById('wizard-progress').style.display = 'block';
document.getElementById('wizard-progress').scrollIntoView({behavior:'smooth'});
var body = {
device_path: selectedDevice,
mount_name: mountName,
label: label,
create_partition: document.getElementById('create-partition').value === 'true',
set_default: setDefault,
confirm: 'FORMÁZÁS'
};
fetch('/api/storage/init', {
method: 'POST',
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
body: JSON.stringify(body)
}).then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) {
showProgressError(data.error || 'Ismeretlen hiba');
return;
}
// Start polling
pollTimer = setInterval(pollProgress, 1500);
})
.catch(function(e) {
showProgressError('Hálózati hiba: ' + e.message);
});
});
var stepOrder = ['validating','partitioning','formatting','mounting','permissions','done'];
function pollProgress() {
fetch('/api/storage/init/status')
.then(function(r){ return r.json(); })
.then(function(data) {
if (!data.ok) return;
updateProgressUI(data);
if (data.done) {
clearInterval(pollTimer);
if (data.step === 'done') {
showDone('/mnt/' + document.getElementById('mount-name').value.trim());
}
}
})
.catch(function(){});
}
function updateProgressUI(data) {
// Update step icons
var currentIdx = stepOrder.indexOf(data.step);
stepOrder.forEach(function(s, i) {
var el = document.getElementById('pstep-' + s);
if (!el) return;
var icon = el.querySelector('.disk-step-icon');
if (i < currentIdx) {
el.className = 'disk-step disk-step-done';
icon.textContent = '✅';
} else if (i === currentIdx) {
el.className = 'disk-step disk-step-active';
icon.textContent = data.step === 'error' ? '❌' : '⏳';
} else {
el.className = 'disk-step';
icon.textContent = '○';
}
});
// Progress bar
var pct = data.pct || 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-percent').textContent = pct + '%';
document.getElementById('progress-msg').textContent = data.msg || '';
if (data.step === 'error' || data.error) {
showProgressError(data.error || data.msg || 'Ismeretlen hiba');
}
}
function showProgressError(msg) {
clearInterval(pollTimer);
document.getElementById('progress-error').textContent = 'Hiba: ' + msg;
document.getElementById('progress-error').style.display = 'block';
document.getElementById('wizard-progress').querySelector('h3').textContent = 'Inicializálás sikertelen';
}
function showDone(mountPath) {
document.getElementById('wizard-progress').style.display = 'none';
document.getElementById('wizard-done').style.display = 'block';
document.getElementById('done-path').textContent = mountPath;
document.getElementById('wizard-done').scrollIntoView({behavior:'smooth'});
}
</script>
{{template "layout_end" .}}
{{end}}