After extracting authenticateInSession to AuthService, the mock doesn't
populate the session. Replace session-attribute assertions with verify()
calls that confirm the controller correctly delegates to authService.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove duplicated private authenticateInSession from AuthController and
HouseholdController. Add a single public implementation on AuthService
with session fixation protection built in. HouseholdController now
injects AuthService and passes role "user" for invite-accepted accounts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same invalidatedAt gap as getInviteInfo: a superseded invite (status
still 'pending', invalidatedAt set) could still be used to create an
account and join the household.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Superseded invites had invalidatedAt set but status stayed 'pending',
so they passed the validity check and could still be viewed and accepted.
Add invalidatedAt != null guard to getInviteInfo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- createInvite: use saveAndFlush when invalidating existing invite so the
UPDATE is guaranteed to hit the DB before the new INSERT, preventing
duplicate key violation on uq_household_invite_active
- acceptInvite: also set invalidated_at when marking invite as used, so
accepted invites are fully removed from the partial unique index and
cannot block future invite creation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SecurityConfig: /** covers /v1/invites/{code}/accept (two path segments);
/* only matched one segment so the accept endpoint was returning 401
- HouseholdIdentityPanel + page: use --green-dark bg (matching BrandPanel
on login) instead of --green-tint; text updated to white/--green-light
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dev databases that accumulated multiple pending invites before V026 was
written would fail to create uq_household_invite_active. Added a cleanup
UPDATE that marks all-but-the-latest invite per household as invalidated
before the index is created.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
removeMember now checks the planner count before deleting a planner
member. Throws ConflictException("Cannot remove the last planner")
when only one planner remains, matching the spec requirement in S4.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces hardcoded \"https://yourapp.com\" with a Spring property.
- application.yml: app.base-url defaults to http://localhost:5173
- application-docker.yml: reads APP_BASE_URL env var, same default
- HouseholdService: injects @Value("${app.base-url}") and uses it in
toInviteResponse() to build shareUrl
- HouseholdServiceTest: sets field via ReflectionTestUtils in @BeforeEach;
adds test asserting shareUrl starts with configured base URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>