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:
@@ -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">← 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}}) — {{.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/<név>) 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, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// 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}}
|
||||
Reference in New Issue
Block a user