Frontend: C1 — Weekly planner (home screen) #26
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The app's home screen. Shows a 7-day dinner planner with meal slots, variety score, and actions. The most complex screen — three fundamentally different layouts by breakpoint.
Journey: J2 (plan the week) + J3 (cook tonight) + J4 (adapt on the fly)
Role: Planner (full access) / Household member (read-only, actions hidden)
Screen: C1
Variety Score — ALWAYS VISIBLE
The variety score (0–10) must be visible on all breakpoints without any extra navigation. It is the core value proposition.
--yellow-tintbg,--yellow-light1px border,--radius-lg--yellow-lighttrack,--yellowfill--yellow-textMobile (< 768px)
5-region vertical stack:
--color-border| has meal =--green| today =--yellow-text--yellow-tintbg +--yellow-lightborder--green-tintbg +--green-lightborder--yellow-tintbgTablet (768px–1024px)
Same stack but wider:
Desktop (> 1024px)
Completely different — 3-panel horizontal model:
--radius-lg, flex:1 to fill height--color-surfacebg, 1px--color-border,--shadow-card--green-lightborder,--shadow-raised--yellow-tintbg--green-tintbgrgba(242,193,46,0.25)bg +--yellow-text--yellow-textRole Behavior
Acceptance Criteria
Spec file:
specs/frontend/j2-plan-the-week.html— screen C1 with full planner spec: mobile phone frame, tablet frame, desktop 3-panel frame, annotation notes for all breakpoints, complete agent table (50+ measurement rows), and LLM implementation guide.👨💻 Kai — Frontend Engineer
C1 is easily the most complex screen in the project — three fundamentally different layouts, role-gating, real-time variety score, and week navigation. Here's what I'm thinking through before I write a line of code.
Component decomposition
I'm planning to split this into:
PlannerHeader,VarietyScoreCard,WeekStrip,DayMealCard(selected day),RemainingDayList(mobile),CalendarGrid(desktop),DayDetailPanel(desktop). That's 7 focused files rather than one monster component. Each stays short and testable.Questions before I start:
position: stickywithin a scrolling main column, or is it a CSS grid withheight: 100vhon the outer container andoverflow-y: autoonly on the center column? This determines whether I usegridorflexat the top level.--yellow-text. On a potentially dark chip background, is contrast verified? I want to flag this for Atlas before I hardcode it.SSR / data loading concerns:
+page.server.ts. What's the API shape — one call for the full week, or 7 individual meal-slot calls??week=search param, or a client-side state update with a subsequent fetch? The spec doesn't say, and it affects how I wire thePlannerHeaderbuttons.Role behavior:
+page.server.tsand pass areadonly: booleanprop down. I won't scatter auth checks across individual components. Ismemberthe only restricted role, or are there others I need to handle here?Reactivity:
$state<number>for day index). The remaining days list is$derivedfrom that. No SSR needed for the selection — correct?Svelte 5 notes:
{#each}over the 7 days — straightforward.--green-lightborder,--shadow-raised) is pure CSS, no JS state needed.This is a big screen. I'd like to agree on the data shape and week navigation strategy before building, so I'm not refactoring mid-flight.
🔧 Backend Engineer
C1 is the screen that ties together the most backend domains at once — planning, variety scoring, ingredient data, and role access. Let me flag what the API layer needs to support this cleanly.
Week data endpoint:
The 3-panel desktop and the mobile day-strip both need all 7 days at once. I'd propose a single
GET /api/weeks/{year}/{week}endpoint returning the full week's meal slots with denormalized recipe data (name, tags, ingredients for repeat detection). A separate call per day would be 7 round-trips on page load — not acceptable.nullentries (so the client always gets exactly 7 items) or omitted? The spec shows empty slots differently on mobile vs desktop — having them explicit asnullsimplifies frontend logic considerably.Variety score:
Role enforcement:
GET /api/weeks/{year}/{week}endpoint should be accessible to both planners and members (members get read-only data, just no mutation endpoints). Is authorization on the write endpoints (POST /api/plan,DELETE /api/plan/{id}) enforced at the service layer, or only at the controller?household_idfilter on every query — this needs to be in the repository layer, not optional.Week navigation:
?offset=-1)? I'd keep it simple: absolute ISO week param, client builds the next/prev values.Data integrity:
(household_id, planned_date)? If a planner can only assign one dinner per day, the DB should enforce it, not just the application.cooking_logis referenced in B4 but feeds the variety algorithm here — what's the join strategy? Is the variety score computed fresh on each week load, or cached?Happy to draft the DDL and the
GET /api/weeksresponse schema if we align on the above first.🧪 QA Engineer
C1 is the most test-surface-rich screen in the app. Three breakpoints, two roles, variety score, week navigation, ingredient repeat warnings, and today/selected state — I'm mapping out the full coverage matrix now so nothing slips through.
Happy paths to cover:
Bad paths / edge cases I'm already thinking about:
Component test checklist (Svelte):
VarietyScoreCard: renders score, progress bar width matches percentage, warning rows appear for repeated ingredients, "Review variety →" link only visible on desktopWeekStrip: 7 chips always rendered, correct dot color per slot state (empty/filled/today), today chip has yellow treatment, selected chip has green treatmentDayMealCard: "Cook now" and "Swap" hidden whenreadonly=trueCalendarGrid(desktop): empty tile shows dashed border + "+" icon, hover state applies correctlyIntegration tests:
GET /api/weeks/{year}/{week}as planner → 200 with full dataE2E:
One specific question: for the mobile "Remaining days list" — the spec says "only days after selected." If the user selects Sunday (last day), the remaining list is empty. Is there a designed empty state for that, or does the section just disappear?
🔒 Sable — Security Engineer
C1 is the app's main surface and it touches multi-tenancy, role access, and real-time data. Here's my threat model for this screen.
Broken access control (OWASP #1) — highest priority:
household_idderived from the authenticated session — never from a URL parameter or request body the client controls. A planner navigating to/planner?week=2026-W14must only ever see their own household's data, regardless of what week they request.+page.server.ts(returningreadonly: truein the page data) or purely in the component? It must be server-side. A member manipulating client-side state to show planner controls is cosmetic — the real risk is if the member can call mutation endpoints directly.POST /api/planorDELETE /api/plan/{id}directly? These must return 403, verified by an integration test.IDOR risk:
plan_id— confirm that the backend validatesplan_idbelongs to the requesting user's household before executing any swap.Information leakage:
Week navigation:
No XSS surface I see on C1 directly — recipe names and ingredient names come from the database via the backend. As long as Kai is not using
{@html}for these (and shouldn't be — plain text rendering is fine), we're clean.One open question: The "Review variety →" link on desktop goes to C3. Is C3 accessible to members, or planners only? If members can reach it via this link, C3 needs the same read-only enforcement.
🎨 Atlas — UI/UX Designer
C1 is the core value screen — the one users land on every day. The spec exists and is the authoritative reference, but I want to flag a few design concerns before implementation begins.
Variety score — always visible, no exceptions:
position: stickybelow the top nav, or is it acceptable for it to scroll away? The spec needs to clarify this explicitly. I'd argue sticky is the right call given "always visible" is called out as the core value proposition.Token usage I'll be watching for:
--yellow-text" on a chip that has--yellow-tintbg.--yellow-texton--yellow-tintneeds a contrast check — 4.5:1 required for the 3px dot (actually this is non-text so 3:1 applies, but it's still worth verifying).--color-surfacebg + 1px--color-border. This is the default card pattern — correct.--green-lightborder +--shadow-raised. Confirm that--shadow-raisedis appropriate here and not just--shadow-card. The spec says "interactive surfaces" get--shadow-raised.rgba(242,193,46,0.25)is a hardcoded color value, not a design token. Should this be--yellow-tintwith reduced opacity, or should I add a--yellow-badgetoken to the design system? I'd prefer a named token over a magic rgba.Typography non-negotiables:
VarietyScoreCard: Fraunces 28px (mobile/tablet) / 40px (desktop), weight 300. No weight above 600 anywhere — this is fine at 300.Layout questions:
Accessibility:
role="tab"orrole="button"witharia-selectedand keyboard navigation (arrow keys between days). Tab-based navigation pattern fits this interaction model.VarietyScoreCardshould berole="progressbar"witharia-valuenow,aria-valuemin="0",aria-valuemax="10".aria-label="Add meal for [day]"— not just "+".Overall the spec is thorough. My main open item is the sticky vs. scrolling behavior of the variety banner on mobile — that's a design decision with real UX implications and it needs to be explicit before Kai builds it.