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
+27 -14
View File
@@ -67,6 +67,12 @@ func New(hubURL, apiKey, assetsDir, fallbackDir string, logger *log.Logger, debu
}
}
func (s *Syncer) dbg(format string, args ...interface{}) {
if s.debug {
s.logger.Printf("[DEBUG] [assets] "+format, args...)
}
}
// Sync fetches the manifest from the Hub, compares checksums, and downloads
// changed/new files. It also removes local files not in the Hub manifest.
func (s *Syncer) Sync(ctx context.Context) error {
@@ -85,6 +91,7 @@ func (s *Syncer) Sync(ctx context.Context) error {
}()
s.logger.Println("[INFO] Asset sync starting...")
syncStart := time.Now()
if err := os.MkdirAll(s.assetsDir, 0755); err != nil {
s.setError(fmt.Errorf("create assets dir: %w", err))
@@ -92,27 +99,24 @@ func (s *Syncer) Sync(ctx context.Context) error {
}
// 1. Fetch Hub manifest
if s.debug {
s.logger.Printf("[DEBUG] Asset sync: fetching manifest from %s/api/v1/assets/manifest", s.hubURL)
}
s.dbg("fetching manifest from %s/api/v1/assets/manifest", s.hubURL)
manifestStart := time.Now()
manifest, err := s.fetchManifest(ctx)
if err != nil {
s.setError(fmt.Errorf("fetch manifest: %w", err))
return err
}
if s.debug {
s.logger.Printf("[DEBUG] Asset sync: manifest has %d files", len(manifest.Files))
}
s.dbg("manifest fetched in %s: %d files, generated=%s",
time.Since(manifestStart).Round(time.Millisecond), len(manifest.Files), manifest.Generated)
// 2. Build local hash map
hashStart := time.Now()
localHashes, err := s.buildLocalHashes()
if err != nil {
s.setError(fmt.Errorf("scan local assets: %w", err))
return err
}
if s.debug {
s.logger.Printf("[DEBUG] Asset sync: %d local files found", len(localHashes))
}
s.dbg("local hash scan: %d files in %s", len(localHashes), time.Since(hashStart).Round(time.Millisecond))
// 3. Download changed/new files
hubFiles := make(map[string]bool, len(manifest.Files))
@@ -124,17 +128,22 @@ func (s *Syncer) Sync(ctx context.Context) error {
totalBytes += entry.Size
if localHash, ok := localHashes[entry.Filename]; ok && localHash == entry.SHA256 {
s.dbg("file %s: hash match (%s), skipping", entry.Filename, entry.SHA256[:12]+"...")
skipped++
continue
}
if s.debug {
s.logger.Printf("[DEBUG] Asset sync: downloading %s (remote sha256=%s)", entry.Filename, entry.SHA256[:12]+"...")
reason := "new"
if localHash, ok := localHashes[entry.Filename]; ok {
reason = fmt.Sprintf("hash mismatch (local=%s remote=%s)", localHash[:12]+"...", entry.SHA256[:12]+"...")
}
s.dbg("file %s: downloading (%s, %d bytes)", entry.Filename, reason, entry.Size)
dlStart := time.Now()
if err := s.downloadFile(ctx, entry.Filename); err != nil {
s.logger.Printf("[WARN] Failed to download asset %s: %v", entry.Filename, err)
continue
}
s.dbg("file %s: downloaded in %s", entry.Filename, time.Since(dlStart).Round(time.Millisecond))
downloaded++
}
@@ -143,9 +152,7 @@ func (s *Syncer) Sync(ctx context.Context) error {
for name := range localHashes {
if !hubFiles[name] {
path := filepath.Join(s.assetsDir, name)
if s.debug {
s.logger.Printf("[DEBUG] Asset sync: removing stale file %s", name)
}
s.dbg("removing stale file %s", name)
if err := os.Remove(path); err != nil {
s.logger.Printf("[WARN] Failed to remove stale asset %s: %v", name, err)
} else {
@@ -169,6 +176,7 @@ func (s *Syncer) Sync(ctx context.Context) error {
s.logger.Printf("[INFO] Asset sync complete: %d downloaded, %d unchanged, %d removed (%d total files)",
downloaded, skipped, removed, len(manifest.Files))
s.dbg("sync completed in %s", time.Since(syncStart).Round(time.Millisecond))
return nil
}
@@ -223,9 +231,11 @@ func (s *Syncer) fetchManifest(ctx context.Context) (*HubManifest, error) {
resp, err := s.httpClient.Do(req)
if err != nil {
s.dbg("fetchManifest: HTTP request failed: %v", err)
return nil, fmt.Errorf("HTTP request: %w", err)
}
defer resp.Body.Close()
s.dbg("fetchManifest: HTTP %d, content-length=%d", resp.StatusCode, resp.ContentLength)
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
@@ -270,13 +280,16 @@ func (s *Syncer) downloadFile(ctx context.Context, filename string) error {
resp, err := s.httpClient.Do(req)
if err != nil {
s.dbg("downloadFile %s: HTTP request failed: %v", filename, err)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
s.dbg("downloadFile %s: unexpected HTTP %d", filename, resp.StatusCode)
return fmt.Errorf("HTTP %d for %s", resp.StatusCode, filename)
}
s.dbg("downloadFile %s: HTTP %d, content-length=%d", filename, resp.StatusCode, resp.ContentLength)
// Atomic write: write to .tmp, rename
dst := filepath.Join(s.assetsDir, filepath.Base(filename))