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
+67 -9
View File
@@ -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 MonSun 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)}`
}