Backlog: reflect shipped product catalog state
Replace the speculative product-catalog design notes with a shipped/remaining breakdown, capture the decisions made along the way (visible override pill, whole-override reset, unconditional family-product delete, recipes share the catalog), and tighten the auto-assign-section item to reflect that only the frontend pre-fill on product pick is left. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+25
-53
@@ -46,55 +46,31 @@ Speculative or longer-term ideas live in `ideas.md`.
|
|||||||
|
|
||||||
## Product catalog
|
## Product catalog
|
||||||
|
|
||||||
### Introduce Product entity + family-scoped overrides
|
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.
|
||||||
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
|
#### Shipped
|
||||||
- `Product` (global catalog, read-only): `Id`, `Name`, `DefaultUnit?`, `Brand?`, `Notes?`, search keys.
|
- **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.
|
||||||
- `FamilyProduct` (family-owned): `Id`, `FamilyId`, `Name`, plus the same detail fields. Family-only products live here.
|
- **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.
|
||||||
- `FamilyProductOverride` (family-scoped edit of a global product): `FamilyId`, `ProductId`, overridden fields. The effective view = global + family override merged.
|
- **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`).
|
||||||
- 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.
|
- **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.
|
||||||
- `ShoppingListItem` and `RecipeIngredient` get optional `ProductId` (or `FamilyProductId`); free-form `Name` remains as a fallback for legacy rows / pure-text entries.
|
- **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)`.
|
||||||
|
- **Seed data:** ~50 hand-curated common-groceries items in `Data/Seed/products.json`, applied by `CatalogSeeder` on startup (idempotent on `Name`).
|
||||||
|
|
||||||
#### API
|
#### Remaining
|
||||||
- `/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.
|
- **Auto-assign section from product on add.** The write path remembers `(product, store) → section` and the backend has a helper that reads it back, but the add-item form doesn't call it on product pick yet. Described in detail in *Auto-assign section from product* further down.
|
||||||
- `/api/products` POST — create a `FamilyProduct` (family scope).
|
- **"Add '<text>' 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.
|
||||||
- `/api/products/{id}` PUT — write a `FamilyProductOverride` if the id refers to a global product, or update the `FamilyProduct` directly if family-owned.
|
- **Seed expansion to ~2–3k 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`.
|
||||||
- All scoped by the authed user's `FamilyId`.
|
- **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.
|
||||||
|
|
||||||
#### Seed data
|
#### Decisions on record
|
||||||
- **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.
|
- **Overrides are visibly marked, not transparent.** A small "Edited" pill in any product surface tells the user this row differs from the catalog default.
|
||||||
- Catalog seeding runs as a one-time data migration, not on every startup.
|
- **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.
|
||||||
- **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.
|
- **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.
|
||||||
#### 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 '<text>' 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
|
## Shopping list items
|
||||||
|
|
||||||
@@ -167,15 +143,11 @@ When a user picks a product from the typeahead on the shopping list add form, pr
|
|||||||
|
|
||||||
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.
|
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.
|
**State of play:** `ProductStoreSection` is shipped — table, indexes, and the write path that records `(family, store, product) → section` whenever an item with a product link is saved with a chosen section. A backend helper in `ShoppingListEndpoints` already reads the effective section back. **The missing piece is the frontend:** `onItemProductChange` in `lists/[id]/+page.svelte` doesn't call the read path on product pick yet, so the section dropdown still defaults to "Uncategorized" until the user sets it.
|
||||||
|
|
||||||
**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`.
|
**To finish:** expose the lookup via an endpoint (`GET /api/products/{id}/section?storeId=...` or roll it into the typeahead response when a `storeId` is supplied), call it on `onItemProductChange`, and pre-select the returned section. Same treatment for the recipe → list copy path so adding a recipe to a list lands ingredients in the right sections.
|
||||||
|
|
||||||
**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.
|
**Scope note:** the section pre-fill is family-scoped memory (`ProductStoreSection` rows they've created), not a global default — different families organize differently.
|
||||||
|
|
||||||
**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
|
## Recipes
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user