Commit Graph

58 Commits

Author SHA1 Message Date
16e1539ac0 chore: merge master — adopt SlotResponse.SlotRecipe in SuggestionItem
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>
2026-04-10 10:08:38 +02:00
a5bb5d45a3 docs(config): annotate multipart limits explaining JSON body is not covered
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:38:48 +02:00
b2a798d90e docs(tests): clarify why fake base64 is acceptable in allowed-image-type test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:38:29 +02:00
23c821937f test(recipes): add JPEG input test for ImageCompressor
Confirms the compressor accepts JPEG data URIs as input (not just PNG).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:38:01 +02:00
9df6d6f0c6 test(recipes): verify null preview is stored when compressor returns null
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:37:24 +02:00
ed769b18a4 fix(recipe): add server-side image size limit and use .matches() for type check
- @Size(max=7_000_000) on heroImageUrl enforces ~5 MB cap at bean validation
- ALLOWED_IMAGE_PATTERN uses .matches() for unambiguous full-string check
- Tests: oversized image → 400, empty ingredients list → 400

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:27:35 +02:00
f11cca534f feat(recipe): compress hero image to 400px preview on save
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>
2026-04-10 09:14:35 +02:00
46f2ec45a3 feat(backend): limit multipart upload to 5 MB file / 6 MB request
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:09:14 +02:00
90cff0c4d2 feat(recipe): validate heroImageUrl content type before persisting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:08:45 +02:00
dbc78a1883 test(recipe): cover null serves/cookTimeMin and capitalised effort rejection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 09:00:16 +02:00
30ba53099c refactor(recipes): drop is_child_friendly column and remove from all layers
V025 migration drops the column. Removed from Recipe entity, RecipeDetailResponse,
RecipeSummaryResponse, RecipeRepository JPQL, RecipeService, and RecipeController.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 08:56:57 +02:00
520dae5adf feat(recipes): add image upload, fix save 500, seed HelloFresh data
- 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>
2026-04-09 20:23:28 +02:00
0596fddcd3 refactor(planning): extract applyPenalties helper to unify score formula
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
8dfc3df06b fix(planning): hasConflict only when scoreDelta strictly negative
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>
2026-04-09 16:33:12 +02:00
ea070b4760 fix(planning): replace existing slot in simulation instead of appending
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>
2026-04-09 16:33:12 +02:00
9928591b48 refactor(planner): extract computeCurrentScore helper in PlanningService
Eliminates duplicated currentSlots→score pattern that appeared in both
getSuggestions and getVarietyPreview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
89a549a1c8 test(planner): assert hasConflict=true for neutral scoreDelta on empty plan
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>
2026-04-09 16:33:12 +02:00
c24281dd4c test(planner): cover topN=0 and topN=-1 boundary in SuggestionsTest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 16:33:12 +02:00
8051fcbe22 refactor(planner): extract MAX_VARIETY_SCORE constant in PlanningService
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>
2026-04-09 16:33:12 +02:00
b673a466e9 feat(planner): replace simulatedScore with scoreDelta + hasConflict in SuggestionItem
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>
2026-04-09 16:33:12 +02:00
116e400a91 refactor(planning): extract applyPenalties helper to unify score formula
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 15:08:49 +02:00
77cdccb26c fix(planning): hasConflict only when scoreDelta strictly negative
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>
2026-04-09 12:47:41 +02:00
1611ddabf6 fix(planning): replace existing slot in simulation instead of appending
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>
2026-04-09 12:31:24 +02:00
0a9e8032cf refactor(planner): extract computeCurrentScore helper in PlanningService
Eliminates duplicated currentSlots→score pattern that appeared in both
getSuggestions and getVarietyPreview.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:11:44 +02:00
f84a647b8d test(planner): assert hasConflict=true for neutral scoreDelta on empty plan
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>
2026-04-09 12:11:00 +02:00
e17e8d4630 test(planner): cover topN=0 and topN=-1 boundary in SuggestionsTest
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 12:10:33 +02:00
482597bb6a refactor(planner): extract MAX_VARIETY_SCORE constant in PlanningService
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>
2026-04-09 12:09:08 +02:00
d008a17735 feat(planner): replace simulatedScore with scoreDelta + hasConflict in SuggestionItem
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>
2026-04-09 11:33:52 +02:00
ea7113ec53 fix(backend): add role guard to variety-preview and extract shared scoring method
- 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>
2026-04-09 08:11:45 +02:00
7175b56833 feat(planning): add GET /v1/week-plans/{planId}/variety-preview endpoint
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>
2026-04-08 22:36:03 +02:00
a52b0a9d24 feat(planning): enforce planner role on slot mutation endpoints
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>
2026-04-08 22:34:28 +02:00
e3afe1b4f2 test(shopping): add HTTP-level role guard test and blank customName validation test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:52:09 +02:00
eb5ee1ab5a test(shopping): add missing service tests for stale items, dedup, and household isolation
- 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>
2026-04-06 19:50:27 +02:00
9d210befa1 fix(security): add @Valid constraints on AddItemRequest to prevent oversized input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:49:06 +02:00
40a6a0e92d fix(security): use generic forbidden message to avoid leaking required role
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:46:24 +02:00
40ee4dad53 refactor(shopping): extract mergeKey helper to eliminate duplicate key construction
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:44:44 +02:00
92922533ac feat(shopping): finalize GET /v1/shopping-list endpoint and regenerate OpenAPI types
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>
2026-04-04 18:49:08 +02:00
16b70bd818 feat(shopping): add GET /v1/shopping-lists endpoint and planner-only guard
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>
2026-04-04 18:35:31 +02:00
5325f4827e feat(shopping): refactor generateFromPlan to merge strategy
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>
2026-04-04 18:33:15 +02:00
c26c2e1973 feat(shopping): add getByWeekStart to ShoppingService
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>
2026-04-04 18:30:41 +02:00
93e8bf9e41 feat(shopping): extend response with generatedAt, filteredStaplesCount, RecipeRef
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>
2026-04-04 18:29:07 +02:00
7e254fc280 feat(shopping): add week-based shopping list repository query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:23:43 +02:00
3be9f502c6 feat(auth): add @RequiresHouseholdRole annotation with interceptor
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>
2026-04-04 18:22:47 +02:00
2f690eb3cb feat(common): add ForbiddenException with 403 handler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:18:27 +02:00
2253c76287 feat(shopping): add generated_at column to shopping_list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 18:17:12 +02:00
27e09a77d6 fix(recipes): cast JPQL null params to avoid lower(bytea) error on PostgreSQL
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>
2026-04-04 08:10:59 +02:00
09333ccc0a test(auth): verify security context is stored in session after login and signup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 18:55:25 +02:00
93ce1eaeac refactor(auth): add comments, clearContext on logout, explain session auth
- Add comment to SecurityConfig explaining why CSRF is disabled
- Add SecurityContextHolder.clearContext() to logout for clean thread state
- Add Javadoc on authenticateInSession() explaining manual session setup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 18:52:16 +02:00
0aa65214fc fix(auth): resolve broken signup/login flow end-to-end
Three root causes fixed:

