Job 1's bulk run apply'd 184 recipes and 182 of them failed with the
same error: POST /api/foods -> 400 UNIQUE constraint failed:
ingredient_foods.name, ingredient_foods.group_id. Cause: Mealie's
name_normalized strips punctuation/whitespace/case more aggressively
than our local _build_name_index's plain .lower(), so the cache misses,
the create_food fires blindly, and Mealie's UNIQUE constraint kills
the call. Whole-recipe apply was wrapped in try/except at the bulk
runner so the recipe got marked errored — but applied_at was still
set to NOW(), making the rerun think we'd already tried. We had, but
the recipe's still unparsed.
Two fixes:
1. sterilizer._resolve_food / _resolve_unit replace the inline
create-on-miss block. Order: local cache → Mealie search-endpoint
tie-break → create. On any UNIQUE-flavored 400 from create, fall
back to one more search to adopt whatever Mealie has under the
normalized form. Mealie's search endpoint applies its own
name_normalized internally so we don't have to mirror its rules.
_search_for_match takes "foods" or "units" and looks for an exact
case-insensitive match against name or pluralName, with a fallback
to "trust Mealie's ranker" when there's exactly one hit.
2. db.mark_proposal_applied no longer sets applied_at on error. On
success: applied_at=NOW(), apply_error=NULL. On error: applied_at
stays NULL, apply_error gets the message. list_approved_unapplied_
proposals keys off applied_at IS NULL, so a rerun naturally retries
only the failed recipes.
Net effect: rerun can now successfully apply the 182 failed recipes
without re-walking them, and won't waste calls on the 2 that did go
through.