# 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: ```go 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`) ```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)