Files
YesChef/BACKLOG.md
T
Josh Rogers 6d84aad94b Pre-fill list section on product pick; tighten backend warnings
- Adds GET /api/products/{kind}/{id}/section?storeId=... exposing the
  per-store memory the list page mirrors when a product is picked, so the
  section dropdown reflects what the backend would auto-assign on POST.
- Treats backend warnings as errors via Directory.Build.props; fixes the
  surfaced warnings (obsolete PostgreSqlBuilder ctor, nullable string[]
  in IsEquivalentTo, redundant nullable flow).
- Annotates wire-exposed enums (ProductKind, UnitKind, UnitCategory,
  UnitCategoryFlags) with JsonStringEnumConverter so they round-trip as
  strings regardless of caller options. Unblocks the integration tests
  that deserialize DTOs via GetFromJsonAsync without the global converter.
2026-05-15 21:30:00 -05:00

17 KiB
Raw Blame History

Backlog

Informal list of work items that aren't yet scheduled. Convert to GitHub issues if/when a remote is added.

Speculative or longer-term ideas live in ideas.md.

Foundations

Account & session lifecycle

  • JWT refresh tokens. Today's tokens are signed with HS256 and have no refresh path; they expire and the user gets bounced to login. Add refresh tokens with rotation + revocation list.
  • Account deletion / data export. GDPR-style request handling. Family-scoped: deleting a user shouldn't delete the family's data, but should redact PII.
  • Member removal (tombstone) flow. When a family admin removes a member, keep the CheckedByUserId / RemovedByUserId FKs intact for history but tombstone the user so they can no longer authenticate, and ensure OnTokenValidated rejects tokens for removed users.

Real-time conflict resolution

  • The deferred SignalR-versioning plan in memory addresses connect-window races. It does not address edit conflicts (two family members renaming the same store, unchecking-then-checking the same item simultaneously).
  • Decide a strategy per resource: last-write-wins is fine for IsChecked toggles; for renames and other field edits, optimistic concurrency with a RowVersion (xmin) is safer.
  • SignalR pushes already broadcast updates; conflict surfaces server-side at SaveChanges and needs a retry/merge story per endpoint.

