v0.25.0 — Debug page: operator testing & diagnostics dashboard
Debug-mode-only dashboard (/debug) with 8 collapsible sections: system diagnostics, notification testing, backup triggers, storage simulation, hub & connectivity, self-update dry-run, DR/setup wizard, and in-memory log viewer. Migrates debug dump from API router to web server. Adds ring buffer log capture, storage disconnect simulation, event history tracking, and cross-drive/self-update test methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,687 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
)
|
||||
|
||||
// DebugCallbacks holds functions that need main.go wiring (modules not directly on Server).
|
||||
type DebugCallbacks struct {
|
||||
TriggerHubReportPush func() error
|
||||
TriggerHubInfraPush func() error
|
||||
TriggerLocalInfraWrite func() error
|
||||
TriggerSetupMode func() error
|
||||
HubConnectivityTest func() (statusCode int, latencyMs int64, err error)
|
||||
GiteaConnectivityTest func() (statusCode int, latencyMs int64, err error)
|
||||
}
|
||||
|
||||
// debugPageHandler renders the debug dashboard page.
|
||||
func (s *Server) debugPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.baseData("debug", "Debug")
|
||||
s.executeTemplate(w, r, "debug", data)
|
||||
}
|
||||
|
||||
// handleDebugAPI dispatches /api/debug/* routes.
|
||||
func (s *Server) handleDebugAPI(w http.ResponseWriter, r *http.Request) {
|
||||
subpath := strings.TrimPrefix(r.URL.Path, "/api/debug/")
|
||||
|
||||
switch {
|
||||
// Section 1: Diagnostic dump
|
||||
case subpath == "dump" && r.Method == http.MethodGet:
|
||||
s.debugDump(w, r)
|
||||
|
||||
// Section 2: Notification & Event testing
|
||||
case subpath == "event/test" && r.Method == http.MethodPost:
|
||||
s.debugTestEvent(w, r)
|
||||
case subpath == "event/history" && r.Method == http.MethodGet:
|
||||
s.debugEventHistory(w, r)
|
||||
|
||||
// Section 3: Backup testing
|
||||
case subpath == "backup/dbdump" && r.Method == http.MethodPost:
|
||||
s.debugTriggerDBDump(w, r)
|
||||
case subpath == "backup/crossdrive" && r.Method == http.MethodPost:
|
||||
s.debugTriggerCrossDrive(w, r)
|
||||
case subpath == "backup/integrity" && r.Method == http.MethodPost:
|
||||
s.debugTriggerIntegrity(w, r)
|
||||
case subpath == "backup/infra" && r.Method == http.MethodPost:
|
||||
s.debugTriggerInfraBackup(w, r)
|
||||
|
||||
// Section 4: Storage simulation
|
||||
case subpath == "storage/simulate-disconnect" && r.Method == http.MethodPost:
|
||||
s.debugSimulateDisconnect(w, r)
|
||||
case subpath == "storage/simulate-reconnect" && r.Method == http.MethodPost:
|
||||
s.debugSimulateReconnect(w, r)
|
||||
case subpath == "storage/watchdog-status" && r.Method == http.MethodGet:
|
||||
s.debugWatchdogStatus(w, r)
|
||||
|
||||
// Section 5: Hub & connectivity
|
||||
case subpath == "hub/push" && r.Method == http.MethodPost:
|
||||
s.debugHubPush(w, r)
|
||||
case subpath == "hub/infra-push" && r.Method == http.MethodPost:
|
||||
s.debugHubInfraPush(w, r)
|
||||
case subpath == "hub/test-connectivity" && r.Method == http.MethodPost:
|
||||
s.debugHubConnectivity(w, r)
|
||||
case subpath == "hub/preferences-sync" && r.Method == http.MethodPost:
|
||||
s.debugPreferencesSync(w, r)
|
||||
case subpath == "gitea/test-connectivity" && r.Method == http.MethodPost:
|
||||
s.debugGiteaConnectivity(w, r)
|
||||
|
||||
// Section 6: Self-update
|
||||
case subpath == "selfupdate/dry-run" && r.Method == http.MethodPost:
|
||||
s.debugSelfUpdateDryRun(w, r)
|
||||
|
||||
// Section 7: DR / Setup
|
||||
case subpath == "dr/trigger-setup" && r.Method == http.MethodPost:
|
||||
s.debugTriggerSetupWizard(w, r)
|
||||
case subpath == "dr/infra-status" && r.Method == http.MethodGet:
|
||||
s.debugInfraBackupStatus(w, r)
|
||||
|
||||
// Section 8: Log viewer
|
||||
case subpath == "logs" && r.Method == http.MethodGet:
|
||||
s.debugLogBuffer(w, r)
|
||||
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// writeDebugJSON writes a standard JSON response for debug endpoints.
|
||||
func writeDebugJSON(w http.ResponseWriter, status int, ok bool, message string, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
resp := map[string]interface{}{
|
||||
"ok": ok,
|
||||
}
|
||||
if message != "" {
|
||||
if ok {
|
||||
resp["message"] = message
|
||||
} else {
|
||||
resp["error"] = message
|
||||
}
|
||||
}
|
||||
if data != nil {
|
||||
resp["data"] = data
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// ── Section 1: Diagnostic dump ──────────────────────────────────────
|
||||
|
||||
func (s *Server) debugDump(w http.ResponseWriter, r *http.Request) {
|
||||
dump := make(map[string]interface{})
|
||||
|
||||
// Controller info
|
||||
configHash := ""
|
||||
configPath := s.cfg.Paths.DataDir // approximate; configPath isn't on Server
|
||||
if data, err := os.ReadFile(filepath.Join(filepath.Dir(configPath), "controller.yaml")); err == nil {
|
||||
h := sha256.Sum256(data)
|
||||
configHash = hex.EncodeToString(h[:])
|
||||
}
|
||||
dump["controller"] = map[string]interface{}{
|
||||
"version": s.version,
|
||||
"uptime_seconds": int(time.Since(s.startTime).Seconds()),
|
||||
"config_hash": configHash,
|
||||
"logging_level": s.cfg.Logging.Level,
|
||||
"pid": os.Getpid(),
|
||||
}
|
||||
|
||||
// Storage
|
||||
storagePaths := s.settings.GetStoragePaths()
|
||||
storageEntries := make([]map[string]interface{}, 0, len(storagePaths))
|
||||
for _, sp := range storagePaths {
|
||||
entry := map[string]interface{}{
|
||||
"path": sp.Path,
|
||||
"label": sp.Label,
|
||||
"disconnected": sp.Disconnected,
|
||||
"decommissioned": sp.Decommissioned,
|
||||
}
|
||||
if !sp.Disconnected && !sp.Decommissioned {
|
||||
if di := system.GetDiskUsage(sp.Path); di != nil {
|
||||
entry["total_gb"] = di.TotalGB
|
||||
entry["used_gb"] = di.UsedGB
|
||||
entry["used_percent"] = di.UsedPercent
|
||||
}
|
||||
}
|
||||
storageEntries = append(storageEntries, entry)
|
||||
}
|
||||
dump["storage"] = storageEntries
|
||||
|
||||
// Stacks
|
||||
allStacks := s.stackMgr.GetStacks()
|
||||
deployed := 0
|
||||
running := 0
|
||||
stopped := 0
|
||||
stackList := make([]map[string]interface{}, 0)
|
||||
for _, st := range allStacks {
|
||||
if !st.Deployed {
|
||||
continue
|
||||
}
|
||||
deployed++
|
||||
info := map[string]interface{}{
|
||||
"name": st.Name,
|
||||
"state": string(st.State),
|
||||
}
|
||||
if st.Meta.DisplayName != "" {
|
||||
info["display_name"] = st.Meta.DisplayName
|
||||
}
|
||||
containerNames := make([]string, 0, len(st.Containers))
|
||||
for _, c := range st.Containers {
|
||||
containerNames = append(containerNames, c.Name)
|
||||
switch c.State {
|
||||
case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy:
|
||||
running++
|
||||
default:
|
||||
stopped++
|
||||
}
|
||||
}
|
||||
info["containers"] = containerNames
|
||||
stackList = append(stackList, info)
|
||||
}
|
||||
dump["stacks"] = map[string]interface{}{
|
||||
"deployed": deployed,
|
||||
"running": running,
|
||||
"stopped": stopped,
|
||||
"list": stackList,
|
||||
}
|
||||
|
||||
// Backup
|
||||
if s.backupMgr != nil {
|
||||
backupInfo := map[string]interface{}{
|
||||
"enabled": true,
|
||||
"running": s.backupMgr.IsRunning(),
|
||||
}
|
||||
dbDump, backupSt := s.backupMgr.GetStatus()
|
||||
if dbDump != nil {
|
||||
backupInfo["last_db_dump"] = map[string]interface{}{
|
||||
"time": dbDump.LastRun,
|
||||
"success": dbDump.Success,
|
||||
}
|
||||
}
|
||||
if backupSt != nil {
|
||||
backupInfo["last_backup"] = map[string]interface{}{
|
||||
"time": backupSt.LastRun,
|
||||
"success": backupSt.Success,
|
||||
}
|
||||
if backupSt.RepoStats != nil {
|
||||
backupInfo["repo_size"] = backupSt.RepoStats.TotalSize
|
||||
backupInfo["snapshot_count"] = backupSt.RepoStats.SnapshotCount
|
||||
}
|
||||
}
|
||||
dump["backup"] = backupInfo
|
||||
} else {
|
||||
dump["backup"] = map[string]interface{}{"enabled": false}
|
||||
}
|
||||
|
||||
// Hub
|
||||
hubInfo := map[string]interface{}{
|
||||
"url": s.cfg.Hub.URL,
|
||||
"enabled": s.cfg.Hub.Enabled,
|
||||
}
|
||||
if s.hubPushStatusFn != nil {
|
||||
st := s.hubPushStatusFn()
|
||||
hubInfo["last_attempt"] = st.LastAttempt
|
||||
hubInfo["last_success"] = st.LastSuccess
|
||||
hubInfo["last_error"] = st.LastError
|
||||
hubInfo["consecutive_failures"] = st.Consecutive
|
||||
}
|
||||
dump["hub"] = hubInfo
|
||||
|
||||
// Scheduler
|
||||
if s.scheduler != nil {
|
||||
jobs := s.scheduler.GetJobs()
|
||||
jobList := make([]map[string]interface{}, 0, len(jobs))
|
||||
for _, j := range jobs {
|
||||
entry := map[string]interface{}{
|
||||
"name": j.Name,
|
||||
"running": j.Running,
|
||||
}
|
||||
if j.Interval > 0 {
|
||||
entry["type"] = "every"
|
||||
entry["interval"] = j.Interval.String()
|
||||
} else if j.Schedule != "" {
|
||||
entry["type"] = "daily"
|
||||
entry["schedule"] = j.Schedule
|
||||
}
|
||||
if !j.LastRun.IsZero() {
|
||||
entry["last_run"] = j.LastRun
|
||||
}
|
||||
if j.LastErr != nil {
|
||||
entry["last_error"] = j.LastErr.Error()
|
||||
}
|
||||
jobList = append(jobList, entry)
|
||||
}
|
||||
dump["scheduler"] = jobList
|
||||
}
|
||||
|
||||
// Health
|
||||
healthReport := monitor.RunHealthCheck(s.cfg, s.cpuCollector, storagePaths, s.logger)
|
||||
dump["health"] = map[string]interface{}{
|
||||
"status": healthReport.Status,
|
||||
"issues": healthReport.Issues,
|
||||
"warnings": healthReport.Warnings,
|
||||
}
|
||||
|
||||
// Notifications
|
||||
prefs := s.settings.GetNotificationPrefs()
|
||||
dump["notifications"] = map[string]interface{}{
|
||||
"email": prefs.Email,
|
||||
"enabled_events": prefs.EnabledEvents,
|
||||
"cooldown_hours": prefs.CooldownHours,
|
||||
}
|
||||
|
||||
// Self-update
|
||||
if s.updater != nil {
|
||||
status := s.updater.GetStatus()
|
||||
dump["self_update"] = map[string]interface{}{
|
||||
"enabled": true,
|
||||
"auto": s.cfg.SelfUpdate.AutoUpdate,
|
||||
"last_check": status.LastCheck,
|
||||
}
|
||||
} else {
|
||||
dump["self_update"] = map[string]interface{}{"enabled": false}
|
||||
}
|
||||
|
||||
// Alerts
|
||||
if s.alertManager != nil {
|
||||
dump["alerts"] = s.alertManager.GetAlerts()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(dump)
|
||||
}
|
||||
|
||||
// ── Section 2: Notification & Event testing ─────────────────────────
|
||||
|
||||
func (s *Server) debugTestEvent(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
EventType string `json:"event_type"`
|
||||
Severity string `json:"severity"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Érvénytelen kérés", nil)
|
||||
return
|
||||
}
|
||||
if req.EventType == "" {
|
||||
req.EventType = "test"
|
||||
}
|
||||
if req.Severity == "" {
|
||||
req.Severity = "info"
|
||||
}
|
||||
|
||||
if s.notifier == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Notifier nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
|
||||
statusCode, err := s.notifier.PushTestEventSync(req.EventType, req.Severity,
|
||||
fmt.Sprintf("Teszt esemény: %s (%s)", req.EventType, req.Severity))
|
||||
if err != nil {
|
||||
writeDebugJSON(w, http.StatusOK, false, err.Error(), map[string]interface{}{
|
||||
"hub_status": statusCode,
|
||||
})
|
||||
return
|
||||
}
|
||||
writeDebugJSON(w, http.StatusOK, true, fmt.Sprintf("Esemény elküldve (HTTP %d)", statusCode),
|
||||
map[string]interface{}{"hub_status": statusCode})
|
||||
}
|
||||
|
||||
func (s *Server) debugEventHistory(w http.ResponseWriter, r *http.Request) {
|
||||
if s.notifier == nil {
|
||||
writeDebugJSON(w, http.StatusOK, true, "", []interface{}{})
|
||||
return
|
||||
}
|
||||
history := s.notifier.GetEventHistory(20)
|
||||
writeDebugJSON(w, http.StatusOK, true, "", history)
|
||||
}
|
||||
|
||||
// ── Section 3: Backup testing ───────────────────────────────────────
|
||||
|
||||
func (s *Server) debugTriggerDBDump(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Backup manager nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := s.backupMgr.RunDBDumps(context.Background()); err != nil {
|
||||
s.logger.Printf("[WARN] Debug DB dump failed: %v", err)
|
||||
}
|
||||
}()
|
||||
writeDebugJSON(w, http.StatusOK, true, "DB dump elindítva", nil)
|
||||
}
|
||||
|
||||
func (s *Server) debugTriggerCrossDrive(w http.ResponseWriter, r *http.Request) {
|
||||
if s.crossDriveRunner == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Cross-drive runner nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := s.crossDriveRunner.RunAllConfigured(context.Background()); err != nil {
|
||||
s.logger.Printf("[WARN] Debug cross-drive failed: %v", err)
|
||||
}
|
||||
}()
|
||||
writeDebugJSON(w, http.StatusOK, true, "Cross-drive mentés elindítva", nil)
|
||||
}
|
||||
|
||||
func (s *Server) debugTriggerIntegrity(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Backup manager nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := s.backupMgr.RunIntegrityCheck(context.Background()); err != nil {
|
||||
s.logger.Printf("[WARN] Debug integrity check failed: %v", err)
|
||||
}
|
||||
}()
|
||||
writeDebugJSON(w, http.StatusOK, true, "Integritás ellenőrzés elindítva", nil)
|
||||
}
|
||||
|
||||
func (s *Server) debugTriggerInfraBackup(w http.ResponseWriter, r *http.Request) {
|
||||
if s.debugCallbacks == nil || s.debugCallbacks.TriggerLocalInfraWrite == nil {
|
||||
writeDebugJSON(w, http.StatusNotImplemented, false, "Nem bekötött", nil)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := s.debugCallbacks.TriggerLocalInfraWrite(); err != nil {
|
||||
s.logger.Printf("[WARN] Debug infra backup failed: %v", err)
|
||||
}
|
||||
}()
|
||||
writeDebugJSON(w, http.StatusOK, true, "Infra mentés elindítva", nil)
|
||||
}
|
||||
|
||||
// ── Section 4: Storage simulation ───────────────────────────────────
|
||||
|
||||
func (s *Server) debugSimulateDisconnect(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Path == "" {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Érvénytelen kérés: path szükséges", nil)
|
||||
return
|
||||
}
|
||||
if s.storageWatchdog == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Storage watchdog nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
stopped, err := s.storageWatchdog.SimulateDisconnect(r.Context(), req.Path)
|
||||
if err != nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
writeDebugJSON(w, http.StatusOK, true,
|
||||
fmt.Sprintf("Leválasztás szimulálva: %s (%d app leállítva)", req.Path, len(stopped)),
|
||||
map[string]interface{}{"stopped_stacks": stopped})
|
||||
}
|
||||
|
||||
func (s *Server) debugSimulateReconnect(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Path == "" {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Érvénytelen kérés: path szükséges", nil)
|
||||
return
|
||||
}
|
||||
if s.storageWatchdog == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Storage watchdog nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
if err := s.storageWatchdog.SimulateReconnect(r.Context(), req.Path); err != nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
writeDebugJSON(w, http.StatusOK, true,
|
||||
fmt.Sprintf("Visszacsatlakozás szimulálva: %s", req.Path), nil)
|
||||
}
|
||||
|
||||
func (s *Server) debugWatchdogStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if s.storageWatchdog == nil {
|
||||
writeDebugJSON(w, http.StatusOK, true, "", []interface{}{})
|
||||
return
|
||||
}
|
||||
status := s.storageWatchdog.GetDebugStatus()
|
||||
writeDebugJSON(w, http.StatusOK, true, "", status)
|
||||
}
|
||||
|
||||
// ── Section 5: Hub & connectivity ───────────────────────────────────
|
||||
|
||||
func (s *Server) debugHubPush(w http.ResponseWriter, r *http.Request) {
|
||||
if s.debugCallbacks == nil || s.debugCallbacks.TriggerHubReportPush == nil {
|
||||
writeDebugJSON(w, http.StatusNotImplemented, false, "Nem bekötött", nil)
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
err := s.debugCallbacks.TriggerHubReportPush()
|
||||
latency := time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
writeDebugJSON(w, http.StatusOK, false, err.Error(), map[string]interface{}{"latency_ms": latency})
|
||||
return
|
||||
}
|
||||
writeDebugJSON(w, http.StatusOK, true, "Hub jelentés elküldve",
|
||||
map[string]interface{}{"latency_ms": latency})
|
||||
}
|
||||
|
||||
func (s *Server) debugHubInfraPush(w http.ResponseWriter, r *http.Request) {
|
||||
if s.debugCallbacks == nil || s.debugCallbacks.TriggerHubInfraPush == nil {
|
||||
writeDebugJSON(w, http.StatusNotImplemented, false, "Nem bekötött", nil)
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
err := s.debugCallbacks.TriggerHubInfraPush()
|
||||
latency := time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
writeDebugJSON(w, http.StatusOK, false, err.Error(), map[string]interface{}{"latency_ms": latency})
|
||||
return
|
||||
}
|
||||
writeDebugJSON(w, http.StatusOK, true, "Infra backup elküldve a Hubra",
|
||||
map[string]interface{}{"latency_ms": latency})
|
||||
}
|
||||
|
||||
func (s *Server) debugHubConnectivity(w http.ResponseWriter, r *http.Request) {
|
||||
if s.debugCallbacks == nil || s.debugCallbacks.HubConnectivityTest == nil {
|
||||
writeDebugJSON(w, http.StatusNotImplemented, false, "Nem bekötött", nil)
|
||||
return
|
||||
}
|
||||
statusCode, latency, err := s.debugCallbacks.HubConnectivityTest()
|
||||
data := map[string]interface{}{
|
||||
"status_code": statusCode,
|
||||
"latency_ms": latency,
|
||||
}
|
||||
if err != nil {
|
||||
writeDebugJSON(w, http.StatusOK, false, err.Error(), data)
|
||||
return
|
||||
}
|
||||
writeDebugJSON(w, http.StatusOK, true,
|
||||
fmt.Sprintf("Hub elérhető (HTTP %d, %dms)", statusCode, latency), data)
|
||||
}
|
||||
|
||||
func (s *Server) debugPreferencesSync(w http.ResponseWriter, r *http.Request) {
|
||||
if s.notifier == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Notifier nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
prefs := s.settings.GetNotificationPrefs()
|
||||
if err := s.notifier.SyncPreferences(prefs.Email, prefs.EnabledEvents, prefs.CooldownHours); err != nil {
|
||||
writeDebugJSON(w, http.StatusOK, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
writeDebugJSON(w, http.StatusOK, true, "Preferenciák szinkronizálva", nil)
|
||||
}
|
||||
|
||||
func (s *Server) debugGiteaConnectivity(w http.ResponseWriter, r *http.Request) {
|
||||
if s.debugCallbacks == nil || s.debugCallbacks.GiteaConnectivityTest == nil {
|
||||
writeDebugJSON(w, http.StatusNotImplemented, false, "Nem bekötött", nil)
|
||||
return
|
||||
}
|
||||
statusCode, latency, err := s.debugCallbacks.GiteaConnectivityTest()
|
||||
data := map[string]interface{}{
|
||||
"status_code": statusCode,
|
||||
"latency_ms": latency,
|
||||
}
|
||||
if err != nil {
|
||||
writeDebugJSON(w, http.StatusOK, false, err.Error(), data)
|
||||
return
|
||||
}
|
||||
writeDebugJSON(w, http.StatusOK, true,
|
||||
fmt.Sprintf("Gitea elérhető (HTTP %d, %dms)", statusCode, latency), data)
|
||||
}
|
||||
|
||||
// ── Section 6: Self-update ──────────────────────────────────────────
|
||||
|
||||
func (s *Server) debugSelfUpdateDryRun(w http.ResponseWriter, r *http.Request) {
|
||||
if s.updater == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Self-update nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
result := s.updater.DryRun()
|
||||
writeDebugJSON(w, http.StatusOK, true, "", result)
|
||||
}
|
||||
|
||||
// ── Section 7: DR / Setup ───────────────────────────────────────────
|
||||
|
||||
func (s *Server) debugTriggerSetupWizard(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Confirm string `json:"confirm"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Érvénytelen kérés", nil)
|
||||
return
|
||||
}
|
||||
if req.Confirm != "RESET" {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Érvénytelen megerősítés — írja be: RESET", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Pre-check: verify infra backup exists on at least one drive
|
||||
if !s.hasInfraBackupOnDrive() {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false,
|
||||
"Nincs infra backup egyetlen meghajtón sem! Először készítsen infra backupot.", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Write marker file
|
||||
markerPath := filepath.Join(s.cfg.Paths.DataDir, ".needs-setup")
|
||||
if err := os.WriteFile(markerPath, []byte("debug-triggered\n"), 0644); err != nil {
|
||||
writeDebugJSON(w, http.StatusInternalServerError, false,
|
||||
fmt.Sprintf("Marker fájl írási hiba: %v", err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
writeDebugJSON(w, http.StatusOK, true, "Controller újraindítása setup módba...", nil)
|
||||
|
||||
// Exit after response is sent so the container restarts into setup mode
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *Server) debugInfraBackupStatus(w http.ResponseWriter, r *http.Request) {
|
||||
storagePaths := s.settings.GetStoragePaths()
|
||||
drives := make([]map[string]interface{}, 0, len(storagePaths))
|
||||
|
||||
for _, sp := range storagePaths {
|
||||
if sp.Decommissioned || sp.Disconnected {
|
||||
continue
|
||||
}
|
||||
driveInfo := map[string]interface{}{
|
||||
"path": sp.Path,
|
||||
"label": sp.Label,
|
||||
"has_backup": false,
|
||||
}
|
||||
|
||||
infraDir := backup.InfraBackupDir(sp.Path)
|
||||
info, err := os.Stat(infraDir)
|
||||
if err == nil && info.IsDir() {
|
||||
driveInfo["has_backup"] = true
|
||||
driveInfo["last_modified"] = info.ModTime()
|
||||
|
||||
// List files
|
||||
entries, _ := os.ReadDir(infraDir)
|
||||
files := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
files = append(files, e.Name())
|
||||
}
|
||||
driveInfo["files"] = files
|
||||
}
|
||||
|
||||
drives = append(drives, driveInfo)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"drives": drives,
|
||||
}
|
||||
if s.hubPushStatusFn != nil {
|
||||
st := s.hubPushStatusFn()
|
||||
data["hub_infra_push"] = map[string]interface{}{
|
||||
"last_attempt": st.LastAttempt,
|
||||
"last_success": st.LastSuccess,
|
||||
"last_error": st.LastError,
|
||||
}
|
||||
}
|
||||
|
||||
writeDebugJSON(w, http.StatusOK, true, "", data)
|
||||
}
|
||||
|
||||
// hasInfraBackupOnDrive checks if any connected storage drive has an infra backup.
|
||||
func (s *Server) hasInfraBackupOnDrive() bool {
|
||||
for _, sp := range s.settings.GetStoragePaths() {
|
||||
if sp.Decommissioned || sp.Disconnected {
|
||||
continue
|
||||
}
|
||||
infraDir := backup.InfraBackupDir(sp.Path)
|
||||
if info, err := os.Stat(infraDir); err == nil && info.IsDir() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ── Section 8: Log viewer ───────────────────────────────────────────
|
||||
|
||||
func (s *Server) debugLogBuffer(w http.ResponseWriter, r *http.Request) {
|
||||
if s.logBuffer == nil {
|
||||
writeDebugJSON(w, http.StatusOK, true, "", map[string]interface{}{
|
||||
"entries": []interface{}{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
level := r.URL.Query().Get("level")
|
||||
if level == "" {
|
||||
level = "DEBUG"
|
||||
}
|
||||
|
||||
limit := 200
|
||||
if v := r.URL.Query().Get("limit"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 1000 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
|
||||
var after time.Time
|
||||
if v := r.URL.Query().Get("after"); v != "" {
|
||||
if t, err := time.Parse(time.RFC3339Nano, v); err == nil {
|
||||
after = t
|
||||
}
|
||||
}
|
||||
|
||||
entries, total := s.logBuffer.Entries(level, limit, after)
|
||||
writeDebugJSON(w, http.StatusOK, true, "", map[string]interface{}{
|
||||
"entries": entries,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user