Frontend: D1 — Shopping list #30
Reference in New Issue
Block a user
Delete Branch "%!s()"
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
Shared shopping checklist generated from the week's meal plan. Pantry staples are auto-filtered. Anyone can add or remove items.
Journey: J5 — Generate shopping list
Role: Planner generates / All household members can view, check off, and add items
Screen: D1
Layout
Mobile (< 768px)
Desktop (> 1024px)
--color-pagebg, 20px 24px padding):--color-surfacebg, border-left, 20px padding):Checklist Rows
Shopping List Generation
Custom Items
Acceptance Criteria
Spec file:
specs/frontend/j5-shopping-list.html— screen D1 with mobile (checklist) desktop (split checklist + recipe reference panel) previews, agent table, and LLM implementation guide.Frontend: D1 — Shopping list (live shared checklist)to Frontend: D1 — Shopping list🎨 Atlas — UI/UX Designer
Questions & Observations
Suggestions
🖥️ Kai — Senior Frontend Engineer
Questions & Observations
loadfunction in+page.server.tsfetches the list on navigation/refresh, and form actions handle check/uncheck/add. Clean and simple.use:enhance) that POSTs to the backend on every toggle, or should we batch changes client-side and sync on a "Save" action? Individual form actions per checkbox are the simplest approach and fit the "no live list" model — each check is a server round-trip, and the page state updates viainvalidateAll()or returned data.Suggestions
+page.server.tsactions) for all mutations: check item, uncheck item, add custom item, generate list. Each action calls the backend API with the session cookie. Useuse:enhancefor progressive enhancement — the page works without JS.+page.server.tsfetches the full list from the backend API. No client-side state management needed beyond what Svelte's reactivity gives us from thedataprop.ShoppingHeader.svelte(eyebrow counts),ChecklistSection.svelte(unchecked items),CheckedOffSection.svelte(checked items below divider),AddCustomItem.svelte(inline form),RecipeReferencePanel.svelte(desktop right panel),FilteredStaples.svelte(desktop right panel section).hidden lg:block. No need for separate routes or layouts.⚙️ Backend Engineer — Senior Backend Engineer
Questions & Observations
shopping_list(id, household_id, week, created_by, created_at)shopping_list_item(id, list_id, ingredient_name, quantity, unit, checked, checked_by, source_recipe_ids[], is_custom, sort_order)source_recipe_idsbe a join table or auuid[]array? Join table is cleaner for querying "which recipes use this item."pantry_stapletable? The generation endpoint needs to exclude items matching the household's staples list. Matching logic (by name? by ingredient ID?) needs to be consistent with the merging logic above.Suggestions
POST /api/shopping-lists/generate— planner-only, creates/regenerates list for current weekGET /api/shopping-lists/current— returns the current week's list with all itemsPATCH /api/shopping-lists/{listId}/items/{itemId}— toggle checked statePOST /api/shopping-lists/{listId}/items— add custom item@Transactionalon the generation service method — if merging fails midway, the whole operation rolls back. Don't leave a half-generated list in the database.generation_versionorgenerated_attimestamp on the list so the frontend can show "Generated on [date]" and the regenerate action is clearly a destructive replace.🔒 Sable — Security Engineer
Questions & Observations
GET /current— only return the list for the user's householdPATCH /items/{itemId}— verify the item belongs to a list owned by the user's household (not just that the user is authenticated). An attacker could enumerate item IDs across households otherwise (IDOR).POST /items— verify the list belongs to the user's householdPOST /generate— verify planner role AND household membershipgenerateendpoint must reject requests from members with 403. Verify this is enforced in the service layer, not just the controller annotation.{item.name}), never{@html}— Svelte's default escaping handles XSS here, but this should be a review checkpoint.Suggestions
VARCHAR(200)or similar CHECK constraint) and the API validation layer (@Size(max=200)).🧪 QA Engineer — Senior QA Engineer
Questions & Observations
Suggestions
🎨 Atlas — UI/UX Designer (Round 2)
The spec update (
e12fb72) addresses my main concern — liveness is gone, "Shared with household" replaces "N members online" everywhere. The visual previews now match the sync model. A few things remain:Resolved
Still open from Round 1
use:enhanceform action that returns updated data, the answer is likely "immediately via returned server state" — but this should be explicit in the spec.New observation
🖥️ Kai — Senior Frontend Engineer (Round 2)
Spec update looks good — section 4 now clearly says "server-authoritative, form actions, no WebSocket/SSE." This is exactly the model I'd implement. A few remaining items:
Resolved
use:enhance, page refresh for other users' changesStill open from Round 1
GROUP BYquery that merges server-side, which confirms backend-side merging. Good. But thesource_recipe_idstracking isn't in that query — the generation endpoint needs to return asources: string[]field per item so the frontend can render "For: Tomato pasta, Thai curry."+page.server.tsknow the user is a planner vs member? Is this in the session data fromhooks.server.ts? This needs to be in the layout data so the generate/regenerate button can be conditionally rendered. Not a spec issue per se, but worth confirming before implementation.New observation
i.is_staple = falsefor filtering, implying a boolean on the ingredient table. But section 3 says "Pantry staples (defined in A3/D3)" which suggests a separate household-level staples list. These are different models — a globalis_stapleflag vs a per-household staples list. The backend needs to clarify which one before I can build the correct API contract.⚙️ Backend Engineer — Senior Backend Engineer (Round 2)
The spec update removes real-time broadcast from the data operations —
CHECK OFFandADD CUSTOMare now simple mutations without broadcast. This simplifies the backend significantly: standard REST endpoints, no event system needed.Resolved
Still open from Round 1
ingredient_idfor grouping, which implies a shared ingredient master table. But our current recipe model stores ingredients as free-text strings per recipe (name + quantity + unit). If that's the case, we need to decide: (a) introduce an ingredient master table now, or (b) match by case-insensitive name for v1. Option (b) is simpler but fragile. This is a blocking design decision before implementation.is_staplemodel mismatch (flagged by Kai): The spec SQL usesi.is_staple = false(global flag on ingredient), but A3/D3 defines staples per household. If staples are household-scoped, the query needs to join against ahousehold_stapletable instead. This affects the data model and the generation query.New observation
shopping_list_itemtable doesn't need aversionorupdated_atfor conflict detection. A simplechecked BOOLEAN DEFAULT falseis sufficient. Keeps the schema lean.🔒 Sable — Security Engineer (Round 2)
The spec update is a net positive for security. Removing WebSocket/SSE eliminates an entire authentication surface. The remaining attack surface is standard REST + session cookies, which is already well-handled by the existing Spring Security + SvelteKit hooks setup.
Resolved
Still open from Round 1
item.list.household_id == user.household_id. This is not a spec change — it's an implementation requirement. Flag it for code review.INSERTin section 6 doesn't show constraints. Backend should enforceVARCHAR(200)+@Size(max=200).@PreAuthorizeon the controller (which can be bypassed if another code path calls the service directly).New observation
🧪 QA Engineer — Senior QA Engineer (Round 2)
The spec update addresses the sync model cleanly — section 4 is now "Sync model" with explicit rules. This unblocks writing accurate test cases. Here's where we stand:
Resolved
Still open from Round 1
Updated test plan given spec changes
Recommendation
Update the issue body to match the spec — the stale "real-time" and "members-online" language will cause confusion if someone reads the issue without checking the spec file.
⚙️ Backend Engineer — Discussion Summary
Interactive discussion working through all open backend design decisions. All items resolved.
Resolved items
ingredient_id(UUID), already implemented inShoppingService.generateFromPlan(). Merge key isingredientId + "|" + unit. No name-based matching fragility — the "Karotten vs Möhren" concern from Round 1 doesn't apply since each ingredient has a canonical entry per household.is_stapleboolean on theingrediententity, scoped per household (not a global flag). Already implemented —ingredient.isStaple()check in the generation loop. The spec SQL'si.is_staple = falseis accurate.is_checkedgenerateFromPlan()needs refactoring — it always creates a new list today.shopping_list,shopping_list_item,ingredient) covers the need. One addition:generated_at timestamptzcolumn onshopping_list, updated on each generate/merge. New migration needed.GET /api/shopping-lists?week=2026-W14— returns the list for the given week + household, or 404 if none existsPOST /api/shopping-lists/generatewith{ "week": "2026-W14" }— generates or merges the list for that weekPATCH /api/shopping-lists/{listId}/items/{itemId}/check— toggle checked state (any household member)POST /api/shopping-lists/{listId}/items— add custom item (any household member)DELETE /api/shopping-lists/{listId}/items/{itemId}— remove item (any household member)No unresolved items
The existing codebase already had solid foundations — most of the concerns raised in the review rounds (ingredient matching, staples model, unit handling) were already solved by the data model. The main implementation work is refactoring
generateFromPlan()to support the merge strategy and adding thegenerated_atcolumn.🎨 Atlas — UI/UX Designer Discussion Summary
Interactive discussion resolving all open UI/UX design decisions for D1. All items resolved.
Resolved items
use:enhancereturn data. After checking an item, it moves to the "Checked off" section and counts update based on the server response. No optimistic UI, no waiting for full page refresh.cursor:pointer, no hover effect. Context for shopping, not navigation to recipe detail.--color-text-muted, positioned below the eyebrow ("5 Artikel übrig · 2 abgehakt"). Showsgenerated_atfrom the backend, updated on each regeneration/merge. Format: "Zuletzt aktualisiert: Fr, 4. Apr., 14:32"No unresolved items
The UI decisions are now fully specified. Combined with the backend discussion (merge-on-regenerate, week-based API,
generated_atcolumn), this feature is ready for implementation.Implementation Complete
Branch:
feat/issue-30-shopping-list(15 commits)Backend (10 commits)
2253c76Addgenerated_atcolumn toshopping_list(V022 migration)2f690ebAddForbiddenException+ 403 handler inGlobalExceptionHandler3be9f50Add@RequiresHouseholdRoleannotation withHandlerInterceptor— reusable role guard7e254fcAddfindByHouseholdIdAndWeekPlanWeekStarttoShoppingListRepository93e8bf9ExtendShoppingListResponsewithgeneratedAt,filteredStaplesCount,RecipeRefc26c2e1AddgetByWeekStart()toShoppingService(defaults to current Monday)5325f48RefactorgenerateFromPlan()to merge strategy (preserves custom items + check states)16b70bdAddGET /v1/shopping-listendpoint +@RequiresHouseholdRole("planner")on generate9292253Finalize endpoint naming + regenerate OpenAPI typesFrontend (6 commits)
e831480+page.server.ts— load function (shopping list + week plan) + form actions (check, addItem, generate)2151dffShoppingHeader.svelte— title, eyebrow counts, timestamp, planner-only generate/regenerate button7bdadbeChecklistItem.svelte— checkbox row with name, quantity, recipe source label, strikethrough5ac8f17AddCustomItem.svelte— expandable inline form for custom items6cc7983RecipeReferencePanel.svelte— desktop right panel with recipe cards + filtered staples info7411411+page.svelte— responsive mobile/desktop layout with 3 empty state variantsTest Results
npm run check— 0 errorsAcceptance Criteria Coverage