Frontend: A2 — Household setup + invite #19
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?
Summary
Planner names the household and optionally generates an invite link for household members. Step 1 of 3 in onboarding.
Journey: J6 — Household setup
Role: Planner only
Layout
Mobile (< 768px)
Desktop (> 1024px)
--color-surfacebg, 3 numbered steps--color-pagebg, centered, max-width 420pxProgress Steps
Form Fields
household_member(role=planner) → A3Behavior
Acceptance Criteria
Spec file:
specs/frontend/j6-household-setup.html— screen A2 with mobile + desktop previews, agent table, and LLM implementation guide.🧑💻 Kai — Frontend Engineer
Questions & Observations
Layout group: A1 is
(auth)(no nav, no session). A2 is post-signup — the user is authenticated. But A2 also has no standard nav chrome; it shows a progress sidebar instead. Should A2 live in a new(onboarding)route group with its own layout, or does it re-use the(auth)bare layout? If we add more onboarding screens later (A3, etc.), a dedicated(onboarding)/+layout.sveltewith the progress sidebar baked in is cleaner than repeating the sidebar in every page.A3 route path: The Continue button redirects to A3 — same problem we had with A2 in issue #18. What's the exact path?
/household/staples? Needs to be confirmed before the form action can be written.Invite link generation flow: "Generate invite link" is a button that triggers an API call and then displays the link. Is this a second named form action (
actions.generateInvite) in+page.server.ts, or a+server.tsendpoint hit viafetch? A form action withuse:enhanceis simpler and avoids client-side state management for the generated link. But it requires the household to already exist (or be created atomically). Which comes first — household creation or invite generation?Household created on Continue or earlier?: The spec says "Continue → saves household + creates
household_member". Does "Generate invite link" work without having saved the household yet? If the invite code must be attached to a household ID on the backend, the household needs to exist before the invite is generated. This ordering matters for the UX and the form action structure.Component split:
ProgressSidebar.svelte+HouseholdSetupForm.sveltefeels right. ProgressSidebar is likely reused across A2, A3 — confirm whether it should live in a shared components folder or just be co-located with the onboarding layout.Suggestions
$derived()on the name$state()handles this cleanly.🗄️ Backend Engineer — Spring Boot / PostgreSQL
Questions & Observations
Atomicity of Continue action: The spec says Continue "saves household + creates
household_member(role=planner)". Are these two separate inserts wrapped in a single transaction, or two separate API calls? They should be a single atomic operation — if household creation succeeds buthousehold_memberinsert fails, you'd have an orphaned household with no planner. One endpoint, one transaction."Existing invite code system": The spec references "the existing invite code system from the backend". Is this already implemented? If so, what does the endpoint look like —
POST /api/households/{id}/invite-codes? If it doesn't exist yet, this issue implicitly requires backend work. That dependency needs to be surfaced explicitly.Ordering problem: Invite codes must be associated with a household ID. But if the household is only created on Continue, how does "Generate invite link" work before the user clicks Continue? Either: (a) the household is created as a draft when the user lands on A2, (b) invite generation is only possible after Continue, or (c) the invite is generated client-side (weak — avoid). This is a significant design question.
Household name validation: What are the constraints? Min length (1? 2?)? Max length? Allowed characters? The spec shows "Smith family" as an example but gives no bounds. The DB column needs a CHECK constraint and the API needs
@Sizevalidation.Planner role assignment: The
household_memberis created withrole=planner. Isplannera value in a DB enum? And is it possible for the authenticated user to already have a household_member record (e.g., if they somehow reached A2 twice)? The endpoint should handle the duplicate case gracefully — 409 or idempotent.Suggestions
POST /api/households— creates household + household_member in a single transaction and returns the new household ID. Invite generation is a separate subsequent call:POST /api/households/{id}/invite-codes.household_member(user_account_id)if a user can only belong to one household. This enforces the business rule at the database level.🧪 QA Engineer — Test Coverage
Questions & Observations
The acceptance criteria only cover the happy path and layout. Here's what's missing:
Missing AC — bad paths & edge cases:
Missing AC — structural:
Suggestions
HouseholdSetupForm: Cover initial state (Continue disabled), name input enables Continue, generate invite link renders link, form submission calls the action.ProgressSidebar: Correct step highlighted givencurrentStepprop. Steps rendered with correct labels.🔒 Sable — Security Engineer
Questions & Observations
Authentication gate on A2: A2 is post-signup, so the user is authenticated. But is this enforced? The
hooks.server.tsshould redirect unauthenticated requests to/loginbefore they ever reach/household/setup. This must be confirmed — not assumed. A newly signed-up user who loses their session mid-onboarding and refreshes should land on login, not on a broken A2 page.Authorization — planner only: The issue says "Role: Planner only". At this point in the flow, the user has just signed up and has no household yet — so they have no
household_memberrecord and no role. How is "planner only" enforced? Is A2 accessible to any authenticated user who has no household, or is there a specific flag? The authorization logic needs to be explicit, not implicit.Invite code security: The invite code system is referenced but not specified here. Per the project threat model: codes must be UUIDv4 minimum (not guessable), single-use, and time-limited (expiry). If the existing backend system doesn't enforce all three, it's a gap. Specifically: what's the expiry window? 24 hours? 7 days? And is the code invalidated immediately upon use (or only after the invited user completes setup)?
Invite link exposure: The generated link is displayed in the UI and shared via messaging apps. This means the link will appear in message history, possibly on third-party servers. The time-limited + single-use constraints are the primary defense. Confirm these are non-negotiable.
CSRF on Continue action: Using SvelteKit form actions with
use:enhancecovers this via the built-in Origin check. If the invite generation uses afetchPOST instead, it needs an explicit CSRF token. Flag which pattern is chosen.Suggestions
household_memberrecord. The second check prevents a user from creating multiple households by hitting the endpoint repeatedly or replaying the request.<pre>block to prevent accidental re-submission.🎨 Atlas — UI/UX Designer
Questions & Observations
Progress sidebar — completed step treatment: The spec defines current step (green circle) and future steps (subtle circle), but doesn't define the visual for a completed step. When the user advances to A3, step 1 should look "done" — typically a checkmark icon or filled green circle with reduced prominence. Should completed steps use the same green circle as the current step, or a distinct "done" state? This matters for A3 and especially for the "You're ready" confirmation step.
Invite link display: After clicking "Generate invite link", where does the link appear? Inline below the button? In a separate card/box? The spec doesn't define this. Suggested treatment: a
--color-surfacebg container with--radius-md, the URL infont-mono(DM Mono), and a copy icon button. Does this match your intent?"Generate invite link" button style: Is this a secondary/outline button, or the same primary green as Continue? Since Continue is the primary action and invite generation is optional, the generate button should be visually subordinate — likely a ghost/outline variant.
Household name input placeholder: The spec shows "Smith family" as an example — is that the placeholder text, or just an illustration in the spec? Placeholder copy should be finalized (German or English?).
Continue button — disabled state: Should the Continue button be visually disabled (grayed out) when the name field is empty, or just show a validation error on attempt? Disabled-until-valid is a UX pattern that prevents confusion, but it can also frustrate users who don't know why the button isn't working. A subtle disabled state with an accessible
aria-disabledand a tooltip on hover would be ideal.Mobile: "Step 1 of 3" text: What's the typography?
font-sans, 14px,--color-text-subtle? This should match the spec token system.Suggestions
ProgressSidebar.svelte) that accepts acurrentStep: numberprop — not hardcoded per screen.$statetoggle, reset after 2 seconds). No toast needed — inline feedback is sufficient.aria-labelon the indicator.🧑💻 Kai — Frontend Engineer — Implementation Discussion
Worked through all open frontend items. Everything resolved.
Resolved
Route group — No route group. A2 lives at
src/routes/household/setup/+page.svelte— no(onboarding)or(auth)wrapper.A3 route path —
/household/staples. Continue action doesthrow redirect(303, '/household/staples').Household creation / invite ordering — Household is created when the user clicks Continue (the button that redirects to the invite page). Invite generation is not A2's concern — it lives on the subsequent page. No chicken-and-egg problem on A2.
Invite generation pattern — Standard
actions.defaultin+page.server.ts. Creates household + household_member in one action, then redirects. Invite logic is handled separately on the next page.ProgressSidebar location — Shared component at
$lib/components/ProgressSidebar.svelte. Reused across A2, A3, and any future onboarding steps.Unresolved
None.
Ready to implement. Form action creates household + household_member atomically, redirects to
/household/staples. ProgressSidebar is a shared component accepting acurrentStepprop.✅ Implementation complete — branch
feat/issue-19-household-setupWhat was built
All 4 plan items delivered with red/green/refactor TDD. 183 tests pass, 0 type errors.
Commits
b9ef06fProgressSidebarcomponent — app logo + 3 steps, active/completed/future states, aria-labels175bfbeHouseholdSetupFormcomponent — name input, disabled-until-valid Continue button, server error displaye85a7ca+page.server.ts— load guard (redirects to/plannerif user already has a household), POST action creates household viaPOST /v1/households, redirects to/household/staples6de7f5a+page.svelte— desktop: 300px progress sidebar (step 1 active) + flex form area; mobile: "Schritt 1 von 3" eyebrow + form. Stub/household/staplespage.Files created
frontend/src/lib/components/ProgressSidebar.svelte+.test.ts(8 tests)frontend/src/lib/onboarding/HouseholdSetupForm.svelte+.test.ts(9 tests)frontend/src/routes/household/setup/+page.server.ts+page.server.test.ts(8 tests)frontend/src/routes/household/setup/+page.svelte+page.test.ts(8 tests)frontend/src/routes/household/staples/+page.svelte(stub)Deferred from scope
Suggested next action
Open a PR:
/review-pron this branch, or implement A3 (pantry staples) next.