feat: comprehensive debug logging across all controller modules

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>
This commit is contained in:
2026-02-26 18:14:43 +01:00
parent f6caea8067
commit 95c821deb2
54 changed files with 5015 additions and 82 deletions
+39
View File
@@ -12,6 +12,7 @@ import (
"sync"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/appexport"
"gitea.dooplex.hu/admin/felhom-controller/internal/assets"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
@@ -78,6 +79,9 @@ type Server struct {
// App-to-app integration manager (optional)
integrationMgr *integrations.Manager
// App export/import engine (optional)
appExporter *appexport.Exporter
// Debug mode support
logBuffer *LogBuffer
debugCallbacks *DebugCallbacks
@@ -102,6 +106,13 @@ func NewServer(cfg *config.Config, stackMgr *stacks.Manager, cpuCollector *syste
loginAttempts: make(map[string]*loginAttempt),
done: make(chan struct{}),
}
if cfg.Logging.Level == "debug" {
logger.Printf("[DEBUG] [web] NewServer: initializing web server v%s", version)
logger.Printf("[DEBUG] [web] NewServer: backup=%v crossDrive=%v scheduler=%v alertMgr=%v notifier=%v updater=%v",
backupMgr != nil, crossDrive != nil, sched != nil, alertMgr != nil, notif != nil, updater != nil)
}
s.loadTemplates()
go s.cleanupSessions()
@@ -138,6 +149,10 @@ func (s *Server) loadTemplates() {
s.tmpl = template.Must(
template.New("").Funcs(s.templateFuncMap()).ParseFS(templateFS, "templates/*.html"),
)
if s.isDebug() {
names := s.tmpl.Templates()
s.logger.Printf("[DEBUG] [web] loadTemplates: loaded %d templates", len(names))
}
}
// SetRestoreState puts the server into DR restore mode with the given plan.
@@ -190,6 +205,11 @@ func (s *Server) SetDebugCallbacks(dc *DebugCallbacks) {
s.debugCallbacks = dc
}
// SetAppExporter sets the app export/import engine.
func (s *Server) SetAppExporter(e *appexport.Exporter) {
s.appExporter = e
}
// SetStartTime records the controller start time for uptime calculation.
func (s *Server) SetStartTime(t time.Time) {
s.startTime = t
@@ -221,6 +241,10 @@ func (s *Server) InRestoreMode() bool {
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if s.isDebug() {
s.logger.Printf("[DEBUG] [web] ServeHTTP: %s %s from %s", r.Method, path, r.RemoteAddr)
}
// DR restore mode: intercept all routes except restore page, static, and restore API
if s.InRestoreMode() {
switch {
@@ -283,6 +307,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.storageAttachHandler(w, r)
case path == "/settings/storage/migrate-drive":
s.migrateDrivePageHandler(w, r)
case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/export"):
name := strings.TrimPrefix(path, "/stacks/")
name = strings.TrimSuffix(name, "/export")
s.exportPageHandler(w, r, name)
case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/migrate"):
name := strings.TrimPrefix(path, "/stacks/")
name = strings.TrimSuffix(name, "/migrate")
@@ -295,6 +323,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(path, "/stacks/")
name = strings.TrimSuffix(name, "/deploy")
s.deployHandler(w, r, name)
case path == "/import":
s.importPageHandler(w, r)
case path == "/static/style.css":
s.serveCSSHandler(w, r)
case path == "/static/chart.min.js":
@@ -324,6 +354,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// the controller host (felhom.DOMAIN) pass through normally.
func (s *Server) CatchAllMiddleware(next http.Handler) http.Handler {
controllerHost := "felhom." + s.cfg.Customer.Domain
if s.isDebug() {
s.logger.Printf("[DEBUG] [web] CatchAllMiddleware: controller host=%s", controllerHost)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := r.Host
if idx := strings.LastIndex(host, ":"); idx != -1 {
@@ -335,6 +368,9 @@ func (s *Server) CatchAllMiddleware(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
return
}
if s.isDebug() {
s.logger.Printf("[DEBUG] [web] CatchAllMiddleware: non-controller host=%s, serving catch-all page", host)
}
s.serveCatchAll(w, r, host)
})
}
@@ -405,6 +441,9 @@ func (s *Server) findStackBySubdomain(subdomain string) (*stacks.Stack, bool) {
// ServeStorageAPI handles /api/storage/* routes (JSON API for disk operations).
func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) {
if s.isDebug() {
s.logger.Printf("[DEBUG] [web] ServeStorageAPI: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
}
s.storageAPIHandler(w, r)
}