package main import ( "context" "fmt" "time" "go-eve-pi/esi" "go-eve-pi/options" "go-eve-pi/repositories" "go-eve-pi/routes" "go-eve-pi/types" wh "go-eve-pi/webhook" logger "git.site.quack-lab.dev/dave/cylogger" ) // Orchestrator manages timer-based monitoring type Orchestrator struct { esiClient esi.ESIInterface ssoClient routes.SSOInterface database repositories.DatabaseInterface webhook wh.Webhook expiryAlert *time.Duration expiryCritical *time.Duration alertExpiryTimers map[string]*time.Timer // Track extractor expiry times criticalExpiryTimers map[string]*time.Timer // Track critical extractor expiry times } // NewOrchestrator creates a new orchestrator instance func NewOrchestrator(esiClient esi.ESIInterface, ssoClient routes.SSOInterface, database repositories.DatabaseInterface, webhook wh.Webhook) *Orchestrator { return &Orchestrator{ esiClient: esiClient, ssoClient: ssoClient, database: database, webhook: webhook, alertExpiryTimers: make(map[string]*time.Timer), criticalExpiryTimers: make(map[string]*time.Timer), } } // Start begins the orchestrator's timer-based operations func (o *Orchestrator) Start() { logger.Info("Starting orchestrator with cache validity: %s", options.GlobalOptions.CacheValidity) // Parse expiry warning and critical durations expiryWarning, err := time.ParseDuration(options.GlobalOptions.ExpiryWarning) if err != nil { logger.Error("Invalid expiry warning duration %s: %v", options.GlobalOptions.ExpiryWarning, err) return } o.expiryAlert = &expiryWarning expiryCritical, err := time.ParseDuration(options.GlobalOptions.ExpiryCritical) if err != nil { logger.Error("Invalid expiry critical duration %s: %v", options.GlobalOptions.ExpiryCritical, err) return } o.expiryCritical = &expiryCritical // Set up timers once o.setupTimers() logger.Info("Orchestrator started successfully") } // Stop stops all timers and cleans up resources func (o *Orchestrator) Stop() { logger.Info("Stopping orchestrator") // Stop all alert timers for key, timer := range o.alertExpiryTimers { timer.Stop() delete(o.alertExpiryTimers, key) } // Stop all critical timers for key, timer := range o.criticalExpiryTimers { timer.Stop() delete(o.criticalExpiryTimers, key) } logger.Info("Orchestrator stopped") } // setupTimers sets up all timers based on current extractor data func (o *Orchestrator) setupTimers() { logger.Info("Setting up timers for all characters") // Get all characters from database characters, err := o.database.Character().GetAllCharacters() if err != nil { logger.Error("Failed to get all characters: %v", err) return } logger.Info("Found %d characters to process", len(characters)) if len(characters) == 0 { logger.Info("No characters found in database") return } // Process each character and set up timers for i, char := range characters { logger.Info("Processing character %d/%d: %s", i+1, len(characters), char.CharacterName) o.setupCharacterTimers(char) } logger.Info("Completed timer setup for all characters") } // setupCharacterTimers sets up timers for a single character's extractors func (o *Orchestrator) setupCharacterTimers(char types.Character) { logger.Info("Setting up timers for character: %s (ID: %d)", char.CharacterName, char.ID) planets, err := o.esiClient.GetCharacterPlanets(context.Background(), int(char.ID), char.AccessToken) if err != nil { logger.Warning("Failed to get planets for character %s: %v", char.CharacterName, err) return } logger.Info("Got %d planets for character %s", len(planets), char.CharacterName) planetIds := make([]int64, len(planets)) for i, planet := range planets { planetIds[i] = planet.PlanetID } // Get extractors for this character logger.Info("Getting extractors for character %s", char.CharacterName) extractors, err := routes.GetExtractorsForCharacter(o.esiClient, int(char.ID), char.AccessToken, planetIds) if err != nil { logger.Warning("Failed to get extractors for character %s: %v", char.CharacterName, err) return } logger.Info("Got %d extractors for character %s", len(extractors), char.CharacterName) // Set up timers for each extractor for _, extractor := range extractors { o.setupExtractorTimers(char.CharacterName, extractor) } logger.Info("Completed timer setup for character %s", char.CharacterName) } // setupExtractorTimers sets up timers for a single extractor func (o *Orchestrator) setupExtractorTimers(characterName string, extractor routes.ExtractorInfo) { if extractor.ExpiryDate == "N/A" { logger.Info("Extractor %d has no expiry date, skipping", extractor.ExtractorNumber) return } logger.Info("Setting up timers for extractor %d with expiry date %s", extractor.ExtractorNumber, extractor.ExpiryDate) expiryTime, err := time.Parse(time.RFC3339, extractor.ExpiryDate) if err != nil { logger.Warning("Failed to parse expiry date %s: %v", extractor.ExpiryDate, err) return } // Create timer key for this extractor key := fmt.Sprintf("%s_%s_%d", characterName, extractor.PlanetName, extractor.ExtractorNumber) // Set warning timer to fire at the exact warning time warningAlertAt := expiryTime.Add(-*o.expiryAlert) logger.Info("Setting warning timer for extractor %d to fire at %s", extractor.ExtractorNumber, warningAlertAt.Format(time.RFC3339)) o.alertExpiryTimers[key] = time.AfterFunc(time.Until(warningAlertAt), func() { o.sendExpiryAlert(characterName, extractor, false) }) // Set critical timer to fire at the exact critical time criticalAlertAt := expiryTime.Add(-*o.expiryCritical) logger.Info("Setting critical timer for extractor %d to fire at %s", extractor.ExtractorNumber, criticalAlertAt.Format(time.RFC3339)) o.criticalExpiryTimers[key] = time.AfterFunc(time.Until(criticalAlertAt), func() { o.sendExpiryAlert(characterName, extractor, true) }) } // sendExpiryAlert sends an alert for extractor expiry func (o *Orchestrator) sendExpiryAlert(characterName string, extractor routes.ExtractorInfo, isCritical bool) { alertType := "WARNING" if isCritical { alertType = "CRITICAL" } message := fmt.Sprintf("%s: Extractor %d on %s expires at %s", alertType, extractor.ExtractorNumber, extractor.PlanetName, extractor.ExpiryDate) logger.Info("Sending %s alert for character %s: %s", alertType, characterName, message) o.sendWebhook(characterName, message) } // sendWebhook sends a webhook notification func (o *Orchestrator) sendWebhook(characterName, message string) { if o.webhook == nil { logger.Warning("Webhook not configured, skipping notification: %s", message) return } logger.Info("Sending webhook notification for character %s: %s", characterName, message) // Send webhook with character name as topic and message as content err := o.webhook.Post("EvePI", characterName, message) if err != nil { logger.Error("Failed to send webhook for character %s: %v", characterName, err) } }