Files
deploy-felhom-compose/TASK.md
T

8.5 KiB

0.12.2 - Bug 4: Restore Section ("Visszaállítás") Simplification — TASK.md

Current State

Backup architecture (what goes where)

Restic snapshots (daily, on SSD):

  • /opt/docker/stacks/ — all Docker configs, compose files, app.yaml
  • /opt/docker/db-dumps/ — DB dump SQL files
  • /opt/docker/felhom-controller/controller.yaml
  • App HDD data paths (if backup enabled) — e.g., /mnt/hdd_1/storage/immich/

Cross-drive backups (per-app, rsync/restic to secondary drive):

  • Copies app user data between drives (e.g., HDD→SSD or SSD→HDD)
  • Separate from restic — not part of snapshot restore

Key detail: SnapshotInfo.Paths []string from restic snapshots --json includes the backup source paths. Snapshots created BEFORE an app's HDD backup was enabled won't have that app's HDD paths listed. This can be used for filtering.

Current restore UI flow (backups.html Section 7)

  1. App dropdown — only apps with HasHDDData && BackupEnabled (filters out auto-only apps)
  2. Snapshot dropdown — loads ALL snapshots from /api/backup/snapshots (no filtering by app)
  3. Path display — shows HDD paths that will be restored (technical, confusing for customers)
  4. Warning block — "FELÜLÍRJA a jelenlegi adatait... NEM vonható vissza!... Javasoljuk az alkalmazás leállítását"
  5. Confirmation checkbox — "Megértettem, visszaállítás saját felelősségre"
  6. Restore button → POST /backup/restore with stack_name + snapshot_id

Current restore backend (RestoreApp in restore.go)

  • Validates backup enabled + snapshot exists
  • Resolves app's HDD paths via GetStackHDDMounts()
  • Runs restic restore {snapshot} --target / --include {path1} --include {path2}
  • Only restores HDD data — does NOT restore Docker configs or DB dumps

Problems

  1. Snapshot selector shows ALL snapshots — user can pick one that predates their app's backup setup → restore fails or does nothing
  2. Path display is confusing — customers don't need to see /mnt/hdd_1/storage/immich/
  3. Warning is intimidating — multiple strong statements, feels dangerous
  4. No auto-stop — just "recommended" to stop the app, user can forget
  5. DB-only restore is implicitly possible (pick old snapshot) but not clear what it does

Proposed Design

Simplified UI (3 elements instead of 6)

┌─────────────────────────────────────────────────┐
│  Visszaállítás                                   │
│                                                   │
│  Alkalmazás:  [▼ Immich                      ]   │
│  Pillanatkép: [▼ 2026-02-17 03:00 (a3f2b1)  ]   │
│                                                   │
│  ⚠ A visszaállítás felülírja az alkalmazás      │
│    jelenlegi adatait a kiválasztott mentés        │
│    állapotával. Az alkalmazás a folyamat során    │
│    automatikusan leáll és újraindul.             │
│                                                   │
│  ☐ Megértettem, visszaállítás indítása           │
│                                                   │
│  [ Visszaállítás indítása ]                      │
└─────────────────────────────────────────────────┘

Changes

# What Details
1 Filter snapshots by app When app selected, /api/backup/snapshots?stack={name} returns only snapshots whose Paths contain at least one of the app's HDD paths. Show snapshot time in human-friendly format, no raw IDs
2 Remove path display Customer doesn't need to see mount paths. The restore handler knows what to include
3 Single calm warning Replace 3-line warning + separate checkbox label with one concise block
4 Auto-stop + restart Restore handler stops the app's containers before restore, restarts after. Eliminates "javasoljuk leállítását" advice
5 Hide DB-only restore Not exposed in UI — support contacts Viktor for CLI-level restic restore

Implementation Plan

Backend changes

1. Filtered snapshots API (internal/web/api_router.go or router.go)

Extend GET /api/backup/snapshots with optional ?stack={name} query param:

