feat(variety): implement C3 warning cards with recipe names and swap links (V1) #51
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?
Überblick
Die
/planner/varietySeite zeigt derzeit technische Tag-Codes in Warnkarten (MON, WED — erwäge einen Tausch). Der Planer muss selbst nachschlagen, welches Gericht betroffen ist, und dann manuell zum Planer navigieren.V1 "Erweiterte Karten" löst dies mit minimalem Aufwand: Warnkarten erhalten strukturierte Zeilen pro betroffenem Tag — mit Wochentag-Abkürzung, Rezeptname und direktem "Tauschen →" Link.
Spec:
specs/frontend/c3-variety-rework-v1-spec.html(auf master)Mockups:
specs/frontend/c3-variety-rework.html(auf master)Scope
VarietyWarningCards.svelteist bereits auf das neueActionWarning-Format aktualisiertBetroffene Dateien
frontend/src/routes/(app)/planner/variety/+page.svelteDAY_SHORT-Konstante,slotsByDay-Derived,actionWarnings-Derived, Template-Update (2×), Import-Bereinigungfrontend/src/lib/planner/VarietyWarningCards.sveltefrontend/src/lib/planner/variety.tscomputeWarningsbleibt, aber Import wird entferntImplementierungsschritte
Schritt 1 —
DAY_SHORT-Konstante in+page.svelteergänzen:Schritt 2 —
slotsByDay-Derived aufbauen (nach bestehenden$derived-Deklarationen):Schritt 3 —
actionWarnings-Derived ersetzencomputeWarnings():Schritt 4 — Template (Mobile + Desktop, je 2 Stellen):
Schritt 5 — Import bereinigen:
Abnahmekriterien
/planner?week={weekStart}&swap={slotId}varietyScore === null→ kein Warnkarten-Rendering (leere-Woche-State bleibt)computeWarningsentfernt, TypeScript kompiliert fehlerfrei🧑💻 Kai — Frontend Engineer
Questions & Observations
$derived.by()usage is correct: Using$derived.by()for the two derived maps is the right Svelte 5 pattern. No concerns there.TypeScript interfaces inline in
+page.svelte:WarningItemandActionWarningare defined directly in the page file. IfVarietyWarningCards.sveltealso needs to reference these types (for its props), they'll need to live in a sharedtypes.tsor a dedicatedvariety.types.ts. Define them there from the start rather than inlining and moving later.weekStartprop added toVarietyWarningCards: The template diff adds{weekStart}as a new prop. Where doesweekStartcome from in the page? Confirm it's already returned by the server load function. If it isn't, that's a hidden backend dependency — surface it before implementation starts.DAY_SHORTas a module-level constant: This is a static lookup table. Declare it as aconstoutside the component (module scope), not inside the<script>block. It's not reactive and doesn't need to be re-evaluated per render.Swap link construction: The swap link
/planner?week={weekStart}&swap={slotId}is assembled as a template string. EnsureweekStartis formatted as a URL-safe date string (ISO formatYYYY-MM-DD) — not a locale-formatted string or a Date object. Add a note in the AC or implementation steps to clarify the expected format.Null-safe slot lookup: The
slotsByDayderivation correctly filters slots whereslot.dayOfWeek && slot.recipe?.name && slot.id. But confirm that slot IDs in the weekPlan response are always numeric and never0(falsy). Aslot.id === 0would silently drop a valid slot.Suggestions
WarningItemandActionWarninginterface declarations to$lib/planner/types.tssoVarietyWarningCards.sveltecan import them for its props type.variety-page.test.tsred-first. Priority tests:slotsByDaybuilds correctly from fixture data,actionWarningsfilters out single-day warnings (AC-5),actionWarningsreturns[]whenvarietyScore === null(AC-7).🎨 Atlas — UI/UX Designer
Questions & Observations
"Tauschen →" link visual treatment: The spec mentions a "Tauschen →" link per row but doesn't specify the visual pattern. Is this an
<a>styled as a text link, a ghost button, or an inline chevron-icon button? The design system hasrole="button"patterns and explicit button sizing rules (13px, weight 500, tracking 0.04em). Clarify whether this follows the button spec or the link spec — they have different visual weights.→arrow glyph: The spec uses a literal→Unicode character. Is this intentional (typographic arrow), or should it be the design system's chevron SVG icon? SVG icons scale cleanly and can be coloured with currentColor; the Unicode arrow is lighter to implement but less consistent with the rest of the icon vocabulary.Row layout inside warning cards: Each row has: day abbreviation + recipe name + swap link. What's the layout? Left-aligned day abbreviation (fixed width?), then recipe name (truncating at one line?), then swap link pushed to the right? The spec body describes the content but not the layout mechanics. Define this explicitly to avoid per-implementer variation.
Touch target for swap link (AC-9): The AC requires ≥ 44px row height on mobile. A text-link-styled
<a>with default padding rarely hits 44px. The row itself should setmin-height: 44pxwithdisplay: flex; align-items: centerto guarantee this — don't rely on the link alone.Warning card title typography: Titles like "Tofu mehrfach diese Woche" or "Nudeln in mehreren Gerichten" are dynamically generated. What's the max reasonable length? At narrow mobile widths, a long ingredient name + suffix could wrap to 3 lines. Confirm the design handles multi-line card titles gracefully.
Suggestions
→in "Tauschen →" could be replaced by the standard chevron icon used elsewhere in the app for navigation affordance — keeps the visual language consistent without adding a new glyph.🧪 QA Engineer — Test Coverage
Questions & Observations
AC-5 is the sharpest edge case: Tags with only one affected day must produce no warning card. Test this explicitly with a fixture where
repeat.days.length === 1— a common off-by-one when the filter uses< 2vs<= 1.AC-7 — null varietyScore: The
actionWarningsderivation returns[]whenvsis falsy. Test this withvarietyScore === nulland alsovarietyScore === undefined(if the load can return either).Empty
itemsafter slot lookup: TheslotsByDayfilter only includes slots that have adayOfWeek,recipe.name, andid. A warning could reference a day that has no recipe (slot empty or not yet planned). The.filter(x => x !== null)handles this, but test the case where all items for a warning group are null —items.length === 0should not push toresult. The current implementation handles this (if (items.length > 0)), but add an explicit test for it.duplicatesInPlanlogic: The duplicate recipe detection usesObject.entries(slotsByDay).filter(([, s]) => s.recipeName === name). Test with a week that has the same recipe on 3 days — verify all 3 appear as items, not just the first match.DAY_SHORTfallback: The mapping usesDAY_SHORT[day] ?? day— if an unknown day code appears, it falls back to the raw code. Test with an unexpected day value (e.g.,"HOLIDAY") to confirm graceful fallback.Swap link URL format: Write a test that asserts the generated
hrefis/planner?week=2026-04-07&swap=42(exact format) — not just that a link exists. This catchesweekStartformat regressions early.Suggestions
actionWarningsderivation logic into a pure function (computeActionWarnings(varietyScore, slotsByDay, weekStart)) and test it directly as a unit test — no component mount required. This is faster and more precise than testing through a rendered component.data-testid="warning-swap-link"to each "Tauschen →" anchor for reliable selection in Playwright and Testing Library tests.🔒 Sable — Security Engineer
Questions & Observations
slotIdin swap link query param: The "Tauschen →" link passes?swap={slotId}as a URL parameter. The planner page must validate server-side that theslotIdbelongs to the requesting user's household before opening the swap drawer. URL params are user-controlled — enumerate any slotId and you can attempt to swap it. This is an existing concern that this feature makes more prominent by adding direct links.weekStartin swap link:/planner?week={weekStart}—weekStartis a date string used for navigation. Confirm the planner page validates this as a valid ISO date and ignores malformed values, rather than using it directly in a backend query without sanitization.Data in
slotsByDay: TheslotsByDaymap is derived fromweekPlandata loaded server-side. Confirm the server load function returns only data belonging to the authenticated user's household — no cross-household slot IDs should ever reach the client.No new API surface: This issue introduces no new endpoints — existing weekPlan load data is reused. This is a security positive. The only security-relevant output is the swap link, which is handled by the existing planner's server-side logic.
computeWarningsimport removal: The oldcomputeWarningsfunction is being removed from the import. Confirm no other code path still calls it with unvalidated data — unused imports can hide previously safe validation logic that was doing more than just display work.Suggestions
?swap=query param handler — confirm it validates household ownership ofslotIdand returns 403 (not 404 or a silent no-op) for unauthorized IDs.⚙️ Backend Engineer
Questions & Observations
No backend changes — confirmed: The derivation runs entirely on weekPlan data already returned by the load function. This is correct for V1 scope.
weekStartavailability: The swap link requires aweekStartdate string. Confirm the existing weekPlan API response already includes the week start date (or that+page.server.tsderives it from the plan). If not, a minor addition to the response DTO is needed before frontend work begins.slot.idalways present: TheslotsByDayderivation filters onslot.idbeing truthy. Confirm the existingWeekPlanResponseDTO always includes slot IDs in its slots list. If slots can be returned without IDs (e.g., virtual or unsaved slots), the filtering logic silently drops them — that should be documented as intentional behaviour.computeWarningsdeprecation: The oldcomputeWarningsfunction invariety.tsis being replaced by the newactionWarningsderived state. IscomputeWarningsused anywhere else in the codebase (e.g., in tests, other pages)? Confirm via a grep before deleting the import — dead code is fine to remove, but broken imports produce build failures.Future backend suggestion endpoint: When the suggestion ranking backend work happens (noted as a separate issue), the
tagRepeatsandingredientOverlapsdata structures returned by the variety score API will need to be stable. The frontend is now consumingrepeat.days,repeat.tagName,overlap.days,overlap.ingredientName— changes to these field names will break this feature silently.Suggestions
tagRepeatsandingredientOverlapsin the variety score API spec (or OpenAPI schema). The frontend is now tightly coupled to these field names — a schema change without a corresponding frontend update will cause silent rendering failures, not a build error.