controller v0.269.0: whole restore from the second drive; crash loops stopped; exact image digests; steps judged by their own .felhom.yml (decisions 26-28, R-661 R-666 R-667 R-668 R-664 R-665 R-662, 09 6.4 part 6)
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
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:
@@ -1685,17 +1685,15 @@ func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// customer gets the route that CAN return their files instead of a success message over an app
|
||||
// listing photos it cannot open.
|
||||
//
|
||||
// `accept_missing_files=1` is the explicit, separately-worded second step („csak az adatbázist és
|
||||
// a beállításokat"). It is deliberately not a sibling of the main button: two controls whose
|
||||
// difference is "your data comes back" are never siblings (R-48).
|
||||
acceptMissingFiles := r.FormValue("accept_missing_files") == "1"
|
||||
if !acceptMissingFiles {
|
||||
if legs := s.backupMgr.DeclaredDriveFileLegs(stackName); len(legs) > 0 {
|
||||
msg := s.missingFileLegsRefusal(r.Context(), stackName)
|
||||
s.logger.Printf("[WARN] [web] restore refused for %s: unit carries no file leg (%d drive path(s))", stackName, len(legs))
|
||||
http.Redirect(w, r, "/backups/restore?flash_error="+url.QueryEscape(msg), http.StatusFound)
|
||||
return
|
||||
}
|
||||
// R-662 (v0.269.0): the „csak az adatbázist és a beállításokat" second step that used to be read here
|
||||
// (`accept_missing_files=1`) is REMOVED. No page ever sent it, the backup layer refused it anyway, and
|
||||
// it is the database-only restore under existing files that `09` §3 decision 25 ruled out (option B).
|
||||
// The way back for such an app is the second drive's whole restore (decision 26) or the off-site one.
|
||||
if legs := s.backupMgr.DeclaredDriveFileLegs(stackName); len(legs) > 0 {
|
||||
msg := s.missingFileLegsRefusal(r.Context(), stackName)
|
||||
s.logger.Printf("[WARN] [web] restore refused for %s: unit carries no file leg (%d drive path(s))", stackName, len(legs))
|
||||
http.Redirect(w, r, "/backups/restore?flash_error="+url.QueryEscape(msg), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[WARN] [web] Restore requested (async): stack=%s, snapshot=%s from %s", stackName, snapshotID, r.RemoteAddr)
|
||||
@@ -2058,6 +2056,30 @@ func (s *Server) backupTier2UnitRestoreHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
// v0.269.0 (`09` §3 decision 26, R-661): for an app whose files live on the drive, this button is the
|
||||
// WHOLE restore — files by the four rules, then the unit. The unit-only restore below would refuse it
|
||||
// (R-538), which is what left such an app with no way back from the second drive.
|
||||
if s.backupMgr.HasDriveFileLegs(stackName) {
|
||||
s.logger.Printf("[WARN] [web] Tier-2 WHOLE restore requested (async, files + database): stack=%s from %s", stackName, r.RemoteAddr)
|
||||
s.backupMgr.BeginRestoreOp("tier2-whole-restore", stackName)
|
||||
go func() {
|
||||
start := time.Now()
|
||||
res, err := s.backupMgr.RestoreTier2Whole(stackName)
|
||||
files := s.note("note.restore.whole_files", res.Files.Restored, res.Files.Replaced, res.Files.KeptNewer, res.Files.Unchanged)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Tier-2 whole restore failed (async): stack=%s: %v", stackName, err)
|
||||
s.backupMgr.EndRestoreOp(false, s.note("note.restore.full_unit_failed", s.noteErr(err))+" "+files)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] Tier-2 whole restore completed (async): stack=%s in %s (files restored %d, replaced %d, kept-newer %d, unchanged %d; volumes %d/%d, dbs %d/%d)",
|
||||
stackName, time.Since(start), res.Files.Restored, res.Files.Replaced, res.Files.KeptNewer, res.Files.Unchanged,
|
||||
res.Unit.VolumesReplayed, res.Unit.ManifestVolumes, res.Unit.DBsReplayed, res.Unit.ManifestDBs)
|
||||
s.backupMgr.EndRestoreOp(true, s.unitRestoreOutcomeMsg(stackName, res.Unit)+" "+files)
|
||||
}()
|
||||
http.Redirect(w, r, "/backups/apps?"+flashQuery("flash", "flash.restore.full_started"), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Pre-flight, before any op is begun and before the app is stopped: does this copy hold a unit
|
||||
// this restore can actually open? Only the no-unit case is pre-flighted; every other refusal keeps
|
||||
// its existing async path, so this cannot alter behaviour anywhere else.
|
||||
@@ -2659,6 +2681,7 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
|
||||
// v0.264.0: the update outcomes (default ON). A type the page cannot render is a type every
|
||||
// Save drops — so they are listed here AND carry a checkbox.
|
||||
"app_update_undone", "app_update_held",
|
||||
"app_stopped_unhealthy", // v0.269.0, decision 28
|
||||
"storage_reconnected", "health_recovered",
|
||||
} {
|
||||
if r.FormValue("event_"+evt) == "on" {
|
||||
|
||||
Reference in New Issue
Block a user