Frontend: C2 — Meal suggestions (variety-aware) #27

Closed
opened 2026-04-02 11:28:42 +02:00 by marcel · 6 comments
Owner

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)

  • Context banner (--green-tint bg, collapsible): day label + filter summary
  • Ranked suggestion cards below

Desktop (> 1024px)

  • Sidebar (224px) + topbar ("Suggestions for [day]")
  • Left panel (280px, --color-surface bg): "This week so far" mini meal cards + "Filter reasons" bullets
  • Right panel (flex:1, --color-page bg): ranked suggestion cards

Suggestion Cards

  • Ranking number (large, muted)
  • Recipe name
  • Metadata: time, effort, primary protein
  • Reasoning badge explaining why this suggestion ranks well or has warnings:
    • Good: "✓ Fresh protein · effort balance" — green badge
    • Warning: "⚠ Chicken again — 2 days apart" — yellow badge

Sorting (J2 context)

Sorted by variety algorithm (not effort):

  1. Avoids ingredients used in past 3 days
  2. Avoids same protein as adjacent days
  3. Balances effort — if previous 2 days were hard, easy meals surface first

Behavior

  • Pick a suggestion → writes week_plan_slot → returns to C1
  • "Browse full library" link → B1 in selection mode (pick a recipe manually)
  • Variety score updates immediately after selection

Acceptance Criteria

  • Mobile: context banner + ranked suggestion list
  • Desktop: 2-panel with "week so far" context + suggestions
  • Suggestions sorted by variety algorithm
  • Reasoning badges explain ranking (green = good, yellow = warning)
  • Pick → saves to plan slot → returns to C1
  • "Browse full library" fallback to B1
## 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) - Context banner (`--green-tint` bg, collapsible): day label + filter summary - Ranked suggestion cards below ### Desktop (> 1024px) - Sidebar (224px) + topbar ("Suggestions for [day]") - Left panel (280px, `--color-surface` bg): "This week so far" mini meal cards + "Filter reasons" bullets - Right panel (flex:1, `--color-page` bg): ranked suggestion cards ## Suggestion Cards - Ranking number (large, muted) - Recipe name - Metadata: time, effort, primary protein - Reasoning badge explaining why this suggestion ranks well or has warnings: - Good: "✓ Fresh protein · effort balance" — green badge - Warning: "⚠ Chicken again — 2 days apart" — yellow badge ## Sorting (J2 context) Sorted by **variety algorithm** (not effort): 1. Avoids ingredients used in past 3 days 2. Avoids same protein as adjacent days 3. Balances effort — if previous 2 days were hard, easy meals surface first ## Behavior - Pick a suggestion → writes `week_plan_slot` → returns to C1 - "Browse full library" link → B1 in selection mode (pick a recipe manually) - Variety score updates immediately after selection ## Acceptance Criteria - [ ] Mobile: context banner + ranked suggestion list - [ ] Desktop: 2-panel with "week so far" context + suggestions - [ ] Suggestions sorted by variety algorithm - [ ] Reasoning badges explain ranking (green = good, yellow = warning) - [ ] Pick → saves to plan slot → returns to C1 - [ ] "Browse full library" fallback to B1
marcel added the kind/featurepriority/medium labels 2026-04-02 11:30:13 +02:00
Author
Owner

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.

