package integrations import ( "context" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // OnStackStop is called when a stack is stopped. // Revokes active integrations where this stack is provider or target. // Keeps enabled=true so OnStackStart can re-apply them later. func (m *Manager) OnStackStop(_ context.Context, stackName string) { m.mu.Lock() defer m.mu.Unlock() all := m.sett.GetIntegrationsForProvider(stackName) targetAll := m.sett.GetIntegrationsForTarget(stackName) for k, v := range targetAll { all[k] = v } if m.isDebug() { m.logger.Printf("[DEBUG] [integrations] OnStackStop: stack=%s integrationsFound=%d", stackName, len(all)) } 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] [integrations] Cannot build context for integration %s revoke: %v", key, err) continue } if m.isDebug() { m.logger.Printf("[DEBUG] [integrations] OnStackStop: revoking %s", key) } if err := handler.Revoke(ac); err != nil { m.logger.Printf("[WARN] [integrations] Integration revoke on stop failed for %s: %v", key, err) } if provider == stackName { state.Status = "provider_stopped" } else { state.Status = "target_unavailable" } _ = m.sett.SetIntegrationState(key, state) m.logger.Printf("[INFO] [integrations] Integration %s suspended (stack %s stopped)", key, stackName) } } // OnStackStart is called when a stack starts. // Re-applies integrations that were previously enabled but are not currently active. // Waits briefly for the stack manager to refresh container state. func (m *Manager) OnStackStart(_ context.Context, stackName string) { if m.isDebug() { m.logger.Printf("[DEBUG] [integrations] OnStackStart: stack=%s, waiting 5s for state refresh", stackName) } // Brief delay so the stack manager's periodic status refresh // picks up the new container state (runs every 30s). time.Sleep(5 * time.Second) m.mu.Lock() defer m.mu.Unlock() all := m.sett.GetIntegrationsForProvider(stackName) targetAll := m.sett.GetIntegrationsForTarget(stackName) for k, v := range targetAll { all[k] = v } if m.isDebug() { m.logger.Printf("[DEBUG] [integrations] OnStackStart: stack=%s integrationsFound=%d", stackName, len(all)) } for key, state := range all { if !state.Enabled || state.Status == "active" { continue } provider, target, ok := ParseIntegrationKey(key) if !ok { continue } // Both provider and target must be running (or starting) to re-apply. // StateStarting = container running but healthcheck hasn't passed yet — still connectable. provStack, pOk := m.stacks.GetStack(provider) if !pOk || !provStack.Deployed || !isStackUp(provStack.State) { if m.isDebug() { m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — provider %s not up (found=%v deployed=%v state=%v)", key, provider, pOk, pOk && provStack.Deployed, func() stacks.ContainerState { if pOk { return provStack.State }; return "" }()) } continue } if target != "filebrowser" { tgtStack, tOk := m.stacks.GetStack(target) if !tOk || !tgtStack.Deployed || !isStackUp(tgtStack.State) { if m.isDebug() { m.logger.Printf("[DEBUG] [integrations] OnStackStart: skipping %s — target %s not up (found=%v deployed=%v state=%v)", key, target, tOk, tOk && tgtStack.Deployed, func() stacks.ContainerState { if tOk { return tgtStack.State }; return "" }()) } continue } } handler, hOk := m.handlers[key] if !hOk { continue } if m.isDebug() { m.logger.Printf("[DEBUG] [integrations] OnStackStart: re-applying %s (currentStatus=%s)", key, state.Status) } ac, err := m.buildApplyContext(provider, target) if err != nil { m.logger.Printf("[WARN] [integrations] Cannot re-apply integration %s on start: %v", key, err) continue } if err := handler.Apply(ac); err != nil { m.logger.Printf("[ERROR] [integrations] Integration re-apply on start 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] [integrations] Integration %s re-activated (stack %s started)", key, stackName) } } // isStackUp returns true if the stack is running or starting (healthcheck pending). func isStackUp(state stacks.ContainerState) bool { return state == stacks.StateRunning || state == stacks.StateStarting } // OnStackRemove is called when a stack is removed. // Permanently revokes and deletes integration state. func (m *Manager) OnStackRemove(_ context.Context, stackName string) { m.mu.Lock() defer m.mu.Unlock() all := m.sett.GetIntegrationsForProvider(stackName) targetAll := m.sett.GetIntegrationsForTarget(stackName) for k, v := range targetAll { all[k] = v } if m.isDebug() { m.logger.Printf("[DEBUG] [integrations] OnStackRemove: stack=%s integrationsFound=%d", stackName, len(all)) } for key, state := range all { provider, target, ok := ParseIntegrationKey(key) if !ok { continue } if state.Enabled { if m.isDebug() { m.logger.Printf("[DEBUG] [integrations] OnStackRemove: revoking enabled integration %s", key) } handler, hOk := m.handlers[key] if hOk { ac, _ := m.buildApplyContext(provider, target) if ac != nil { _ = handler.Revoke(ac) } } } _ = m.sett.RemoveIntegrationState(key) m.logger.Printf("[INFO] [integrations] Integration %s removed (stack %s removed)", key, stackName) } }