feat(staples): rework settings page — tile layout, seed catalog, add ingredient #59

Open
opened 2026-04-10 20:22:55 +02:00 by marcel · 6 comments
Owner

Overview

The current /household/staples page has three compounding problems:

  1. Visual inconsistency — plain divs with no tile shell, looks like a different app than /settings and /members
  2. Empty on day one — new households have no ingredients; the page is blank until recipes are created
  3. No way to add ingredients — if a staple never appears in a recipe (salt, olive oil), it can never be marked as a staple

This issue covers all three fixes as a single cohesive feature.

Design spec: specs/staples-settings-tile.html


Scope

A — Tile redesign (visual)

Wrap each CategorySection in the same tile shell as SettingsCard:

Token Value
border-radius --radius-xl (16px)
background --color-surface
border 1px solid --color-border
box-shadow --shadow-card / hover --shadow-raised
padding 28px (mobile: 20px)

Grid change — switch from md:grid-cols-3 gap-[24px_32px] to sm:grid-cols-2 gap-4 max-w-[820px] (matches /settings exactly).

Tile header — replace bare 10px uppercase category label with:

  • 16px / 500 title (matches SettingsCard)
  • count badge: 7 / 18 (green when > 0, gray when 0)
  • Alle / Keine micro-buttons (10px, pill-shaped, --color-border outline)

Chip overflow — cap visible chips at 8 per tile, show +N weitere … expand trigger.

Page header — match /settings and /members exactly:

← Einstellungen
[h1 Vorräte]                    [23 gewählt]
Automatisch gespeichert. Gilt ab der nächsten Einkaufsliste.

B — German starter catalog (seed on household creation)

When a new household is created, automatically seed ~100 common German ingredients across 8 categories. All seeded with isStaple = false.

Categories + sortOrder:

  1. Gemüse
  2. Gewürze & Kräuter
  3. Öle & Essig
  4. Milch & Käse
  5. Getreide & Hülsenfrüchte
  6. Fleisch & Fisch
  7. Backzutaten
  8. Saucen & Würzmittel

Implementation: HouseholdSeedService.java called from HouseholdService.createHousehold(). Seed is idempotent (skip if ingredient already exists by citext name + household).

