# 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 ### Introduce Product entity + family-scoped overrides A `Product` becomes the canonical thing being bought. Shopping list items and recipe ingredients reference a product instead of (or in addition to) carrying a free-form name. Goals: 1. Ship an extensive pre-populated grocery catalog so users don't enter "Bananas" from scratch. 2. Allow free-form product creation when something isn't in the catalog. 3. Allow editing of pre-populated product details (e.g. assigning a default section per store). 4. **Custom products and edits are scoped to the family that made them** — never visible to other families. 5. Long-term: track family-level edits/additions to surface promotion candidates for the global catalog. #### Data model sketch - `Product` (global catalog, read-only): `Id`, `Name`, `DefaultUnit?`, `Brand?`, `Notes?`, search keys. - `FamilyProduct` (family-owned): `Id`, `FamilyId`, `Name`, plus the same detail fields. Family-only products live here. - `FamilyProductOverride` (family-scoped edit of a global product): `FamilyId`, `ProductId`, overridden fields. The effective view = global + family override merged. - Per-store-per-product section assignment: `ProductStoreSection` (`FamilyId`, `ProductId`-or-`FamilyProductId`, `StoreId`, `StoreSectionId`). This is what auto-assigns "Bananas → Produce" on next add at that store, answering the open question in the sections feature. - `ShoppingListItem` and `RecipeIngredient` get optional `ProductId` (or `FamilyProductId`); free-form `Name` remains as a fallback for legacy rows / pure-text entries. #### API - `/api/products?q=` — search effective catalog (global ∪ family additions, with overrides applied). Returns merged results without exposing whether a hit came from the global catalog vs. family. - `/api/products` POST — create a `FamilyProduct` (family scope). - `/api/products/{id}` PUT — write a `FamilyProductOverride` if the id refers to a global product, or update the `FamilyProduct` directly if family-owned. - All scoped by the authed user's `FamilyId`. #### Seed data - **Decision (2026-05-06):** start with a hand-curated common-groceries list (~2–3k items). High-signal, fast to ship, no licensing entanglement, gives the typeahead something useful on day one. - Catalog seeding runs as a one-time data migration, not on every startup. - **Decision (2026-05-06):** recipes share the same product catalog. `RecipeIngredient` gets an optional `ProductId` — typeahead during entry, with **free-form text entry always available as a fallback** (for "salt to taste", "1 onion", or anything not in the catalog). This unblocks the "add recipe to grocery list" flow described below. #### Future: catalog ingestion tooling - Build tooling to ingest from public-domain / open-source catalog sources so the curated list can grow without manual data entry. In scope when the curated list starts to feel limiting. - Candidate sources to support: - **USDA FoodData Central** (public domain) — Branded Foods (~400k with UPCs) and Foundation/SR Legacy (generic items). Bulk CSV/JSON downloads available. - **OpenFoodFacts** (ODbL — share-alike, attribution) — ~3M global products, barcodes, images. Watch out for the share-alike obligation on derived datasets. - Any future open dataset that surfaces. - Tooling concerns: - Normalization pipeline: dedupe by name + brand + UPC, map to our `Product` schema, drop nutrition fields we don't use (or keep behind a flag). - Provenance: record source + source-id on each imported `Product` so we can re-sync or revert. - License compliance: keep per-source attribution metadata; surface in an /about or /credits page if any source requires it. - Re-runnable: idempotent import scripts (no duplicates on re-run); separate from runtime startup. - Curation workflow: imported items shouldn't drown the curated set in the typeahead — likely a quality/popularity flag controls default surfacing. #### Promotion analytics (long-term) - Aggregate `FamilyProduct` creations across families: count distinct families using a given normalized name, with a threshold (e.g. ≥N families with ≥M lists each) before flagging for editorial review. - This requires the multi-tenant model above, AND privacy considerations: only emit aggregated, normalized name counts — never raw family data. - Manual editorial step to actually promote → keeps the global catalog quality high. #### Open questions - Free-form entry on a list — does the user pick from a typeahead (preferred) or can they bypass the catalog entirely with a one-off text item? Recommend typeahead with "Add '' as a new product" affordance. - When a global product is overridden, is the override applied automatically (transparent) or shown as "Edited by your family" in the UI? - Should recipes use the same product catalog, or stay free-form? Big consistency win if they share, but recipe ingredients tend to be more abstract ("flour") than shopping items ("King Arthur AP Flour 5lb"). - How does this interact with the existing `Store.Name` uniqueness — moot if we add `FamilyId` to everything. ## 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 = `. 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). Remaining nice-to-haves: - **Per-store ingredient memory:** remember "last time `Bananas` was bought at Kroger it was in Produce" and auto-assign on next add at that store. Adds an `IngredientSection` mapping table per store. Pairs naturally with the product catalog. - **Recipes → sections:** when pulling recipe ingredients into a list, map them to the list's store's sections (only meaningful once the per-store ingredient memory or product catalog lands). - **Section drag-to-reorder** in the store edit UI — section walk order matters, but reordering today only works by editing `SortOrder` numbers manually. ### Auto-assign section from product When a user picks a product from the typeahead on the shopping list add form, pre-populate the section dropdown with that product's known section for the current store — rather than leaving it as "Uncategorized". This is the "per-store ingredient memory" item above, stated from the user's perspective: choosing "Spaghetti" should already know it belongs in Pasta/Dry Goods at this store. **Data model:** `ProductStoreSection` is already sketched in the product catalog section above — `(FamilyId, ProductId | FamilyProductId, StoreId, StoreSectionId)`. That table is the source of truth. **Write path:** when a user manually changes the section on a list item whose product is linked, offer to remember that choice (or auto-remember silently after N uses). Write to `ProductStoreSection`. **Read path:** on product selection in the add-item form, query the effective section for `(productId, storeId)` and pre-select it in the section dropdown. User can still override. **Dependencies:** requires the product catalog entity to be in place (`ProductId` on `ShoppingListItem`, product typeahead already wired). The `ProductStoreSection` table and its endpoints are net-new work. **Scope note:** the section pre-fill should be scoped to the family's own memory (`ProductStoreSection` rows they've created), not a global default — different families organize differently. ## 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 (2–3 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 `
    ` 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)?