47 KiB
Build Execution Ledger
Active Target
- Current Milestone: Phase 7: Navigation & Domain Expansion (Exercise, Recipe Vault, Settings) — COMPLETE
- Status: Phase 7 Complete (release-ready: all phases 1–7 done)
- Latest Addendum: Phase 3C retrofit — Nutritional Search Engine, Barcode Scanning & Dual-Source Food Cache (2026-10-17)
Phase Checklist
- Phase 1: Foundation & Identity (Passwordless SSO & Encrypted Drift DB)
- Phase 2: Medication Domain, Injection Rotation, Serum Modeling
- Phase 3: Nutrition, Protein Target Tracking, HealthGateway Sync
- Phase 3B: Platform Health Integrations & HealthGateway (architecture §4.7 contract, Settings control surface, native entitlements)
- Phase 3C: Nutritional Search Engine, Barcode Scanning & Dual-Source Food Cache (architecture §4.8 resolution ladder)
- Phase 4: Recipe Web Scraper Engine & Meal Planning Calendar
- Phase 5: Pantry Delta Engine & Auto-Shopping Generator
- Phase 6: Hardening, Native Desktop/Web Polish, CI/CD
- Phase 7: Navigation & Domain Expansion (Exercise, Recipe Vault, Settings)
Phase 2 Deliverables (2026-10-16)
- Domain:
DoseEntryentity, 12-quadrantInjectionSiterotation model,Glp1Compoundvalue object (Sema 168h / Tirze 120h / Lira 13h),IMedicationRepositorycontract. - Domain:
CalculateSerumLeveluse case implementingC(t) = Σ D_i · 0.5^((t − t_i)/t_half). - Data: injection-history CRUD on
AppDatabase+ drift-backedMedicationRepository(record ↔ entity mapping). - Presentation:
MedicationController(Riverpod StateNotifier) with serum estimation, least-recently-used site rotation, and reminder scheduling; refactoredMedicationScreen(serum card, rotation suggestion, interactive log dialog, history with delete). - Platform:
INotificationService/PlatformNotificationService(flutter_local_notifications 17.x; Android/iOS/macOS scheduled reminders, graceful no-op on Web/Windows/Linux); Android boot-receiver manifest entries;timezonedependency added. - Regression fix:
SupabaseAuthServicenow resolves its client lazily (baseline widget test no longer throws when Supabase is uninitialized). - Tests: 33 new unit/widget-fake tests (PK calculator, rotation policy, repository w/ in-memory drift, controller); baseline smoke test green.
Post-Phase-2 Additions
- Development auth bypass (2026-10-16): debug-only "Continue Without Signing In" button on the login screen (
kDebugMode-gated, in-memorydevAuthBypassProvider) lets reviewers enter the app without a Supabase session; the router guard honors the flag and the shell shows an orange "Development mode - not signed in" banner while active. Covered bytest/features/auth/dev_auth_bypass_test.dart(3 tests).
Phase 3 Deliverables (2026-10-16)
- Domain:
FoodLogEntry(+MealTypeenum),HydrationLogEntry,BiometricEntry(+BiometricSourceenum),INutritionRepository,IHealthSyncRepository— all pure Dart. - Data:
AppDatabaseschema v2 (+ migration for v1 installs) withFoodLogsTable,HydrationLogsTable,BiometricLogsTable, range-filtered watch/get/CRUD helpers,clearSyncedRangefor idempotent re-syncs; drift-backedNutritionRepositoryandHealthSyncRepository(record ↔ entity mapping). - Platform:
HealthGatewayrewrite —IHealthGateway+ conditional-import factory; mobile impl (health_gateway_mobile.dart) wrapspackage:healthforWEIGHT(kg→lb normalization) andACTIVE_ENERGY_BURNED(joule→kcal) with double-guarded try/catch; fallback impl (health_gateway_fallback.dart) for Web/Windows/macOS never touches platform channels (reads served from the drift cache, writes via manual entry). - Presentation:
NutritionController(RiverpodAsyncNotifier; today's logs, 100 g protein / 96 oz water targets, meal grouping, quick-add water) andBiometricController(weight history, 7/30-day deltas, manual entry, native sync with per-day active-energy aggregation);LogFoodDialogandLogWeightDialog. - Presentation:
NutritionScreenupgraded — interactive protein/hydration progress meters, +8/+16/+24 oz quick-add buttons, meal-grouped food diary with delete, weight tracker card (latest weight, 7/30-day trend chips, latest active burn) whose "Log Weight / Sync" button runsHealthGatewaysync on Android/iOS and opens the manual dialog on Windows/Web; status snackbars viaref.listen. - Tests: 23 new tests — nutrition repository (in-memory drift: CRUD, day-range filter, watch,
MealTyperound-trip), nutrition controller (targets, grouping, validation, quick-add, delete, failure retention), biometric controller (manual entry, deltas, fallback short-circuit, permission denial, weight+energy sync idempotency). - Regression fix: none required — existing 37 tests untouched and green.
Phase 4 Deliverables (2026-10-16)
- Domain:
Recipe(+MacroProfile),RecipeIngredient(quantity/unit/item parsing: decimals, ASCII + unicode + mixed fractions, known-unit vocabulary),MealPlanSlot(reuses nutritionMealType),IRecipeRepository,IMealPlanRepository— all pure Dart;core/utils/uuid.dartv4 generator (no new dependency). - Domain:
ScrapeRecipeFromUrluse case —package:httpdownload (injectableRecipeHtmlFetcherfor tests),package:htmlDOM parse, JSON-LD@graph/root/array/@type-list navigation,HowToStep/HowToSectioninstruction flattening, ISO-8601prepTime/cookTime,recipeYield, regex nutrition parsing (calories,proteinContent,carbohydrateContent,fatContent), image list/object unwrap, OpenGraph fallback (og:title/og:image/og:description), typedRecipeScrapeException. - Data:
AppDatabaseschema v3 (migration for v1/v2 installs) withRecipesTable(UUID PK, JSON ingredient/instruction columns, macros,createdAt) andMealPlanSlotsTable(UUID PK, plannedDate, mealType, nullablerecipeIdFK withON DELETE SET NULL, customNotes, isCompleted); drift-backedDriftRecipeRepository(URL-deduplicatedimportRecipe,createdAtpreserved on re-import) andDriftMealPlanRepository(half-open[from, to)window, upsert-by-id). - Presentation:
RecipeImportController(RiverpodNotifier; URL submit → loading → preview modal state, live catalog watch stream, confirm-save/dismiss/delete) andMealPlanCalendarController(Monday–Sunday window, next/previous/today navigation, slot assign/upsert/clear, prepared toggle, per-day protein rollup against the nutrition 100 g target, deleted-recipe slot normalization, stale-window refresh guard). - Presentation:
MealPlannerScreenrefactor — URL import bar with spinner,RecipePreviewDialog(image, meta chips, macro tiles, ingredients, steps, save with error surface),SavedRecipesTray(draggable cards with thumbnail + protein badge + quick-assign),WeeklyCalendarView(≥900 dp: 7-column grid; below: tabbed pager) withDragTargetmeal slots, prepared checkboxes, clear menu, per-day protein indicator (progress bar vs. target),AssignSlotDialog(saved-recipe dropdown or custom description), week navigation header. - Tests: 25 new tests —
recipe_scraper_test.dart(JSON-LD@graph/root/array/list-@typeextraction, macro/ingredient/instruction/duration/yield parsing, malformed-JSON fallthrough, OpenGraph fallback,execute()URL validation & error mapping) andmeal_plan_repository_test.dart(startOfWeek, recipe CRUD/URL-dedup/watch, slot window filter/upsert/delete, calendar rollup/assignment/navigation/custom entries/prepared toggles/recipe-deletion normalization, import-controller empty-URL guard). - Regression fix: HowToSection flattening now recurses into
itemListElementbefore using the sectionname(parser ordering bug caught by the new test).
Post-Phase-4 Additions
- Desktop resize overflow fix (2026-10-16): the hatched yellow/black stripes (RenderFlex overflow) shown when shrinking the Windows window are gone — wide-mode day columns now scroll vertically (matching the compact pager), and a
LayoutBuilderinMealPlannerScreencollapses the page into a single scrollable column (with a fixed-height calendar) whenever the body is shorter than ~520 dp. Covered bytest/features/meal_planner/meal_planner_screen_test.dart(4 widget tests pumping the screen at short-compact, short-wide, tall, and extremely-short window sizes; overflow errors fail widget tests, so green proves no stripes).
Phase 5 Deliverables (2026-10-16)
- Domain:
PantryItem(+PantryCategoryaisle enum withinfer()keyword heuristic,ExpirationStatuswarning buckets),ShoppingListItem,IPantryRepository, and two pure-Dart value objects —ingredient_name.dart(lowercase, paren-stripped, plural-folded canonical keys:eggs→egg) andmeasurement_units.dart(mass/volume/count dimensions with lb↔oz↔g↔kg and ml/l/tsp/tbsp/cup conversion). - Domain:
CalculateShoppingDeltause case — aggregates the active 7-dayMealPlanSlotwindow'sRecipeingredient manifests by canonical name, appliesdeficit = totalRequired − currentPantryQuantityin dimension base units (dominant recipe unit wins display, 2-decimal rounding), upsertsisAutoGeneratedrows withsourceRecipeTitles(stable ids, checked-state kept only while the requirement is unchanged), clears stale auto rows (satisfied by stock or superseded by a manual row), and never modifies or removes manual rows. - Data:
AppDatabaseschema v4 (migration for v1–v3 installs) withPantryItemsTable(UUID PK, canonical name, quantity/unit, aisle key, nullableexpirationDate,lastUpdated) andShoppingListItemsTable(UUID PK, required quantity/unit, purchased/auto flags, JSONsourceNotesfor recipe titles); drift-backedDriftPantryRepository(watch/get/upsert/delete for both lists, enum/JSON mapping). - Presentation:
PantryInventoryController(live watch stream, quick ± stock steps clamped at zero, aisle-grouping vs. expiration-sort modes) andShoppingListController(manual add, All/To Buy/Checked filter, check-off with "Add to Pantry?" prompt performing a unit-converting stock merge or aisle-inferred row creation,syncWithMealPlanreading the planner's Monday–Sunday window + recipe catalog). - Presentation:
PantryScreenrefactor — Inventory/Shopping tab header with remaining-count badge, Aisles/Expiration segmented control, categorized tiles with +/− counters and Expired/expiring badges, swipe-to-delete on both lists, prominent "Sync with Meal Plan" banner with run summary + snackbar, auto-row "From: " origin chips, filter segments, check-off → pantry prompt, and Add Pantry Item / Add Shopping Item dialogs. - Tests: 10 new tests in
test/features/pantry/delta_calculator_test.dart— the three required scenarios (300 g vs 800 g → +500 g entry; sufficient stock → no entry; manual rows preserved across repeated syncs) plus stale auto cleanup, cross-recipe plural/unit normalization (4 eggs + 2 egg − 2 → +4 egg), lb/oz scale unification (0.75 lb), check-off → pantry transfer (merge + aisle-inferred create), end-to-endsyncWithMealPlan, and inventory canonicalization/quick-adjust clamp.
Phase 6 Deliverables (2026-10-16)
- Platform:
HealthGatewayhardened —IHealthGatewayfactory now accepts anILocalBiometricReader(core contract); mobile driver guards withkIsWeb+defaultTargetPlatform+dart:iocross-check, probes Health Connect SDK availability (getHealthConnectSdkStatus) and degrades with a debug warning when missing/disabled, validatesREAD_WEIGHT/READ_STEPS/READ_TOTAL_CALORIES_BURNED/READ_ACTIVE_CALORIES_BURNEDpermissions viahasPermissions/requestAuthorizationbefore every read, and keeps all HealthKit entitlement failures insidetry/catch(false + warning, never an unhandled exception). - Platform: fallback driver (
health_gateway_fallback.dart) now serves locally logged biometrics through an injectedILocalBiometricReader(wired to the drift cache byDriftLocalBiometricReaderin the health_sync controller) and emits a debug warning instead of attempting native SDK calls; Android manifest declares the four Health ConnectREAD_*permissions matching the gateway request set. - Security:
CryptoManagerplatform-safe vault config — AndroidAndroidOptions(encryptedSharedPreferences: true), iOS/macOSKeychainAccessibility.first_unlock(Darwin Keychain options), Windows DPAPI-sealed file, Linux libsecret; vault failures degrade to an ephemeral in-memory SQLCipher key instead of crashing. - Testability:
isAuthenticatedProviderextracted inapp_router.dartso integration tests can simulate authenticated/unauthenticated sessions without booting Supabase. - Tests:
test/features/medication/serum_calculator_test.dart(11 tests — semaglutide 168 h / tirzepatide 120 h weekly steady-state peaks/troughs vs. closed-form geometric series, monotonic accumulation, peak-to-trough fluctuation ratios, multi-dose superposition, brand-alias resolution, titration ladder, washout). - Tests:
recipe_scraper_test.dartextended with real-world fixtures (test/fixtures/recipes/bbc_good_food_style.htmlroot-level payload,allrecipes_style.htmlmulti-script complex@graphwithHowToSectionnesting, list-valued yield/image) — 2 new fixture tests. - Tests:
delta_calculator_test.dartextended with a unit-conversion group (kg+g dominant-unit reporting, l+ml volume unification, mass/volume dimension-mismatch guard, unit-synonym/plural canonicalization with cross-dimension stock refusal) — 4 new tests. - Tests:
test/integration/app_navigation_test.dart(4 tests — navigation-rail switching across/medication,/nutrition,/meal-planner,/pantryunder authenticated and dev-bypass conditions, mobile bottom-bar switching, unauthenticated bounce to/login) andtest/features/nutrition/nutrition_widget_test.dart(3 tests — protein/hydrationLinearProgressIndicatorvalues and labels updating reactively on food/water logs, goal-met chips). - CI/CD:
.forgejo/workflows/ci.ymlforgit.kellsupport.com/mtkell/glp-companion(GitHub Actions-compatible syntax for Forgejo Actions) — push tomain+ all PRs inghcr.io/cirruslabs/flutter:stable; checkout → mock.env→flutter pub get→dart run build_runner build --delete-conflicting-outputs→flutter analyze --fatal-infos→flutter test --coverage→flutter build bundle+flutter build web. - Cleanup: dead
lib/features/recipes/data/recipe_scraper.dart(unreferenced Phase-0 scaffold superseded byScrapeRecipeFromUrl) removed with its empty directory; zero TODO/FIXME markers inlib/ortest/. - Docs:
README.mdrewritten (architectural overview + directory layout, local dev prerequisites incl. VS Code/Cline setup and.envconfiguration via new.env.example, SQLCipher AES-256 key lifecycle, CI pipeline details);BUILD_STATE.mdPhase 1–6 checkboxes complete.
Phase 7 Deliverables (2026-10-16)
- Domain:
MuscleGroupenum +ExerciseEntryentity (pure Dart),IExerciseRepositorycontract, session-scopedInMemoryExerciseRepository(drift table deferred to a follow-up migration). - Presentation (exercise):
ExerciseController(weekly distinct-session rollup vs. the recommended 3 sessions/week lean-mass target) +ExerciseScreenwith weekly strength-frequency card, day-grouped working-set log (swipe-to-delete), and theLogExerciseDialog(muscle group / name / sets / reps / weight). - Presentation (Recipe Vault): new
features/recipesscreen — pinned URL import bar wired toRecipeImportController, search field + macroChoiceChipfilters ("Protein > 30g", "Protein > 20g", "Under 500 kcal", All), responsiveGridView.builder(SliverGridDelegateWithMaxCrossAxisExtent) with chunked infinite scroll (24-card pages, 6-card prefetch margin), details bottom sheet, delete confirmation; reuses the meal-planner recipe domain (no duplicate bounded context). - Refactor (meal planner):
MealPlannerScreenstripped to week navigation + 7-day calendar + daily protein rollups; inline import bar andSavedRecipesTrayremoved (moved to Recipe Vault);AssignSlotDialog/drag-drop replaced by the searchableAssignRecipeSheetmodal bottom sheet (Recipe Vault search + custom-entry fallback); empty slots render a "+ Assign Recipe" affordance. - Presentation (Settings):
SettingsScreen— Identity (email, Supabase status badge, sign-out), Targets (daily protein g / water oz inputs feedingNutritionController.updateTargets), Security & Storage (SQLCipher AES-256 badge with web-degradation note), Integrations (Apple Health / Health Connect / HealthGateway toggles viaIntegrationsController, native permission round-trips throughIHealthGateway). - Shell:
app_router.dartregisters/medication,/nutrition,/exercise,/meal-planner,/recipes,/pantry,/settings;AdaptiveHomeScaffolddesktop rail expanded to the 7 specified destinations (medication_liquid,restaurant,fitness_center,calendar_month,menu_book,inventory_2,settings) withNavigationRailLabelType.allinside a height-constrained scroll view (no compact-window overflow); mobile keeps 4 core tabs + a "More" tab opening the secondary-destinations drawer (Exercise, Recipes, Settings). - Tests:
app_navigation_test.dartextended to all 7 rail routes + mobile "More" drawer flow (7 tests total);meal_planner_screen_test.dartrewritten for the calendar-only layout including the assign-sheet opening;SettingsScreenbadge rows useFlexible+ ellipsis so narrow widths never overflow.
Post-Phase-7 Additions
- Recipe import hardening (2026-10-16): in-browser URL imports failed with "could not download" — diagnosed against the live allrecipes recipe: the native fetch succeeds (HTTP 200) and the parser extracts the recipe correctly, so the failure is the web CORS policy (recipe sites never send
Access-Control-Allow-Origin, so Chrome aborts before a status code exists); additionally, requests without a browser-likeUser-Agentdraw HTTP 403 from anti-bot gates. Fixes inScrapeRecipeFromUrl: (1) browser-likeAccept/Accept-Language/User-Agentheaders on native (UA stripped on web — forbidden header); (2) optional--dart-define=RECIPE_PROXY_URL=<url>that routes downloads through a server-side proxy as<proxy>?url=<encoded>; (3) actionable CORS guidance in the web error message when no proxy is configured. Shippedsupabase/functions/recipe-proxy/— a deployable Deno Edge Function relay (http(s)-only, SSRF host guard, 2 MiB cap, 15 s timeout, CORS-enabled) — deploy withsupabase functions deploy recipe-proxyand pass its URL via the define. Tests: 7 new cases inrecipe_scraper_test.dart(header maps, proxy wrapping/encoding, error propagation) — 22/22 scraper tests green. - Web drift assets fix (2026-10-16): every drift operation failed at runtime on
flutter run -d chrome(watchDoseEntries failed,meal plan refresh failed,recipe catalog watch failed, …) becauseweb/was missing the WASM database artifactsconnection/web.dartdocuments as required. Addedweb/sqlite3.wasm(740 KB, downloaded from thesqlite3-3.7.1GitHub release — matches the lockedsqlite3version; drift 2.35.x changelog mandates a 3.x wasm) andweb/drift_worker.js(copied from the lockeddrift-2.35.2package). Both files are not gitignored, so they ship with the repo and the CIflutter build webstep. Verified:flutter build web --no-tree-shake-iconspackages both intobuild/web/. NOTE: bump both files when upgradingdrift/sqlite3(worker JS must match the Dart package version exactly; wasm must be ≥ 3.x). - Android toolchain fixes (2026-10-16):
flutter runon an emulator failed on three pre-existing gaps (Android builds had never been exercised — Phase 6 gates ran on Windows/Web): (1)flutter_local_notificationsrequires core library desugaring → enabledisCoreLibraryDesugaringEnabled+com.android.tools:desugar_jdk_libs:2.1.4inandroid/app/build.gradle.kts; (2) Kotlin incremental compilation aborts when the pub cache lives on a different drive than the project (C: vs D:, "this and base files have different roots") →kotlin.incremental=falseinandroid/gradle.properties; (3) the pinnedhealthplugin declares minSdk 26 vs. Flutter's default 24 →minSdk = maxOf(flutter.minSdkVersion, 26)(Health Connect only ships on Android 8.0+). Verified:flutter build apk --debug→✓ Built build\app\outputs\flutter-apk\app-debug.apk. - Mobile overflow fix (2026-10-16): the compound dropdown in
LogInjectionDialogpainted yellow/black stripes on phone widths —DropdownButtonFormField's selected-item Row cannot ellipsize long labels ("Tirzepatide (Mounjaro / Zepbound)") withoutisExpanded: true. Fixed withisExpanded: true+maxLines: 1/TextOverflow.ellipsison the menu items; regression tests intest/features/medication/log_injection_dialog_test.dartpump the dialog at 320 dp (both long labels) and 390 dp asserting no RenderFlex overflow (128/128 tests passing).
Post-Phase-7 Additions
- App icon branding (2026-10-16): replaced all stock Flutter icons with the "cg" leaf monogram brand mark. Master asset
assets/icon/app_icon.jpg(2048×2048);assets/icon/generate_icons.py(Python/Pillow, Lanczos) regenerates every platform size idempotently: web (favicon.png32,icons/Icon-{192,512}.pngwith feathered circular alpha,icons/Icon-maskable-{192,512}.pngat 80% safe-zone over brand sage#D4E4C7), Android (legacymipmap-*/ic_launcher.png48–192 px + new adaptive icons:mipmap-anydpi-v26/ic_launcher{,_round}.xml,drawable/ic_launcher_background.xmlsage vector,drawable-*/ic_launcher_foreground.png66/108 dp safe circle), iOS (all 15AppIcon.appiconsetPNGs — opaque RGB, no alpha, per App Store validation), macOS (app_icon_{16..1024}.png), Windows (app_icon.icomulti-res 16–256 px). Web polish:manifest.jsonname/description/theme (#375A53teal,#D4E4C7sage) andindex.htmltitle/description/theme-colorupdated from Flutter placeholders. Verified: PIL dimension/mode/alpha assertions on every output + XML well-formedness + ICO frame enumeration;flutter build web --no-tree-shake-iconsandflutter build apk --debugpackage the new assets.
Phase 3B Deliverables (2026-10-17, architecture §4.7 retrofit)
- Platform contract (
lib/core/platform/health_gateway.dart): Phase 3B surface added —HealthProviderType(appleHealth, healthConnect, samsungHealth, enterpriseFhir, manualFallback),HealthConnectionStatus(notConfigured, permissionDenied, connected, unsupportedPlatform),HealthDataPayload(weightLbs / activeCaloriesToday / bloodGlucoseMgDl / timestamp / source), andIHealthGateway.checkStatus/requestAuthorization/fetchLatestBiometrics/disconnectwith defaults derived from the existing primitives. NOTE for future implementors: Dartimplementsnever inherits method bodies — drivers and test fakes mustextends IHealthGateway(or reimplement the members) to pick up the defaults. - Platform (mobile driver):
BLOOD_GLUCOSEadded to the iOS/Androidpackage:healthread sets (mmol/L → mg/dL normalization ×18.0182);checkStatusprobeshasPermissions+ Health Connect SDK readiness before answering;samsungHealthaliases the Health Connect binding on Android (federated Samsung Health + Pixel sensor feeds);disconnectclears the cached SDK probe (neither HealthKit nor Health Connect exposes programmatic revocation — OS-level revoke stays in system settings);fetchLatestBiometricsaggregates today's weight (latest), active energy (sum), and blood glucose (latest). - Platform (fallback driver): inherits the Phase 3B defaults — manualFallback →
connected, enterpriseFhir →notConfigured, native providers →unsupportedPlatform;fetchLatestBiometricscomposes the encrypted drift cache viaILocalBiometricReaderwith sourcemanualFallback; zero platform-channel calls on Web/Windows/macOS (safe dummy states, never a crash). - Presentation (
IntegrationsControllerrewrite): status-based orchestration replacing the old toggles —refreshStatuses()probes all three providers on build (disposal-flag guarded; Riverpod 2.xRefhas nomounted),connectAppleHealth/connectHealthConnectrunrequestAuthorizationround-trips (denial →permissionDenied+ inline snackbar),disconnectProvidertears down via the gateway and re-probes. - Presentation (
SettingsScreen):Health Integrationscard with Apple Health, Health Connect (Samsung & Pixel), and SMART on FHIR rows — live status badges (Connected / Not connected / Permission denied / Unsupported) and Connect/Disconnect authorization buttons; desktop/web render disabled Connect buttons + neutral grey "Unsupported" badges (desktop-safe states, no dead platform channels); the FHIR row stays inert until its OAuth2 connector is provisioned. Identity (Supabase sign-out), Targets, and SQLCipher status cards unchanged;/settingsroute wiring unchanged. - Manifests: Android —
android.permission.health.READ_BLOOD_GLUCOSEadded to the Health Connect permission set,com.google.android.apps.healthdatapackage query +ACTION_SHOW_PERMISSIONS_RATIONALEintent in<queries>, andViewPermissionUsageActivityactivity-alias (VIEW_PERMISSION_USAGE/HEALTH_PERMISSIONS) per architecture §4.7.4; iOS —NSHealthShareUsageDescription+NSHealthUpdateUsageDescriptionadded toios/Runner/Info.plist. - Tests: +13 — fallback driver dummy-state matrix (
fallback_health_gateway_test.dart, 5), integrations controller connect/disconnect/status transitions (integrations_controller_test.dart, 6), Settings screen desktop disabled-states + mobile connect/disconnect badge round trip (settings_screen_test.dart, 2). Existing_FakeHealthGatewayswitched toextendsfor the inherited defaults;app_navigation_test.dartupdated for the renamed "Health Integrations" card title.
Phase 3B Verification Gates (2026-10-17)
dart run build_runner build --delete-conflicting-outputs(191 outputs written)flutter analyze(No issues found!)flutter test(148/148 passing)flutter build windows --debug(✓ Built build\windows\x64\runner\Debug\glpcompanion.exe; the Settings screen's desktop-safe disabled states are asserted headlessly bysettings_screen_test.dart— interactiveflutter run -d windowscompiles the same toolchain)
Phase 3C Deliverables (2026-10-17, architecture §4.8 retrofit)
- Dependencies:
openfoodfacts ^3.7.0(resolves 3.31.0) +mobile_scanner ^5.2.3added topubspec.yaml(http ^1.2.1already present);.envgainsUSDA_FDC_API_KEY=DEMO_KEY(read with anisInitializedguard + DEMO_KEY fallback inUsdaFdcClient.apiKey). - Domain:
FoodProductentity (id, barcode, productName, brand, servingSizeGrams/Unit, calories, protein/carbs/fat/fiber/sodium, imageUrl, lastUpdated) andIFoodSearchRepositorycontract (searchByText / lookupBarcode / cacheProduct) — pure Dart. - Data (persistence):
FoodProductsTableadded toAppDatabase(schema v5 +from < 5migration; text PKid, nullable barcode/brand/imageUrl, real macro columns, datetimelastUpdated) with barcode/substring search helpers (LIKE-wildcard sanitized) andupsertFoodProduct(insertOnConflictUpdate). - Data (sources):
UsdaFdcClient(POST/fdc/v1/foods/search, nutrient-ID mapper 1008/1003/1004/1005/1079/1093, branded GTIN/UPC mode for barcode lookups, injectablehttp.Client) andOpenFoodFactsClient(OpenFoodAPIConfiguration.userAgentwith app identity + contact,SearchTermstext search,getProductV3barcode lookup, per-serving nutrient reads with per-100 g scaling) — both implement the sharedRemoteFoodDataSourceinterface for testability. - Data (repository):
FoodSearchRepositoryimplements the §4.8 ladder — L1 local drift cache (substring/barcode) → L2 Open Food Facts (4-second timeout) → L3 USDA fallback → write-through cache viainsertOnConflictUpdate; source failures degrade non-fatally (debugPrintStack). - Presentation:
FoodSearchController(StateNotifier; 400 ms debounced text queries, immediate barcode lookups, request-token supersession, loading/error/empty states) andFoodSearchScreen(debounced search bar with clear button; AppBar scan button —MobileScannermodal sheet on iOS/Android with UPC-A/EAN-8/13 filters and permission-failure messaging, manual UPC dialog + desktop/web notification otherwise; product tiles with thumbnail/name/brand/calories/protein badge; action bottom sheet with full macros, 0.5×/1×/1.5×/2× serving multiplier, "Log to Diary" →FoodLogsTableviaNutritionController.logFood, "Add to Pantry" →PantryItemsTableviaPantryInventoryController.addItem). - Integration:
/food-searchroute (top-level, above the shell) with?preset=pantry; Nutrition screen gains a "Search Food / Scan Barcode" entry button; Pantry screen inventory tab gains an AppBar "Scan item barcode" action routing to the pantry preset (primary action flips to "Add to Pantry"). - Tests: +10 in
test/features/nutrition/food_search_repository_test.dart— cache hit short-circuit, OFF-first + write-through cache, USDA fallback on OFF failure/empty, dual-miss empty results, second-lookup cache hit, blank-query short-circuit, and the same hierarchy for barcode lookups (cached barcode, OFF barcode hit, USDA branded-GTIN fallback, total-failure degradation). - Regression fix (pre-existing, gate-unblocking):
GoogleHealthConnectordotenv reads now go through anisInitialized-guarded_env()helper —app_navigation_test.dartpreviously crashed withNotInitializedErrorwhenevercheckStatusran in a test/production shell that never calleddotenv.load.
Phase 3C Verification Gates (2026-10-17)
flutter pub get(openfoodfacts 3.31.0 + mobile_scanner 5.2.3 resolved)dart run build_runner build --delete-conflicting-outputs(206 outputs;FoodProductsTablecodegen verified inapp_database.g.dart)flutter analyze(No issues found!)flutter test(164/164 passing, incl. 10 new fallback-sequence tests)- Field fix (2026-10-17, first live run): "food search returns nothing" — two server-side contract violations, both diagnosed with raw HTTP probes:
- USDA 403
API_KEY_MISSING: the FDC API validates the key from the query string only (?api_key=…); the JSON-bodyapiKeyfield is silently ignored.UsdaFdcClientnow builds the URI withqueryParameters: {'api_key': apiKey}and drops the body field (plus sends an identifyingUser-Agent); missing-unit records now default tog(FDC's per-100 g basis) instead ofcount. - Open Food Facts 503: OFF blocks all anonymous search endpoints server-side (both
cgi/search.plandapi/v2/search503 even from raw HTTP with a proper User-Agent) while product/barcode lookups return 200 — the USDA fallback is the designed path for text search and now carries it. Barcode scans still prefer OFF. - UX hardening: the repository now throws
FoodSearchUnavailableExceptionwhen the cache misses AND every remote source errored (vs. answered "no matches"), so the screen shows "Search failed — check your connection" instead of a false "No products found". - Regression tests:
test/features/nutrition/usda_fdc_client_test.dart(+5, MockClient) locks the query-param key shape, DEMO_KEY fallback, nutrient-ID mapping (mg sodium passthrough), Branded-only barcode mode, and non-200 →UsdaFdcException; the total-failure repository test now asserts the throw. Full suite: 170/170.
- USDA 403
flutter build apk --debug/flutter build ios --no-codesign— needs a machine with the mobile toolchains (also exercises the Android/iOS camera-permission additions required bymobile_scanner)- Live dual-source round trip on a device: scan a known UPC → OFF hit populates
FoodProductsTable→ airplane-mode relaunch → same barcode resolves from the SQLCipher cache; delete the row + query a generic food (e.g. "rolled oats") → USDA fallback populates the cache.
Active Verification Gates (Phase 7, 2026-10-16)
dart analyze lib test(Zero issues)flutter test(all passing — 125/125)flutter run -d windows— builds cleanly; all 7 rail destinations switch without overflow (integration tests assert this headlessly)
Phase 6 Verification Gates (2026-10-16, retained)
dart run build_runner build --delete-conflicting-outputs(44 outputs written)flutter analyze --fatal-infos(Zero issues —No issues found!)flutter test(123/123 passing)flutter build web --no-tree-shake-icons(conditional import keepspackage:health/dart:ioout of the web bundle)
Post-Phase-7 Additions
-
Deep-link (OAuth / magic-link callback) registration across platforms (2026-10-17): the custom scheme
com.companionglp.glpcompanion(mirrors the bundle identifiers behindkAuthCallbackRedirectUrl) is now registered in every OS routing table —- iOS:
Runner/Info.plistCFBundleURLTypes(CFBundleTypeRole=Editor, schemecom.companionglp.glpcompanion). - macOS:
Runner/Info.plistCFBundleURLTypes(same shape). - Android:
VIEW/DEFAULT/BROWSABLEintent-filter withandroid:scheme="com.companionglp.glpcompanion"on.MainActivity(existingsingleToplaunch mode routesonNewIntentto the running activity consumed byapp_links). - Windows runner:
RegisterAppLinkProtocol()(windows/runner/utils.cpp) writes the per-user protocol underHKCU\Software\Classes\com.companionglp.glpcompanion(URL Protocol+shell\open\command→"<exe>" "%1", no elevation, re-registered every launch);wWinMaincallsSendAppLinkToInstance()(exported by theapp_linksWindows DLL) before creating a window so a protocol activation while the app is running forwards the URI to the existing instance overWM_COPYDATAinstead of spawning a second window. Cold-start links continue to be read from the command line by the plugin.
- iOS:
-
macOS entitlements fix (pre-existing gap, required for the OAuth round trip):
com.apple.security.network.clientadded toDebugProfile.entitlementsandRelease.entitlements— the sandboxed macOS runner previously had no outbound-network entitlement, so Supabase (including the auth exchange the deep link feeds) was unreachable. -
lib/features/auth/data/supabase_auth_service.dartdoc comment updated to reflect the actual registration points. -
Windows OAuth handoff diagnostics + no-hang guarantees (2026-10-17, after field report: chooser appears then "nothing happens"):
- Runner logs every protocol-activation step to
%TEMP%\glpcompanion-protocol.log(andOutputDebugString): launch argv,SendAppLinkToInstance()outcome, and — in the original instance — everyWM_COPYDATAreceipt withdwData/payload (expecteddwData=1026=WM_USER+2, the app_links tag). - Fallback path hardened: when forwarding fails (e.g. UIPI drops
WM_COPYDATAto an elevated target, or no instance found) the fresh instance now shows + foregrounds its window (taskbar flash fallback) and completes the code exchange itself via the plugin's pending-link path — protocol activation can no longer look like a hang. - Dart:
package:loggingroot listener (kDebugMode) added inlib/main.dartso supabase_flutter's auth/deeplink diagnostics (handle deeplink uri,Error while getSessionFromUrl) are visible underflutter run;logging: ^1.3.0promoted to a direct dependency inpubspec.yaml.
- Runner logs every protocol-activation step to
-
Deferred auth-error surfacing (2026-10-17, after Microsoft-provider field failure looked like a "hang"): OAuth/magic-link handshake failures arrive late — the browser returns an
?error=...callback, GoTrue'snotifyExceptionemits them as stream errors ononAuthStateChange, and nothing rendered them.LoginScreennow also listens toauthStateProvidererrors (router guard is unaffected — it readsisAuthenticatedProvider/live session directly, only bumping its refresh counter) and_messageFortranslates the known "Error getting user email from external provider" failure into actionable guidance. Field-verified via the protocol log: the Windows WM_COPYDATA handoff (dwData=1026forwarded to the running instance) is confirmed working end-to-end. -
Microsoft (Entra ID) provider handshake fixed (2026-10-17): the
server_error/ "Error getting user email from external provider" bounced back to the app was resolved by adding the optionalemailID claim to the Entra app registration (b8852225-23a1-46ef-a28e-365d8ef1cb3f) in the Azure portal (Token configuration → Optional claims). Field-verified: the next Microsoft round trip returned a clean?code=8432da29-...callback (protocol log, 14:40:59) delivered to the running instance through the app_links handoff. -
Single-instance enforcement (2026-10-17): Supabase Auth server logs (
POST /tokengrant_type=pkce → 200 at 18:40:59Z,user_signedupformtkell@kellengineering.com) proved the Microsoft exchange completed — in the wrong window. Two instances were running (protocol log: launches at 14:16 AND 14:37); the PKCE verifier lives in shared secure storage under one fixed key, so the callback was forwarded (Z-order) to the non-initiating instance, which silently completed the exchange server-side while the user's window stayed on the login screen. Fix:AcquireSingleInstanceMutex()(Local\GLPCompanionSingleInstance) in the Windows runner — duplicate normal launches activate the existing window and exit; protocol activations still forward to the sole window. Callback delivery is now deterministic. -
Reactive auth-guard state (2026-10-17):
isAuthenticatedProviderread the non-reactivecurrentSessiongetter inside a plainProvider, caching its first-read value forever (nobody invalidated it). Booting already-signed-in cachedtrue, so sign-out cleared the session (Identity card → "Not signed in", button disabled) but the guard kept passing — no redirect to/login; live sign-in navigation was equally broken but masked because every successful entry had been a boot-time session. Fixed byref.watch(authStateProvider)inside the provider so the boolean is re-derived on every GoTrue transition; test overrides (overrideWithValue) bypass the body and are unaffected. Hardened further: the router'srefreshListenablebump is deferred viaFuture.microtaskso the redirect guard can never re-readisAuthenticatedProviderinside Riverpod's provider-notification cascade (ordering edge where a stale cached value could be observed); field log confirmed thesignedOutevent and local session removal fire correctly, isolating the issue to navigation only. -
End-to-end auth field verification (2026-10-17, user-confirmed): all three passwordless modes exercised live on Windows — Google OAuth, Microsoft (Entra ID) OAuth, and email magic link — each completing the custom-scheme round trip (
com.companionglp.glpcompanion://login-callback/) through the single-instanceWM_COPYDATAhandoff into a persisted session; sign-out now redirects to/loginand re-authentication re-enters the app without a relaunch. -
Desktop cloud health sync — Fitbit connector (2026-10-17, architecture rule 3 "third-party OAuth REST sync" realized). SUPERSEDED THE SAME DAY — Google retired the legacy Fitbit Web API (support ended 2026-09-30, full shutdown 2026-10-30); the connector was replaced with the Google Health API connector below before ever shipping, so no legacy Fitbit code remains in the tree:
- Core:
ICloudHealthConnectorcontract (lib/core/platform/cloud_health/),supportsCloudSynconIHealthGateway, and aHealthProviderType.fitbitslot. Conditional-import discipline preserved:fitbit_connector.dartshims to the io implementation ondart:ioplatforms and an unsupported stub on Web (zero platform channels on web, same as the rest of the fallback driver). - Fitbit connector (
fitbit_connector_io.dart): OAuth2 authorization-code + PKCE (S256) through the system browser with an RFC 8252 loopback redirect (http://localhost:52789/callback, port configurable viaFITBIT_REDIRECT_PORT); tokens in flutter_secure_storage with transparent refresh + 401 retry; weight logs paged in 30-day windows and normalized kg → lb (unit detected via the profile-height heuristic — cm heights always >100, inches ≤90); active calories from today's activity summary (activityCalories). Pure payload parsers isolated infitbit_parser.dartfor unit testing. - Wiring:
FallbackHealthGatewayaccepts injected connectors, merges cloud reads with the local drift cache (newest-first), and routes checkStatus/authorize/disconnect per provider;createHealthGatewaythreads the list through; the mobile driver ignores it (cloud providers readunsupportedPlatformvia the interface defaults).syncFromHealthnow runs when EITHER a native SDK or a cloud connector is available (native permission prompt only for native), persisting imports asBiometricSource.cloudSync(enum value that already existed for this purpose). - UI: Fitbit row in Settings → Health Integrations (Connect runs the browser OAuth; on success the biometrics import immediately; Disconnect clears stored tokens). Desktop explanatory copy added to the Apple Health / Health Connect rows ("Available on iOS/Android — on desktop use Log Weight on the Nutrition screen") so the disabled states read as graceful degradation, not breakage.
- Provider decisions: Withings = same
ICloudHealthConnectorpattern, deferred to a follow-up (needs its own developer app + credentials). MyFitnessPal = NOT viable: no public API since 2018; a CSV/manual import would be the only route and is not planned. - Configuration (not in source control): register an app at dev.fitbit.com (OAuth 2.0, Personal type), callback URL
http://localhost:52789/callback, scopesweight activity; putFITBIT_CLIENT_ID,FITBIT_CLIENT_SECRET, optionallyFITBIT_REDIRECT_PORTin.env. Direct deps added:url_launcher,crypto. - Tests:
test/core/platform/fitbit_parser_test.dart(metric/imperial conversion, window filtering, malformed-payload tolerance, string/number calories, height heuristic);settings_screen_test.dartupdated for the fourth integration row.
- Core:
-
Google Health API migration (2026-10-17): the Fitbit connector was replaced with
GoogleHealthConnectoragainst the successor API before shipping — official docs (developers.google.com/health) confirm the legacy Fitbit Web API shuts down 2026-10-30 with migration to Google Health required:- Connector: same
ICloudHealthConnectorframework; OAuth2 (authorization-code + PKCE,access_type=offline&prompt=consentfor a stable refresh token) viaaccounts.google.comwith the same loopback redirect; scopesgooglehealth.activity_and_fitness.readonly+googlehealth.health_metrics_and_measurements.readonly; tokens undergoogle_health.oauth.tokenswith transparent refresh (keeps the previous refresh token when Google omits it on re-consent). - Data:
GET health.googleapis.com/v4/users/me/dataTypes/{weight|active-energy-burned}/dataPointswithpageSize/nextPageTokenpagination; client-side window filtering (no server filter dependency); pure parser (google_health_parser.dart) normalizes kg → lb via the per-pointunitfield and readsinstantaneousTimeortimeInterval.startTime. - Renames: enum
HealthProviderType.googleHealth, providercreateGoogleHealthConnector(), controllerconnectGoogleHealth(), Settings row "Google Health — Fitbit, Pixel & Wear OS data (replaces the Fitbit API)"; all legacy Fitbit files, parser, and tests deleted; env keysGOOGLE_HEALTH_CLIENT_ID/GOOGLE_HEALTH_CLIENT_SECRET(+ optionalGOOGLE_HEALTH_REDIRECT_PORT); tests:google_health_parser_test.dart(kg/lb, interval vs instantaneous time, windowing, malformed payloads, kcal passthrough). - Google Cloud setup: project → enable Google Health API → OAuth consent screen (External; Testing status limits refresh tokens to 7 days — publish the app to production status for durable tokens) → OAuth client (Web application) with Authorized redirect URI
http://localhost:52789/callback→ copy ID/secret into.env.
- Connector: same
Google Health Verification Gates (2026-10-17, to run locally)
flutter pub get,flutter analyze,flutter test(parser suite + updated settings row counts).- Google Health row wiring fix (2026-10-17, field-reported: row rendered "Unsupported" with a disabled Connect button on Windows): the
dart.library.ioconditional import routes every io platform — including desktop — toMobileHealthGateway(the fallback driver is Web-only, despite its doc comment implying desktop). The mobile driver acceptedcloudConnectorsfor signature parity but discarded them, socheckStatus(googleHealth)fell through to the interface default →unsupportedPlatform. Fix:MobileHealthGatewaynow stores the connectors, exposessupportsCloudSync, routes checkStatus/authorize/disconnect to them first, and merges cloud reads with native reads (Future.wait, non-fatal on provider errors) — desktop gains its only sync source, mobile gains an optional supplement. Web behavior unchanged (stub → unsupported). Merge correction: the driver already implemented the five methods further down the class; the duplicates were removed and the existing implementations edited instead (compile-error round-trip). - Weight-card sync gate (2026-10-17, field-reported: "Log Weight / Sync" always opened the manual dialog on desktop):
_logWeightOrSyncgated onsupportsNativeHealthalone (predating cloud sync). Now: the button syncs when a native SDK exists OR any cloud partner holds a valid grant (newIHealthGateway.isCloudSyncReady()probes connector statuses; both drivers override), falls back to the manual dialog otherwise (the dedicated "Manual" button is unchanged), and the sync outcome (Synced N weight sample(s)…) is surfaced as a snackbar. The card's sync icon now also appears when cloud sync is wired. - Manual Google Health round trip on Windows: Google Cloud credentials in
.env→ Settings → Google Health → Connect → Google consent → loopback redirect → row flips to Connected → weight/active energy appear on the Nutrition dashboard ascloudSyncentries. (Test data: log a weight in the Google Health app first — the API has no sandbox data.)
Deep-Link Verification Gates (2026-10-17)
- Windows runner handoff — FIELD-PROVEN via
%TEMP%\glpcompanion-protocol.log: protocol registration → OS chooser → second process launches with the link in argv →WM_COPYDATA(dwData=1026, the app_links tag) delivered to the running instance → clean forwarder exit. Observed on both the Google and Microsoft success round trips, plus the earlier?error=path (now surfaced on-screen by the login screen). No runner build gate remains — the running binary is the gate. flutter build apk --debug/flutter build ios --no-codesign/flutter build macos— manifests/plists parse and build cleanly (needs a machine with the mobile toolchains; also exercises the iOS/macOSCFBundleURLTypesand Android intent-filter additions).- Supabase Auth redirect allow-list configured (2026-10-17, via Supabase Management API on project
pyrgjdvqbjzakuwvbpxw"glp-companion"):uri_allow_listnow =io.supabase.glpcompanion://login-callback,http://localhost:3000/**,com.companionglp.glpcompanion://login-callback,com.companionglp.glpcompanion://**(previous entries preserved; staleio.supabase.*scheme kept for rollback). Post-write GET verified no collateral changes.site_urlintentionally left athttp://localhost:8080— update tohttps://companionglp.comonce the web deployment is live.