[Unreleased]
[0.2.12] - 2026-07-26
Changed
- Signing in with Google is much quicker. Your data was fetched one piece at a time and then backed up the same way — twelve round trips in a row — and on a new device the app waits on your profile before it can show you anything, so you sat through all of it. Those now happen together, and your profile appears as soon as it arrives rather than after the backup finishes.
- Sharing a product to Open Food Facts works again. Contributions were being turned away before they ever reached Open Food Facts, because the app never said who it was. It now identifies itself the way they ask contributors to.
Added
- "Last launch" in Profile → Experts, data & about — where the app's start-up time actually went, measured on your own device. Added because a slow sign-in could not be explained from a laptop. Durations only; nothing leaves your phone.
Fixed
- "Try instead" no longer suggests something you're allergic to. Alternatives are drawn from the same kind of food as the one being replaced, and they were never checked against your allergies — so with a dairy allergy set, feta came back "Avoid — contains dairy" and offered parmesan, mozzarella and kesong puti underneath it. Every suggestion was guaranteed to carry the same allergen. Suggestions are now filtered the same way verdicts are; without the allergy, the like-for-like swaps are unchanged.
Fixed
- A food you're told to avoid is no longer praised in the same breath. Compliments are gathered as a food is assessed, so a verdict that turned bad afterwards kept them — with a dairy allergy set, feta came back "Avoid" with "a single-ingredient whole food — exactly what to build meals around" printed underneath it. Praise is now withheld from anything rated Avoid.
[0.2.9] - 2026-07-25
Fixed
- A dish is only as judged as its ingredients. If a recipe named an ingredient Susta couldn't look up, there was nothing to weigh it against — so it came back a confident "Buy" and ranked *above* real dishes, because missing data costs nothing: a perfect goal fit with nothing to fault. Such a dish is now kept out of "Suggested for you", and sinks to the bottom of a craving search rather than disappearing. Everyday recipe wording (garlic, bay leaf, black pepper) is unaffected.
- A food is only called protein-dense when its protein is actually known. Density was inferred from the food's category when no figure existed — the same assumption that had heavy cream billed as "the anchor of the plate", and it also feeds the protein-anchored count on your progress page.
[0.2.8] - 2026-07-25
Added
- Your allergies are now in Profile, where you can change them. They were asked once during onboarding and never shown again — so a mistaken tap, or an allergy diagnosed later, had no way back short of wiping the app. Since an allergy forces an "Avoid" on anything containing it, that setting needs to be somewhere you can see and correct. Profile → Allergies, saved as you tap.
[0.2.7] - 2026-07-25
Fixed
- **Allergy warnings now come from what a food *is*, not what it's called. Allergens were matched against the ingredient's name, which failed both ways: gluten never warned at all** — chicken adobo, with half a cup of soy sauce, was a clean "Buy" for a coeliac — and feta, parmesan, mozzarella and kefir were invisible to a dairy allergy, so a dairy-allergic member was *recommended* the Greek salad. Meanwhile milkfish, peanut butter and almond milk were flagged *as* dairy, because "milk" and "butter" appear inside their names. Allergens are now read from the food itself, including the ones a category can't express (butter and ghee, soy sauce brewed with wheat).
- The coach no longer answers medical questions with a food verdict. It matched the food in your sentence and ignored the rest, so *"can I stop my metformin if I eat eggs?"* came back as "eggs are a buy for your goal — evidence grade A", and *"is raw chicken ok if I sear it?"* as a Buy for chicken. Questions about medication, a medical condition, pregnancy or food safety are now declined and pointed at a clinician. Ordinary food questions are unchanged.
- "Buy" is no longer the answer when nothing was actually weighed. The engine started every judgment at "buy" and narrowed from there, so a food it knew nothing about came back a confident green light with no reasons under it. It now marks that verdict as unearned so no screen presents it as a recommendation — while a concern found is still shown, because that *is* evidence.
- Sugar, soda and crisps were being praised as "a single-ingredient whole food — exactly what to build meals around". The check only counted how many ingredients were listed. The coach's answer to "why is soda bad?" opened with that line.
- A grade-A citation could appear under a verdict it had nothing to do with — including butter under the longevity goal, credited to the trial that replaced butter with olive oil.
- Butter, ghee, coconut oil, cream and red meat read as "buy · daily" on the longevity goal, inheriting a low-carb default. They're now "limit" there; olive oil, fish, eggs and the traditional cheeses are unchanged.
- Heavy cream was called protein-dense and named "the anchor of the plate" (2.1 g of protein per 100 g), which also inflated the "% days protein-anchored" figure on your progress page.
- Cook is no longer at the mercy of a bad recipe. A published recipe missing its ingredients could take down the whole Cook screen, and an empty one ranked *first* in "Suggested for you" — no data earned both a perfect goal fit and "you can cook this now".
- The desktop Cook ingredient list showed the original ingredients while the verdict beside it had been worked out on the swapped ones — so you'd cook a different dish from the one that was graded.
[0.2.6] - 2026-07-25
Fixed
- A "Buy" now has to be earned by evidence. A scanned product could be called a great fit on a record with no ingredient list at all — and the card said so, printing "no ingredient list on record" directly beneath "Great fit". Added sugar, seed oils and additives are named in the ingredients, so without them there is nothing to clear the product on. A would-be Buy with no ingredients (or no nutrition figures) is now withheld in favour of reading the label. Limit and Avoid still show — a concern found is still a concern.
- A guessed sugar figure no longer outranks the real checks. Open Food Facts marks values it inferred rather than read off the pack; for added sugars that is the norm — 85 of 96 yoghurts sampled carried a guess. Treating a guessed "0" as fact silenced every sugar check, so a sweetened yoghurt came back as a Buy. Guessed values now give way to the ingredient list and the total-sugar fail-safe, flagged as an estimate. A genuinely declared 0 is still trusted, so plain yoghurt and fruit keep their natural sugar uncounted.
- Reading a label and snapping a meal now work on Android. Every photo came back "no nutrition panel found" — not because the photo was poor, but because the request never left the phone. The Firebase AI SDK combines abort signals with
AbortSignal.any, a browser API React Native's engine doesn't provide, so the call failed instantly. (The SDK publishes no React Native build, so the browser one gets bundled.) Filled the gap at startup; AI features work on device for the first time. - A correction made on one device now reaches the others. Saved products merged local-first, so re-reading a label on your phone pushed the fixed numbers up but a second device's older copy overrode them on every sync — and could push the stale copy back over the good one. Saved products now merge per barcode, preferring whichever was captured by the newer reader.
[0.2.5] - 2026-07-25
Fixed
- A product you taught Susta can be re-read, and an out-of-date one won't be trusted. Taught products win over every live lookup, so one captured before a label-reading fix kept replaying the old, wrong numbers — and because they back up to your account, neither a reinstall nor a private window cleared it. That is why one pack could come back "Buy" by barcode while a photo of its own label said "Avoid". Saved products now record which version of the reader captured them; an out-of-date one that would read "Buy" is withheld rather than shown, and any saved product can be re-read from its label to refresh the numbers everywhere.
- A scan will no longer say "Buy" just because the database knows nothing. A product can be listed with a name and no nutrition figures at all. With nothing to weigh, the engine found no concerns and returned a confident "Buy · Great fit" with no reasons under it — a green light inferred from missing data, and the reason a barcode scan and a photo of the same pack could disagree outright. Susta now says it can't judge and offers to read the label instead. A concern spotted in an ingredient list is real evidence and still shows.
- No more impossible sugar numbers. Added sugar is part of total sugar, so it can never be more — but Open Food Facts is crowd-sourced and some records break that (Coca-Cola declares 60.9 g added against 35 g total per serving, and the card repeated it). Every path now caps added sugar at the total, so a scan can't print a figure that couldn't exist.
- More packs get judged per serving instead of per 100 g. Contributors often leave the unit off the serving size ("30"), which kept those products on the lenient per-100 g path. A bare number is now read as grams when it plausibly describes one serving — and still ignored when it looks like a whole package.
- A barcode scan and a photo of the same pack no longer disagree. When Open Food Facts held only a per-100 g panel, those numbers were judged against per-*serving* limits — and the engine has no notion of basis, so anything with a serving bigger than 100 g/ml was let off lightly. A drink with 19 g of sugar per 240 ml reads 7.9 g per 100 ml: under the limit, and under the high-sugar fail-safe too, so the sugar was dropped entirely and it came back "Buy" while its own label read "Avoid". Per-100 g panels are now scaled to the serving on the pack (as the USDA path always did), and the card tells you when it scaled. Container-sized or unreadable serving sizes are left alone rather than scaled into a wrong answer.
[0.2.4] - 2026-07-25
Added
- Wider net for ultra-processed ingredients. The engine now recognises more seed/"vegetable" oils (peanut, vegetable fat, high-oleic sunflower), industrial additives (maltodextrin, yeast extract, mono-/diglycerides, common colours & preservatives), and refined grains (semolina, degermed, farina) — these have no numeric backstop, so a synonym the list didn't hold couldn't be flagged at all.
Fixed
- Barcode lookups no longer read "Buy" on a sugary product. The label reader was fixed to stop dropping sugar, but the barcode path (Open Food Facts / USDA) still only counted sugar when a sweetener was named — so a database entry with total sugars but no declared "added sugars" and no recognised sweetener term dropped the sugar and could return "Buy." The same product could say Avoid by label and Buy by barcode. Both mappers now also count the sugar once it clears a fail-safe threshold too high to be incidental natural sugar; a declared value (including a declared 0) still wins, so plain dairy and fruit stay honest.
- A label whose ingredient list didn't read is now honest about it. Seed-oil and additive checks depend entirely on the ingredient list; when the photo's nutrition table read but the ingredients didn't, those checks were silently skipped. The card now says so ("verdict uses the nutrition numbers only"), matching what the barcode path already did.
- A high-sugar label no longer reads "Buy." The verdict only counted a product's sugar if the photographed ingredient list happened to contain the word *sugar/syrup/honey* — so a label naming its sweetener differently (or an OCR pass that varied run-to-run) dropped the printed sugar entirely, and a 19 g-sugar product could come back "Buy." The same scan could say Avoid one moment and Buy the next. Now the printed number is authoritative: an explicit "Added Sugars" line is trusted when present (so naturally-sweet foods stay honest), otherwise Total Sugars counts.
- A clear label photo no longer fails with "no nutrition panel found." The reader hard-required a product *name*, but the app asks you to photograph the BACK of the pack (nutrition facts + ingredients) — which usually has no name on it. So a perfectly-read panel was thrown away. The name is now optional (falls back to the brand, then "Scanned product"); the verdict has always used the nutrition + ingredients, not the name. Also fixes the on-again/off-again reads.
[0.2.3] - 2026-07-25
Fixed
- Reliable label & meal photo reading. A photo snapped in the first moments after opening the app could fail (the AI's App Check token wasn't ready yet), so a clear label sometimes came back "unreadable." The reader now retries transient failures with a short backoff, so it self-heals — while a genuinely unreadable photo still returns cleanly.
[0.2.2] - 2026-07-25
Changed
- Daily meal plan now rotates and spans cuisines. Meals change day-to-day (no more the same breakfast/lunch/dinner repeating for days), and the plan leads with your cuisine while blending in international variety from the full recipe database — still personalized by your goal, cuisine, and allergies.
Fixed
- Fixed the daily plan repeating the same meals. The date-seed collided modulo a small recipe pool; a monotonic day rotation now advances through the recipes so consecutive days differ.
[0.2.1] - 2026-07-25
Added
- More global expert voices — added three web-verified non-US researchers to the roster (now 31): Carlos A. Monteiro (Brazil — coined "ultra-processed foods" + the NOVA classification), Stephen J. Simpson (Australia — the protein-leverage hypothesis), and Tim Noakes (South Africa — LCHF, with an honest note that his nutrition advocacy is contested). Extends coverage into South America, Australia, and Africa.
Fixed
- The Scan / food-lookup screen now has a real desktop layout. On wide web it previously rendered the phone column centred with large empty gutters; it's now a two-pane like Cook/Coach — scan + search inputs on the left, the verdict on the right. Phone/narrow web unchanged.
- Coach shows its starter prompts on phone, not just desktop. A fresh phone chat was showing only the greeting; the "Try asking…" starter questions now render on both layouts.
- Desktop scroll sidebars no longer clip card edges. The fixed-width left panes / evidence rail (Cook, Learn, Scan, Coach, admin content + feedback) reserve room for the scrollbar, so it stops overlapping the right border/rounded-corner of the cards.
[0.2.0] - 2026-07-25
Added
- A real desktop web experience — its own surface, not a widened phone. On wide web (≥ 900px) every screen now has a purpose-built desktop layout, while native and narrow web keep the phone UI byte-for-byte (
webWide = Platform.OS === "web" && width >= 900). A top-nav shell (Overview · Plan · Cook · Coach · Progress · a ⌘K food-search field · Log a meal · avatar) replaces the bottom tabs; content widens to ~1180px centred. Per screen: an Overview landing; Plan as a planning canvas with today's meals as the hero and a condensed lifestyle strip; Cook as a master–detail recipe browser; Coach as a readable centred chat beside a live evidence rail; Progress as a 2-column charts dashboard with a report rail; Profile as a two-pane settings screen; Learn as a lessons library; Experts as a roster grid; and the admin content/feedback review queues. - "Log a meal" is a first-class action on desktop — in the top nav, so logging what you ate isn't buried in the Plan/Cook cards; routes to search-or-snap → log.
- The coach offers follow-up prompts after every answer — contextual "Try a follow-up" chips (a food's *why*, a swap, protein ideas; otherwise the starter questions not yet asked) so the conversation never dead-ends.
- Print / PDF the progress report as a clean document — the report's Print/PDF button renders the report on its own (Susta header + the data) into a hidden iframe and prints that, instead of screenshotting the whole app; a print stylesheet also formats full-page printing (chrome hidden, columns reflowed).
- Sign out — including a full sign-out for shared computers. Profile gains an "Account & sign out" section; on web a distinct "Sign out of this browser" ends the session *and* clears this device's local data (so the next person can't see your record), while preserving your cloud backup.
- Import weight, height & steps from Android Health Connect — opt-in, read-only, deduped, in Profile → Health & fitness; feeds your existing record. (iOS HealthKit + write-back to follow.)
- Attach a recent check-up during onboarding (web) — an optional lab-PDF/photo upload step; PII-safe (captured on-device, never uploaded — automatic reading of the numbers is still to come).
- Content grows as data, reviewed in-app — no rebuild — the recipe/lesson library was baked into the app bundle, so expanding it meant a code change + redeploy. Now the baked-in core still ships (and works fully offline), but admin-approved additions live in a single public Firestore doc the app fetches over plain REST (no Firebase SDK, so the offline-first launch path is untouched), caches, and merges at startup. Drafts land in a
content_proposalsqueue reviewed in You → Content review (admin only); Approve publishes to everyone on their next launch, Reject never does. Core always wins on id collisions. Seedocs/admin-guide.md§10. - Admin gating moved to a custom claim (
admin: true) — the admin email was hardcoded in the client bundle (admin.ts, and the oldadmin.html), shipping the operator's identity to every user as reconnaissance/phishing surface. Replaced with a server-set custom claim, keyed identically infirestore.rulesand the app — no identity in any client-reachable code, and future admins need zero code changes. Grant viascripts/set-admin-claim.mjs.
Removed
- The GitHub-PR admin portal and monthly remote routine —
susta.app/admin(site/admin.html), theadminListProposals/adminApproveProposal/adminRejectProposal/submitContentProposalCloud Functions, theGITHUB_ADMIN_TOKENsecret path, and theRemoteTriggermonthly job were removed in favour of the in-app content flow above. The remote routine was fragile and opaque (it failed silently in an unobservable sandbox), and the portal leaked the admin email client-side. The in-app path is reliable, observable, and identity-free. - Recipe database expanded 21 → 36, lessons expanded 10 → 18 — 21 recipes was thin for 5 cuisines × 6 goals × open-ended cravings (only 3 western dishes meant Western users routinely fell back to Filipino via the "never leave you with nothing" pool widening; only 2 recipes were breakfast-eligible at all). Added 15 new recipes targeting the thinnest cuisines (Latin +3→4, Asian +4→7, Western +4→7, Filipino +3→12, Mediterranean +1→6) and widened
BREAKFAST_IDSfrom 2 to 8 dishes. Added 8 new cited lessons (fiber, sodium, processed meat, exercise & insulin sensitivity, meal frequency myths, fermented foods, caffeine, artificial sweeteners), same evidence-graded, honest-about-uncertainty format as the original 10. - Recipes now have actual cooking steps — every dish showed the ingredients and a flavor-text description, but never how to cook it; the
Recipeschema had no field for steps at all. Wrote real step-by-step instructions for all 21 recipes and added a "How to cook it" expandable section (collapsed by default, one tap away) to both Cook and Plan's recipe cards. - "Today's plan" now shows up in Cook — Plan and Cook were two fully disconnected features (different ranking algorithms over the same recipe pool), so a dish planned for dinner had no guaranteed relationship to what Cook suggested — "none of the ones in Plan is shown in Cook." Plan now persists today's picked meals; Cook shows them in their own "Today's plan" section, pinned above "Suggested for you," in the same breakfast/lunch/dinner order. Found via manual testing.
Changed
- Plan & Coach desktop redesigned to v2 — Plan leads with today's meals as the hero and condenses the lifestyle pillars into an expand-on-tap strip beneath (was three equal columns); Coach is a readable centred chat with a categorized cold-start and an evidence rail that's never empty (shows the answer's verdict, a refusal note, or a "how the coach stays honest" explainer).
- Cook desktop scrolls as a proper master–detail — the browse list and the recipe pane scroll independently within the viewport, so a long pantry no longer stretches the page with dead space beside the recipe.
- Shopping list now groups by dish — items from Plan's daily gaps and Cook's per-recipe "Add to shopping list" landed in one flat, unordered pile with no indication of which dish needed what. The underlying data didn't even retain that information (
generateDayPlan's missing-ingredients union deduped across all meals before it ever reached the list). Now every shopping item carries its dish name(s), re-adding the same ingredient for a second dish merges the attribution instead of silently dropping it, and the list renders as sections per dish (manually-added items group under "Added by you"). Found via manual testing. - Onboarding: Google sign-in moved above the fold, restyled to Google's recognizable button — it previously sat below three marketing cards and a legal disclaimer, requiring a scroll to even see it, and was styled in the app's own green (identical to the "Set up on this device" button next to it) rather than Google's standard white/bordered look people already pattern-match from other apps. Now it's the first actionable thing on the welcome screen, in Google's neutral button style (white, bordered, brand-blue "G"), with a "FASTEST" badge instead of relying on caption text nobody reads. Found via family UAT — a tester didn't notice sign-in was an option.
- "No barcode? Search a whole food" dead-ends on cooked dishes — typing a dish/recipe name (e.g. "sinigang") returned a plain "no match... scan the barcode" line — unhelpful, since a home-cooked dish has no barcode at all, and it never pointed at "Snap a meal" (the actually-correct feature, one section above on the same screen). Now the no-match state is a tappable card that opens "Snap a meal" directly. Found via family UAT.
Fixed
- Desktop top nav clipped the avatar at narrow widths — a fixed 240px search plus wide gaps overflowed around 900–1000px and sheared the avatar off; the search now flexes (shrinks to 130px) so everything stays visible from 900px up and centred on wide screens.
- Fixed-width desktop scroll sidebars grew and squeezed the main pane — react-native-web gives
ScrollViewa defaultflex-grow, so the Coach evidence rail and the Learn/admin master lists ballooned past their set width (the Coach chat rendered at 379px while the 360px rail took 737px); pinned them so the primary pane gets the remaining space. - Progress dashboard cards had ragged heights — paired chart cards in a 2-col row now share one height.
- Print produced a screenshot of the app, not the data — see the Print/PDF and print-stylesheet entries above; the report now prints as a clean standalone document.
- Native Google sign-in hid its real error — failures showed only generic text; the app now surfaces the actual error code. (Root-caused: the failure was specific to the EAS-signed build's OAuth client, not App Check or the device — full runbook in
docs/mobile-build.md.) - "Today's why" opened the lesson list, not the lesson — Home's daily lesson card knew exactly which lesson it was teasing but never passed that id anywhere;
onLearnwas a no-argument callback all the way through Home → App → the Learn screen, so tapping it always landed on the generic list. Threaded alessonIdthroughtoday.ts→Home.tsx→App.tsx→Learn.tsx, which already had a working single-lesson view internally (used for verdict-card lesson links) — it just had no way to be told which one to open on entry. Now it opens straight to the lesson. - App icon / logo transparency bug — every leaf+checkmark asset (app icon, Android adaptive icon, splash screen, marketing logo, favicons, PWA icons, Apple touch icon, Play Store listing icon) had a solid opaque-white patch baked in where the checkmark cuts through the leaf, instead of transparent/background-colored. Root cause: the source art was flood-filled for transparency from the canvas edges only, so white trapped *inside* the enclosed checkmark cutout was never cleared. Regenerated all 15 affected files from clean masters, correctly sized and background-treated per context.
- Crave → Cook missed noodle dishes — searching a craving like "a warm noodle soup" matched only on the generic "warm" mood tag (soup/stew/cozy), so results were warm-but-noodle-less (adobo, a mung-bean stew, poached fish). Added a
noodlecraving-vocabulary bucket (app/src/cook/crave.ts) and a real noodle dish, Pancit Bihon, to the recipe database (rules-engine/data/recipes.json) — with the same goal-adapted swap pattern as other dishes (rice noodles → zucchini "zoodles" on a blood-sugar goal, kept as-is on looser goals). Found via UAT. - Android status-bar overlap — screens used React Native's
SafeAreaView(iOS-only), so content drew under the Android status bar. Switched all screens toreact-native-safe-area-context'sSafeAreaView(+ a rootSafeAreaProvider) so the top inset is respected on Android. Found via on-device testing.
Added
- Teach a product with no barcode at all — "Snap a meal" is for cooked dishes and the barcode-miss teach flow only ever triggers after a lookup fails, so a packaged item that simply has no barcode printed on it (common for local/home-based brands) had no path in at all. Added a direct "Package has no barcode at all? Photograph the label instead" entry point next to the barcode field — same label-photo → AI extraction flow, keyed by a locally-generated id instead of a real barcode. Skips the "Add to Open Food Facts" offer for these (OFF needs a genuine barcode/GTIN; never submits a fabricated one). Found via family UAT.
- Google sign-in on native (iOS/Android) — the native Google account picker (
@react-native-google-signin) returns an ID token minted for the Firebase web client, which Firebase accepts viasignInWithCredential; web keeps its popup/redirect flow. Links to an existing anonymous/local account so backed-up data stays yours. Public OAuth client IDs live inapp.json. - Milestone share cards — streaks and earned achievements are now shareable as branded images (reuses the verdict-card image engine): a "Share" button on the streak banner (from a 3-day streak) and tappable earned badges on Progress. Web builds the PNG from canvas; native captures a rendered card. The sharer re-engages *and* pulls new people in — a growth loop.
- Daily "Today" home surface — a fresh, prioritized reason to open each day, right on Home: an outstanding check-in nudge, an expiring-pantry "cook it tonight" prompt, and a rotating "today's why" lesson (deterministic per day). Completes the daily habit loop. Pure logic in
src/today.ts(unit-tested). - Local reminders (native push) — opt-in nudges that keep members coming back, scheduled on-device so no server ever sees the member's data (local-first preserved): a daily check-in nudge (morning / midday / evening), a Sunday weekly review, and an expiring-pantry "cook it before it turns" prompt. Managed in Profile → Reminders; a few, never spam, fully user-controlled. Pure schedule logic in
src/reminders.ts(unit-tested); web shows it as phone-only. (Needs a native build to fire.) - Streaks & achievements — a daily habit loop to keep members coming back: a streak of "days you showed up" (a chip on Home, a banner on Progress) and a grid of honest milestone badges (consistency + real healthy behaviour, e.g. protein-first days). Intrinsic and supportive by design — never punishing, and deliberately no public leaderboard (ranking health behaviour invites gaming and shame). Pure, unit-tested logic in
src/streaks.ts. - Verdict-card image sharing on native (iOS/Android) — the share action now produces the same branded PNG as the web card, rendered off-screen with
react-native-view-shotand shared viaexpo-sharing; falls back to a text share. Part of the native-app track.
[0.1.3] - 2026-07-17
Changed
- Marketing site — landing-page refresh surfacing shipped features (no app behavior changed):
- "Scan, verdict & share" — the scan feature card now calls out turning any verdict into a clean, branded card to share to chats or socials.
- "Why it's different" rounded to a clean 3×3 — added two shipped differentiators: Runs on your device (verdicts compute on-device and work offline; only a barcode number ever leaves) and Never plays doctor (general wellness, never medication advice, defers to your clinician).
- "Named experts, in the open" — a card surfacing the transparent, globally-sourced experts roster (Nobel-tier scientists to popular educators, India to Sweden).
- New "Who is behind Susta's advice?" FAQ entry (structured data) for credibility + SEO.
[0.1.2] - 2026-07-17
Fixed
- Any-orientation barcode scanning — the iOS/Safari (ZXing) scan path now runs a parallel decode on a 90°-rotated frame, so a barcode reads whether its bars are vertical, horizontal, or upside down. Previously that path only decoded upright codes, so a sideways barcode wouldn't scan. (Android's native detector already handled rotation.)
[0.1.1] - 2026-07-17
Field fixes from in-store testing at NTUC FairPrice (Singapore).
Fixed
- Barcode capture: the camera now opens a high-resolution rear stream (1920×1080) and switches to continuous autofocus on open, so codes decode at a normal holding distance — no more cancel-and-reopen or pushing/pulling the phone to find focus. The ZXing fallback runs a
TRY_HARDERpass for small/blurry codes, with faster polling. - Open Food Facts contribution no longer fails silently — it surfaces the exact reason (sign-in needed, not configured, OFF declined, or a real network error).
Changed
- The Open Food Facts share action is now a prominent "Add to Open Food Facts" button (was an easy-to-miss pill), so label-teaches actually turn missing items into coverage.
[0.1.0] - 2026-07-17
Launch-ready private beta — the evidence-honest, goal-conditional food & metabolic-health
companion. Production-deployed at https://susta.app; not yet declared GA.
Added
- Verdict-honesty engine — goal-conditional Buy / Limit / Avoid with evidence grading that grades the strength of the science, never the food.
- Learn layer (teach-as-you-judge) and meal-photo logging — photograph a cooked plate and the engine estimates then judges it.
- Expiry-aware pantry — a "Your pantry" panel in Cook that tracks what you have with category-based use-by estimates; the recommender boosts dishes that use soon-to-expire items; "Suggested for you" is free (only free-text craving search sits behind Plus).
- SF Pro design system — Tailwind v4 marketing site + NativeWind app, cool/vibrant "Apple-HIG" look, portrait PWA.
- Shareable branded verdict cards — a verdict rendered as a postable image (pure canvas, grade qualified as evidence strength so it can't read as a health score).
- Experts roster with transparent credentials/standing, plus 8 verified non-US voices (India, Japan, Singapore, UK, Germany, Sweden, Austria) and a "suggest an expert" flow.
- SEO growth engine — technical foundation (robots/sitemap/JSON-LD) + 220 programmatic
/food/<slug>verdict pages. - Play Store TWA scaffolding — Digital Asset Links served, Bubblewrap config, and a build/publish runbook.
Fixed
- Sync data-loss: append-only logs now merge on pull instead of overwriting; deletes are tombstoned so corrections stick; the profile carries an edit timestamp so settings (e.g. units) survive a refresh; the
pantryMetasync key is allowed by the rules sosyncNowcompletes andlastSyncAtadvances. - Verdict grade decoupled from direction — no more green "Grade A" on an Avoid; the grade now follows the returned verdict's evidence direction.
Security
- USDA FoodData Central API key moved server-side (
fdcProxyCloud Function; key in Secret Manager, never in the client bundle). - App Check (reCAPTCHA Enterprise) enforced on callables — kept off the Auth path so iOS sign-in isn't broken; the reCAPTCHA badge is hidden.
- Firestore Security Rules deny-by-default with per-key envelope validation.
