package main import ( "log" "os" "sync" "text/template" "time" "deadbeef.codes/steven/ynab-portfolio-monitor/ynab" ) var ( configuredProviders []AccountProvider // Any account providers that are successfully configured get added to this slice ynabClient *ynab.Client // YNAB HTTP client lastRefresh time.Time // Timestamp that last data refresh ran - used for rate limiting refreshRunning *sync.Mutex // True if a refresh is currently running t *template.Template // HTML templates in the templates directory ) // Called at program startup or if SIGHUP is received func init() { log.Printf("ynab-portfolio-monitor init") // Load mandatory application configuration from environment variables envVars := make(map[string]string) envVars["ynab_secret"] = os.Getenv("ynab_secret") envVars["ynab_budget_id"] = os.Getenv("ynab_budget_id") // Validate that all required environment variables are set for key, value := range envVars { if value == "" { log.Fatalf("shell environment variable %s is not set", key) } } // Loop through all account providers and attempt to configure them // if configuration fails, the provider will not be used configuredProviders = make([]AccountProvider, 0) for _, p := range allProviders { err := p.Configure() if err != nil { log.Printf("skipping provider '%s': %v", p.Name(), err) continue } configuredProviders = append(configuredProviders, p) log.Printf("enabled provider '%s'", p.Name()) } // ynab client is static and has no persistent data so is initialized here and not in main program loop var err error ynabClient, err = ynab.NewClient(envVars["ynab_budget_id"], envVars["ynab_secret"]) if err != nil { log.Fatalf("failed to create ynab client: %v", err) } // Web Templates // Parse all template files at startup t, err = template.ParseGlob("./templates/*") if err != nil { log.Fatalf("couldn't parse HTML templates: %v", err) } refreshRunning = &sync.Mutex{} } func main() { go webServer() for { // Main program loop refreshData() log.Print("Sleeping for 6 hours...") time.Sleep(time.Hour * 6) } } func refreshData() { refreshRunning.Lock() defer refreshRunning.Unlock() // Only allow a refresh at most once every 5 minutes if time.Now().Before(lastRefresh.Add(time.Minute * 5)) { log.Printf("refresh rate limited") return } lastRefresh = time.Now() // Loop through each configured account provider and attempt to get the account balances, and update YNAB for _, p := range configuredProviders { balances, accountIDs, err := p.GetBalances() if err != nil { log.Printf("failed to get balances with provider '%s': %v", p.Name(), err) continue } if len(balances) != len(accountIDs) { log.Printf("'%s' provider data validation error: mismatched balance and accountID slice lengths - expected the same: balances length = %d, accountIDs length = %d", p.Name(), len(balances), len(accountIDs)) continue } for i := range balances { err = ynabClient.SetAccountBalance(accountIDs[i], balances[i]) if err != nil { log.Printf("failed to update ynab account '%s' balance: %v", accountIDs[i], err) } } } }