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.isDebug() { s.logger.Printf("[DEBUG] [web] restorePageHandler: rendering restore page") } s.restoreMu.RLock() plan := s.restorePlan if plan == nil { s.restoreMu.RUnlock() if s.isDebug() { s.logger.Printf("[DEBUG] [web] restorePageHandler: no restore plan, redirecting to /") } http.Redirect(w, r, "/", http.StatusFound) return } // Snapshot all needed fields under lock before rendering customerID := plan.CustomerID timestamp := plan.Timestamp apps := plan.GetApps() drives := make([]backup.DriveInfo, len(plan.Drives)) copy(drives, plan.Drives) status := plan.GetStatus() s.restoreMu.RUnlock() if s.isDebug() { s.logger.Printf("[DEBUG] [web] restorePageHandler: customer=%s apps=%d drives=%d status=%s", customerID, len(apps), len(drives), status) } 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": customerID, "Timestamp": timestamp, "Apps": apps, "Drives": drives, "PlanStatus": status, } s.executeTemplate(w, r, "restore", data) } // apiRestoreStatus returns the current restore plan status as JSON. func (s *Server) apiRestoreStatus(w http.ResponseWriter, r *http.Request) { if s.isDebug() { s.logger.Printf("[DEBUG] [web] apiRestoreStatus: status poll from %s", r.RemoteAddr) } s.restoreMu.RLock() plan := s.restorePlan if plan == nil { s.restoreMu.RUnlock() jsonError(w, "not in restore mode", http.StatusBadRequest) return } snapshot := plan.Snapshot() s.restoreMu.RUnlock() w.Header().Set("Content-Type", "application/json; charset=utf-8") json.NewEncoder(w).Encode(snapshot) } // apiRestoreAll starts restoring all pending apps sequentially. func (s *Server) apiRestoreAll(w http.ResponseWriter, r *http.Request) { if s.isDebug() { s.logger.Printf("[DEBUG] [web] apiRestoreAll: restore-all requested from %s", r.RemoteAddr) } s.restoreMu.RLock() plan := s.restorePlan s.restoreMu.RUnlock() if plan == nil { jsonError(w, "not in restore mode", http.StatusBadRequest) return } if !plan.TryStartRestore() { if s.isDebug() { s.logger.Printf("[DEBUG] [web] apiRestoreAll: restore already in progress, rejecting") } jsonError(w, "restore already in progress", http.StatusConflict) return } s.logger.Printf("[INFO] [web] Restore-all initiated from %s", r.RemoteAddr) 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.isDebug() { s.logger.Printf("[DEBUG] [web] apiRestoreSkip: skip requested from %s", r.RemoteAddr) } s.restoreMu.RLock() plan := s.restorePlan s.restoreMu.RUnlock() if plan == nil { jsonError(w, "not in restore mode", http.StatusBadRequest) return } s.logger.Println("[INFO] [web] 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] [web] Starting DR restore for all apps") restoreStart := time.Now() s.restoreMu.RLock() plan := s.restorePlan s.restoreMu.RUnlock() if plan == nil { s.logger.Println("[WARN] [web] Restore plan cleared before execution could start") return } // Count pending apps and push DR start event pendingCount := 0 for _, app := range plan.Apps { if app.Status == "pending" { pendingCount++ } } if s.isDebug() { s.logger.Printf("[DEBUG] [web] executeAllRestores: %d pending apps to restore", pendingCount) } if s.notifier != nil { s.notifier.NotifyDRStarted(pendingCount) } successCount, failCount := 0, 0 for i := range plan.Apps { app := &plan.Apps[i] if app.Status != "pending" { continue } plan.UpdateApp(app.Name, "restoring", "") s.logger.Printf("[INFO] [web] Restoring app %s (%s)", app.Name, app.DisplayName) appStart := time.Now() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) err := backup.RestoreAppFromBackup(ctx, app, s.cfg.Paths.StacksDir, s.logger) cancel() if err != nil { plan.UpdateApp(app.Name, "failed", err.Error()) s.logger.Printf("[ERROR] [web] Restore failed for %s: %v", app.Name, err) if s.isDebug() { s.logger.Printf("[DEBUG] [web] executeAllRestores: app=%s failed after %s", app.Name, time.Since(appStart)) } failCount++ } else { plan.UpdateApp(app.Name, "done", "") s.logger.Printf("[INFO] [web] Restore completed for %s", app.Name) if s.isDebug() { s.logger.Printf("[DEBUG] [web] executeAllRestores: app=%s completed in %s", app.Name, time.Since(appStart)) } successCount++ } } plan.SetStatus("done") s.logger.Println("[INFO] [web] All app restores completed") if s.isDebug() { s.logger.Printf("[DEBUG] [web] executeAllRestores: total=%d success=%d fail=%d elapsed=%s", pendingCount, successCount, failCount, time.Since(restoreStart)) } // Push DR completion event if s.notifier != nil { s.notifier.NotifyDRCompleted(successCount, failCount) } // Re-scan stacks so dashboard picks up restored apps if s.stackMgr != nil { if err := s.stackMgr.ScanStacks(); err != nil { s.logger.Printf("[WARN] [web] Post-restore stack scan failed: %v", err) } } } // clearRestoreMode exits restore mode and returns to normal operation. func (s *Server) clearRestoreMode() { s.restoreMu.Lock() defer s.restoreMu.Unlock() s.restorePlan = nil }