GET /v1/recipes was returning RecipeSummaryResponse with no tags and
only heroImagePreview. The planner frontend needs protein tags to pick
gradient backgrounds for tiles without a hero image.
- Replace JPQL constructor projection with entity query + LEFT JOIN FETCH tags
- Map Recipe entity to RecipeSummaryResponse in service (includes tags + heroImageUrl)
- Drop heroImagePreview in favour of heroImageUrl on the summary DTO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves conflict by keeping master's refactor: SuggestionItem now reuses
SlotResponse.SlotRecipe instead of the dedicated SuggestionRecipe record,
removing the duplication and adding heroImageUrl to suggestion responses.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Thumbnailator-based ImageCompressor that resizes uploaded images
to a 400px-wide JPEG preview stored in hero_image_preview. The recipe
list uses the preview instead of the full image URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Store hero image as base64 data URI in text column (V023 migration)
- Add file upload UI to RecipeForm with FileReader preview
- Remove isChildFriendly from RecipeCreateRequest (no form field)
- Fix 500 on save: effort values now lowercase, serves/cookTimeMin changed
from primitive short to nullable Integer to survive omitted fields
- Fix empty categories panel: removed stale tagType=category filter
- Group category tags by type with German headings in recipe form
- Split SuggestionResponse.SuggestionRecipe (no image) from SlotRecipe
- Seed 11 HelloFresh recipes with ingredients, steps and tags (V101)
- Add frontend e2e scaffold, specs and dev yml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neutral suggestions (scoreDelta = 0) are not conflicts — they simply
don't improve variety. Changing scoreDelta <= 0 to scoreDelta < 0
lets empty-plan additions and quality-neutral swaps show without a
misleading ⚠ Variationskonflikt warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
simulateVarietyScore was adding the candidate recipe on top of the
existing slot for slotDate, keeping the old recipe's tag-repeat penalty
in the score. Now the existing slot is excluded before simulating, so
swapping a recipe for one with better variety correctly shows positive
scoreDelta and hasConflict=false.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eliminates duplicated currentSlots→score pattern that appeared in both
getSuggestions and getVarietyPreview.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the surprising-but-correct behavior: recipes on an empty plan
get scoreDelta=0.0, which satisfies scoreDelta<=0, so hasConflict=true.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces magic literal 10.0 with a named constant in all four
scoring sites: getSuggestions, getVarietyPreview, scoreFromSimulatedSlots,
and getVarietyScore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SuggestionItem now exposes scoreDelta (simulatedScore − currentScore) and
hasConflict (scoreDelta ≤ 0) so the frontend can render badges without
needing to pass currentVarietyScore as a separate prop.
PlanningService.getSuggestions() computes currentScore once per request
and derives scoreDelta + hasConflict per candidate. Sorting is unchanged
(scoreDelta desc = simulatedScore desc since currentScore is constant).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neutral suggestions (scoreDelta = 0) are not conflicts — they simply
don't improve variety. Changing scoreDelta <= 0 to scoreDelta < 0
lets empty-plan additions and quality-neutral swaps show without a
misleading ⚠ Variationskonflikt warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
simulateVarietyScore was adding the candidate recipe on top of the
existing slot for slotDate, keeping the old recipe's tag-repeat penalty
in the score. Now the existing slot is excluded before simulating, so
swapping a recipe for one with better variety correctly shows positive
scoreDelta and hasConflict=false.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eliminates duplicated currentSlots→score pattern that appeared in both
getSuggestions and getVarietyPreview.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the surprising-but-correct behavior: recipes on an empty plan
get scoreDelta=0.0, which satisfies scoreDelta<=0, so hasConflict=true.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces magic literal 10.0 with a named constant in all four
scoring sites: getSuggestions, getVarietyPreview, scoreFromSimulatedSlots,
and getVarietyScore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SuggestionItem now exposes scoreDelta (simulatedScore − currentScore) and
hasConflict (scoreDelta ≤ 0) so the frontend can render badges without
needing to pass currentVarietyScore as a separate prop.
PlanningService.getSuggestions() computes currentScore once per request
and derives scoreDelta + hasConflict per candidate. Sorting is unchanged
(scoreDelta desc = simulatedScore desc since currentScore is constant).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @RequiresHouseholdRole("member") to GET /{planId}/variety-preview endpoint
to require household membership (was accessible to any authenticated user)
- Extract scoreFromSimulatedSlots() private method eliminating duplicate logic
between simulateVarietyScore() and the old computeCurrentScore()
- Fix loose variety preview test assertions (isBetween → exact assertEquals)
- Add test verifying negative scoreDelta when candidate is a duplicate recipe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns currentScore, projectedScore, and scoreDelta when a recipe
would be added on a given date. Used by C6 desktop day picker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PATCH, DELETE, and POST slot endpoints now return 403 Forbidden
when called by a household member.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- generateFromPlan removes stale generated items
- sourceRecipes deduplicates when same recipe appears in two slots
- checkItem throws ResourceNotFoundException on household mismatch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renamed endpoint to /v1/shopping-list to avoid Springdoc path conflict.
Added @RequiresHouseholdRole("planner") on generate. Regenerated
frontend OpenAPI schema with all new shopping list endpoints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New week-based lookup endpoint with optional weekStart param (defaults
to current week). Generate endpoint now enforced with @RequiresHouseholdRole.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a shopping list already exists for the week plan, regeneration
now merges: custom items and check states are preserved, existing
generated items are updated, removed recipes' items are deleted,
and new ingredients are added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returns the shopping list for a given week, defaulting to the current
week's Monday when no weekStart is provided. Returns null when no
list exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shopping list response now includes generatedAt timestamp, count of
filtered staples, and recipe names (not just UUIDs) in source references.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reusable annotation for planner-only endpoints. Uses a
HandlerInterceptor that resolves the household role from the
authenticated user and throws 403 if the role doesn't match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When :search or :effort are null, Hibernate passes untyped bind parameters
that PostgreSQL infers as bytea, causing `lower(bytea) does not exist`.
Explicit CAST(… AS string) tells Hibernate to bind them as varchar.
Also fixes the bcrypt hash in V100 dev seed (was wrong, dev/dev now works).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>