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

Closed
opened 2026-04-04 16:41:02 +02:00 by marcel · 8 comments
Owner

Add to Plan — Screens C4–C6

Spec: specs/frontend/j2-add-meal.html

Three missing interaction surfaces for getting a recipe into the weekly plan. All share the same backend write (PATCH /api/week-plan/{weekId}/slots/{date} { recipe_id }), but they cover two fundamentally different entry points.


Context: Two entry points, different intent

Entry Known Unknown Flow
Tap "+" or empty slot in planner (C1) Day Recipe C4: show recipe picker
Tap "Zur Woche +" on recipe card (C5) Recipe Day C6: show day picker

Add ≠ Swap. J4 Swap replaces a filled slot with a system-suggested recipe. C4/C6 fill an empty slot with a user-chosen recipe. Do not reuse the swap sheet component.


C4 — Recipe picker (from planner)

Entry: tap the + button in the C1 nav header, or tap an empty day slot chip (mobile) / empty calendar tile (desktop).

Mobile

  • Bottom sheet slides up (75vh max), planner dims to 40% opacity behind it
  • Drag handle + Rezept wählen · {Day, Date} header + × close
  • Search input (client-side filter)
  • "Empfohlen" section: 2–4 recipes, sorted by variety_delta DESC (≠ J4 which sorts by effort ASC)
    • Green badge ↑ +N Punkte if delta > 0
    • Yellow badge ⚠ {reason} if delta ≤ 0 (still selectable)
  • "Alle Rezepte" section: full library, no badges
  • Tap + Wählen → immediate PATCH → dismiss sheet → undo toast 4s

Desktop

  • Tap empty tile → tile highlights (solid green border, green-tint bg, "Wählen…" label)
  • Right detail panel transitions to recipe-picker state: search input + Empfohlen + Alle Rezepte list
  • Clicking a recipe row → PATCH → panel transitions to day-detail for the now-filled slot
  • No undo toast needed on desktop (panel immediately shows result with Swap option)

API

GET  /api/suggestions?week={id}&day={date}   → sorted variety_delta DESC
GET  /api/recipes?sort=name
PATCH /api/week-plan/{weekId}/slots/{date}   { recipe_id }

C5 — Recipe card quick actions

All recipe cards in the Recipes tab (B1) gain two always-visible action buttons below the tags row.

Button Style Action
🍳 Jetzt kochen Green filled (primary) Navigate to /cook/{recipeId} (J3)
📅 Zur Woche + Green-tint bg / green-dark text / green-light border (secondary) Open C6 day picker

Always visible — not hover-gated. Touch-capable desktops have unreliable hover; both actions need to be reachable without mouse interaction.

Sizes: 10px mobile / 11px desktop, font-weight: 500, border-radius: var(--radius-md).


C6 — Day picker (from recipe card)

Entry: tap Zur Woche + on any recipe card (C5). Recipe is known; user picks the day.

Mobile

Compact bottom sheet (~55vh). Recipe list dims to 28% opacity behind it.

Sheet structure:

  1. Drag handle
  2. Header: recipe name + "Zu welchem Tag hinzufügen?" subtitle + ×
  3. Week label + ‹ › week nav
  4. 7-day strip with slot states (see below)
  5. Confirm button (updates label/color based on selection)

Day chip states:

Class Border Background Meaning
.empty dashed green-light green-tint Invite selection
.filled solid color-border color-surface + green dot Has a meal, still selectable
.today solid yellow yellow-tint Today
.sel-empty 2px green-dark green-tint Selected empty → green confirm btn
.sel-filled 2px orange-dark orange-tint Selected filled → show replace warning

Replace confirmation (V2): inline dp-warn note below the strip ("Ersetzt Tomaten-Pasta am Mittwoch. Undo möglich.") + orange confirm button. No modal dialog — consistent with J4.

Week nav: ‹ › loads prev/next week. If current week is fully filled, auto-advance to next week.

Desktop

  • Right detail panel enters day-picker state
  • Active recipe card: 2px green border, green-tint bg; Zur Woche + button becomes green filled Tag wählen…
  • Other recipe cards: 45% opacity
  • Panel shows: recipe name header + mini 7-day strip + confirm button + variety preview (↑ Score: 8 → 9 · Neues Protein via GET /api/variety/preview?add={id}&date={date})
  • Variety preview is desktop-only; omitted on mobile to keep the sheet compact

API

PATCH /api/week-plan/{weekId}/slots/{date}  { recipe_id }   (add or replace)
DELETE /api/week-plan/{weekId}/slots/{date}                  (undo)
GET   /api/variety/preview?add={recipeId}&date={date}        (desktop only)

Desktop panel state machine

idle
  ↓ empty tile click / "+" button
recipe-picker  ←→  (× closes back to idle)
  ↓ recipe row click
day-detail

day-detail
  ↓ "Swap" button click
→ J4 swap flow (separate)

idle / day-detail
  ↑ "Zur Woche +" on recipe card (from Recipes tab)
day-picker  ←→  (× closes back to previous state)
  ↓ confirm
day-detail (for newly filled date)

Tap counts

Flow Mobile Desktop
C4 via empty slot 2 (slot → pick recipe) 2 (tile → click row)
C4 via "+" button 2 ("+" → pick recipe, day pre-selected) 2
C6 empty slot 2 ("Zur Woche +" → pick day) 2
C6 filled slot 3 ("Zur Woche +" → pick day → confirm replace) 3

Open questions for discussion

  1. C4 entry via "+": When no day is pre-selected (user taps + in the nav, not a specific slot), should the sheet pre-select the next empty day automatically, or should it require the user to first select a day from a strip at the top of the sheet?

  2. C5 button labels: "Zur Woche +" is short enough for mobile cards at 320px. Alternative: "Einplanen". Does the team prefer the explicit + suffix or a verb-only label?

  3. C6 fully-filled week: If the current week has no empty slots, auto-advance to next week on sheet open? Or show the current week with all filled slots (all selectable for replace)?

  4. Variety preview in C6 mobile: Currently omitted for compactness. Could show a single-line score delta ("↑ +1 Punkt") below the confirm button without adding much height. Worth adding?

  5. C2 vs C4: The existing C2 spec (j2-plan-the-week.html) defines a full-page suggestions screen navigated to from C1. C4 proposes a bottom sheet instead. Should C2 be retired in favour of C4, or kept as an alternative "deep browse" mode accessible from C4's "Alle Rezepte" section?

