v0.12.0 — Backup page overhaul: unified app rows, bug fixes, sequential chaining

Bug fixes:
- GetFullStatus() returns deep copy; CrossDriveSummary/UnconfiguredApps/CrossDriveWarnings
  are always nil in the copy so the handler builds them fresh (fixes duplicate-apps bug)
- Replace binary IsMountPoint check with tiered CheckBackupDestination() — path-not-exist,
  not-writable, system-drive (warning), disk >90-95% full; shown as warning vs critical
- Remove dead settingsAppBackupHandler / POST /settings/app-backup route (toggle wrote
  to settings.json but nothing consumed the flag)

Architecture:
- Unified per-app backup rows: new AppBackupRow struct + buildAppBackupRows() replaces
  the two old sections with expandable rows showing all 3 layers per app
- Sequential backup chaining: cross-drive runs immediately after restic (removed
  independent cross-drive-daily/cross-drive-weekly scheduler jobs)
- Deploy page: remove "Csak kézi indítás" schedule option; add weekly consistency note

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 17:56:28 +01:00
parent e002d712cf
commit 1de244646b
10 changed files with 637 additions and 154 deletions
+30 -16
View File
@@ -585,29 +585,43 @@ func (m *Manager) RefreshCache(nextDBDump, nextBackup time.Time) {
// GetFullStatus returns the cached backup status for page rendering.
// Returns instantly — no subprocess calls.
// Returns a deep copy so callers can safely append to slice fields without
// polluting the cache (which would cause duplicate entries on repeated calls).
func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupStatus {
m.mu.Lock()
defer m.mu.Unlock()
if m.cachedStatus != nil {
// Deep copy — callers (backupsHandler) append to CrossDriveSummary,
// UnconfiguredApps, and CrossDriveWarnings. If we returned the cache
// pointer directly, every page load would accumulate more entries.
status := *m.cachedStatus
status.AppDataInfo = make([]AppBackupInfo, len(m.cachedStatus.AppDataInfo))
copy(status.AppDataInfo, m.cachedStatus.AppDataInfo)
// These three slices are assembled by the handler from AppDataInfo + settings;
// they must always start empty so the handler builds them fresh.
status.CrossDriveSummary = nil
status.UnconfiguredApps = nil
status.CrossDriveWarnings = nil
// Update dynamic fields that don't need subprocess calls
m.cachedStatus.Running = m.running
m.cachedStatus.NextDBDump = nextDBDump
m.cachedStatus.NextBackup = nextBackup
m.cachedStatus.LastDBDump = m.lastDBDump
m.cachedStatus.LastBackup = m.lastBackup
status.Running = m.running
status.NextDBDump = nextDBDump
status.NextBackup = nextBackup
status.LastDBDump = m.lastDBDump
status.LastBackup = m.lastBackup
// Update snapshot history
m.cachedStatus.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory))
copy(m.cachedStatus.SnapshotHistory, m.snapshotHistory)
status.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory))
copy(status.SnapshotHistory, m.snapshotHistory)
// Reverse so newest first
for i, j := 0, len(m.cachedStatus.SnapshotHistory)-1; i < j; i, j = i+1, j-1 {
m.cachedStatus.SnapshotHistory[i], m.cachedStatus.SnapshotHistory[j] = m.cachedStatus.SnapshotHistory[j], m.cachedStatus.SnapshotHistory[i]
for i, j := 0, len(status.SnapshotHistory)-1; i < j; i, j = i+1, j-1 {
status.SnapshotHistory[i], status.SnapshotHistory[j] = status.SnapshotHistory[j], status.SnapshotHistory[i]
}
// Synthesize LastBackup from snapshot history if not in memory (e.g., after restart)
if m.cachedStatus.LastBackup == nil && len(m.cachedStatus.SnapshotHistory) > 0 {
latest := m.cachedStatus.SnapshotHistory[0] // already reversed, newest first
m.cachedStatus.LastBackup = &BackupStatus{
if status.LastBackup == nil && len(status.SnapshotHistory) > 0 {
latest := status.SnapshotHistory[0] // already reversed, newest first
status.LastBackup = &BackupStatus{
LastRun: latest.Time,
Success: latest.Success,
Snapshot: &SnapshotResult{
@@ -617,10 +631,10 @@ func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupSta
}
// Synthesize LastDBDump from DumpFiles on disk if not in memory
if m.cachedStatus.LastDBDump == nil && len(m.cachedStatus.DumpFiles) > 0 {
if status.LastDBDump == nil && len(status.DumpFiles) > 0 {
var results []DumpResult
var latestTime time.Time
for _, f := range m.cachedStatus.DumpFiles {
for _, f := range status.DumpFiles {
results = append(results, DumpResult{
DB: DiscoveredDB{StackName: f.StackName, DBType: f.DBType, ContainerName: f.StackName},
FilePath: f.FileName,
@@ -630,14 +644,14 @@ func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupSta
latestTime = f.ModTime
}
}
m.cachedStatus.LastDBDump = &DBDumpStatus{
status.LastDBDump = &DBDumpStatus{
LastRun: latestTime,
Results: results,
Success: true,
}
}
return m.cachedStatus
return &status
}
// No cache yet — return a minimal status (first page load before cache is populated)