Generate the week's Supply run from its plan
CI / ci (push) Successful in 5m2s
CI / ci (pull_request) Successful in 4m29s

Add internal/shopping with Generate(weekStart), one entry point for the
first generation and every regeneration. It builds one group per plan
day (dinner_id, date, position by date) and one item per ingredient of
the dinner's recipe, in ingredient order, reusing the values recipe
grounding stored: a fetched recipe's normalized name with qty and unit
joined for display, or a manual recipe's line as typed with no qty. A
day whose dinner has no recipe or no ingredients gets no group and is
reported in Skipped. Regenerating keeps the id and needed flag of each
item whose (dinner_id, name) is unchanged, matching repeated names in
order; new items are needed and items no longer planned are removed. It
is deterministic and makes no model call. A frozen list is refused.

Migration 0016 makes shopping_lists.title nullable (never empty) so a
list exists untitled until the AI titles it, and adds shopping_groups.date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 03:58:18 -04:00
co-authored by Claude Opus 5.5
parent 7003ff92ec
commit 1ca66931bb
12 changed files with 1552 additions and 10 deletions
+2 -2
View File
@@ -102,8 +102,8 @@ Columns ending in `_ct` hold AES-256-GCM ciphertext (via `secretbox`).
| `week_plans` | week_start (unique), status (`draft` / `synced` / `changed`), ai_note, ai_run_id, calendar_synced_at, revision (raised by every swap and stored draft). |
| `plan_days` | plan_id, date (unique per plan), dinner_id, is_new, calendar_event_id (set while the chosen calendar shows the day as planned; cleared when its dinner changes, another calendar is chosen or Google Calendar is disconnected). |
| `rotation_reviews` | dinner_id + plan_date (unique), status, decision (`keep` / `drop` / `not_made`), name (set only by a drop: the dinner's name when dropped, kept whatever it is later renamed, so a drop stays among the passed names), decided_at, updated_by. |
| `shopping_lists` | week_start (unique), plan_id, title, status (`current` / `archived`), sent_to_chat_at, frozen_at. |
| `shopping_groups` / `shopping_items` | One group per dinner/day; item name, qty, needed (bool), position. |
| `shopping_lists` | week_start (unique), plan_id, title (the AI-written post title; NULL until written or when the AI fails, when the week label is shown instead; never empty), status (`current` / `archived`), sent_to_chat_at, frozen_at. |
| `shopping_groups` / `shopping_items` | One group per plan day with ingredients: dinner_id, date, position (by date). Item name, qty (the display quantity, NULL when the recipe gives none), needed (bool), position. Made by `shopping.Generate` from the plan, with no model call: an item is a grounded recipe ingredient's normalized name with qty and unit joined ("2 cups"), or a manual recipe's line as typed with no qty. Regenerating keeps the id and needed flag of an item whose (dinner_id, name) is unchanged. |
| `ai_settings` | One row: provider, base_url, model, temperature, system_prompt, api_key_ct, api_key_last4, key_verified_at. |
| `recipe_search_settings` | One row: provider (`searxng` / `brave`), base_url (the SearXNG instance), api_key_ct and api_key_last4 (Brave). |
| `integrations_google` | One row: refresh_token_ct, calendar_id, calendar_name, connected_at, last_sync_error. |