Skip to content

fix(ios): make the scan-to-meal loop work and log what was confirmed - #4

Draft
SamGu-NRX wants to merge 17 commits into
mainfrom
feat/minimal-product-20261007
Draft

SamGu-NRX wants to merge 17 commits into
mainfrom
feat/minimal-product-20261007

Conversation

@SamGu-NRX

@SamGu-NRX SamGu-NRX commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

What changed

Cooking from a real scan reaches the meal log again

Since d48f385 (March 16), "Start cooking" in the recipe preview saved the recipe for the live assistant and sent the user back to Home. Nothing presented CookingGuideView. Its celebration screen is the only path that logs the meal and takes ingredients out of the Kitchen. Without the Gemini Live backend, a cooked meal couldn't be recorded at all.

"Start cooking" now opens the offline step-by-step guide after the preview sheet finishes dismissing. Finishing the guide returns Home; closing it early leaves the user on the results. "Cook hands-free with Le Chef" keeps the live-assistant route as a secondary action. The sheet-to-cover sequencing lives in CookingLaunchFlow.

Logged meals show up in today's numbers

CookingHistory left cookedAt nil, and GRDB writes nil as SQL NULL, which bypasses the column's CURRENT_TIMESTAMP default. Those meals counted toward the streak but dropped out of today's nutrition and weekly history. The model now sets the time when the meal is created.

recordCooking also returned lastInsertedRowID after inserting the day's streak row. On the first meal of a day it returned the streak's ID, and Apple Health sync used that as the meal identifier. It now returns the history row ID.

A real photo resolves to ingredients that recipes use

  • Duplicate photo: the capture view appends shots itself, and ScanView appended the last one again. Three photos were scanned as [2nd, 3rd, 3rd].
  • Catalog before curated foods: camera labels went to the USDA catalog first, so "bell_pepper" matched a catalog alias. No bundled recipe uses that ingredient, while OCR of the same word found the curated one. Labels now resolve as user correction, then curated ingredient, then catalog (IngredientIdentityResolution).
  • Wrong foods: the lexicon mapped 31 terms to a different food, such as oat milk to dairy milk, mustard to soy sauce and oranges to lemon. Those mappings are removed. Unsupported phrases are now masked before OCR falls back to single words.

The Kitchen matches what the user confirmed

  • Practice data: demo, tutorial and replay reviews wrote their fixture ingredients into the real Kitchen. Cooking from demo or tutorial results also logged real meals. IngredientReviewView(savesToInventory:) and RecipeResultsView(logsMeals:) are now explicit. Practice cooking says it isn't saved.
  • Revisited reviews: going back from results and correcting an item used to leave the original in the Kitchen. Intake now reconciles the review session in one transaction. It adds newly confirmed foods, empties untouched lots the user un-confirmed, and restores them if confirmed again. It never touches a lot that cooking has used. A failed save shows an alert with retry or continue-without-saving.
  • Matching scope: recipe matching after a real scan now uses the confirmed scan plus the active Kitchen, the same basis Home uses.
  • Stale tabs: Kitchen and Home observe the inventory tables directly, so they refresh after a scan or a cooked meal. That includes MealLogService's in-transaction consumption, which never posted the change notification.

Numbers follow what was confirmed

  • Estimated amounts: photo intake guesses one typical unit per detection, so a carton of eggs becomes 50 g. Those lots are now flagged (migration v16, backfilled from existing photo-intake events), and the Kitchen shows "≈ 50g est." Recipe nutrition never reads inventory grams, so a guess can't reach macros.
  • Required ingredients only: reports counted optional ingredients while the preview, consumption and Apple Health did not. Fried rice showed 286.9 kcal before cooking and 338.5 kcal in the journal. All reports now count required ingredients only. The journal also stops reading every stored serving count as the recipe's full count.
  • Portion size: the meal-photo Small/Large control scaled the displayed macros only. It is now stored (v17) and applied to reports, consumption and Apple Health.
  • Swaps: substitutions made while cooking (v18) now change reports, consumption, Apple Health and the celebration's numbers. Swaps chosen in the preview carry into the guide, the guide shows the substitute's adjusted amount, and only swaps of required ingredients are saved.

The Kitchen stores the amount the review showed

Onboarding's kitchen review showed name-based estimates such as egg ~50 g. Scan intake then stored a separate catalog-unit estimate with a 30 g floor, so the same egg was saved as 120 g. Intake now uses the review's estimator and label, so a corrected detection is estimated from the chosen ingredient, and the unused catalog-unit estimator is gone.

Why

These are the defects that stopped photo → confirmed ingredients → recipe → cooking → logged meal from working, or made its numbers disagree with what the user confirmed. The bar is the stage-4 direction in planning/stage_4-gemini_live/user-direction-and-feature-intent.md (lines 52-60): the model proposes, the user confirms, and only confirmed amounts count as exact.

