feat: Add-to-Plan flows C4/C5/C6 — recipe picker, quick actions, day picker #44

Merged
marcel merged 13 commits from feat/issue-42-add-to-plan into master 2026-04-09 09:53:08 +02:00
Owner

Summary

Implements issue #42 — three missing Add-to-Plan flows:

  • C4 (Add from planner): Tap + or an empty slot in the planner → bottom sheet (mobile) or right panel (desktop) opens a RecipePicker showing AI-ranked suggestions and a search-filtered recipe list. Selecting a recipe calls POST /v1/week-plans/{id}/slots or PATCH for filled slots.
  • C5 (Recipe quick actions): Every RecipeCard gains two action buttons — "Jetzt kochen" (navigates to cook mode) and "Zur Woche +" (opens C6 day picker). Buttons are only shown when onplan callback is provided.
  • C6 (Day picker from recipe): Tap "Zur Woche +" on any recipe card → DayPicker bottom sheet lets the user pick any day in the week. Filled slots show a replace warning. Prev/next week navigation fetches updated slot data client-side. Confirms via addSlot / updateSlot form action.

Backend additions:

  • GET /v1/week-plans/{planId}/variety-preview?recipeId=&date= — returns current/projected variety score delta for preview badges in C4
  • @RequiresHouseholdRole("planner") guard added to addSlot, updateSlot, deleteSlot (403 for members)

Undo: a 4-second UndoBar toast appears after every successful add/swap. Tapping "Rückgängig" calls DELETE /v1/week-plans/{planId}/slots/{slotId}.

Removed: the C2 /planner/suggestions page route is deleted; all entry points now go through the new inline flows.

Test plan

  • All backend tests green: cd backend && ./mvnw test (289 tests)
  • All frontend tests green: cd frontend && npm run test (548 pass, 2 pre-existing failures in recipes/page.test.ts)
  • Type check: cd frontend && npm run check (2 pre-existing errors in test-setup.ts only)
  • Manually open planner, tap an empty slot → RecipePicker appears
  • Manually pick a recipe → slot fills, UndoBar appears, "Rückgängig" removes it
  • Open recipe list, tap "Zur Woche +" → DayPicker appears with current week slots
  • Navigate weeks in DayPicker → slots update without closing the sheet

🤖 Generated with Claude Code

