feat: drive migration & Tier 2 restic deprecation (v0.18.0)

Phase 1: Deprecate restic as Tier 2 method (rsync only), auto-migrate on startup
Phase 2: Enhanced per-app migration with backup awareness, DB dump copy, auto-cleanup
Phase 3: Full drive migration with decommissioned state, rollback support, wizard UI
Phase 4: Hub report includes decommissioned drive state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 21:49:14 +01:00
parent bdbe170a54
commit 99bf3ca7a8
22 changed files with 1725 additions and 402 deletions
+6 -22
View File
@@ -296,7 +296,7 @@
<div class="backup-layer-row">
<span class="tier-label">2. mentés</span>
{{if .Tier2Configured}}
<span class="layer-method">{{.Tier2MethodLabel}}</span>
<span class="layer-method">rsync</span>
<span class="layer-dest">→ {{.Tier2Dest}}</span>
<span class="layer-schedule">{{.Tier2Schedule}}</span>
{{if .Tier2LastRun}}
@@ -308,7 +308,7 @@
{{end}}
{{if .Tier2SizeHuman}}<span class="tier-size">{{.Tier2SizeHuman}}</span>{{end}}
<span class="tier-contents">{{.BackupContents}}</span>
{{if .Tier2Browsable}}<span class="tier-browsable" title="A mentés böngészhető fájlrendszerben">📁</span>{{end}}
<span class="tier-browsable" title="A mentés böngészhető fájlrendszerben">📁</span>
<div class="layer-actions">
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs btn-outline">Beállítás</a>
<button class="btn btn-xs btn-outline"
@@ -482,26 +482,10 @@
{{range .Tier2DriveGroups}}
<div class="drive-detail-card">
<div class="drive-detail-header">{{.DestLabel}} <span class="relative-time mono">({{.DestPath}})</span></div>
{{if .ResticItems}}
<div class="method-group">
<div class="method-group-label">Restic:</div>
{{range .ResticItems}}
<div class="repo-info-row">
<span class="repo-label">{{.DisplayName}}</span>
<span class="repo-value">{{if .SizeHuman}}{{.SizeHuman}}{{else}}—{{end}}</span>
</div>
{{end}}
</div>
{{end}}
{{if .RsyncItems}}
<div class="method-group">
<div class="method-group-label">Rsync:</div>
{{range .RsyncItems}}
<div class="repo-info-row">
<span class="repo-label">{{.DisplayName}}</span>
<span class="repo-value">{{if .SizeHuman}}{{.SizeHuman}}{{else}}—{{end}}</span>
</div>
{{end}}
{{range .Items}}
<div class="repo-info-row">
<span class="repo-label">{{.DisplayName}}</span>
<span class="repo-value">{{if .SizeHuman}}{{.SizeHuman}}{{else}}—{{end}}</span>
</div>
{{end}}
</div>
@@ -141,31 +141,6 @@
{{end}}
</select>
</div>
<div class="settings-row">
<span class="settings-label">
Módszer
<span class="info-tooltip" tabindex="0">
<span class="info-icon">i</span>
<span class="info-tooltip-text">
<strong>Egyszerű másolat (rsync):</strong> Tükörszerű másolat, a fájlok közvetlenül böngészhetők.
Nem titkosított, nem verziózott — mindig a legfrissebb állapotot tartalmazza.
<br><br>
<strong>Titkosított mentés (restic):</strong> Titkosított, tömörített, verziózott mentés.
Korábbi állapotok visszaállíthatók. Nem böngészhető közvetlenül —
visszaállításhoz a vezérlőpult szükséges.
</span>
</span>
</span>
<select name="cross_drive_method" id="cd-method" class="form-control cross-drive-field" style="max-width:20rem"
{{if not (and .CrossDriveConfig .CrossDriveConfig.Enabled)}}disabled{{end}}>
<option value="rsync" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Method "rsync")}}selected{{end}}>
Egyszerű másolat (rsync)
</option>
<option value="restic" {{if and .CrossDriveConfig (eq .CrossDriveConfig.Method "restic")}}selected{{end}}>
Titkosított mentés (restic)
</option>
</select>
</div>
<div class="settings-row">
<span class="settings-label">Ütemezés</span>
<div>
+33 -10
View File
@@ -38,10 +38,19 @@
<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>A régi adatok megmaradnak biztonsági másolatként</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">
@@ -58,6 +67,8 @@
<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>
@@ -75,16 +86,18 @@
<div class="settings-card" id="migrate-done-card" style="display:none">
<h3>✅ Adatáthelyezés kész!</h3>
<p style="margin-top:.75rem;color:var(--text-secondary)">
Az alkalmazás az új tárolóról fut.<br>
A régi adatok a korábbi helyen megmaradtak biztonsági másolatként.
<p id="done-msg" style="margin-top:.75rem;color:var(--text-secondary)">
Az alkalmazás az új tárolóról fut.
</p>
<div class="alert alert-warning" style="margin-top:1rem">
<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>
<li>Ha minden rendben, törölheted a korábbi adatokat</li>
</ol>
</div>
<div style="margin-top:1.5rem;display:flex;gap:.75rem;flex-wrap:wrap">
@@ -111,10 +124,12 @@ function startMigrate() {
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: {'Content-Type': 'application/json'},
body: JSON.stringify({stack_name: stackName, target_path: targetPath})
body: JSON.stringify({stack_name: stackName, target_path: targetPath, auto_delete_stale: autoDelete})
})
.then(function(r){ return r.json(); })
.then(function(data) {
@@ -129,7 +144,7 @@ function startMigrate() {
});
}
var migStepOrder = ['stopping','copying','updating','starting','done'];
var migStepOrder = ['stopping','copying','updating','starting','cleaning','backing_up','done'];
function pollMigProgress() {
fetch('/api/storage/migrate/status')
@@ -194,8 +209,16 @@ 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'});
// Show the delete button (old data is at the source path)
document.getElementById('migrate-delete-old-btn').style.display = '';
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() {
@@ -0,0 +1,218 @@
{{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: {'Content-Type': 'application/json'},
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}}
@@ -205,18 +205,20 @@ function pollUntilBack() {
{{if .StoragePaths}}
<div class="storage-paths-list">
{{range .StoragePaths}}
<div class="storage-path-item{{if .Disconnected}} storage-disconnected{{end}}">
<div class="storage-path-item{{if .Disconnected}} storage-disconnected{{else if .Decommissioned}} storage-decommissioned{{end}}">
<div class="storage-path-header">
<div class="storage-path-info">
<div class="storage-path-label-wrap" id="label-wrap-{{.Path}}">
<span class="storage-path-label" id="label-display-{{.Path}}">{{.Label}}</span>
{{if not .Disconnected}}<button class="btn btn-xs btn-ghost" onclick="editStorageLabel('{{.Path}}', '{{.Label}}')" title="Átnevezés">✏️</button>{{end}}
{{if not (or .Disconnected .Decommissioned)}}<button class="btn btn-xs btn-ghost" onclick="editStorageLabel('{{.Path}}', '{{.Label}}')" title="Átnevezés">✏️</button>{{end}}
</div>
<span class="storage-path-path mono">{{.Path}}</span>
</div>
<div class="storage-path-badges">
{{if .Disconnected}}
<span class="badge badge-error">Leválasztva</span>
{{else if .Decommissioned}}
<span class="badge state-gray">Kiváltva</span>
{{else}}
{{if .IsDefault}}<span class="badge state-green">Alapértelmezett</span>{{end}}
{{if .Schedulable}}<span class="badge" style="background:rgba(0,136,204,0.15);color:var(--accent-light)">Aktív</span>{{else}}<span class="badge state-gray">Inaktív</span>{{end}}
@@ -236,6 +238,20 @@ function pollUntilBack() {
<div class="storage-path-actions" id="storage-actions-{{.Path}}">
<button class="btn btn-xs btn-primary" onclick="storageReconnect('{{.Path}}')">Csatlakoztatás</button>
</div>
{{else if .Decommissioned}}
<div class="storage-path-details">
<div class="storage-disconnected-info">
<span class="form-hint">Adatok átköltöztetve ide: <strong>{{.MigratedToLabel}}</strong> ({{.MigratedTo}})</span>
{{if .DecommissionedAt}}<span class="form-hint">Időpont: {{.DecommissionedAt}}</span>{{end}}
</div>
</div>
<div class="storage-path-actions">
<form method="POST" action="/settings/storage/remove" style="display:inline"
onsubmit="return confirm('Biztosan eltávolítja a(z) {{.Label}} ({{.Path}}) meghajtót a rendszerből?\n\nA meghajtó adatai NEM törlődnek.')">
<input type="hidden" name="storage_path" value="{{.Path}}">
<button type="submit" class="btn btn-xs btn-outline">Eltávolítás a rendszerből</button>
</form>
</div>
{{else}}
<div class="storage-path-details">
{{if .DiskInfo}}
@@ -311,6 +327,9 @@ function pollUntilBack() {
<button type="submit" class="btn btn-xs btn-danger-outline">Eltávolítás</button>
</form>
{{end}}
{{if and (gt .AppCount 0) .HasOtherPaths}}
<a href="/settings/storage/migrate-drive?source={{.Path}}" class="btn btn-xs btn-outline">📦 Összes adat átköltöztetése</a>
{{end}}
</div>
{{end}}
</div>
+6 -1
View File
@@ -2264,7 +2264,12 @@ a.stat-card:hover {
opacity: 0.75;
border-style: dashed;
}
.storage-disconnected .storage-disconnected-info {
.storage-decommissioned {
opacity: 0.6;
border-color: var(--border-color);
}
.storage-disconnected .storage-disconnected-info,
.storage-decommissioned .storage-disconnected-info {
display: flex;
flex-direction: column;
gap: .25rem;