Frontend: C1 — Weekly planner (home screen) #26

Closed
opened 2026-04-02 11:28:30 +02:00 by marcel · 6 comments
Owner

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-tint bg, --yellow-light 1px border, --radius-lg
  • Score: Fraunces 28px (mobile/tablet) / 40px (desktop) weight 300
  • "/10" denominator in smaller DM Sans muted
  • Progress bar: 3–5px height, --yellow-light track, --yellow fill
  • Warning row: "⚠ [ingredient] in N meals" in --yellow-text
  • Desktop: "Review variety →" link to C3

Mobile (< 768px)

5-region vertical stack:

  1. Top nav (sticky): "This week" title (Fraunces 20px) + prev/next/add buttons
  2. Variety banner: always visible, never collapses
  3. Day strip: 7-column grid (repeat(7, 1fr), 2–3px gap)
    • Chip: day abbreviation (7px uppercase) + date (11px) + dot indicator (3px circle)
    • Dot: no meal = --color-border | has meal = --green | today = --yellow-text
    • Today chip: --yellow-tint bg + --yellow-light border
    • Selected chip: --green-tint bg + --green-light border
  4. Selected day card: meal name (Fraunces 20px), tags, 2 buttons ("Cook now" primary, "Swap" ghost). Today card: 2px solid yellow border + --yellow-tint bg
  5. Remaining days list: only days after selected, scroll area. Rows: date column (26–36px) + meal info + swap button. Empty slots not shown (only grey dot in strip)

Tablet (768px–1024px)

Same stack but wider:

  • Day strip: 5–6px gap, 3-letter abbreviations (Mon, Tue), 14px date, 10px radius chips
  • Selected card: 2-column (info left, 3 buttons right: Cook/View/Swap, min-width 120px)
  • Description text visible (13px muted) — new on tablet
  • Remaining days: 2-column CSS grid, 8px gap, card style with arrow icon
  • Empty slots: dashed border + "+" icon
  • Variety banner: adds 4px progress bar
  • Nav: static inline pills (not fixed)

Desktop (> 1024px)

Completely different — 3-panel horizontal model:

  • Left sidebar (224px): sticky, 100vh, variety widget at bottom
  • Main calendar (flex:1): 7-column grid (repeat(7, 1fr), 8px gap), 20px padding. Only panel that scrolls.
    • Column headers: 9px day name (uppercase muted) + 24×24px date badge
    • Today column: yellow date badge + 2px yellow bottom border
    • Selected column: green-tint date badge + 2px green bottom border
    • Meal tiles: 8px 8px 10px padding, --radius-lg, flex:1 to fill height
      • At rest: --color-surface bg, 1px --color-border, --shadow-card
      • Hover: --green-light border, --shadow-raised
      • Today: 2px yellow border + --yellow-tint bg
      • Selected: 2px green border + --green-tint bg
      • Ingredient repeat badge: rgba(242,193,46,0.25) bg + --yellow-text
    • Empty tiles: 1px dashed border, transparent bg, centered "+" icon + "Add meal" label
  • Right detail panel (280px): sticky, 100vh
    • Header: day name (Fraunces 16px) + date + "dinner"
    • Meal info: name (Fraunces 17px), tags, 3 buttons (View recipe / Cook mode / Swap meal)
    • Ingredients list: 8px eyebrow label + rows (11px). Repeat ingredients: both name and qty in --yellow-text
  • Topbar: "Weekly planner" (Fraunces 20px) + prev/next week + week range + "Today" button + "+ Add meal" button

Role Behavior

  • Planner: "+ Add meal", "Swap", edit actions all visible
  • Household member: all action buttons hidden, read-only view

Acceptance Criteria

  • Variety score visible on all breakpoints without extra navigation
  • Mobile: day strip + selected card + remaining list
  • Tablet: wider strip + 2-col card + 2-col remaining grid
  • Desktop: 3-panel with 7-column calendar grid + detail panel
  • Today highlight: yellow treatment on all breakpoints
  • Selected day: green treatment on all breakpoints
  • Ingredient repeat warnings visible on tiles and in detail panel
  • Household members see read-only view (no action buttons)
  • Week navigation (prev/next/today)
