MeshiTrack/docs/phases/phase-6-recipes.md

8.4 KiB

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

// 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

{ 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:
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.