Commit Graph

36 Commits

Author SHA1 Message Date
Josh Rogers 09003b963d Replace x delete buttons with trash can icon
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:08:28 -05:00
Josh Rogers bd540e506f Backlog: structured multi-step recipe instructions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:03:59 -05:00
Josh Rogers 0d20e446e0 Backlog: document auto-assign section from product feature
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:57:37 -05:00
Josh Rogers 7b7e871827 Give shopping list item name field full-width row
The name field was crammed into a single flex row with qty/unit inputs,
section dropdown, and Add button, leaving it too narrow to use. Moved
it above the controls row so it spans full width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:56:19 -05:00
Josh Rogers b31ff77548 Fix unit/product kind enums serializing as integers
Two bugs found during exploratory testing:

1. UnitKind and ProductKind enums were serialized as 0/1 instead of
   "Global"/"Family" because no global JSON converter was registered.
   Added JsonStringEnumConverter to ConfigureHttpJsonOptions so all
   enum responses (kind, category) serialize as strings. This also
   fixes UnitCategory coming back as a number.

2. units.svelte.ts triggered a Svelte 5 state_unsafe_mutation error
   because the `all` getter called load() (which mutates $state) from
   inside a $derived expression in QuantityInput. Wrapped the load()
   call in untrack() so the side-effect runs outside the reactive
   tracking context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:37:16 -05:00
