feat(backup): async restore family — no proxy-timeout error page on a succeeding restore (v0.102.0)

Re-adjudicates F4: /backup/restore, /backup/tier2/restore, /backup/offbox/restore
blocked the HTTP request until completion, so through cloudflared's 100s cap a
customer got an error page while the restore succeeded (offbox worse — bounded
on r.Context(), canceling the SFTP restore mid-flight). Convert all three to the
offboxRun async shape: fast-path IsRunning refuse, background goroutine
(offbox ctx off r.Context() -> Background+30m), instant redirect. Add mutex-
guarded op-status (opstatus.go) + GET /api/backup/restore-status + a 3s-polling
backups.html banner (neutral running, red on failure). Restore single-flight
unchanged. Tests + red-proof (sync handler blocks indefinitely vs <500ms async).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-06 20:23:49 +02:00
parent 9d5a588ca3
commit c529a455af
9 changed files with 429 additions and 41 deletions
@@ -13,6 +13,10 @@
<div class="flash flash-error">{{.Backup.FlashError}}</div>
{{end}}{{end}}
<!-- 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>
{{if not .Backup}}
<div class="backup-empty-state">
<div class="backup-empty-icon">&#128737;</div>
@@ -597,6 +601,43 @@
{{end}}
<script>
// 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' ? 'NAS-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;
}
if (st.last && sawRunning) {
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);
})();
function toggleBackupDetail(header) {
var detail = header.nextElementSibling;
var icon = header.querySelector('.expand-icon');