## Summary Implements issue #42 — three missing Add-to-Plan flows: - **C4 (Add from planner)**: Tap `+` or an empty slot in the planner → bottom sheet (mobile) or right panel (desktop) opens a `RecipePicker` showing AI-ranked suggestions and a search-filtered recipe list. Selecting a recipe calls `POST /v1/week-plans/{id}/slots` or `PATCH` for filled slots. - **C5 (Recipe quick actions)**: Every `RecipeCard` gains two action buttons — "Jetzt kochen" (navigates to cook mode) and "Zur Woche +" (opens C6 day picker). Buttons are only shown when `onplan` callback is provided. - **C6 (Day picker from recipe)**: Tap "Zur Woche +" on any recipe card → `DayPicker` bottom sheet lets the user pick any day in the week. Filled slots show a replace warning. Prev/next week navigation fetches updated slot data client-side. Confirms via `addSlot` / `updateSlot` form action. **Backend additions:** - `GET /v1/week-plans/{planId}/variety-preview?recipeId=&date=` — returns current/projected variety score delta for preview badges in C4 - `@RequiresHouseholdRole("planner")` guard added to `addSlot`, `updateSlot`, `deleteSlot` (403 for members) **Undo:** a 4-second `UndoBar` toast appears after every successful add/swap. Tapping "Rückgängig" calls `DELETE /v1/week-plans/{planId}/slots/{slotId}`. **Removed:** the C2 `/planner/suggestions` page route is deleted; all entry points now go through the new inline flows. ## Test plan - [ ] All backend tests green: `cd backend && ./mvnw test` (289 tests) - [ ] All frontend tests green: `cd frontend && npm run test` (548 pass, 2 pre-existing failures in `recipes/page.test.ts`) - [ ] Type check: `cd frontend && npm run check` (2 pre-existing errors in `test-setup.ts` only) - [ ] Manually open planner, tap an empty slot → RecipePicker appears - [ ] Manually pick a recipe → slot fills, UndoBar appears, "Rückgängig" removes it - [ ] Open recipe list, tap "Zur Woche +" → DayPicker appears with current week slots - [ ] Navigate weeks in DayPicker → slots update without closing the sheet 🤖 Generated with [Claude Code](https://claude.com/claude-code)
marcel added 10 commits 2026-04-09 08:02:11 +02:00
PATCH, DELETE, and POST slot endpoints now return 403 Forbidden
when called by a household member.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns currentScore, projectedScore, and scoreDelta when a recipe
would be added on a given date. Used by C6 desktop day picker.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shared wrapper for C4, C6, and future sheet flows. Handles dim overlay,
drag handle, focus trap, Escape dismiss, and backdrop click dismiss.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C4 sheet content: Empfohlen section with variety delta badges,
Alle Rezepte with client-side search filter. GET /planner endpoint
proxies suggestions to backend for lazy client-side loading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7-chip week strip with 5 slot states, inline replace warning,
confirm button, and prev/next week navigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows undo notification after slot add/replace. Rückgängig button
calls onundo, auto-dismisses after 4s via ondismiss callback.
Also patches test-setup for userEvent + fake timers compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Always-visible "Jetzt kochen" and "Zur Woche +" buttons shown
when onplan prop is provided. Restructured card to avoid nested
interactive elements.

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

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: ⚠️ Approved with concerns

Blockers

1. allRecipes in RecipePicker is a placeholder, not the full recipe library

frontend/src/routes/(app)/planner/+page.svelte and recipes/+page.svelte both pass:

allRecipes={weekPlan?.slots?.map((s) => s.recipe).filter(Boolean) ?? []}

This gives RecipePicker only the recipes already in the current week plan — not the full library. The component is designed to show a search-filtered list of all recipes, so this is a functional gap. There's even a comment in the code acknowledging it as a placeholder. Either load the full recipe list server-side or fetch it client-side, but ship it working.

2. Slot actions duplicated verbatim in two server files

planner/+page.server.ts and recipes/+page.server.ts contain identical addSlot, updateSlot, and deleteSlot implementations. Extract to $lib/server/slotActions.ts and re-export. Duplication here means any bug fix or auth change needs applying twice.

Suggestions

  • await tick() before requestSubmit() is the right call — good pattern, keep it.
  • PanelState discriminated union is clean and readable. No complaints.
  • The +server.ts proxy for suggestions is a reasonable workaround for the server-only apiClient.
  • UndoBar's 4s auto-dismiss with $effect + setTimeout cleanup is correct Svelte 5 idiom.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ⚠️ Approved with concerns** ### Blockers **1. `allRecipes` in RecipePicker is a placeholder, not the full recipe library** `frontend/src/routes/(app)/planner/+page.svelte` and `recipes/+page.svelte` both pass: ```svelte allRecipes={weekPlan?.slots?.map((s) => s.recipe).filter(Boolean) ?? []} ``` This gives RecipePicker only the recipes already in the current week plan — not the full library. The component is designed to show a search-filtered list of all recipes, so this is a functional gap. There's even a comment in the code acknowledging it as a placeholder. Either load the full recipe list server-side or fetch it client-side, but ship it working. **2. Slot actions duplicated verbatim in two server files** `planner/+page.server.ts` and `recipes/+page.server.ts` contain identical `addSlot`, `updateSlot`, and `deleteSlot` implementations. Extract to `$lib/server/slotActions.ts` and re-export. Duplication here means any bug fix or auth change needs applying twice. ### Suggestions - `await tick()` before `requestSubmit()` is the right call — good pattern, keep it. - `PanelState` discriminated union is clean and readable. No complaints. - The `+server.ts` proxy for suggestions is a reasonable workaround for the server-only `apiClient`. - UndoBar's 4s auto-dismiss with `$effect` + `setTimeout` cleanup is correct Svelte 5 idiom.
