v0.15.1: Backup page Részletek overhaul with per-drive tier sections
Replace Tároló section with collapsible Részletek containing 3 tiers: - Tier 1: per-drive restic repo stats with storage labels - Tier 2: cross-drive items grouped by destination, split by method - Tier 3: remote backup placeholder Restore UI now shows tier + drive labels in snapshot dropdown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,15 @@ type SnapshotRecord struct {
|
||||
HasStats bool `json:"has_stats"` // false for historical entries loaded from restic
|
||||
}
|
||||
|
||||
// DriveRepoInfo holds per-drive restic repository statistics for the Részletek section.
|
||||
type DriveRepoInfo struct {
|
||||
DrivePath string
|
||||
DriveLabel string // filled by handler from settings
|
||||
TotalSize string
|
||||
TotalSizeBytes int64
|
||||
SnapshotCount int
|
||||
}
|
||||
|
||||
// CrossDriveSummaryItem holds display data for one app's cross-drive backup.
|
||||
type CrossDriveSummaryItem struct {
|
||||
StackName string
|
||||
@@ -81,9 +90,10 @@ type FullBackupStatus struct {
|
||||
DiscoveredDBs []DiscoveredDB
|
||||
|
||||
// Restic
|
||||
LastBackup *BackupStatus
|
||||
SnapshotHistory []SnapshotRecord
|
||||
RepoStats *RepoStats
|
||||
LastBackup *BackupStatus
|
||||
SnapshotHistory []SnapshotRecord
|
||||
RepoStats *RepoStats
|
||||
PerDriveRepoStats []DriveRepoInfo // per-drive Tier 1 restic stats
|
||||
|
||||
// Schedule
|
||||
DBDumpSchedule string
|
||||
@@ -565,6 +575,66 @@ func (m *Manager) ListSnapshots(limit int) ([]SnapshotInfo, error) {
|
||||
return allSnapshots, nil
|
||||
}
|
||||
|
||||
// ListAllSnapshots returns snapshots from both primary and secondary restic repos.
|
||||
// Primary snapshots get Tier=1, secondary snapshots get Tier=2.
|
||||
func (m *Manager) ListAllSnapshots(limit int) ([]SnapshotInfo, error) {
|
||||
drives := m.activeDrives()
|
||||
var allSnapshots []SnapshotInfo
|
||||
|
||||
// Tier 1: primary repos (same as ListSnapshots)
|
||||
for _, drive := range drives {
|
||||
repoPath := PrimaryResticRepoPath(drive)
|
||||
if !m.restic.RepoExists(repoPath) {
|
||||
continue
|
||||
}
|
||||
snapshots, err := m.restic.ListSnapshots(repoPath, 0)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Could not list snapshots from %s: %v", repoPath, err)
|
||||
continue
|
||||
}
|
||||
for i := range snapshots {
|
||||
snapshots[i].RepoPath = repoPath
|
||||
snapshots[i].Tier = 1
|
||||
}
|
||||
allSnapshots = append(allSnapshots, snapshots...)
|
||||
}
|
||||
|
||||
// Tier 2: secondary restic repos on cross-drive destinations
|
||||
if m.settings != nil {
|
||||
destPaths := make(map[string]bool)
|
||||
for _, cfg := range m.settings.GetAllCrossDriveConfigs() {
|
||||
if cfg != nil && cfg.Method == "restic" && cfg.DestinationPath != "" {
|
||||
destPaths[cfg.DestinationPath] = true
|
||||
}
|
||||
}
|
||||
for destPath := range destPaths {
|
||||
repoPath := SecondaryResticRepoPath(destPath)
|
||||
if !m.restic.RepoExists(repoPath) {
|
||||
continue
|
||||
}
|
||||
snapshots, err := m.restic.ListSnapshots(repoPath, 0)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] Could not list secondary snapshots from %s: %v", repoPath, err)
|
||||
continue
|
||||
}
|
||||
for i := range snapshots {
|
||||
snapshots[i].RepoPath = repoPath
|
||||
snapshots[i].Tier = 2
|
||||
}
|
||||
allSnapshots = append(allSnapshots, snapshots...)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort newest first
|
||||
sort.Slice(allSnapshots, func(i, j int) bool {
|
||||
return allSnapshots[i].Time.After(allSnapshots[j].Time)
|
||||
})
|
||||
if limit > 0 && len(allSnapshots) > limit {
|
||||
allSnapshots = allSnapshots[:limit]
|
||||
}
|
||||
return allSnapshots, nil
|
||||
}
|
||||
|
||||
// SetStackProvider sets the stack data provider for app data discovery.
|
||||
// C3: Write is protected by mutex since stackProvider is read by concurrent goroutines.
|
||||
func (m *Manager) SetStackProvider(provider StackDataProvider) {
|
||||
@@ -632,6 +702,29 @@ func (m *Manager) DumpStackDB(ctx context.Context, stackName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// perDriveRepoStats returns per-drive restic repository statistics for the Részletek section.
|
||||
func (m *Manager) perDriveRepoStats() []DriveRepoInfo {
|
||||
drives := m.activeDrives()
|
||||
var infos []DriveRepoInfo
|
||||
for _, drive := range drives {
|
||||
repoPath := PrimaryResticRepoPath(drive)
|
||||
if !m.restic.RepoExists(repoPath) {
|
||||
continue
|
||||
}
|
||||
stats, err := m.restic.Stats(repoPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
infos = append(infos, DriveRepoInfo{
|
||||
DrivePath: drive,
|
||||
TotalSize: stats.TotalSize,
|
||||
TotalSizeBytes: stats.TotalSizeBytes,
|
||||
SnapshotCount: stats.SnapshotCount,
|
||||
})
|
||||
}
|
||||
return infos
|
||||
}
|
||||
|
||||
// aggregateRepoStats combines stats from all primary restic repos.
|
||||
func (m *Manager) aggregateRepoStats() *RepoStats {
|
||||
drives := m.activeDrives()
|
||||
@@ -758,6 +851,7 @@ func (m *Manager) RefreshCache(nextDBDump, nextBackup time.Time) {
|
||||
|
||||
// Expensive calls (outside lock)
|
||||
status.RepoStats = m.aggregateRepoStats()
|
||||
status.PerDriveRepoStats = m.perDriveRepoStats()
|
||||
|
||||
// Scan dump files from per-drive per-stack paths
|
||||
files := m.listAllDumpFiles()
|
||||
@@ -831,6 +925,8 @@ func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupSta
|
||||
status := *m.cachedStatus
|
||||
status.AppDataInfo = make([]AppBackupInfo, len(m.cachedStatus.AppDataInfo))
|
||||
copy(status.AppDataInfo, m.cachedStatus.AppDataInfo)
|
||||
status.PerDriveRepoStats = make([]DriveRepoInfo, len(m.cachedStatus.PerDriveRepoStats))
|
||||
copy(status.PerDriveRepoStats, m.cachedStatus.PerDriveRepoStats)
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user