feat(planner): C1 — Weekly planner home screen #39
Reference in New Issue
Block a user
Delete Branch "feat/issue-26-weekly-planner"
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
Implements issue #26: the weekly planner home screen (C1) with three-breakpoint layout.
readonly(household member) mode?week=param, full SvelteKit navigationnullfor 404 (no plan yet)createPlanPOST action for initializing a new weekTest plan
page.server.test.ts) covering load, URL param, variety score, 404 handling, createPlan actionVarietyScoreCardtests: score rendering, progress bar aria, overlap warnings, review link visibilityWeekStriptests: 7 chips, today/selected states, dot indicators, selectDay callbackDayMealCardtests: recipe display, readonly hiding, today styling, empty state, metadataCloses #26
🤖 Generated with Claude Code
👨💻 Kai — Frontend Engineer
Verdict: ⚠️ Approved with concerns
Good overall structure — Svelte 5 runes used correctly,
$derivedfor computed values,$effectfor the week-change side effect, callback prop instead ofcreateEventDispatcher. TDD discipline is visible. But I have blockers.Blockers
1. Logic bug in desktop detail panel —
{#if !isPlanner === false}+page.svelteline ~270:{#if !isPlanner === false}evaluates as(!isPlanner) === false, which simplifies toisPlanner. 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.
Tauschenbutton has no actionDayMealCard.svelte: The "Tauschen" button hastype="button"but noonclickhandler and nohref. 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.tstestsweek.tsexports 7 utility functions (getWeekStart,prevWeek,nextWeek,formatDayAbbr,weekDays,formatDayLabel,formatDayFull,isToday,formatWeekRange). None are unit-tested.getWeekStartspecifically 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.
isTodayinweek.tsuses local time but the rest uses UTCisTodaybuildstodayStrfromnew Date()using localgetFullYear/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).benutzertype castThe layout data type isn't threaded into the page type. The
anycast hides a type error and will break iflocals.benutzeris renamed. UsePageDatafrom./$typesand extend it, or access via$page.datawhich includes the layout data.Suggestions
remainingSlots.filter((s: any) => s.recipe)is called twice in the template withanycast. Extract to a$derivedvariable and type it properly.<p>— fine for now but this should be a comment or removed entirely, not rendered text.isTodayis imported fromweek.tsin the page script but the derivedtodaystring is computed inline with an IIFE — redundant. UseisTodayconsistently.🧪 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.tshas zero tests — 9 exported functions, 0 coveragegetWeekStart,prevWeek,nextWeek,weekDays,formatDayLabel,formatDayFull,isToday,formatWeekRange,formatDayAbbrare all untested. These are pure utility functions driving the entire planner — week navigation, today detection, day strip rendering. At minimum I need tests for:getWeekStartwith a Sunday input (the-6edge case)getWeekStartwith a Monday input (no shift)prevWeek/nextWeekweek boundary correctnessweekDaysreturns exactly 7 entries, starting on weekStartisTodayreturns true for today, false for yesterday/tomorrow2.
DayMealCard: "Tauschen" button has no action — test is incompleteThe 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
onswapcallback prop and test it's called, or (b) convert the button to an<a>withhrefand test the destination URL.3. No test for
WeekStripwith today AND selected being the same dayWhen today is the selected day, both
data-todayanddata-selectedare true on the same chip. The CSS classes are applied conditionally with&& !isTodayDayguards. Need a test asserting both attributes are set correctly when today === selectedDay.4. No test for
VarietyScoreCardwith multiple overlapsOnly 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.
createPlanaction: no test for missingweekStartin formDataIf
formData.get('weekStart')returnsnull, 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
+page.sveltethat verifies theisPlannerbehavior — actions hidden for member, shown for planner. This is a critical acceptance criterion from the issue.WeekStripcallback test useschip.click()directly — prefer@testing-library/user-eventuserEvent.click()for more realistic simulation.🔒 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-awarefetch, 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.
createPlanaction:weekStartfrom formData is not validated+page.server.tsline 31:const weekStart = formData.get('weekStart') as stringis passed directly toapi.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
weekStartvalues 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.
isPlannerrole check is client-side only — no server guard oncreatePlanactionThe
createPlanaction in+page.server.tsdoes not check whether the user is a planner before executing. A household member who crafts a POST to?/createPlanwould 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: checklocals.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)
/recipes/{id}/cook) — these are UUIDs per the schema, acceptable.goto()uses a developer-constructed URL/planner?week=${newWeekStart}—newWeekStartis computed from trusted utility functions, not from user input. No open redirect risk.hooks.server.tsvia session-derivedhousehold_id. The planner page does not pass household ID as a parameter, which is correct.🎨 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>withtext-[9px] uppercase text-[var(--color-text-muted)]above the badge.2.
DayMealCardmissing green treatment for selected (non-today) stateThe
DayMealCardonly appliesborder-[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 hasisTodayprop but noisSelectedprop. Fix: add anisSelectedprop and apply the green border/bg whenisSelected && !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-0but 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:
WeekStripusesnarrowabbreviations everywhere — spec requires 3-letter on tabletThe spec says tablet should use "3-letter abbreviations (Mon, Tue)" —
shortin Intl terms. TheformatDayAbbrfunction is called with'narrow'unconditionally, giving single-letter abbreviations at all breakpoints. The WeekStrip needs to receive aabbrprop 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
VarietyScoreCardscore text:font-[300]is correct (Fraunces weight 300). The 28px mobile / 40px desktop split is implemented correctly withmd:text-[40px]. Good.h-[4px]— acceptable.text-[13px] font-medium tracking-[0.04em] font-[var(--font-sans)]— exactly right per the design system rules.--radius-lgonDayMealCard— the spec uses--radius-lgfor meal tiles. Correct.hover:border-[var(--green-light)] hover:shadow-[var(--shadow-raised)]— correct per spec.+icon: accessiblearia-label="Gericht wählen für [day]"is missing — the+span has no label for screen readers.🔧 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-planswith{ params: { query: { weekStart } } }— matches the schema definition forgetWeekPlanwhich takesweekStart: stringas a query parameter.GET /v1/week-plans/{id}/variety-score— matchesgetVarietyScorewith path parameterid. Called only after a successful plan fetch, ID sourced from the plan response. No IDOR risk from the frontend side.POST /v1/week-planswith{ body: { weekStart } }— matchescreateWeekPlan/CreateWeekPlanRequest.Response handling:
null— correct behavior for the "no plan yet" state.?? null) — this is acceptable for a read-only enrichment field. The page degrades gracefully without the score.Data shape:
WeekPlanResponse.slots→SlotResponse[]→ each slot hasslotDateandrecipe: SlotRecipe— the frontend maps these correctly.VarietyScoreResponseshape accessed as.score,.ingredientOverlaps— matches the schema.One note
The frontend treats a
weekStartURL parameter that doesn't correspond to a Monday as valid input to the API. The backendgetWeekPlanaccepts any date string forweekStart— 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 theweekURL param is a Monday before using it. Not a blocker, but worth confirming with the backend team.- 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>