func (r *Router) snapshotsHandler(w http.ResponseWriter, req *http.Request) {
    snapshots, err := r.backupMgr.ListSnapshots(50)
    // ... existing error handling ...
    
    // Filter by stack if requested
    if stackName := req.URL.Query().Get("stack"); stackName != "" {
        hddMounts := r.stackProvider.GetStackHDDMounts(stackName)
        if len(hddMounts) > 0 {
            snapshots = filterSnapshotsByPaths(snapshots, hddMounts)
        }
    }
    
    writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: snapshots})
}

func filterSnapshotsByPaths(snapshots []backup.SnapshotInfo, requiredPaths []string) []backup.SnapshotInfo {
    var filtered []backup.SnapshotInfo
    for _, snap := range snapshots {
        for _, req := range requiredPaths {
            for _, sp := range snap.Paths {
                if strings.HasPrefix(req, sp) || strings.HasPrefix(sp, req) {
                    filtered = append(filtered, snap)
                    goto next
                }
            }
        }
        next:
    }
    return filtered
}

2. Auto-stop/restart in RestoreApp (internal/backup/restore.go)

func (m *Manager) RestoreApp(stackName, snapshotID string) error {
    // ... existing validation ...
    
    // Stop the app's containers before restore
    if m.stackProvider != nil {
        m.logger.Printf("[INFO] Stopping %s for restore...", stackName)
        if err := m.stackProvider.StopStack(stackName); err != nil {
            m.logger.Printf("[WARN] Could not stop %s: %v (proceeding anyway)", stackName, err)
        }
    }
    
    // Execute restore (existing logic)
    if err := m.restic.RestoreAppData(snapshotID, hddMounts); err != nil {
        // Try to restart even on failure
        m.stackProvider.StartStack(stackName)
        return err
    }
    
    // Restart after successful restore
    if m.stackProvider != nil {
        m.logger.Printf("[INFO] Restarting %s after restore...", stackName)
        m.stackProvider.StartStack(stackName)
    }
    
    return nil
}

Frontend changes

3. Template simplification (backups.html Section 7)

  • Remove restore-paths div entirely
  • Replace warning text with single concise block
  • Keep app dropdown + snapshot dropdown + confirm checkbox + button
  • Update onRestoreAppChange() JS to call /api/backup/snapshots?stack={name}

4. Snapshot display format

  • Show: 2026-02-17 vasárnap 03:00 instead of a3f2b1 — 2026.02.17. 3:00:00
  • Keep snapshot ID in option.value, show human time in label

Design Decisions

D1: Snapshot dropdown format → Show date (id) — human-friendly time with short ID in parentheses for support debugging. E.g. 2026-02-17 vasárnap 03:00 (a3f2b1)

D2: Zero filtered snapshots → Show inline message instead of empty dropdown: "Még nincs mentés felhasználói adattal." (shortened from original)

D3: Auto-stop/restart → Yes, stacks must be stopped during restore. Check if StackProvider interface already exposes Stop/Start; extend it if not.

D4: Progress indication → Keep current simple behavior (button text change + page redirect) for v0.13.0. Async polling (like backup progress) is a future enhancement.

D5: Restore scope → User data (HDD paths) only. Docker configs and DB dumps are NOT restored — that's a disaster recovery task handled via CLI/support.


Files to modify

File Changes
internal/web/api_router.go (or router.go) Add ?stack= filter to snapshots endpoint
internal/backup/restore.go Add auto-stop/restart logic
internal/backup/types.go Add filterSnapshotsByPaths helper if needed
internal/web/templates/backups.html Simplify restore section HTML
internal/web/templates/backups.html (JS) Update onRestoreAppChange() to pass stack param
internal/stacks/ (maybe) Ensure StackProvider exposes Stop/Start if needed for Q3

Estimated effort

  • Backend: ~2 hours (snapshot filtering + auto-stop/restart + interface extension)
  • Frontend: ~1 hour (template simplification + JS update)
  • Testing: ~1 hour (restore flow with different apps/snapshots)