**Spec file:** [`specs/frontend/j2-plan-the-week.html`](../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.
Author
Owner

👨‍💻 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-tint bg, collapsible, shows day label + filter summary (e.g., "Avoiding chicken · Balancing effort")
  • SuggestionCard — the ranked card: ranking number + recipe name + metadata (time, effort, protein) + ReasoningBadge
  • ReasoningBadge — green badge (good fit) or yellow badge (warning). Needs two visual variants and accepts a text label
  • WeekSoFarPanel — desktop-only left panel: mini meal cards for the current week + filter reason bullets
  • C2Layout — orchestrates mobile (full-width list) vs desktop (280px left panel + flex right panel)

The "Browse full library" fallback

  • This link navigates to B1 in "selection mode" — meaning B1 needs to know it was opened from C2 (for a specific day/slot) so it can write back to that slot on selection.
  • How is this context passed? URL parameter (/recipes?selectFor=slotId) is the cleanest SvelteKit approach. The +page.server.ts for B1 then reads the selectFor param and switches behavior.
  • On selection in B1, the action should: write week_plan_slot, then redirect back to C1 (not C2). Does that match the intended flow?

The reasoning badge — data driven

  • The badge text (e.g., "✓ Fresh protein · effort balance") comes from the API — the backend generates the reasoning per suggestion. The frontend just needs to render a green or yellow badge with the provided text.
  • Do not compute reasoning client-side — the sorting algorithm lives server-side. The badge text is part of the suggestion response.

Collapsible context banner

  • "Collapsible" — is this a toggle (open/closed persisted in $state) or an accordion that opens on tap? My lean: simple $state toggle with a chevron icon. No persistence across page loads needed.

Questions

  • When the user picks a suggestion and the route transitions back to C1, does C2 need to be pre-loaded in the route history, or is it always a one-way forward flow (C1 → C2 → C1)?
  • Are suggestion cards paginated, or is the full list returned in one response? If the household has 50+ recipes, that's a long list to scroll.
  • Does the ranking number (large, muted) have a max — e.g., does it only show ranks 1–10 and truncate, or does it show all results ranked?
  • The "filter reasons" bullets in the desktop left panel — are these the same filter conditions as the context banner summary on mobile (just different presentation), or different content?
  • After picking a suggestion, is C2 removed from the browser history (replace vs push) so Back doesn't return to the suggestions list?
## 👨‍💻 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-tint` bg, collapsible, shows day label + filter summary (e.g., "Avoiding chicken · Balancing effort") - `SuggestionCard` — the ranked card: ranking number + recipe name + metadata (time, effort, protein) + `ReasoningBadge` - `ReasoningBadge` — green badge (good fit) or yellow badge (warning). Needs two visual variants and accepts a text label - `WeekSoFarPanel` — desktop-only left panel: mini meal cards for the current week + filter reason bullets - `C2Layout` — orchestrates mobile (full-width list) vs desktop (280px left panel + flex right panel) **The "Browse full library" fallback** - This link navigates to B1 in "selection mode" — meaning B1 needs to know it was opened from C2 (for a specific day/slot) so it can write back to that slot on selection. - How is this context passed? URL parameter (`/recipes?selectFor=slotId`) is the cleanest SvelteKit approach. The `+page.server.ts` for B1 then reads the `selectFor` param and switches behavior. - On selection in B1, the action should: write `week_plan_slot`, then redirect back to C1 (not C2). Does that match the intended flow? **The reasoning badge — data driven** - The badge text (e.g., "✓ Fresh protein · effort balance") comes from the API — the backend generates the reasoning per suggestion. The frontend just needs to render a green or yellow badge with the provided text. - Do not compute reasoning client-side — the sorting algorithm lives server-side. The badge text is part of the suggestion response. **Collapsible context banner** - "Collapsible" — is this a toggle (open/closed persisted in `$state`) or an accordion that opens on tap? My lean: simple `$state` toggle with a chevron icon. No persistence across page loads needed. **Questions** - When the user picks a suggestion and the route transitions back to C1, does C2 need to be pre-loaded in the route history, or is it always a one-way forward flow (C1 → C2 → C1)? - Are suggestion cards paginated, or is the full list returned in one response? If the household has 50+ recipes, that's a long list to scroll. - Does the ranking number (large, muted) have a max — e.g., does it only show ranks 1–10 and truncate, or does it show all results ranked? - The "filter reasons" bullets in the desktop left panel — are these the same filter conditions as the context banner summary on mobile (just different presentation), or different content? - After picking a suggestion, is C2 removed from the browser history (replace vs push) so Back doesn't return to the suggestions list?
Author
Owner

🛠️ 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/slot
  • Response per item should include:
    {
      "rank": 1,
      "recipeId": "uuid",
      "name": "Pasta al limone",
      "cookTime": 25,
      "effort": "EASY",
      "primaryProtein": "VEGETARIAN",
      "reasoningBadge": {
        "type": "GOOD",
        "label": "Fresh protein · effort balance"
      }
    }
    
  • The reasoning badge label must be generated server-side — the client cannot derive it without running the same algorithm

The variety algorithm — three filter rules

  • Avoids ingredients used in the past 3 days: requires a query across week_plan_slot → recipe → recipe_ingredient for the adjacent days. This is a join-heavy query — worth checking the query plan with an EXPLAIN ANALYZE.
  • Avoids same protein as adjacent days: requires knowing the primaryProtein of the previous and next day's planned meal.
  • Balances effort: if the previous 2 days were HARD, surface EASY meals 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}/suggestions must 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.
  • This endpoint should be idempotent: picking the same recipe for the same slot twice should be a no-op (not an error).

