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.
-