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:
2026-02-21 20:18:57 +01:00
parent be7803c0ac
commit 7f48786312
16 changed files with 2283 additions and 233 deletions
+56 -10
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
@@ -64,7 +65,7 @@ func main() {
log.Printf("[WARN] Config load failed (%s), using defaults: %v", *configPath, err)
}
logger := setupLogger(cfg)
logger, logBuffer := setupLogger(cfg)
// --- Setup mode: if no customer ID configured, run setup wizard ---
if setup.NeedsSetup(cfg) {
@@ -583,8 +584,6 @@ func main() {
if assetsSyncer != nil {
apiRouter.SetAssetsSyncer(assetsSyncer)
}
apiRouter.SetDebugDumpDeps(sched, hubPusher, alertMgr, Version, startTime)
// --- Initialize web server ---
webServer := web.NewServer(cfg, stackMgr, cpuCollector, backupMgr, crossDriveRunner, sched, sett, alertMgr, notifier, updater, logger, Version)
webServer.SetStorageWatchdog(storageWatchdog)
@@ -602,6 +601,53 @@ func main() {
}
})
}
if logBuffer != nil {
webServer.SetLogBuffer(logBuffer)
}
webServer.SetStartTime(startTime)
// Wire debug callbacks (only in debug mode)
if cfg.Logging.Level == "debug" {
dc := &web.DebugCallbacks{}
if hubPusher != nil {
dc.TriggerHubReportPush = func() error {
r := report.BuildReport(cfg, *configPath, stackMgr, backupMgr, cpuCollector, metricsStore, Version, sett.GetStoragePaths(), logger)
return hubPusher.Push(r)
}
dc.TriggerHubInfraPush = func() error {
pushInfraBackup(cfg, sett, stackProv, hubPusher, logger)
return nil
}
}
dc.TriggerLocalInfraWrite = func() error {
writeLocalInfraBackup(cfg, sett, stackProv, logger)
return nil
}
dc.HubConnectivityTest = func() (int, int64, error) {
start := time.Now()
resp, err := http.Get(cfg.Hub.URL + "/healthz")
latency := time.Since(start).Milliseconds()
if err != nil {
return 0, latency, err
}
resp.Body.Close()
return resp.StatusCode, latency, nil
}
if cfg.Git.RepoURL != "" {
dc.GiteaConnectivityTest = func() (int, int64, error) {
start := time.Now()
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Head(cfg.Git.RepoURL)
latency := time.Since(start).Milliseconds()
if err != nil {
return 0, latency, err
}
resp.Body.Close()
return resp.StatusCode, latency, nil
}
}
webServer.SetDebugCallbacks(dc)
}
// --- Initialize drive migrator ---
driveMigrator := &storage.DriveMigrator{
@@ -652,6 +698,8 @@ func main() {
mux.HandleFunc("/api/health", apiRouter.HealthHandler)
// Storage API routes handled by web server (longer prefix takes precedence over /api/)
mux.Handle("/api/storage/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeStorageAPI))))
// Debug API routes handled by web server (debug-mode gating inside handler)
mux.Handle("/api/debug/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeDebugAPI))))
// Self-update API — accepts session auth OR hub API key (for external triggering)
// CsrfProtect exempts Bearer-token requests automatically.
mux.Handle("/api/selfupdate/", selfUpdateAuthMiddleware(cfg, webServer, webServer.CsrfProtect(http.HandlerFunc(apiRouter.ServeHTTP))))
@@ -711,15 +759,13 @@ func selfUpdateAuthMiddleware(cfg *config.Config, webServer *web.Server, next ht
})
}
func setupLogger(cfg *config.Config) *log.Logger {
// For now, log to stdout. File logging will be added later.
logger := log.New(os.Stdout, "", log.LstdFlags)
func setupLogger(cfg *config.Config) (*log.Logger, *web.LogBuffer) {
if cfg.Logging.Level == "debug" {
logger.SetFlags(log.LstdFlags | log.Lshortfile)
logBuffer := web.NewLogBuffer(1000)
logger := log.New(io.MultiWriter(os.Stdout, logBuffer), "", log.LstdFlags|log.Lshortfile)
return logger, logBuffer
}
return logger
return log.New(os.Stdout, "", log.LstdFlags), nil
}
// stackAdapter implements backup.StackDataProvider using stacks.Manager.