feat: pre-launch UX overhaul (mobile/Flutter) — full audit, Waves 0–3 - #155
Conversation
App Store Guideline 4.8 requires Sign in with Apple wherever a third-party social login (Google) is offered. Adds the native credential flow (sign_in_with_apple + hashed-nonce verification through supabase.auth.signInWithIdToken), the official SignInWithAppleButton placed above Google on iOS/macOS, and the com.apple.developer.applesignin entitlement wired into all Runner build configs. Enabling the capability in the Apple Developer account and the Apple provider in the Supabase dashboard remain account-side steps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the launch-blocking account layer to Settings. Account deletion (App Store Guideline 5.1.1(v)) pushes a type-to-confirm screen in the brand's calm terracotta register and calls DELETE /api/v1/account, then signs out. Sign out gains a Cupertino confirmation sheet (previously a one-tap drawer action), and 'Export my data' fetches GET /api/v1/account and shares the JSON via the system share sheet. New api_client.deleteAccount()/exportMyData() helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was no password recovery on mobile at all (the forgotPassword string was translated but never rendered). Adds a 'Forgot password?' link on the sign-in form that opens a recovery screen: it sends resetPasswordForEmail with a redirect through the web /auth/callback to the web /reset-password page (reusing the web reset surface, so no in-app deep-link handling is needed), then holds on a persistent check-your-email state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first frames were a pure-white native launch (on a cream-paper brand) then a bare Material spinner. Sets the native LaunchScreen background to cream so there's no white flash, and replaces the splash spinner with the Lora 'Nhẩm' wordmark breathing gently on the cream surface (paused under reduced motion). Offline font bundling (allowRuntimeFetching=false) is deferred — it needs the Vietnamese-subset static font files and an on-device verify to avoid regressing rendering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (12)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughLarge Flutter mobile update adding Apple Sign-In, a unified auth surface, dashboard day paging, nutrition screen redesign, logging feed lifecycle changes, instant-commit settings editors, and app-wide localization and icon updates. ChangesMobile App Feature Refresh
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Retire the web-ported hamburger drawer in favour of an HIG-native three-tab cream bottom bar: Today (dashboard) / Log (logging, center) / Patterns (nutrition). Hairline top border, no elevation, safe-area padding, hidden when the keyboard is open. selectionClick haptic on tab switch; instant branch swaps. PopScope returns to Today on system back from a secondary tab. Settings/account moves to a 32px avatar disc in the header's right slot (carrying the onboarding pulse-dot), pushing the Settings screen as a root CupertinoPage with swipe-back. Groups + Admin stay reachable as routes but are off the bar until built. Delete the drawer (tab_scaffold drawer) and the mobile sidebar port. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the hardcoded, unlocalized English "Hello" — the one Lora-per-screen moment — with a localized time-of-day greeting driven by the device clock and app locale (Chào buổi sáng / Good morning, etc.). A greeting, not an interpretation. Adds dashboard.greeting.* keys to en + vi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e formatting
Stop censoring over-target days. The dashboard hero now shows the honest number
("184 over") with the eyebrow flipping to OVER TARGET in espresso ink — never
red, never a pill, never an icon. The clamp(0) that made a 2,340-kcal day read
as a neutral "0 / 2,000" is gone.
Unify ring semantics: both calorie rings (dashboard + logging) now fill with
CONSUMED calories (fill up as you eat), matching the week strip and heatmap
framing — instead of the hero ring draining from remaining. When over target,
the base arc completes in tan and an overflow arc continues past 12 o'clock in
~40%-alpha terracotta. The in-ring label flips "left" → "over".
Locale-aware number formatting via intl (en "2,000" / vi "2.000") across the
ring, hero, and logging summary; localize the heatmap weekday initials and the
ring "left"/"over" labels. Adds dashboard.left/over/caloriesOverTarget keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…surance A failed analysis no longer wipes the user's words. On error the raw text is restored into the composer AND the failed attempt renders as a feed card: the input as a Lora quote, a terracotta one-liner, "Try again" as the primary action (re-runs the same meal), and a quiet Discard — wiring the previously unused logging.discard string. Keep the composer editable during analysis (the requestId-supersede mechanism already handles overlap): MealInput's `disabled` becomes `analyzing`, which only swaps Submit→Stop while leaving the field live. Add a ~20s reassurance line to the streaming card so a slow pipeline doesn't read as a stall. Adds logging.failedAttempt.* and logging.streaming.stillWorking to en + vi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app had zero HapticFeedback calls. Define and apply the contract (iOS-first): - selectionClick on tab switches (bottom bar, already), day selection (week strip + timeline picker), segment changes (nutrition range pills, settings tab strip), and stepper taps (meal-entry quantity). - lightImpact on meal submit and sheet-open (sign-out action sheet). - mediumImpact as the success cue on save (meal confirm, weight log, profile). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sh-then-pop On completion the streaming card no longer unmounts and waits for a refetch to pop a fresh pending card — the emotional peak of the analysis. Instead the run is held in state (status done + the locally-held stream.result, previously unused) and the card morphs in place: per-row macros are already real, the totals row counts up ~600ms (new CountUpText), and the spinner row slides out as the Edit/Confirm CTA slides up into the slot it vacated. One lightImpact marks the landing. A ScrollController brings the answer into view on submit and on done (nothing scrolled it before). The reveal card carries the user's own raw words as its Lora quote, and on confirm it tears down the stream so it hands off to the refetched persisted card — one continuous object from typed words to saved meal. The refetch/reset now fires only on confirm, not on done. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o-refresh - Submit button keeps its 32pt visual but gets a 44pt tap target; edit steppers keep their 28pt visual inside a 40pt target (under 44 so two steppers + the count still fit a narrow row). - The whole persisted-card header row toggles expand, not just the ~24px chevron. - Pull-to-refresh (RefreshIndicator.adaptive → Cupertino spinner on iOS) on the day list, refetching the day + the meal-dates strip — none existed anywhere. - The streaming skeleton no longer pads every meal to 3 ghost dishes: anonymous rows fill in only before any names arrive, and just one (logging one coffee no longer implies over-detection). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…with PageView The hand-rolled three-week carousel never animated: its base offset was a constant translateX(-100%), so paging swapped content with no slide and the 200ms duration was dead. Replace it with a PageView anchored so the current week sits at a fixed index and forward paging is capped at this week (itemCount). Swipe and the chevrons now both slide a full week with a selection haptic on landing; opening the strip jumps to the selected week. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web shows two data-completeness notices the Flutter port dropped (their strings shipped translated with zero Dart refs): - legacyMacroWarning: when any persisted meal has unknown macros the day can't be totalled, so the summary ring is replaced by a quiet muted note instead of rendering a wrong total. - partialDayNotice: a past day with real meals but under half the calorie target reads as under-logged, so a Lora-italic terracotta note (ported 1:1 from the web threshold isLikelyPartialDay = calories < 0.5 × target) says it's set aside from trends and invites folding it back in. The partialYesterdayPrompt is deferred (needs a separate yesterday-totals fetch + dismiss state). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ection loop
A confirmed meal was a tombstone: nothing could remove it, so one fat-finger
confirm permanently poisoned the day's totals and trends. Add an iOS
trailing-swipe on persisted cards (terracotta, never red) that removes the meal
with a 5-second undo: the day heals immediately (the meal drops out of the
ring/bars optimistically) and the DELETE /api/v1/meals/{id} fires only if the
undo window closes — undo just restores the snapshot, a server rejection
restores it too. New strings (remove/undo/mealRemoved) in both locales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
weight_trend.dart and weight_chart_utils.dart had no importers anywhere (the verdict engine was amputated in the port; weight_chart.dart only references a separate loadingWeightTrend string). Per the founder design direction interpretive prose verdicts stay internal, so the dashboard.progressStatus.* strings they fed go too — both locales. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…asts - Delete the serifSemiBold Lora variant (the bible's hardest type rule) and convert its three callers (display, numDisplay, the calorie-ring figure) to serifRegular so a bold-Lora regression can't recur. - Real ellipsis … replaces literal "..." across both locales (loading/streaming phase strings, placeholders). - Add a SnackBarThemeData (espresso pill, cream text, accent action, soft radius, floating) so sign-out errors and the undo toast render branded instead of the stock dark-gray Material pill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swap the Material Icons in the logging widgets for their Lucide one-for-one equivalents (square, arrowUp, minus, plus, check, pencil, refreshCw, circleAlert, chevronLeft/Right/Down, calendar, trash2, utensilsCrossed) — the package was already installed and the shell uses it, so the feed no longer ships a second icon DNA. The intended mappings were already noted in code comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping a past day used to refetch the entire dashboard bundle (profile + 90d heatmap + weight) just to swap the meal list, swap the card for a loading line, keep "remaining" framing, show no date, and replay every mount animation. Now the Today card is a PageView synced to the week strip: - tap a strip day or swipe the card to browse (selection haptic on swipe; the strip already fired one on tap), - per-day slice fetch via the lightweight /api/v1/logging/day endpoint — today still reads the warm bundle so the first page costs no round-trip, - a date line in the card (Today / Yesterday / localized weekday-month-day), - the remaining figure counts up ~300ms on day-swap (reuses CountUpText), macro bars + ring retarget without remounting, - the pager animates its own height to the active page so the surrounding ListView reflows instead of clipping taller days. First-run: when the user has never logged anything (zero meals today AND zero historical logged/partial heatmap cells — there's no explicit has-logged-before flag, so this is the reliable gate; an existing user is never mistaken for first-run), the card collapses to one Lora "What did you eat today?" question with no ring and no "% on track". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new user saw "0% on track" over an empty heatmap grid — a percentage computed from zero data. The header line is now suppressed entirely until at least one logged day exists, so the consistency section reads as a blank grid waiting to fill, not a 0% failure. Pairs with the first-run dashboard collapse. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tent on refetch failure The nutrition screen was the only surface forced to ClampingScrollPhysics, had no pull-to-refresh, and a failed refetch nuked the editorial stack the user was reading (guard() replaced the value with a bare error). - swap Clamping for AlwaysScrollable + Bouncing, matching the rest of the app - add a RefreshIndicator (pull-to-refresh re-runs the current range) - refetch() now uses copyWithPrevious so a failure retains the prior overview underneath the error — content stays put - wire the previously-unused nutrition.errors.overviewToast key: a refetch failure that still holds content surfaces as a toast instead of a blank page Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping a steady/background nutrient row pushed a duplicate of the overview bar. Push a real Cupertino detail route (swipe-back, selection haptic) that delivers what the row hint promises: - A point figure + target progress, re-tokened on dashboard_tokens (solid white card, one radius, DM Sans scale, point value only — no ranges or confidence surfaced). - A 7/30/90d coverage sparkline that degrades to discrete dots when coverage is thin (the trend.pointMode copy was written for exactly this). We hold window averages, not a per-day series, so it renders the resolved point honestly rather than fabricating a line. - Food candidates as FULL rows — name, serving, rationale, and caution per Vietnamese food — the content the overview renders as name-only pills. The API already ships all four keys per candidate. Deletes the orphaned inline nutrient_detail.dart (the duplicate bar). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nutrition screen was a verbatim port of the web screen the drift watchlist condemns — translucent stacked cards, six eyebrows, a 24/16 radius mix — sitting one tap from the re-tokened dashboard. - Solid white cards (kCardSurface + kCardShadow), no alpha surfaces: daily-rhythm, steady list, more-nutrients list. - One radius (kCardRadius) — drop the 24/16 mix. - Collapse six eyebrows to two: keep the editorial header eyebrow and the steady-list eyebrow; drop rhythm / focus / more-nutrients / pull-quote eyebrows (the content groups now stand on structure). The verdict sentence is untouched; nutrition.confidence.* logic is untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Felt save: on a successful profile save the floating bar now morphs into a 'Changes saved · N kcal/day' confirmation — wiring the previously unused settings.saved key — with the calorie target counting old→new (CountUpText, reduced-motion aware), holding ~1.6s before dissolving. The success haptic already fired; the morph gives it a visible payoff. Honest error state: a flaky profile fetch previously fell through to the re-onboarding 'Start setup' empty state, stranding a configured user in a false 'set up your profile' dead-end. Distinguish a load error (now a neutral message + retry) from a genuinely-absent profile (the only case that earns the empty state). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A user who skipped onboarding had their settings profile pre-filled with weight 65 / height 165 / age 25 — fabricated values that could be saved as if they were real data. Start those numeric fields EMPTY when the profile carries none; validateBodyMetrics already requires them, so the user is forced to enter genuine values (the hint placeholders still suggest typical numbers without persisting them). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the edit-amounts surface, stepping a count-unit dish down to 0 now strikes the row (line-through name + dimmed macros) — a clear 'this one's out' cue before confirm drops it. Grams floor at minDishGrams, so only count units can reach 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings mixed Material and Lucide icon DNAs. Swap the remaining Material glyphs one-for-one to Lucide: person_outline→user, chevron_right→ chevronRight, arrow_back→arrowLeft (settings list + back header), keyboard_arrow_down→chevronDown and check→check (custom select), public→globe and place→mapPin (regional panel). One icon DNA on the surface now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the "login wall titled Welcome back" with a real pre-auth welcome screen: Lora wordmark, a typing demo that resolves into a point result chip, then three stacked options (Apple, Google, Continue with email). Collapse the sign-in/sign-up tab split into one email path — a single form whose primary action signs in, with a quiet toggle to account-creation in place. Sign-up no longer flashes a vanishing SnackBar: it cross-fades to a "Check your email" state that names the address and offers resend on a 30s cooldown. Google OAuth now holds a "Finishing sign-in…" state across the Safari app-switch instead of clearing the spinner, dropping it only if the app resumes still signed-out. Replace the two e.toString() error leaks in the controller with warm localized copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ip, flat card
Localize the hardcoded English that leaked the moment a user picked Tiếng Việt
("About You" + its hint, and the unlock placeholder's "this side turns into…"
desktop-two-pane leftover) into onboarding.bodyMetrics keys (en + vi).
Replace the banned flag emoji in the language toggle with a Lora language-code
monogram in a tinted disc (EN / VI), and swap its Material check for Lucide.
Sex is a binary choice, so render it as the 2-segment OptionStrip instead of a
select popover. Flatten the daily-target hero from an accent gradient to a flat
white card with a hairline border (dashboard token system: solid cards).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e-pinned A second submit while an unconfirmed reveal is showing now refreshes the origin day first, so the server-side pending analysis resurfaces as a pending-confirmation card instead of visibly vanishing. The stream state carries its loggedDate: the streaming/reveal cards only render on the day they were submitted on, and _confirmReveal updates the origin date's caches. The streaming→reveal swap comment no longer claims there's no remount, and the two loudest discontinuities are gone — the revealing card matches the streaming card's surface background, and item rows crossfade in place instead of replaying FadeInLeft. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cale A resolved save now baselines exactly the payload it sent (markSavedAs); if the values are dirty again — edited during the in-flight PUT — another commit is queued immediately instead of silently baselining the edit away. On failure the controls roll back to their last-saved values, a device-side locale change is reverted (device and server never diverge), and the error line gains a Try again affordance that re-applies the attempted values. The silently-skipped commit path (incomplete body metrics) also reverts the locale and surfaces a quiet localized hint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nutrient detail's sparkline band can only ever hold one point (a single resolved average), so the fabricated 0.5 mid-rail dot for targetless nutrients is gone, the band is omitted entirely when there is nothing meaningful to plot, and its eyebrow now reads 'Average vs target' instead of the untrue 'Target progress' (orphaned key removed from both locales). The figure caption drops to meta type, bringing the screen to max two eyebrows. Remaining new animations now honor MediaQuery.disableAnimations: the reveal and pager count-ups land in place, both macro-bar sweeps render at rest, and the looping pulse skeletons and StreamingEntry spin/pulse freeze into calm static states. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Day-error card re-clothed in terracotta nham-danger on cream (no literal Tailwind reds) - New-code Lora w500 (serifMedium) call sites in settings_screen drop to serifRegular — Lora never above 400 (pre-existing sites in nutrition/onboarding left as-is) - Macro shorthand unified to 'P: 38g' (with space) on meal-entry item rows and persisted-card group rows, matching their totals rows - Today-card meal rows name the unit: '412 kcal', not a bare number - '% on track' suppressed until 3 scored days (spec), not 1 - /welcome count-up uses locale-aware formatCount (vi '2.000') instead of hand-rolled comma grouping - Goal-pace subline localizes the per-week suffix (vi '/tuần') and the decimal separator - biologicalSex is a real required selection: null until chosen, placeholder in the select, localized validation message — no silent male default baking a wrong BMR (consistent with the 65/165/25 fix) - PlaceholderScreen header gains a back affordance (pop, else home) so a groups/admin deep link can't strand the user - Instant-commit save-success haptic unified to mediumImpact - _isFirstRun comment now admits the >90d-stale-user limit, and the first-run card gains three localized time-of-day suggestion chips that open the composer prefilled Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bundles the static OFL TTFs under assets/google_fonts/ — Lora Regular/Italic/Medium and DM Sans Regular/Medium/SemiBold/Bold, the exact weight/style set the app's type system uses — named per the Google Fonts API convention so google_fonts resolves them from assets, and disables runtime fetching in main.dart. A cold offline start now renders in the brand fonts instead of falling back while a network fetch races first paint. Lora's TTFs carry full Vietnamese coverage (verified ẩ ở ự đ ọ in the cmap); DM Sans has no Vietnamese subset — identical coverage to the previous runtime fetch, so no regression. The audit listed only Lora Regular+Italic and DM Sans Regular/Medium/Bold, but with fetching disabled the other live variants (Lora 500, DM Sans 600) would throw and drop to the system fallback — so the full in-use set is bundled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # apps/mobile-flutter/lib/features/logging/data/logging_providers.dart # apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart # apps/mobile-flutter/lib/features/logging/widgets/meal_input.dart
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart (1)
77-77:⚠️ Potential issue | 🔴 CriticalHandle the
Future<bool>return value fromrefetch()across all callers.The method signature changed from
Future<void>toFuture<bool>. Three call sites found innutrition_screen.dartrequire updates:
Line 96 (
RefreshIndicator.onRefresh): Type mismatch—RefreshIndicatorexpectsFuture<void>, butrefetch()now returnsFuture<bool>. Either convert the return type back toFuture<void>or wrap the call appropriately.Lines 125 & 137 (
InlineError.onRetrycallbacks): Currently ignoring the boolean return value. Since the method is documented to return success/failure for caller feedback, these should await the result and display a toast on failure.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart` at line 77, The refetch() method in nutrition_overview_provider.dart now returns Future<bool> instead of Future<void>, but its callers in nutrition_screen.dart have not been updated. At the RefreshIndicator.onRefresh call site (around line 96), the type mismatch must be resolved by either converting refetch() back to return Future<void> or wrapping the call with a closure that discards the bool return value to match the RefreshIndicator contract. At the two InlineError.onRetry callback sites (around lines 125 and 137), update them to await the bool result from refetch() and use the returned boolean to determine whether to display a failure toast notification to the user, replacing the current behavior of ignoring the return value entirely.
🧹 Nitpick comments (1)
apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart (1)
92-96: 💤 Low valueInconsistent formatting of
refetch()calls.The
onRefreshcallback splits a simple method chain across five lines, while the identical pattern at lines 125 and 137 uses a single line. The multi-line format doesn't improve readability here.♻️ Simplify to match the style at lines 125 and 137
- onRefresh: - () => - ref - .read(nutritionOverviewProvider(_range).notifier) - .refetch(), + onRefresh: () => ref.read(nutritionOverviewProvider(_range).notifier).refetch(),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart` around lines 92 - 96, The onRefresh callback at lines 92-96 is formatted across multiple lines in a way that is inconsistent with the same refetch() pattern used at lines 125 and 137. Consolidate the onRefresh callback to a single line by moving the lambda arrow and the ref.read(nutritionOverviewProvider(_range).notifier).refetch() call onto one line to match the formatting style used elsewhere in the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart`:
- Around line 37-40: The `_lastOverviewByAccount` cache dictionary grows without
bound and is never cleared, causing a memory leak over the app's lifetime as
different userId:timezone combinations accumulate. Fix this by implementing
cache eviction: either listen to session state changes (such as when
currentSessionProvider becomes null on sign-out) to call
`_lastOverviewByAccount.clear()`, or implement a bounded LRU cache using
LinkedHashMap with a maximum size limit (e.g., 10 entries) and remove the oldest
entry when adding a new one beyond the limit. If implementing the LRU approach,
replace the direct map assignments to `_lastOverviewByAccount` at the locations
where overview data is cached with a helper method that enforces the size
constraint.
In `@apps/mobile-flutter/test/widget_test.dart`:
- Around line 28-33: The EasyLocalization widget in the widget_test.dart file
lacks an explicit startLocale parameter, which causes the test to initialize in
whatever locale matches the system environment rather than English. Add the
startLocale property set to const Locale('en') in the EasyLocalization widget
configuration to ensure the app always starts with the English locale during
testing, making the text assertions on lines 39-40 reliable across different
system locales.
---
Outside diff comments:
In
`@apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart`:
- Line 77: The refetch() method in nutrition_overview_provider.dart now returns
Future<bool> instead of Future<void>, but its callers in nutrition_screen.dart
have not been updated. At the RefreshIndicator.onRefresh call site (around line
96), the type mismatch must be resolved by either converting refetch() back to
return Future<void> or wrapping the call with a closure that discards the bool
return value to match the RefreshIndicator contract. At the two
InlineError.onRetry callback sites (around lines 125 and 137), update them to
await the bool result from refetch() and use the returned boolean to determine
whether to display a failure toast notification to the user, replacing the
current behavior of ignoring the return value entirely.
---
Nitpick comments:
In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart`:
- Around line 92-96: The onRefresh callback at lines 92-96 is formatted across
multiple lines in a way that is inconsistent with the same refetch() pattern
used at lines 125 and 137. Consolidate the onRefresh callback to a single line
by moving the lambda arrow and the
ref.read(nutritionOverviewProvider(_range).notifier).refetch() call onto one
line to match the formatting style used elsewhere in the file.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7d3c7aa8-9953-4b0b-bc69-27b5043c882e
⛔ Files ignored due to path filters (7)
apps/mobile-flutter/assets/google_fonts/DMSans-Bold.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/DMSans-Medium.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/DMSans-Regular.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/DMSans-SemiBold.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/Lora-Italic.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/Lora-Medium.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/Lora-Regular.ttfis excluded by!**/*.ttf
📒 Files selected for processing (55)
apps/mobile-flutter/assets/l10n/en.jsonapps/mobile-flutter/assets/l10n/vi.jsonapps/mobile-flutter/lib/data/api_client.dartapps/mobile-flutter/lib/features/auth/providers/auth_form_controller.dartapps/mobile-flutter/lib/features/auth/widgets/auth_page.dartapps/mobile-flutter/lib/features/auth/widgets/email_auth_form.dartapps/mobile-flutter/lib/features/auth/widgets/welcome_demo.dartapps/mobile-flutter/lib/features/dashboard/data/dashboard_providers.dartapps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dartapps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dartapps/mobile-flutter/lib/features/dashboard/widgets/today_section.dartapps/mobile-flutter/lib/features/logging/data/logging_keys.dartapps/mobile-flutter/lib/features/logging/data/logging_providers.dartapps/mobile-flutter/lib/features/logging/data/manual_log_providers.dartapps/mobile-flutter/lib/features/logging/data/stream_analysis_controller.dartapps/mobile-flutter/lib/features/logging/screens/logging_screen.dartapps/mobile-flutter/lib/features/logging/widgets/count_up.dartapps/mobile-flutter/lib/features/logging/widgets/feed_area.dartapps/mobile-flutter/lib/features/logging/widgets/meal_entry.dartapps/mobile-flutter/lib/features/logging/widgets/meal_input.dartapps/mobile-flutter/lib/features/logging/widgets/partial_yesterday_prompt.dartapps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dartapps/mobile-flutter/lib/features/logging/widgets/streaming_entry.dartapps/mobile-flutter/lib/features/nutrition/logic/verdict_logic.dartapps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dartapps/mobile-flutter/lib/features/nutrition/screens/nutrient_detail_screen.dartapps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dartapps/mobile-flutter/lib/features/nutrition/widgets/background_section.dartapps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dartapps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dartapps/mobile-flutter/lib/features/nutrition/widgets/inline_error.dartapps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dartapps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dartapps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dartapps/mobile-flutter/lib/features/onboarding/screens/screen_body_metrics.dartapps/mobile-flutter/lib/features/onboarding/screens/welcome_setup_screen.dartapps/mobile-flutter/lib/features/onboarding/widgets/language_toggle.dartapps/mobile-flutter/lib/features/settings/controls/country_select.dartapps/mobile-flutter/lib/features/settings/controls/custom_select.dartapps/mobile-flutter/lib/features/settings/controls/option_strip.dartapps/mobile-flutter/lib/features/settings/logic/profile_payload.dartapps/mobile-flutter/lib/features/settings/panels/body_metrics.dartapps/mobile-flutter/lib/features/settings/screens/account_section.dartapps/mobile-flutter/lib/features/settings/screens/settings_screen.dartapps/mobile-flutter/lib/features/settings/widgets/instant_commit_editor.dartapps/mobile-flutter/lib/features/settings/widgets/profile_form_controller.dartapps/mobile-flutter/lib/features/settings/widgets/profile_form_values.dartapps/mobile-flutter/lib/features/settings/widgets/region_editor.dartapps/mobile-flutter/lib/main.dartapps/mobile-flutter/lib/router.dartapps/mobile-flutter/lib/shell/placeholder_screen.dartapps/mobile-flutter/lib/shell/tab_scaffold.dartapps/mobile-flutter/lib/theme/nham_typography.dartapps/mobile-flutter/pubspec.yamlapps/mobile-flutter/test/widget_test.dart
💤 Files with no reviewable changes (1)
- apps/mobile-flutter/lib/features/nutrition/logic/verdict_logic.dart
✅ Files skipped from review due to trivial changes (1)
- apps/mobile-flutter/assets/l10n/vi.json
🚧 Files skipped from review as they are similar to previous changes (26)
- apps/mobile-flutter/lib/features/dashboard/data/dashboard_providers.dart
- apps/mobile-flutter/lib/theme/nham_typography.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/background_section.dart
- apps/mobile-flutter/lib/features/logging/widgets/count_up.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dart
- apps/mobile-flutter/lib/features/onboarding/widgets/language_toggle.dart
- apps/mobile-flutter/lib/features/settings/widgets/region_editor.dart
- apps/mobile-flutter/lib/features/settings/widgets/instant_commit_editor.dart
- apps/mobile-flutter/lib/features/onboarding/screens/welcome_setup_screen.dart
- apps/mobile-flutter/lib/features/settings/logic/profile_payload.dart
- apps/mobile-flutter/lib/features/logging/data/logging_keys.dart
- apps/mobile-flutter/lib/features/auth/widgets/welcome_demo.dart
- apps/mobile-flutter/lib/features/settings/screens/account_section.dart
- apps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dart
- apps/mobile-flutter/lib/shell/tab_scaffold.dart
- apps/mobile-flutter/lib/features/logging/screens/logging_screen.dart
- apps/mobile-flutter/lib/features/logging/widgets/partial_yesterday_prompt.dart
- apps/mobile-flutter/lib/features/logging/widgets/streaming_entry.dart
- apps/mobile-flutter/lib/features/auth/providers/auth_form_controller.dart
- apps/mobile-flutter/lib/router.dart
- apps/mobile-flutter/lib/features/logging/widgets/meal_input.dart
- apps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dart
- apps/mobile-flutter/lib/features/dashboard/widgets/today_section.dart
- apps/mobile-flutter/assets/l10n/en.json
- apps/mobile-flutter/lib/features/settings/screens/settings_screen.dart
- apps/mobile-flutter/lib/features/logging/widgets/meal_entry.dart
| final Map<String, NutritionOverview> _lastOverviewByAccount = {}; | ||
|
|
||
| String _overviewCacheKey(String? userId) => | ||
| '${userId ?? 'signed-out'}:${nutritionTimezoneOffset()}'; |
There was a problem hiding this comment.
Memory leak: unbounded global cache never evicted.
_lastOverviewByAccount grows without bound and is never cleared. Each unique userId:timezone combination adds an entry that persists for the app's lifetime, even after sign-out or account deletion. Over time (especially with timezone changes from travel/DST or multiple accounts), this will leak memory.
♻️ Recommended fix: add cache eviction on sign-out
Option 1: Clear the entire cache when the session ends (simple, but loses data across sign-out/sign-in):
// In a listener or dispose callback where you detect sign-out:
ref.listen(currentSessionProvider, (prev, next) {
if (next == null && prev != null) {
_lastOverviewByAccount.clear();
}
});Option 2: Implement LRU eviction with a maximum cache size (e.g., keep only the last 10 entries):
final _lastOverviewByAccount = LinkedHashMap<String, NutritionOverview>();
const _maxCacheSize = 10;
void _updateCache(String key, NutritionOverview overview) {
if (_lastOverviewByAccount.length >= _maxCacheSize && !_lastOverviewByAccount.containsKey(key)) {
_lastOverviewByAccount.remove(_lastOverviewByAccount.keys.first);
}
_lastOverviewByAccount[key] = overview;
}Then replace direct map assignments with _updateCache(cacheKey, overview); at lines 55 and 87.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/mobile-flutter/lib/features/nutrition/providers/nutrition_overview_provider.dart`
around lines 37 - 40, The `_lastOverviewByAccount` cache dictionary grows
without bound and is never cleared, causing a memory leak over the app's
lifetime as different userId:timezone combinations accumulate. Fix this by
implementing cache eviction: either listen to session state changes (such as
when currentSessionProvider becomes null on sign-out) to call
`_lastOverviewByAccount.clear()`, or implement a bounded LRU cache using
LinkedHashMap with a maximum size limit (e.g., 10 entries) and remove the oldest
entry when adding a new one beyond the limit. If implementing the LRU approach,
replace the direct map assignments to `_lastOverviewByAccount` at the locations
where overview data is cached with a helper method that enforces the size
constraint.
| EasyLocalization( | ||
| supportedLocales: const [Locale('en'), Locale('vi')], | ||
| path: 'assets/l10n', | ||
| fallbackLocale: const Locale('en'), | ||
| child: const ProviderScope(child: NhamApp()), | ||
| ), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify current test coverage for explicit locale pinning in widget tests.
rg -n -C2 "EasyLocalization\\(|startLocale:|fallbackLocale:" apps/mobile-flutter/testRepository: VoMinhKhoii/Nham
Length of output: 747
🏁 Script executed:
# Read the widget test file around the mentioned lines to see the assertion at 39-40
cat -n apps/mobile-flutter/test/widget_test.dart | sed -n '25,45p'Repository: VoMinhKhoii/Nham
Length of output: 724
🏁 Script executed:
# Search for other EasyLocalization usages in tests to see patterns
rg -A 5 "EasyLocalization(" apps/mobile-flutter/test --type dartRepository: VoMinhKhoii/Nham
Length of output: 145
🌐 Web query:
EasyLocalization startLocale vs fallbackLocale behavior Flutter
💡 Result:
In the EasyLocalization package for Flutter, startLocale and fallbackLocale serve distinct purposes in how the application determines which language to display [1][2]. startLocale The startLocale property is used to override the device's default locale [2][3]. If specified, it forces the application to attempt to load this locale when the app initializes, rather than automatically detecting the user's system language [2]. It is important to note that the effectiveness of startLocale depends on the saveLocale property [4]. By default, saveLocale is set to true, meaning the package stores the user's last selected locale in device storage [3][5]. If a saved locale exists, it typically takes precedence over startLocale [4]. To ensure startLocale is always used (or to ensure it takes precedence over previously saved preferences), developers often need to set saveLocale to false [4]. fallbackLocale The fallbackLocale property defines the locale that the application should use when a requested or detected locale is not present in the supportedLocales list [1][2]. It serves as a safety net to prevent the app from failing or displaying errors if the user's device locale is not supported by your application's translation files [2]. Additionally, when used in conjunction with settings like useFallbackTranslations, the fallbackLocale can also be used as a secondary source for individual missing keys, allowing the app to fetch a translation from the fallback file if the key is missing from the primary locale file [1][2]. Summary of Behavior 1. startLocale: Determines the initial language when the app first launches, overriding the device system settings (provided saveLocale does not override it) [2][4]. 2. fallbackLocale: Provides a default locale if the currently selected or device locale is not included in the list of supported locales, or serves as a source for missing translations when enabled [1][2]. Top results: [1][2][4][3]
Citations:
- 1: https://pub.dev/documentation/easy_localization/latest/easy_localization/EasyLocalization-class.html
- 2: https://github.com/aissat/easy_localization/blob/develop/README.md
- 3: https://pub.dev/documentation/easy_localization/latest/
- 4: Ignore _savedLocale on start when saveLocale is false (#430) aissat/easy_localization#683
- 5: https://pub.dev/packages/easy_localization
Add explicit startLocale to pin English locale for test assertions.
Lines 39-40 assert English text, but without startLocale in the EasyLocalization setup, the app can initialize in any supported locale matching the environment's system locale. fallbackLocale only provides a safety net for unsupported locales, not an explicit initial locale. This will cause the test to fail in environments with Vietnamese or other non-English system locale.
Suggested fix
await tester.pumpWidget(
EasyLocalization(
supportedLocales: const [Locale('en'), Locale('vi')],
path: 'assets/l10n',
fallbackLocale: const Locale('en'),
+ startLocale: const Locale('en'),
child: const ProviderScope(child: NhamApp()),
),
);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile-flutter/test/widget_test.dart` around lines 28 - 33, The
EasyLocalization widget in the widget_test.dart file lacks an explicit
startLocale parameter, which causes the test to initialize in whatever locale
matches the system environment rather than English. Add the startLocale property
set to const Locale('en') in the EasyLocalization widget configuration to ensure
the app always starts with the English locale during testing, making the text
assertions on lines 39-40 reliable across different system locales.
Logging surface redesign: - Restore left slide-in nav drawer; remove bottom tab bar so the input bar owns the bottom edge (sidebar.dart, tab_scaffold.dart, app_header.dart) - Date chip morphs in place into the week strip (fixed-height cross-dissolve, full-width, hamburger hidden when expanded) — feed never shifts (DateMorph) - Two-line meal input: text on line 1, mode selector + send on line 2; minimal Claude-Code-style mode sheet (Normal/Cheat/Manual) - Optimistic save with keep-previous-data + top toast (no full reload) - Streaming meal card: single loading step, streams items like web, lightning icon for Normal mode - Meal time as centered divider (── 1:04 AM ──); consistent subtle borderFaint separators; removed left timeline rail/gutter - Empty state: rotating "What did you eat?" prompts, no chips/subtitle - Manual log sheet: Lucide icons, macros with units, prominent save Font: swap DM Sans -> Be Vietnam Pro globally (native font family) to fix broken Vietnamese diacritics; Lora serif unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mobile (apps/mobile-flutter): - Flatten to one view: compact DaySummary, 2-col Vitamins/Minerals grid with inline progress bars + average/goal in g·mg, and a single "Suggested foods" CTA (bottom sheet over the timeline's most under-target nutrients). Drop the per-nutrient detail screen, spotlight/steady/background sections and the Vitamin-D education card. - Date toggle moves to the header (AppHeader.trailing) as a Today/7d/30d segmented control; calorie hero switches from Lora to the sans hero with a top-right over/under target chip (hidden when no goal). - Macro stacked line-area trend chart (fl_chart) for 7d/30d from the overview daySeries; kcal y-axis to ≥3000 with dashed gridlines; Today keeps the composition bar. Centered macro legend with longer pills + ±% vs target. - On-target nutrient cards greened (fresh emerald); ceilings/limited unchanged. - FAO/WHO · Vietnam RDA · NASEM DRI source attribution with a citations sheet. - All toasts moved to the top: showTopToast gains success/error + action variants; 7 bottom SnackBars converted (incl. the logging undo). - Parse daySeries into the mobile NutritionOverview model. Web API (lib/nutrition): add a 1-day '1d' range end-to-end (schemas, types, date-range, summary, overview mapper) + verdict-hero Record + messages, so the mobile "Today" view resolves to a single day. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rchy - Suggested-foods sheet: drop the verbose serving/rationale/caution cards for a compact per-nutrient row — name, how-much-short (−N%), and food-name chips. Removes the now-unused FoodCandidateRow. - Drawer: give the header a prominent avatar + name + email identity block and drop the duplicate footer account card, so the hierarchy reads identity → nav → account actions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…known Sex-dependent micronutrients (iron, vitamin A, zinc, magnesium…) previously resolved to an "unsupported" null target when a profile had no biological sex, so those cards showed "no target". Now an unknown sex falls back to the mean of the male/female RDA (e.g. VN iron (10+24)/2 = 17 mg), and an age-unknown VN female keeps the premenopausal iron floor (24 mg) instead of null — every catalogued nutrient scores. Sex-independent nutrients are unaffected (mean == value). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…any nutrient
Replace the hand-curated 8-nutrient food catalog with a query over
vietnamese_food_composition: top foods by per-100g density for ANY card
nutrient (default + extended), condiments excluded, raw/cooked variants deduped.
Returns a pool (18) so clients can cycle alternatives without refetching.
- candidates action is now a DB query; input schema widened to CANDIDATE_NUTRIENTS
(default + more); CandidatesResponse shape is now { nutrient, foods:[{id, name,
nameEn, amount, unit}] }.
- every card nutrient sets supportsCandidates: true.
- web FoodChipRow renders DB foods (locale-aware name) for any nutrient.
- delete the curated catalog + its test; update candidates/schema tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Consume the new DB-derived candidates shape ({foods:[{name,nameEn,amount,
unit}]}); the provider now works for ANY card nutrient (drop the supported-8
gate + candidate_nutrients.dart).
- Foods sheet shows a few foods per nutrient; tap to reserve (pinned + check),
"Don't have these?" cycles only the unreserved slots through the fetched pool
— no refetch (instant). Locale-aware food names.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ranking by raw per-100g density surfaced dense-but-impractical items — dried spices topping vitamin K, fortified breakfast cereals, oils, sweets, shake mixes, and yeast/leavening agents. Exclude those whole categories (USDA SR + VN FCT naming variants) plus fortified/formulated name markers in the "Other" catch-all, while keeping whole-food groups (meats, vegetables, fruits, legumes, fish, nuts, grains, roots). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resolveMicronutrientTargets assigned `unit` (typed as plain string) into MicronutrientTarget.unit (`'mg' | 'mcg'`), which fails tsc. Narrow the local to the row's own unit type. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Long USDA food names overflowed the chip Wrap row. Cap each chip at 62% of screen width with a Flexible + ellipsis label so names truncate instead of pushing past the row edge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vi.json had only the 'other' variant for verdict.partialNote; the singular path would fail to resolve. Vietnamese doesn't inflect plurals, so 'one' mirrors 'other'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart (2)
572-635: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winKeep pull-to-refresh available for empty feeds.
RefreshIndicator.adaptiveonly wraps the populatedListView; empty, loading, and pending-only states use plainSingleChildScrollView, so users cannot refresh when the feed has no persisted meals.Suggested direction
+ Widget _refreshable(Widget child) => RefreshIndicator.adaptive( + onRefresh: _refresh, + color: NhamColors.accent, + child: child, + ); + @@ - return SingleChildScrollView( + return _refreshable(SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + physics: const AlwaysScrollableScrollPhysics(), child: Padding( padding: const EdgeInsets.symmetric(vertical: NhamSpacing.sp6), child: Center(child: body), ), - ); + ));Apply the same wrapper/physics to the other empty or footer-only scroll branches.
Also applies to: 638-641
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart` around lines 572 - 635, The empty/loading/footer-only branches in feed_area.dart currently return plain SingleChildScrollView, so they bypass the RefreshIndicator.adaptive used by the populated ListView. Update the scroll branches around the persistedMeals.isEmpty logic to use the same refreshable wrapper and scroll physics as the main feed path, including the body-only and _Footer rendering cases, so pull-to-refresh remains available when there are no persisted meals.
273-285: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPin failed-attempt cards to the submitted day.
Streaming/reveal cards are date-guarded, but
_failedTextis not. If an analysis fails after the user switches dates, the retry card and restored composer text can appear on the wrong day.Suggested fix
String? _failedText; + String? _failedDate; @@ setState(() { _failedText = null; + _failedDate = null; _revealRawInput = null; _inFlightText = text; }); @@ if (next.status == StreamStatus.error) { final text = _inFlightText; + final failedDate = next.loggedDate ?? widget.date; setState(() { _failedText = text; + _failedDate = failedDate; _inFlightText = null; }); - if (text != null && _inputController.getText().trim().isEmpty) { + if (failedDate == widget.date && + text != null && + _inputController.getText().trim().isEmpty) { _inputController.setText(text); } @@ - final hasFailedAttempt = _failedText != null; + final hasFailedAttempt = + _failedText != null && _failedDate == widget.date;Also applies to: 362-376
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart` around lines 273 - 285, The failed-attempt retry card is not pinned to the day it was submitted, so it can show on a different selected date after a stream error. Update the error handling in feed_area.dart around the StreamStatus.error path in the feed/state flow (including the _failedText logic and the card rendering path) to capture the active submitted day when the request starts and only surface the failed card when the current day still matches that captured day. Apply the same date-guarding behavior to the related retry-card rendering code referenced by the 362-376 block so the restored composer text and failed attempt stay tied to the original day.apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart (1)
460-503: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDisable future-day cells before they reach logging flows.
isFutureis computed, but the tap handler still selects those dates. Because the current centered week includes days after today, users can navigate to and log against future dates.Suggested fix
- final labelColor = isSelected ? NhamColors.text : NhamColors.textMuted; + final disabled = isFuture; + final labelColor = + disabled + ? NhamColors.textMuted50 + : isSelected + ? NhamColors.text + : NhamColors.textMuted; @@ - onTapDown: (_) => setState(() => _pressed = true), - onTapUp: (_) => setState(() => _pressed = false), - onTapCancel: () => setState(() => _pressed = false), - onTap: () => widget.onSelect(widget.date), + onTapDown: disabled ? null : (_) => setState(() => _pressed = true), + onTapUp: disabled ? null : (_) => setState(() => _pressed = false), + onTapCancel: disabled ? null : () => setState(() => _pressed = false), + onTap: disabled ? null : () => widget.onSelect(widget.date),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart` around lines 460 - 503, Future-day cells in the TimelinePicker are still tappable even though isFuture is already computed. Update the day cell logic in the widget build/tap handling so future dates cannot be selected or sent to widget.onSelect, and keep the existing visual treatment for isFuture consistent with a disabled state. Use the isFuture, GestureDetector, and onSelect paths in timeline_picker.dart to gate both tap callbacks and pressed feedback for future dates.
🧹 Nitpick comments (1)
components/nutrition/rows/food-chip-row.tsx (1)
27-65: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDestructure the
useQueryresult.Reading the whole query object opts out of TanStack Query's tracked-property optimization, so this component subscribes to more state than it uses. Please destructure the fields you read (
data,error,isError,isLoading,isSuccess) and verify that this is intentional.♻️ Proposed fix
- const query = useQuery({ + const { data, error, isError, isLoading, isSuccess } = useQuery({ queryKey: ['nutrition', 'candidates', nutrient], queryFn: () => getFoodSourceCandidates({ nutrient }), enabled, retry: false, staleTime: 60_000, }); - if (query.isLoading) { + if (isLoading) { return <ChipRowSkeleton variant={variant} />; } @@ - if (query.isError) { + if (isError) { console.error( '[nutrition] candidates query failed for nutrient', nutrient, - query.error + error ); @@ - if (query.isSuccess && !query.data.foods.length) { + if (isSuccess && !data.foods.length) { @@ - if (!query.data?.foods.length) { + if (!data?.foods.length) { @@ - const chips = query.data.foods.slice(0, limit); + const chips = data.foods.slice(0, limit);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/nutrition/rows/food-chip-row.tsx` around lines 27 - 65, The food-chip-row component is reading the full useQuery result object, which disables TanStack Query’s tracked-property optimization. Update the useQuery call in food-chip-row.tsx to destructure only the fields actually used in the render path, namely data, error, isError, isLoading, and isSuccess, and then use those local bindings throughout the component to keep subscriptions minimal.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart`:
- Around line 15-17: Replace the custom Apple glyph/button in AppleButton with
Apple’s official SignInWithAppleButton control so branding and platform behavior
come from the system-provided widget. Update the AppleButton widget
implementation to use the provided Apple sign-in button rather than the
hand-rolled UI, keeping the existing auth action wiring intact while preserving
the current placement within the auth flow.
- Around line 39-47: The auth CTA in AppleButton is built with GestureDetector
and Opacity, so it is not exposed as a real accessible button or disabled
control. Replace the current interaction wrapper in AppleButton’s build logic
with a proper button primitive that supports semantics, focus, and disabled
state, and keep the pressed/busy visual behavior tied to that button instead of
manual tap handlers.
In `@apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart`:
- Around line 26-42: The shimmer still runs its AnimationController even when
reduced-motion is enabled, because _ShimmerState::_c starts repeating
unconditionally and build() only skips rendering. Update Shimmer/_ShimmerState
to start and stop the controller based on
MediaQuery.disableAnimationsOf(context) (or an equivalent media-change hook) so
the ticker is paused when animations are disabled and resumed when they are
allowed.
In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart`:
- Around line 245-255: Initialize _visibleAnchor from the same anchor used by
PageController initialPage instead of _selectedAnchor, so TimelinePicker starts
with the actual page being shown. Update the setup around _visibleAnchor,
_pageController, and _canNavigateNext in timeline_picker.dart so the right
chevron reflects the real visible page when selectedDate is on the current page.
Also make the same adjustment anywhere the current-page visibility is derived in
the related logic around the page handling flow.
In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart`:
- Around line 138-142: The nutrient grouping in nutrition_screen.dart is
treating every non-vitamin item as a mineral, so NutrientGroup.other can be
rendered under the wrong header. Update the logic around the
all/vitamins/minerals lists in the nutrition screen to filter minerals
explicitly by NutrientGroup.mineral (or equivalent) and keep NutrientGroup.other
separate so it does not fall into the Minerals section.
In `@apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart`:
- Around line 171-176: The week label formatting in _label currently hard-codes
day/month order, so update the week branch to use the locale-aware short date
skeleton instead of the fixed pattern. Keep the existing weekday logic for
non-week units, and make the change in macro_trend_chart.dart within the _label
helper so week labels follow each locale’s conventions.
In `@apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart`:
- Around line 37-38: The active range selection in range_selector.dart is using
_ranges.indexOf(resolvedRange) directly, so unsupported model values like 90d
resolve to -1 and get clamped to the first segment. Update the RangeSelector
logic to normalize resolvedRange to one of the supported _ranges values before
calculating activeIndex, and base the selected segment on that normalized value
so backend-resolved auto values map to the intended UI range.
- Around line 42-44: The range selector touch target is too small because the
SizedBox in range_selector.dart constrains the control to 34px height. Update
the layout in the range selector widget so the tappable area reaches at least
44pt, adjusting the parent SizedBox and any internal padding/segment sizing in
the range selector implementation to preserve the visual design while meeting
the target.
In `@apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart`:
- Around line 49-54: The citations bottom sheet in source_attribution.dart is
using a non-scrollable Column, which can overflow with longer localized text or
larger accessibility fonts. Update the sheet layout in the widget built by the
builder and any related citation content sections so the content is wrapped in a
scrollable container such as SingleChildScrollView or ListView, while preserving
the existing Container styling and bottom-sheet structure.
In
`@apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart`:
- Around line 235-247: The chip actions in SuggestedFoodsSheet are interactive
but their current padding can fall below the 44pt mobile touch target. Update
the chip/widget builders in suggested_foods_sheet.dart, especially the
GestureDetector/AnimatedContainer path for the chip item and the refresh row
block, so their tappable area is at least 44pt tall while preserving the
existing layout and visuals. Use the existing chip action widget logic in the
affected builder functions to add vertical padding/minimum constraints rather
than shrinking the hit area.
- Around line 176-180: The rotating slot count in suggested_foods_sheet.dart is
widened to num by clamp, which then breaks the _cursor update logic. Update the
rotatingSlots calculation in the logic that builds the rotating FoodCandidate
list so it is converted back to int before being used in the modulo/update path,
keeping the _cursor assignment type-safe. Refer to the rotatingSlots variable
and the _cursor update in the suggested_foods_sheet widget logic when making the
fix.
In `@apps/mobile-flutter/lib/shared/widgets/top_toast.dart`:
- Around line 22-48: `showTopToast` currently inserts a new `OverlayEntry` every
time, which allows multiple top toasts to overlap; update the toast flow to
serialize or replace the active entry so only one `_TopToast` can be visible at
once. Use the existing `showTopToast`, `OverlayEntry`, and `_TopToast` logic to
track the current toast and either dismiss it or queue the next toast before
calling `overlay.insert`, ensuring the undo toast in the logging feed can remain
visible and actionable.
In `@apps/mobile-flutter/lib/shell/app_header.dart`:
- Around line 41-56: The current AppHeader layout in the build method still
centers the middle content relative to only a fixed spacer, so any wider
trailing widget shifts the title off-center. Update the Row-based structure in
app_header.dart so leading and trailing are independently anchored while the
middle child is centered against the full available header width, preserving the
centering contract for variable-width trailing content like the nutrition range
selector.
In `@apps/mobile-flutter/lib/shell/sidebar.dart`:
- Around line 97-102: The sidebar filtering in sidebar.dart hardcodes isAdmin to
false, which hides the adminOnly nav item for every user. Update the logic
around the _navItems filter to use the real admin role/claim source instead of
the constant, or remove the /admin entry until an isAdmin provider is available.
Keep the fix localized to the sidebar item selection path so the admin gating
reflects actual session state.
---
Outside diff comments:
In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart`:
- Around line 572-635: The empty/loading/footer-only branches in feed_area.dart
currently return plain SingleChildScrollView, so they bypass the
RefreshIndicator.adaptive used by the populated ListView. Update the scroll
branches around the persistedMeals.isEmpty logic to use the same refreshable
wrapper and scroll physics as the main feed path, including the body-only and
_Footer rendering cases, so pull-to-refresh remains available when there are no
persisted meals.
- Around line 273-285: The failed-attempt retry card is not pinned to the day it
was submitted, so it can show on a different selected date after a stream error.
Update the error handling in feed_area.dart around the StreamStatus.error path
in the feed/state flow (including the _failedText logic and the card rendering
path) to capture the active submitted day when the request starts and only
surface the failed card when the current day still matches that captured day.
Apply the same date-guarding behavior to the related retry-card rendering code
referenced by the 362-376 block so the restored composer text and failed attempt
stay tied to the original day.
In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart`:
- Around line 460-503: Future-day cells in the TimelinePicker are still tappable
even though isFuture is already computed. Update the day cell logic in the
widget build/tap handling so future dates cannot be selected or sent to
widget.onSelect, and keep the existing visual treatment for isFuture consistent
with a disabled state. Use the isFuture, GestureDetector, and onSelect paths in
timeline_picker.dart to gate both tap callbacks and pressed feedback for future
dates.
---
Nitpick comments:
In `@components/nutrition/rows/food-chip-row.tsx`:
- Around line 27-65: The food-chip-row component is reading the full useQuery
result object, which disables TanStack Query’s tracked-property optimization.
Update the useQuery call in food-chip-row.tsx to destructure only the fields
actually used in the render path, namely data, error, isError, isLoading, and
isSuccess, and then use those local bindings throughout the component to keep
subscriptions minimal.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b31d7c71-026d-49b9-8223-d87e6a6196db
⛔ Files ignored due to path filters (4)
apps/mobile-flutter/assets/google_fonts/BeVietnamPro-Bold.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/BeVietnamPro-Medium.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/BeVietnamPro-Regular.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/BeVietnamPro-SemiBold.ttfis excluded by!**/*.ttf
📒 Files selected for processing (77)
apps/mobile-flutter/assets/l10n/en.jsonapps/mobile-flutter/assets/l10n/vi.jsonapps/mobile-flutter/lib/features/auth/widgets/apple_button.dartapps/mobile-flutter/lib/features/auth/widgets/auth_page.dartapps/mobile-flutter/lib/features/auth/widgets/auth_submit_button.dartapps/mobile-flutter/lib/features/auth/widgets/auth_text_field.dartapps/mobile-flutter/lib/features/auth/widgets/confirm_email_view.dartapps/mobile-flutter/lib/features/auth/widgets/google_button.dartapps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dartapps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dartapps/mobile-flutter/lib/features/dashboard/widgets/compact_weight_log.dartapps/mobile-flutter/lib/features/dashboard/widgets/dashboard_tokens.dartapps/mobile-flutter/lib/features/dashboard/widgets/section_header.dartapps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dartapps/mobile-flutter/lib/features/dashboard/widgets/today_section.dartapps/mobile-flutter/lib/features/dashboard/widgets/weight_chart.dartapps/mobile-flutter/lib/features/logging/screens/logging_screen.dartapps/mobile-flutter/lib/features/logging/widgets/dashed_divider.dartapps/mobile-flutter/lib/features/logging/widgets/empty_state.dartapps/mobile-flutter/lib/features/logging/widgets/feed_area.dartapps/mobile-flutter/lib/features/logging/widgets/manual_log_sheet.dartapps/mobile-flutter/lib/features/logging/widgets/meal_entry.dartapps/mobile-flutter/lib/features/logging/widgets/meal_input.dartapps/mobile-flutter/lib/features/logging/widgets/meal_mode_sheet.dartapps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dartapps/mobile-flutter/lib/features/logging/widgets/streaming_entry.dartapps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dartapps/mobile-flutter/lib/features/logging/widgets/timeline_rail.dartapps/mobile-flutter/lib/features/nutrition/logic/candidate_nutrients.dartapps/mobile-flutter/lib/features/nutrition/providers/candidates_response.dartapps/mobile-flutter/lib/features/nutrition/providers/food_candidates_provider.dartapps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dartapps/mobile-flutter/lib/features/nutrition/widgets/background_section.dartapps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dartapps/mobile-flutter/lib/features/nutrition/widgets/day_summary.dartapps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dartapps/mobile-flutter/lib/features/nutrition/widgets/focus_section.dartapps/mobile-flutter/lib/features/nutrition/widgets/food_chip_row.dartapps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dartapps/mobile-flutter/lib/features/nutrition/widgets/nutrient_grid_card.dartapps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dartapps/mobile-flutter/lib/features/nutrition/widgets/pull_quote.dartapps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dartapps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dartapps/mobile-flutter/lib/features/nutrition/widgets/spotlight_row.dartapps/mobile-flutter/lib/features/nutrition/widgets/steady_section.dartapps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dartapps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dartapps/mobile-flutter/lib/features/onboarding/widgets/onboarding_wizard.dartapps/mobile-flutter/lib/features/settings/screens/account_section.dartapps/mobile-flutter/lib/features/settings/screens/settings_screen.dartapps/mobile-flutter/lib/main.dartapps/mobile-flutter/lib/models/nutrition.dartapps/mobile-flutter/lib/shared/widgets/target_progress_bar.dartapps/mobile-flutter/lib/shared/widgets/top_toast.dartapps/mobile-flutter/lib/shell/app_header.dartapps/mobile-flutter/lib/shell/sidebar.dartapps/mobile-flutter/lib/theme/nham_colors.dartapps/mobile-flutter/lib/theme/nham_typography.dartapps/mobile-flutter/pubspec.yamlcomponents/nutrition/rows/food-chip-row.tsxcomponents/nutrition/sections/verdict-hero.tsxlib/api/contracts/nutrition.tslib/nutrition/__tests__/actions/candidates.test.tslib/nutrition/__tests__/catalog/food-source-candidates.test.tslib/nutrition/__tests__/catalog/reference-targets.test.tslib/nutrition/__tests__/schemas.test.tslib/nutrition/actions/candidates.tslib/nutrition/actions/overview/mapper.tslib/nutrition/catalog/food-source-candidates.tslib/nutrition/catalog/nutrients.tslib/nutrition/catalog/reference-targets.tslib/nutrition/pattern/date-range.tslib/nutrition/pattern/summary.tslib/nutrition/schemas.tslib/nutrition/types.tsmessages/en.json
💤 Files with no reviewable changes (13)
- apps/mobile-flutter/lib/features/logging/widgets/dashed_divider.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/food_chip_row.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/spotlight_row.dart
- apps/mobile-flutter/lib/features/logging/widgets/timeline_rail.dart
- apps/mobile-flutter/lib/features/nutrition/logic/candidate_nutrients.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/background_section.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/steady_section.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/pull_quote.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/focus_section.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dart
✅ Files skipped from review due to trivial changes (3)
- apps/mobile-flutter/lib/features/auth/widgets/google_button.dart
- apps/mobile-flutter/lib/features/auth/widgets/auth_submit_button.dart
- apps/mobile-flutter/assets/l10n/en.json
🚧 Files skipped from review as they are similar to previous changes (13)
- apps/mobile-flutter/lib/main.dart
- apps/mobile-flutter/lib/features/auth/widgets/auth_text_field.dart
- apps/mobile-flutter/lib/features/auth/widgets/confirm_email_view.dart
- apps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dart
- apps/mobile-flutter/lib/features/settings/screens/account_section.dart
- apps/mobile-flutter/lib/features/auth/widgets/auth_page.dart
- apps/mobile-flutter/lib/features/logging/screens/logging_screen.dart
- apps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dart
- apps/mobile-flutter/lib/features/dashboard/widgets/today_section.dart
- apps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dart
- apps/mobile-flutter/lib/features/settings/screens/settings_screen.dart
- apps/mobile-flutter/assets/l10n/vi.json
- apps/mobile-flutter/lib/features/logging/widgets/meal_entry.dart
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart (2)
572-635: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winKeep pull-to-refresh available for empty feeds.
RefreshIndicator.adaptiveonly wraps the populatedListView; empty, loading, and pending-only states use plainSingleChildScrollView, so users cannot refresh when the feed has no persisted meals.Suggested direction
+ Widget _refreshable(Widget child) => RefreshIndicator.adaptive( + onRefresh: _refresh, + color: NhamColors.accent, + child: child, + ); + @@ - return SingleChildScrollView( + return _refreshable(SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + physics: const AlwaysScrollableScrollPhysics(), child: Padding( padding: const EdgeInsets.symmetric(vertical: NhamSpacing.sp6), child: Center(child: body), ), - ); + ));Apply the same wrapper/physics to the other empty or footer-only scroll branches.
Also applies to: 638-641
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart` around lines 572 - 635, The empty/loading/footer-only branches in feed_area.dart currently return plain SingleChildScrollView, so they bypass the RefreshIndicator.adaptive used by the populated ListView. Update the scroll branches around the persistedMeals.isEmpty logic to use the same refreshable wrapper and scroll physics as the main feed path, including the body-only and _Footer rendering cases, so pull-to-refresh remains available when there are no persisted meals.
273-285: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPin failed-attempt cards to the submitted day.
Streaming/reveal cards are date-guarded, but
_failedTextis not. If an analysis fails after the user switches dates, the retry card and restored composer text can appear on the wrong day.Suggested fix
String? _failedText; + String? _failedDate; @@ setState(() { _failedText = null; + _failedDate = null; _revealRawInput = null; _inFlightText = text; }); @@ if (next.status == StreamStatus.error) { final text = _inFlightText; + final failedDate = next.loggedDate ?? widget.date; setState(() { _failedText = text; + _failedDate = failedDate; _inFlightText = null; }); - if (text != null && _inputController.getText().trim().isEmpty) { + if (failedDate == widget.date && + text != null && + _inputController.getText().trim().isEmpty) { _inputController.setText(text); } @@ - final hasFailedAttempt = _failedText != null; + final hasFailedAttempt = + _failedText != null && _failedDate == widget.date;Also applies to: 362-376
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart` around lines 273 - 285, The failed-attempt retry card is not pinned to the day it was submitted, so it can show on a different selected date after a stream error. Update the error handling in feed_area.dart around the StreamStatus.error path in the feed/state flow (including the _failedText logic and the card rendering path) to capture the active submitted day when the request starts and only surface the failed card when the current day still matches that captured day. Apply the same date-guarding behavior to the related retry-card rendering code referenced by the 362-376 block so the restored composer text and failed attempt stay tied to the original day.apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart (1)
460-503: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDisable future-day cells before they reach logging flows.
isFutureis computed, but the tap handler still selects those dates. Because the current centered week includes days after today, users can navigate to and log against future dates.Suggested fix
- final labelColor = isSelected ? NhamColors.text : NhamColors.textMuted; + final disabled = isFuture; + final labelColor = + disabled + ? NhamColors.textMuted50 + : isSelected + ? NhamColors.text + : NhamColors.textMuted; @@ - onTapDown: (_) => setState(() => _pressed = true), - onTapUp: (_) => setState(() => _pressed = false), - onTapCancel: () => setState(() => _pressed = false), - onTap: () => widget.onSelect(widget.date), + onTapDown: disabled ? null : (_) => setState(() => _pressed = true), + onTapUp: disabled ? null : (_) => setState(() => _pressed = false), + onTapCancel: disabled ? null : () => setState(() => _pressed = false), + onTap: disabled ? null : () => widget.onSelect(widget.date),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart` around lines 460 - 503, Future-day cells in the TimelinePicker are still tappable even though isFuture is already computed. Update the day cell logic in the widget build/tap handling so future dates cannot be selected or sent to widget.onSelect, and keep the existing visual treatment for isFuture consistent with a disabled state. Use the isFuture, GestureDetector, and onSelect paths in timeline_picker.dart to gate both tap callbacks and pressed feedback for future dates.
🧹 Nitpick comments (1)
components/nutrition/rows/food-chip-row.tsx (1)
27-65: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDestructure the
useQueryresult.Reading the whole query object opts out of TanStack Query's tracked-property optimization, so this component subscribes to more state than it uses. Please destructure the fields you read (
data,error,isError,isLoading,isSuccess) and verify that this is intentional.♻️ Proposed fix
- const query = useQuery({ + const { data, error, isError, isLoading, isSuccess } = useQuery({ queryKey: ['nutrition', 'candidates', nutrient], queryFn: () => getFoodSourceCandidates({ nutrient }), enabled, retry: false, staleTime: 60_000, }); - if (query.isLoading) { + if (isLoading) { return <ChipRowSkeleton variant={variant} />; } @@ - if (query.isError) { + if (isError) { console.error( '[nutrition] candidates query failed for nutrient', nutrient, - query.error + error ); @@ - if (query.isSuccess && !query.data.foods.length) { + if (isSuccess && !data.foods.length) { @@ - if (!query.data?.foods.length) { + if (!data?.foods.length) { @@ - const chips = query.data.foods.slice(0, limit); + const chips = data.foods.slice(0, limit);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/nutrition/rows/food-chip-row.tsx` around lines 27 - 65, The food-chip-row component is reading the full useQuery result object, which disables TanStack Query’s tracked-property optimization. Update the useQuery call in food-chip-row.tsx to destructure only the fields actually used in the render path, namely data, error, isError, isLoading, and isSuccess, and then use those local bindings throughout the component to keep subscriptions minimal.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart`:
- Around line 15-17: Replace the custom Apple glyph/button in AppleButton with
Apple’s official SignInWithAppleButton control so branding and platform behavior
come from the system-provided widget. Update the AppleButton widget
implementation to use the provided Apple sign-in button rather than the
hand-rolled UI, keeping the existing auth action wiring intact while preserving
the current placement within the auth flow.
- Around line 39-47: The auth CTA in AppleButton is built with GestureDetector
and Opacity, so it is not exposed as a real accessible button or disabled
control. Replace the current interaction wrapper in AppleButton’s build logic
with a proper button primitive that supports semantics, focus, and disabled
state, and keep the pressed/busy visual behavior tied to that button instead of
manual tap handlers.
In `@apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart`:
- Around line 26-42: The shimmer still runs its AnimationController even when
reduced-motion is enabled, because _ShimmerState::_c starts repeating
unconditionally and build() only skips rendering. Update Shimmer/_ShimmerState
to start and stop the controller based on
MediaQuery.disableAnimationsOf(context) (or an equivalent media-change hook) so
the ticker is paused when animations are disabled and resumed when they are
allowed.
In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart`:
- Around line 245-255: Initialize _visibleAnchor from the same anchor used by
PageController initialPage instead of _selectedAnchor, so TimelinePicker starts
with the actual page being shown. Update the setup around _visibleAnchor,
_pageController, and _canNavigateNext in timeline_picker.dart so the right
chevron reflects the real visible page when selectedDate is on the current page.
Also make the same adjustment anywhere the current-page visibility is derived in
the related logic around the page handling flow.
In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart`:
- Around line 138-142: The nutrient grouping in nutrition_screen.dart is
treating every non-vitamin item as a mineral, so NutrientGroup.other can be
rendered under the wrong header. Update the logic around the
all/vitamins/minerals lists in the nutrition screen to filter minerals
explicitly by NutrientGroup.mineral (or equivalent) and keep NutrientGroup.other
separate so it does not fall into the Minerals section.
In `@apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart`:
- Around line 171-176: The week label formatting in _label currently hard-codes
day/month order, so update the week branch to use the locale-aware short date
skeleton instead of the fixed pattern. Keep the existing weekday logic for
non-week units, and make the change in macro_trend_chart.dart within the _label
helper so week labels follow each locale’s conventions.
In `@apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart`:
- Around line 37-38: The active range selection in range_selector.dart is using
_ranges.indexOf(resolvedRange) directly, so unsupported model values like 90d
resolve to -1 and get clamped to the first segment. Update the RangeSelector
logic to normalize resolvedRange to one of the supported _ranges values before
calculating activeIndex, and base the selected segment on that normalized value
so backend-resolved auto values map to the intended UI range.
- Around line 42-44: The range selector touch target is too small because the
SizedBox in range_selector.dart constrains the control to 34px height. Update
the layout in the range selector widget so the tappable area reaches at least
44pt, adjusting the parent SizedBox and any internal padding/segment sizing in
the range selector implementation to preserve the visual design while meeting
the target.
In `@apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart`:
- Around line 49-54: The citations bottom sheet in source_attribution.dart is
using a non-scrollable Column, which can overflow with longer localized text or
larger accessibility fonts. Update the sheet layout in the widget built by the
builder and any related citation content sections so the content is wrapped in a
scrollable container such as SingleChildScrollView or ListView, while preserving
the existing Container styling and bottom-sheet structure.
In
`@apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart`:
- Around line 235-247: The chip actions in SuggestedFoodsSheet are interactive
but their current padding can fall below the 44pt mobile touch target. Update
the chip/widget builders in suggested_foods_sheet.dart, especially the
GestureDetector/AnimatedContainer path for the chip item and the refresh row
block, so their tappable area is at least 44pt tall while preserving the
existing layout and visuals. Use the existing chip action widget logic in the
affected builder functions to add vertical padding/minimum constraints rather
than shrinking the hit area.
- Around line 176-180: The rotating slot count in suggested_foods_sheet.dart is
widened to num by clamp, which then breaks the _cursor update logic. Update the
rotatingSlots calculation in the logic that builds the rotating FoodCandidate
list so it is converted back to int before being used in the modulo/update path,
keeping the _cursor assignment type-safe. Refer to the rotatingSlots variable
and the _cursor update in the suggested_foods_sheet widget logic when making the
fix.
In `@apps/mobile-flutter/lib/shared/widgets/top_toast.dart`:
- Around line 22-48: `showTopToast` currently inserts a new `OverlayEntry` every
time, which allows multiple top toasts to overlap; update the toast flow to
serialize or replace the active entry so only one `_TopToast` can be visible at
once. Use the existing `showTopToast`, `OverlayEntry`, and `_TopToast` logic to
track the current toast and either dismiss it or queue the next toast before
calling `overlay.insert`, ensuring the undo toast in the logging feed can remain
visible and actionable.
In `@apps/mobile-flutter/lib/shell/app_header.dart`:
- Around line 41-56: The current AppHeader layout in the build method still
centers the middle content relative to only a fixed spacer, so any wider
trailing widget shifts the title off-center. Update the Row-based structure in
app_header.dart so leading and trailing are independently anchored while the
middle child is centered against the full available header width, preserving the
centering contract for variable-width trailing content like the nutrition range
selector.
In `@apps/mobile-flutter/lib/shell/sidebar.dart`:
- Around line 97-102: The sidebar filtering in sidebar.dart hardcodes isAdmin to
false, which hides the adminOnly nav item for every user. Update the logic
around the _navItems filter to use the real admin role/claim source instead of
the constant, or remove the /admin entry until an isAdmin provider is available.
Keep the fix localized to the sidebar item selection path so the admin gating
reflects actual session state.
---
Outside diff comments:
In `@apps/mobile-flutter/lib/features/logging/widgets/feed_area.dart`:
- Around line 572-635: The empty/loading/footer-only branches in feed_area.dart
currently return plain SingleChildScrollView, so they bypass the
RefreshIndicator.adaptive used by the populated ListView. Update the scroll
branches around the persistedMeals.isEmpty logic to use the same refreshable
wrapper and scroll physics as the main feed path, including the body-only and
_Footer rendering cases, so pull-to-refresh remains available when there are no
persisted meals.
- Around line 273-285: The failed-attempt retry card is not pinned to the day it
was submitted, so it can show on a different selected date after a stream error.
Update the error handling in feed_area.dart around the StreamStatus.error path
in the feed/state flow (including the _failedText logic and the card rendering
path) to capture the active submitted day when the request starts and only
surface the failed card when the current day still matches that captured day.
Apply the same date-guarding behavior to the related retry-card rendering code
referenced by the 362-376 block so the restored composer text and failed attempt
stay tied to the original day.
In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart`:
- Around line 460-503: Future-day cells in the TimelinePicker are still tappable
even though isFuture is already computed. Update the day cell logic in the
widget build/tap handling so future dates cannot be selected or sent to
widget.onSelect, and keep the existing visual treatment for isFuture consistent
with a disabled state. Use the isFuture, GestureDetector, and onSelect paths in
timeline_picker.dart to gate both tap callbacks and pressed feedback for future
dates.
---
Nitpick comments:
In `@components/nutrition/rows/food-chip-row.tsx`:
- Around line 27-65: The food-chip-row component is reading the full useQuery
result object, which disables TanStack Query’s tracked-property optimization.
Update the useQuery call in food-chip-row.tsx to destructure only the fields
actually used in the render path, namely data, error, isError, isLoading, and
isSuccess, and then use those local bindings throughout the component to keep
subscriptions minimal.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b31d7c71-026d-49b9-8223-d87e6a6196db
⛔ Files ignored due to path filters (4)
apps/mobile-flutter/assets/google_fonts/BeVietnamPro-Bold.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/BeVietnamPro-Medium.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/BeVietnamPro-Regular.ttfis excluded by!**/*.ttfapps/mobile-flutter/assets/google_fonts/BeVietnamPro-SemiBold.ttfis excluded by!**/*.ttf
📒 Files selected for processing (77)
apps/mobile-flutter/assets/l10n/en.jsonapps/mobile-flutter/assets/l10n/vi.jsonapps/mobile-flutter/lib/features/auth/widgets/apple_button.dartapps/mobile-flutter/lib/features/auth/widgets/auth_page.dartapps/mobile-flutter/lib/features/auth/widgets/auth_submit_button.dartapps/mobile-flutter/lib/features/auth/widgets/auth_text_field.dartapps/mobile-flutter/lib/features/auth/widgets/confirm_email_view.dartapps/mobile-flutter/lib/features/auth/widgets/google_button.dartapps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dartapps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dartapps/mobile-flutter/lib/features/dashboard/widgets/compact_weight_log.dartapps/mobile-flutter/lib/features/dashboard/widgets/dashboard_tokens.dartapps/mobile-flutter/lib/features/dashboard/widgets/section_header.dartapps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dartapps/mobile-flutter/lib/features/dashboard/widgets/today_section.dartapps/mobile-flutter/lib/features/dashboard/widgets/weight_chart.dartapps/mobile-flutter/lib/features/logging/screens/logging_screen.dartapps/mobile-flutter/lib/features/logging/widgets/dashed_divider.dartapps/mobile-flutter/lib/features/logging/widgets/empty_state.dartapps/mobile-flutter/lib/features/logging/widgets/feed_area.dartapps/mobile-flutter/lib/features/logging/widgets/manual_log_sheet.dartapps/mobile-flutter/lib/features/logging/widgets/meal_entry.dartapps/mobile-flutter/lib/features/logging/widgets/meal_input.dartapps/mobile-flutter/lib/features/logging/widgets/meal_mode_sheet.dartapps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dartapps/mobile-flutter/lib/features/logging/widgets/streaming_entry.dartapps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dartapps/mobile-flutter/lib/features/logging/widgets/timeline_rail.dartapps/mobile-flutter/lib/features/nutrition/logic/candidate_nutrients.dartapps/mobile-flutter/lib/features/nutrition/providers/candidates_response.dartapps/mobile-flutter/lib/features/nutrition/providers/food_candidates_provider.dartapps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dartapps/mobile-flutter/lib/features/nutrition/widgets/background_section.dartapps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dartapps/mobile-flutter/lib/features/nutrition/widgets/day_summary.dartapps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dartapps/mobile-flutter/lib/features/nutrition/widgets/focus_section.dartapps/mobile-flutter/lib/features/nutrition/widgets/food_chip_row.dartapps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dartapps/mobile-flutter/lib/features/nutrition/widgets/nutrient_grid_card.dartapps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dartapps/mobile-flutter/lib/features/nutrition/widgets/pull_quote.dartapps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dartapps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dartapps/mobile-flutter/lib/features/nutrition/widgets/spotlight_row.dartapps/mobile-flutter/lib/features/nutrition/widgets/steady_section.dartapps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dartapps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dartapps/mobile-flutter/lib/features/onboarding/widgets/onboarding_wizard.dartapps/mobile-flutter/lib/features/settings/screens/account_section.dartapps/mobile-flutter/lib/features/settings/screens/settings_screen.dartapps/mobile-flutter/lib/main.dartapps/mobile-flutter/lib/models/nutrition.dartapps/mobile-flutter/lib/shared/widgets/target_progress_bar.dartapps/mobile-flutter/lib/shared/widgets/top_toast.dartapps/mobile-flutter/lib/shell/app_header.dartapps/mobile-flutter/lib/shell/sidebar.dartapps/mobile-flutter/lib/theme/nham_colors.dartapps/mobile-flutter/lib/theme/nham_typography.dartapps/mobile-flutter/pubspec.yamlcomponents/nutrition/rows/food-chip-row.tsxcomponents/nutrition/sections/verdict-hero.tsxlib/api/contracts/nutrition.tslib/nutrition/__tests__/actions/candidates.test.tslib/nutrition/__tests__/catalog/food-source-candidates.test.tslib/nutrition/__tests__/catalog/reference-targets.test.tslib/nutrition/__tests__/schemas.test.tslib/nutrition/actions/candidates.tslib/nutrition/actions/overview/mapper.tslib/nutrition/catalog/food-source-candidates.tslib/nutrition/catalog/nutrients.tslib/nutrition/catalog/reference-targets.tslib/nutrition/pattern/date-range.tslib/nutrition/pattern/summary.tslib/nutrition/schemas.tslib/nutrition/types.tsmessages/en.json
💤 Files with no reviewable changes (13)
- apps/mobile-flutter/lib/features/logging/widgets/dashed_divider.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/food_chip_row.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/spotlight_row.dart
- apps/mobile-flutter/lib/features/logging/widgets/timeline_rail.dart
- apps/mobile-flutter/lib/features/nutrition/logic/candidate_nutrients.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/daily_rhythm.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/background_section.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/verdict_hero.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/steady_section.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/nutrient_row.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/pull_quote.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/focus_section.dart
- apps/mobile-flutter/lib/features/nutrition/widgets/editorial_header.dart
✅ Files skipped from review due to trivial changes (3)
- apps/mobile-flutter/lib/features/auth/widgets/google_button.dart
- apps/mobile-flutter/lib/features/auth/widgets/auth_submit_button.dart
- apps/mobile-flutter/assets/l10n/en.json
🚧 Files skipped from review as they are similar to previous changes (13)
- apps/mobile-flutter/lib/main.dart
- apps/mobile-flutter/lib/features/auth/widgets/auth_text_field.dart
- apps/mobile-flutter/lib/features/auth/widgets/confirm_email_view.dart
- apps/mobile-flutter/lib/features/logging/widgets/persisted_meal_card.dart
- apps/mobile-flutter/lib/features/settings/screens/account_section.dart
- apps/mobile-flutter/lib/features/auth/widgets/auth_page.dart
- apps/mobile-flutter/lib/features/logging/screens/logging_screen.dart
- apps/mobile-flutter/lib/features/dashboard/screens/dashboard_screen.dart
- apps/mobile-flutter/lib/features/dashboard/widgets/today_section.dart
- apps/mobile-flutter/lib/features/dashboard/widgets/adherence_heatmap.dart
- apps/mobile-flutter/lib/features/settings/screens/settings_screen.dart
- apps/mobile-flutter/assets/l10n/vi.json
- apps/mobile-flutter/lib/features/logging/widgets/meal_entry.dart
🛑 Comments failed to post (14)
apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart (2)
15-17: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail echo "== AppleButton call sites ==" rg -n -C3 '\bAppleButton\s*\(' apps/mobile-flutter/lib echo echo "== Platform gating near auth flow ==" rg -n -C3 'Platform\.isIOS|TargetPlatform\.iOS|defaultTargetPlatform|kIsWeb' \ apps/mobile-flutter/lib/features/authRepository: VoMinhKhoii/Nham
Length of output: 2489
🌐 Web query:
What are Apple's current Sign in with Apple branding requirements for custom buttons, and do they allow replacing the provided button with a custom-rendered Apple logo glyph?💡 Result:
Apple's branding requirements for Sign in with Apple prioritize the use of system-provided buttons, which are specifically designed to ensure a consistent user experience and maintain brand integrity [1][2]. Apple provides dedicated APIs for native apps (e.g., ASAuthorizationAppleIDButton) and web integrations (Sign in with Apple JS) that allow for standardized customization, such as adjusting button styles, text labels (e.g., "Sign in with Apple" vs "Sign up with Apple"), and alignment [3][4]. Regarding your specific questions: 1. Branding Requirements for Custom Buttons: Apple strongly recommends and typically requires the use of their provided button components [1][5][6]. While developers sometimes attempt to build custom buttons, they must strictly adhere to the Human Interface Guidelines (HIG) to avoid App Store rejection [7]. Official documentation consistently emphasizes that you should use the provided Apple-generated button artwork and assets rather than creating your own custom interpretations [8]. 2. Replacing the Button with a Custom-Rendered Apple Logo Glyph: You are not permitted to create your own custom-rendered Apple logo or replace the provided button with a custom implementation [8]. Apple requires you to use the logo artwork downloaded from their official Design Resources [8]. If you need a minimalist button, Apple provides an official "logo-only" mode (available in both web and native API implementations) that displays the Apple logo without accompanying text [3]. You should use this official configuration rather than attempting to render a custom glyph or button [3][8]. In summary, for compliance and to avoid rejection during app review, you should use the official API-provided buttons and choose from the supported configuration modes (such as "logo-only" or standard text-based buttons) provided by Apple's development frameworks [1][4][7].
Citations:
- 1: https://developer.apple.com/sign-in-with-apple/usage-guidelines-for-websites-and-other-platforms/
- 2: https://developer.apple.com/news/?id=03042020d
- 3: https://developer.apple.com/documentation/signinwithapple/displaying-sign-in-with-apple-buttons-on-the-web
- 4: https://developer.apple.com/documentation/signinwithapple/displaying-sign-in-with-apple-buttons-in-your-app
- 5: https://blog.xmartlabs.com/blog/why-sign-in-with-apple-and-integration-guide/
- 6: https://sarunw.com/posts/sign-in-with-apple-1/
- 7: https://johncodeos.com/how-to-add-sign-in-with-apple-button-to-your-ios-app-using-swift/
- 8: https://developer.apple.com/forums/thread/763171
Use Apple’s provided Sign in with Apple button here. This custom Apple glyph/button is shown on both iOS and macOS, and Apple’s branding rules call for the official control rather than a hand-rolled logo.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart` around lines 15 - 17, Replace the custom Apple glyph/button in AppleButton with Apple’s official SignInWithAppleButton control so branding and platform behavior come from the system-provided widget. Update the AppleButton widget implementation to use the provided Apple sign-in button rather than the hand-rolled UI, keeping the existing auth action wiring intact while preserving the current placement within the auth flow.
39-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use a real button primitive here.
GestureDetector+Opacitydoes not expose the auth CTA as a proper button/disabled control to accessibility services. That makes the Apple sign-in entry point unreliable for VoiceOver/Switch Control users.Suggested direction
- child: GestureDetector( - onTapDown: - widget.busy ? null : (_) => setState(() => _pressed = true), - onTapUp: widget.busy ? null : (_) => setState(() => _pressed = false), - onTapCancel: - widget.busy ? null : () => setState(() => _pressed = false), - onTap: widget.busy ? null : widget.onPressed, + child: Semantics( + button: true, + enabled: !widget.busy, + label: tr('auth.dialog.continueWithApple'), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.busy ? null : widget.onPressed, + onHighlightChanged: (pressed) { + if (_pressed != pressed) { + setState(() => _pressed = pressed); + } + }, + borderRadius: BorderRadius.circular(NhamRadii.buttonXl),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.return Opacity( opacity: widget.busy ? 0.6 : 1.0, child: Semantics( button: true, enabled: !widget.busy, label: tr('auth.dialog.continueWithApple'), child: Material( color: Colors.transparent, child: InkWell( onTap: widget.busy ? null : widget.onPressed, onHighlightChanged: (pressed) { if (_pressed != pressed) { setState(() => _pressed = pressed); } }, borderRadius: BorderRadius.circular(NhamRadii.buttonXl),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/auth/widgets/apple_button.dart` around lines 39 - 47, The auth CTA in AppleButton is built with GestureDetector and Opacity, so it is not exposed as a real accessible button or disabled control. Replace the current interaction wrapper in AppleButton’s build logic with a proper button primitive that supports semantics, focus, and disabled state, and keep the pressed/busy visual behavior tied to that button instead of manual tap handlers.apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart (1)
26-42: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
Stop the shimmer ticker when animations are disabled.
_cstarts repeating ininitState(), andbuild()only skips theShaderMask. Reduced-motion users still pay for a live animation controller on every frame. Start/stop the controller from the current media setting instead of only bypassing the paint effect.Suggested direction
class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin { late final AnimationController _c = AnimationController( vsync: this, duration: const Duration(milliseconds: 1300), - )..repeat(); + ); + + bool? _animationsDisabled; + + `@override` + void didChangeDependencies() { + super.didChangeDependencies(); + final disabled = MediaQuery.disableAnimationsOf(context); + if (_animationsDisabled == disabled) return; + _animationsDisabled = disabled; + if (disabled) { + _c.stop(); + } else { + _c.repeat(); + } + }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin { late final AnimationController _c = AnimationController( vsync: this, duration: const Duration(milliseconds: 1300), ); bool? _animationsDisabled; `@override` void didChangeDependencies() { super.didChangeDependencies(); final disabled = MediaQuery.disableAnimationsOf(context); if (_animationsDisabled == disabled) return; _animationsDisabled = disabled; if (disabled) { _c.stop(); } else { _c.repeat(); } } `@override` void dispose() { _c.dispose(); super.dispose(); } `@override` Widget build(BuildContext context) { // Honor reduced-motion: hold a static base tint, no sweep. if (MediaQuery.disableAnimationsOf(context)) return widget.child; return AnimatedBuilder(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/dashboard/widgets/skeleton.dart` around lines 26 - 42, The shimmer still runs its AnimationController even when reduced-motion is enabled, because _ShimmerState::_c starts repeating unconditionally and build() only skips rendering. Update Shimmer/_ShimmerState to start and stop the controller based on MediaQuery.disableAnimationsOf(context) (or an equivalent media-change hook) so the ticker is paused when animations are disabled and resumed when they are allowed.apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart (1)
245-255: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Initialize
_visibleAnchorfrom the actual initial page.For selected dates still rendered on the current page,
_visibleAnchorcan be a past date while thePageViewis already on today’s page. That leaves the right chevron enabled even though there is no future page.Suggested fix
class _TimelineStripState extends State<TimelineStrip> { - late String _visibleAnchor = _selectedAnchor; + late final int _initialPage = _pageForAnchor(_selectedAnchor); + late String _visibleAnchor = _anchorForPage(_initialPage); late final PageController _pageController = - PageController(initialPage: _pageForAnchor(_selectedAnchor)); + PageController(initialPage: _initialPage);Also applies to: 309-344
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/logging/widgets/timeline_picker.dart` around lines 245 - 255, Initialize _visibleAnchor from the same anchor used by PageController initialPage instead of _selectedAnchor, so TimelinePicker starts with the actual page being shown. Update the setup around _visibleAnchor, _pageController, and _canNavigateNext in timeline_picker.dart so the right chevron reflects the real visible page when selectedDate is on the current page. Also make the same adjustment anywhere the current-page visibility is derived in the related logic around the page handling flow.apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart (1)
138-142: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Don't group
NutrientGroup.otherunder “Minerals”.Line 142 treats every non-vitamin as a mineral, but the model also has
NutrientGroup.other. Those cards will render under the wrong header as soon as that enum value appears.Proposed fix
final all = [...overview.micronutrients, ...overview.moreNutrients]; final vitamins = all.where((c) => c.group == NutrientGroup.vitamin).toList(); -final minerals = - all.where((c) => c.group != NutrientGroup.vitamin).toList(); +final minerals = + all.where((c) => c.group == NutrientGroup.mineral).toList(); +final other = + all.where((c) => c.group == NutrientGroup.other).toList();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/nutrition/screens/nutrition_screen.dart` around lines 138 - 142, The nutrient grouping in nutrition_screen.dart is treating every non-vitamin item as a mineral, so NutrientGroup.other can be rendered under the wrong header. Update the logic around the all/vitamins/minerals lists in the nutrition screen to filter minerals explicitly by NutrientGroup.mineral (or equivalent) and keep NutrientGroup.other separate so it does not fall into the Minerals section.apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart (1)
171-176: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
In Dart intl 0.20.2, isDateFormat.Md(locale)the correct locale-aware replacement forDateFormat('d/M', locale)when formatting short month/day labels?💡 Result:
Yes, DateFormat.Md(locale) is the recommended and correct locale-aware replacement for a manually defined pattern like DateFormat('d/M', locale) [1]. In the Dart intl package, DateFormat.Md is a named constructor that uses a predefined "skeleton" format [2][1]. When you use a skeleton constructor, the package automatically looks up the appropriate date format pattern for the specified locale [1]. Conversely, when you provide an explicit string pattern—such as 'd/M'—to the DateFormat constructor, the package treats it as a static format string, meaning it will not be adapted to suit local conventions [1]. By using DateFormat.Md(locale), you ensure that the date is displayed according to the formatting rules defined for that specific locale in the CLDR (Common Locale Data Repository), rather than forcing a specific 'd/M' structure that may be incorrect or unnatural for certain cultures [1].
Citations:
- 1: http://api.flutter.dev/flutter/package-intl_intl/DateFormat/DateFormat.html
- 2: https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html
Use the locale-aware month/day skeleton for week labels.
DateFormat('d/M', locale)hard-codes the order, so locales that expect month/day formatting won’t get their local short date pattern.DateFormat.Md(locale)will adapt the label to the locale’s conventions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/nutrition/widgets/macro_trend_chart.dart` around lines 171 - 176, The week label formatting in _label currently hard-codes day/month order, so update the week branch to use the locale-aware short date skeleton instead of the fixed pattern. Keep the existing weekday logic for non-week units, and make the change in macro_trend_chart.dart within the _label helper so week labels follow each locale’s conventions.apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart (2)
37-38: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Normalize unsupported resolved ranges before clamping.
resolvedRangecan still be90dat the model layer, but_ranges.indexOf('90d')is-1, so this highlights the1dsegment. If the backend resolvesautoto90d, the control will show the wrong active range.Proposed fix
- final activeIndex = - _ranges.indexOf(resolvedRange).clamp(0, _ranges.length - 1); + final normalizedRange = + _ranges.contains(resolvedRange) ? resolvedRange : '30d'; + final activeIndex = _ranges.indexOf(normalizedRange);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.final normalizedRange = _ranges.contains(resolvedRange) ? resolvedRange : '30d'; final activeIndex = _ranges.indexOf(normalizedRange);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart` around lines 37 - 38, The active range selection in range_selector.dart is using _ranges.indexOf(resolvedRange) directly, so unsupported model values like 90d resolve to -1 and get clamped to the first segment. Update the RangeSelector logic to normalize resolvedRange to one of the supported _ranges values before calculating activeIndex, and base the selected segment on that normalized value so backend-resolved auto values map to the intended UI range.
42-44: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
This control is still below the 44pt touch-target goal.
The selector is rendered at 34px tall here, so each segment falls short of the 44pt target called out in the PR objectives. That makes the primary range switch harder to hit than the surrounding header controls.
Proposed fix
child: SizedBox( width: 168, - height: 34, + height: 44, child: Container(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.child: SizedBox( width: 168, height: 44,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/nutrition/widgets/range_selector.dart` around lines 42 - 44, The range selector touch target is too small because the SizedBox in range_selector.dart constrains the control to 34px height. Update the layout in the range selector widget so the tappable area reaches at least 44pt, adjusting the parent SizedBox and any internal padding/segment sizing in the range selector implementation to preserve the visual design while meeting the target.apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart (1)
49-54: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the citations sheet scrollable.
The localized citation copy is in a non-scrollable
Column, so longer translations or large text settings can overflow the bottom sheet.Proposed fix
context: context, backgroundColor: Colors.transparent, builder: (_) => Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.8, + ), decoration: const BoxDecoration( color: NhamColors.surface, borderRadius: BorderRadius.vertical(top: Radius.circular(NhamRadii.xxl)), ), @@ - Padding( - padding: EdgeInsets.fromLTRB( - NhamSpacing.sp5, - NhamSpacing.sp1, - NhamSpacing.sp5, - bottomInset + NhamSpacing.sp5, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(tr('nutrition.sources.intro'), style: dashMeta()), - const SizedBox(height: NhamSpacing.sp4), - _Citation( - label: tr('nutrition.targetSources.whoFao'), - detail: tr('nutrition.sources.whoFao'), - ), - _Citation( - label: tr('nutrition.targetSources.vietnamRda'), - detail: tr('nutrition.sources.vietnamRda'), - ), - _Citation( - label: tr('nutrition.targetSources.nasem'), - detail: tr('nutrition.sources.nasem'), - ), - ], - ), + Flexible( + child: SingleChildScrollView( + padding: EdgeInsets.fromLTRB( + NhamSpacing.sp5, + NhamSpacing.sp1, + NhamSpacing.sp5, + bottomInset + NhamSpacing.sp5, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(tr('nutrition.sources.intro'), style: dashMeta()), + const SizedBox(height: NhamSpacing.sp4), + _Citation( + label: tr('nutrition.targetSources.whoFao'), + detail: tr('nutrition.sources.whoFao'), + ), + _Citation( + label: tr('nutrition.targetSources.vietnamRda'), + detail: tr('nutrition.sources.vietnamRda'), + ), + _Citation( + label: tr('nutrition.targetSources.nasem'), + detail: tr('nutrition.sources.nasem'), + ), + ], + ), + ), ),Also applies to: 84-110
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/nutrition/widgets/source_attribution.dart` around lines 49 - 54, The citations bottom sheet in source_attribution.dart is using a non-scrollable Column, which can overflow with longer localized text or larger accessibility fonts. Update the sheet layout in the widget built by the builder and any related citation content sections so the content is wrapped in a scrollable container such as SingleChildScrollView or ListView, while preserving the existing Container styling and bottom-sheet structure.apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart (2)
176-180: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n== Mobile docs ==\n' for f in apps/docs/mobile/development.md apps/docs/mobile/releasing.md apps/docs/mobile/architecture.md; do echo "--- $f" wc -l "$f" done printf '\n== Suggested foods sheet outline ==\n' ast-grep outline apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart --view expanded || true printf '\n== Relevant lines around 160-220 ==\n' sed -n '160,220p' apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart printf '\n== Search for rotatingSlots/cursor usage ==\n' rg -n "rotatingSlots|_cursor|_visibleCount|reserved.length|clamp\\(" apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dartRepository: VoMinhKhoii/Nham
Length of output: 2849
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n== Suggested foods sheet (lines 220-360) ==\n' sed -n '220,360p' apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart printf '\n== Find widget definitions in file ==\n' rg -n "class _FoodChip|class _RefreshButton|GestureDetector|InkWell|SizedBox|Container|padding|constraints|minimumSize|hit" apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart printf '\n== Search for any explicit tap target guidance in mobile docs ==\n' rg -n "44|touch target|hit area|minimum size|interactive" apps/docs/mobile/development.md apps/docs/mobile/releasing.md apps/docs/mobile/architecture.mdRepository: VoMinhKhoii/Nham
Length of output: 4202
Cast
rotatingSlotstointbefore the_cursorupdate.
clamp(...)widens this value tonum, so the modulo assignment to_cursoratapps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart:207-210does not type-check unlessrotatingSlotsis converted first.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart` around lines 176 - 180, The rotating slot count in suggested_foods_sheet.dart is widened to num by clamp, which then breaks the _cursor update logic. Update the rotatingSlots calculation in the logic that builds the rotating FoodCandidate list so it is converted back to int before being used in the modulo/update path, keeping the _cursor assignment type-safe. Refer to the rotatingSlots variable and the _cursor update in the suggested_foods_sheet widget logic when making the fix.
235-247: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve 44pt touch targets for the chip actions.
The food chips and refresh row are interactive but their current padding can render below the mobile 44pt target size called out by the PR’s ergonomics goals.
Proposed fix
child: AnimatedContainer( duration: const Duration(milliseconds: 120), - constraints: BoxConstraints(maxWidth: maxWidth), + constraints: BoxConstraints(maxWidth: maxWidth, minHeight: 44), padding: EdgeInsets.fromLTRB( reserved ? 8 : 10, 6, @@ behavior: HitTestBehavior.opaque, onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 44), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2),Also applies to: 283-287
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/features/nutrition/widgets/suggested_foods_sheet.dart` around lines 235 - 247, The chip actions in SuggestedFoodsSheet are interactive but their current padding can fall below the 44pt mobile touch target. Update the chip/widget builders in suggested_foods_sheet.dart, especially the GestureDetector/AnimatedContainer path for the chip item and the refresh row block, so their tappable area is at least 44pt tall while preserving the existing layout and visuals. Use the existing chip action widget logic in the affected builder functions to add vertical padding/minimum constraints rather than shrinking the hit area.apps/mobile-flutter/lib/shared/widgets/top_toast.dart (1)
22-48: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Serialize top toasts instead of stacking them.
Each call inserts a fresh
OverlayEntryat the sametop: 0slot, so concurrent toasts overlap. That can hide the undo toast thatapps/mobile-flutter/lib/features/logging/widgets/feed_area.dart:181-206keeps open for 5 seconds before finalizing delete, leaving the delete window running without a visible/actionable affordance. Keep a single active toast or queue replacements before inserting the next entry.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/shared/widgets/top_toast.dart` around lines 22 - 48, `showTopToast` currently inserts a new `OverlayEntry` every time, which allows multiple top toasts to overlap; update the toast flow to serialize or replace the active entry so only one `_TopToast` can be visible at once. Use the existing `showTopToast`, `OverlayEntry`, and `_TopToast` logic to track the current toast and either dismiss it or queue the next toast before calling `overlay.insert`, ensuring the undo toast in the logging feed can remain visible and actionable.apps/mobile-flutter/lib/shell/app_header.dart (1)
41-56: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
trailingbreaks the centering contract.This layout only keeps
childcentered when the right slot is 44px wide. Any widertrailingwidget shifts the header content left, which already affects callers like the nutrition range selector. Use a layout that independently pins leading/trailing and centers the middle content against the full header width.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/shell/app_header.dart` around lines 41 - 56, The current AppHeader layout in the build method still centers the middle content relative to only a fixed spacer, so any wider trailing widget shifts the title off-center. Update the Row-based structure in app_header.dart so leading and trailing are independently anchored while the middle child is centered against the full available header width, preserving the centering contract for variable-width trailing content like the nutrition range selector.apps/mobile-flutter/lib/shell/sidebar.dart (1)
97-102: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Admin users can never see the
/adminitem.
isAdminis hardcoded tofalse, so theadminOnlyrow is filtered out for every session. That ships a missing role gate rather than an admin-only one. Please thread the real role/claim into this filter or remove the item until the role source exists.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/mobile-flutter/lib/shell/sidebar.dart` around lines 97 - 102, The sidebar filtering in sidebar.dart hardcodes isAdmin to false, which hides the adminOnly nav item for every user. Update the logic around the _navItems filter to use the real admin role/claim source instead of the constant, or remove the /admin entry until an isAdmin provider is available. Keep the fix localized to the sidebar item selection path so the admin gating reflects actual session state.
Iron's postmenopausal drop (24 → 10 mg at 50) was a 4-condition special-case in the resolver with a magic 10 that wasn't in the data table. Express it through the existing ageBands mechanism instead, so the resolver's sex branch collapses to a plain lookup. Behavior-identical: age-unknown still resolves to the <50 band (female 24). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bucket unit Two thermo-review cleanups: - supportsCandidates was always true since candidates went universal, and isSpotlightCandidate re-checked SUPPORTED_CANDIDATE_NUTRIENTS (identical to DEFAULT_NUTRIENTS) on cards already filtered to those. Remove the dead field + set/type across the DTO, aggregation, web detail, mobile model, and showChips gates. Behavior-identical. - Replace the duplicated "'1d' || '7d' ? day : week" expression (two copies in the mapper) with a RANGE_BUCKET_UNIT lookup table, matching the other range-keyed records. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The input's filled area rendered ~5pt shorter than the Update button: isDense sizes the InputDecorator fill to its content and centers it inside the SizedBox(52), leaving transparent gaps. Drop the fixed heights and use IntrinsicHeight + CrossAxisAlignment.stretch so the button matches the field's actual filled height (now pixel-equal at 52). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the 3-stop gradient wash for a solid card + accent border (hierarchy from the border, not alpha), drop the sparkle-chip icon, and unify the four-size font zoo (10/11/11/12) onto the design-system scale (eyebrow / 14 semibold title / 13 body / 13 CTA). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-launch UX overhaul — mobile / Flutter (Waves 0–3, full audit)
Implements
docs/ux-audit-2026-06-11.mdend-to-end for the Flutter app (apps/mobile-flutter) — 30 commits this session. Founder design direction honored: bounded estimates/confidence stay internal; voice through component design, not prose.Gate (whole branch):
flutter analyze→ No issues found (only pre-existing SPM deprecation warnings forsign_in_with_apple/flutter_secure_storage). Native build / TestFlight not run from this PR.✅ Wave 0 — compliance & cold start
/api/v1/account) · forgot-password · branded cream cold-start + wordmark splash.✅ Wave 1 — the core loop
lightImpact, ScrollController) instead of unmount-then-pop · never destroy typed input + failed-attempt retry card + ~20s reassurance · over-target state + consumed-fills ring unification + overflow arc · iOS swipe-to-remove + undo on persisted cards · strike-row at qty 0 · localized greeting · locale number/weekday formatting · partial-day + legacy-macro notices ported.✅ Wave 2 — navigation
selectionClick,PopScope) — drawer deleted · Settings → header avatar → Cupertino swipe-back · ergonomics (44pt targets, whole-header tap, weekPageView, pull-to-refresh).✅ Wave 3 — surface depth
PageViewsynced to the week strip, per-day slice fetch, date line, count-up, bars retarget without remount) · first-run staging (day-0 single Lora card; "% on track" suppressed until data exists) · deadweight_trend/weight_chart_utils/progressStatusdeleted.dashboard_tokenscards, eyebrows collapsed · iOS-hands (bouncing scroll, pull-to-refresh,copyWithPreviousso a failed refetch keeps content,overviewToastwired)./welcomecounts the target up in Lora 40.✅ Craft
SnackBarThemeData·serifSemiBolddeleted (Lora never bold) · real ellipsis · full Lucide DNA — zero Material icons remain app-wide · espresso scrim.👀 Needs device sign-off (analyze-clean; not run on hardware)
/welcomecount-up; language live-switch with the monogram toggle.⏳ Known residuals (small, intentional)
/api/v1/onboarding/screenstep contract (kOnboardingTotalSteps3→4 remap); needs care, not a blind edit.1.0.1, keep in sync with pubspec); Privacy/Terms copy a placeholder URL (nourl_launcherdep / real legal pages yet).⚙️ Founder prerequisites (code complete; nothing committed)
SUPABASE_SERVICE_ROLE_KEYon the backend (shared with web) for account deletion.🤖 Generated with Claude Code
🔍 Adversarial audit + hardening round (all findings fixed in 8 follow-up commits)
An independent adversarial review verified the branch (analyze clean, zero Material icons confirmed) and found real defects — all fixed:
email_not_confirmedroutes to the confirm-email state; errors render inline in terracotta.pendingRemovalIdsfilter — the resurrect race and double-swipe snapshot bug are structurally gone.P: 38gconsistency; "412 kcal"; 3-day %-suppression; locale number + "/wk" fixes; sex no longer silently defaults to male; deep-link back affordance; first-run suggestion chips.allowRuntimeFetching = false— cold offline launch no longer renders in Roboto. Vietnamese glyphs verified present in the Lora binaries.📱 Device smoke-test checklist (the audit's sharpened list — run on
vmkhoiii)Summary by CodeRabbit
🆕 Update (this session) — Nutrition tab redesign + backend
1drangeMobile — Nutrition tab rebuilt as a single dense view
showTopToast, success/error variants) — every bottomSnackBarconverted.Backend (web, same branch) —
1d"Today" range + DB-derived foods1drange end-to-end (date-range,summary, mapper, contracts) so "Today" returns single-day intake.Targets fact-check (this session)
WHO/FAO 2004 + NASEM 2019 values (the path a no-country profile resolves) verified against canonical sources — all correct (Ca 1000, Mg 260/220, Zn 7.0/4.9, Vit A 600/500, Vit C 45, Vit K 65/55, folate 400, B12 2.4; NASEM K 3400/2600, Na 2300). Caveats: the Vietnam-RDA-path
vitaminA 850/vitaminK 150read high vs. WHO/FAO and are worth a spot-check against the 2016 tables (they only fire for VN-country profiles).Gates
flutter analyze→ No issues found; webtscclean; 89/89 nutrition tests pass;biomeclean.Known gaps / follow-ups (not blocking)
lib/features/nutrition/logic/{format_date,verdict_logic}.dartare unused (orphaned by the refactor) — left in place; can remove on request.