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:
@@ -1,5 +1,33 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### What was just completed (2026-02-18 session 46)
|
||||||
|
- **v0.12.9 — Tier 2 for All Apps + Status Dot Update:**
|
||||||
|
|
||||||
|
**Fix 1: Tier 2 now configurable for ALL apps — not just HDD apps (`crossdrive.go`)**
|
||||||
|
- Removed `len(mounts) == 0` error gate from `RunAppBackup()` — empty mounts = config-only backup
|
||||||
|
- rsync: DB dump copy (`_db/`) + config rsync (`_config/`) still runs even with zero HDD mounts
|
||||||
|
- restic: config dir + DB dump dir still appended even without mount paths
|
||||||
|
- Non-HDD apps (Mealie, Gokapi, etc.) can now be protected against drive failure via Tier 2
|
||||||
|
|
||||||
|
**Fix 2: Status dot logic updated, HasHDDData gate removed (`handlers.go`)**
|
||||||
|
- `buildAppBackupRows()`: "auto" (gray) status removed — all apps start yellow ("Csak helyi mentés")
|
||||||
|
- Green requires Tier 2 configured + last status "ok" (not just "configured but never run")
|
||||||
|
- Tier2 section is now unconditional — no `if app.HasHDDData` gate
|
||||||
|
- Cross-drive summary loop: removed `if !app.HasHDDData { continue }` — all apps in summary
|
||||||
|
|
||||||
|
**Fix 3: Backup page template updates (`backups.html`)**
|
||||||
|
- Tier 2 row shown for all apps (removed `{{if .HasHDDData}}` gate)
|
||||||
|
- Meta badge: non-HDD apps show "Konfig" or "Konfig + DB" instead of "Auto"
|
||||||
|
- Tier 3 placeholder row added (grayed out "Hamarosan / távoli offsite")
|
||||||
|
- Button text: "Összes HDD mentés" → "Összes 2. mentés futtatása most"
|
||||||
|
|
||||||
|
**Fix 4: Deploy page cross-drive section visible for all deployed apps (`deploy.html`)**
|
||||||
|
- Removed `{{if .StorageInfo}}` double-gate — section now shows for all deployed apps
|
||||||
|
- Updated heading: "Másolat másik meghajtóra (felhasználói adatok)" → "2. mentés — másolat másik meghajtóra"
|
||||||
|
- Updated hint: "mint az alkalmazás adattárolója" → "a meghibásodás elleni védelem érdekében"
|
||||||
|
|
||||||
|
**Files modified (4):** `internal/backup/crossdrive.go`, `internal/web/handlers.go`, `internal/web/templates/backups.html`, `internal/web/templates/deploy.html`
|
||||||
|
|
||||||
### What was just completed (2026-02-18 session 45)
|
### What was just completed (2026-02-18 session 45)
|
||||||
- **v0.12.8 — Complete Cross-Drive Backup + Per-Tier UI:**
|
- **v0.12.8 — Complete Cross-Drive Backup + Per-Tier UI:**
|
||||||
|
|
||||||
|
|||||||
@@ -95,12 +95,8 @@ func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) e
|
|||||||
return fmt.Errorf("destination validation failed: %w", err)
|
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)
|
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
|
// Safety: destination must not overlap with any source
|
||||||
for _, m := range mounts {
|
for _, m := range mounts {
|
||||||
|
|||||||
@@ -407,9 +407,6 @@ func (s *Server) backupsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, app := range fullStatus.AppDataInfo {
|
for _, app := range fullStatus.AppDataInfo {
|
||||||
if !app.HasHDDData {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cfg, hasCfg := crossConfigs[app.StackName]
|
cfg, hasCfg := crossConfigs[app.StackName]
|
||||||
if !hasCfg || cfg == nil {
|
if !hasCfg || cfg == nil {
|
||||||
fullStatus.UnconfiguredApps = append(fullStatus.UnconfiguredApps, backup.CrossDriveSummaryItem{
|
fullStatus.UnconfiguredApps = append(fullStatus.UnconfiguredApps, backup.CrossDriveSummaryItem{
|
||||||
@@ -519,7 +516,7 @@ type AppBackupRow struct {
|
|||||||
Tier1LastStatus string // "ok", "error", ""
|
Tier1LastStatus string // "ok", "error", ""
|
||||||
Tier1DBStatus string // "ok", "error", "" — separate DB dump status for warning
|
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
|
Tier2Configured bool
|
||||||
Tier2Method string // "rsync", "restic"
|
Tier2Method string // "rsync", "restic"
|
||||||
Tier2MethodLabel string // "rsync", "restic"
|
Tier2MethodLabel string // "rsync", "restic"
|
||||||
@@ -602,18 +599,15 @@ func (s *Server) buildAppBackupRows(
|
|||||||
Tier1DBStatus: tier1DBStatus,
|
Tier1DBStatus: tier1DBStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default status = auto (no user data, just config)
|
// Status dot — start as yellow (1 tier only)
|
||||||
row.Status = "auto"
|
row.Status = "yellow"
|
||||||
row.StatusText = "Automatikus mentés"
|
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 {
|
if !hasCfg || cfg == nil || !cfg.Enabled {
|
||||||
// HDD data backed up via nightly restic (mandatory), but no second copy
|
// Only Tier 1 — no second copy
|
||||||
row.Tier2Configured = false
|
row.Tier2Configured = false
|
||||||
row.Status = "yellow"
|
|
||||||
row.StatusText = "Nincs második másolat (csak helyi mentés)"
|
|
||||||
} else {
|
} else {
|
||||||
row.Tier2Configured = true
|
row.Tier2Configured = true
|
||||||
row.Tier2Method = cfg.Method
|
row.Tier2Method = cfg.Method
|
||||||
@@ -642,31 +636,30 @@ func (s *Server) buildAppBackupRows(
|
|||||||
switch cfg.LastStatus {
|
switch cfg.LastStatus {
|
||||||
case "ok":
|
case "ok":
|
||||||
row.Tier2StatusBadge = "Sikeres"
|
row.Tier2StatusBadge = "Sikeres"
|
||||||
|
row.Status = "green"
|
||||||
|
row.StatusText = "Mentés rendben"
|
||||||
case "error":
|
case "error":
|
||||||
row.Tier2StatusBadge = "Hiba"
|
row.Tier2StatusBadge = "Hiba"
|
||||||
row.Status = "yellow"
|
// Status stays yellow
|
||||||
row.StatusText = "Utolsó mentés sikertelen"
|
row.StatusText = "Utolsó mentés sikertelen"
|
||||||
case "running":
|
case "running":
|
||||||
row.Tier2StatusBadge = "Fut..."
|
row.Tier2StatusBadge = "Fut..."
|
||||||
default:
|
default:
|
||||||
row.Tier2StatusBadge = "—"
|
row.Tier2StatusBadge = "—"
|
||||||
|
// Tier2 configured but never run — stay yellow
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destination health check
|
// Destination health check — can downgrade green to yellow/red
|
||||||
if cfg.Enabled && cfg.DestinationPath != "" {
|
if cfg.DestinationPath != "" {
|
||||||
if err := s.crossDriveRunner.ValidateDestination(cfg.DestinationPath); err != nil {
|
if err := s.crossDriveRunner.ValidateDestination(cfg.DestinationPath); err != nil {
|
||||||
if strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "not writable") {
|
if strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "not writable") {
|
||||||
row.Status = "red"
|
row.Status = "red"
|
||||||
row.StatusText = "Mentési cél nem elérhető"
|
row.StatusText = "Mentési cél nem elérhető"
|
||||||
} else {
|
} else if row.Status != "red" {
|
||||||
row.Status = "yellow"
|
row.Status = "yellow"
|
||||||
row.StatusText = "Figyelmeztetés"
|
row.StatusText = "Figyelmeztetés"
|
||||||
}
|
}
|
||||||
row.Warnings = append(row.Warnings, err.Error())
|
row.Warnings = append(row.Warnings, err.Error())
|
||||||
} else if row.Status != "yellow" {
|
|
||||||
row.Status = "green"
|
|
||||||
row.StatusText = "Mentés rendben"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,7 @@
|
|||||||
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
|
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
|
||||||
<span class="mono app-backup-size" style="font-size:.8rem">{{.HDDSizeHuman}}</span>
|
<span class="mono app-backup-size" style="font-size:.8rem">{{.HDDSizeHuman}}</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="meta-badge">Auto</span>
|
<span class="meta-badge">Konfig{{if .HasDB}} + DB{{end}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<span class="expand-icon">▶</span>
|
<span class="expand-icon">▶</span>
|
||||||
@@ -281,7 +281,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<!-- Tier 2: Cross-drive backup (opt-in, different device) -->
|
<!-- Tier 2: Cross-drive backup (opt-in, different device) -->
|
||||||
{{if .HasHDDData}}
|
|
||||||
<div class="backup-layer-row">
|
<div class="backup-layer-row">
|
||||||
<span class="tier-label">2. mentés</span>
|
<span class="tier-label">2. mentés</span>
|
||||||
{{if .Tier2Configured}}
|
{{if .Tier2Configured}}
|
||||||
@@ -310,7 +309,13 @@
|
|||||||
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs">Beállítás →</a>
|
<a href="/stacks/{{.StackName}}/deploy" class="btn btn-xs">Beállítás →</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{{if .Warnings}}
|
{{if .Warnings}}
|
||||||
<div class="layer-warnings">
|
<div class="layer-warnings">
|
||||||
@@ -325,7 +330,7 @@
|
|||||||
|
|
||||||
{{if .Backup.CrossDriveSummary}}
|
{{if .Backup.CrossDriveSummary}}
|
||||||
<div class="cross-drive-actions" style="margin-top:1rem">
|
<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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,7 +93,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .AlreadyDeployed}}
|
{{if .AlreadyDeployed}}
|
||||||
{{if .StorageInfo}}
|
|
||||||
<div class="deploy-cross-drive">
|
<div class="deploy-cross-drive">
|
||||||
<h4>Biztonsági mentés</h4>
|
<h4>Biztonsági mentés</h4>
|
||||||
|
|
||||||
@@ -106,7 +105,7 @@
|
|||||||
|
|
||||||
<hr style="border-color:var(--border);margin:1rem 0">
|
<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}}
|
{{if .BackupDestWarning}}
|
||||||
<div class="alert {{if eq .BackupDestWarningSeverity "critical"}}alert-error{{else}}alert-warning{{end}}" style="margin-bottom:1rem">{{.BackupDestWarning}}</div>
|
<div class="alert {{if eq .BackupDestWarningSeverity "critical"}}alert-error{{else}}alert-warning{{end}}" style="margin-bottom:1rem">{{.BackupDestWarning}}</div>
|
||||||
@@ -212,12 +211,11 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="form-hint" style="margin-top:.75rem;color:var(--text-muted)">
|
<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>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{if and (not .AlreadyDeployed) .MemoryInfo}}
|
{{if and (not .AlreadyDeployed) .MemoryInfo}}
|
||||||
{{with .MemoryInfo}}
|
{{with .MemoryInfo}}
|
||||||
|
|||||||
Reference in New Issue
Block a user