Merge branch 'recipes-calories'
All checks were successful
CI / update (push) Successful in 4m37s

recipes: nutrition calculator with BLS/USDA matching, manual overwrites, and skip

Dual-source nutrition system using BLS (German, primary) and USDA (English, fallback)
with ML embedding matching (multilingual-e5-small / all-MiniLM-L6-v2), hybrid
substring-first search, and position-aware scoring heuristics.

Includes per-recipe and global manual ingredient overwrites, ingredient skip/exclude,
referenced recipe nutrition (base refs + anchor tags), section-name dedup,
amino acid tracking, and reactive client-side calculator with NutritionSummary component.

recipes: overhaul nutrition editor UI and defer saves to form submission

- Nutrition mappings and global overwrites are now local-only until
  the recipe is saved, preventing premature DB writes on generate/edit
- Generate endpoint supports ?preview=true for non-persisting previews
- Show existing nutrition data immediately instead of requiring generate
- Replace raw checkboxes with Toggle component for global overwrites,
  initialized from existing NutritionOverwrite records
- Fix search dropdown readability (solid backgrounds, proper theming)
- Use fuzzy search (fzf-style) for manual nutrition ingredient lookup
- Swap ingredient display: German primary, English in brackets
- Allow editing g/u on manually mapped ingredients
- Make translation optional: separate save (FAB) and translate buttons
- "Vollständig neu übersetzen" now triggers actual full retranslation
- Show existing translation inline instead of behind a button
- Replace nord0 dark backgrounds with semantic theme variables
This commit is contained in:
2026-04-02 19:46:24 +02:00
33 changed files with 722586 additions and 73 deletions

View File

@@ -0,0 +1,25 @@
import mongoose from 'mongoose';
/**
* Global nutrition overwrites — manually map ingredient names to BLS/USDA entries.
* Checked during nutrition generation before embedding search.
* Can also mark ingredients as excluded (skipped).
*/
const NutritionOverwriteSchema = new mongoose.Schema({
// The normalized ingredient name this overwrite matches (German, lowercase)
ingredientNameDe: { type: String, required: true },
// Optional English name for display
ingredientNameEn: { type: String },
// What to map to
source: { type: String, enum: ['bls', 'usda', 'skip'], required: true },
fdcId: { type: Number },
blsCode: { type: String },
nutritionDbName: { type: String },
// Whether this ingredient should be excluded from nutrition calculation
excluded: { type: Boolean, default: false },
}, { timestamps: true });
NutritionOverwriteSchema.index({ ingredientNameDe: 1 }, { unique: true });
delete mongoose.models.NutritionOverwrite;
export const NutritionOverwrite = mongoose.model('NutritionOverwrite', NutritionOverwriteSchema);

View File

@@ -163,6 +163,25 @@ const RecipeSchema = new mongoose.Schema(
}
},
// Nutrition calorie/macro mapping for each ingredient
nutritionMappings: [{
sectionIndex: { type: Number, required: true },
ingredientIndex: { type: Number, required: true },
ingredientName: { type: String },
ingredientNameDe: { type: String },
source: { type: String, enum: ['bls', 'usda', 'manual'] },
fdcId: { type: Number },
blsCode: { type: String },
nutritionDbName: { type: String },
matchConfidence: { type: Number },
matchMethod: { type: String, enum: ['exact', 'embedding', 'manual', 'none'] },
gramsPerUnit: { type: Number },
defaultAmountUsed: { type: Boolean, default: false },
unitConversionSource: { type: String, enum: ['direct', 'density', 'usda_portion', 'estimate', 'manual', 'none'] },
manuallyEdited: { type: Boolean, default: false },
excluded: { type: Boolean, default: false },
}],
// Translation metadata for tracking changes
translationMetadata: {
lastModifiedGerman: {type: Date},
@@ -177,6 +196,6 @@ RecipeSchema.index({ "translations.en.translationStatus": 1 });
import type { RecipeModelType } from '$types/types';
let _recipeModel: mongoose.Model<RecipeModelType>;
try { _recipeModel = mongoose.model<RecipeModelType>("Recipe"); } catch { _recipeModel = mongoose.model<RecipeModelType>("Recipe", RecipeSchema); }
export const Recipe = _recipeModel;
// Delete cached model on HMR so schema changes (e.g. new fields) are picked up
delete mongoose.models.Recipe;
export const Recipe = mongoose.model<RecipeModelType>("Recipe", RecipeSchema);