95c821deb2
Add detailed [DEBUG] logging to every controller module when logging.level is set to "debug". Each module with stateful debug uses SetDebug(bool) wired from main.go. Covers stacks, backup, cloudflare, integrations, system, monitor, settings, scheduler, web handlers, storage, metrics, API, selfupdate, and assets. Also includes the app export/import (.fab bundles) feature from v0.32.0 and its debug page integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
206 lines
5.8 KiB
Go
206 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
|
|
}
|
|
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
|
|
}
|