fix(api): F1 — register GET /api/backup/snapshots so the restore panel can populate
The backups.html restore panel fetched /api/backup/snapshots (a restic-era route that no longer existed), so the snapshot dropdown never populated and the "Visszaállítás indítása" button could never enable — customers could not restore anything from the UI (drill finding F1, DRILL-appdata-restore-2026-07-04). - backup.Manager.ListRestorePoints: the keep-side restore has exactly ONE restore point per app (the current recovery unit); time = newest artifact mtime among manifest/db-dumps/volume-dumps; tier always 1 (Tier-2 copies are NOT restorable via POST /backup/restore — never listed); drive_label from the storage registry, empty for the SSD fallback. - api: /backup/snapshots route + validStackParam guard (same semantics as web.validStackName; traversal → 400, unknown stack → 404, no unit → ok+[]). - Tests dispatch through Router.ServeHTTP (the bug WAS a missing route) + unit tests for newest-mtime/label/empty semantics. Companion red-proof: hollow always-[] implementation fails TestListRestorePoints_UnitOnDisk + TestBackupSnapshots_UnitOnDisk (verified, reverted). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -252,6 +252,10 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
case path == "/backup/status" && req.Method == http.MethodGet:
|
||||
r.backupStatus(w, req)
|
||||
|
||||
// GET /api/backup/snapshots?stack=<name> — restorable keep-side backups for the restore panel
|
||||
case path == "/backup/snapshots" && req.Method == http.MethodGet:
|
||||
r.backupSnapshots(w, req)
|
||||
|
||||
// POST /api/backup/run
|
||||
case path == "/backup/run" && req.Method == http.MethodPost:
|
||||
r.triggerBackup(w, req)
|
||||
@@ -831,6 +835,44 @@ func (r *Router) backupStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: data})
|
||||
}
|
||||
|
||||
// validStackParam reports whether a stack name from a request is a safe single path segment
|
||||
// (same semantics as web.validStackName — see internal/web/validate.go; duplicated here because
|
||||
// api ↔ web would be a circular import). Rejects traversal/escape so the name can never become
|
||||
// a filesystem path outside the app's namespace root.
|
||||
func validStackParam(name string) bool {
|
||||
if name == "" || name == "." || name == ".." {
|
||||
return false
|
||||
}
|
||||
if strings.ContainsAny(name, "/\\\x00") {
|
||||
return false
|
||||
}
|
||||
return name == filepath.Clean(name)
|
||||
}
|
||||
|
||||
// backupSnapshots lists the restorable keep-side backups for one app — the data source of the
|
||||
// /backups restore panel's snapshot dropdown (F1: this route was fetched by the template but never
|
||||
// existed, so the dropdown could never populate and the restore button never enabled).
|
||||
func (r *Router) backupSnapshots(w http.ResponseWriter, req *http.Request) {
|
||||
stack := req.URL.Query().Get("stack")
|
||||
if !validStackParam(stack) {
|
||||
r.dbg("backupSnapshots: invalid stack param %q", stack)
|
||||
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid stack name"})
|
||||
return
|
||||
}
|
||||
if r.backupMgr == nil {
|
||||
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "Backup not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
points, found := r.backupMgr.ListRestorePoints(stack)
|
||||
if !found {
|
||||
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: "stack not found: " + stack})
|
||||
return
|
||||
}
|
||||
r.dbg("backupSnapshots: stack=%s points=%d", stack, len(points))
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: points})
|
||||
}
|
||||
|
||||
// triggerBackup runs the app-data database dumps. Disk-tier (restic) backup has
|
||||
// moved to the host agent (slice 8C).
|
||||
func (r *Router) triggerBackup(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user