# 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> 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/_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( 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( name: "start_date", table: "prime_schedules", type: "TEXT", nullable: false, defaultValue: new DateOnly(2000, 1, 1)); migrationBuilder.AddColumn( name: "end_date", table: "prime_schedules", type: "TEXT", nullable: false, defaultValue: new DateOnly(2099, 12, 31)); migrationBuilder.AddColumn( 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 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> 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 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 Stored { get; } = new(); public List Upserts { get; } = new(); public List Deletes { get; } = new(); public Task> ListAsync() => Task.FromResult(Stored.ToList()); public Task UpsertAsync(PrimeScheduleDto dto) { Upserts.Add(dto); return Task.FromResult(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 `` element, alongside the other style selectors). Uses existing dynamic-resource tokens — no hardcoded colors: ```xml ``` 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 `` inside the Prime `DataTemplate` (currently columns `Auto,*,Auto,Auto,Auto,Auto` with the `ThemedDatePicker` and Mon–Fri checkbox) with: ```xml