## 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-tint` bg, `--yellow-light` 1px border, `--radius-lg` - Score: Fraunces 28px (mobile/tablet) / 40px (desktop) weight 300 - "/10" denominator in smaller DM Sans muted - Progress bar: 3–5px height, `--yellow-light` track, `--yellow` fill - Warning row: "⚠ [ingredient] in N meals" in `--yellow-text` - Desktop: "Review variety →" link to C3 ## Mobile (< 768px) 5-region vertical stack: 1. **Top nav** (sticky): "This week" title (Fraunces 20px) + prev/next/add buttons 2. **Variety banner**: always visible, never collapses 3. **Day strip**: 7-column grid (repeat(7, 1fr), 2–3px gap) - Chip: day abbreviation (7px uppercase) + date (11px) + dot indicator (3px circle) - Dot: no meal = `--color-border` | has meal = `--green` | today = `--yellow-text` - Today chip: `--yellow-tint` bg + `--yellow-light` border - Selected chip: `--green-tint` bg + `--green-light` border 4. **Selected day card**: meal name (Fraunces 20px), tags, 2 buttons ("Cook now" primary, "Swap" ghost). Today card: 2px solid yellow border + `--yellow-tint` bg 5. **Remaining days list**: only days after selected, scroll area. Rows: date column (26–36px) + meal info + swap button. Empty slots not shown (only grey dot in strip) ## Tablet (768px–1024px) Same stack but wider: - Day strip: 5–6px gap, 3-letter abbreviations (Mon, Tue), 14px date, 10px radius chips - Selected card: 2-column (info left, 3 buttons right: Cook/View/Swap, min-width 120px) - Description text visible (13px muted) — new on tablet - Remaining days: 2-column CSS grid, 8px gap, card style with arrow icon - Empty slots: dashed border + "+" icon - Variety banner: adds 4px progress bar - Nav: static inline pills (not fixed) ## Desktop (> 1024px) Completely different — 3-panel horizontal model: - **Left sidebar** (224px): sticky, 100vh, variety widget at bottom - **Main calendar** (flex:1): 7-column grid (repeat(7, 1fr), 8px gap), 20px padding. Only panel that scrolls. - Column headers: 9px day name (uppercase muted) + 24×24px date badge - Today column: yellow date badge + 2px yellow bottom border - Selected column: green-tint date badge + 2px green bottom border - Meal tiles: 8px 8px 10px padding, `--radius-lg`, flex:1 to fill height - At rest: `--color-surface` bg, 1px `--color-border`, `--shadow-card` - Hover: `--green-light` border, `--shadow-raised` - Today: 2px yellow border + `--yellow-tint` bg - Selected: 2px green border + `--green-tint` bg - Ingredient repeat badge: `rgba(242,193,46,0.25)` bg + `--yellow-text` - Empty tiles: 1px dashed border, transparent bg, centered "+" icon + "Add meal" label - **Right detail panel** (280px): sticky, 100vh - Header: day name (Fraunces 16px) + date + "dinner" - Meal info: name (Fraunces 17px), tags, 3 buttons (View recipe / Cook mode / Swap meal) - Ingredients list: 8px eyebrow label + rows (11px). Repeat ingredients: both name and qty in `--yellow-text` - **Topbar**: "Weekly planner" (Fraunces 20px) + prev/next week + week range + "Today" button + "+ Add meal" button ## Role Behavior - **Planner**: "+ Add meal", "Swap", edit actions all visible - **Household member**: all action buttons hidden, read-only view ## Acceptance Criteria - [ ] Variety score visible on all breakpoints without extra navigation - [ ] Mobile: day strip + selected card + remaining list - [ ] Tablet: wider strip + 2-col card + 2-col remaining grid - [ ] Desktop: 3-panel with 7-column calendar grid + detail panel - [ ] Today highlight: yellow treatment on all breakpoints - [ ] Selected day: green treatment on all breakpoints - [ ] Ingredient repeat warnings visible on tiles and in detail panel - [ ] Household members see read-only view (no action buttons) - [ ] Week navigation (prev/next/today)
marcel added the kind/featurepriority/high labels 2026-04-02 11:30:10 +02:00
Author
Owner

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.