# Add to Plan — Screens C4–C6 Spec: `specs/frontend/j2-add-meal.html` Three missing interaction surfaces for getting a recipe into the weekly plan. All share the same backend write (`PATCH /api/week-plan/{weekId}/slots/{date} { recipe_id }`), but they cover two fundamentally different entry points. --- ## Context: Two entry points, different intent | Entry | Known | Unknown | Flow | |---|---|---|---| | Tap "+" or empty slot in planner (C1) | Day | Recipe | C4: show recipe picker | | Tap "Zur Woche +" on recipe card (C5) | Recipe | Day | C6: show day picker | **Add ≠ Swap.** J4 Swap replaces a filled slot with a system-suggested recipe. C4/C6 fill an empty slot with a user-chosen recipe. Do not reuse the swap sheet component. --- ## C4 — Recipe picker (from planner) Entry: tap the `+` button in the C1 nav header, or tap an empty day slot chip (mobile) / empty calendar tile (desktop). ### Mobile - Bottom sheet slides up (75vh max), planner dims to 40% opacity behind it - Drag handle + `Rezept wählen · {Day, Date}` header + `×` close - Search input (client-side filter) - **"Empfohlen"** section: 2–4 recipes, sorted by `variety_delta DESC` (≠ J4 which sorts by effort ASC) - Green badge `↑ +N Punkte` if delta > 0 - Yellow badge `⚠ {reason}` if delta ≤ 0 (still selectable) - **"Alle Rezepte"** section: full library, no badges - Tap `+ Wählen` → immediate PATCH → dismiss sheet → undo toast 4s ### Desktop - Tap empty tile → tile highlights (solid green border, green-tint bg, "Wählen…" label) - Right detail panel transitions to **recipe-picker state**: search input + Empfohlen + Alle Rezepte list - Clicking a recipe row → PATCH → panel transitions to day-detail for the now-filled slot - No undo toast needed on desktop (panel immediately shows result with Swap option) ### API ``` GET /api/suggestions?week={id}&day={date} → sorted variety_delta DESC GET /api/recipes?sort=name PATCH /api/week-plan/{weekId}/slots/{date} { recipe_id } ``` --- ## C5 — Recipe card quick actions All recipe cards in the Recipes tab (B1) gain two always-visible action buttons below the tags row. | Button | Style | Action | |---|---|---| | `🍳 Jetzt kochen` | Green filled (primary) | Navigate to `/cook/{recipeId}` (J3) | | `📅 Zur Woche +` | Green-tint bg / green-dark text / green-light border (secondary) | Open C6 day picker | **Always visible** — not hover-gated. Touch-capable desktops have unreliable hover; both actions need to be reachable without mouse interaction. Sizes: 10px mobile / 11px desktop, `font-weight: 500`, `border-radius: var(--radius-md)`. --- ## C6 — Day picker (from recipe card) Entry: tap `Zur Woche +` on any recipe card (C5). Recipe is known; user picks the day. ### Mobile Compact bottom sheet (~55vh). Recipe list dims to 28% opacity behind it. Sheet structure: 1. Drag handle 2. Header: recipe name + "Zu welchem Tag hinzufügen?" subtitle + `×` 3. Week label + `‹ ›` week nav 4. 7-day strip with slot states (see below) 5. Confirm button (updates label/color based on selection) **Day chip states:** | Class | Border | Background | Meaning | |---|---|---|---| | `.empty` | dashed `green-light` | `green-tint` | Invite selection | | `.filled` | solid `color-border` | `color-surface` + green dot | Has a meal, still selectable | | `.today` | solid `yellow` | `yellow-tint` | Today | | `.sel-empty` | 2px `green-dark` | `green-tint` | Selected empty → green confirm btn | | `.sel-filled` | 2px `orange-dark` | `orange-tint` | Selected filled → show replace warning | **Replace confirmation (V2):** inline `dp-warn` note below the strip ("Ersetzt **Tomaten-Pasta** am Mittwoch. Undo möglich.") + orange confirm button. No modal dialog — consistent with J4. Week nav: `‹ ›` loads prev/next week. If current week is fully filled, auto-advance to next week. ### Desktop - Right detail panel enters **day-picker state** - Active recipe card: 2px green border, green-tint bg; `Zur Woche +` button becomes green filled `Tag wählen…` - Other recipe cards: 45% opacity - Panel shows: recipe name header + mini 7-day strip + confirm button + **variety preview** (`↑ Score: 8 → 9 · Neues Protein` via `GET /api/variety/preview?add={id}&date={date}`) - Variety preview is desktop-only; omitted on mobile to keep the sheet compact ### API ``` PATCH /api/week-plan/{weekId}/slots/{date} { recipe_id } (add or replace) DELETE /api/week-plan/{weekId}/slots/{date} (undo) GET /api/variety/preview?add={recipeId}&date={date} (desktop only) ``` --- ## Desktop panel state machine ``` idle ↓ empty tile click / "+" button recipe-picker ←→ (× closes back to idle) ↓ recipe row click day-detail day-detail ↓ "Swap" button click → J4 swap flow (separate) idle / day-detail ↑ "Zur Woche +" on recipe card (from Recipes tab) day-picker ←→ (× closes back to previous state) ↓ confirm day-detail (for newly filled date) ``` --- ## Tap counts | Flow | Mobile | Desktop | |---|---|---| | C4 via empty slot | 2 (slot → pick recipe) | 2 (tile → click row) | | C4 via "+" button | 2 ("+" → pick recipe, day pre-selected) | 2 | | C6 empty slot | 2 ("Zur Woche +" → pick day) | 2 | | C6 filled slot | 3 ("Zur Woche +" → pick day → confirm replace) | 3 | --- ## Open questions for discussion 1. **C4 entry via "+":** When no day is pre-selected (user taps `+` in the nav, not a specific slot), should the sheet pre-select the next empty day automatically, or should it require the user to first select a day from a strip at the top of the sheet? 2. **C5 button labels:** "Zur Woche +" is short enough for mobile cards at 320px. Alternative: "Einplanen". Does the team prefer the explicit `+` suffix or a verb-only label? 3. **C6 fully-filled week:** If the current week has no empty slots, auto-advance to next week on sheet open? Or show the current week with all filled slots (all selectable for replace)? 4. **Variety preview in C6 mobile:** Currently omitted for compactness. Could show a single-line score delta ("↑ +1 Punkt") below the confirm button without adding much height. Worth adding? 5. **C2 vs C4:** The existing C2 spec (j2-plan-the-week.html) defines a full-page suggestions screen navigated to from C1. C4 proposes a bottom sheet instead. Should C2 be retired in favour of C4, or kept as an alternative "deep browse" mode accessible from C4's "Alle Rezepte" section?
Author
Owner

Spec committed: specs/frontend/j2-add-meal.html (commit 693ec2b)

