53e9bf0224
gates / gates (push) Successful in 26s
R-237: /backups/restore listed apps that are CURRENTLY DEPLOYED and CURRENTLY TOGGLED ON for future off-site backups. A rebuilt box has neither, so a household that had just lost everything was shown nothing to restore while the repository held their snapshots — measured live on the R-201 re-walk. To restore an app you had to select it, to select it you had to have installed it, and to know what to install you had to see the backup you could not see. The store is now the source of the list (offsite_restore_list.go), built on the existing R-193 OffsiteInventoryList. Installed-ness became a property OF a row, never a filter on it. Every case is answered rather than hidden: a snapshot for an app that is not installed is offered and says it will reinstall first; an installed app with no snapshot is shown as having nothing; an unreadable store renders as UNKNOWN (R-225's rule, one screen over) AND keeps the action, because "we could not look" is not "there is nothing"; no-target is its own state. The felhom-offbox and _shares marker tags are excluded from the app list. R-238 classified as a HARNESS ARTIFACT: mode=full without confirm=1 is step 1 of a deliberate two-step — it starts no job by design and redirects carrying &full_prep=<app>, which deriveWizardStep requires to reveal the commit. A driver that did not carry it forward landed back on the intent step. The operator's browser run completed the same restore. The wizard's precedence rules were NOT re-keyed: a stale ?full_prep= must never resurrect a commit button mid-restore. The residue WAS real and is fixed: neither branch of that step wrote anything to the log, so a refusal — including by the headroom gate — left no trace on the box. Both branches now log, and so does the concurrent-op refusal. resolveWizardApp is removed: it was dead once the gate moved, and its test pinned the defect's behaviour (an untoggled app refused), which would have read as policy. 28 packages ok, 9/9 gates OK. Three red-proofs, each asserted to have applied.
386 lines
20 KiB
HTML
386 lines
20 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}}
|
||
|
||
<!-- Visszaállítás a távoli (3. mentés) tárolóból. Display list = the offbox-toggled apps.
|
||
v0.124.0 moved these rows here from the Távoli mentés toggle list; v0.154.0 (R-48) reduced each
|
||
row to a SINGLE „Visszaállítás…" entry — the actions themselves are unchanged, they moved into
|
||
the wizard at /backups/restore/app. -->
|
||
{{if .OffboxConfigured}}
|
||
<div class="backup-section-card">
|
||
<h3>Visszaállítás a távoli tárolóból</h3>
|
||
<p class="form-hint" style="margin:-0.25rem 0 1rem">Válaszd ki az alkalmazást, és a következő oldalon döntsd el, mit szeretnél: ellenőrzést külön mappába, csak a hiányzó fájlok visszahozását, vagy teljes visszaállítást. Egyik sem indul el kérdés nélkül.</p>
|
||
<!-- R-237: driven by what is IN THE STORE, not by what is deployed and toggled. A rebuilt box has
|
||
neither and used to be told there was nothing to restore, while its snapshots sat in the
|
||
repository the whole time. Installed-ness is a property OF a row, never a filter on it. -->
|
||
{{if eq .OffsiteStoreState "unreadable"}}
|
||
<p class="form-hint">Nem tudjuk elolvasni a távoli tárolót, ezért <strong>nem tudjuk, mi van benne</strong>. Ez nem azt jelenti, hogy üres — próbáld újra később, vagy jelezd az üzemeltetőnek.</p>
|
||
{{else if eq .OffsiteStoreState "no-target"}}
|
||
<p class="form-hint">A távoli tároló kapcsolódási adatai még nem érkeztek meg ehhez a géphez, ezért még nem tudjuk megmutatni, mi van benne. Ez magától rendeződik.</p>
|
||
{{end}}
|
||
{{if .OffsiteRestoreRows}}
|
||
<div class="app-row-list">
|
||
{{range .OffsiteRestoreRows}}
|
||
{{template "app_list_row" dict "Slug" .Slug "Name" .DisplayName}}
|
||
{{if .Restorable}}
|
||
{{if and .InStore (not .Installed)}}
|
||
<span class="form-hint" style="margin-right:.5rem">Nincs telepítve — a visszaállítás előbb újratelepíti.</span>
|
||
{{end}}
|
||
{{if .StoreUnknown}}
|
||
<span class="form-hint" style="margin-right:.5rem">Nem tudjuk, van-e mentése — a tárolót nem sikerült elolvasni.</span>
|
||
{{end}}
|
||
<!-- R-48: ONE entry per app. The five inline forms that used to live here — verify,
|
||
prepare, the revealed commit, the missing-only merge and the true reconstitution —
|
||
were separable only by layout, and two of them differed by whether the customer's
|
||
data comes back at all. They are now described intents inside the wizard. -->
|
||
<a href="/backups/restore/app?name={{.App}}" class="btn btn-xs btn-outline">Visszaállítás…</a>
|
||
{{else}}
|
||
<!-- Shown, not hidden: "installed but nothing in the store" is an answer. Silently
|
||
dropping the row is what made R-220's empty list unreadable, one screen over. -->
|
||
<span class="form-hint">Nincs mentése a távoli tárolóban — nincs mit visszaállítani.</span>
|
||
{{end}}
|
||
{{template "app_list_row_end"}}
|
||
{{end}}
|
||
</div>
|
||
{{else if eq .OffsiteStoreState "known"}}
|
||
<p class="form-hint">A távoli tároló üres — nincs mit visszaállítani.</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(); }
|
||
});
|
||
});
|
||
}
|
||
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}}
|