Offline shopping (PWA)

  • Service worker exists but no offline behavior is tracked. Killer use case: standing in the freezer aisle with bad signal and needing to check items off.
  • Strategy:
    • Cache the active list and its product/section data on first load.
    • Queue mutations (toggle checked, soft-remove, add item) to IndexedDB while offline.
    • On reconnect, replay queued mutations against the API; merge conflicts using the strategy above.
    • SignalR reconnect already exists — needs to play nicely with replay (don't double-apply local-then-server events).
  • Visible offline indicator + "X pending sync" badge so users know what's local-only.

Backup & restore strategy

  • Single-tenant today, so a misconfigured volume = one family's loss. With multi-tenancy, you're custodian of all families' data — turns this from "good practice" into "non-negotiable."
  • Decide: Postgres logical backups (pg_dump) on a schedule, written to an external location (S3-compatible / cloud blob). Include retention policy (daily for N days, weekly for M weeks).
  • Restore runbook documenting how to recover, ideally rehearsed on a fresh environment.

UX foundations

Onboarding & empty states

  • A brand-new family logs in to nothing — no stores, no lists, no recipes. Currently the lists page just renders empty.
  • First-run experience: a guided "Add your first store → create your first list" flow, or at minimum well-designed empty states with primary CTAs on each tab.
  • Coordinate with the "block create-list when no stores exist" item — same surface, different angle.

Accessibility pass

  • A family app gets used one-handed in noisy stores. Targets: WCAG AA contrast, large hit targets (44×44 minimum on touch), keyboard nav for desktop, screen-reader labels on icon-only buttons (the 📋 📖 🏪 nav uses emoji + text — confirm the text is the accessible name, not the emoji).
  • One-time audit + fixes; ongoing checklist for new components.

Item reorder within section

  • ShoppingListItem.SortOrder exists on the entity. Verify whether the UI exposes drag-to-reorder; if not, add it. Same drag-and-drop pattern recommended for store sections lands here too.

Product catalog

The product-catalog foundation has shipped. A Product is the canonical thing being bought; ShoppingListItem and RecipeIngredient carry an optional ProductId/FamilyProductId link with the free-form Name retained as a fallback for one-off text entries.

Shipped

  • Entities & schema: Product (global, read-only), FamilyProduct (family-owned), FamilyProductOverride (per-family edit of a global product, composite PK on FamilyId + ProductId), and ProductStoreSection (per-store-per-product section memory). AllowedUnitCategories on each so the unit dropdown can be narrowed per product.
  • API (/api/products): search with transparent override merge (returns isOverridden per row); POST to create a FamilyProduct; PUT /family/{id} and PUT /global/{id} (the latter upserts a FamilyProductOverride); DELETE /family/{id} and DELETE /global/{id}/override to remove.
  • Frontend typeahead: wired into the shopping list add-item form and both recipe new/edit forms. Picking a product attaches the link; editing the input after a pick clears it. No-match Enter creates a pure-text item (ProductId = null).
  • Catalog management page (/products): search the effective catalog, add a family product, edit a global product (writes an override) or family product, "Reset to catalog default" for an overridden global, and delete a family product. Linked from the lists page.
  • Override indicator: "Edited" pill on overridden rows in both the catalog page and the typeahead dropdown.
  • ProductStoreSection write path: when an item is saved/checked with (productId, sectionId), the mapping is remembered for (family, store, product).
  • Auto-assign section from product on add. GET /api/products/{kind}/{id}/section?storeId=... exposes the remembered mapping; onItemProductChange on the list page calls it and pre-fills the section dropdown so the user can see (and override) what the backend would auto-assign. The recipe → list copy path already lands ingredients in remembered sections server-side.
  • Seed data: ~50 hand-curated common-groceries items in Data/Seed/products.json, applied by CatalogSeeder on startup (idempotent on Name).

Remaining

  • "Add '' as a new product" affordance in the typeahead. Today the typeahead only suggests existing catalog rows; an unmatched name becomes a pure-text item. Adding an explicit affordance to promote that name into a FamilyProduct from the same dropdown is still open.
  • Seed expansion to ~23k curated items. Current seed file has ~50 entries. Growth is a data exercise, not a code one — keep products.json the source of truth, keep the seeder idempotent on Name.
  • Catalog ingestion tooling (future). When the curated list starts feeling limiting, build re-runnable importers for public datasets so we don't grow by hand-typing.
    • Candidate sources: USDA FoodData Central (public domain — Branded Foods ~400k with UPCs, Foundation/SR Legacy generics) and OpenFoodFacts (ODbL share-alike + attribution — ~3M global products, barcodes, images; watch the share-alike obligation on derived datasets).
    • Tooling concerns: normalize and dedupe by name+brand+UPC; record source + source-id on each imported Product for re-sync/revert; per-source attribution metadata for license compliance; idempotent scripts kept out of the runtime startup path; a quality/popularity flag so imports don't drown the curated set in the typeahead.
  • Promotion analytics (long-term). Aggregate FamilyProduct creations across families to surface promotion candidates for the global catalog. Requires the multi-tenant model AND privacy guardrails: only emit aggregated, normalized name counts — never raw family data. Manual editorial step to actually promote.

Decisions on record

  • Overrides are visibly marked, not transparent. A small "Edited" pill in any product surface tells the user this row differs from the catalog default.
  • Recipes share the catalog with shopping lists (decided 2026-05-06). RecipeIngredient carries an optional ProductId; free-form text remains for "salt to taste" / "1 onion" cases.
  • Reset granularity is whole-override. A single "Reset to catalog default" button removes the entire FamilyProductOverride row. Per-field reset was considered and rejected as over-engineering for v1.
  • Family-product delete is unconditional. FKs from ShoppingListItem/RecipeIngredient are SET NULL, ProductStoreSection cascades — so deleting a family product is safe; linked rows keep their text and lose the link.

Shopping list items

Structured quantities + unit of measure

Replace free-form Quantity strings with a structured (Quantity, UnitOfMeasure) pair. Both ShoppingListItem and RecipeIngredient use the same model.

Unit catalog

  • Global UnitOfMeasure table — curated, app-wide base catalog.
  • Family-scoped FamilyUnitOfMeasure mirrors the FamilyProduct pattern: families can add their own units (we won't capture every possible unit upfront), visible only to that family.
  • The "effective unit catalog" exposed to a family = global that family's custom units (same merge pattern as products).
  • Fields:
    • Id
    • SingularName — full description, singular ("each", "pound", "box")
    • PluralName — full description, plural ("each", "pounds", "boxes")
    • Abbreviation — short form ("ea", "lb", "bx")
    • Category (enum: Weight | Volume | Count | Packaging)
    • IsBase — canonical-in-category flag, reserved for future conversions
    • SortOrder
    • FamilyUnitOfMeasure adds FamilyId and otherwise mirrors the same shape.
  • Display rules of thumb (pin during UX work):
    • Compact contexts (list rows, ingredient rows): use Abbreviation ("2 lb bananas", "1 ea milk").
    • Expanded contexts (item detail, edit forms): use SingularName / PluralName and pluralize by quantity.
    • Always show the abbreviation in the UoM dropdown alongside the full name (e.g. "Pound (lb)") so users can scan either.
  • Validation: SingularName, PluralName, and Abbreviation are all required on the unit row. Uniqueness — at minimum, Abbreviation must be unique within the effective catalog (global + a given family's customs) so list items render unambiguously.
  • Promotion candidate: family-added units that show up across many families are flagged for editorial review, same pipeline as the product catalog.
  • Suggested seed (revise during design):
    • Count: each, dozen
    • Weight: oz, lb, g, kg
    • Volume: tsp, tbsp, cup, fl oz, pint, quart, gallon, ml, L
    • Packaging: box, bag, case, bottle, can, jar, pack, bunch, head, loaf, carton, roll
  • Pluralization handled by the PluralLabel field — render "1 box" vs "2 boxes" correctly without ad-hoc string logic.

Optionality + defaults

  • Recipes: both Quantity and UnitOfMeasureId are required on RecipeIngredient. A recipe step needs to know how much and in what unit. For free-text approximations like "salt to taste", use the IsApproximate + QuantityNote escape hatch noted further down — that path lets the row satisfy the required fields with sentinel values while still rendering as approximate text.
  • Shopping lists: users don't have to enter a quantity or a unit on ShoppingListItem. "Bananas" without specifying anything is valid input.
  • List-only defaults: if a shopping list item is saved without these fields, persist Quantity = 1 and UnitOfMeasureId = <id of "each">. UI renders implicitly — "Bananas" rather than "1 each Bananas" — but the underlying row always has structured values, which keeps the model uniform with recipes and downstream features (catalog stats, recipe→list copy) work without nullable-handling branches.
  • Implication: "each" must exist in the seed unit catalog and have a stable, well-known id (or lookup-by-code) the backend can rely on as the default.

Product ↔ unit relationship (many-to-many, single per instance)

  • ProductAllowedUnit join: (ProductId, UnitOfMeasureId). Says which units this product can be sold/measured in.
  • Example: "Nails" → allowed units = {box, bag, lb}. The user can buy a box, a bag, or a pound of nails — but a given list item is exactly one of those.
  • Each ShoppingListItem and RecipeIngredient carries Quantity (decimal) and UnitOfMeasureId (FK) — single unit per row. Validation: the chosen UnitOfMeasureId must be in the product's ProductAllowedUnit set (when the row references a product). Free-form rows (no ProductId) accept any unit.
  • When the catalog doesn't yet know a unit is valid for a product, allow the user to add it on the fly — that becomes a family-scoped extension to ProductAllowedUnit (mirrors how family overrides work for other product fields).

API & migration

  • Quantity becomes decimal? on both ShoppingListItem and RecipeIngredient; existing string Quantity on RecipeIngredient migrates via best-effort parse (numeric prefix → quantity, trailing word → unit lookup, residue → notes). Anything that can't be parsed stays in a QuantityNote string.
  • /api/units endpoint to list available units (cached on the client).
  • Product create/update accepts a list of allowed unit ids.

UX

  • Quantity input = numeric field + unit dropdown. Unit dropdown is filtered to the product's allowed units (or all units when no product is selected). Default to the most-used unit for that product based on family history (longer-term polish).
  • Display: render with the correct singular/plural label — "1 box of nails", "3 boxes of nails", "1.5 lbs of bananas".

Out of scope for v1

  • Cross-unit conversions (lb ↔ oz, cup ↔ ml). Don't auto-merge "2 lbs apples" + "1 lb apples" — show as two rows. Pure-volume and pure-weight conversions are doable later via the IsBase flag and a conversion factor; weight↔volume requires per-product density and is out of scope indefinitely.
  • Unit locale preferences (display lb vs kg by user locale). Store what was entered.

Open questions

  • How aggressively do we pre-populate ProductAllowedUnit for the curated catalog? At minimum, common-sense defaults per category (produce → lb/each; dairy → gallon/quart/oz; etc.). Could ship with sensible defaults and let families extend.
  • "A few", "to taste", "some" — these are real recipe quantities that don't fit (decimal, unit). Probably modeled as a special IsApproximate flag with an optional QuantityNote rather than forcing them into the structured shape.

Per-store sections — remaining polish

The base feature is shipped (entity, default seed on store create, list view groups by section, per-store ingredient memory via ProductStoreSection auto-assigning on item add and recipe-to-list copy). Remaining nice-to-haves:

  • Section drag-to-reorder in the store edit UI — section walk order matters, but reordering today only works by editing SortOrder numbers manually.

Recipes

Structured multi-step instructions

Replace the single free-form instructions textarea with an ordered list of discrete steps, each in its own text box.

UX: steps are numbered automatically. Users can add a step, remove a step, and reorder steps (drag or up/down arrows). Each step is a short textarea (23 rows) rather than a single-line input, to accommodate steps with sub-detail.

Data model: a new RecipeStep entity — (Id, RecipeId, SortOrder, Text) — replaces the Recipe.Instructions string. Migration: split existing Instructions on double-newline (or numbered-list pattern) into individual rows; anything that doesn't parse cleanly lands as a single step.

API: Recipe GET returns steps: [{ id, sortOrder, text }] instead of instructions: string. POST/PUT accept the same shape. The old Instructions column can be kept nullable for backward compatibility during the migration window, then dropped.

Backward compatibility: the recipe detail view currently renders instructions as whitespace-pre-wrap. Switch it to a numbered <ol> once steps are in place; the edit page replaces the textarea with the step list.

Open questions:

  • Keep Instructions as a migration fallback column, or do a single-step cutover migration?
  • Should steps support rich text (bold ingredient names, timers) or stay plain text for v1?

Stores

Store types / categorization

  • Add a type (or category) to Store so users can classify each one: grocery (Publix, Kroger), home improvement (Lowes, Home Depot), big box (Sam's, Costco), etc.
  • Backend: new column on Store, migration, exposed in create/update payloads. Likely a fixed enum maintained by the app rather than user-defined, but confirm.
  • Frontend: type selector in the add/edit store form. Consider grouping or filtering the stores list by type once the dataset grows.
  • Open questions:
    • Fixed enum of types vs. user-extensible list? (Recommend fixed to start — easier to reason about, matches how shopping lists are likely scoped per store.)
    • Starter set of types? Suggested: Grocery, Home Improvement, Big Box, Pharmacy, Specialty, Other.
    • Does store type affect anything beyond display (e.g., filtering recipes/ingredients to grocery-only stores)?