1. CSRF blocked all backend POSTs — Spring Security's CSRF filter ran
   before permitAll() authorization, returning 401 for signup and login.
   Disabled CSRF since SvelteKit is the only client (never the browser
   directly) and handles its own CSRF via Origin header checks.

2. Login/signup didn't establish Spring Security authentication — they
   stored email in the HTTP session manually but never set the
   SecurityContext, so Principal in /v1/auth/me was always null and
   hooks.server.ts redirected every authenticated request to /login.
   Fixed with authenticateInSession() helper that sets and persists
   the SecurityContext under SPRING_SECURITY_CONTEXT_KEY. Login also
   now invalidates the old session before creating a new one to prevent
   session fixation.

3. redirect() missing throw in hooks.server.ts, signup action, and
   login action — SvelteKit never saw the redirect, so pages silently
   reloaded with no navigation. Also forward JSESSIONID from backend
   response to browser explicitly, since SvelteKit does not
   auto-forward Set-Cookie for cross-origin server-side fetches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 17:31:29 +02:00
9713412d42 Remove service interfaces — use concrete classes directly
Each domain had a single-implementation interface (e.g. AdminService
interface + AdminServiceImpl). Merged implementation into the service
class and deleted the redundant interfaces per KISS principle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 11:04:41 +02:00