Author
Owner

🔒 Sable — Security Engineer

Verdict: 🚫 Changes requested

Blockers

1. No input validation on slot mutation actions

planner/+page.server.ts and recipes/+page.server.ts — all three slot actions (addSlot, updateSlot, deleteSlot) cast form values directly to strings with no validation:

const planId = formData.get('planId') as string;
const slotId = formData.get('slotId') as string;
const recipeId = formData.get('recipeId') as string;

These values go straight into API path parameters (/v1/week-plans/{id}/slots/{slotId}). A blank or malformed planId/slotId will make nonsensical API calls. The deleted pickSuggestion action validated everything — UUID format check, non-empty guard. These replacements need the same treatment. At minimum:

if (!planId || !slotId || !/^[0-9a-f-]{36}$/.test(planId)) {
  return { success: false, error: 'Ungültige Eingabe.' };
}

Apply to all three actions in both files.

2. GET /planner suggestions proxy leaks household data without explicit auth check

frontend/src/routes/(app)/planner/+server.ts fetches /v1/week-plans/{planId}/variety-preview using the session cookie — fine in principle. But there's no check that the planId query param belongs to the current user's household. If the backend enforces household isolation on that endpoint (which it should via @RequiresHouseholdRole), this is defended. Confirm the backend endpoint has the annotation — the variety-preview endpoint in WeekPlanController is missing @RequiresHouseholdRole per the backend diff.

Suggestions

  • SvelteKit form actions running server-side with session cookies: correct trust model, no issues.
  • readonly prop in DayMealCard hiding actions when not in planner role is a good defence-in-depth UI guard, but don't rely on it for real access control — the backend must be the final gatekeeper.
## 🔒 Sable — Security Engineer **Verdict: 🚫 Changes requested** ### Blockers **1. No input validation on slot mutation actions** `planner/+page.server.ts` and `recipes/+page.server.ts` — all three slot actions (`addSlot`, `updateSlot`, `deleteSlot`) cast form values directly to strings with no validation: ```typescript const planId = formData.get('planId') as string; const slotId = formData.get('slotId') as string; const recipeId = formData.get('recipeId') as string; ``` These values go straight into API path parameters (`/v1/week-plans/{id}/slots/{slotId}`). A blank or malformed `planId`/`slotId` will make nonsensical API calls. The deleted `pickSuggestion` action validated everything — UUID format check, non-empty guard. These replacements need the same treatment. At minimum: ```typescript if (!planId || !slotId || !/^[0-9a-f-]{36}$/.test(planId)) { return { success: false, error: 'Ungültige Eingabe.' }; } ``` Apply to all three actions in both files. **2. `GET /planner` suggestions proxy leaks household data without explicit auth check** `frontend/src/routes/(app)/planner/+server.ts` fetches `/v1/week-plans/{planId}/variety-preview` using the session cookie — fine in principle. But there's no check that the `planId` query param belongs to the current user's household. If the backend enforces household isolation on that endpoint (which it should via `@RequiresHouseholdRole`), this is defended. Confirm the backend endpoint has the annotation — the variety-preview endpoint in `WeekPlanController` is missing `@RequiresHouseholdRole` per the backend diff. ### Suggestions - SvelteKit form actions running server-side with session cookies: correct trust model, no issues. - `readonly` prop in DayMealCard hiding actions when not in planner role is a good defence-in-depth UI guard, but don't rely on it for real access control — the backend must be the final gatekeeper.
Author
Owner

🧪 Senior QA Engineer

Verdict: ⚠️ Approved with concerns

Blockers

1. Slot actions have no validation-path tests

