Phase A foundation. Cobb 2026-04-29: 'go big or go home' on density-table
aggregator — this commit lands the schema + seed data so the aggregator
engine has something to look up against in step 2.
DB:
- migration 010: cauldron_foods (canonical_name PK, density_g_per_ml,
default_unit_class enum mass/volume/count/mixed, common_size_g,
category, usda_fdc_id, source enum)
- migration 011: cauldron_food_mapping (per-household Mealie food_id →
cauldron canonical food_id, used by aggregator + foods-dedupe later)
Seed data:
- scripts/build_foods_seed.py — extractor that walks USDA SR Legacy
foodPortions, derives density g/ml from cup/tbsp/tsp/fl-oz/ml/etc
measurements (handles SR Legacy's quirk of putting unit in 'modifier'
with measureUnit.name='undetermined'), filters out babyfood / branded
/ fast-food / alcoholic-beverage clutter, normalizes names, categorizes
via longest-keyword-wins
- cauldron/data/foods_seed_usda.json — 2,462 foods with density values
derived from USDA. 636KB, ships in the image.
- cauldron/data/README.md — regen instructions + known issues / iteration
plan (next pass: claude-curated cleanup → ~500-800 high-relevance entries
+ count-based foods like egg/onion that USDA doesn't cover)
Loader (cauldron/foods.py):
- load_seed_if_empty(db) called on app startup right after migrate().
Idempotent — won't reload if table is non-empty.
- reload_seed(db) for forced reloads (INSERT IGNORE).
- search_food(db, name) helper for the aggregator + UI.
Categories present in seed:
produce-vegetable: 300, spice: 256, dairy: 207, condiment: 197,
legume: 189, meat: 166, beverage: 153, baking: 129, produce-fruit: 128,
oil-fat: 126, nut-seed: 115, grain: 89, other: 407
The 407 'other' bucket and the verbose USDA names ('mayonnaise, reduced
fat, with olive oil') will get cleaned up via clawdforge in step 3.
For now the aggregator can already do the math against this seed; the
unit-conversion engine is the next commit.
|
||
|---|---|---|
| cauldron | ||
| scripts | ||
| .env.example | ||
| .gitignore | ||
| compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| README.md | ||
| requirements.txt | ||
cauldron
Mealie-backed AI meal planner + shopping list for the family. LAN-only,
internal tool. Mealie at recipes.sulkta.com is the source of truth for
recipes / meal plans / shopping lists; cauldron is the AI layer + Abby's
branded UI on top.
Status
v0.1 — backend bones (current). Ingredient sterilizer endpoint working. No UI yet; bearer-auth API only. Frontend + Authentik OIDC arrives in v0.2. Native Kotlin Android in v0.5.
Surface (v0.1)
GET /healthz liveness + clawdforge upstream
GET /api/recipes list Mealie recipes (paginated)
POST /api/sterilize/preview/<slug> dry-run AI parse, return proposals
POST /api/sterilize/apply/<slug> write parses back to Mealie
All routes except /healthz require Authorization: Bearer <ADMIN_BEARER>.
Architecture
Abby's phone (later: Kotlin app)
│
▼
cauldron (Flask, port 7790, LAN-only)
├─ Mealie API client ─── recipes.sulkta.com (source of truth)
├─ clawdforge client ─── 192.168.0.5:8800 (claude -p runner)
└─ Authentik OIDC (v0.2)
cauldron does NOT hold its own database in v0.1 — all state lives in Mealie. A small Postgres/MariaDB schema lands in v0.2 for Abby-specific prefs + chat history.
Ingredient sterilizer
Mealie's CRF parser is mediocre. Cobb's hand-typed recipes have lots of free-form quantity strings ("about 2 cups cooked white rice", "1 small handful kale", "a pinch of salt") that don't aggregate cleanly into a shopping list.
The sterilizer batches all ingredients of one recipe into a single Sonnet call (via clawdforge), gets back parallel structured parses, then on apply links each parse to existing Mealie food/unit records (creating any missing by name) and PUTs the recipe back.
Preview is non-destructive — review proposals before apply.
# Dry-run preview
curl -sS -X POST -H "Authorization: Bearer $ADMIN_BEARER" \
http://192.168.0.5:7790/api/sterilize/preview/spaghetti-bolognese | jq .
# Apply (creates missing foods/units by default)
curl -sS -X POST -H "Authorization: Bearer $ADMIN_BEARER" \
http://192.168.0.5:7790/api/sterilize/apply/spaghetti-bolognese | jq .
Deploy
ssh lucycd /mnt/user/appdata && git clone <gitea-url> cauldron && cd cauldron/build(or wherever the deploy convention lands)- Drop
.envat/mnt/cache/appdata/secrets/cauldron.env(chmod 600 root:root)CLAWDFORGE_TOKENis already populated by the bootstrap (seememory/2026-04-28.md)MEALIE_API_TOKEN— mint atrecipes.sulkta.com→ user → API tokensADMIN_BEARER— pick 32 bytes of entropySECRET_KEY— 32 bytes for Flask sessions
docker compose up -d --build- Smoke:
curl http://192.168.0.5:7790/healthz
Roadmap
- v0.1 ✓ — sterilizer backend + Flask shell
- v0.2 — Authentik OIDC, Abby-branded web UI, palette CSS, postgres for prefs
- v0.3 — meal plan generator (week → Mealie meal plan write)
- v0.4 — shopping list aggregator (read meal plan → consolidated grocery list)
- v0.5 — native Kotlin + Compose Android app (read-only shopping list + plan view)
Repo layout
cauldron/
├─ cauldron/
│ ├─ config.py env-driven config
│ ├─ forge.py clawdforge HTTP client
│ ├─ mealie.py Mealie API client
│ ├─ sterilizer.py ingredient parse + apply pipeline
│ └─ server.py Flask app
├─ Dockerfile
├─ compose.yml
├─ requirements.txt
└─ .env.example