"Browse full library" — B1 in selection mode

  • When B1 is opened in selection mode, and a recipe is picked, the write should use the same PUT /api/week-plan-slots/{slotId}/recipe endpoint. No special endpoint needed for the selection mode path.

Questions

  • What's the maximum number of suggestions returned? Should it be capped (e.g., top 20) or return all eligible recipes ranked?
  • Is "primary protein" a first-class field on the recipe entity, or derived from ingredients at query time? If derived, the algorithm needs a defined protein-classification rule for ingredients.
  • Does the suggestions endpoint exclude recipes already planned elsewhere in the current week, or can the same recipe appear on multiple days?
  • What's the behavior when the household recipe library is empty or has fewer recipes than needed to fill the suggestions list?
## 🛠️ 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/slot - Response per item should include: ```json { "rank": 1, "recipeId": "uuid", "name": "Pasta al limone", "cookTime": 25, "effort": "EASY", "primaryProtein": "VEGETARIAN", "reasoningBadge": { "type": "GOOD", "label": "Fresh protein · effort balance" } } ``` - The reasoning badge label must be generated server-side — the client cannot derive it without running the same algorithm **The variety algorithm — three filter rules** - Avoids ingredients used in the past 3 days: requires a query across `week_plan_slot → recipe → recipe_ingredient` for the adjacent days. This is a join-heavy query — worth checking the query plan with an EXPLAIN ANALYZE. - Avoids same protein as adjacent days: requires knowing the `primaryProtein` of the previous and next day's planned meal. - Balances effort: if the previous 2 days were `HARD`, surface `EASY` meals 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}/suggestions` must 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. - This endpoint should be idempotent: picking the same recipe for the same slot twice should be a no-op (not an error). **"Browse full library" — B1 in selection mode** - When B1 is opened in selection mode, and a recipe is picked, the write should use the same `PUT /api/week-plan-slots/{slotId}/recipe` endpoint. No special endpoint needed for the selection mode path. **Questions** - What's the maximum number of suggestions returned? Should it be capped (e.g., top 20) or return all eligible recipes ranked? - Is "primary protein" a first-class field on the recipe entity, or derived from ingredients at query time? If derived, the algorithm needs a defined protein-classification rule for ingredients. - Does the suggestions endpoint exclude recipes already planned elsewhere in the current week, or can the same recipe appear on multiple days? - What's the behavior when the household recipe library is empty or has fewer recipes than needed to fill the suggestions list?
Author
Owner

🧪 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 against
  • shouldNotSuggestRecipesFromOtherHouseholds() — isolation test

