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.
68 lines
3.0 KiB
HTML
68 lines
3.0 KiB
HTML
{{define "backups_flash"}}
|
|
{{if .Backup}}{{if .Backup.FlashSuccess}}
|
|
<div class="flash flash-success">{{.Backup.FlashSuccess}}</div>
|
|
{{end}}{{end}}
|
|
{{if .Backup}}{{if .Backup.FlashError}}
|
|
<div class="flash flash-error">{{.Backup.FlashError}}</div>
|
|
{{end}}{{end}}
|
|
{{end}}
|
|
|
|
{{define "backups_empty"}}
|
|
<div class="backup-empty-state">
|
|
<div class="backup-empty-icon">🛡</div>
|
|
<h3>Biztonsági mentés nincs beállítva</h3>
|
|
<p>A biztonsági mentés funkció nem aktív.<br>
|
|
Kérjük, vegye fel a kapcsolatot a Felhom csapattal a beállításhoz.</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{define "restore_banner"}}
|
|
<!-- Part B: async restore progress banner — polls /api/backup/restore-status; neutral while running,
|
|
red only on failure (exception-color principle). Hidden until a restore op is seen. -->
|
|
<div id="restore-banner" class="flash" style="display:none"></div>
|
|
{{end}}
|
|
|
|
{{define "restore_banner_js"}}
|
|
// Part B: restore-progress banner. Polls the async restore op-status every 3s. Shows a neutral
|
|
// "in progress" while running (including on a fresh page load mid-op), success on completion, and the
|
|
// error state ONLY on failure. Stops polling when idle after a terminal result was shown.
|
|
(function(){
|
|
var banner = document.getElementById('restore-banner');
|
|
if (!banner) return;
|
|
var sawRunning = false;
|
|
function opLabel(op){ return op === 'tier2-restore' ? 'Fájl-visszaállítás'
|
|
: op === 'offbox-restore' ? 'Távoli visszaállítás' : 'Visszaállítás'; }
|
|
function render(st){
|
|
if (st.running) {
|
|
sawRunning = true;
|
|
banner.className = 'flash';
|
|
banner.style.display = 'block';
|
|
banner.textContent = opLabel(st.op) + ' folyamatban' + (st.stack ? ': ' + st.stack : '') + '…';
|
|
return;
|
|
}
|
|
// R-351: `sawRunning` alone showed a terminal result ONLY to a page that watched the op
|
|
// happen. A restore that finished before this page was opened — or inside one poll interval
|
|
// — was shown to nobody, which is how a completed restore became unknowable from any screen.
|
|
// `last_recent` is the server's verdict, using the one window in internal/backup.
|
|
if (st.last && (sawRunning || st.last_recent)) {
|
|
banner.style.display = 'block';
|
|
if (st.last.ok) {
|
|
banner.className = 'flash flash-success';
|
|
banner.textContent = st.last.message || (opLabel(st.last.op) + ' kész.');
|
|
} else {
|
|
banner.className = 'flash flash-error';
|
|
banner.textContent = st.last.message || (opLabel(st.last.op) + ' sikertelen.');
|
|
}
|
|
}
|
|
}
|
|
function poll(){
|
|
fetch('/api/backup/restore-status', {headers: {'Accept':'application/json'}})
|
|
.then(function(r){ return r.json(); })
|
|
.then(function(j){ if (j && j.data) render(j.data); })
|
|
.catch(function(){});
|
|
}
|
|
poll();
|
|
setInterval(poll, 3000);
|
|
})();
|
|
{{end}}
|