From 65c0da4a2b54397341aa31b3cb7f886e624cd2e4 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Wed, 25 Feb 2026 20:54:03 +0100 Subject: [PATCH] Fix FileBrowser integration config lost after SyncFileBrowserMounts SyncFileBrowserMounts regenerates config.yaml from scratch, overwriting any integration config. The old approach used an async OnStackStart hook after container restart, which failed due to timing issues (stack state not yet refreshed). New approach: ReapplyConfigForTarget() writes integration config synchronously after config generation but before container restart, with a no-op RestartStack since the caller handles restart. Co-Authored-By: Claude Opus 4.6 --- controller/internal/integrations/manager.go | 47 +++++++++++++++++++++ controller/internal/web/handlers.go | 9 ++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/controller/internal/integrations/manager.go b/controller/internal/integrations/manager.go index 348a7bd..8cf8c01 100644 --- a/controller/internal/integrations/manager.go +++ b/controller/internal/integrations/manager.go @@ -184,6 +184,53 @@ func (m *Manager) buildApplyContext(provider, target string) (*ApplyContext, err }, nil } +// ReapplyConfigForTarget applies all active integrations targeting the given stack, +// writing config changes only (no container restart). Use this when the caller +// handles container restart separately (e.g. SyncFileBrowserMounts). +func (m *Manager) ReapplyConfigForTarget(targetName string) { + m.mu.Lock() + defer m.mu.Unlock() + + all := m.sett.GetIntegrationsForTarget(targetName) + for key, state := range all { + if !state.Enabled || state.Status == "disabled" { + continue + } + + provider, target, ok := ParseIntegrationKey(key) + if !ok { + continue + } + + handler, hOk := m.handlers[key] + if !hOk { + continue + } + + ac, err := m.buildApplyContext(provider, target) + if err != nil { + m.logger.Printf("[WARN] Cannot build context for integration %s reapply: %v", key, err) + continue + } + + // Override RestartStack to no-op — caller handles restart + ac.RestartStack = func(string) error { return nil } + + if err := handler.Apply(ac); err != nil { + m.logger.Printf("[WARN] Integration config reapply failed for %s: %v", key, err) + state.Status = "error" + state.LastError = err.Error() + _ = m.sett.SetIntegrationState(key, state) + continue + } + + state.Status = "active" + state.LastError = "" + _ = m.sett.SetIntegrationState(key, state) + m.logger.Printf("[INFO] Integration %s config reapplied for %s", key, targetName) + } +} + // loadDecryptedEnv reads app.yaml for a stack and decrypts sensitive values. func (m *Manager) loadDecryptedEnv(s *stacks.Stack) map[string]string { stackDir := filepath.Dir(s.ComposePath) diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index b0ca9a0..701519d 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -1605,6 +1605,11 @@ func (s *Server) SyncFileBrowserMounts() { return } + // Re-apply active integrations into config.yaml (before container restart) + if s.integrationMgr != nil { + s.integrationMgr.ReapplyConfigForTarget("filebrowser") + } + // Generate and write compose (includes config.yaml mount) compose := generateFileBrowserCompose(domain, storageMounts) if err := os.WriteFile(composePath, []byte(compose), 0644); err != nil { @@ -1621,10 +1626,6 @@ func (s *Server) SyncFileBrowserMounts() { s.logger.Printf("[ERROR] Failed to recreate FileBrowser: %s — %v", string(out), err) } else { s.logger.Printf("[INFO] FileBrowser mounts synced — %d storage path(s), config updated", len(paths)) - // Re-apply active integrations (config regeneration overwrites config.yaml) - if s.integrationMgr != nil { - go s.integrationMgr.OnStackStart(context.Background(), "filebrowser") - } } }