33 KiB
TASK: Backup Page Overhaul — Unified App Backup Status & Bug Fixes
Summary
Overhaul the "Biztonsági mentés" page to show a single unified per-app backup overview with expandable detail rows, fix critical bugs (duplicate apps, misleading errors, dead toggles), and implement sequential backup chaining so cross-drive backups run after the nightly backup completes.
Bug Fixes (Critical)
Bug 1: Duplicate unconfigured apps on every page load
Root cause: GetFullStatus() returns a pointer to m.cachedStatus. The backupsHandler() then appends to UnconfiguredApps and CrossDriveSummary on that cached object. Every page load appends again → 3 apps × 3 loads = 9 entries.
File: internal/backup/backup.go — GetFullStatus()
Fix: Return a deep copy of the cached status. Specifically, CrossDriveSummary, UnconfiguredApps, CrossDriveWarnings, and AppDataInfo slices must not share backing arrays with the cache.
func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupStatus {
m.mu.Lock()
defer m.mu.Unlock()
if m.cachedStatus != nil {
// Deep copy to prevent callers from mutating cached slices
status := *m.cachedStatus
status.AppDataInfo = make([]AppBackupInfo, len(m.cachedStatus.AppDataInfo))
copy(status.AppDataInfo, m.cachedStatus.AppDataInfo)
status.CrossDriveSummary = nil // rebuilt by handler
status.UnconfiguredApps = nil // rebuilt by handler
status.CrossDriveWarnings = nil // rebuilt by handler
// ... (keep existing dynamic field updates on &status)
return &status
}
// ... (rest unchanged)
}
Alternative (simpler): Don't populate CrossDriveSummary/UnconfiguredApps in the returned status at all — move that logic into a separate method or let backupsHandler build them from AppDataInfo + settings. This is cleaner architecturally since the cache shouldn't hold UI-assembly data.
Recommendation: Alternative approach. The handler already builds this data; the cache should only hold the expensive-to-compute parts (app discovery, restic stats, snapshot history).
Bug 2: Misleading error message for non-mount-point destinations
Root cause: When cross-drive destination is configured to /mnt/hdd_placeholder, IsMountPoint() returns false (it's a directory on the system SSD, not a separate mount point). The code then shows: "A cél tárhely (/mnt/hdd_placeholder) nem elérhető! Ellenőrizd a meghajtó csatlakozását." — suggesting a disconnected drive, which is incorrect.
File: internal/web/handlers.go — deployHandler() (line ~13818), backupsHandler() (line ~14036)
Current logic:
if !system.IsMountPoint(path) || !system.IsWritable(path) {
// "nem elérhető! Ellenőrizd a meghajtó csatlakozását."
}
Fix: Replace the single check with tiered validation:
func validateBackupDestination(path string) (warning string, blocked bool) {
// 1. Path doesn't exist at all
if _, err := os.Stat(path); os.IsNotExist(err) {
return "A cél tárhely (" + path + ") nem létezik!", true
}
// 2. Path exists but not writable
if !system.IsWritable(path) {
return "A cél tárhely (" + path + ") nem írható! Ellenőrizd a jogosultságokat.", true
}
// 3. Path is on the system drive (not a separate mount point)
if !system.IsMountPoint(path) {
return "A cél tárhely (" + path + ") a rendszermeghajtón van. " +
"Meghajtóhiba esetén az eredeti adat és a mentés is elveszhet. " +
"Külső meghajtó használata javasolt.", false // warning, not blocked
}
// 4. All good
return "", false
}
blocked = true→ red alert, backup should not runblocked = false, warning != ""→ yellow alert, backup can run but user is informed- No warning → green
Bug 3: Dead BackupEnabled toggle
Root cause: The BackupEnabled flag per app (toggled via settingsAppBackupHandler) saves to settings.json but nothing reads it to actually include/exclude apps from any backup process. There is no Backrest deployment; the nightly restic backup is a cron-driven script that doesn't consult settings.json.
Fix: Remove the settingsAppBackupHandler and the bulk toggle form from the backup page. This toggle is replaced by the new unified per-app status rows (see Architecture section below). The concept of "include this app in nightly backup" becomes implicit: if the app has a DB, it gets dumped; if it has Docker volumes, they're in the restic paths; if it has HDD data, cross-drive must be explicitly configured.
Files to modify:
internal/web/handlers.go— removesettingsAppBackupHandler()internal/web/server.go— remove route registration forPOST /settings/app-backupinternal/web/templates/backups.html— remove the bulk toggle forminternal/settings/settings.go— deprecateSetAppBackupBulk(),IsAppBackupEnabled()(keep getters for migration, remove after one version)
Architecture Changes
1. Backup Coverage Model
Each deployed app has up to 3 backup layers:
| Layer | What | How | Needs user config? |
|---|---|---|---|
| DB dump | PostgreSQL/MariaDB databases | backup-db-dump.sh via systemd timer |
No (auto-discovered) |
| Docker volumes | App configs, state, SQLite DBs | Nightly restic snapshot of named volumes | No (automatic for all deployed apps) |
| User data (HDD) | Photos, documents, media files | Cross-drive rsync/restic to second drive | Yes — requires second drive + config |
Status colors per app:
| Color | Meaning |
|---|---|
| Green | All applicable backup layers are configured and last run was successful |
| Yellow | Warning: last backup run failed/stale, OR destination drive has low space, OR backup is to system drive |
| Red | Critical: app has HDD user data but no cross-drive backup configured, OR backup destination unreachable/disconnected |
| Gray/Auto | App has no user-configurable backup needs (DB + volumes only, fully automatic) |
Logic for computing status per app:
if app.HasHDDData:
if no cross-drive configured:
→ RED ("Felhasználói adatokról nincs mentés")
elif destination unreachable/disconnected:
→ RED ("Mentési cél nem elérhető")
elif last cross-drive run failed:
→ YELLOW ("Utolsó mentés sikertelen")
elif destination is system drive:
→ YELLOW ("Mentés a rendszermeghajtóra — külső meghajtó javasolt")
elif destination drive >85% full:
→ YELLOW ("Mentési meghajtó majdnem megtelt")
else:
→ GREEN
else:
→ GREEN/AUTO (no user action needed)
# Additionally, cross-cutting checks:
if app.HasDB and last DB dump for this app failed:
→ YELLOW (even if user data is fine)
2. Unified Per-App Backup Rows (UI)
Replace the current two sections ("Alkalmazás adatok" + "Másolatok másik meghajtóra") with a single section containing one expandable row per deployed app.
Collapsed row (default):
┌──────────────────────────────────────────────────────────────────────┐
│ ● Immich Külső tárhely (hdd_1) 92 MB ▶ Részletek │
│ ● Paperless-ngx Külső tárhely (hdd_1) 12 MB ▶ Részletek │
│ ● Gokapi Auto ▶ Részletek │
│ ● Mealie Auto ▶ Részletek │
│ ● RomM Külső tárhely (hdd_1) 340 MB ▶ Részletek │
└──────────────────────────────────────────────────────────────────────┘
●= color indicator (green/yellow/red)- Storage label + size only shown for apps with HDD data
- "Auto" badge for apps with only automatic backups
▶ Részletek= expand button (triangle/chevron)
Expanded row (after clicking):
┌──────────────────────────────────────────────────────────────────────┐
│ ● Immich Külső tárhely (hdd_1) 92 MB ▼ Részletek │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Adatbázis mentés Auto Utolsó: 02-17 02:31 │ │
│ │ Docker kötetek Auto Utolsó: 02-17 03:02 │ │
│ │ Felhasználói adatok rsync → Külső HDD (hdd_1) │ │
│ │ Ütemezés: Naponta │ │
│ │ Utolsó: 02-17 03:35 Sikeres │ │
│ │ [Beállítás] [Futtatás most] │ │
│ └────────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ ● Gokapi Auto ▼ Részletek │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Adatbázis mentés — (nincs adatbázis) │ │
│ │ Docker kötetek Auto Utolsó: 02-17 03:02 │ │
│ │ Felhasználói adatok — (nincs HDD adat) │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
For apps with HDD data but NO cross-drive configured (RED status):
│ Felhasználói adatok ⚠ Nincs beállítva │
│ Mentés beállítása az alkalmazás │
│ oldalán: [Immich beállítások →] │
For apps with HDD data and destination is system drive (YELLOW):
│ Felhasználói adatok rsync → Rendszer SSD (hdd_placeholder) │
│ ⚠ Rendszermeghajtóra mentés — │
│ meghajtóhiba esetén mindkét másolat │
│ elveszhet. Külső meghajtó javasolt. │
No second drive at all — top-level warning
If zero apps have cross-drive configured AND at least one app has HDD data, show a prominent card above the app list:
┌──────────────────────────────────────────────────────────────────────┐
│ ⚠ Felhasználói adatokról nincs biztonsági mentés │
│ │
│ A szerveren tárolt fotók, dokumentumok és egyéb fájlok jelenleg │
│ csak egy példányban léteznek. Külső meghajtó csatlakoztatásával │
│ biztonsági másolat készíthető a 3-2-1 szabály szerint. │
│ │
│ [Meghajtó beállítása →] │
└──────────────────────────────────────────────────────────────────────┘
3. Sequential Backup Chaining
Current flow:
02:30 systemd timer → backup-db-dump.sh (DB dumps)
03:00 cron/scheduler → restic backup (Docker volumes + DB dumps)
03:30 independent scheduler → cross-drive jobs (own schedule)
New flow:
02:30 systemd timer → backup-db-dump.sh (DB dumps)
03:00 scheduler → restic backup (Docker volumes + DB dumps)
↓ on completion (success or fail)
scheduler → cross-drive jobs (for apps with daily schedule)
(weekly jobs: only triggered on configured day, e.g., Sunday)
Implementation:
The controller's main scheduler loop already triggers DB dump and restic backup in sequence. After the restic backup completes, add:
// After restic backup completes
if crossDriveRunner != nil {
schedule := "daily"
if time.Now().Weekday() == time.Sunday {
// Also run weekly jobs on Sunday
crossDriveRunner.RunAllScheduled(ctx, "weekly")
}
crossDriveRunner.RunAllScheduled(ctx, schedule)
}
User-facing schedule options on deploy page (cross-drive config):
| Option | Meaning |
|---|---|
| Naponta | Runs every night after nightly backup completes |
| Hetente (vasárnap) | Runs only on Sunday night after nightly backup |
Remove the "Csak kézi indítás" (manual only) schedule option. If a user doesn't want automated cross-drive, they can disable the toggle. Manual trigger ("Futtatás most") remains available regardless.
Weekly + daily DB consistency:
When the user selects "Hetente" for cross-drive backup, show an informational note:
ℹ Heti mentés esetén visszaállításkor az adatbázis is a mentés napjára
áll vissza a konzisztencia érdekében. A mentés napja és a visszaállítás
között keletkezett adatbázis-változások elvesznek (max. 7 nap).
Restore implication: When restoring user data from a weekly cross-drive snapshot, the restore process should also restore the DB dump from the same date (matching by timestamp). This ensures DB ↔ file consistency.
4. Cross-Drive Destination Validation
Replace the current binary IsMountPoint || !IsWritable check with tiered validation.
New function: internal/system/mounts_linux.go
type DestinationHealth struct {
Exists bool
Writable bool
MountPoint bool // different device from parent
SystemDrive bool // on same device as /
UsedPercent float64 // disk usage percentage
FreeGB float64
Warning string // human-readable warning (Hungarian)
Blocked bool // if true, backup should not run
Severity string // "ok", "warning", "critical"
}
func CheckBackupDestination(path string) DestinationHealth { ... }
Validation tiers:
| Condition | Severity | Blocked? | Message |
|---|---|---|---|
| Path doesn't exist | critical | yes | "A cél tárhely nem létezik" |
| Path not writable | critical | yes | "A cél tárhely nem írható" |
| Same block device as source | critical | yes | "A forrás és a cél azonos meghajtón van" |
| Path is on system drive (/) | warning | no | "Mentés a rendszermeghajtóra — meghajtóhiba esetén mindkét másolat elveszhet" |
| Destination >90% full | warning | no | "A mentési meghajtó majdnem megtelt (X% használt)" |
| Destination >95% full | critical | yes | "A mentési meghajtó megtelt (X%)" |
| Mount point, writable, healthy | ok | no | "" |
Same block device detection:
func isSameBlockDevice(pathA, pathB string) bool {
var statA, statB syscall.Stat_t
if err := syscall.Stat(pathA, &statA); err != nil {
return false
}
if err := syscall.Stat(pathB, &statB); err != nil {
return false
}
return statA.Dev == statB.Dev
}
This replaces the current IsMountPoint() check in the destination dropdown filtering and the health warning logic.
5. Drive Disconnection Notifications
Trigger: During the nightly backup chain, before running cross-drive jobs, check all configured destinations. If any are unreachable:
for _, app := range appsWithCrossDrive {
health := system.CheckBackupDestination(app.DestinationPath)
if health.Blocked {
notifier.NotifyBackupFailed(
"Mentési meghajtó nem elérhető",
fmt.Sprintf("%s mentési célja (%s) nem elérhető: %s",
app.DisplayName, app.DestinationPath, health.Warning),
)
}
}
Also check on controller startup (in main.go boot sequence) — if a configured destination is unreachable, log a warning and set the status. The backup page will show it immediately.
Also check in the 15-minute hub report — include destination health in the status payload so the hub dashboard shows drive problems centrally.
Implementation Steps
Step 1: Fix duplicate apps bug (Bug 1)
Files:
internal/backup/backup.go—GetFullStatus(): return deep copy with nil cross-drive/unconfigured slicesinternal/web/handlers.go—backupsHandler(): verify no mutation of cached data
Test: Load backup page 5 times → unconfigured apps count stays consistent.
Step 2: Fix destination validation (Bug 2 + Architecture item 4)
Files:
internal/system/mounts_linux.go— addCheckBackupDestination(),isSameBlockDevice()internal/system/mounts_other.go— add stubsinternal/web/handlers.go—deployHandler(): replaceIsMountPoint || !IsWritablewithCheckBackupDestination()internal/web/handlers.go—backupsHandler(): same replacement for cross-drive warningsinternal/web/templates/deploy.html— update warning display to handlewarningvscriticalseverity
Test:
- Configure cross-drive to
hdd_placeholder→ shows yellow warning about system drive, backup still allowed - Configure cross-drive to
hdd_1(external) → green, no warning - Disconnect external drive → shows red "nem elérhető"
- Configure source and dest on same drive → blocked
Step 3: Remove dead BackupEnabled toggle (Bug 3)
Files:
internal/web/handlers.go— removesettingsAppBackupHandler()internal/web/server.go— removePOST /settings/app-backuprouteinternal/web/templates/backups.html— remove bulk toggle form, remove "Aktív/Inaktív" badges from old section
Test: Backup page loads without toggle form. No 500 errors.
Step 4: Unified per-app backup rows (Architecture item 2)
New struct:
// AppBackupRow holds all backup information for one app, used by the template.
type AppBackupRow struct {
StackName string
DisplayName string
Status string // "green", "yellow", "red", "auto"
StatusText string // short Hungarian text for the indicator tooltip
// Storage info (HDD apps only)
HasHDDData bool
StorageLabel string
HDDSizeHuman string
// Backup layers
DBBackup *BackupLayerInfo // nil if app has no DB
VolumeBackup *BackupLayerInfo // always present for deployed apps
UserDataBackup *BackupLayerInfo // nil if app has no HDD data
// Warnings (shown in expanded view)
Warnings []string
}
type BackupLayerInfo struct {
Type string // "db", "volume", "userdata"
Label string // "Adatbázis mentés", "Docker kötetek", "Felhasználói adatok"
Auto bool // true if no user config needed
Configured bool // true if backup is set up for this layer
Method string // "restic", "rsync", "" (for auto)
Destination string // label of destination (for cross-drive)
Schedule string // "Naponta", "Hetente", "" (for auto)
LastRun string // formatted timestamp
LastStatus string // "ok", "error", "running", ""
LastError string // error message if failed
StatusBadge string // "Sikeres", "Hiba", "Fut...", "Auto", "—"
ConfigURL string // link to configure (deploy page)
}
Files:
internal/web/handlers.go— newbuildAppBackupRows()function, called frombackupsHandler()internal/web/templates/backups.html— replace both sections with unified expandable rowsinternal/web/templates/style.css— new styles for expandable rows, status indicators, layer detail grid
Template structure (backups.html):
<div class="backup-section-card">
<h3>Alkalmazások mentési állapota</h3>
{{if .NoUserDataBackupWarning}}
<div class="alert alert-error" style="margin-bottom:1.5rem">
Felhasználói adatokról nincs biztonsági mentés.
A szerveren tárolt fotók, dokumentumok és egyéb fájlok jelenleg
csak egy példányban léteznek.
<a href="/settings">Meghajtó beállítása</a>
</div>
{{end}}
{{range .AppBackupRows}}
<div class="app-backup-row" data-status="{{.Status}}">
<div class="app-backup-row-header" onclick="toggleBackupDetail(this)">
<span class="status-dot status-{{.Status}}" title="{{.StatusText}}"></span>
<span class="app-backup-row-name">{{.DisplayName}}</span>
<div class="app-backup-row-meta">
{{if .HasHDDData}}
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
<span class="mono app-backup-size">{{.HDDSizeHuman}}</span>
{{else}}
<span class="meta-badge meta-badge-auto">Auto</span>
{{end}}
</div>
<span class="expand-icon">▶</span>
</div>
<div class="app-backup-row-detail" style="display:none">
<!-- DB layer -->
<div class="backup-layer-row">
<span class="layer-label">Adatbázis mentés</span>
{{if .DBBackup}}
<span class="layer-badge">Auto</span>
{{if .DBBackup.LastRun}}<span class="layer-last">{{.DBBackup.LastRun}}</span>{{end}}
{{else}}
<span class="layer-na">— (nincs adatbázis)</span>
{{end}}
</div>
<!-- Volume layer -->
<div class="backup-layer-row">
<span class="layer-label">Docker kötetek</span>
<span class="layer-badge">Auto</span>
{{if .VolumeBackup.LastRun}}<span class="layer-last">{{.VolumeBackup.LastRun}}</span>{{end}}
</div>
<!-- User data layer -->
<div class="backup-layer-row">
<span class="layer-label">Felhasználói adatok</span>
{{if .UserDataBackup}}
{{if .UserDataBackup.Configured}}
<span class="layer-method">{{.UserDataBackup.Method}}</span>
<span class="layer-dest">→ {{.UserDataBackup.Destination}}</span>
<span class="layer-schedule">{{.UserDataBackup.Schedule}}</span>
{{if .UserDataBackup.LastRun}}
<span class="layer-last">{{.UserDataBackup.LastRun}}
{{.UserDataBackup.StatusBadge}}</span>
{{end}}
<div class="layer-actions">
<a href="/stacks/{{$.StackName}}/deploy" class="btn btn-xs">Beallitas</a>
<button class="btn btn-xs btn-outline"
onclick="triggerCrossDriveBackup('{{$.StackName}}', this)">
Futtatás most</button>
</div>
{{else}}
<span class="layer-unconfigured">Nincs beállítva</span>
<a href="/stacks/{{$.StackName}}/deploy">Beállítás →</a>
{{end}}
{{else}}
<span class="layer-na">— (nincs HDD adat)</span>
{{end}}
</div>
<!-- Warnings -->
{{range .Warnings}}
<div class="backup-layer-warning">{{.}}</div>
{{end}}
</div>
</div>
{{end}}
</div>
JS for expand/collapse:
function toggleBackupDetail(header) {
const detail = header.nextElementSibling;
const icon = header.querySelector('.expand-icon');
if (detail.style.display === 'none') {
detail.style.display = 'block';
icon.textContent = '▼';
} else {
detail.style.display = 'none';
icon.textContent = '▶';
}
}
Test:
- All deployed apps appear in list
- Apps with HDD data show storage label + size
- Apps without HDD data show "Auto" badge
- Expanding shows correct layer info
- Red dot for unconfigured user data
- Green dot for fully covered apps
Step 5: Sequential backup chaining (Architecture item 3)
Files:
cmd/controller/main.go— modify nightly scheduler to chain cross-drive after resticinternal/backup/crossdrive.go—RunAllScheduled()already exists, verify it handles "daily" and "weekly" correctlyinternal/web/templates/deploy.html— update schedule dropdown (remove "Csak kézi indítás", add weekly consistency note)
Current scheduler code (conceptual location in main.go):
// In the nightly backup goroutine
go func() {
// ... wait for scheduled time ...
// Phase 1: DB dumps
backupMgr.RunDBDump(ctx)
// Phase 2: Restic snapshot
backupMgr.RunBackup(ctx)
// Phase 3: Cross-drive (NEW — chained)
if crossDriveRunner != nil {
crossDriveRunner.RunAllScheduled(ctx, "daily")
if isWeeklyTriggerDay() { // e.g., Sunday
crossDriveRunner.RunAllScheduled(ctx, "weekly")
}
}
}()
Deploy page schedule dropdown update:
<select name="cross_drive_schedule" ...>
<option value="daily">Naponta (az éjszakai mentés után)</option>
<option value="weekly">Hetente, vasárnap (az éjszakai mentés után)</option>
</select>
<!-- Show note for weekly -->
<div class="form-hint weekly-note" style="display:none">
Heti mentés esetén visszaállításkor az adatbázis is a mentés napjára
áll vissza a konzisztencia érdekében. A mentés napja és a visszaállítás
között keletkezett adatbázis-változások elvesznek (max. 7 nap).
</div>
Test:
- Set app to daily → cross-drive runs after nightly backup every night
- Set app to weekly → cross-drive runs only on Sunday night
- Manual trigger still works regardless of schedule
Step 6: Drive disconnection detection & notification (Architecture item 5)
Files:
cmd/controller/main.go— startup drive checkinternal/backup/crossdrive.go— pre-run destination check, notification on failureinternal/web/handlers.go—backupsHandler(): include destination health in row data
Startup check (main.go):
// After settings are loaded, check all configured cross-drive destinations
crossConfigs := sett.GetAllCrossDriveConfigs()
for appName, cfg := range crossConfigs {
if cfg == nil || !cfg.Enabled || cfg.DestinationPath == "" {
continue
}
health := system.CheckBackupDestination(cfg.DestinationPath)
if health.Blocked {
logger.Printf("[WARN] Backup destination for %s unreachable: %s (%s)",
appName, cfg.DestinationPath, health.Warning)
}
}
Pre-run check (crossdrive.go RunAppBackup):
func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) error {
cfg := r.sett.GetCrossDriveConfig(stackName)
// ... existing checks ...
// NEW: Tiered destination validation
health := system.CheckBackupDestination(cfg.DestinationPath)
if health.Blocked {
r.sett.UpdateCrossDriveStatus(stackName, "error", health.Warning, 0, "")
// Send notification
if r.notifier != nil {
r.notifier.NotifyBackupFailed(
"Mentési meghajtó nem elérhető",
fmt.Sprintf("%s mentési célja (%s): %s", stackName, cfg.DestinationPath, health.Warning),
)
}
return fmt.Errorf("destination blocked: %s", health.Warning)
}
// ... proceed with backup ...
}
Hub report inclusion:
Add destination health summary to the 15-minute hub status report payload so the central dashboard can show drive problems.
Test:
- Disconnect external USB drive → backup page shows red status, notification sent
- Reconnect → status recovers on next page load
- Controller restart with disconnected drive → warning in logs
Step 7: Deploy page updates
Files:
internal/web/templates/deploy.html— update cross-drive sectioninternal/web/handlers.go—deployHandler(): useCheckBackupDestination()for warnings
Changes:
- Remove "Csak kézi indítás" schedule option
- Add weekly consistency note (shown/hidden via JS based on dropdown)
- Replace flat error message with tiered warning/critical display
- Add warning on app deploy page if app has HDD data but no cross-drive configured:
{{if and .StorageInfo (not .CrossDriveConfig)}}
<div class="alert alert-warning">
Az alkalmazás felhasználói adatairól nincs biztonsági mentés.
Állítsd be alább, vagy csatlakoztass egy
<a href="/settings">külső meghajtót</a>.
</div>
{{end}}
Step 8: Version bump & testing
- Update CHANGELOG.md, CONTEXT.md
- Bump version (suggest 0.12.0 — this is a significant feature change)
- Build + deploy to demo-felhom.eu
- Full test cycle:
- Backup page loads without duplicates ✓
- All deployed apps appear in unified list ✓
- Expandable rows show correct layer info ✓
- Apps without HDD data show "Auto" ✓
- Immich (HDD data, cross-drive configured) → green ✓
- Paperless/RomM (HDD data, no cross-drive) → red ✓
- Configure cross-drive to hdd_placeholder → yellow warning ✓
- Configure cross-drive to hdd_1 → green ✓
- Cross-drive runs after nightly backup (check logs) ✓
- Disconnect external drive → red on backup page, notification sent ✓
- Manual trigger still works ✓
Files Summary
New files:
- None (all changes in existing files)
Modified files:
| File | Changes |
|---|---|
internal/backup/backup.go |
Deep copy in GetFullStatus(), remove cross-drive/unconfigured population from cache |
internal/backup/crossdrive.go |
Pre-run destination validation, notification on blocked destination |
internal/system/mounts_linux.go |
CheckBackupDestination(), isSameBlockDevice() |
internal/system/mounts_other.go |
Stubs for new functions |
internal/web/handlers.go |
buildAppBackupRows(), remove settingsAppBackupHandler(), updated deployHandler() validation |
internal/web/server.go |
Remove POST /settings/app-backup route |
internal/web/templates/backups.html |
Complete rewrite of app backup sections → unified expandable rows |
internal/web/templates/deploy.html |
Updated schedule dropdown, weekly note, tiered warnings, unconfigured warning |
internal/web/templates/style.css |
New styles for expandable rows, status dots, layer detail grid |
internal/settings/settings.go |
Deprecate SetAppBackupBulk(), IsAppBackupEnabled() |
cmd/controller/main.go |
Sequential chaining in scheduler, startup drive check |
Design Decisions
Why merge the two backup sections?
Separation between "Alkalmazás adatok" and "Másolatok másik meghajtóra" forced users to mentally correlate information across two lists. A single per-app row with expandable detail gives complete backup coverage at a glance without cross-referencing.
Why remove the BackupEnabled toggle?
It wrote to settings.json but nothing consumed the flag. No Backrest deployment exists; the nightly restic script doesn't check settings.json. Showing a toggle that doesn't affect anything is worse than showing nothing — it gives false confidence.
Why sequential chaining instead of independent schedules?
DB ↔ file consistency. If cross-drive runs at a different time than the DB dump, a restore could produce an inconsistent state (DB references files that don't exist, or vice versa). Chaining ensures DB dump → restic → cross-drive all happen in one window, and a "restore to date X" operation can grab all three from the same night.
Why allow system drive as backup target with warning?
For small data (configs, documents), having a second copy on the system SSD is better than no copy at all. For large data (Immich photos), the system drive may not have enough space and a drive failure would lose both copies. The warning lets users make an informed choice rather than blocking a useful configuration for small-data apps.
Why "Hetente" restores also roll back the DB?
A weekly user data snapshot from Sunday combined with a daily DB from Wednesday would create inconsistency — the DB would reference files from Mon-Wed that don't exist in Sunday's snapshot. Rolling both back to the same point (Sunday night) guarantees consistency at the cost of losing up to 7 days of DB changes. This is the correct trade-off for data integrity.