Full ingredient list is in the design spec (## 7).


C — Add ingredient inline (per category tile)

Each tile gets a + Zutat hinzufügen trigger at the bottom (below chips, separated by a 1px border). Tapping it replaces the trigger with an inline text input — no modal, no sheet.

Flow:

  1. User types name → Enter or button to save
  2. POST /v1/ingredients with { name, categoryId } — new backend endpoint needed
  3. Optimistic: new chip appears immediately as selected (isStaple: true)
  4. Escape or cancels with no change
  5. Duplicate (409): input gets error border + "Pfeffer" existiert bereits message + existing chip briefly highlighted with green outline ring
  6. Generic error: Konnte nicht gespeichert werden. inline

Newly created ingredients default to isStaple = true (user explicitly added it — safe to assume they want it tracked).


Tasks

Backend

  • POST /v1/ingredients endpoint (IngredientController) — { name, categoryId? } → 201 / 409
  • HouseholdSeedService — seed categories + ingredients on household creation
  • Unit tests for seed service (idempotency) and new POST endpoint

Frontend

  • CategorySection.svelte — tile shell, count badge, Alle/Keine, overflow, add-ingredient footer
  • StaplesManager.svelte — grid class update, handleSelectAll/handleDeselectAll/handleAddIngredient
  • New API route POST /household/staples/ingredients/+server.ts
  • +page.svelte — updated h1 row with total count
  • StapleChip.svelte — optional highlight prop for duplicate feedback
  • E2E / component tests for add flow (success, duplicate, cancel)

Out of scope

  • Recipe form typeahead (separate issue)
  • Ingredient rename / delete on the staples page
  • Category management UI
## Overview The current `/household/staples` page has three compounding problems: 1. **Visual inconsistency** — plain divs with no tile shell, looks like a different app than `/settings` and `/members` 2. **Empty on day one** — new households have no ingredients; the page is blank until recipes are created 3. **No way to add ingredients** — if a staple never appears in a recipe (salt, olive oil), it can never be marked as a staple This issue covers all three fixes as a single cohesive feature. **Design spec:** `specs/staples-settings-tile.html` --- ## Scope ### A — Tile redesign (visual) Wrap each `CategorySection` in the same tile shell as `SettingsCard`: | Token | Value | |---|---| | `border-radius` | `--radius-xl` (16px) | | `background` | `--color-surface` | | `border` | `1px solid --color-border` | | `box-shadow` | `--shadow-card` / hover `--shadow-raised` | | `padding` | `28px` (mobile: `20px`) | **Grid change** — switch from `md:grid-cols-3 gap-[24px_32px]` to `sm:grid-cols-2 gap-4 max-w-[820px]` (matches `/settings` exactly). **Tile header** — replace bare `10px uppercase` category label with: - `16px / 500` title (matches `SettingsCard`) - count badge: `7 / 18` (green when > 0, gray when 0) - **Alle** / **Keine** micro-buttons (10px, pill-shaped, `--color-border` outline) **Chip overflow** — cap visible chips at 8 per tile, show `+N weitere …` expand trigger. **Page header** — match `/settings` and `/members` exactly: ``` ← Einstellungen [h1 Vorräte] [23 gewählt] Automatisch gespeichert. Gilt ab der nächsten Einkaufsliste. ``` --- ### B — German starter catalog (seed on household creation) When a new household is created, automatically seed ~100 common German ingredients across 8 categories. All seeded with `isStaple = false`. **Categories + sortOrder:** 1. Gemüse 2. Gewürze & Kräuter 3. Öle & Essig 4. Milch & Käse 5. Getreide & Hülsenfrüchte 6. Fleisch & Fisch 7. Backzutaten 8. Saucen & Würzmittel **Implementation:** `HouseholdSeedService.java` called from `HouseholdService.createHousehold()`. Seed is idempotent (skip if ingredient already exists by citext name + household). Full ingredient list is in the design spec (`## 7`). --- ### C — Add ingredient inline (per category tile) Each tile gets a `+ Zutat hinzufügen` trigger at the bottom (below chips, separated by a 1px border). Tapping it replaces the trigger with an inline text input — no modal, no sheet. **Flow:** 1. User types name → **Enter** or **✓** button to save 2. `POST /v1/ingredients` with `{ name, categoryId }` — new backend endpoint needed 3. Optimistic: new chip appears immediately as selected (`isStaple: true`) 4. **Escape** or **✕** cancels with no change 5. **Duplicate (409):** input gets error border + `"Pfeffer" existiert bereits` message + existing chip briefly highlighted with green outline ring 6. **Generic error:** `Konnte nicht gespeichert werden.` inline **Newly created ingredients default to `isStaple = true`** (user explicitly added it — safe to assume they want it tracked). --- ## Tasks ### Backend - [ ] `POST /v1/ingredients` endpoint (`IngredientController`) — `{ name, categoryId? }` → 201 / 409 - [ ] `HouseholdSeedService` — seed categories + ingredients on household creation - [ ] Unit tests for seed service (idempotency) and new POST endpoint ### Frontend - [ ] `CategorySection.svelte` — tile shell, count badge, Alle/Keine, overflow, add-ingredient footer - [ ] `StaplesManager.svelte` — grid class update, `handleSelectAll`/`handleDeselectAll`/`handleAddIngredient` - [ ] New API route `POST /household/staples/ingredients/+server.ts` - [ ] `+page.svelte` — updated h1 row with total count - [ ] `StapleChip.svelte` — optional `highlight` prop for duplicate feedback - [ ] E2E / component tests for add flow (success, duplicate, cancel) --- ## Out of scope - Recipe form typeahead (separate issue) - Ingredient rename / delete on the staples page - Category management UI
Author
Owner

Design spec committed to master: 2ad75cc

Files:

  • specs/staples-settings-redesign.html — 5 initial concept variations (accordion, sidebar nav, search-first, checklist table, enhanced current)
  • specs/staples-settings-tile.html — chosen direction with full machine-readable implementation spec (tile anatomy, seed catalog, add-ingredient inline flow)
Design spec committed to master: `2ad75cc` **Files:** - `specs/staples-settings-redesign.html` — 5 initial concept variations (accordion, sidebar nav, search-first, checklist table, enhanced current) - `specs/staples-settings-tile.html` — chosen direction with full machine-readable implementation spec (tile anatomy, seed catalog, add-ingredient inline flow)
Author
Owner

🔒 Sable — Security Engineer

Access Control

  • Household scoping on POST /v1/ingredients is the critical path here. The categoryId in the request body must be validated to belong to the requesting user's household before inserting. If it's not checked, an attacker can supply a valid categoryId from a different household and inject ingredients into it — classic IDOR. The existing PATCH endpoint already uses householdResolver — make sure POST follows the same pattern.
  • The issue says POST requires planner role (good). Verify this is enforced at the Spring Security / SecurityFilterChain level, not just assumed from calling context. A member who discovers the endpoint should get 403, not 201.

Input Validation

  • No max length defined for ingredient name. What's the column type — varchar(n) or text? Without a constraint, a malicious client could store a multi-megabyte string. Add a @Size(max = 100) or similar on the DTO and a CHECK constraint at the DB level.
  • The duplicate error message "Pfeffer" existiert bereits reflects user input back to the UI. Svelte's template rendering is text-safe by default (no {@html}), so XSS risk is low — but confirm the backend never includes raw input in a 409 response body that gets rendered via {@html} anywhere.

Optimistic UI + Error Handling

  • The frontend adds the chip optimistically before server confirmation. If the server responds with 403 (e.g., role changed mid-session), the spec only describes "generic error" handling. The optimistically-added chip must be removed in that case — not just shown with an error message below the input.

Seed Service

  • Static seed data in Java constants = no injection risk. If the seed source ever becomes externalizable (config file, admin UI), add parameterized-query enforcement at that point.

Rate Limiting

  • POST /v1/ingredients could be hammered to bloat a household's ingredient catalog. Consider a max-ingredients-per-household guard in the service layer (if (count >= MAX_INGREDIENTS) throw HouseholdIngredientLimitExceededException). Not blocking, but worth a config constant even if the limit is generous (e.g., 500).
## 🔒 Sable — Security Engineer ### Access Control - **Household scoping on `POST /v1/ingredients` is the critical path here.** The `categoryId` in the request body must be validated to belong to the requesting user's household before inserting. If it's not checked, an attacker can supply a valid categoryId from a different household and inject ingredients into it — classic IDOR. The existing PATCH endpoint already uses `householdResolver` — make sure POST follows the same pattern. - The issue says POST requires `planner` role (good). Verify this is enforced at the Spring Security / `SecurityFilterChain` level, not just assumed from calling context. A `member` who discovers the endpoint should get 403, not 201. ### Input Validation - **No max length defined for ingredient name.** What's the column type — `varchar(n)` or `text`? Without a constraint, a malicious client could store a multi-megabyte string. Add a `@Size(max = 100)` or similar on the DTO and a CHECK constraint at the DB level. - The duplicate error message `"Pfeffer" existiert bereits` reflects user input back to the UI. Svelte's template rendering is text-safe by default (no `{@html}`), so XSS risk is low — but confirm the backend never includes raw input in a 409 response body that gets rendered via `{@html}` anywhere. ### Optimistic UI + Error Handling - The frontend adds the chip optimistically before server confirmation. If the server responds with 403 (e.g., role changed mid-session), the spec only describes "generic error" handling. The optimistically-added chip must be removed in that case — not just shown with an error message below the input. ### Seed Service - Static seed data in Java constants = no injection risk. If the seed source ever becomes externalizable (config file, admin UI), add parameterized-query enforcement at that point. ### Rate Limiting - `POST /v1/ingredients` could be hammered to bloat a household's ingredient catalog. Consider a max-ingredients-per-household guard in the service layer (`if (count >= MAX_INGREDIENTS) throw HouseholdIngredientLimitExceededException`). Not blocking, but worth a config constant even if the limit is generous (e.g., 500).
Author
Owner

⚙️ Backend Engineer

Seed Service Design

  • Transaction boundary: The full seed (categories + ingredients) should run in a single @Transactional method. If one ingredient insert fails, everything rolls back cleanly. Don't seed categories in one transaction and ingredients in another.
  • Idempotency edge case: The spec says "skip if ingredient already exists by citext name + household." But what about categories? If "Gemüse" already exists (e.g., user created it manually before seed ran), do we add the seed ingredients into that existing category, or skip them? Clarify the idempotency strategy: match-and-merge vs. skip-entire-category.
  • Event-driven vs. direct call: Calling HouseholdSeedService directly from HouseholdService.createHousehold() is fine for v1. If household creation later gains async steps, consider an @EventListener(HouseholdCreatedEvent.class) — but don't over-engineer now.

POST /v1/ingredients Design

  • isStaple default inconsistency: Recipe-created ingredients default to isStaple = false. This endpoint defaults to true. That's an intentional design decision per the spec ��� just make sure it's documented in the service/DTO so it doesn't surprise a future developer.
  • categoryId is optional per the spec ({ name, categoryId? }). If omitted, the ingredient has no category and won't appear in any category tile. Is that the desired behavior? An uncategorized ingredient is invisible on the staples page. Consider whether categoryId should be required, or whether uncategorized ingredients show up in a catch-all tile.
  • Response DTO: The issue says "201 + { id, name, isStaple, categoryId }". The existing IngredientResponse DTO should cover this — confirm it includes categoryId and that the mapper is correct before writing the controller.
  • Status codes: 201 for creation ✓, 409 for duplicate ✓. What about 404 if categoryId is provided but doesn't exist? Add that path explicitly.

Flyway

  • No Flyway migration is listed in the tasks. If the Ingredient entity gains a maxLength constraint (as Sable flagged), that needs a migration. Add a task for it.
## ⚙️ Backend Engineer ### Seed Service Design - **Transaction boundary:** The full seed (categories + ingredients) should run in a single `@Transactional` method. If one ingredient insert fails, everything rolls back cleanly. Don't seed categories in one transaction and ingredients in another. - **Idempotency edge case:** The spec says "skip if ingredient already exists by citext name + household." But what about categories? If "Gemüse" already exists (e.g., user created it manually before seed ran), do we add the seed ingredients into that existing category, or skip them? Clarify the idempotency strategy: match-and-merge vs. skip-entire-category. - **Event-driven vs. direct call:** Calling `HouseholdSeedService` directly from `HouseholdService.createHousehold()` is fine for v1. If household creation later gains async steps, consider an `@EventListener(HouseholdCreatedEvent.class)` — but don't over-engineer now. ### `POST /v1/ingredients` Design - **`isStaple` default inconsistency:** Recipe-created ingredients default to `isStaple = false`. This endpoint defaults to `true`. That's an intentional design decision per the spec ��� just make sure it's documented in the service/DTO so it doesn't surprise a future developer. - **`categoryId` is optional** per the spec (`{ name, categoryId? }`). If omitted, the ingredient has no category and won't appear in any category tile. Is that the desired behavior? An uncategorized ingredient is invisible on the staples page. Consider whether `categoryId` should be required, or whether uncategorized ingredients show up in a catch-all tile. - **Response DTO:** The issue says "201 + `{ id, name, isStaple, categoryId }`". The existing `IngredientResponse` DTO should cover this — confirm it includes `categoryId` and that the mapper is correct before writing the controller. - **Status codes:** 201 for creation ✓, 409 for duplicate ✓. What about 404 if `categoryId` is provided but doesn't exist? Add that path explicitly. ### Flyway - No Flyway migration is listed in the tasks. If the `Ingredient` entity gains a `maxLength` constraint (as Sable flagged), that needs a migration. Add a task for it.
Author
Owner

🎨 Atlas — UI/UX Designer

Tile Header on Mobile

  • Long category names + badge + two buttons = cramped. "Getreide & Hülsenfrüchte" with a badge and "Alle"/"Keine" in one flex row on a 320px screen will overflow or wrap awkwardly. Options: (a) abbreviate button labels to icons (✓/✗) on mobile, (b) move the bulk-action buttons below the title on small screens, (c) only show them on hover/focus (desktop-only). Please prototype this before implementation starts.

Duplicate Highlight Signal

  • The duplicate chip highlight uses outline-[var(--green-light)] — this is the same color as the focus ring. A keyboard user focusing a chip while the duplicate highlight is active will see identical visual states, which is confusing. Consider using outline-[var(--yellow-light)] for the "this already exists" signal, or a brief background flash to --yellow-tint. Yellow reads as "attention" without conflicting with green's "selected/active" meaning.

Tile Hover State

  • The spec gives tiles hover:shadow-raised + hover:border-border-hover. Since the tile is not a link or button (it doesn't navigate), this hover state can mislead users into expecting a click action. Recommend keeping shadow-card as static elevation and only applying hover on the individual Alle/Keine buttons and chips. Alternatively, the hover state is fine if we add cursor: default explicitly on the tile wrapper to suppress pointer cursor.

Empty Category Edge Case

  • A tile with zero ingredients (e.g., a custom user-created category, or a seed category where all ingredients were deleted) would show only the header row and the "+ Zutat hinzufügen" trigger. That's visually valid but needs to be tested in the component — the tile shouldn't collapse or look broken at minimum height.

Overflow Trigger Contrast

  • The +N weitere … text is --color-text-muted (6B6A63) on --color-surface (F5F4EE). Contrast ratio is ~3.8:1 — below 4.5:1 for normal-size text. Either bump to --color-text on rest state or increase font weight to 500 so it qualifies as "large text" (3:1 threshold). The underline helps but doesn't fix the contrast ratio.
## 🎨 Atlas — UI/UX Designer ### Tile Header on Mobile - **Long category names + badge + two buttons = cramped.** "Getreide & Hülsenfrüchte" with a badge and "Alle"/"Keine" in one flex row on a 320px screen will overflow or wrap awkwardly. Options: (a) abbreviate button labels to icons (✓/✗) on mobile, (b) move the bulk-action buttons below the title on small screens, (c) only show them on hover/focus (desktop-only). Please prototype this before implementation starts. ### Duplicate Highlight Signal - The duplicate chip highlight uses `outline-[var(--green-light)]` — this is **the same color as the focus ring**. A keyboard user focusing a chip while the duplicate highlight is active will see identical visual states, which is confusing. Consider using `outline-[var(--yellow-light)]` for the "this already exists" signal, or a brief background flash to `--yellow-tint`. Yellow reads as "attention" without conflicting with green's "selected/active" meaning. ### Tile Hover State - The spec gives tiles `hover:shadow-raised + hover:border-border-hover`. Since the tile is **not a link or button** (it doesn't navigate), this hover state can mislead users into expecting a click action. Recommend keeping `shadow-card` as static elevation and only applying hover on the individual Alle/Keine buttons and chips. Alternatively, the hover state is fine if we add `cursor: default` explicitly on the tile wrapper to suppress pointer cursor. ### Empty Category Edge Case - A tile with zero ingredients (e.g., a custom user-created category, or a seed category where all ingredients were deleted) would show only the header row and the "+ Zutat hinzufügen" trigger. That's visually valid but needs to be tested in the component — the tile shouldn't collapse or look broken at minimum height. ### Overflow Trigger Contrast - The `+N weitere …` text is `--color-text-muted` (6B6A63) on `--color-surface` (F5F4EE). Contrast ratio is ~3.8:1 — below 4.5:1 for normal-size text. Either bump to `--color-text` on rest state or increase font weight to 500 so it qualifies as "large text" (3:1 threshold). The underline helps but doesn't fix the contrast ratio.
Author
Owner

💻 Kai — Frontend Engineer

Reactive State for categories

  • StaplesManager currently initializes stapleState from categories (a prop from the load function). For handleAddIngredient, the spec says to update categories reactively with categories = categories.map(...). But categories is currently a plain prop, not $state. It needs to be wrapped: let categories = $state(data.categories) or managed internally. Confirm this before writing the handler — mutating a prop directly won't trigger reactivity in Svelte 5.

autofocus on Dynamic Input

  • autofocus on an input that appears after a state change (i.e., addMode = true) is unreliable in Svelte 5 + SSR environments. Use a Svelte action instead:
    function focusOnMount(node: HTMLElement) {
      node.focus();
    }
    
    <input use:focusOnMount ... />
    
    This fires after the DOM is updated, not on initial page load.

Overflow + New Ingredient Visibility

  • When expanded = false (showing only 8 chips) and a new ingredient is added via the inline form, the new chip could silently land beyond the visible limit. Users would see the count badge increment but no chip appear — confusing. Recommendation: auto-expand the tile on successful add (expanded = true) so the new chip is always visible immediately.

highlight Timeout in StapleChip

  • The spec uses setTimeout(() => { addHighlightId = null; }, 2000) in CategorySection. This is a side effect — wrap it in $effect or ensure it's called only in browser context (if (browser) setTimeout(...)). In SSR, setTimeout will execute server-side if not guarded.

New Route Naming

  • The existing PATCH lives at /household/staples/+server.ts. Adding a sub-route at /household/staples/ingredients/+server.ts is clean. Just make sure the SvelteKit route doesn't conflict with a potential +page.svelte at /household/staples/ingredients if that page is ever added.

Component Split

  • CategorySection.svelte is taking on tile shell, header, bulk actions, chip cloud, overflow, AND the inline add form. That's a lot for one file. Consider extracting the add form to CategoryAddInput.svelte — it has its own state (addMode, addInput, addError, addHighlightId) and its own error display. The parent just receives onAddIngredient callback.
## 💻 Kai — Frontend Engineer ### Reactive State for `categories` - `StaplesManager` currently initializes `stapleState` from `categories` (a prop from the load function). For `handleAddIngredient`, the spec says to update `categories` reactively with `categories = categories.map(...)`. But `categories` is currently a plain prop, not `$state`. It needs to be wrapped: `let categories = $state(data.categories)` or managed internally. Confirm this before writing the handler — mutating a prop directly won't trigger reactivity in Svelte 5. ### `autofocus` on Dynamic Input - `autofocus` on an input that appears after a state change (i.e., `addMode = true`) is unreliable in Svelte 5 + SSR environments. Use a Svelte action instead: ```js function focusOnMount(node: HTMLElement) { node.focus(); } ``` ```svelte <input use:focusOnMount ... /> ``` This fires after the DOM is updated, not on initial page load. ### Overflow + New Ingredient Visibility - When `expanded = false` (showing only 8 chips) and a new ingredient is added via the inline form, the new chip could silently land beyond the visible limit. Users would see the count badge increment but no chip appear — confusing. **Recommendation:** auto-expand the tile on successful add (`expanded = true`) so the new chip is always visible immediately. ### `highlight` Timeout in `StapleChip` - The spec uses `setTimeout(() => { addHighlightId = null; }, 2000)` in `CategorySection`. This is a side effect — wrap it in `$effect` or ensure it's called only in browser context (`if (browser) setTimeout(...)`). In SSR, `setTimeout` will execute server-side if not guarded. ### New Route Naming - The existing PATCH lives at `/household/staples/+server.ts`. Adding a sub-route at `/household/staples/ingredients/+server.ts` is clean. Just make sure the SvelteKit route doesn't conflict with a potential `+page.svelte` at `/household/staples/ingredients` if that page is ever added. ### Component Split - `CategorySection.svelte` is taking on tile shell, header, bulk actions, chip cloud, overflow, AND the inline add form. That's a lot for one file. Consider extracting the add form to `CategoryAddInput.svelte` — it has its own state (`addMode`, `addInput`, `addError`, `addHighlightId`) and its own error display. The parent just receives `onAddIngredient` callback.
Author
Owner

🧪 QA Engineer

Backend — POST /v1/ingredients test matrix

Every path needs a test:

Path Expected
Valid name + valid categoryId 201 + created ingredient
Valid name + no categoryId 201 (confirm ingredient saved without category)
Duplicate name, same case 409
Duplicate name, different case ("pfeffer" vs "Pfeffer") 409 — citext must catch this
Empty/blank name 400 or 422
Name at max length boundary 201
Name exceeding max length 400 or 422
Valid name + categoryId from different household 403 or 404
Valid name + non-existent categoryId 404
Unauthenticated request 401
member role (not planner) 403

Backend — Seed service

  • Idempotency test: Run seed twice on the same household, assert ingredient count equals the expected number (no duplicates created).
  • Isolation test: Create two households, assert each gets its own full seed — no cross-household ingredient leakage.
  • Transaction test: Mock one insert to fail mid-seed, assert zero ingredients and zero categories are persisted (rollback confirmed).

Frontend — CategorySection component tests

Missing cases that need explicit tests:

  • Add flow — success: User types name + clicks ✓ → input clears, tile returns to rest state, new chip is visible and selected.
  • Add flow — duplicate (409): Input gets error border, error message appears, existing chip gets highlight class.
  • Add flow — cancel (Escape / ✕): addMode returns false, no chip added, no error shown.
  • Add flow — empty input: ✓ button disabled, pressing Enter does nothing.
  • Overflow expand: With 10 chips, only 8 shown + trigger visible. Click trigger → all 10 visible, trigger gone.
  • Overflow + add: After successful add when tile was collapsed, tile auto-expands and new chip is visible.
  • Alle button: All chips in category toggle to selected, count badge updates.
  • Keine button: All chips deselect, badge turns gray.
  • Badge color: Selected > 0 → green; selected = 0 → gray.

Missing acceptance criteria in the issue

The issue doesn't define what "done" looks like for the seed. Suggest adding:

  • A fresh household shows all 8 categories populated on /household/staples
  • Running seed twice doesn't create duplicate categories or ingredients
  • A household created before this feature ships is unaffected (seed only runs on createHousehold, not retroactively)
## 🧪 QA Engineer ### Backend — `POST /v1/ingredients` test matrix Every path needs a test: | Path | Expected | |---|---| | Valid name + valid categoryId | 201 + created ingredient | | Valid name + no categoryId | 201 (confirm ingredient saved without category) | | Duplicate name, same case | 409 | | Duplicate name, different case ("pfeffer" vs "Pfeffer") | 409 — citext must catch this | | Empty/blank name | 400 or 422 | | Name at max length boundary | 201 | | Name exceeding max length | 400 or 422 | | Valid name + categoryId from different household | 403 or 404 | | Valid name + non-existent categoryId | 404 | | Unauthenticated request | 401 | | `member` role (not planner) | 403 | ### Backend — Seed service - **Idempotency test:** Run seed twice on the same household, assert ingredient count equals the expected number (no duplicates created). - **Isolation test:** Create two households, assert each gets its own full seed — no cross-household ingredient leakage. - **Transaction test:** Mock one insert to fail mid-seed, assert zero ingredients and zero categories are persisted (rollback confirmed). ### Frontend — `CategorySection` component tests Missing cases that need explicit tests: - **Add flow — success:** User types name + clicks ✓ → input clears, tile returns to rest state, new chip is visible and selected. - **Add flow — duplicate (409):** Input gets error border, error message appears, existing chip gets highlight class. - **Add flow — cancel (Escape / ✕):** `addMode` returns false, no chip added, no error shown. - **Add flow — empty input:** ✓ button disabled, pressing Enter does nothing. - **Overflow expand:** With 10 chips, only 8 shown + trigger visible. Click trigger → all 10 visible, trigger gone. - **Overflow + add:** After successful add when tile was collapsed, tile auto-expands and new chip is visible. - **Alle button:** All chips in category toggle to selected, count badge updates. - **Keine button:** All chips deselect, badge turns gray. - **Badge color:** Selected > 0 → green; selected = 0 → gray. ### Missing acceptance criteria in the issue The issue doesn't define what "done" looks like for the seed. Suggest adding: - A fresh household shows all 8 categories populated on `/household/staples` - Running seed twice doesn't create duplicate categories or ingredients - A household created before this feature ships is unaffected (seed only runs on `createHousehold`, not retroactively)
Sign in to join this conversation.