Spec committed: [`specs/frontend/j2-add-meal.html`](http://heim-nas:3005/marcel/mealprep/src/branch/master/specs/frontend/j2-add-meal.html) (commit `693ec2b`)
Author
Owner

🎨 Atlas — UI/UX Designer

Overall this spec is solid and consistent with the existing system. A few things I want the team to think about before we implement.

Design system consistency

  • C2 retirement (Open Question #5) — I'd advocate for retiring C2 as a separate screen. The bottom sheet in C4 already covers the "which recipe for this day?" question, and the full-library browse is accessible via the search input. A separate full-page suggestions screen creates two different mental models for the same task. My recommendation: C4 replaces C2; the C2 spec in j2-plan-the-week.html should be marked deprecated and eventually removed.

  • Sheet height inconsistency — C4 is specced at 75vh, C6 at ~55vh. The difference is intentional (C4 needs room for the two-section recipe list; C6 only needs a 7-chip strip and confirm button). But the drag handle and dim overlay need to be the exact same component. Make sure Kai builds one BottomSheet wrapper component and both screens use it, rather than two separate implementations.

  • "Alle Rezepte" in C4 — missing variety warnings — The spec says the full library section has no variety badges. I'd reconsider. A recipe like "Hähnchen-Curry" that creates a conflict should still carry a subtle warning even when browsed outside the Empfohlen section — at minimum a icon without the verbose badge. Otherwise the user can unknowingly select something bad. The badge should be optional (show when delta ≤ 0 only), not section-gated.

Accessibility gaps

  • Day chips in C6 — The .empty / .filled / .sel-empty / .sel-filled states rely entirely on color and border style. These need aria-label attributes: aria-label="Samstag, 5. April – leer" / aria-label="Mittwoch, 2. April – belegt: Tomaten-Pasta". The dashed vs solid border is invisible to screen readers and insufficient for colorblind users alone.

  • Bottom sheet focus management — When the sheet opens, focus must move to the first interactive element (the close button or search input). When it closes, focus must return to the trigger element (the + button or empty slot). This isn't specced and needs to be.

  • Drag handle — The 32×4px handle is too small a tap target. Add aria-hidden="true" to it and provide an explicit close button (×) as the keyboard-accessible dismiss mechanism (already shown in the spec — just making sure it's not dropped in implementation).

Open question responses

  • Q1 (C4 "+" pre-selection): Auto-select the next empty day silently. Showing a day-strip at the top of the recipe picker adds a step and defeats the tap count target. If all days are filled, the sheet should not open at all — instead show a brief inline message: "Alle Tage dieser Woche sind bereits belegt." with week navigation.

  • Q2 (C5 button labels): Keep "Zur Woche +" — "Einplanen" is vaguer and doesn't communicate that it goes to the current week's plan specifically. The + makes the additive action legible at a glance.

  • Q4 (Variety preview in C6 mobile): Add the single-line delta below the confirm button. One line (↑ +1 Punkt in green or ⚠ −1 Punkt in yellow) is worth the 20px height. Users making a deliberate planning choice from the recipe screen deserve this signal.

## 🎨 Atlas — UI/UX Designer Overall this spec is solid and consistent with the existing system. A few things I want the team to think about before we implement. ### Design system consistency - **C2 retirement (Open Question #5)** — I'd advocate for retiring C2 as a separate screen. The bottom sheet in C4 already covers the "which recipe for this day?" question, and the full-library browse is accessible via the search input. A separate full-page suggestions screen creates two different mental models for the same task. My recommendation: C4 replaces C2; the C2 spec in `j2-plan-the-week.html` should be marked deprecated and eventually removed. - **Sheet height inconsistency** — C4 is specced at 75vh, C6 at ~55vh. The difference is intentional (C4 needs room for the two-section recipe list; C6 only needs a 7-chip strip and confirm button). But the drag handle and dim overlay need to be the exact same component. Make sure Kai builds one `BottomSheet` wrapper component and both screens use it, rather than two separate implementations. - **"Alle Rezepte" in C4 — missing variety warnings** — The spec says the full library section has no variety badges. I'd reconsider. A recipe like "Hähnchen-Curry" that creates a conflict should still carry a subtle warning even when browsed outside the Empfohlen section — at minimum a `⚠` icon without the verbose badge. Otherwise the user can unknowingly select something bad. The badge should be optional (show when delta ≤ 0 only), not section-gated. ### Accessibility gaps - **Day chips in C6** — The `.empty` / `.filled` / `.sel-empty` / `.sel-filled` states rely entirely on color and border style. These need `aria-label` attributes: `aria-label="Samstag, 5. April – leer"` / `aria-label="Mittwoch, 2. April – belegt: Tomaten-Pasta"`. The dashed vs solid border is invisible to screen readers and insufficient for colorblind users alone. - **Bottom sheet focus management** — When the sheet opens, focus must move to the first interactive element (the close button or search input). When it closes, focus must return to the trigger element (the `+` button or empty slot). This isn't specced and needs to be. - **Drag handle** — The 32×4px handle is too small a tap target. Add `aria-hidden="true"` to it and provide an explicit close button (`×`) as the keyboard-accessible dismiss mechanism (already shown in the spec — just making sure it's not dropped in implementation). ### Open question responses - **Q1 (C4 "+" pre-selection):** Auto-select the next empty day silently. Showing a day-strip at the top of the recipe picker adds a step and defeats the tap count target. If all days are filled, the sheet should not open at all — instead show a brief inline message: `"Alle Tage dieser Woche sind bereits belegt."` with week navigation. - **Q2 (C5 button labels):** Keep "Zur Woche +" — "Einplanen" is vaguer and doesn't communicate that it goes to the *current week's plan* specifically. The `+` makes the additive action legible at a glance. - **Q4 (Variety preview in C6 mobile):** Add the single-line delta below the confirm button. One line (`↑ +1 Punkt` in green or `⚠ −1 Punkt` in yellow) is worth the 20px height. Users making a deliberate planning choice from the recipe screen deserve this signal.
Author
Owner

💻 Kai — Frontend Engineer

Good spec to work from. My main concern is state management complexity — the panel state machine and shared sheet component need deliberate design decisions before we touch a line of Svelte.

Component architecture

  • One BottomSheet component, three consumers — C4, C6, and J4 swap all use a bottom sheet with a drag handle, dim overlay, and × close. The spec says "do not reuse the swap sheet component" for content, which is right — the contents are different. But the wrapper (dim overlay, border-radius, shadow, drag handle, focus trap, close on backdrop click) should be a single BottomSheet.svelte with a snippet prop for body content. Building three separate sheet wrappers is exactly the kind of duplication that causes divergence.

  • Panel state machine — The desktop detail panel has 4 states: idle | day-detail | recipe-picker | day-picker. This needs to live in a $state variable at the planner page level, not inside individual sub-components. I'm thinking:

    type PanelState =
      | { mode: 'idle' }
      | { mode: 'day-detail'; date: string }
      | { mode: 'recipe-picker'; date: string }
      | { mode: 'day-picker'; recipeId: string }
    

    Each panel content component just receives the resolved state as a prop. No child components reach up to mutate parent state.

  • URL vs in-memory state — Should the panel state be reflected in the URL (e.g. ?panel=recipe-picker&date=2026-04-05)? Pros: deep-linkable, browser back works. Cons: adds complexity, probably unnecessary for v1. My vote: keep it in-memory with $state. If users request deep-linking later, we add it.

Data loading

  • C4 search is client-side — The spec says "client-side filter of visible list". That means we load the full recipe library when the sheet opens. That's fine for small libraries, but we should load it lazily (when sheet first opens, not on page mount). Use $effect that triggers fetch('/api/recipes') on sheet open, cache the result in a module-level store so repeated opens don't re-fetch.

  • C6 variety preview on every day selectionGET /api/variety/preview fires on each chip click on desktop. This is potentially 7 requests if the user browses all days. Debounce by 150ms or cancel the previous request with an AbortController before issuing the next one.

  • SSR — The bottom sheets and panel picker states are purely client-side interactions. The underlying data (recipes, suggestions, week plan) should still load server-side in +page.server.ts. The sheet/panel components themselves should be inside an {#if browser} guard or mounted only client-side to avoid SSR hydration mismatches on the $state panel mode.

Undo toast

  • Is there an existing toast component? I don't see one in the current screens. If not, this is a non-trivial piece of infrastructure: a globally-mounted component, a writable store or Svelte 5 $state context, auto-dismiss timer, and an accessible role="status" live region. Worth flagging as a separate task — it will be needed by J4 too if it isn't already built.

TDD notes

  • Bottom sheet: test that focus moves to first interactive element on open; test that × and backdrop click both call the dismiss callback; test keyboard Escape dismisses.
  • Day picker chips: test that clicking .empty chip sets correct selection state; test that clicking .filled chip shows the replace warning; test confirm button label changes based on selection.
  • Recipe quick actions (C5): test that "Jetzt kochen" navigates to correct route; test that "Zur Woche +" opens the day picker sheet.
  • Undo toast: test that it auto-dismisses after 4s; test that "Rückgängig" calls the undo action.
## 💻 Kai — Frontend Engineer Good spec to work from. My main concern is state management complexity — the panel state machine and shared sheet component need deliberate design decisions before we touch a line of Svelte. ### Component architecture - **One `BottomSheet` component, three consumers** — C4, C6, and J4 swap all use a bottom sheet with a drag handle, dim overlay, and `×` close. The spec says "do not reuse the swap sheet component" for content, which is right — the contents are different. But the _wrapper_ (dim overlay, border-radius, shadow, drag handle, focus trap, close on backdrop click) should be a single `BottomSheet.svelte` with a snippet prop for body content. Building three separate sheet wrappers is exactly the kind of duplication that causes divergence. - **Panel state machine** — The desktop detail panel has 4 states: `idle | day-detail | recipe-picker | day-picker`. This needs to live in a `$state` variable at the planner page level, not inside individual sub-components. I'm thinking: ```ts type PanelState = | { mode: 'idle' } | { mode: 'day-detail'; date: string } | { mode: 'recipe-picker'; date: string } | { mode: 'day-picker'; recipeId: string } ``` Each panel content component just receives the resolved state as a prop. No child components reach up to mutate parent state. - **URL vs in-memory state** — Should the panel state be reflected in the URL (e.g. `?panel=recipe-picker&date=2026-04-05`)? Pros: deep-linkable, browser back works. Cons: adds complexity, probably unnecessary for v1. My vote: keep it in-memory with `$state`. If users request deep-linking later, we add it. ### Data loading - **C4 search is client-side** — The spec says "client-side filter of visible list". That means we load the full recipe library when the sheet opens. That's fine for small libraries, but we should load it lazily (when sheet first opens, not on page mount). Use `$effect` that triggers `fetch('/api/recipes')` on sheet open, cache the result in a module-level store so repeated opens don't re-fetch. - **C6 variety preview on every day selection** — `GET /api/variety/preview` fires on each chip click on desktop. This is potentially 7 requests if the user browses all days. Debounce by 150ms or cancel the previous request with an `AbortController` before issuing the next one. - **SSR** — The bottom sheets and panel picker states are purely client-side interactions. The underlying data (recipes, suggestions, week plan) should still load server-side in `+page.server.ts`. The sheet/panel components themselves should be inside an `{#if browser}` guard or mounted only client-side to avoid SSR hydration mismatches on the `$state` panel mode. ### Undo toast - Is there an existing toast component? I don't see one in the current screens. If not, this is a non-trivial piece of infrastructure: a globally-mounted component, a writable store or Svelte 5 `$state` context, auto-dismiss timer, and an accessible `role="status"` live region. Worth flagging as a separate task — it will be needed by J4 too if it isn't already built. ### TDD notes - Bottom sheet: test that focus moves to first interactive element on open; test that `×` and backdrop click both call the dismiss callback; test keyboard `Escape` dismisses. - Day picker chips: test that clicking `.empty` chip sets correct selection state; test that clicking `.filled` chip shows the replace warning; test confirm button label changes based on selection. - Recipe quick actions (C5): test that "Jetzt kochen" navigates to correct route; test that "Zur Woche +" opens the day picker sheet. - Undo toast: test that it auto-dismisses after 4s; test that "Rückgängig" calls the undo action.
Author
Owner

🔧 Backend Engineer

The API surface is clean and the intent is clear. Before implementation, I want to nail down the semantics of three things: the suggestion algorithm, the PATCH idempotency, and the undo model.

API semantics

  • PATCH /api/week-plan/{weekId}/slots/{date} — replace semantics need a decision. The spec says this endpoint both adds (empty slot) and replaces (filled slot) a recipe. That's fine — but what does it return? I'd go with 200 OK + the updated slot as a WeekPlanSlotDTO. A 204 No Content is tempting but gives the frontend nothing to work with for the panel transition. Also: should the endpoint reject a recipe_id that doesn't belong to the household? That's an authorization question, but the shape of the 403 response needs to be consistent with the rest of the API.

  • DELETE /api/week-plan/{weekId}/slots/{date} for undo. This is semantically "clear the slot", not "delete a resource" — DELETE is appropriate if clearing is idempotent. But the undo scenario is: the user replaced recipe A with recipe B, then taps undo. The spec implies DELETE just clears the slot (slot goes back to empty), not restores to recipe A. Is that the intended behavior? A user who accidentally replaced a planned meal might expect the original recipe back. Worth deciding explicitly — if restoration is needed, we need to store previous state server-side, which is a bigger lift.

  • GET /api/suggestions?week={id}&day={date} — algorithm spec missing. The endpoint is listed but variety_delta is not defined anywhere in the backend. This is business logic that lives in a VarietyService (or SuggestionService). Before implementing, we need to define: what does variety_delta measure? Ingredient overlap score? Protein category diversity? Without a clear algorithm spec, two developers will implement this differently. I'd propose: variety_delta = (projected_score_with_recipe) - (current_score) — but that needs the variety score algorithm to already be tested and stable.

  • {weekId} vs {date} — mixed identifiers. The slots endpoint uses PATCH /api/week-plan/{weekId}/slots/{date} where weekId is a plan ID and date is a specific day. The variety preview uses GET /api/variety/preview?add={recipeId}&date={date} with no weekId. The backend needs to derive the week context from the date parameter alone for the preview endpoint. Make sure the service layer consistently resolves week context from date — don't let two different resolution strategies live in the codebase.

Authorization

  • Household isolation on weekId — the WeekPlanService must verify that the authenticated user's household owns the weekId before any read or write. This check belongs in the service layer, not just the controller. If weekId is an opaque UUID (not sequential), enumeration is harder, but it's not a substitute for explicit authorization.

  • Role check on PATCH/DELETE — members have read-only access to C1. The spec says member role hides edit actions in the UI, but the backend must enforce this independently. PATCH and DELETE on week plan slots should return 403 for members, not just be hidden in the frontend.

Suggestions

  • variety_delta should be pre-computed or fast. Computing a score delta for every recipe in the library on each GET /api/suggestions request is potentially expensive. Consider: limit suggestions to the top N by delta (already implied by "2–4 items"), and only compute delta for recipes that pass a lightweight pre-filter (e.g., already indexed by protein category). Don't optimize prematurely, but don't design an algorithm that requires a full table scan per request either.

  • Open question #3 (fully-filled week in C6): From a backend perspective, auto-advance to next week should happen client-side based on the slot data returned by GET /api/week-plan/{weekId}. The backend doesn't need a special "auto-advance" endpoint — the frontend reads the slot states and navigates accordingly. Keep the backend dumb here.

## 🔧 Backend Engineer The API surface is clean and the intent is clear. Before implementation, I want to nail down the semantics of three things: the suggestion algorithm, the PATCH idempotency, and the undo model. ### API semantics - **`PATCH /api/week-plan/{weekId}/slots/{date}` — replace semantics need a decision.** The spec says this endpoint both adds (empty slot) and replaces (filled slot) a recipe. That's fine — but what does it return? I'd go with `200 OK` + the updated slot as a `WeekPlanSlotDTO`. A `204 No Content` is tempting but gives the frontend nothing to work with for the panel transition. Also: should the endpoint reject a `recipe_id` that doesn't belong to the household? That's an authorization question, but the shape of the 403 response needs to be consistent with the rest of the API. - **`DELETE /api/week-plan/{weekId}/slots/{date}` for undo.** This is semantically "clear the slot", not "delete a resource" — `DELETE` is appropriate if clearing is idempotent. But the undo scenario is: the user replaced recipe A with recipe B, then taps undo. The spec implies `DELETE` just clears the slot (slot goes back to empty), not restores to recipe A. Is that the intended behavior? A user who accidentally replaced a planned meal might expect the original recipe back. Worth deciding explicitly — if restoration is needed, we need to store previous state server-side, which is a bigger lift. - **`GET /api/suggestions?week={id}&day={date}` — algorithm spec missing.** The endpoint is listed but `variety_delta` is not defined anywhere in the backend. This is business logic that lives in a `VarietyService` (or `SuggestionService`). Before implementing, we need to define: what does variety_delta measure? Ingredient overlap score? Protein category diversity? Without a clear algorithm spec, two developers will implement this differently. I'd propose: `variety_delta = (projected_score_with_recipe) - (current_score)` — but that needs the variety score algorithm to already be tested and stable. - **`{weekId}` vs `{date}` — mixed identifiers.** The slots endpoint uses `PATCH /api/week-plan/{weekId}/slots/{date}` where `weekId` is a plan ID and `date` is a specific day. The variety preview uses `GET /api/variety/preview?add={recipeId}&date={date}` with no `weekId`. The backend needs to derive the week context from the `date` parameter alone for the preview endpoint. Make sure the service layer consistently resolves week context from date — don't let two different resolution strategies live in the codebase. ### Authorization - **Household isolation on `weekId`** — the `WeekPlanService` must verify that the authenticated user's household owns the `weekId` before any read or write. This check belongs in the service layer, not just the controller. If `weekId` is an opaque UUID (not sequential), enumeration is harder, but it's not a substitute for explicit authorization. - **Role check on PATCH/DELETE** — members have read-only access to C1. The spec says member role hides edit actions in the UI, but the backend must enforce this independently. `PATCH` and `DELETE` on week plan slots should return `403` for members, not just be hidden in the frontend. ### Suggestions - **`variety_delta` should be pre-computed or fast.** Computing a score delta for every recipe in the library on each `GET /api/suggestions` request is potentially expensive. Consider: limit suggestions to the top N by delta (already implied by "2–4 items"), and only compute delta for recipes that pass a lightweight pre-filter (e.g., already indexed by protein category). Don't optimize prematurely, but don't design an algorithm that requires a full table scan per request either. - **Open question #3 (fully-filled week in C6):** From a backend perspective, auto-advance to next week should happen client-side based on the slot data returned by `GET /api/week-plan/{weekId}`. The backend doesn't need a special "auto-advance" endpoint — the frontend reads the slot states and navigates accordingly. Keep the backend dumb here.
Author
Owner

🧪 QA Engineer

No acceptance criteria are defined in the spec. That needs to change before implementation starts — I can't write tests without knowing what "done" looks like. Here's what I'd expect, plus every edge case I see.

Missing acceptance criteria

The spec describes UI behavior thoroughly but never states: "this feature is complete when…". Before we build, let's agree on at minimum:

  1. A user can add a recipe to an empty slot from C1 in ≤ 2 taps on both mobile and desktop
  2. A user can add a recipe to any slot from the recipe list in ≤ 2 taps (empty) or ≤ 3 taps (replace)
  3. The undo toast appears after every successful add/replace and correctly reverses the action
  4. A member role cannot add or replace meals (403 from backend, hidden UI)
  5. The variety badge in C4 accurately reflects the projected score delta

Edge cases I want covered

C4 — Recipe picker:

  • Search with no results → empty state message, not a blank list
  • GET /api/suggestions returns an error (network fail, 500) → graceful degradation: show "Alle Rezepte" only, no crash
  • The week has 0 empty slots and user taps "+" → what happens? (Atlas suggested an inline message — needs a test)
  • Adding a recipe while another household member is doing the same slot simultaneously → last-write-wins is acceptable, but the UI should reflect the final state after the PATCH response, not assume success

C6 — Day picker:

  • Week strip shows 7 days but current week starts mid-week (e.g., plan starts Wednesday) → do previous days appear as disabled? greyed out?
  • The ‹ › week navigation: what's the minimum week? Can a user navigate to a past week and add a meal there? If not, the button needs to be disabled for current/past weeks.
  • Replace confirmation: the warning shows "Ersetzt Tomaten-Pasta" — what if the recipe name is 60 characters long? Overflow behavior of dp-warn is not specced.
  • Undo after replace: spec says DELETE clears the slot. Is the replaced recipe (Tomaten-Pasta in the example) recoverable? If undo = clear (not restore), the user loses data. This needs explicit documentation at minimum.

C5 — Quick actions:

  • "Jetzt kochen" on a recipe that has never been cooked → J3 should handle this, but verify the navigation doesn't crash
  • "Zur Woche +" when there is no active week plan (household just created, no plan for the current week) → does C6 create a new plan, or does the backend return 404?

Undo toast:

  • User taps undo after 3.9 seconds → undo should work (within 4s window)
  • User navigates away before 4s → toast disappears, undo is lost — this should be considered acceptable behavior per spec, but it needs a test confirming DELETE is not called after navigation
  • Two rapid add actions → do two separate toasts appear? Does undo only reverse the most recent?

E2E test plan (Playwright)

Critical paths to cover:

  1. @critical — Mobile: open planner → tap empty Saturday → sheet appears → tap recipe → slot fills → undo toast → tap Rückgängig → slot empties
  2. @critical — Desktop: click empty Saturday tile → panel shows picker → click recipe → panel shows day-detail for Saturday
  3. @critical — Recipe list → "Zur Woche +" on recipe → day picker → select empty day → confirm → recipe appears in planner
  4. @smoke — Recipe list → "Jetzt kochen" → navigates to cook mode
  5. @full — Day picker: select filled day → replace warning appears → confirm → old recipe gone, new recipe in slot → undo → slot empties (not restores)

Use data-testid attributes on: the + button, each day chip (keyed by date), the sheet overlay, the confirm button, the undo toast, and the "Rückgängig" link.

## 🧪 QA Engineer No acceptance criteria are defined in the spec. That needs to change before implementation starts — I can't write tests without knowing what "done" looks like. Here's what I'd expect, plus every edge case I see. ### Missing acceptance criteria The spec describes UI behavior thoroughly but never states: "this feature is complete when…". Before we build, let's agree on at minimum: 1. A user can add a recipe to an empty slot from C1 in ≤ 2 taps on both mobile and desktop 2. A user can add a recipe to any slot from the recipe list in ≤ 2 taps (empty) or ≤ 3 taps (replace) 3. The undo toast appears after every successful add/replace and correctly reverses the action 4. A member role cannot add or replace meals (403 from backend, hidden UI) 5. The variety badge in C4 accurately reflects the projected score delta ### Edge cases I want covered **C4 — Recipe picker:** - Search with no results → empty state message, not a blank list - `GET /api/suggestions` returns an error (network fail, 500) → graceful degradation: show "Alle Rezepte" only, no crash - The week has 0 empty slots and user taps "+" → what happens? (Atlas suggested an inline message — needs a test) - Adding a recipe while another household member is doing the same slot simultaneously → last-write-wins is acceptable, but the UI should reflect the final state after the PATCH response, not assume success **C6 — Day picker:** - Week strip shows 7 days but current week starts mid-week (e.g., plan starts Wednesday) → do previous days appear as disabled? greyed out? - The `‹ ›` week navigation: what's the minimum week? Can a user navigate to a past week and add a meal there? If not, the `‹` button needs to be disabled for current/past weeks. - Replace confirmation: the warning shows "Ersetzt **Tomaten-Pasta**" — what if the recipe name is 60 characters long? Overflow behavior of `dp-warn` is not specced. - Undo after replace: spec says `DELETE` clears the slot. Is the replaced recipe (Tomaten-Pasta in the example) recoverable? If undo = clear (not restore), the user loses data. This needs explicit documentation at minimum. **C5 — Quick actions:** - "Jetzt kochen" on a recipe that has never been cooked → J3 should handle this, but verify the navigation doesn't crash - "Zur Woche +" when there is no active week plan (household just created, no plan for the current week) → does C6 create a new plan, or does the backend return 404? **Undo toast:** - User taps undo after 3.9 seconds → undo should work (within 4s window) - User navigates away before 4s → toast disappears, undo is lost — this should be considered acceptable behavior per spec, but it needs a test confirming `DELETE` is not called after navigation - Two rapid add actions → do two separate toasts appear? Does undo only reverse the most recent? ### E2E test plan (Playwright) Critical paths to cover: 1. `@critical` — Mobile: open planner → tap empty Saturday → sheet appears → tap recipe → slot fills → undo toast → tap Rückgängig → slot empties 2. `@critical` — Desktop: click empty Saturday tile → panel shows picker → click recipe → panel shows day-detail for Saturday 3. `@critical` — Recipe list → "Zur Woche +" on recipe → day picker → select empty day → confirm → recipe appears in planner 4. `@smoke` — Recipe list → "Jetzt kochen" → navigates to cook mode 5. `@full` — Day picker: select filled day → replace warning appears → confirm → old recipe gone, new recipe in slot → undo → slot empties (not restores) Use `data-testid` attributes on: the `+` button, each day chip (keyed by date), the sheet overlay, the confirm button, the undo toast, and the "Rückgängig" link.
Author
Owner

🔐 Sable — Security Engineer

This feature introduces three new write endpoints and one new read endpoint. My main concerns are IDOR on the week plan, cross-household recipe injection, and the missing role check on the mutation endpoints.

Broken access control (OWASP #1)

  • IDOR on weekIdPATCH /api/week-plan/{weekId}/slots/{date} takes a weekId in the path. If this ID is sequential or guessable (e.g., an auto-increment integer), an authenticated user from household A could modify household B's week plan by incrementing the ID. Mitigation: use UUIDs for weekId, and — more importantly — validate in the service layer that weekId belongs to the authenticated user's household on every request. UUID alone is not authorization.

  • Cross-household recipe injectionPATCH /api/week-plan/{weekId}/slots/{date} { recipe_id } accepts a recipe_id in the request body. The backend must verify that recipe_id belongs to the authenticated user's household before writing it to the slot. If it doesn't, an attacker could enumerate recipes from other households and assign them to their own plan — exposing the existence of another household's recipes (information leak) and potentially corrupting the variety score logic with foreign data.

  • Member role on mutation endpoints — The spec says members see a read-only C1. But the frontend hiding buttons is not a security control. PATCH and DELETE on week plan slots must return 403 Forbidden for authenticated users with the member role. This check belongs in the service layer, enforced independently of the UI. Verify it's covered in integration tests: shouldReturn403WhenMemberAttemptsToAddMeal().

  • GET /api/suggestions?week={id}&day={date} — same household isolation requirement as above. A member from household B passing household A's week={id} must get a 403 or 404, not a list of suggestions computed from household A's meal history.

Information leakage

  • GET /api/variety/preview?add={recipeId}&date={date} — this endpoint computes a score delta. If it returns a meaningful response for a recipeId that doesn't belong to the user's household, it leaks: (a) the recipe exists, (b) its ingredient composition (indirectly, via score delta). Validate that recipeId belongs to the caller's household before computing anything.

  • Error responses — The replace warning message "Ersetzt Tomaten-Pasta" is rendered client-side from the existing slot data (already loaded in C1). That's fine — no server-side information leak there. But if the PATCH returns a 409 or 422 (e.g., slot already modified by another member), make sure the error response body doesn't include internal identifiers or stack traces.

Undo endpoint

  • DELETE /api/week-plan/{weekId}/slots/{date} — same household ownership check required as the PATCH. Also: confirm this endpoint is not callable by unauthenticated users. A DELETE with no session should return 401, not 403 or 200.

No new attack surface beyond the above

  • No file uploads, no user-controlled URLs, no WebSocket connections introduced here.
  • The reason text in variety badges comes from the backend algorithm, not from user-entered recipe names — so no XSS vector there, as long as recipe names are properly escaped when rendered (use Svelte's default text interpolation, not {@html}).
  • The search input in C4 is a client-side filter over already-fetched data — no server-side query injection possible from this input.
## 🔐 Sable — Security Engineer This feature introduces three new write endpoints and one new read endpoint. My main concerns are IDOR on the week plan, cross-household recipe injection, and the missing role check on the mutation endpoints. ### Broken access control (OWASP #1) - **IDOR on `weekId`** — `PATCH /api/week-plan/{weekId}/slots/{date}` takes a `weekId` in the path. If this ID is sequential or guessable (e.g., an auto-increment integer), an authenticated user from household A could modify household B's week plan by incrementing the ID. Mitigation: use UUIDs for `weekId`, and — more importantly — validate in the service layer that `weekId` belongs to the authenticated user's household on every request. UUID alone is not authorization. - **Cross-household recipe injection** — `PATCH /api/week-plan/{weekId}/slots/{date} { recipe_id }` accepts a `recipe_id` in the request body. The backend must verify that `recipe_id` belongs to the authenticated user's household before writing it to the slot. If it doesn't, an attacker could enumerate recipes from other households and assign them to their own plan — exposing the existence of another household's recipes (information leak) and potentially corrupting the variety score logic with foreign data. - **Member role on mutation endpoints** — The spec says members see a read-only C1. But the frontend hiding buttons is not a security control. `PATCH` and `DELETE` on week plan slots must return `403 Forbidden` for authenticated users with the `member` role. This check belongs in the service layer, enforced independently of the UI. Verify it's covered in integration tests: `shouldReturn403WhenMemberAttemptsToAddMeal()`. - **`GET /api/suggestions?week={id}&day={date}`** — same household isolation requirement as above. A member from household B passing household A's `week={id}` must get a 403 or 404, not a list of suggestions computed from household A's meal history. ### Information leakage - **`GET /api/variety/preview?add={recipeId}&date={date}`** — this endpoint computes a score delta. If it returns a meaningful response for a `recipeId` that doesn't belong to the user's household, it leaks: (a) the recipe exists, (b) its ingredient composition (indirectly, via score delta). Validate that `recipeId` belongs to the caller's household before computing anything. - **Error responses** — The replace warning message "Ersetzt **Tomaten-Pasta**" is rendered client-side from the existing slot data (already loaded in C1). That's fine — no server-side information leak there. But if the PATCH returns a 409 or 422 (e.g., slot already modified by another member), make sure the error response body doesn't include internal identifiers or stack traces. ### Undo endpoint - **`DELETE /api/week-plan/{weekId}/slots/{date}`** — same household ownership check required as the PATCH. Also: confirm this endpoint is not callable by unauthenticated users. A `DELETE` with no session should return `401`, not `403` or `200`. ### No new attack surface beyond the above - No file uploads, no user-controlled URLs, no WebSocket connections introduced here. - The `reason` text in variety badges comes from the backend algorithm, not from user-entered recipe names — so no XSS vector there, as long as recipe names are properly escaped when rendered (use Svelte's default text interpolation, not `{@html}`). - The search input in C4 is a client-side filter over already-fetched data — no server-side query injection possible from this input.
Author
Owner

🗄️ Backend Engineer — Discussion Summary

Working through the open backend items raised during review. Six items discussed and resolved.


Resolved

  1. PATCH return value — Response envelope is { slot: WeekPlanSlotDTO, weekVarietyScore: double }. Single round-trip gives the client both the updated slot and the refreshed week score. No separate GET needed.

  2. Undo window — Not time-limited. Undo persists as long as the user stays in the current screen. Semantics: Undo Add (was empty) = DELETE slot; Undo Replace (was filled) = PATCH slot with client-held previousRecipeId.

  3. variety_delta algorithmsimulateVarietyScore() already exists in PlanningService and is used in getSuggestions(). Only missing piece: expose scoreDelta = simulatedScore - currentScore as a field on SuggestionResponse.SuggestionItem. No new scoring logic needed.

  4. Week plan auto-creation — No auto-create on "Zur Woche +". When no active plan exists, the button switches label to "Wochenplan erstellen" and routes the user to plan creation first. Avoids orphaned slots.

  5. Variety preview endpoint ��� Add weekId as a query param for consistency with other planning endpoints. Signature: GET /api/plans/{planId}/variety-preview?recipeId=&date=&weekId=.

  6. Household isolation — Follow the established pattern: @PreAuthorize("isAuthenticated()") on controller methods for readability; service-layer findPlan(planId, householdId) + findRecipe(recipeId, householdId) for ownership enforcement. No custom PermissionEvaluator needed.


Overall read

The spec is well-scoped. The main backend work is small: add scoreDelta to the suggestion DTO, update the variety preview endpoint signature, and wire up two new controller methods (PATCH/DELETE slot) following the existing patterns. Nothing architecturally novel — the hard parts are already in place.

## 🗄️ Backend Engineer — Discussion Summary Working through the open backend items raised during review. Six items discussed and resolved. --- ### ✅ Resolved 1. **PATCH return value** — Response envelope is `{ slot: WeekPlanSlotDTO, weekVarietyScore: double }`. Single round-trip gives the client both the updated slot and the refreshed week score. No separate GET needed. 2. **Undo window** — Not time-limited. Undo persists as long as the user stays in the current screen. Semantics: Undo Add (was empty) = DELETE slot; Undo Replace (was filled) = PATCH slot with client-held `previousRecipeId`. 3. **variety_delta algorithm** — `simulateVarietyScore()` already exists in `PlanningService` and is used in `getSuggestions()`. Only missing piece: expose `scoreDelta = simulatedScore - currentScore` as a field on `SuggestionResponse.SuggestionItem`. No new scoring logic needed. 4. **Week plan auto-creation** — No auto-create on "Zur Woche +". When no active plan exists, the button switches label to "Wochenplan erstellen" and routes the user to plan creation first. Avoids orphaned slots. 5. **Variety preview endpoint** ��� Add `weekId` as a query param for consistency with other planning endpoints. Signature: `GET /api/plans/{planId}/variety-preview?recipeId=&date=&weekId=`. 6. **Household isolation** — Follow the established pattern: `@PreAuthorize("isAuthenticated()")` on controller methods for readability; service-layer `findPlan(planId, householdId)` + `findRecipe(recipeId, householdId)` for ownership enforcement. No custom `PermissionEvaluator` needed. --- ### Overall read The spec is well-scoped. The main backend work is small: add `scoreDelta` to the suggestion DTO, update the variety preview endpoint signature, and wire up two new controller methods (PATCH/DELETE slot) following the existing patterns. Nothing architecturally novel — the hard parts are already in place.
Author
Owner

💻 Kai — Frontend Engineer — Implementation Discussion Summary

Working through the open frontend architecture decisions before touching any code. Seven items discussed and resolved.


Resolved

  1. BottomSheet component API — One shared BottomSheet.svelte wrapper. Props: open: boolean, onclose: () => void, height: string (default '75vh'), children snippet. Wrapper owns: dim overlay (fixed opacity — Atlas picks the value), drag handle, focus trap on open, focus return on close, Escape key dismiss. C4, C6, and J4 swap all use it; content is provided by each consumer via the children snippet.

  2. Panel state machine — Discriminated union at planner page level:

    type PanelState =
      | { mode: 'idle' }
      | { mode: 'day-detail'; date: string }
      | { mode: 'recipe-picker'; date: string }
      | { mode: 'day-picker'; recipeId: string }
    

    Each page owns its own $state<PanelState> independently. No shared cross-page panel store. Child components receive resolved state as props and call an onTransition callback — no child mutates parent state directly.

  3. URL vs in-memory panel state — In-memory $state only. No query param reflection for v1. Browser back while panel is open = close panel + navigate back, which is acceptable.

  4. Undo bar — No global toast infrastructure. Local undo button rendered in the screen, driven by page-level $state<UndoAction | null>. Set on PATCH success, cleared on navigation away. Type:

    type UndoAction =
      | { type: 'add'; weekId: string; date: string }
      | { type: 'replace'; weekId: string; date: string; previousRecipeId: string }
    
  5. "Zur Woche +" active plan check — Recipes page +page.server.ts loads { recipes, activePlan: WeekPlanSummary | null }. Button renders "Zur Woche +" when activePlan is set, "Wochenplan erstellen" when null. No client-side fetch, no hydration flicker.

  6. C4 lazy loading + C6 debounce — Recipe library (GET /api/recipes) loaded on sheet first-open via $effect, cached in a module-level variable so repeat opens don't re-fetch. Variety preview (GET /api/variety/preview) on desktop chip selection uses AbortController — cancel in-flight request before issuing next one. No debounce timer needed.

  7. C2 retirement/planner/suggestions route is already implemented but will be deleted as part of this feature. Files to remove: +page.svelte, +page.server.ts, page.server.test.ts under src/routes/(app)/planner/suggestions/. Any links to this route are removed or redirected. C4 bottom sheet is the replacement.


Overall read

The spec is implementable as written. The main frontend work is: BottomSheet.svelte wrapper, PanelState machine in the planner page, the two sheet content components (C4 RecipePicker, C6 DayPicker), C5 quick action buttons, and C2 deletion. No new routing complexity, no global state infrastructure.

## 💻 Kai — Frontend Engineer — Implementation Discussion Summary Working through the open frontend architecture decisions before touching any code. Seven items discussed and resolved. --- ### ✅ Resolved 1. **BottomSheet component API** — One shared `BottomSheet.svelte` wrapper. Props: `open: boolean`, `onclose: () => void`, `height: string` (default `'75vh'`), `children` snippet. Wrapper owns: dim overlay (fixed opacity — Atlas picks the value), drag handle, focus trap on open, focus return on close, `Escape` key dismiss. C4, C6, and J4 swap all use it; content is provided by each consumer via the `children` snippet. 2. **Panel state machine** — Discriminated union at planner page level: ```ts type PanelState = | { mode: 'idle' } | { mode: 'day-detail'; date: string } | { mode: 'recipe-picker'; date: string } | { mode: 'day-picker'; recipeId: string } ``` Each page owns its own `$state<PanelState>` independently. No shared cross-page panel store. Child components receive resolved state as props and call an `onTransition` callback — no child mutates parent state directly. 3. **URL vs in-memory panel state** — In-memory `$state` only. No query param reflection for v1. Browser back while panel is open = close panel + navigate back, which is acceptable. 4. **Undo bar** — No global toast infrastructure. Local undo button rendered in the screen, driven by page-level `$state<UndoAction | null>`. Set on PATCH success, cleared on navigation away. Type: ```ts type UndoAction = | { type: 'add'; weekId: string; date: string } | { type: 'replace'; weekId: string; date: string; previousRecipeId: string } ``` 5. **"Zur Woche +" active plan check** — Recipes page `+page.server.ts` loads `{ recipes, activePlan: WeekPlanSummary | null }`. Button renders "Zur Woche +" when `activePlan` is set, "Wochenplan erstellen" when null. No client-side fetch, no hydration flicker. 6. **C4 lazy loading + C6 debounce** — Recipe library (`GET /api/recipes`) loaded on sheet first-open via `$effect`, cached in a module-level variable so repeat opens don't re-fetch. Variety preview (`GET /api/variety/preview`) on desktop chip selection uses `AbortController` — cancel in-flight request before issuing next one. No debounce timer needed. 7. **C2 retirement** — `/planner/suggestions` route is already implemented but will be deleted as part of this feature. Files to remove: `+page.svelte`, `+page.server.ts`, `page.server.test.ts` under `src/routes/(app)/planner/suggestions/`. Any links to this route are removed or redirected. C4 bottom sheet is the replacement. --- ### Overall read The spec is implementable as written. The main frontend work is: `BottomSheet.svelte` wrapper, `PanelState` machine in the planner page, the two sheet content components (C4 RecipePicker, C6 DayPicker), C5 quick action buttons, and C2 deletion. No new routing complexity, no global state infrastructure.
Sign in to join this conversation.