The new addSlot/updateSlot/deleteSlot actions in both planner/page.server.test.ts and recipes/page.server.test.ts only test the happy path. The deleted pickSuggestion had tests for missing slotDate, invalid planId, API errors returning { success: false }, etc. The replacement actions need the same coverage — otherwise the security team's validation fix (see Sable's comment) will ship with no regression protection.

2. recipes/+server.ts has zero tests

The new GET /recipes endpoint used by DayPicker's week navigation is completely untested. At minimum: returns 200 with plan data, returns 401 when not authenticated, handles missing week param gracefully.

Suggestions

  • RecipePicker and DayPicker component tests are thorough (11 + 15 tests). Good baseline.
  • BottomSheet has 7 tests covering open/close/Escape — solid.
  • UndoBar auto-dismiss test coverage is correct, including fake timer usage.
  • No E2E tests for the full add-meal flow (C4/C5/C6). Given the complexity of the form submission chain (bind:this + requestSubmit() + use:enhance), a Playwright test covering the happy path would catch regressions that unit tests can't. Flagging as a suggestion since E2E setup may be out of scope for this issue.
  • planner/+server.ts (suggestions proxy) has no tests either — same concern as recipes/+server.ts.
## 🧪 Senior QA Engineer **Verdict: ⚠️ Approved with concerns** ### Blockers **1. Slot actions have no validation-path tests** The new `addSlot`/`updateSlot`/`deleteSlot` actions in both `planner/page.server.test.ts` and `recipes/page.server.test.ts` only test the happy path. The deleted `pickSuggestion` had tests for missing `slotDate`, invalid `planId`, API errors returning `{ success: false }`, etc. The replacement actions need the same coverage — otherwise the security team's validation fix (see Sable's comment) will ship with no regression protection. **2. `recipes/+server.ts` has zero tests** The new `GET /recipes` endpoint used by DayPicker's week navigation is completely untested. At minimum: returns 200 with plan data, returns 401 when not authenticated, handles missing `week` param gracefully. ### Suggestions - RecipePicker and DayPicker component tests are thorough (11 + 15 tests). Good baseline. - BottomSheet has 7 tests covering open/close/Escape — solid. - UndoBar auto-dismiss test coverage is correct, including fake timer usage. - No E2E tests for the full add-meal flow (C4/C5/C6). Given the complexity of the form submission chain (`bind:this` + `requestSubmit()` + `use:enhance`), a Playwright test covering the happy path would catch regressions that unit tests can't. Flagging as a suggestion since E2E setup may be out of scope for this issue. - `planner/+server.ts` (suggestions proxy) has no tests either — same concern as `recipes/+server.ts`.
Author
Owner

🎨 Atlas — UI/UX Designer

Verdict: ⚠️ Approved with concerns

Blockers

1. Section header font-size is 7px

RecipePicker.svelte has:

<h4 class="... text-[7px] ...">Vorschläge</h4>

7px is illegible on any screen. This looks like a typo for text-[11px] or text-xs (12px). Ship this as-is and it'll be a visible bug on the first demo.

Suggestions

2. Emoji in action buttons may break the design system

RecipeCard.svelte uses "🍳 Jetzt kochen" and "📅 Zur Woche +" as button labels. Emoji rendering varies wildly across platforms and doesn't inherit font styling. If the design system uses icon components or SVG icons, swap these. If emoji is an intentional choice for now, keep it — but flag it for the design token pass.

3. UndoBar z-index vs. mobile bottom navigation

UndoBar.svelte uses z-50 and is positioned fixed bottom-4. On mobile, if there's a bottom navigation bar, the UndoBar will overlap it. Check the mobile layout in the planner and recipes pages to ensure the safe area / bottom nav height is accounted for (e.g. bottom-[calc(1rem+env(safe-area-inset-bottom))] or add bottom padding matching the nav height).

4. BottomSheet drag handle is decorative only

