controller v0.296.0: a backup run cut off by a power cut or restart is said on the backup pages (R-519); the whole-system backup text states today's measurement (R-518)
gates / gates (push) Successful in 30s

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 12:14:54 +02:00
parent 477e2548db
commit ff69074514
21 changed files with 2700 additions and 21 deletions
+24 -5
View File
@@ -299,6 +299,9 @@ type Manager struct {
// R-550: persistence of the above (restore_record.go) and the per-app interrupted notices.
opRecordPath string
opInterrupted map[string]RestoreOpResult
// R-519: the app-data run record (run_record.go) and the start of the last run a stop cut off (zero = none).
runRecordPath string
runInterrupted time.Time
// Cached status for page rendering (refreshed periodically)
cachedStatus *FullBackupStatus
@@ -311,9 +314,13 @@ type FullBackupStatus struct {
Running bool
// DB Dumps
LastDBDump *DBDumpStatus
DumpFiles []DumpFileInfo
DiscoveredDBs []DiscoveredDB
LastDBDump *DBDumpStatus
// InterruptedRunAt (R-519): the start of the last app-data run a power cut or a restart cut off; zero = the
// newest run finished. Shown on /backups and /backups/apps until a run ends with every step OK.
InterruptedRunAt time.Time
Interrupted bool // InterruptedRunAt is set (the template gate: a time.Time is always "true" to `if`)
DumpFiles []DumpFileInfo
DiscoveredDBs []DiscoveredDB
// Schedule
DBDumpSchedule string
@@ -548,6 +555,11 @@ func (m *Manager) beginOffsiteRunStamp(runID string) func() {
func (m *Manager) runDBDumpsInternal(ctx context.Context) error {
start := time.Now()
m.logger.Printf("[INFO] [backup] Starting database dump run")
// R-519: the run is on record while it runs; a stop before markRunEnded leaves it "running", which the next
// start turns into the page's interrupted-run notice (run_record.go). Every return path ends the record.
m.markRunStarted(start)
runOK := false
defer func() { m.markRunEnded(runOK) }()
// R-181: open the per-run admission scope HERE, because this function is the single orchestrator
// of all three write legs. Each app's reserve verdict is taken at its first write of this run and
@@ -688,6 +700,7 @@ func (m *Manager) runDBDumpsInternal(ctx context.Context) error {
// residue, invisible in the snapshot list. Guarded (deployed + different current drive only).
m.pruneStalePrimaryDirs()
runOK = allOK
// No silent partials: a DB-dump or volume-dump failure fails the whole run.
if !allOK {
return fmt.Errorf("some backup steps failed: %s", strings.Join(failedSummaryLines(summary), "; "))
@@ -1210,6 +1223,7 @@ func (m *Manager) RefreshCache(nextDBDump time.Time) {
m.mu.Lock()
status.Running = m.running
status.LastDBDump = m.lastDBDump
status.InterruptedRunAt, status.Interrupted = m.runInterrupted, !m.runInterrupted.IsZero()
// Cross-check lastDBDump results inside lock to prevent torn writes.
if m.lastDBDump != nil && len(files) > 0 {
@@ -1252,7 +1266,8 @@ func (m *Manager) GetFullStatus(nextDBDump time.Time) *FullBackupStatus {
// Update dynamic fields that don't need subprocess calls
status.Running = m.running
status.NextDBDump = nextDBDump
status.SingleCopyWarning = m.singleCopyWarning() // F6: honest single-drive signal
status.SingleCopyWarning = m.singleCopyWarning() // F6: honest single-drive signal
status.InterruptedRunAt, status.Interrupted = m.runInterrupted, !m.runInterrupted.IsZero() // R-519
// Deep-copy lastDBDump so callers cannot mutate shared state.
if m.lastDBDump != nil {
copyDump := *m.lastDBDump
@@ -1278,10 +1293,12 @@ func (m *Manager) GetFullStatus(nextDBDump time.Time) *FullBackupStatus {
latestTime = f.ModTime
}
}
// R-519: after a restart the run's result is not in memory, so it is SYNTHESISED from the files. A run
// the stop cut off left a fresh .sql beside last night's tars — "Success" must not be read off that.
status.LastDBDump = &DBDumpStatus{
LastRun: latestTime,
Results: results,
Success: true,
Success: m.runInterrupted.IsZero(),
}
}
@@ -1295,6 +1312,8 @@ func (m *Manager) GetFullStatus(nextDBDump time.Time) *FullBackupStatus {
DBDumpSchedule: m.cfg.Backup.DBDumpSchedule,
NextDBDump: nextDBDump,
SingleCopyWarning: m.singleCopyWarning(), // F6
InterruptedRunAt: m.runInterrupted, // R-519
Interrupted: !m.runInterrupted.IsZero(),
}
if m.lastDBDump != nil {
copyDump := *m.lastDBDump