985388c6e9
gates / gates (push) Successful in 10s
Part 3 (not droppable) and the engine half of Part 2. No version bump yet - one bump and
one bake at the end of the session.
PART 3a - a second press really did start a second run. Established with a test BEFORE any
change: both offboxReconstituteHandler and offboxPlaceHandler answered "...elindult" and
overwrote the first restore's op/stack. Cause: every restore handler gated on
backupMgr.IsRunning() - the CONCURRENCY flag, which the restore goroutine acquires AFTER the
handler returns (offbox_reconstitute.go:180, offbox_restore.go:393). Seven sites. The wizard
had read the correct flag since v0.154.0 and said so in a comment; the handlers never moved.
New Server.restoreOpBlocked() reads BOTH flags - the display flag covers the whole off-box
restore, the concurrency flag is the only one the nightly backup holds - and the refusal now
names the running app and a route.
PART 3b - the page DOES refresh; the defect was the RESULT. backups_shared.html gated the
terminal result on a page-local sawRunning flag, so a restore that finished before the page
was opened, or inside one 3s poll, was shown to nobody. The 2026-08-21 OpenGist restore took
8.666s and no screen ever said it completed - the answer existed only in docker logs.
RestoreOpStatus.LastRecent now carries the server's verdict. The 10-minute window moved to
internal/backup as RestoreResultWindow and internal/web's constant is an alias: one
expression, two surfaces. Also removed the wizard's self-contradiction, which said the state
refreshes automatically AND that you must refresh the page.
PART 2 (engine) - every recovery unit manifest has carried drive and namespace_root since
schema 1, and NO non-test code read either back. The reconstitution opened the manifest and
took only the coherence stamp, then resolved its destination from the live app. A restore
into a different destination succeeded silently under a green message. New
backup/offbox_placement.go: CheckPlacement (pure, total), PlacementMismatchMessage,
recordedPlacementFromScratch. Compared before the safety dump and before the first byte.
A mismatch is NAMED and refused; ackPlacementChange lets the customer proceed deliberately -
a separate field from confirm=1, because one click must not carry two decisions. An UNKNOWN
recording is never a mismatch: refusing on an absence would strand every pre-field unit.
The not-installed refusal (R-253) now names the drive the backup recorded.
RED-PROOFS, each mutation asserted applied and reverted to 0:
B both guards removed (count asserted 2) -> the restore WAS seen starting with no drive
attached: no error, full 3.00s run, wrote into /tmp/mutant-destination
C Mismatch forced false -> the silent divergent restore returned
E Known() forced true -> the fabricated empty prefill appeared
D Mismatch forced true -> 8 ordinary reconstitute tests broke, proving reachability both ways
Note on D: the existing fixtures write a schema-1 manifest with NO drive, so they are
scenario-E shaped. The matching case is covered in the scenario table, not by them.
Gates 11/11 OK. Suite 28 packages ok. Hungarian verified as hex, no BOM, no mojibake sentinels.
NOT in this commit, still open: Part 2's scenario-A prefill UI, Part 1's deploy-page
visibility line, Part 1's specification document, Part 4's measurement.
180 lines
11 KiB
HTML
180 lines
11 KiB
HTML
{{define "backups_restore_wizard"}}
|
|
{{template "layout_start" .}}
|
|
|
|
<!-- R-48: the offsite restore wizard. One entry per app on /backups/restore leads here, and the
|
|
three intents are separately-described CARDS with a consequence sentence each — never sibling
|
|
buttons whose difference is whether the data comes back. Server-rendered throughout: every step
|
|
is a real form POST to the EXISTING endpoint, so the page works with JavaScript disabled. -->
|
|
|
|
<div class="page-header">
|
|
<div style="display:flex;align-items:center;gap:.5rem">
|
|
<a href="/backups/restore" class="btn btn-sm btn-outline">← Vissza</a>
|
|
<h2>Visszaállítás — {{.AppDisplayName}}</h2>
|
|
</div>
|
|
<span class="domain-badge">{{.Domain}}</span>
|
|
</div>
|
|
|
|
{{template "backups_flash" .}}
|
|
{{template "restore_banner" .}}
|
|
|
|
<!-- Phase strip: the customer can see there IS a sequence, and where they are in it. The round-2
|
|
incident's second half was that the decisive step appeared only after the first was pressed,
|
|
with nothing signposting that a second step existed at all. -->
|
|
{{$phase := printf "%s" .Wizard.Phase}}
|
|
<div class="restore-wizard-phases">
|
|
<span class="restore-wizard-phase{{if eq $phase "elokeszites"}} is-current{{end}}">Előkészítés</span>
|
|
<span class="restore-wizard-phase{{if eq $phase "megerosites"}} is-current{{end}}">Megerősítés</span>
|
|
<span class="restore-wizard-phase{{if eq $phase "vegrehajtas"}} is-current{{end}}">Végrehajtás</span>
|
|
<span class="restore-wizard-phase{{if eq $phase "eredmeny"}} is-current{{end}}">Eredmény</span>
|
|
</div>
|
|
|
|
{{with .LastResult}}
|
|
<!-- „Eredmény": the outcome of the restore that just finished, for THIS app. The redirect flash says
|
|
the same thing but does not survive a reload; this does, for restoreResultWindow. -->
|
|
<div class="settings-card">
|
|
<h3>Eredmény</h3>
|
|
<div class="alert {{if .OK}}alert-info{{else}}alert-error{{end}}">{{.Message}}</div>
|
|
<p class="form-hint">Befejezve: {{fmtTime .FinishedAt}}. Ha szeretnéd, alább újra indíthatsz egy visszaállítást.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if eq (printf "%s" .Wizard.Step) "execution"}}
|
|
<!-- EXECUTION — every mutation form is suppressed server-side. The manager's single-flight would
|
|
refuse them anyway; offering a control guaranteed to fail is the same dishonesty class R-48
|
|
addresses. The live progress comes from the existing restore-status poll (the banner above). -->
|
|
<div class="settings-card">
|
|
<h3>Végrehajtás</h3>
|
|
<p>Jelenleg egy mentési vagy visszaállítási művelet fut{{with .RunningStack}} ({{.}}){{end}}. Amíg ez tart, új visszaállítás nem indítható.</p>
|
|
<!-- R-351: this used to say the state refreshes automatically AND that you must refresh the page
|
|
when it finishes. Both cannot be true, and the second half was the one people believed. The
|
|
banner now carries the terminal result too (RestoreOpStatus.LastRecent), so the sentence can
|
|
describe what the page actually does. -->
|
|
<p class="form-hint">Az állapot fent automatikusan frissül, és a művelet eredménye is ott jelenik meg, amint elkészült.</p>
|
|
<div class="form-actions">
|
|
<a href="/backups/restore/app?name={{.App}}" class="btn btn-sm btn-outline">Állapot frissítése</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{else if eq (printf "%s" .Wizard.Step) "prepare-confirm"}}
|
|
<!-- MEGERŐSÍTÉS — the size gate. The preparation has measured what needs downloading; the customer
|
|
confirms with the size in front of them, before any transfer starts. -->
|
|
<div class="settings-card">
|
|
<h3>Megerősítés — teljes visszaállítás előkészítése</h3>
|
|
<p>A teljes visszaállításhoz a mentés teljes tartalmát le kell tölteni a távoli tárolóból{{with .FullPrepSize}} — a becsült méret: <strong>{{.}}</strong>{{end}}. A letöltés a meghajtón egy külön előkészítő mappába kerül; az élő adataid ebben a lépésben még nem változnak.</p>
|
|
<p class="form-hint">A letöltés a mérettől és a kapcsolat sebességétől függően hosszabb ideig is tarthat. Ha elkészült, ezen az oldalon választhatod ki, hogy csak a hiányzó fájlokat hozod vissza, vagy teljes visszaállítást kérsz.</p>
|
|
<div class="form-actions">
|
|
<form method="POST" action="/backup/offbox/restore">{{.CSRFField}}
|
|
<input type="hidden" name="app" value="{{.App}}">
|
|
<input type="hidden" name="mode" value="full">
|
|
<input type="hidden" name="confirm" value="1">
|
|
<button type="submit" class="btn btn-primary">Előkészítés indítása{{with .FullPrepSize}} (~{{.}}){{end}}</button>
|
|
</form>
|
|
<a href="/backups/restore/app?name={{.App}}" class="btn btn-outline">Mégsem</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{else}}
|
|
<!-- INTENT — three cards, each with its own consequence sentence. Card order is deliberate:
|
|
harmless first, irreversible-looking last. -->
|
|
|
|
<div class="settings-card">
|
|
<h3>1. Ellenőrzés külön mappába (beállítások és adatbázis)</h3>
|
|
<!-- R-204 item 3: this card's scope is stated BEFORE the choice, not only in the outcome. It
|
|
restores the recovery unit only; the customer's own files stay in the backup. Saying „a
|
|
mentés tartalma" here was how a disaster-recovery customer chose the one intent that does
|
|
not return their documents. -->
|
|
<p>Az alkalmazás beállításait és adatbázisát hozza vissza egy külön ellenőrző mappába. A saját fájljaidat (dokumentumok, képek, feltöltések) <strong>nem</strong> hozza vissza — azokhoz a 3. pont teljes visszaállítása kell. Az élő adataid nem változnak.</p>
|
|
<div class="form-actions">
|
|
<form method="POST" action="/backup/offbox/restore">{{.CSRFField}}
|
|
<input type="hidden" name="app" value="{{.App}}">
|
|
<input type="hidden" name="mode" value="unit">
|
|
<button type="submit" class="btn btn-outline"{{if not .Wizard.VerifyEnabled}} disabled{{end}}>Ellenőrzés indítása</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="settings-card">
|
|
<h3>2. Hiányzó fájlok visszahozása</h3>
|
|
<p>Csak a hiányzó fájlokat másolja vissza a meglévők közé. 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.</p>
|
|
{{if .Wizard.PlaceEnabled}}
|
|
<div class="form-actions">
|
|
<form method="POST" action="/backup/offbox/place">{{.CSRFField}}
|
|
<input type="hidden" name="app" value="{{.App}}">
|
|
<button type="submit" class="btn btn-outline">Hiányzó fájlok visszahozása</button>
|
|
</form>
|
|
</div>
|
|
{{else}}
|
|
<p class="form-hint">Ehhez előbb elő kell készíteni a teljes mentést — lásd a 3. pontot.</p>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="settings-card restore-danger-card">
|
|
<h3>3. Teljes visszaállítás (fájlok + adatbázis)</h3>
|
|
<p>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.</p>
|
|
|
|
{{if .Wizard.RestoreEnabled}}
|
|
<!-- Pair honesty (R-43): what the two halves of this restore actually ARE. A restore is the one
|
|
operation whose result cannot be inspected before committing to it. -->
|
|
<div class="alert alert-info" style="margin-bottom:.75rem">
|
|
<strong>Az előkészített mentés:</strong>
|
|
{{if not .Pair.DumpsAt.IsZero}}adatbázis-mentés ideje: {{fmtTime .Pair.DumpsAt}}.{{else}}az adatbázis-mentés ideje nem állapítható meg.{{end}}
|
|
</div>
|
|
{{if .Pair.Skewed}}
|
|
<div class="alert alert-warning" style="margin-bottom:.75rem">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.</div>
|
|
{{end}}
|
|
{{if .Pair.LooksEmpty}}
|
|
<div class="alert alert-warning" style="margin-bottom:.75rem">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.</div>
|
|
{{end}}
|
|
<div class="form-actions">
|
|
<form method="POST" action="/backup/offbox/reconstitute">{{.CSRFField}}
|
|
<input type="hidden" name="app" value="{{.App}}">
|
|
<input type="hidden" name="confirm" value="1">
|
|
<button type="button" class="btn btn-danger"
|
|
data-restore-app="{{.App}}"
|
|
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 indítása</button>
|
|
</form>
|
|
</div>
|
|
{{else}}
|
|
<p class="form-hint">A teljes visszaállításhoz először le kell tölteni a mentés teljes tartalmát. Az előkészítés megmutatja a méretet, mielőtt bármi elindulna — az élő adataid az előkészítés alatt nem változnak.</p>
|
|
<div class="form-actions">
|
|
<form method="POST" action="/backup/offbox/restore">{{.CSRFField}}
|
|
<input type="hidden" name="app" value="{{.App}}">
|
|
<input type="hidden" name="mode" value="full">
|
|
<button type="submit" class="btn btn-outline"{{if not .Wizard.PrepareEnabled}} disabled{{end}}>Teljes visszaállítás előkészítése</button>
|
|
</form>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<script>
|
|
{{template "restore_banner_js"}}
|
|
|
|
/* Carried VERBATIM from backups_restore.html (R-43). The double-confirm is the good part of the old
|
|
surface — it states the DB half's age and any warning before the customer commits — so it moves
|
|
with the action rather than being rewritten. felhomConfirm is the house inline idiom, never the
|
|
OS-modal confirm() (F-11). */
|
|
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(); }
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
|
|
{{template "layout_end" .}}
|
|
{{end}}
|