diff --git a/docs/superpowers/specs/2026-06-02-prime-recurring-weekdays-design.md b/docs/superpowers/specs/2026-06-02-prime-recurring-weekdays-design.md new file mode 100644 index 0000000..178d030 --- /dev/null +++ b/docs/superpowers/specs/2026-06-02-prime-recurring-weekdays-design.md @@ -0,0 +1,116 @@ +# Prime: recurring weekday schedule + +**Date:** 2026-06-02 +**Status:** Approved + +## Problem + +The Prime feature fires a single non-interactive "ping" prompt to warm up the +Claude usage window. Today a schedule is defined by a **date range** +(`StartDate`/`EndDate`) plus a `TimeOfDay` and a single `WorkdaysOnly` toggle. +This is awkward for the real use case: the user wants a *recurring* morning ping +on specific weekdays, not a bounded calendar window. + +Desired behavior: pick the **days of the week** (e.g. Mon–Fri) and a **time**. +The schedule recurs forever. Whenever the worker is running and it is one of the +selected days, the ping fires at (or shortly after) the chosen time. Concretely: +the worker autostarts on login, detects it is an eligible day around the target +time, and fires the ping. + +## Decisions + +- **Catch-up window:** unchanged. Keep the existing 30-minute catch-up — if the + worker boots within 30 min after the target time, the ping fires immediately; + otherwise it waits for the next eligible day. (User chose "keep current 30 min".) +- **Day picker UI:** seven compact **toggle buttons** in one row (Mo Tu We Th Fr + Sa Su), highlighted when selected — not labeled checkboxes. + +## Design + +### 1. Data model + +`PrimeScheduleEntity` (`ClaudeDo.Data/Models`): + +- **Remove:** `StartDate`, `EndDate`, `WorkdaysOnly` +- **Add:** `Days` — a `[Flags] enum PrimeDays` (`Monday=1, Tuesday=2, Wednesday=4, + Thursday=8, Friday=16, Saturday=32, Sunday=64`), stored as a single + `days_of_week INTEGER` column. +- **Keep:** `TimeOfDay`, `Enabled`, `LastRunAt`, `PromptOverride`, `CreatedAt`. + +Rationale for a bitmask over a CSV string or 7 bool columns: one column, trivial +EF mapping (int), and a clean eligibility check. + +`PrimeScheduleEntityConfiguration`: drop the `start_date`/`end_date`/ +`workdays_only` property mappings; map `Days` to `days_of_week` (int, required, +default 31 = Mon–Fri). + +### 2. Scheduling logic — `NextDueCalculator` + +- Drop all `StartDate`/`EndDate` gating (the `EndDate < today` early-out, the + `StartDate > today` clamps, and the bounds check in `IsEligibleDay`). +- `IsEligibleDay(s, d)` becomes: does `s.Days` contain the flag for + `d.DayOfWeek`? (Map `System.DayOfWeek` → `PrimeDays`.) +- The existing forward search (loops up to 8 days ahead) now simply walks to the + next selected weekday. +- `alreadyFiredToday` (compares `LastRunAt`'s local date to today) is unchanged. +- The 30-min catch-up (`FireImmediately`) is unchanged. +- A schedule with `Days == 0` (none selected) is never eligible. UI validation + prevents saving that state. + +### 3. UI — `SettingsModalView.axaml` + `PrimeScheduleRowViewModel` + +Row template changes: +- **Remove** the `ThemedDatePicker` (range) and the single "Mon–Fri" checkbox. +- **Add** a horizontal row of 7 `ToggleButton`s (Mo Tu We Th Fr Sa Su), styled + to highlight when checked, bound to seven bool properties on the row VM. +- Keep the enabled checkbox, the time `TextBox`, the last-run label, and the + remove button. + +`PrimeScheduleRowViewModel`: +- Replace `StartDate`/`EndDate`/`WorkdaysOnly` with seven `[ObservableProperty]` + bools: `Monday`…`Sunday`. +- Constructor decomposes `dto.Days` into the seven bools. +- `ToDto()` composes the seven bools back into the `Days` int. + +`PrimeClaudeTabViewModel`: +- `AddSchedule` default: Mon–Fri selected, time 07:00, enabled. +- `Validate`: replace the `StartDate > EndDate` check with "at least one day must + be selected"; keep the time-range (00:00–23:59) check. + +Update the explainer `TextBlock` text to describe weekday recurrence (keep the +"fires immediately if started within 30 minutes of the target time" note). + +### 4. Migration + +New EF Core migration in `ClaudeDo.Data/Migrations`: +- Add `days_of_week INTEGER NOT NULL DEFAULT 31`. +- Backfill from existing rows: `workdays_only = 1` → `31` (Mon–Fri), + `workdays_only = 0` → `127` (all 7 days). +- Drop `start_date`, `end_date`, `workdays_only`. +- Update the model snapshot. + +### 5. DTOs + +Both copies of `PrimeScheduleDto` (Worker `ClaudeDo.Worker.Prime` and UI +`ClaudeDo.Ui.Services`) are passed over SignalR and must stay structurally +compatible. In both: remove `StartDate`, `EndDate`, `WorkdaysOnly`; add a single +`int Days` field (serializes cleanly as JSON; avoids sharing the enum across +projects). `PrimeScheduler.ToDto` maps `entity.Days` → `(int)`. + +`PrimeScheduleRepository`: update `UpsertAsync` (copy `Days` instead of the three +removed fields) and `ListAsync` ordering (order by `TimeOfDay` instead of +`StartDate`). + +### 6. Tests + +- `NextDueCalculatorTests` — rewrite cases around weekday sets (e.g. Mon–Fri + skips weekend; single-day schedule; catch-up still fires; already-fired-today + skips to next eligible day). +- `PrimeSchedulerTests` — update fixture DTOs to the new shape. +- `PrimeScheduleRepositoryTests` — update entity construction and assertions. +- `PrimeClaudeTabViewModelTests` — update for the day-bool VM and new validation. + +## Out of scope + +- Per-schedule catch-up tuning (rejected; fixed 30 min). +- Multiple times per day, timezones, or holiday calendars.