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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user