UI changes

No captures. The app hasn't run on a device or simulator for this change, because heavy local builds are gated on shared-Mac disk admission. Visible changes:

  • In the recipe preview, "Start cooking" is now the primary button, with a text button for Le Chef below it.
  • Kitchen rows show "≈ … est." for photo-estimated amounts.
  • Review shows a save-failure alert.
  • Practice cooking shows a note and hides rating and photo.
  • Guide and preview rows show the substitute's adjusted amount.

Verification

  • Simulator test run: the app builds and all 149 XCTests pass (0 failed, 0 skipped, 32 suites). That's Xcode 27.0, iPhone 17 simulator on iOS 26.5, derived data built from scratch, run from the head of feat(ios): let meal-photo confidence set how much the user confirms #5, which includes this branch. It covers MigrationUpgradeTests, which upgrades a populated v15 database (old NULL-timestamp meals; photo-intake, grocery and manual lots; consumption events) through v18. Every meal, lot and event survives, and only the photo-intake lots get the estimate flag. The UIKit-dependent suites and the source-string tutorial checks pass too.
  • Regression proof: the timestamp, history-ID, required-only nutrition and lexicon tests fail on main and pass here.
  • Typecheck: the whole app module typechecks with plain swiftc against the iOS 27 SDK.
  • Reviews: two independent read-only reviews ran. Their findings are fixed in this branch: optional-swap persistence, substitute display amounts, reconfirm after retirement, and OCR plural masking.
  • CI: red, for a reason that predates this branch. main doesn't build on the runner's Xcode 16.4 because of Sendable errors in notification code; fix(ios): read notification permission without sending UNNotificationSettings #6 fixes that. Tests also can't launch an iOS 26 target on that runner's iOS 18.5 simulator, which fix(ci): override simulator deployment target on Actions #2 addresses.
  • Loop in the running app: a fresh install on the iPhone 17 simulator (iOS 26.5), built from the head of feat(ios): let meal-photo confidence set how much the user confirms #5:
    • Onboarding: went through onboarding, including the fridge-photo step with a bundled fixture photo. Its 12 confirmed items appeared in Kitchen, each marked "≈ … est.".
    • Scan: Scan Ingredients from the camera button, a library photo, a correction ("Choose another" → Avocado), then Find Recipes. Results said "Based on 14 ingredients" (the scan plus the Kitchen).
    • Cook: best match → Start cooking opened the offline guide → all 8 steps → "I Made This!" → rated → Done.
    • Results: Progress showed 348 of 2,000 cal and the meal under Recent Meals. Kitchen refreshed without a restart: chicken breast ≈120 → ≈7 g, onion ≈120 → ≈70 g, carrot ≈120 → ≈100 g (one of three servings), with the scanned avocado and cheese added.
    • Bug found: the onboarding review/stored amount mismatch above came out of this run.
  • Not verified: recognition quality. On a busy taco-fridge photo the scan found only two weak text matches and missed the avocados, tomatoes and limes. A physical device wasn't tested either.

Risk

  • Migrations: v16-v18 add a column with a backfill, a column with a default, and a new table. They can't be undone after release.
  • Old NULL timestamps: meals saved under the bug keep NULL cooked_at. Their real cooking time is unrecoverable, so they stay out of dated reports.

Checklist

  • The scope is focused; related changes belong together
  • I explained what changed and why
  • N/A before/after images: the app couldn't run locally; the changes are listed under UI changes
  • N/A video: same reason

SamGu-NRX and others added 2 commits October 7, 2026 07:27
- Open the offline cooking guide from "Start cooking", so finishing a
  recipe logs the meal and uses Kitchen inventory without the live
  backend. Cooking with Le Chef stays as a secondary option.
- Save meals with a timestamp and return the history row ID, so they
  appear in today's nutrition and Apple Health sync names the right meal.
- Scan each camera photo once, resolve camera labels to curated recipe
  ingredients before the USDA catalog, and stop OCR from mapping foods
  to different ones (oat milk to milk, mustard to soy sauce).
- Keep demo and tutorial reviews and cooking out of the real Kitchen and
  meal history.
- Reconcile a revisited scan review in one transaction, match recipes
  against the confirmed scan plus the Kitchen, and refresh Kitchen and
  Home after any inventory change.
- Mark photo-estimated amounts in the Kitchen (migration v16), and log
  the confirmed portion (v17) and cooking swaps (v18) wherever nutrition
  or consumption is computed. Reports count required ingredients only,
  matching the recipe preview.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Onboarding's kitchen review showed name-based estimates (egg ~50 g), but
