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)
- App dropdown — only apps with
HasHDDData && BackupEnabled(filters out auto-only apps) - Snapshot dropdown — loads ALL snapshots from
/api/backup/snapshots(no filtering by app) - Path display — shows HDD paths that will be restored (technical, confusing for customers)
- Warning block — "FELÜLÍRJA a jelenlegi adatait... NEM vonható vissza!... Javasoljuk az alkalmazás leállítását"
- Confirmation checkbox — "Megértettem, visszaállítás saját felelősségre"
- Restore button → POST
/backup/restorewithstack_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
- Snapshot selector shows ALL snapshots — user can pick one that predates their app's backup setup → restore fails or does nothing
- Path display is confusing — customers don't need to see
/mnt/hdd_1/storage/immich/ - Warning is intimidating — multiple strong statements, feels dangerous
- No auto-stop — just "recommended" to stop the app, user can forget
- 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-pathsdiv 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:00instead ofa3f2b1 — 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)