Backend integration tests

  • shouldReturn403WhenMemberRequestsSuggestions()
  • shouldReturn404WhenWeekPlanDoesNotExist()
  • shouldReturn404WhenWeekPlanBelongsToOtherHousehold()
  • shouldReturnSuggestionsOrderedByVarietyScore()
  • shouldUpdateVarietyScoreAfterPickingFromSuggestions() — pick a suggestion, verify score in response
  • shouldHandleEmptyWeekPlanWithNoPlannedMeals()

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 toggled
  • WeekSoFarPanel: renders mini cards for planned days, shows filter reasons bullets
  • Empty suggestions state: renders "No suggestions available" or similar when list is empty
  • "Browse full library" link: present and navigates to correct URL with selectFor param

E2E tests — the planning flow

  • Navigate from C1 (empty slot) → C2 → verify suggestions are ranked → pick suggestion → verify C1 updates slot and variety score
  • Desktop: verify 2-panel layout renders correctly
  • "Browse full library": navigate to B1, pick a recipe, verify return to C1 with slot filled
  • Verify that after picking, C2 is not in browser history (Back goes to C1, not C2)

Algorithm edge cases to define and test

  • Day 1 of the week: no previous days. Suggestions should rank by effort balance only, since there's no adjacent-day context.
  • All 7 days planned when suggesting for an empty slot: replacing a slot (full week) vs filling an empty slot (partial week).
  • A recipe that appears multiple times in the library? (Should not happen if recipes are unique by household — but confirm the uniqueness constraint exists.)

Questions

  • Is there an explicit sorting tiebreaker when two recipes have the same variety score? Random? Alphabetical? Most recently added?
  • Does the suggestions list update reactively on C2 if another household member adds a recipe to the library while C2 is open? (Probably not needed for v1, but worth stating explicitly.)
  • Is the "filter reasons" text in the desktop panel and context banner generated server-side or derived from the suggestion list client-side?
## 🧪 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 against - `shouldNotSuggestRecipesFromOtherHouseholds()` — isolation test **Backend integration tests** - `shouldReturn403WhenMemberRequestsSuggestions()` - `shouldReturn404WhenWeekPlanDoesNotExist()` - `shouldReturn404WhenWeekPlanBelongsToOtherHousehold()` - `shouldReturnSuggestionsOrderedByVarietyScore()` - `shouldUpdateVarietyScoreAfterPickingFromSuggestions()` — pick a suggestion, verify score in response - `shouldHandleEmptyWeekPlanWithNoPlannedMeals()` **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 toggled - `WeekSoFarPanel`: renders mini cards for planned days, shows filter reasons bullets - Empty suggestions state: renders "No suggestions available" or similar when list is empty - "Browse full library" link: present and navigates to correct URL with `selectFor` param **E2E tests — the planning flow** - Navigate from C1 (empty slot) → C2 → verify suggestions are ranked → pick suggestion → verify C1 updates slot and variety score - Desktop: verify 2-panel layout renders correctly - "Browse full library": navigate to B1, pick a recipe, verify return to C1 with slot filled - Verify that after picking, C2 is not in browser history (Back goes to C1, not C2) **Algorithm edge cases to define and test** - Day 1 of the week: no previous days. Suggestions should rank by effort balance only, since there's no adjacent-day context. - All 7 days planned when suggesting for an empty slot: replacing a slot (full week) vs filling an empty slot (partial week). - A recipe that appears multiple times in the library? (Should not happen if recipes are unique by household — but confirm the uniqueness constraint exists.) **Questions** - Is there an explicit sorting tiebreaker when two recipes have the same variety score? Random? Alphabetical? Most recently added? - Does the suggestions list update reactively on C2 if another household member adds a recipe to the library while C2 is open? (Probably not needed for v1, but worth stating explicitly.) - Is the "filter reasons" text in the desktop panel and context banner generated server-side or derived from the suggestion list client-side?
Author
Owner

🔐 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}/suggestions must 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.
  • The response includes recipe names, metadata, and reasoning badges — this is household-owned data. A planner from Household A must never see suggestions drawn from Household B's recipe library.
  • Uniform 404 for "plan not found" and "plan belongs to another household" — don't distinguish between the two in error responses.