**Spec file:** [`specs/frontend/j2-plan-the-week.html`](../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.
Author
Owner

👨‍💻 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:

  • The issue references a 50+ row agent table in the spec file. I'll treat that as the source of truth for exact pixel values — but if there's any conflict between the spec HTML and this issue text, which wins?
  • For the 3-panel desktop layout: are the left sidebar and right detail panel position: sticky within a scrolling main column, or is it a CSS grid with height: 100vh on the outer container and overflow-y: auto only on the center column? This determines whether I use grid or flex at the top level.
  • Day strip: the dot color for "today" uses --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:

  • Week data (all 7 meal slots) needs to load server-side in +page.server.ts. What's the API shape — one call for the full week, or 7 individual meal-slot calls?
  • Week navigation (prev/next) — is this a full SvelteKit navigation with a ?week= search param, or a client-side state update with a subsequent fetch? The spec doesn't say, and it affects how I wire the PlannerHeader buttons.
  • The variety score: does it come embedded in the week response, or is it a separate endpoint? If separate, does it need to update reactively as meals are swapped (client-side)?

Role behavior:

  • Member read-only: I'll derive visibility from the session in +page.server.ts and pass a readonly: boolean prop down. I won't scatter auth checks across individual components. Is member the only restricted role, or are there others I need to handle here?

Reactivity:

  • Selected day state is clearly client-side ($state<number> for day index). The remaining days list is $derived from that. No SSR needed for the selection — correct?
  • Ingredient repeat warnings in tiles: does the backend compute these and send them in the week response, or do I compute them client-side from the ingredient lists?

Svelte 5 notes:

  • Day strip will use a keyed {#each} over the 7 days — straightforward.
  • The hover state on desktop tiles (--green-light border, --shadow-raised) is pure CSS, no JS state needed.
  • Wake lock is not needed here (that's B4) — confirmed.

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.

## 👨‍💻 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:** - The issue references a 50+ row agent table in the spec file. I'll treat that as the source of truth for exact pixel values — but if there's any conflict between the spec HTML and this issue text, which wins? - For the 3-panel desktop layout: are the left sidebar and right detail panel `position: sticky` within a scrolling main column, or is it a CSS grid with `height: 100vh` on the outer container and `overflow-y: auto` only on the center column? This determines whether I use `grid` or `flex` at the top level. - Day strip: the dot color for "today" uses `--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:** - Week data (all 7 meal slots) needs to load server-side in `+page.server.ts`. What's the API shape — one call for the full week, or 7 individual meal-slot calls? - Week navigation (prev/next) — is this a full SvelteKit navigation with a `?week=` search param, or a client-side state update with a subsequent fetch? The spec doesn't say, and it affects how I wire the `PlannerHeader` buttons. - The variety score: does it come embedded in the week response, or is it a separate endpoint? If separate, does it need to update reactively as meals are swapped (client-side)? **Role behavior:** - Member read-only: I'll derive visibility from the session in `+page.server.ts` and pass a `readonly: boolean` prop down. I won't scatter auth checks across individual components. Is `member` the only restricted role, or are there others I need to handle here? **Reactivity:** - Selected day state is clearly client-side (`$state<number>` for day index). The remaining days list is `$derived` from that. No SSR needed for the selection — correct? - Ingredient repeat warnings in tiles: does the backend compute these and send them in the week response, or do I compute them client-side from the ingredient lists? **Svelte 5 notes:** - Day strip will use a keyed `{#each}` over the 7 days — straightforward. - The hover state on desktop tiles (`--green-light` border, `--shadow-raised`) is pure CSS, no JS state needed. - Wake lock is not needed here (that's B4) — confirmed. 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.
Author
Owner

🔧 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.

  • What's the canonical week identifier? ISO week number + year? A Monday date? We need to agree before I design the route.
  • Should empty slots be returned as null entries (so the client always gets exactly 7 items) or omitted? The spec shows empty slots differently on mobile vs desktop — having them explicit as null simplifies frontend logic considerably.

Variety score:

  • Does the variety score live in the week response or on a dedicated endpoint? I'd lean toward embedding it in the week response to avoid a second round-trip on load.
  • What exactly feeds the score? The issue mentions "ingredient repeat warnings" in tiles. Is the algorithm server-side only, or does the frontend receive the raw ingredient overlap data and render its own warnings?
  • The warning format "⚠ [ingredient] in N meals" implies the backend sends structured data (ingredient name + count), not a pre-formatted string. Confirm?

Role enforcement:

  • The 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 isolation: fetching week data must be scoped to the authenticated user's household. A member of household A must never receive household B's plan. I'd add a household_id filter on every query — this needs to be in the repository layer, not optional.

Week navigation:

  • Prev/next week: is this just a different URL param hitting the same endpoint, or does the backend need to support a "relative" navigation concept (e.g., ?offset=-1)? I'd keep it simple: absolute ISO week param, client builds the next/prev values.

Data integrity:

  • When a meal is assigned to a slot, is there a UNIQUE constraint on (household_id, planned_date)? If a planner can only assign one dinner per day, the DB should enforce it, not just the application.
  • cooking_log is 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/weeks response schema if we align on the above first.

## 🔧 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. - What's the canonical week identifier? ISO week number + year? A Monday date? We need to agree before I design the route. - Should empty slots be returned as `null` entries (so the client always gets exactly 7 items) or omitted? The spec shows empty slots differently on mobile vs desktop — having them explicit as `null` simplifies frontend logic considerably. **Variety score:** - Does the variety score live in the week response or on a dedicated endpoint? I'd lean toward embedding it in the week response to avoid a second round-trip on load. - What exactly feeds the score? The issue mentions "ingredient repeat warnings" in tiles. Is the algorithm server-side only, or does the frontend receive the raw ingredient overlap data and render its own warnings? - The warning format "⚠ [ingredient] in N meals" implies the backend sends structured data (ingredient name + count), not a pre-formatted string. Confirm? **Role enforcement:** - The `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 isolation: fetching week data must be scoped to the authenticated user's household. A member of household A must never receive household B's plan. I'd add a `household_id` filter on every query — this needs to be in the repository layer, not optional. **Week navigation:** - Prev/next week: is this just a different URL param hitting the same endpoint, or does the backend need to support a "relative" navigation concept (e.g., `?offset=-1`)? I'd keep it simple: absolute ISO week param, client builds the next/prev values. **Data integrity:** - When a meal is assigned to a slot, is there a UNIQUE constraint on `(household_id, planned_date)`? If a planner can only assign one dinner per day, the DB should enforce it, not just the application. - `cooking_log` is 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/weeks` response schema if we align on the above first.
Author
Owner

🧪 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:

  • Full week with all 7 slots filled: variety score visible, all tiles rendered, today highlighted in yellow, selected day in green
  • Partial week (some slots empty): empty slots handled correctly at each breakpoint (hidden on mobile list, dashed border on tablet/desktop)
  • Member role: all action buttons absent, page otherwise identical to planner view
  • Week navigation: prev/next loads the correct week's data, "Today" button snaps back to current week

Bad paths / edge cases I'm already thinking about:

  • Empty week (no meals planned at all): variety score = 0, all slots empty — does the UI gracefully handle a week array of all-nulls?
  • Today is in a past or future week that the user navigated to: is "today" still highlighted, or does the highlight only appear when today's week is loaded?
  • Week spanning a year boundary (e.g., Dec 29 – Jan 4): does the week identifier handle this correctly? ISO week 53 → week 1 edge case.
  • Very long recipe names in mobile day card (Fraunces 20px): does the text truncate, or does it push the layout?
  • Ingredient repeat warning: what's the threshold for showing the warning badge? Is it ≥2 occurrences? What if the same ingredient appears in all 7 meals?
  • Member user navigating directly to C1: should see read-only view immediately — no flash of planner controls.

Component test checklist (Svelte):

  • VarietyScoreCard: renders score, progress bar width matches percentage, warning rows appear for repeated ingredients, "Review variety →" link only visible on desktop
  • WeekStrip: 7 chips always rendered, correct dot color per slot state (empty/filled/today), today chip has yellow treatment, selected chip has green treatment
  • DayMealCard: "Cook now" and "Swap" hidden when readonly=true
  • CalendarGrid (desktop): empty tile shows dashed border + "+" icon, hover state applies correctly

Integration tests:

  • GET /api/weeks/{year}/{week} as planner → 200 with full data
  • Same endpoint as member → 200 (read-only data, no difference in response, role enforced on write endpoints)
  • Same endpoint as unauthenticated → 401
  • Same endpoint with a different household's week ID → 403 (IDOR check)

E2E:

  • Critical journey: load C1, navigate to next week, select a day, verify selected day card updates — @critical
  • Member user: log in as member, load C1, confirm no "Swap" or "Add meal" buttons are in the DOM at all

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?

## 🧪 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:** - Full week with all 7 slots filled: variety score visible, all tiles rendered, today highlighted in yellow, selected day in green - Partial week (some slots empty): empty slots handled correctly at each breakpoint (hidden on mobile list, dashed border on tablet/desktop) - Member role: all action buttons absent, page otherwise identical to planner view - Week navigation: prev/next loads the correct week's data, "Today" button snaps back to current week **Bad paths / edge cases I'm already thinking about:** - Empty week (no meals planned at all): variety score = 0, all slots empty — does the UI gracefully handle a week array of all-nulls? - Today is in a past or future week that the user navigated to: is "today" still highlighted, or does the highlight only appear when today's week is loaded? - Week spanning a year boundary (e.g., Dec 29 – Jan 4): does the week identifier handle this correctly? ISO week 53 → week 1 edge case. - Very long recipe names in mobile day card (Fraunces 20px): does the text truncate, or does it push the layout? - Ingredient repeat warning: what's the threshold for showing the warning badge? Is it ≥2 occurrences? What if the same ingredient appears in all 7 meals? - Member user navigating directly to C1: should see read-only view immediately — no flash of planner controls. **Component test checklist (Svelte):** - `VarietyScoreCard`: renders score, progress bar width matches percentage, warning rows appear for repeated ingredients, "Review variety →" link only visible on desktop - `WeekStrip`: 7 chips always rendered, correct dot color per slot state (empty/filled/today), today chip has yellow treatment, selected chip has green treatment - `DayMealCard`: "Cook now" and "Swap" hidden when `readonly=true` - `CalendarGrid` (desktop): empty tile shows dashed border + "+" icon, hover state applies correctly **Integration tests:** - `GET /api/weeks/{year}/{week}` as planner → 200 with full data - Same endpoint as member → 200 (read-only data, no difference in response, role enforced on write endpoints) - Same endpoint as unauthenticated → 401 - Same endpoint with a different household's week ID → 403 (IDOR check) **E2E:** - Critical journey: load C1, navigate to next week, select a day, verify selected day card updates — @critical - Member user: log in as member, load C1, confirm no "Swap" or "Add meal" buttons are in the DOM at all 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?
Author
Owner

🔒 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:

  • The week data API must scope every query by household_id derived from the authenticated session — never from a URL parameter or request body the client controls. A planner navigating to /planner?week=2026-W14 must only ever see their own household's data, regardless of what week they request.
  • Member role enforcement: the issue says members see a read-only view. I want to confirm: is the role check happening server-side in +page.server.ts (returning readonly: true in 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.
  • Specific question: can a member call POST /api/plan or DELETE /api/plan/{id} directly? These must return 403, verified by an integration test.

IDOR risk:

  • The meal slot and recipe IDs visible in the week response — are these UUIDs or sequential integers? Sequential IDs on a multi-tenant app are a classic IDOR vector. If they're integers, a member of household A can guess household B's plan IDs.
  • The "Swap" action (J4) will presumably reference a plan_id — confirm that the backend validates plan_id belongs to the requesting user's household before executing any swap.

Information leakage:

  • The ingredient repeat warnings expose data about what's in the plan. For the read-only member view, is this information appropriate to show? I'm assuming yes (it's their household too), but worth confirming the intended access model.
  • Error responses from the week API should not expose internal identifiers, SQL errors, or stack traces. Especially important for the "wrong household" 403 case — the response should be generic, not "household 42 not found."

Week navigation:

  • Prev/next week buttons could theoretically be used to enumerate other weeks and check for data. This is fine as long as household scoping is enforced. No additional concern there.

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.

## 🔒 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:** - The week data API must scope every query by `household_id` derived from the authenticated session — never from a URL parameter or request body the client controls. A planner navigating to `/planner?week=2026-W14` must only ever see their own household's data, regardless of what week they request. - Member role enforcement: the issue says members see a read-only view. I want to confirm: is the role check happening server-side in `+page.server.ts` (returning `readonly: true` in 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. - Specific question: can a member call `POST /api/plan` or `DELETE /api/plan/{id}` directly? These must return 403, verified by an integration test. **IDOR risk:** - The meal slot and recipe IDs visible in the week response — are these UUIDs or sequential integers? Sequential IDs on a multi-tenant app are a classic IDOR vector. If they're integers, a member of household A can guess household B's plan IDs. - The "Swap" action (J4) will presumably reference a `plan_id` — confirm that the backend validates `plan_id` belongs to the requesting user's household before executing any swap. **Information leakage:** - The ingredient repeat warnings expose data about what's in the plan. For the read-only member view, is this information appropriate to show? I'm assuming yes (it's their household too), but worth confirming the intended access model. - Error responses from the week API should not expose internal identifiers, SQL errors, or stack traces. Especially important for the "wrong household" 403 case — the response should be generic, not "household 42 not found." **Week navigation:** - Prev/next week buttons could theoretically be used to enumerate other weeks and check for data. This is fine as long as household scoping is enforced. No additional concern there. **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.
Author
Owner

🎨 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:

  • The acceptance criterion says "visible on all breakpoints without extra navigation." On mobile, the variety banner is region 2 in the stack — it will scroll out of view as the user scrolls down through the day list. Should it be position: sticky below 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:

  • Day strip dot colors: "today = --yellow-text" on a chip that has --yellow-tint bg. --yellow-text on --yellow-tint needs 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).
  • Meal tiles at rest: --color-surface bg + 1px --color-border. This is the default card pattern — correct.
  • Hover state: --green-light border + --shadow-raised. Confirm that --shadow-raised is appropriate here and not just --shadow-card. The spec says "interactive surfaces" get --shadow-raised.
  • Desktop ingredient repeat badge: rgba(242,193,46,0.25) is a hardcoded color value, not a design token. Should this be --yellow-tint with reduced opacity, or should I add a --yellow-badge token to the design system? I'd prefer a named token over a magic rgba.

Typography non-negotiables:

  • Score in VarietyScoreCard: Fraunces 28px (mobile/tablet) / 40px (desktop), weight 300. No weight above 600 anywhere — this is fine at 300.
  • Step numbers (Fraunces) vs. body text (DM Sans) split is correct throughout.
  • Button text: 13px, weight 500, tracking 0.04em — applies to "Cook now", "Swap", "View recipe", "+ Add meal". Kai should use the shared button utility, not re-specify these per component.

Layout questions:

  • Mobile: the "Selected day card" and the "Remaining days list" — is there a defined maximum height on the card before the remaining list begins, or does the card grow freely and push the list down?
  • Desktop right detail panel (280px): the ingredients list says "11px" rows. That's below our comfortable reading size for body text. Is this intentional given the panel is supplementary information, or should it be 13px (text-sm)?
  • The "Today" button in the desktop topbar: what's the visual state when the user is already on the current week? Disabled? Active/highlighted? The spec should define this.

Accessibility:

  • The 7-column day strip chips need role="tab" or role="button" with aria-selected and keyboard navigation (arrow keys between days). Tab-based navigation pattern fits this interaction model.
  • The progress bar in VarietyScoreCard should be role="progressbar" with aria-valuenow, aria-valuemin="0", aria-valuemax="10".
  • Empty meal tiles with a "+" button: the "+" needs an 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.

## 🎨 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:** - The acceptance criterion says "visible on all breakpoints without extra navigation." On mobile, the variety banner is region 2 in the stack — it will scroll out of view as the user scrolls down through the day list. Should it be `position: sticky` below 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:** - Day strip dot colors: "today = `--yellow-text`" on a chip that has `--yellow-tint` bg. `--yellow-text` on `--yellow-tint` needs 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). - Meal tiles at rest: `--color-surface` bg + 1px `--color-border`. This is the default card pattern — correct. - Hover state: `--green-light` border + `--shadow-raised`. Confirm that `--shadow-raised` is appropriate here and not just `--shadow-card`. The spec says "interactive surfaces" get `--shadow-raised`. - Desktop ingredient repeat badge: `rgba(242,193,46,0.25)` is a hardcoded color value, not a design token. Should this be `--yellow-tint` with reduced opacity, or should I add a `--yellow-badge` token to the design system? I'd prefer a named token over a magic rgba. **Typography non-negotiables:** - Score in `VarietyScoreCard`: Fraunces 28px (mobile/tablet) / 40px (desktop), weight 300. No weight above 600 anywhere — this is fine at 300. - Step numbers (Fraunces) vs. body text (DM Sans) split is correct throughout. - Button text: 13px, weight 500, tracking 0.04em — applies to "Cook now", "Swap", "View recipe", "+ Add meal". Kai should use the shared button utility, not re-specify these per component. **Layout questions:** - Mobile: the "Selected day card" and the "Remaining days list" — is there a defined maximum height on the card before the remaining list begins, or does the card grow freely and push the list down? - Desktop right detail panel (280px): the ingredients list says "11px" rows. That's below our comfortable reading size for body text. Is this intentional given the panel is supplementary information, or should it be 13px (text-sm)? - The "Today" button in the desktop topbar: what's the visual state when the user is already on the current week? Disabled? Active/highlighted? The spec should define this. **Accessibility:** - The 7-column day strip chips need `role="tab"` or `role="button"` with `aria-selected` and keyboard navigation (arrow keys between days). Tab-based navigation pattern fits this interaction model. - The progress bar in `VarietyScoreCard` should be `role="progressbar"` with `aria-valuenow`, `aria-valuemin="0"`, `aria-valuemax="10"`. - Empty meal tiles with a "+" button: the "+" needs an `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.
Sign in to join this conversation.