Commit Graph

319 Commits

Author SHA1 Message Date
73af11e84b fix(invite): reject invalidated invites in acceptInvite
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>
2026-04-10 22:22:54 +02:00
0ab1ba0b1b fix(invite): reject invalidated invites in getInviteInfo
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>
2026-04-10 22:22:07 +02:00
44fd398701 fix(invite): saveAndFlush invalidation before INSERT + set invalidated_at on accept
- 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>
2026-04-10 22:04:24 +02:00
6aed303627 fix(join): permit /v1/invites/** (not just /*) + match panel color to login
- 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>
2026-04-10 22:00:47 +02:00
c5ec3396b2 fix(migration): deduplicate active invites before creating unique index in V026
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>
2026-04-10 21:51:19 +02:00
6950b3d8db feat(join): implement A4 join household page (/join/[token])
- schema.d.ts: add GET /v1/invites/{code}, InviteInfoResponse, AcceptInviteRequest; update acceptInvite operation
- hooks.server.ts: add /join to PUBLIC_ROUTES; redirect authenticated users on /join/* to /
- +page.server.ts: load validates token (invalid:true on 404); action creates account + joins + sets session cookie
- HouseholdIdentityPanel.svelte: logo, household name (Fraunces), inviter text, static permissions list
- JoinForm.svelte: name/email/password + show/hide toggle, "Haushalt beitreten" CTA, field errors, pre-fill
- +page.svelte: no-chrome layout, mobile (banner+form stacked) / desktop (400px panel + flex:1) split, invalid-token inline state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 21:31:03 +02:00
92f25e56fc feat(invite): add GET /v1/invites/{code} + rework POST accept as signup+join
- V027 migration: add invited_by FK column on household_invite
- HouseholdInvite entity: add invitedBy field, set on createInvite
- New DTOs: InviteInfoResponse, AcceptInviteRequest
- HouseholdService: add getInviteInfo(), rewrite acceptInvite(code, name, email, password) — creates UserAccount + joins household in one transaction
- HouseholdController: GET /v1/invites/{code} (unauthenticated), POST /v1/invites/{code}/accept creates session after join
- SecurityConfig: permitAll() for /v1/invites/*, sessionFixation().changeSessionId()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 21:24:26 +02:00
60d84c0c94 fix(members): add error toasts for invite failures + Content-Type header
- handleInviteClick: show toast and bail early when POST /invites fails
- handleRegenerate: show toast when regeneration POST fails
- handleRoleChange: add Content-Type: application/json header on PATCH

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 20:31:19 +02:00
9d3be84a0c fix(members): guard against removing the last planner from household
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>
2026-04-10 20:30:55 +02:00
eb5699577b feat(backend): make invite share URL base configurable via app.base-url
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>
2026-04-10 20:07:53 +02:00
05476ecaab fix(members/invites): unwrap ApiResponse before returning to client
POST /members/invites was returning the full ApiResponseInviteResponse
wrapper. The client set activeInvite directly from the response body,
so shareUrl/inviteCode/expiresAt were missing (nested under .data).
Fixed to return data?.data — the inner InviteResponse — matching the
shape that InvitePanel and page.server.ts already expect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 19:57:13 +02:00
c40b0fe095 fix(members): create invite on first click when no active invite exists
When activeInvite is null and the user clicks the invite card, POST to
/members/invites first to generate a code, then toggle the panel open.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 19:55:00 +02:00
4e67ff4258 feat(members): align grid UI to spec — avatar colors, badges, join date, invite panel
- MemberCard: white bg, 1px border + shadow-card, centered column layout,
  avatar color by role (green-dark/blue), role badge with role-specific colors,
  join date "seit DD.MM.YYYY", Du-badge below join date, ⋯ kebab with icons
  and divider, inline role-control with Abbrechen, blue editing border #B5D4F4
- InviteCard: white bg, 1.5px dashed border, min-height 180px, plus circle,
  label "Mitglied einladen", full hover state (green border/bg/icon/label)
- InvitePanel: white bg, title "Einladelink teilen", description, mono link
  box, yellow expiry pill when ≤ 24h, text-link "Neuen Link generieren"
- RemoveDialog: white bg, padding 28px 32px, "?" in title, updated body text
- +page.server.ts: expose householdName from locals.haushalt
- +page.svelte: subtitle "{n} Mitglieder · {householdName}"
- Tests: add join date format test, Abbrechen test, InvitePanel title test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 19:54:26 +02:00
df3b774f0c fix(members): match settings page padding and h1 typography
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 19:45:32 +02:00
1b5704c8b5 feat(members): implement /members page — Kachel-Ansicht (E2, issue #48)
Backend:
- Rename V006 migration to V026 (avoid conflict with existing V006)
- Migration adds invalidated_at + partial unique index on household_invite

Frontend:
- Toast.svelte — new system component (message + dismiss)
- SegmentedControl.svelte — new system component (options, value, onchange)
- members/+page.server.ts — loads members + active invite
- members/[userId]/+server.ts — DELETE/PATCH proxy
- members/invites/+server.ts — POST (regenerate) proxy
- MemberCard.svelte — tile with avatar, kebab, inline role edit
- RemoveDialog.svelte — confirmation dialog (desktop modal + BottomSheet mobile)
- InviteCard.svelte + InvitePanel.svelte — invite management UI
- MemberGrid.svelte — responsive 4/2-col grid with sorted members
- members/+page.svelte — page composing all components with optimistic updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 19:01:08 +02:00
b04f2c51d2 feat(members): update schema.d.ts with GET invites, DELETE/PATCH member types
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 18:42:52 +02:00
d1e4b6c49e feat(members): implement DELETE/PATCH member + GET invites backend endpoints
- Add V006 migration: invalidated_at column + partial unique index on household_invite
- Add findByHouseholdIdAndInvalidatedAtIsNull, findByHouseholdIdAndUserId, countByHouseholdIdAndRole
- Add ChangeRoleRequest DTO
- HouseholdService: getActiveInvite, createInvite (regenerate), removeMember, changeMemberRole
- HouseholdController: GET /v1/households/mine/invites, DELETE/PATCH /v1/households/mine/members/{userId}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 18:41:38 +02:00
27163e3d72 feat(nav): remove Mitglieder link from desktop sidebar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:32:37 +02:00
5904102b1a refactor(settings): document benutzer non-null assertion in page.server.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:23:31 +02:00
d66120b191 refactor(settings): replace hardcoded #C0BFB8 with --color-border-hover token
Add --color-border-hover to the design system neutrals and replace the
hardcoded hex in all three card definitions (settings hub ×2, SettingsCard).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:23:22 +02:00
98c8aa9610 refactor(settings): remove dead accent prop from SettingsCard
The green left border was removed from the design — the accent prop,
data-accent attribute, and inline style were never used on the hub page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:22:54 +02:00
af275642b0 refactor(staples): remove redundant {#if !isOnboarding} guards in else block
The outer {:else} already guarantees isOnboarding is false — inner guards
were always-true dead conditions unreachable by tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:22:20 +02:00
dde78baa84 fix(settings): remove green left border from Vorräte card — no active state on hub page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 17:07:30 +02:00
6e559d9f9d refactor(staples): remove ctx=settings — default view is settings, only ctx=onboarding differs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:57:48 +02:00
ef39a97f57 fix(settings): align hub page with V2 spec — padding, H1, card radius/padding/sizes
- Content area: p-[16px_20px] md:p-[40px_56px], max-w-[820px] grid
- H1: display font, 28px, weight 500, tracking -0.02em
- Cards: --radius-xl, p-[28px], shadow-card
- Card title: 16px (was 14px)
- Stat number: 28px font-light tracking-[-0.02em] (was 36px)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:49:56 +02:00
824bb9445f refactor(staples): move household/staples route into (app) group — adds sidebar nav
Onboarding context uses fixed inset overlay to cover AppShell.
Settings context inherits AppShell layout by default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:47:25 +02:00
b0fc9f55c1 feat(nav): pass extraPaths to isActiveRoute in DesktopSidebar and MobileTabBar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:45:32 +02:00
2ed5186ac8 feat(nav): add extraPaths to NavItem — Einstellungen active on /household/staples
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:44:11 +02:00
48802a04f7 feat(settings): add autosave hint text below StaplesManager on D3 when ctx=settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:30:19 +02:00
0b3d062ed1 feat(settings): add ← Einstellungen back-link on D3 staples page when ctx=settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:28:39 +02:00
109b41b434 feat(settings): implement settings hub page with three cards (Vorräte, Haushalt, Profil)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:27:18 +02:00
3f9fb900c4 feat(settings): add SettingsCard component with title, href, cta, meta, accent props
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:24:38 +02:00
33cccd3d63 feat(settings): add +page.server.ts loading stapleCount, memberCount, userName
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 16:23:15 +02:00
cfbde18435 test(planner): clear shared mocks before each RecipePickerDrawer test
Adds beforeEach(vi.clearAllMocks) to prevent shared vi.fn() state in
baseProps from leaking across tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:18:50 +02:00
4835231f6d test(planner): cover Entfernen hidden when slot.id is null
Adds regression test for the {#if slot.id} guard on the remove button —
QA flagged the missing negative test case for optimistic slots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:18:16 +02:00
3f9bd2b226 refactor(planner): replace hardcoded values with design tokens
- border-radius: 10px → var(--radius-lg) in both tile components
- opacity: 0.42 → var(--opacity-dimmed) in DesktopDayTile
- var(--yellow) → var(--color-ring-today) for today ring and date circle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:17:50 +02:00
9423cd673c refactor(planner): type tag mapping callback as TagItem in server load
Replaces (t: any) with (t: TagItem) so the API response shape is
validated against the shared TagItem interface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:16:50 +02:00
4c87d9c134 feat(planner): sanitize heroImageUrl before embedding in CSS url()
Extracts sanitizeForCssUrl helper that strips '"()\ before the URL
is embedded in url("..."). Prevents CSS injection via the hero image
field in inline style bindings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:16:10 +02:00
e5c361fe42 refactor(planner): import shared types in reasoningTags instead of re-declaring
Removes local TagItem, Recipe, SlotRecipe, Slot, SlotMap definitions
and imports Recipe, Slot, SlotMap from types.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:13:55 +02:00
a8a781f1e9 refactor(planner): import shared types in EmptyDayTile instead of re-declaring
Removes local TagItem, SuggestionRecipe, TopSuggestion, Slot interfaces
and imports Suggestion, SlotMap from types.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:13:25 +02:00
b0800ca4f3 refactor(planner): import shared types in DesktopDayTile instead of re-declaring
Removes local TagItem, SlotRecipe, Slot, Suggestion interfaces and
imports Recipe, Slot, Suggestion from types.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:12:51 +02:00
66447a7ea0 refactor(planner): export Slot and SlotMap from types.ts
Adds shared Slot and SlotMap interfaces so DesktopDayTile,
EmptyDayTile, and reasoningTags can import rather than re-declare.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 14:12:03 +02:00
7f4413852d fix(planner): bump front face font sizes again
name: 17→19px, meta: 12→14px, tags: 10→12px,
day-abbr: 11→13px, day-num: 12→14px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:53:29 +02:00
eb3f6fad25 fix(planner): bump front face font sizes
name: 15→17px, meta: 10→12px, tags: 8→10px,
day-abbr: 9→11px, day-num: 10→12px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:52:05 +02:00
fc682bfc54 fix(planner): increase tile front face recipe name to 15px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:50:28 +02:00
38528a50e5 fix(planner): eliminate front-face bleed by removing preserve-3d
transform-style:preserve-3d on a parent with box-shadow/transition
causes Chrome to fail backface-visibility:hidden. Replace with
independent per-face rotateY transforms:
  front: 0deg → -180deg (flipped)
  back:  180deg → 0deg (flipped)
No preserve-3d needed — each face is its own compositing layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:49:28 +02:00
a43a8ec33f fix(planner): prevent front face bleeding through flipped card
overflow:hidden on direct children of preserve-3d flattens the 3D
context in Chrome, causing backface-visibility:hidden to fail.

Move border-radius + overflow to inner wrapper divs (.card-front-inner,
.card-back-inner) and keep the face elements themselves free of those
properties. Also add -webkit-backface-visibility:hidden and
will-change:transform for consistent GPU compositing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:47:10 +02:00
8679ebc6e3 fix(planner): fix flip tile pointer events and selected ring hover
- backface-visibility hides elements visually but not to pointer events;
  disable pointer events on the hidden face explicitly so the X button
  on the back face is clickable and the front face doesn't intercept clicks
- Add .scene-selected:hover rule so green ring is not overwritten by the
  higher-specificity .scene:hover box-shadow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:42:06 +02:00
0ae1767649 feat(planner): align tile design with spec
Front face:
- Full dual gradient overlay (dark top 32% → transparent → dark bottom 55%)
- Day abbreviation + date number pill at top of each tile
- Recipe name 13px/weight-300 with text-shadow
- Meta line (cookTimeMin · effort) below name
- Glassmorphism tag pills (protein + cuisine only)
- State rings via box-shadow (yellow for today, green for selected)
- Dimming (opacity 0.42) on non-selected filled tiles

Back face:
- Koch-Modus as green primary button
- Entfernen as red outline (transparent bg)
- All buttons 11px / weight 500

EmptyDayTile: add day header + spec-aligned suggestion list layout
Page: remove external column header (now rendered inside each tile)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:29:58 +02:00
d54ac6a37a feat(planner): use cuisine gradient as fallback when no protein tag
Fallback chain: heroImageUrl → protein gradient → cuisine gradient → surface.
Also rename --gradient-cuisine-italienisch → --gradient-cuisine-deutsch
(actual seed tag) with an earthy warm-grey colour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 12:12:03 +02:00