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
}