glp-companion/BUILD_STATE.md

34 KiB
Raw Blame History

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 3B retrofit — Platform Health Integrations & HealthGateway end-to-end (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 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: DoseEntry entity, 12-quadrant InjectionSite rotation model, Glp1Compound value object (Sema 168h / Tirze 120h / Lira 13h), IMedicationRepository contract.
  • Domain: CalculateSerumLevel use case implementing C(t) = Σ D_i · 0.5^((t − t_i)/t_half).
  • Data: injection-history CRUD on AppDatabase + drift-backed MedicationRepository (record ↔ entity mapping).
  • Presentation: MedicationController (Riverpod StateNotifier) with serum estimation, least-recently-used site rotation, and reminder scheduling; refactored MedicationScreen (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; timezone dependency added.
  • Regression fix: SupabaseAuthService now 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-memory devAuthBypassProvider) 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 by test/features/auth/dev_auth_bypass_test.dart (3 tests).

Phase 3 Deliverables (2026-10-16)

  • Domain: FoodLogEntry (+ MealType enum), HydrationLogEntry, BiometricEntry (+ BiometricSource enum), INutritionRepository, IHealthSyncRepository — all pure Dart.
  • Data: AppDatabase schema v2 (+ migration for v1 installs) with FoodLogsTable, HydrationLogsTable, BiometricLogsTable, range-filtered watch/get/CRUD helpers, clearSyncedRange for idempotent re-syncs; drift-backed NutritionRepository and HealthSyncRepository (record ↔ entity mapping).
  • Platform: HealthGateway rewrite — IHealthGateway + conditional-import factory; mobile impl (health_gateway_mobile.dart) wraps package:health for WEIGHT (kg→lb normalization) and ACTIVE_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 (Riverpod AsyncNotifier; today's logs, 100 g protein / 96 oz water targets, meal grouping, quick-add water) and BiometricController (weight history, 7/30-day deltas, manual entry, native sync with per-day active-energy aggregation); LogFoodDialog and LogWeightDialog.
  • Presentation: NutritionScreen upgraded — 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 runs HealthGateway sync on Android/iOS and opens the manual dialog on Windows/Web; status snackbars via ref.listen.
  • Tests: 23 new tests — nutrition repository (in-memory drift: CRUD, day-range filter, watch, MealType round-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 nutrition MealType), IRecipeRepository, IMealPlanRepository — all pure Dart; core/utils/uuid.dart v4 generator (no new dependency).
  • Domain: ScrapeRecipeFromUrl use case — package:http download (injectable RecipeHtmlFetcher for tests), package:html DOM parse, JSON-LD @graph/root/array/@type-list navigation, HowToStep/HowToSection instruction flattening, ISO-8601 prepTime/cookTime, recipeYield, regex nutrition parsing (calories, proteinContent, carbohydrateContent, fatContent), image list/object unwrap, OpenGraph fallback (og:title/og:image/og:description), typed RecipeScrapeException.
  • Data: AppDatabase schema v3 (migration for v1/v2 installs) with RecipesTable (UUID PK, JSON ingredient/instruction columns, macros, createdAt) and MealPlanSlotsTable (UUID PK, plannedDate, mealType, nullable recipeId FK with ON DELETE SET NULL, customNotes, isCompleted); drift-backed DriftRecipeRepository (URL-deduplicated importRecipe, createdAt preserved on re-import) and DriftMealPlanRepository (half-open [from, to) window, upsert-by-id).
  • Presentation: RecipeImportController (Riverpod Notifier; URL submit → loading → preview modal state, live catalog watch stream, confirm-save/dismiss/delete) and MealPlanCalendarController (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: MealPlannerScreen refactor — 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) with DragTarget meal 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-@type extraction, macro/ingredient/instruction/duration/yield parsing, malformed-JSON fallthrough, OpenGraph fallback, execute() URL validation & error mapping) and meal_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 itemListElement before using the section name (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 LayoutBuilder in MealPlannerScreen collapses the page into a single scrollable column (with a fixed-height calendar) whenever the body is shorter than ~520 dp. Covered by test/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 (+ PantryCategory aisle enum with infer() keyword heuristic, ExpirationStatus warning buckets), ShoppingListItem, IPantryRepository, and two pure-Dart value objects — ingredient_name.dart (lowercase, paren-stripped, plural-folded canonical keys: eggs → egg) and measurement_units.dart (mass/volume/count dimensions with lb↔oz↔g↔kg and ml/l/tsp/tbsp/cup conversion).
  • Domain: CalculateShoppingDelta use case — aggregates the active 7-day MealPlanSlot window's Recipe ingredient manifests by canonical name, applies deficit = totalRequired − currentPantryQuantity in dimension base units (dominant recipe unit wins display, 2-decimal rounding), upserts isAutoGenerated rows with sourceRecipeTitles (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: AppDatabase schema v4 (migration for v1–v3 installs) with PantryItemsTable (UUID PK, canonical name, quantity/unit, aisle key, nullable expirationDate, lastUpdated) and ShoppingListItemsTable (UUID PK, required quantity/unit, purchased/auto flags, JSON sourceNotes for recipe titles); drift-backed DriftPantryRepository (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) and ShoppingListController (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, syncWithMealPlan reading the planner's Monday–Sunday window + recipe catalog).
  • Presentation: PantryScreen refactor — 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-end syncWithMealPlan, and inventory canonicalization/quick-adjust clamp.

Phase 6 Deliverables (2026-10-16)

  • Platform: HealthGateway hardened — IHealthGateway factory now accepts an ILocalBiometricReader (core contract); mobile driver guards with kIsWeb + defaultTargetPlatform + dart:io cross-check, probes Health Connect SDK availability (getHealthConnectSdkStatus) and degrades with a debug warning when missing/disabled, validates READ_WEIGHT/READ_STEPS/READ_TOTAL_CALORIES_BURNED/READ_ACTIVE_CALORIES_BURNED permissions via hasPermissions/requestAuthorization before every read, and keeps all HealthKit entitlement failures inside try/catch (false + warning, never an unhandled exception).
  • Platform: fallback driver (health_gateway_fallback.dart) now serves locally logged biometrics through an injected ILocalBiometricReader (wired to the drift cache by DriftLocalBiometricReader in the health_sync controller) and emits a debug warning instead of attempting native SDK calls; Android manifest declares the four Health Connect READ_* permissions matching the gateway request set.
  • Security: CryptoManager platform-safe vault config — Android AndroidOptions(encryptedSharedPreferences: true), iOS/macOS KeychainAccessibility.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: isAuthenticatedProvider extracted in app_router.dart so 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.dart extended with real-world fixtures (test/fixtures/recipes/bbc_good_food_style.html root-level payload, allrecipes_style.html multi-script complex @graph with HowToSection nesting, list-valued yield/image) — 2 new fixture tests.
  • Tests: delta_calculator_test.dart extended 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, /pantry under authenticated and dev-bypass conditions, mobile bottom-bar switching, unauthenticated bounce to /login) and test/features/nutrition/nutrition_widget_test.dart (3 tests — protein/hydration LinearProgressIndicator values and labels updating reactively on food/water logs, goal-met chips).
  • CI/CD: .forgejo/workflows/ci.yml for git.kellsupport.com/mtkell/glp-companion (GitHub Actions-compatible syntax for Forgejo Actions) — push to main + all PRs in ghcr.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 by ScrapeRecipeFromUrl) removed with its empty directory; zero TODO/FIXME markers in lib/ or test/.
  • Docs: README.md rewritten (architectural overview + directory layout, local dev prerequisites incl. VS Code/Cline setup and .env configuration via new .env.example, SQLCipher AES-256 key lifecycle, CI pipeline details); BUILD_STATE.md Phase 1–6 checkboxes complete.