"Browse full library" — B1 in selection mode with a selectFor parameter

  • The selectFor=slotId parameter in the URL must be validated server-side in B1's +page.server.ts. Do not trust the client-provided slotId without verifying: (1) the slot exists, (2) it belongs to the caller's household, (3) the caller is a planner.
  • If the slotId is 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).
  • This is a potential IDOR vector: a planner could craft a URL with a slotId from another household's week plan and attempt to assign a recipe to it.

Reasoning badge content — no user-generated text injection

  • The reasoning badge label is generated server-side based on the variety algorithm. If any recipe metadata (name, tags) feeds into the badge text, ensure the badge text is server-sanitized before inclusion. The frontend should never use {@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. The slotId must be verified against the caller's household, and the caller must be a planner.
  • This is the same endpoint as swap — so if the swap endpoint is secured correctly, C2's pick action inherits those controls. Just verify it's not a separate, less-secured endpoint.

Information disclosure via suggestions

  • The reasoning badge exposes information about the current week's plan indirectly (e.g., "Chicken again — 2 days apart" reveals that chicken was planned on adjacent days). This is by design and acceptable since C2 is planner-only. Just worth documenting in the threat model.

Questions

  • Is the slotId included 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 — if slotId is a UUID, that's low risk, but worth being explicit.
  • Does the suggestions endpoint log accesses? Failed authorization attempts (member trying to access suggestions) should be logged as security events.
  • Is there any caching of suggestions on the server? If so, cache keys must include householdId to prevent cross-household cache poisoning.
## 🔐 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}/suggestions` must 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. - The response includes recipe names, metadata, and reasoning badges — this is household-owned data. A planner from Household A must never see suggestions drawn from Household B's recipe library. - Uniform 404 for "plan not found" and "plan belongs to another household" — don't distinguish between the two in error responses. **"Browse full library" — B1 in selection mode with a `selectFor` parameter** - The `selectFor=slotId` parameter in the URL must be validated server-side in B1's `+page.server.ts`. Do not trust the client-provided `slotId` without verifying: (1) the slot exists, (2) it belongs to the caller's household, (3) the caller is a planner. - If the `slotId` is 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). - This is a potential IDOR vector: a planner could craft a URL with a `slotId` from another household's week plan and attempt to assign a recipe to it. **Reasoning badge content — no user-generated text injection** - The reasoning badge label is generated server-side based on the variety algorithm. If any recipe metadata (name, tags) feeds into the badge text, ensure the badge text is server-sanitized before inclusion. The frontend should never use `{@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. The `slotId` must be verified against the caller's household, and the caller must be a planner. - This is the same endpoint as swap — so if the swap endpoint is secured correctly, C2's pick action inherits those controls. Just verify it's not a separate, less-secured endpoint. **Information disclosure via suggestions** - The reasoning badge exposes information about the current week's plan indirectly (e.g., "Chicken again — 2 days apart" reveals that chicken was planned on adjacent days). This is by design and acceptable since C2 is planner-only. Just worth documenting in the threat model. **Questions** - Is the `slotId` included 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 — if `slotId` is a UUID, that's low risk, but worth being explicit. - Does the suggestions endpoint log accesses? Failed authorization attempts (member trying to access suggestions) should be logged as security events. - Is there any caching of suggestions on the server? If so, cache keys must include `householdId` to prevent cross-household cache poisoning.
Author
Owner

🎨 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

  • "Large, muted" — my spec: Fraunces, 32px, weight 300, --color-muted text, positioned to the left of the card content as an oversized ordinal. This creates the "editorial list" feel appropriate for a curated ranked list.
  • The number should be right-aligned in a fixed 40px column so card content aligns cleanly across all ranks.

Suggestion card anatomy

  • Card: --color-surface bg, --radius-md (6px), --shadow-card, 12px 16px padding
  • Recipe name: DM Sans 15px, --color-text, weight 500
  • Metadata row: 12px, --color-muted — "25 min · Easy · Chicken" with · separators
  • Reasoning badge: inline below metadata, pill shape (--radius-full), 11px, weight 500
    • Green badge: --green-tint bg, --green-dark text, "✓ Fresh protein · effort balance"
    • Yellow badge: --yellow-tint bg, --yellow-text text, "⚠ Chicken again — 2 days apart"
  • "Pick" link: right-aligned, --green-dark text, 13px/500 — not a button, a text link (lightweight action in a list context)

Desktop left panel — "This week so far"

  • Mini meal cards: 1 line each — day abbreviation + recipe name. Day in --color-muted 11px, name in 13px --color-text.
  • Empty days: "— Empty" in --color-muted italic? No — our system doesn't use italic. Just --color-muted text: "— Not planned".
  • "Filter reasons" bullets: 12px, --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

  • Collapsed: single line — "Wednesday · Avoiding chicken, balancing effort" with a down chevron. --green-tint bg, 12px padding.
  • Expanded: shows the full filter reason bullets (same content as desktop left panel). Smooth CSS max-height transition.
  • Banner should default to collapsed if the user has already seen it this session, expanded on first visit.

"Browse full library" link

  • Positioned at the bottom of the suggestions list, after all ranked cards. DM Sans 13px, --color-muted text, centered. Not a button.
  • On desktop, also available in the left panel footer.

Questions

  • How many suggestion cards are expected in a typical load? This affects whether we need a "Show more" pattern or infinite scroll vs. a fixed capped list.
  • The "Pick" link — after tapping, should the card show a loading state (spinner in place of "Pick") while the server writes the slot, or is it an instant optimistic update?
  • Should the reasoning badge text ever wrap to two lines, or should it be single-line with ellipsis truncation?
## 🎨 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** - "Large, muted" — my spec: Fraunces, 32px, weight 300, `--color-muted` text, positioned to the left of the card content as an oversized ordinal. This creates the "editorial list" feel appropriate for a curated ranked list. - The number should be right-aligned in a fixed 40px column so card content aligns cleanly across all ranks. **Suggestion card anatomy** - Card: `--color-surface` bg, `--radius-md` (6px), `--shadow-card`, 12px 16px padding - Recipe name: DM Sans 15px, `--color-text`, weight 500 - Metadata row: 12px, `--color-muted` — "25 min · Easy · Chicken" with `·` separators - Reasoning badge: inline below metadata, pill shape (`--radius-full`), 11px, weight 500 - Green badge: `--green-tint` bg, `--green-dark` text, "✓ Fresh protein · effort balance" - Yellow badge: `--yellow-tint` bg, `--yellow-text` text, "⚠ Chicken again — 2 days apart" - "Pick" link: right-aligned, `--green-dark` text, 13px/500 — not a button, a text link (lightweight action in a list context) **Desktop left panel — "This week so far"** - Mini meal cards: 1 line each — day abbreviation + recipe name. Day in `--color-muted` 11px, name in 13px `--color-text`. - Empty days: "— Empty" in `--color-muted` italic? No — our system doesn't use italic. Just `--color-muted` text: "— Not planned". - "Filter reasons" bullets: 12px, `--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** - Collapsed: single line — "Wednesday · Avoiding chicken, balancing effort" with a down chevron. `--green-tint` bg, 12px padding. - Expanded: shows the full filter reason bullets (same content as desktop left panel). Smooth CSS `max-height` transition. - Banner should default to collapsed if the user has already seen it this session, expanded on first visit. **"Browse full library" link** - Positioned at the bottom of the suggestions list, after all ranked cards. DM Sans 13px, `--color-muted` text, centered. Not a button. - On desktop, also available in the left panel footer. **Questions** - How many suggestion cards are expected in a typical load? This affects whether we need a "Show more" pattern or infinite scroll vs. a fixed capped list. - The "Pick" link — after tapping, should the card show a loading state (spinner in place of "Pick") while the server writes the slot, or is it an instant optimistic update? - Should the reasoning badge text ever wrap to two lines, or should it be single-line with ellipsis truncation?
Sign in to join this conversation.