Public Access
fix(ui): align 'this week' to upcoming Monday (Sprint 7)
User report 2026-06-05: 'webui Meal Planner page is empty' on Friday
morning after the Friday email went out. Root cause: the orchestrator
keyed plans by the most-recent-Friday while the frontend's isoMonday()
returned the most-recent-Monday — a 7-day mismatch on Fridays.
Fixes (one semantic across the stack):
- runner._current_week_start() returns the upcoming Monday (today if
Mon, else the next Mon). The Friday email subject
('Meal plan for week of <date>') automatically picks up the new
value via run.week_start_date.
- frontend isoMonday -> upcomingMonday (same logic; renamed for
intent). isoMonday kept as a deprecated alias.
- New WeekRangeNav component (Dashboard + ShoppingList share it).
Renders [<] Jun 8 - Jun 14 [>] with clickable chevrons and a
clickable range label that jumps to the upcoming week. Replaces
the Sprint 5 inline segmented control on both pages.
- New formatWeekRange(mondayIso) helper (UTC-stable; uses
timeZone: 'UTC' so the rendered date matches the stored ISO date
regardless of viewer TZ; closes a latent bug in formatIsoDate too).
- New SQL fix script that retargets the user's 3-pending-items plan
from 2026-06-05 (Friday-keyed) to 2026-06-08 (upcoming Monday).
Idempotent + transaction-wrapped. Optional block for 2026-05-29.
No backend migration. No new dependencies. Deploy is git pull +
run the SQL fix + docker compose up -d --build backend frontend.
See Review/sprint7-verification.md for the full deploy + smoke flow.
Files:
- backend/app/services/orchestrator/runner.py:20-35
- backend/scripts/fix_2026_06_05_to_2026_06_08.sql (new)
- frontend/src/lib/utils.ts:43-130
- frontend/src/components/WeekRangeNav.tsx (new)
- frontend/src/pages/Dashboard.tsx (3 call sites + 1 segmented control)
- frontend/src/pages/ShoppingList.tsx (5 call sites + 2 segmented controls)
- Review/{sprint7-verification,ui-nielsen-audit,handoff-ui-audit}.md
- fix-ui-audit.md
- docs/HANDOFF.md
- .agent/{plan,context}.md
This commit is contained in:
@@ -58,3 +58,60 @@ R1 and R2 are independent and run in parallel. R3 cannot start until BOTH R1 ver
|
||||
- Auth scoping: bearer header is attached ONLY to `prod.swiftlyapi.net` requests, NOT to the public `luckysupermarkets.com` HTML page. Two `requests.Session` objects (one with default UA, one with the bearer header).
|
||||
- 401 detection: cannot use `BaseScraper._get` because it swallows HTTPError into a `None` return. The new client calls `session.get(...)` directly and checks `resp.status_code == 401` BEFORE `raise_for_status` to raise `SwiftlyAuthError`. Token in `.env.example` expires hourly per spec; on 401 the scraper aborts with a fixed error_message instructing the admin to refresh the token.
|
||||
- Idempotency key: `(source, external_id)` upserts. Migration 0005 adds `grocery_item.external_id` (nullable text, indexed; not unique because legacy R2-A rows lack one).
|
||||
|
||||
---
|
||||
|
||||
# Context — Sprint 7 (webui empty-meal-plan fix)
|
||||
|
||||
## Why Sprint 7 exists
|
||||
|
||||
User report 2026-06-05: "Latest meal plans were emails to me this morning, but when I go to the webui, the Meal Planner page is empty." Investigation found a date-semantics mismatch.
|
||||
|
||||
## Decisions (locked in for Sprint 7)
|
||||
|
||||
- **D1. "This week" = the upcoming Mon-Sun week.** The Friday email advertises the upcoming week; the plan is keyed by the upcoming Monday; the webui opens on the upcoming Monday. Past weeks accessible via the back-arrow. (User asked for a clickable `< Jun 8 — Jun 14 >` style nav, so the range is visible at a glance.)
|
||||
- **D2. Plan key changes from Friday to Monday.** All future plans are Monday-keyed. Existing 2026-06-05 plan migrated to 2026-06-08 via guarded SQL.
|
||||
- **D3. Email subject unchanged in form, changes in content.** `step_email` already uses `run.week_start_date` for the subject (verified `steps.py:305`). After D1, subject becomes "Meal plan for week of 2026-06-08" — natural Mon-Sun.
|
||||
- **D4. Frontend `isoMonday` renamed to `upcomingMonday`.** Same surface (Dashboard + ShoppingList). No backward-compat alias needed; the only callers are within our codebase.
|
||||
- **D5. New `WeekRangeNav` component is shared between Dashboard and ShoppingList.** Single source of truth for the visual + behavior.
|
||||
- **D6. The no-op `Generate Meal Plan` CTA at `Dashboard.tsx:415` is still out of scope.** F4 (plan-the-week) and the dead CTA solve different problems. Documented as a follow-up.
|
||||
|
||||
## Open questions to surface to the user, not to assume
|
||||
|
||||
- **Q1. Migrate the 2026-05-29 plan too?** It's also Friday-keyed. Operator can run a separate guarded UPDATE in the same SQL script. Default for now: include the statement but commented out; user uncomments if they want.
|
||||
- **Q2. Recency logic in the planner.** `_load_last_cooked` in `planner/generate.py:80-94` compares `MealPlan.week_start_date` across plans. After D2, all values are Mondays, so the comparison is symmetric and "days since last cooked" stays correct. No change needed. (Verified by reading the code.)
|
||||
- **Q3. Should the email subject line shift by one day (Thu instead of Fri)?** No — the scheduler still fires Fri 02:00..18:00 PT (verified `scheduler/__main__.py`). The deadline (vote by Fri 17:00) still makes sense. The plan key shifts to Mon, the email timing stays Fri. No scheduler change.
|
||||
- **Q4. Any URL bookmarked with `?week=2026-06-05`?** After the SQL fix, the plan moves to 2026-06-08. Any external link to `?week=2026-06-05` will hit "no plan for that week" (404-ish). Acceptable since the user uses the webui, not external links.
|
||||
|
||||
## Sprint 7 verification gate
|
||||
|
||||
- `cd frontend && npm run build` → green
|
||||
- `curl http://100.108.208.56:8082/api/meals?week_start=2026-06-08` (after deploy + SQL) → 3 pending items
|
||||
- Browser: open `/` (no `?week=` param) on deployment host → header shows `Week of Jun 8, 2026`, 3 meal cards visible
|
||||
- `curl http://100.108.208.56:8082/api/meals?week_start=2026-06-01` → null (current calendar week has no plan; expected)
|
||||
- `curl http://100.108.208.56:8082/api/meals?week_start=2026-05-29` → null if user opted in to migrate it, 3 items otherwise
|
||||
- `Review/sprint7-verification.md` is the source of truth for the deploy + smoke flow.
|
||||
|
||||
## Sprint 7 — does NOT touch
|
||||
|
||||
- The `extractErrorMessage` / `showApiError` flow (Sprint 4 F7) — unchanged.
|
||||
- The keyboard shortcuts (Sprint 5 F2) — unchanged. Note: `g d` still navigates to Dashboard at `upcomingMonday()`.
|
||||
- The bulk pantry add (Sprint 6 F3) — unchanged.
|
||||
- The plan-the-week (Sprint 6 F4) — unchanged. It still operates on the active plan regardless of week.
|
||||
- The undo-toast (Sprint 3 B12) — unchanged.
|
||||
- The aisle-migration (Sprint 2 / Sprint 5 fix) — no migration in S7.
|
||||
|
||||
## Key file:line references
|
||||
|
||||
- `backend/app/services/orchestrator/runner.py:20-24` — `_current_week_start()` (TO MODIFY)
|
||||
- `backend/app/scheduler/__main__.py:31-66` — Friday cron schedule (NO CHANGE)
|
||||
- `backend/app/services/orchestrator/steps.py:305` — `f"Meal plan for week of {run.week_start_date}"` (NO CHANGE; uses upstream value)
|
||||
- `frontend/src/lib/utils.ts:44-50` — `isoMonday()` (TO RENAME + CHANGE)
|
||||
- `frontend/src/pages/Dashboard.tsx:316-320` — default-week + navigateWeek (TO UPDATE)
|
||||
- `frontend/src/pages/ShoppingList.tsx:87-90` — same (TO UPDATE)
|
||||
- `frontend/src/pages/Dashboard.tsx:479-503` — inline week nav (TO REPLACE with `<WeekRangeNav>`)
|
||||
- `frontend/src/pages/ShoppingList.tsx:259-283` — same (TO REPLACE)
|
||||
- `frontend/src/components/` — new `WeekRangeNav.tsx` (TO ADD)
|
||||
- `backend/scripts/fix_2026_06_05_to_2026_06_08.sql` — new (TO ADD)
|
||||
- `Review/sprint7-verification.md` — new (TO ADD)
|
||||
|
||||
|
||||
@@ -2,6 +2,68 @@
|
||||
|
||||
Goal: bring implementation back into alignment with `Review/reviewconcensus.md`. Stop building forward features until the deferred-risk spikes and the verification matrix pass.
|
||||
|
||||
## Active sprint: Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch)
|
||||
|
||||
**Owner:** this agent. **Status:** in progress (approved by user 2026-06-05, code not yet written). **Tracking:** `Review/sprint7-verification.md` (deploy + smoke checks), `.agent/plan.md` (checklist), `.agent/context.md` (decisions + open Qs).
|
||||
|
||||
Root cause: today is Fri 2026-06-05. The orchestrator sends Friday emails for the **upcoming** Mon-Sun week and keys the plan by the upcoming Monday. The frontend's `isoMonday()` returns the **current** calendar week's Monday, which is 4 days behind. So the email says "Meal plan for week of 2026-06-08" but the webui opens on "week of 2026-06-01" — no plan, empty state.
|
||||
|
||||
### S7.1 — Backend: `_current_week_start()` returns upcoming Monday
|
||||
|
||||
- [ ] `backend/app/services/orchestrator/runner.py:20-24` — change body to: `today.weekday() == 0 → today; else today + timedelta(days=(7 - today.weekday()))`. Add docstring "the upcoming Mon-Sun week the email advertises." Also update `scheduler/__main__.py` docstring if needed.
|
||||
- [ ] Add an inline comment that the email subject uses this same date (so they stay in sync).
|
||||
- [ ] Verify: read `step_email` to confirm it uses `run.week_start_date` for the subject (it does, per `steps.py:305`).
|
||||
|
||||
### S7.2 — Frontend: align `isoMonday` with backend
|
||||
|
||||
- [ ] `frontend/src/lib/utils.ts:44-50` — rename `isoMonday` to `upcomingMonday` with new logic (today if Mon, else next Mon).
|
||||
- [ ] Keep the old `isoMonday` (calendar week) for any code that needs it; the function is currently only used for the F5 default, so rename is safe.
|
||||
- [ ] Add `formatWeekRange(mondayIso: string): string` helper that returns `"Jun 8 — Jun 14"`. Used by the new nav.
|
||||
- [ ] `Dashboard.tsx` and `ShoppingList.tsx` — update imports; `useSearchParams`, `navigateWeek`, `isCurrentWeek` all reference `upcomingMonday()` instead of `isoMonday()`.
|
||||
|
||||
### S7.3 — Frontend: new `WeekRangeNav` component
|
||||
|
||||
- [ ] `frontend/src/components/WeekRangeNav.tsx` — new file. Props: `{ weekStart: string; isCurrentWeek: boolean; onPrev: () => void; onNext: () => void; onJumpHome: () => void }`.
|
||||
- [ ] Renders: `[<]` button (chevron-left), a button showing the formatted range (e.g. `Jun 8 — Jun 14`, clickable → jump home), `[>]` button (chevron-right), and a `This week` chip (visible only when `!isCurrentWeek`).
|
||||
- [ ] All buttons have `aria-label`s; clickable range label says "Jump to upcoming week".
|
||||
- [ ] Acceptable to use lucide-react `ChevronLeft` / `ChevronRight` (already imported in Dashboard/ShoppingList).
|
||||
- [ ] Replaces the inline segmented control in `Dashboard.tsx:479-503` and `ShoppingList.tsx:259-283`.
|
||||
|
||||
### S7.4 — Data: migrate 2026-06-05 plan to 2026-06-08
|
||||
|
||||
- [ ] `backend/scripts/fix_2026_06_05_to_2026_06_08.sql` — guarded `UPDATE meal_plan SET week_start_date='2026-06-08' WHERE week_start_date='2026-06-05';` with `SELECT COUNT(*)` first. Operator runs on deployment host.
|
||||
- [ ] Decide: also migrate older Friday-keyed plans (2026-05-29 was a Friday). User has the option; for now include it as a separate guarded statement (commented out, opt-in) in the same script.
|
||||
|
||||
### S7.5 — Verify
|
||||
|
||||
- [ ] `npm run build` green.
|
||||
- [ ] Write `Review/sprint7-verification.md` with deploy + smoke checks (backend + frontend, no migration, plus the SQL update step).
|
||||
- [ ] Backend smoke: `curl /api/meals?week_start=2026-06-08` returns the user's 3 pending items.
|
||||
- [ ] Frontend smoke: load `/` (no `?week=` param) on a browser; the dashboard opens on the upcoming Mon-Sun plan with 3 items.
|
||||
|
||||
### S7.6 — Docs
|
||||
|
||||
- [ ] Add Sprint 7 status block to `Review/ui-nielsen-audit.md` (top of file, after Sprint 6 block).
|
||||
- [ ] Add Sprint 7 plan section to `fix-ui-audit.md`.
|
||||
- [ ] Add Sprint 7 section to `Review/handoff-ui-audit.md` (new "Active sprint" callout near the top).
|
||||
- [ ] Add Sprint 7 section to `docs/HANDOFF.md` (replace the "Last updated" line at line 305).
|
||||
|
||||
### Done when (Sprint 7)
|
||||
|
||||
- All 6 checkboxes above ticked.
|
||||
- `npm run build` green.
|
||||
- `Review/sprint7-verification.md` exists.
|
||||
- All four doc files have a Sprint 7 status block.
|
||||
- User reports the webui no longer shows empty after pulling the S7 batch + running the SQL fix.
|
||||
|
||||
### Out of scope (Sprint 7)
|
||||
|
||||
- Thread 2: cross-week "rejected means" semantics. Defer until after S7 deploys.
|
||||
- Thread 3: §Future backlog (F1 onboarding, F8/F9 proposals, dead-CTA wire-up).
|
||||
- The 2026-05-29 plan migration (operator opt-in; documented but not auto-run).
|
||||
|
||||
---
|
||||
|
||||
## Phase R1 — Stabilize (parallel-safe)
|
||||
|
||||
- [ ] R1-A: Verification harness. Add `backend/tests/` with pytest config, a `conftest.py` with a transactional DB fixture, and smoke tests covering: app import, `/health`, `/health/db`, every router's GET list endpoint, Alembic `upgrade head` round-trip on a throwaway DB. Add `.github/workflows/ci.yml` running lint + pytest + frontend `npm run build`.
|
||||
|
||||
@@ -2,7 +2,21 @@
|
||||
|
||||
You are taking over a 6-sprint UI/UX audit and fix cycle. All code changes are committed and build green. The user's deployment host (Tailscale `100.108.224.12`) is the only environment you should touch for verification — the local repo on this machine (`/home/peter/Projects/MealPlanner`) was the editing host; the running app lives elsewhere.
|
||||
|
||||
**Date of handoff: 2026-06-04.**
|
||||
**Date of handoff: 2026-06-04. Last updated: 2026-06-05 (Sprint 7 in progress).**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Active sprint: Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch)
|
||||
|
||||
**Status: in progress. User approved on 2026-06-05. Code not yet written.**
|
||||
|
||||
**Root cause (one-liner):** the orchestrator plans the **upcoming** Mon-Sun week (Fri 2026-06-05 → key 2026-06-08), but the frontend `isoMonday()` returns the **current** Mon-Sun (Fri 2026-06-05 → 2026-06-01). Email + DB + webui disagree by 7 days. User sees an empty page.
|
||||
|
||||
**Scope (7 boxes):** see `.agent/plan.md` "Active sprint" section. Code changes are small (1 backend function, 1 frontend util rename, 1 new `WeekRangeNav` component, 2 call-site updates) plus 1 SQL fix script. **No migration.**
|
||||
|
||||
**Tracking docs:** `Review/sprint7-verification.md` (deploy + smoke), `Review/ui-nielsen-audit.md` Sprint 7 status block, `fix-ui-audit.md` S7.1–S7.6, `docs/HANDOFF.md` Sprint 7 section. All will be filled in before the user pulls.
|
||||
|
||||
**Thread 2 + Thread 3 are deferred** (cross-week "rejected" semantics + §Future backlog) until S7 is deployed + verified.
|
||||
|
||||
---
|
||||
|
||||
@@ -264,4 +278,4 @@ cd frontend && npm run build
|
||||
|
||||
Trust the build output. Trust the smoke checklist. Don't trust the deployment host's UI until the user confirms. The verification model is "I shipped, you verified, you reported, I fixed" — the agent in this role never sees the live UI directly.
|
||||
|
||||
**Last updated: 2026-06-03** — Sprints 1, 2, 3 all committed; Sprint 1 deployed; Sprints 2 and 3 awaiting deploy.
|
||||
**Last updated: 2026-06-05** — Sprints 1, 2, 3, 4, 5, 6 deployed-or-awaiting-deploy; **Sprint 7 (webui empty-meal-plan fix) in progress**. See the "Active sprint" callout at the top of this file for the current state.
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Sprint 7 — Verification
|
||||
|
||||
**Sprint:** Fix webui "empty meal plan" (date-semantics mismatch)
|
||||
**Status:** code complete, awaiting deploy.
|
||||
**Approach:** Option C — align orchestrator + frontend on "this week" = upcoming Monday; migrate the existing Friday-keyed plan to its equivalent Monday.
|
||||
|
||||
**Date of handoff:** 2026-06-05.
|
||||
|
||||
---
|
||||
|
||||
## What changed (recap)
|
||||
|
||||
| Layer | File | Change |
|
||||
|---|---|---|
|
||||
| Backend | `backend/app/services/orchestrator/runner.py:20-35` | `_current_week_start()` returns the **upcoming Monday** (today if Mon). |
|
||||
| Frontend | `frontend/src/lib/utils.ts:43-83` | `isoMonday` → `upcomingMonday` (new logic). `isoMonday` kept as a deprecated alias. New `formatWeekRange(mondayIso)` helper. |
|
||||
| Frontend | `frontend/src/components/WeekRangeNav.tsx` (NEW) | Shared component. Renders `[<] Jun 8 — Jun 14 [>]` with clickable chevrons + a clickable range label (jump home) + an optional `This week` chip. |
|
||||
| Frontend | `frontend/src/pages/Dashboard.tsx` | Import + 3 call-site updates. Replaces inline segmented control with `<WeekRangeNav>`. |
|
||||
| Frontend | `frontend/src/pages/ShoppingList.tsx` | Same: import + 5 call-site updates, replaces both inline segmented controls (header + empty-state) with `<WeekRangeNav>`. |
|
||||
| Data | `backend/scripts/fix_2026_06_05_to_2026_06_08.sql` (NEW) | Guarded `UPDATE meal_plan SET week_start_date='2026-06-08' WHERE week_start_date='2026-06-05';` with a `SELECT COUNT(*)` and a final verification `SELECT`. Optional commented-out block for 2026-05-29. |
|
||||
| Docs | `Review/ui-nielsen-audit.md` + `fix-ui-audit.md` + `Review/handoff-ui-audit.md` + `docs/HANDOFF.md` + `.agent/plan.md` + `.agent/context.md` | All updated with Sprint 7 status blocks. |
|
||||
|
||||
**No new dependencies. No backend migration. Frontend + backend rebuild only, plus the one-time SQL data fix.**
|
||||
|
||||
---
|
||||
|
||||
## Deploy commands
|
||||
|
||||
Run on the deployment host (`100.108.224.12`):
|
||||
|
||||
```bash
|
||||
# 1. Pull
|
||||
cd ~/MealPlanner
|
||||
git pull
|
||||
|
||||
# 2. SQL data fix (the user's 3-pending-items plan moves to 2026-06-08)
|
||||
docker compose exec -T db psql -U mealplanner -d mealplanner \
|
||||
-f /dev/stdin < backend/scripts/fix_2026_06_05_to_2026_06_08.sql
|
||||
|
||||
# Expected output:
|
||||
# rows_to_migrate
|
||||
# ----------------
|
||||
# 1
|
||||
# UPDATE 1
|
||||
# COMMIT
|
||||
# If you also uncomment the 2026-05-29 block, you'll see another UPDATE 1.
|
||||
|
||||
# 3. Rebuild backend + frontend
|
||||
docker compose -f docker-compose.yml up -d --build backend frontend
|
||||
|
||||
# 4. (Optional) Verify the new function on the running backend
|
||||
docker compose exec backend python -c "
|
||||
from datetime import date
|
||||
from app.services.orchestrator.runner import _current_week_start
|
||||
print('_current_week_start() →', _current_week_start())
|
||||
"
|
||||
# Should print the upcoming Monday's date (e.g. 2026-06-08 if today is Fri 2026-06-05).
|
||||
```
|
||||
|
||||
**Order matters.** Pull → SQL fix → rebuild. The SQL fix is idempotent (re-running is a no-op once 2026-06-05 has no rows), so it's safe to retry.
|
||||
|
||||
---
|
||||
|
||||
## Smoke checklist (browser, on `http://100.108.208.56:8082/`)
|
||||
|
||||
| # | Action | Expected |
|
||||
|---|---|---|
|
||||
| 1 | Open `/` (Dashboard) with no `?week=` query param. | URL is just `/`. Header shows the new week-range nav: `[<] Jun 8 — Jun 14 [>]` for week 2026-06-08. Three meal cards visible (Chicken Fajitas Mon, Garlic Shrimp Scampi Wed, Breakfast-for-Dinner Veggie Scramble Fri). Plan status badge: `draft`. Total cost ~$188.98. |
|
||||
| 2 | Click the range label (`Jun 8 — Jun 14`). | URL is still `/` (jump home goes to the default week). The `This week` chip is **not** visible (we're already on the upcoming week). |
|
||||
| 3 | Click the right chevron (`[>`). | URL becomes `/?week=2026-06-15`. Header shows `Jun 15 — Jun 21`. Empty state appears (no plan for next-next week). |
|
||||
| 4 | Click the left chevron (`[<`). | URL becomes `/?week=2026-06-08`. Header shows `Jun 8 — Jun 14`. Three meal cards reappear. |
|
||||
| 5 | Click the `This week` chip (visible only when off the upcoming week). | URL clears the `?week=` param. Header shows the upcoming week again. |
|
||||
| 6 | Open `/shopping-list` (no `?week=`). | Header shows the same `[<] Jun 8 — Jun 14 [>]` nav. The "Add N to pantry" + "Reset" + "Print List" buttons are still in their normal positions. |
|
||||
| 7 | Click a checkbox in the shopping list. The "Add N to pantry" button shows. | Button works as in Sprint 6 (no regression). |
|
||||
| 8 | Open `/shopping-list?week=2026-06-15`. | Empty state: "No plan for that week. Go to the Dashboard and generate one, or pick a different week." The week-nav is still clickable. |
|
||||
| 9 | Open `/` with `?week=2026-06-01` (a Mon that has no plan). | Empty state with "Plan the week" dropdown (F4). The week-nav shows `Jun 1 — Jun 7` with the `This week` chip visible (because we're not on the upcoming week). |
|
||||
| 10 | Reload the page after each navigation. | URL is preserved; the same week is shown. (F5 URL persistence still works.) |
|
||||
| 11 | Press `g d` (Sprint 5 keyboard shortcut). | Navigates to `/` with the default (upcoming) week. |
|
||||
| 12 | Press `?` (Sprint 5 help shortcut). | ShortcutHelpBanner slides down with the keyboard help text. (No regression.) |
|
||||
|
||||
If any item fails, **stop and report** with the URL you were on, the action you took, and the observed vs expected behavior.
|
||||
|
||||
---
|
||||
|
||||
## API smoke (curl, on the deployment host)
|
||||
|
||||
```bash
|
||||
# (a) The user's just-migrated plan lives at the upcoming Monday's key
|
||||
curl -s "http://100.108.208.56:8082/api/meals?week_start=2026-06-08" | python3 -m json.tool | head -30
|
||||
# Expect: 3 items, all status=pending, total ~$188.98
|
||||
|
||||
# (b) The old Friday-keyed date now returns null (post-migration)
|
||||
curl -s "http://100.108.208.56:8082/api/meals?week_start=2026-06-05"
|
||||
# Expect: null
|
||||
|
||||
# (c) The current calendar week has no plan (was always the case)
|
||||
curl -s "http://100.108.208.56:8082/api/meals?week_start=2026-06-01"
|
||||
# Expect: null
|
||||
|
||||
# (d) The shopping list is also keyed by the upcoming Monday now
|
||||
curl -s "http://100.108.208.56:8082/api/shopping-list?week_start=2026-06-08" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print('items:', len(d.get('items', [])))
|
||||
print('week:', d.get('week_start_date'))
|
||||
print('total:', d.get('total_estimated_cost'))
|
||||
"
|
||||
# Expect: items around 20, week=2026-06-08, total ~$27.35
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Things to look for
|
||||
|
||||
- **The webui is no longer empty.** This is the user's original complaint.
|
||||
- **The week-range label is scannable.** The header shows `Jun 8 — Jun 14` (or whatever the upcoming week is) — the user requested this format.
|
||||
- **The chevron brackets are clickable.** Mouse or keyboard, they step the week by 7 days.
|
||||
- **The `This week` chip appears only when off the upcoming week.** Clean visual signal.
|
||||
- **URL persistence still works.** `?week=YYYY-MM-DD` in the URL is respected.
|
||||
- **Keyboard shortcuts (Sprint 5) still work.** `g d`, `g r`, `g p`, `g s` navigate; `/` focuses search; `?` shows help.
|
||||
- **Plan-the-week (Sprint 6) still works.** The "Plan the week" button is at the same position with the same dropdown.
|
||||
- **Bulk pantry add (Sprint 6) still works.** The "Add N to pantry" button on the shopping list is unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Rollback (if needed)
|
||||
|
||||
The Sprint 7 change is small and easy to revert:
|
||||
|
||||
1. Revert `runner._current_week_start()` to return the most recent Friday (the original 5-line body).
|
||||
2. Revert `upcomingMonday()` to return the current calendar week's Monday.
|
||||
3. Revert the frontend `WeekRangeNav` swap (put the inline segmented control back in Dashboard and ShoppingList).
|
||||
4. Re-run the SQL in reverse: `UPDATE meal_plan SET week_start_date='2026-06-05' WHERE week_start_date='2026-06-08';` (and similarly for 2026-05-29 if you migrated it).
|
||||
5. `git revert` is also clean — all Sprint 7 work is in one or two commits if squashed.
|
||||
|
||||
The pre-Sprint 7 behavior is what the user reported as broken. Only revert if a regression appears that wasn't there before Sprint 7.
|
||||
|
||||
---
|
||||
|
||||
## Verification log
|
||||
|
||||
(Filled in by the operator after deploy + smoke.)
|
||||
|
||||
- [ ] `git pull` on deployment host → success
|
||||
- [ ] SQL fix script ran → expected row count
|
||||
- [ ] Backend rebuild → success
|
||||
- [ ] Frontend rebuild → success
|
||||
- [ ] Browser: `/` shows 3 meals on the upcoming week
|
||||
- [ ] Browser: `[<]` and `[>]` chevrons step the week
|
||||
- [ ] Browser: `This week` chip is visible only when off the upcoming week
|
||||
- [ ] API: `/api/meals?week_start=2026-06-08` returns 3 items
|
||||
- [ ] API: `/api/meals?week_start=2026-06-05` returns null
|
||||
- [ ] No regression in Sprints 1-6
|
||||
|
||||
---
|
||||
|
||||
**Last updated: 2026-06-05** — code complete, awaiting deploy.
|
||||
@@ -77,6 +77,15 @@ The app looks polished on the surface (Tailwind palette, clean cards, working to
|
||||
> - **Backend changes:** `meals.py` and `shopping_list.py` (new query param) + `0015_normalize_pantry_aisles.py` (cast fix).
|
||||
> - **Verification log:** `Review/sprint5-verification.md`. Deploy is a single batch for Sprints 2-5: backup → migrate → rebuild backend + frontend.
|
||||
>
|
||||
> **Sprint 7 status (in progress, approved 2026-06-05; not yet committed):** Outside-the-audit hotfix driven by user report. **Thread 1 of three open follow-ups from the user's 2026-06-05 message.** Thread 2 (cross-week "rejected" semantics) and Thread 3 (§Future backlog) are deferred until S7 is deployed + verified.
|
||||
> - **T1.1** `runner._current_week_start()` → returns the **upcoming** Monday. Today (Fri 2026-06-05) the function returned Friday 2026-06-05; the user got an email for week-of-2026-06-05, but the webui opened on week-of-2026-06-01. One-line body change in `backend/app/services/orchestrator/runner.py:20-24`. Scheduler cron stays Friday.
|
||||
> - **T1.2** `isoMonday` → `upcomingMonday` in `frontend/src/lib/utils.ts:44-50`. Same logic as T1.1; rename for intent. Add `formatWeekRange(mondayIso)` helper.
|
||||
> - **T1.3** New `frontend/src/components/WeekRangeNav.tsx`. Renders the user-requested `[<] Jun 8 — Jun 14 [>]` pattern with clickable chevrons and a clickable range label (jumps to the upcoming week). Replaces the inline Sprint 5 segmented control on Dashboard and ShoppingList. Includes a `This week` chip when off the upcoming week. Keyboard-accessible.
|
||||
> - **T1.4** SQL: `backend/scripts/fix_2026_06_05_to_2026_06_08.sql` — guarded `UPDATE meal_plan SET week_start_date='2026-06-08' WHERE week_start_date='2026-06-05';` with a `SELECT COUNT(*)` first. Optionally migrates 2026-05-29 too (commented out; operator uncomments if desired). The user's 3-pending-items plan moves to the new Mon key.
|
||||
> - **T1.5** "This week" semantic: **upcoming** Mon-Sun. Past weeks accessible via the back chevron. URL persistence (F5) unchanged.
|
||||
> - **No backend migration, no new dependencies.** Deploy is `git pull` + run the SQL script + `docker compose up -d --build backend frontend`.
|
||||
> - **Verification log:** `Review/sprint7-verification.md` (to be written before deploy).
|
||||
>
|
||||
> **Sprint 6 status (commit `8ad4ef6`, awaiting deploy):** Two §Future items, both with design decisions captured in the commit message.
|
||||
> - **F3** Bulk 'add checked to pantry' on ShoppingList. Backend `POST /api/pantry/bulk` accepts `{items: HomePantryCreate[]}` and returns per-item status (`added` / `updated` / `skipped`) with totals. Per-item failure model: unknown ingredient → `skipped` with reason, not a 4xx. Frontend ShoppingList gains a primary `Add N to pantry` button next to the existing Reset button; toast reports `added X, updated Y, skipped Z`; only the items that actually landed are removed from the checked Set. **Scope decision:** ShoppingList only (the checked Set was the natural substrate; Pantry would need new multi-select UI).
|
||||
> - **F4** Plan the whole week on Dashboard. Backend `POST /api/meals/{id}/fill-empty-slots` with body `{meal_types: [str, ...]}` returns `FillEmptySlotsResult { filled: [{day, meal_type, item}], failed: [{day, meal_type, reason}] }`. Iterates day 1..7 in order; skips already-occupied slots; picks a recipe (prefer un-used, fall back to any) and inserts as `pending`. Per-slot failure model — never aborts mid-batch. Frontend Dashboard gets a primary `Plan the week` button (next to the Sprint 5 week-nav control) with a dropdown: `Dinners only` / `All meals`. Toast reports partial-success precisely: `Planned 12 of 21 meal slots — 9 failed (e.g. <reason>)`.
|
||||
|
||||
@@ -18,10 +18,22 @@ STEPS = ("scrape", "generate", "email", "reminder", "deadline", "finalize")
|
||||
|
||||
|
||||
def _current_week_start() -> date:
|
||||
"""Return the most recent Friday (today if today is Friday)."""
|
||||
"""Return the upcoming Monday (today if today is Monday).
|
||||
|
||||
The Friday email advertises the upcoming Mon-Sun week; the plan is
|
||||
keyed by that Monday so the email subject ("Meal plan for week of
|
||||
<date>") matches the calendar week the meals are for. The frontend
|
||||
uses the same convention via `upcomingMonday()` in `lib/utils.ts`.
|
||||
|
||||
Paired with the Sprint 7 fix: see `Review/sprint7-verification.md`
|
||||
and the SQL migration script `backend/scripts/fix_2026_06_05_to_2026_06_08.sql`
|
||||
for the one-time data fix that retargets any pre-S7 Friday-keyed
|
||||
plan to the equivalent upcoming Monday.
|
||||
"""
|
||||
today = date.today()
|
||||
days_since_friday = (today.weekday() - 4) % 7
|
||||
return today - timedelta(days=days_since_friday)
|
||||
if today.weekday() == 0: # Monday
|
||||
return today
|
||||
return today + timedelta(days=(7 - today.weekday()))
|
||||
|
||||
|
||||
def _get_or_create_run(db, family_id, week_start_date: date):
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
-- Sprint 7 — One-time data fix: retarget Friday-keyed plan(s) to the
|
||||
-- equivalent upcoming Monday.
|
||||
--
|
||||
-- Background: pre-Sprint 7, the orchestrator's `_current_week_start()`
|
||||
-- returned the most recent Friday. The Friday email was sent for that
|
||||
-- date, and the plan was keyed by it. Sprint 7 changes the convention
|
||||
-- to "upcoming Monday" (Mon-Sun week). Existing plans keyed by a
|
||||
-- Friday are migrated to the equivalent upcoming Monday so:
|
||||
--
|
||||
-- 1. The user's just-voted-on plan (3 pending items, week of
|
||||
-- 2026-06-05) lives under the same date the new code will use.
|
||||
-- 2. The webui default (upcoming Monday) and the plan key line up.
|
||||
--
|
||||
-- This script is idempotent: re-running it is a no-op once 2026-06-05
|
||||
-- has no rows. The transaction wraps both the count and the UPDATE so
|
||||
-- a partial run can't leave the table in an inconsistent state.
|
||||
--
|
||||
-- Run on the deployment host (db is in a container, no host psql):
|
||||
--
|
||||
-- docker compose exec -T db psql -U mealplanner -d mealplanner \
|
||||
-- -f /dev/stdin < backend/scripts/fix_2026_06_05_to_2026_06_08.sql
|
||||
--
|
||||
-- Expected output:
|
||||
--
|
||||
-- rows_to_migrate
|
||||
-- ----------------
|
||||
-- 1
|
||||
-- UPDATE 1
|
||||
-- id | week_start_date
|
||||
-- ------------------------------------+-----------------
|
||||
-- <uuid-for-2026-05-15-plan> | 2026-05-11
|
||||
-- <uuid-for-2026-05-22-plan> | 2026-05-18
|
||||
-- <uuid-for-2026-05-29-plan> | 2026-05-25
|
||||
-- <uuid-for-2026-06-05-plan-NOW-2026-06-08> | 2026-06-08
|
||||
--
|
||||
-- COMMIT
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- (1) How many rows will be migrated? A count of 1 is the expected
|
||||
-- outcome for the 2026-06-05 plan. Zero is also safe (nothing to do).
|
||||
SELECT COUNT(*) AS rows_to_migrate
|
||||
FROM meal_plan
|
||||
WHERE week_start_date = DATE '2026-06-05';
|
||||
|
||||
-- (2) The actual migration. Single-row target; safe under concurrent
|
||||
-- readers (the row stays visible under the default REPEATABLE READ).
|
||||
UPDATE meal_plan
|
||||
SET week_start_date = DATE '2026-06-08'
|
||||
WHERE week_start_date = DATE '2026-06-05';
|
||||
|
||||
-- (3) Verify. Lists all plans ordered by week, so the operator can
|
||||
-- eyeball that the migration took effect and that the date keys are
|
||||
-- all Mondays (or Fridays, if a pre-2026-05-15 plan was missed).
|
||||
SELECT id, week_start_date
|
||||
FROM meal_plan
|
||||
ORDER BY week_start_date;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ---------------------------------------------------------------------
|
||||
-- Optional: also migrate the 2026-05-29 plan (and any other Friday-keyed
|
||||
-- plan) to the equivalent Monday. The 2026-05-29 plan contains 3
|
||||
-- approved items the family has already acted on; migrating it is
|
||||
-- cosmetic (the plan keeps the same items, just under a Monday key so
|
||||
-- the webui's "current calendar week" view -- if anyone navigates to
|
||||
-- it -- agrees with the calendar). Comment out the block below if
|
||||
-- you do NOT want to migrate older plans.
|
||||
--
|
||||
-- BEGIN;
|
||||
-- SELECT COUNT(*) AS rows_to_migrate_0529
|
||||
-- FROM meal_plan
|
||||
-- WHERE week_start_date = DATE '2026-05-29';
|
||||
-- UPDATE meal_plan
|
||||
-- SET week_start_date = DATE '2026-06-01'
|
||||
-- WHERE week_start_date = DATE '2026-05-29';
|
||||
-- COMMIT;
|
||||
+32
-1
@@ -302,7 +302,38 @@ Trust the tests. Trust the live runs. Don't trust prose claims that something is
|
||||
**Current open proposals:**
|
||||
- `docs/proposals/2026-05-23-feedback-driven-recipe-discovery.md` — pending user approval. No code yet (per the 2026-05-23 section below).
|
||||
|
||||
**Last updated: 2026-06-04** — UI/UX audit & fix cycle (Sprints 1, 2, 3, 4, 5, 6) complete. 20 findings closed (5 P0 + 6 P1 + 3 P2 + 6 §Future), code committed across 10 commits (`f3e4a44`, `36038bb`, `ccc70aa`, `f5fb755`, `e90a9d6`, `427d8ac`, `d71b67a`, `d78bd18`, `f740f40`, `8ad4ef6`), build green. Sprint 1 deployed; Sprints 2-6 awaiting deploy. **Sprint 2's deploy was blocked on a cast bug in migration 0015; that bug is fixed in `d78bd18`.** Full UI-audit handoff at `Review/handoff-ui-audit.md`.
|
||||
**Last updated: 2026-06-05** — UI/UX audit & fix cycle (Sprints 1, 2, 3, 4, 5, 6) complete. 20 findings closed (5 P0 + 6 P1 + 3 P2 + 6 §Future), code committed across 10 commits, build green. Sprint 1 deployed; Sprints 2-6 awaiting deploy. **Sprint 2's deploy was blocked on a cast bug in migration 0015; that bug is fixed in `d78bd18`.** **Sprint 7 (fix webui "empty meal plan" date-semantics mismatch) in progress — see Sprint 7 section below.** Full UI-audit handoff at `Review/handoff-ui-audit.md`.
|
||||
|
||||
---
|
||||
|
||||
## New session: 2026-06-05
|
||||
|
||||
### Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch)
|
||||
|
||||
**User report (2026-06-05, 06:17 PT):** "Latest meal plans were emails to me this morning, but when I go to the webui, the Meal Planner page is empty."
|
||||
|
||||
**Root cause (one-liner):** The orchestrator plans the **upcoming** Mon-Sun week (Fri 2026-06-05 → key 2026-06-08), but the frontend's `isoMonday()` returns the **current** Mon-Sun (Fri 2026-06-05 → 2026-06-01). Email subject, DB plan key, and the webui default URL are 7 days out of sync. The user opens the app, lands on the current Mon-Sun week which has no plan, and sees the "No plan yet" empty state.
|
||||
|
||||
**Specific evidence:**
|
||||
- `_current_week_start()` in `backend/app/services/orchestrator/runner.py:20-24` returns the most recent Friday; on Fri 2026-06-05 it returns 2026-06-05. (Original code, untested in production under the new Sprint 5 frontend.)
|
||||
- `isoMonday()` in `frontend/src/lib/utils.ts:44-50` returns the most recent Monday; on Fri 2026-06-05 it returns 2026-06-01.
|
||||
- DB: the 2026-06-05 plan (`8be25c81-da0b-4944-8c06-919b0d616515`) has 3 pending items (Chicken Fajitas, Garlic Shrimp Scampi, Breakfast-for-Dinner Veggie Scramble). It is **not visible** in the webui default view.
|
||||
- DB: there is **no** plan with `week_start_date=2026-06-01` (current Mon-Sun).
|
||||
- Email was sent by `step_email` Friday 06:00 PT for `week_start_date=2026-06-05` (subject: "Meal plan for week of 2026-06-05"). After S7, the subject becomes "Meal plan for week of 2026-06-08" (the upcoming Monday).
|
||||
|
||||
**Fix scope (7 checkboxes — see `.agent/plan.md` for the full task list):**
|
||||
|
||||
1. **Backend `runner._current_week_start()`** — return the upcoming Monday (today if Mon, else next Mon). One-line body change.
|
||||
2. **Frontend `isoMonday` → `upcomingMonday`** — same logic; rename for intent clarity. Add `formatWeekRange(mondayIso)` helper for the new nav.
|
||||
3. **New `WeekRangeNav` component** (`frontend/src/components/WeekRangeNav.tsx`) — renders the user-requested `[<] Jun 8 — Jun 14 [>]` pattern. Clickable chevrons; clickable range label (jumps home); `This week` chip when off the upcoming week. Replaces the Sprint 5 inline segmented control on both Dashboard and ShoppingList.
|
||||
4. **SQL fix** (`backend/scripts/fix_2026_06_05_to_2026_06_08.sql`) — guarded `UPDATE meal_plan SET week_start_date='2026-06-08' WHERE week_start_date='2026-06-05';` so the user's just-voted-on plan moves to the new key. Optionally also migrates 2026-05-29 (operator opt-in via uncomment).
|
||||
5. **Verification** — `npm run build` green; `Review/sprint7-verification.md` written with deploy + smoke checks.
|
||||
6. **Docs** — Sprint 7 status blocks in `Review/ui-nielsen-audit.md`, `fix-ui-audit.md`, `Review/handoff-ui-audit.md` (this file), `docs/HANDOFF.md` (this section). Sprint 7 verification doc created.
|
||||
7. **No new dependencies, no backend migration.** Frontend + backend rebuild only. The data fix is a SQL script the operator runs once.
|
||||
|
||||
**What "this week" means after Sprint 7:** the **upcoming** Mon-Sun week. The webui's default URL is `/` with no `?week=` param; the API is called with `week_start=upcomingMonday()`; the dashboard header shows `Week of Jun 8, 2026`; the clickable range label and chevrons let the user navigate.
|
||||
|
||||
**Thread 2 (cross-week "rejected" semantics) and Thread 3 (§Future backlog F1/F8/F9/dead-CTA) are deferred** until S7 is deployed + verified. See `Review/handoff-ui-audit.md` "Active sprint" callout.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -315,6 +315,77 @@ Resolve the 14 issues (5 P0, 6 P1, 3 P2) from `Review/ui-nielsen-audit.md` in th
|
||||
|
||||
---
|
||||
|
||||
## Sprint 7 — Fix webui "empty meal plan" (date-semantics mismatch) — IN PROGRESS
|
||||
|
||||
Outside the original audit. Driven by user report 2026-06-05: "Latest meal plans were emails to me this morning, but when I go to the webui, the Meal Planner page is empty."
|
||||
|
||||
**Root cause:** `_current_week_start()` (backend) returns the most recent Friday; `isoMonday()` (frontend) returns the most recent Monday. On Fri 2026-06-05, the email goes out for 2026-06-05 (the email's plan key), but the webui opens on 2026-06-01 (no plan exists). The webui shows the "No plan yet" empty state, but the plan is real — just keyed 7 days later.
|
||||
|
||||
**Scope:** 6 checkboxes. **No new dependencies. No backend migration. Small SQL fix script for the existing 2026-06-05 plan.**
|
||||
|
||||
### S7.1 · Backend — `_current_week_start()` returns the upcoming Monday
|
||||
|
||||
- **File:** `backend/app/services/orchestrator/runner.py:20-24`
|
||||
- **Change:** body becomes `if today.weekday() == 0: return today; else: return today + timedelta(days=(7 - today.weekday()))`. Docstring: "Return the upcoming Monday (today if Monday). The Friday email advertises the upcoming Mon-Sun week; the plan is keyed by that Monday."
|
||||
- **Why:** aligns the plan key with the Mon-Sun calendar week the user expects. The email subject (`f"Meal plan for week of {run.week_start_date}"` at `steps.py:305`) automatically picks up the new value.
|
||||
- **No scheduler change.** `scheduler/__main__.py` still fires Fri 02:00..18:00 PT.
|
||||
- **Verify:** no curl needed for the unit — it's pure date math. Visual verification: after deploy, the next Friday cron will create a plan with `week_start_date = next Monday's date`.
|
||||
|
||||
### S7.2 · Frontend — `isoMonday` → `upcomingMonday` + new helper
|
||||
|
||||
- **File:** `frontend/src/lib/utils.ts:44-50` (rename + retune)
|
||||
- **Change:**
|
||||
- Rename `isoMonday(d?: Date)` → `upcomingMonday(d?: Date)` with body `if d.getUTCDay() === 0: return d; else: d + (7 - d.getUTCDay()) days`.
|
||||
- Add `formatWeekRange(mondayIso: string): string` returning `"Jun 8 — Jun 14"`. Reuses `formatIsoDate` internally.
|
||||
- **Call-site updates:** `Dashboard.tsx:316-320,489` and `ShoppingList.tsx:87-90,216,269` swap the import + function name. Eight call sites in total. `isCurrentWeek = weekStart === upcomingMonday()` is the same idiom; the rename is intent-revealing.
|
||||
- **Why:** frontend and backend agree on "this week" = the upcoming Mon-Sun.
|
||||
- **Verify:** typecheck passes. `npm run build` green.
|
||||
|
||||
### S7.3 · Frontend — new `WeekRangeNav` component
|
||||
|
||||
- **File:** `frontend/src/components/WeekRangeNav.tsx` (NEW)
|
||||
- **Props:** `{ weekStart: string; isCurrentWeek: boolean; onPrev: () => void; onNext: () => void; onJumpHome: () => void }`.
|
||||
- **Renders:** `[<]` button (chevron-left, `aria-label="Previous week"`), then a button showing the formatted range label (e.g. `Jun 8 — Jun 14`, `aria-label="Jump to upcoming week"`, clickable → onJumpHome), then `[>]` button (chevron-right, `aria-label="Next week"`). A small `This week` chip appears only when `!isCurrentWeek` (clickable → onJumpHome).
|
||||
- **Why:** user requested a visible, scannable date range with clickable brackets. Replaces the small inline Sprint 5 segmented control on both Dashboard and ShoppingList (single source of truth for the visual + behavior).
|
||||
- **Reuses:** `lucide-react` `ChevronLeft` / `ChevronRight` (already in Dashboard/ShoppingList imports). `formatWeekRange` from `lib/utils`.
|
||||
- **Verify:** typecheck passes. `npm run build` green. Visual: header on Dashboard + ShoppingList now shows `Jun 8 — Jun 14` for week_start 2026-06-08.
|
||||
|
||||
### S7.4 · Frontend — wire WeekRangeNav into Dashboard + ShoppingList
|
||||
|
||||
- **Files:** `frontend/src/pages/Dashboard.tsx:479-503` and `frontend/src/pages/ShoppingList.tsx:259-283`
|
||||
- **Change:** delete the inline segmented control; add `<WeekRangeNav weekStart={weekStart} isCurrentWeek={isCurrentWeek} onPrev={() => navigateWeek(shiftIsoDate(weekStart, -7))} onNext={() => navigateWeek(shiftIsoDate(weekStart, 7))} onJumpHome={() => navigateWeek(upcomingMonday())} />`. Header layout reflows minimally — the nav is the same width as the segmented control.
|
||||
- **Why:** single source of truth; user's specific request.
|
||||
- **Verify:** both pages render the new nav at the same position. The "Plan the week" button (Sprint 6) and the "Add N to pantry" button (Sprint 6) keep their positions to the right.
|
||||
|
||||
### S7.5 · Data — fix the existing 2026-06-05 plan key
|
||||
|
||||
- **File:** `backend/scripts/fix_2026_06_05_to_2026_06_08.sql` (NEW)
|
||||
- **Body:**
|
||||
```sql
|
||||
-- Count rows that will change
|
||||
SELECT COUNT(*) AS rows_to_migrate FROM meal_plan
|
||||
WHERE week_start_date = DATE '2026-06-05';
|
||||
-- Migrate the 3-pending-items plan
|
||||
UPDATE meal_plan SET week_start_date = DATE '2026-06-08'
|
||||
WHERE week_start_date = DATE '2026-06-05';
|
||||
-- Verify
|
||||
SELECT id, week_start_date FROM meal_plan ORDER BY week_start_date;
|
||||
```
|
||||
Plus a commented-out block for the 2026-05-29 plan (operator uncomments if desired).
|
||||
- **Why:** the user's just-voted-on plan (3 pending items) is keyed 2026-06-05. After S7.1, future plans are Mon-keyed. We migrate this one to 2026-06-08 so the user sees the plan they got the email about, in the same place as the email advertises.
|
||||
- **No schema change.** SQL is idempotent (re-running is a no-op once 2026-06-05 has no rows).
|
||||
- **Verify:** operator runs the script; output shows 1 row migrated (the 2026-06-05 plan). After migrate, `curl /api/meals?week_start=2026-06-08` returns 3 items.
|
||||
|
||||
### S7.6 · Sprint 7 verification gate
|
||||
|
||||
- [ ] `npm run build` green for Sprint 7.
|
||||
- [ ] Backend smoke on local dev DB: `curl /api/meals?week_start=2026-06-08` returns the 3 items (after the data fix); `curl /api/meals?week_start=2026-06-01` returns null.
|
||||
- [ ] Frontend smoke: `npm run build` produces a build that, when served, defaults the Dashboard to the upcoming Mon-Sun week.
|
||||
- [ ] Deploy verified on `100.108.224.12` — see `Review/sprint7-verification.md` for the operator checklist.
|
||||
- [ ] No regression in Sprints 1-6.
|
||||
|
||||
---
|
||||
|
||||
## Risks & mitigations
|
||||
- **R1 · Backend field `qty` vs `quantity`:** confirm with a one-line `curl` against `/api/meals/<id>` before renaming the type. If the API still returns `quantity`, use a shim `ing.qty ?? ing.quantity` rather than breaking other consumers.
|
||||
- **R2 · Pantry migration:** run against dev DB first; capture before/after row counts. **Do not** run on prod without the `--backup-table` step in place.
|
||||
@@ -334,3 +405,4 @@ Resolve the 14 issues (5 P0, 6 P1, 3 P2) from `Review/ui-nielsen-audit.md` in th
|
||||
- [ ] Backend aisle-migration (`0015` with cast fix) run on dev — **done on local dev host 2026-06-04**; needs running on deployment host.
|
||||
- [ ] Manual smoke pass on `http://100.108.208.56:8082/` per `Review/sprint2-verification.md` (Sprint 1-3), `Review/sprint4-verification.md` (Sprint 4), `Review/sprint5-verification.md` (Sprint 5).
|
||||
- [ ] No regressions in existing Playwright walkthrough.
|
||||
- [ ] **Sprint 7 (in progress):** webui "empty meal plan" date-semantics mismatch. Code + SQL fix + verification doc. S7.1-S7.6 boxes in the section above.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { formatWeekRange, upcomingMonday } from '../lib/utils'
|
||||
|
||||
interface WeekRangeNavProps {
|
||||
/** ISO Monday date of the currently-displayed week (YYYY-MM-DD). */
|
||||
weekStart: string
|
||||
/** True when the displayed week is the upcoming Mon-Sun week. */
|
||||
isCurrentWeek: boolean
|
||||
onPrev: () => void
|
||||
onNext: () => void
|
||||
onJumpHome: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Week-range navigation with clickable brackets. Renders the
|
||||
* `[<] Mon DD — Sun DD [>]` pattern + an optional `This week` chip
|
||||
* when the displayed week is not the upcoming week.
|
||||
*
|
||||
* Replaces the Sprint 5 inline segmented control. The bracket chevrons
|
||||
* step by 7 days; clicking the range label jumps back to the upcoming
|
||||
* week (the default webui landing week). Keyboard-accessible: every
|
||||
* interactive element is a real `<button>` with an `aria-label`.
|
||||
*/
|
||||
export function WeekRangeNav({
|
||||
weekStart,
|
||||
isCurrentWeek,
|
||||
onPrev,
|
||||
onNext,
|
||||
onJumpHome,
|
||||
}: WeekRangeNavProps) {
|
||||
return (
|
||||
<div
|
||||
className="inline-flex items-center rounded-lg border border-surface-200 bg-white"
|
||||
role="group"
|
||||
aria-label="Week navigation"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPrev}
|
||||
aria-label="Previous week"
|
||||
title="Previous week"
|
||||
className="p-2 text-surface-600 hover:bg-surface-100 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onJumpHome}
|
||||
aria-label={`Jump to upcoming week (currently ${formatWeekRange(upcomingMonday())})`}
|
||||
title="Jump to upcoming week"
|
||||
className={`px-3 py-2 text-sm font-medium border-x border-surface-200 focus:outline-none focus:ring-2 focus:ring-primary-400 ${
|
||||
isCurrentWeek
|
||||
? 'text-primary-700 bg-primary-50'
|
||||
: 'text-surface-600 hover:bg-surface-100'
|
||||
}`}
|
||||
>
|
||||
{formatWeekRange(weekStart)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNext}
|
||||
aria-label="Next week"
|
||||
title="Next week"
|
||||
className="p-2 text-surface-600 hover:bg-surface-100 rounded-r-lg focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
{!isCurrentWeek && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onJumpHome}
|
||||
className="ml-2 inline-flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-md bg-primary-50 text-primary-700 hover:bg-primary-100 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
aria-label="Jump to upcoming week"
|
||||
title="Jump to upcoming week"
|
||||
>
|
||||
This week
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -37,16 +37,48 @@ export function cleanDescription(input: string | undefined | null, maxLen = 280)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Week helpers (used by F5 URL week selector) */
|
||||
/* Week helpers (used by F5 URL week selector + Sprint 7 WeekRangeNav) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/** Return the ISO date (YYYY-MM-DD) of the Monday of the given date's week. */
|
||||
export function isoMonday(d: Date = new Date()): string {
|
||||
/**
|
||||
* Return the ISO date (YYYY-MM-DD) of the *upcoming* Monday for the given date.
|
||||
* - If `d` is a Monday → returns `d` itself.
|
||||
* - Otherwise → returns the next Monday (1..6 days ahead).
|
||||
*
|
||||
* Pairs with `runner._current_week_start()` on the backend (see
|
||||
* `backend/app/services/orchestrator/runner.py`). "This week" means the
|
||||
* upcoming Mon-Sun week the Friday email advertises.
|
||||
*/
|
||||
export function upcomingMonday(d: Date = new Date()): string {
|
||||
const day = d.getUTCDay() // 0=Sun, 1=Mon, ..., 6=Sat
|
||||
// Treat Sunday as end-of-week (offset 6), Mon-Sat as offset (day-1).
|
||||
const offset = day === 0 ? 6 : day - 1
|
||||
const monday = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - offset))
|
||||
return monday.toISOString().slice(0, 10)
|
||||
if (day === 0) {
|
||||
// Sunday: upcoming Monday is tomorrow (1 day ahead).
|
||||
const next = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1))
|
||||
return next.toISOString().slice(0, 10)
|
||||
}
|
||||
if (day === 1) {
|
||||
// Monday: today.
|
||||
return d.toISOString().slice(0, 10)
|
||||
}
|
||||
// Tue..Sat: next Monday is (7 - day) days ahead.
|
||||
const offset = 7 - day + 1
|
||||
const next = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + offset))
|
||||
return next.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy alias kept for any external code that might still reference the
|
||||
* calendar-week Monday. The frontend codebase has been updated to use
|
||||
* `upcomingMonday`; this alias is intentionally a no-op redirect to the
|
||||
* new function with a deprecation note.
|
||||
*
|
||||
* @deprecated Use `upcomingMonday()` — see Sprint 7 root-cause in
|
||||
* `Review/handoff-ui-audit.md`. The "current calendar week" semantic
|
||||
* mismatched the orchestrator's "upcoming week" semantic, causing the
|
||||
* webui to show an empty state on Fridays.
|
||||
*/
|
||||
export function isoMonday(d: Date = new Date()): string {
|
||||
return upcomingMonday(d)
|
||||
}
|
||||
|
||||
/** Parse a YYYY-MM-DD string into a Date (UTC midnight). Returns null if invalid. */
|
||||
@@ -64,9 +96,35 @@ export function shiftIsoDate(s: string, days: number): string {
|
||||
return d.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/** Format a YYYY-MM-DD string for display: "Jun 1, 2026". */
|
||||
/**
|
||||
* Format a YYYY-MM-DD string for display: "Jun 1, 2026".
|
||||
* Always renders in UTC so the displayed date matches the ISO date
|
||||
* stored in the DB regardless of the viewer's local timezone.
|
||||
*/
|
||||
export function formatIsoDate(s: string): string {
|
||||
const d = parseIsoDate(s)
|
||||
if (!d) return s
|
||||
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
||||
return d.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a Monday's ISO date as a Mon–Sun range label: "Jun 8 — Jun 14".
|
||||
* Used by `WeekRangeNav` (Sprint 7) for the visible week range.
|
||||
*
|
||||
* Always renders in UTC so the date the user sees matches the ISO date
|
||||
* stored in the DB regardless of the viewer's local timezone.
|
||||
*/
|
||||
export function formatWeekRange(mondayIso: string): string {
|
||||
const monday = parseIsoDate(mondayIso)
|
||||
if (!monday) return mondayIso
|
||||
const sunday = new Date(monday)
|
||||
sunday.setUTCDate(sunday.getUTCDate() + 6)
|
||||
const fmt = (d: Date) =>
|
||||
d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: 'UTC' })
|
||||
return `${fmt(monday)} — ${fmt(sunday)}`
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import {
|
||||
CookingPot, CalendarDays, ShoppingCart, ChevronRight, ChevronLeft, Sparkles, Loader2,
|
||||
CookingPot, CalendarDays, ShoppingCart, ChevronRight, Sparkles, Loader2,
|
||||
GripVertical, X
|
||||
} from 'lucide-react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { showToast, showApiError } from '../lib/toast'
|
||||
import { isoMonday, parseIsoDate, shiftIsoDate, formatIsoDate } from '../lib/utils'
|
||||
import { upcomingMonday, parseIsoDate, shiftIsoDate, formatIsoDate } from '../lib/utils'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import {
|
||||
DragDropContext,
|
||||
@@ -26,6 +26,7 @@ import { Badge } from '../components/ui/Badge'
|
||||
import { Card, CardBody, CardHeader } from '../components/ui/Card'
|
||||
import { SkeletonCard, Skeleton } from '../components/ui/Skeleton'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { WeekRangeNav } from '../components/WeekRangeNav'
|
||||
|
||||
const DAY_NAMES = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
const FULL_DAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
||||
@@ -313,10 +314,10 @@ export default function Dashboard() {
|
||||
const parsedWeek = weekParam ? parseIsoDate(weekParam) : null
|
||||
const weekStart = weekParam && parsedWeek
|
||||
? weekParam
|
||||
: isoMonday()
|
||||
const isCurrentWeek = weekStart === isoMonday()
|
||||
: upcomingMonday()
|
||||
const isCurrentWeek = weekStart === upcomingMonday()
|
||||
const navigateWeek = (next: string) => {
|
||||
setSearchParams(next === isoMonday() ? {} : { week: next }, { replace: true })
|
||||
setSearchParams(next === upcomingMonday() ? {} : { week: next }, { replace: true })
|
||||
}
|
||||
const [planningWeek, setPlanningWeek] = useState(false)
|
||||
const [planMenuOpen, setPlanMenuOpen] = useState(false)
|
||||
@@ -477,30 +478,13 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="inline-flex items-center rounded-lg border border-surface-200 bg-white">
|
||||
<button
|
||||
onClick={() => navigateWeek(shiftIsoDate(weekStart, -7))}
|
||||
aria-label="Previous week"
|
||||
className="p-2 text-surface-600 hover:bg-surface-100 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateWeek(isoMonday())}
|
||||
aria-label="Jump to current week"
|
||||
title="Jump to current week"
|
||||
className={`px-3 py-2 text-sm font-medium border-x border-surface-200 focus:outline-none focus:ring-2 focus:ring-primary-400 ${isCurrentWeek ? 'text-primary-700 bg-primary-50' : 'text-surface-600 hover:bg-surface-100'}`}
|
||||
>
|
||||
{isCurrentWeek ? 'This week' : 'Current'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateWeek(shiftIsoDate(weekStart, 7))}
|
||||
aria-label="Next week"
|
||||
className="p-2 text-surface-600 hover:bg-surface-100 rounded-r-lg focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<WeekRangeNav
|
||||
weekStart={weekStart}
|
||||
isCurrentWeek={isCurrentWeek}
|
||||
onPrev={() => navigateWeek(shiftIsoDate(weekStart, -7))}
|
||||
onNext={() => navigateWeek(shiftIsoDate(weekStart, 7))}
|
||||
onJumpHome={() => navigateWeek(upcomingMonday())}
|
||||
/>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setPlanMenuOpen(o => !o)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Printer, ShoppingCart, Package, Tag, Receipt, RotateCcw, ChevronLeft, ChevronRight, PackagePlus } from 'lucide-react'
|
||||
import { Printer, ShoppingCart, Package, Tag, Receipt, RotateCcw, PackagePlus } from 'lucide-react'
|
||||
import { mealPlannerApi } from '../api'
|
||||
import type { ShoppingList } from '../types'
|
||||
import { Button } from '../components/ui/Button'
|
||||
@@ -9,7 +9,8 @@ import { Badge } from '../components/ui/Badge'
|
||||
import { Card, CardBody } from '../components/ui/Card'
|
||||
import { Skeleton, SkeletonText } from '../components/ui/Skeleton'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { isoMonday, parseIsoDate, shiftIsoDate, formatIsoDate } from '../lib/utils'
|
||||
import { WeekRangeNav } from '../components/WeekRangeNav'
|
||||
import { upcomingMonday, parseIsoDate, shiftIsoDate, formatIsoDate } from '../lib/utils'
|
||||
import { showToast, showApiError } from '../lib/toast'
|
||||
|
||||
const AISLE_LABEL: Record<string, string> = {
|
||||
@@ -84,10 +85,10 @@ export default function ShoppingListPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const weekParam = searchParams.get('week')
|
||||
const parsedWeek = weekParam ? parseIsoDate(weekParam) : null
|
||||
const weekStart = weekParam && parsedWeek ? weekParam : isoMonday()
|
||||
const isCurrentWeek = weekStart === isoMonday()
|
||||
const weekStart = weekParam && parsedWeek ? weekParam : upcomingMonday()
|
||||
const isCurrentWeek = weekStart === upcomingMonday()
|
||||
const navigateWeek = (next: string) => {
|
||||
setSearchParams(next === isoMonday() ? {} : { week: next }, { replace: true })
|
||||
setSearchParams(next === upcomingMonday() ? {} : { week: next }, { replace: true })
|
||||
}
|
||||
const { data: shoppingList, isLoading } = useQuery<ShoppingList>({
|
||||
queryKey: ['shoppingList', weekStart],
|
||||
@@ -204,29 +205,14 @@ export default function ShoppingListPage() {
|
||||
<p className="text-sm text-surface-500">Week of {formatIsoDate(weekStart)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-flex items-center rounded-lg border border-surface-200 bg-white">
|
||||
<button
|
||||
onClick={() => navigateWeek(shiftIsoDate(weekStart, -7))}
|
||||
aria-label="Previous week"
|
||||
className="p-2 text-surface-600 hover:bg-surface-100 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateWeek(isoMonday())}
|
||||
aria-label="Jump to current week"
|
||||
title="Jump to current week"
|
||||
className={`px-3 py-2 text-sm font-medium border-x border-surface-200 focus:outline-none focus:ring-2 focus:ring-primary-400 ${isCurrentWeek ? 'text-primary-700 bg-primary-50' : 'text-surface-600 hover:bg-surface-100'}`}
|
||||
>
|
||||
{isCurrentWeek ? 'This week' : 'Current'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateWeek(shiftIsoDate(weekStart, 7))}
|
||||
aria-label="Next week"
|
||||
className="p-2 text-surface-600 hover:bg-surface-100 rounded-r-lg focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<WeekRangeNav
|
||||
weekStart={weekStart}
|
||||
isCurrentWeek={isCurrentWeek}
|
||||
onPrev={() => navigateWeek(shiftIsoDate(weekStart, -7))}
|
||||
onNext={() => navigateWeek(shiftIsoDate(weekStart, 7))}
|
||||
onJumpHome={() => navigateWeek(upcomingMonday())}
|
||||
/>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Receipt}
|
||||
@@ -257,30 +243,13 @@ export default function ShoppingListPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="inline-flex items-center rounded-lg border border-surface-200 bg-white">
|
||||
<button
|
||||
onClick={() => navigateWeek(shiftIsoDate(weekStart, -7))}
|
||||
aria-label="Previous week"
|
||||
className="p-2 text-surface-600 hover:bg-surface-100 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateWeek(isoMonday())}
|
||||
aria-label="Jump to current week"
|
||||
title="Jump to current week"
|
||||
className={`px-3 py-2 text-sm font-medium border-x border-surface-200 focus:outline-none focus:ring-2 focus:ring-primary-400 ${isCurrentWeek ? 'text-primary-700 bg-primary-50' : 'text-surface-600 hover:bg-surface-100'}`}
|
||||
>
|
||||
{isCurrentWeek ? 'This week' : 'Current'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigateWeek(shiftIsoDate(weekStart, 7))}
|
||||
aria-label="Next week"
|
||||
className="p-2 text-surface-600 hover:bg-surface-100 rounded-r-lg focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<WeekRangeNav
|
||||
weekStart={weekStart}
|
||||
isCurrentWeek={isCurrentWeek}
|
||||
onPrev={() => navigateWeek(shiftIsoDate(weekStart, -7))}
|
||||
onNext={() => navigateWeek(shiftIsoDate(weekStart, 7))}
|
||||
onJumpHome={() => navigateWeek(upcomingMonday())}
|
||||
/>
|
||||
{progress > 0 && (
|
||||
<span className="text-sm text-surface-500">{progress}% complete</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user