diff --git a/README.md b/README.md index 93711bc..3b1a098 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ -# home +# home — dissolved -Home skill repo \ No newline at end of file +This skill repo was dissolved per the 2026-07-20 one-canon knowledge +architecture decision. Household (Hawks Nest) knowledge lives at +https://docs.fritzlab.net/household/ — update the docs site, not this repo. diff --git a/SKILL.md b/SKILL.md index 6444409..948d83e 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,45 +1,21 @@ --- 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, - Mealie, meals, recipes, meal planning, meals.vino.network. -user-invocable: true + DISSOLVED. Do not use. Household (Hawks Nest) knowledge lives in the + canonical docs site: https://docs.fritzlab.net/household/ +user-invocable: false --- - -- 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`) - +# home — dissolved - -- Do not disable active automations without understanding their dependencies first. - +This skill was dissolved per the one-canon knowledge architecture decision +(decisions/2026-07-20-one-canon-knowledge-architecture on docs.fritzlab.net). +All content migrated to the canon in docs#142 (a0e98f75): - +- https://docs.fritzlab.net/household/ — landing page +- reference/home-assistant.md → household/home-assistant.md, household/home-assistant-app.md, household/home-assistant-av.md +- reference/boat.md → household/boat.md +- reference/kiosk.md → household/kiosk.md +- reference/mealie.md → household/mealie.md - - - - - - +Do not add knowledge here. Update https://docs.fritzlab.net instead. diff --git a/reference/boat.md b/reference/boat.md deleted file mode 100644 index d078d25..0000000 --- a/reference/boat.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: boat -description: Boat Information ---- - - - - diff --git a/reference/home-assistant.md b/reference/home-assistant.md deleted file mode 100644 index b6b8300..0000000 --- a/reference/home-assistant.md +++ /dev/null @@ -1,92 +0,0 @@ -# Home Assistant (app layer) - -Dashboards (Lovelace), the Great Room AV stack, and household automations on `home.vino.network`. Access/SSH/API/Z-Wave infra is owned by the **fritzlab** skill [[home-assistant]] (`reference/home-assistant.md`). Token: `VINO_HA_TOKEN` in `~/.env`. - - -Config is version-controlled at **`dfritz/home-assistant`** (code.fritzlab.net; clone `~/code/git/code.fritzlab.net/dfritz/home-assistant`): a manual mirror of the safe parts of HAOS `/config` — YAML config + includes, blueprints, `custom_components/frame_art`, `www/` cards, and a `dashboards/` snapshot of the storage-mode Lovelace (export, not live-sync). Gitignored: `secrets.yaml`, `.storage/`, the DB, `*.pem`, logs, deps/venv. Externally-managed components stay in their own repos: celebright → `dfritz/celebright`, hdmi_matrix → `dfritz/haas-hdmi-matrix-custom-component`. No CI auto-deploy — sync via `rsync -e "ssh -6"` to/from `home-assistant.vino.network:/config/`. - - - -## Nest thermostats - -Matter Server 9.1.0 and the `matter` integration provide local control for three -Nest Learning Thermostat 4th gen nodes plus their bridged sensors. Primary devices -are `Entryway Nest`, `Master Bedroom Nest`, and `Sport Court Nest`; climates are -`climate.entryway_nest`, `climate.master_bedroom_nest`, and -`climate.sport_court_nest`. Thermostats carry `Matter Nest` + `Matter HVAC` labels; -child devices are ` Sensor Nest` and unlabeled. All three are forced-air -`heat_cool` zones. - -The cloud `nest` fallback integration is not configured. It requires a dedicated -Vino Web OAuth client in Google Cloud project `home-assistant-471823` (redirect -`https://my.home-assistant.io/redirect/oauth`) and a separate Device Access project; -do not reuse Lodge OAuth credentials, Device Access project, or Pub/Sub subscription. -Target cloud naming is ` Google Nest` and `climate._google_nest`, labels -`Google Nest` + `HVAC`. - -Recovery backup `pre-nest-normalization-2026-07-18` (`a7b09a9b`) contains HA -registry/dashboard state without the database. - - - -## Editing dashboards (Lovelace) -Both dashboards (`Overview` url_path `lovelace`, `Map`) are **storage mode** (UI-managed), not YAML — no file to edit. Drive over the WS API: -- Read: `{"type":"lovelace/config"}` (no `url_path` = default `Overview`) → full config (`views[]`). -- Write: `{"type":"lovelace/config/save","config":}` — **whole-config read-modify-write**, not per-view. Always back up the fetched config first; a bad save replaces every view. -- Views use `subview: true` for the per-device remotes (reached by nav, not the top tab bar). Card commands: copy a *proven* button from a working view rather than guessing service/command strings. -- No HACS, no custom themes (`frontend/get_themes` empty). Custom Lovelace cards: drop the JS in `/config/www/` (root-owned → `ssh -6 … sudo tee`), register via `{"type":"lovelace/resources/create","res_type":"module","url":"/local/.js?v=N"}`. Served at `https://home.vino.network/local/.js`. Browser caches resources — bump `?v=` and hard-refresh (mobile app: restart) to pick up changes. - - - -## Great Room TV / AV topology -TV = **Samsung Frame** (`media_player.the_frame_tv` power+vol+source `['TV','HDMI']`; `remote.the_frame_tv` needs Samsung `KEY_*` codes — see ). WoL power-on MAC `C8:A6:EF:AE:E5:FA` via `script.turn_on_great_room_tv_2`. - -Sources feed an **HDMI matrix** (see ); `select.hdmi_matrix_vino_network_output_1` (named "Great Room TV") = current TV input, `..._output_3` = speakers. Options incl. `Roku-Ultra`, `Roku-3`, `FireTvStick`, `From-GR-AVR`, `Kiosk1/2`. - -**Matrix-source audio is the Denon AVR**, not the TV: zones `media_player.great_room` (+ `media_player.kitchen`), source `HDMI Matrix`; volume = `media_player.great_room`, and the TV's own speaker is set near-mute by the building block. TV-direct audio = the TV itself (the remote card's volume is context-aware — see ). - -Source switching = `script.set_mode_great_room_*`: -- `movie` → Roku Ultra (alias `set-mode-great-room-roku-ultra`), `roku_3`, `fire_tv_stick`, `music`, `camera_scanner`. -- Each sets both matrix outputs then runs `script.building_block_set_great_room_to_hdmi_matrix` (TV→HDMI, AVR zones on, vol 0.55, ALL ZONE STEREO). - -Source devices: Roku (`remote.roku_ultra`/`roku_3`, commands **lowercase** `home/back/up/down/left/right/select/info`); Fire TV (`remote.fire_tv_stick`, commands **UPPERCASE** `HOME/BACK/UP/DOWN/LEFT/RIGHT/CENTER/INFO/MENU`). App shortcuts via `media_player.select_source` (Roku Plex source = `Plex - Free Movies & TV`). - -Great Room AVR = Denon AVR-X2700H. `setup_error` plus a timeout on -`:8080/goform/Deviceinfo.xml` is a wedged receiver network stack; the narrow repair -is Denon telnet command `NSRBT`, wait for Deviceinfo HTTP 200, then reload the -`Great Room` config entry. This preserves power/mute state and restores -`media_player.great_room` + `media_player.kitchen`. - - - -## HDMI matrix (whole-house AV routing) -No Hassle AV **NHAV-1632** 16×32 HDMI/HDBaseT matrix at `hdmi-matrix.vino.network` (172.24.15.50, IPv4-only); the `admin/admin` web login is cosmetic — no auth on any endpoint, the LAN is the boundary. Drives every house display (Living Room, Office, bedrooms, Terrace, Great Room, speakers) through 16 output pairs (HDBaseT run + mirrored local HDMI). - -Integration `hdmi_matrix` (repo `dfritz/haas-hdmi-matrix-custom-component`, its own repo — gitignored from the config mirror) is built on the device's binary state protocol. One HA device keyed on the matrix MAC: -- `select.hdmi_matrix_vino_network_output_1..16` — source per output (**enabled**); friendly names track the device's live port names. -- `switch.…_output_N_stream` (blank one output's video) + `binary_sensor.…_input_N_signal` (live source detect) — **disabled by default**, enable as needed. -- `hdmi_matrix.send_command` service — raw `!`/`#` device command (wire numbering is 0-based). -Coordinator delta-polls every 2 s (idle poll ~32 B); front-panel/IR/web changes converge within a poll. Full protocol + a stdlib decoder (`tools/dump_state.py`) live in the repo's `docs/device-api.md` — not restated here. - -**Deploy:** rsync `custom_components/hdmi_matrix/` → `/config/custom_components/hdmi_matrix/`, then a full `ha core restart` (a config-entry reload does NOT re-import changed Python; `ha` CLI needs the login shell for `SUPERVISOR_TOKEN`, sudo drops it). NEVER stage a backup dir inside `custom_components/` — HA scans every subdir, and a dotted name (`hdmi_matrix.bak`) plus its `manifest.json` shadows the `hdmi_matrix` domain and breaks import. - -**Physical (rear panel):** a port LED lights only when its TX drives live video into an OPEN, unlinked port; a linked receiver or a no-signal input reads dark; blinking = link-training failure. The RemoteTV EDID slots are a last-seen sink cache, not live state — the matrix exposes NO live HDBaseT link status anywhere. Known-bad: matrix **port 1 (Output1, the Great Room TV feed) HDBaseT transmitter is suspect-dead** — its config is identical to working ports yet it stays dark through stream-toggle, source re-switch, standby cycle, and reboot; migrate that run to a spare output (12–16 are proven-good) if it stays dark. - - - -## Virtual remotes (Lovelace subviews, Overview dashboard) -- `great-room-tv-remote` — **the main one.** A bespoke custom card `custom:great-room-remote-card`: plus-shaped D-pad, context-aware volume, source pills (set_mode scripts), apps/playback, power toggle. Fits an iPhone with no scrolling; details in . -- `roku-ultra-remote`, `roku-3-remote`, `fire-tv-stick-remote` — older stock single-device remotes (still present). - -URL: `https://home.vino.network/lovelace/great-room-tv-remote`. Launcher: a "Great Room TV" navigate-button under the **Virtual Remotes** heading on the `kitchen_greatroom` view (subviews aren't in the tab bar, so they need a navigate-button entry point). - - -Source-of-truth: `dfritz/home-assistant` repo → `www/great-room-remote-card.js` (vanilla custom element, no build step). Deployed copy → HA `/config/www/great-room-remote-card.js`, resource `/local/great-room-remote-card.js?v=N`. Robust layout: plus-shaped CSS-grid D-pad (no abspos) with Back/Home/Info column beside it, wrapping grids, single delegated click handler. Fits an iPhone with no scroll (~630px, ≤660 budget); apps row collapses in TV mode. -**Control target is explicit + visible** (a "Controlling: X" line + a `[Follow source] | [TV]` toggle): -- *Follow source* (default): D-pad/transport/apps drive the device the matrix is on (Roku `select`/lowercase, Fire TV `CENTER`/UPPERCASE). Falls back to TV when the matrix state isn't a known source (e.g. `Viewport16`). -- *TV*: forces `remote.the_frame_tv`. One-tap fallback if a source device (asleep Roku → "Error communicating with Roku API") doesn't respond. **Samsung `samsungtv` integration needs `KEY_*` codes** (`KEY_UP/DOWN/LEFT/RIGHT/ENTER/RETURN/HOME/INFO`, `KEY_VOLUP/VOLDOWN`) — lowercase words return HTTP 200 but the TV silently ignores them (this is why the original lowercase remote never actually drove the TV). -- Tapping a source pill switches the matrix (`set_mode` script) and snaps target back to Follow source. -**Volume/mute are context-aware too** (per-profile `volume` entity): TV target → `media_player.the_frame_tv` (Samsung supports `volume_set`); source target → `media_player.great_room` (Denon AVR zone, the audio path for matrix sources). The slider uses a timestamp guard (not `activeElement`) so frequent `hass` updates don't snap the thumb back mid-drag on iOS. Frame TV can't launch apps via `select_source` (source_list only `['TV','HDMI']`), so apps only show for Roku/Fire. All entities/commands live in `DEFAULTS` (config-overridable). -**Deploy after edit:** `node --check` then `cat www/great-room-remote-card.js | ssh -6 home-assistant.vino.network 'sudo tee /config/www/great-room-remote-card.js'`, bump resource `?v=` (WS `lovelace/resources/update`, resource_id `7ee4fcac14ea4a3b9ef76c0625a89721`), hard-refresh. - - diff --git a/reference/kiosk.md b/reference/kiosk.md deleted file mode 100644 index 78ffc2d..0000000 --- a/reference/kiosk.md +++ /dev/null @@ -1,85 +0,0 @@ -# Kiosks (wall displays) - -Full-screen web kiosks on the two Nest compute nodes, each driving its attached display (DP/HDMI) via Chromium. Run as Kubernetes pods on the msp001 cluster (the Nest site). Infra/build/troubleshooting: see fritzlab skill `msp001.md` + the `fritzlab/kiosk` repo. - - -| Node | Pod (ns `kiosk`) | Output | Showing | -|---|---|---|---| -| host101 | `kiosk-host101` | card0-DP-1 (4K) | kiosk.vino.network | -| host102 | `kiosk-host102` | card0-DP-1 (4K) | kiosk.vino.network | -Each node drives one screen; multi-screen would need host100's GT 730 (see KIOSK-DESIGN.md). - - - -Design target = **3840×2160 (4K UHD), devicePixelRatio 1, 24-bit color, -landscape-primary**. Both displays render at native 4K; the Chromium kiosk -has no toolbar/taskbar, so viewport == screen == avail.screen == 3840×2160 -(no CSS px scaling — 1 CSS px = 1 device px). Layout for the kiosk page should -look right at full 4K landscape. (Confirmed on-screen 2026-06-01.) - - - -The default page both screens show is `https://kiosk.vino.network` — an -editorial wall-art homepage: a **live iOS-Weather-style animated sky** + -live local-time clock + current weather from `api.open-meteo.com`. The -sky is driven by the Open-Meteo `weather_code` (+ `wind_speed`) + -`sunrise`/`sunset`: sun (clear/partly days) and moon (clear/partly nights) -**arc across the sky by real time-of-day**, with a warm dawn/dusk horizon -glow. Conditions rendered: clear, partly, cloudy, fog, drizzle, rain, -**sleet/freezing-rain (ice)**, snow (**blizzard/blowing-snow when wind -≥24 mph**), thunderstorm (**hail on codes 96/99**), plus stars on clear -nights and lightning in storms. The **moon mirrors its -actual phase** (lit fraction + waxing/waning terminator, computed locally -from the date — NOT an extra API call). Weather fetches on load then every -10m, with **fast retry-with-backoff (15s→5m) while failing** so a fetch that -fails at pod start doesn't sit on "Weather unavailable" for the full 10m -(fixed 2026-06-04; live weather scene 2026-06-04). A frozen clock/stale weather on one screen = -that node's Chromium (or the whole node) is wedged, NOT a page bug — the -display holds the last-good frame; recover by graceful pod restart (and see -fritzlab `msp001.md` if the node itself is unreachable). -A **live** clock but "Weather unavailable" + a stale day/night scene (night -shown in daytime) is the opposite case — NOT Chromium: the pod can't reach -`api.open-meteo.com`, so `wx()` fails and `SUN` (sunrise/sunset) freezes at -its last-good values → wrong palette. Root cause is usually msp001 pod -egress/DNS loss from a UDM BGP wipe (now auto-healed) — see fritzlab -`msp001.md` . Confirm with `kubectl --context msp001 exec -n kiosk - -- curl -sm10 https://api.open-meteo.com/v1/forecast?...` from the pod. -Source repo: `websites/kiosk.vino.network` (clone at `~/code/git/code.fritzlab.net/websites/kiosk.vino.network/`), -content in `html/`, served from Garage S3 via the standard site-publish flow -(fritzlab `gitops.md` ). - -Location + house name are **query params on `KIOSK_URL`**, so -one page serves any household: `?zip=&name=`. `name` sets -the eyebrow + tab title; `zip` is geocoded once at load (Zippopotam.us) → lat/lon -+ "City, State" place line, and Open-Meteo runs `timezone=auto` so the clock -follows that zip's timezone. No params → Hawks Nest / Minnetrista 55331 / -`America/Chicago` defaults (bad zip falls back to these too). To point a screen -at a different house, set `KIOSK_URL=https://kiosk.vino.network/?zip=...&name=...` -per pod via GitOps (see for the apps-repo edit + hard-refresh). - -**"Update the kiosk page" = edit the page content**, NOT the KIOSK_URL: edit -`html/index.html` (or `404.html`) in that repo, commit + push to main → the -`Publish` Gitea Action (site-publish) syncs to the bucket in ~30s. Verify with -`curl -s https://kiosk.vino.network/`. - -**How screens actually pick up a new deploy:** the Chromium kiosk fetches the -URL ONCE at pod start and never re-polls on its own (`entrypoint.sh` only -re-fetches if cage/chromium exits or the pod restarts) — `Cache-Control: -must-revalidate` alone does NOT update a running screen. So the PAGE must -self-reload. `index.html` polls its own ETag every 5m and `location.reload()`s -only when it changes (no periodic flashing). Any new kiosk page MUST carry that -ETag-poll snippet, else the screens stay on the old content until a pod restart. - - - -This changes WHICH URL a screen loads (different site), distinct from updating -the kiosk page content above. The displayed URL is the `KIOSK_URL` env per pod. -**GitOps (only reliable way):** edit `KIOSK_URL` in `fritzlab/apps/msp001/kiosk/kiosk/manifests/deployments.yaml` (block `kiosk-host101` or `kiosk-host102`), commit + push to `apps` main, then force the sync now instead of waiting ~3min for the poll: `kubectl --context msp001 -n argocd annotate application kiosk --overwrite argocd.argoproj.io/refresh=hard`. Pod recreates on the new page. -**Do NOT use `kubectl set env` for this:** ArgoCD selfHeal reverts it to the *stale* git target (the value from the last commit it polled, not your new one) within seconds — the pod recreates but on the old URL. Push to git + hard-refresh instead. (Verified 2026-06-01.) - - -`kubectl --context msp001 get pods -n kiosk` (want 1/1 Running). On-screen page check needs a screenshot via Chromium CDP (localhost-only) — see fritzlab `msp001.md` . Blank-screen causes: (1) cage logs `Swapchain for output ... failed test` on repeat → dirty DRM/GBM state from an UNCLEAN prior exit; (2) `getty@tty1` re-took DRM master (re-mask on the node); (3) page down (Chromium holds last-good frame). - -**Restart kiosk pods GRACEFULLY — never `--force`/`--grace-period=0`.** A hard kill stops cage from dropping DRM master, leaving the next cage with a dirty swapchain → black. Use `kubectl --context msp001 delete pod -n kiosk -l node=host101` (or `rollout restart deploy -n kiosk`). The image self-heals a wedged swapchain in-pod (entrypoint kills+relaunches cage after 5 swapchain failures) and a `tcpSocket:9222` liveness probe restarts a dead Chromium — but a clean shutdown is still the first line of defense. Recovery for an already-black screen = one graceful delete. (Diagnosed + hardened 2026-06-01.) - -Want a dashboard, photo slideshow, or Home Assistant wall panel instead of a website — just point `KIOSK_URL` at it (e.g. `https://home.vino.network`). [[home-assistant]] for HA URLs. diff --git a/reference/mealie.md b/reference/mealie.md deleted file mode 100644 index 58ccef6..0000000 --- a/reference/mealie.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -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. -