v0.297.0: burn-down round 2 — 24 small rows (R-591 R-568 R-567 R-363 R-547 R-10 R-552 R-251 R-104 R-619 R-362 R-675 R-256 R-257 R-240 R-365 R-425 R-565 R-564 R-603 R-454 R-208 R-457-swept) + the banner countdown and deepCopyStack twins; MinAgent 0.131.0
gates / gates (push) Failing after 50s

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 20:12:56 +02:00
parent 114ff2761a
commit 1453cfc69b
68 changed files with 3953 additions and 116 deletions
+38 -15
View File
@@ -1165,6 +1165,11 @@ func (s *Server) backupsRemoteHandler(w http.ResponseWriter, r *http.Request) {
data["AbandonActive"] = true
data["AbandonDaysLeft"] = st.DaysLeft
data["AbandonDate"] = st.DueAt.Format("2006-01-02")
// R-365: DaysLeft is a ceiling, so 0 means the due moment has PASSED (the terminal step waits
// for the next daily sweep, up to ~29 h). The future-tense „… napján véglegesen töröljük" then
// named a date in the past; the overdue sentence says the deletion is due and when it runs.
// Pinned by TestR365_OverdueCountdownIsNotFutureTense.
data["AbandonOverdue"] = st.DaysLeft <= 0
// R-302: this block makes the SAME retrieval promise as the banner, under a different verb
// („visszaszerezhetők" vs the banner's „visszaszerezheted"), which is why it was a fourth
// instance nobody had counted. Same single derivation — fixing one surface and not the other
@@ -1727,25 +1732,43 @@ func (s *Server) tier2DestLabel(destPath, systemDataPath, lang string) string {
return filepath.Base(strings.TrimSuffix(destPath, "/"+backup.FelhomDataDir))
}
// missingFileLegsRefusal builds the Hungarian sentence shown when a unit restore is refused because
// the unit carries no copy of the app's files (R-538). It names the route that CAN return them, and
// when there is none it says so rather than implying one exists.
// missingFileLegsRefusal builds the sentence shown when a unit restore is refused because the unit
// carries no copy of the app's files (R-538). It names the route that CAN return them, and when there
// is none it says so rather than implying one exists.
//
// The three branches are the three real states, in the order a customer can act on them: the off-site
// copy (a full restore brings files AND database), the second drive (its file half is its own
// action), and nothing.
func (s *Server) missingFileLegsRefusal(ctx context.Context, stackName string) string {
const head = "Ez a mentés nem tartalmazza az alkalmazás fájljait, ezért nem állítjuk vissza az adatbázist föléjük — a fájlok így a helyükön maradnak. "
// The branches are the real states, in the order a customer can act on them: the off-site copy (a full
// restore brings files AND database), the second drive's WHOLE copy (decision 26, v0.269.0 — the
// „Teljes visszaállítás a másolatból" button brings files AND database back), the second drive's file
// copy alone (its file half is its own action), and nothing. R-675: the whole-copy branch was missing,
// so a household with a whole copy was sent to the file-only action. Rendered in the reader's language.
func (s *Server) missingFileLegsRefusal(ctx context.Context, lang, stackName string) string {
offsite, whole, files := false, false, false
if s.backupMgr != nil {
rows, _ := s.offsiteRestoreRows(ctx)
if row := resolveOffsiteRestoreApp(rows, stackName); row != nil {
return head + "A fájlok a távoli másolatból állíthatók vissza: Biztonsági mentés → Visszaállítás, „Teljes visszaállítás (fájlok + adatbázis)”."
}
if cov, err := s.backupMgr.Tier2RestoreCoverage(stackName); err == nil && cov.CanRestore() {
return head + "A fájlok a második meghajtó másolatából állíthatók vissza: „Fájlok visszaállítása”."
offsite = resolveOffsiteRestoreApp(rows, stackName) != nil
if !offsite {
whole = s.backupMgr.WholeOnTier(stackName, backup.UpdateTierSecondDrive)
if cov, err := s.backupMgr.Tier2RestoreCoverage(stackName); err == nil && cov.CanRestore() {
files = true
}
}
}
return head + "Ezekről a fájlokról jelenleg nincs másolat — kapcsold be a távoli mentést, vagy csatlakoztass egy második meghajtót."
return s.msgLang(lang, "restore.refuse.files.head") + s.msgLang(lang, missingFileLegsRouteKey(offsite, whole, files))
}
// missingFileLegsRouteKey picks the route sentence for missingFileLegsRefusal (R-675). Pure, so every
// branch is pinned by TestR675_RefusalNamesTheWholeCopy.
func missingFileLegsRouteKey(offsite, wholeOnSecondDrive, filesOnSecondDrive bool) string {
switch {
case offsite:
return "restore.refuse.files.offsite"
case wholeOnSecondDrive:
return "restore.refuse.files.second_drive_whole"
case filesOnSecondDrive:
return "restore.refuse.files.second_drive"
default:
return "restore.refuse.files.none"
}
}
func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
@@ -1791,7 +1814,7 @@ func (s *Server) backupRestoreHandler(w http.ResponseWriter, r *http.Request) {
// 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)
msg := s.missingFileLegsRefusal(r.Context(), s.langFor(r), 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