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 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 20:54:03 +01:00
parent e9551a27b9
commit 65c0da4a2b
2 changed files with 52 additions and 4 deletions
@@ -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)
+5 -4
View File
@@ -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")
}
}
}