The drag handle div in BottomSheet.svelte has no touch event handling — it's purely visual. This is fine for now if gesture-based dismissal isn't in scope, but users will try to drag it. Consider adding a note in the component or implementing basic touch drag.

5. DayPicker chip states are well designed

The 5 data-state values (today, has-meal, empty, replace, past) give clear visual affordance. Good work.

## 🎨 Atlas — UI/UX Designer **Verdict: ⚠️ Approved with concerns** ### Blockers **1. Section header font-size is 7px** `RecipePicker.svelte` has: ```svelte <h4 class="... text-[7px] ...">Vorschläge</h4> ``` 7px is illegible on any screen. This looks like a typo for `text-[11px]` or `text-xs` (12px). Ship this as-is and it'll be a visible bug on the first demo. ### Suggestions **2. Emoji in action buttons may break the design system** `RecipeCard.svelte` uses "🍳 Jetzt kochen" and "📅 Zur Woche +" as button labels. Emoji rendering varies wildly across platforms and doesn't inherit font styling. If the design system uses icon components or SVG icons, swap these. If emoji is an intentional choice for now, keep it — but flag it for the design token pass. **3. UndoBar z-index vs. mobile bottom navigation** `UndoBar.svelte` uses `z-50` and is positioned `fixed bottom-4`. On mobile, if there's a bottom navigation bar, the UndoBar will overlap it. Check the mobile layout in the planner and recipes pages to ensure the safe area / bottom nav height is accounted for (e.g. `bottom-[calc(1rem+env(safe-area-inset-bottom))]` or add bottom padding matching the nav height). **4. BottomSheet drag handle is decorative only** The drag handle div in `BottomSheet.svelte` has no touch event handling — it's purely visual. This is fine for now if gesture-based dismissal isn't in scope, but users will try to drag it. Consider adding a note in the component or implementing basic touch drag. **5. DayPicker chip states are well designed** The 5 `data-state` values (`today`, `has-meal`, `empty`, `replace`, `past`) give clear visual affordance. Good work.
Author
Owner

⚙️ Backend Engineer

Verdict: 🚫 Changes requested

Blockers

1. variety-preview endpoint is missing @RequiresHouseholdRole

WeekPlanController.java — the new GET /{planId}/variety-score endpoint doesn't have @RequiresHouseholdRole on it, while addSlot, updateSlot, and deleteSlot do. Any authenticated user can fetch variety preview data for any plan ID. Add the annotation:

@RequiresHouseholdRole("member")  // read access is fine for members
@GetMapping("/{planId}/variety-preview")
public VarietyPreviewResponse getVarietyPreview(...) { ... }

2. computeCurrentScore duplicates simulateVarietyScore logic

PlanningService.java — the new private computeCurrentScore method computes the variety score from existing slot recipe IDs. This is the same logic as the existing simulateVarietyScore but without the proposed recipe added. Refactor: extract a shared private scoreFromRecipeIds(Set<UUID> recipeIds, VarietyScoreConfig config) and call it from both getVarietyScore, simulateVarietyScore, and computeCurrentScore. Eliminates three places maintaining identical score math.

Suggestions

  • VarietyPreviewResponse as a record is clean. No complaints.
  • The variety preview test asserting scoreDelta > -1.0 is too loose — it can't actually fail. Pin the expected delta to the seed data's known score: e.g. assertEquals(0.0, response.scoreDelta(), 0.01) for an empty plan.
  • PlanningServiceTest variety preview tests are a good start but only cover the "empty plan" case. Add a case with existing slots to verify the delta direction.
