8e61cd7ec4
Add structured operational logging at INFO, WARN, and ERROR levels to every controller module. Standardize custom prefixes ([GEO], [SCHED], [SYNC]) to use [INFO/WARN/ERROR] [module] format. Fix misleveled logs (WARN->ERROR for data loss scenarios, WARN->INFO for routine operations). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
207 lines
5.8 KiB
Go
207 lines
5.8 KiB
Go
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] 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")
|
|
restoreStart := time.Now()
|
|
|
|
s.restoreMu.RLock()
|
|
plan := s.restorePlan
|
|
s.restoreMu.RUnlock()
|
|
if plan == nil {
|
|
s.logger.Println("[WARN] 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] 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] 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] 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] 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] 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
|
|
}
|