Add scheduled tasks with a Project tab section and lead tools

This commit is contained in:
Ian Gulliver
2026-10-21 15:15:20 -07:00
parent 4b4763779d
commit f445e1cac3
21 changed files with 1689 additions and 347 deletions
+124
View File
@@ -0,0 +1,124 @@
package schedule
import (
"errors"
"fmt"
"slices"
"strings"
"time"
"g.fc.run/ai/byre/internal/api"
)
var ErrInvalid = errors.New("invalid schedule")
var Weekdays = []api.Weekday{api.Mon, api.Tue, api.Wed, api.Thu, api.Fri, api.Sat, api.Sun}
var goWeekdays = map[api.Weekday]time.Weekday{
api.Mon: time.Monday,
api.Tue: time.Tuesday,
api.Wed: time.Wednesday,
api.Thu: time.Thursday,
api.Fri: time.Friday,
api.Sat: time.Saturday,
api.Sun: time.Sunday,
}
func invalid(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrInvalid, fmt.Sprintf(format, args...))
}
func Timing(save api.ScheduleSave) (api.Schedule, error) {
sc := api.Schedule{
Title: strings.TrimSpace(save.Title),
Every: save.Every,
Weekdays: []api.Weekday{},
Hour: save.Hour,
Minute: save.Minute,
}
if sc.Title == "" {
return sc, invalid("title is required")
}
if save.Minute < 0 || save.Minute > 59 {
return sc, invalid("minute must be 0 to 59")
}
if save.Hour < 0 || save.Hour > 23 {
return sc, invalid("hour must be 0 to 23")
}
switch save.Every {
case api.Hour, api.Day, api.Week, api.Month:
default:
return sc, invalid("every must be hour, day, week or month")
}
if save.Every == api.Hour && save.Hour != 0 {
return sc, invalid("an hourly schedule runs at a minute past every hour, so hour must be 0")
}
if (save.Weekdays != nil && len(*save.Weekdays) > 0) != (save.Every == api.Week) {
return sc, invalid("weekdays are required for a weekly schedule, and only allowed for one")
}
if save.Weekdays != nil {
for _, d := range Weekdays {
if slices.Contains(*save.Weekdays, d) {
sc.Weekdays = append(sc.Weekdays, d)
}
}
for _, d := range *save.Weekdays {
if _, found := goWeekdays[d]; !found {
return sc, invalid("unknown weekday %q", d)
}
}
}
if (save.DayOfMonth != nil) != (save.Every == api.Month) {
return sc, invalid("day_of_month is required for a monthly schedule, and only allowed for one")
}
if save.DayOfMonth != nil {
if *save.DayOfMonth < 1 || *save.DayOfMonth > 31 {
return sc, invalid("day_of_month must be 1 to 31")
}
sc.DayOfMonth = *save.DayOfMonth
}
return sc, nil
}
func Next(sc api.Schedule, after time.Time) time.Time {
t := after.Local()
y, m, d := t.Date()
at := func(y int, m time.Month, d, h int) time.Time {
return time.Date(y, m, d, h, sc.Minute, 0, 0, time.Local)
}
switch sc.Every {
case api.Hour:
c := at(y, m, d, t.Hour())
for !c.After(t) {
c = c.Add(time.Hour)
}
return c
case api.Day:
c := at(y, m, d, sc.Hour)
if !c.After(t) {
c = at(y, m, d+1, sc.Hour)
}
return c
case api.Week:
days := map[time.Weekday]bool{}
for _, w := range sc.Weekdays {
days[goWeekdays[w]] = true
}
for i := 0; i <= 7; i++ {
c := at(y, m, d+i, sc.Hour)
if days[c.Weekday()] && c.After(t) {
return c
}
}
case api.Month:
for i := 0; i <= 1; i++ {
first := time.Date(y, m+time.Month(i), 1, 0, 0, 0, 0, time.Local)
last := first.AddDate(0, 1, -1).Day()
c := at(first.Year(), first.Month(), min(sc.DayOfMonth, last), sc.Hour)
if c.After(t) {
return c
}
}
}
panic(fmt.Sprintf("schedule %s has no next run after %s", sc.Id, after))
}