# Changelog
All notable changes to CookTrace are documented here.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
## [Unreleased]
---
## [1.4.0] - 2026-10-02
CookTrace on your wrist, offline mode for a shop with no signal, kitchen roles for a shared household library, cooking more than one dish at once, preliminary foldable support, and Spanish.
### Added
- **CookTrace on your wrist.** A Wear OS app for watches on Wear OS 3 and up. Your shopping list by aisle, ticked off with a trolley in one hand, working with no signal. Press Cook on the phone and the recipe arrives as two checklists, ingredients and steps, and a step that says "simmer for 20 minutes" offers that timer. Several timers run at once and each buzzes on your wrist, "I cooked this" writes straight into your cook diary, and a tile and a watch face complication show what is left to buy or the timer running. Either device can tick something off and the other follows. [Wear OS](https://traceapps.github.io/docs/cooktrace/wear/).
- **Offline mode.** Built for a supermarket with no signal: the shopping list opens, and you can add, tick, edit, remove and clear. Recipes, cookbooks, the pantry and the diary read from what the app has already seen, pictures included, and the installed app opens with no connection at all. The pantry, the cook diary, your recipes and their notes, settings, your profile, and photos of what you cooked, a recipe or a pantry item can all change offline, and go up on their own when the connection returns, with an amber cloud on the menu button while anything is waiting. Importing, sharing, kitchens, editing a cookbook, Trace and anything admin need a connection and say so; a change your server refuses is set aside and named in plain words. [Cook and shop without a connection](https://traceapps.github.io/docs/cooktrace/features/#offline).
- **Kitchen roles, so a household can share one recipe library without sharing the keys to the server** ([#52](https://github.com/TraceApps/cooktrace/issues/52)). The Head Chef gives each member a role: a Sous Chef edits what is shared into the Kitchen, a Line Cook cooks from it. Deleting, sharing onward, visibility, category, rating and favorites stay with the recipe's owner. A Kitchen can also be handed to another member, the way out of the old "owner cannot leave" dead end. Thanks to @herver1971 for the idea and the first implementation in [#53](https://github.com/TraceApps/cooktrace/pull/53). [Kitchens](https://traceapps.github.io/docs/cooktrace/kitchens/).
- **Cook more than one thing at once.** Start a second recipe while the first is still going, and a slim bar shows what is on the go from anywhere in the app: one tap goes back to the dish, or picks from several. Timers change color as they run down, and one that goes off names the dish it belongs to.
- **Preliminary foldable support.** Half open like a book, the crease becomes a divider rather than something content sits across:
- A recipe opens like a cookbook: the ingredients on the page left of the fold, the method on the right.
- Settings puts its section list on one side and the section itself on the other.
- Shopping deals its aisles into columns, and the pantry spotlight shows when there is room for it.
- The cook diary keeps its toolbar under the header, and a year of cooking fits the width.
- Manage splits its rail and its grid on the room available.
- Dialogs, sheets, running timers and Trace keep off the crease, and a menu opened near it takes the roomier side.
- In laptop posture Trace sits on the half lying flat, leaving the recipe readable on the half standing up.
- Recipes, lists and photos still cross the fold freely, since an opened foldable is a bigger screen to read on.
- The same two-column layouts appear on a tablet or any wide window, not only on a desktop-sized screen.
- Diagnostics reports what the hinge is doing, so you can tell whether your phone reports one at all.
- **Spanish.** Pick it in Settings → Regional & Units, or it follows your browser or phone. Translated by @herver1971 on Weblate. Thank you!
- **Settings has a Support page**, next to About: Ko-fi and GitHub Sponsors, plus free ways to help (star the repo, report a bug, translate).
### Changed
- **Search ignores accents.** "oregano" finds "Orégano" and "cafe" finds "Café", in every search box, the REST API and the MCP tools, on names and brands alike. Text without accents matches exactly as before. Thanks to @herver1971 for the pantry fix in [#62](https://github.com/TraceApps/cooktrace/pull/62) that started it.
- **Update checks are off until you turn them on, and your server does the asking.** Every browser and phone used to ask GitHub directly every 4 hours. Setup now asks, and a fresh install contacts nothing on its own. Existing installs keep checking as before, and `UPDATE_CHECK=off` keeps them off for good. Reported on r/selfhosted.
- **Fonts are served by your own instance.** The app loaded its fonts from Google on every page load, so Google saw the address of everyone who opened it. Reported on r/selfhosted.
- **The in-app updater reuses an update it already downloaded.** Coming back to Updates goes straight to installing, and the button says Install. Older downloads are cleared so they stop piling up on the phone.
- **About links to the TraceApps family** instead of naming the other apps.
- **A connection problem says what kind it was.** The diagnostic log records a timeout, an address that would not resolve, a refused connection or a rejected certificate, and how long it waited, where all of them used to read "Failed to fetch".
### Fixed
- **The Docker image starts on virtual machines whose CPU cannot run the PDF reader** ([#59](https://github.com/TraceApps/cooktrace/issues/59), thanks @bajtekv). It stopped with "Illegal instruction" before the server came up. Only PDF import is affected on such a machine now, and it says so.
- **The check on a pantry item can be unticked again** ([#55](https://github.com/TraceApps/cooktrace/issues/55)). Ticking it, or restocking from the shopping list, puts 1 in On Hand, unticking sets 0, and the card, the button and the item's sheet agree. Thanks to @herver1971 for the report and the fix in [#56](https://github.com/TraceApps/cooktrace/pull/56).
- **A cookbook cover uploads from Manage, and so does a recipe video** ([#63](https://github.com/TraceApps/cooktrace/issues/63), thanks @herver1971). Both said "Upload failed" and saved nothing, so a smart cookbook could not get a cover at all.
- **The All search shows your own pantry.** With the All chip, or several sources pinned, the pantry part of the results was always empty. Your items appear badged as yours, and tapping one opens it.
- **The cook dashboard no longer disappears after one failed request** at launch; the heatmap shows on its own data and the stats are retried.
- **The shopping list's Add button fits on a phone** instead of running off the right edge.
- **A file of the wrong type, or over the size limit, is turned away with a clear message** instead of a server error. A file that is too large says what the limit is.
- **"A New Version Is Available" on the web says what it means, and Reload works.** A tab left open also notices a new version now.
- **The Trace button no longer covers sheets and dialogs** opened on top of it.
- **Settings on desktop:** pages line up with the section list beside them, and the list keeps its place when you open a section instead of scrolling back to Profile.
### Security
- **multer** bumped 2.3.0 to 2.4.0, closes [GHSA-3pph-fpjx-jg34](https://github.com/advisories/GHSA-3pph-fpjx-jg34) (moderate: an upload cut off at just the wrong moment could leave its file on disk). Both routes that write uploads to disk require signing in.
- **undici** bumped 6.28.0 to 6.29.0, and the copy cheerio uses 7.29.0 to 7.30.0, closes [GHSA-3wwx-pv8p-q78v](https://github.com/advisories/GHSA-3wwx-pv8p-q78v) (moderate: a WebSocket server could crash the process). CookTrace opens no WebSocket connections.
- **nodemailer** bumped 9.1.1 to 10.0.12, closes [GHSA-6vj9-mwq6-2f5v](https://github.com/advisories/GHSA-6vj9-mwq6-2f5v) (moderate: separate mail transports could share one TLS server name).
- **fast-uri** bumped 3.1.7 to 3.1.8, closes [GHSA-hrr3-gc8f-f4qj](https://github.com/advisories/GHSA-hrr3-gc8f-f4qj) (moderate: a host written with percent-encoded letters was not normalized the same way). It only comes in through the MCP server, which is off by default.
- **ip-address** bumped 10.7.0 to 10.7.3, closes [GHSA-j6r3-76f7-8jcv](https://github.com/advisories/GHSA-j6r3-76f7-8jcv) and [GHSA-h3mg-xc3c-68pw](https://github.com/advisories/GHSA-h3mg-xc3c-68pw) (moderate: subnet checks across IPv4 and IPv6, and unbounded work on a long IPv6 address). It only comes in through the MCP server's rate limiter.
- **brace-expansion** bumped 5.0.9 to 5.0.12 and 2.1.4 to 2.1.7, closes [GHSA-q2hr-2g5m-vwhr](https://github.com/advisories/GHSA-q2hr-2g5m-vwhr) (moderate: slow expansion of a crafted pattern). Build tooling only; neither the app nor the server uses it.
- `npm audit` reports 0 vulnerabilities for the app and the server.
---
## [1.4.0-dev04] - 2026-09-29 (pre-release)
A dev pre-release of the 1.4.0 minor. Spanish, a Support page in Settings, uploads that say what went wrong, and security updates.
### Added
- **Spanish.** CookTrace can be used in Spanish: pick it in Settings → Regional & Units, or it follows your browser or phone. Translated almost in full by @herver1971 on Weblate. Thank you!
- **Settings has a Support page**, next to About: Ko-fi and GitHub Sponsors, plus free ways to help (star the repo, report a bug, translate). It replaces the support row that used to sit in About.
### Changed
- **The in-app updater reuses an update it already downloaded.** Coming back to Updates goes straight to installing instead of downloading the whole APK again, and the button says Install. Older downloads are cleared so they stop piling up on the phone.
- **About links to the TraceApps family** instead of naming the other apps, a list that had already gone out of date.
### Fixed
- **A file of the wrong type, or over the size limit, is turned away with a clear message.** Uploading one answered with a server error instead of saying what was wrong. A file that is too large now says what the limit is.
- **A cookbook cover uploads from Manage, and so does a recipe video.** Both said "Upload failed" and saved nothing, although the file had already reached your server. Manage → Cookbooks is the only place a smart cookbook gets its cover, so smart cookbooks could not have one at all. Thanks to @herver1971 for the report and the diagnosis in [#63](https://github.com/TraceApps/cooktrace/issues/63).
- **A cookbook cover picked with no connection is saved as a picture file** once you are back online, not stored whole inside the cookbook, where it made every list of cookbooks heavier to load. Updating a cookbook now checks its cover the way creating one always did.
- **The shopping list's Add button fits on a phone.** On a phone-width screen the button beside the item picker ran off the right edge, leaving only "+ A" showing.
- **Settings pages line up with the section list** on desktop and foldables. Most pages started 12px below the list beside it.
- **The Settings section list keeps its place** on desktop and foldables. Every click in it scrolled the list back to Profile.
### Security
- **multer** bumped 2.3.0 to 2.4.0, closes [GHSA-3pph-fpjx-jg34](https://github.com/advisories/GHSA-3pph-fpjx-jg34) (moderate: an upload cut off at just the wrong moment could leave its file behind on disk). Both routes that write uploads to disk require signing in.
- **undici** bumped 6.28.0 to 6.29.0, and the copy cheerio uses 7.29.0 to 7.30.0, closes [GHSA-3wwx-pv8p-q78v](https://github.com/advisories/GHSA-3wwx-pv8p-q78v) (moderate: a WebSocket server could crash the process). CookTrace opens no WebSocket connections.
- **nodemailer** bumped 9.1.1 to 10.0.12 on the server, closes [GHSA-6vj9-mwq6-2f5v](https://github.com/advisories/GHSA-6vj9-mwq6-2f5v) (moderate: separate mail transports could share one TLS server name). Removed from the web app's own dependencies, where nothing used it.
- `npm audit` reports 0 vulnerabilities for the app and the server.
---
## [1.4.0-dev03] - 2026-09-27 (pre-release)
A dev pre-release of the 1.4.0 minor. A first pass at foldables, and search that no longer cares about accents.
### Added
- **Preliminary foldable support.** Half open like a book, the crease becomes a divider rather than something content sits across:
- A recipe opens like a cookbook: the ingredients on the page left of the fold, the method on the right.
- Settings puts its section list on one side and the section itself on the other.
- Shopping deals its aisles into columns, and the pantry spotlight shows when there is room for it.
- The cook diary keeps its toolbar under the header, and a year of cooking fits the width.
- Manage splits its rail and its grid on the room available.
- Dialogs, sheets, running timers and Trace keep off the crease, and a menu opened near it takes the roomier side rather than being cut in half by the hinge.
- In laptop posture Trace sits on the half lying flat, leaving the recipe readable on the half standing up.
- Recipes, lists and photos still cross the fold freely, since an opened foldable is a bigger screen to read on.
- Diagnostics reports what the hinge is doing, so you can tell whether your phone reports one at all.
### Fixed
- **Search ignores accents.** Typing "oregano" finds "Orégano", "cafe" finds "Café", "limon" finds "Limón". This holds for the pantry, recipes, cookbooks, the cook diary, tags, units, the Manage lists, Settings, Trace, the public API and the MCP tools, on names and brands alike. A kitchen kept in Spanish, Portuguese, French or another language with accents no longer looks like the item is missing because nobody types the accent on a phone. Text without accents matches exactly as before. Thanks to @herver1971 for the report and the pantry fix in [#62](https://github.com/TraceApps/cooktrace/pull/62).
- **The All search shows your own pantry again.** With the All chip, or several sources pinned, the pantry part of the results was always empty. Your items appear alongside the OFF, USDA and NutriTrace results now, badged as yours, and tapping one opens that item instead of starting a duplicate of it.
- **The cook dashboard no longer disappears after one failed stats call.** A single failed request at launch hid the heatmap and the summary tiles for the whole session, with no retry. The heatmap shows on its own data now, and the stats call retries once.
- **A connection problem says what kind it was.** When the app cannot reach your server, the diagnostic log records the kind of failure and how long it waited. A timeout, an address that would not resolve, a refused connection and a rejected certificate all used to read "Failed to fetch".
### Security
- No dependency changes. `npm audit --omit=dev` reports 0 vulnerabilities for the app and the server.
---
## [1.4.0-dev02] - 2026-09-25 (pre-release)
Second dev pre-release of the 1.4.0 minor. Three fixes, each from a real
install: the image not starting on some virtual machines, the in-app
updater offering the watch build to phones, and the pantry stock check
that could not be unticked.
### Fixed
- **The Docker image would not start on some virtual machines**, stopping with "Illegal instruction" before the server came up ([#59](https://github.com/TraceApps/cooktrace/issues/59), thanks @bajtekv). The PDF reader used for recipe imports loads a graphics library whose prebuilt binary needs CPU instructions that some virtual CPUs, including QEMU's default, do not provide. Loading it killed the whole process at startup, and that kind of crash cannot be caught. PDFs are now read in a separate short-lived process, so the server starts and runs normally everywhere, and on a machine whose CPU cannot run the reader only PDF import is affected, with a message saying so rather than a dead container. A PDF that takes more than a minute is given up on instead of holding the request open.
- **The in-app updater can no longer hand a phone the watch build.** A release carries both APKs, and they share a package id so the watch app installs straight over the phone one. The updater took whichever `.apk` the release listed first, which is upload order and no promise at all. It now picks the phone's build by name, and offers nothing at all rather than a watch build.
- **The check on a pantry item can be unticked again, and ticking it puts 1 in On Hand.** Tapping + on an item's photo marked it in stock but left On Hand blank, and from then on the check could not be unticked: every tap marked it in stock again, and the only way out was opening the item and typing 0. The item's own sheet also showed it as Out of Stock while the card showed it checked. Ticking now sets On Hand to 1 (or keeps the number that was there), unticking sets it to 0, and the card, the button and the sheet agree. [#55](https://github.com/TraceApps/cooktrace/issues/55)
---
## [1.4.0-dev01] - 2026-09-22 (pre-release)
First dev pre-release of the 1.4.0 minor. Two headlines: CookTrace on a watch, with your shopping list and a recipe you can cook from, and offline mode, so a supermarket with no signal stops being a problem. Also in: kitchen roles, so a household can share a recipe library without sharing the keys to the server, update checks that are off until you ask for them, and fonts served by your own instance instead of Google.
### Added
- **Kitchen roles, so a household can share one recipe library without sharing the keys to the server** ([#52](https://github.com/TraceApps/cooktrace/issues/52)). A recipe shared into a Kitchen used to be read-only for everyone but its owner, so fixing a quantity meant asking them or making everyone an admin. Each member now has a role its Head Chef sets: a Sous Chef edits what is shared into the Kitchen, a Line Cook cooks from it. Deleting, sharing onward, visibility, category, rating and favourites stay with the owner. A Kitchen can also be handed to another member, which is the way out of the old "owner cannot leave" dead end. Thanks to @herver1971 for the idea and the first implementation in [#53](https://github.com/TraceApps/cooktrace/pull/53). [Kitchens](https://traceapps.github.io/docs/cooktrace/kitchens/).
- **CookTrace on your wrist.** A Wear OS app for watches running Wear OS 3 and up. The shopping list is its home: your list by aisle, ticked off with a trolley in one hand, working in a shop with no signal. Press Cook on the phone and the recipe arrives on the watch as two checklists, ingredients and steps, and a step that says "simmer for 20 minutes" offers that timer rather than making you dial it. Several timers run at once and each buzzes on your wrist. "I cooked this" writes straight into your cook diary. Either device can tick something off and the other follows. [Wear OS](https://traceapps.github.io/docs/cooktrace/wear/).
- **Offline mode** ([#211](https://github.com/TraceApps/nutritrace/issues/211) in NutriTrace, the same idea here). A supermarket with no signal is the case this is built for: your shopping list opens, you can add, tick, edit, remove and clear, and it is all there when you come back into range. Recipes, cookbooks, the pantry and the diary read from what this browser has already seen, pictures included, so you can cook from a recipe with no signal. What you change goes up on its own when the connection returns, as the very requests the app would have made. The menu button shows an amber cloud while anything is waiting. [Cook and shop without a connection](https://traceapps.github.io/docs/cooktrace/features/#offline).
- **What else works in offline mode.** The pantry (adding, editing, removing, in or out of stock), the cook diary, your recipes and their notes, settings, your profile and picture, and photos everywhere you can attach one: what you cooked, a recipe's own photo, a cookbook cover and a pantry item.
- **Offline mode says what it cannot reach.** Importing recipes, sharing a recipe or cookbook, kitchens and their members, Trace and anything admin need a connection. A change your server refuses is set aside and named in plain words, everything else still goes up, and the reason is written to the diagnostics log, while a server that is merely busy is retried instead.
### Changed
- **Update checks are off until you turn them on, and your server does the asking.** Every browser and phone used to ask GitHub directly every 4 hours. Setup now asks, skipping the question leaves checks off, and a fresh install contacts nothing on its own. Existing installs keep checking as before. `UPDATE_CHECK=off` keeps them off for good. Reported on r/selfhosted.
### Fixed
- **A list you have already opened is still there offline after your changes go up.** When queued work reached your server, the app dropped its copy of the lists that change touched so they would be read again, but nothing reads a screen you do not open. Come back offline without opening it and your shopping list said it needed a connection, empty. Those lists are read back the moment the change goes up now.
- **A browser low on room keeps your shopping list.** Making space for something you changed cleared the whole copy at once, taking the list you were standing in the shop with. It now gives up the oldest half first, and only clears everything if that is still not enough. What the browser keeps also counts a read as recent use, so the list you look at is the last thing dropped rather than the first.
- **"A New Version Is Available" on the web now says what it means, and Reload works.** The browser banner used the Android wording and its Reload button could do nothing at all, and a tab left open never noticed a new version. Same fix in all four Trace apps.
- **Ticking things off on the phone no longer wakes the watch once per tap.** Each change was sent to the watch on its own, so a shop run kept the radio busy; they go in one batch now.
- **The Trace button no longer covers what's on top of it.** It floated above every sheet and dialog, so wherever you had dragged it, it could sit over a title or a button. Same fix as NutriTrace [#233](https://github.com/TraceApps/nutritrace/issues/233).
- **The installed app survives a reload with no connection.** It kept only a fallback page, so its own code came from the network and reopening it in a dead zone left a blank screen.
- **Something deleted while online stays deleted when the connection goes.** An older copy of the list could be carried over, so what you removed came back the moment you were offline. Reported in testing.
- **A photo kept offline is scaled to something a request comfortably carries**, so your server never turns it away after you have been told it was saved.
- **Adding a photo with no connection no longer fails on an installed app.** The part of the app that keeps a photo was fetched from your server at the exact moment there was nothing to fetch from.
- **A change made with no connection is answered in the shape that screen expects**, so nothing looks like it failed when it was saved and waiting.
- **A picture kept offline holds its transparency, and an unusual camera format is converted rather than lost.** A drawing could come back with a black background, and an iPhone's HEIC would have been refused on arrival without saying so.
- **The copy this browser keeps now has a ceiling**, and if storage runs out, what you have changed is kept and the copy makes way for it.
- **A sync no longer empties the copy this browser keeps.** Everything was cleared once the queue went up, so losing signal again left you with nothing to look at.
- **Work changed while a sync was running no longer waits for you to do something else** before it goes up.
- **What a row created offline became is now remembered on disk**, not just while the page stays open, so a sync that stopped halfway can't leave work queued against an id your server never had.
- **Fonts are served by your own instance.** The app loaded Inter and the icon font from Google on every page load, so Google saw the address of everyone who opened it, whatever your settings said. Reported on r/selfhosted.
### Security
- No security fixes this cycle. `npm audit` reports 0 vulnerabilities for the app and the server, and there are no open Dependabot alerts. The privacy changes above (fonts, update checks) came out of a review on r/selfhosted.
---
## [1.3.0] - 2026-09-20
Minor release. Big themes: CookTrace opens up to other software (a Model
Context Protocol server, personal access tokens, outgoing webhooks, a
public REST API, and NutriTrace federation in both directions), a
smarter shopping list, and a batch of Android fixes.
**Action needed when you update: the container now listens on port 3003
instead of 3001.** If your compose file has `"3003:3001"`, change it to
`"3003:3003"`; if a reverse proxy or tunnel reaches the container
directly (`cooktrace:3001`, or a Traefik `loadbalancer.server.port=3001`
label), point it at `3003`. Until you do, CookTrace won't respond after
the update. Installs that set `PORT` themselves are not affected, and
the host port stays 3003, so bookmarks and the Android app's server
address keep working.
### Changed
- **The container now listens on port 3003, the same as the host port. Action needed when you update.** The image used to listen on 3001 inside the container while the sample compose file published it on 3003, so the two numbers never matched, and 3001 was also NutriTrace's port. Both are 3003 now. See the note above for what to change. The weekly summary email's Open CookTrace button, used when no app URL is set, pointed at `localhost:3000` and now points at `localhost:3003`, and running from source starts the server on `:3003`, so it no longer collides with a NutriTrace checkout on the same machine.
- **Trace settings now match NutriTrace.** The Base URL and API Key fields save when you leave them (or press Enter) instead of through a Save button beside each field, which on a phone in portrait sat past the edge of the screen; the connection is only re-tested when the value changed. On a server where AI is configured through environment variables, the section says so, shows the provider and model the server actually uses, and hides the base URL and API key fields. Smart Log gains a Voice Input Language setting for when you speak a different language than your device is set to.
- **The shopping list fills the screen on desktop.** On wide screens the aisle and recipe cards sat in rows as tall as their tallest card, leaving empty space under the short groups. Short groups now stack into that space, and the cards re-pack as groups collapse, items are checked off, or the window is resized. Phones and Flat view are unchanged.
- **Food Sources settings reorganized.** NutriTrace Federation now lives inside Settings, Food Sources under its own "NutriTrace" sub-heading (between USDA and Barcode Scanner) instead of a separate top-level section, since federation is currently used purely as another food source.
- **Sync status pill in the sidebar**, with one colour rule for sync state across the app.
- **Claude Fable 5.1 in Trace's model list.** It is now the most capable Claude option; Fable 5 stays selectable, marked as previous.
### Added
- **Model Context Protocol (MCP) server.** CookTrace exposes a read + write + destructive MCP endpoint at `/api/mcp` so Claude Desktop, Cursor, Codex, and other MCP-aware agents can search recipes, browse the pantry and shopping list, log a cook, and (with the right scope) create recipes or pantry items. Fourteen tools across three independently-gated tiers (`mcp:read` / `mcp:write` / `mcp:destroy`), each requiring both a server-side env flag and a matching token scope. Off by default. See [docs/cooktrace/mcp.md](https://traceapps.github.io/docs/cooktrace/mcp/).
- **Personal access tokens.** New Settings, API Tokens section (admin, multi-user mode) to mint, scope, and revoke tokens. Shared by MCP, the public API, and federation.
- **Outgoing webhooks.** Configure a target URL in Settings, Webhooks and CookTrace fires a signed HTTP POST the instant a recipe is logged as cooked, the shopping list is fully checked off, or a pantry item runs out of stock. Off by default (`WEBHOOKS_ENABLED=1`). HMAC-SHA256 signed, 3 delivery attempts with backoff, and a "send test event" button. Target URLs are validated against an SSRF guard. See [docs/cooktrace/webhooks.md](https://traceapps.github.io/docs/cooktrace/webhooks/).
- **General-purpose public REST API** at `/api/v1/cook-diary`, `/api/v1/shopping`, and a pantry stock write route, for your own scripts and automations. Off by default (`PUBLIC_API_ENABLED=1`; `PUBLIC_API_WRITE_ENABLED=1` unlocks the writes). Reuses the `mcp:read`/`mcp:write` token scopes. See [docs/cooktrace/public-api.md](https://traceapps.github.io/docs/cooktrace/public-api/).
- **NutriTrace federation, both directions.** NutriTrace can now pull your CookTrace recipes (`GET /api/v1/recipes`, with per-ingredient nutrition resolved through variant/generic inheritance) and your pantry (`GET /api/v1/pantry`, leaf rows shaped for NT's foods library, with search and paging). Gated by new `read:recipes` and `read:pantry` token scopes, independent of each other and of MCP.
- **NutriTrace joins the Pantry search chips.** A connected NutriTrace instance's food catalog is a fourth search source alongside Open Food Facts and USDA when adding a pantry item, picked the same way (chip, long-press to pin, included in "All" mode).
- **Shopping API for sister apps.** A `shopping` token scope covering list, add, check, and clear, always on and reaching nothing but the shopping list. It is what NoteTrace uses to send a checklist's items across and show the list back.
- **The shopping list combines duplicate items.** In By Aisle and Flat views, items with the same name and unit show as one row with the amounts added up and a pill for each recipe they came from. Checking, removing, dragging or re-aisling that row applies to every copy behind it. Different units stay separate, and a copy with no amount makes the row show none rather than a wrong total. By Recipe view still lists each recipe's own items, and nothing changed in the database, sync, or the Android app's storage.
- **Clear Checked can restock your pantry.** The confirmation lists the matching pantry items that are currently out of stock, ticked by default, so what you bought goes back in stock without a second trip through the Pantry tab. An item is only offered when the row is linked to a pantry item or its name matches exactly one; a generic such as Milk gets a dropdown for which variant you bought; the toast has an Undo.
- **Support the iOS fund.** The README and Settings, About name what the fund covers.
### Fixed
- **The shopping list froze mid-drag.** Reordering threw an `each_key_duplicate` error and left the page unresponsive, because the drag library renames its placeholder to the dragged row's own id one frame after a drag starts, so a fast pointer move could hand Svelte the same id twice.
- **Shopping search crashed when two pantry items shared a name.** Suggestions are deduplicated by name inside the shared picker, which also protects the seven other screens that use it (recipe category, tags, tools, pantry category, cookbook tags, Kitchens invite).
- **Cook history and comments returned 403 on a recipe shared with you.** Both reads checked only ownership or group visibility and ignored the kitchen share that granted access to the recipe itself.
- **Marking a pantry item back in stock left its quantity at 0**, so it still read as out of stock and couldn't be toggled out again. An explicit `null` quantity now clears the stored value instead of being treated as "leave unchanged".
- **Pantry "Expiring Soon" showed already-expired items as still counting down** on wide screens (for example "63d past" instead of "Expired"). The ribbon lists both soon-to-expire and already-expired items, so it is now titled "Expiring & Expired".
- **An expanded generic pantry item's photo ballooned to a huge size on desktop** when it had variants.
- **Android: the back button closes what's open first.** With a sheet, dialog, menu or photo viewer open, back left the page underneath with it still showing. Back now closes the newest sheet, dialog, menu, picker, photo viewer or Trace chat first, one at a time, then the sidebar, then goes back a page.
- **Android: dragging the Trace button or a reorder handle no longer refreshes the page.** Dragging those while scrolled to the top was treated as pull-to-refresh. Pulling down anywhere else still refreshes.
- **Android: sheets and dialogs stay below the status bar**, including with the keyboard up, where the JSON import dialog's buttons could sit off the top of the screen. Same fix as NutriTrace [#228](https://github.com/TraceApps/nutritrace/issues/228).
- **Error and delete colours come from the theme.** Everything used an `--error` token that was never defined, so it fell back to two different hardcoded reds and ignored the light theme. They all use the real `--danger` token now.
### Security
- **Comments could be posted to any recipe by ID.** `POST /api/recipes/:id/comments` had no ownership, share, or visibility check at all; it now requires the same access reading comments does.
- **Backup archives are no longer reachable from the public uploads directory.** `BACKUPS_PATH` defaults to a directory inside `UPLOADS_PATH`, and `/uploads` is served ahead of the auth middleware so an Android WebView `` can load images without an `Authorization` header. A full-backup ZIP sitting there was fetchable by URL, while every `/api/full-backup` route is admin-only. It now returns 404. Scheduled backups are off by default, so an install that never enabled them and never created one by hand had nothing there to reach, and there is no directory listing, so a filename had to be known or guessed. The archive holds a full database dump, so if yours has been internet-facing with backups enabled, a look through your access log for `/uploads/backups/` will settle it either way. A custom `BACKUPS_PATH` elsewhere inside the uploads directory is covered too.
- **Uploads** are served from a sandboxed set of safe extensions.
- **multer** bumped 2.2.0 to 2.3.0, closes a HIGH advisory (denial of service via aborted uploads holding file handles open).
- **nodemailer** bumped 9.0.3 to 9.1.1 (root and server), closes a moderate advisory (recipient-header validation bypass).
- **adm-zip** bumped 0.6.0 to 0.6.1, closes the symlink-extraction advisory.
- **devalue** bumped 5.8.1 to 5.9.4, closes [GHSA-9rgm-9g3h-6x36](https://github.com/advisories/GHSA-9rgm-9g3h-6x36) (denial of service via malformed input, moderate).
- `npm audit` reports 0 vulnerabilities for the app and the server.
---
## [1.3.0-dev.03] - 2026-09-19 (pre-release)
Third dev pre-release of the 1.3.0 minor. **Action needed when you
update:** the container now listens on port 3003 instead of 3001 (see
Changed). Also Android fixes for the back button, pull-to-refresh and
dialog placement, webhooks for changes made in the Android app, and a
desktop shopping layout that fills the screen.
### Changed
- **The container now listens on port 3003, the same as the host port. Action needed when you update.** The image used to listen on 3001 inside the container while the sample compose file published it on 3003, so the two numbers never matched, and 3001 was also NutriTrace's port. Both are 3003 now. If your compose file has `"3003:3001"`, change it to `"3003:3003"`; if a reverse proxy or tunnel reaches the container directly (`cooktrace:3001`, or a Traefik `loadbalancer.server.port=3001` label), point it at `3003`. Until you do, CookTrace won't respond after the update. Installs that set `PORT` themselves are not affected, and the host port stays 3003, so bookmarks and the Android app's server address keep working. The weekly summary email's Open CookTrace button, used when no app URL is set, pointed at `localhost:3000` and now points at `localhost:3003`, and running from source starts the server on `:3003`, so it no longer collides with a NutriTrace checkout on the same machine.
- **The shopping list fills the screen on desktop.** On wide screens the aisle and recipe cards sat in rows as tall as their tallest card, leaving empty space under the short groups. Short groups now stack into that space, so more of the list fits without scrolling, and the cards re-pack as groups collapse, items are checked off, or the window is resized. Phones and Flat view are unchanged.
### Fixed
- **Dragging the Trace button or a reorder handle no longer refreshes the page.** In the Android app connected to a server, dragging the Trace button, a shopping list or recipe ingredient handle, or a cookbook card downward while the page was scrolled to the top was treated as pull-to-refresh and synced. Dragging those no longer counts as a pull; pulling down anywhere else still refreshes as before.
- **The Android back button closes what's open first.** Back only knew how to go back a page, so with a sheet, dialog, menu or photo viewer open it left the page underneath with it still showing. Back now closes the newest sheet, dialog, menu, picker, photo viewer or Trace chat first, one at a time, the same as its own close button (a dialog closes as Cancel, and the camera stops). The sync merge questions still need an answer, so back leaves them open. It also closes the slide-out sidebar if that's showing. With nothing open, back goes back a page and then offers to exit, as before.
- **Sheets and dialogs stay below the status bar.** The JSON import dialog, with the keyboard up, started above the top of the screen, so its close button and its Import button couldn't be reached; on a tall phone it reached under the status bar even without the keyboard. The same could happen to the shopping list and Cook Diary dialogs, Log a Cook, the photo viewer, the cookbook dialogs, the other import dialogs and the shared sheet used across the app. The Android app draws under the status bar, and these were capped only at a share of the screen, so one that filled its cap (a tall one, or any with the keyboard up) could start under the status bar. They now always stop below it and scroll their content instead. Nothing changes where there's room, or on a computer. Same fix as NutriTrace [#228](https://github.com/TraceApps/nutritrace/issues/228).
- **Webhooks never fired for changes made in the Android app.** The app saves locally and uploads through sync, and the sync upload ran Kitchen auto-share but no webhook checks, so cooking a recipe, finishing the shopping list, or running out of a pantry item on the phone sent nothing. The upload now fires `meal.cooked`, `shopping_list.completed` and `pantry.out_of_stock` on the same transitions the web routes use, after the write commits, and sends one completion event per upload however many items it checked.
### Security
- **devalue** (pulled in by Svelte) bumped 5.8.1 → 5.9.4, closes [GHSA-9rgm-9g3h-6x36](https://github.com/advisories/GHSA-9rgm-9g3h-6x36) (denial of service via malformed input, moderate).
---
## [1.3.0-dev.02] - 2026-09-17 (pre-release)
Second dev pre-release of the 1.3.0 minor. Outgoing webhooks and a
general public REST API, a shopping API for sister apps, shopping-list
items that combine and can restock the pantry, plus a batch of fixes
found in a review of everything since dev.01.
### Added
- **Outgoing webhooks.** Configure a target URL in Settings, Webhooks and CookTrace fires a signed HTTP POST the instant a recipe is logged as cooked, the shopping list is fully checked off, or a pantry item runs out of stock. Off by default (`WEBHOOKS_ENABLED=1`). HMAC-SHA256 signed, 3 delivery attempts with backoff, a "send test event" button to verify a target without waiting for a real event. Target URLs are validated against a shared SSRF guard (blocks loopback/private/link-local/cloud-metadata addresses unless `ALLOW_PRIVATE_WEBHOOK_URLS=1`), the same guard image-localizer.js now uses internally for its own external-image downloads. See `docs/webhooks.md`.
- **General-purpose public REST API** at `/api/v1/cook-diary`, `/api/v1/shopping`, and a pantry stock write route, for your own scripts and automations rather than the NutriTrace federation contract the rest of `/api/v1` documents. Off by default (`PUBLIC_API_ENABLED=1`; `PUBLIC_API_WRITE_ENABLED=1` additionally unlocks logging a cook, checking a shopping item, and updating pantry stock). Reuses the `mcp:read`/`mcp:write` token scopes MCP already defines, one token works for both interfaces. See `docs/public-api.md`.
- **Shopping API for sister apps.** A `shopping` token scope covering list, add, check, and clear, so a sister app (NoteTrace) can drive the list without the general public API switch.
- **The shopping list combines duplicate items.** In By Aisle and Flat views, items with the same name and unit show as a single row with the amounts added up and a pill for each recipe they came from. Checking, removing, dragging or re-aisling that row applies to every copy behind it; editing the amount folds them into one item, while editing just the name or unit keeps each recipe's own amount. Different units stay separate, and a copy with no amount makes the row show none rather than a wrong total. By Recipe view still lists each recipe's own items, and nothing changed in the database, sync, or the Android app's storage.
- **Clear Checked can restock your pantry.** The confirmation now lists the matching pantry items that are currently out of stock, ticked by default, so what you bought goes back in stock without a second trip through the Pantry tab. An item is only offered when the row is linked to a pantry item or its name matches exactly one, a generic like Bread gets a dropdown to pick the variant, and the toast has an Undo.
- **Support the iOS fund.** The README and Settings, About name what the fund covers (both developer accounts, tax and fees included).
### Changed
- **Trace settings now match NutriTrace.** The Base URL and API Key fields save when you leave them (or press Enter) instead of through a Save button beside each field, which on a phone in portrait sat past the edge of the screen; the connection is only re-tested when the value changed. On a server where AI is configured through environment variables, the section now says so at the top, shows the provider and model the server actually uses (rather than your own settings, greyed out), and hides the base URL and API key fields since the server holds them. Smart Log gains a Voice Input Language setting for when you speak a different language than your device is set to.
- **Claude Fable 5.1 in Trace's model list.** It is now the most capable Claude option; Fable 5 stays selectable, marked as previous.
- **Sync status pill in the sidebar**, with one colour rule for sync state across the app.
- Toasts can now carry an action button, which the restock flow uses for Undo.
### Fixed
- **The shopping list froze mid-drag.** Reordering threw `each_key_duplicate` and left the page unresponsive: svelte-dnd-action renames its placeholder to the dragged row's own id one frame after a drag starts, so a fast pointer move could hand Svelte the same id twice.
- **Shopping search crashed when two pantry items shared a name.** Suggestions are now deduplicated by name inside the shared picker, which also protects the seven other screens that use it (recipe category, tags, tools, pantry category, cookbook tags, Kitchens invite).
- **Cook history and comments returned 403 on a recipe shared with you.** Both reads checked only ownership or group visibility and ignored the kitchen share that granted access to the recipe itself.
- **Marking a planned meal as cooked never fired `meal.cooked`.** The webhook was wired into the two insert paths but not into the update that flips a planned entry, which is how the Diary does it.
- **`PATCH /api/v1/pantry/:id/stock` returned 403 for a write-scoped token.** The read-only federation router gated every method on `read:pantry`, so the documented write route was unreachable.
- **Settings, Webhooks could not save anything.** Its requests carried no CSRF header (or Bearer token on native), so create, enable/disable, delete and test all came back 403.
- **Marking a pantry item back in stock left its quantity at 0**, so it still read as out of stock and could not be toggled out again. An explicit `null` quantity now clears the stored value instead of being treated as "leave unchanged".
### Security
- **Comments could be posted to any recipe by ID.** `POST /api/recipes/:id/comments` had no ownership, share, or visibility check at all; it now requires the same access reading comments does.
- **MCP read tools were handed to a `mcp:write`-only token.** Read tools registered unconditionally, so only two of the three advertised tiers were really enforced. Each tier now requires its own scope.
- **Backup archives are no longer reachable from the public uploads directory.** `BACKUPS_PATH` defaults to a directory inside `UPLOADS_PATH`, and `/uploads` is served ahead of the auth middleware so an Android WebView `` can load images without an `Authorization` header. A full-backup ZIP sitting in that directory was therefore fetchable by URL, while every `/api/full-backup` route is admin-only. It now returns 404 like anything else outside the served set. Scheduled backups are off by default, so an install that never enabled them and never created one by hand had nothing there to reach; there is no directory listing either, so a filename had to be known or guessed. The archive holds a full database dump, so if yours has been internet-facing with backups enabled, a look through your access log for `/uploads/backups/` will settle it either way. The exclusion tests the resolved filesystem path rather than the request URL, since `express.static` percent-decodes a path before opening the file while a route prefix matches the raw one, and the two disagree on exactly the inputs an attacker would pick. A custom `BACKUPS_PATH` pointing somewhere else inside the uploads directory is now covered too, rather than only the default `backups` name.
- **The SSRF guard classified only the first resolved address.** A host publishing both a public and a private record could pass the check and then be connected to privately, since the request that follows resolves independently. Every resolved address must now pass.
- **adm-zip** bumped 0.6.0 → 0.6.1, closing the symlink-extraction advisory (no fixed release existed when this was last reviewed).
- **Uploads** are served from a sandboxed set of safe extensions.
---
## [1.3.0-dev.01] - 2026-09-10 (pre-release)
NutriTrace can now pull a user's CookTrace recipes and pantry directly
through a new read-only federation API, CookTrace gets a Model
Context Protocol server for AI agents, and NutriTrace joins Open Food
Facts and USDA as a fourth Pantry search source. Plus a pantry
display bug fix and dependency security bumps.
### Added
- **NutriTrace can pull CookTrace recipes.** New `GET /api/v1/recipes` (search) and `GET /api/v1/recipes/:id` (full detail) let a connected NutriTrace instance search and import a recipe as an NT meal: servings, portion/unit, image, the stored nutrition rollup, and a per-ingredient nutrition snapshot resolved through variant/generic inheritance (an ingredient linked to a generic whose variants each carry their own numbers still ships real values instead of a blank). Gated by a new `read:recipes` token scope, independent of the MCP scopes.
- **NutriTrace federation: pantry-read endpoint.** New `GET /api/v1/pantry` returns every leaf pantry row for the token owner (standalone items and variants), shaped for direct POST into NutriTrace's foods library. Generic parents that have variants are deliberately skipped: their leaf variants carry the real nutrition, and a placeholder next to the leaves in NT's foods list would be misleading. Variant rows carry the parent name in the display name ("Flour, Bread") so they read cleanly on the NT side. Uses the same 4-level nutrition resolver `/api/v1/recipes` uses, and derives calories from carbs / protein / fat via Atwater factors when a pantry row only stored macros. Gated by the new `read:pantry` scope (independent of `read:recipes` so users can grant just one).
- **`/api/v1/pantry` gains `q`, `limit`, and `offset`.** Backs NutriTrace's Foods-tab CookTrace source chip (search-and-pick a single pantry row). Omitting `q` still returns everything, which is what a bulk import uses. Filtering and paging both run after the leaf-only pass, so `total` always counts importable rows rather than raw pantry rows. `q` matches the composed display name (plus brand and category), so a variant stored as "Bread" under a "Flour" generic is still found by typing `flour`.
- **`read:pantry` and `read:recipes` API-token scopes.** Ticked at token-creation time in Settings, API Tokens, New Token; independent of the MCP scopes and of each other.
- **Model Context Protocol (MCP) server.** CookTrace now exposes a read + write + destructive MCP endpoint at `/api/mcp` so Claude Desktop, Cursor, Codex, and other MCP-aware agents can search recipes, browse the pantry and shopping list, log a cook, and (with the right scope) create recipes or pantry items directly. Fourteen tools across three independently-gated tiers (`mcp:read` / `mcp:write` / `mcp:destroy`), each requiring both a server-side env flag and a matching token scope. Off by default. See [docs/cooktrace/mcp.md](https://traceapps.github.io/docs/cooktrace/mcp/) for setup.
- **Personal access tokens.** New Settings → API Tokens section (admin, multi-user mode) to mint, scope, and revoke tokens. Currently the sole consumer is MCP and NutriTrace federation; built as a general-purpose token store for future API surfaces.
- **NutriTrace joins the Pantry search chips.** A connected NutriTrace instance's food catalog is now a fourth search source alongside Open Food Facts and USDA when adding a pantry item, picked the same way (chip, long-press to pin, included in "All" mode).
- **Food Sources settings reorganized.** NutriTrace Federation now lives inside Settings → Food Sources under its own "NutriTrace" sub-heading (between USDA and Barcode Scanner) instead of a separate top-level section, since federation is currently used purely as another food source.
### Fixed
- **Pantry "Expiring Soon" spotlight showed already-expired items as still counting down** on wide screens (for example "63d past" instead of "Expired"). The ribbon deliberately lists both soon-to-expire and already-expired items together, so it's now titled "Expiring & Expired" and the day-count label reads "Expired" once a date has passed.
- **Expanded generic pantry item's photo ballooned to a huge size on desktop** when it had variants. A full-row grid span wasn't being respected by the photo's own full-width sizing.
### Security
- **multer** bumped 2.2.0 → 2.3.0, closes a HIGH advisory (denial of service via aborted uploads holding file handles open).
- **nodemailer** bumped 9.0.3 → 9.1.1 (root and server), closes a moderate advisory (recipient-header validation bypass).
---
## [1.2.0] - 2026-09-02
Minor release. Big themes: ingredient-to-step linking with inline Cook
Mode rendering, a wide-screen desktop pass across six main pages plus
the Cookbook view, real drag-and-drop cookbook reordering with a
settable cover image, and a large batch of fixes accumulated since
v1.1.3 (Kitchen auto-share, CSRF on import dialogs, email links behind
a reverse proxy, single-user-mode data adoption, pantry sort/search
edge cases, and more).
### Added
- **Ingredient links on recipe steps.** Editor gets a collapsible "Link ingredients" panel per step with chip toggles for every named ingredient. In Cook Mode each step renders its linked ingredients inline right below the step text so quantities are visible without scrolling back to the top. Tapping an inline ingredient checks it off in the top list too, and marking a step done cascades the check to its linked ingredients. Tandoor imports carry their existing step-to-ingredient adjacency across automatically ([#40](https://github.com/TraceApps/cooktrace/issues/40)).
- **Wide-screen desktop layouts across the six main pages.** Settings switches to a two-pane rail-plus-content shell matching NutriTrace and LiftTrace. Manage's section-swap gets a cross-fade plus a sort control and an "Unused" quick-filter chip. Shopping tiles category groups into a masonry grid with a sticky toolbar, auto-collapse for finished sections, and a progress bar. Diary switches its day list to a card grid on wide screens, with roomier Month cells (more cook pills fit per day) and bigger Photos tiles. Pantry gets a category-chip wrap, a variant-row span for expanded generics, and an amber "Expiring soon" ribbon. Recipes' loading skeleton renders more placeholder cards (8 to 12) so it fills a wide grid instead of leaving a gap under a short first batch. Recipe view, Recipe editor, and Cookbook view also get wider ultrawide-monitor caps so nothing is left with dead space on either side past ~1440-1480px.
- **Cookbook view gets search, drag-and-drop reorder, and a settable cover image.** Search box + sort dropdown (Manual Order / A-Z / Favorites First) once a cookbook has more than one recipe. Manual order supports picking a card up from anywhere on it (not just a small handle) and dropping it into a new position, with the rest of the grid sliding smoothly out of the way. Cover image is settable from the same picker recipe step photos use. Cookbook recipe cards also now match the Recipes tab's cards exactly (size, category badge, star rating, tags, pantry-match pill, uniform row heights); they were previously thinner and missing most of that.
- **Diary search + meal-type filter.** Sticky toolbar gains a text search across recipe names, notes, and cook name, plus Breakfast / Lunch / Dinner / Snack quick chips. Each day-header shows a cook-count pill.
- **Shared recipes get a category filter.** Chips are scoped to only the categories that appear in what other users shared with you (search and sort already applied there).
- **Kitchens invite autocomplete.** Type-to-narrow picker pulls from the server user list (multi-user mode only). Freeform typing still works as a fallback.
- **Admin inline edit user.** Full name and email edit in place from Settings → User Management. Username stays immutable as the stable identifier.
- **PWA update flow.** In-app prompt asks to reload when a new version is out. Red dot on the Settings nav icon while an update is pending. Update-check cadence is configurable (Hourly / Every 4 hours / Every 12 hours / Daily / Manual).
- **iOS PWA viewport lock.** Horizontal touch pan no longer drifts the whole page on iPhone Safari or the installed PWA. Rubber-band bounce stays contained inside the app.
- **Server honors `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` env vars** via undici's `EnvHttpProxyAgent`, so every outbound scrape / import / AI call routes through a configured forward proxy without per-call-site changes. A missing scheme on either var now gives a clear startup error instead of an opaque one.
### Changed
- **Pantry source-chip (OFF / USDA / All) is now a filter, not an append.** With an active query, picking one of these hides the local pantry list and shows only that source's external results. Local pantry stays visible when the Pantry chip is picked or when the query is empty. Grid / list view toggle hides accordingly when the pantry list itself is hidden.
- **Move/Copy dialog's toggle slides between Move and Copy** instead of snapping, and dropped the redundant "(keep here too)" / "(remove from here)" explainer text.
- Profile page renders without its own header when opened from inside Settings; the Save button moves inline for the embedded view.
- Recipe close button reliably returns to the Recipes list from any origin.
- Cookbook import dialog's "Settings → Trace Assistant" link jumps straight to `/settings/ai` instead of the Settings index.
### Fixed
- **Kitchen auto-share was silently dropping every recipe created on the native Android app.** The mobile-write path (`/api/sync/push`) went straight to the DB and never called the fan-out hook, so Kitchen members saw nothing new from anyone cooking on their phone. Every recipe insert path now fans out uniformly. Toggling auto-share off then back on backfills any recipes created before this cut.
- **Kitchen "N recipes shared" count double-counted** in kitchens with two or more members. Now counts distinct recipes.
- **File / cookbook / URL / bulk import dialogs returned "Invalid CSRF token" on PWA.** Raw fetch calls now attach the CSRF header via a shared helper so every mutating import endpoint works ([#43](https://github.com/TraceApps/cooktrace/issues/43)).
- **Password reset links, invite links, and the SMTP test email's embedded logo rendered as `http://` behind a TLS-terminating reverse proxy**, which mail providers flagged as spam. Now honors `X-Forwarded-Proto` and `X-Forwarded-Host` ([#42](https://github.com/TraceApps/cooktrace/pull/42), thanks @clifmo).
- **Data left behind in single-user mode is adopted on upgrade.** Instances that already enabled user management on an earlier build had their unowned rows stranded for good. Startup now adopts them, once, when exactly one account exists.
- **Enabling user management no longer strands data written in single-user mode** ([TraceApps/docs#2](https://github.com/TraceApps/docs/issues/2)). Cookbooks, recipe / pantry categories, custom + disabled units, and Trace chat history are now claimed alongside recipes / pantry / diary / shopping, in one transaction.
- **Deleting an account no longer leaves its hidden-unit preferences behind.** `disabled_units` is now included in every account-removal path (self-delete, admin delete, disable user management, lockout recovery) and the OIDC first-login bootstrap.
- **Android native app showed a blank screen when opening a recipe someone shared with you.** The single-recipe read path now falls back to a server fetch on a local cache miss, instead of rendering against `null`.
- **Recipe step photos now persist.** They were being silently dropped on every save before.
- **Pantry items no longer disappear when sorted A-Z** if their category slug doesn't match a current pantry-categories catalog entry ([#41](https://github.com/TraceApps/cooktrace/issues/41), thanks @xiaojwus). Orphaned buckets now fall into Uncategorized instead of vanishing.
- **Variant Create button** enables when only an override name is typed. Button label now reflects which name will actually be created.
- **"All" mode pantry search results** render with full name, brand, barcode, and thumbnail instead of a blank row.
- **External OFF / USDA search results** render at full opacity instead of picking up the local out-of-stock dimming.
- **Split-chip pills (OFF, USDA)** light up as one unit on hover and active state instead of just the half the cursor is over.
- **Recipe save now guards against a stale or misbehaving client wiping `ingredients` / `steps` / `tags` / `tools`** by sending an empty value over existing content. Legitimate deletes via the per-item delete flows still work.
- **Clearing the Trace chat now asks for confirmation** instead of wiping the conversation on a single tap, with no way back.
- **Cook-diary rows with multiple broken phone-local photos now converge in one sync cycle** instead of one cycle per photo.
- **Missing-photo placeholder in the diary photos view** is more robust to future markup changes around it.
- **Ingredient-name suggestions on mobile no longer cover the keyboard and most of the screen.** The field used a native `` / `