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:
@@ -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)}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user