## ⚙️ Backend Engineer **Verdict: 🚫 Changes requested** ### Blockers **1. `variety-preview` endpoint is missing `@RequiresHouseholdRole`** `WeekPlanController.java` — the new `GET /{planId}/variety-score` endpoint doesn't have `@RequiresHouseholdRole` on it, while `addSlot`, `updateSlot`, and `deleteSlot` do. Any authenticated user can fetch variety preview data for any plan ID. Add the annotation: ```java @RequiresHouseholdRole("member") // read access is fine for members @GetMapping("/{planId}/variety-preview") public VarietyPreviewResponse getVarietyPreview(...) { ... } ``` **2. `computeCurrentScore` duplicates `simulateVarietyScore` logic** `PlanningService.java` — the new private `computeCurrentScore` method computes the variety score from existing slot recipe IDs. This is the same logic as the existing `simulateVarietyScore` but without the proposed recipe added. Refactor: extract a shared private `scoreFromRecipeIds(Set<UUID> recipeIds, VarietyScoreConfig config)` and call it from both `getVarietyScore`, `simulateVarietyScore`, and `computeCurrentScore`. Eliminates three places maintaining identical score math. ### Suggestions - `VarietyPreviewResponse` as a record is clean. No complaints. - The variety preview test asserting `scoreDelta > -1.0` is too loose — it can't actually fail. Pin the expected delta to the seed data's known score: e.g. `assertEquals(0.0, response.scoreDelta(), 0.01)` for an empty plan. - `PlanningServiceTest` variety preview tests are a good start but only cover the "empty plan" case. Add a case with existing slots to verify the delta direction.
marcel added 2 commits 2026-04-09 08:19:42 +02:00
- Add @RequiresHouseholdRole("member") to GET /{planId}/variety-preview endpoint
  to require household membership (was accessible to any authenticated user)
- Extract scoreFromSimulatedSlots() private method eliminating duplicate logic
  between simulateVarietyScore() and the old computeCurrentScore()
- Fix loose variety preview test assertions (isBetween → exact assertEquals)
- Add test verifying negative scoreDelta when candidate is a duplicate recipe

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix 7px → 11px font-size on section headers in RecipePicker
- Extract shared slotActions.ts with UUID validation for planId/slotId/recipeId
- Load full recipe list in planner page load (was placeholder current-week slots)
- Update planner/+page.svelte to pass data.recipes as allRecipes to RecipePicker
- Update planner and recipes page.server.ts to use shared slot action helpers
- Fix planner page.server tests: add recipes mock for parallel GET load
- Update action tests to use valid UUIDs (were 'plan-1'/'r1' style strings)
- Add validation-path tests for blank/invalid input on all slot actions
- Add tests for recipes/+server.ts GET endpoint (DayPicker week navigation)

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

Review concerns addressed — e5d96cd

All blockers from the team review have been resolved. Here's what changed:

🔒 Sable (Security) — both blockers fixed

  • UUID validation on slot actions: Extracted $lib/server/slotActions.ts with UUID regex guard on planId, slotId, recipeId. Invalid or missing values return { success: false, error: 'Ungültige Eingabe.' } before any API call.
  • @RequiresHouseholdRole("member") on variety-preview: Added annotation to GET /{planId}/variety-preview in WeekPlanController.javaea7113e.

⚙️ Backend — both blockers fixed

  • computeCurrentScore duplication: Extracted scoreFromSimulatedSlots(List<SimulatedSlot>, VarietyScoreConfig, Set<UUID>) private method. Both simulateVarietyScore and the variety-preview current-score path now call it — ea7113e.
  • Loose test assertions: Replaced isBetween(0.0, 10.0) with exact values (isEqualTo(10.0)). Added getVarietyPreviewShouldReturnNegativeDeltaForDuplicateRecipe test that verifies scoreDelta == -2.0 (duplicate penalty = 2×default weight) — ea7113e.

👨‍💻 Felix (Frontend) — both blockers fixed

  • allRecipes placeholder: Planner +page.server.ts now loads GET /v1/recipes in parallel with the week plan. RecipePicker receives data.recipes (full library) instead of current-week slots — e5d96cd.
  • Duplicated slot actions: Both planner/+page.server.ts and recipes/+page.server.ts now import from $lib/server/slotActions.tse5d96cd.

