Public Access
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
100 lines
3.0 KiB
Python
100 lines
3.0 KiB
Python
"""
|
|
Orchestrator runner.
|
|
|
|
run_step(step_name, week_start_date) — execute one step for all families
|
|
run_week(week_start_date) — execute all steps in sequence
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import date, timedelta
|
|
from typing import Optional
|
|
|
|
from app.database import SessionLocal
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
STEPS = ("scrape", "generate", "email", "reminder", "deadline", "finalize")
|
|
|
|
|
|
def _current_week_start() -> date:
|
|
"""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()
|
|
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):
|
|
from app.models import WeeklyRun
|
|
|
|
run = (
|
|
db.query(WeeklyRun)
|
|
.filter(
|
|
WeeklyRun.family_id == family_id,
|
|
WeeklyRun.week_start_date == week_start_date,
|
|
)
|
|
.first()
|
|
)
|
|
if run is None:
|
|
run = WeeklyRun(
|
|
family_id=family_id,
|
|
week_start_date=week_start_date,
|
|
status="pending",
|
|
)
|
|
db.add(run)
|
|
db.flush()
|
|
db.refresh(run)
|
|
return run
|
|
|
|
|
|
def run_step(step_name: str, week_start_date: Optional[date] = None) -> None:
|
|
from app.models import FamilyProfile
|
|
from app.services.orchestrator import steps as s
|
|
|
|
step_fns = {
|
|
"scrape": s.step_scrape,
|
|
"generate": s.step_generate,
|
|
"email": s.step_email,
|
|
"reminder": s.step_reminder,
|
|
"deadline": s.step_deadline,
|
|
"finalize": s.step_finalize,
|
|
}
|
|
if step_name not in step_fns:
|
|
raise ValueError(f"Unknown step: {step_name!r}. Valid: {list(step_fns)}")
|
|
|
|
week = week_start_date or _current_week_start()
|
|
db = SessionLocal()
|
|
try:
|
|
families = db.query(FamilyProfile).all()
|
|
if not families:
|
|
logger.warning("run_step(%s): no family profiles, skipping", step_name)
|
|
return
|
|
for family in families:
|
|
run = _get_or_create_run(db, family.id, week)
|
|
try:
|
|
step_fns[step_name](run, db)
|
|
except Exception:
|
|
logger.exception(
|
|
"run_step(%s) failed for family %s", step_name, family.id
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def run_week(week_start_date: Optional[date] = None) -> None:
|
|
week = week_start_date or _current_week_start()
|
|
for step in STEPS:
|
|
run_step(step, week)
|