Josh Rogers 68292c2906 Harden recipe edit page and cover allowedUnitCategories projection
Edit page now surfaces load and save errors instead of failing silently,
round-trips sourceUrl through PUT, warns before discarding unsaved
changes, and offers an explicit Cancel button. Delete moved off the
detail page's primary action row into a less-prominent footer link so a
mis-tap on Edit can no longer destroy the recipe. Added integration
tests covering AllowedUnitCategories in the recipe GET projection for
all four product-link shapes (global no-override, global with override,
family product, unlinked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:01:06 -05:00
Josh Rogers 4e4d80410c Add recipe edit page
Recipe GET now returns effective AllowedUnitCategories per ingredient so
QuantityInput can filter units when editing. ProductTypeahead accepts an
optional initialSelectedName so it can detect when a pre-linked ingredient
name has been edited and clear the stale product link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:44:52 -05:00
Josh Rogers ee98fc8134 Auto-learn product allowed-unit categories from recipe / list writes
When a recipe ingredient or list item is saved with both a product link and
a unit link, the unit's category is OR'd into the product's AllowedUnitCategories
so the dropdown filter populates itself from real usage instead of needing
explicit configuration. Family products are mutated directly; global products
gain (or extend) a per-family FamilyProductOverride so the global catalog
stays read-only.

The learner only widens flag sets — it never narrows or clears them — so
repeated writes are idempotent. No-ops when the global product already
covers the category (no spurious override rows).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:32:23 -05:00
Josh Rogers fd6b0accc8 Filter unit dropdown by product allowed-unit categories
Adds a UnitCategoryFlags column to Product, FamilyProduct, and
FamilyProductOverride so each product can advertise which unit categories
it is typically packaged by (e.g. flour: Weight | Volume). The product
endpoints round-trip the flag, search projects the effective value with
the override applied, and the frontend QuantityInput soft-filters its
dropdown by the selected product's flag, with a "show all units" escape
hatch for ad-hoc overrides.

No backend rejection on a unit outside the allowed set — the flag is
purely a hint. Default value is None (no filter), so existing data is
unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:17:43 -05:00
Josh Rogers fb1bc2b7e1 Add structured quantities + units to shopping list items
Mirrors the Phase 2 work on RecipeIngredient: ShoppingListItem grows
Quantity (decimal), UnitOfMeasureId / FamilyUnitOfMeasureId, IsApproximate,
and QuantityNote. The recipe-to-list copy now carries structured fields
verbatim instead of folding them into the free-form Name, and the
unit-in-use guard now also blocks deleting a family unit that's referenced
by a shopping list item.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:18:26 -05:00
Josh Rogers c7f9c31952 Add structured quantities + units to recipe ingredients
Phase 2 of structured quantities + UoM. Replaces the free-form Quantity
string on RecipeIngredient with a structured (Quantity decimal, UnitOfMeasureId
or FamilyUnitOfMeasureId) pair, plus an IsApproximate + QuantityNote
escape hatch for "to taste" style entries. The unit FK pair mirrors the
existing Product / FamilyProduct pattern, with the same at-most-one and
tenant-scoping validation. Existing string Quantity values are dropped
per the agreed wipe-to-null migration plan.

Frontend ships a QuantityInput component (numeric field + unit dropdown
fed by a runes-cached effective catalog from /api/units) and a shared
formatter for read-only display. Recipe -> shopping list copy folds the
structured quantity into the item Name for now; Phase 3 will move the
fields onto ShoppingListItem directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:36:25 -05:00
Josh Rogers 559d80c104 Add unit-of-measure catalog foundation
Phase 1 of structured quantities + UoM. Introduces a global UnitOfMeasure
catalog (Code-keyed for stable backend lookup of canonical units like
"each") and FamilyUnitOfMeasure for family-scoped customs, mirroring the
product-catalog pattern. Endpoints expose the merged effective catalog
plus CRUD for family customs. Abbreviation uniqueness is enforced per
table at the DB layer and across tables at the API layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:17:30 -05:00
Josh Rogers 6c8f0167e5 Add product catalog with per-store section memory
Introduces a global Products catalog plus per-family overrides and
private FamilyProducts, exposed via /api/products with a merged
search. Shopping list items and recipe ingredients gain optional
ProductId/FamilyProductId links, and a new ProductStoreSection
table remembers which section a product was last placed in at a
given store so future adds auto-assign the right section.

Frontend gets a reusable ProductTypeahead component, wired into
list-item add and recipe ingredient entry with free-form fallback.

A startup CatalogSeeder loads ~115 curated staples from an embedded
JSON resource via INSERT ... ON CONFLICT DO NOTHING; skipped under
the Testing environment so integration tests keep a clean slate.
2026-05-09 21:29:51 -05:00
Josh Rogers 5c6abc1e43 Allow store deletion when only archived lists reference it
Archived lists are no longer part of anyone''s workflow, so they
shouldn''t block a store from being deleted. The handler now blocks
only on non-archived lists and purges archived ones (items cascade)
in the same transaction.

Also grooms BACKLOG.md to remove items already shipped (Family/multi-
tenant epic, password reset, email invites, auth rate limiting,
picked-up-vs-removed, per-store sections base feature, add-recipe-to-
list, store delete confirm + duplicate-name 409).
2026-05-08 23:38:18 -05:00
Josh Rogers 4adfc9d0bf Fix dev-db.ps1 status to exit cleanly when container is absent
docker inspect exits non-zero when the container is missing; under
$ErrorActionPreference='Stop' that propagated and made `dev-db.ps1
status` exit 1 even though the script printed the right output. Swallow
the inner exit code locally so callers see exit 0.
2026-05-08 23:13:57 -05:00
Josh Rogers c689644997 Document scripts/dev-db.ps1 + dev-up.ps1 in CLAUDE.md
So future agents (and humans) discover the helpers instead of stitching
together the raw docker / dotnet / npm commands. Also notes that
LoggingEmailSender prints invite and reset links to the API log when
SMTP is unconfigured locally.
2026-05-08 23:11:17 -05:00
Josh Rogers aa530cba97 Add scripts/dev-up.ps1 to launch the whole local dev stack
Single command brings up Postgres (via dev-db.ps1), the backend API,
and the Vite frontend. API and frontend each get their own PowerShell
window so logs do not interleave and Ctrl+C only kills the intended
process. Email links print to the API window via LoggingEmailSender.
2026-05-08 23:10:27 -05:00
Josh Rogers 4fdceb2120 Add scripts/dev-db.ps1 helper for the local Postgres container
Wraps docker for the start / stop / reset / status / logs lifecycle of
a postgres:17 container named "yeschef-pg" on localhost:5432, matching
the connection string baked into appsettings.json. Uses a named volume
so data survives a stop; reset wipes both container and volume for a
truly fresh DB.
2026-05-08 23:07:49 -05:00
Josh Rogers 09bec105f6 Collapse migrations, require email at registration
No deployed environments yet, so consolidate the entire migration history
into a single Initial migration and tighten the schema accordingly.

- User.Email is now non-nullable (required); the partial unique index
  used to tolerate legacy null emails is gone in favor of a plain
  unique index.
- Both register paths require an email up front. Invite-path
  registrations must match the invited address (server ignores any
  mismatched client value); family-code registrations bind whatever the
  user supplies but stay unconfirmed (EmailConfirmedAt = null) since
  the family code does not prove email ownership.
- Validation order in /register reworked: invite/family-code resolution
  runs before duplicate-name/email checks so a consumed token surfaces
  a clean "invitation invalid" error instead of getting masked by the
  duplicate-email response.
- All 14 prior migrations replaced with a single Initial migration.
- Test fixtures, builders, and unit tests updated to supply emails.
- Login page register form now collects an email field; invite-bound
  registrations show the invited address as a read-only input.

Local dev DBs need to be recreated (drop the yeschef-pgdata volume or
the yeschef Postgres database). No production data exists yet.
2026-05-08 22:58:27 -05:00
Josh Rogers af085cfb90 Add password reset flow
Users with a confirmed email can now reset a forgotten password via
emailed single-use links.

Backend
- New PasswordResetToken entity (hash-only, 15-minute TTL).
- POST /api/auth/forgot-password always returns 200, never disclosing
  whether an email is registered. Internally only emits a reset email
  when a user exists with EmailConfirmedAt set, and burns any existing
  outstanding tokens for that user before issuing a new one.
- POST /api/auth/reset-password validates the token, rotates the
  password hash, and consumes the token. Single-use, expiry-checked.
- Both endpoints are rate-limited (forgot 5/hr, reset 10/15min) per the
  same partitioning the login/register endpoints already use.
- Reset email template added; uses AppBaseUrl plumbed in the previous PR.

Frontend
- /forgot-password page (email field, generic confirmation message
  regardless of whether the email is registered).
- /reset-password page reads ?token=, validates the new password
  client-side, posts to the API, then redirects to /login.
- "Forgot your password?" link added under the login form.

Tests
- 9 new integration tests cover the happy path, single-use enforcement,
  expired/unknown tokens, short-password rejection, silent 200 for
  unknown email, no email for unconfirmed users, and outstanding-token
  invalidation when a fresh request is made.
2026-05-08 22:47:33 -05:00
Josh Rogers d9ffe18b21 Add email-based invites and email confirmation in one flow
Family admins can now invite new members by email. The recipient gets a
templated email with a single-use, time-limited join link; clicking it
opens the registration form bound to the invite, and submitting the
form simultaneously consumes the invite and marks the email confirmed.
Self-registration via the shareable family code remains available.

Backend
- New Invite entity (token hash only — raw token never stored), with
  per-family uniqueness on the active hash.
- User gains nullable Email and EmailConfirmedAt; partial unique index
  so legacy rows with no email do not collide.
- /api/family/invites — admin endpoints to list pending, issue, resend
  (rotates the token), and revoke.
- /api/auth/invite/{token} — public lookup returning email + family name
  so the registration form can show "you have been invited to X".
- /api/auth/register accepts InviteToken; the invite vouches for the
  email, so any client-supplied email field is ignored. Falls back to
  FamilyCode when no invite token is present.
- AppUrlOptions / AppBaseUrl plumbed through so emails build absolute
  links to the deployed frontend.

Frontend
- /login reads ?invite=<token>, looks it up, and switches the form into
  invite-registration mode (pre-binding to the invited email + family).
- /family admin section gains an invite-by-email form, a pending list,
  and resend/revoke actions with a confirmation modal.

Tests
- 14 new integration tests covering: admin issue, member-forbidden,
  lookup, valid/expired/consumed/unknown-token registration, resend
  rotation, revocation, pending-only list filter, and conflict for
  inviting an existing member. RecordingEmailSender captures dispatched
  messages so tests can assert on the link without standing up SMTP.
2026-05-08 22:42:55 -05:00
Josh Rogers a1635218a8 Add SMTP infrastructure + auth rate limiting
Foundation for the upcoming email-based invite and password-reset flows.

- IEmailSender abstraction with SmtpEmailSender (MailKit 4.16) and a
  LoggingEmailSender fallback used automatically when SMTP is unconfigured
  so local dev works without a real SMTP server.
- Fixed-window rate limits keyed by remote IP: 10 / 15 min on /login,
  5 / hour on /register. Returns 429 with Retry-After. Bypassed in the
  Testing environment so the existing integration suite is unaffected.
- New env vars (SMTP_*, APP_BASE_URL) plumbed through docker-compose
  and documented in .env.example.
2026-05-08 22:35:57 -05:00
Josh Rogers 86603b4f4a Backlog: email-based admin invites + email confirmation in one flow
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:06:03 -05:00
Josh Rogers fa1d4b4f62 Add per-store sections to group list items by walk order
Each store gets a StoreSection catalog (Produce, Dairy, etc.). Default
sections are seeded on store creation; admins can rename, reorder, add,
or delete. ShoppingListItem.SectionId is a nullable FK that sets to
null when the section is deleted, so items survive section churn.

The list detail view groups items by section in walk order, with
Uncategorized appended last. Section dropdowns on each row (and the
add-item form) let users assign or reassign on the fly. SignalR
broadcasts include sectionId on adds and a new ItemSectionChanged
event for live re-grouping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:05:57 -05:00
Josh Rogers de5c18f3e6 Add per-family invite codes and admin roles
First member into a family becomes Admin; subsequent members default to
Member. JWTs carry a family_role claim that is refreshed from the DB on
each request so promotions and demotions take effect immediately.

New /api/family endpoints let admins view the roster, regenerate the
invite code, change roles, and remove members. Last-admin and
self-removal guards prevent locking the family out of management.

The /family page exposes the same actions in the UI; the bottom nav now
links there. Members see the roster but not the invite code.

Existing deployments get a one-time backfill at startup: any family with
members but no admin gets its earliest-joined member promoted.
2026-05-08 21:29:15 -05:00
Josh Rogers d4db819e72 Polish Store endpoints: 409 conflicts and confirm-before-delete
Backend pre-checks duplicate names on POST/PUT and returns 409
Conflict (replacing the previous 500 from the unique constraint).
DELETE-with-active-lists also returns 409 instead of 400 for
semantic accuracy.

Frontend addStore and saveEdit now surface API errors via toast
instead of failing silently. Delete is now gated by a confirmation
modal so accidental clicks no longer destroy data.
2026-05-08 21:07:57 -05:00
Josh Rogers 7fcae09afb Distinguish picked-up from removed shopping list items
Soft-remove items via RemovedAt/RemovedByUserId instead of hard
deleting so the row survives for undo and future history reporting.
DELETE now sets the removal fields; a new POST .../restore clears
them. Active list reads (summary, detail, check toggle) filter to
RemovedAt IS NULL. Frontend surfaces an Undo toast on remove and
handles a new ItemRestored SignalR event.
2026-05-08 20:07:41 -05:00
Josh Rogers 9b2db931ee Scope all data access by FamilyId for multi-tenant isolation
Adds FamilyMembership join (UserId, FamilyId, Role) and a non-null
FamilyId FK on Store, ShoppingList, ShoppingListItem, Recipe, and
RecipeIngredient. FamilyId is denormalized on items/ingredients so the
tenant filter is a single column predicate without joins. Store name
uniqueness is now scoped per family.

JWT issuance stamps a family_id claim; ClaimsPrincipalExtensions exposes
GetFamilyId(). Register validates the supplied invite code against
Family.InviteCode (replacing the env-var equality check) and writes a
FamilyMembership row. OnTokenValidated rejects requests whose user has
been removed from the claimed family since login.

Every endpoint filters by FamilyId on read and stamps it on write.
Cross-family storeId references on list create/update return 400. The
SignalR hub verifies list ownership on JoinList and uses a per-family
overview group, so cross-tenant fan-out is structurally impossible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:05:23 -05:00
Josh Rogers 7c1cfd62e6 Introduce Family entity and bootstrap default family on startup
Foundation for the multi-tenant migration: adds the Family table with a
unique InviteCode, and a startup hook that bootstraps a single default
family from the FamilyCode config when the table is empty. No behavior
change yet — the table exists and is seeded but nothing reads it.

Also fixes the backend test command in CLAUDE.md: dotnet test on the
.NET 10 SDK with MTP rejects the --solution switch and positional
project args, so we now use Push-Location + --project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 23:00:00 -05:00
Josh Rogers 6f71f8c2d6 Add Playwright MCP server config and ignore its scratch dir
Checks in `.mcp.json` so the Playwright MCP server is available to
all contributors out of the box, and ignores `.playwright-mcp/` where
the server stashes per-session snapshots and console logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:09:20 -05:00
Josh Rogers 88c24b03ca Add reusable toast notification system
Introduces a runes-based toast module (`$lib/toast.svelte.ts`) with
success/info/warning/error variants, auto-dismiss, optional action
button, plus a `Toaster` viewport mounted in the root layout.
Migrates the lone `alert(e.message)` call site (stores delete) to
`toast.error()`. Backlog updated to remove the now-completed
foundational item and rewrite dependent items to reference the
shipped API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:08:52 -05:00
Josh Rogers 6780fb366e Add BACKLOG.md and ideas.md from product exploration session
Captures the design direction from a multi-feature exploratory session:
- Multi-tenant Family epic (decided)
- Product + unit catalogs with family-scoped overrides and free-form additions
- Per-store sections / departments with grouped list UI
- Structured quantities + UoM (required on recipes, optional on lists with sensible defaults)
- Pick-up vs remove distinction for shopping list items
- Add-recipe-to-list flow
- Stores polish: types, confirm-before-delete, duplicate-name bug, toast component
- Foundational items (auth lifecycle, conflict resolution, offline PWA, backup/restore, onboarding, accessibility)

ideas.md holds longer-term explorations (recipe URL import, product images,
item notes, meal planning, observability).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:32:56 -05:00
Josh Rogers cde619e730 Add Vitest unit tests and Playwright e2e suite for frontend
Replaces the ad-hoc test-e2e.mjs scripts with a proper test stack:
Vitest covers $lib (api, auth, signalr) with mocked fetch/SignalR/navigation,
and @playwright/test covers auth, stores, lists, recipes, and SignalR realtime
sync between two browser contexts. Tests use uniqueName() for every entity
since the backend has no per-test reset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:17:55 -05:00
Josh Rogers 76e8de9484 Add TUnit-based unit and integration tests for backend
Set up YesChef.Api.UnitTests and YesChef.Api.IntegrationTests projects
running on TUnit + Microsoft.Testing.Platform. Integration tests use a
single Postgres 17 Testcontainer per session and clone a migrated
template database per test (`CREATE DATABASE … TEMPLATE …`) so tests
remain fully isolated and run in parallel without replaying migrations
each time.

Test-author DX is built around fluent entity builders, a TestDataFactory
for common scenarios, and a two-level base hierarchy
(IntegrationTest / AuthenticatedIntegrationTest) whose `[Before(Test)]`
hooks stand up the per-test database, app factory, default user, and
authenticated HttpClient — leaving each test body focused on the action
under test.

Adds src/backend/global.json to opt `dotnet test` into MTP mode on the
.NET 10 SDK, and updates CLAUDE.md with how to run the tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 20:56:29 -05:00
Josh Rogers 7ca2dc46d9 Add CLAUDE.md with repo orientation for Claude Code
Captures stack shape, common commands (with Windows-friendly --prefix/--project
forms), backend feature-folder layout, JWT + SignalR auth flow, and required
.env keys.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 19:40:12 -05:00
Josh Rogers 48d30df07b Initial commit: YesChef family shopping list and recipe app
Backend (.NET 10 minimal API):
- Vertical slice architecture with feature folders
- Postgres via EF Core with initial migration
- JWT auth with family invite code registration
- REST endpoints for stores, shopping lists, items, recipes
- SignalR hub for real-time list collaboration (per-list groups
  and lists-overview group for live list creation/archival/progress)
- Multi-stage Dockerfile

Frontend (SvelteKit + Svelte 5 runes, Tailwind v4):
- Mobile-first PWA with web manifest and service worker
- Bottom-nav layout, login/register, lists overview, list detail,
  stores management, recipes (list/create/detail with add-to-list)
- SignalR client with reference-counted connection
- Real-time updates on both lists overview and list detail pages

Infrastructure:
- docker-compose.yml with postgres, backend, frontend services
  and Traefik labels for path-based routing (/api, /hubs to backend)
- .env.example with required config

End-to-end tests (Playwright):
- test-e2e.mjs: single-user flow (auth, stores, lists, items, recipes)
- test-e2e-multiuser.mjs: two-user real-time sync coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 19:32:39 -05:00