feat(planner): C1 — Weekly planner home screen #39

Merged
marcel merged 2 commits from feat/issue-26-weekly-planner into master 2026-04-03 11:07:57 +02:00
Owner

Summary

Implements issue #26: the weekly planner home screen (C1) with three-breakpoint layout.

  • Mobile/tablet: sticky top nav, variety banner, 7-day strip with dot indicators, selected day card, remaining days list
  • Desktop: 3-panel layout — left sidebar (224px, variety widget at bottom), main 7-column calendar grid, right detail panel (280px)
  • VarietyScoreCard: score + progress bar (role=progressbar with aria attributes) + ingredient overlap warnings + optional review link
  • WeekStrip: 7 chips with today/selected/meal-present states using design tokens
  • DayMealCard: recipe display with action buttons hidden for readonly (household member) mode
  • Week navigation: prev/next/today via URL ?week= param, full SvelteKit navigation
  • Server: loads week plan + variety score from API, returns null for 404 (no plan yet)
  • Actions: createPlan POST action for initializing a new week

Test plan

  • 9 server tests (page.server.test.ts) covering load, URL param, variety score, 404 handling, createPlan action
  • 7 VarietyScoreCard tests: score rendering, progress bar aria, overlap warnings, review link visibility
  • 6 WeekStrip tests: 7 chips, today/selected states, dot indicators, selectDay callback
  • 6 DayMealCard tests: recipe display, readonly hiding, today styling, empty state, metadata
  • All 373 tests pass, 0 type errors

Closes #26

🤖 Generated with Claude Code

## Summary Implements issue #26: the weekly planner home screen (C1) with three-breakpoint layout. - **Mobile/tablet**: sticky top nav, variety banner, 7-day strip with dot indicators, selected day card, remaining days list - **Desktop**: 3-panel layout — left sidebar (224px, variety widget at bottom), main 7-column calendar grid, right detail panel (280px) - **VarietyScoreCard**: score + progress bar (role=progressbar with aria attributes) + ingredient overlap warnings + optional review link - **WeekStrip**: 7 chips with today/selected/meal-present states using design tokens - **DayMealCard**: recipe display with action buttons hidden for `readonly` (household member) mode - **Week navigation**: prev/next/today via URL `?week=` param, full SvelteKit navigation - **Server**: loads week plan + variety score from API, returns `null` for 404 (no plan yet) - **Actions**: `createPlan` POST action for initializing a new week ## Test plan - [x] 9 server tests (`page.server.test.ts`) covering load, URL param, variety score, 404 handling, createPlan action - [x] 7 `VarietyScoreCard` tests: score rendering, progress bar aria, overlap warnings, review link visibility - [x] 6 `WeekStrip` tests: 7 chips, today/selected states, dot indicators, selectDay callback - [x] 6 `DayMealCard` tests: recipe display, readonly hiding, today styling, empty state, metadata - [x] All 373 tests pass, 0 type errors Closes #26 🤖 Generated with [Claude Code](https://claude.com/claude-code)
marcel added 1 commit 2026-04-03 11:01:37 +02:00
Three-breakpoint layout (mobile/tablet/desktop) with VarietyScoreCard,
WeekStrip, DayMealCard components. Server loads week plan and variety
score via API; read-only role behavior derived from benutzer.rolle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

👨‍💻 Kai — Frontend Engineer

Verdict: ⚠️ Approved with concerns

Good overall structure — Svelte 5 runes used correctly, $derived for computed values, $effect for the week-change side effect, callback prop instead of createEventDispatcher. TDD discipline is visible. But I have blockers.

Blockers

