diff --git a/SKILL.md b/SKILL.md index c1c4485..3d355fb 100644 --- a/SKILL.md +++ b/SKILL.md @@ -3,7 +3,8 @@ name: home description: > Activate this skill for any work related to the Fritz household (Hawks Nest). TRIGGER when: user mentions Hawks Nest, boat, MasterCraft, Minnetrista, Hawks Point, - home automation, Home Assistant, Lutron, Z-Wave, lights, automations, 4400 Hawks. + home automation, Home Assistant, Lutron, Z-Wave, lights, automations, 4400 Hawks, + Mealie, meals, recipes, meal planning, meals.vino.network. user-invocable: true --- @@ -11,6 +12,7 @@ user-invocable: true - Address: 4400 Hawks Pt, Minnetrista, MN 55331 - Names: Home, The Hawks Nest - Home Assistant: home.vino.network (HAOS on msp001 VM 123) — see fritzlab skill for access +- Mealie: https://meals.vino.network (sjc001 cluster, `mealie` namespace, group `Home` / household `Family`) @@ -28,4 +30,8 @@ user-invocable: true summary="Boat details: Stranger Fins, 2025 MasterCraft 21 — specs and info" categories=["boat", "fun", "entertainment"] keywords=["MasterCraft", "NXT", "boat", "Stranger Fins", "wake", "surf"] /> + diff --git a/reference/mealie.md b/reference/mealie.md new file mode 100644 index 0000000..58ccef6 --- /dev/null +++ b/reference/mealie.md @@ -0,0 +1,144 @@ +--- +name: mealie +description: Adding and editing recipes in the family Mealie instance, including the non-obvious ingredient-parsing requirements +--- + + +- URL: https://meals.vino.network (Mealie v3.18.x, OIDC via Authentik) +- Hosted in sjc001 cluster, `mealie` namespace +- Family group/household: `Home` / `Family` +- API token: stored in `code/git/code.fritzlab.net/fritzlab/agent/.env` as + `MEALIE_TOKEN` (paired with `MEALIE_URL=https://meals.vino.network`). + Regenerate at https://meals.vino.network/user/profile/api-tokens. +- All endpoints take `Authorization: Bearer $MEALIE_TOKEN`. + + + +- **Every recipe must carry exactly one meal-type tag**: `Breakfast`, `Lunch`, `Dinner`, + or `Dessert`. When importing or editing a recipe, add the appropriate one; preserve + any other tags already on the recipe. If meal type is ambiguous (e.g. banana bread → + breakfast or dessert), ask the user — don't pick silently. +- Meal-type tag IDs in the family Mealie: + - `Breakfast` = `38fb71bf-a89d-45ee-9d21-0b1e5f716569` + - `Lunch` = `5f0ce972-2243-4ef6-b3c7-ad24f8561e84` + - `Dinner` = `bc992ba9-80ce-4407-8d1e-e69a7f659ebb` + - `Dessert` = `e7adc953-3f23-4032-804b-ada18b3a2da9` +- To append a tag without clobbering: GET the recipe, take `tags`, append the new + `{id, name, slug}` object, PATCH back with `{"name": , "tags": [...]}`. + + + +GET /api/users/self — confirms the token works and returns group/household. + + + +Mealie scrapes recipe sites that publish schema.org Recipe JSON-LD. +POST /api/recipes/create/html-or-json with `{"url":"https://..."}` — returns the new slug. +Falls back to scraping HTML if no JSON-LD. PDFs and image URLs do NOT work here. + + + +Two-step: create then patch. Mealie has no single endpoint that takes a full recipe at once. + +1. POST /api/recipes body `{"name": "..."}` + Returns the slug as a **raw JSON string** (e.g. `"my-recipe"`), not an object. + If a recipe with the same name exists, Mealie auto-suffixes (`-1`, `-2`). To avoid + duplicates, GET /api/recipes first and match by name before posting. + +2. PATCH /api/recipes/{slug} with the full Recipe-Input body: + - `name`, `description`, `prepTime`, `cookTime`, `totalTime` (free strings) + - `recipeYield` (string), `recipeServings` (number), `recipeYieldQuantity` (number) + - `recipeIngredient: [...]` — see ingredient-parsing below + - `recipeInstructions: [{text, ingredientReferences: []}, ...]` + - `tags: [{id, name, slug}, ...]`, `recipeCategory: [{id, name, slug}, ...]` + - `notes: [{title, text}, ...]` + - `orgURL: "..."` — source link + - DO NOT include `slug` in the PATCH body; it triggers a rename attempt that conflicts + with the current slug and returns 400 "Recipe already exists". + + + +Same as step 2 above: PATCH /api/recipes/{slug} with the fields to change. Other fields +are preserved. To replace the entire ingredient list, send the full new `recipeIngredient` +array. To clear a list, send `[]`. + +DELETE /api/recipes/{slug} removes the recipe. + + + +Mealie has an NLP parser (also `brute` and `openai` variants) that splits raw strings like +`"1½ tablespoons mayo"` into structured `{quantity, unit, food, note}` objects. The unit +and food are objects with their own `id` referencing rows in `/api/units` and `/api/foods`. + +POST /api/parser/ingredients + body: `{"parser":"nlp", "ingredients":["1½ tablespoons mayo", "4 ears fresh corn, husked", ...]}` + returns one ParsedIngredient per input. + +Parser gotchas that bite every time: + +1. **Unit / food objects with `id: null` cannot be PATCHed onto a recipe** — server raises + `ValueError: Expected 'id' to be provided for unit/food`. The parser returns `id: null` + whenever the unit/food name isn't already in the database. Workflow: after parsing, + resolve each name against `/api/units` and `/api/foods` (auto-creating missing ones via + POST), then set `ing.unit.id` and `ing.food.id` before PATCHing the recipe. + +2. **Seed the locale before parsing** or the parser fuzzy-matches against whatever you + created previously. Concrete bite: with only `tablespoon` in the unit table, every + "1 teaspoon" parses to `unit_id = ` because the Levenshtein distance + is 1. Pre-seed once per instance: + - POST /api/groups/seeders/units `{"locale":"en-US"}` + - POST /api/groups/seeders/foods `{"locale":"en-US"}` + These populate the standard US units (cup, teaspoon, tablespoon, ounce, etc.) and + common foods. After seeding the parser stops conflating teaspoon/tablespoon. Already + done on the family Mealie 2026-05-22. + +3. **Sections in an ingredient list** (e.g. "Cookie dough" vs "Buttercream" in the same + recipe): set `title` on the FIRST ingredient of each section. Convention used in the + family import script: prefix the raw input with `[Section Name]`, the helper strips + the bracket and promotes it to `title` on the first ingredient of that group. + +4. **Instructions also have a hidden required field**: each step object must include + `ingredientReferences: []`. The OpenAPI schema says it defaults to `[]`, but PATCH's + `model_dump(exclude_unset=True)` excludes it and the SQLAlchemy model then raises + `RecipeInstruction.__init__() missing 1 required positional argument: 'ingredient_references'`. + Always send `ingredientReferences: []` explicitly. + + + +RecipeTag and RecipeCategory objects in a PATCH body need **all three** of `id`, `name`, +`slug`. Sending just name+slug produces a misleading 400 "Recipe already exists" (the +real cause is a SQL integrity error logged server-side as `SQL Integrity Error on recipe +controller action`). + +Create with POST /api/organizers/tags or POST /api/organizers/categories — body +`{"name":"..."}` — response contains the `id` and `slug` to reuse. + +Existing IDs in the family Mealie: +- Tag `Mexican Street Eats Class` = `bb6841c5-3b6d-4569-b707-3d14bea83d7b` +- Category `Mexican` = `6ee64be6-83ce-4d6e-926c-62edfb791a41` + + + +- `400 "Recipe already exists"` on PATCH → almost always means tag/category/food/unit + is missing an `id`. Check the server logs (`kubectl --context sjc001 -n mealie logs + -l app=mealie`) for `SQL Integrity Error` to confirm. +- `500 TypeError` on PATCH → missing `ingredientReferences: []` on an instruction. +- `500 ValueError: Expected 'id' to be provided for unit` → parsed ingredient still has + `unit.id: null` or `food.id: null`; resolve via /api/units, /api/foods first. +- `500` on POST /api/foods or /api/units when the name already exists → list and dedupe + before creating, or catch the UniqueViolation. + + + +A working ingest script for the Mexican Street Eats PDF lives at +`/tmp/add_mealie_recipes.py` during the 2026-05-22 session. Pattern to reuse: + +1. Build food + unit name→id caches once (GET /api/foods, /api/units, paginate). +2. For each raw ingredient line, POST to /api/parser/ingredients. +3. For each parsed result with `unit.id: null` or `food.id: null`, look up by lowercased + name in the cache; POST to create if missing; update cache. +4. For sectioned recipes, strip `[Section]` prefix and set `title` on first item. +5. Build PATCH body with `recipeIngredient`, `recipeInstructions` (each with + `ingredientReferences: []`), `tags` and `recipeCategory` (with ids). +6. POST to /api/recipes for the name, then PATCH /api/recipes/{slug} with the body. +