NutriTrace
Self-hosted personal nutrition and calorie tracker
Alternative to: myfitnesspal, cronometer, lose it, waistline

v1.2.1-dev03
2026-08-29Third dev pre-release of the 1.2.1 patch cycle. Focused fix run on the OFF local-mirror path: seven community-reported bugs (#185 through #191) that only surface when a self-hoster has OFF_LOCAL_DB set. Default installs (public OFF API path) are unaffected.
Added
- Chinese (Simplified) translation updates. Community contribution via Weblate.
Fixed
- Product names with apostrophes were silently dropped from mirror search results (#185). Under
@duckdb/node-api1.4 the LIST columns come back wrapped asDuckDBListValue(real array under.items, struct fields under.entries), and the old_asArrayfell 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 emptyproduct_nameand got filtered out client-side._asArraynow recursively unwraps.itemsand.entriesbefore 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). 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 LASTas the within-rank tiebreak when the column exists (detected once at init frominformation_schemaso custom parquets without it don’t binder-error), withcode ASCas a deterministic fallback. - The OFF quality-tier filter classed every mirror row as “Unknown” and hid all results under any tier selection (#187).
_toOffProductwasn’t forwarding thecompletenessscore even though the client’s tier bucketer + result ranker both expected it. Now passed through on both the parquet and legacy.duckdbbranches, guarded by a type check so rows without the field passundefined(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). The scanner path uses a separate exact-code endpoint; text search never matched the
codecolumn. 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). The response
countwasrows.length(page size), so the client’shasMoremath could never fire. Now runs a parallelCOUNT(*)with the same predicates and returns the real total (Number()-wrapped so the BigInt serializes). BothORDER BYclauses also picked upcode ASCas 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). 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.searchByNamenow 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). 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_duplicateon the search-results list and killed the render loop. Both search-result each blocks (visibleApiResultsand 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.
Security
- No CVE-driven dependency bumps this cycle.
npm audit --productionreports 0 vulnerabilities; no open Dependabot alerts.