Mealie¶
Mealie is the recipe and meal-planning app on the personal VM, port 9925. It drives the weekly dinner plan and the grocery list. Long-lived JWT auth, Authorization: Bearer $MEALIE_TOKEN.
Three gotchas that cost real debugging¶
- List endpoints return
items, notresults—{items:[...], total, total_pages}. Page untilpage >= total_pages. - Recipe URL slug is
slugify(name), not a free-form key. Mealie derives the slug from the recipe name. Look a recipe up at/api/recipes/<slugify(name)>. After a create 500, re-GET by the name-derived slug, never by the 500 text body. - Shopping items are generated from recipe ingredients, not free-text.
POST /api/households/shopping/itemswith aname500s and does not commit, leavingname:nulljunk. The working path isPOST /api/households/shopping/lists/{listId}/recipe/{recipeId}with body{}— it adds each ingredient with correct quantity, unit, and note, de-duped by food and with quantities summed across multiple recipe refs.
500 means success
The 500 on Mealie writes is a benign commit-then-crash. The transaction commits, then the event bus throws. Always verify by GET, never blind-retry — a retry re-triggers or slug-collides.
The event-bus bug¶
POST /api/recipes/create/url is broken, it hits a Pydantic Event validation error in _publish_recipe_created. It commits a shell (name, description, orgURL, servings) but drops ingredients, instructions, and image. PUT /api/recipes/{slug}/image (multipart) is broken the same way, plus a UnicodeDecodeError when the error handler tries to decode JPEG bytes as UTF-8.
What works: POST /api/recipes/test-scrape-url returns full scraped recipe JSON including the image array, and PUT /api/recipes works with a JSON array body. Media files live at data/recipes/{recipe_id}/images/original.webp and are served at GET /api/media/recipes/{recipe_id}/images/original.webp. The image field is just a label token, not the filename.
Shopping list rebuild¶
The weekly builder rebuilds the recipe-derived part of the list from the current plan instead of add-only. Recipe-derived quantities sum across every recipe reference, so repeated add-only runs accumulate stale quantities — the symptom is "7 packages packer brisket". Before re-adding, clear every unchecked item that has at least one recipeReferences entry. Checked items and hand-typed items are preserved.
Rebuild is slow, don't assume it crashed
A single recipe POST routinely takes over 30 seconds because of the commit-then-crash handler, and a full 7-recipe rebuild can take 3–5 minutes. A hardcoded 30-second timeout raises mid-rebuild and leaves the list partially populated. Use a 180-second timeout and run it in the background.
Kitchen constraints shape the plan¶
The weekly planner picks recipes against the appliances actually in the kitchen: Traeger, Weber charcoal grill, air fryer, electric stove and oven, rice maker, Instant Pot, sous vide, crockpot.
The one hard rule is safety, not convenience: no unattended oven or stovetop while the whole family is out of the house during the eating window. Only the crockpot, sous vide, and Traeger are unattended-safe. So on a night with an evening activity, dinner has to be one of:
- cooked ahead and finished before leaving (crockpot on LOW around 12:30, done by 5:00),
- running on an unattended-safe appliance while out,
- or simply eaten out, which is a legitimate plan rather than a failure.
Soccer and gymnastics end at 6:00–6:15 with bedtime by 8:00, so those nights need dinner ready on arrival — Instant Pot or rice cooker started as you leave, holding on keep-warm when you walk in. A stovetop or oven recipe cannot work on those nights.
Multi-day projects get scheduled by the serve day. A brisket is a three-day thing: trim and 24-hour dry brine, overnight on the Traeger, pull and rest. The builder computes the day-1 start and writes a prep alert so the morning briefing warns you when to begin.
The 401 that wasn't Mealie's fault¶
Mealie was pinging gotify with 401s that looked like a broken token. It was a kiosk session expiring — tokenTime: 48 hours on the web-session JWT. The diagnosis is in the blog post The 401 spam that wasn't HA.
See also¶
- Notifications — the loggifly side
- Blog post: The 401 spam that wasn't HA