feat(staples): rework settings page — tile layout, seed catalog, add ingredient #59
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Overview
The current
/household/staplespage has three compounding problems:/settingsand/membersThis issue covers all three fixes as a single cohesive feature.
Design spec:
specs/staples-settings-tile.htmlScope
A — Tile redesign (visual)
Wrap each
CategorySectionin the same tile shell asSettingsCard:border-radius--radius-xl(16px)background--color-surfaceborder1px solid --color-borderbox-shadow--shadow-card/ hover--shadow-raisedpadding28px(mobile:20px)Grid change — switch from
md:grid-cols-3 gap-[24px_32px]tosm:grid-cols-2 gap-4 max-w-[820px](matches/settingsexactly).Tile header — replace bare
10px uppercasecategory label with:16px / 500title (matchesSettingsCard)7 / 18(green when > 0, gray when 0)--color-borderoutline)Chip overflow — cap visible chips at 8 per tile, show
+N weitere …expand trigger.Page header — match
/settingsand/membersexactly: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:
Implementation:
HouseholdSeedService.javacalled fromHouseholdService.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ügentrigger 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:
POST /v1/ingredientswith{ name, categoryId }— new backend endpoint neededisStaple: true)"Pfeffer" existiert bereitsmessage + existing chip briefly highlighted with green outline ringKonnte nicht gespeichert werden.inlineNewly created ingredients default to
isStaple = true(user explicitly added it — safe to assume they want it tracked).Tasks
Backend
POST /v1/ingredientsendpoint (IngredientController) —{ name, categoryId? }→ 201 / 409HouseholdSeedService— seed categories + ingredients on household creationFrontend
CategorySection.svelte— tile shell, count badge, Alle/Keine, overflow, add-ingredient footerStaplesManager.svelte— grid class update,handleSelectAll/handleDeselectAll/handleAddIngredientPOST /household/staples/ingredients/+server.ts+page.svelte— updated h1 row with total countStapleChip.svelte— optionalhighlightprop for duplicate feedbackOut of scope
Design spec committed to master:
2ad75ccFiles:
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)🔒 Sable — Security Engineer
Access Control
POST /v1/ingredientsis the critical path here. ThecategoryIdin 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 useshouseholdResolver— make sure POST follows the same pattern.plannerrole (good). Verify this is enforced at the Spring Security /SecurityFilterChainlevel, not just assumed from calling context. Amemberwho discovers the endpoint should get 403, not 201.Input Validation
varchar(n)ortext? 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."Pfeffer" existiert bereitsreflects 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
Seed Service
Rate Limiting
POST /v1/ingredientscould 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).⚙️ Backend Engineer
Seed Service Design
@Transactionalmethod. If one ingredient insert fails, everything rolls back cleanly. Don't seed categories in one transaction and ingredients in another.HouseholdSeedServicedirectly fromHouseholdService.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/ingredientsDesignisStapledefault inconsistency: Recipe-created ingredients default toisStaple = false. This endpoint defaults totrue. 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.categoryIdis 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 whethercategoryIdshould be required, or whether uncategorized ingredients show up in a catch-all tile.{ id, name, isStaple, categoryId }". The existingIngredientResponseDTO should cover this — confirm it includescategoryIdand that the mapper is correct before writing the controller.categoryIdis provided but doesn't exist? Add that path explicitly.Flyway
Ingrediententity gains amaxLengthconstraint (as Sable flagged), that needs a migration. Add a task for it.🎨 Atlas — UI/UX Designer
Tile Header on Mobile
Duplicate Highlight Signal
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 usingoutline-[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
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 keepingshadow-cardas static elevation and only applying hover on the individual Alle/Keine buttons and chips. Alternatively, the hover state is fine if we addcursor: defaultexplicitly on the tile wrapper to suppress pointer cursor.Empty Category Edge Case
Overflow Trigger Contrast
+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-texton 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.💻 Kai — Frontend Engineer
Reactive State for
categoriesStaplesManagercurrently initializesstapleStatefromcategories(a prop from the load function). ForhandleAddIngredient, the spec says to updatecategoriesreactively withcategories = categories.map(...). Butcategoriesis 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.autofocuson Dynamic Inputautofocuson an input that appears after a state change (i.e.,addMode = true) is unreliable in Svelte 5 + SSR environments. Use a Svelte action instead:Overflow + New Ingredient Visibility
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.highlightTimeout inStapleChipsetTimeout(() => { addHighlightId = null; }, 2000)inCategorySection. This is a side effect — wrap it in$effector ensure it's called only in browser context (if (browser) setTimeout(...)). In SSR,setTimeoutwill execute server-side if not guarded.New Route Naming
/household/staples/+server.ts. Adding a sub-route at/household/staples/ingredients/+server.tsis clean. Just make sure the SvelteKit route doesn't conflict with a potential+page.svelteat/household/staples/ingredientsif that page is ever added.Component Split
CategorySection.svelteis 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 toCategoryAddInput.svelte— it has its own state (addMode,addInput,addError,addHighlightId) and its own error display. The parent just receivesonAddIngredientcallback.🧪 QA Engineer
Backend —
POST /v1/ingredientstest matrixEvery path needs a test:
memberrole (not planner)Backend — Seed service
Frontend —
CategorySectioncomponent testsMissing cases that need explicit tests:
addModereturns false, no chip added, no error shown.Missing acceptance criteria in the issue
The issue doesn't define what "done" looks like for the seed. Suggest adding:
/household/staplescreateHousehold, not retroactively)