1. Logic bug in desktop detail panel — {#if !isPlanner === false}
+page.svelte line ~270: {#if !isPlanner === false} evaluates as (!isPlanner) === false, which simplifies to isPlanner. So the block shows only for planners. But it wraps "Rezept ansehen" and "Koch-Modus" — those are read-only actions that members should also see. This is a logic inversion bug that means members on desktop see no buttons at all when a recipe is selected.

Fix: replace with {#if true} or just remove the conditional wrapper. Keep only the {#if isPlanner} guard around "Gericht tauschen".

2. Tauschen button has no action
DayMealCard.svelte: The "Tauschen" button has type="button" but no onclick handler and no href. It renders but does nothing. Should navigate to /planner/suggestions?day={slot.slotDate} or emit a callback. Leaving it as a dead button is a UX bug and confuses tests that assert it's present.

3. Missing $lib/planner/week.ts tests
week.ts exports 7 utility functions (getWeekStart, prevWeek, nextWeek, formatDayAbbr, weekDays, formatDayLabel, formatDayFull, isToday, formatWeekRange). None are unit-tested. getWeekStart specifically has a Sunday edge case (day === 0 ? -6 : 1 - day) that needs a test. These are pure functions — they're trivial to test and the logic is non-trivial.

4. isToday in week.ts uses local time but the rest uses UTC
isToday builds todayStr from new Date() using local getFullYear/getMonth/getDate — local timezone. All other functions use UTC (T00:00:00Z + timeZone: 'UTC'). A user in UTC+2 at 23:30 UTC would have a different "today" from the strip. Use UTC consistently or document the intentional mismatch.

5. (data as any).benutzer type cast
The layout data type isn't threaded into the page type. The any cast hides a type error and will break if locals.benutzer is renamed. Use PageData from ./$types and extend it, or access via $page.data which includes the layout data.

Suggestions

  • remainingSlots.filter((s: any) => s.recipe) is called twice in the template with any cast. Extract to a $derived variable and type it properly.
  • Desktop column header shows date number only — missing the day name abbreviation that the spec calls for ("9px day name (uppercase muted)"). The date badge renders but there's no day-name row above it.
  • Left sidebar content area says "Navigation" as a placeholder <p> — fine for now but this should be a comment or removed entirely, not rendered text.
  • isToday is imported from week.ts in the page script but the derived today string is computed inline with an IIFE — redundant. Use isToday consistently.
## 👨‍💻 Kai — Frontend Engineer **Verdict: ⚠️ Approved with concerns** Good overall structure — Svelte 5 runes used correctly, `$derived` for computed values, `$effect` for the week-change side effect, callback prop instead of `createEventDispatcher`. TDD discipline is visible. But I have blockers. ### Blockers **1. Logic bug in desktop detail panel — `{#if !isPlanner === false}`** `+page.svelte` line ~270: `{#if !isPlanner === false}` evaluates as `(!isPlanner) === false`, which simplifies to `isPlanner`. So the block shows only for planners. But it wraps "Rezept ansehen" and "Koch-Modus" — those are read-only actions that *members* should also see. This is a logic inversion bug that means members on desktop see no buttons at all when a recipe is selected. Fix: replace with `{#if true}` or just remove the conditional wrapper. Keep only the `{#if isPlanner}` guard around "Gericht tauschen". **2. `Tauschen` button has no action** `DayMealCard.svelte`: The "Tauschen" button has `type="button"` but no `onclick` handler and no `href`. It renders but does nothing. Should navigate to `/planner/suggestions?day={slot.slotDate}` or emit a callback. Leaving it as a dead button is a UX bug and confuses tests that assert it's present. **3. Missing `$lib/planner/week.ts` tests** `week.ts` exports 7 utility functions (`getWeekStart`, `prevWeek`, `nextWeek`, `formatDayAbbr`, `weekDays`, `formatDayLabel`, `formatDayFull`, `isToday`, `formatWeekRange`). None are unit-tested. `getWeekStart` specifically has a Sunday edge case (`day === 0 ? -6 : 1 - day`) that needs a test. These are pure functions — they're trivial to test and the logic is non-trivial. **4. `isToday` in `week.ts` uses local time but the rest uses UTC** `isToday` builds `todayStr` from `new Date()` using local `getFullYear/getMonth/getDate` — local timezone. All other functions use UTC (`T00:00:00Z` + `timeZone: 'UTC'`). A user in UTC+2 at 23:30 UTC would have a different "today" from the strip. Use UTC consistently or document the intentional mismatch. **5. `(data as any).benutzer` type cast** The layout data type isn't threaded into the page type. The `any` cast hides a type error and will break if `locals.benutzer` is renamed. Use `PageData` from `./$types` and extend it, or access via `$page.data` which includes the layout data. ### Suggestions - `remainingSlots.filter((s: any) => s.recipe)` is called twice in the template with `any` cast. Extract to a `$derived` variable and type it properly. - Desktop column header shows date number only — missing the day name abbreviation that the spec calls for ("9px day name (uppercase muted)"). The date badge renders but there's no day-name row above it. - Left sidebar content area says "Navigation" as a placeholder `<p>` — fine for now but this should be a comment or removed entirely, not rendered text. - `isToday` is imported from `week.ts` in the page script but the derived `today` string is computed inline with an IIFE — redundant. Use `isToday` consistently.
Author
Owner

🧪 QA Engineer

Verdict: 🚫 Changes requested

Solid test setup overall — I can see TDD was practiced: server tests, component tests for all three new components. But there are critical gaps that must be closed before merge.

Blockers

1. week.ts has zero tests — 9 exported functions, 0 coverage
getWeekStart, prevWeek, nextWeek, weekDays, formatDayLabel, formatDayFull, isToday, formatWeekRange, formatDayAbbr are all untested. These are pure utility functions driving the entire planner — week navigation, today detection, day strip rendering. At minimum I need tests for:

  • getWeekStart with a Sunday input (the -6 edge case)
  • getWeekStart with a Monday input (no shift)
  • prevWeek / nextWeek week boundary correctness
  • weekDays returns exactly 7 entries, starting on weekStart
  • isToday returns true for today, false for yesterday/tomorrow

2. DayMealCard: "Tauschen" button has no action — test is incomplete
The test asserts the button is present but doesn't test what happens when clicked (because nothing happens). A dead button that passes a presence test is not a behavior test. Either: (a) add an onswap callback prop and test it's called, or (b) convert the button to an <a> with href and test the destination URL.

3. No test for WeekStrip with today AND selected being the same day
When today is the selected day, both data-today and data-selected are true on the same chip. The CSS classes are applied conditionally with && !isTodayDay guards. Need a test asserting both attributes are set correctly when today === selectedDay.

4. No test for VarietyScoreCard with multiple overlaps
Only one overlap is tested. The {#each} loop behavior with 0, 1, and N items needs coverage. Particularly: does the list render correctly with 3+ overlapping ingredients?

5. createPlan action: no test for missing weekStart in formData
If formData.get('weekStart') returns null, it's cast to string and sent as "null" to the API. This needs a validation test — what happens when the form field is absent? The action should validate the input and return a 400-equivalent error, not silently send garbage to the backend.

6. Load function: no test for variety score API failure (partial failure)
The server test covers 404 on the week plan, but what happens if the week plan loads successfully (200) and the variety score fetch fails? Currently the code does varietyScore: varietyScore ?? null — which silently returns null. Need a test asserting that a failed variety score fetch still returns the weekPlan (not null).

Suggestions

  • Add a test for +page.svelte that verifies the isPlanner behavior — actions hidden for member, shown for planner. This is a critical acceptance criterion from the issue.
  • WeekStrip callback test uses chip.click() directly — prefer @testing-library/user-event userEvent.click() for more realistic simulation.
  • E2E: the week navigation (prev/next/today) is the top critical journey and has no E2E test. Even a Playwright smoke test covering load → click next → load new week would catch navigation regressions early.
## 🧪 QA Engineer **Verdict: 🚫 Changes requested** Solid test setup overall — I can see TDD was practiced: server tests, component tests for all three new components. But there are critical gaps that must be closed before merge. ### Blockers **1. `week.ts` has zero tests — 9 exported functions, 0 coverage** `getWeekStart`, `prevWeek`, `nextWeek`, `weekDays`, `formatDayLabel`, `formatDayFull`, `isToday`, `formatWeekRange`, `formatDayAbbr` are all untested. These are pure utility functions driving the entire planner — week navigation, today detection, day strip rendering. At minimum I need tests for: - `getWeekStart` with a Sunday input (the `-6` edge case) - `getWeekStart` with a Monday input (no shift) - `prevWeek` / `nextWeek` week boundary correctness - `weekDays` returns exactly 7 entries, starting on weekStart - `isToday` returns true for today, false for yesterday/tomorrow **2. `DayMealCard`: "Tauschen" button has no action — test is incomplete** The test asserts the button is present but doesn't test what happens when clicked (because nothing happens). A dead button that passes a presence test is not a behavior test. Either: (a) add an `onswap` callback prop and test it's called, or (b) convert the button to an `<a>` with `href` and test the destination URL. **3. No test for `WeekStrip` with today AND selected being the same day** When today is the selected day, both `data-today` and `data-selected` are true on the same chip. The CSS classes are applied conditionally with `&& !isTodayDay` guards. Need a test asserting both attributes are set correctly when today === selectedDay. **4. No test for `VarietyScoreCard` with multiple overlaps** Only one overlap is tested. The `{#each}` loop behavior with 0, 1, and N items needs coverage. Particularly: does the list render correctly with 3+ overlapping ingredients? **5. `createPlan` action: no test for missing `weekStart` in formData** If `formData.get('weekStart')` returns `null`, it's cast to string and sent as `"null"` to the API. This needs a validation test — what happens when the form field is absent? The action should validate the input and return a 400-equivalent error, not silently send garbage to the backend. **6. Load function: no test for variety score API failure (partial failure)** The server test covers 404 on the week plan, but what happens if the week plan loads successfully (200) and the variety score fetch fails? Currently the code does `varietyScore: varietyScore ?? null` — which silently returns null. Need a test asserting that a failed variety score fetch still returns the weekPlan (not null). ### Suggestions - Add a test for `+page.svelte` that verifies the `isPlanner` behavior — actions hidden for member, shown for planner. This is a critical acceptance criterion from the issue. - `WeekStrip` callback test uses `chip.click()` directly — prefer `@testing-library/user-event` `userEvent.click()` for more realistic simulation. - E2E: the week navigation (prev/next/today) is the top critical journey and has no E2E test. Even a Playwright smoke test covering load → click next → load new week would catch navigation regressions early.
Author
Owner

🔒 Sable — Security Engineer

Verdict: ⚠️ Approved with concerns

No XSS vectors — all user content is rendered as text, no {@html}. Server-side load is clean: uses SvelteKit's session-aware fetch, household scoping is enforced at the API layer (existing infrastructure). Role check is server-derived. No new attack surface. But two items need attention.

Blockers

1. createPlan action: weekStart from formData is not validated
+page.server.ts line 31: const weekStart = formData.get('weekStart') as string is passed directly to api.POST('/v1/week-plans', { body: { weekStart } }). No format validation. A user can submit any string — including a multi-thousand-character payload or a date like "2099-01-01".

Attack scenario: a member (read-only) who discovers this action endpoint can submit arbitrary weekStart values to probe the backend. The backend should reject invalid dates, but defense-in-depth requires the frontend to validate too. Add a regex check: /^\d{4}-\d{2}-\d{2}$/.test(weekStart) before the API call. If it fails, return { success: false, error: 'Ungültiges Datum.' }.

2. isPlanner role check is client-side only — no server guard on createPlan action
The createPlan action in +page.server.ts does not check whether the user is a planner before executing. A household member who crafts a POST to ?/createPlan would attempt to create a week plan. The backend will (should) reject this with 403, but there is no SvelteKit-layer guard.

Add at the top of createPlan: check locals.benutzer?.rolle === 'planer' and return { success: false, error: 'Keine Berechtigung.' } if not. Fail fast server-side, not just client-side via hidden UI.

Observations (no action needed)

  • Recipe IDs and plan IDs exposed in URLs (/recipes/{id}/cook) — these are UUIDs per the schema, acceptable.
  • goto() uses a developer-constructed URL /planner?week=${newWeekStart}newWeekStart is computed from trusted utility functions, not from user input. No open redirect risk.
  • Household scoping: confirmed to be handled at the API layer in hooks.server.ts via session-derived household_id. The planner page does not pass household ID as a parameter, which is correct.
  • No sensitive data visible in the page data — recipe names and slot IDs only.
## 🔒 Sable — Security Engineer **Verdict: ⚠️ Approved with concerns** No XSS vectors — all user content is rendered as text, no `{@html}`. Server-side load is clean: uses SvelteKit's session-aware `fetch`, household scoping is enforced at the API layer (existing infrastructure). Role check is server-derived. No new attack surface. But two items need attention. ### Blockers **1. `createPlan` action: `weekStart` from formData is not validated** `+page.server.ts` line 31: `const weekStart = formData.get('weekStart') as string` is passed directly to `api.POST('/v1/week-plans', { body: { weekStart } })`. No format validation. A user can submit any string — including a multi-thousand-character payload or a date like `"2099-01-01"`. Attack scenario: a member (read-only) who discovers this action endpoint can submit arbitrary `weekStart` values to probe the backend. The backend should reject invalid dates, but defense-in-depth requires the frontend to validate too. Add a regex check: `/^\d{4}-\d{2}-\d{2}$/.test(weekStart)` before the API call. If it fails, return `{ success: false, error: 'Ungültiges Datum.' }`. **2. `isPlanner` role check is client-side only — no server guard on `createPlan` action** The `createPlan` action in `+page.server.ts` does not check whether the user is a planner before executing. A household member who crafts a POST to `?/createPlan` would attempt to create a week plan. The backend will (should) reject this with 403, but there is no SvelteKit-layer guard. Add at the top of `createPlan`: check `locals.benutzer?.rolle === 'planer'` and return `{ success: false, error: 'Keine Berechtigung.' }` if not. Fail fast server-side, not just client-side via hidden UI. ### Observations (no action needed) - Recipe IDs and plan IDs exposed in URLs (`/recipes/{id}/cook`) — these are UUIDs per the schema, acceptable. - `goto()` uses a developer-constructed URL `/planner?week=${newWeekStart}` — `newWeekStart` is computed from trusted utility functions, not from user input. No open redirect risk. - Household scoping: confirmed to be handled at the API layer in `hooks.server.ts` via session-derived `household_id`. The planner page does not pass household ID as a parameter, which is correct. - No sensitive data visible in the page data — recipe names and slot IDs only.
Author
Owner

🎨 Atlas — UI/UX Designer

Verdict: ⚠️ Approved with concerns

The token usage is mostly correct and the three-breakpoint structure matches the spec intent. But several spec requirements are missing or misimplemented. I'll separate blockers from suggestions.

Blockers

1. Desktop calendar column header missing day name
The spec says columns need "9px day name (uppercase muted)" above the date badge. The implementation renders only the date number in the badge — there's no day abbreviation row. The column header <div> contains only the date badge. Fix: add a <p> with text-[9px] uppercase text-[var(--color-text-muted)] above the badge.

2. DayMealCard missing green treatment for selected (non-today) state
The DayMealCard only applies border-[var(--yellow)] bg-[var(--yellow-tint)] for today. The spec says selected day should have green treatment (border-[var(--green)] bg-[var(--green-tint)]) when not today. The component has isToday prop but no isSelected prop. Fix: add an isSelected prop and apply the green border/bg when isSelected && !isToday.

3. Variety banner "always visible" — mobile banner scrolls away
The spec acceptance criterion: "Variety score visible on all breakpoints without extra navigation." On mobile, the variety banner is rendered below the sticky header but is not itself sticky — it will scroll out of view as soon as the user scrolls the day list. The header is sticky top-0 but the variety div is not. Fix: either make the variety banner sticky below the header, or include it inside the sticky header region. This is the core value proposition of the screen.

4. Tablet: WeekStrip uses narrow abbreviations everywhere — spec requires 3-letter on tablet
The spec says tablet should use "3-letter abbreviations (Mon, Tue)" — short in Intl terms. The formatDayAbbr function is called with 'narrow' unconditionally, giving single-letter abbreviations at all breakpoints. The WeekStrip needs to receive a abbr prop or handle breakpoint-specific formatting. Since CSS breakpoints can't affect JS, one approach: always render the short (3-letter) abbreviation and use CSS truncation for mobile. Or render both and toggle with CSS visibility.

Suggestions

  • VarietyScoreCard score text: font-[300] is correct (Fraunces weight 300). The 28px mobile / 40px desktop split is implemented correctly with md:text-[40px]. Good.
  • Progress bar height: spec says 3–5px (mobile/tablet), 4px (desktop). Implementation is h-[4px] — acceptable.
  • Button text tokens: all buttons use text-[13px] font-medium tracking-[0.04em] font-[var(--font-sans)] — exactly right per the design system rules.
  • --radius-lg on DayMealCard — the spec uses --radius-lg for meal tiles. Correct.
  • Desktop meal tiles: hover state hover:border-[var(--green-light)] hover:shadow-[var(--shadow-raised)] — correct per spec.
  • Empty tile + icon: accessible aria-label="Gericht wählen für [day]" is missing — the + span has no label for screen readers.
  • The "Wochenplan erstellen" empty state is clean and well-placed. Correct button token usage.
## 🎨 Atlas — UI/UX Designer **Verdict: ⚠️ Approved with concerns** The token usage is mostly correct and the three-breakpoint structure matches the spec intent. But several spec requirements are missing or misimplemented. I'll separate blockers from suggestions. ### Blockers **1. Desktop calendar column header missing day name** The spec says columns need "9px day name (uppercase muted)" above the date badge. The implementation renders only the date number in the badge — there's no day abbreviation row. The column header `<div>` contains only the date badge. Fix: add a `<p>` with `text-[9px] uppercase text-[var(--color-text-muted)]` above the badge. **2. `DayMealCard` missing green treatment for selected (non-today) state** The `DayMealCard` only applies `border-[var(--yellow)] bg-[var(--yellow-tint)]` for today. The spec says selected day should have green treatment (`border-[var(--green)] bg-[var(--green-tint)]`) when not today. The component has `isToday` prop but no `isSelected` prop. Fix: add an `isSelected` prop and apply the green border/bg when `isSelected && !isToday`. **3. Variety banner "always visible" — mobile banner scrolls away** The spec acceptance criterion: "Variety score visible on all breakpoints without extra navigation." On mobile, the variety banner is rendered below the sticky header but is not itself sticky — it will scroll out of view as soon as the user scrolls the day list. The header is `sticky top-0` but the variety div is not. Fix: either make the variety banner sticky below the header, or include it inside the sticky header region. This is the core value proposition of the screen. **4. Tablet: `WeekStrip` uses `narrow` abbreviations everywhere — spec requires 3-letter on tablet** The spec says tablet should use "3-letter abbreviations (Mon, Tue)" — `short` in Intl terms. The `formatDayAbbr` function is called with `'narrow'` unconditionally, giving single-letter abbreviations at all breakpoints. The WeekStrip needs to receive a `abbr` prop or handle breakpoint-specific formatting. Since CSS breakpoints can't affect JS, one approach: always render the short (3-letter) abbreviation and use CSS truncation for mobile. Or render both and toggle with CSS visibility. ### Suggestions - `VarietyScoreCard` score text: `font-[300]` is correct (Fraunces weight 300). The 28px mobile / 40px desktop split is implemented correctly with `md:text-[40px]`. Good. - Progress bar height: spec says 3–5px (mobile/tablet), 4px (desktop). Implementation is `h-[4px]` — acceptable. - Button text tokens: all buttons use `text-[13px] font-medium tracking-[0.04em] font-[var(--font-sans)]` — exactly right per the design system rules. - `--radius-lg` on `DayMealCard` — the spec uses `--radius-lg` for meal tiles. Correct. - Desktop meal tiles: hover state `hover:border-[var(--green-light)] hover:shadow-[var(--shadow-raised)]` — correct per spec. - Empty tile `+` icon: accessible `aria-label="Gericht wählen für [day]"` is missing — the `+` span has no label for screen readers. - The "Wochenplan erstellen" empty state is clean and well-placed. Correct button token usage.
Author
Owner

🔧 Backend Engineer

Verdict: Approved

This PR is frontend-only — no Spring Boot changes, no database schema changes. From a backend contract perspective, the integration looks correct.

Checked

API endpoint usage is correct:

  • GET /v1/week-plans with { params: { query: { weekStart } } } — matches the schema definition for getWeekPlan which takes weekStart: string as a query parameter.
  • GET /v1/week-plans/{id}/variety-score — matches getVarietyScore with path parameter id. Called only after a successful plan fetch, ID sourced from the plan response. No IDOR risk from the frontend side.
  • POST /v1/week-plans with { body: { weekStart } } — matches createWeekPlan / CreateWeekPlanRequest.

Response handling:

  • The load function handles the 404 case (no plan for the week) by returning null — correct behavior for the "no plan yet" state.
  • Variety score failure is silently swallowed (?? null) — this is acceptable for a read-only enrichment field. The page degrades gracefully without the score.

Data shape:

  • WeekPlanResponse.slotsSlotResponse[] → each slot has slotDate and recipe: SlotRecipe — the frontend maps these correctly.
  • VarietyScoreResponse shape accessed as .score, .ingredientOverlaps — matches the schema.

One note

The frontend treats a weekStart URL parameter that doesn't correspond to a Monday as valid input to the API. The backend getWeekPlan accepts any date string for weekStart — it's unclear whether the backend normalizes to Monday or returns 400. If the backend normalizes silently, this is fine. If it returns an error for non-Monday dates, the frontend should validate that the week URL param is a Monday before using it. Not a blocker, but worth confirming with the backend team.

## 🔧 Backend Engineer **Verdict: ✅ Approved** This PR is frontend-only — no Spring Boot changes, no database schema changes. From a backend contract perspective, the integration looks correct. ### Checked **API endpoint usage is correct:** - `GET /v1/week-plans` with `{ params: { query: { weekStart } } }` — matches the schema definition for `getWeekPlan` which takes `weekStart: string` as a query parameter. - `GET /v1/week-plans/{id}/variety-score` — matches `getVarietyScore` with path parameter `id`. Called only after a successful plan fetch, ID sourced from the plan response. No IDOR risk from the frontend side. - `POST /v1/week-plans` with `{ body: { weekStart } }` — matches `createWeekPlan` / `CreateWeekPlanRequest`. **Response handling:** - The load function handles the 404 case (no plan for the week) by returning `null` — correct behavior for the "no plan yet" state. - Variety score failure is silently swallowed (`?? null`) — this is acceptable for a read-only enrichment field. The page degrades gracefully without the score. **Data shape:** - `WeekPlanResponse.slots` → `SlotResponse[]` → each slot has `slotDate` and `recipe: SlotRecipe` — the frontend maps these correctly. - `VarietyScoreResponse` shape accessed as `.score`, `.ingredientOverlaps` — matches the schema. ### One note The frontend treats a `weekStart` URL parameter that doesn't correspond to a Monday as valid input to the API. The backend `getWeekPlan` accepts any date string for `weekStart` — it's unclear whether the backend normalizes to Monday or returns 400. If the backend normalizes silently, this is fine. If it returns an error for non-Monday dates, the frontend should validate that the `week` URL param is a Monday before using it. Not a blocker, but worth confirming with the backend team.
marcel added 1 commit 2026-04-03 11:07:52 +02:00
- Fix logic bug `{#if !isPlanner === false}` - view/cook buttons now visible for all roles, swap only for planner
- Convert Tauschen from dead button to link with suggestions href
- Add week.ts unit tests (23 tests covering getWeekStart Sunday edge case, prevWeek/nextWeek, weekDays, isToday, formatWeekRange)
- Fix isToday to use UTC consistently (.toISOString().slice(0,10)) instead of local date
- Add server-side role guard to createPlan action (403 for members)
- Add weekStart format validation in createPlan action
- Add isSelected prop to DayMealCard with green treatment
- Make variety banner sticky on mobile (always visible per spec)
- Add day name abbreviation above date badge in desktop column headers
- Remove placeholder Navigation text from desktop sidebar
- Add aria-label to desktop empty tile buttons
- Add variety score partial failure test, multiple overlaps test, WeekStrip today+selected test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel merged commit 05e47c3dac into master 2026-04-03 11:07:57 +02:00
marcel deleted branch feat/issue-26-weekly-planner 2026-04-03 11:07:57 +02:00
Sign in to join this conversation.