Ryanhub - file viewer
filename: automation/scheduler.go
branch: main
back to repo
package automation

import (
	"context"
	"strings"
	"sync"
	"time"

	"assistant/agent"
	"assistant/config"
	"assistant/util"
)

// Start launches background jobs. Call once from main.
func Start(ctx context.Context, cfg *config.Config, ag *agent.Agent, tel *agent.Telemetry) {
	loc := time.Local
	if tz := strings.TrimSpace(cfg.Automation.Timezone); tz != "" {
		l, err := time.LoadLocation(tz)
		if err != nil {
			util.Logf("automation: invalid timezone %q, using host local: %v", tz, err)
		} else {
			loc = l
		}
	}
	var mu sync.Mutex
	lastFired := map[string]string{} // key -> "2006-01-02 15:04" in loc
	go func() {
		tick := time.NewTicker(15 * time.Second)
		defer tick.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-tick.C:
				now := time.Now().In(loc)
				if tel != nil {
					tel.SetAutomationNext("morning_briefing", nextDailyAt(now, cfg.Automation.MorningBriefingHour))
					tel.SetAutomationNext("after_dinner_recap", nextDailyAt(now, cfg.Automation.AfterDinnerRecapHour))
				}
				stamp := now.Format("2006-01-02 15:04")
				runOnce := func(name string, hour int, fn func(context.Context, *config.Config, *agent.Agent, *agent.Telemetry)) {
					if now.Hour() != hour || now.Minute() != 0 {
						return
					}
					mu.Lock()
					if lastFired[name] == stamp {
						mu.Unlock()
						return
					}
					lastFired[name] = stamp
					mu.Unlock()
					runCtx, cancel := context.WithTimeout(context.Background(), 20*time.Minute)
					fn(runCtx, cfg, ag, tel)
					cancel()
				}
				runOnce("morning_briefing", cfg.Automation.MorningBriefingHour, runMorningBriefing)
				runOnce("after_dinner_recap", cfg.Automation.AfterDinnerRecapHour, runAfterDinnerRecap)
			}
		}
	}()
	util.Logf("automation: morning briefing scheduled at %02d:00 (%s)",
		cfg.Automation.MorningBriefingHour, loc.String())
	util.Logf("automation: after dinner recap scheduled at %02d:00 (%s)",
		cfg.Automation.AfterDinnerRecapHour, loc.String())
}

func nextDailyAt(now time.Time, hour int) time.Time {
	// next time at HH:00:00 in the provided timezone.
	candidate := time.Date(now.Year(), now.Month(), now.Day(), hour, 0, 0, 0, now.Location())
	if !candidate.After(now) {
		candidate = candidate.Add(24 * time.Hour)
	}
	return candidate
}