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:
@@ -1,5 +1,29 @@
|
||||
## Changelog
|
||||
|
||||
### v0.102.0 — async restore family: no more proxy-timeout error page on a succeeding restore (2026-07-06)
|
||||
|
||||
Re-adjudicates campaign **F4** (`felhom.eu/documentation/audits/RERUN-p1p3-2026-07-06.md`): all three
|
||||
restore surfaces (`/backup/restore`, `/backup/tier2/restore`, `/backup/offbox/restore`) blocked the
|
||||
HTTP request until completion. Through cloudflared's hard 100s cap + traefik, a real customer got an
|
||||
**error page while the restore silently succeeded**; the off-box one was worse — it bounded on
|
||||
`r.Context()`, so a proxy read-timeout **canceled the SFTP restore mid-flight**.
|
||||
|
||||
- **Async family** (mirrors the existing `offboxRunHandler` shape): each handler fast-path refuses a
|
||||
concurrent op (`IsRunning()` → "Egy mentési/visszaállítási művelet már fut."), then runs the restore
|
||||
in a **background goroutine** and redirects immediately with a "Visszaállítás elindult…" flash. The
|
||||
offbox restore's context moved from `r.Context()` to `context.Background()+30m` (fixes the mid-flight
|
||||
cancel). The restore functions' internal single-flight acquire is unchanged.
|
||||
- **Op-status surface** (`internal/backup/opstatus.go`): mutex-guarded in-memory current-op + terminal
|
||||
`last{op,stack,ok,message,finished_at}`, deep-copy getter; new `GET /api/backup/restore-status`
|
||||
(distinct from `/backup/status`, which proxies the agent's PBS status). In-memory, lost on restart
|
||||
(same precedent as notification cooldowns).
|
||||
- **UI** (`backups.html`): a progress banner polls the status every 3s — neutral while running (shown
|
||||
even on a fresh page load mid-op), success on completion, red **only** on failure.
|
||||
- Tests: `opstatus_test.go` (begin→running→terminal, deep-copy, failure); `async_restore_test.go`
|
||||
(handler returns <500ms while the restore parks in a blocking provider + op-status transitions;
|
||||
double-click refused with no second launch). Red-proof: the pre-fix synchronous handler blocks the
|
||||
request indefinitely (test killed at 30s) vs <500ms async.
|
||||
|
||||
### v0.101.0 — campaign findings F3 (sync deadline) + F2 evidence gap (agent refusal surfacing) (2026-07-06)
|
||||
|
||||
From the no-mercy campaign (`felhom.eu/documentation/audits/CAMPAIGN-nomercy-2026-07-06.md`).
|
||||
|
||||
Reference in New Issue
Block a user