package web import ( "context" "encoding/json" "net/http" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/backup" ) // restorePageHandler renders the full-page DR restore UI. func (s *Server) restorePageHandler(w http.ResponseWriter, r *http.Request) { if s.restorePlan == nil { http.Redirect(w, r, "/", http.StatusFound) return } data := map[string]interface{}{ "Title": "Katasztrófa utáni visszaállítás", "CustomerName": s.cfg.Customer.Name, "Domain": s.cfg.Customer.Domain, "Version": s.version, "CustomerID": s.restorePlan.CustomerID, "Timestamp": s.restorePlan.Timestamp, "Apps": s.restorePlan.GetApps(), "Drives": s.restorePlan.Drives, "PlanStatus": s.restorePlan.Status, } s.render(w, "restore", data) } // apiRestoreStatus returns the current restore plan status as JSON. func (s *Server) apiRestoreStatus(w http.ResponseWriter, r *http.Request) { if s.restorePlan == nil { jsonError(w, "not in restore mode", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(s.restorePlan.Snapshot()) } // apiRestoreAll starts restoring all pending apps sequentially. func (s *Server) apiRestoreAll(w http.ResponseWriter, r *http.Request) { if s.restorePlan == nil { jsonError(w, "not in restore mode", http.StatusBadRequest) return } if s.restorePlan.Status == "restoring" { jsonError(w, "restore already in progress", http.StatusConflict) return } s.restorePlan.Status = "restoring" go s.executeAllRestores() jsonResponse(w, map[string]interface{}{ "ok": true, "message": "Visszaállítás elindítva", }) } // apiRestoreSkip exits restore mode without restoring. func (s *Server) apiRestoreSkip(w http.ResponseWriter, r *http.Request) { if s.restorePlan == nil { jsonError(w, "not in restore mode", http.StatusBadRequest) return } s.logger.Println("[INFO] User skipped DR restore — entering normal mode") s.clearRestoreMode() jsonResponse(w, map[string]interface{}{ "ok": true, "message": "Visszaállítás kihagyva", }) } // executeAllRestores runs the restore for each pending app sequentially. func (s *Server) executeAllRestores() { s.logger.Println("[INFO] Starting DR restore for all apps") for i := range s.restorePlan.Apps { app := &s.restorePlan.Apps[i] if app.Status != "pending" { continue } s.restorePlan.UpdateApp(app.Name, "restoring", "") s.logger.Printf("[INFO] Restoring app %s (%s)", app.Name, app.DisplayName) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) err := backup.RestoreAppFromBackup(ctx, app, s.cfg.Paths.StacksDir, s.logger) cancel() if err != nil { s.restorePlan.UpdateApp(app.Name, "failed", err.Error()) s.logger.Printf("[ERROR] Restore failed for %s: %v", app.Name, err) } else { s.restorePlan.UpdateApp(app.Name, "done", "") s.logger.Printf("[INFO] Restore completed for %s", app.Name) } } s.restorePlan.Status = "done" s.logger.Println("[INFO] All app restores completed") // Re-scan stacks so dashboard picks up restored apps if s.stackMgr != nil { if err := s.stackMgr.ScanStacks(); err != nil { s.logger.Printf("[WARN] Post-restore stack scan failed: %v", err) } } // Auto-clear restore mode after a brief delay so user can see final status go func() { time.Sleep(5 * time.Second) // Only auto-clear if user hasn't already navigated away if s.restorePlan != nil && s.restorePlan.AllDone() { // Keep plan visible — user clicks "continue to dashboard" to clear } }() } // clearRestoreMode exits restore mode and returns to normal operation. func (s *Server) clearRestoreMode() { s.restoreMu.Lock() defer s.restoreMu.Unlock() s.restorePlan = nil }