# Changelog All notable changes to NutriTrace are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- ## [Unreleased] --- ## [1.4.0] - 2026-10-09 Minor release. Big themes: offline mode in the browser, an Android app whose sync keeps your accounts, offline edits and deletes straight, preliminary foldable support, search that ignores accents, Open Food Facts products that come in with the right values, Spanish and Russian, two privacy changes (fonts from your own server, update checks off until you ask), and security fixes worth updating for. > **Upgrade note.** The database updates itself on first start. Update the Android app too, not only the server: the protection between accounts and a critical Capacitor fix live in the app. New optional settings: `PUBLIC_URL` (the address emailed links use), `ALLOW_PRIVATE_IMAGE_URLS=1` (lets every account, not only an admin, download photos from your network) and `UPDATE_CHECK=off`. Existing installs keep checking for updates as before; new installs ask first. ### Added - **Offline mode** ([#211](https://github.com/TraceApps/nutritrace/issues/211)). Your diary, your own foods, meals and recipes, manual activity, settings and goals, the fasting timer and your profile are kept in the browser, so the app opens in a dead zone and the day still adds up. What you change goes up on its own when the connection returns, merged the same way the Android app's changes are, and the menu button shows an amber cloud while anything is waiting. Your own foods are searchable offline from the first load. Searches beyond them, lookups, the wellness providers, Trace and admin need a connection and say so. Works on iPhone too. [What works offline](https://traceapps.github.io/docs/nutritrace/features/#offline). - **Preliminary foldable support.** NutriTrace knows where a foldable's crease is, in the Android app and in browsers that report it. Half open like a book, the day's meals fall onto the two pages with the summary below, Goals puts targets and preview a page each, and Settings puts its list on one side; dialogs, sheets, menus and Trace keep off the fold; in laptop posture Trace sits on the half lying flat. Opened flat, the wider layouts turn on. - **Body composition through MCP and the REST API** ([#234](https://github.com/TraceApps/nutritrace/pull/234)). A `get_body_composition` tool and `/api/v1/body-composition` return your scale readings over a date range, each source kept separate. Contributed by @kgenerozov. - **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). About also links to the TraceApps family. ### Changed - **Update checks are off until you turn them on.** Every browser and phone used to ask GitHub directly every 4 hours. Setup now asks, and a fresh install contacts nothing on its own; the web app asks through your server, and the Android app asks only once you say yes. Existing installs keep checking as before, and `UPDATE_CHECK=off` turns the server's checks 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. - **Search ignores accents.** "platano" finds "Plátano" and "cafe" finds "Café" everywhere you search, offline and on a local Open Food Facts mirror too. - **Trace's attach button offers Camera or Gallery on phones and in the Android app** ([#254](https://github.com/TraceApps/nutritrace/pull/254)), like the other Trace apps. Thanks @librarian. - **Trace's new-food card no longer picks a meal for you.** It starts on Don't Log, so saving a food touches only your foods. - **The in-app updater reuses an update it already downloaded**, and the button says Install. - **Trace's model lists are current.** Claude Haiku 5.5 is the new default, Claude Sonnet 5.5 and Opus 5.5, GPT-6 Luna, and Gemini 3.8 Flash and 3.1 Flash Lite are added, Gemini 3.1 Pro is listed as the preview it is, and Gemini 2.5 leaves the list (open to existing Google users only). A model you already chose keeps working; shut-down Gemini models switch to the default. ### Fixed - **Syncing the Android app no longer wipes the day's note or a food's CookTrace origin.** The newer note wins, a cleared note included. - **Foods and meals edited offline in the Android app reach the server after the app restarts.** - **Editing or deleting a food or meal in the Android app before it syncs no longer changes a different one**, and diary items logged from it link to it once it syncs. - **A phone whose clock is off no longer decides which edit wins.** The newer edit wins by the server's clock, and a phone whose edit lost gets the winning copy. - **Marking a day or meal complete in the Android app while offline sticks**, and an older change no longer undoes a newer one made elsewhere. - **A setting changed in the Android app before it reached the server is no longer set back** when the app loads your settings. - **Connecting the Android app to a server with Upload or Merge sends everything, once**: recipes, activities, fasts and Health Connect data too, without duplicates even when the upload runs again. - **Clear all data reaches the Android app**, wellness, workouts and fasts included, and clears fasts on the server too. - **Recipe servings set in the Android app survive syncing** ([#255](https://github.com/TraceApps/nutritrace/pull/255)), and **editing a food or recipe in the app no longer clears its photo** ([#256](https://github.com/TraceApps/nutritrace/pull/256)). Thanks @librarian. - **Health Connect counts every part of a night's sleep, and sleep stages show up** ([#236](https://github.com/TraceApps/nutritrace/issues/236)). A night split where you woke counted only its last piece; a night recorded by two apps counts once, and a nap no longer replaces it. Thanks @kgenerozov for the report and the analysis behind it. - **Food logged through MCP, the API or a diet import no longer appears twice after you save the day** ([#239](https://github.com/TraceApps/nutritrace/issues/239)). Entries already stored are repaired when the server starts; duplicates that already happened need deleting by hand. Thanks @Chrristin for the report and the fix it builds on. - **Changing a diary entry from a household unit to grams converts instead of multiplying** ([#237](https://github.com/TraceApps/nutritrace/issues/237)). A slice of bread changed to 66 g read about 8,700 kcal. Thanks @herver1971 for the report, the root cause and the test. - **The top of the Diary is one solid block.** Entries no longer show through between the date bar, the week strip and the Mark Day Complete bar; that bar no longer runs into the date bar, reads in full on phones, and the week strip's day preview opens above it. - **A recipe photo changed while editing shows in the web diary** ([#261](https://github.com/TraceApps/nutritrace/pull/261), [#262](https://github.com/TraceApps/nutritrace/pull/262)). Photos already saved the wrong way are converted when the server starts. Thanks @librarian. - **A saved meal that logs a food you've since deleted names those ingredients**, and one failing ingredient no longer drops the rest of the meal. - **Refresh from OFF brings a food up to date with Open Food Facts** ([#241](https://github.com/TraceApps/nutritrace/issues/241)), products with both "as sold" and "as prepared" values come in "as sold", and products with no values no longer come in as 0 kcal. Thanks @fatman00 for the report. - **Open Food Facts products with no name in their main language are found** ([#238](https://github.com/TraceApps/nutritrace/issues/238)), and show in your chosen Open Food Facts language when they have one. Thanks @meggiman for the report. - **Searches and scans on a local Open Food Facts mirror no longer fail now and then**, and Trace, Smart Log and the meal editor use a product's real values when a search result leaves them out. - **Opening a recipe or food from CookTrace, Mealie, Open Food Facts or USDA no longer shows the one you opened before** ([#260](https://github.com/TraceApps/nutritrace/issues/260)). Thanks @herver1971. - **Trace shows your newest messages when you open it.** Past 100 messages, the latest ones never appeared. - **Trace works with OpenAI-compatible endpoints that stream unless told not to** ([#258](https://github.com/TraceApps/nutritrace/issues/258)) **and with ones that check every field** ([#259](https://github.com/TraceApps/nutritrace/issues/259)). Thanks @jsapede for the reports. - **Trace can create a food without adding it to your diary.** - **The Trace button no longer covers what's on top of it, and the Copy sheet's buttons no longer sit under Android's navigation bar** ([#233](https://github.com/TraceApps/nutritrace/issues/233)). Thanks @nomad64 for the report. - **SSO with an email that already has an account no longer creates a second account.** When the provider doesn't mark the email verified (Authentik's default since 2025.10), sign-in is refused with a pointer to link the provider from your profile. - **SSO works with Authelia 4.39 and later out of the box**, the SSO callback works at `/api/oidc/callback` and without the provider number, and `OIDC_ENABLE_EMAIL_PASSWORD_LOGIN` works for providers added in Settings. - **Signing in on a plain-HTTP address explains what's wrong instead of looping**, and the app no longer loads behind the sign-in screen. - **The weekly summary email is sent.** It failed every time, so no one received it. - **"A New Version Is Available" on the web says what it means, and Reload works.** - **The installed app keeps working when NutriTrace is served from a subpath** ([#250](https://github.com/TraceApps/nutritrace/pull/250)). Thanks @kgenerozov. - **A file of the wrong type, or over the size limit, is turned away with a clear message** instead of a server error. - **Settings pages line up with the section list, and the list keeps its place**, on desktop and foldables. ### Translations - **Spanish**, translated almost in full on [Weblate](https://hosted.weblate.org/projects/nutritrace) by @herver1971. Choose it in Settings, Regional & Units, Language. - **Russian**, translated almost in full on Weblate by Михаил Малов. - **German** is complete again, and the diagnostics note about GitHub issues now translates as a whole sentence ([#253](https://github.com/TraceApps/nutritrace/pull/253)). Thanks @KAiSER086. A few newer labels show in English until they're translated. ### Security - **Sync can no longer change another account's data.** Any account could overwrite or delete another account's foods, meals, activities or fasts through sync by naming their ids. - **Signing in to another account on the same phone no longer shows or sends the previous account's data**, its settings and unsaved drafts included. If that account left changes that never synced, the app asks before discarding them. - **The server answers as the account whose token a request carries, never as the one a leftover sign-in cookie names.** After an account switch on Android, Health Connect data could go to the previous account. The app now forgets NutriTrace's sign-in cookie on every switch and sign-out, and keeps other sites' cookies, such as a sign-in gate in front of your server. - **Diary items no longer show another account's food details.** Only your own foods, foods shared with you, and ingredients of meals shared with you fill an item in. - **The CookTrace and Mealie connections forward only the requests the app makes.** Any account could send any request through them, including to other services on your network. - **A photo given as a link is only kept when it is an image**, downloads never reach cloud-metadata addresses and check every redirect, and only an admin can download from your network unless `ALLOW_PRIVATE_IMAGE_URLS=1`. Push notifications and `/api/proxy` follow the same rules. - **The image proxy passes images only**, and images can't act as a page. - **A password reset link can no longer be pointed at someone else's site.** Reset, invite and sharing emails link to `PUBLIC_URL` when set, or to an address an admin uses. - **Names in emails can no longer carry markup.** Everything an email shows is escaped. - **The Android app's SSO sign-in no longer passes the session token through the `nutritrace://` link.** It carries a single-use code only the app that started the sign-in can redeem. - **Uploaded files get unguessable names.** - **Dependency updates:** Capacitor 8.5.3 in the Android app ([GHSA-rvm3-566m-v7fv](https://github.com/advisories/GHSA-rvm3-566m-v7fv), critical: remote content could be loaded as part of the app), proxy-addr 2.0.8 ([GHSA-jqcg-44mw-7w3h](https://github.com/advisories/GHSA-jqcg-44mw-7w3h), critical; NutriTrace trusts no proxies, so it wasn't exposed), nodemailer 10.0.12 ([GHSA-6vj9-mwq6-2f5v](https://github.com/advisories/GHSA-6vj9-mwq6-2f5v)), @modelcontextprotocol/sdk 1.32.1 ([GHSA-6qxp-vccf-f47h](https://github.com/advisories/GHSA-6qxp-vccf-f47h); NutriTrace uses only its server side), undici 6.29.0 ([GHSA-3wwx-pv8p-q78v](https://github.com/advisories/GHSA-3wwx-pv8p-q78v)) and ip-address 10.7.2 ([GHSA-2vr4-cq9g-pvrc](https://github.com/advisories/GHSA-2vr4-cq9g-pvrc)), plus build tools. `npm audit` reports 0 vulnerabilities for the app and the server. --- ## [1.4.0-dev05] - 2026-10-08 (pre-release) A dev pre-release of the 1.4.0 minor. Russian, Android sync rebuilt so accounts, offline edits and deletes stay right, SSO fixes, the weekly summary email, and security updates. ### Added - **Russian.** NutriTrace can be used in Russian: pick it in Settings → Regional & Units. Translated almost in full by Михаил Малов on Weblate; only the new account-switch prompts are still in English. Thank you! ### Changed - **Trace's attach button offers Camera or Gallery on phones and in the Android app** ([#254](https://github.com/TraceApps/nutritrace/pull/254)), like the other Trace apps. On a computer it opens the file picker. Thanks @librarian. ### Fixed - **The weekly summary email is sent.** It failed every time, so no one received it. With Weekly Summary on, it now arrives with the notification, and its weight change counts weights logged in the diary. - **SSO with an email that already has an account no longer creates a second account.** When the provider doesn't mark the email verified (Authentik's default since 2025.10), sign-in is refused with a pointer to link the provider from your profile. - **SSO works with Authelia 4.39 and later out of the box.** Email, username and groups are read from userinfo when the ID token leaves them out. - **The SSO callback works without the provider number, and at `/api/oidc/callback`**, the address older docs gave. Both ended on a blank page. - **`OIDC_ENABLE_EMAIL_PASSWORD_LOGIN` works for providers added in Settings.** - **Signing in on a plain-HTTP address explains what's wrong instead of looping**, links to the fix (`INSECURE_COOKIES=1`), and says so in the server log. - **The app no longer loads behind the sign-in screen.** It waits to learn who is signed in. - **The installed app keeps working when NutriTrace is served from a subpath** ([#250](https://github.com/TraceApps/nutritrace/pull/250)). Thanks @kgenerozov. - **Recipe servings set in the Android app survive syncing** ([#255](https://github.com/TraceApps/nutritrace/pull/255)). Thanks @librarian. - **Editing a food or recipe in the Android app no longer clears its photo** ([#256](https://github.com/TraceApps/nutritrace/pull/256)). Thanks @librarian. - **The diagnostics note about GitHub issues translates as a whole sentence** ([#253](https://github.com/TraceApps/nutritrace/pull/253)), and the German translation is complete. Thanks @KAiSER086. - **Trace works with OpenAI-compatible endpoints that stream unless told not to** ([#258](https://github.com/TraceApps/nutritrace/issues/258), reported by @jsapede). Chat and Smart Log failed with "Unexpected non-whitespace character after JSON". - **Trace's tools work with OpenAI-compatible endpoints that check every field** ([#259](https://github.com/TraceApps/nutritrace/issues/259), reported by @jsapede). - **Opening a recipe or food from CookTrace, Mealie, Open Food Facts or USDA no longer shows the one you opened before** ([#260](https://github.com/TraceApps/nutritrace/issues/260)). Unsaved typing still comes back when you reopen the same item. Thanks @herver1971. - **The Diary's Mark Day Complete bar sits below the date bar** instead of running into it, at every screen size, in the browser and the Android app. - **Syncing the Android app no longer wipes the day's note or a food's CookTrace origin.** The newer note wins, a cleared note included. Older servers and older apps keep working. - **Foods and meals edited offline in the Android app reach the server after the app restarts.** - **Editing or deleting a food or meal in the Android app before it syncs no longer changes a different one.** Diary items logged from it link to it once it syncs. - **Clear all data reaches the Android app**, wellness, workouts and fasts included, and clears fasts on the server too. - **A phone whose clock is off no longer decides which edit wins.** The newer edit wins by the server's clock, and a phone whose edit lost gets the winning copy. - **Marking a day or meal complete in the Android app while offline sticks**, and an older change no longer undoes a newer one made elsewhere. - **A setting changed in the Android app before it reached the server is no longer set back** when the app loads your settings. It goes up with the next sync. - **Connecting the Android app to a server with Upload or Merge also sends recipes, activities, fasts and Health Connect data, without duplicates**, even when the upload runs again. Anything that doesn't go up goes with the next sync. ### Security - **Sync can no longer change another account's data.** Any account could overwrite or delete another account's foods, meals, activities or fasts through sync by naming their ids. - **Diary items no longer show another account's food details.** Only your own foods, foods shared with you, and ingredients of meals shared with you fill an item in. - **The image proxy passes images only**, and images can't act as a page. It passed through whatever came back from the app's own address, before sign-in. - **A password reset link can no longer be pointed at someone else's site.** Reset, invite and sharing emails link to `PUBLIC_URL` when set, or to an address an admin uses, and keep the `BASE_URL` subpath. - **Names in emails can no longer carry markup.** Everything an email shows is escaped. - **The CookTrace and Mealie connections forward only the requests the app makes.** Any account could send any request through them, including to other services on your network. - **A photo given as a link is only kept when it is an image.** Downloads never reach cloud-metadata addresses and check every redirect; only an admin can download from your network, other accounts only with `ALLOW_PRIVATE_IMAGE_URLS=1`. - **Push notifications never reach cloud-metadata addresses** and follow redirects only on the same server. The open `/api/proxy` checks every redirect too. - **The Android app's SSO sign-in no longer passes the session token through the `nutritrace://` link.** It carries a single-use code only the app that started the sign-in can redeem. - **Uploaded files get unguessable names.** - **Signing in to another account on the same phone no longer shows or sends the previous account's data**, its settings and unsaved drafts included. If that account left changes that never synced, the app asks before discarding them. The same user id on another server counts as another account. - **The server answers as the account whose token a request carries, never as the one a leftover sign-in cookie names.** After an account switch on Android, Health Connect data could go to the previous account. The app also forgets NutriTrace's sign-in cookie on every switch, connect, disconnect and sign-out, and keeps other sites' cookies, such as a sign-in gate in front of your server. - **Capacitor** bumped 8.3.4 to 8.5.3 in the Android app, closes [GHSA-rvm3-566m-v7fv](https://github.com/advisories/GHSA-rvm3-566m-v7fv) (critical: remote content could be loaded as part of the app through Capacitor's internal HTTP proxy path). - **proxy-addr** bumped 2.0.7 to 2.0.8 on the server, closes [GHSA-jqcg-44mw-7w3h](https://github.com/advisories/GHSA-jqcg-44mw-7w3h) (critical: an IPv4-mapped IPv6 address could pass as a trusted proxy). NutriTrace trusts no proxy addresses, so it wasn't exposed. - **@modelcontextprotocol/sdk** bumped 1.30.0 to 1.32.1, closes [GHSA-6qxp-vccf-f47h](https://github.com/advisories/GHSA-6qxp-vccf-f47h) (high: its OAuth client could send credentials to a server an MCP server picked). NutriTrace only uses its MCP server side. - **brace-expansion** and **source-map-js** bumped, closing [GHSA-q2hr-2g5m-vwhr](https://github.com/advisories/GHSA-q2hr-2g5m-vwhr), [GHSA-qhr7-859c-m2p7](https://github.com/advisories/GHSA-qhr7-859c-m2p7), [GHSA-6j4f-fj2g-mc7p](https://github.com/advisories/GHSA-6j4f-fj2g-mc7p) and [GHSA-68fv-2mgg-jv7q](https://github.com/advisories/GHSA-68fv-2mgg-jv7q) (high: denial of service). Both are only used to build the app. - `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.** NutriTrace can be used in Spanish: pick it in Settings → Regional & Units. Translated almost in full by @herver1971 on Weblate; only the new Support page is still in English. 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. - **About links to the TraceApps family.** ### 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. ### 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. - **Settings pages line up with the section list** on desktop and foldables. Every page started 12px below the list beside it. - **The Settings section list keeps its place** on desktop and foldables. Opening a section from the Settings page scrolled the list beside it back to Profile. ### Security - **undici** bumped 6.28.0 to 6.29.0, closes [GHSA-3wwx-pv8p-q78v](https://github.com/advisories/GHSA-3wwx-pv8p-q78v) (moderate: a WebSocket server could crash the process). NutriTrace opens no WebSocket connections. - **ip-address** bumped 10.4.0 to 10.7.2, closes [GHSA-2vr4-cq9g-pvrc](https://github.com/advisories/GHSA-2vr4-cq9g-pvrc) (moderate: one IPv6 range was not recognized as local). It only comes in through the MCP server's rate limiter; NutriTrace's own SSRF guard does not use it. - **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) Third dev pre-release of 1.4.0: preliminary foldable support, search that ignores accents, and Open Food Facts products that come in with the right values. ### Added - **Preliminary foldable support.** NutriTrace knows where a foldable's crease is, in the Android app and in browsers that report it, and lays out around it: - Half open like a book, Settings puts its section list on one side and the section on the other, and the day's meals fall onto the two pages. - Dialogs, sheets, pickers, menus and Trace keep off the fold, and a menu opened near it takes the roomier side. - In laptop posture, Trace sits on the half lying flat, leaving the day readable on the half standing up. - Opened flat, or on any screen that size, the wider layouts turn on: Settings in two panes, Foods two cards across, side rails on Diary and Goals, the food and meal editors side by side, and Wellness insights alongside. - Diagnostics shows what the hinge reports. Charts and photos still cross the fold, since an opened foldable is a bigger screen to read them on. ### Changed - **Trace's new-food card no longer picks a meal for you.** It starts on Don't Log, so saving a food touches only your foods, and Save & Add to Diary turns on once you choose a meal under Log to. ### Fixed - **Search ignores accents.** "platano" finds "Plátano" and "cafe" finds "Café" wherever you search: your foods, meals and recipes, activities, Settings, Smart Log, Trace, the public API and MCP, offline too. A local Open Food Facts mirror finds accented names as well. Text without accents matches as before. - **Searches and scans on a local Open Food Facts mirror no longer fail now and then.** Two at once could fail, and the server then asked Open Food Facts instead, or answered nothing when air-gapped. - **Refresh from OFF brings a food up to date with Open Food Facts** ([#241](https://github.com/TraceApps/nutritrace/issues/241)). It only filled empty fields, and a server with a local Open Food Facts mirror answered from its older copy. It now updates the values Open Food Facts has, converted to the food's portion. Thanks @fatman00 for the report. - **Open Food Facts products with both "as sold" and "as prepared" values come in "as sold"** ([#241](https://github.com/TraceApps/nutritrace/issues/241)). Open Food Facts' API gave NutriTrace only the "as prepared" values for these, such as canned beans, drink powders and baby formula. - **Open Food Facts products with no values no longer come in as 0 kcal** ([#241](https://github.com/TraceApps/nutritrace/issues/241)). Search results say "No values listed", the product can't be added to the diary or a meal until it has values, and Trace and Smart Log say so. If Open Food Facts only has "as prepared" values, the food editor offers them and notes it on the food. - **Trace, Smart Log and the meal editor use a product's real values when a search result leaves them out.** They used to save it as 0 kcal. ### Security - No security fixes this cycle. `npm audit` reports 0 vulnerabilities for the app and the server, there are no open Dependabot alerts, and the only new dependency is androidx.window 1.3.0 on Android, which tells the app where a foldable's crease is. --- ## [1.4.0-dev02] - 2026-09-25 (pre-release) Second dev pre-release of 1.4.0, all fixes: food logged through MCP no longer doubles when you save the day, Health Connect counts a whole night's sleep, and more fixes from tester reports. ### Changed - Chinese (Simplified) translation updated through Weblate. ### Fixed - **Food logged through MCP, the API or a diet import no longer appears twice after you save the day** ([#239](https://github.com/TraceApps/nutritrace/issues/239)). Saving the day in the app kept a second copy of each entry, and those entries could not be deleted from the app. Entries already stored are repaired when the server starts, but duplicates that already happened need deleting by hand. Restoring an old export is fixed too. Thanks @Chrristin for the report and the fix it builds on. - **Health Connect counts every part of a night's sleep, and sleep stages show up** ([#236](https://github.com/TraceApps/nutritrace/issues/236)). Samsung Health splits a night wherever you woke, and only the last piece counted, so 00:46-05:00 plus 05:45-06:53 read as 68 minutes. Every piece counts now, a night recorded by two apps counts once, and a nap no longer replaces it. Deep, Light, REM and the Sleep Quality cards now fill in when you sync from the app, not just from the hourly background sync. Thanks @kgenerozov for the report and the analysis behind it. - **Changing a diary entry from a household unit to grams converts instead of multiplying** ([#237](https://github.com/TraceApps/nutritrace/issues/237)). A slice of bread changed to 66 g read about 8,700 kcal instead of 159. Entries already saved on Android are fixed too. Thanks @herver1971 for the report, the root cause and the test. - **Open Food Facts products with no name in their main language are found** ([#238](https://github.com/TraceApps/nutritrace/issues/238)). They were reported as missing even with a name in another language, and some dropped out of searches. If you picked a language other than English for Open Food Facts, names show in that language when a product has one. Thanks @meggiman for the report. - **Trace can create a food without adding it to your diary.** Asking it to save a food and not log it got you a diary entry anyway. It now shows the review card with Save to Foods. - **A saved meal that logs a food you've since deleted now tells you which ingredients those are**, so you can fix the meal. - **One ingredient failing no longer drops the rest of the meal.** Every ingredient is attempted, and anything that didn't make it is named. ### Security - No security fixes this cycle. `npm audit` reports 0 vulnerabilities for the app and the server, there are no open Dependabot alerts, and no dependencies changed since 1.4.0-dev01. --- ## [1.4.0-dev01] - 2026-09-22 (pre-release) First dev pre-release of the 1.4.0 minor. The headline is offline mode: your diary and your own foods are kept in the browser, what you log offline goes up on its own, and the installed app opens in a dead zone. Also in: body composition through MCP and the REST API, update checks that are off until you ask for them, and fonts served by your own instance instead of Google. ### Added - **Body composition through MCP and the REST API** ([#234](https://github.com/TraceApps/nutritrace/pull/234)). A new `get_body_composition` read tool returns your saved scale readings over a date range, keeping each source separate rather than merging them. Also at `/api/v1/body-composition` when the Public API is on. Contributed by @kgenerozov. [MCP tools](https://traceapps.github.io/docs/reference/mcp-tools/). - **Offline mode** ([#211](https://github.com/TraceApps/nutritrace/issues/211)). Your diary and your own foods are kept in the browser, so the app opens in a dead zone, the day still adds up, and what you log goes up on its own when the connection returns, merged the same way the Android app's changes are. The menu button shows an amber cloud while anything is waiting. Works the same on iPhone, since it doesn't rely on Background Sync. [What works offline](https://traceapps.github.io/docs/nutritrace/features/#offline). - **What else works in offline mode** ([#211](https://github.com/TraceApps/nutritrace/issues/211)). Your own foods, meals and recipes, manual activity, settings and goals, the fasting timer, your profile and picture, and the wellness figures you have already seen. Anything you make offline can be used straight away and keeps its identity when the queue goes up. - **Offline mode says what it cannot reach** ([#211](https://github.com/TraceApps/nutritrace/issues/211)). Searches, lookups, photos, the wellness providers, Trace and anything admin need a connection, and each says so where you would otherwise see an empty screen or a wrong answer. If your server refuses what is waiting, it says why and keeps trying. ### 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 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. - **The Copy sheet's buttons no longer sit under Android's navigation bar** ([#233](https://github.com/TraceApps/nutritrace/issues/233)). That one sheet overwrote the spacing that keeps content clear of the bar. Thanks @nomad64 for the report. - **The Trace button no longer covers what's on top of it** ([#233](https://github.com/TraceApps/nutritrace/issues/233)). It floated above every sheet and dialog, so wherever you had dragged it, it could sit over a title or a button. Thanks @nomad64 for the report. - **Something deleted while online stays deleted when the connection goes.** An older copy of the list could be carried over from before you signed in, 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. It travels with the app now. - **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, rather than the app refusing to log anything offline. - **Work queued while a sync was running no longer waits for you to do something else** before it goes up, and a change made before the page had built its connection now carries the token your server asks for. - **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. - **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. - **A profile saved before the app had finished checking your server no longer goes to the wrong place.** Your name and picture could be written to local settings instead of your account. - **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. They are split by script, so a page still downloads only the alphabets it needs. 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.1] - 2026-09-20 Patch release. A Default Field setting for the add and edit sheets, and a round of Android fixes: the back button closes what's open first, dragging the Trace button no longer refreshes the page, Settings categories open at their top, and sheets and dialogs stay below the status bar. ### Changed - **Choose which box the cursor starts in when adding food** ([#224](https://github.com/TraceApps/nutritrace/issues/224)). Since 1.3.0 the add and edit sheets put the cursor in the first box, Serving Size, so changing the number of servings took an extra tap every time. Settings, Diary, Default Field picks Number of Servings or Serving Size for adding a food, adding several at once, and editing a diary entry. It defaults to Number of Servings and follows your account across devices. Thanks @nomad64 for the report. ### Fixed - **Moving the Trace button no longer refreshes the page** ([#225](https://github.com/TraceApps/nutritrace/issues/225)). In the Android app connected to a server, dragging the Trace button downward while the page was scrolled to the top was treated as pull-to-refresh and synced. The same could happen when dragging a reorder handle down (meal names, nutrients, body stats, Statistics categories, Meal Editor ingredients) or moving the photo cropper. Dragging those no longer counts as a pull; pulling down anywhere else still refreshes as before. Thanks @nomad64 for the report. - **The Android back button closes what's open first** ([#226](https://github.com/TraceApps/nutritrace/issues/226)). Back only knew how to go back a page, so with a sheet like Body Stats open it did nothing visible, and elsewhere it could leave the page with a sheet or dialog still up. Back now closes the newest sheet, dialog, menu, picker or panel first, one at a time, the same as its own close button (a dialog closes as Cancel, and a 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. Thanks @nomad64 for the report. - **A Settings category now opens at its top** ([#227](https://github.com/TraceApps/nutritrace/issues/227)). On a phone, picking a category from lower down the Settings list (Import & Export, for example) opened it at the bottom, because the category kept the list's scroll position. Categories now open at their top, and going back to the list returns you to where you were on it. A setting found through Settings search still scrolls straight to its match. Thanks @nomad64 for the report. - **Body Stats no longer slides under the status bar** ([#228](https://github.com/TraceApps/nutritrace/issues/228)). In the Android app, opening Body Stats from the Diary brings up the keyboard, and the sheet grew taller than the space above it, so its title and close button ended up under the status bar where they couldn't be tapped. The sheet now stays below the status bar and scrolls its fields instead, with the title and close button always in reach. The other Diary sheets built the same way (Save to Library, Copy To, the date picker) and the Wellness date picker got the same limit. Thanks @nomad64 for the report. - **Other sheets and dialogs stay below the status bar too.** Found while fixing #228: the shared sheet used across the app, the Goals sheets, the nutrition summary, the Statistics calendar, the Wellness workout details and Smart Log. 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. ### Translations - **German wording updates** on [Weblate](https://hosted.weblate.org/projects/nutritrace) by KAiSER: clearer labels for Delete, Body Stats, estimated values, the nutrition label reader and backup deletion. The new Default Field setting shows in English until it's translated. ### Security - No security fixes this cycle. `npm audit` reports 0 vulnerabilities for the app and the server, and there are no open Dependabot alerts. --- ## [1.3.1-dev01] - 2026-09-19 (pre-release) First dev pre-release of the 1.3.1 patch cycle. Adds a Default Field setting for the add and edit sheets, and a round of Android fixes: the back button closes what's open first, dragging the Trace button no longer refreshes the page, Settings categories open at their top, and sheets and dialogs stay below the status bar. ### Changed - **Choose which box the cursor starts in when adding food** ([#224](https://github.com/TraceApps/nutritrace/issues/224)). Since 1.3.0 the add and edit sheets put the cursor in the first box, Serving Size, so changing the number of servings took an extra tap every time. Settings, Diary, Default Field picks Number of Servings or Serving Size for adding a food, adding several at once, and editing a diary entry. It defaults to Number of Servings and follows your account across devices. Thanks @nomad64 for the report. ### Fixed - **Moving the Trace button no longer refreshes the page** ([#225](https://github.com/TraceApps/nutritrace/issues/225)). In the Android app connected to a server, dragging the Trace button downward while the page was scrolled to the top was treated as pull-to-refresh and synced. The same could happen when dragging a reorder handle down (meal names, nutrients, body stats, Statistics categories, Meal Editor ingredients) or moving the photo cropper. Dragging those no longer counts as a pull; pulling down anywhere else still refreshes as before. Thanks @nomad64 for the report. - **The Android back button closes what's open first** ([#226](https://github.com/TraceApps/nutritrace/issues/226)). Back only knew how to go back a page, so with a sheet like Body Stats open it did nothing visible, and elsewhere it could leave the page with a sheet or dialog still up. Back now closes the newest sheet, dialog, menu, picker or panel first, one at a time, the same as its own close button (a dialog closes as Cancel, and a 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. Thanks @nomad64 for the report. - **A Settings category now opens at its top** ([#227](https://github.com/TraceApps/nutritrace/issues/227)). On a phone, picking a category from lower down the Settings list (Import & Export, for example) opened it at the bottom, because the category kept the list's scroll position. Categories now open at their top, and going back to the list returns you to where you were on it. A setting found through Settings search still scrolls straight to its match. Thanks @nomad64 for the report. - **Body Stats no longer slides under the status bar** ([#228](https://github.com/TraceApps/nutritrace/issues/228)). In the Android app, opening Body Stats from the Diary brings up the keyboard, and the sheet grew taller than the space above it, so its title and close button ended up under the status bar where they couldn't be tapped. The sheet now stays below the status bar and scrolls its fields instead, with the title and close button always in reach. The other Diary sheets built the same way (Save to Library, Copy To, the date picker) and the Wellness date picker got the same limit. Thanks @nomad64 for the report. - **Other sheets and dialogs stay below the status bar too.** Found while fixing #228: the shared sheet used across the app, the Goals sheets, the nutrition summary, the Statistics calendar, the Wellness workout details and Smart Log. 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. ### Translations - **German wording updates** on [Weblate](https://hosted.weblate.org/projects/nutritrace) by KAiSER: clearer labels for Delete, Body Stats, estimated values, the nutrition label reader and backup deletion. The new Default Field setting shows in English until it's translated. ### Security - No security fixes this cycle. `npm audit` reports 0 vulnerabilities for the app and the server, and there are no open Dependabot alerts. --- ## [1.3.0] - 2026-09-19 Minor release. Big themes: recipes and pantry items from CookTrace, an opt-in way to mark diary days complete, new ways to reach your data from your own tools (a REST API, outgoing webhooks and more MCP tools), Cronometer imports working again, a round of iPhone fixes, better Open Food Facts search, and several security fixes worth updating for. > **Upgrade note.** The database picks up its new columns automatically on first start; nothing to do. To import CookTrace pantry items you need a CookTrace version with the `read:pantry` permission and a token created with it ticked. Existing CookTrace tokens keep working for recipes. ### Added - **CookTrace recipes and pantry items.** If you also run CookTrace, a CookTrace chip on the Foods screen searches it: pick a recipe to open it in the Meal Editor with its ingredients, servings, photo and nutrition, or a pantry item to open it in the Food Editor. Settings, Connected Services, CookTrace can also import every recipe or every pantry item in one go. Imported items stay linked to CookTrace, so importing again updates what you already have instead of making a copy, and when a recipe changes in CookTrace, NutriTrace offers to pull the update rather than overwriting anything. Opt-in, with a token you create in CookTrace. - **Mark diary days complete** ([#207](https://github.com/TraceApps/nutritrace/issues/207)). Off by default (Settings, Diary, Show Day Completion). Close a day from the date bar, or tick meals one at a time and the day closes when they're all done. Completed days get a check on the week strip and date picker, Statistics counts them, the weekly summary includes them, and on Android the bedtime notification has a Close today button. Marks sync between your devices. Thanks @tellis82 for the suggestion. - **A REST API for your own scripts and automations.** `/api/v1/diary`, `/api/v1/goals`, `/api/v1/meals`, `/api/v1/steps` and `/api/v1/profile`. Off by default (`PUBLIC_API_ENABLED=1`; `PUBLIC_API_WRITE_ENABLED=1` also allows logging food, water, meals and body stats). Uses the same tokens as MCP. See `docs/public-api.md`. - **Outgoing webhooks.** Set a URL in Settings, Webhooks and NutriTrace sends a signed HTTP POST when you log a food, water or a body stat, or reach a daily nutrition goal. Off by default (`WEBHOOKS_ENABLED=1`). Failed deliveries are retried, a send button next to each webhook fires a test event, and addresses on your local network are refused unless `ALLOW_PRIVATE_WEBHOOK_URLS=1`. See `docs/webhooks.md`. - **More MCP tools** ([#215](https://github.com/TraceApps/nutritrace/pull/215), [#219](https://github.com/TraceApps/nutritrace/pull/219)). Diary entries and daily totals over a date range, optional dates on recent foods and recent meals, daily steps from connected wearables, and your gender and date of birth. Any `mcp:read` token can read your date of birth, so an AI client connected over MCP can see it. Thanks @kgenerozov for contributing both. - **Average Heart Rate card in Wellness** ([#205](https://github.com/TraceApps/nutritrace/issues/205)). Health Connect already recorded it; now it shows, next to Resting Heart Rate. Thanks @kgenerozov for the report. - **Health Connect weight can fill in Body Stats** ([#200](https://github.com/TraceApps/nutritrace/issues/200)). Optional (Settings, Wellness, Mirror wellness weight to Body Stats). Your scale reading fills that day's Body Stats weight, so the Diary widget and the Weight goal use it too. A weight you entered yourself is never overwritten. Thanks @tmzhuang for the suggestion. - **Custom nutrients in the Food Editor** ([#201](https://github.com/TraceApps/nutritrace/issues/201)). Nutrients you add in Settings, Nutrients can now be filled in on a food and count toward your diary totals. Thanks @caioqv-dev for the report. - **Share new items automatically** ([#183](https://github.com/TraceApps/nutritrace/issues/183)). Settings, Sharing, Auto Share: set it to Everyone and every food, meal and recipe you create from then on is shared with your group. Existing items are left as they are. Thanks @nomad64 for the suggestion. - **Select All and Select None when managing foods** ([#175](https://github.com/TraceApps/nutritrace/issues/175)). Both follow your current filters and search. Thanks @nomad64 for the suggestion. - **Forward proxy support** ([#177](https://github.com/TraceApps/nutritrace/issues/177)). `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` send all of the server's outgoing requests through your proxy. Thanks @yoyo-san for the suggestion. - **Screen layout details in diagnostic logs.** With Diagnostic Mode on (Settings, Diagnostics), the log also records screen sizes and positions, which helps sort out display problems on phones I can't test on. Nothing you type is recorded. ### Changed - **Open Food Facts search keeps the best match on top** ([#213](https://github.com/TraceApps/nutritrace/issues/213), [#192](https://github.com/TraceApps/nutritrace/issues/192)). Results with a photo and more complete data used to be moved up across the whole page, which could bury the product you searched for when it had no photo. Now OFF's own order wins whenever one result is clearly a better match, and photos and completeness only sort results that match about equally well. With a local OFF mirror, results are sorted by completeness instead of photos, since mirror entries rarely have them. Thanks @NoBackups and @systems-monitor for the reports. - **Typing over a filled-in number replaces it** ([#170](https://github.com/TraceApps/nutritrace/issues/170)). Sheets that open with a value already in the box select it, so you type the new number instead of adding to the old one. The remaining sheets, including the Goals editors, also focus their first box and save on Enter. - **Claude Fable 5.1 in Trace's model list.** It is now the most capable Claude option; Fable 5 stays available, marked as previous. ### Fixed - **iPhone and iPad no longer zoom in when you tap a text field** ([#212](https://github.com/TraceApps/nutritrace/issues/212)). Other devices are unchanged. Thanks @Sanket-Pati-007 for the report. - **Diary's summary bar sits at the bottom of the iPhone screen** ([#208](https://github.com/TraceApps/nutritrace/issues/208)). It could float with a see-through gap underneath. Opening NutriTrace from the Home Screen could also leave a black strip along the bottom until you scrolled; there's now a workaround for that too. Thanks @zen-pavel for the report. - **Copy buttons work over plain http.** On an http address, such as a LAN IP, every Copy button failed. They now work there too. - **Foods panels no longer cover the header when you scroll** ([#217](https://github.com/TraceApps/nutritrace/issues/217)). On a wide window, loading more search results could move the Sources and food details panels over the search bar. Coming back from the food editor also returns you to where you were in the list instead of the top. Thanks @fatman00 for the report. - **Cronometer imports work again.** Cronometer stopped including nutrition in its per-food export on at least some accounts, so importing it failed. NutriTrace now also imports the Daily Nutrition export, as one entry per meal (tick "Include diary group rows") or one per day. Evening entries are no longer saved as morning, supplements and spices are no longer skipped, dates in your account's own format are read correctly, and the daily Total row isn't counted twice. - **"As percent" macro goals show and convert correctly** ([#209](https://github.com/TraceApps/nutritrace/issues/209), [#210](https://github.com/TraceApps/nutritrace/pull/210)). Applying a macro preset could turn a gram target into a huge percentage, and the Goals page showed percent goals as grams. If you hit this, applying your preset again fixes it. Thanks @drunkenpeleg for the report and the fix. - **Health Connect body composition no longer saves as 0** ([#206](https://github.com/TraceApps/nutritrace/issues/206)). Bone mass, lean mass, body fat, temperature, breathing rate, VO2 max and BMR were stored as 0, and BMR was in the wrong unit. Existing zeros are cleared on the next launch and your next sync fills in the real values. Thanks @kgenerozov for the report. - **Health Connect asks for every permission it needs** ([#204](https://github.com/TraceApps/nutritrace/issues/204)). If you only granted some before, you'll see one prompt for the rest the next time you turn it on. Thanks @kgenerozov for the report. - **Per-weekday calorie and macro targets are used correctly** ([#203](https://github.com/TraceApps/nutritrace/issues/203)). Diary's Remaining used the week's highest target instead of that day's, and Statistics now compares against your weekly average. Thanks @kgenerozov for the report. - **Slow loading when a food had a photo saved in it** ([#199](https://github.com/TraceApps/nutritrace/issues/199)). A photo stored inside a food was copied into every diary entry that used it, which could make the diary a 50 MB download. Existing photos are moved to normal files on the next restart. Thanks @tellis82 for the report. - **Diary week strip** ([#180](https://github.com/TraceApps/nutritrace/issues/180), [#168](https://github.com/TraceApps/nutritrace/issues/168)). Each day's bar now uses that day's own goal, so logging activity no longer shifts the others, and editing a serving size updates the day's ring straight away. Thanks @drekkym for the report. - **Foods no longer looks empty while it's loading** ([#178](https://github.com/TraceApps/nutritrace/issues/178)). Thanks @tmzhuang for the report. - **A stuck connection can't block syncing for minutes.** A stalled sync now gives up after 30 seconds instead of holding up every later sync for 10 to 19 minutes. - **USDA foods split monounsaturated and polyunsaturated fat correctly** ([#179](https://github.com/TraceApps/nutritrace/pull/179)). Foods you already imported keep the old split until imported again. Thanks @herver1971 for the fix. - **Wide-screen Diary shows the multi-select buttons** ([#198](https://github.com/TraceApps/nutritrace/issues/198)), and the Foods manage-mode buttons stay at the right edge on very wide windows. Thanks @tellis82 for the report. - **Local OFF mirror search** ([#185](https://github.com/TraceApps/nutritrace/issues/185) to [#191](https://github.com/TraceApps/nutritrace/issues/191)). Products with an apostrophe in the name are found, popular products come before obscure copies, the quality filter no longer hides everything, typed barcodes are found, results load past the first page, words match in any order, and a duplicate barcode can no longer freeze the app. Thanks @systems-monitor for the reports. - **Mealie recipe images load when Mealie is on your home network.** - **Reordering items within a meal is kept after syncing.** - **Password reset and invite links use https behind a reverse proxy.** - **Turning on user management keeps everything from single-user mode.** Activity, fasting, Trace chat history, wearable data and wearable connections were left behind when the first account was created; they now move to it. Instances that already switched get them back on the next start. - **Deleting an account removes its wearable data and connections**, and single-user mode shows all of its wearable data again. - **MCP tools refuse dates that don't exist.** A date such as 2026-02-31 read back as an empty day, and logging to it created a diary entry for it. It's now an error. ### Translations - **German**, fully translated on [Weblate](https://hosted.weblate.org/projects/nutritrace) by KAiSER. Choose it in Settings, Regional & Units, Language. A few labels are still in English in every language because they haven't been made translatable yet. ### Security - **Backups can no longer be downloaded from the uploads folder.** When scheduled backups were on, a backup file could be fetched by anyone who knew or guessed its name, without signing in. If your instance is on the internet and had backups turned on, check your access log for requests to `/uploads/backups/`. - **Uploaded files can no longer run as web pages.** Files are stored under an extension that matches what they actually are, and everything under `/uploads` is served in a way that can't run scripts. - **Dependency updates:** nodemailer 9.1.1 ([GHSA-8m3c-c648-2xjj](https://github.com/advisories/GHSA-8m3c-c648-2xjj), high), multer 2.4.0 ([GHSA-wc9g-mqfw-jrwm](https://github.com/advisories/GHSA-wc9g-mqfw-jrwm), [GHSA-qfvm-cv95-jqjf](https://github.com/advisories/GHSA-qfvm-cv95-jqjf), [GHSA-535w-7cp7-47q4](https://github.com/advisories/GHSA-535w-7cp7-47q4), high), fast-uri 3.1.8 ([GHSA-5jgf-p345-68v8](https://github.com/advisories/GHSA-5jgf-p345-68v8) and three related, high), hono 4.13.8, qs 6.16.0, adm-zip 0.6.1 ([GHSA-vwc7-r8mq-g2x9](https://github.com/advisories/GHSA-vwc7-r8mq-g2x9)) and devalue 5.9.4 ([GHSA-9rgm-9g3h-6x36](https://github.com/advisories/GHSA-9rgm-9g3h-6x36)), all moderate, plus updated build tools. `npm audit` reports 0 vulnerabilities. --- ## [1.3.0-dev02] - 2026-09-17 (pre-release) Second dev pre-release of the 1.3.0 minor. Adds MCP date-range reads, outgoing webhooks and a general-purpose REST API, import of Cronometer's Daily Nutrition export, and a round of iPhone fixes, plus security fixes for uploads and five server dependencies. ### Added - **MCP date-range reads** ([#215](https://github.com/TraceApps/nutritrace/pull/215)). New `list_diary_entries_range` and `get_daily_totals_range` tools, and optional `start`/`end` dates on `get_recent_foods` and `get_recent_meals`. Bounds are inclusive and either can be left out to leave that side open; with neither, the range tools cover the last 90 days. `list_diary_entries_range` returns at most 366 logged days per call, since full item lists get large; `get_daily_totals_range` has no limit. The existing single-day tools are unchanged. Thanks @kgenerozov. - **General-purpose public REST API** at `/api/v1/diary`, `/api/v1/goals`, and `/api/v1/meals`, for your own scripts and automations rather than the sister-app federation contract the rest of `/api/v1` documents. Off by default (`PUBLIC_API_ENABLED=1`; `PUBLIC_API_WRITE_ENABLED=1` additionally unlocks logging a food, water, a meal, or a body stat). Reuses the `mcp:read`/`mcp:write` token scopes MCP already defines, one token works for both interfaces. See `docs/public-api.md`. - **Outgoing webhooks.** Configure a target URL in Settings, Webhooks and NutriTrace fires a signed HTTP POST the instant a food is logged, water is logged, a body stat is logged, or a daily nutrition goal is reached. 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`). See `docs/webhooks.md`. - **Diagnostic logs now record screen layout.** With Diagnostic Mode on (Settings, Diagnostics), NutriTrace writes a one-line snapshot of the layout at startup, on each page change, when a field gains or loses focus, when the keyboard or browser toolbar changes the visible area, on zoom and rotation, on returning to the app, and when scrolling stops. Each line has the screen, window, visible-area and page sizes, zoom level, the device's safe-area insets, where the tab bar and Diary's summary bar sit, and the focused field's font size. It helps sort out display problems on phones I can't test on directly, like the iPhone reports in [#208](https://github.com/TraceApps/nutritrace/issues/208) and [#212](https://github.com/TraceApps/nutritrace/issues/212): turn logging on, reproduce the problem, then Copy the logs and send them. Only sizes and positions are recorded, never what you type, and nothing is measured while logging is off. ### Changed - **Claude Fable 5.1 in Trace's model list.** It is now the most capable Claude option; Fable 5 stays selectable, marked as previous. ### Fixed - **iPhone and iPad no longer zoom in when you tap a text field** ([#212](https://github.com/TraceApps/nutritrace/issues/212)). Safari on iOS zooms the page into any field whose text is smaller than 16px as soon as it gets focus, and leaves it zoomed, which pushed sheets like Body Stats partly off screen. NutriTrace's fields are 15px, so on iPhone and iPad they are now 16px (the Foods search, Settings search, Body Stats and every other field). Fields that are larger on purpose, like the calorie box in Quick Calories, keep their size. Android, desktop and Mac are unchanged. Thanks @Sanket-Pati-007 for the report. - **Copy buttons work when NutriTrace is opened over plain http** ([#212](https://github.com/TraceApps/nutritrace/issues/212)). Browsers only provide the clipboard to secure (HTTPS) pages, so on an http address, such as a LAN IP or a tailnet without HTTPS, every Copy button failed: diagnostic logs, the calibration export, API tokens, webhook secrets, the Docker update command and the wellness redirect URIs. They now fall back to the browser's older copy command, which still works there. The wellness redirect URI buttons also show an error now if copying fails, instead of doing nothing. - **Open Food Facts search keeps its best matches at the top** ([#213](https://github.com/TraceApps/nutritrace/issues/213)). NutriTrace reorders each page of OFF results so entries with a photo and more complete data come first, but it did that across the whole page, overriding how well each result matched the search. A product with no photo and sparse data, common for regions OFF contributors photograph less, could sink far down the list: searching "Blue Label Marie Biscuits", which OFF ranks first by a wide margin, put it 38th, under a beef steak. Results now keep OFF's order wherever its relevance scores are clearly different, and photos, completeness and Nutri-Score only sort results that match about equally well (such as the dozen identical "Nutella" entries). A Nutri-Score of "unknown" no longer counts as having one. Separately, the check added in #192 to tell a local OFF mirror's results from public ones didn't work, because OFF's public search responds in the same format, so public results were being sorted as mirror results; mirror results are now recognised by having no relevance score. Local mirror ordering is otherwise unchanged. Thanks @NoBackups for the report and the digging. - **Macro goals set "As percent" now show and convert correctly** ([#209](https://github.com/TraceApps/nutritrace/issues/209), [#210](https://github.com/TraceApps/nutritrace/pull/210)). Applying a macro preset kept a stale "As percent" flag, so the preset's gram target was read as a percentage and Diary and Statistics showed it hugely inflated (a 137 g protein target became 626 g). Presets now clear the flag. Separately, the Goals page's Macros card, preview ring and preset chip read a percent goal as grams (30% showed as "30 g"), and ticking "As percent" only changed the label instead of converting the value. Both now convert, so Goals, Diary and Statistics agree. If you already hit the preset bug, re-applying your preset once clears it. Thanks @drunkenpeleg for the report and the fix. - **Cronometer import now handles the Daily Nutrition export, because the per-food one no longer has nutrition.** Cronometer has stopped including the nutrient columns in its "Export Food & Recipe Entries" file, at least on free accounts: it arrives as just `Day, Time, Group, Food Name, Amount, Category`, confirmed on three separate accounts in August and September 2026. NutriTrace previously rejected that file with a generic error and its instructions named an export button ("Servings") that Cronometer had renamed, so there was no way to get Cronometer data in at all. NutriTrace now imports **Export Daily Nutrition** as well: tick "Include diary group rows" and each day's meals import as one entry per meal, otherwise you get one entry per day. That is coarser than per-food, but it is the only export Cronometer still fills with numbers. The per-food import stays for files that do have nutrition, and a per-food file without it now says exactly that and points to the export that works. Two bugs found by running real exports through the importer are fixed along the way: Cronometer writes 12-hour times ("7:45 PM") and the AM/PM was being dropped, so every evening entry was saved as morning; and entries Cronometer exports with blank calories, such as supplements and most spices, were skipped along with their vitamins and minerals. Dates are no longer assumed to be month-first, since Cronometer writes them in the account's own format, and the day's "Total" row is left out so a day's calories are not counted twice (recognised by its numbers as well as its name, so a translated total doesn't slip through). ZIP detection reads the file's contents instead of trusting its name. - **Diary's summary bar sits flush with the bottom of the screen on iPhone** ([#208](https://github.com/TraceApps/nutritrace/issues/208)). On an iPhone the summary bar could float with a see-through strip below it. With the tab bar on, the two bars were positioned separately (the summary bar at a distance worked out from the tab bar's height plus the home-indicator area), so anything that moved one without the other pulled them apart; they now sit together in one container anchored to the bottom of the screen. With the menu button instead of the tab bar, the bar was lifted clear of the home indicator but nothing painted the space underneath, so the diary showed through; the bar's own background now fills it. Layout on Android and desktop is unchanged. Thanks @zen-pavel for the report and the screenshots. - **Workaround for a black strip along the bottom of the iPhone home-screen app** ([#208](https://github.com/TraceApps/nutritrace/issues/208)). When NutriTrace is opened from the Home Screen, iOS can size the app shorter than the screen by the height of the status bar, leaving a black strip at the bottom (about 62px on a Pro Max) until something is scrolled. The reporter's screenshots show exactly that, and the strip is gone in the one taken after scrolling. On Home Screen launches only, the page now starts out a full screen tall, which should make iOS correct the size straight away instead of waiting for a scroll. This could not be tested on an iPhone here, so the new layout lines in the diagnostic logs record the screen, window and page heights to confirm whether it works. Safari tabs, Android and desktop are unaffected. ### Security - **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. - **Uploaded files can no longer be served as web pages.** Uploads were saved under whatever file extension the uploader sent, and the extension decides what type a file is served as, so a file uploaded as audio but named `.html` would be served as a page from your NutriTrace address. Uploads are now saved under an extension that matches their actual type (anything unrecognised becomes `.bin` and downloads instead of opening), and every file under `/uploads` is served with `X-Content-Type-Options: nosniff` and a sandboxing content security policy, so nothing stored there can run script. Uploaded images still load as before. - **Server dependency updates clear five packages with known vulnerabilities.** multer 2.4.0 ([GHSA-wc9g-mqfw-jrwm](https://github.com/advisories/GHSA-wc9g-mqfw-jrwm), [GHSA-qfvm-cv95-jqjf](https://github.com/advisories/GHSA-qfvm-cv95-jqjf), [GHSA-535w-7cp7-47q4](https://github.com/advisories/GHSA-535w-7cp7-47q4), high: denial of service through crafted multipart uploads; [GHSA-qvfw-j98x-7q72](https://github.com/advisories/GHSA-qvfw-j98x-7q72), low), fast-uri 3.1.8 ([GHSA-5jgf-p345-68v8](https://github.com/advisories/GHSA-5jgf-p345-68v8), [GHSA-f65p-4m7j-42xc](https://github.com/advisories/GHSA-f65p-4m7j-42xc), [GHSA-fph4-wmhf-6fwf](https://github.com/advisories/GHSA-fph4-wmhf-6fwf), [GHSA-jqff-g426-hqxp](https://github.com/advisories/GHSA-jqff-g426-hqxp), high: URL host confusion), hono 4.13.8 (three moderate advisories), qs 6.16.0 (two moderate advisories), and adm-zip 0.6.1 ([GHSA-vwc7-r8mq-g2x9](https://github.com/advisories/GHSA-vwc7-r8mq-g2x9), moderate: extraction follows symlinks). All are minor or patch updates; the full test suite passes on them, and uploads were retested against a running server with identical results. `npm audit --omit=dev` now reports 0 vulnerabilities for both the app and the server. Pulling the new image is all that's needed. --- ## [1.3.0-dev01] - 2026-09-10 (pre-release) Minor release. Big themes: two-way catalog federation with CookTrace, opt-in diary day completion, a cluster of Open Food Facts local-mirror search fixes, and correctness fixes for Health Connect body composition and per-weekday calorie targets. CookTrace federation, in short: if you run CookTrace alongside NutriTrace, the two catalogs now talk to each other. Recipes you build in CookTrace import into NutriTrace's recipe catalog, one at a time from the Foods search or all at once from Settings. Pantry items come across into the foods library the same way, individually through a new CookTrace chip on the Foods tab or in bulk. Imported rows keep a link back to their source, so an edit on the CookTrace side surfaces a refresh prompt instead of silently drifting, and re-importing updates the same row rather than duplicating it. Opt-in and per-user, using a token you mint in CookTrace. Community translations are open on Weblate. > **Upgrade note.** The foods table gains three federation columns automatically on first boot, no action needed. Pantry import additionally requires your CookTrace instance updated to a build carrying the `read:pantry` scope, plus a freshly minted CookTrace token with that scope ticked. Existing recipe-pull tokens keep working for recipes. > **Version note.** This cycle was previously numbered 1.2.1. Since it adds features rather than only fixing bugs, it is renumbered to 1.3.0 per semver. The `v1.2.1-dev01` through `v1.2.1-dev04` pre-releases are superseded by this one; everything they contained is folded into the notes below. ### Added - **Import CookTrace recipes.** On the Foods screen's Recipes tab, a CookTrace chip searches your CookTrace instance and opens the picked recipe in the Meal Editor, pre-filled with its ingredients, servings, photo and totals. Settings, Connected Services, CookTrace, Import All Recipes does the whole catalog at once, with live progress and per-recipe failures counted as skips rather than aborting the batch. Recipes keep the totals CookTrace computed: its rollup does density-aware conversion (it knows "2 cups flour" in grams via `g_per_cup`), which cannot be reproduced from the ingredient rows alone, so saving preserves those figures instead of substituting a weaker items-derived sum. Editing the ingredients yourself, or running Recompute, hands the calculation back to NutriTrace. Ingredient nutrition resolves through the CookTrace pantry the way you would expect, with a generic taking its designated variant's values, a variant with no values of its own inheriting from its generic, calories derived from carbohydrate, protein and fat when only macros were recorded, and an unlinked ingredient still matching a same-named pantry row. Each ingredient carries its pantry photo. - **Import CookTrace pantry items.** A CookTrace chip on the Foods tab searches your pantry and opens the picked row in the Food editor; Settings, Connected Services, CookTrace, Import Pantry Items brings the whole pantry across in one action. Generic parents that have variants are skipped, since the variants carry the real nutrition and a bare "Flour" next to "Flour, Bread" would be unusable; standalone items and variants both come across, with variants named "Parent, Child" so they read cleanly. Searching matches that composed name, so a variant stored as "Bread" under a "Flour" generic is still found by typing `flour`. A row whose barcode already matches a food you created yourself is left untouched rather than overwritten, and reported separately so the counts reflect what actually changed. Requires a CookTrace token carrying the new `read:pantry` scope. - **Imported rows stay linked to their source.** Re-importing anything from CookTrace updates the row you already have instead of creating a second copy. Open a CookTrace-imported recipe and NutriTrace quietly checks the source in the background: if CookTrace's copy is newer, a banner offers to pull the latest name, ingredients and totals, and it never overwrites without your say-so. If the source recipe has been deleted outright, a warning banner offers **Unlink** (keep your copy, drop the CookTrace link) or **Delete** (remove your copy). Being offline never triggers that banner. The provenance itself, including the From CookTrace badge and the link back to the original, survives a full-backup restore and propagates to your other devices through the normal sync path. - **Diary day completion** ([#207](https://github.com/TraceApps/nutritrace/issues/207)). Opt-in via Settings, Diary, Show Day Completion (off by default; when off the diary looks exactly like it did before this feature). When on: a check toggle in the date bar closes the current day, marked days show a green check on the week strip / date picker / date-bar label, Statistics grows a "Marked complete: N of M days" KPI in the right rail plus a row of tiny green dots below the chart's x-axis, the weekly summary push and email both include a "Marked complete: X of 7 days" line, and the Android bedtime notification carries a Close today quick action that marks the day without opening the app. Per-meal companion: each meal card gets a check toggle, day-close confirms empty-and-unmarked slots (fasters mark breakfast skipped once and stop being re-prompted), and marking every populated meal auto-closes the day. Cross-device sync uses offline-safe rules: an offline device pushing null cannot clear a mark the other device set, and per-meal sets union-merge across devices. Storage adds `completed_at TEXT` and `completed_meals TEXT` on the diary row (both covered by full-backup export/import and by cross-device sync). First time you mark a day complete without meal reminders enabled, a one-time tip points at Settings, Notifications so you can catch a missing snack earlier next time. - **Average Heart Rate card in Wellness** ([#205](https://github.com/TraceApps/nutritrace/issues/205)). Health Connect was already ingesting `avg_heart_rate` into local `wellness_data`, but Wellness had no card definition for it, so the value silently existed on disk without a surface. Now shows as a distinct card in the Heart group alongside Resting Heart Rate. Distinct from Resting HR (fully at rest); this is the day's average from your wearable, useful for spotting an unusually high or low day. Thanks @kgenerozov. - **Opt-in "Mirror wellness weight to Body Stats" toggle** ([#200](https://github.com/TraceApps/nutritrace/issues/200)). Settings → Wellness has a new toggle (off by default). When on, weight readings that come in via Health Connect also populate that day's diary Body Stats weight, so the Body Stats widget on Diary and the Weight goal on Goals both light up from your scale reading. Manual Body Stats entries are never overwritten; the mirror only fills days that are currently empty. Withings has always applied this mirror unconditionally, so its behavior stays the same regardless of the toggle. - **Custom nutrients now appear in the Food Editor** ([#201](https://github.com/TraceApps/nutritrace/issues/201)). Nutrients you create in Settings → Nutrients (Copper, Manganese, Selenium, whatever) now show up in the Food Editor's nutrition grid, accept a per-100g value, save correctly, re-hydrate on next edit, flow through into diary totals, and participate in the linked-portion proportional-scaling. Bulk food import and the MCP create_food tool still recognize built-in ids only; follow-ups if anyone asks. - **Auto Share default for new foods, meals, and recipes** ([#183](https://github.com/TraceApps/nutritrace/issues/183)). Settings → Sharing has a new "Auto Share" block above the existing "Bulk Share" one. Set the Visibility dropdown to Everyone and every new food, meal, and recipe you create (in the app or via MCP) is shared with your group automatically. Existing items are left alone; Bulk Share is still the tool for retroactive changes. Only Private and Everyone are exposed for now; "share with specific users" as an automatic default is a separate follow-up. Admin's global sharing toggle still wins: when sharing is off server-wide every new item is forced to Private regardless of the per-user setting. - **Select All / Select None on the Foods manage-mode bar** ([#175](https://github.com/TraceApps/nutritrace/issues/175) follow-up). Both target the current filtered list, so source, category, and search filters scope the selection instead of blanket-selecting the whole library. - **Forward-proxy support for outbound server requests** ([#177](https://github.com/TraceApps/nutritrace/issues/177)). `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` (standard `curl` / `requests` convention, lowercase variants also honored) route every outbound fetch through the configured proxy. No changes at call sites. - **Autofocus + Enter-to-submit on the remaining sheets** ([#170](https://github.com/TraceApps/nutritrace/issues/170)). Foods quantity prompt, Foods multi-portion, and Diary edit-item sheets focus the first numeric input on open and submit on Enter, matching Quick Calories / Activity / Water / Body Stats which already had this. - **Chinese (Simplified) translation.** Community contribution via Weblate. ### Changed - **Sheets now select the existing value on focus** ([#170](https://github.com/TraceApps/nutritrace/issues/170) follow-up). Typing over a pre-filled input replaces it in one keystroke instead of appending, matching the Body Stats weight-edit pattern. Applies to the Foods quantity prompt, Foods multi-portion, Diary edit-item, Quick Calories, Add Activity (name + duration), plus the two Goals editor sheets (per-nutrient goal and Water goal). The Goals editor and Water goal sheets also picked up autofocus and Enter-to-save which they were missing. ### Fixed - **Mealie images now self-host when your Mealie instance is on the same LAN.** The image self-hosting step has an SSRF guard that refuses to fetch from any private or loopback host (10.x, 172.16.x, 192.168.x, 127.x, link-local, IPv6 ULA). On a home-server setup where NutriTrace and Mealie sit on the same network, that silently blocked every recipe image: the raw private-IP URL got stored instead, and any client whose network could not reach that address showed an empty slot. NutriTrace now trusts the Mealie base URL you saved in Settings for image fetches only, since configuring it proves you can reach the host. Instances on genuinely separate networks still cannot fetch the image, because the server has no route to the private address; the recipe itself imports either way. - **Health Connect body composition no longer lands as 0** ([#206](https://github.com/TraceApps/nutritrace/issues/206)). The pinned `@devmaxime/capacitor-health-connect` 1.1.0 only custom-converts a few record types (Weight, Steps, Sleep, RestingHeartRate). Everything else arrives in JavaScript as the AndroidX `connect-client` Kotlin `toString()` string. Bone Mass, Lean Body Mass, BodyFat, Body Temperature, Respiratory Rate, Vo2 Max, and Basal Metabolic Rate previously read those strings via `latest.mass?.inKilograms || 0` and stored 0 in `wellness_data`, so a real 25.4 kg Lean Mass rendered as "0.0 kg". Also, BMR arrived in Watts and was silently written as kcal/day. Fixed with a shared parser layer (`src/lib/health-connect-parsers.js`) that handles every Kotlin string shape (`mass=1.5 kilograms`, `Mass{value=1.5, unit=KILOGRAMS}`, `Mass(inKilograms=1.5)`, no-space `1.5kg`, POUNDS conversion, and the Power/Percentage/Temperature analogues). BMR Watts convert to kcal/day using the correct physical factor (1 W = 20.65 kcal/day). Values that fail to parse are OMITTED (`missing != zero`) so nothing bogus gets stored. **One-shot cleanup** on next app launch prunes existing `wellness_data` rows saved as 0 for these metrics; the next Health Connect sync writes the correct values. Thanks @kgenerozov for the crisp repro and the AndroidX / plugin-internals detective work. - **Health Connect: partial grants no longer skip missing permissions** ([#204](https://github.com/TraceApps/nutritrace/issues/204)). The permission request used to return early as soon as any read permission was granted, so a user who granted Steps or Sleep first never got asked for Heart Rate, Resting Heart Rate, or the four other types the read code queries (Distance, Total / Active calories, Resting Heart Rate). It now reconciles the desired list against currently granted permissions and requests just the missing ones, with a per-name fallback if the plugin rejects the batch (guards against the `@devmaxime` plugin's whole-batch-reject when a name isn't in its record-type map). **User-visible behavior change**: users who previously granted only a subset will see one Health Connect dialog asking for the missing types the next time they toggle Enable in Settings, Wellness. Fully-granted users see no dialog. Thanks @kgenerozov for the repro. - **Diary Remaining and Statistics goal line now honor per-weekday calorie / macro targets** ([#203](https://github.com/TraceApps/nutritrace/issues/203)). With "Different target per weekday" enabled, both surfaces were reading the weekly peak instead of the day's actual target: a Sunday=2400 / Tuesday=2700 setup showed 404 kcal Remaining on Sunday after 2296 kcal logged, when it should have been 104. Diary now resolves the target from `days[weekday]` based on the viewed calendar date (parsed as local midnight so it can't flip across time zones), and Statistics uses the seven-day average of `days[]` for its range-based chart line and "vs goal" KPI (peak was misleading for a period summary). Percent-based macros derive from the correct per-day calorie target. Shared (single-target) users see no change. Thanks @kgenerozov for the crisp repro and root-cause trace. - **/api/diary payload was ballooning to 50MB when a food had a base64 image** ([#199](https://github.com/TraceApps/nutritrace/issues/199)). The diary hydrator was stamping a food's `img_url` onto every diary item referencing it across every date; a 430KB camera photo on one recipe would multiply into tens of megabytes across a busy library. Three-part fix: the hydrator now skips `data:` URLs so it can't amplify them, the sync-push path routes incoming `img_url` values through the same `/uploads/` localizer that direct POST already uses (so native camera-photo foods stop landing raw), and a one-shot boot migration converts any existing `data:` `img_url` in foods and meals to real `/uploads/` files. On next restart your diary thumbnails reappear automatically with no manual re-pick. Diagnosed by @tellis82. - **Foods appeared empty after leaving and returning to the section** ([#178](https://github.com/TraceApps/nutritrace/issues/178)). A slow initial `/api/foods` response rendered the "No foods yet" empty state over the user's real library until the fetch resolved. Foods now holds a loading spinner while the initial load is in flight so the empty state only appears when the library is genuinely empty. - **Diary week strip bars shifted whenever activity was logged, and drifted again when navigating between days** ([#180](https://github.com/TraceApps/nutritrace/issues/180)). The strip was applying the current day's activity-adjusted goal as a single denominator across every column. Each day now resolves its own goal from its own dynamic TDEE (in dynamic mode) plus its own effective active kcal (respecting the manual / wearable policy). Logging or removing activity on any day refreshes the strip immediately. - **Diary week strip ring was stale after editing a serving size** (#168 follow-up). The strip's refresh signature now digests per-item scaled calories through the same helper the strip renders with, so any portion / quantity / unit swap updates the day's ring immediately without a reload. - **Sync push and pull could hold the sync lock open for 10 to 19 minutes when a connection stalled.** Both requests now abort after 30 seconds, so a wedged link surfaces as a visible sync failure instead of blocking every subsequent pull-to-refresh for the OS TCP-retry window. - **Foods manage-mode buttons floated inward at ≥1440px instead of anchoring to the right edge** ([#175](https://github.com/TraceApps/nutritrace/issues/175) follow-up). The detail preview pane hides while manage mode is active, and the manage bar drops its 452px right inset so the buttons sit flush with the viewport at every width. - **USDA import stored total polyunsaturated fat under monounsaturated, and read only oleic acid for the monounsaturated figure** ([#179](https://github.com/TraceApps/nutritrace/pull/179)). FoodData Central nutrient IDs `1292` (total monounsaturated) and `1293` (total polyunsaturated) now map correctly; ID `1268` (MUFA 18:1, a single fatty acid, not a category total) is no longer used. Totals for fat and energy were never affected, only the split between the two unsaturated categories. Existing rows keep the old values until they are re-imported. Fix contributed by @herver1971. - **Multi-select cancel + delete were invisible in the wide-screen Diary layout** ([#198](https://github.com/TraceApps/nutritrace/issues/198)). The wide-layout rule was hiding the whole top-right action row (correct for the normal water / summary / body-stats icons since the right rail carries parity for those), but the multi-select cancel + delete live in the same row and the rail has no select-mode UI, so wide-screen users had no visible way to act on a selection. Row now stays visible whenever select mode is active. Diagnosed by @tellis82 to the exact selector. - **Diary merge preserves client-side reorder** (port of the same fix landed in LiftTrace on 2026-08-31). If you drag an item to reorder within a meal on the client, the server merge now honors that order instead of restoring the prior sequence on the next pull. - **Reset / invite email links and test-email origin now honor `X-Forwarded-Proto`** so instances behind a reverse proxy generate the correct HTTPS URLs instead of falling back to `http://`. - **Forward-proxy setup: friendlier error when the URL is missing a scheme** ([#177](https://github.com/TraceApps/nutritrace/issues/177) follow-up from @yoyo-san). Bare `user:pass@host:port` in `HTTP_PROXY` / `HTTPS_PROXY` fails at startup with undici's `Invalid URL` message. Startup log now names the fix (add the `http://` prefix, embed credentials in the URL) and links to the docs. Docs also picked up an explicit "Value format" section covering the scheme requirement, authenticated-proxy syntax with URL-encoded credentials, and the credentials-in-logs caveat. - **Product names with apostrophes were silently dropped from mirror search results** ([#185](https://github.com/TraceApps/nutritrace/issues/185)). Under `@duckdb/node-api` 1.4 the LIST columns come back wrapped as `DuckDBListValue` (real array under `.items`, struct fields under `.entries`), and the old `_asArray` fell through to the string fallback, which stringified the wrapper into DuckDB's SQL repr and doubled the embedded single quotes. Neither the JSON parser nor the Python-repr parser accepted that escape, so any product with an apostrophe in the name (Ben & Jerry's, KELLOGG'S, Dunkin', anything possessive) came back with an empty `product_name` and got filtered out client-side. `_asArray` now recursively unwraps `.items` and `.entries` before the string fallback, and the downstream field readers already tolerated both shapes. - **Search results within a rank were coming back in effectively random order, burying popular products** ([#186](https://github.com/TraceApps/nutritrace/issues/186)). The parquet search branch's only sort key was the prefix-match rank; within a rank the parallel table scan filled pages in scan order, which put near-zero-scan regional clones ahead of mainline flagship products. Added `popularity_key DESC NULLS LAST` as the within-rank tiebreak when the column exists (detected once at init from `information_schema` so custom parquets without it don't binder-error), with `code ASC` as a deterministic fallback. - **The OFF quality-tier filter classed every mirror row as "Unknown" and hid all results under any tier selection** ([#187](https://github.com/TraceApps/nutritrace/issues/187)). `_toOffProduct` wasn't forwarding the `completeness` score even though the client's tier bucketer + result ranker both expected it. Now passed through on both the parquet and legacy `.duckdb` branches, guarded by a type check so rows without the field pass `undefined` (which the client already null-checks). - **Typed or pasted barcodes in the search box returned "No results" even when the mirror had the product** ([#188](https://github.com/TraceApps/nutritrace/issues/188)). The scanner path uses a separate exact-code endpoint; text search never matched the `code` column. Added to the WHERE on both branches, so pasted barcodes now resolve and every plain-HTTP or desktop install (no camera scanner) gets a working barcode lookup. - **Mirror search never loaded more than the first page even when hundreds of results matched** ([#189](https://github.com/TraceApps/nutritrace/issues/189)). The response `count` was `rows.length` (page size), so the client's `hasMore` math could never fire. Now runs a parallel `COUNT(*)` with the same predicates and returns the real total (`Number()`-wrapped so the BigInt serializes). Both `ORDER BY` clauses also picked up `code ASC` as a final unique tiebreak so pages partition cleanly without duplicating or dropping rows across boundaries. - **Multi-word searches only matched exact adjacent phrases in the exact typed order** ([#190](https://github.com/TraceApps/nutritrace/issues/190)). The whole query was one `%q%` pattern, so "dunkin croissant" would silently miss a product named "Bacon Egg and Cheese Croissant (Dunkin')" because the two words are not adjacent, and reversing the words changed results. `searchByName` now tokenizes on whitespace and ANDs per-token substring matches against name, brands, and code. The prefix-rank CASE still uses the full phrase so exact-phrase starts sort first. - **A search returning two rows with the same barcode froze the entire app until reload** ([#191](https://github.com/TraceApps/nutritrace/issues/191)). The stock OFF snapshot contains 60 duplicated codes across 4.7M rows (two genuinely different products can share a barcode via OFF data errors), which triggered Svelte's `each_key_duplicate` on the search-results list and killed the render loop. Both search-result each blocks (`visibleApiResults` and the all-mode `_allModeItems`) now suffix their keys with the list index so a duplicate key is physically impossible. Duplicate rows stay visible on purpose so users can pick which of two barcode-collision entries matches their product. - **Enabling user management no longer strands data written in single-user mode** ([TraceApps/docs#2](https://github.com/TraceApps/docs/issues/2)). An instance running with no accounts writes every row under a placeholder owner. Registering the first account only re-parented foods, meals and the diary, so the Activity log, fasting sessions, Trace chat history, wearable data (wellness and workouts) and the Fitbit / Google Health / Withings / Garmin connections all became invisible to the new admin. All of them are now claimed, in one transaction. The same handover runs whether the first account is created with a password or by the first OIDC sign-in; both paths now share one implementation instead of keeping separate copies that drifted. - **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, since the handover only ever ran while the first account was being created. Startup now adopts them, once, when exactly one account exists. Zero accounts is ordinary single-user mode and is left alone; two or more is reported in the log rather than guessed at. - **Deleting an account no longer leaves its wearable data and OAuth tokens in the database.** `wellness_data`, `workouts` and the Fitbit / Google Health / Withings / Garmin token rows carry no foreign key to `users`, so they survived both the admin delete-user action and self-service account deletion. Live refresh tokens for a deleted account stayed on disk. All four removal paths (self-delete, admin delete, disable user management, lockout recovery) now clear them. Genuinely unowned rows are untouched, since disabling returns the instance to single-user mode where they must stay readable. - **`/api/wellness/latest` no longer hides wearable metrics in single-user mode.** It read only the `NULL` owner while the pollers write `0`, so every Fitbit-sourced metric was invisible to that endpoint. Now reads both, matching the equivalent query in `routes/withings.js`. Wearable tables receive two different placeholder owners (the pollers write `0`, the Android sync writes `NULL`) and carry UNIQUE indexes on `user_id`, so the two sets are merged rather than colliding and aborting the registration. The same incomplete list existed a second time in the OIDC first-login bootstrap, so an instance whose first account arrives via SSO had the identical bug; both paths now share one implementation. ### Security - **nodemailer bumped to 9.1.1** ([GHSA-8m3c-c648-2xjj](https://github.com/advisories/GHSA-8m3c-c648-2xjj), high). `resolveContent()` on a `MailMessage` bypassed `disableFileAccess` / `disableUrlAccess` when called with the legacy signature. NutriTrace only sends templated transactional mail with no caller-supplied attachment paths, so practical exposure was low, but the patched release is a minor bump within 9.x with no migration. `npm audit --production` now reports 0 vulnerabilities. --- ## [1.2.0] - 2026-08-23 Minor release. Big themes: a first-pass wide-screen redesign across every section of the app, Model Context Protocol support for AI agents, a proper in-app update notification loop that actually tells you when a new version is out, the AI Assistant learning to be terse and use plain text, comma decimal separator supported everywhere numbers get typed, and a batch of fixes for pain points reported by the community since v1.1.2. Community translations are open on Weblate. ### Added - **Wide-screen redesign at ≥1280px, first pass.** Every routed section (Diary, Foods, Statistics, Goals, Wellness, Settings) plus the Food and Meal editors got a proper desktop layout in this cycle. The goal is a genuinely useful experience on the extra horizontal real estate instead of a stretched-out phone stack. This is the starting point, not the finish line. Wider breakpoint coverage (foldables and small tablets currently fall back to the mobile layout), a per-user layout-width picker, and the full two-pane Settings shell are all on the backlog for future releases. `Force Mobile Layout` remains the opt-out for anyone who prefers the compact stack on wide screens. - **Model Context Protocol (MCP) server** ([#103](https://github.com/TraceApps/nutritrace/issues/103)). NutriTrace now exposes a read + write MCP endpoint so Claude, Continue, and other MCP-aware agents can search your food catalog, log entries, propose meals, and read diary history without a separate API dance. Read tools cover foods (search / list / detail), meals (search / recent / detail), diary (by date / range / averages), and body stats. Write tools cover diary logging, quick calories, food catalog creation, and meal creation. All gated behind the `mcp:read` / `mcp:write` API-key scopes. Tolerates common nutriment aliases (`protein`, `carbs`, `vitamin-b12`) automatically. See [/reference/mcp-tools/](https://traceapps.github.io/docs/reference/mcp-tools/) for the full tool list. Thanks to @javydekoning for the meal-side tool feedback. - **`POST /api/v1/activity` endpoint** ([#154](https://github.com/TraceApps/nutritrace/issues/154)). External services can log a manual activity entry via API using the new `write:activity` scope. Supports `external_id` for idempotent posts. - **Comma decimal separator** across all numeric inputs ([#160](https://github.com/TraceApps/nutritrace/issues/160)). Portion, quantity, nutrition per 100 g, weight and body measurements, water custom amount, Quick Calories, recipe amount, and all goal targets accept either `,` or `.`. Comma-locale users can type "2,5" without fighting the form. - **Shared image cropper** across Food / Meal / Recipe editors ([#159](https://github.com/TraceApps/nutritrace/pull/159), thanks @librarian). Resize handle, touch support, unified visual. Previously each editor had its own cropper and the Recipe one was stuck top-left with no resize. - **Editor draft persistence.** If Android's low-memory killer terminates the app while you're editing a food or meal (Samsung's camera-mode lmkd is the usual culprit), your in-progress typing is restored on next open. A banner surfaces the restored draft with a Discard button so you can wipe it in one tap. Photos persist too via IndexedDB. - **Screen wake lock** during AI-Assistant and Scan-Label requests ([#158](https://github.com/TraceApps/nutritrace/issues/158)). Screen no longer sleeps mid-request and drops the connection. ### Changed - **In-app updates now actually tell you when a new version is out.** The update-check system was already present but silent in most cases: the PWA silently auto-swapped, Android only surfaced either a banner or a notification (not both), the check ran at most once per 24h, and there was no in-app cue anywhere else. Now: PWA users get a "Reload" prompt when a fresh bundle is deployed, Android shows both a banner AND a one-shot OS notification, check frequency is configurable in Settings → Updates (Hourly / Every 4 Hours / Every 12 Hours / Once a Day / Manual Only, default every 4 hours), a re-check fires when the tab regains focus so a laptop resumed from sleep updates immediately, and a small dot appears on the Settings nav icon while an update is pending. Skip-this-version clears every surface for that version in one tap. - **AI Assistant is quieter and plain-text by default** ([#163](https://github.com/TraceApps/nutritrace/issues/163)). Response length defaults to 2 to 4 sentences. Markdown is off so `**bold**` and `##headings` no longer show as literal characters. - **AI Assistant asks fewer questions before logging** ([#163](https://github.com/TraceApps/nutritrace/issues/163)). A single substring hit in your local foods auto-logs the same way an exact hit did. The prior "6+ hits silently discarded" bug is also fixed. - **Manual estimate path in the AI Assistant** ([#163](https://github.com/TraceApps/nutritrace/issues/163)). Asking "don't search, just estimate it" routes to a review card with the estimated nutrition instead of running a catalog search. - **Confirmation before clearing the AI chat history.** Was a one-tap wipe with no way back. ### Fixed - **Add Activity sheet crashed on Android** ([#162](https://github.com/TraceApps/nutritrace/issues/162), thanks @knew3be for the diagnosis). A missing method on the native backend threw before the sheet could render. Bug present since v1.0.1. Also fixed the underlying divergence risk (proxy now surfaces missing methods as a warning + rejected promise instead of a silent sync crash). - **Clear all items in a meal actually clears on the server** ([#169](https://github.com/TraceApps/nutritrace/issues/169), thanks @simonmoreau). The bulk clear was filtering items on the client but not sending delete markers, so the server put them right back on the next refresh. - **Add-to-Diary button no longer duplicates entries under mash-click** ([#156](https://github.com/TraceApps/nutritrace/issues/156), thanks @sunjam). The button now disables the moment you tap it and stays disabled until the save completes. - **Statistics default metric honors category order** ([#155](https://github.com/TraceApps/nutritrace/issues/155)). Opening the Statistics tab picks the first metric under your Settings → Statistics ordering, skipping wearables that aren't connected, body stats you've hidden, and water when it's turned off. - **Health Connect no longer overwrites past dates with today's data** ([#161](https://github.com/TraceApps/nutritrace/issues/161), thanks @knew3be). Viewing an old day and tapping Sync used to write today's steps and weight into that day. Per-metric read errors are now surfaced to Diagnostic Logs so silent failures can be diagnosed. - **Android external https images load directly** instead of being routed through `/api/proxy` (was slow and unnecessary on native). - **Android diary photo hydration is fast again** on days with many entries. Was full-scanning the foods + meals tables per read; now targeted at the items actually on the day. ### Translations - **Weblate is open for community translations** at [hosted.weblate.org/projects/nutritrace](https://hosted.weblate.org/projects/nutritrace). Contribute a locale (or improve an existing one) without a GitHub round-trip. See [CONTRIBUTING.md](CONTRIBUTING.md#translations) for the workflow. Direct JSON pull requests still work too. ### Security - No new vulnerabilities. `npm audit` reports 0. - Two new dependencies for the features above: `@modelcontextprotocol/sdk` (MCP) and `zod` (MCP schema validation). Both actively maintained. --- ## [1.2.0-dev07] - 2026-08-22 (pre-release) Seventh dev pre-release of the 1.2.0 minor. Two big themes: wide-screen layouts (rails now stay put on scroll) and a proper update-notification loop that finally tells you when a new version is out. ### Added - **Updates: check-frequency picker** in Settings → Updates (Hourly / Every 4 Hours / Every 12 Hours / Once a Day / Manual Only). Defaults to every 4 hours. Persisted per-user across devices. - **Updates: red dot on the Settings nav icon** whenever a new version is available and hasn't been dismissed. Clears the moment you tap "Skip This Version" or the banner's dismiss button. ### Changed - **Updates: PWA now actually notices new deploys.** The service worker is prompt-based instead of silent-auto-update, so a fresh build no longer swaps out from under you with no warning. A "Reload" banner offers to apply the new bundle. Also polls for `sw.js` changes on the same cadence you pick above, and on visibility change, so a long-lived tab surfaces updates in minutes instead of the browser's default 24h. - **Updates: Android shows both an OS notification AND an in-app banner** (previously it was either/or). Both respect the same "Skip This Version" flag — one dismiss clears every surface for that version. - **Wide-screen layouts on Diary + Foods stay welded to the viewport.** The Diary right rail (Body Stats / Water / Activity widgets) and the Foods page's Sources sidebar + food preview no longer drift on scroll or slip off near the bottom of the page. Fixed properly via the portal-plus-position:fixed pattern instead of sticky. ### Fixed - **Wellness page: `getLatestWellness` no longer crashes** — mentioned it in dev06 notes but the fix relies on the proxy stub that also landed in this cycle; both are now working together on Android. ### Security - No new dependencies. `npm audit` reports 0 vulnerabilities. --- ## [1.2.0-dev06] - 2026-08-21 (pre-release) Sixth dev pre-release of the 1.2.0 minor. Bug fixes across the community issue tracker plus follow-ups on the dev05 comma decimal work. ### Changed - **AI Assistant: shorter, plain-text answers** ([#163](https://github.com/TraceApps/nutritrace/issues/163)). Default response length is now 2 to 4 sentences instead of paragraphs, and markdown is off so `**bold**` and `##headings` no longer show as literal characters. - **Fewer questions before logging** ([#163](https://github.com/TraceApps/nutritrace/issues/163)). A single substring hit in your local foods now auto-logs the same way an exact hit did. Previously it asked which one. The prior 6+ hits bug that silently discarded all local matches is also fixed. - **Manual estimate path in the AI Assistant** ([#163](https://github.com/TraceApps/nutritrace/issues/163)). Explicitly asking to skip the database ("don't search, just estimate") now routes to a review card with the estimated nutrition instead of running a catalog search. - **Confirmation before clearing the AI chat history.** A single tap used to wipe the whole conversation with no way back. - **Settings sidebar sliding highlight** on the desktop rail. ### Fixed - **Add Activity sheet opens on Android again** ([#162](https://github.com/TraceApps/nutritrace/issues/162)). A missing method on the native backend was throwing before the sheet could render. Also fixed the underlying class of bug (the proxy now surfaces missing methods as a warning + rejected promise instead of a silent crash). - **Health Connect no longer overwrites past dates with today's data** ([#161](https://github.com/TraceApps/nutritrace/issues/161)). Viewing an old day and tapping Sync used to write today's steps and weight into that day. The sync now refuses on any non-today view, and the button is hidden there. - **Per-metric Health Connect read errors are logged to Diagnostic Logs** ([#161](https://github.com/TraceApps/nutritrace/issues/161)). Silent `catch {}` blocks on ten of the read paths now emit the actual error so wrong record type names surface instead of vanishing. - **Clear all items actually clears the meal** ([#169](https://github.com/TraceApps/nutritrace/issues/169)). The bulk clear was filtering items on the client but not telling the server they were deleted, so the server put them right back. Individual delete worked because it emits the tombstone. - **Body Stats widget on the diary rail** ([#168](https://github.com/TraceApps/nutritrace/issues/168)). Weight now shows immediately after refresh instead of only after opening the sheet, and clearing the field or entering 0 removes the weight (was silently ignored). - **Editor draft no longer leaks between different foods or meals** ([#157](https://github.com/TraceApps/nutritrace/issues/157) follow-up). Every session was writing to the same draft key, so typing while editing food A would appear as pre-filled data when adding a new food. Restored drafts now show a banner with a Discard button, and the photo persists too (moved to IndexedDB so it stops fighting localStorage quota). - **Comma decimal separator now works on the remaining number fields** ([#160](https://github.com/TraceApps/nutritrace/issues/160) follow-up). Body Stats sheet, Foods portion + servings prompt, Diary edit sheet, water custom amount, Onboarding wizard, Goals editor, Fasting custom hours, Water container settings, Activity duration and kcal. The dev05 pass missed these. ### Translations - **Weblate is open for community translations** at [hosted.weblate.org/projects/nutritrace](https://hosted.weblate.org/projects/nutritrace). Contribute a locale (or improve an existing one) without a GitHub round-trip. Direct JSON pull requests still work too. See [CONTRIBUTING.md](CONTRIBUTING.md#translations) for the workflow. - Czech translation started via Weblate (early, most strings still fall back to English). ### Security - No new dependencies. `npm audit` reports 0 vulnerabilities. --- ## [1.2.0-dev05] - 2026-08-16 (pre-release) Fifth dev pre-release of the 1.2.0 minor. Bug fixes from the community issue tracker plus a shared image cropper contributed by @librarian. ### Added - **POST /api/v1/activity** ([#154](https://github.com/TraceApps/nutritrace/issues/154)). External services can log a manual activity entry via API using the new `write:activity` scope. Supports `external_id` for idempotent posts. - **MCP `create_food` accepts common nutriment aliases** ([#103](https://github.com/TraceApps/nutritrace/issues/103) followup). `protein`, `carbs`, `vitamin-b12` and other legacy keys are now mapped to the canonical NUTRIMENTS names. A one-time backfill also renames any prior rows written with the wrong keys. ### Changed - **Comma decimal separator supported across numeric inputs** ([#160](https://github.com/TraceApps/nutritrace/issues/160)). Portion, quantity, nutrition per 100 g, weight, body measurements, water custom amount, Quick Calories, and recipe amount all accept either `,` or `.`. Comma-locale users can type "2,5" without fighting the form. - **Image cropper is now shared across Food/Meal/Recipe editors** ([#159](https://github.com/TraceApps/nutritrace/pull/159), thanks @librarian). One component, resize handle, and proper touch support. Meal editor used to be mouse-only and had a hardcoded 200 px box stuck near the top-left; now centered, resizable, and works on phones. - **Cropper output standardized at 512 px** and the resize handle grown to 44 px for comfortable tap targets. - **Stats default metric honors category order** ([#155](https://github.com/TraceApps/nutritrace/issues/155)). Opening the Statistics tab picks the first metric under Settings > Statistics category order, skipping wearables that aren't connected, body stats you've hidden, and water when it's turned off. - **Screen wake lock held during in-flight AI and Scan Label requests** ([#158](https://github.com/TraceApps/nutritrace/issues/158)). Slow-model calls no longer die on screen timeout mid-request. - **Editor drafts persist to localStorage** ([#157](https://github.com/TraceApps/nutritrace/issues/157)). If the OS kills the WebView while the camera is up (Samsung lmkd behavior on lower-RAM devices), the Food/Meal editor restores what you'd typed on the next open. 4-hour TTL. ### Fixed - **Add-to-Diary button no longer duplicates entries under mash-click** ([#156](https://github.com/TraceApps/nutritrace/issues/156)). The button now disables the moment you tap it and stays disabled until the save completes, so extra taps during a slow write don't fire extra PUTs. - **Same-day copied diary items no longer look like duplicates on the local device.** Each copy gets a fresh uuid so the row keys are unique. - **Android diary photo hydration no longer full-scans the foods and meals tables on every diary read.** Queries are now targeted at the items actually on the day. - **Android external https images load directly** instead of being routed through `/api/proxy` (was slow and unnecessary on native). ### Security - No new dependencies. `npm audit` reports 0 vulnerabilities. --- ## [1.2.0-dev04] - 2026-08-11 (pre-release) Fourth dev pre-release of the 1.2.0 minor. MCP gets meal-side read tools (thanks to feedback from @javydekoning on #103), an Android hydration bug that made recipe-only days render as ghost placeholders is fixed, and a couple of Settings labels are tidied up. ### Added - **MCP: three new read tools for saved meals** ([#103](https://github.com/TraceApps/nutritrace/issues/103)). Agents connecting via MCP can now search, list, and inspect saved meals without the numeric `meal_id` up front. - **`search_meals`** — text search over saved meals by name. Query is **optional**: with no query, browses the whole catalog (covers the `list_meals` ask). Recipes excluded by default; `include_recipes: true` opts them in. - **`get_recent_meals`** — most-recently-used saved meals, ordered by `last_used_at`. - **`get_meal_details`** — full contents of one saved meal or recipe (items[] + per-item nutrition + meta). - All three sit under the existing `mcp:read` scope; no new env flag or token change needed. Docs: [/reference/mcp-tools/](https://traceapps.github.io/docs/reference/mcp-tools/). ### Fixed - **Recipes added as the first item of a day on Android no longer render as a 100g placeholder with no image or nutrition** (PR #153, thanks @librarian). Recipe-only days took a hydration shortcut that returned unresolved Promises instead of actual items; downstream image + nutrition passes read undefined fields and rendered a ghost row. The shortcut now awaits its children properly. - **Recipe logging on Android no longer bumps the wrong "recently used" counter** (PR #153, thanks @librarian). `addDiaryItem` was routing every usage bump to `markFoodUsed`, so logging a recipe silently incremented `usage_count` on whatever random food happened to share the recipe's numeric id. It now routes recipe items to `markMealUsed` instead. ### Changed - **Settings copy: "desktop" terminology aligned with the large-screen redesign.** The Force Mobile Layout description now says "large screens" (the trigger is a viewport-width breakpoint, so Android tablets and foldables get the layout too), and the Diary settings group heading is now "Rail Widgets" (the subtext already clarified the ≥1280px threshold). --- ## [1.2.0-dev03] - 2026-08-11 (pre-release) Third dev pre-release of the 1.2.0 minor. Data-safety upgrade to how diary items and water round-trip between clients and the server. ### Fixed - **Diary items and water are now merged per-entry across devices instead of replaced wholesale.** Every diary item and water log carries a stable identifier, and deletions travel as explicit tombstones. If a client's local copy of a day is stale, its next write can no longer overwrite items or water that the server holds; the merge preserves anything the client didn't address. Existing rows get identifiers assigned automatically on the first server start after upgrade. No user-visible change under normal use; the safety kicks in when two devices race, when a mobile local cache thins out, or when an offline delete is synced later. --- ## [1.2.0-dev02] - 2026-08-10 (pre-release) Second dev pre-release of the 1.2.0 minor, and a substantial one. Every major page (Diary, Foods, Settings, Wellness, Statistics, Goals) now has a proper large-screen layout with rails, wider content areas, and page-specific extras like drag-to-copy meals and click-to-drill charts. Editors get a two-column layout. Deep-links wire the whole app to Statistics. A handful of bugs are fixed, notably an Android pull-to-refresh that fired mid-page. AI model presets are refreshed to current generations. Opt-in trace-level request diagnostics arrive server-side. > **Note on the large-screen redesign.** This is a first pass. Every page listed under "Large-screen layouts" below is expected to change further as feedback comes in and as I keep using it myself. If a widget placement, rail behavior, or interaction feels off, please open a Discussion or an issue with a screenshot; nothing here is locked in. ### Added **Large-screen layouts (first pass, iterating)** - **Diary large-screen layout.** Two-column shell with a Day Summary widget that mirrors the full Nutrition Summary sheet (same numbers, same layout, same % vs g toggle). Right rail carries Water, Body Stats (merged Weight + Measurements), Notes, and configurable per-widget visibility toggles. 7-day week strip anchors on the viewed date (not real-world "today") with a hover preview that respects your Date Format preference. Drag meals onto week-strip days to copy them forward. Drag items between meals within the same day. Nutrient widget in the rail excludes calories / protein / carbs / fat (those already live in the Day Summary). All configured Water containers render (not just the first three). A "Weekly trend" indicator sits on the Day Summary KPI strip (rolling 7 days vs previous 7). - **Foods large-screen layout.** Left filter rail, wider detail pane, adjusted grid, bottom-nav clearance, rail empty state. - **Settings large-screen layout.** Two-pane shell with an always-visible search bar. Sections regrouped for scannability across Diary, Appearance, Regional/Foods, Statistics/Water/Nutrients/Sharing, Notifications/Backup/Import-Export, and Components. Profile becomes a first-class rail section that inline-expands in the welcome hero. Cross-fade between sections. - **Wellness, Statistics, and Goals large-screen layouts.** Each page gets a left rail (providers / metrics / your goals), a wider content area, and (where useful) a right rail (insights / detail / drill-in). Rails collapse cleanly on narrow screens. - **Editor two-column layout.** FoodEditor and MealEditor gain a sticky left column on wide screens plus a two-column nutrition grid; Show All Nutrients now slides open. - **Force Mobile Layout toggle.** New Settings option turns every large-screen layout above off entirely and falls back to the single-column mobile view app-wide. **Statistics deep-linking + polish** - **Deep-link into Statistics from anywhere.** Diary's Day Summary, every Wellness metric card, and every Goals row now have a "View trend" affordance that jumps to Statistics with the right metric and range preselected. - **Statistics polish pass.** Left rail is searchable and scrollable, charts are clickable to drill into a specific date, timelines auto-group by month past 60 days, an empty state suggests what to log next, and CSV export is available on every chart. **Server + AI** - **Opt-in trace-level request diagnostics.** `LOG_LEVEL=trace` adds the most verbose server level, while request bodies require the separate `TRACE_REQUEST_BODIES=1` switch. Pre-parser instrumentation counts observed bytes and catches aborted or oversized requests without trusting `Content-Length`. Post-parser tracing emits bounded, credential-redacted contents for selected routes including sync pushes. API calls carry `X-Request-ID` across app and server logs, 413s emit one structured warning, static and upload traffic stays out of INFO logs, invalid logging configuration fails at startup, and the server startup line now includes its resolved NutriTrace version. Thanks to @librarian (PR #150). - **Model Context Protocol (MCP) server** ([#103](https://github.com/TraceApps/nutritrace/issues/103), thanks @javydekoning). *(promoted from dev01 for release-notes visibility)* 12 tools across three tiers, each gated by its own env flag AND its own token scope. Read tools (5) enabled with `MCP_ENABLED=1` and `mcp:read`. Write tools (4) add `MCP_WRITE_ENABLED=1` and `mcp:write`; everything lands as normal editable diary entries. Destructive tools (3) add `MCP_DESTROY_ENABLED=1`, `mcp:destroy`, AND every call must include `confirm: true`. Tombstoned diary days refuse writes rather than silently resurrect. Every tool query is scoped on `user_id`. Full setup + Claude Desktop config at [docs/nutritrace/mcp/](https://traceapps.github.io/docs/nutritrace/mcp/). ### Fixed - **AI Assistant custom model didn't persist** ([#151](https://github.com/TraceApps/nutritrace/issues/151), thanks @pliddle). Typing a model ID into the Custom field on any provider now saves on every keystroke instead of reverting to the last preset the next time you opened Settings. NutriTrace-only bug; the same code path was already correct in LiftTrace and CookTrace. - **Android pull-to-refresh fired mid-page.** Scrolling downward anywhere on a page could trigger the sync spinner because the gesture recognizer accepted any downward swipe. It now only fires at true top-of-scroll, matching Android's standard SwipeRefreshLayout behavior. - **Whole-app remount on every navigation.** The app shell was keying `
` on the full URL, which force-remounted on every intra-app link (flashing content, losing transient state on the incoming view). Now keys on the top-level route segment so same-shell nav stays live. ### Changed - **Refreshed AI model presets.** Google Gemini added 3.5 Flash Lite / 3.6 Flash / 3.1 Pro to the top of the dropdown; OpenAI added GPT-5.6 Luna / Terra / full. Previous-generation entries (Gemini 2.5, GPT-4o, Claude Opus 4.8) remain in the dropdown labeled "(previous)" and continue to work unchanged. Defaults for new installs bumped to Gemini 3.6 Flash and GPT-5.6 Luna. ### Infrastructure - **i18n CI check ported from LiftTrace.** GitHub Actions now enforces no-duplicate keys across ARB files, and every code-referenced translation key must resolve. - **Docs:** roadmap updated to reflect the current API token scope set (adds `write:workouts` and `mcp:*`). --- ## [1.1.2] - 2026-08-08 Patch release. Full OFF integration migration to the two current-canonical endpoints (search-a-licious for text search, v3 for barcode / product detail), a Kilojoules-vs-Calories goal storage cleanup, and smaller UI polish. Six code-review passes on the accumulated dev diff turned up 18 issues across the cycle; the resulting release has been through more static-analysis scrutiny than most NT releases to date. ### Fixed - **Disabled Calorie Goal Mode buttons now look and explain themselves** (#147, thanks @dominicbui). Dynamic mode requires a connected wearable that reports calorie burn (Fitbit, Garmin, or Google Health). It was correctly gated on the code side, but the disabled visual style was barely distinguishable from an inactive button and the reason ("Connect a wearable first") only appeared as a hover tooltip so on mobile you got no feedback at all. Disabled seg-control buttons now render clearly grayed out, and a visible helper line under the mode selector explains what Dynamic needs and where to set it up. - **Kilojoules goal and Calories goal no longer drift out of sync** (#146, thanks @dominicbui). Kilojoules was defined as a separately-editable nutrient goal, so it stored under `$goals.kilojoules` independently from `$goals.calories`. Switching between fixed and adaptive calorie modes, or setting one goal via the Goals page and another via mode-switching, could leave the two out of sync (Goals page showed one target, the diary math used the other). Kilojoules is now a display-mode alias for Calories: the underlying storage is always `$goals.calories` in kcal, and the Kilojoules row reads/writes through the same key with a kJ display conversion. One-time migration on next Goals page open: if you had a Kilojoules goal but no Calories goal, it's transferred to Calories cleanly; if you had both set to different values, Calories wins (that's the canonical storage moving forward, and it matches what the diary was already using). ### Changed - **Barcode scan of an OFF-known product now shows the nutrition-facts sheet first instead of jumping into the editor.** Matches the flow when you tap an OFF search result: view nutrition, then Add to Diary or Edit. Only barcodes that OFF doesn't know go straight into the editor (nothing to view, must enter manually). Same-app-consistent UX regardless of whether you got to the food via search or scan. - **Open Food Facts search moved to search-a-licious for better relevance and future-proofing** (#133). NutriTrace now hits OFF's dedicated ElasticSearch-backed search service (`search.openfoodfacts.org/search`) for text queries instead of the deprecated v2 API. Multi-word queries rank more sensibly, French / German / Spanish queries respect the language preference correctly via `langs=`, and barcode lookups moved from `/api/v0/` to the current `/api/v3/` product endpoint (v2 was officially deprecated by OFF). Local OFF mirror (`OFF_LOCAL_DB`) unaffected. The proxy still intercepts both URL shapes and the frontend accepts both response envelopes so mirror + live paths keep working identically. NutriTrace is now on the two endpoints OFF actively recommends and matches CookTrace's OFF codepath so future fixes port cleanly between the two apps. - **Add-time hydration for OFF foods.** Because search-a-licious deliberately drops `serving_size`, `serving_quantity`, `nutrition_data_per`, and the `_serving` variants of nutriments (index space savings), NutriTrace now fetches full product detail via v3 the moment a user taps an OFF search result. Restores the Import Portion As setting, alt-units convenience picker ("1 slice (35g)"), and the cross-system nutrition-basis warning. Cached in-session per barcode so second taps are instant. Adds an invisible ~200-500ms once per unique food. - **User queries with special characters no longer break OFF search.** `Ben & Jerry's`, `M&M's`, `Häagen-Dazs` etc. are now Lucene-escaped before being sent, so reserved operators (`+`, `"`, `&&`, `||`, brackets, etc.) don't trip the search parser. --- ## [1.1.1] - 2026-08-05 Patch release. Restores OFF food-name search for the majority of self-hosters (broken in v1.1.0 by a response-shape parser mismatch), plus a handful of UI polish fixes. ### Fixed - **Open Food Facts name search returns results again for non-mirror users** ([#133](https://github.com/TraceApps/nutritrace/issues/133), thanks @JacosVerksted for spotting). v1.1.0 changed the OFF search URL to the v2 API but left the response parser reading `data.hits` (the search-a-licious envelope key) instead of `data.products` (v2's key). Self-hosters running the local OFF mirror weren't affected because the mirror always returns `hits`, but anyone without the mirror got zero results for every text query. Parser now reads whichever envelope is present so both paths work. Full search-a-licious migration is queued for a following release for typo tolerance + code parity with CookTrace. - **Missing space between amount and unit for named serving units** (#143, thanks @javydekoning). Rows like `8Nugget(s)` now render as `8 Nugget(s)`. Uses the FDA-nutrition-label + MyFitnessPal / MacroFactor consumer convention: short SI abbreviations stay tight (`500g`, `250ml`, `100kcal`), named / word units get a space (`8 Nugget(s)`, `3 slices`, `1 cup`). New shared helper `amountAndUnit()` in `lib/units.js` centralizes the rule; applied across the Diary main rows, split-recipe children, action-sheet subtitles, Foods list, Foods bulk-import modal, Meal Editor per-serving line + food-picker, and Smart Log preview. - **"Save as template" on Add Activity now actually surfaces the saved templates.** Two contradictory gates on the suggestion dropdown were keeping saved-template rows from ever rendering: the outer dropdown required a typed query to show, and the templates section required an empty query to show. Templates therefore saved to the database correctly but never appeared in the picker. Dropdown now opens on focus when any templates exist, and typed text fuzzy-filters templates alongside the compendium + past-name suggestions. Thanks to @tellis82 for reporting in [Discussion #121](https://github.com/TraceApps/nutritrace/discussions/121). --- ## [1.1.0] - 2026-08-04 > **⚠ Upgrade note.** The Android app identifier change (`app.nutritrace.local`, see "Changed" below) invalidates the WebView's cached auth cookie. Server-connected Android users will need to re-enter their server URL and sign back in once after upgrading; standalone Android users lose their theme / accent / display prefs but keep all food, meal, and diary data (that lives in local SQLite, unaffected). PWA / browser users are not affected. ### Added - **Editable log time on diary entries** (#135, thanks @caioqv-dev). Tap any diary item to edit and the sheet shows a Logged Time picker next to Number of Servings (or above Save on Quick Calories entries). Fixes the common "ate lunch at 2pm but only logged it at 10pm" case where the entry's timestamp was stuck at whenever you added it. The picker only appears when **Settings → Diary → Show Timestamps** is on, keeping the edit sheet clean for users who don't care about per-item times. Uses the same in-app time picker as Fasting / Notifications / Backup schedules. Moving an entry to a different day is a separate feature and stays out of scope here. - **Foods page: configurable default search source** (#128, thanks @sunjam). New setting under **Settings → Foods → Default Search Source** picks which chip the Foods page opens with — All, My Foods, OFF, USDA, or Mealie. Existing users keep the current default (My Foods); power users who add new foods often can switch to All to fan out to OFF/USDA/Mealie on every visit. - **Optional email on the "Create Admin Account" form.** Shows up only when SMTP is configured via environment variables (`SMTP_HOST`/`SMTP_USER`/etc. in docker-compose), so we know the server can actually reach that address at that point. Stored on the admin's user record for password-reset and invite emails later. Missing this field was called out in #122 — thanks @sunjam. - **Accent-tinted browser chrome.** The browser's tab bar / address strip now picks up your current accent color via ``. Running multiple instances side by side (personal + family, or NT + CT + LT together) — pick a distinct accent per install and the tabs are visually different at a glance. Favicon and icons stay the branded NutriTrace mark. - **Pull-to-refresh sync + smarter connection banner (Android).** In native server mode, swipe down from the top of any page to trigger a manual sync. When sync fails, the on-screen banner now analyzes what went wrong (no network vs cellular-only vs server unreachable vs HTTP error) with actionable copy and a Retry button, instead of a generic "sync error". Contributed by @librarian (#124). - **In-app updates.** New Settings → Updates panel checks GitHub Releases for a newer version and, on Android, downloads the signed APK with a progress bar and hands off to the system installer via FileProvider. Admins on the PWA see a server-update banner comparing the running server version against the latest release, with a copy-paste `docker-compose` upgrade command. Opt-in stable or dev-build channels. Same shared signing key means Android upgrades in place with no reinstall. ### Changed - **Redesigned Settings with per-section drill-in and search.** The single scrolling Settings page has been replaced with a per-section drill-in layout: each section (Appearance, Diary, Foods, Goals, Notifications, Backup, etc.) opens on its own sub-page with a smooth back-arrow animation. New in-Settings search jumps directly to any specific field across all sections, with deep-link support so the target row scroll-highlights on arrival. Sidebar keeps the current section highlighted while you're inside its sub-pages. Same settings, much less scroll. - **Bitwarden / password managers now show a real app identifier instead of "localhost" (Android).** The Android app used to serve its WebView from `https://localhost/`, so autofill entries saved through Bitwarden / 1Password / etc. showed up as "localhost" — indistinguishable from any other localhost app. NutriTrace now identifies itself as `app.nutritrace.local`, which reads clearly in autofill dialogs and in your saved-credentials list. **One-time upgrade cost:** the origin change orphans locally cached web-only state, so on first launch after upgrading you'll need to re-enter your server URL + log in again (server-connected users), and your theme / accent / display prefs will reset to defaults (standalone users). **Your food, meal, and diary data is unaffected** — that lives in a local SQLite database that's separate from the WebView. - **SMTP "Username" field relabeled to "Email or Username".** Most SMTP providers (Gmail, Outlook, etc.) want the full email as the username. Label change removes the guesswork. - **Fitbit connect card hidden for new users.** Fitbit's Web API is being wound down; Google Health is the recommended path forward. Users who already have Fitbit connected continue to see the card and their data keeps syncing until the September 2026 cutoff. Nothing changes for existing connections. ### Fixed - **Micronutrient goal units read "mcg" instead of the confusing "MG"** (#137, thanks @dominicbui). The Goals editor for Vitamin K / Vitamin A / Vitamin D / Folate (B9) / Vitamin B12 used the micro sign (`µg`) in the label, which the app's uppercase form-label style case-mapped to Greek Capital Mu (`Μ`) — visually identical to Latin `M` in most fonts. So `µg` rendered as `MG` and looked like milligrams. Switched to the unambiguous `mcg` spelling everywhere (matching notification and USDA API conventions already in use). Custom-nutrient dropdown also updated. Legacy custom nutrients still stored with `µg` are display-normalized to `mcg` in the Goals editor so no data migration is needed. - **Open Food Facts country filtering now works for country-specific searches** (#130, thanks @JacosVerksted for #131). OFF name search moved to the v2 API with the correct `countries_tags=en:` filter — the legacy `countries_tags_en=` shape stopped returning country-filtered results. Previously-saved country prefs auto-backfill the `en:` prefix so existing users keep their setting. Country dropdown also expanded from 15 to 30 entries and alphabetized: added Argentina, Austria, Belgium, Chile, Denmark, Finland, Ireland, Netherlands, New Zealand, Norway, Poland, Portugal, Singapore, South Africa, South Korea, Sweden, Switzerland. - **App icon no longer shows a white halo.** The bundled icon PNGs had ~15px of solid white padding baked into their corners. On tinted browser chrome the halo was visible around the tab favicon; in-app the icon looked framed. Corners now clear cleanly through to the tab background. Icon URLs also cache-busted with the app version so shipped icon fixes actually take effect without users needing to clear their browser cache. - **Create Admin form password field no longer crushed** (#122). The password input on the Enable User Management form was rendering as a colored sliver because of a flex-layout bug. Password + Confirm now sit symmetrically side-by-side, each with its own eye toggle sharing show/hide state. - **Bundled assets load offline again.** In native server mode, `/icons/`, `/fonts/`, `/templates/`, and `/vendor/` paths were being rewritten to the configured server URL even though those assets ship inside the Android APK. When the server was unreachable, icons and fonts silently disappeared. Now short-circuits to the WebView's local origin. Contributed by @librarian (#123). - **OpenAI-compatible endpoints accept vision requests again** (#114). Image content blocks are normalised on the AI proxy before forwarding, so a request with an image attached goes through whether the block is a string URL or an object with `image_url.url`. - **GPT-5.6-era chat parameters supported.** The AI proxy translates the newer `max_completion_tokens` and `reasoning_effort` fields when talking to models that require them, so calls to GPT-5.6 and equivalents don't 400 on the older `max_tokens` field name. ### Security - **fast-uri bumped to 3.1.5** (regexp DoS, moderate). Sub-dependency picked up via the standard transitive tree. - **brace-expansion bumped to 2.1.4 / 5.0.9** (regexp DoS, moderate). Standard `npm audit fix` update to the resolved sub-dependency versions. --- ## [1.0.3] - 2026-07-25 ### Added - **Origin-country flag on OFF search results.** Each Open Food Facts result now shows a small flag emoji next to the food name when OFF has origin data for it (from `origins_tags`, falling back to `manufacturing_places_tags`). Lets you see at a glance whether that "yogurt" is French, American, or Australian without opening the entry. Countries OFF doesn't have origin data for show no flag rather than a misleading placeholder. - **Data-completeness indicator on OFF search results.** Small colored dot next to the kcal on each OFF row: green when the entry has most nutriment fields filled in, yellow for partial, grey for sparse. Long-press or hover shows the exact percentage. Helps pick the more trustworthy of two similar entries. - **USDA data-type badge on USDA search results.** Small color-coded letter next to the kcal on each USDA row: F (Foundation, curated laboratory-analyzed staples), L (SR Legacy, established reference data), S (Survey/FNDDS, dietary composite), B (Branded, manufacturer-submitted). Letter + color tells you which tier the entry came from so you can favor curated Foundation entries over manufacturer-submitted branded ones for common searches. - **"Custom…" option on the Model dropdown for Claude, OpenAI, and Gemini.** Lets you enter any model ID the vendor supports without waiting for the preset list to catch up. Same behavior the OpenAI Compatible provider has always had. - **Retirement remap for retired Gemini models.** Saved selections of `gemini-1.5-*` or `gemini-2.0-*` (both retired by Google) are quietly upgraded to the current default at request time, avoiding 404 errors after the vendor pulled the model. - **`LICENSES.md`** at the repo root formally lists the four food data sources (Open Food Facts, USDA, Mealie, Local Foods) and their licenses, plus the operator obligations that apply if the `OFF_LOCAL_DB` local mirror is enabled on a multi-user instance. - **In-app ODbL disclosure banner** for admin users when the local Open Food Facts mirror is active, so operators serving other users don't miss the share-alike consideration. - **SSO-only mode via environment variable.** Set `OIDC_ENABLE_EMAIL_PASSWORD_LOGIN=0` (or `false` / `no`) at boot to disable password login server-wide, so users must sign in via an OIDC provider. Locks the corresponding admin UI toggle with an env-lock note. Mirrors the pattern of other env-locked settings. Applies to NutriTrace, LiftTrace, and CookTrace since all three share the same shape (asked for on LT #16). - **Per-source tier filter dropdowns on the Foods search bar.** A caret next to the OFF and USDA source chips opens a checkbox panel to narrow results by tier — OFF completeness bucket (High / Medium / Low / Unknown) or USDA data type (Foundation / SR Legacy / Survey / Branded / Experimental). Defaults to all tiers active; a small dot on the source chip signals when a subset is applied. Filter is client-side (no extra API calls) and also applies in All-mode and multi-source mode. - **Long-press to combine sources on the Foods search bar.** Long-press any source chip to pin it alongside the currently-active source — results become a merged, multi-source view with per-row source badges (same fan-out as All-mode, but limited to your pinned set). Long-press an already-pinned chip to remove it. Regular tap on any chip exits multi mode back to single-source. OFF completeness dot, origin-country flag, and USDA data-type badge all render in the merged results too, so quality signals stay visible. ### Changed - **OFF search results now sorted by data quality within each fetched page.** Entries with images and more complete nutrition data surface higher than sparse ones. OFF's server-side relevance still picks the initial batch; the re-rank runs within the batch to reduce the "many near-identical variants" search noise. - **USDA search results now sorted by data-type tier within each fetched page.** Foundation entries and SR Legacy come first, then Survey/FNDDS, then Branded. Same pattern as OFF: USDA's server-side relevance still picks the initial batch; the re-rank surfaces the curated tiers above the ~1M brand-submitted entries that would otherwise dominate common searches like "chicken" or "milk". - **Claude model presets refreshed.** Sonnet bumped to Sonnet 5, Opus 4.8 added as a "smartest" tier option, older Sonnet 4.6 removed. ### Fixed - **OIDC sign-in now works on Android first-install for OIDC-only servers** (#110, reported by @ImmanuelMc). NativeSetup previously required a username + password to submit, so users on Authentik / Keycloak / Authelia-backed servers with password login disabled had no way to complete initial setup — the OIDC button never appeared because it lived on the post-setup Login screen, but Login never loaded because the server URL was never persisted. The setup form is now a two-step flow: enter server URL → app fetches `/api/auth/status` → renders whichever auth methods the server actually supports (password fields only when enabled, OIDC provider buttons with logos when configured, both when both). After OIDC callback completes the setup gate re-evaluates so the app lands on the main screen instead of staying visually stuck. - **OIDC callbacks no longer fail on the first attempt** with a spurious `callback_failed` error. openid-client v5's default outgoing HTTP timeout was 3500ms, tight enough that cold token-exchange or userinfo requests to Authentik / Keycloak / etc. would sometimes time out. Bumped to 10 seconds, matching most other OAuth client library defaults. - **Info icon on meal rows no longer escapes the card on narrow viewports** (#106, reported by @javydekoning). Flex sizing on the food-item button had `width: 100%` while being a flex child, pushing the info icon past the parent boundary on Firefox and iOS Safari when the food name was long. Now uses `flex: 1; min-width: 0`. - **`offSearchCountry` setting now actually filters OFF searches.** The Settings → Connected Services → Open Food Facts → Search Country dropdown was previously stored but never applied to the search URL. Now correctly narrows OFF search results to the selected country. - **`offSearchLanguage` setting now actually localizes OFF results.** Same bug pattern as country: the Settings → Connected Services → Open Food Facts → Search Language dropdown was previously stored but never sent to OFF. Now passed as `lc=` to both the search and barcode-lookup endpoints, so product names, ingredients, categories, and labels come back in the user's chosen language when OFF has translations. ### Security - **fast-uri bumped to 3.1.4** (GHSA-4c8g-83qw-93j6, high). ReDoS in URI parsing. - **brace-expansion bumped to 5.0.8** (GHSA-mh99-v99m-4gvg, high). DoS via unbounded expansion length causing out-of-memory. Follow-up to the 5.0.7 bump in v1.0.2, which fixed a different CVE. - **body-parser bumped to 2.3.0** (GHSA-v422-hmwv-36x6, low). DoS when an invalid `limit` value silently disables size enforcement. --- ## [1.0.5] - 2026-07-29 ### Fixed - **Backups, SMTP config, and other admin panels now work in single-user mode.** With User Management turned off, `POST /api/full-backup`, `PUT /api/app-config`, `POST /api/app-config/test-email`, `POST /api/off-local/refresh`, `GET /api/updates/server-status`, and related admin routes all returned 403 "Admin only" even though NutriTrace's own frontend correctly identifies the sole owner as effectively-admin. `requireAdmin` middleware now passes through when User Management is off, mirroring `requireAuth`. Fourteen previously-broken admin routes come back to life. LiftTrace already had this fix; parity restored. Reported in [#118](https://github.com/traceapps/nutritrace/issues/118). --- ## [1.0.4] - 2026-07-28 ### Changed - **Fitbit connect card hidden for new users.** Fitbit's Web API is being wound down; Google Health is the recommended path forward. Users who already have Fitbit connected continue to see the card and their data keeps syncing until the September 2026 cutoff. Nothing changes for existing connections. ### Fixed - **OpenAI-compatible endpoints accept vision (image) chat again** (#114, reported by @javydekoning). Env-locked deployments running the AI proxy against a strict OpenAI-schema endpoint (LiteLLM in front of Bedrock, etc.) rejected image messages with `invalid content type=image`. The client now emits OpenAI-shape image parts for `oai-compat` and the proxy defensively normalises any Anthropic-shape image parts to `image_url` at the boundary before forwarding. Also mirrored to CookTrace and LiftTrace since they share the same proxy code. - **GPT-5.6-era chat parameters supported** (#115, contributed by @librarian). OpenAI's newer chat models require `max_completion_tokens` instead of the deprecated `max_tokens`, and reject function-tool requests unless `reasoning_effort: 'none'` is set. Both the client and server AI paths now branch on the model and base URL to emit the correct parameters, and third-party OpenAI-compatible endpoints (Ollama, LM Studio, LiteLLM proxies) keep receiving `max_tokens` since most haven't adopted the new field yet. Backward-compatible with older OpenAI models. --- ## [1.0.3] - 2026-07-25 ### Added - **Origin-country flag on OFF search results.** Each Open Food Facts result now shows a small flag emoji next to the food name when OFF has origin data for it (from `origins_tags`, falling back to `manufacturing_places_tags`). Lets you see at a glance whether that "yogurt" is French, American, or Australian without opening the entry. Countries OFF doesn't have origin data for show no flag rather than a misleading placeholder. - **Data-completeness indicator on OFF search results.** Small colored dot next to the kcal on each OFF row: green when the entry has most nutriment fields filled in, yellow for partial, grey for sparse. Long-press or hover shows the exact percentage. Helps pick the more trustworthy of two similar entries. - **USDA data-type badge on USDA search results.** Small color-coded letter next to the kcal on each USDA row: F (Foundation, curated laboratory-analyzed staples), L (SR Legacy, established reference data), S (Survey/FNDDS, dietary composite), B (Branded, manufacturer-submitted). Letter + color tells you which tier the entry came from so you can favor curated Foundation entries over manufacturer-submitted branded ones for common searches. - **"Custom…" option on the Model dropdown for Claude, OpenAI, and Gemini.** Lets you enter any model ID the vendor supports without waiting for the preset list to catch up. Same behavior the OpenAI Compatible provider has always had. - **Retirement remap for retired Gemini models.** Saved selections of `gemini-1.5-*` or `gemini-2.0-*` (both retired by Google) are quietly upgraded to the current default at request time, avoiding 404 errors after the vendor pulled the model. - **`LICENSES.md`** at the repo root formally lists the four food data sources (Open Food Facts, USDA, Mealie, Local Foods) and their licenses, plus the operator obligations that apply if the `OFF_LOCAL_DB` local mirror is enabled on a multi-user instance. - **In-app ODbL disclosure banner** for admin users when the local Open Food Facts mirror is active, so operators serving other users don't miss the share-alike consideration. - **SSO-only mode via environment variable.** Set `OIDC_ENABLE_EMAIL_PASSWORD_LOGIN=0` (or `false` / `no`) at boot to disable password login server-wide, so users must sign in via an OIDC provider. Locks the corresponding admin UI toggle with an env-lock note. Mirrors the pattern of other env-locked settings. Applies to NutriTrace, LiftTrace, and CookTrace since all three share the same shape (asked for on LT #16). - **Per-source tier filter dropdowns on the Foods search bar.** A caret next to the OFF and USDA source chips opens a checkbox panel to narrow results by tier — OFF completeness bucket (High / Medium / Low / Unknown) or USDA data type (Foundation / SR Legacy / Survey / Branded / Experimental). Defaults to all tiers active; a small dot on the source chip signals when a subset is applied. Filter is client-side (no extra API calls) and also applies in All-mode and multi-source mode. - **Long-press to combine sources on the Foods search bar.** Long-press any source chip to pin it alongside the currently-active source — results become a merged, multi-source view with per-row source badges (same fan-out as All-mode, but limited to your pinned set). Long-press an already-pinned chip to remove it. Regular tap on any chip exits multi mode back to single-source. OFF completeness dot, origin-country flag, and USDA data-type badge all render in the merged results too, so quality signals stay visible. ### Changed - **OFF search results now sorted by data quality within each fetched page.** Entries with images and more complete nutrition data surface higher than sparse ones. OFF's server-side relevance still picks the initial batch; the re-rank runs within the batch to reduce the "many near-identical variants" search noise. - **USDA search results now sorted by data-type tier within each fetched page.** Foundation entries and SR Legacy come first, then Survey/FNDDS, then Branded. Same pattern as OFF: USDA's server-side relevance still picks the initial batch; the re-rank surfaces the curated tiers above the ~1M brand-submitted entries that would otherwise dominate common searches like "chicken" or "milk". - **Claude model presets refreshed.** Sonnet bumped to Sonnet 5, Opus 4.8 added as a "smartest" tier option, older Sonnet 4.6 removed. ### Fixed - **OIDC sign-in now works on Android first-install for OIDC-only servers** (#110, reported by @ImmanuelMc). NativeSetup previously required a username + password to submit, so users on Authentik / Keycloak / Authelia-backed servers with password login disabled had no way to complete initial setup — the OIDC button never appeared because it lived on the post-setup Login screen, but Login never loaded because the server URL was never persisted. The setup form is now a two-step flow: enter server URL → app fetches `/api/auth/status` → renders whichever auth methods the server actually supports (password fields only when enabled, OIDC provider buttons with logos when configured, both when both). After OIDC callback completes the setup gate re-evaluates so the app lands on the main screen instead of staying visually stuck. - **OIDC callbacks no longer fail on the first attempt** with a spurious `callback_failed` error. openid-client v5's default outgoing HTTP timeout was 3500ms, tight enough that cold token-exchange or userinfo requests to Authentik / Keycloak / etc. would sometimes time out. Bumped to 10 seconds, matching most other OAuth client library defaults. - **Info icon on meal rows no longer escapes the card on narrow viewports** (#106, reported by @javydekoning). Flex sizing on the food-item button had `width: 100%` while being a flex child, pushing the info icon past the parent boundary on Firefox and iOS Safari when the food name was long. Now uses `flex: 1; min-width: 0`. - **`offSearchCountry` setting now actually filters OFF searches.** The Settings → Connected Services → Open Food Facts → Search Country dropdown was previously stored but never applied to the search URL. Now correctly narrows OFF search results to the selected country. - **`offSearchLanguage` setting now actually localizes OFF results.** Same bug pattern as country: the Settings → Connected Services → Open Food Facts → Search Language dropdown was previously stored but never sent to OFF. Now passed as `lc=` to both the search and barcode-lookup endpoints, so product names, ingredients, categories, and labels come back in the user's chosen language when OFF has translations. ### Security - **fast-uri bumped to 3.1.4** (GHSA-4c8g-83qw-93j6, high). ReDoS in URI parsing. - **brace-expansion bumped to 5.0.8** (GHSA-mh99-v99m-4gvg, high). DoS via unbounded expansion length causing out-of-memory. Follow-up to the 5.0.7 bump in v1.0.2, which fixed a different CVE. - **body-parser bumped to 2.3.0** (GHSA-v422-hmwv-36x6, low). DoS when an invalid `limit` value silently disables size enforcement. --- ## [1.0.2] - 2026-07-22 Patch release. Fixes long-press to open the food/meal/recipe action sheet on iOS Safari, plus a high-severity brace-expansion CVE bump. ### Fixed - **Long-press works on Foods/Meals/Recipes lists in iOS Safari.** The Diary tab already used a manual touch timer to detect long-press; the Foods tab only listened for `contextmenu`, which iOS Safari doesn't dispatch for long-press on non-text elements. Ported the same touch-timer pattern to Foods/Meals/Recipes so long-press opens the edit/clone/delete action sheet across every browser. (#102, reported by @javydekoning) ### Security - **brace-expansion bumped to 5.0.7 (CVE-2026-13149, high).** Regex denial-of-service in the expansion parser. Transitive dep; no direct code change required. --- ## [1.0.1] - 2026-07-19 Patch release. Activity MET auto-estimate now uses your latest weight from Body Stats or a smart-scale sync (Withings, Fitbit, Garmin, Health Connect) instead of the static onboarding value, and the hint no longer points at a non-existent Profile field. Small polish on how the Activity row renders in the Diary. ### Fixed - **Activity MET auto-estimate uses fresh weight.** Reads from Body Stats or wellness data, not just the onboarding Wizard's static field. (#99, reported by @tellis82) - **MET hint shows weight in your preferred unit.** Imperial gets `kg (lb)`, metric stays `kg`. - **Activity diary rows format distance with units.** "20" now renders as "20 mi" / "20 km" per your setting. --- ## [1.0.0] - 2026-07-19 First stable release under the new semver scheme. Retires -rc.N. Delivers cross-source food search with per-row source badges and infinite-scroll pagination across USDA + Open Food Facts, a MET-based exercise compendium with reusable templates and live kcal preview, a grams-vs-percent toggle on the Macro Ring, a branded HTML email test flow, multi-tag Docker publishing so you can pin :1.0, :1, or :latest depending on how aggressively you want updates, and a high-severity adm-zip CVE patch in the backup restore path. Every future release uses strict semver: PATCH for bug fixes, MINOR for new features, MAJOR for breaking changes. Existing rc.N image tags and release assets stay live indefinitely; anyone pinned to a specific rc release is unaffected. ### Added - **Search Across All Enabled Food Sources at Once.** New "All" chip on the Foods tab fires every enabled external source in parallel — Local, From Others (shared), Mealie, USDA, and Open Food Facts — merging results into a single list with a small colored source badge on each row. Skip the source-flip dance when you're not sure which database has the food you're after. Per-source counts strip at the bottom shows what each source contributed ("Local · 3", "USDA · 20 of 20,968"). Infinite scroll loads more as you keep scrolling. (#96, reported by @sunjam) - **Activity Log Now Has an Exercise Compendium.** New MET-based picker in the Add Activity sheet, seeded with ~130 activities from the 2024 Adult Compendium of Physical Activities — running, cycling, swimming, weight lifting, dancing, sports, home activities, and more. Type into the name field and matches surface as a typeahead; tap one to attach its MET value. Live kcal preview computes from `MET × body weight × duration` and auto-fills the calorie field, with the math shown for transparency. Save any entry as a template ("Evening bike commute") and it pins to the top of your picker next time. Custom / freeform activities still work exactly as before. Also exports as a separate CSV under Settings → Import & Export. (#77, reported by @maxerbox) - **Grams vs Percent Toggle on the Macro Ring.** New `%|g` pill above the ring on the Nutrition Summary sheet flips the legend between the percent macro split (`32% Protein · 45% Carbs · 23% Fat`) and consumed-against-goal grams (`203/301 g · 372/453 g · 45/67 g`). Grams mode also swaps the color-coded pill cards below the ring to show `consumed/goal`. Ring center picks up the calorie color, the goal below the big number is bigger and bolder for scannability, and the ring interior gets a subtle calorie tint so the ring center reads as part of the same visual family as the macro cards. (#95, requested by @traebertthomas-cpu) - **Infinite Scroll on External Food Searches.** USDA and Open Food Facts searches show a "Showing X of Y" indicator with the real total hit count from the API and load the next page as you scroll. USDA page size bumped from 20 to 50 per fetch, matching what other self-hosted nutrition apps use — a common query like "chicken" now surfaces 50 hits per page instead of 20, and you can keep scrolling into the 20,000+ total available. - **Multi-Tag Docker Publishing.** Each release now publishes four tags to GHCR: `:1.0.0` (exact), `:1.0` (latest patch on the 1.0 line), `:1` (latest minor on the 1.x line), and `:latest`. Pick your update aggressiveness: pin `:1.0` for bugfix-only updates, `:1` to auto-receive new minor features, or `:latest` for everything. ### Fixed - **Email Test Renders Branded HTML + Batch-Save Bug.** The Send Test button in Settings → Email now sends a branded HTML preview instead of a raw plaintext dump, and the batch-save path stopped losing the first field on save. Also allows overriding the test recipient without changing the persisted admin address. ### Security - **adm-zip Bumped 0.5 → 0.6 (CVE-2026-39244, high).** Prototype pollution vulnerability in the ZIP entry parser. Used by the Full Backup import path. No known exploit in the wild for that specific code path, patched in this release to close it. --- ## [1.0.0-rc.55] - 2026-07-13 ### Fixed - Health Connect: Samsung Health and other single-record-per-day step sources no longer under-count during the day. Steps that read 4,938 at 11:47 while the Health Connect Viewer showed 9,878 now match. Same fix covers Distance, Total Calories, Active Calories, Floors, and Hydration. Reported by @traebertthomas-cpu in #93. - Health Connect: Exercise sessions (Samsung Health workouts, etc.) now sync into the Diary and Statistics workout log. Permission was being requested but never read. Also reported by @traebertthomas-cpu in #91. --- ## [1.0.0-rc.54] - 2026-07-10 ### Fixed - **Health Connect Users' Android Sync Unstuck.** Pending wellness rows on the phone were tripping a SQL bug on push, which stopped every subsequent pull. Symptom: items added on the browser weren't reaching the phone. Every user with Health Connect enabled on Android was silently affected. Fixed. (#89, reported by duplaja) - **More Accurate Time to Sound Sleep.** Server-side ttss derivation now credits the initial 5-10 min settling-in awake segment, matching the Fitbit app. Closes the consistent -5 to -7 min under-count on nights with a short initial wake segment. Applies to Health Connect and Google Health syncs. ### Added - **Server-Side Proxy for Local LLM Endpoints.** Env-locked AI now supports `AI_PROVIDER=oai-compat` alongside claude / openai / gemini. Point `AI_BASE_URL` at a private network Ollama, LM Studio, LocalAI, vLLM, or similar and the server calls it directly, so the browser never has to see the endpoint. Solves the mixed-content + Docker-internal-DNS gap for self-hosters running a local LLM sidecar. Same capability added to LiftTrace and CookTrace. (#90, reported by gentlecolts) ### Improved - **Backup + Restore Fidelity Pass.** Eight silent data-loss scenarios closed across the full backup, JSON export/import, and Android local backup flows. Highlights: OFF unit metadata (nutrition_basis, alt_units, density_g_ml) from rc.50 now survives a restore, federation API tokens survive backups, JSON settings import correctly pushes to the server on PWA, and native restore no longer double-inserts on a second pass. - **"Copy to Another Date" Defaults to Today.** Copying a meal or item from a past date now pre-fills today in the picker instead of the source date. One less tap for the common "same lunch again today" case. --- ## [1.0.0-rc.53] - 2026-07-05 ### Added - **Reorder + Hide Statistics Categories.** You can now shape the Statistics page to your habits. Head to Settings → Statistics → Categories, drag any metric row to change the order it appears in the chip strip, or toggle it off if you never look at it. Works across nutrients, body stats, water, and wellness metrics in a single flat list. (#85) - **Bulk Delete on Foods, Meals, and Recipes.** Long-press any item in your catalog and pick "Select Multiple" from the menu. Tap other items to add them to the selection, then hit delete for one confirmation instead of a dozen. Matches the multi-select UX the Diary already uses. Diary entries that reference deleted items keep their nutrition snapshot but lose the link to the source. ### Fixed - **Sync Icon Rotation Direction.** The sync buttons on the Wellness page (Fitbit, Garmin, Withings, Google Health) now rotate in the direction their arrows point. Previously the arrowheads and the rotation ran in opposite directions, which looked wrong every time. (#86) ### Security - **Dependency Security Updates.** Bumped `multer` 1.4.5-lts.1 → 2.2.0 (patches three high-severity DoS CVEs on the LTS line), `nodemailer` 8.0.7 → 9.0.3 (patches five CVEs including a TLS OAuth certificate-validation bypass and CRLF header injection), and `vite` 6.4.1 → 6.4.3 (dev-only, patches a `server.fs.deny` bypass on Windows). Clears every actionable Dependabot alert. No user-facing changes; self-hosters running Docker just need to pull the new image. --- ## [1.0.0-rc.52] - 2026-07-03 ### Fixed - **Data Loss When Editing From Mobile + PWA in the Same Day.** Recording body stats on mobile, then editing the diary later on the PWA (or vice versa), could silently wipe the earlier entry when the two devices' sync windows overlapped. Every append and save path now refetches the latest server state before writing, so a stale cache can't overwrite fresh data from another device. (#81 reported by tellis82) - **Browser Password Suggestions Now Pass Validation.** The signup, invite, reset, and change-password fields now carry the `passwordrules` HTML attribute. When you click "suggest strong password" in Safari, 1Password, Bitwarden, or your browser's built-in manager, the generated password will include a special character so it passes on the first try. Previously the generator would produce passwords without a special character and the field would reject them. (#82 reported by CloCkWeRX) ### Changed - **New Animated Page Banner Styles.** The illustrated SVG banner headers are retired, replaced with four animated gradient styles you can pick between: Shimmer, Drift, Pulse, and Aurora. Settings → Appearance → Banner Style lets you switch. All four honour the system's Reduce Motion preference. --- ## [1.0.0-rc.51] - 2026-06-19 ### Added - **Trace AI Now Logs Real Foods.** Asking the AI Assistant to "add an apple to snacks" or "log Greek yogurt for breakfast" now creates a real food entry with full nutrition (protein, carbs, fat, fiber, vitamins) instead of a Quick Calories row with just kcal. Searches your local catalog first, then Open Food Facts. Custom meal layouts (renamed or reordered meals) are honored too, so "snacks" routes to the right meal even if you've moved it. (#79 reported by acaonweb) ### Changed - **Unit Metadata Is Now Opt-In.** The nutrition basis, serving units, and density fields added in rc.50 are now hidden by default in the Food Editor, Add to Diary sheet, and Foods list rows. Flip Settings → Diary → "Show Unit Metadata" to bring them back. Existing rc.50 users see no change (the toggle is auto-enabled on upgrade); new installs get a cleaner default. The Food Editor still shows the fields when a food has data populated so you never lose access to your own values. ### Fixed - **Settings Search Now Finds More Diary Toggles.** Show Unit Metadata, Warn About Unit Conversions, Show Daily Notes, Show Quick Calories Button, Adjust Calorie Goal From Activity, and the wearable / manual entry policy were all missing from search keyword lookup. Now findable via natural terms like "unit", "density", "quick calories", "daily notes", "earn back". --- ## [1.0.0-rc.50] - 2026-06-14 ### Added - **Open Food Facts Serving Sizes + Density.** Foods scanned or searched from Open Food Facts now auto-import their serving units (1 slice, 1 cookie, 1 bottle, etc.) and can be logged directly in those units instead of converting to grams in your head. The Food Editor lets you add, rename, or remove serving units on any food, plus set an optional density (g/ml) for accurate cross-system conversion on liquids like oil or honey. Optional warning toggle in Settings → Diary surfaces when the picked unit doesn't match the food's nutrition basis without a density set. (#69 reported by duplaja, #70 reported by maxerbox) - **Raspberry Pi Support.** The Docker image now ships as a multi-arch manifest (amd64 + arm64), so `docker pull` works on Pi 4 / Pi 5 without falling back to a source build. (#76 reported by acaonweb) ### Fixed - **Adding Foods with Photos to the Diary No Longer Fails.** A food with a phone-camera photo could blow past the diary's body size limit on a busy day, causing saves to silently fail with a 413 error. Photos now save as proper uploaded files instead of inline base64 in the food record. Existing bloated diary entries get trimmed automatically on their next save. (#74 reported by kilkalabs) - **Removing a Food or Meal Photo Now Actually Removes It.** Tapping the X to clear a photo, then saving, was leaving the old photo in place. The clear is honored now. (kilkalabs follow-up on #74) - **Readiness on Severe HRV Crash Days Reads More Accurately.** Days when both HRV and resting heart rate were significantly off baseline were reading about 13-15 points higher than Fitbit. Raised the interaction penalty cap so the formula fully reflects the combined crash signal. - **README docker-compose Snippet Now Includes `env_file`.** The copy-pastable snippet was missing the directive that forwards `INSECURE_COOKIES` and other `.env` variables into the container, so anyone copying from the README hit the login-loop bug. (#41 follow-up reported by drekkym) --- ## [1.0.0-rc.42] - 2026-05-30 ### Added - **AI Meal Photos.** Attach a meal photo in Trace and get an editable Nutrition Facts card to review before anything writes to your diary. Two card variants depending on what you ask for: Quick Calories (one-off entry, just kcal + macros for today) or Food entry (saves a reusable Food in your library with the option to also log it to today's meal). Estimates cover the full nutrition profile (fiber, sugars, sodium, sat fat, cholesterol, vitamins, minerals) at the portion size visible in the photo. Pick the destination meal on the card before confirming. - **LiftTrace workout sync.** Receive completed workouts from a LiftTrace install via the federation API. Synced sessions appear in Workout History alongside Fitbit / Garmin / Health Connect pushes, and their calories flow into the dynamic calorie goal lookup. Set up by giving LiftTrace an API token from Settings → API Tokens with the `write:workouts` scope. New "Prefer Wearable Data Over LiftTrace" toggle under Settings → Wellness (default ON) prevents double-counting when both sources cover the same day. - **Voice Input Language picker for Smart Log.** Settings → AI Assistant → Smart Log → Voice Input Language. Pick the language the mic listens in, independently of your device locale. Lets users who keep their device set to English speak in their native language for food logging. Defaults to "Auto (use device language)" so behavior is unchanged unless you set it. - **API Tokens panel now available to single-user installs.** Was previously gated behind a `NT_FEATURES_API` env var and multi-user mode. Now any admin (including the synthetic admin on solo installs) sees Settings → API Tokens directly. Makes wiring LiftTrace federation a one-step setup. - **Wizard celebration screen.** Onboarding now ends with a brief "You're All Set" beat (trophy + confetti, ~1.8s, honors prefers-reduced-motion) instead of snapping straight to the diary. ### Fixed - **Local OFF mirror search returned empty results.** Backend was finding hits but the foods list dropped them all due to a DuckDB v1 shape regression in how VARCHAR fields are returned. Searching the local mirror returns real products again. Server-side fix; restart the container to pick it up. - **Tapping a reminder notification on Android now opens NutriTrace.** Meal / water / weigh-in reminders had no tap target, so tapping just dismissed the notification. Now they launch the app via the standard task-resume path. - **Env-locked AI deployments now support the full Trace tool set.** The server-side AI proxy used when `AI_API_KEY` is set in env was built before tool use shipped, so on env-locked installs Trace silently dropped every tool call (`get_diary`, `get_wellness`, `log_quick_calories`, etc.) and replied with text only. Now full parity with the direct-key configuration across Claude, OpenAI, and Gemini. - **Backup list errors no longer silently swallowed.** Settings → Backup's full-backup list was catching network errors AND non-2xx responses into an empty list, so a failed fetch looked identical to "no backups exist." Now surfaces the server error message + status code. ### Changed - New shared loading spinner with reduced-motion fallback; adopted in API Tokens loading row + Meal Editor food picker. - Trace AI can now see manually-entered Body Water %, matching how it already saw Body Fat %. - Settings search now finds the body-stat visibility toggles via keywords like "body water", "hydration", "muscle", "bone". --- ## [1.0.0-rc.41] - 2026-05-28 ### Fixed - **Settings: long labels next to dropdowns rendered vertically.** Some rows like Water → Display Unit were squeezing the label down to a single-character column and stacking it letter by letter. Capped the small-dropdown width so labels render normally again. - **OFF local mirror: scanning a UPC-A barcode no longer makes a pointless call to the public OFF API.** Scans now use the same canonical 13-digit format Open Food Facts stores its codes in, so the local mirror hits on the first lookup instead of after a roundtrip. (Issue #22 followup, reported by duplaja) --- ## [1.0.0-rc.40] — 2026-05-28 ### Fixed - **Diary: meal kcal footer truncated meals over 1000 kcal.** The per-meal subtotal at the bottom of each diary card was dropping digits after the thousands comma, so a 1,234 kcal lunch displayed as "1 kcal". (Issue #51, reported by LoveHonorGirth) - **OFF local mirror: barcode scans threw a JS error on the client.** The Parquet schema adapter assumed `product_name` lists were always real Arrays, but the DuckDB Node API can return list-like iterables that broke the assumption and leaked through as a non-string field, crashing the client's `.trim()` call. Now coerces to a real Array up front and guarantees a string output in both schema branches. (Issue #22 followup, reported by duplaja) ### Changed - **OFF local mirror Docker example now uses a parent-directory bind mount** (`./off-mirror:/data/off-mirror` with `OFF_LOCAL_DB=/data/off-mirror/off.parquet`) across `docker-compose.yml`, `DEPLOY.md`, and `.env.example`. The previous single-file bind-mount example tripped Docker's auto-create-as-directory behavior on a fresh host path, which then broke the atomic-swap refresh with `EISDIR`. Existing setups that already work are unaffected. (Issue #22 followup) ### Added - **Per-query debug logs for the OFF local mirror.** With `LOG_LEVEL=debug`, every barcode and name-search lookup now prints whether it was served from the local mirror or fell through to the remote OFF API, with hit counts for searches. - **Loading indicator for barcode scan lookups.** Slow Open Food Facts lookups (cold local-mirror queries, overloaded remote API, sluggish networks) used to leave the user staring at the Foods tab between camera close and editor open with no feedback. A centered "Looking up barcode" card now appears if the lookup hasn't returned within 400ms; fast lookups still flash nothing. Library hits remain instant with no indicator. - **Body Water % as a body stat.** New manual-entry field on the body stats sheet, alongside Body Fat %. Withings already auto-syncs this via type 77 (and continues to), so connected users see it without typing; non-Withings users can now log the number their smart scale displays. Available as a metric on Goals, Statistics, and the Settings visibility toggle list, with the same hide-by-default behavior as other optional body stats. (Issue #52, requested by LoveHonorGirth) --- ## [1.0.0-rc.39] — 2026-05-27 ### Added - **OIDC RP-initiated logout (PWA + Android).** Signing out now also ends the session at your identity provider, so the next sign-in isn't silently completed by a still-alive IdP session. Requires registering two Post Logout Redirect URIs at your IdP (HTTPS root for PWA, `nutritrace://oidc-callback` for Android). Thanks to @jimmielightner. (Issue #48, PR #49) - **Edit any Quick Calories entry.** Tap a Quick Calories row in the diary to change the kcal, name, or any of the three optional macros. ### Changed - **OFF local mirror now uses the Hugging Face Parquet snapshot** since OFF retired the previous DuckDB source. Existing setups keep working without renaming; format is auto-detected. (Issue #22 followup) - **Quick Calories diary row uses your custom name** when a meal has just one Quick Calories entry. Multi-entry meals still collapse to "Quick Calories × N". - **Diary edit sheet macro pills get the four-color treatment** (yellow / purple / green / orange) to match the Quick Calories card and Foods quantity sheet. - **Number-input spinner arrows removed app-wide** across food, meal, recipe, and diary edits. ### Fixed - **Docker image now actually loads DuckDB.** Base swapped from Alpine to Debian-slim so the OFF mirror feature works end to end. Image grows ~30 MB. (Reported by duplaja) - **Goals: body-stat fallback now appears on the initial "Your Goals" tab** instead of only after switching tabs. (Issue #46 followup from duplaja) - **Quick Calories: macro input width grows with the typed value** so the "g" stays flush against the digits regardless of length. --- ## [1.0.0-rc.38] — 2026-05-25 ### Added - **Gradient page banners.** New Settings → Appearance → Page Banners dropdown (Animated / Gradient / Off). Gradient is a compact, accent-tinted header with a glassy backdrop: middle ground between the tall illustrated banners and the bare compact header. New installs default to Gradient; existing users keep whatever they had. Header action buttons (water, stats, kebab, etc.) get a frosted-pill treatment in Gradient mode so they stay readable against the accent backdrop. - **Quick Calories** (Issue #42). Bolt button on each meal section opens a fast-entry sheet for logging just calories without food or portion data, Fitbit-style. Honors your energy unit (kcal / kJ). Optional protein / carbs / fat fields under the kcal input for MFP-style "log the macros from the label" use. Settings → Diary toggles to show/hide the button and choose Summed or Separate display when you have multiple Quick Calorie entries in one meal. Trace AI can also log entries via voice or chat ("log 240 kcal for lunch with 20g protein"). - **Logging streak in Trace.** Ask "what's my food logging streak" and Trace answers from the actual diary: consecutive days walking back from today (or yesterday if today isn't logged yet) until the first gap. Counts any diary engagement (food, water, body stats, notes), matching the typical user mental model of "I tracked today." ### Changed - **Statistics: days with food but 0 g of a nutrient now count as 0-day entries** instead of being silently dropped (Issue #45). Affects average, min, max, days-logged tally, chart, and history list. Avoiding added sugars on a day you otherwise logged food now shows as a 0 bar, not a blank, and your "days logged" matches across nutrients. - **Goals: weight, body fat, and measurements fall back to the most recent reading within the last 30 days** when today has no entry (Issue #46). A subtle "as of May 18" subtext under the value tells you the number isn't from today. Encourages weekly weighers without forcing daily re-entry. Nutrient and water goals are unchanged; those reset daily by design. - **AI assistant: nutrition-history queries no longer capped at 90 days** (Issue #44). The `get_diary_averages` window now goes back up to 10 years, and the tool does a single bulk fetch instead of one HTTP call per requested day, dramatically faster for long-history users (e.g. multi-year MyFitnessPal imports). ### Fixed - **AI assistant streak hallucinations.** Mini-class models would invent streak numbers instead of calling the streak tool. Your streak is now pre-computed into Trace's context block on every chat round, so the answer is right in front of the model and doesn't depend on it picking the right tool. --- ## [1.0.0-rc.37] — 2026-05-24 ### Added - **Local Open Food Facts mirror** (self-hoster opt-in, env-gated). Point NutriTrace at a local DuckDB copy of OFF and barcode + name lookups hit it first for instant, network-free responses. Stale-mirror false negatives auto-heal by falling through to the public OFF API on a miss, so a week-old mirror still feels live. True air-gap mode (`OFF_LOCAL_ONLY=1`) blocks all remote traffic, including OFF account uploads. See `DEPLOY.md` → Local Open Food Facts mirror for the full setup. (Issue #22) - **In-app refresh control for the OFF mirror.** Settings → Connected Services → Open Food Facts (admin only) shows mirror size, last refresh, and an Auto-Refresh schedule (Off / Daily / Weekly / Monthly, default Weekly). A Refresh Now button kicks an immediate full download with live progress in the banner. Initial download happens automatically on first boot when the env var is set; no manual `wget` required. Refreshes use atomic swap, so no container restart is needed and a failed mid-flight download never corrupts the running mirror. - **Connection status banner for push notifications**, matching the pattern used by USDA / Mealie / AI / Wellness integrations. Top-of-card pill shows the configured provider; a Send Test button posts a test notification through whichever delivery method is active. ### Changed - **Sleep score tuning.** Very short nights (under 5 hours) no longer balloon the deep+REM percentage above realistic ceilings (35% under 5h, 40% under 6h), bringing calculated scores closer to Fitbit's own. - **About → Disclaimer strengthened.** Spells out the medical conditions NutriTrace is not designed for (diabetes, eating disorders, allergies, pregnancy, breastfeeding, pediatric needs, kidney/liver disease, metabolic disorders), and underscores that Trace AI answers and Open Food Facts data can be incorrect. ### Fixed - **Bulk-imported foods now sync immediately to Android.** After a successful bulk import on server-connected Android, the app triggers a full sync so the foods appear in the foods tab without waiting 30-60 seconds for the periodic sync. (Issue #39 followup) --- ## [1.0.0-rc.36] — 2026-05-23 ### Fixed - **Bulk-imported foods didn't show up on Android.** The server wasn't stamping `updated_at` on new rows, so the Android differential sync skipped them. PWA was unaffected. Includes a migration that recovers rows already imported. (Issue #39) - **AI Assistant chat panel could stop opening** after a tool-use round. Duplicate message keys broke the panel render until history was cleared. Keys are now position-stable and the server drops back-to-back identical history inserts. (Issue #40) ### Added - **Scan Label**. On the Add/Edit Food screen, take a photo of a nutrition label and the AI Assistant fills in the values. Requires AI Assistant to be configured. ### Changed - **Custom nutriment order is reflected in the food and meal editors.** Reordering nutrients in Settings now changes their display order everywhere. - **Vitamin D, Calcium, Iron, and Potassium are visible by default** on new installs, matching the FDA's mandatory Nutrition Facts label fields. ### Fixed - **kcal/kJ inconsistencies when energy unit was set to kJ.** Food editor, diary footer, meal totals, statistics charts, goals, weekly summary email, and push notifications all respect the chosen unit now. Internal storage stays kcal; only the display converts. (Issue #38) - **Single-user mode: only the first food item added each day was visible** in the UI. A SQLite quirk meant NULL user_id rows never collided on upsert, so each save inserted a new row instead of updating. A one-time migration on first boot recovers items already in the database. (Issue #37) - **AI features failed on Android when configured via server env vars.** Smart Log voice (Trace hold-to-record), Scan Label, the chat panel, and Goal Insights all routed through a code path that didn't send the auth token Android needs, so calls returned 401 even though the env-lock was set correctly. The voice path also threw "AI provider not configured" before it ever reached the proxy. Both gaps are closed. (Issue #36 followup) --- ## [1.0.0-rc.34] — 2026-05-22 ### Fixed - **Trace AI chat showed "API key required" when AI was configured via env vars but no per-user key was set.** The chat panel's setup gate was checking the per-user key only and didn't know about env-locked configuration. Users who only set `AI_API_KEY` in env (never opened Settings to configure a per-user key) couldn't get past the setup screen even though the proxy was correctly handling auth on the server side. (Issue #36) --- ## [1.0.0-rc.33] — 2026-05-22 ### Fixed - **Trace AI chat said "API key required" even with `AI_API_KEY` set in env.** The chat path was using a separate code path from Settings → Test, which is why the test button said "Connected" but the actual chat refused to send. (Issue #36) --- ## [1.0.0-rc.32] — 2026-05-22 ### Fixed - **Barcode scanner stuck after a scan**. The camera could get stuck re-opening until Android refused. - **AI Assistant env vars**. Setting `AI_ENABLED=true` in compose now actually enables the assistant (was locking the toggle OFF). Removing AI_* env vars and restarting now properly unlocks the UI. (Issue #36) ### Changed - **Barcode that doesn't match Open Food Facts** now opens the food editor with the barcode pre-filled so you can enter it and contribute back. - **Share to OFF** is now smart: the button flips to "View on OFF" for products already on Open Food Facts, opening the wiki page where edits actually take effect. - **Open Food Facts contribution** now sends nutrition correctly when your portion isn't 100g, and uploads your product image. Both were broken silently before. --- ## [1.0.0-rc.31] — 2026-05-22 ### Fixed - **Data APIs in single-user mode**. After disabling user management, the diary, foods, meals, and app-config endpoints were returning 503 "Setup required" on every call. The rc.30 fix patched the wizard redirect, but a separate server-side setup gate was still blocking data APIs. Both gates now respect the same single-user-mode flag. (Issue #34) --- ## [1.0.0-rc.30] — 2026-05-22 ### Changed - **Framework upgrades**. Svelte 4 → 5, Vite 5 → 6, Express 4 → 5, bcryptjs 2 → 3, plus the supporting Vite plugin bumps. Compat mode keeps the existing component code working without rewrites. The main app bundle drops about 22% (1.57 MB → 1.21 MB) and the underlying Svelte 4 SSR security advisories no longer apply. ### Fixed - **Disabling user management was sending you back to the setup wizard** on every reload. The server now remembers that user management was intentionally disabled and lets you back to the diary. (Issue #34) - **Import Nutrition History action row could visually overlap** the Skip / Merge / Replace radios when re-importing a file with duplicate dates. (Issue #33) - **Wellness scores now refresh** when today's sleep data finishes syncing after the morning snapshot ran, so the readiness number reflects the latest sleep input rather than getting stuck on the first incomplete value. ### UI polish - The live nutrition preview pills on the Add to Diary sheet now match the diary's macro color scheme (calories yellow, protein purple, carbs green, fat orange). --- ## [1.0.0-rc.29] — 2026-05-21 ### Fixed - **Food no longer duplicates on the server when saving several new foods back-to-back on Android (server-connected mode).** A race between the immediate save POST and the background sync push could insert a second copy of the previous food. Saves now go through a single sync path. PWA and native-local users were not affected. (Issue #32) - **"Most Used" and "Recently Used" food sort orders now work on Android.** The sync engine was dropping usage counters when pulling foods and meals from the server, so the local sort keys never reflected real usage. Counters from the server (including favorites) now flow into the local mirror correctly. PWA was unaffected. Existing counters catch up the next time each food or meal is edited; a fresh sign-out / sign-in also forces a full re-pull. --- ## [1.0.0-rc.28] — 2026-05-20 ### Added - **Bulk Food Import** in Settings → Import & Export. Paste a JSON snippet or upload a CSV to add many foods at once. Download a template right from the modal. The template is built from the live nutrient catalog, so any future nutrient will appear in it automatically. Useful for users who want to paste an LLM-extracted nutrition label or sweep in a hand-rolled spreadsheet. (Issue #21) - **Live nutrition preview** on the Add to Diary sheet. Calories plus protein, carbs, and fat update as you change serving size, unit, or number of servings. (Issue #30) - **Mass-aware unit conversion** when scaling nutrition. Changing the unit on a food (g, mg, kg, oz, lb, ml, l, tsp, tbsp, fl oz, cup) now actually converts the nutrition values. Previously the unit was just a label and the math ignored it. The full list of which units convert is documented in the project README. (Issue #29) - **Custom Units**. Add your own units (e.g. "shot", "scoop", "stick") in Settings → Custom Units. They appear under a Custom group in every unit picker. Custom units do not get mass conversion since they have no fixed gram weight; they scale by portion count only. The Settings card explains this on the spot. - **Day rollover**. If you leave NutriTrace open overnight and reopen it the next morning, the diary now auto-advances to the new today. Same for tabs that stay open across midnight. - **Connection status banners** across every integration. AI Assistant, Mealie, USDA, SMTP, Fitbit, Garmin, Withings, and Health Connect each show a consistent banner with current status and a one-tap Test / Sync / Disconnect button. - **Sync Now** button on the Settings cards for Withings, Garmin, and Health Connect. Previously only Fitbit had one in Settings. ### Changed - **AI Assistant Google Gemini models updated.** Added Gemini 2.5 Flash (new default for the Gemini provider, free-tier eligible) and Gemini 2.5 Flash Lite. Removed the older Gemini 2.0 Flash (Google deprecated it Feb 18, 2026, shutting down June 1, 2026) and the legacy Gemini 1.5 Flash fallback (Google already shut it down). If you had one of the retired models selected, you're auto-migrated to Gemini 2.5 Flash. (Issue #27) - **Food Editor proportional scaling default is now context-aware.** Editing an existing food opens with proportional scaling ON (so changing serving size preserves density). Adding a new food from scratch opens with it OFF (so typing label values one-by-one doesn't silently cross-scale). You can flip it either way with the link icon next to the unit field. (Issue #28) - **Settings reorganized.** The previous "Import from another app" section was merged into a new "Import & Export" section that houses every flow that moves food data in or out: JSON backup import/export, Bulk Food Import, MFP/Cronometer/LoseIt diary importer, and Diary-as-CSV export. - **Settings auto-save by default.** Most settings now save automatically when you change them, instead of needing a Save button click. Includes the AI Assistant key + Base URL, Mealie URL + token, USDA key, all SMTP fields, session duration, and other text/dropdown settings. The wellness Connect buttons (Fitbit, Garmin, Withings) still need an explicit click since they trigger an OAuth flow. - **Unit picker is uniform across the app.** Food editor, meal editor, recipe yield, Add to Diary, diary edit, and multi-portion sheets all use the same grouped picker with categories (Mass Metric/US, Volume Metric/US, Count). Free-text input is no longer accepted in the picker; add your own units in Settings → Custom Units if you need something off the catalog. - **Compact page header** when banners are disabled. If you have page banners turned off in Settings → Appearance, the title now sits beside the menu button instead of below it, freeing roughly 40 pixels of vertical space on every page. (Parity with LiftTrace's compact header.) - **Barcode scanner switched to a fully on-device path on Android.** No longer requires Google Play Services to scan a barcode. Same scanning engine and accuracy as before. Works on degoogled Android ROMs (GrapheneOS, /e/, CalyxOS). The PWA scanner is unchanged. (Issue #31) ### Fixed - **Diary item edit sheet** now scales nutrition correctly when you change the unit. The live macro pills above Save reflect the change in real time. - **Water log sheet's close button** stays visible when the content scrolls. - **Title-case slips** across various dropdown options, dialog labels, and button text. - **Dark-mode black text** on the new Settings → Import & Export rows. ### Security - **nodemailer updated** to 8.0.7 (was 8.0.3). Patches two open SMTP command-injection advisories (envelope.size parameter, CRLF in transport name). Only affects servers configured with outbound SMTP for invites and password resets. --- ## [1.0.0-rc.27] — 2026-05-18 ### Fixes - **MyFitnessPal import now handles the current export from Reports → Export.** The export at myfitnesspal.com/reports/export is aggregated per meal per day (no Food column), and the importer was rejecting it as unrecognized. Importing now creates one diary entry per meal per day with brand "MyFitnessPal" and the meal's macro + mineral totals preserved. Individual foods can't be reconstructed because they aren't in the export. The older per-food Premium ZIP shape is still supported when present. - **MyFitnessPal import error now shows the actual columns it saw.** Any future MFP-import failure includes the header row in the error message so it's possible to tell at a glance whether MyFitnessPal has changed the export format again. --- ## [1.0.0-rc.26] — 2026-05-17 ### Fixes - **Wellness page now populates on first load for Health Connect users.** Previously the data only appeared after navigating to a different day and back. Health Connect now counts as a connected source when deciding whether to fetch on page load. - **Mealie connection Test button.** The Test button was always reporting "Failed" because the request was missing the CSRF token. The actual Mealie integration was unaffected (recipe search from Foods worked correctly), but the misleading Failed indicator made it look like the integration was broken. Test now matches the runtime path. - Foods page no longer logs a "Function called outside component initialization" error to the console on load. --- ## [1.0.0-rc.25] — 2026-05-16 ### Fixes - **Health Connect updates throughout the day now reach the server.** When Health Connect re-read a metric (e.g. step count went from 500 to 1,382 as the day progressed), the local row updated but stayed marked as already-synced, so the next push to the server skipped it. The server kept the early value and the web showed a stale snapshot. Now any value change re-queues the row for push. --- ## [1.0.0-rc.24] — 2026-05-16 ### Fixes - **Health Connect data now populates the web Wellness page tiles, sparklines, sleep insights, and the readiness card.** Final piece of the Health Connect pipeline (after rc.22 and rc.23). The web's data endpoint now returns Health Connect rows alongside Fitbit rows, and the sparkline / sleep insight / readiness fetches now fire for Health Connect users (not just Fitbit users), so the synced metrics fully populate the Wellness page. --- ## [1.0.0-rc.23] — 2026-05-16 ### Fixes - **Health Connect data now renders on the web Wellness page.** Companion fix to rc.22. The server stores Health Connect entries synced from Android, and the web app now recognizes Health Connect as an active integration so the data shows up in the Activity / Sleep / Heart / Body tabs (managed from your Android app's Settings → Wellness). --- ## [1.0.0-rc.22] — 2026-05-16 ### Fixes - **Health Connect data now flows to the web app.** Health Connect entries synced from the Android app push up to the server during differential sync, so the same data renders in the web Wellness page and on any other connected device. Existing Health Connect history on a device backfills automatically on the first sync after this update. --- ## [1.0.0-rc.21] — 2026-05-15 ### New Features - **Recipe yields.** Recipes can now declare how many servings they make. Set "Yields" in the recipe editor and adding one serving to the diary uses per-serving weight + nutrition. Existing recipes keep their old behavior until you set a yields value. - **Per-serving Open Food Facts import.** New option in Settings → Connected Services → Open Food Facts → Import Portion As. When set to Per Serving, barcode scans and OFF searches prefill the food with per-serving values whenever the product has serving data on OFF. - **Intermittent Fasting tracker.** Track fasts from a Diary widget: default goal, last-fast hint, named custom-hour presets, history with delete, and a recurring schedule that auto-starts a fast on chosen days at a chosen time. Tap the start time on an active fast to edit it inline. - **Android biometric sign-in.** Fingerprint and face unlock for the Android app in server-connected mode. Toggle in Profile. - **Adaptive TDEE.** Calorie Goals can learn your true daily energy expenditure from a rolling 35-day window of weight and diary trend, instead of the static Harris-Benedict estimate. ### Improvements - Settings forms use sliding-pill segmented controls. Goals section renamed to Calorie Goals. - Sharing rework: per-category sharing form, source filter on Meals/Recipes, and read-only editors when viewing someone else's shared food. - User management: invite by email, revoke pending invites, block invites to existing accounts. Optional zxcvbn-backed password strength policy. - Wizard summary clarifies the TDEE label and shows the goal-factor math. - Android local-only mode reaches feature parity with server mode for Adaptive TDEE and the Fasting tracker. - AI Assistant (Trace) gains two new tools: `get_adaptive_tdee` and `get_activity_log`. - `INSECURE_COOKIES` env var is now documented in `.env.example` and the README env-vars table. Required for self-hosters on plain HTTP without TLS in front. ### Fixes - Wrong food images in the diary (cross-pollination across foods that share an Open Food Facts basename). Diary images are now live-resolved from the current foods and meals tables on every read, routed by recipe vs food, and disambiguated by brand. - Duplicate food rows when scanning the same barcode in rapid succession. - Invite "stuck on Creating…" when the email field was invalid. - Password policy now persists when saved. - Revoke invite no longer surfaces a fake "Failed to fetch" message. - Session config loads correctly on Android by sending auth headers on the initial GET. --- ## [1.0.0-rc.17] — 2026-05-04 ### Food Sources - **Open Food Facts** now has its own enable/disable toggle in Settings → Food Sources, defaulting on. Privacy-conscious users can opt out of OFF queries; the source picker on the Foods page hides OFF when disabled. - OFF moved above USDA in the Food Sources section to reflect that it's the primary search source. - USDA description now links directly to the free API key signup at `fdc.nal.usda.gov`. - The OFF account section clarifies that an account is only required to upload edits — searching works without one — and links to the OFF account signup. ### Wizard - Integrations step gets the same signup links on the OFF and USDA cards, and the OFF subtitle now describes it as a search source rather than just an upload destination. - Integration summary at the bottom of the step no longer mis-labels cards as "Skipped" when you simply hadn't filled in credentials yet. Only cards you explicitly clicked "Skip this" on count as skipped now. ### Persistent sidebar (tablet / desktop) - Fixed a layout bug where pinning the sidebar caused page banners and the right edge of content to clip. Affected Diary, Foods, Wellness, Statistics, Goals, and Settings. - Fixed a gap between the page header and sticky sub-bars (Foods filter, Diary date picker, Settings search, Wellness date + tab bars) when the sidebar was pinned. - Fixed the persistent-sidebar toggle silently flipping itself off after about 30 seconds. Cause: form-factor preferences were being overwritten by stale server values on every settings sync. - Persistent-sidebar toggle is now correctly hidden on phones (768px viewport required) and reacts to live tablet rotation between portrait and landscape. --- ## [1.0.0-rc.16] — 2026-05-04 ### Fixed - **Fresh Docker installs failed at startup with `SqliteError: no such column: email`** in rc.14 and rc.15. A schema-rebuild migration ran before the `email` column was added, so its `INSERT … SELECT email FROM users` couldn't find the column on a brand-new database. Existing installs were unaffected. Reported by @Skilly2, confirmed by @cearum — thanks both. --- ## [1.0.0-rc.15] — 2026-05-04 ### Energy units (kJ vs kcal) - The energy-unit setting is now respected throughout the UI. Previously it was honored for goals and storage, but Diary food rows, the daily summary, the macro ring, the Foods picker, Meal editor, Smart Log, Wizard summary, the Wellness "Calories Burned" tile, the Withings BMR tile, and the Nutrition Import preview all rendered "kcal" regardless of the setting. Reported by an Australian user on Lemmy. Internal storage stays in kcal, only the display layer flips. - The AI assistant (Trace) now speaks in your chosen energy unit when summarizing workouts and basal metabolic rate, instead of always saying "kcal". - Statistics now defaults its energy metric to **Kilojoules** when your energy unit is kJ, instead of always defaulting to Calories. ### Wizard - Now auto-detects your device locale on first run. If your locale is `en-AU` or `en-NZ`, kilojoules are pre-selected; everywhere else still defaults to kcal. You can still toggle either way manually. --- ## [1.0.0-rc.14] — 2026-05-03 — Android app available ### Android app - NutriTrace is now available as a native Android app, alongside the existing PWA. Download the signed APK directly from the GitHub Releases page. - **Standalone or connected** — use it pure-offline with everything stored on the device, or connect it to a NutriTrace server for sync. - **Wellness via deep links** — Fitbit, Garmin, and Withings OAuth flows open the system browser and return to the app via `nutritrace://`. - **Health Connect** — read steps, sleep, heart rate, body weight, and more from any Health Connect-compatible source. No external account required. - **Native barcode scanning** via Google's ML Kit, with a Code Scanner fallback. - **Native notifications** — water reminders, meal reminders, weigh-in prompts, and goal celebrations all schedule through Android's notification system, including in deep doze. - **OIDC SSO on Android** — Authentik / Keycloak / Pocket ID / etc. open in Chrome Custom Tabs and return via deep link. Same SSO setup as the PWA. Issues and feedback welcome on GitHub. ### Settings - **New layout** — Settings now groups sections by frequency-of-use rather than by accident. Goals leads Data & Tracking (was hidden behind setup-and-forget customizations). Notifications moves into the App group. Admins get a dedicated **Admin** group containing Users, Authentication, and Email (SMTP), shown only when relevant. About is the standalone footer. - **"User Management" → "Users"** since the section is admin-gated and the longer label was redundant. - **"Delete my account"** is no longer duplicated under Backup & Restore — Profile → Danger zone is the single source. - **Page banners**, **Show yesterday's meals**, and **Goal pulse animation** descriptions tightened for clarity. ### Wizard - Target weight now defaults to your current weight instead of a hardcoded "lose 10" placeholder. Tap Next to maintain; nudge it up or down if you're trying to gain or lose. ### About + docs - Settings → About now shows a platform tag (Android / PWA) next to the version, useful at a glance and for bug reports. - README Roadmap and Experimental features list updated to reflect what's actually shipping vs still in flight. ### Nutrition Import (Cronometer + Spreadsheet) - Fixed a bug where imported items had calories multiplied by their gram weight (a 750g entry showed `722,903 kcal` instead of `964 kcal`). Cronometer and Spreadsheet adapters were treating the parsed gram count as a serving multiplier; now each row imports as one diary item with totals already in nutrition. The "NaNg" portion display had the same root cause and is also fixed. - Visual separation between the duplicate-day radio options and the Cancel / Import buttons in the import preview. --- ## [1.0.0-rc.13] — 2026-05-03 ### Authentication (OIDC SSO) - Configure providers entirely from `.env` or `docker-compose.yml`. Single-provider shorthand uses `OIDC_*`; multi-provider uses `OIDC_PROVIDER__*`. Anything you define this way shows up in the Settings UI with a lock badge and is read-only there, so the source of truth stays in your config. - OIDC moved out of User Management into its own top-level Settings section called **Authentication**. README walkthrough rewritten to call out the prerequisite (User Management must be on) up front. ### Settings - **My Profile** is now a hero card at the very top of Settings, showing your avatar, display name, and an admin pill when applicable. Tap to edit. - **Log Out** lives inside Profile, next to your name and avatar. - Profile editing now works in single-user mode (was previously unreachable — the page bounced back). - Switching between single-user and multi-user no longer loses your wizard-configured settings or profile data. ### AI Assistant - Trace now has always-available access to your profile: name, nickname, age, gender, height, weight, target weight, activity level. Ask "what's my name" or "how old am I" and you get an answer without a tool round-trip. --- ## [1.0.0-rc.12] — 2026-05-02 ### AI Assistant - New "OpenAI Compatible" provider — point at Ollama, LM Studio, DeepSeek, Groq, or any `/v1/chat/completions` endpoint. Local endpoints don't need an API key. - Trace greets you by name when you've set one in your Profile. ### Diary - Portable JSON export now has a self-describing header (version, counts) and restores Activity entries on import. ### Profile - Polish on linked accounts, security card, and danger zone — content fits properly at any viewport size. ### Diagnostics - Exported logs lead with a version + platform header so recipients know what they're looking at. ### Fixed - Nutrition Import "Invalid CSRF token" on Preview — the upload was missing its auth header. Reported by the community. --- ## [1.0.0-rc.11] — 2026-05-02 ### Wellness - Daily Readiness and Stress Management cards now show interpretive copy under the score — a lead sentence based on the band + a driver line that calls out which sub-component (HRV / RHR / Sleep) is dragging things down, with one concrete action. ### Diary - Diary lines lead with the *actual amount eaten* as the primary number; fractional quantities show an explicit multiplier ("× 1.5") so the math is unambiguous. ### Barcode scanning - Scans check your existing library first instead of always re-fetching from OFF / USDA. - Inline scan icon inside the barcode input on FoodEditor — scan without leaving the editor. - Duplicate-barcode warning when the barcode is already used by another food. - Leading-zero normalization so scans match library entries that were typed manually. ### Profile + User Management - Linked-accounts row no longer clips on narrow viewports. - Manual-add user form restructured — password input no longer squished, role uses a proper chevron-styled select, manual-add and invite read as alternatives separated by a divider. - Smaller polish to the Security card and Danger Zone delete button. ### Fixed - Settings → Backup & Restore no longer freezes the page in Android local-only mode. --- ## [1.0.0-rc.10] — 2026-05-01 — Fix stuck "Could not reach server" banner after login ### Fixed - **Diary banner persistence** — when the app was opened while not authenticated, the diary's pre-login fetch returned 401 and stamped a synthetic empty-day placeholder into the local cache. After signing in, the diary's cache-skip logic saw the placeholder and skipped the fetch — leaving the "Could not reach server" banner up forever even though authenticated requests were now succeeding. Three small fixes: the failed-load placeholder is gone (cache reads as "no entry" so the next mount re-fetches), the diary onMount additionally re-fetches whenever the previous load errored, and a successful login now re-pulls server settings + clears any lingering diary error flag so the next view is clean. Same root cause was making **settings appear reverted to defaults** after login (the initial `loadServerSettings()` failed at 401 pre-login and was never retried). Fixed by the same login-transition hook. Reported in a community issue against rc.13; same bug existed in rc.9. --- ## [1.0.0-rc.9] — 2026-05-01 ### Single Sign-On (OIDC) - Sign in with your existing identity provider — Authentik, Keycloak, Pocket ID, Authelia, Auth0, Google, or any OIDC 1.0 provider. Settings → User Management → OIDC providers, with a guided picker that pre-fills sensible defaults per IdP. - Auto-link verified-email accounts on first sign-in (default ON) and an opt-in auto-register-new-users toggle for blanket onboarding. - Admin role mapping via group claims, runtime password-login disable for OIDC-only instances, and Profile → Linked accounts to attach SSO to an existing password account. - Client secrets encrypted at rest. Discovery cached, PKCE + state + nonce validated on every callback. ### Import from another app *(experimental)* - Bring past days into NutriTrace from **MyFitnessPal**, **Lose It!**, **Cronometer**, or a generic spreadsheet. Settings → Import from another app. - Preview shows day count, date range, and any unmapped meal labels before commit. Per-date conflict policy: skip, merge, or replace. ### Settings reorganization - Cleaner group structure with renamed sections (Integrations / Food Sources) so labels don't collide. - Long labels no longer push toggles or icons off-screen on narrower viewports. ### User Management polish - Inline role change on the user list (with a last-admin guard that refuses to demote the only admin). - Admin row action to reset another user's password. - "Delete my account" Danger zone on the Profile page. - Invite-by-email is now the primary "Add user" path; direct-add stays as a quieter secondary option for environments without SMTP. - Profile shortcut redesigned with a gradient avatar + role pill. ### Reliability + security - Sign-out actually signs you out — previously the server cookie wasn't cleared, leaving you in a half-state. - Theme no longer flashes to defaults every 30 seconds when settings poll. - Diary doesn't briefly flash meal cards on every tab switch. - Database migration hardened against a foreign-key cascade that could wipe per-user data on upgrade. Existing deploys are unaffected going forward. - Sync engine repopulates server-side rows that go missing, instead of silently dropping the change. - Photo uploads downscale before saving, so phone photos no longer silently fail to save when they exceed the server's payload limit. - Forgot-password endpoint timing-padded so response time can no longer be used to enumerate registered emails. - JWT rotates on password change, so old sessions on other devices stop working. - Stress score recalibrated against accumulated calibration data. ### For self-hosters - Backups now include OIDC providers and per-user links. `client_secret` is encrypted in the dump; restoring to a host with a different `JWT_SECRET` (and no `TOKEN_ENC_KEY` override) requires re-entering secrets. - New env var `TOKEN_ENC_KEY` for independent rotation of at-rest encryption keys. --- ## [1.0.0-rc.8] — 2026-04-30 — Manual activity logging (issue #3) Adds an opt-in **Activity** section to the Diary so you can log exercise calories without a wearable. Closes [#3](https://github.com/TraceApps/nutritrace/issues/3). ### Added - **Activity section on the Diary.** Turn it on in Settings → Diary → "Show activity section" (off by default). Each entry takes a name plus calories burned, with optional duration and distance. Multiple entries per day, edit or delete via long-press. Sits below your meals and visually mirrors them — green segmented bar showing each entry's share of the day's burn, total at the bottom. - **Optional earn-back toggle.** Settings → Diary → "Adjust calorie goal from activity" (off by default). When on, today's burn raises your calorie remaining for the day. The expanded Remaining panel shows the math — *"Goal 2,000 + Activity 540 = 2,540 kcal"* — so it's clear what bumped the number. Macro targets (protein / carbs / fat) stay anchored to your base goal regardless. - **Wearable + manual coexistence policy.** When you have both a wearable integration and manual entries on the same day, a Settings radio decides how they combine: *Wearable wins* (default — manual entries display but don't move the goal math), *Manual wins* (replaces the wearable's burn for the day), or *Add together* (with a double-count warning). The policy section only appears when the earn-back toggle is on. - **Trace can log activity for you.** New `add_activity_entry` tool — say *"I hiked 10 miles, about 1000 calories"* in chat or hold-to-record on the FAB and Trace logs it directly. If you don't supply a number, Trace can estimate one from your body profile — gated by a separate "Estimate activity calories" toggle in Settings → AI Assistant, which only enables when your weight, height, date of birth, and sex are all set. - **Wellness hint on the Activity header.** When any wearable integration is enabled, a subtle line under "Activity" reminds you that synced device workouts (Garmin runs, Fitbit rides, etc.) live on the Wellness page — Activity is for what you log manually. - **Negative remaining values.** Goal totals in Remaining mode now show how far you are *over* a target (e.g. `−20 g Fat` instead of `0 g`) for calories, protein, carbs, fat, sodium, sugars, fiber, and other tracked nutrients. Water still clamps at zero. ### Storage & backups - New `activity_log` table covered by Full Backup ZIP, JSON export, and the Clear Data path. --- ## [1.0.0-rc.6] — 2026-04-28 — i18n scaffolding, subpath support, wizard cleanup ### Added - **Internationalization (i18n) scaffolding.** `svelte-i18n` wired up with a JSON-per-locale model under `src/i18n/`. New "Language" picker in Settings → Regional & Units that drives the active locale reactively. ~210 source strings extracted across navigation, page titles, full auth flow (Login / ForgotPassword / ResetPassword / AcceptInvite / Profile), the wizard, primary actions in Diary / Foods / Goals / FoodEditor / MealEditor, common toasts/buttons, and the Trace AI assistant FAB. New `npm run i18n:check` script reports per-locale coverage. Translation contributor guide added to `CONTRIBUTING.md → Translations` (covers adding a new language, conventions, and the "use regulatory nutrient terms not literal translations" gotcha for nutrition labels). Server-side strings (email subjects, push bodies, AI system prompts) intentionally out of scope for now. - **Reverse-proxy / subpath support via `BASE_URL` env var.** Lets users mount NutriTrace at `/your-prefix/` for reverse-proxy deployments without URL rewriting. Server mounts all middleware + routes inside an Express sub-router at the prefix; the client reads the basePath from `window.__NT_CONFIG__` (injected at HTML serve time). `apiUrl()` and `resolveAssetUrl()` in `src/lib/platform.js` are the single point where the prefix gets applied. Vite `base: './'` so generated asset URLs are relative; PWA manifest `start_url` and `scope` are `'./'` so PWA install works at any subpath. Default empty `BASE_URL` is identical to pre-feature behavior — existing deployments don't migrate. New "Reverse Proxy with Subpath" section in `DEPLOY.md` with Caddy / nginx / Traefik configs and OAuth callback guidance. Closes the feature request from issue #3 (tellis82). - **Shared `DatePicker` + `DateInput` components.** New `src/components/ui/DatePicker.svelte` extracted from inline calendar duplicates in Diary and Wellness — month/year nav, year/month grid pickers, day grid, locale-aware. `src/components/ui/DateInput.svelte` wraps DatePicker with a masked text input + calendar trigger button for form-style date entry (Profile birthday, Wizard dob step). Manual entry is now masked: only digits accepted, separators auto-inserted in the user's chosen format (ISO / US / EU), capped at 8 digits. ### Changed - **Wizard cleanup pass.** Removed duplicate birthday + gender fields from the Create Account form — the dedicated `dob` and `gender` wizard steps already capture both. Eliminates the previous mismatch where the auth form offered M/F/Non-binary/Prefer-not but the BMR-bound dedicated step only accepts M/F. Variant-logic comment added in source explaining the three usermgmt-step variants (native local skip / PWA force-create / PWA toggle). Top-bar Skip button now routes through the same confirmation modal as the welcome screen's "I'll do this later" link instead of bailing immediately. Per-integration "Skip" buttons relabeled to "Skip this" so they're visually distinct from the wizard-level skip. AI provider card renamed from "AI Buddy" (legacy waistline-ai-buddy naming) to "AI Assistant" with description mentioning Trace. ### Fixed - **Subpath-mode bypass paths.** Several modules constructed API URLs locally instead of going through `apiUrl()`: `src/stores/auth.js` had a duplicate `_apiUrl` helper, `src/lib/api.js` used `_resolveBaseUrl()`, `src/lib/api-cached.js` had `_base()`, `src/lib/sync.js` had `_baseUrl()`, `src/stores/settings.js` had `_settingsUrl()`, and Wizard / AcceptInvite / ResetPassword had raw `fetch('/api/...')` calls. All consolidated through `platform.apiUrl()` so the BASE_URL prefix gets applied uniformly. Prevents the 404 wave seen during the first subpath test. - **Static image references at subpath.** `` and similar absolute paths 404'd when running at a subpath. Touched components: Sidebar (visible on every page), Login / ForgotPassword / ResetPassword / AcceptInvite / NativeSetup / Settings → About panel, plus `src/lib/notifications.js` Web Notification icon. All routed through `resolveAssetUrl()` which prefixes `/uploads/`, `/api/`, `/icons/`, `/fonts/` paths with the basePath in PWA mode. - **CSRF rejected settings PUTs.** Fresh JWTs (issued post-CSRF-feature) require an `X-CSRF-Token` header on state-changing requests, but `settings.js` and Wizard's `_putConfig` never sent one. Added the header in both. Existing daily-use installs were shielded by old JWTs lacking the csrf field; this fix lands defensively before any logout-then-login could trigger the bug. - **Login flow now refreshes CSRF on sign-in.** Login.svelte previously called `currentUser.set(data.user)` and `loadServerSettings()` immediately, but the new JWT's csrf field never made it into localStorage until the next page reload — so the post-login wave of reactive settings saves all 403'd. Now awaits `loadAuthState()` (which fetches `/api/auth/me` and stores csrf) before settings load. ### Notes - Reverse-proxy / subpath support marked done in `FUTURE.md` Infrastructure section. New "Internationalization (i18n)" entry added to `FUTURE.md` between UI/UX Polish and Code/Performance. - Three volunteer translators raised hands for French (Lemmy), Dutch (Reddit), and German (Lemmy) on launch threads; outreach pending until translation tracking issue is opened on the public repo. - Caveat: `BASE_URL` should be picked at install time. Changing it against existing data leaves stale image URLs in old diary item snapshots since the snapshotted `imgUrl` was stored with the prefix from when it was logged. Documented in `DEPLOY.md`. --- ## [1.0.0-rc.5] — 2026-04-28 — Post-launch fixes + Docker secrets ### Added - **Sodium ↔ salt auto-derivation across the food editor and imports.** The food editor auto-fills the missing field via the regulatory factor (sodium_mg = salt_g × 400) when only one is entered, and shows a calculator icon next to the derived field so the value's origin stays visible. The same derivation runs at the data-source layer for OFF, USDA, and Mealie imports — products that ship only one of (sodium, salt) come in with both populated and the `_derived` flag set. A one-time idempotent backfill at server startup fills the missing field on existing foods + meals, and an equivalent backfill runs at native-SQLite init on Android so phones with cached pre-derivation rows self-heal too. - **Docker / Swarm secret file support.** New `server/docker-entrypoint.sh` reads any `*_FILE` env var at container startup, loads the referenced file, and exports the value as the corresponding env var before Node starts. Covers `JWT_SECRET_FILE`, `RECOVERY_TOKEN_FILE`, `SMTP_PASS_FILE`, `AI_API_KEY_FILE`, and any other server env var. Errors loudly if both `NAME` and `NAME_FILE` are set or if the file is unreadable. Documented in `DEPLOY.md` with a working compose snippet. Thanks to @clifmo for the contribution (#2). ### Fixed - **Disconnect from server now fully clears cached auth state.** Disconnecting was leaving `wl:userId`, `nt:cachedUser`, `nt:cachedUserMgmt`, and `nt:csrf` behind in localStorage, so the app continued to think the user was signed in (showing only Logout instead of Connect/Login). `disconnectServer` now wipes the cached auth keys and resets the `currentUser` and `userMgmtActive` stores. - **Logout actually invalidates the server session.** The previous logout flow only cleared client state, leaving the JWT cookie valid; the next page load would silently re-authenticate. Logout now calls `auth.js logout()` which posts `/api/auth/logout` to invalidate the session server-side. `_refreshAuthFromServer` also treats a 401 from `/api/auth/me` as logged-out (clearing local state) instead of "server error, keep cached", so the Login route appears as expected. - **Settings restore after re-login.** After a logout + re-login on the same device, settings like Trace FAB visibility and the Wellness section toggle weren't reapplying even though the server returned them. Per-user-scoped localStorage keys (`wl_u_`) survive logout, so `DB.setSetting` was early-exiting on unchanged JSON values and never dispatching the `wl:setting` event the stores listen for. Added a `force=true` flag to `setSetting` and routed `loadServerSettings` through it after re-login. ### Notes - New `Internationalization (i18n)` section in `FUTURE.md` covering svelte-i18n + Weblate as the planned translation contribution path. Tracked as v1.1 / v1.2 work. --- ## [1.0.0-rc.4] — 2026-04-27 — Diary image freshening + migration UX ### Fixed - **Stale diary images now self-heal.** Diary items snapshot every food field at log time, including `imgUrl`. If a food got an image AFTER a diary entry was logged with that food, the snapshot stayed empty forever (spread captured the empty value, snapshot semantics protected history). Cosmetic fields like images shouldn't be frozen the same way as name/macros, so a live-lookup pass now fills empty `imgUrl` from the foods table at read time. Applied at three layers so all surfaces inherit it: `GET /api/diary/*` (PWA users), `/api/sync/pull` (Android sync), and `dbGetDiaryDate` / `dbGetAllDiary` in `db-native.js` (Android local-SQLite reads). Wrapped in try/catch so a query error never breaks the calling endpoint. New shared helper at `server/lib/diary-helpers.js` to keep server code DRY. ### Changed - **Connect-to-server migration flow now shows counts and surfaces errors.** When connecting from standalone mode with existing local data, the merge dialog now displays per-table counts (foods, meals, recipes, diary days, settings) so the user knows what's about to move. The upload pass collects per-table success counts and per-row errors instead of silently `.catch(() => {})`-ing them; a summary screen after the upload shows what landed and the first 5 error messages if anything failed. Progress bar replaces the vague spinner during upload. New `src/lib/migrate.js` shared helper. (Mirrors a recent LiftTrace improvement.) ### Notes - New `Infrastructure → OIDC / SSO support` entry in `FUTURE.md` covering Authentik / Keycloak / Authelia / Pocket-ID integration as a tracked post-1.0 enhancement. --- ## [1.0.0-rc.3] — 2026-04-27 — Calibration export window includes today ### Fixed - **Calibration export was dropping today's row.** The 30-day window in `_generateCalibExport()` ran from `today-30` to `today-1`, silently excluding today even though today's seeded `*_actual` scores are typically what the user just paste-confirmed via the dev console snippet. The data was being fetched (the API call's `to=` param uses today) but never iterated during day-row construction. Window shifted to `today-29` → `today` inclusive — same 30-day length, ends today instead of yesterday. --- ## [1.0.0-rc.2] — 2026-04-27 — Sleep Debt reads the actual sleep goal ### Fixed - **Sleep Debt card on Wellness page hardcoded 8h** when the user's sleep goal was stored under `.max` (the default — "Minimum goal" toggle off in the goal editor). Code only checked `.min`, fell back to 480 minutes. Fix: use the same `max ?? min ?? default` fallback chain used elsewhere in the codebase (Goals.svelte, calorie targets, etc.). Users with custom sleep goals now see debt computed against their actual target. --- ## [1.0.0-rc.1] — 2026-04-26 — First public release candidate After many months of development as the v0.x-beta series, NutriTrace is going public for wider testing. This release candidate is a snapshot of the v0.39.x line that has been running stably in single-user and multi-user setups, with all major feature work complete. The full beta history is preserved below. ### What's in v1.0.0-rc.1 Aggregate feature surface (everything below is shipped): - **Diary** — configurable meals, multi-ingredient meals + recipes, body stats, water tracking, day-level free-text notes, per-meal ⋮ menu (copy / move / save-as-meal / clear / copy-to-date), long-press item actions - **Foods & Meals** — personal food database with photos, barcodes, categories; barcode scanner; meal/recipe builder; OFF + USDA + Mealie import; fuzzy search - **Statistics** — charts for any tracked metric, average / trend / goal overlays, dynamic-goal "Base Goal" line - **Goals** — calorie + nutrient goals with templates, Wizard TDEE calculation, optional Dynamic Calorie Goal (Experimental) - **AI Assistant (Trace)** — Claude / OpenAI / Gemini, tool use across all providers (real diary + meals + wellness + workouts + goals queries), Smart Log voice food entry (hold-to-record), Goal Insights (Experimental) - **Wellness Integrations** — Fitbit, Withings, Garmin, Health Connect (Android). Computed Trace Sleep / Readiness / Stress scores prioritizing day-to-day consistency - **Notifications** — device notifications + push services (Apprise / Gotify / ntfy), water / meal / weigh-in / bedtime / wellness alerts / weekly summary - **Backup & Restore** — full ZIP backup, JSON export/import, CSV diary export, Waistline import - **Multi-user + sharing** — optional account system, food sharing with private / group / specific visibility - **Self-hosted** — single Docker container, AGPL-3.0, no telemetry, no cloud sync, all data on your hardware - **PWA** — works offline-first in any modern browser, installable to home screen on Android ### Distribution - Source: [github.com/traceapps/nutritrace](https://github.com/traceapps/nutritrace) (AGPL-3.0) - Docker image: `ghcr.io/traceapps/nutritrace:1.0.0-rc.1` and `:latest` ### Known limitations (post-1.0 work — see FUTURE.md) - **Native Android app** — in development and testing, public release planned during the v1.x cycle. Android users can install the PWA via Chrome's "Add to Home Screen" today. - **Public demo instance** — coming post-1.0 on existing infrastructure - **Local / self-hosted LLM support** — high-priority post-1.0 (Ollama, LocalAI, LM Studio, vLLM via OpenAI-compatible adapter) - **iOS** — pending hardware + Apple Developer account access ### Reporting bugs `Settings → Diagnostics → View logs` → Copy / Share → attach to a [GitHub issue](https://github.com/traceapps/nutritrace/issues). Templates provided for bugs, feature requests, and wellness integration test reports. --- ## [0.39.36-beta] — 2026-04-26 — Trace FAB self-heals off-screen positions ### Fixed - **Trace FAB invisible on desktop PWA after viewport changes.** The FAB position is persisted in `localStorage` (`wl:aiFabPos`), but clamping was only applied during a drag — never on load or window resize. A position dragged on a wider monitor (or saved at a different viewport size) could end up off-screen on a smaller one, with no way to recover short of clearing localStorage. Mobile devices weren't affected because they have separate localStorage origins. Fix: added `_clampFabPos()` helper that clamps to current viewport bounds; runs on mount (writes back the corrected value) and inside `_updatePanelPos` on every resize. LiftTrace already had this guard — NutriTrace was the lone outlier. ### Added - **Collapsible "Yesterday's Meals" + "Saved Meals" sections** in Foods → Meals tab. Each section header is now a clickable row with a chevron; collapse state persists per-section (`foodsYesterdayCollapsed`, `foodsSavedCollapsed`, both default expanded). The new "Saved Meals" header only renders when "Yesterday's Meals" is also visible — acts as a divider between the two parallel sections rather than a redundant label on a single tab. Search behavior unchanged: typing hides both headers and results take over; user-set collapse state preserved across search clears. ### Fixed - **Yesterday-meal info-sheet thumbnails missing on Android.** The `` tag passed `it.imgUrl` raw, so external food images (Open Food Facts, USDA, Mealie) were blocked by the WebView. Wrapped with `resolveAssetUrl()` so external URLs route through `/api/proxy` in native server mode (matches Diary's existing pattern). Also fixed the load-error fallback: a broken image now swaps to the placeholder glyph instead of leaving a blank slot. PWA was unaffected — same-origin browser fetch wasn't blocked. - **Long-press selecting surrounding text.** Diary items and Foods rows opened the action sheet but also visually selected nearby words. Added `user-select: none`, `-webkit-user-select: none`, `-webkit-touch-callout: none` to `.diary-item` and `.food-item-btn` so the long-press gesture only triggers the menu — no text selection, no copy/paste callout. ### Changed - **Component + symbol rename completes the FitBot → Trace work** from v0.39.34. User-facing strings were already done; this bump aligns the internals so the codebase reads consistently and matches LiftTrace's `Trace.svelte` / `TraceFace.svelte` / `SettingsTrace.svelte` layout. No behavior change. - Files: `AIFitBot.svelte` → `Trace.svelte`, `FitBotFace.svelte` → `TraceFace.svelte`, `SettingsAI.svelte` → `SettingsTrace.svelte` (renamed via `git mv`, history preserved). - Symbols + tags: `` → ``, `` → ``, `` → ``. - CSS class `.fitbot-face` → `.trace-face`. Global `window.__fitbotHoldRec` → `__traceHoldRec`. Console tags `[fitbot-hold]` → `[trace-hold]`. Settings search keyword `'fitbot'` → `'trace'`. - Migration code at `App.svelte` and `stores/settings.js` still references the literal `'FitBot'` — required so existing installs detect the legacy name and bump it to `'Trace'`. - **LiftTrace label parity** (cross-app cohesion, applied in LiftTrace v0.10.0-beta.3): both apps now share the exact same AI Assistant section UX — section label "AI Assistant", toggle "Enable AI Assistant", description "Adds a floating chat button to all pages". The brand name "Trace" appears only as the default value of the user-customizable assistant-name field, so renaming the assistant doesn't leave stale labels around the rest of the UI. --- ## [0.39.34-beta] — 2026-04-26 — AI Assistant rename + Wizard skip + diagnostic logs Share ### Changed - **AI Assistant default name "FitBot" → "Trace"** — ties to the broader brand (TraceApps + NutriTrace + "Trace Every Bite") and drops the dated "[X]Bot" pattern. Existing users on the literal default ("FitBot") get auto-migrated to "Trace" in `App.svelte`. Users who picked their own custom name keep it. The setting label is "Assistant name" — change to anything you like in Settings → AI Assistant. - **Wizard "I'll do this later" skip link** added to the welcome step. Confirmation modal explains "calorie targets won't be calculated automatically" and that Settings can finish setup any time. Link is hidden when forced account creation is required (multi-user mode setup). - **Bedtime reminder sub-controls now slide in/out** when toggled — was abrupt show/hide. Wrapped the `_notifBedtime` and `_notifBedtimeWindDown` conditional bodies in `transition:slide` divs. - **Diagnostic logs viewer adds a Share button** next to Copy/Clear (borrowed from LiftTrace). On native uses `@capacitor/share` to open the Android share sheet (Gmail, Files, Drive, etc.); on PWA uses Web Share API; falls back to clipboard. Lets users send logs straight from the app to their email or a GitHub issue without manual paste-juggling. ### Notes - All user-facing "FitBot" strings in source + docs are now "Trace" or "AI Assistant" depending on context. Internal symbol rename followed in v0.39.35-beta. --- ## [0.39.33-beta] — 2026-04-26 — Settings audit cleanup (round 2) ### Removed (legacy) - **Legacy shared `wellnessSync*` keys** — `wellnessSyncMode`, `wellnessSyncSchedule`, `wellnessSyncTime` were the original sync-config trio before per-source settings landed in v0.30. They've been kept around as fallback seeds for the per-source values; with no public installs predating per-source, they're now safe to remove. Per-source defaults are now standalone (`'auto'` mode, daily interval). `wellnessSyncRange` stays — it's still the active shared range setting. - Server scheduler's `wellnessSyncMode` fallback path also removed. ### Changed - **Wizard now uses the shared `` component** for all on/off switches (user-management opt-in step + 5 notification step toggles). Removed the bespoke `.fake-toggle` and the on/off-pill styled buttons. One toggle visual across the whole app. --- ## [0.39.32-beta] — 2026-04-26 — Settings audit cleanup ### Fixed - **Wizard-collected user profile fields now sync across devices** — `gender`, `dob`, `height_cm`, `weight_kg`, `target_weight`, `activity`, and `tdee` were being written to localStorage but weren't in `USER_PREFS`, so multi-device users lost their profile after the wizard. Added to USER_PREFS. - **`barcodeFlashlight` toggle visibility was inverted** — was hidden on native (where it's actually useful) and shown on PWA (where there's no flashlight). Flipped. - **Goals section disabled-state message** pointed to "Connected Services" — wellness trackers actually live in **Wellness**. Fixed. ### Added - **Statistics → Include today in trends setting** (`statsIncludeToday`, default off) — persisted version of the inline checkbox already on the Statistics page. The inline checkbox now updates this setting directly, so per-session and persistent choices share state. - **Diary toggle descriptions** — every toggle in Settings → Diary now has a one-line description explaining what it controls. Same for Foods. - **Search keywords** for Help Improve section + "reset / defaults / clear settings" added to Backup so users can find the existing Danger Zone reset. ### Changed - **"Celebrate goals" → "Goal pulse animation"** — was easily confused with the Notifications "Goal Celebrations" toggle (different feature). Description clarifies the distinction. ### Removed (dead code) - `notifCalorieGoal`, `withingsDataPriority`, `loopBannerAnimations` — settings that existed in the store but had zero read sites anywhere in the codebase. - `goalsShowCalories`, `goalsShowWeight` local lets in Settings.svelte that were read but never rendered. - `diaryTotalsMode` import in Settings.svelte (still used in Diary.svelte; just not exposed in Settings UI). --- ## [0.39.31-beta] — 2026-04-26 — In-app diagnostic logs ### Added - **In-app log capture** — `src/lib/log-capture.js` wraps `console.{log,info,warn,error,debug}` into a 500-line in-memory ring buffer, captured from app boot. Includes uncaught errors and unhandled promise rejections automatically. - **Settings → Help Improve NutriTrace → View diagnostic logs** — opens a sheet with the buffer contents, plus Copy and Clear buttons. User pastes into a GitHub issue when filing a bug. Nothing leaves the device automatically. - **Verbose diagnostic logging toggle** in the same Settings section — turns ON the high-detail `_dlog` calls in `sync.js`, `notifications.js`, `health-connect.js`, and the settings store (these were dev-build-only before). Off by default. Ideal flow: turn on → reproduce the bug → View logs → Copy → file issue → turn off. ### Changed - The dev-only `_dlog` helpers across the client now also activate when `localStorage.nt:verboseLogging === '1'` in production builds. Same gating, just adds an opt-in path. --- ## [0.39.30-beta] — 2026-04-26 ### Added - **Statistics: today excluded from cumulative-metric trends by default** — calories, protein/carbs/fat, water, steps, active minutes, and other accumulating metrics no longer include today's partial-day value in the chart or summary stats. A small "Include today (partial day)" checkbox above the chart lets users opt back in. Point-in-time metrics (sleep score, weight, HRV, RHR, SpO2, body composition) are unaffected and always include today. Eliminates the "today looks like a dip" distortion that affected average/trend lines. - **Yesterday's meal info sheet now shows item thumbnails** — each food item gets its image (with placeholder icon if none), matching the visual pattern used elsewhere in the app. ### Changed - **Yesterday's meals card layout** — info icon moved before the add button so the primary action (add) sits at the rightmost position per Material Design convention. Generic "restaurant" icon replaced with `mealIcon()` so each meal type (Breakfast, Lunch, Dinner, Snacks, custom names) gets its own glyph. --- ## [0.39.29-beta] — 2026-04-26 — Pre-RC1 polish ### Added - **Yesterday's meal info button** in the Foods picker — tap the (i) icon next to a yesterday's-meal card to open a sheet listing every item in that meal (name, brand, quantity, kcal each). The "Add this meal" button at the bottom is the same action as the main row tap. - **Calibration data export** in Settings → Help Improve NutriTrace — generates an anonymized JSON of the last 30 days of Fitbit/Garmin readings (no user ID, no exact dates — just day-of-week and offset). User reviews the JSON before sharing in GitHub Discussions to help tune the score formulas. - **GitHub issue templates** — `bug_report.md`, `feature_request.md`, and `integration_test_report.md` (the last specifically for community-tested wearables/scales/Health Connect setups). Each includes log location guidance and a redaction reminder. - **README** — new "Principles", "Roadmap", "Help us test", and "Troubleshooting" sections. Sets the values story up front, names the testing gaps honestly, and gives contributors a clear on-ramp. ### Deferred to FUTURE.md - Per-meal totals popup nutrient filter — should it always show all nutrients (vs respecting the day-summary's `diaryShowAllNutrients` toggle)? Defer the call until we have user feedback on what they reach for. --- ## [0.39.28-beta] — 2026-04-26 — Weigh-in reminder false-positive fix ### Fixed - **Device-side weigh-in reminder no longer fires when weight is already logged** — the WorkManager periodic check runs every 15 min and was firing on early ticks within the reminder window before today's Withings/HC sync brought the weight into the local cache. Once the weight arrived, later ticks correctly identified it but the original notification stayed in the tray. Three-part fix in `ReminderWorker.java`: 1. **Self-heal**: when a tick finds the weight, cancel any previously-posted notification with the same ID. The notification disappears from the tray once data syncs. 2. **Once-per-day dedup**: track "fired today" in SharedPreferences. Prevents the multi-fire-then-Android-mute pattern. 3. **Staleness gate**: if `sync_meta.last_sync_at` is older than 1 hour on a server-connected device, defer to the server scheduler's push reminder instead of firing locally on stale data. Local-only devices (no `last_sync_at` row) are unaffected. --- ## [0.39.27-beta] — 2026-04-26 ### Changed - **Meal header kcal hidden when macro footer is on** — the macro summary footer at the bottom of each meal already shows kcal at the end of `... · N kcal`, so showing it in the header too was redundant. Now it only appears in the header when `diaryShowMacroSummary` is off (in which case the header is the only place to see the meal total). --- ## [0.39.26-beta] — 2026-04-26 ### Changed - **Per-meal totals popup tap target** — moved from the kcal text on the meal header to the **macro footer at the bottom of each meal** (the P/C/F bar + percentages). That's the real "summary at a glance" element, so making it the tap target for the detailed breakdown is the natural fit. --- ## [0.39.25-beta] — 2026-04-26 ### Added - **Per-meal nutrition totals popup** — tap the kcal text on any meal header in Diary to open a bottom sheet with the full nutrient breakdown for that meal: 4 macro pills (Protein/Carbs/Fat/kcal) + every visible nutriment with a value, plus item count. Mirrors the day-level Nutrition Summary but scoped to one meal. Respects the user's "Show all nutrients" toggle. --- ## [0.39.24-beta] — 2026-04-25 — Bulletproof DB-open recovery ### Fixed - v0.39.23 cleanup of leftover encryption state was conditional on localStorage markers — but on at least one device the markers were absent while the DB file was still encrypted from a prior install, so the cleanup didn't run and the open failed with SQLITE_NOTADB. - New behavior: try-then-wipe-then-retry pattern. Always attempt to open first. If it fails (any reason), unconditionally wipe the DB file (via plugin's `deleteDatabase` AND a Filesystem fallback, in case the plugin call silently no-ops on an encrypted file it can't open) and try again. Server-connected devices re-sync from the server; clean unencrypted DBs from v0.39.19 or earlier are unaffected because the first open succeeds. --- ## [0.39.23-beta] — 2026-04-25 — Roll back native SQLite encryption ### Changed - **Reverted SQLCipher encryption from v0.39.20** — `@capacitor-community/sqlite` v8's secret-store semantics are unreliable in practice. The error sequence we hit: 1. v0.39.20: `setEncryptionSecret` called every launch → "file is not a database (26)" on second launch (passphrase rebinding bug) 2. v0.39.22: switched to `isSecretStored` gating → "setEncryptionSecret: state for nutritrace_localSQLdb not correct" (plugin requires no connections, but state tracking is fragile) - Modern Android encrypts the entire app data directory at the OS level (file-based encryption, default since Android 7), so the local SQLite is not in cleartext on a locked device anyway. SQLCipher was a defense-in-depth layer; losing it doesn't materially change the threat model for normal users. - New behavior in `db-native.js`: one-shot cleanup on first launch — clears the encrypted DB file, secure-store secret, and `nt:db_*` localStorage markers from any prior v0.39.20–22 install. Then opens unencrypted as before. Server-connected devices re-sync from the server (your phone will see a brief empty state then everything flows back). - `capacitor.config.ts`: `iosIsEncryption`/`androidIsEncryption` back to `false`. - FUTURE.md updated: native SQLite encryption deferred to v1.1, with a note to investigate alternatives (Android Keystore + per-row JS encryption, different plugin, or simply rely on Android's OS-level FBE). ### What this means for you - Your phone will run the cleanup on first launch (delete the broken encrypted DB), then re-sync from the server. No data loss because the server is the source of truth for everything. - If you were on v0.39.19 or earlier and never installed v0.39.20–22, this is just a normal update with no migration. --- ## [0.39.22-beta] — 2026-04-25 — Encryption open auto-recovery ### Fixed - **SQLCipher decrypt failure auto-recovery** — v0.39.20 called `setEncryptionSecret` on every launch. The plugin's secure-store semantics in @capacitor-community/sqlite v8 don't reliably re-bind the passphrase to existing encrypted files when called twice; some devices got "file is not a database (26)" on the second launch. - New behavior: 1. Use `isSecretStored()` to check if the plugin secure store already has a passphrase. Only call `setEncryptionSecret` on first setup. Eliminates the re-binding bug. 2. If the encrypted open still fails (e.g., the secret in the secure store and localStorage have drifted on an older install), auto-recover on **server-connected** devices: clear secret + DB, re-derive a fresh passphrase, recreate the DB, let server sync repopulate. **Local-only** devices fail loudly with a recovery message instead of wiping data. - New `_emergencyResetDb()` helper for the recovery path; calls `clearEncryptionSecret()` + `deleteDatabase()` + clears localStorage markers. --- ## [0.39.21-beta] — 2026-04-25 — Final audit cleanup ### Security - **Push-test SSRF fixed** — `/api/settings/push-test` (gotify branch) was accepting `body.url` and `body.token` and proxying to whatever the caller supplied, then falling back to saved settings. The client never sends those body fields anyway, so removing them eliminates an authed-user SSRF without changing any user-visible behavior. ntfy and Apprise paths already only used saved settings. - **AI chat payload caps** — `/api/ai/chat` now rejects `messages.length > 60` and `messages + systemPrompt > 200 KB`. Bounds the worst case for a misbehaving client (or compromised account) burning the admin's AI budget. - **Backup file extension guard** — `/api/full-backup/:name/{download,delete}` now require the requested filename to end in `.zip`. Without this, an admin could request any file that happened to live in `BACKUPS_DIR` (which is under `UPLOADS_DIR` by default). This brings the audit punch list to **complete** except for two intentionally-deferred items already in FUTURE.md (Android cleartext lockdown — right before Play Store; dep major bumps `better-sqlite3 9→11` + `multer 1→2` — v1.1 with full regression testing). --- ## [0.39.20-beta] — 2026-04-25 — Capacitor SQLite encryption (SQLCipher) ### Security - **Native SQLite is now encrypted at rest** — `androidIsEncryption: true` (and iOS) flips on SQLCipher in the Capacitor build. Per-device passphrase is generated on first launch (32 random bytes via `crypto.getRandomValues`) and stored in localStorage. Defense-in-depth on top of Android's existing file-based app-data encryption. ### Migration (automatic, two paths) - **Server-connected devices** (your Pixel falls here): on first launch after the update, `db-native.js` detects the unencrypted DB, deletes it, creates a fresh encrypted DB, and the next sync repopulates from the server. **Zero data loss** because the local DB is just a cache of authoritative server data. You may briefly see an empty diary while the first sync completes. - **Local-only devices**: migration is **deferred**, not silent — wiping local SQLite without a server safety net would lose data. The DB stays unencrypted; a `nt:db_encryption_pending=1` flag is set in localStorage. Users explicitly trigger the upgrade after exporting a Local Full Backup. UI for that lives in the next release (already exported the helpers `runLocalEncryptionUpgrade()` + `isEncryptionPending()` from `db-native.js`). ### What this protects against - Lost/stolen rooted device (where Android's OS-level encryption can be bypassed) - File-system-level backups that include the DB file - Future zero-day exploits against Android's data-directory encryption ### What this does NOT change - The encryption secret lives in localStorage in the same app data directory as the DB. An attacker who can read one can read the other. This is a defense-in-depth layer, not an air-gap. Android Keystore-backed key would require a custom plugin. ### Notes for self-hosters / developers - If you uninstall + reinstall the app, the localStorage secret is wiped along with everything else — the encrypted DB file becomes unrecoverable. Same outcome as `Clear Storage` in Android Settings. Re-install pulls from the server (server-connected mode) or starts fresh (local-only). --- ## [0.39.19-beta] — 2026-04-25 — Upload magic-byte validation ### Security - **`/api/upload` now magic-byte validates** — was trusting the client-supplied `Content-Type`. After multer writes the file, the first bytes are inspected against known image signatures (JPEG, PNG, WebP, GIF, HEIC/HEIF, AVIF, BMP). Files that don't match are deleted from disk and the request is rejected with 400. SVG is intentionally NOT supported (script-execution risk; food/avatar uploads never need it). - New module: `server/lib/image-magic.js` — pure-stdlib magic-byte detector with both file-path and in-memory-buffer entry points. --- ## [0.39.18-beta] — 2026-04-25 — Docker image org rename ### Changed - **Docker image canonical path is now `ghcr.io/traceapps/nutritrace`** — `docker-compose.yml`, `README.md`, and `DEPLOY.md` all reference the new path. - **CI publishes Docker image** to `ghcr.io/traceapps/nutritrace` on push to `main`. - **FUTURE.md repo-split notes updated** to reflect actual current structure (`traceapps/nutritrace-dev` private monorepo, `traceapps/nutritrace` public-on-1.0). --- ## [0.39.17-beta] — 2026-04-25 — Pre-1.0 security hardening (Phase 2) ### Security - **OAuth tokens encrypted at rest** — Fitbit, Withings, and Garmin access/refresh tokens are now encrypted with AES-256-GCM using a key derived (HKDF-SHA256) from `JWT_SECRET` (override with `TOKEN_ENC_KEY`). A leaked database file or backup no longer hands out long-lived wearable-API credentials. New module: `server/lib/token-crypto.js`. Migration is **lazy**: existing plaintext tokens continue to work and are re-written encrypted on the next refresh cycle. Rotating `JWT_SECRET` invalidates both sessions and stored OAuth tokens — users will have to reconnect their wearables; document this if you ever rotate. - **Mealie proxy SSRF fixed** — `/api/mealie/proxy` now requires `baseUrl` to match the user's saved `mealieBaseUrl` setting (multi-user). An authed user can no longer use the server as an open proxy to probe internal hosts or cloud-metadata endpoints. Single-user mode unchanged (no auth boundary to defend in that mode). - **Image-localizer SSRF fixed** — `POST /api/foods` and `/api/meals` (which run `localizeImage`) now refuse URLs whose hostname resolves to private/loopback/link-local IP ranges (10.x, 127.x, 172.16-31.x, 192.168.x, 169.254.x including cloud metadata, IPv6 ULA + loopback). Non-http(s) protocols also rejected. - **Auth cookie secure-by-default** — `secure: true` is the new default (was tied to `NODE_ENV=production` which most self-hosters never set). Set `INSECURE_COOKIES=1` to opt out for plain-HTTP LAN deploys (logged as a warning at startup). - **Body parser tightened** — global JSON limit lowered from 50 MB to 1 MB. The two endpoints that legitimately handle large payloads (`/api/data/import`, `/api/sync/push`) get per-route 25 MB limits. Caps memory abuse from a single authed user. - **Backup upload limit reduced** — 2 GB → 512 MB by default (override with `BACKUP_UPLOAD_MAX_MB`). Bounds disk-fill abuse from repeated half-finished uploads. - **Session duration capped at 1 year** — previously `session_hours = 0` meant 100 years. Override with `MAX_SESSION_HOURS` env var if you really want longer. ### Notes for self-hosters - **HTTPS is now expected by default.** If your server is on plain HTTP (LAN, dev), set `INSECURE_COOKIES=1` in your `.env` or auth cookies will be rejected by the browser. - **OAuth tokens migrate transparently** on next sync — no admin action required. - **`JWT_SECRET` is now load-bearing for OAuth too.** If you ever rotate it, users must reconnect Fitbit/Withings/Garmin. Set a separate `TOKEN_ENC_KEY` if you want independent rotation. --- ## [0.39.16-beta] — 2026-04-25 — Pre-1.0 security & polish (Phase 1) ### Security - **CSRF exemption narrowed** — was skipping ALL `/api/wellness/*` POSTs (sync, recalculate, seed-scores, disconnect). Now only OAuth `/callback` paths are exempt; everything else requires the X-CSRF-Token header for cookie sessions. - **JWT_SECRET refuse-to-start in production** — server now exits with a clear error if `NODE_ENV=production` and `JWT_SECRET` isn't set, instead of silently using the dev default. - **Per-username brute-force throttle** — login + recover + forgot-password add a per-username bucket (5/15min) on top of the existing per-IP bucket (10/15min). NAT users behind one IP no longer share an account-lockout pool. - **Recovery token now uses constant-time compare** — was vulnerable to timing-attack guessing. - **`/api/auth/users/list` gated behind sharing_enabled** — was exposing every account's display name + username to every authenticated user. Now returns `[]` unless food/meal sharing is on. - **`/api/auth/forgot-password` is constant-time + always returns 200** — was leaking SMTP-not-configured state and email existence via timing. - **CSRF token rotated on password change** — old sessions on other devices stop working after a password change. - **bcrypt cost factor bumped 10 → 12** — current best practice. Existing hashes remain valid (bcrypt verifies regardless of cost). - **Image proxy hostname allowlist** — replaced `.includes()` with strict suffix match. `i.imgur.com.evil.tld` no longer slips through. Also rejects non-http(s) protocols. - **Backup restore zip-slip + zip-bomb defense** — image extraction now rejects `..`, absolute paths, and any path that resolves outside `UPLOADS_DIR`. Hard cap of 10,000 entries and 5 GB total uncompressed. - **Celebration key validation** — `/api/settings/claim-celebration` now requires keys match `/^[a-z_][a-z0-9_-]{0,39}$/`. Stops a misbehaving client spamming arbitrary keys into `app_config`. - **Rate limits added** — new `server/middleware/rate-limit.js` token-bucket limiter applied to: `/api/ai/chat` (30/min/user), `/api/upload` (60/min/user), `/api/proxy` (60/min/IP), and `/api/wellness/*/callback` (10/min/IP). ### Changed - **Console log noise cut** — `src/lib/sync.js`, `src/lib/notifications.js`, `src/lib/health-connect.js`, `src/stores/settings.js` — verbose `console.log` calls now gated on `import.meta.env.DEV`. Wellness readiness/stress debug calibration logs untouched (intentional). ### Performance - **Barcode scanner libs lazy-loaded** — zxing + html5-qrcode + quagga2 (~870 KB combined) were loaded on every page; now loaded on first scanner open and cached for subsequent opens. Removes ~870 KB from PWA cold-start payload. - **Logo file size** — `public/icons/logo.png` was 1,147 KB rendered at 32-56px in 6 places; replaced with the existing 47 KB icon-192. Saves ~1.1 MB on cold load. ### Repo - **CONTRIBUTING.md added** — short guide for issues + PRs. - **`.dockerignore` excludes `android/`, `*.apk`, `*.aab`, `*.keystore`** — speeds Docker build context. - **FUTURE.md** — Health Connect moved from "needs testing" to done. ### Notes for self-hosters - After this version, set `JWT_SECRET` in your `.env` (generate with `openssl rand -base64 48`) — the server will refuse to start in production without it. - The new `RECOVERY_TOKEN`, `JWT_SECRET`, and CSRF behaviors are documented in DEPLOY.md. --- ## [0.39.15-beta] — 2026-04-25 ### Fixed - **Calibration seeds no longer overwritten by Fitbit sync** — `/api/wellness/fitbit/seed-scores` now writes to dedicated `*_actual` metric_types (`sleep_score_actual`, `readiness_score_actual`, `stress_score_actual`) instead of overwriting the calculated `*_score` values. Previously every Fitbit sync recomputed `sleep_score` from raw inputs and silently blew away seeded actuals — the stress chain has been using stale calculated values for weeks. - **Wellness UI prefers actuals when present** — sleep/readiness/stress display, the readiness card, the stress card, and the stress 30-day history chain all now prefer `*_actual` over `*_score`. When no actual is seeded, behavior is unchanged (live calc). - **Self-retiring design** — once formulas are dialed in and seeding stops, `*_actual` rows naturally roll off the 30-day stress window and the system transitions to using calc only. No flag flip, no migration step. --- ## [0.39.14-beta] — 2026-04-24 ### Fixed - **Weigh-in reminder now fires correctly** — server scheduler's `wellness_data` skip query was using strict `user_id = ?` (missing the NULL-fallback that the diary query uses). If the user was multi-user but their wellness_data rows had NULL user_id (older installs, or synced-from-Fitbit/Withings rows), the skip check missed the weight and fired the reminder anyway. - **Water reminder multi-user fallback** — water goal skip check now also falls back to NULL user_id rows (matches meal + weigh-in pattern). - **Weigh-in logging** (server + Android) — both checks now log what they found: `skipping weigh-in for user=X — diary.body_stats.weight=Y` or `firing weigh-in — no weight found`. Makes future diagnosis immediate instead of silent. --- ## [0.39.13-beta] — 2026-04-21 ### Changed - **Empty meal slots are now tappable** — when a meal has no items, the "Tap to add food" row itself opens the food picker. Previously the row was passive and users had to hunt for the + button. Label updated from "Tap + to add food" to "Tap to add food"; the + button still works as before. --- ## [0.39.12-beta] — 2026-04-21 ### Added - **Stress Management driver chips** — the Stress card in Wellness → Heart now shows the three contributing factors (HRV / Resting HR / Sleep) with color-coded values, matching the pattern used by Daily Readiness. Easier to see why stress is where it is on any given day. ### Removed - **Readiness "Locked at sync" debug strip + refresh button** — was a dev/calibration tool used while tuning formulas. Regular users never needed it. The `/api/wellness/fitbit/recalculate` endpoint remains for DevTools use during future tuning. --- ## [0.39.11-beta] — 2026-04-19 ### Added - **Fuzzy food search** — local food/meal/recipe search now handles typos and partial matches (e.g. "chiken" finds "Chicken Breast"). Uses word-by-word matching first, then edit-distance tolerance for queries ≥ 4 chars. External source search (OFF/USDA) unchanged. No toggle — degrades silently to exact match when fuzzy adds nothing. - **Search empty state in Foods** — when a local search returns no results, shows "No matches for '…'" with a hint to try Open Food Facts/USDA. Previously the list was silently empty. - **Statistics empty state context** — "No data" message on the chart now includes a contextual hint: nutrition metrics suggest logging food, wellness metrics suggest connecting a tracker, body stats suggest a different date range. - **Statistics dynamic goal line label** — when Dynamic Calorie Goal is active, the calorie goal line on the Statistics chart is now labeled "Base Goal" instead of "Goal" to clarify that the actual daily goal varies. ### Changed - **Bundle code splitting** — chart.js, jszip, and emoji-picker-element are now split into separate async chunks. Cuts initial JS parse time on first load; chunks are loaded on demand. ### Fixed - Settings split (background task — SettingsNotifications, SettingsBackup, SettingsUserManagement, SettingsAI sub-components) --- ## [0.39.10-beta] — 2026-04-19 ### Fixed - **Foods tab switch scroll position (take 2)** — previous fix (0.39.9) reset scroll in a reactive block, which ran AFTER Svelte painted the new tab at the old offset, causing a visible jump. Now the reset is wired to the Tabs `change` event so it fires synchronously on click (before the bind propagates) with a `requestAnimationFrame` follow-up. Result: the new tab renders starting at top with no perceptible jump. --- ## [0.39.9-beta] — 2026-04-19 ### Removed - **Foods picker "Recently Added" section** — removed (was added in 0.39.7). Didn't feel useful in practice. ### Fixed - **Foods/Meals/Recipes tab switch** — when a tab was scrolled down and you switched to another, the new tab started at the same scroll offset instead of at the top. Now jumps to top on switch (no animation). --- ## [0.39.8-beta] — 2026-04-19 ### Added - **Notes shown in quick-add card** — when a food, recipe, or item with saved notes (e.g. "1 serving = 150g cooked") is added via the portion/quantity prompt, its notes appear above the inputs. Auto-hidden when empty — no setting needed. Same treatment in the multi-add portion sheet. - **FitBot — new `get_meals` tool** — lets the assistant look up the user's saved Meals and Recipes (with items, totals, and notes). Supports an optional name filter so queries like "what's in my 'Usual breakfast'" work reliably. ### Changed - **FitBot `get_diary` now returns day notes + per-item notes + brand** — so when the user asks "why did I eat badly yesterday?" the assistant can read their own context back to them. - **Full backup + JSON import** now round-trip the new `diary.notes` column. Previous backups (without notes) still restore fine. ### Fixed - Client-side local-backup flow always included notes via `NtApi.getAllDiary()`; the matching server import paths were missing `notes` in their INSERT statements — restoring a backup with notes used to silently drop them. Fixed in both `/api/data/import` and `/api/full-backup/restore`. --- ## [0.39.7-beta] — 2026-04-19 ### Added - **Recently Added foods** — when picking food for a meal (Foods tab, empty search), a "Recently Added" section appears above results showing the 10 most recent unique items you've logged. One tap adds it to the target meal. - **Save meal to library** — new action in the meal `⋮` sheet: "Save as meal…" — converts a diary meal into a reusable Meal entry with a name you choose. - **Day notes** — per-day free-text notes on the diary (e.g. "felt bloated after lunch"). Collapsible card at the bottom of the diary; a small pencil indicator appears next to the date when a note exists. Toggleable in Settings → Diary display → "Show daily notes" (default on). Notes sync across devices like the rest of diary data. ### Changed - Diary table gains a `notes` column (auto-migration on server + native Android). --- ## [0.39.6-beta] — 2026-04-19 ### Added - **Meal-level actions** — new `⋮` button on each meal header opens an action sheet: - **Copy items to…** — duplicate all items from one meal into another on the same day. - **Move items to…** — transfer all items from one meal to another on the same day. - **Copy meal to another date…** — pick a date + target meal; items are appended there (source day unchanged). - **Clear all items** — with confirm dialog, removes every item in the meal for the current day. - Empty meals open to a single "Add food" shortcut so the entry point is always consistent. --- ## [0.39.5-beta] — 2026-04-19 ### Added - **Password strength indicator** — real-time weak/fair/strong bar on AcceptInvite, ResetPassword, Profile change-password, and Wizard admin account forms. Shared `src/lib/validation.js` mirrors LiftTrace. - **Wizard integration step summary** — shows which services (Open Food Facts, USDA, Mealie, FitBot AI) are configured vs skipped. - **MealEditor inline Add Ingredient button** — always visible at the bottom of the ingredient list with running totals (kcal · N items). - **Foods source filter reset toast** — subtle info toast when switching tabs silently resets the source back to Local. - **Foods pick-mode confirm label** — button now reads "Add N" (was just a checkmark) so users know exactly how many items they're adding. - **Statistics quick-select chips** — Last 7d / 30d / 90d / This month / Last month / YTD on the custom date range. - **Goal editor section hierarchy** — sheet now grouped into Display / Goal Behavior / Target sections with hint subtitles under each toggle explaining what it does. ### Changed - `showInfo()` toast helper added to `src/stores/toast.js` for subtle info messages. --- ## [0.39.4-beta] — 2026-04-19 ### Changed - **Sleep score formula tuned** (19-day calibration) — reduced quality bonus ceiling (8→6) and HRV component ceiling (15→12, null default 10→8). Targets the "great night" overshoot pattern where the formula was giving 95-100 for nights Fitbit scored 81-89. Sleep MAE 2.16→1.74, max gap 11→9. Indirectly improves stress accuracy since sleep is 60% of the stress formula. --- ## [0.39.3-beta] — 2026-04-18 ### Added - **Bedtime reminder** — new notification type for sleep. Evening nudge at user-set time (default 22:30) with optional wind-down pre-reminder (15/30/45/60/90 min before). Smart message toggle adjusts the text based on last night's sleep vs goal (e.g., "You slept 5h last night — prioritize an earlier bedtime"). Fires on server scheduler (push) and native Android worker (local). ### Fixed - **Weigh-in reminder skips if already weighed in** — previously fired at the scheduled time regardless. Now checks both `diary.body_stats.weight` (manual entry) and `wellness_data` with `metric_type='weight_kg'` (Withings, Health Connect sync). Fixed on both server scheduler and native Android worker. --- ## [0.39.2-beta] — 2026-04-17 ### Changed - **Sleep stages mobile layout** — on narrow screens (under 500px), sleep stage breakdown now renders as a vertical list (colored dot + label + duration + %) instead of floating labels that overlap when segments are small. Tablet/desktop (≥500px) layout unchanged — stacked bar with midpoint-positioned labels. Responsive across phones, foldables (closed/open), and phone landscape. --- ## [0.39.1-beta] — 2026-04-16 ### Added - **Scheduled syncs now include workouts** — Fitbit workout activity logs sync alongside metrics on every scheduled cycle (when workoutsEnabled is true). - **Full sync range** — all scheduled device syncs (Fitbit, Garmin, Withings) now sync the user's full configured sync range, not just today. - **Health Connect goal checks** — HC manual sync triggers step + wellness goal celebrations. Works in local mode (no server needed). - **GPS Retry button** — "No GPS data available" placeholder now includes a Retry button. - **Device-agnostic goal alerts** — step goal and wellness alerts fire once from merged data across ALL sources after all device syncs complete. ### Fixed - **Fitbit logId precision** — logIds exceeding JavaScript's MAX_SAFE_INTEGER were silently rounded by JSON.parse, causing GPS/TCX fetches to fail with wrong IDs. Now extracted as strings from raw JSON before parsing. - **GPS flag protection** — `has_gps` can only go 0→1 on re-sync (MAX), never downgrade. Prevents scheduled re-syncs from hiding GPS data when Fitbit API inconsistently returns hasGps:false. - **Workout deduplication** — cleans up duplicate workout entries created by logId precision fix (keeps entry with GPS data or newer one). - **Step goal notification spam** — three independent paths were firing step notifications with different dedup keys. Unified all paths to use `_celeb_` prefix with matching key names. Steps removed from generic `checkGoals()` to prevent double-fire. - **Unified notification dedup** — server push-notify.js and client notifications.js now share the same dedup keys in app_config via `/api/settings/claim-celebration` endpoint. All goal types covered: steps, calories, water, wellness alerts, workouts, sync failures. Works cross-device. --- ## [0.39.0-beta] — 2026-04-12 ### Added - **Self-service account deletion** — users can delete their own account and all associated data from Settings → Data & Privacy → Danger Zone. Cascades all foods, meals, diary, settings, wellness data, chat history. Prevents the last admin from deleting themselves. - **Per-device sync scheduling** — each connected service (Fitbit, Garmin, Withings, Health Connect) now has independent sync controls: mode (auto/scheduled/manual), interval (30min to daily), and active window. Daily interval shows a "Sync At" time picker; sub-daily shows start/end active window. Falls back to legacy shared settings for existing users. - **Diary item Replace action** — long-press a diary item → Replace opens the food picker in the same meal slot. Picks a new food, deletes the old one. Context menu reordered: Edit, Replace, Move to meal, Select multiple, Delete. - **Mandatory account creation on PWA** — server blocks all data APIs (503) when no users exist. First visit forces account creation via the wizard. Android local mode unchanged. ### Changed - **Email templates polished** — added greeting lines, hidden preheader text for inbox previews, unified accent green (#00C47A), section headers via helper, stat row units dimmed, unsubscribe moved to footer slot. Optimized email logo from 1.1MB to 19KB (120x120 retina). - **Wellness tab renamed** — "Movement" → "Activity" (matches Fitbit, Garmin, Apple Health conventions). - **Recipe portion sheet unified** — now uses the shared Sheet component matching the diary food picker layout (side-by-side serving size + unit, total amount summary bar). - **Notifications badge removed** — "Experimental" label removed from Notifications section header. - **Time format respected everywhere** — TimePicker, water logs, workout times, FitBot chat timestamps all honor the 12h/24h setting. TimePicker shows 24h grid (00-23) when configured. - **Readiness/stress formulas tuned** (6-day calibration) — readiness: HRV concave power curve (pow 0.7, gain 80), weights 0.75/0.05/0.12+4, MAE 7.3→4.5. Stress: sleep weight 0.60, HRV gain 40, smoothing 0.40/0.60, MAE 6.0→3.17. ### Fixed - **PWA stale cache** — replaced NavigateFallback with NetworkFirst for navigation. Deploys are picked up on next page load; no more manual cache clears. - **FitBot false unread dot** — persisted seen count in localStorage so component remounts don't show phantom notifications. - **Recipe portion sheet backdrop** — clicking outside no longer cancels ingredient input; only the X button dismisses. - **Scheduler dedup persistent** — all notification timestamps stored in app_config DB (survives container restarts). Weekly summary dedup check moved after day/hour gate so timestamp only burns on actual send. - **Sync timeout** — POST/PUT/DELETE pass-through uses 30s timeout (was 3s, causing Fitbit/Garmin/Withings sync failures on Android). --- ## [0.38.9-beta] — 2026-04-11 ### Fixed - **Smart Log: food swap now recalculates portion size** — switching to an alternate food match was keeping the original food's serving size in grams. Now recalculates based on the new food's base portion, so nutrition values display and save correctly. - **Android: instant local data loading when server is down** — Foods page was blocking on a `getSharingStatus()` server call before loading local data. Moved server calls to non-blocking background. Goals page had the same issue with dynamic calorie goal fetch. Both pages now render cached data instantly. - **Android: image map loaded before app renders** — moved `loadImageMap()` from App.svelte `onMount` (runs after first render) to main.js boot sequence (runs before App mounts). Diary food images now display on first paint instead of after a delay. - **Faster server-down detection** — reduced all network timeouts from 5-8s to 3s. `checkOnline()` now short-circuits for 15s after a failed check, so repeated sync cycles don't stack timeouts. - **Settings search: added missing keywords** — Smart Log, Voice, Quick Log, Goal Insights now searchable in AI section. Weekly Summary, Email Summary added to Notifications. Health Connect added to Wellness. --- ## [0.38.8-beta] — 2026-04-10 ### Fixed - **Android: app now works correctly when server is unreachable but phone has internet** — previously the background auth refresh could clear the cached user on server error (e.g., 502 from Cloudflare tunnel), causing the app to show a login screen and preventing cached data from loading. The native auth refresh now never clears `currentUser`; it only updates on a valid server response. - **API pass-through timeout** — `NtApiCached` pass-through methods (`get`, `post`, `put`, `del`) now use an 8-second timeout so they fail fast instead of hanging when the server is unreachable. --- ## [0.38.7-beta] — 2026-04-09 ### Added - **Cross-device chat sync** — FitBot chat history is now part of the differential sync pull. Messages sent from one device show up on another on the next sync cycle (30s on native, visibilitychange on PWA). No more refresh-to-see. - Server: `/api/sync/pull` now returns `chat_history` rows newer than `since`. - Client sync engine dispatches `nt:chat-updated` after pulling new rows. - `AIFitBot` listens for `nt:chat-updated`, `nt:sync-complete`, and `visibilitychange` — merging new messages in place (deduped by role+content+time) and surfacing an unread dot if the panel is closed. ### Changed - **Sync bar trimmed to errors only** — removed "Syncing…", "Synced", and "Offline — changes saved locally" states. The bar now only appears when there's a real sync error worth the user's attention. Removes the every-30s flash that added visual noise without actionable information. - **Connection badge — offline-only** — the cloud badge on the menu button no longer shows a green "online" state. It only surfaces when offline (red `cloud_off`). Follows the "errors are interesting, success is boring" principle; matches conventions used by Gmail, Slack, etc. --- ## [0.38.6-beta] — 2026-04-09 ### Changed - **Settings UX polish** — Goals section is now always visible; when no device is connected, the Dynamic Calorie Goal toggle is shown in a disabled state with an explanation ("Connect a device in Connected Services to enable"). Previously the entire Goals section was hidden. - **Toggle component** — now accepts a `disabled` prop; disabled toggles are visually dimmed and non-interactive. - **Notifications descriptions clarified** — "Device Notifications" now reads "Alerts delivered directly to this device — native on Android, browser pop-ups on desktop/PWA"; "Push Service" now reads "Server-relayed alerts via Apprise, Gotify, or ntfy — useful for PWA users or Home Assistant". - **Smart Log setting description** — shortened to one line; expanded "Quick start" hint trimmed to trigger words only (removed duplicate how-to text). - **Goal Insights description** — shortened from two sentences to one. ### Removed - `isAndroid`, `isIos` exports from `platform.js` — were never imported anywhere in the codebase. - `isServerConnected()` export from `platform.js` — was never called anywhere in the codebase. - `setImageMap` from the import statement in `sync.js` — was imported but never used in that file. --- ## [0.38.5-beta] — 2026-04-09 ### Added - **Weekly Summary Email** — optional digest delivered by both push notification and email (if SMTP is configured). Covers: avg daily calories vs goal, goal hit rate, avg protein/carbs/fat, avg water, avg steps, avg calories burned, avg sleep, avg resting HR, avg readiness/stress scores, and weight change for the week. - **User-configurable delivery day and time** — choose any day of the week and exact time in Settings → Notifications → Weekly Summary. Previously hardcoded to Sunday 9am. - Settings: `weeklySummaryDay` (0=Sun…6=Sat, default Sun) and `weeklySummaryTime` (HH:MM, default 09:00). --- ## [0.38.4-beta] — 2026-04-09 ### Added - **FitBot Goal Insights** (Experimental) — new `get_diary_averages` tool gives FitBot access to your average daily intake over any period (calories, protein, carbs, fat, water), logging consistency %, and weight trend. When **Goal Insights** is enabled in Settings → AI Assistant, FitBot proactively compares your actual averages against your goals and offers evidence-based adjustments when patterns are consistent (>10% gap, 2+ weeks). Always asks before suggesting changes. - **Health Connect in FitBot context** — today's Health Connect data (steps, calories, sleep, HR, HRV, weight) now included in FitBot's daily summary alongside Fitbit/Garmin/Withings. - **FitBot tool descriptions improved** — each tool now explicitly states what data it provides and when to use it, reducing hallucination on borderline queries. --- ## [0.38.3-beta] — 2026-04-09 ### Added - **Dynamic Calorie Goal** (Experimental) — adjusts your daily calorie goal based on yesterday's calories burned from a connected device (Fitbit, Garmin, or Health Connect). Enable in **Settings → Goals**. - Three goal factors: Lose (−20%), Maintain (×1.0), Gain (+20%) applied to yesterday's TDEE. - Falls back to your fixed calorie goal if no device data is available for the prior day. - Diary bottom bar shows a "⚡ Dynamic · X kcal goal" pill when active. - Goals page shows a "⚡ Dynamic" badge on the calories row and the effective goal with "based on X burned" annotation. - New server endpoint `/api/wellness/calories-out` merges calories_out across all sources (priority: Garmin > Health Connect > Fitbit). - Settings section hidden unless at least one device (Fitbit, Garmin, Health Connect) is connected. --- ## [0.38.2-beta] — 2026-04-08 ### Smart Log — water logging - Smart Log can now log water intake. Say "drank a glass of water", "500ml of water", or use a container name from your configured water containers (e.g. "had my protein shaker"). - Parser recognizes `kind: 'water'` items. `_matchWater` resolves the amount to ml using: (1) exact/fuzzy match against user's configured container names + volumes, (2) explicit ml/oz/L amounts, (3) generic container defaults (glass=240ml, bottle=500ml, mug=350ml, etc.). - Water items skip the diary food entry flow entirely — `saveItems` calls `addWaterLog` in the diary store, same as tapping the water button manually. - New `addWaterLog(amountMl, date)` exported from `src/stores/diary.js`. - Modal shows a blue **Water** badge; meal slot picker and quantity field are hidden for water rows. An editable ml input lets the user correct the amount before confirming. - Container names are injected into the AI parser prompt verbatim (same pattern as meal slot names) so custom names like "Protein Shaker", "Nalgene", "Thermos" resolve correctly. --- ## [0.38.1-beta] — 2026-04-08 ### Added - **Water log editing** — tap any water log entry to edit the amount inline. Input opens in place with the current value pre-filled in your display unit (ml/oz/L/G). Press Enter or tap ✓ to save, Escape or ✕ to cancel. Delete button still works as before (stopPropagation prevents accidental edit trigger). --- ## [0.38.0-beta] — 2026-04-08 ### Security - **CSRF protection** — synchronizer token pattern. A random 16-byte hex token is embedded in the JWT at issue time. The server verifies the `X-CSRF-Token` request header against the decoded token on all state-changing requests (POST/PUT/PATCH/DELETE). Bearer token requests (native app) are exempt — they're inherently CSRF-safe. Single-user mode and legacy tokens (issued before this version) are also exempt for a seamless migration window. New sessions are fully protected. - Token added to JWT in `server/middleware/auth.js` (`signToken`); returned from `/api/auth/me`; verified in new `server/middleware/csrf.js`. - `X-CSRF-Token` added to CORS `Access-Control-Allow-Headers`. - Client injects header in `_NtApiHttp._fetch` (covers all NtApi calls), `Settings._fetchOpts` (backup/restore/full-backup), `aiChat.callAIProxy`, `notifications` push-test calls, and `mealieApi` proxy calls. ### Docs - **README: Environment variables** — added `RECOVERY_TOKEN`, `LOG_LEVEL`, `AI_PROVIDER`, `AI_API_KEY`, `AI_MODEL`, `AI_ENABLED` to the env vars reference table. - **README: Wellness Integrations** — step-by-step Fitbit and Withings OAuth app registration with exact callback URL format; Garmin note about partnership requirement. --- ## [0.37.0-beta] — 2026-04-08 ### Smart Log v3.2 — meals, recipes, yesterday Smart Log can now match against three new record types in addition to individual foods: - **Saved meals** — say *"my X meal"*, *"the X meal"*, or *"for lunch I had my morning bowl meal"* and the AI tags the item as kind=meal. Smart Log searches `getMeals()`, picks the best name match, and **expands the meal into individual diary entries** when added (same as the Foods page meal-add flow). Each ingredient becomes its own diary item. - **Saved recipes** — same as meals but uses `getRecipes()` (the `is_recipe=1` subset). Trigger words: *"my X recipe"*, *"made the X recipe"*, *"from my X recipe"*. Same expansion behavior. - **Yesterday's diary** — say *"same as yesterday for lunch"*, *"yesterday's breakfast"*, or *"repeat yesterday's dinner"*. The AI tags it as kind=yesterday with the meal slot name. Smart Log fetches yesterday's diary, filters items in that slot, and copies them to today as new entries. #### How the dispatch works - Parser prompt now extracts a `kind` field per item: `food` (default), `meal`, `recipe`, or `yesterday` - `matchItem` is now a router that dispatches to `_matchFood` (existing), `_matchMeal`, or `_matchYesterday` - `_matchMeal(parsedItem, isRecipe)` searches meals or recipes by name with token + fuzzy substring fallback - `_matchYesterday(parsedItem)` resolves the slot name to an index, fetches yesterday's diary, returns a synthetic meal-like record with `_yesterdaySlot` and `_yesterdayDate` metadata - `saveItems` detects expansion-type matches (meal/recipe/yesterday with `food.items[]`) and writes each sub-item as its own diary entry instead of one combined entry #### Modal updates - New badge colors in SmartLogModal: **Meal** (purple), **Recipe** (pink), **Yesterday** (green) alongside the existing **Local** and **OFF** - Expansion-type rows show "Expands to N items" + a "Show items" details disclosure listing each ingredient - Quantity field is hidden for expansion-type rows since the meal already has its own portions baked in ### Documentation - **New "Smart Log" section in README** with the full feature description, all four matchable source types, trigger words, meal slot detection rules, what it can/can't do, privacy story, and cost. ~80 lines of user-facing docs replacing the inline help block. - **Settings help text trimmed** to a brief quick-start + the three main trigger words + a link to the README section. Used to be ~30 lines, now ~10. ### Fixed - **Recording pillbox text centering** — the floating "● Listening… release to log" pill above the FitBot button when recording was off-center because the inline-style positioning shifted by `-60px` (a fixed offset that didn't account for the pill's actual width). Now uses `transform: translateX(-50%)` (or `translateX(50%)` for the default `right:50px` anchor) so the pill always centers on the FAB regardless of text length. Padding bumped to 8/16, font-size to 13, line-height to 1.2 to prevent descender clipping. - **Cancel-state pill color** — the cancel-preview text used inline `color:#fca5a5` which fought the parent `color:#fff`. Now applied via `.cancel` class on the pill itself with a matching border color tint. --- ## [0.36.3-beta] — 2026-04-08 ### Smart Log v3.1 — recording polish - **Red FAB during recording** — the FitBot button now turns red (universal recording color) when hold-to-record activates. Stronger heartbeat ring pulse uses red instead of accent color so it's unambiguous. Reverts to the normal accent gradient when recording stops. - **Audio beeps on start/end** via Web Audio API — short 80ms tone at 1000Hz when recording starts, lower 600Hz tone on commit, lowest 350Hz tone on cancel. Generated in-browser, no asset files. Gated by the existing `barcodeBeep` setting (users who muted barcode scans probably don't want voice beeps either). - **Hold threshold 400ms → 700ms** — old 400ms threshold was too close to the natural "hold to drag" intent. 700ms is far enough above the 6px drag threshold that drag-then-release reliably wins, while still feeling deliberate. The haptic buzz at threshold is the "go" signal so the user knows recording started. - **Slide-off-to-cancel** — finger > 100px from FAB center while recording → cancel preview activates: FAB greys out, recording hint changes to "✕ Release to cancel", light haptic confirms threshold crossed. Releasing in cancel state aborts the recording instead of committing. Same gesture pattern as iOS voice memos and WhatsApp voice messages. - **Recording hint tooltip** — small floating pill above the FAB during recording. Shows "● Listening… release to log" in normal state, "✕ Release to cancel" in cancel-preview state. Backdrop-blurred dark pill, follows the FAB if user has dragged it. ### Build infra - **Dockerfile fix** (committed yesterday) — `scripts/postinstall.cjs` is now copied before `npm install` so the postinstall hook can find it during CI builds. --- ## [0.36.2-beta] — 2026-04-07 ### Smart Log v3 — hold-to-record on the FitBot button - **Removed the dedicated mic FAB**. Smart Log voice is now triggered by **press-and-hold on the FitBot floating button** instead. Single global entry point that works on every page. - **400ms hold threshold** — tap = open chat (existing behavior), hold past 400ms = enter recording mode. Drag still works (movement before threshold cancels the hold timer and resumes drag). - **Visual morph** — robot face fades out, microphone icon fades in, and a stronger heartbeat ring pulse surrounds the FAB. The FAB scales up 8% during recording for clear feedback. - **Haptic feedback** via `@capacitor/haptics`: medium impact when recording starts (crossing the 400ms threshold), light impact on release. Tactile confirmation without needing to look at the screen. - **Native Android voice** via the existing `@capacitor-community/speech-recognition` plugin (uses the OS speech recognizer through an Android intent — no Web Speech API quirks). PWA still uses Web Speech API. - **Auto-jump to review phase** — the AI parses + matches the transcript before the modal opens, so the user goes directly from "release the button" to "review and confirm" with no input/parsing screen in between. New `openMode="preParsed"` mode in the Smart Log modal. - **Globally available** — the modal is mounted from inside `AIFitBot.svelte` so the gesture works on every page (Statistics, Foods, Wellness, etc.), not just Diary. Diary's local Smart Log state was removed entirely. - **Renamed `QuickLogModal.svelte` → `SmartLogModal.svelte`** to match the user-facing name (setting key `quickLogEnabled` is unchanged for backwards compat). - **Settings help text** — when Smart Log is enabled in Settings → AI Assistant, an expanded help section appears below the toggle explaining the hold gesture, drag-vs-hold disambiguation, custom meal name support, and the privacy story (audio stays on-device; transcript goes to user's configured AI provider; food matching is local-first). ### Build infra - **Added `@capacitor/haptics` dependency** for tactile feedback on the hold gesture. Graceful fallback to no haptic if unavailable. --- ## [0.36.1-beta] — 2026-04-07 ### Quick Log → Smart Log v2 (rebranded + native voice + custom meal support) - **Renamed Quick Log → Smart Log** in Settings UI label and modal title. The setting key (`quickLogEnabled`) is unchanged so existing user preferences carry over. - **Native voice input on Android** via `@capacitor-community/speech-recognition` plugin (uses Android's system speech recognizer through OS-level intent — no Google cloud dependency, no Web Speech API quirks). Requests RECORD_AUDIO permission on first use. Works offline if the user has on-device recognition available. - **Floating mic FAB on the Diary page** — replaces the per-meal sparkle button. Single 56px circle bottom-right (above the FitBot FAB), tap to open Smart Log in voice mode and start listening immediately. - **AI parses the target meal slot from natural language** — say "for breakfast I had 2 eggs and toast" and the AI returns both the items AND the meal slot. The parser prompt now includes the user's actual configured meal names verbatim, so custom meal slots like "Pre-workout", "Snack 1/2/3", or renamed defaults ("Morning Bowl") all work. - **Robust meal slot resolution** in `quick-log.js`: exact case-insensitive match → shortest substring match → canonical-word fuzzy fallback (breakfast/lunch/dinner/snack with prefix-priority for numbered duplicates). - **Auto-submit on voice input** — when the native plugin returns a transcript, Smart Log immediately parses without requiring a separate tap. - **Clear listening state** — pulsing mic button + "Listening… speak now" banner during voice capture. ### Build infra - **Postinstall script extracted to `scripts/postinstall.cjs`** — replaces the inline `node -e` one-liner. Patches both `@devmaxime/capacitor-health-connect` (Health Connect SDK version + proguard) and `@capacitor-community/speech-recognition` (proguard) in-place after `npm install`. Adding more plugin patches is now a 5-line addition instead of an unreadable one-liner. - **AndroidManifest** — added `RECORD_AUDIO` permission and the standard `` entry filter for `android.speech.RecognitionService` so the OS exposes the speech recognizer to the app. --- ## [0.36.0-beta] — 2026-04-07 ### Added - **Quick Log (experimental)** — natural-language food entry powered by FitBot's AI provider. Type or speak something like "2 eggs and toast" and the AI parses it into structured items, then a deterministic pipeline matches each item against your local food database (frequency-ranked by usage in your diary), falls back to Open Food Facts, then to "not found" so the confirmation modal can flag it. Mic button uses the Web Speech API where available. Confirmation modal lets you swap matches, edit quantities, change meal slots, and remove items before tapping Add. - New module `src/lib/quick-log.js` (parseInput → matchItems → saveItems pipeline) - New component `src/components/diary/QuickLogModal.svelte` (bottom-sheet on mobile, centered card on desktop, input/parsing/review/saving phases) - New `quickLogEnabled` setting in Settings → AI Assistant, gated behind `aiEnabled` - Sparkle icon button (`auto_awesome`) appears next to each meal's `+` button on the Diary page when both AI and Quick Log are enabled - Works in PWA, native + server, and native local-only modes — same paths as FitBot - **Donation links** — README has a Support section with GitHub Sponsors and Ko-fi badge placeholders. Settings → About now has a "Support development" row with the same buttons. - **`.github/FUNDING.yml`** — GitHub will display a "Sponsor" button on the repo once the placeholders are replaced with real account handles. ### Fixed - **README license inconsistency** — said MIT but actual `LICENSE` file is AGPL-3.0. README now correctly says AGPL-3.0 with a note that the Android app is distributed separately on the Play Store. --- ## [0.35.2-beta] — 2026-04-07 ### Added - **Local Full Backup (.zip)** — new `src/lib/local-backup.js` module produces a self-contained ZIP archive with all foods, meals, recipes, diary, wellness data, workouts, settings, AND embedded image binaries. Designed for true phone-to-phone transfer without needing a server. Restore extracts the ZIP, writes images back to Capacitor Filesystem on native (or re-encodes as data: URLs on PWA), then upserts everything into the local database. Manifest version 1, DEFLATE compressed level 6, JSZip 3.10.1 dependency added. Settings → Data → Local Backup. - **`bulkSet({...})` settings helper** — single-API-call bulk write for onboarding flows. Wizard's `_saveIntegrations`, `_saveNotifications`, `finish`, and `skip` functions now batch their writes (~15 keys) into one PUT `/api/settings/bulk` request instead of firing 15 separate debounced pushes. New server endpoint `PUT /api/settings/bulk` accepts `{ settings: { key: value, ... } }`, runs the upserts in a single transaction, applies the same `isServerOnlyKey` security filter as the per-key endpoint. - **`USER_PREFS` and `DEVICE_PREFS` exported** from `src/stores/settings.js` so other modules (local-backup) can introspect which settings should be included in portable exports vs left local. ### Cleanup - **Defunct `notifGotifyEnabled` setting cleanup migration** — server's `db.js` now runs an idempotent startup cleanup that drops orphan rows for keys that no longer have any code reading them. Currently just `notifGotifyEnabled` (replaced by `notifPushService` dropdown in v0.32.0). Easy to extend with future deprecations via the `DEFUNCT_KEYS` array. --- ## [0.35.1-beta] — 2026-04-07 ### Header layout polish - **Hamburger floats over the banner** — reclaim ~62px of vertical space on every page. The 62px gap above the banner was wasted real estate (especially on mobile, where it ate ~27% of the viewport along with the safe-area inset). Hamburger button restyled as a translucent backdrop-blur dark pill (rgba(0,0,0,0.35), 10px blur, 160% saturate, white text+border) so it reads cleanly over both banner imagery AND plain page background. - **Removed double safe-top padding** that caused a black bar above the banner on initial render — `.page-shell` no longer applies `padding-top` because its child `.page-header` already accounts for safe-area-top. Editor pages (FoodEditor, MealEditor) get the inset restored via a new `.page-shell.editor-page` selector since they use `.editor-header` instead. - **Banner header thickness +20px** (`padding-bottom: 52 → 72px`) for more breathing room. All sticky sub-bars (Diary date, Wellness date+tabs, Foods sticky, Settings search) updated +48 to compensate for the additional `--hamburger-row` height. - **Uniform H1 alignment** — all 6 page headers (Diary, Foods, Goals, Statistics, Wellness, Settings) now share a single `.page-header h1` rule in `base.css` (28px / 800 / 1.1 / -0.02em letter-spacing / forced 40px height). Removed the redundant per-page overrides in Diary.svelte and Wellness.svelte that previously caused a ~9px vertical offset. - **Title sits below the hamburger in its own row** — added `--hamburger-row` CSS variable (48px = 40px hamburger + 8px gap) to `.page-header` top padding when the hamburger is visible. Title's left edge aligns with the hamburger button's left edge (viewport x=12) for a clean vertical line. ### Sidebar drawer - **Heavier dark frosted-glass scrim** behind the slide-in sidebar — 55% black + 28px blur + 180% saturate (was using generic `--overlay`/`--backdrop-blur` tokens). Page content behind the sidebar reads as quiet background texture so the nav items pop. - **Mobile hamburger access restored** — the previous viewport gate I added inadvertently hid the hamburger entirely on small phones. Now only `sidebarPinned` (the persistent/desktop-style mode) is gated to ≥768px. The drawer-style hamburger + slide-in overlay is always available whenever `navStyle` includes 'sidebar'. Tablets and desktop still get the option to pin it open. --- ## [0.35.0-beta] — 2026-04-07 ### Security - **Server-only setting keys filtered from client responses** — OAuth app credentials (`withings_client_secret`, `fitbit_client_secret`, etc.) were being returned by `GET /api/settings` and the differential sync pull endpoint. Added shared module `server/lib/server-only-keys.js` with explicit allowlist + regex pattern fallback (`_client_secret$`, `_consumer_secret$`, `_redirect_uri$`, `_client_id$`, `_api_secret$`). Filter applied at every read/write boundary: GET/PUT `/api/settings`, GET/POST `/api/sync` settings filter, server-side rejection on PUT with 403. ### Settings sync overhaul (root-cause fix for the missing-mealNames bug) - **Split `SERVER_SETTINGS` into `USER_PREFS` and `DEVICE_PREFS`** in `src/stores/settings.js`. USER_PREFS travel across devices (nutrition, units, integrations, notifications, behavioral prefs). DEVICE_PREFS stay local: `appearance`, `navStyle`, `sidebarPersistent`, `disableAnimations`, `barcodeFlashlight` — these depend on form factor / device hardware and shouldn't be forced to match across phone + desktop. - **`loadServerSettings()` now mirrors all server settings into native SQLite** `user_settings` on Android, marking them `synced` so the differential sync engine doesn't re-push. Background workers (ReminderWorker, HealthConnectSyncWorker) now read fresh values. Phone went from 60 → 86 settings after the first cold-start with this fix. - **Global `wl:setting` listener** in `src/stores/settings.js` catches direct `DB.setSetting()` writes (16+ legacy bypass call sites) and triggers `scheduleSave` + native SQLite mirror for `USER_PREFS` keys. Fixes the entire bypass class without touching individual call sites. - **`DB.setSetting` short-circuits** if value is unchanged — prevents listener floods on mount and avoids double-firing the server push. ### Added - **Sidebar viewport gate** — sidebar nav style is force-hidden on screens < 768px (small phones in any orientation) regardless of user preference. Tablets, foldables, and desktop keep the option. The setting itself is preserved across rotations/resizes. Settings UI now also uses the width gate instead of `!isNative` so the persistent-sidebar toggle appears on tablet/foldable native installs. - **Per-device sync range tiers** in Settings → Wellness — replaces single `SYNC_RANGE_OPTIONS` with Recommended (1d/1w/1m/3m for all devices) + Advanced ⚠ tier per device: Fitbit 6m/1y, Garmin 6m only, Withings 6m/1y. Custom day input clamped per device (Garmin 180, Fitbit/Withings 365). Each device gets a one-line warning under Advanced explaining why longer ranges may fail or be slow. ### FitBot redesign - **Animated robot face SVG** (FitBotFace.svelte) replaces the `smart_toy` Material icon in all 5 places FitBot is shown (FAB, header, welcome screen, message avatar, typing indicator). Pure-CSS animations: blinking eyes, eye-darting, pulsing antenna, twinkling cheek lights, breathing mouth. - **FAB visual upgrade** — glassmorphism (backdrop-filter blur+saturate, white border, inner radial highlight, depth shadows), theme-aware animated gradient (shifts between `--accent` and `--accent-2`, never hardcoded colors), concentric heartbeat ring pulse using `--accent-dim` (replaces vertical bobbing). - **Responsive panel layout** — replaces right-side slide-in with: bottom sheet on mobile (88vh, drag handle, dimmed backdrop) / floating card on desktop (420×640, no backdrop, anchored to FAB position). Card pops up next to wherever the user dragged the FAB (4-quadrant logic clamped to viewport). ### Fixed - **Meal reminder labels** — both ReminderWorker.java and server scheduler had a bug where missing/short `mealNames` fell back to hardcoded defaults `['Breakfast','Lunch','Dinner','Snacks']` and would fire wrong labels like "Time to log your Dinner!" at 1pm. Now: if mealNames is missing or shorter than `notifMealTimes`, unmatched indices show generic "Time to log your meal!" instead of impersonating a wrong slot. - **BarcodeScanner double-submit** — `onCode()` checked `detected` flag at the top but never set it, so rapid camera detections (multiple frames or two engines firing for the same barcode) could both dispatch scan events. `doManual()` had no guard at all. Both now set `detected=true` on entry; continuous mode resets after 1.5s cooldown. - **Workout list "max" → "peak" HR** — compact workout list said "94 avg · 154 max bpm" while the expanded detail card said "Peak HR" for the same value. Now consistent. --- ## [0.34.0-beta] — 2026-04-06 ### Added - **Native Android background reminders via WorkManager** — new Kotlin/Java native worker (`ReminderWorker.java`) runs every 15 min (Android floor) to check SQLite directly and fire meal/water/weigh-in notifications only when warranted. Skips reminders for meals already logged in today's diary, water goals already met, etc. Works even when the app is closed/killed without depending on the JS layer. - **Native Health Connect background sync via WorkManager** — new Kotlin `HealthConnectSyncWorker` (CoroutineWorker) reads the androidx Health Connect SDK directly (no JS plugin bridge needed) and writes results into the local SQLite `wellness_data` table under `source='health_connect'`. Reads 13 metric types: steps, distance, total/active calories, avg HR, resting HR, weight, body fat, SpO2, respiratory rate, sleep session with stages, floors, hydration. Runs every hour when the user has Health Connect enabled. - **WorkerScheduler.java** — centralized worker enqueue/cancel logic. HC sync worker is only enqueued when `healthConnectEnabled = true` in user_settings; toggling off calls `cancelUniqueWork` so the OS doesn't run anything for HC at all (zero battery cost when disabled). Re-evaluation triggers from MainActivity onCreate (app open) and from ReminderWorker every 15 min so Settings toggles take effect within 15 min without a reopen. - **Kotlin support added to Android project** — `kotlin-gradle-plugin:2.0.21` + `kotlinx-coroutines-android:1.8.1`, enabling native suspend/coroutine code for the HC SDK (~3x less boilerplate vs Java). - **`_USE_NATIVE_WORKER` kill switch** in `src/lib/notifications.js` — when `true` (default), the JS-side `LocalNotifications` scheduling for water/meal/weigh-in early-returns after cancelling any pending OS notifications, making WorkManager the sole source of local reminders. JS code preserved for fallback testing. ### Battery-conscious worker design - `NetworkType.NOT_REQUIRED` — never wakes the radio - `setRequiresBatteryNotLow(true)` — skips when battery < 15% - Read-only SQLite where possible; HC writes use WAL-friendly transactions - Single ReminderWorker handles all reminder types (3x fewer wake-ups vs separate workers) - `ExistingPeriodicWorkPolicy.KEEP` — survives app restarts without re-enqueueing - Permission gate: HC worker bails immediately if no HC permissions granted - No exact alarms, no wake locks, no foreground service - Each invocation: ~50ms SQLite read + (optionally) ~50ms HC reads + ~20ms write ### Fixed - **Fitbit workout Peak HR was always 220** — `heartRateZones[].max` is the zone boundary (Peak zone always tops at 220), not actual HR. Now fetches per-minute intraday HR data from `/1/user/-/activities/heart/date/.../1d/1min/time/.../...` for each activity's time window and stores the actual highest recorded heart rate. Label renamed to "Peak HR". - **Peak HR timezone bug** — initial intraday fix used `toTimeString()` which converts to server timezone, fetching the wrong HR window (often resting hours). Now parses HH:mm directly from the activity's ISO startTime to preserve the user's local time. - **Goal celebration repeats across app reloads** — `_celebratedToday` Set was in-memory only and reset on every reload, causing repeat celebrations for goals already hit earlier in the day. Now persisted to localStorage with date key. Affects all goals: water, calories, protein, carbs, fat, steps, sleep, etc. - **Stress score formula display string** — debug/info text said `0.60×... + 0.40×...` but actual code uses `0.50/0.50` smoothing since v0.30.0. Display now matches the math. ### Changed - **Fitbit score calibration data collection** — `reference_fitbit_scores.md` now tracks readiness/stress formula output alongside actuals (not just sleep). User pastes `[readiness]` and `[stress]` console JSON blocks daily; all components, baselines, inputs are logged for retroactive refit. --- ## [0.33.0-beta] — 2026-04-05 ### Added - **Health Connect → Statistics integration** — all wellness metrics (steps, active min, sleep, resting HR, HRV, SpO2) and body metrics (weight, body fat, muscle mass) from Health Connect now feed into Statistics charts on native, alongside Fitbit/Garmin/Withings sources - **Health Connect Visible Metrics filter** — Settings → Wellness → Health Connect now has chip toggles for 25 metrics (steps, distance, sleep stages, HR, weight, body fat, SpO2, BMR, etc.) matching Fitbit/Garmin/Withings sections - **Statistics respects visible metrics filter** — hidden wellness metrics (per `wellnessMetrics` setting) no longer appear in the Statistics dropdown, consistent with Wellness page behavior - **Workout Peak HR from intraday API** — Fitbit workout sync now fetches actual peak heart rate from the per-minute intraday HR endpoint for each activity's time window, instead of using the heart rate zone ceiling - **Health Connect string-record parsing** — plugin returns BodyFat, BloodPressure, and SpO2 as Kotlin `toString()` strings; added regex parsers for each - **Health Connect Weight string fallback** — defensive parser for Weight records in case the plugin returns a string instead of an object ### Fixed - **Repeat goal celebration notifications** — `_celebratedToday` Set was in-memory only and reset on every app reload, causing repeat celebrations for goals already hit earlier in the day. Now persisted to localStorage with date key. Affects all goals: water, calories, protein, carbs, fat, steps, sleep, etc. - **Workout "Max HR" always 220** — was reading `heartRateZones[].max` (the zone boundary, not actual HR; Peak zone always tops at 220). Now uses real intraday HR data; label renamed to "Peak HR" - **Peak HR timezone bug** — initial intraday fix used `toTimeString()` which converts to server timezone, fetching the wrong HR window. Now parses HH:mm directly from the activity's ISO startTime to preserve local time - **Meal reminder sent even after meal logged** — scheduler diary query now falls back to `user_id IS NULL` rows for users with diary entries created before user management was enabled; explicit `Number()` conversion on meal index comparison - **Scheduler `reminderMin is not defined`** — leftover undefined variable from the 30-min delay revert - **Statistics: Health Connect data ignored** — Statistics page only queried Fitbit/Garmin/Withings server endpoints; now also reads `health_connect` source from local SQLite on native - **Body weight/body fat charts ignored Health Connect** — `isBodyDevice` only checked `withingsEnabled`; now also checks `healthConnectEnabled` and falls back to HC data when no Withings reading is available ### Changed - **Wellness `toggleMetric` covers HC metric IDs** — includes `active_calories`, `avg_heart_rate`, `blood_pressure_systolic/diastolic`, `body_temperature`, `sleep_awake_min`, `water_ml` so the visibility toggle properly tracks them - **Scheduler logging elevated to info** — meal reminder check now logs at info level so push notification debugging is visible without enabling debug mode --- ## [0.32.0-beta] — 2026-04-03 ### Added - **Bidirectional settings sync** — settings changes on Android now sync to server via differential sync engine; server setting changes (from PWA) pull down to Android and update stores in real-time - **Wellness offline cache** — Wellness page reads from local SQLite on Android, showing synced Fitbit/Garmin/Withings/Health Connect data even when offline - **Local wellness data for sparklines, readiness, and stress** — all range-based data loads (sparklines, sleep insights, readiness, stress) use local SQLite on native instead of server API - **Workout history with GPS route maps** — Fitbit activity log sync via `activities/list` endpoint; TCX GPS parsing for route data; Leaflet/OpenStreetMap route display with HR-colored polyline segments; workout detail modal with map, duration, distance, calories, HR, and steps - **`workoutsEnabled` setting toggle** — Settings → Wellness → Fitbit; enables/disables workout log sync and display - **Comma formatting on large numbers** — `toLocaleString()` applied to calories, steps, and other large numbers across Diary, Foods, Wellness, and Statistics - **Fitbit OAuth `location` scope** — added to authorize request for GPS/TCX route access - **FitBot AI tool use** — FitBot now fetches real data on demand via function calling instead of hallucinating from context; tools: `get_wellness_data`, `get_body_composition`, `get_diary`, `get_workouts`, `get_goals`; supports Claude, OpenAI, and Gemini; queries any date range; tool execution loop up to 5 rounds - **FitBot image attachments** — attach images to FitBot messages; camera on native (via `@capacitor/camera`), file picker always available on PWA, camera option shown on PWA if webcam is detected - **Mobile OAuth via system browser** — Fitbit, Garmin, and Withings OAuth on Android now opens the system browser via `@capacitor/browser` with callback via `nutritrace://` deep link (AndroidManifest intent filter for `nutritrace://callback`) - **PWA settings poll** — PWA polls server for setting changes every 30 seconds and on tab focus (`visibilitychange`) for near-real-time sync - **Diary scroll position save/restore** — diary saves and restores exact scroll position when adding food (page-transition is a fixed scroll container) - **Notification system** — 10 notification types (water reminders, meal reminders, weigh-in, goal celebrations, calorie goal, step goal progress, wellness alerts, workout summaries, weekly summary, sync failures); delivery via device notifications + push service - **Push service support** — dropdown: Apprise, Gotify, ntfy; each with own config; native uses CapacitorHttp (no CORS), PWA proxies through server; test button for all services - **Scheduled wellness sync** — new sync mode alongside Auto and Manual; server-side scheduler runs every 15 minutes; frequency options: every 6h, every 12h, daily, weekly - **Server-side scheduler** — push reminders (water, meal, weigh-in) via configured push service for PWA users; weekly summary on Sundays; scheduled wellness sync trigger - **Repeating local notifications** — water, meal, weigh-in reminders use `every: 'day'` for infinite repeat; re-scheduled on every app open - **Eye icon on all password/token fields** — OFF password, SMTP password, new user password, admin password, Gotify token, ntfy token all have visibility toggle - **Local mode audit** — Fitbit/Garmin/Withings show disabled state with explanation in native local mode; Health Connect promoted as local alternative; Gotify works in local mode via CapacitorHttp; FitBot reads from local SQLite ### Fixed - **Health Connect section spacing** — uniform `padding-top:16px` matching Fitbit, Garmin, and Withings sections - **Wellness "No Device Connected" on offline** — no longer shows connection prompt when cached wellness data is available - **Debug logging cleanup** — removed verbose push food details and raw JSON result logging from sync engine - **Statistics history rows missing comma formatting** — large numbers (calories, steps) in history rows now formatted with `toLocaleString()` - **Settings Wellness cards missing scoped styles** — `.section-body`, `.settings-card`, `.setting-row` styles now scoped inside SettingsWellness.svelte, matching uniform card appearance in Connected Services - **"Save & Connect" HTML entity** — raw `&` entity no longer displays as literal text in button labels - **Settings Wellness connection status loading slow** — Fitbit/Withings/Garmin status API calls now run in parallel; connection status auto-loads on component mount instead of waiting for section expand - **Workouts table in full backup** — workouts table now included in full backup dump and restore - **Settings sync feedback loop** — `_suppressSync` flag prevents feedback loop when loading server settings into stores; 10-second recently-changed protection window prevents server pull from overwriting local changes; settings written to SQLite immediately on `.set()` (not debounced) - **Statistics units showing "kcal" for all metrics** — replaced broken `getMetricUnit()` function calls with reactive `$: _metricUnit` variable - **`_DB` reference error crashing settings sync** — `_DB` used before definition in `Statistics.svelte` and `Settings.svelte`; fixed declaration order - **Gradle 9.3 proguard compatibility** — `proguard-android.txt` → `proguard-android-optimize.txt` ### Changed - **Readiness and stress score formulas recalibrated** — 10-day dataset used to tune coefficients; MAE improved from 2.5→1.4 (readiness) and 2.5→1.6 (stress); see `reference_fitbit_scores.md` for formula versions and calibration log --- ## [0.29.0-beta] — 2026-04-01 ### Added - **Phase 2: Differential sync infrastructure** — push/pull endpoints with timestamp tracking; only changed records sync instead of full merge - **Phase 2: Offline cache layer** — `NtApiCached` tries server first, falls back to local SQLite when offline - **Phase 2: Image caching for offline mode** — downloads server images to device storage for offline access - **Sync status bar** — progress phases (pushing/pulling/caching images) with visual feedback - **Connection badge on hamburger menu** — green dot when connected to server, red when offline - **Three-way merge dialog on server connect** — upload/download/merge options when reconnecting - **Sync on app startup and resume** — automatic differential sync when app launches or returns from background - **Local fonts for Android** — Material Symbols + Inter bundled in app, identical to CDN versions - **Android back button navigation** — navigates back within app history, double-tap to exit - **Wellness page: generic "No Device Connected" messaging on mobile** — no API setup prompts on Android ### Fixed - **Fitbit score calibration** — readiness HRV neutral set to 62, penalty 400 (uses 30-day baseline) - **Score locking** — readiness/stress snapshot on first sync of the day; recalculate button for manual tuning - **Wellness date calculations** — use local timezone instead of UTC - **Profile page** — missing `resolveAssetUrl` import broke profile loading - **Images survive server disconnect** — `loadImageMap` awaited, `NtApiNative` uses `resolveAssetUrl` - **Sync bar portalled to body** — stays fixed at top, doesn't scroll with page content - **Server: soft deletes on all tables** — `updated_at` tracking for differential sync - **Server: /uploads served before auth middleware** — Android WebView can load images without token - **Server: ALTER TABLE migration** — uses constant defaults (SQLite limitation workaround) - **Source chip clicks** — reactive loop breaking OFF/USDA selection fixed - **Settings search bar** — sticky below header - **Foods search bar** — uniform style with barcode icon inside pill - **Wellness tab bar** — sticky below date bar - **Wellness pill position** — uses DOM measurement for pixel-perfect alignment on mobile --- ## [0.28.0-beta] — 2026-03-31 ### Added - **Android app via Capacitor 8 (Phase 1 complete)** — full native Android build wrapping the Svelte PWA - Offline-first with local SQLite database (`@capacitor-community/sqlite`) - NativeSetup wizard: "Use Locally" (pure offline) or "Connect to Server" (enter URL + authenticate) - Server connection with Bearer token auth and data merge dialog (push local data, choose settings winner) - Native barcode scanner via Google ML Kit (`@capacitor-mlkit/barcode-scanning`) - Native camera for food, meal, and avatar photos (`@capacitor/camera`) - OFF/USDA food search via `CapacitorHttp` (CORS bypass in WebView) - All images resolve to server URL in connected mode - All API calls include auth token in native server mode - Service worker disabled inside Capacitor (prevents offline.html redirect) - App icon at all Android `mipmap` densities - Wizard: measurement system step (metric/imperial) with appropriate defaults - Password visibility toggle on connect forms - Settings hidden in local mode: User Management, Email, Food Sharing, persistent sidebar - Full backup works in server-connected mode - **Server CORS** — allows `Authorization` header and all origins for native app support - **Server auth** — `authenticate` middleware accepts Bearer token in `Authorization` header (in addition to cookie) - **Login response** — JWT token now included in response body (for native app token storage) ### Fixed - **OFF/USDA source chip clicks** — reactive loop was breaking chip navigation for both PWA and Android; fixed reactive dependency chain - **`_extFetch` recursive call** — infinite loop on PWA caused by self-referencing fetch wrapper; corrected call target - **Page banners default to on** — banners now enabled by default on fresh installs - **Long-press menu for external results** — OFF/USDA search results now show "Save to My Foods" instead of Edit/Delete in the long-press action sheet --- ## [0.25.0-beta] — 2026-03-30 ### Added - **Food & meal sharing** — multi-user groups can now share foods, meals, and recipes between members; admin enables sharing via Settings → Sharing; each item can be set to Private / Everyone / Specific People; shared items appear in a "Group Catalogue" tab in Foods; adding a shared item to diary auto-copies it to your own catalogue first (copy-on-use model; originals remain unaffected); `_shared_by` badge shows contributor attribution - **FoodEditor/MealEditor sharing section** — new Sharing card at bottom of food and meal/recipe editors; shows visibility selector (Private / Everyone / Specific) and user-picker chips when Specific is selected; only visible when multi-user is active and item is owned by the current user - **Settings → Sharing section** — admin toggle to enable/disable food sharing instance-wide; per-user default visibility preference - **Tab favicon** — browser tab and bookmark now show the NutriTrace logo (SVG preferred, PNG fallback) - **Edit credentials button** — Fitbit, Withings, and Garmin connection cards now show a pencil icon button when configured but not connected, allowing API credentials to be updated without needing to disable the integration entirely ### Fixed - **OAuth credential change** — after disconnecting Fitbit/Withings/Garmin, credentials were read-only (only "Connect" button visible); now shows an edit button to modify Client ID, Secret, and Redirect URI before reconnecting --- ## [0.24.0-beta] — 2026-03-30 ### Added - **Card tooltips** — Sleep Debt, Chronotype, Daily Readiness, and Stress Management cards now have hover tooltips explaining what each metric measures and noting that they always reflect current/rolling data, not the specific date selected in the date picker ### Changed - **Heart tab metric order** — reordered to match Fitbit's Vitals section: Resting HR → SpO2 → Respiratory Rate → HRV → Skin Temp Variation → Cardio Fitness - **Skin Temp Variation moved to Heart tab** — was incorrectly grouped under Sleep; moved to Heart where it belongs - **Skin Temp Variation displayed in °F** — stored as °C from Fitbit API, converted to °F for display (variation × 9/5, no offset since it's a delta) ### Fixed - **Daily Readiness / Stress Management score inflation** — today's HRV was included in the baseline mean (circular: a low-HRV day pulled the baseline down, making the ratio look better). Fixed by using history-only values for baseline calculation; today is counted only for the minimum-data threshold check. Scores now match Fitbit's (readiness ±1, stress converging) - **Fitbit `temperature` scope added** — skin temp variation was always returning null because the `/temp/skin` endpoint requires the `temperature` OAuth scope which was not being requested; users need to re-authorize Fitbit to grant this scope - **Withings `user.cardiovascular` scope removed** — added in previous version but Withings requires explicit developer approval for this scope; caused "scope not allowed" errors on reconnect for standard developer apps - **OAuth state persisted to DB** — all three integrations (Fitbit PKCE, Withings state, Garmin request tokens) now store OAuth state in a new `oauth_state` table instead of in-memory Maps; server restarts during the auth redirect window no longer cause "invalid state" or "token expired" errors --- ## [0.23.0-beta] — 2026-03-30 ### Added - **Daily Readiness Score** — new card on the Wellness Heart tab; calculates a 1–100 score from 30 days of personal HRV, RHR, sleep, and activity history; asymmetric HRV model (below-baseline penalised 2.75× harder than above); HRV×RHR interaction penalty fires when both signals are bad simultaneously; shows Optimal/Good/Fair/Low/Poor label with colour coding; 4-column driver breakdown (HRV · RHR · Sleep · Penalties); calibrating state shown when fewer than 7 days of HRV history exist; constants reverse-engineered from 6 actual ground-truth data points (avg error ±1.2 pts, max 2 pts) ### Changed - **AI chat renamed AIBuddy → AIFitBot** — component file renamed to `AIFitBot.svelte`; no user-facing name change (assistant name is still configurable) - **AI assistant data access expanded** — system prompt and context now include Garmin data (steps, activity, sleep, HR, HRV, SpO2, body battery, stress, max HR), full Fitbit metrics (AZM, floors, distance, sleep score, SpO2, respiratory rate, VO2 Max, skin temp), full Withings metrics (bone mass, body water, visceral fat, vascular age, metabolic age), water intake, and a note when no wellness data is available; welcome screen updated with a "Sleep & recovery" quick chip - **AI message timestamps** — messages from today show time only (e.g. "3:45 PM"); messages from previous days show date prefix in the user's preferred format (e.g. "03/29 · 3:45 PM") - **Settings search bar** — changed `top` from `56px` to `0` so the bar snaps directly to the top when the banner scrolls away, eliminating the crawl-through-banner effect ### Fixed - **Wellness goals first-load** — wellness goal progress bars were blank on first visit to the Goals page because `fitbitEnabled`/`garminEnabled` stores hadn't resolved yet when `onMount` ran; moved fetch to a reactive statement that fires as soon as either store becomes true --- ## [0.22.0-beta] — 2026-03-29 ### Added - **Sleep Debt card** — Sleep tab now shows cumulative sleep debt over last 7 or 14 nights (configurable with range chips); calculated as sum of `max(0, goal − actual)` per night - **Chronotype card** — classifies sleep type (Early Bird / Morning Type / Intermediate / Evening Type / Night Owl) from average sleep midpoint across the selected range; requires ≥5 nights of timing data; shows "Building profile…" with count when insufficient data; includes emoji + plain-language description matching Fitbit's style - **Sleep start/end extraction (Fitbit)** — `sleep_start_min` and `sleep_end_min` now parsed from Fitbit `startTime`/`endTime` fields and stored in wellness_data (minutes past midnight) - **Sleep start/end extraction (Garmin)** — `sleep_start_min` and `sleep_end_min` derived from `startTimeInSeconds + startTimeOffsetInSeconds` (local epoch → UTC hours/minutes); `sleep_end_min` computed from start + `durationInSeconds` - **7-day sparklines on metric cards** — each Movement / Sleep / Heart metric card now displays a small inline SVG sparkline showing the last 7 days of that metric; loaded in background, does not block current-day display - **Statistics — wellness metrics** — Statistics page now includes a Wellness section (when Fitbit/Garmin/Withings are enabled) with Steps, Active Minutes, Sleep, Resting HR, HRV, SpO2, and Muscle Mass; supports all date ranges including a 365-day window for the 'all' range - **Statistics — device-first body composition** — when Withings is connected, weight and body fat pull from Withings device data first and fall back to diary manual entries; no source toggle needed; applied automatically - **Hover tooltips on wellness metric cards** — each metric card has a `title` attribute with a plain-language explanation of what the metric measures and why it matters ### Changed - **Trends tab removed** — the Wellness Trends tab has been replaced by inline sparklines on each metric card; reduces duplication with Statistics and keeps the view focused - **Sleep stage legend redesigned** — proportional flex row below the bar; each segment's label and value are centered under its corresponding bar segment; segments narrower than 3% are hidden to avoid overflow - **Wellness goals — today's progress** — Wellness goals now show the actual today total and a progress bar (same as nutrient/body stat goals); fetches today's Fitbit + Garmin data on Goals load - **Statistics body composition** — device-first merge replaces the manual Diary/Device source toggle; cleaner UX, no extra UI state ### Fixed - **Reactive double-load for sleep insights** — split the reactive block into two: one marks `_insightsLoaded = false` when deps change, the other calls `loadSleepInsights()` only when stale; eliminates the race condition that caused duplicate fetches --- ## [0.21.0-beta] — 2026-03-29 ### Added - **Withings segmental lean + muscle mass** — correct positional type mapping for types 173 (lean mass) and 175 (muscle mass); five readings per measurement group are assigned to torso, left leg, left arm, right leg, right arm in order; removed incorrect prior type mappings - **Withings additional body metrics** — extracellular water (type 168), intracellular water (type 169), visceral fat index (type 170), metabolic age (type 227); displayed on Body tab and togglable in Settings - **Fitbit Cardio Fitness (VO2 Max)** — fixed endpoint (removed erroneous `/1d` suffix); range response (e.g. "39-43") stored as midpoint; label renamed to "Cardio Fitness" throughout to match Fitbit's own terminology - **Fitbit skin temperature variation** — synced from `/temp/skin` endpoint (Pixel Watch 4 and compatible devices); shown on Sleep tab - **Garmin max heart rate** — extracted from dailies `maxHeartRate` field; shown on Heart tab - **Sleep score estimation (Fitbit)** — sleep score endpoint not available in public API; estimated from duration, deep+REM%, SpO2, and HRV; calibrated to within ±1 pt on 3 actual days; Garmin device score takes priority when both sources are present - **Settings toggles** — added for all new metrics: skin temp variation (Fitbit), max HR (Garmin), extracellular water, intracellular water, visceral fat index, metabolic age (Withings) ### Changed - **Segmental Analysis** — removed the % toggle (values were misleading); replaced with an explanatory note; "Fat" column renamed to "Lean" to correctly reflect what the data represents (lean mass, not fat mass) - **Sleep stage legend** — values now display in h/m format (e.g. "1h 13m") instead of raw minutes; applied to both the legend and bar tooltips - **displayData merge** — Garmin sleep score takes priority over Fitbit estimated score; all other metrics still prefer Fitbit when both are present ### Fixed - **Withings OAuth scope** — removed `user.cardiovascular` from default scope (caused re-auth failures); ECG requires re-auth only when explicitly needed - **Wellness Trends unit conversion** — muscle mass and weight charts now correctly convert to lbs on the y-axis and in tooltips when the app unit is set to lb --- ## [0.20.0-beta] — 2026-03-29 ### Added - **Metric visibility toggles** — Settings → Wellness now includes a "Visible Metrics" card with chip toggles for every wellness metric, grouped by section (Movement, Sleep, Heart, Garmin, Body, Body Scan, Segmental); hidden metrics are excluded from Wellness display and future reports; data is always synced regardless of visibility; defaults to all visible with a "Reset to defaults" button - **Expanded Withings metrics** — now captures heart pulse during weigh-in (meastype 11), segmental fat mass per limb (right arm, left arm, torso, right leg, left leg); displays in a new Segmental Analysis table (muscle + fat per limb) on the Body tab - **Withings ECG** — syncs ECG recordings from `/v2/heart` endpoint after each measurement sync; stores `ecg_heart_rate` (latest reading) and `ecg_afib` (Normal / Detected per day); requires re-authorization to grant `user.cardiovascular` scope - **Fixed Withings type-174 duplicate bug** — `visceral_fat` was being silently overwritten by a second `174` mapping; corrected to a single `visceral_fat` entry - **Expanded Garmin metrics** — now extracts moderate/vigorous intensity minutes from dailies; respiration rate and sleep score from sleep response (already fetched) - **Fitbit Active Zone Minutes** — synced from `/activities/active-zone-minutes` endpoint using the existing `activity` scope - **Fitbit VO2 Max** — synced from `/cardioscore` endpoint; requires re-authorization to grant `cardio_fitness` scope - **New metric cards** in Wellness — Active Zone Min, Moderate Intensity, Vigorous Intensity (Movement tab); Sleep Score (Sleep tab); VO2 Max (Heart tab); Heart Pulse, ECG Heart Rate, AFib Detection (Body Scan Scores); ECG & AFib chip on Withings connect screen ### Changed - Visibility filtering extended to Body, Body Scan Scores, Garmin-specific, and Segmental sections (previously only applied to Movement/Sleep/Heart) - **Labs section removed** from Settings — it had been reduced to a redirect note; credentials are fully managed per-integration in Settings → Wellness --- ## [0.19.0-beta] — 2026-03-29 ### Added - **Garmin integration (Experimental)** — OAuth 1.0a flow via Garmin Health API; syncs steps, distance, active minutes, calories, floors, sleep stages, resting HR, HRV, SpO2, Body Battery, and Stress score; requires a Garmin Health API partnership (not a free developer program) - **GarminIcon** — triangle brand mark SVG component (`currentColor`) matching the Garmin logo - **Garmin sync button** — appears in the fixed topbar alongside Fitbit/Withings when connected; shows GarminIcon at rest, spinning sync icon while active - **Garmin card in Settings → Wellness** — with purple "Experimental" badge, enable toggle, sync range chips + custom input, and inline credential setup form (Consumer Key/Secret/Redirect URI) - **Garmin-specific metrics in Heart tab** — Body Battery (peak/low) and Avg Stress shown in a dedicated Garmin card - **Merged activity display** — Fitbit data takes priority; Garmin fills in when Fitbit has no value for a metric (movement, sleep, heart tabs) ### Fixed - **Nerve Activity (EDA) display** — Withings Body Scan nerve measurement (meastype 226) is raw electrodermal activity in µS, not a 0–100 score; unit corrected from `/100` → `µS` and label updated to "Nerve Activity" to accurately reflect what the API returns --- ## [0.18.0-beta] — 2026-03-29 ### Added - **Per-user Fitbit & Withings credentials** — each user registers their own developer OAuth app; credentials stored in user_settings (multi-user) or app_config (single-user), no admin required - **Inline credential setup in Settings → Wellness** — when a tracker is enabled but not yet configured, the credential form appears inline with step-by-step instructions; no separate Labs section needed - **Last synced timestamp** — `/status` routes now return `lastSyncedAt`; Settings → Wellness shows "Last synced X minutes ago" next to each connected device - **DEPLOY.md** — full self-hosting guide: Docker Compose setup, all env vars, first-run walkthrough, Fitbit & Withings OAuth app registration steps with required scopes and redirect URI format ### Changed - Settings → Labs now shows a brief note directing users to Settings → Wellness for credential setup - Redirect URI suggestion auto-filled from `window.location.origin` (matches actual deployment URL instead of placeholder) --- ## [0.17.0-alpha] — 2026-03-29 ### Added - **Wellness settings section** — dedicated "Wellness" section in Settings (between AI Assistant and Labs) for all user-facing wellness controls: Activity Tracking toggle, Sync Mode selector, and per-integration cards (Fitbit + Withings) each with an enable toggle, sync range (chips + custom input), and a 4-state connection UI (loading / connected+disconnect / configured+connect / admin-required) ### Changed - Settings → Labs now contains only admin API credentials (Fitbit Client ID/Secret, Withings Client ID/Secret); all operational controls moved to the new Wellness section - Non-admin users see an info card in Labs noting that credentials are managed by an admin --- ## [0.16.0-alpha] — 2026-03-29 ### Added - **Sliding pill tabs** — `Tabs.svelte` now uses an animated sliding pill indicator (same transition as BottomNav) on Foods (Foods/Meals/Recipes), MealEditor picker, and anywhere else the `` component is used - **Wellness tab bar pill** — Wellness Movement/Sleep/Heart/Body/Trends tab bar gets the same sliding pill treatment - **Wellness sync buttons in topbar** — Fitbit and Withings sync buttons are now fixed to the top-right corner (same row and height as the hamburger menu), portalled to `document.body` so they stay on screen while scrolling; each shows its brand logo at rest and a spinning sync icon while active - **FitbitIcon + WithingsIcon** — monochrome SVG brand mark components (`currentColor`) for use anywhere in the app - **Disconnect in Settings** — Fitbit and Withings each show a "Connected device" row (with account ID) and a Disconnect button inside Settings → Labs; connection status fetched when Labs section opens - **Custom sync range** — Fitbit and Withings sync range now support any number of days via an inline number input alongside the preset chips; input highlights accent when a custom value is active - **Multi-select in MealEditor ingredient picker** — checkbox-based multi-select across all three tabs (Foods, Meals, Recipes); selecting multiple items opens a stacked per-item portion sheet before batch-adding; single tap still opens the single-item flow ### Changed - Wellness sync bars removed from content area — sync is now always accessible from the fixed topbar buttons regardless of active tab - Wellness disconnect moved from topbar to Settings → Labs (more appropriate home for device management) - Settings Appearance: Celebrate goals, Page banners, Loop banner animations descriptions now render below the label (block `
`) instead of inline (``), consistent with Persistent sidebar ### Fixed - Wellness sync buttons now stay visible while scrolling (portal + position:fixed, unaffected by Svelte fade transition stacking context) --- ## [0.15.0-alpha] — 2026-03-28 ### Added - **AI wellness context** — AI Buddy now includes today's Fitbit and Withings data (steps, active minutes, sleep, HR, HRV, weight, body fat, etc.) in its system prompt so it can speak to your full health picture - **Wellness goal celebrations** — metric cards pulse with the same `goal-pulse` animation as Diary when a tracked metric (steps, active minutes, sleep duration) crosses its goal for the day; respects the "Celebrate goals" and "Disable animations" settings ### Changed - Wellness tab bar now uses `flex: 1 0 auto` so tabs are equally spaced on wide screens and horizontally scrollable on mobile without shrinking ### Fixed - Wellness tab bar buttons were all left-aligned on desktop after the scroll fix; restored equal distribution while preserving scrollability on small screens --- ## [0.14.0-alpha] — 2026-03-28 ### Added - **WellnessBanner** — animated SVG banner for the Wellness page header: shoe-print trail walking left→right with sequential stamp animation, floating Zzz's looping upward beside a crescent moon, and twinkling stars; dual radial glow gradients (warm left / cool right); full `no-anim` / `no-loop` class support and `prefers-reduced-motion` media query - **Fitbit sync range** — Settings → Labs: chip selector for how far back the manual Sync button fetches (1 day / 1 week / 1 month / 3 months / 1 year); auto-sync always covers today only; server supports `{ from, to }` range with 250ms throttle and 429 rate-limit detection ### Changed - Sidebar version string updated to v0.14.0-alpha - Wellness tab restored to Statistics' slot in BottomNav; Statistics restored alongside it; Wellness only appears when the `wellnessEnabled` setting is on; Wellness inserts after Foods (where Water used to be) - Foods/Meals/Recipes multi-select: searching no longer clears selection; only switching tabs resets it ### Fixed - Water card banner title position corrected from `padding-bottom: 52px` to `16px` - Wellness title: removed inline icon from h1 to match all other page headers - Fitbit OAuth redirect: callback redirected to `/?fitbit=connected#/wellness` (real query string) instead of `/#/wellness?connected=1` (inside hash fragment) — the latter caused svelte-spa-router to fall through to `* → Diary` - Fitbit OAuth callback URL: the correct redirect URI to register in the Fitbit developer portal and in Settings → Labs is `https://your-domain.com/api/wellness/fitbit/callback` --- ## [0.13.0-alpha] — 2026-03-28 ### Added - **Wellness section** — new nav entry (replaces the Stats slot in BottomNav; sits between Foods and Goals in Sidebar) with dedicated `/wellness` route; powered by Fitbit integration with full OAuth 2.0 PKCE flow - **Fitbit integration** — connects to Fitbit API to sync: Steps, Distance, Floors Climbed, Active Minutes, Calories Burned (Movement tab); Sleep Duration, Efficiency, Deep/Light/REM/Wake stages with visual stage breakdown bar (Sleep tab); Resting Heart Rate, HRV (RMSSD), SpO2, Respiratory Rate (Heart tab) - **Wellness DB tables** — `wellness_data` (source-keyed per-metric storage for future Garmin/Withings/Google Health support) and `fitbit_tokens` (per-user OAuth tokens) added to SQLite schema - **Settings → Labs section** — new "Experimental" section with Activity Tracking toggle, auto/manual sync mode selector, and Fitbit API credential fields (Client ID, Client Secret, Redirect URI with auto-suggested value + copy button); credential fields shown to admins only in multi-user mode - **Fitbit OAuth server routes** — `GET /api/wellness/fitbit/authorize` (PKCE redirect), `GET /api/wellness/fitbit/callback` (token exchange), `POST /api/wellness/fitbit/sync` (fetch all metrics), `GET /api/wellness/fitbit/data` (read stored data), `DELETE /api/wellness/fitbit/disconnect` - **Wellness goals** — Steps, Active Minutes, and Sleep Duration goal fields in Goals page when Wellness is enabled (both "Your Goals" and "All Fields" tabs) - **Date navigation on Wellness** — browse historical data by day (same UX as Diary); auto-sync on open with 15-minute cooldown when sync mode is set to auto ### Changed - BottomNav: Stats tab replaced by Wellness (`monitor_heart` icon); Stats remains accessible via Sidebar and Settings start-page - BottomNav Stats tab replaced by Wellness (`monitor_heart` icon); Stats remains accessible via Sidebar --- ## [0.12.0-alpha] — 2026-03-28 ### Added - **Water card in Diary** — the `water_drop` topbar button now opens a full-featured sheet: animated SVG bottle (fill, wave, overflow drip effects), amount/goal stats, progress bar, quick-add container grid with custom-amount input, and a deletable per-entry log; works for any diary date (not just today) - **Water card banner** — WaterBanner (waves, drops, bubbles) rendered as a 110px strip at the top of the sheet, matching the visual style of all other page banners; "Water" title overlaid at bottom-left in gradient text, consistent with every other page header - **Water card empty state** — faded water drop icon + "No water logged yet today" message shown when no water has been logged, matching the standalone Water page - **First-run integrations step** — new wizard step between activity and summary; cards for Open Food Facts, USDA FoodData Central, Mealie, and AI Buddy; each individually skippable; AI card auto-hidden if configured via env vars; all saved values written to `user_settings` (included in backup) - **PWA offline fallback** — `public/offline.html` served by the service worker when the server is unreachable during a cold open; branded "Can't reach your server / Try again" page instead of a browser error - **Server-error banners** — Diary and Foods show a subtle inline "Could not reach server — retry" banner when the initial data load fails; Foods suppresses the "no items" empty state during an error ### Changed - Water page removed — standalone `/water` route, nav entry (bottom nav, sidebar), and Settings start-page option all removed; all functionality lives in the Diary water card - `alert()` calls replaced with `showError()` toasts in FoodEditor (camera denied, OFF upload failures) ### Fixed - Water card banner title position matches page-header banner proportions (`padding-bottom: 52px`, title at bottom-left) - Quick-add container buttons centered in the sheet (flex-wrap with `justify-content: center`) --- ## [0.11.0-alpha] — 2026-03-28 ### Added - **Diary multi-delete** — long-press any diary item → action sheet → "Select multiple" to enter select mode; circles appear on each item, header shows count with cancel and trash; batch removes all selected in one write - **Multi-select when adding food** — in pick mode (Diary → Foods), circle button on each row toggles selection independently of the row tap; header confirms selection count with a check button; stacked portion sheet for multiple items when prompt-quantity is on - **OFF upload verification** — after contributing to Open Food Facts, app waits 3 seconds then does a follow-up barcode lookup to confirm the product is live; shows "Confirmed" or "Submitted — may take a few minutes" with a direct link to the product page - **OFF duplicate check** — before uploading to Open Food Facts, checks if the barcode already exists; warns the user that uploading will update an existing community entry, with option to cancel or continue - **Hover tooltips** — every icon-only button across the app now shows a description on hover (native `title` attribute); covers all navigation, action, editor, and utility buttons - **Branded email templates** — invite and password reset emails redesigned with NutriTrace logo, "Trace Every Bite" tagline, mint accent stripe, and CTA button; automatically switches between dark and light layouts based on the recipient's OS preference (`prefers-color-scheme`); copyright footer ### Changed - Thumbnails increased from 40–44 px → 52 px across Foods, Diary, and MealEditor for improved readability - "Share to OFF" button now shows "Submitted!" on success (previously "Contributed!") ### Fixed - `contributeToOFF` was accidentally defined inside `_USDA_NUTRIENT_MAP` object instead of `API`, causing "is not a function" error on every OFF upload attempt - UTC vs. local timezone mismatch in Goals page (today's totals were fetching the wrong diary date for US timezones); same fix applied to Foods yesterday's meals lookup --- ## [0.10.0-alpha] — 2026-03-22 ### Added - **Animated page banners** — optional decorative banners on all main routes (Diary, Foods, Water, Goals, Statistics, Settings); can be disabled in Appearance settings; Foods banner features a typewriter "Today's Menu" animation with floating food silhouettes (fork, apple, carrot, spoon) - **Full-screen ingredient picker in MealEditor** — tabbed overlay (Foods / Meals / Recipes) with search; replaces the previous inline search - **Water goal moved to Goals page** — consolidated alongside nutrition and body stat goals; removed from Settings - **Env-var config locking** — SMTP and other server settings can be set via environment variables in `docker-compose.yml`; locked fields are disabled in the Settings UI - **Sign-out button** — added to sidebar footer in multi-user mode - **Per-meal icons in action sheet** — "Move to meal" sheet shows the correct meal icon for each slot - **Scroll position preservation** — Foods page restores scroll position after adding a food to diary and navigating back - **Backup improvements** — upload & restore from a local ZIP file; mobile-optimized backup table layout ### Changed - Settings page restructured and reworded throughout; all sections have descriptions - Goals page: "Your Goals" tab now categorized (Body Stats / Nutrients / Water) - Diary header layout: date navigation above the title, action icons fixed top-right (same level as hamburger) - Service worker no longer precaches `index.html` — eliminates stale UI after deploys - Camera constraints simplified — no longer requests a specific resolution, fixing narrow viewfinder on portrait phones ### Fixed - Recipe nutrition not preserved when added to diary - Recipe nutrition not scaling correctly when portion size changes in editor - Waistline recipe nutrition not scaled to portion size on import - Diary edit sheet not rescaling nutrition when serving size is changed - FoodsBanner silhouettes collapsing on desktop (percentage height had no resolved parent) - Scroll restoration using incorrect method on some browsers - Broken ingredient images from stale Waistline image paths - Banner scaling distorting on wide/desktop screens - Goals page reactive statements not updating when totals changed - PWA manifest corrected so app installs standalone instead of as browser shortcut - HTTP caching disabled on all API calls — prevents stale data after import or restore --- ## [0.9.0-alpha] — 2026-03-10 ### Added - **Goal templates** — save and apply named sets of nutrition/macro goals - **Settings search** — filter all settings by keyword - **Drag-to-reorder** — meal names, visible nutrients, and body stats order all drag-reorderable in Settings - **Photo URL input** — add a photo to any food, meal, or recipe via a direct URL - **Waistline Android import** — import foods, diary entries, meals, recipes, and images from a Waistline backup - **GitHub Actions CI** — pushes to `main` automatically build and publish to GitHub Container Registry - **Proportional nutrition scaling** — lock icon in FoodEditor scales all nutrients proportionally when serving size changes; real-time preview as you type ### Changed - Food, meal, and recipe list cards redesigned — shows calories per default portion - Trans fat, polyunsaturated fat, and monounsaturated fat set to hidden by default - Sodium visible by default; salt hidden by default (US Nutrition Facts convention) - Settings: drag-to-reorder nutrients and body stats order ### Fixed - Meal/recipe nutrition totals showing 0 kcal in list view - Proportional scaling math and snapshot logic - Category import and add/remove bugs in FoodEditor - Waistline import: base64 images uploaded to server, ingredient references resolved, image URLs corrected --- ## [0.8.0-alpha] — 2026-03-01 ### Added - **SQLite backend** — all data migrated from IndexedDB to a server-side SQLite database via Express API - **Docker support** — single multi-stage container (Svelte build + Express server); `docker compose up -d` - **Optional user management** — JWT authentication, user profiles, admin/user roles, invite system (email or copyable link) - **Password reset** — forgot password flow via email with time-limited token - **Full server-side backup** — creates a ZIP of all data + uploaded images; download, restore, or delete from Settings - **Server-side settings sync** — settings tied to account; persist across devices and survive container rebuilds - **Mealie integration** — browse and import recipes from a self-hosted Mealie instance (proxied server-side) - **AI Buddy** — floating chat panel with multi-provider AI support (OpenAI, Anthropic, etc.) for nutrition questions - **Water tracking** — log water intake by container type; progress shown in diary and statistics - **USDA FoodData Central** — search the USDA database directly from the Foods page - **Open Food Facts contribution** — share locally-created foods back to the OFF database - **Session timeout** — configurable (never / 8h / 1d / 7d / 30d / 90d / 1y); admin-only setting - **Appearance settings** — theme (light/dark/system), accent color, nav style, animation toggle - **Barcode scanner** — scan barcodes to look up foods via Open Food Facts - **Camera photo capture** — take or crop photos for foods and meals directly in the editor - **Statistics page** — charts for calories, macros, weight, and other tracked values over time; bar and line modes - **README** — setup and configuration guide ### Changed - Renamed from Waistline Web to **NutriTrace** — new logo, name, and Docker image - Serving size editable directly in the add-to-diary prompt and diary edit sheet ### Fixed - OFF search switched from deprecated CGI endpoint to working search API - Proxy added for OFF and USDA requests to avoid CORS errors - Nutrition calculation: values correctly treated as per-serving, not per-100g --- ## [0.1.0-alpha] — 2026-02-15 Initial release — Svelte 4 PWA forked from Waistline Web concept. ### Added - Diary with meal groups, daily macro summary, and calorie progress bar - Foods database — add, edit, and delete custom foods - Meals and recipes — group foods into reusable meals; recipes scale by portion - Goals — set calorie and macro targets with progress indicators - Settings — units, date/time format, display preferences, meal name customization - Open Food Facts integration — search and import foods by name or barcode - IndexedDB local storage — all data stored on-device, no account required