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>
824 lines
26 KiB
Go
824 lines
26 KiB
Go
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/appexport"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/report"
|
|
"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)
|
|
GetTelemetryPreview func() ([]report.AppTelemetry, 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: Telemetry testing
|
|
case subpath == "telemetry" && r.Method == http.MethodGet:
|
|
s.debugTelemetry(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)
|
|
|
|
// Section 9: App Export/Import
|
|
case subpath == "appexport/status" && r.Method == http.MethodGet:
|
|
s.debugAppExportStatus(w, r)
|
|
case subpath == "appexport/bundles" && r.Method == http.MethodGet:
|
|
s.debugAppExportBundles(w, r)
|
|
case subpath == "appexport/cleanup" && r.Method == http.MethodPost:
|
|
s.debugAppExportCleanup(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: Telemetry testing ───────────────────────────────────────
|
|
|
|
func (s *Server) debugTelemetry(w http.ResponseWriter, r *http.Request) {
|
|
if s.debugCallbacks == nil || s.debugCallbacks.GetTelemetryPreview == nil {
|
|
writeDebugJSON(w, http.StatusNotImplemented, false, "Nem bekötött", nil)
|
|
return
|
|
}
|
|
start := time.Now()
|
|
telemetry, err := s.debugCallbacks.GetTelemetryPreview()
|
|
latency := time.Since(start).Milliseconds()
|
|
if err != nil {
|
|
writeDebugJSON(w, http.StatusOK, false, err.Error(), map[string]interface{}{"latency_ms": latency})
|
|
return
|
|
}
|
|
|
|
totalErrors := 0
|
|
totalWarnings := 0
|
|
for _, app := range telemetry {
|
|
totalErrors += app.LogErrors
|
|
totalWarnings += app.LogWarnings
|
|
}
|
|
|
|
writeDebugJSON(w, http.StatusOK, true,
|
|
fmt.Sprintf("Telemetria összegyűjtve: %d app, %d hiba, %d figyelmeztetés (%dms)",
|
|
len(telemetry), totalErrors, totalWarnings, latency),
|
|
map[string]interface{}{
|
|
"latency_ms": latency,
|
|
"app_count": len(telemetry),
|
|
"total_errors": totalErrors,
|
|
"total_warnings": totalWarnings,
|
|
"app_telemetry": telemetry,
|
|
})
|
|
}
|
|
|
|
// ── 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,
|
|
})
|
|
}
|
|
|
|
// ── Section 9: App Export/Import ─────────────────────────────────────
|
|
|
|
func (s *Server) debugAppExportStatus(w http.ResponseWriter, r *http.Request) {
|
|
if s.appExporter == nil {
|
|
writeDebugJSON(w, http.StatusOK, true, "", map[string]interface{}{
|
|
"available": false,
|
|
})
|
|
return
|
|
}
|
|
|
|
info := s.appExporter.GetDebugInfo()
|
|
|
|
// Scan for bundles
|
|
drives := s.storageDriveList()
|
|
bundles := appexport.ScanForBundles(drives)
|
|
|
|
// Scan for stale temp files
|
|
staleFiles := appexport.ScanForStaleTempFiles(drives)
|
|
|
|
info["bundle_count"] = len(bundles)
|
|
info["stale_temp_files"] = staleFiles
|
|
info["stale_temp_count"] = len(staleFiles)
|
|
info["available"] = true
|
|
|
|
// Export dirs
|
|
exportDirs := make([]map[string]interface{}, 0, len(drives))
|
|
for _, d := range drives {
|
|
dir := appexport.ExportDir(d.Path)
|
|
dirInfo := map[string]interface{}{
|
|
"path": dir,
|
|
"label": d.Label,
|
|
}
|
|
if stat, err := os.Stat(dir); err == nil {
|
|
dirInfo["exists"] = true
|
|
dirInfo["modified"] = stat.ModTime()
|
|
} else {
|
|
dirInfo["exists"] = false
|
|
}
|
|
exportDirs = append(exportDirs, dirInfo)
|
|
}
|
|
info["export_dirs"] = exportDirs
|
|
|
|
writeDebugJSON(w, http.StatusOK, true, "", info)
|
|
}
|
|
|
|
func (s *Server) debugAppExportBundles(w http.ResponseWriter, r *http.Request) {
|
|
if s.appExporter == nil {
|
|
writeDebugJSON(w, http.StatusBadRequest, false, "App export not available", nil)
|
|
return
|
|
}
|
|
|
|
drives := s.storageDriveList()
|
|
bundles := appexport.ScanForBundles(drives)
|
|
|
|
writeDebugJSON(w, http.StatusOK, true,
|
|
fmt.Sprintf("%d csomag található", len(bundles)),
|
|
map[string]interface{}{"bundles": bundles})
|
|
}
|
|
|
|
func (s *Server) debugAppExportCleanup(w http.ResponseWriter, r *http.Request) {
|
|
if s.appExporter == nil {
|
|
writeDebugJSON(w, http.StatusBadRequest, false, "App export not available", nil)
|
|
return
|
|
}
|
|
|
|
drives := s.storageDriveList()
|
|
staleFiles := appexport.ScanForStaleTempFiles(drives)
|
|
|
|
if len(staleFiles) == 0 {
|
|
writeDebugJSON(w, http.StatusOK, true, "Nincs eltávolítandó temp fájl", nil)
|
|
return
|
|
}
|
|
|
|
removed := 0
|
|
for _, f := range staleFiles {
|
|
if err := os.Remove(f); err != nil {
|
|
s.logger.Printf("[WARN] Failed to remove stale temp file %s: %v", f, err)
|
|
} else {
|
|
s.logger.Printf("[INFO] Removed stale temp file: %s", f)
|
|
removed++
|
|
}
|
|
}
|
|
|
|
writeDebugJSON(w, http.StatusOK, true,
|
|
fmt.Sprintf("%d/%d temp fájl eltávolítva", removed, len(staleFiles)), nil)
|
|
}
|