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:
2026-02-19 08:23:33 +01:00
parent 0c0cacbe7c
commit 2befa6877b
7 changed files with 415 additions and 99 deletions
+99 -3
View File
@@ -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