Phase 7 Deliverables (2026-10-16)

  • Domain: MuscleGroup enum + ExerciseEntry entity (pure Dart), IExerciseRepository contract, session-scoped InMemoryExerciseRepository (drift table deferred to a follow-up migration).
  • Presentation (exercise): ExerciseController (weekly distinct-session rollup vs. the recommended 3 sessions/week lean-mass target) + ExerciseScreen with weekly strength-frequency card, day-grouped working-set log (swipe-to-delete), and the LogExerciseDialog (muscle group / name / sets / reps / weight).
  • Presentation (Recipe Vault): new features/recipes screen — pinned URL import bar wired to RecipeImportController, search field + macro ChoiceChip filters ("Protein > 30g", "Protein > 20g", "Under 500 kcal", All), responsive GridView.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): MealPlannerScreen stripped to week navigation + 7-day calendar + daily protein rollups; inline import bar and SavedRecipesTray removed (moved to Recipe Vault); AssignSlotDialog/drag-drop replaced by the searchable AssignRecipeSheet modal 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 feeding NutritionController.updateTargets), Security & Storage (SQLCipher AES-256 badge with web-degradation note), Integrations (Apple Health / Health Connect / HealthGateway toggles via IntegrationsController, native permission round-trips through IHealthGateway).
  • Shell: app_router.dart registers /medication, /nutrition, /exercise, /meal-planner, /recipes, /pantry, /settings; AdaptiveHomeScaffold desktop rail expanded to the 7 specified destinations (medication_liquid, restaurant, fitness_center, calendar_month, menu_book, inventory_2, settings) with NavigationRailLabelType.all inside 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.dart extended to all 7 rail routes + mobile "More" drawer flow (7 tests total); meal_planner_screen_test.dart rewritten for the calendar-only layout including the assign-sheet opening; SettingsScreen badge rows use Flexible + 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-like User-Agent draw HTTP 403 from anti-bot gates. Fixes in ScrapeRecipeFromUrl: (1) browser-like Accept/Accept-Language/User-Agent headers 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. Shipped supabase/functions/recipe-proxy/ — a deployable Deno Edge Function relay (http(s)-only, SSRF host guard, 2 MiB cap, 15 s timeout, CORS-enabled) — deploy with supabase functions deploy recipe-proxy and pass its URL via the define. Tests: 7 new cases in recipe_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, …) because web/ was missing the WASM database artifacts connection/web.dart documents as required. Added web/sqlite3.wasm (740 KB, downloaded from the sqlite3-3.7.1 GitHub release — matches the locked sqlite3 version; drift 2.35.x changelog mandates a 3.x wasm) and web/drift_worker.js (copied from the locked drift-2.35.2 package). Both files are not gitignored, so they ship with the repo and the CI flutter build web step. Verified: flutter build web --no-tree-shake-icons packages both into build/web/. NOTE: bump both files when upgrading drift/sqlite3 (worker JS must match the Dart package version exactly; wasm must be ≥ 3.x).
  • Android toolchain fixes (2026-10-16): flutter run on an emulator failed on three pre-existing gaps (Android builds had never been exercised — Phase 6 gates ran on Windows/Web): (1) flutter_local_notifications requires core library desugaring → enabled isCoreLibraryDesugaringEnabled + com.android.tools:desugar_jdk_libs:2.1.4 in android/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=false in android/gradle.properties; (3) the pinned health plugin 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 LogInjectionDialog painted yellow/black stripes on phone widths — DropdownButtonFormField's selected-item Row cannot ellipsize long labels ("Tirzepatide (Mounjaro / Zepbound)") without isExpanded: true. Fixed with isExpanded: true + maxLines: 1/TextOverflow.ellipsis on the menu items; regression tests in test/features/medication/log_injection_dialog_test.dart pump 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.png 32, icons/Icon-{192,512}.png with feathered circular alpha, icons/Icon-maskable-{192,512}.png at 80% safe-zone over brand sage #D4E4C7), Android (legacy mipmap-*/ic_launcher.png 48–192 px + new adaptive icons: mipmap-anydpi-v26/ic_launcher{,_round}.xml, drawable/ic_launcher_background.xml sage vector, drawable-*/ic_launcher_foreground.png 66/108 dp safe circle), iOS (all 15 AppIcon.appiconset PNGs — opaque RGB, no alpha, per App Store validation), macOS (app_icon_{16..1024}.png), Windows (app_icon.ico multi-res 16–256 px). Web polish: manifest.json name/description/theme (#375A53 teal, #D4E4C7 sage) and index.html title/description/theme-color updated from Flutter placeholders. Verified: PIL dimension/mode/alpha assertions on every output + XML well-formedness + ICO frame enumeration; flutter build web --no-tree-shake-icons and flutter build apk --debug package 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), and IHealthGateway.checkStatus / requestAuthorization / fetchLatestBiometrics / disconnect with defaults derived from the existing primitives. NOTE for future implementors: Dart implements never inherits method bodies — drivers and test fakes must extends IHealthGateway (or reimplement the members) to pick up the defaults.
  • Platform (mobile driver): BLOOD_GLUCOSE added to the iOS/Android package:health read sets (mmol/L → mg/dL normalization ×18.0182); checkStatus probes hasPermissions + Health Connect SDK readiness before answering; samsungHealth aliases the Health Connect binding on Android (federated Samsung Health + Pixel sensor feeds); disconnect clears the cached SDK probe (neither HealthKit nor Health Connect exposes programmatic revocation — OS-level revoke stays in system settings); fetchLatestBiometrics aggregates 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; fetchLatestBiometrics composes the encrypted drift cache via ILocalBiometricReader with source manualFallback; zero platform-channel calls on Web/Windows/macOS (safe dummy states, never a crash).
  • Presentation (IntegrationsController rewrite): status-based orchestration replacing the old toggles — refreshStatuses() probes all three providers on build (disposal-flag guarded; Riverpod 2.x Ref has no mounted), connectAppleHealth/connectHealthConnect run requestAuthorization round-trips (denial → permissionDenied + inline snackbar), disconnectProvider tears down via the gateway and re-probes.
  • Presentation (SettingsScreen): Health Integrations card 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; /settings route wiring unchanged.
  • Manifests: Android — android.permission.health.READ_BLOOD_GLUCOSE added to the Health Connect permission set, com.google.android.apps.healthdata package query + ACTION_SHOW_PERMISSIONS_RATIONALE intent in <queries>, and ViewPermissionUsageActivity activity-alias (VIEW_PERMISSION_USAGE / HEALTH_PERMISSIONS) per architecture §4.7.4; iOS — NSHealthShareUsageDescription + NSHealthUpdateUsageDescription added to ios/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 _FakeHealthGateway switched to extends for the inherited defaults; app_navigation_test.dart updated 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 by settings_screen_test.dart — interactive flutter run -d windows compiles the same toolchain)

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 keeps package:health/dart:io out 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 behind kAuthCallbackRedirectUrl) is now registered in every OS routing table —
    • iOS: Runner/Info.plist CFBundleURLTypes (CFBundleTypeRole=Editor, scheme com.companionglp.glpcompanion).
    • macOS: Runner/Info.plist CFBundleURLTypes (same shape).
    • Android: VIEW/DEFAULT/BROWSABLE intent-filter with android:scheme="com.companionglp.glpcompanion" on .MainActivity (existing singleTop launch mode routes onNewIntent to the running activity consumed by app_links).
    • Windows runner: RegisterAppLinkProtocol() (windows/runner/utils.cpp) writes the per-user protocol under HKCU\Software\Classes\com.companionglp.glpcompanion (URL Protocol + shell\open\command → "<exe>" "%1", no elevation, re-registered every launch); wWinMain calls SendAppLinkToInstance() (exported by the app_links Windows DLL) before creating a window so a protocol activation while the app is running forwards the URI to the existing instance over WM_COPYDATA instead of spawning a second window. Cold-start links continue to be read from the command line by the plugin.
  • macOS entitlements fix (pre-existing gap, required for the OAuth round trip): com.apple.security.network.client added to DebugProfile.entitlements and Release.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.dart doc 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 (and OutputDebugString): launch argv, SendAppLinkToInstance() outcome, and — in the original instance — every WM_COPYDATA receipt with dwData/payload (expected dwData=1026 = WM_USER+2, the app_links tag).
    • Fallback path hardened: when forwarding fails (e.g. UIPI drops WM_COPYDATA to 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:logging root listener (kDebugMode) added in lib/main.dart so supabase_flutter's auth/deeplink diagnostics (handle deeplink uri, Error while getSessionFromUrl) are visible under flutter run; logging: ^1.3.0 promoted to a direct dependency in pubspec.yaml.
  • 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's notifyException emits them as stream errors on onAuthStateChange, and nothing rendered them. LoginScreen now also listens to authStateProvider errors (router guard is unaffected — it reads isAuthenticatedProvider/live session directly, only bumping its refresh counter) and _messageFor translates 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=1026 forwarded 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 optional email ID 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 /token grant_type=pkce → 200 at 18:40:59Z, user_signedup for mtkell@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): isAuthenticatedProvider read the non-reactive currentSession getter inside a plain Provider, caching its first-read value forever (nobody invalidated it). Booting already-signed-in cached true, 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 by ref.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's refreshListenable bump is deferred via Future.microtask so the redirect guard can never re-read isAuthenticatedProvider inside Riverpod's provider-notification cascade (ordering edge where a stale cached value could be observed); field log confirmed the signedOut event 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-instance WM_COPYDATA handoff into a persisted session; sign-out now redirects to /login and re-authentication re-enters the app without a relaunch.
  • 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/macOS CFBundleURLTypes and Android intent-filter additions).
  • Supabase Auth redirect allow-list configured (2026-10-17, via Supabase Management API on project pyrgjdvqbjzakuwvbpxw "glp-companion"): uri_allow_list now = io.supabase.glpcompanion://login-callback,http://localhost:3000/**,com.companionglp.glpcompanion://login-callback,com.companionglp.glpcompanion://** (previous entries preserved; stale io.supabase.* scheme kept for rollback). Post-write GET verified no collateral changes. site_url intentionally left at http://localhost:8080 — update to https://companionglp.com once the web deployment is live.