# REPORT — v0.102.0: async restore family (no proxy-timeout error page on a succeeding restore) **Date:** 2026-07-06 · **Class:** implementation (code + tests + deploy + live acceptance). **Baseline:** `main` @ `9d5a588` (v0.101.0) → `c529a45`. Part B of the pilot-blockers bundle (A = agent v0.73.0 F2, C = catalog healthcheck sweep). Provenance: RERUN F4 re-adjudication. ## The bug (F4 re-adjudicated) All three restore surfaces blocked the HTTP request until completion: `/backup/restore` (→ `RestoreFromRecoveryUnit`), `/backup/tier2/restore` (→ `RestoreTier2Files`), `/backup/offbox/restore` (→ `RestoreOffbox`). Through cloudflared's hard 100s cap + traefik a real customer got an **error page while the restore silently succeeded**; the off-box handler was worse — it bounded on `r.Context()`, so a proxy read-timeout **canceled the SFTP restore mid-flight**. ## The fix - **Async family** (mirrors the live `offboxRunHandler`): each handler fast-path refuses a concurrent op (`IsRunning()` → "Egy mentési/visszaállítási művelet már fut."), runs the restore in a **background goroutine**, and redirects immediately with "Visszaállítás elindult…". 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** (`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 PBS status). In-memory (lost on restart). - **UI** (`backups.html`): a banner polls the status every 3s — neutral running (shown even on a fresh page load mid-op), success on completion, red only on failure. ## Tests + red-proof `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: reverting the handler to synchronous blocks the request indefinitely (killed at 30s) vs <500ms async. Full gate `go build/vet/test ./...` = PASS. ## Deploy + live acceptance Built + pushed `:0.102.0` on 180, deployed to guest 9201 (golden/bootstrap), healthy; `GET /api/backup/restore-status` live. **Restore drill (vaultwarden):** planted canary → backup → vandalized → `POST /backup/restore` returned in **0.018s** (302, "elindult" flash); status transitioned `running:true → false` with terminal `{op:restore, ok:true}`; **canary restored bit-identical**. **External path (F4):** `POST https://felhom.demo-felhom.eu/backup/restore` (through cloudflared) returned in **0.235s** with 302 — no proxy error. **Tier-2:** `POST /backup/tier2/restore` async 0.007s; its failure landed in the terminal status (B3 failure-visibility). Double-click refusal covered by the unit test. ## Not changed The restore functions' internal single-flight; `/api/backup/status` (agent-PBS) semantics; the restore-button CSRF/form mechanism (only the handler tail went async); no op-status persistence (documented).