062357f778
Closes the two findings from DIAG-immich-restore-2026-07-19. Viktor deleted 11
immich photos to test offsite restore; both runs flashed success and the photos
stayed gone. Two independent defects.
R-43 — no offsite path could restore a database. All three buttons were
file-only: the two "visszaállítás" actions staged to a scratch folder and never
touched postgres, and place-to-live merged only MISSING files. For a DB-indexed
app the bytes returned and the app still could not see them. The dump was
carried INTO every snapshot and could never be replayed OUT of one.
New ReconstituteFromOffsite (/backup/offbox/reconstitute): safety dump → stop →
files overwritten to the snapshot version → start → the snapshot's own dump
replayed → health wait. Two invariants:
- nothing is ever deleted (-a, no --ignore-existing, no --delete): a file
created after the snapshot survives as an extra;
- the undo exists before the act — the pre-restore- dump is verified ON DISK
before anything is stopped, overwritten or replayed; if it cannot be taken
the operation refuses with zero changes.
The replay reads the SCRATCH unit: the live unit is never overwritten, so
replaying from it would replay the current DB over itself and restore nothing.
R-44 — a manual push shipped an unrefreshed dump (up to ~24h old). That day's
predated the customer's account by four hours and probed to asset:0/user:0/
album:0 inside 52MB whose bulk was immich's shipped geodata. Every run, manual
AND nightly, now refreshes dumps + units BEFORE capturing. Order is the
mechanism: the gap can only ADD files the DB does not reference yet, never
remove one it does. Manifests carry offsite_run_id + dumps_at, so coherence is
verifiable at restore time rather than assumed; the periodic refresh carries a
prior stamp forward and never invents one.
Honesty surfaces, all warn-level and none a gate: unstamped (pre-v0.148) pairs
report their skew, ValidateDump gained an EXACT-match accounts-table sniff for
customer-empty dumps, the completion flash states an outcome instead of a
mechanism, and the missing-only button now says what it does NOT do.
11 tests; 5 red-proofs run and reverted. Two of those found real test weaknesses
rather than confirming strength — the first undo mutation was caught by a second
guard, and the first table-matching test did not discriminate between the two
matchers at all. Both tests were rewritten to the cases that separate them.
NOT in scope: R-41's catalog invariant check, nightly cadence, retention, quota
math, tier-2, and v0.147.x progress semantics beyond one added phase line.
Live acceptance (§9) has NOT run: no capability-map flip, customer-restore row
stays MISSING, R-3 stays DRAFT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9Nn14TWGzKoqAJAiVwC2s
425 lines
22 KiB
HTML
425 lines
22 KiB
HTML
{{define "backups_restore"}}
|
||
{{template "layout_start" .}}
|
||
|
||
<div class="page-header">
|
||
<h2>Biztonsági mentés — Visszaállítás</h2>
|
||
<span class="domain-badge">{{.Domain}}</span>
|
||
</div>
|
||
|
||
{{template "backups_flash" .}}
|
||
{{template "restore_banner" .}}
|
||
|
||
{{if not .Backup}}
|
||
{{template "backups_empty" .}}
|
||
{{else}}
|
||
|
||
<!-- Section 7: Restore -->
|
||
{{if .Backup.AppDataInfo}}
|
||
<div class="backup-section-card">
|
||
<h3>Visszaállítás</h3>
|
||
<div class="restore-section">
|
||
<div class="restore-form-row">
|
||
<label class="restore-label">Alkalmazás:</label>
|
||
<select id="restore-app" class="restore-select" onchange="onRestoreAppChange()">
|
||
<option value="">— Válasszon —</option>
|
||
{{range .Backup.AppDataInfo}}
|
||
<option value="{{.StackName}}" data-has-hdd="{{.HasHDDData}}" data-has-db="{{.HasDBDump}}" data-has-volumes="{{.HasVolumeData}}">{{.DisplayName}}</option>
|
||
{{end}}
|
||
</select>
|
||
</div>
|
||
<div class="restore-form-row">
|
||
<label class="restore-label">Pillanatkép:</label>
|
||
<select id="restore-snapshot" class="restore-select" onchange="onRestoreConfirmChange()">
|
||
<option value="">— Válasszon alkalmazást —</option>
|
||
</select>
|
||
</div>
|
||
<div id="restore-type-info" class="restore-info" style="display:none;margin-bottom:0.5rem">
|
||
</div>
|
||
<div id="restore-no-snapshots" class="restore-warning" style="display:none;">
|
||
Még nincs mentés felhasználói adattal.
|
||
</div>
|
||
<div class="restore-warning">
|
||
<svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A visszaállítás felülírja az alkalmazás jelenlegi adatait a kiválasztott mentés állapotával.
|
||
Az alkalmazás a folyamat során automatikusan leáll és újraindul.
|
||
</div>
|
||
<div class="restore-confirm">
|
||
<label>
|
||
<input type="checkbox" id="restore-confirm-cb" onchange="onRestoreConfirmChange()">
|
||
Megértettem, visszaállítás indítása.
|
||
</label>
|
||
</div>
|
||
<div class="restore-actions">
|
||
<button type="button" class="btn btn-sm btn-danger" id="restore-btn" disabled onclick="submitRestore()">Visszaállítás indítása</button>
|
||
</div>
|
||
<div style="margin-top: 1rem; text-align: center; border-top: 1px solid var(--line); padding-top: 1rem;">
|
||
<a href="/import" class="btn btn-sm btn-outline">Importálás mentett csomagból (.fab)</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
<!-- Ellenőrző visszaállítás a távoli (3. mentés) tárolóból — the per-app restore-to-verify
|
||
forms moved here from the Távoli mentés toggle list (v0.124.0 IA split). Display list =
|
||
the offbox-toggled apps; the action itself is unchanged. -->
|
||
{{if .OffboxConfigured}}
|
||
<div class="backup-section-card">
|
||
<h3>Ellenőrző visszaállítás a távoli tárolóból</h3>
|
||
<p class="form-hint" style="margin:-0.25rem 0 1rem">A távoli mentésre kijelölt alkalmazások legutóbbi pillanatképe egy külön ellenőrző mappába állítható vissza — a meglévő adatok nem változnak.</p>
|
||
{{if .OffboxToggledCount}}
|
||
<div class="app-row-list">
|
||
{{range .OffboxApps}}
|
||
{{if .Enabled}}
|
||
{{template "app_list_row" dict "Slug" .Slug "Name" .DisplayName}}
|
||
<form method="POST" action="/backup/offbox/restore" style="display:inline">{{$.CSRFField}}
|
||
<input type="hidden" name="app" value="{{.Name}}">
|
||
<input type="hidden" name="mode" value="unit">
|
||
<button type="submit" class="btn btn-xs btn-outline">Visszaállítás ellenőrzéshez (konfiguráció + adatbázis)</button>
|
||
</form>
|
||
<form method="POST" action="/backup/offbox/restore" style="display:inline">{{$.CSRFField}}
|
||
<input type="hidden" name="app" value="{{.Name}}">
|
||
<input type="hidden" name="mode" value="full">
|
||
<button type="submit" class="btn btn-xs btn-outline">Teljes visszaállítás előkészítése</button>
|
||
</form>
|
||
{{if $.FullPrepApp}}{{if eq $.FullPrepApp .Name}}
|
||
<form method="POST" action="/backup/offbox/restore" style="display:inline">{{$.CSRFField}}
|
||
<input type="hidden" name="app" value="{{.Name}}">
|
||
<input type="hidden" name="mode" value="full">
|
||
<input type="hidden" name="confirm" value="1">
|
||
<button type="submit" class="btn btn-xs btn-primary">Teljes visszaállítás indítása (~{{$.FullPrepSize}})</button>
|
||
</form>
|
||
{{end}}{{end}}
|
||
{{if $.OffboxScratchReady}}{{if index $.OffboxScratchReady .Name}}
|
||
<form method="POST" action="/backup/offbox/place" style="display:inline">{{$.CSRFField}}
|
||
<input type="hidden" name="app" value="{{.Name}}">
|
||
<button type="submit" class="btn btn-xs btn-outline">Helyreállítás az élő adatok közé (csak a hiányzó fájlok)</button>
|
||
</form>
|
||
<span class="form-hint" style="display:block;margin-top:.25rem">A meglévő fájlokat nem írja felül. Adatbázist nem állít vissza — törölt tartalom ettől nem jelenik meg újra.</span>
|
||
{{$pair := index $.OffboxPairInfo .Name}}
|
||
<form method="POST" action="/backup/offbox/reconstitute" style="display:inline">{{$.CSRFField}}
|
||
<input type="hidden" name="app" value="{{.Name}}">
|
||
<input type="hidden" name="confirm" value="1">
|
||
<button type="button" class="btn btn-xs btn-primary"
|
||
data-restore-app="{{.Name}}"
|
||
data-restore-when="{{if not $pair.DumpsAt.IsZero}}{{fmtTime $pair.DumpsAt}}{{end}}"
|
||
data-restore-skewed="{{if $pair.Skewed}}1{{end}}"
|
||
data-restore-empty="{{if $pair.LooksEmpty}}1{{end}}"
|
||
onclick="confirmFullRestore(this)">Teljes visszaállítás (fájlok + adatbázis)</button>
|
||
</form>
|
||
<span class="form-hint" style="display:block;margin-top:.25rem">A fájlokat a mentés szerinti változatra állítja vissza és az adatbázist is visszatölti. Semmit nem töröl: a mentés óta létrejött fájlok megmaradnak. A jelenlegi adatbázisról előtte biztonsági mentés készül.</span>
|
||
{{if $pair.Skewed}}
|
||
<span class="form-hint" style="display:block;margin-top:.25rem;color:var(--warn)">Az adatbázis-mentés régebbi{{if not $pair.DumpsAt.IsZero}} ({{fmtTime $pair.DumpsAt}}){{end}} — a fájlok és az adatbázis eltérő időpontból származnak.</span>
|
||
{{end}}
|
||
{{if $pair.LooksEmpty}}
|
||
<span class="form-hint" style="display:block;margin-top:.25rem;color:var(--warn)">A mentett adatbázis üresnek tűnik (nincs benne felhasználói fiók) — elképzelhető, hogy a mentés korábbi, mint az adataid.</span>
|
||
{{end}}
|
||
{{end}}{{end}}
|
||
{{template "app_list_row_end"}}
|
||
{{end}}
|
||
{{end}}
|
||
</div>
|
||
{{else}}
|
||
<p class="form-hint">Nincs távoli mentésre jelölt alkalmazás — a kijelölés a <a href="/backups/remote">Távoli mentés</a> oldalon történik.</p>
|
||
{{end}}
|
||
|
||
<!-- R-7b: the shares source. Not an app row — it has no per-app toggle and no recovery unit —
|
||
so it is its own entry. The reserved `_shares` key never appears here; the label always
|
||
comes from the display mapping. -->
|
||
{{if .SharesRestoreOffered}}
|
||
<div class="app-row-list" style="margin-top:1rem">
|
||
{{template "app_list_row" dict "Slug" "" "Name" .SharesDisplayName}}
|
||
<form method="POST" action="/backup/shares/restore" style="display:inline">{{$.CSRFField}}
|
||
<button type="submit" class="btn btn-xs btn-outline">Megosztások visszaállítása</button>
|
||
</form>
|
||
{{if .SharesScratchReady}}
|
||
<form method="POST" action="/backup/shares/place" style="display:inline">{{$.CSRFField}}
|
||
<button type="submit" class="btn btn-xs btn-outline">Helyreállítás az élő adatok közé (csak a hiányzó fájlok)</button>
|
||
</form>
|
||
<span class="form-hint" style="display:block;margin-top:.25rem">A meglévő fájlokat nem írja felül. A már létező megosztás-beállítások változatlanok maradnak.</span>
|
||
{{end}}
|
||
{{template "app_list_row_end"}}
|
||
</div>
|
||
{{end}}
|
||
|
||
<!-- v0.147.0 (4a): what the verification restores actually LEFT on disk. Until now the result of
|
||
an ellenőrző visszaállítás was invisible — the flash said "a verification folder on the
|
||
drive" without naming it, and nothing listed what had accumulated. Full path, size and date,
|
||
so the copy can be found, opened, and cleaned up. -->
|
||
<div class="backup-tier-divider" style="margin-top:1.5rem"></div>
|
||
<h3 style="margin-bottom:.5rem">Meglévő ellenőrző másolatok</h3>
|
||
{{if .OffsiteRestoreCopies}}
|
||
<p class="form-hint" style="margin-bottom:.75rem">Ezek a visszaállított másolatok helyet foglalnak a meghajtón. A tényleges adataidat nem érintik, bármikor törölhetők.</p>
|
||
<div class="app-row-list">
|
||
{{range .OffsiteRestoreCopies}}
|
||
{{template "app_list_row" dict "Slug" .Stack "Name" .Stack "Secondary" (printf "%s · %s · %s" .SizeHuman (.Created.Format "2006. 01. 02. 15:04") .Path)}}
|
||
<form method="POST" action="/backup/offbox/verify-copy/delete" style="display:inline">
|
||
{{$.CSRFField}}
|
||
<input type="hidden" name="stack" value="{{.Stack}}">
|
||
<input type="hidden" name="confirm" value="1">
|
||
<button type="button" class="btn btn-xs btn-danger-outline"
|
||
data-copy-path="{{.Path}}"
|
||
onclick="confirmDeleteVerifyCopy(this)">Másolat törlése</button>
|
||
</form>
|
||
{{template "app_list_row_end"}}
|
||
{{end}}
|
||
</div>
|
||
{{else}}
|
||
<p class="form-hint">Nincs ellenőrző másolat a meghajtón.</p>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|
||
|
||
|
||
<!-- Hordozható mentéscsomag (.fab) — v0.124.0, decision 3: PORTABILITY, not a backup tier (no
|
||
scheduling, no status surface; point-in-time framing everywhere). The download reuses the
|
||
EXISTING export pipeline (same producer as a drive export → byte-identical bundle) staged
|
||
under the data dir, then streams through a guarded endpoint. Import stays drive-scan. -->
|
||
<div class="backup-section-card">
|
||
<h3>Hordozható mentéscsomag (.fab)</h3>
|
||
<p class="form-hint" style="margin:-0.25rem 0 1rem">Hordozható pillanatfelvétel — bárhol tárolhatod, és bármikor visszatöltheted egy meghajtóról. A folyamatos védelmet az 1–3. szintű mentés adja.</p>
|
||
{{if .OffboxApps}}
|
||
<div class="form-row" style="max-width:420px"><label>Jelszavas titkosítás (opcionális)</label>
|
||
<input type="password" id="fab-dl-password" class="form-input" autocomplete="new-password" placeholder="Opcionális jelszó">
|
||
<span class="form-hint">Üresen hagyva a csomag titkosítás nélkül készül.</span>
|
||
</div>
|
||
<div class="app-row-list">
|
||
{{range .OffboxApps}}
|
||
{{template "app_list_row" dict "Slug" .Slug "Name" .DisplayName}}
|
||
<button type="button" class="btn btn-xs btn-outline fab-dl-btn" data-stack="{{.Name}}" onclick="fabDownload(this)">Letöltés (.fab)</button>
|
||
{{template "app_list_row_end"}}
|
||
{{end}}
|
||
</div>
|
||
<div class="schedule-actions" style="margin-top:.75rem">
|
||
<button type="button" class="btn btn-xs btn-outline" id="fab-dl-all" onclick="fabDownloadAll()">Összes letöltése (egyenként)</button>
|
||
<span class="form-hint" style="margin-left:.5rem">A csomagok egyenként készülnek és töltődnek le — pillanatfelvétel a mostani állapotról.</span>
|
||
</div>
|
||
<div id="fab-dl-status" class="form-hint" style="margin-top:.5rem"></div>
|
||
{{else}}
|
||
<p class="form-hint">Nincs telepített alkalmazás.</p>
|
||
{{end}}
|
||
</div>
|
||
|
||
{{end}}
|
||
|
||
<script>
|
||
{{template "restore_banner_js"}}
|
||
|
||
// .fab download flow (v0.124.0): estimate → inline confirm (size shown BEFORE starting) →
|
||
// start (the EXISTING async export, staging dest) → poll → browser GET (guarded stream; the
|
||
// server removes the staged bundle after the stream). The batch runs apps ONE AT A TIME.
|
||
var fabQueue = [];
|
||
function fabSetStatus(msg){ document.getElementById('fab-dl-status').textContent = msg; }
|
||
function fabPassword(){ var el = document.getElementById('fab-dl-password'); return el ? el.value : ''; }
|
||
function fabDownload(btn){
|
||
var stack = btn.getAttribute('data-stack');
|
||
fetch('/api/export/download/estimate?stack=' + encodeURIComponent(stack))
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(j){
|
||
if (!j.ok) { fabSetStatus('Hiba: ' + (j.error || 'a becslés sikertelen')); return; }
|
||
var size = (j.data && j.data.total_size_human) || '?';
|
||
felhomConfirm(btn, 'A csomag becsült mérete: ' + size + '. Elindítod a letöltést? (Pillanatfelvétel a mostani állapotról.)', function(){
|
||
fabStart(stack, null);
|
||
});
|
||
})
|
||
.catch(function(){ fabSetStatus('Hiba: a becslés nem érhető el.'); });
|
||
}
|
||
// v0.147.0 (4a): the ONLY delete this page offers. Two inline acknowledgements — the house
|
||
// double-confirm idiom (deploy.html's stale-data delete), never native confirm(), which is an
|
||
// OS-modal that blocks browser automation (F-11). The first step names the exact path so the
|
||
// customer is agreeing to a specific directory, not to the word "delete".
|
||
function confirmDeleteVerifyCopy(btn){
|
||
var path = btn.getAttribute('data-copy-path') || '';
|
||
felhomConfirm(btn, 'Biztosan törlöd ezt az ellenőrző másolatot? ' + path + ' — a tényleges adataid változatlanok maradnak.', function(){
|
||
felhomConfirm(btn, 'UTOLSÓ MEGERŐSÍTÉS: a másolat véglegesen törlődik.', function(){
|
||
var f = btn.closest('form');
|
||
if (f) { if (f.requestSubmit) f.requestSubmit(); else f.submit(); }
|
||
});
|
||
});
|
||
}
|
||
/* R-43: the true offsite restore overwrites live files and replays a database, so it double-confirms
|
||
and — unlike the old missing-only merge — states the DB half's age and any warning BEFORE the
|
||
customer commits. The honesty lines are already rendered under the button; repeating the decisive
|
||
ones here means the person clicking "Igen" has read them. */
|
||
function confirmFullRestore(btn){
|
||
var app = btn.getAttribute('data-restore-app') || '';
|
||
var when = btn.getAttribute('data-restore-when') || '';
|
||
var skewed = btn.getAttribute('data-restore-skewed') === '1';
|
||
var empty = btn.getAttribute('data-restore-empty') === '1';
|
||
var q = 'Teljes visszaállítás: ' + app + (when ? ' — a mentés ideje: ' + when : '') + '.';
|
||
if (skewed) { q += ' FIGYELEM: a fájlok és az adatbázis eltérő időpontból származnak.'; }
|
||
if (empty) { q += ' FIGYELEM: a mentett adatbázis üresnek tűnik.'; }
|
||
q += ' A fájlok a mentés szerinti változatra állnak vissza, semmi nem törlődik.';
|
||
felhomConfirm(btn, q, function(){
|
||
felhomConfirm(btn, 'UTOLSÓ MEGERŐSÍTÉS: az alkalmazás leáll, az adatbázis visszatöltődik, majd újraindul. A jelenlegi adatbázisról biztonsági mentés készül.', function(){
|
||
var f = btn.closest('form');
|
||
if (f) { if (f.requestSubmit) f.requestSubmit(); else f.submit(); }
|
||
});
|
||
});
|
||
}
|
||
function fabStart(stack, next){
|
||
fetch('/api/export/download/start', {method:'POST', headers:Object.assign({'Content-Type':'application/json'}, csrfHeaders()), body: JSON.stringify({stack_name: stack, password: fabPassword()})})
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(j){
|
||
if (!j.ok) { fabSetStatus('Hiba (' + stack + '): ' + (j.error || 'az export nem indult el')); if (next) next(); return; }
|
||
fabSetStatus('Csomag készítése: ' + stack + '…');
|
||
fabPoll(stack, next);
|
||
})
|
||
.catch(function(){ fabSetStatus('Hiba: az export nem indult el.'); if (next) next(); });
|
||
}
|
||
function fabPoll(stack, next){
|
||
var iv = setInterval(function(){
|
||
fetch('/api/export/status').then(function(r){ return r.json(); }).then(function(j){
|
||
if (!j || j.running || !j.done) return; // keep polling until the job reports done
|
||
clearInterval(iv);
|
||
if (j.error) { fabSetStatus('Hiba (' + stack + '): ' + j.error); if (next) next(); return; }
|
||
var base = (j.output_path || '').split('/').pop();
|
||
if (!base) { fabSetStatus('Hiba: a csomag útvonala hiányzik.'); if (next) next(); return; }
|
||
fabSetStatus('Letöltés: ' + base + (j.output_size ? ' (' + j.output_size + ')' : ''));
|
||
window.location.href = '/api/export/download?file=' + encodeURIComponent(base);
|
||
if (next) setTimeout(next, 3000); // let the stream begin before the next export starts
|
||
}).catch(function(){});
|
||
}, 2000);
|
||
}
|
||
function fabDownloadAll(){
|
||
var btns = document.querySelectorAll('.fab-dl-btn');
|
||
fabQueue = Array.prototype.map.call(btns, function(b){ return b.getAttribute('data-stack'); });
|
||
fabRunQueue();
|
||
}
|
||
function fabRunQueue(){
|
||
var stack = fabQueue.shift();
|
||
if (!stack) { fabSetStatus('Minden csomag elkészült.'); return; }
|
||
fabStart(stack, fabRunQueue);
|
||
}
|
||
|
||
// Restore section
|
||
var huDays = ['vasárnap', 'hétfő', 'kedd', 'szerda', 'csütörtök', 'péntek', 'szombat'];
|
||
function formatSnapshot(s) {
|
||
var t = new Date(s.time);
|
||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||
var label = t.getFullYear() + '-' + pad(t.getMonth()+1) + '-' + pad(t.getDate()) +
|
||
' ' + huDays[t.getDay()] + ' ' + pad(t.getHours()) + ':' + pad(t.getMinutes()) +
|
||
' (' + s.short_id + ')';
|
||
var tierLabel = s.tier === 2 ? '2. szint' : '1. szint';
|
||
if (s.drive_label) {
|
||
label += ' — ' + tierLabel + ', ' + s.drive_label;
|
||
} else {
|
||
label += ' — ' + tierLabel;
|
||
}
|
||
return label;
|
||
}
|
||
|
||
function onRestoreAppChange() {
|
||
var sel = document.getElementById('restore-app');
|
||
var appName = sel.value;
|
||
var snapSel = document.getElementById('restore-snapshot');
|
||
var noSnaps = document.getElementById('restore-no-snapshots');
|
||
var typeInfo = document.getElementById('restore-type-info');
|
||
|
||
document.getElementById('restore-confirm-cb').checked = false;
|
||
document.getElementById('restore-btn').disabled = true;
|
||
noSnaps.style.display = 'none';
|
||
typeInfo.style.display = 'none';
|
||
|
||
if (!appName) {
|
||
snapSel.innerHTML = '<option value="">— Válasszon alkalmazást —</option>';
|
||
return;
|
||
}
|
||
|
||
// Determine restore type from data attributes
|
||
var opt = sel.options[sel.selectedIndex];
|
||
var hasHDD = opt.getAttribute('data-has-hdd') === 'true';
|
||
var hasDB = opt.getAttribute('data-has-db') === 'true';
|
||
var hasVolumes = opt.getAttribute('data-has-volumes') === 'true';
|
||
|
||
if (hasHDD || hasVolumes) {
|
||
typeInfo.innerHTML = 'Teljes visszaállítás: adatbázis + konfiguráció + felhasználói adatok a kiválasztott pillanatképből.';
|
||
typeInfo.className = 'restore-info';
|
||
} else if (hasDB) {
|
||
typeInfo.innerHTML = 'Adatbázis és konfiguráció visszaállítása — az alkalmazásnak nincs külön felhasználói adata.';
|
||
typeInfo.className = 'restore-info restore-info-partial';
|
||
} else {
|
||
typeInfo.innerHTML = 'Csak konfiguráció visszaállítása (compose fájlok, beállítások).';
|
||
typeInfo.className = 'restore-info restore-info-partial';
|
||
}
|
||
typeInfo.style.display = 'block';
|
||
|
||
snapSel.innerHTML = '<option value="">— Betöltés... —</option>';
|
||
|
||
fetch('/api/backup/snapshots?stack=' + encodeURIComponent(appName))
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
snapSel.innerHTML = '<option value="">— Válasszon —</option>';
|
||
if (data.ok && data.data && data.data.length > 0) {
|
||
// Group by tier
|
||
var tier1 = data.data.filter(function(s) { return s.tier !== 2; });
|
||
var tier2 = data.data.filter(function(s) { return s.tier === 2; });
|
||
|
||
if (tier1.length > 0) {
|
||
var grp1 = document.createElement('optgroup');
|
||
grp1.label = '1. szint — Helyi mentés (ajánlott)';
|
||
tier1.forEach(function(s) {
|
||
var o = document.createElement('option');
|
||
o.value = s.short_id;
|
||
o.textContent = formatSnapshot(s);
|
||
grp1.appendChild(o);
|
||
});
|
||
snapSel.appendChild(grp1);
|
||
}
|
||
if (tier2.length > 0) {
|
||
var grp2 = document.createElement('optgroup');
|
||
grp2.label = '2. szint — Másodlagos másolat';
|
||
tier2.forEach(function(s) {
|
||
var o = document.createElement('option');
|
||
o.value = s.short_id;
|
||
o.textContent = formatSnapshot(s);
|
||
grp2.appendChild(o);
|
||
});
|
||
snapSel.appendChild(grp2);
|
||
}
|
||
} else {
|
||
snapSel.innerHTML = '<option value="">— Nincs elérhető mentés —</option>';
|
||
noSnaps.style.display = 'block';
|
||
}
|
||
});
|
||
}
|
||
|
||
function onRestoreConfirmChange() {
|
||
var cb = document.getElementById('restore-confirm-cb');
|
||
var app = document.getElementById('restore-app').value;
|
||
var snap = document.getElementById('restore-snapshot').value;
|
||
document.getElementById('restore-btn').disabled = !(cb.checked && app && snap);
|
||
}
|
||
|
||
function submitRestore() {
|
||
var app = document.getElementById('restore-app').value;
|
||
var snap = document.getElementById('restore-snapshot').value;
|
||
if (!app || !snap) return;
|
||
|
||
var btn = document.getElementById('restore-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Visszaállítás folyamatban...';
|
||
|
||
var form = document.createElement('form');
|
||
form.method = 'POST';
|
||
form.action = '/backup/restore';
|
||
|
||
var fc = document.createElement('input');
|
||
fc.type = 'hidden'; fc.name = '_csrf';
|
||
fc.value = (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
|
||
form.appendChild(fc);
|
||
|
||
var f1 = document.createElement('input');
|
||
f1.type = 'hidden'; f1.name = 'stack_name'; f1.value = app;
|
||
form.appendChild(f1);
|
||
|
||
var f2 = document.createElement('input');
|
||
f2.type = 'hidden'; f2.name = 'snapshot_id'; f2.value = snap;
|
||
form.appendChild(f2);
|
||
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
}
|
||
</script>
|
||
|
||
{{template "layout_end" .}}
|
||
{{end}}
|