🎨 Atlas (UI/UX) — blocker fixed

  • 7px section header font: Fixed both Empfohlen and Alle Rezepte section headers in RecipePicker.svelte from font-size: 7px11pxe5d96cd.

🧪 QA — both blockers fixed

  • Validation-path tests: Added 6 new tests across planner/page.server.test.ts and recipes/page.server.test.ts covering blank planId, invalid UUID planId/slotId, missing slotDate.
  • recipes/+server.ts untested: Added recipes/server.test.ts with 5 tests covering: plan returned on success, { plan: null } when week param missing, API error, data without id, correct query param forwarded.

Suggestions deferred

  • Emoji in action buttons (design system question — left as-is pending design token pass)
  • UndoBar z-index vs mobile bottom nav (no bottom nav in current layout — leaving for when nav is added)
  • BottomSheet drag handle gesture handling (out of scope for this issue)
## Review concerns addressed — e5d96cd All blockers from the team review have been resolved. Here's what changed: ### 🔒 Sable (Security) — ✅ both blockers fixed - **UUID validation on slot actions**: Extracted `$lib/server/slotActions.ts` with UUID regex guard on `planId`, `slotId`, `recipeId`. Invalid or missing values return `{ success: false, error: 'Ungültige Eingabe.' }` before any API call. - **`@RequiresHouseholdRole("member")` on variety-preview**: Added annotation to `GET /{planId}/variety-preview` in `WeekPlanController.java` — ea7113e. ### ⚙️ Backend — ✅ both blockers fixed - **`computeCurrentScore` duplication**: Extracted `scoreFromSimulatedSlots(List<SimulatedSlot>, VarietyScoreConfig, Set<UUID>)` private method. Both `simulateVarietyScore` and the variety-preview current-score path now call it — ea7113e. - **Loose test assertions**: Replaced `isBetween(0.0, 10.0)` with exact values (`isEqualTo(10.0)`). Added `getVarietyPreviewShouldReturnNegativeDeltaForDuplicateRecipe` test that verifies `scoreDelta == -2.0` (duplicate penalty = 2×default weight) — ea7113e. ### 👨‍💻 Felix (Frontend) — ✅ both blockers fixed - **`allRecipes` placeholder**: Planner `+page.server.ts` now loads `GET /v1/recipes` in parallel with the week plan. `RecipePicker` receives `data.recipes` (full library) instead of current-week slots — e5d96cd. - **Duplicated slot actions**: Both `planner/+page.server.ts` and `recipes/+page.server.ts` now import from `$lib/server/slotActions.ts` — e5d96cd. ### 🎨 Atlas (UI/UX) — ✅ blocker fixed - **7px section header font**: Fixed both `Empfohlen` and `Alle Rezepte` section headers in `RecipePicker.svelte` from `font-size: 7px` → `11px` — e5d96cd. ### 🧪 QA — ✅ both blockers fixed - **Validation-path tests**: Added 6 new tests across `planner/page.server.test.ts` and `recipes/page.server.test.ts` covering blank planId, invalid UUID planId/slotId, missing slotDate. - **`recipes/+server.ts` untested**: Added `recipes/server.test.ts` with 5 tests covering: plan returned on success, `{ plan: null }` when week param missing, API error, data without id, correct query param forwarded. ### Suggestions deferred - Emoji in action buttons (design system question — left as-is pending design token pass) - UndoBar z-index vs mobile bottom nav (no bottom nav in current layout — leaving for when nav is added) - BottomSheet drag handle gesture handling (out of scope for this issue)
marcel added 1 commit 2026-04-09 09:51:39 +02:00
- hooks.server.ts: replace type-cast with actual mapping so isPlanner works
- planner page: set min-h/min-w 40px on prev/next/heute week buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel merged commit 5b8d336d21 into master 2026-04-09 09:53:08 +02:00
marcel deleted branch feat/issue-42-add-to-plan 2026-04-09 09:53:09 +02:00
Sign in to join this conversation.