Setup initial project
This commit is contained in:
commit
db79af06f7
119 changed files with 20761 additions and 0 deletions
237
docs/phases/phase-6-recipes.md
Normal file
237
docs/phases/phase-6-recipes.md
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
# Phase 6 — Recipe Management
|
||||
|
||||
**Goal**: Enter, import, and manage recipes. Auto-calculate nutrition from product library ingredients. Highlight nutritional warnings. This phase ties into the product library (Phase 5) and will be consumed by meal planning (Phase 8).
|
||||
|
||||
**Depends on**: Phase 0, Phase 5
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. `Recipe` MongoDB schema and full CRUD API
|
||||
2. Automatic nutrition calculation from ingredient list
|
||||
3. Nutritional warning generation
|
||||
4. Recipe scaling (adjust servings)
|
||||
5. Recipe import via LLM (plain text → structured)
|
||||
6. Recipe editor web UI with live nutrition sidebar
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Recipe Schema
|
||||
|
||||
```typescript
|
||||
// packages/shared/src/types/recipe.ts
|
||||
export interface Recipe {
|
||||
id: string;
|
||||
householdId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
servings: number;
|
||||
prepTime?: number; // minutes
|
||||
cookTime?: number; // minutes
|
||||
totalTime?: number; // minutes (auto-calculated or manual)
|
||||
ingredients: RecipeIngredient[];
|
||||
steps: RecipeStep[];
|
||||
tags: string[]; // e.g., 'vegetarian', 'quick', 'meal-prep'
|
||||
cuisine?: string; // e.g., 'Italian', 'Japanese'
|
||||
imageUrl?: string;
|
||||
source?: RecipeSource;
|
||||
totalNutrition: NutritionInfo; // Denormalized, computed on save
|
||||
perServingNutrition: NutritionInfo; // Denormalized, computed on save
|
||||
warnings: NutritionWarning[]; // Computed on save
|
||||
isFavorite: boolean;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface RecipeIngredient {
|
||||
productId: string; // Reference to Product
|
||||
productName: string; // Denormalized for display
|
||||
quantity: number;
|
||||
unit: ServingUnit;
|
||||
preparation?: string; // e.g., 'diced', 'minced', 'melted'
|
||||
isOptional: boolean;
|
||||
nutritionContribution: NutritionInfo; // Per-ingredient computed nutrition
|
||||
}
|
||||
|
||||
export interface RecipeStep {
|
||||
order: number;
|
||||
instruction: string;
|
||||
duration?: number; // minutes
|
||||
tip?: string;
|
||||
}
|
||||
|
||||
export interface RecipeSource {
|
||||
type: 'manual' | 'url' | 'llm_import' | 'text_import';
|
||||
url?: string;
|
||||
importedAt?: Date;
|
||||
}
|
||||
|
||||
export enum NutritionWarning {
|
||||
HIGH_CALORIES = 'high_calories', // > 800 kcal/serving
|
||||
HIGH_SODIUM = 'high_sodium', // > 1500mg/serving
|
||||
HIGH_SUGAR = 'high_sugar', // > 25g/serving
|
||||
HIGH_SATURATED_FAT = 'high_saturated_fat', // > 13g/serving
|
||||
LOW_PROTEIN = 'low_protein', // < 10g/serving
|
||||
LOW_FIBER = 'low_fiber', // < 3g/serving
|
||||
HIGH_CHOLESTEROL = 'high_cholesterol', // > 200mg/serving
|
||||
}
|
||||
```
|
||||
|
||||
### MongoDB Indexes
|
||||
|
||||
```javascript
|
||||
{ householdId: 1, name: 'text', tags: 'text', cuisine: 'text' }
|
||||
{ householdId: 1, 'ingredients.productId': 1 } // Find recipes using a product
|
||||
{ householdId: 1, tags: 1 }
|
||||
{ householdId: 1, isFavorite: 1 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### RecipesModule
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
| ------ | -------------------------------- | --------------------------------------- | ------ |
|
||||
| GET | `/recipes` | List/search recipes (paginated) | member |
|
||||
| GET | `/recipes/:id` | Get single recipe | member |
|
||||
| POST | `/recipes` | Create recipe | member |
|
||||
| PATCH | `/recipes/:id` | Update recipe | member |
|
||||
| DELETE | `/recipes/:id` | Soft-delete recipe | admin |
|
||||
| POST | `/recipes/:id/scale` | Get scaled version (preview, not saved) | member |
|
||||
| POST | `/recipes/import-text` | Import from plain text via LLM | member |
|
||||
| POST | `/recipes/import-url` | Import from URL via LLM | member |
|
||||
| GET | `/recipes/by-product/:productId` | Find recipes using a specific product | member |
|
||||
|
||||
### Query Parameters for GET `/recipes`
|
||||
|
||||
```
|
||||
?q=pasta # Full-text search
|
||||
&tags=vegetarian,quick # Filter by tags
|
||||
&cuisine=Italian # Filter by cuisine
|
||||
&maxCalories=600 # Filter by per-serving calories
|
||||
&isFavorite=true # Favorites only
|
||||
&cursor=abc123
|
||||
&limit=20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### 6.1 — Shared Types & Validation
|
||||
|
||||
- Add recipe types to `packages/shared/src/types/recipe.ts`
|
||||
- Zod schemas:
|
||||
- `CreateRecipeSchema` — ingredients must reference valid productIds
|
||||
- `UpdateRecipeSchema` — partial
|
||||
- `ScaleRecipeSchema` — `{ targetServings: number }`
|
||||
- `ImportRecipeTextSchema` — `{ text: string }`
|
||||
- `ImportRecipeUrlSchema` — `{ url: string }`
|
||||
|
||||
### 6.2 — Nutrition Calculation Service
|
||||
|
||||
- `NutritionCalculatorService`:
|
||||
|
||||
```typescript
|
||||
class NutritionCalculatorService {
|
||||
/**
|
||||
* For each ingredient:
|
||||
* 1. Lookup the product by productId
|
||||
* 2. Convert ingredient quantity/unit to product's servingUnit
|
||||
* 3. Calculate nutrition proportionally: (ingredient_qty / serving_size) * nutrition_per_serving
|
||||
* 4. Sum across all ingredients → totalNutrition
|
||||
* 5. Divide by servings → perServingNutrition
|
||||
*/
|
||||
calculateRecipeNutrition(
|
||||
ingredients: RecipeIngredient[],
|
||||
servings: number,
|
||||
): RecipeNutritionResult;
|
||||
|
||||
/**
|
||||
* Check per-serving nutrition against warning thresholds
|
||||
*/
|
||||
generateWarnings(perServingNutrition: NutritionInfo): NutritionWarning[];
|
||||
}
|
||||
```
|
||||
|
||||
- Unit conversion helper: handle common conversions (g ↔ oz, ml ↔ cups, etc.)
|
||||
- Not all conversions are possible (density-dependent) — log warning, use best approximation
|
||||
- This is explicitly **informative, not clinical-grade accurate**
|
||||
|
||||
### 6.3 — Recipe CRUD with Auto-Calculation
|
||||
|
||||
- On `POST /recipes` and `PATCH /recipes/:id`:
|
||||
1. Validate ingredients exist in product library
|
||||
2. Call `NutritionCalculatorService.calculateRecipeNutrition()`
|
||||
3. Call `NutritionCalculatorService.generateWarnings()`
|
||||
4. Store computed `totalNutrition`, `perServingNutrition`, `warnings` on document
|
||||
- On product nutrition update (Phase 5 edit), trigger background recalculation:
|
||||
- Find all recipes where `ingredients[].productId == updatedProductId`
|
||||
- Recalculate each recipe's nutrition
|
||||
|
||||
### 6.4 — Recipe Scaling
|
||||
|
||||
- `POST /recipes/:id/scale` with `{ targetServings: number }`:
|
||||
- Returns a scaled **preview** (not persisted) with adjusted ingredient quantities and recalculated nutrition
|
||||
- `scaledQuantity = originalQuantity * (targetServings / originalServings)`
|
||||
|
||||
### 6.5 — Recipe Import (LLM)
|
||||
|
||||
- `POST /recipes/import-text`:
|
||||
- Accepts `{ text: string }` (pasted recipe)
|
||||
- Calls `ILlmProvider.parseRecipe(text)`
|
||||
- LLM returns structured: `{ name, servings, ingredients[]: { name, quantity, unit }, steps[] }`
|
||||
- Service attempts to match ingredient names to existing products (fuzzy match by name)
|
||||
- Returns structured recipe for user review — unmatched ingredients flagged for manual product creation
|
||||
- `POST /recipes/import-url`:
|
||||
- Calls `ILlmProvider.parseRecipeFromUrl(url)`
|
||||
- Same flow as text import
|
||||
|
||||
- With `NoOpLlmProvider`: returns `{ available: false }`
|
||||
|
||||
### 6.6 — Web UI: Recipe Management
|
||||
|
||||
- `/recipes` page:
|
||||
- Search bar, tag and cuisine filters
|
||||
- Recipe card grid: image, name, time, calories/serving, warning badges
|
||||
- Favorites tab
|
||||
- `/recipes/new` and `/recipes/:id/edit`:
|
||||
- Recipe metadata form (name, description, servings, times, cuisine, tags)
|
||||
- Ingredient editor:
|
||||
- Autocomplete from product library
|
||||
- Quantity + unit inputs
|
||||
- "Add ingredient" button, drag-to-reorder
|
||||
- Per-ingredient nutrition shown inline
|
||||
- Steps editor: ordered text areas, optional duration per step
|
||||
- **Live nutrition sidebar**: updates as ingredients are added/changed
|
||||
- Shows total and per-serving macros
|
||||
- Warning badges with explanations
|
||||
- "Scale" button: adjust servings in sidebar to see scaled amounts
|
||||
- `/recipes/:id` detail page:
|
||||
- Full recipe view with ingredients, steps, nutrition panel
|
||||
- "Import from text" and "Import from URL" buttons in recipe list page
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Can create a recipe with ingredients linked to products
|
||||
- [ ] Nutrition is automatically calculated and stored on the recipe
|
||||
- [ ] Warnings are generated for recipes exceeding thresholds
|
||||
- [ ] Recipe scaling returns correctly adjusted quantities
|
||||
- [ ] Editing a product's nutrition triggers recipe recalculation
|
||||
- [ ] Text/URL import endpoint delegates to LLM provider interface
|
||||
- [ ] Web UI shows live nutrition as ingredients are added
|
||||
- [ ] Full-text search finds recipes by name, tags, cuisine
|
||||
|
||||
---
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
Medium. Nutrition calculation logic and unit conversion require careful implementation. UI is moderately complex with the live sidebar.
|
||||
Loading…
Add table
Add a link
Reference in a new issue