Add plan_draft schedule drafting next week (Feature 20)
CI / ci (push) Successful in 7m12s
CI / ci (pull_request) Successful in 7m25s

Register plan.Service.DraftSchedule() as the plan_draft schedule: at the
household's plan_draft_dow/time it runs StartScheduledDraft for the ISO
week after the fire date (period plan_draft:YYYY-Www of that week), which
enqueues the same draft_week job as POST /plans/{weekStart}/draft. A
stored draft emits plan_ready:{weekStart} from the job; an AI failure is
recorded in ai_runs and emits nothing. A week that already has a plan is
left as it is.

Fake-clock tests in internal/scheduler cover drafting when absent,
skipping when present, no duplicate after restart, the AI failure path
and a settings change without restart.

Maestro task 598184a8-56fe-4ed8-887b-7780c0aebf12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 09:06:41 -04:00
co-authored by Claude Opus 5.5
parent c54d809bfd
commit 7783971bc1
5 changed files with 475 additions and 8 deletions
+12 -3
View File
@@ -780,7 +780,7 @@ purge are fixed and not configurable.
| Schedule | When | Does |
| --- | --- | --- |
| `suggestions_weekly` | Sunday 08:00 | New batch of Side quests (`suggest.Service.WeeklySchedule`). |
| `plan_draft` | Friday 09:00 | AI drafts next week's plan. |
| `plan_draft` | Friday 09:00 | AI drafts next week's plan (`plan.Service.DraftSchedule`). |
| `shopping_chat_autosend` | Saturday 08:00 | Sends the Supply run to Google Chat. |
| `day_of_reminder` | Daily 16:30 | Tonight's dinner reminder. |
| `rotation_check` | Daily 09:00 | Asks "Keep it in rotation?" for tried dinners. |
@@ -814,6 +814,14 @@ Its run calls `StartScheduledBatch(period.Week)`, which starts the same
batch and job as `POST /api/v1/suggestions/batches` (`StartBatch(manual)`),
with the `scheduled` trigger, and returns at once; the batch runs as a job. When a
batch is already pending or running the period starts none and logs it.
`plan.Service.DraftSchedule()` is `plan_draft`, keyed by the ISO week
after the fire date (`scheduler.WeeklyForNextWeek`). Its run calls
`StartScheduledDraft(period.Week)`, which enqueues the same `draft_week`
job as `POST /api/v1/plans/{weekStart}/draft` without confirm, so a stored
draft emits `plan_ready:{weekStart}` from the job and a failed one (e.g.
`ai_failed`, recorded in `ai_runs`) emits nothing. When the week already
has a plan, in any status, it enqueues nothing; when a draft of the week
is already queued or running it joins it. Either way it logs it.
Fake-clock tests of a schedule live in `internal/scheduler`, where
`SetClock` is visible to tests.
@@ -825,8 +833,9 @@ Fake-clock tests of a schedule live in `internal/scheduler`, where
- Where each event is emitted, with its idempotency key. Each is stored by
`notify.EmitTx` in the transaction of the change it is about, so the
change and its notification commit together or not at all:
- `plan_ready:{weekStart}` (URL `/plan`) when a draft job stores the
AI's draft of a week. Only the first draft of a week notifies: a
- `plan_ready:{weekStart}` (URL `/plan`) when a draft job, asked for
by the API or the `plan_draft` schedule, stores the AI's draft of a
week. Only the first draft of a week notifies: a
redraft (Rebuild week) has the same key, and a manual swap never
drafts. Its title is "Next week's plan is ready" (or "This week's",
or "The plan for the week of Oct 26") and its body the drafted