R-729 + R-545: the household can remove its own off-site target („Távoli mentési cél törlése")

POST /backup/offbox/clear (reveal-then-confirm) forgets the target and deletes ssh_key + known_hosts;
nothing on the target is touched. repo_password is kept whenever anything could depend on it (hub
sealed package — the R-241 rule; escrowed; a successful run; snapshots) and deleted only otherwise.
Refused for the Felhom tier (rclone-pinned), while the single-flight is held, and while an
abandonment countdown runs. i18n parity fixtures of backups_remote gain the additive block only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 21:24:28 +02:00
parent 6f1ba1fe43
commit 27b373b93c
23 changed files with 612 additions and 0 deletions
@@ -331,6 +331,49 @@ func (s *Server) offboxResetHandler(w http.ResponseWriter, r *http.Request) {
offboxRedirect(w, r, "flash.offbox.restart_started", false)
}
// offboxClearHandler is the household's „Távoli mentési cél törlése" press (R-729 / R-545): forget the
// target and delete its transport secrets, never the repository. Reveal-then-confirm like the reset
// (confirm=1). The refusals and what is kept are backup.ClearOffboxTarget's; this maps them to words.
func (s *Server) offboxClearHandler(w http.ResponseWriter, r *http.Request) {
if s.backupMgr == nil {
offboxRedirect(w, r, "flash.offbox.mgr_unavailable", true)
return
}
_ = r.ParseForm()
if r.FormValue("confirm") != "1" {
offboxRedirect(w, r, "flash.offbox.clear_needs_confirmation", true)
return
}
res, err := s.backupMgr.ClearOffboxTarget()
switch {
case errors.Is(err, backup.ErrOffboxClearNoTarget):
offboxRedirect(w, r, "flash.offbox.target_not_set", true)
return
case errors.Is(err, backup.ErrOffboxClearHubTier):
offboxRedirect(w, r, "flash.offbox.clear_hub_tier", true)
return
case errors.Is(err, backup.ErrOffboxClearBusy):
offboxRedirect(w, r, "flash.offbox.clear_busy", true)
return
case errors.Is(err, backup.ErrOffboxClearAbandonOn):
offboxRedirect(w, r, "flash.offbox.clear_abandon_pending", true)
return
case err != nil:
s.logger.Printf("[ERROR] [web] off-site target clear failed: %v", err)
offboxRedirect(w, r, "flash.offbox.save_failed", true)
return
}
if !res.KeptRepoPassword {
// The key is gone, so a copy still staged on the agent for an unfinished ceremony is a stale
// secret with nothing behind it. Best-effort, like the confirm path's wipe.
if werr := s.wipeStagedEscrow(r.Context()); werr != nil {
s.logger.Printf("[WARN] [web] off-site target cleared, but the agent-staged escrow copy was not wiped: %v", werr)
}
}
s.reportTriggerNow()
offboxRedirect(w, r, "flash.offbox.target_cleared", false)
}
// offboxStatusHandler (Part C) is the poll source for the remote-backup run status — the page polls it
// after "Távoli mentés most" and flips to the terminal state without a manual reload. Session-auth'd.
func (s *Server) offboxStatusHandler(w http.ResponseWriter, r *http.Request) {