v0.12.9: Tier 2 for all apps + status dot update

- Tier 2 cross-drive backup now configurable for all apps (not just HDD apps)
- Non-HDD apps (Mealie, Gokapi) can back up config + DB to secondary drive
- Status dot: removed "auto" gray — all apps start yellow, green = 2+ tiers OK
- Backup page: Tier 2 row always shown, Tier 3 placeholder added
- Deploy page: cross-drive config visible for all deployed apps
- Meta badges: non-HDD apps show "Konfig" or "Konfig + DB" instead of "Auto"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 12:11:27 +01:00
parent f9c0338894
commit 3f2557fe26
5 changed files with 98 additions and 78 deletions
+1 -5
View File
@@ -95,12 +95,8 @@ func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) e
return fmt.Errorf("destination validation failed: %w", err)
}
// Resolve HDD mounts for this app
// Resolve HDD mounts for this app (may be empty for config-only apps)
mounts := r.stackProvider.GetStackHDDMounts(stackName)
if len(mounts) == 0 {
r.updateStatus(stackName, "error", "no HDD data paths found for this app", time.Since(start), "")
return fmt.Errorf("no HDD data paths found for %s", stackName)
}
// Safety: destination must not overlap with any source
for _, m := range mounts {
+58 -65
View File
@@ -407,9 +407,6 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
}
for _, app := range fullStatus.AppDataInfo {
if !app.HasHDDData {
continue
}
cfg, hasCfg := crossConfigs[app.StackName]
if !hasCfg || cfg == nil {
fullStatus.UnconfiguredApps = append(fullStatus.UnconfiguredApps, backup.CrossDriveSummaryItem{
@@ -519,7 +516,7 @@ type AppBackupRow struct {
Tier1LastStatus string // "ok", "error", ""
Tier1DBStatus string // "ok", "error", "" — separate DB dump status for warning
// Tier 2: Cross-drive backup (only for apps with HDD data)
// Tier 2: Cross-drive backup (configurable for all apps)
Tier2Configured bool
Tier2Method string // "rsync", "restic"
Tier2MethodLabel string // "rsync", "restic"
@@ -602,71 +599,67 @@ func (s *Server) buildAppBackupRows(
Tier1DBStatus: tier1DBStatus,
}
// Default status = auto (no user data, just config)
row.Status = "auto"
row.StatusText = "Automatikus mentés"
// Status dot — start as yellow (1 tier only)
row.Status = "yellow"
row.StatusText = "Csak helyi mentés (1 szint)"
if app.HasHDDData {
cfg, hasCfg := crossConfigs[app.StackName]
cfg, hasCfg := crossConfigs[app.StackName]
if !hasCfg || cfg == nil || !cfg.Enabled {
// HDD data backed up via nightly restic (mandatory), but no second copy
row.Tier2Configured = false
row.Status = "yellow"
row.StatusText = "Nincs második másolat (csak helyi mentés)"
} else {
row.Tier2Configured = true
row.Tier2Method = cfg.Method
row.Tier2MethodLabel = cfg.Method // "rsync" or "restic"
row.Tier2Browsable = cfg.Method == "rsync"
row.Tier2Dest = destLabels[cfg.DestinationPath]
if row.Tier2Dest == "" {
row.Tier2Dest = cfg.DestinationPath
if !hasCfg || cfg == nil || !cfg.Enabled {
// Only Tier 1 — no second copy
row.Tier2Configured = false
} else {
row.Tier2Configured = true
row.Tier2Method = cfg.Method
row.Tier2MethodLabel = cfg.Method // "rsync" or "restic"
row.Tier2Browsable = cfg.Method == "rsync"
row.Tier2Dest = destLabels[cfg.DestinationPath]
if row.Tier2Dest == "" {
row.Tier2Dest = cfg.DestinationPath
}
switch cfg.Schedule {
case "daily":
row.Tier2Schedule = "Naponta"
case "weekly":
row.Tier2Schedule = "Hetente"
default:
row.Tier2Schedule = cfg.Schedule
}
if cfg.LastRun != "" {
if t, err := time.Parse(time.RFC3339, cfg.LastRun); err == nil {
row.Tier2LastRun = t.In(loc).Format("01-02 15:04")
}
switch cfg.Schedule {
case "daily":
row.Tier2Schedule = "Naponta"
case "weekly":
row.Tier2Schedule = "Hetente"
default:
row.Tier2Schedule = cfg.Schedule
}
if cfg.LastRun != "" {
if t, err := time.Parse(time.RFC3339, cfg.LastRun); err == nil {
row.Tier2LastRun = t.In(loc).Format("01-02 15:04")
}
}
row.Tier2LastStatus = cfg.LastStatus
row.Tier2LastError = cfg.LastError
row.Tier2SizeHuman = cfg.LastSizeHuman
switch cfg.LastStatus {
case "ok":
row.Tier2StatusBadge = "Sikeres"
case "error":
row.Tier2StatusBadge = "Hiba"
row.Status = "yellow"
row.StatusText = "Utolsó mentés sikertelen"
case "running":
row.Tier2StatusBadge = "Fut..."
default:
row.Tier2StatusBadge = "—"
}
// Destination health check
if cfg.Enabled && cfg.DestinationPath != "" {
if err := s.crossDriveRunner.ValidateDestination(cfg.DestinationPath); err != nil {
if strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "not writable") {
row.Status = "red"
row.StatusText = "Mentési cél nem elérhető"
} else {
row.Status = "yellow"
row.StatusText = "Figyelmeztetés"
}
row.Warnings = append(row.Warnings, err.Error())
} else if row.Status != "yellow" {
row.Status = "green"
row.StatusText = "Mentés rendben"
}
row.Tier2LastStatus = cfg.LastStatus
row.Tier2LastError = cfg.LastError
row.Tier2SizeHuman = cfg.LastSizeHuman
switch cfg.LastStatus {
case "ok":
row.Tier2StatusBadge = "Sikeres"
row.Status = "green"
row.StatusText = "Mentés rendben"
case "error":
row.Tier2StatusBadge = "Hiba"
// Status stays yellow
row.StatusText = "Utolsó mentés sikertelen"
case "running":
row.Tier2StatusBadge = "Fut..."
default:
row.Tier2StatusBadge = "—"
// Tier2 configured but never run — stay yellow
}
// Destination health check — can downgrade green to yellow/red
if cfg.DestinationPath != "" {
if err := s.crossDriveRunner.ValidateDestination(cfg.DestinationPath); err != nil {
if strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "not writable") {
row.Status = "red"
row.StatusText = "Mentési cél nem elérhető"
} else if row.Status != "red" {
row.Status = "yellow"
row.StatusText = "Figyelmeztetés"
}
row.Warnings = append(row.Warnings, err.Error())
}
}
}
@@ -257,7 +257,7 @@
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
<span class="mono app-backup-size" style="font-size:.8rem">{{.HDDSizeHuman}}</span>
{{else}}
<span class="meta-badge">Auto</span>
<span class="meta-badge">Konfig{{if .HasDB}} + DB{{end}}</span>
{{end}}
</div>
<span class="expand-icon"></span>
@@ -281,7 +281,6 @@
{{end}}
</div>
<!-- Tier 2: Cross-drive backup (opt-in, different device) -->
{{if .HasHDDData}}
<div class="backup-layer-row">
<span class="tier-label">2. mentés</span>
{{if .Tier2Configured}}
@@ -310,7 +309,13 @@
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs">Beállítás →</a>
{{end}}
</div>
{{end}}
<!-- Tier 3: Remote backup (future) -->
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">3. mentés</span>
<span class="layer-badge" style="background:var(--bg-tertiary);color:var(--text-muted)">Hamarosan</span>
<span class="tier-location">távoli (offsite)</span>
<span class="tier-contents" style="font-style:normal;color:var(--text-muted)">B2 / S3 / SFTP — hamarosan elérhető</span>
</div>
</div>
{{if .Warnings}}
<div class="layer-warnings">
@@ -325,7 +330,7 @@
{{if .Backup.CrossDriveSummary}}
<div class="cross-drive-actions" style="margin-top:1rem">
<button class="btn btn-sm btn-outline" onclick="triggerAllCrossDrive(this)">Összes HDD mentés futtatása most</button>
<button class="btn btn-sm btn-outline" onclick="triggerAllCrossDrive(this)">Összes 2. mentés futtatása most</button>
</div>
{{end}}
</div>
@@ -93,7 +93,6 @@
{{end}}
{{if .AlreadyDeployed}}
{{if .StorageInfo}}
<div class="deploy-cross-drive">
<h4>Biztonsági mentés</h4>
@@ -106,7 +105,7 @@
<hr style="border-color:var(--border);margin:1rem 0">
<p style="font-weight:500;margin-bottom:1rem">Másolat másik meghajtóra (felhasználói adatok):</p>
<p style="font-weight:500;margin-bottom:1rem">2. mentés — másolat másik meghajtóra:</p>
{{if .BackupDestWarning}}
<div class="alert {{if eq .BackupDestWarningSeverity "critical"}}alert-error{{else}}alert-warning{{end}}" style="margin-bottom:1rem">{{.BackupDestWarning}}</div>
@@ -212,12 +211,11 @@
</form>
<div class="form-hint" style="margin-top:.75rem;color:var(--text-muted)">
A cél meghajtó legyen más fizikai eszköz, mint az alkalmazás adattárolója.
A cél meghajtó legyen más fizikai eszköz a meghibásodás elleni védelem érdekében.
</div>
{{end}}
</div>
{{end}}
{{end}}
{{if and (not .AlreadyDeployed) .MemoryInfo}}
{{with .MemoryInfo}}