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
+32 -16
View File
@@ -62,6 +62,12 @@ func NewUpdater(cfg *config.SelfUpdateConfig, gitCfg *config.GitConfig, currentV
}
}
func (u *Updater) dbg(format string, args ...interface{}) {
if u.debug {
u.logger.Printf("[DEBUG] [selfupdate] "+format, args...)
}
}
// SetBackupRunningCheck sets the callback to check if a backup is in progress.
func (u *Updater) SetBackupRunningCheck(fn func() bool) {
u.mu.Lock()
@@ -140,10 +146,10 @@ func (u *Updater) CheckForUpdate() CheckResult {
result.UpdateAvailable = true
}
if u.debug {
u.logger.Printf("[DEBUG] [SELFUPDATE] Version comparison: current=%s, latest=%s, cmp=%d, updateAvailable=%v",
u.currentVer, latestStr, cmp, result.UpdateAvailable)
}
u.dbg("version comparison: current=%s (%d.%d.%d), latest=%s (%d.%d.%d), cmp=%d, updateAvailable=%v",
u.currentVer, currentVer.Major, currentVer.Minor, currentVer.Patch,
latestStr, latestVer.Major, latestVer.Minor, latestVer.Patch,
cmp, result.UpdateAvailable)
u.mu.Lock()
u.latestVersion = latestStr
@@ -163,9 +169,7 @@ func (u *Updater) queryRegistry() (string, error) {
// Gitea registry V2: GET /v2/<owner>/<repo>/tags/list
url := fmt.Sprintf("https://gitea.dooplex.hu/v2/%s/tags/list", registryImagePath(u.cfg.Image))
if u.debug {
u.logger.Printf("[DEBUG] [SELFUPDATE] Registry API URL: %s (user: %s)", url, u.gitCfg.Username)
}
u.dbg("queryRegistry: url=%s user=%s", url, u.gitCfg.Username)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
@@ -176,9 +180,11 @@ func (u *Updater) queryRegistry() (string, error) {
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
u.dbg("queryRegistry: HTTP request failed: %v", err)
return "", fmt.Errorf("HTTP request failed: %w", err)
}
defer resp.Body.Close()
u.dbg("queryRegistry: HTTP %d", resp.StatusCode)
if resp.StatusCode == 401 {
return "", fmt.Errorf("authentication failed (401)")
@@ -195,9 +201,7 @@ func (u *Updater) queryRegistry() (string, error) {
return "", fmt.Errorf("decoding response: %w", err)
}
if u.debug {
u.logger.Printf("[DEBUG] [SELFUPDATE] Registry returned %d tags: %v", len(tagsResp.Tags), tagsResp.Tags)
}
u.dbg("queryRegistry: %d tags returned: %v", len(tagsResp.Tags), tagsResp.Tags)
// Find highest semver tag
var highest *Version
@@ -293,9 +297,11 @@ func (u *Updater) DryRun() *DryRunResult {
// TriggerUpdate starts the self-update process. Returns error immediately if
// preconditions fail. The actual update runs in a goroutine.
func (u *Updater) TriggerUpdate(initiatedBy string) error {
u.dbg("TriggerUpdate: initiatedBy=%s currentVer=%s", initiatedBy, u.currentVer)
u.mu.Lock()
if u.updateRunning {
u.mu.Unlock()
u.dbg("TriggerUpdate: rejected — update already running")
return fmt.Errorf("Frissítés már folyamatban")
}
@@ -334,6 +340,7 @@ func (u *Updater) TriggerUpdate(initiatedBy string) error {
previousImage := fmt.Sprintf("%s:%s", u.cfg.Image, u.currentVer)
u.logger.Printf("[INFO] Starting self-update: %s → %s (initiated by: %s)", u.currentVer, targetVersion, initiatedBy)
u.dbg("TriggerUpdate: target=%s image=%s previousImage=%s", targetVersion, targetImage, previousImage)
go u.performUpdate(targetVersion, targetImage, previousImage, initiatedBy)
@@ -348,6 +355,7 @@ func (u *Updater) performUpdate(targetVersion, targetImage, previousImage, initi
u.mu.Unlock()
}()
u.dbg("performUpdate: starting — target=%s image=%s", targetVersion, targetImage)
// 1. Write pending state
state := &UpdateState{
Status: "pending",
@@ -364,7 +372,9 @@ func (u *Updater) performUpdate(targetVersion, targetImage, previousImage, initi
}
// 2. Docker pull
u.dbg("performUpdate: step 2 — docker pull %s", targetImage)
u.logger.Printf("[INFO] Pulling image: %s", targetImage)
pullStart := time.Now()
pullOut, pullErr := runCommand("docker", "pull", targetImage)
if pullErr != nil {
state.Status = "failed"
@@ -375,8 +385,10 @@ func (u *Updater) performUpdate(targetVersion, targetImage, previousImage, initi
return
}
u.logger.Printf("[INFO] Image pulled successfully: %s", targetImage)
u.dbg("performUpdate: docker pull completed in %s", time.Since(pullStart).Round(time.Millisecond))
// 3. Update compose file (replace image tag)
u.dbg("performUpdate: step 3 — updating compose file %s", u.composePath)
if err := u.updateComposeFile(targetImage); err != nil {
state.Status = "failed"
state.Error = fmt.Sprintf("compose update failed: %v", err)
@@ -388,6 +400,7 @@ func (u *Updater) performUpdate(targetVersion, targetImage, previousImage, initi
u.logger.Printf("[INFO] Compose file updated with new image: %s", targetImage)
// 4. Docker compose up -d (this kills the current container)
u.dbg("performUpdate: step 4 — docker compose up -d")
u.logger.Printf("[INFO] Running docker compose up -d — container will restart")
composeDir := strings.TrimSuffix(u.composePath, "/docker-compose.yml")
upOut, upErr := runCommand("docker", "compose", "-f", u.composePath, "-p", "felhom-controller", "up", "-d")
@@ -417,12 +430,12 @@ func (u *Updater) updateComposeFile(newImage string) error {
// Replace image line: "image: gitea.dooplex.hu/admin/felhom-controller:..." → new image
re := regexp.MustCompile(`(image:\s*)gitea\.dooplex\.hu/admin/felhom-controller:\S+`)
if u.debug {
// Log old image line for debugging
oldMatch := re.Find(data)
if oldMatch != nil {
u.logger.Printf("[DEBUG] [SELFUPDATE] Compose file edit: %q → %q", string(oldMatch), "image: "+newImage)
}
// Log old image line for debugging
oldMatch := re.Find(data)
if oldMatch != nil {
u.dbg("updateComposeFile: %q → %q", string(oldMatch), "image: "+newImage)
} else {
u.dbg("updateComposeFile: no matching image line found in %s", u.composePath)
}
newData := re.ReplaceAll(data, []byte("${1}"+newImage))
@@ -447,6 +460,7 @@ func (u *Updater) updateComposeFile(newImage string) error {
// Called once from main.go before the scheduler starts.
// Returns the state if a pending update was detected, nil otherwise.
func (u *Updater) VerifyStartup() *UpdateState {
u.dbg("VerifyStartup: checking update state in %s", u.dataDir)
state, err := LoadState(u.dataDir)
if err != nil {
u.logger.Printf("[WARN] Failed to load update state on startup: %v — clearing", err)
@@ -454,8 +468,10 @@ func (u *Updater) VerifyStartup() *UpdateState {
return nil
}
if state == nil || state.Status != "pending" {
u.dbg("VerifyStartup: no pending update (state=%v)", state)
return nil
}
u.dbg("VerifyStartup: pending update found — target=%s previous=%s", state.TargetVersion, state.PreviousVersion)
// Compare current version with target
currentVer, curErr := ParseVersion(u.currentVer)