From 09c7525a12afe4b41efeac3cd3c971533cab6d38 Mon Sep 17 00:00:00 2001 From: MealPlanner Date: Fri, 5 Jun 2026 07:46:55 -0700 Subject: [PATCH] fix(ui): align 'this week' to upcoming Monday (Sprint 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ') 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 --- .agent/context.md | 57 +++++++ .agent/plan.md | 62 +++++++ Review/handoff-ui-audit.md | 18 +- Review/sprint7-verification.md | 157 ++++++++++++++++++ Review/ui-nielsen-audit.md | 9 + backend/app/services/orchestrator/runner.py | 18 +- .../scripts/fix_2026_06_05_to_2026_06_08.sql | 77 +++++++++ docs/HANDOFF.md | 33 +++- fix-ui-audit.md | 72 ++++++++ frontend/src/components/WeekRangeNav.tsx | 81 +++++++++ frontend/src/lib/utils.ts | 76 ++++++++- frontend/src/pages/Dashboard.tsx | 42 ++--- frontend/src/pages/ShoppingList.tsx | 73 +++----- 13 files changed, 679 insertions(+), 96 deletions(-) create mode 100644 Review/sprint7-verification.md create mode 100644 backend/scripts/fix_2026_06_05_to_2026_06_08.sql create mode 100644 frontend/src/components/WeekRangeNav.tsx diff --git a/.agent/context.md b/.agent/context.md index 8af8ded..afffe24 100644 --- a/.agent/context.md +++ b/.agent/context.md @@ -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 ``) +- `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) + diff --git a/.agent/plan.md b/.agent/plan.md index f167018..332e937 100644 --- a/.agent/plan.md +++ b/.agent/plan.md @@ -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`. diff --git a/Review/handoff-ui-audit.md b/Review/handoff-ui-audit.md index eef1882..d12286f 100644 --- a/Review/handoff-ui-audit.md +++ b/Review/handoff-ui-audit.md @@ -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. diff --git a/Review/sprint7-verification.md b/Review/sprint7-verification.md new file mode 100644 index 0000000..75ee0db --- /dev/null +++ b/Review/sprint7-verification.md @@ -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 ``. | +| Frontend | `frontend/src/pages/ShoppingList.tsx` | Same: import + 5 call-site updates, replaces both inline segmented controls (header + empty-state) with ``. | +| 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. diff --git a/Review/ui-nielsen-audit.md b/Review/ui-nielsen-audit.md index 276e6b5..9bb429b 100644 --- a/Review/ui-nielsen-audit.md +++ b/Review/ui-nielsen-audit.md @@ -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. )`. diff --git a/backend/app/services/orchestrator/runner.py b/backend/app/services/orchestrator/runner.py index da9f398..9de59bd 100644 --- a/backend/app/services/orchestrator/runner.py +++ b/backend/app/services/orchestrator/runner.py @@ -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 + ") 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): diff --git a/backend/scripts/fix_2026_06_05_to_2026_06_08.sql b/backend/scripts/fix_2026_06_05_to_2026_06_08.sql new file mode 100644 index 0000000..a0a94be --- /dev/null +++ b/backend/scripts/fix_2026_06_05_to_2026_06_08.sql @@ -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 +-- ------------------------------------+----------------- +-- | 2026-05-11 +-- | 2026-05-18 +-- | 2026-05-25 +-- | 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; diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index f175e05..faa0b41 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -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. --- diff --git a/fix-ui-audit.md b/fix-ui-audit.md index cb85701..bd925bb 100644 --- a/fix-ui-audit.md +++ b/fix-ui-audit.md @@ -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 ` 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/` 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. diff --git a/frontend/src/components/WeekRangeNav.tsx b/frontend/src/components/WeekRangeNav.tsx new file mode 100644 index 0000000..1921ccb --- /dev/null +++ b/frontend/src/components/WeekRangeNav.tsx @@ -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 ` + + + {!isCurrentWeek && ( + + )} + + ) +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index ca19a7e..e712d84 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -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)}` } diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 96a93be..2db0e31 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -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() {
-
- - - -
+ navigateWeek(shiftIsoDate(weekStart, -7))} + onNext={() => navigateWeek(shiftIsoDate(weekStart, 7))} + onJumpHome={() => navigateWeek(upcomingMonday())} + />
-
- - - +
+ navigateWeek(shiftIsoDate(weekStart, -7))} + onNext={() => navigateWeek(shiftIsoDate(weekStart, 7))} + onJumpHome={() => navigateWeek(upcomingMonday())} + />
-
- - - -
+ navigateWeek(shiftIsoDate(weekStart, -7))} + onNext={() => navigateWeek(shiftIsoDate(weekStart, 7))} + onJumpHome={() => navigateWeek(upcomingMonday())} + /> {progress > 0 && ( {progress}% complete )}