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:
MealPlanner
2026-06-05 07:46:55 -07:00
parent a616138e7c
commit 09c7525a12
13 changed files with 679 additions and 96 deletions
+72
View File
@@ -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.