scan intake stored a separate catalog-unit estimate with a 30 g floor
(egg 120 g), so the Kitchen disagreed with what the user had just
confirmed. Intake now uses the review's estimator and label, so a
corrected detection is estimated from the chosen ingredient. The unused
catalog-unit estimator is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SamGu-NRX and others added 14 commits October 7, 2026 18:21
On pushed screens such as Review Ingredients, Skip tour sat 88 pt from the top, inside the
navigation bar. The bar takes every tap in that band, so Skip was visible but dead and the user
had to page through all seven cards. The top placements now start below the top safe area,
which includes the navigation bar on pushed screens and only the status bar on Home, so Home
keeps its 88 pt offset. The placement logic moves into SpotlightSkipLayout so it can be tested.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
141 of the 166 bundled recipes start their instructions with "Source: https://www.bbcgoodfood.com/...",
and all 166 number their steps. The guide used each line as a page, so step 1 was the URL and
every later page printed "1." under its own "02 of 7".

CookingGuideSteps now builds the pages: a "Source:" line becomes an attribution, and a leading
"1. " or "1) " is removed (numbers like "1.5 cups" stay). The last step shows
"Recipe from bbcgoodfood.com", linking to the original page, so the source stays visible.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The preview's green check meant "required", not "you have it", so an ingredient the results
card listed as missing (Cucumber in the walk) appeared checked. The icon now follows the
search's missing IDs: a check for a required ingredient the user has, the results card's
exclamation glyph and a "Missing" line for one they don't. Optional and substituted rows get a
dashed circle, which makes no claim about the kitchen. VoiceOver reads the same state.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hero is the first exact match, else the first near match, and the grid and "Almost there"
lists still contained it, so the same recipe appeared twice. RecommendationSections now owns
the hero choice (bestMatch) and the lists under it (belowBestMatch); the header counts still
use the full sections. When the hero is the only result, the divider and empty list are not
drawn, and the tutorial replay skips its "Browse Alternatives" card instead of waiting for a
list that never appears.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The chip row looped over fridge, pantry and freezer, while the On Hand list also showed an
"Other" section for items with no known storage location, so that section had no chip.
KitchenLocationOrder now gives one order to both, and the chip row shows a chip for every
location with items, "Other" last.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
4138a04 moved the corner Skip button below the top safe area, but that inset was never
measured on a pushed screen, so its tests only proved the arithmetic. Skip now sits in the
card's control row as a text button in the overlay's secondary white, so no navigation bar can
cover it on any screen. It keeps the leading edge as Back appears, Back sits beside Next, and
the step dots move to their own row above. At large text sizes Skip drops below the row. The
label uses a scaling text style and a 44 pt minimum hit height.

The corner button, its placement and collision code, and SpotlightSkipLayout with its tests
are removed. SpotlightTourProgress holds the remaining rule: Skip on every step but the last,
where "Let's go" ends the tour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… trait

The chips, including the new Other chip, were about 28 pt tall to the touch. The capsule keeps
its size and the tappable area grows to 44 pt around it, which makes the chip row 16 pt taller.
VoiceOver now hears which chip is selected through the isSelected trait. There is no unit test:
a minimum frame and an accessibility trait have no pure logic to test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Recipe results, recipe preview, ingredient review and demo mode scrolled to each tutorial
highlight with the spotlightMove spring even with Reduce Motion on. All four now call
AppMotion.spotlightScroll(reduceMotion:), which returns no animation in that case, matching
what Home already did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The scroll callbacks on ingredient review, demo mode, recipe preview and recipe results read
reduceMotion through the view, so the view's state held the spotlight coordinator, which held
the callback, which held the view. SpotlightCoordinator.scrollToAnchors(with:reduceMotion:after:)
now builds the callback capturing only the scroll proxy, the Bool and the delay. Each screen
sets it again when Reduce Motion changes and clears it with stopScrolling() on disappear.

No unit test: the change is which values a closure captures, with no pure logic to assert.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With no exact matches, the first near match becomes the hero and, since 9cc7418, leaves
"Almost there", the only place that named its missing ingredients. The hero now shows a
"Missing" label and the same muted chips. RecipeMissingIngredientChips holds the chip style
for both places.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Selecting Other and then cooking away its last item removed the Other chip but left the
filter on Other, hiding every remaining item with no chip to clear it. The view kept its own
copy of the selection next to the view model's; now the view model's selectedLocation is the
only one, and after each refresh or removal it falls back to All when its location has no
items. The test drives the real path: an in-memory database, a depleting write, and the
inventory observer's refresh.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
f206b2b put names(for:displayName:) on a View, so it inherited the main actor and its test
failed to compile under Swift 6: the nonisolated test sent a non-Sendable closure across
isolation. The helper is pure and touches no UI state, so it is now nonisolated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
InventoryActiveItem title-cases ingredient names ("Olive Oil"), so the test's lowercase name
assertions failed while the reset itself worked. IDs don't depend on display formatting.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant