feat(worker): compute prime due-time from weekday bitmask

Also fixes PrimeScheduleRepository.ListAsync to sort client-side
(SQLite EF Core does not support TimeSpan in ORDER BY clauses).
This commit is contained in:
mika kuns
2026-06-02 16:32:51 +02:00
parent dff06d9e35
commit bed4255a5e
4 changed files with 73 additions and 62 deletions

View File

@@ -12,10 +12,8 @@ public sealed class PrimeScheduleRepository
public async Task<IReadOnlyList<PrimeScheduleEntity>> ListAsync(CancellationToken ct = default)
{
var rows = await _context.PrimeSchedules.AsNoTracking()
.OrderBy(s => s.TimeOfDay)
.ToListAsync(ct);
return rows;
var rows = await _context.PrimeSchedules.AsNoTracking().ToListAsync(ct);
return rows.OrderBy(s => s.TimeOfDay).ToList();
}
public async Task<PrimeScheduleEntity?> GetAsync(Guid id, CancellationToken ct = default) =>

View File

@@ -1,3 +1,5 @@
using ClaudeDo.Data.Models;
namespace ClaudeDo.Worker.Prime;
public sealed record NextDue(PrimeScheduleDto Schedule, DateTimeOffset At, bool FireImmediately);
@@ -22,30 +24,24 @@ public static class NextDueCalculator
private static NextDue? ComputeFor(PrimeScheduleDto s, DateTimeOffset now, TimeSpan catchUp)
{
if (s.EndDate < DateOnly.FromDateTime(now.LocalDateTime)) return null;
if ((PrimeDays)s.Days == PrimeDays.None) return null;
var todayLocal = DateOnly.FromDateTime(now.LocalDateTime);
var alreadyFiredToday = s.LastRunAt is { } last &&
DateOnly.FromDateTime(last.LocalDateTime) == todayLocal;
if (!alreadyFiredToday)
if (!alreadyFiredToday && IsEligibleDay(s, todayLocal))
{
var startOrToday = s.StartDate > todayLocal ? s.StartDate : todayLocal;
if (startOrToday == todayLocal && IsEligibleDay(s, todayLocal))
{
var todayTarget = ToOffset(todayLocal, s.TimeOfDay, now.Offset);
if (todayTarget >= now)
return new NextDue(s, todayTarget, false);
if (now <= todayTarget + catchUp)
return new NextDue(s, now, true);
}
var todayTarget = ToOffset(todayLocal, s.TimeOfDay, now.Offset);
if (todayTarget >= now)
return new NextDue(s, todayTarget, false);
if (now <= todayTarget + catchUp)
return new NextDue(s, now, true);
}
var d = todayLocal.AddDays(1);
if (s.StartDate > d) d = s.StartDate;
for (int i = 0; i < 8; i++)
for (int i = 0; i < 7; i++)
{
if (d > s.EndDate) return null;
if (IsEligibleDay(s, d))
return new NextDue(s, ToOffset(d, s.TimeOfDay, now.Offset), false);
d = d.AddDays(1);
@@ -53,13 +49,20 @@ public static class NextDueCalculator
return null;
}
private static bool IsEligibleDay(PrimeScheduleDto s, DateOnly d)
private static bool IsEligibleDay(PrimeScheduleDto s, DateOnly d) =>
((PrimeDays)s.Days & ToFlag(d.DayOfWeek)) != PrimeDays.None;
private static PrimeDays ToFlag(DayOfWeek dow) => dow switch
{
if (d < s.StartDate || d > s.EndDate) return false;
if (!s.WorkdaysOnly) return true;
var dow = d.ToDateTime(TimeOnly.MinValue).DayOfWeek;
return dow != DayOfWeek.Saturday && dow != DayOfWeek.Sunday;
}
DayOfWeek.Monday => PrimeDays.Monday,
DayOfWeek.Tuesday => PrimeDays.Tuesday,
DayOfWeek.Wednesday => PrimeDays.Wednesday,
DayOfWeek.Thursday => PrimeDays.Thursday,
DayOfWeek.Friday => PrimeDays.Friday,
DayOfWeek.Saturday => PrimeDays.Saturday,
DayOfWeek.Sunday => PrimeDays.Sunday,
_ => PrimeDays.None,
};
private static DateTimeOffset ToOffset(DateOnly day, TimeSpan time, TimeSpan offset) =>
new(day.Year, day.Month, day.Day, time.Hours, time.Minutes, time.Seconds, offset);

View File

@@ -2,10 +2,8 @@ namespace ClaudeDo.Worker.Prime;
public sealed record PrimeScheduleDto(
Guid Id,
DateOnly StartDate,
DateOnly EndDate,
int Days,
TimeSpan TimeOfDay,
bool WorkdaysOnly,
bool Enabled,
DateTimeOffset? LastRunAt,
string? PromptOverride);