Phases 6-7

This commit is contained in:
Aerilyn Weber 2026-05-14 14:47:23 +09:00
parent 76a516a417
commit 029940b079
111 changed files with 17247 additions and 447 deletions

View file

@ -181,6 +181,8 @@ interface PaginatedResponse<T> {
- `ImportProductsSchema` — array of `CreateProductSchema` (for JSON import)
- All schemas imported from `'zod/v4'`; enums use `z.enum(Object.values(...))`.
**Status**: DONE (types, enums, validation schemas all exist with tests)
### 5.2 — Mongoose Schema & Repository
- `packages/api/src/modules/products/schemas/product.schema.ts` — Mongoose schema with `timestamps: true`, `deletedAt` index, partial unique index on `(householdId, barcode)`.
@ -195,7 +197,35 @@ interface PaginatedResponse<T> {
- `bulkCreate(householdId, items[])` — uses `insertMany` with `ordered: false`
- All read queries use `.lean().exec()`.
### 5.3 — Barcode Lookup Service
**Status**: DONE (repository exists with tests)
### 5.3 — Products Service
- `ProductsService` with business logic:
- Dedup check on create (same name + brand within household)
- Barcode collision check (409 ConflictError)
- Soft-delete with `deletedAt` timestamp
- Coordinate barcode lookup (call BarcodeService)
- Coordinate import (validate + bulkCreate)
- Smart-add (call LLM provider)
**Status**: NOT STARTED
### 5.4 — Products Routes
- Route plugin registered via `fp()`:
- `GET /products` — list/search (paginated)
- `GET /products/:id` — get single
- `POST /products` — create
- `PATCH /products/:id` — update
- `DELETE /products/:id` — soft-delete (admin)
- `GET /products/barcode/:code` — barcode lookup
- `POST /products/import` — bulk import
- `POST /products/smart-add` — LLM placeholder
**Status**: NOT STARTED
### 5.5 — Barcode Lookup Service
- `BarcodeService`:
- First check local DB for matching barcode (per household)
@ -208,7 +238,9 @@ interface PaginatedResponse<T> {
- Failures (network, 404, malformed): return `{ found: false }`; do not throw
- Outbound HTTP via `undici` with a 5s timeout and a configurable User-Agent (`MeshiTrack/<version> (+self-hosted)`)
### 5.4 — LLM Provider Interface
**Status**: NOT STARTED
### 5.6 — LLM Provider Interface
- `packages/api/src/modules/llm/interfaces/llm-provider.interface.ts`:
@ -231,14 +263,18 @@ export const LLM_PROVIDER = Symbol('LLM_PROVIDER');
- `NoOpLlmProvider`: implements interface, returns `null` for all methods, logs a warning
- `LlmModule`: provides `LLM_PROVIDER` via factory, selectable by env var `LLM_PROVIDER_TYPE`
### 5.5 — Smart Add Endpoint
**Status**: DONE (interface + NoOp provider exist with tests)
### 5.7 — Smart Add Endpoint
- `POST /products/smart-add` accepts `{ text?: string, image?: file }`
- Calls `ILlmProvider.extractNutrition()`
- If LLM returns data, pre-fill a product and return to client for review (not auto-saved)
- If LLM unavailable (`NoOpLlmProvider`), return `{ available: false, message: 'LLM not configured' }`
### 5.6 — Import Endpoint
**Status**: NOT STARTED (blocked on 5.3/5.4)
### 5.8 — Import Endpoint
- `POST /products/import` accepts multipart CSV or JSON file (max 5 MB, 5000 rows)
- Validate each row against `CreateProductSchema`; reject rows with imperial `servingUnit` values with a clear error message
@ -248,7 +284,16 @@ export const LLM_PROVIDER = Symbol('LLM_PROVIDER');
- `tags` is a `;`-separated list
- `servingUnit``{g, ml, piece, slice}`
### 5.7 — Web UI: Product Library
**Status**: NOT STARTED (blocked on 5.3/5.4)
### 5.9 — Web: API Client Service
- `packages/web/src/services/products.ts` — fetch functions for all product endpoints
- Unit tests in `packages/web/src/services/__tests__/products.test.ts`
**Status**: NOT STARTED
### 5.10 — Web UI: Product Library
- `/products` page (Server Component for initial fetch; client island for filters):
- Search bar with debounced full-text search (300ms)
@ -263,6 +308,15 @@ export const LLM_PROVIDER = Symbol('LLM_PROVIDER');
- Barcode field with "Lookup" button (calls `/products/barcode/:code`)
- "Smart Add" tab (text input or image upload)
- Import dialog: file upload with preview, row count, and error display
- Component tests for page and interactive components
**Status**: NOT STARTED
### 5.11 — CI Verification
- `npm run build` passes
- `npm run test:cov` passes (100% lines/functions/statements, 90% branches)
- `npm run lint` passes
---
@ -276,10 +330,12 @@ export const LLM_PROVIDER = Symbol('LLM_PROVIDER');
- [ ] Barcode collisions within a household return 409
- [ ] Bulk import processes a CSV with 100+ products and reports per-row errors
- [ ] Web UI allows searching, filtering, adding, and editing products
- [ ] Web UI has component tests for all pages and interactive components
- [ ] `ILlmProvider` interface is defined and injectable
- [ ] Smart Add endpoint returns graceful `{ available: false }` with the NoOp provider
- [ ] All product queries are scoped to `householdId`
- [ ] Unit + integration tests meet coverage targets (100% lines/functions/statements, 90% branches)
- [ ] `npm run build`, `npm run test:cov`, and `npm run lint` all pass
---

View file

@ -6,264 +6,99 @@
---
## Deliverables
## Implementation Workflow (Vertical Slice)
1. `MealPlan` and `NutritionTarget` MongoDB schemas
2. Meal plan CRUD API with daily/weekly views
3. Recipe suggestion engine (algorithmic, not LLM-dependent)
4. Nutrition targets per user with daily tracking
5. Shopping gap analysis (what's needed beyond pantry)
6. Meal plan web UI with weekly calendar and suggestion panel
Following the project's standard workflow, Phase 8 will be implemented in the following order:
### 8.1 — Shared Layer: Types & Validation
- **Types**: Define `MealPlan` and `NutritionTarget` in `packages/shared/src/types/`.
- **Enums**: Define `MealType` and `MealPlanStatus` in `packages/shared/src/enums/`.
- **Validation**: Create Zod schemas in `packages/shared/src/validation/`:
- `CreateMealPlanSchema`: Ensures 7 days, valid `weekStartDate`.
- `UpdateMealPlanSchema`: Supports granular updates (add/move/remove meal).
- `NutritionTargetSchema`: Validates daily macros and calorie goals.
- **Unit Tests**: Test Zod schemas for all edge cases (e.g., negative calories, overlapping weeks).
### 8.2 — Database Layer: Repositories
- **MealPlanRepository**:
- `findByHousehold(householdId, weekStartDate)`: Unique constraint per household per week.
- `findCurrent(householdId)`: Returns the plan for the current calendar week.
- `updateMeal(id, dayIndex, mealId, data)`: Granular update for a specific meal slot.
- **NutritionTargetRepository**:
- `findByUser(userId, householdId)`: Get active targets for a specific user.
- `upsert(userId, householdId, data)`: Update or create targets.
- **Integration Tests**: Verify MongoDB unique indexes and household scoping.
### 8.3 — Service Layer: Suggestion Engine & Planning
- **MealPlanService**:
- Manage CRUD operations and status transitions (Draft -> Active).
- Compute `dailyNutritionTotal` dynamically using the `NutritionCalculatorService` from Phase 6.
- **NutritionTargetService**:
- Manage user-specific nutritional goals and presets (Maintenance, Loss, Gain).
- **SuggestionEngineService** (Core Algorithm):
- **Scoring Algorithm**:
- `IngredientCoverage` (40%): Matches recipe ingredients vs current pantry items.
- `FreshnessUrgency` (30%): Bonus for using items with `urgency: urgent` or `expiringSoon`.
- `NutritionBalance` (20%): Complements existing meals in the day vs user targets.
- `Variety` (10%): Penalizes recipes used in the last 14 days.
- Uses `RecipesService` to fetch catalog and `PantryService` to fetch current inventory.
- **ShoppingGapService**:
- Analyze meal plans vs pantry to identify missing ingredients for Phase 9 integration.
- **Unit Tests**: Test scoring weights and ingredient matching logic (100% coverage required).
### 8.4 — Route Layer: API Endpoints
- **MealPlanRoutes**:
- `GET /api/v1/households/:householdId/meal-plans`: List plans.
- `GET /api/v1/households/:householdId/meal-plans/current`: Get current week.
- `POST /api/v1/households/:householdId/meal-plans`: Create plan.
- `PATCH /api/v1/households/:householdId/meal-plans/:id`: Update structure.
- `GET /api/v1/households/:householdId/meal-plans/suggestions`: Get algorithmic suggestions.
- **NutritionTargetRoutes**:
- `GET /api/v1/households/:householdId/nutrition-targets`: Get user targets.
- `POST /api/v1/households/:householdId/nutrition-targets`: Set targets.
- **Integration Tests**: Verify all endpoints return correctly shaped responses (using `toProductResponse` style mappers).
### 8.5 — Web Layer: API Client & Components
- **API Client**: Implement `meal-plans.ts` and `nutrition-targets.ts` in `packages/web/src/services/`.
- **Hooks**: Create `useMealPlan` and `useSuggestions` hooks for state management.
- **Components**:
- `CalendarGrid`: Weekly view using CSS Grid and drag-and-drop.
- `NutritionProgress`: Daily progress bars vs targets.
- `SuggestionPanel`: Sidebar showing ranked recipes with "reasoning" tooltips.
- `ShoppingGapView`: Breakdown of missing items.
- **Component Tests**: Use React Testing Library to verify drag-and-drop and progress bar calculations.
### 8.6 — Verification Gate
- `npm run build`
- `npm run test:cov` (100% API/Shared, 90% Web thresholds)
- `npm run lint`
---
## Data Model
### MealPlan Schema
## Technical Details
### Scoring Algorithm Constants
```typescript
// packages/shared/src/types/meal-plan.ts
export interface MealPlan {
id: string;
householdId: string;
weekStartDate: Date; // Monday of the planning week
days: MealPlanDay[];
status: MealPlanStatus;
shoppingListId?: string; // Auto-generated shopping list (Phase 9 link)
createdBy: string;
createdAt: Date;
updatedAt: Date;
}
export interface MealPlanDay {
date: Date;
meals: PlannedMeal[];
dailyNutritionTotal: NutritionInfo; // Computed
}
export interface PlannedMeal {
id: string; // UUID for drag-and-drop reference
type: MealType;
recipeId?: string; // Linked recipe
recipeName: string; // Denormalized
servings: number;
customName?: string; // For non-recipe meals
customNutrition?: NutritionInfo; // Manual override for non-recipe meals
perServingNutrition: NutritionInfo; // From recipe or custom
notes?: string;
}
export enum MealType {
BREAKFAST = 'breakfast',
LUNCH = 'lunch',
DINNER = 'dinner',
SNACK = 'snack',
}
export enum MealPlanStatus {
DRAFT = 'draft',
ACTIVE = 'active',
COMPLETED = 'completed',
}
```
### NutritionTarget Schema
```typescript
// packages/shared/src/types/nutrition-target.ts
export interface NutritionTarget {
id: string;
userId: string;
householdId: string;
dailyCalories: number;
proteinG: number;
carbsG: number;
fatG: number;
fiberG?: number;
sodiumMg?: number;
sugarG?: number;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
```
### MongoDB Indexes
```javascript
{ householdId: 1, weekStartDate: 1 } // unique per household per week
{ householdId: 1, status: 1 }
{ householdId: 1, 'days.meals.recipeId': 1 } // Find plans using a recipe
```
---
## API Endpoints
### MealPlanModule
| Method | Path | Description | Auth |
| ------ | ------------------------------ | ---------------------------------------------- | ------ |
| GET | `/meal-plans` | List meal plans (paginated) | member |
| GET | `/meal-plans/current` | Get current week's plan | member |
| GET | `/meal-plans/:id` | Get specific plan | member |
| POST | `/meal-plans` | Create new week plan | member |
| PATCH | `/meal-plans/:id` | Update plan (add/move/remove meals) | member |
| DELETE | `/meal-plans/:id` | Delete plan (draft only) | admin |
| POST | `/meal-plans/:id/activate` | Set plan as active | member |
| GET | `/meal-plans/:id/shopping-gap` | What's needed beyond current pantry | member |
| GET | `/meal-plans/suggestions` | Get recipe suggestions for current pantry | member |
| POST | `/meal-plans/suggest-with-llm` | LLM-powered meal plan generation (placeholder) | member |
### NutritionTargetModule
| Method | Path | Description | Auth |
| ------ | ------------------------ | -------------------------------- | ------ |
| GET | `/nutrition-targets` | Get current user's active target | member |
| POST | `/nutrition-targets` | Set nutrition targets | member |
| PATCH | `/nutrition-targets/:id` | Update targets | member |
---
## Tasks
### 8.1 — Shared Types & Validation
- Add all types above to `packages/shared`
- Zod schemas for create/update operations
- Meal plan day validation (7 days per plan, valid dates)
### 8.2 — Meal Plan CRUD
- Standard CRUD with computed `dailyNutritionTotal` per day:
- Sum `perServingNutrition * servings` for all meals in each day
- On meal plan creation: default to 7 empty days (MondaySunday)
- On update: support granular operations:
- `addMeal(dayIndex, meal)`
- `removeMeal(dayIndex, mealId)`
- `moveMeal(fromDay, toDay, mealId)` — for drag-and-drop
- `updateMeal(dayIndex, mealId, updates)`
### 8.3 — Recipe Suggestion Engine (Core Algorithm)
This is the **key differentiating feature** — algorithmic, no LLM required.
```typescript
class RecipeSuggestionService {
/**
* Score and rank recipes based on pantry state and user preferences.
*
* Input:
* - Current pantry items (with freshness urgency)
* - Recipe catalog for the household
* - User's nutrition targets (optional)
* - Already planned meals this week (to avoid repetition)
*
* Scoring per recipe:
* ingredientCoverageScore (0-40 pts): % of ingredients available in pantry
* freshnessUrgencyScore (0-30 pts): bonus for using urgent/use-soon items
* nutritionBalanceScore (0-20 pts): how well it complements the day's existing meals vs targets
* varietyScore (0-10 pts): penalty for recently planned recipes
*
* Output per suggestion:
* - recipe (id, name, perServingNutrition)
* - score (total)
* - availableIngredients[]: items from pantry that match
* - missingIngredients[]: items not in pantry (with estimated cost from Phase 9 if available)
* - urgentIngredients[]: pantry items with urgency=urgent that this recipe would use
* - reasoning: human-readable explanation of why this recipe was suggested
*/
suggestRecipes(context: SuggestionContext): Promise<RecipeSuggestion[]>;
}
```
**Ingredient matching logic**:
- Match recipe ingredient's `productId` against pantry items with `status: sealed|opened`
- Check quantity: is there enough? (approximate — compare units, flag if unclear)
- Prefer items with higher freshness urgency
**Scoring weights** (configurable per household):
```typescript
const DEFAULT_WEIGHTS = {
ingredientCoverage: 40,
freshnessUrgency: 30,
nutritionBalance: 20,
variety: 10,
const WEIGHTS = {
COVERAGE: 0.4, // Do I have the items?
URGENCY: 0.3, // Should I use these items now?
NUTRITION: 0.2, // Does it fit my macros?
VARIETY: 0.1 // Have I eaten this too much?
};
```
### 8.4 — Shopping Gap Analysis
- `GET /meal-plans/:id/shopping-gap`:
- For each recipe in the meal plan, list required ingredients
- Cross-reference with current pantry (available quantity vs needed quantity)
- Return:
```typescript
interface ShoppingGap {
coveredByPantry: ShoppingGapItem[]; // Already have enough
needToBuy: ShoppingGapItem[]; // Partially or fully missing
pantryItemsUsed: PantryItemUsage[]; // Which pantry items will be consumed
}
interface ShoppingGapItem {
productId: string;
productName: string;
totalNeeded: { quantity: number; unit: ServingUnit };
availableInPantry: { quantity: number; unit: ServingUnit };
shortfall: { quantity: number; unit: ServingUnit };
usedInRecipes: string[]; // Recipe names
}
```
- This output feeds directly into Phase 9's auto-generated shopping lists
### 8.5 — LLM Suggestion Placeholder
- `POST /meal-plans/suggest-with-llm`:
- Builds a context object: pantry summary, nutrition targets, dietary preferences
- Calls `ILlmProvider.suggestMealPlan(context)`
- With `NoOpLlmProvider`: returns `{ available: false }`
- When wired (Phase 10): returns a full week meal plan draft
### 8.6 — Web UI: Meal Planning
- `/meal-plans` page:
- **Weekly calendar grid**: 7 columns (MonSun) × 4 rows (Breakfast, Lunch, Dinner, Snack)
- Each cell: drop zone for recipes, shows meal name + calorie badge
- **Drag-and-drop**: drag recipes from suggestion panel or between cells
- **Daily nutrition summary row** at bottom: calories, protein, carbs, fat bars
- Color-coded vs user's nutrition targets (under = blue, on-target = green, over = red)
- **Week navigation**: previous/next week arrows
- **Suggestion panel** (sidebar or drawer):
- "Suggestions based on your pantry" — ranked list from suggestion engine
- Each suggestion shows: recipe name, match score, "Uses: [urgent items]", "Need to buy: [missing items]"
- Click to expand: full ingredient match breakdown
- "Add to plan" button → pick day + meal type
- **Shopping gap tab**: shows what's needed beyond pantry, "Generate shopping list" button (Phase 9 integration)
- `/nutrition-targets` settings:
- Daily macro targets form (calories, protein, carbs, fat)
- Preset templates: "Maintenance", "Weight loss", "Muscle gain", "Custom"
- Visual preview: donut chart of macro ratios
### Integration Points
- **PantryService**: Fetch items where `status` is `sealed` or `opened`.
- **RecipesService**: Fetch recipes and their computed nutrition/ingredients.
- **NutritionCalculator**: Reuse for summing daily plan totals.
---
## Acceptance Criteria
- [ ] Can create a weekly meal plan and add meals to specific days/slots
- [ ] Daily nutrition totals are computed and displayed
- [ ] Suggestion engine returns ranked recipes based on pantry state
- [ ] Suggestions prioritize recipes using soon-to-expire pantry items
- [ ] Shopping gap analysis correctly identifies missing ingredients
- [ ] Drag-and-drop works in the weekly calendar UI
- [ ] Nutrition targets can be set per user
- [ ] Daily nutrition bars show progress vs targets
- [ ] Variety scoring penalizes recently used recipes
---
## Estimated Effort
Large. The suggestion engine scoring algorithm, shopping gap analysis, and calendar UI with drag-and-drop are all significant features.
- [ ] Weekly meal plans can be created and managed via a calendar UI.
- [ ] Drag-and-drop allows moving recipes between days and meal types.
- [ ] The suggestion engine prioritizes recipes using urgent pantry items.
- [ ] Daily nutrition bars reflect progress against user-defined macro targets.
- [ ] Shopping gap analysis identifies missing ingredients for the planned week.
- [ ] Full unit and integration test coverage as per project standards.
- [ ] Build, Lint, and Coverage checks all pass.

View file

@ -315,7 +315,14 @@ interface StoreComparisonResult {
- Match items to products (fuzzy), match store to stores
- With `NoOpLlmProvider`: returns `{ available: false }`
### 9.10 — Web UI: Grocery Management
### 9.10 — Web: API Client Services
- `packages/web/src/services/shopping-lists.ts` — fetch functions for shopping list endpoints
- `packages/web/src/services/prices.ts` — fetch functions for price record endpoints
- Unit tests in `packages/web/src/services/__tests__/shopping-lists.test.ts`
- Unit tests in `packages/web/src/services/__tests__/prices.test.ts`
### 9.11 — Web UI: Grocery Management
- `/shopping-lists` page:
- Active lists at top, completed/archived below
@ -326,16 +333,23 @@ interface StoreComparisonResult {
- Each item: checkbox, name, quantity, estimated price
- Check off: expand to enter actual price (optional)
- Real-time sync indicator ("2 members shopping")
- "Done Shopping" button prompts "Add items to pantry?"
- "Done Shopping" button prompts "Add items to pantry?"
- `/stores` page:
- Store list with CRUD
- Store list with CRUD (already exists from Phase 4, extend if needed)
- Per-store: total spent, last visit, product count
- `/prices` page (analytics):
- Product search price history line chart (per store, color-coded)
- Product search price history line chart (per store, color-coded)
- Store comparison table
- Spending over time bar chart
- Price alerts panel
- **Shopping list widget on dashboard**: shows active lists with quick-check functionality
- Component tests for all pages and interactive components
### 9.12 — CI Verification
- `npm run build` passes
- `npm run test:cov` passes (100% lines/functions/statements, 90% branches)
- `npm run lint` passes
---
@ -349,6 +363,8 @@ interface StoreComparisonResult {
- [ ] Price analytics show spending trends and alerts
- [ ] Auto-generated lists from meal plans correctly reflect shopping gap
- [ ] Receipt parsing endpoint delegates to LLM provider
- [ ] Web UI has component tests for all pages and interactive components
- [ ] `npm run build`, `npm run test:cov`, and `npm run lint` all pass
---