Frontend: C2 — Meal suggestions (variety-aware) #27
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
Ranked, variety-aware recipe suggestions for filling a meal slot. Filters avoid ingredient repetition, same-protein consecutive days, and balance effort across the week.
Journey: J2 — Plan the week
Role: Planner only
Screen: C2
Layout
Mobile (< 768px)
--green-tintbg, collapsible): day label + filter summaryDesktop (> 1024px)
--color-surfacebg): "This week so far" mini meal cards + "Filter reasons" bullets--color-pagebg): ranked suggestion cardsSuggestion Cards
Sorting (J2 context)
Sorted by variety algorithm (not effort):
Behavior
week_plan_slot→ returns to C1Acceptance Criteria
Spec file:
specs/frontend/j2-plan-the-week.html— screen C2 with mobile (context banner + ranked list) + desktop (split context panel + suggestions) previews, agent table, and LLM implementation guide.👨💻 Kai — Frontend Engineer
C2 is a smart list screen with a two-panel desktop layout and a context-aware mobile banner. The primary complexity is the suggestion card's reasoning badge and keeping the variety score in sync after selection.
Component split for C2
SuggestionContextBanner— mobile-only,--green-tintbg, collapsible, shows day label + filter summary (e.g., "Avoiding chicken · Balancing effort")SuggestionCard— the ranked card: ranking number + recipe name + metadata (time, effort, protein) +ReasoningBadgeReasoningBadge— green badge (good fit) or yellow badge (warning). Needs two visual variants and accepts a text labelWeekSoFarPanel— desktop-only left panel: mini meal cards for the current week + filter reason bulletsC2Layout— orchestrates mobile (full-width list) vs desktop (280px left panel + flex right panel)The "Browse full library" fallback
/recipes?selectFor=slotId) is the cleanest SvelteKit approach. The+page.server.tsfor B1 then reads theselectForparam and switches behavior.week_plan_slot, then redirect back to C1 (not C2). Does that match the intended flow?The reasoning badge — data driven
Collapsible context banner
$state) or an accordion that opens on tap? My lean: simple$statetoggle with a chevron icon. No persistence across page loads needed.Questions
🛠️ Backend Engineer — Suggestions API & Variety Algorithm
C2 is where the variety algorithm does its heaviest work — filtering, scoring, and ranking recipes server-side before sending suggestions to the client. The design of this endpoint is critical to get right.
Suggestions endpoint design
GET /api/week-plans/{weekPlanId}/suggestions?slotDay=MON— returns ranked recipe list for a given day/slotThe variety algorithm — three filter rules
week_plan_slot → recipe → recipe_ingredientfor the adjacent days. This is a join-heavy query — worth checking the query plan with an EXPLAIN ANALYZE.primaryProteinof the previous and next day's planned meal.HARD, surfaceEASYmeals first.These rules produce a score per recipe. The scoring weights (how much does protein repetition penalize vs ingredient overlap?) need to be specified before implementation. Are they hardcoded or configurable?
Planner-only
GET /api/week-plans/{weekPlanId}/suggestionsmust return 403 for members and 404 for plans not belonging to the caller's household.Slot assignment on "Pick"
PUT /api/week-plan-slots/{slotId}/recipe— same endpoint as the swap flow. The body is{ recipeId: UUID }. The service returns the updated variety score in the response so C1 can update immediately."Browse full library" — B1 in selection mode
PUT /api/week-plan-slots/{slotId}/recipeendpoint. No special endpoint needed for the selection mode path.Questions
🧪 QA Engineer — Test Coverage Plan for C2
C2 has the most complex business logic of all the planning screens — the variety algorithm must be tested rigorously because it's the core intelligence of the product. Here's what I want covered.
Backend unit tests — the variety algorithm is the critical path
shouldRankRecipesWithFreshProteinHigherThanRepeatedProtein()shouldPenalizeRecipesWithIngredientsUsedInPreviousThreeDays()shouldSurfaceEasyMealsWhenPreviousTwoDaysWereHard()shouldGenerateGreenBadgeWhenRecipeMeetsAllCriteria()shouldGenerateYellowBadgeWithWarningWhenProteinRepeats()shouldReturnEmptyListWhenHouseholdHasNoRecipes()shouldReturnAllRecipesUnrankedWhenNoDaysArePlannedYet()— no context to filter againstshouldNotSuggestRecipesFromOtherHouseholds()— isolation testBackend integration tests
shouldReturn403WhenMemberRequestsSuggestions()shouldReturn404WhenWeekPlanDoesNotExist()shouldReturn404WhenWeekPlanBelongsToOtherHousehold()shouldReturnSuggestionsOrderedByVarietyScore()shouldUpdateVarietyScoreAfterPickingFromSuggestions()— pick a suggestion, verify score in responseshouldHandleEmptyWeekPlanWithNoPlannedMeals()Frontend component tests
SuggestionCard: renders rank number, recipe name, metadata, reasoning badge (green variant), reasoning badge (yellow warning variant)SuggestionContextBanner: renders day label, filter summary, collapses when toggledWeekSoFarPanel: renders mini cards for planned days, shows filter reasons bulletsselectForparamE2E tests — the planning flow
Algorithm edge cases to define and test
Questions
🔐 Sable — Security Engineer
C2 is a read-heavy planner-only feature. The risk profile is moderate — no write operations on this screen itself (write happens via the slot assignment endpoint), but the suggestions endpoint leaks household recipe data and the "Browse full library" flow introduces a selection mode that needs careful authorization.
Suggestions endpoint — household isolation
GET /api/week-plans/{weekPlanId}/suggestionsmust verify: (1) the week plan exists, (2) it belongs to the caller's household, (3) the caller is a planner. All three checks must happen in the service layer."Browse full library" — B1 in selection mode with a
selectForparameterselectFor=slotIdparameter in the URL must be validated server-side in B1's+page.server.ts. Do not trust the client-providedslotIdwithout verifying: (1) the slot exists, (2) it belongs to the caller's household, (3) the caller is a planner.slotIdis invalid or belongs to another household, B1 in selection mode should reject the param and render in normal browsing mode (no silent failure, but also no exposure).slotIdfrom another household's week plan and attempt to assign a recipe to it.Reasoning badge content — no user-generated text injection
{@html}to render badge text.Slot assignment on pick — same risks as swap
PUT /api/week-plan-slots/{slotId}/recipe— all the same IDOR and authorization checks apply here as in the J4 swap flow. TheslotIdmust be verified against the caller's household, and the caller must be a planner.Information disclosure via suggestions
Questions
slotIdincluded in the URL when navigating to C2 (e.g.,/plan/suggestions?slotId=...), or is it stored in server-side session state? URL params are visible in logs and browser history — ifslotIdis a UUID, that's low risk, but worth being explicit.householdIdto prevent cross-household cache poisoning.🎨 Atlas — UI/UX Designer
C2 is where the app's intelligence becomes visible to the user. The ranking number, reasoning badge, and context panels are doing real UX work — communicating why a suggestion is ranked where it is. The design must make that reasoning feel helpful, not overwhelming.
Ranking number — visual hierarchy
--color-mutedtext, positioned to the left of the card content as an oversized ordinal. This creates the "editorial list" feel appropriate for a curated ranked list.Suggestion card anatomy
--color-surfacebg,--radius-md(6px),--shadow-card, 12px 16px padding--color-text, weight 500--color-muted— "25 min · Easy · Chicken" with·separators--radius-full), 11px, weight 500--green-tintbg,--green-darktext, "✓ Fresh protein · effort balance"--yellow-tintbg,--yellow-texttext, "⚠ Chicken again — 2 days apart"--green-darktext, 13px/500 — not a button, a text link (lightweight action in a list context)Desktop left panel — "This week so far"
--color-muted11px, name in 13px--color-text.--color-muteditalic? No — our system doesn't use italic. Just--color-mutedtext: "— Not planned".--color-muted, bullet points explaining the active filters (e.g., "Avoiding chicken — used Mon & Wed", "Balancing effort — 2 hard days this week").Mobile context banner — collapsible
--green-tintbg, 12px padding.max-heighttransition."Browse full library" link
--color-mutedtext, centered. Not a button.Questions