984 lines
37 KiB
Markdown
984 lines
37 KiB
Markdown
# Prime Recurring Weekday Schedule — Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Replace the Prime schedule's date-range model with a recurring weekday model — pick a set of weekdays plus a time, and the ping fires on the next eligible day the worker is running.
|
||
|
||
**Architecture:** A `[Flags] PrimeDays` weekday bitmask stored as a single `days_of_week` int column replaces `StartDate`/`EndDate`/`WorkdaysOnly`. `NextDueCalculator` walks forward to the next selected weekday; the existing 30-minute catch-up and already-fired-today logic are untouched. UI swaps the range picker + Mon–Fri checkbox for seven toggle buttons. Both SignalR DTO copies carry a single `int Days`.
|
||
|
||
**Tech Stack:** .NET 8, EF Core (SQLite), Avalonia 12 (CommunityToolkit.Mvvm), SignalR, xUnit.
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-06-02-prime-recurring-weekdays-design.md`
|
||
|
||
**Build/test note:** `dotnet build ClaudeDo.slnx` needs .NET 9; on .NET 8 build individual csproj. Commands in this plan use the per-project form.
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
- `src/ClaudeDo.Data/Models/PrimeDays.cs` — **new**, `[Flags]` enum.
|
||
- `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs` — swap fields.
|
||
- `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs` — column mapping.
|
||
- `src/ClaudeDo.Data/Migrations/*` — new migration + snapshot.
|
||
- `src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs` — upsert fields + ordering.
|
||
- `src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs` — `int Days`.
|
||
- `src/ClaudeDo.Worker/Prime/NextDueCalculator.cs` — weekday eligibility.
|
||
- `src/ClaudeDo.Worker/Prime/PrimeScheduler.cs` — `ToDto` mapping.
|
||
- `src/ClaudeDo.Worker/Hub/WorkerHub.cs` — list/upsert mapping.
|
||
- `src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs` — `int Days`.
|
||
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs` — 7 day bools.
|
||
- `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs` — defaults + validation.
|
||
- `src/ClaudeDo.Ui/Design/IslandStyles.axaml` — `day-toggle` style class.
|
||
- `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml` — row template.
|
||
- Tests: `NextDueCalculatorTests`, `PrimeSchedulerTests`, `PrimeScheduleRepositoryTests`, `PrimeClaudeTabViewModelTests`.
|
||
- Docs: `src/ClaudeDo.Data/CLAUDE.md`, root `CLAUDE.md`.
|
||
|
||
---
|
||
|
||
## Task 1: PrimeDays enum + entity + configuration
|
||
|
||
**Files:**
|
||
- Create: `src/ClaudeDo.Data/Models/PrimeDays.cs`
|
||
- Modify: `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs`
|
||
- Modify: `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs`
|
||
|
||
- [ ] **Step 1: Create the flags enum**
|
||
|
||
`src/ClaudeDo.Data/Models/PrimeDays.cs`:
|
||
|
||
```csharp
|
||
namespace ClaudeDo.Data.Models;
|
||
|
||
[Flags]
|
||
public enum PrimeDays
|
||
{
|
||
None = 0,
|
||
Monday = 1,
|
||
Tuesday = 2,
|
||
Wednesday = 4,
|
||
Thursday = 8,
|
||
Friday = 16,
|
||
Saturday = 32,
|
||
Sunday = 64,
|
||
Weekdays = Monday | Tuesday | Wednesday | Thursday | Friday, // 31
|
||
All = Weekdays | Saturday | Sunday, // 127
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Swap entity fields**
|
||
|
||
In `src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs`, remove `StartDate`, `EndDate`, `WorkdaysOnly` and add `Days`. Result:
|
||
|
||
```csharp
|
||
namespace ClaudeDo.Data.Models;
|
||
|
||
public sealed class PrimeScheduleEntity
|
||
{
|
||
public Guid Id { get; set; } = Guid.NewGuid();
|
||
public PrimeDays Days { get; set; } = PrimeDays.Weekdays;
|
||
public TimeSpan TimeOfDay { get; set; }
|
||
public bool Enabled { get; set; } = true;
|
||
public DateTimeOffset? LastRunAt { get; set; }
|
||
public string? PromptOverride { get; set; }
|
||
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Update entity configuration**
|
||
|
||
In `src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs`, replace the `start_date`/`end_date`/`workdays_only` property lines with a `days_of_week` mapping (EF maps the enum to INTEGER automatically):
|
||
|
||
```csharp
|
||
builder.Property(s => s.Days).HasColumnName("days_of_week")
|
||
.IsRequired().HasDefaultValue(PrimeDays.Weekdays);
|
||
builder.Property(s => s.TimeOfDay).HasColumnName("time_of_day").IsRequired();
|
||
builder.Property(s => s.Enabled).HasColumnName("enabled").IsRequired().HasDefaultValue(true);
|
||
```
|
||
|
||
Leave `Id`, `LastRunAt`, `PromptOverride`, `CreatedAt` mappings unchanged. Add `using ClaudeDo.Data.Models;` if not present (it already is).
|
||
|
||
- [ ] **Step 4: Build the Data project**
|
||
|
||
Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`
|
||
Expected: FAILS — `PrimeScheduleRepository`, snapshot, etc. still reference removed fields. That is expected; Tasks 2–3 fix it. (If you prefer a clean build gate, proceed to Task 2 before building.)
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ClaudeDo.Data/Models/PrimeDays.cs src/ClaudeDo.Data/Models/PrimeScheduleEntity.cs src/ClaudeDo.Data/Configuration/PrimeScheduleEntityConfiguration.cs
|
||
git commit -m "feat(data): model Prime schedule as weekday bitmask"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Repository
|
||
|
||
**Files:**
|
||
- Modify: `src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs`
|
||
|
||
- [ ] **Step 1: Update `ListAsync` ordering**
|
||
|
||
The old ordering used `StartDate`. Order by `TimeOfDay`:
|
||
|
||
```csharp
|
||
public async Task<IReadOnlyList<PrimeScheduleEntity>> ListAsync(CancellationToken ct = default)
|
||
{
|
||
var rows = await _context.PrimeSchedules.AsNoTracking()
|
||
.OrderBy(s => s.TimeOfDay)
|
||
.ToListAsync(ct);
|
||
return rows;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Update `UpsertAsync` field copy**
|
||
|
||
Replace the three removed-field assignments with `Days`:
|
||
|
||
```csharp
|
||
else
|
||
{
|
||
existing.Days = entity.Days;
|
||
existing.TimeOfDay = entity.TimeOfDay;
|
||
existing.Enabled = entity.Enabled;
|
||
existing.PromptOverride = entity.PromptOverride;
|
||
}
|
||
```
|
||
|
||
Leave `GetAsync`, `DeleteAsync`, `UpdateLastRunAsync` unchanged.
|
||
|
||
- [ ] **Step 3: Commit** (build verified after migration in Task 3)
|
||
|
||
```bash
|
||
git add src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs
|
||
git commit -m "feat(data): persist weekday bitmask in prime schedule repo"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: EF migration
|
||
|
||
**Files:**
|
||
- Create: `src/ClaudeDo.Data/Migrations/<timestamp>_PrimeWeekdays.cs` (generated)
|
||
- Modify: `src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs` (generated)
|
||
|
||
- [ ] **Step 1: Generate the migration**
|
||
|
||
Run from repo root:
|
||
|
||
```bash
|
||
dotnet ef migrations add PrimeWeekdays --project src/ClaudeDo.Data/ClaudeDo.Data.csproj --startup-project src/ClaudeDo.Worker/ClaudeDo.Worker.csproj
|
||
```
|
||
|
||
Expected: a new `*_PrimeWeekdays.cs` file and an updated snapshot. (If `dotnet ef` is unavailable, hand-write the migration using the body below.)
|
||
|
||
- [ ] **Step 2: Replace the generated `Up` body with an explicit backfill**
|
||
|
||
EF's auto-generated drop/add would discard existing schedules' weekday intent. Edit the new migration's `Up` to add the column, backfill from `workdays_only`, then drop the old columns:
|
||
|
||
```csharp
|
||
protected override void Up(MigrationBuilder migrationBuilder)
|
||
{
|
||
migrationBuilder.AddColumn<int>(
|
||
name: "days_of_week",
|
||
table: "prime_schedules",
|
||
type: "INTEGER",
|
||
nullable: false,
|
||
defaultValue: 31);
|
||
|
||
migrationBuilder.Sql(
|
||
"UPDATE prime_schedules SET days_of_week = CASE WHEN workdays_only = 1 THEN 31 ELSE 127 END;");
|
||
|
||
migrationBuilder.DropColumn(name: "start_date", table: "prime_schedules");
|
||
migrationBuilder.DropColumn(name: "end_date", table: "prime_schedules");
|
||
migrationBuilder.DropColumn(name: "workdays_only", table: "prime_schedules");
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Replace the generated `Down` body**
|
||
|
||
```csharp
|
||
protected override void Down(MigrationBuilder migrationBuilder)
|
||
{
|
||
migrationBuilder.AddColumn<DateOnly>(
|
||
name: "start_date", table: "prime_schedules",
|
||
type: "TEXT", nullable: false, defaultValue: new DateOnly(2000, 1, 1));
|
||
migrationBuilder.AddColumn<DateOnly>(
|
||
name: "end_date", table: "prime_schedules",
|
||
type: "TEXT", nullable: false, defaultValue: new DateOnly(2099, 12, 31));
|
||
migrationBuilder.AddColumn<bool>(
|
||
name: "workdays_only", table: "prime_schedules",
|
||
type: "INTEGER", nullable: false, defaultValue: true);
|
||
|
||
migrationBuilder.Sql(
|
||
"UPDATE prime_schedules SET workdays_only = CASE WHEN days_of_week = 127 THEN 0 ELSE 1 END;");
|
||
|
||
migrationBuilder.DropColumn(name: "days_of_week", table: "prime_schedules");
|
||
}
|
||
```
|
||
|
||
Add `using System;` at the top of the migration file if `DateOnly` defaults require it (the existing AddPrimeSchedules migration already imports `System`).
|
||
|
||
- [ ] **Step 4: Build the Data project**
|
||
|
||
Run: `dotnet build src/ClaudeDo.Data/ClaudeDo.Data.csproj`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ClaudeDo.Data/Migrations
|
||
git commit -m "feat(data): migrate prime schedules to days_of_week bitmask"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Worker DTO + NextDueCalculator (TDD)
|
||
|
||
**Files:**
|
||
- Modify: `src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs`
|
||
- Modify: `src/ClaudeDo.Worker/Prime/NextDueCalculator.cs`
|
||
- Test: `tests/ClaudeDo.Worker.Tests/Prime/NextDueCalculatorTests.cs`
|
||
|
||
- [ ] **Step 1: Update the Worker DTO**
|
||
|
||
`src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs`:
|
||
|
||
```csharp
|
||
namespace ClaudeDo.Worker.Prime;
|
||
|
||
public sealed record PrimeScheduleDto(
|
||
Guid Id,
|
||
int Days,
|
||
TimeSpan TimeOfDay,
|
||
bool Enabled,
|
||
DateTimeOffset? LastRunAt,
|
||
string? PromptOverride);
|
||
```
|
||
|
||
- [ ] **Step 2: Rewrite the calculator tests**
|
||
|
||
Replace the entire body of `tests/ClaudeDo.Worker.Tests/Prime/NextDueCalculatorTests.cs`. Note: 2026-05-05 is a Tuesday; 2026-05-08 is a Friday; 2026-05-09/10 are Sat/Sun; 2026-05-11 is a Monday.
|
||
|
||
```csharp
|
||
using ClaudeDo.Data.Models;
|
||
using ClaudeDo.Worker.Prime;
|
||
|
||
namespace ClaudeDo.Worker.Tests.Prime;
|
||
|
||
public class NextDueCalculatorTests
|
||
{
|
||
private static PrimeScheduleDto Schedule(
|
||
PrimeDays days, TimeSpan time,
|
||
bool enabled = true, DateTimeOffset? lastRun = null) =>
|
||
new(Guid.NewGuid(), (int)days, time, enabled, lastRun, null);
|
||
|
||
[Fact]
|
||
public void Disabled_Schedule_Returns_Null()
|
||
{
|
||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
|
||
var s = Schedule(PrimeDays.All, new(7, 0, 0), enabled: false);
|
||
Assert.Null(NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30)));
|
||
}
|
||
|
||
[Fact]
|
||
public void No_Days_Selected_Returns_Null()
|
||
{
|
||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
|
||
var s = Schedule(PrimeDays.None, new(7, 0, 0));
|
||
Assert.Null(NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30)));
|
||
}
|
||
|
||
[Fact]
|
||
public void Future_Same_Day_Returns_Today_At_Target()
|
||
{
|
||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2)); // Tue
|
||
var s = Schedule(PrimeDays.All, new(7, 0, 0));
|
||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||
Assert.NotNull(r);
|
||
Assert.Equal(new DateTimeOffset(2026, 5, 5, 7, 0, 0, now.Offset), r!.At);
|
||
Assert.False(r.FireImmediately);
|
||
}
|
||
|
||
[Fact]
|
||
public void Within_CatchUp_Window_Fires_Immediately()
|
||
{
|
||
var now = new DateTimeOffset(2026, 5, 5, 7, 15, 0, TimeSpan.FromHours(2));
|
||
var s = Schedule(PrimeDays.All, new(7, 0, 0));
|
||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||
Assert.NotNull(r);
|
||
Assert.True(r!.FireImmediately);
|
||
}
|
||
|
||
[Fact]
|
||
public void Past_CatchUp_Window_Skips_To_Next_Eligible_Day()
|
||
{
|
||
var now = new DateTimeOffset(2026, 5, 5, 9, 0, 0, TimeSpan.FromHours(2)); // Tue
|
||
var s = Schedule(PrimeDays.All, new(7, 0, 0));
|
||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||
Assert.NotNull(r);
|
||
Assert.Equal(new DateOnly(2026, 5, 6), DateOnly.FromDateTime(r!.At.LocalDateTime));
|
||
}
|
||
|
||
[Fact]
|
||
public void Weekdays_Only_Skips_Weekend()
|
||
{
|
||
var now = new DateTimeOffset(2026, 5, 8, 8, 0, 0, TimeSpan.FromHours(2)); // Fri, past catch-up
|
||
var s = Schedule(PrimeDays.Weekdays, new(7, 0, 0));
|
||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||
Assert.NotNull(r);
|
||
Assert.Equal(DayOfWeek.Monday, r!.At.LocalDateTime.DayOfWeek);
|
||
Assert.Equal(new DateOnly(2026, 5, 11), DateOnly.FromDateTime(r.At.LocalDateTime));
|
||
}
|
||
|
||
[Fact]
|
||
public void Single_Day_Schedule_Targets_That_Weekday()
|
||
{
|
||
var now = new DateTimeOffset(2026, 5, 5, 8, 0, 0, TimeSpan.FromHours(2)); // Tue, past catch-up
|
||
var s = Schedule(PrimeDays.Friday, new(7, 0, 0));
|
||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||
Assert.NotNull(r);
|
||
Assert.Equal(DayOfWeek.Friday, r!.At.LocalDateTime.DayOfWeek);
|
||
Assert.Equal(new DateOnly(2026, 5, 8), DateOnly.FromDateTime(r.At.LocalDateTime));
|
||
}
|
||
|
||
[Fact]
|
||
public void Already_Fired_Today_Skips_To_Next_Eligible_Day()
|
||
{
|
||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
|
||
var lastRun = new DateTimeOffset(2026, 5, 5, 7, 1, 0, TimeSpan.FromHours(2));
|
||
var s = Schedule(PrimeDays.All, new(7, 0, 0), lastRun: lastRun);
|
||
var r = NextDueCalculator.Compute(new[] { s }, now, TimeSpan.FromMinutes(30));
|
||
Assert.NotNull(r);
|
||
Assert.Equal(new DateOnly(2026, 5, 6), DateOnly.FromDateTime(r!.At.LocalDateTime));
|
||
}
|
||
|
||
[Fact]
|
||
public void Multiple_Schedules_Returns_Earliest()
|
||
{
|
||
var now = new DateTimeOffset(2026, 5, 5, 6, 0, 0, TimeSpan.FromHours(2));
|
||
var early = Schedule(PrimeDays.All, new(7, 0, 0));
|
||
var late = Schedule(PrimeDays.All, new(9, 0, 0));
|
||
var r = NextDueCalculator.Compute(new[] { late, early }, now, TimeSpan.FromMinutes(30));
|
||
Assert.NotNull(r);
|
||
Assert.Equal(early.Id, r!.Schedule.Id);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Run the tests to verify they fail**
|
||
|
||
Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter FullyQualifiedName~NextDueCalculatorTests`
|
||
Expected: FAIL — `PrimeScheduleDto` no longer has `StartDate`/`EndDate`/`workdaysOnly`, and the calculator still references them (compile errors).
|
||
|
||
- [ ] **Step 4: Rewrite the calculator**
|
||
|
||
Replace the entire body of `src/ClaudeDo.Worker/Prime/NextDueCalculator.cs`:
|
||
|
||
```csharp
|
||
using ClaudeDo.Data.Models;
|
||
|
||
namespace ClaudeDo.Worker.Prime;
|
||
|
||
public sealed record NextDue(PrimeScheduleDto Schedule, DateTimeOffset At, bool FireImmediately);
|
||
|
||
public static class NextDueCalculator
|
||
{
|
||
public static NextDue? Compute(
|
||
IEnumerable<PrimeScheduleDto> schedules,
|
||
DateTimeOffset now,
|
||
TimeSpan catchUp)
|
||
{
|
||
NextDue? best = null;
|
||
foreach (var s in schedules)
|
||
{
|
||
if (!s.Enabled) continue;
|
||
var due = ComputeFor(s, now, catchUp);
|
||
if (due is null) continue;
|
||
if (best is null || due.At < best.At) best = due;
|
||
}
|
||
return best;
|
||
}
|
||
|
||
private static NextDue? ComputeFor(PrimeScheduleDto s, DateTimeOffset now, TimeSpan catchUp)
|
||
{
|
||
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 && 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 d = todayLocal.AddDays(1);
|
||
for (int i = 0; i < 7; i++)
|
||
{
|
||
if (IsEligibleDay(s, d))
|
||
return new NextDue(s, ToOffset(d, s.TimeOfDay, now.Offset), false);
|
||
d = d.AddDays(1);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static bool IsEligibleDay(PrimeScheduleDto s, DateOnly d) =>
|
||
((PrimeDays)s.Days & ToFlag(d.DayOfWeek)) != PrimeDays.None;
|
||
|
||
private static PrimeDays ToFlag(DayOfWeek dow) => dow switch
|
||
{
|
||
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);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Run the calculator tests**
|
||
|
||
Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter FullyQualifiedName~NextDueCalculatorTests`
|
||
Expected: still FAILS to build — `PrimeScheduler.ToDto` and `WorkerHub` mappings reference removed fields. Proceed to Tasks 5–6, then re-run.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/ClaudeDo.Worker/Prime/PrimeScheduleDto.cs src/ClaudeDo.Worker/Prime/NextDueCalculator.cs tests/ClaudeDo.Worker.Tests/Prime/NextDueCalculatorTests.cs
|
||
git commit -m "feat(worker): compute prime due-time from weekday bitmask"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: PrimeScheduler.ToDto + scheduler tests
|
||
|
||
**Files:**
|
||
- Modify: `src/ClaudeDo.Worker/Prime/PrimeScheduler.cs:104-105`
|
||
- Test: `tests/ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs`
|
||
|
||
- [ ] **Step 1: Update the `ToDto` mapping**
|
||
|
||
Replace the `ToDto` method in `PrimeScheduler.cs`:
|
||
|
||
```csharp
|
||
private static PrimeScheduleDto ToDto(Data.Models.PrimeScheduleEntity e) =>
|
||
new(e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride);
|
||
```
|
||
|
||
- [ ] **Step 2: Update scheduler test fixtures**
|
||
|
||
In `tests/ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs`, every `new PrimeScheduleEntity { ... }` initializer sets `StartDate`/`EndDate`/`WorkdaysOnly`. Replace those three lines in each of the three initializers (lines ~48-52, ~89-94, ~131-136) with a single `Days` assignment. Each initializer becomes:
|
||
|
||
```csharp
|
||
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
||
{
|
||
Id = id,
|
||
Days = PrimeDays.All,
|
||
TimeOfDay = new TimeSpan(7, 0, 0),
|
||
Enabled = true,
|
||
CreatedAt = DateTimeOffset.UtcNow,
|
||
});
|
||
```
|
||
|
||
Add `using ClaudeDo.Data.Models;` to the file's usings if not already present (it is, via line 1).
|
||
|
||
- [ ] **Step 3: Run scheduler + calculator tests**
|
||
|
||
Run: `dotnet test tests/ClaudeDo.Worker.Tests --filter "FullyQualifiedName~Prime"`
|
||
Expected: still build-fails until `WorkerHub` (Task 6) compiles. After Task 6, this command must PASS.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add src/ClaudeDo.Worker/Prime/PrimeScheduler.cs tests/ClaudeDo.Worker.Tests/Prime/PrimeSchedulerTests.cs
|
||
git commit -m "test(worker): adapt prime scheduler tests to weekday model"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: WorkerHub mapping + repository tests
|
||
|
||
**Files:**
|
||
- Modify: `src/ClaudeDo.Worker/Hub/WorkerHub.cs:488-518`
|
||
- Test: `tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs`
|
||
|
||
- [ ] **Step 1: Update `ListPrimeSchedules`**
|
||
|
||
```csharp
|
||
public async Task<List<PrimeScheduleDto>> ListPrimeSchedules()
|
||
{
|
||
using var ctx = _dbFactory.CreateDbContext();
|
||
var rows = await new PrimeScheduleRepository(ctx).ListAsync();
|
||
return rows.Select(e => new PrimeScheduleDto(
|
||
e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride)).ToList();
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Update `UpsertPrimeSchedule`**
|
||
|
||
```csharp
|
||
public async Task<PrimeScheduleDto> UpsertPrimeSchedule(PrimeScheduleDto dto)
|
||
{
|
||
using var ctx = _dbFactory.CreateDbContext();
|
||
var repo = new PrimeScheduleRepository(ctx);
|
||
var existing = await repo.GetAsync(dto.Id);
|
||
var entity = new ClaudeDo.Data.Models.PrimeScheduleEntity
|
||
{
|
||
Id = dto.Id == Guid.Empty ? Guid.NewGuid() : dto.Id,
|
||
Days = (ClaudeDo.Data.Models.PrimeDays)dto.Days,
|
||
TimeOfDay = dto.TimeOfDay,
|
||
Enabled = dto.Enabled,
|
||
PromptOverride = dto.PromptOverride,
|
||
CreatedAt = existing?.CreatedAt ?? DateTimeOffset.UtcNow,
|
||
LastRunAt = existing?.LastRunAt,
|
||
};
|
||
await repo.UpsertAsync(entity);
|
||
_primeSignal.Signal();
|
||
return new PrimeScheduleDto(entity.Id, (int)entity.Days, entity.TimeOfDay,
|
||
entity.Enabled, entity.LastRunAt, entity.PromptOverride);
|
||
}
|
||
```
|
||
|
||
`DeletePrimeSchedule` is unchanged.
|
||
|
||
- [ ] **Step 3: Update repository tests**
|
||
|
||
In `tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs`, replace each entity initializer's `StartDate`/`EndDate`/`WorkdaysOnly` lines with `Days = PrimeDays.Weekdays,` (drop them where only `StartDate`/`EndDate` appear). The three initializers become:
|
||
|
||
```csharp
|
||
// Upsert_Then_List_RoundTrips
|
||
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
||
{
|
||
Id = id,
|
||
Days = PrimeDays.Weekdays,
|
||
TimeOfDay = new TimeSpan(7, 0, 0),
|
||
Enabled = true,
|
||
CreatedAt = DateTimeOffset.UtcNow,
|
||
});
|
||
```
|
||
|
||
```csharp
|
||
// UpdateLastRunAt_Persists
|
||
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
||
{
|
||
Id = id,
|
||
Days = PrimeDays.Weekdays,
|
||
TimeOfDay = new TimeSpan(7, 0, 0),
|
||
Enabled = true,
|
||
CreatedAt = DateTimeOffset.UtcNow,
|
||
});
|
||
```
|
||
|
||
```csharp
|
||
// Delete_Removes_Row
|
||
await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity
|
||
{
|
||
Id = id,
|
||
Days = PrimeDays.All,
|
||
TimeOfDay = TimeSpan.Zero,
|
||
Enabled = true,
|
||
CreatedAt = DateTimeOffset.UtcNow,
|
||
});
|
||
```
|
||
|
||
Add an assertion in `Upsert_Then_List_RoundTrips` after the existing time assertion:
|
||
|
||
```csharp
|
||
Assert.Equal(PrimeDays.Weekdays, rows[0].Days);
|
||
```
|
||
|
||
- [ ] **Step 4: Build worker + run all worker tests**
|
||
|
||
Run: `dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj && dotnet test tests/ClaudeDo.Worker.Tests`
|
||
Expected: PASS (all Prime + repository tests green).
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ClaudeDo.Worker/Hub/WorkerHub.cs tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs
|
||
git commit -m "feat(worker): map prime schedule weekday bitmask over the hub"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: UI DTO + ViewModels + tests (TDD)
|
||
|
||
**Files:**
|
||
- Modify: `src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs`
|
||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs`
|
||
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs`
|
||
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs`
|
||
|
||
- [ ] **Step 1: Update the UI DTO**
|
||
|
||
`src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs` (keep `PrimeFiredEvent` unchanged):
|
||
|
||
```csharp
|
||
namespace ClaudeDo.Ui.Services;
|
||
|
||
public sealed record PrimeScheduleDto(
|
||
Guid Id,
|
||
int Days,
|
||
TimeSpan TimeOfDay,
|
||
bool Enabled,
|
||
DateTimeOffset? LastRunAt,
|
||
string? PromptOverride);
|
||
|
||
public sealed record PrimeFiredEvent(
|
||
Guid ScheduleId,
|
||
bool Success,
|
||
string Message,
|
||
DateTimeOffset FiredAt);
|
||
```
|
||
|
||
- [ ] **Step 2: Rewrite the row VM**
|
||
|
||
Replace the body of `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs`:
|
||
|
||
```csharp
|
||
using ClaudeDo.Ui.Services;
|
||
using CommunityToolkit.Mvvm.ComponentModel;
|
||
|
||
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||
|
||
public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||
{
|
||
private const int Mon = 1, Tue = 2, Wed = 4, Thu = 8, Fri = 16, Sat = 32, Sun = 64;
|
||
|
||
public Guid Id { get; }
|
||
public bool IsExisting { get; }
|
||
|
||
[ObservableProperty] private bool _enabled;
|
||
[ObservableProperty] private bool _monday;
|
||
[ObservableProperty] private bool _tuesday;
|
||
[ObservableProperty] private bool _wednesday;
|
||
[ObservableProperty] private bool _thursday;
|
||
[ObservableProperty] private bool _friday;
|
||
[ObservableProperty] private bool _saturday;
|
||
[ObservableProperty] private bool _sunday;
|
||
[ObservableProperty] private TimeSpan _timeOfDay;
|
||
[ObservableProperty] private DateTimeOffset? _lastRunAt;
|
||
|
||
public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";
|
||
|
||
partial void OnLastRunAtChanged(DateTimeOffset? value) => OnPropertyChanged(nameof(LastRunLabel));
|
||
|
||
public PrimeScheduleRowViewModel(PrimeScheduleDto dto, bool isExisting)
|
||
{
|
||
Id = dto.Id;
|
||
IsExisting = isExisting;
|
||
Enabled = dto.Enabled;
|
||
Monday = (dto.Days & Mon) != 0;
|
||
Tuesday = (dto.Days & Tue) != 0;
|
||
Wednesday = (dto.Days & Wed) != 0;
|
||
Thursday = (dto.Days & Thu) != 0;
|
||
Friday = (dto.Days & Fri) != 0;
|
||
Saturday = (dto.Days & Sat) != 0;
|
||
Sunday = (dto.Days & Sun) != 0;
|
||
TimeOfDay = dto.TimeOfDay;
|
||
LastRunAt = dto.LastRunAt;
|
||
}
|
||
|
||
public int DaysMask()
|
||
{
|
||
int m = 0;
|
||
if (Monday) m |= Mon;
|
||
if (Tuesday) m |= Tue;
|
||
if (Wednesday) m |= Wed;
|
||
if (Thursday) m |= Thu;
|
||
if (Friday) m |= Fri;
|
||
if (Saturday) m |= Sat;
|
||
if (Sunday) m |= Sun;
|
||
return m;
|
||
}
|
||
|
||
public PrimeScheduleDto ToDto() =>
|
||
new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt, null);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Update the tab VM**
|
||
|
||
In `src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs`, replace `Validate` and `AddSchedule`:
|
||
|
||
```csharp
|
||
public string? Validate()
|
||
{
|
||
foreach (var r in Rows)
|
||
{
|
||
if (r.DaysMask() == 0)
|
||
return $"Schedule {r.TimeOfDay:hh\\:mm}: select at least one day.";
|
||
if (r.TimeOfDay < TimeSpan.Zero || r.TimeOfDay >= TimeSpan.FromDays(1))
|
||
return "Time must be between 00:00 and 23:59.";
|
||
}
|
||
return null;
|
||
}
|
||
```
|
||
|
||
```csharp
|
||
[RelayCommand]
|
||
private void AddSchedule()
|
||
{
|
||
var dto = new PrimeScheduleDto(
|
||
Id: Guid.NewGuid(),
|
||
Days: 31, // Mon–Fri
|
||
TimeOfDay: new TimeSpan(7, 0, 0),
|
||
Enabled: true,
|
||
LastRunAt: null,
|
||
PromptOverride: null);
|
||
Rows.Add(new PrimeScheduleRowViewModel(dto, isExisting: false));
|
||
}
|
||
```
|
||
|
||
`LoadAsync`, `SaveAsync`, `RemoveSchedule`, `ApplyFiredEvent` are unchanged.
|
||
|
||
- [ ] **Step 4: Rewrite the tab VM tests**
|
||
|
||
Replace the body of `tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs`:
|
||
|
||
```csharp
|
||
using ClaudeDo.Ui.Services;
|
||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||
|
||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||
|
||
public class PrimeClaudeTabViewModelTests
|
||
{
|
||
private sealed class FakeApi : IPrimeScheduleApi
|
||
{
|
||
public List<PrimeScheduleDto> Stored { get; } = new();
|
||
public List<PrimeScheduleDto> Upserts { get; } = new();
|
||
public List<Guid> Deletes { get; } = new();
|
||
public Task<List<PrimeScheduleDto>> ListAsync() => Task.FromResult(Stored.ToList());
|
||
public Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto)
|
||
{
|
||
Upserts.Add(dto);
|
||
return Task.FromResult<PrimeScheduleDto?>(dto);
|
||
}
|
||
public Task DeleteAsync(Guid id) { Deletes.Add(id); return Task.CompletedTask; }
|
||
}
|
||
|
||
private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time) =>
|
||
new(id, days, time, true, null, null);
|
||
|
||
[Fact]
|
||
public async Task Load_Populates_Rows()
|
||
{
|
||
var api = new FakeApi();
|
||
api.Stored.Add(Dto(Guid.NewGuid(), 31, new TimeSpan(7, 0, 0)));
|
||
var vm = new PrimeClaudeTabViewModel(api);
|
||
await vm.LoadAsync();
|
||
Assert.Single(vm.Rows);
|
||
}
|
||
|
||
[Fact]
|
||
public void AddSchedule_Appends_Row_With_Defaults()
|
||
{
|
||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||
vm.AddScheduleCommand.Execute(null);
|
||
Assert.Single(vm.Rows);
|
||
Assert.True(vm.Rows[0].Enabled);
|
||
Assert.True(vm.Rows[0].Monday);
|
||
Assert.True(vm.Rows[0].Friday);
|
||
Assert.False(vm.Rows[0].Saturday);
|
||
Assert.Equal(new TimeSpan(7, 0, 0), vm.Rows[0].TimeOfDay);
|
||
}
|
||
|
||
[Fact]
|
||
public void Row_Decomposes_And_Recomposes_Days()
|
||
{
|
||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||
vm.AddScheduleCommand.Execute(null);
|
||
var row = vm.Rows[0];
|
||
Assert.Equal(31, row.DaysMask());
|
||
row.Saturday = true;
|
||
Assert.Equal(63, row.DaysMask());
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Save_Diffs_New_And_Removed_Rows()
|
||
{
|
||
var api = new FakeApi();
|
||
var keptId = Guid.NewGuid();
|
||
var deletedId = Guid.NewGuid();
|
||
api.Stored.Add(Dto(keptId, 31, new TimeSpan(7, 0, 0)));
|
||
api.Stored.Add(Dto(deletedId, 31, new TimeSpan(8, 0, 0)));
|
||
|
||
var vm = new PrimeClaudeTabViewModel(api);
|
||
await vm.LoadAsync();
|
||
vm.RemoveScheduleCommand.Execute(vm.Rows.Single(r => r.Id == deletedId));
|
||
vm.AddScheduleCommand.Execute(null);
|
||
|
||
await vm.SaveAsync();
|
||
|
||
Assert.Contains(deletedId, api.Deletes);
|
||
Assert.Equal(2, api.Upserts.Count);
|
||
}
|
||
|
||
[Fact]
|
||
public void Validate_Reports_No_Days_Selected()
|
||
{
|
||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||
vm.AddScheduleCommand.Execute(null);
|
||
var row = vm.Rows[0];
|
||
row.Monday = row.Tuesday = row.Wednesday = row.Thursday = row.Friday = false;
|
||
Assert.NotNull(vm.Validate());
|
||
}
|
||
|
||
[Fact]
|
||
public void Validate_Passes_With_One_Day()
|
||
{
|
||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||
vm.AddScheduleCommand.Execute(null);
|
||
Assert.Null(vm.Validate());
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Run UI tests**
|
||
|
||
Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter FullyQualifiedName~PrimeClaudeTabViewModelTests`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs
|
||
git commit -m "feat(ui): drive prime schedule rows from weekday toggles"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: XAML — toggle-button row
|
||
|
||
**Files:**
|
||
- Modify: `src/ClaudeDo.Ui/Design/IslandStyles.axaml`
|
||
- Modify: `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`
|
||
|
||
- [ ] **Step 1: Add a `day-toggle` style class**
|
||
|
||
Append to `src/ClaudeDo.Ui/Design/IslandStyles.axaml` (inside the root `<Styles>` element, alongside the other style selectors). Uses existing dynamic-resource tokens — no hardcoded colors:
|
||
|
||
```xml
|
||
<Style Selector="ToggleButton.day-toggle">
|
||
<Setter Property="MinWidth" Value="34"/>
|
||
<Setter Property="Padding" Value="6,4"/>
|
||
<Setter Property="Margin" Value="0"/>
|
||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||
<Setter Property="Background" Value="{DynamicResource DeepBrush}"/>
|
||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
|
||
<Setter Property="BorderThickness" Value="1"/>
|
||
<Setter Property="CornerRadius" Value="4"/>
|
||
</Style>
|
||
<Style Selector="ToggleButton.day-toggle:checked /template/ ContentPresenter">
|
||
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||
</Style>
|
||
```
|
||
|
||
If `AccentBrush` is not a defined token, use the brush the project uses for primary/selected affordances (check the `primary` button style in this file and reuse that brush). Final visual pass is the user's.
|
||
|
||
- [ ] **Step 2: Replace the Prime row template**
|
||
|
||
In `src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml`, replace the `<Grid ...>` inside the Prime `DataTemplate` (currently columns `Auto,*,Auto,Auto,Auto,Auto` with the `ThemedDatePicker` and Mon–Fri checkbox) with:
|
||
|
||
```xml
|
||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8">
|
||
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||
<ToggleButton Classes="day-toggle" Content="Mo" IsChecked="{Binding Monday, Mode=TwoWay}"/>
|
||
<ToggleButton Classes="day-toggle" Content="Tu" IsChecked="{Binding Tuesday, Mode=TwoWay}"/>
|
||
<ToggleButton Classes="day-toggle" Content="We" IsChecked="{Binding Wednesday, Mode=TwoWay}"/>
|
||
<ToggleButton Classes="day-toggle" Content="Th" IsChecked="{Binding Thursday, Mode=TwoWay}"/>
|
||
<ToggleButton Classes="day-toggle" Content="Fr" IsChecked="{Binding Friday, Mode=TwoWay}"/>
|
||
<ToggleButton Classes="day-toggle" Content="Sa" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
|
||
<ToggleButton Classes="day-toggle" Content="Su" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
|
||
</StackPanel>
|
||
<TextBox Grid.Column="2" Width="64"
|
||
Text="{Binding TimeOfDay, Mode=TwoWay, Converter={StaticResource TimeSpanToHhmm}}"
|
||
VerticalAlignment="Center"/>
|
||
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
||
MinWidth="80"/>
|
||
<Button Classes="icon-btn" Grid.Column="4" Content="✕"
|
||
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
|
||
CommandParameter="{Binding}"/>
|
||
</Grid>
|
||
```
|
||
|
||
- [ ] **Step 3: Update the explainer text**
|
||
|
||
Replace the intro `TextBlock` Text in the Prime tab (`SettingsModalView.axaml`):
|
||
|
||
```xml
|
||
Text="Prime your Claude usage window by firing a single non-interactive ping on the days you choose, at a chosen time. Only runs while ClaudeDo is open. If the app starts within 30 minutes of the target time, the ping fires immediately."/>
|
||
```
|
||
|
||
- [ ] **Step 4: Remove the now-unused range converter (only if unreferenced)**
|
||
|
||
The `DateOnlyToDateTime` resource on line 23 was used only by the range picker. Grep the file: if `DateOnlyToDateTime` has no other reference, remove the `<conv:DateOnlyToDateTimeConverter x:Key="DateOnlyToDateTime"/>` line. Keep `TimeSpanToHhmm` (still used).
|
||
|
||
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Manual UI check**
|
||
|
||
Start the worker, then the app. Open Settings → Prime Claude. Verify: a row shows 7 toggle buttons with Mon–Fri lit by default; toggling Sat/Sun persists after Save+reopen; clearing all days shows the validation error on Save. (UI correctness can only be confirmed in the running app — state so explicitly if it cannot be run.)
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/ClaudeDo.Ui/Design/IslandStyles.axaml src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml
|
||
git commit -m "feat(ui): replace prime date range with weekday toggle buttons"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: Docs
|
||
|
||
**Files:**
|
||
- Modify: `src/ClaudeDo.Data/CLAUDE.md`
|
||
- Modify: `CLAUDE.md`
|
||
|
||
- [ ] **Step 1: Update the Data CLAUDE.md**
|
||
|
||
In `src/ClaudeDo.Data/CLAUDE.md`, the Models section has no PrimeSchedule line today; add one under Models, and confirm the `prime_schedules` table mention in the Schema section stays accurate:
|
||
|
||
```markdown
|
||
- **PrimeScheduleEntity** — Id, Days (`[Flags] PrimeDays` weekday bitmask, stored as `days_of_week` int), TimeOfDay, Enabled, LastRunAt, PromptOverride, CreatedAt. Recurs on the selected weekdays; no date range.
|
||
```
|
||
|
||
- [ ] **Step 2: Update the root CLAUDE.md if Prime is described**
|
||
|
||
Grep `CLAUDE.md` for "Prime"; if there is a Prime description mentioning a date range, update it to "recurring weekday schedule". If there is no such line, make no change.
|
||
|
||
- [ ] **Step 3: Full test sweep**
|
||
|
||
Run: `dotnet test tests/ClaudeDo.Worker.Tests && dotnet test tests/ClaudeDo.Ui.Tests`
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add src/ClaudeDo.Data/CLAUDE.md CLAUDE.md
|
||
git commit -m "docs: describe recurring-weekday Prime schedule"
|
||
```
|
||
|
||
---
|
||
|
||
## Self-Review Notes
|
||
|
||
- **Spec coverage:** data model (T1), scheduling logic (T4), UI toggles (T7–T8), migration+backfill (T3), both DTOs (T4/T7), tests (T4–T7), out-of-scope items excluded. ✓
|
||
- **Type consistency:** entity `PrimeDays Days`; both DTOs `int Days`; hub/scheduler cast `(int)`/`(PrimeDays)` at boundaries; calculator casts `(PrimeDays)s.Days`; row VM exposes 7 bools + `DaysMask()`. ✓
|
||
- **Build ripple:** a single type change breaks several projects at once, so some intermediate steps note expected build failures; the gating green builds are T3 Step 4 (Data), T6 Step 4 (Worker + tests), T8 Step 4 (App). ✓
|
||
```
|