Commit Graph

13 Commits

Author SHA1 Message Date
Mike Kell 86a7cd9852 fix(android,health): unblock Health Connect grant flow and cloud-backed sync
Field-reported on the alpha APK: the Samsung/Pixel row failed with
'Health permissions were denied' without ever showing a dialog, and the
Google Health connect reported the same error despite a successful OAuth.

- MainActivity now extends FlutterFragmentActivity: package:health v13
  casts the host activity to androidx ComponentActivity to call
  registerForActivityResult, so with plain FlutterActivity the permission
  launcher stayed null and requestAuthorization() silently returned false
  (HealthPlugin.kt:317 'Permission launcher not found')
- add the androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE intent-filter
  required by the health package README on the main activity
- drop STEPS / READ_STEPS from the Android permission set: steps are never
  fetched anywhere and the README ties step access to ACTIVITY_RECOGNITION,
  which this app does not declare (weight, total + active calories, blood
  glucose remain, all declared in the manifest)
- syncFromHealth treats a native permission denial as fatal only when no
  cloud partner holds a grant, so a Google Health-connected sync proceeds
  without the OS permission (native reads degrade to empty)
- tests: biometric controller covers native-denial + cloud-ready sync
  (cloudReady fake override); suite 185/185, analyze clean, debug APK
  builds (also clears the standing flutter build apk --debug gate)
- BUILD_STATE: record both fixes, check off the Android OAuth client and
  connect gates, add the Health Connect on-device verification gate
2026-10-18 11:31:50 -04:00
Mike Kell 0a77b13032 ci(release): fixed google callback authentication 2026-10-18 10:29:06 -04:00
Mike Kell 3d5135c1c5 feat(recipes,settings): add Phase 4B private recipe sources with Mealie integration
- security: add mealie_base_url / mealie_api_token vault keys and
  write/read/deleteMealieCredentials() to CryptoManager; the pairing
  persists only in the platform keystore (Keychain / EncryptedSharedPreferences /
  DPAPI) and never enters controller state
- data: add MealieApiClient - testConnection probes GET /api/app/about with
  GET /api/recipes?perPage=1 fallback (200 = connected); fetchAllRecipes lists
  GET /api/recipes?perPage=-1 and enriches each summary with
  GET /api/recipes/{slug}, mapping recipeIngredient (structured maps + strings),
  recipeInstructions (section flattening), nutrition display strings, ISO-8601
  prep/cook times, yield, image + source URLs; Mealie UUID reused as Recipe.id
- data: add AppDatabase.upsertRecipe (insertOnConflictUpdate) and
  DriftRecipeRepository.syncFromMealie preserving createdAt on re-sync
- presentation: add MealieIntegrationController (async credential load,
  connectivity-gated Test & Save, Sync Now with spinner state, Disconnect,
  isMealieRecipe badge/filter helper) plus api/sync providers
- settings: add Private Recipe Integrations card - Connect Mealie modal with
  server URL hint, obscured API token field, Test & Save before persistence;
  connected state shows endpoint, Sync Now and Disconnect
- recipes: add All / Web Scraped / Mealie source filter chips, subtle Mealie
  badge on synced cards, and AppBar Sync Private Recipes action when configured
- fix _SettingsCard title Row overflow at 390dp (Expanded wrap) caught by the
  mobile navigation widget test; add mealie_parser_test.dart (14 tests), suite
  green at 184/184, flutter analyze clean
- docs: add architecture 4.9 Mealie design and multiplatform UX adaptation
  spec; BUILD_STATE marks Phase 4B complete
2026-10-17 20:46:36 -04:00
Mike Kell e43603be1b feat(nutrition,pantry): add Phase 3C nutritional search engine, barcode scanning, and dual-source food cache
Implements the architecture 4.8 resolution ladder: local SQLCipher cache ->
Open Food Facts (4s timeout) -> USDA FoodData Central fallback, with every
remote hit written through to the new FoodProductsTable (schema v5).

- domain: FoodProduct entity + IFoodSearchRepository contract (pure Dart)
- data: UsdaFdcClient (FDC nutrient-ID mapper 1008/1003/1004/1005/1079/1093,
  api_key sent as query parameter - FDC ignores body keys and 403s otherwise),
  OpenFoodFactsClient (userAgent with app identity, SearchTerms search,
  getProductV3 barcode lookup), shared RemoteFoodDataSource interface
- presentation: FoodSearchController (400ms debounce, request-token
  supersession), FoodSearchScreen (MobileScanner sheet on mobile, manual UPC
  dialog + notification on desktop/web, macro action sheet with serving
  multipliers, Log to Diary / Add to Pantry actions)
- integration: /food-search route with ?preset=pantry, Nutrition screen entry
  button, Pantry screen Scan Item action
- hardening: FoodSearchUnavailableException distinguishes total source outage
  from a healthy empty result; GoogleHealthConnector dotenv reads now guarded
  with isInitialized (fixes NotInitializedError crashing app_navigation_test)
- tests: +16 (fallback sequence cache->OFF->USDA for text and barcode,
  USDA request shape via MockClient); full suite 170/170, analyze clean
2026-10-17 19:37:06 -04:00
Mike Kell 5b5e7e2f57 feat(platform,settings,nutrition): add Google Health cloud sync for desktop
Cloud connector framework: ICloudHealthConnector contract, supportsCloudSync / isCloudSyncReady on IHealthGateway, and a HealthProviderType.googleHealth slot; a conditional-import shim (io implementation / web stub) keeps platform channels out of web builds.

Google Health connector (successor to the retired Fitbit Web API, shutdown 2026-10-30): OAuth2 authorization-code + PKCE through the system browser with a loopback redirect; read-only activity-and-fitness + health-metrics scopes; tokens in flutter_secure_storage with transparent refresh; paginated weight and active-energy-burned dataPoints reads with kg-to-lb normalization from the per-point unit field; pure unit-tested parsers.

Gateway wiring: both drivers store injected connectors (desktop runs MobileHealthGateway through the io conditional import; the fallback driver is web-only); cloud status/authorize/disconnect route first and reads merge native + cloud non-fatally; syncFromHealth accepts cloud-only sources and imports persist as BiometricSource.cloudSync.

UI: Google Health row in Settings with Connect/Disconnect and auto-import on connect; explanatory desktop copy on the Apple Health and Health Connect rows; the Nutrition weight card syncs through a connected cloud partner and surfaces the outcome as a snackbar, degrading to the manual dialog when no source is connected.

Field fixes recorded in BUILD_STATE: io-driver connector wiring, duplicate-implementation merge correction, weight-card sync gate. Tests: google_health_parser_test suite; settings_screen_test updated for the fourth integration row. The legacy Fitbit connector was fully removed before shipping.
2026-10-17 18:06:36 -04:00
Mike Kell ddcdefa596 feat(platform,settings,tests): complete Phase 3B platform health integrations and HealthGateway end-to-end
Implements architecture section 4.7: IHealthGateway now exposes the Phase 3B contract (HealthProviderType, HealthConnectionStatus, HealthDataPayload, checkStatus/requestAuthorization/fetchLatestBiometrics/disconnect) with defaults derived from the existing primitives. Drivers and test fakes switch from implements to extends so Dart inherits the default bodies.

- Mobile driver: BLOOD_GLUCOSE added to iOS/Android read sets (mmol/L to mg/dL normalization); checkStatus probes hasPermissions plus Health Connect SDK readiness; samsungHealth aliases the Health Connect binding on Android (federated Samsung/Pixel feeds); disconnect clears the cached SDK probe.

- Fallback driver: safe dummy states on Windows/Web/macOS (manualFallback connected, enterpriseFhir notConfigured, native unsupportedPlatform); fetchLatestBiometrics serves the encrypted drift cache; zero platform-channel calls.

- Settings: IntegrationsController rewritten from toggles to status orchestration (refreshStatuses, connect/disconnect round-trips, denial to permissionDenied plus snackbar); SettingsScreen gains a Health Integrations card with Apple Health, Health Connect (Samsung & Pixel), and SMART on FHIR rows showing live status badges and Connect/Disconnect buttons with desktop-safe disabled states.

- Manifests: Android adds READ_BLOOD_GLUCOSE, healthdata package query, ACTION_SHOW_PERMISSIONS_RATIONALE intent, and ViewPermissionUsageActivity alias; iOS adds NSHealthShareUsageDescription and NSHealthUpdateUsageDescription.

- Tests: +13 (fallback dummy-state matrix, integrations controller transitions, Settings desktop disabled-states and mobile connect/disconnect round trip); existing fake switched to extends; navigation test updated for the renamed card title.

Verified: build_runner 191 outputs; flutter analyze clean; flutter test 148/148; flutter build windows debug succeeded.
2026-10-17 12:01:59 -04:00
Mike Kell 51364a8f4f feat(platform,tests,ci): complete Phase 6 cross-platform hardening, integration tests, and Forgejo CI/CD
- Harden HealthGateway: kIsWeb/defaultTargetPlatform/dart:io guards, Health
  Connect SDK availability probe with graceful degradation, READ_WEIGHT/
  READ_STEPS/READ_TOTAL_CALORIES_BURNED permission validation before reads,
  HealthKit entitlement failures contained (false + debug warning), and a
  desktop/web fallback that serves locally logged biometrics through the new
  ILocalBiometricReader contract (drift-backed adapter wired in health_sync).
- Platform-safe CryptoManager vault: EncryptedSharedPreferences (Android),
  Keychain first_unlock (iOS/macOS), DPAPI-sealed file (Windows), libsecret
  (Linux); vault failures degrade to an ephemeral in-memory SQLCipher key.
- Declare the four Health Connect READ_* permissions in AndroidManifest.xml.
- Extract isAuthenticatedProvider so integration tests can simulate auth
  sessions without booting Supabase.
- Add 24 tests (99 -> 123): steady-state PK suite (Semaglutide 168h /
  Tirzepatide 120h geometric series, superposition, titration, washout),
  real-world recipe JSON-LD fixtures (BBC-style root, AllRecipes @graph),
  pantry unit-conversion group (kg/g, l/ml, dimension guard, synonyms),
  app navigation integration tests (rail + bar, authenticated + dev-bypass
  + unauthenticated), and reactive nutrition progress-bar widget tests.
- Fix NutritionScreen narrow-width overflows: Flexible protein title and
  Wrap action buttons stack cleanly on compact phones.
- Add .forgejo/workflows/ci.yml for git.kellsupport.com/mtkell/glp-companion
  (push to main + PRs, Flutter container: checkout, mock .env, pub get,
  build_runner, analyze --fatal-infos, test --coverage, build bundle + web).
- Remove dead lib/features/recipes/data/recipe_scraper.dart; rewrite
  README.md (architecture, dev setup, SQLCipher details, CI docs); add
  .env.example; allow it through .gitignore; update BUILD_STATE.md with
  Phase 6 deliverables and complete Phases 1-6 checklist.

Verified: build_runner 44 outputs, flutter analyze --fatal-infos clean,
flutter test 123/123, flutter build web and flutter build bundle succeed.
2026-10-16 14:59:15 -04:00
Mike Kell 747d917aed feat(pantry): complete Phase 5 pantry delta engine and auto-shopping generator
Domain (pure Dart):
- Add PantryItem and ShoppingListItem entities plus the PantryCategory aisle
  enum (produce/meat_seafood/dairy/dry_goods/spices/other) with a keyword
  infer() heuristic and ExpirationStatus warning buckets (expired /
  expiringSoon within 3 days)
- Add measurement_units value object (mass/volume/count dimensions with
  lb/oz/g/kg and ml/l/tsp/tbsp/cup conversion onto a shared base scale) and
  ingredient_name canonicalizer (lowercase, parenthetical stripping,
  plural folding: eggs -> egg, lbs -> lb)
- Define IPantryRepository contract covering inventory and checklist CRUD
- Add CalculateShoppingDelta use case: aggregates the active 7-day meal-plan
  slot window's recipe ingredient manifests by canonical name, computes
  deficit = totalRequired - currentPantryQuantity in base units (dominant
  recipe unit wins display, 2-decimal rounding), upserts isAutoGenerated
  rows with stable ids and source recipe titles, clears stale auto rows once
  stock covers the plan, and never modifies or removes manual rows

Data:
- Bump AppDatabase to 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) plus watch/get/upsert/delete helpers
- Implement DriftPantryRepository (record <-> entity mapping, enum keys,
  JSON recipe-title round-trip)

Presentation:
- Add 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?" unit-converting stock merge or aisle-inferred row
  creation, syncWithMealPlan reading the planner's Monday-Sunday window and
  recipe catalog)
- Refactor PantryScreen: 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,
  "From: <recipe>" origin chips on auto rows, checklist filters, check-off
  -> pantry prompt, and Add Pantry Item / Add Shopping Item dialogs

Testing:
- 10 new tests in delta_calculator_test.dart: 300 g on hand vs 800 g
  requirement -> +500 g entry, sufficient stock -> no entry, manual rows
  preserved across repeated syncs, 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; full suite green (99/99)
- flutter analyze clean (No issues found!) after codegen; BUILD_STATE.md:
  mark Phase 5 complete and record all verification gates
2026-10-16 10:33:14 -04:00
Mike Kell bdee2de35f feat(meal_planner): complete Phase 4 recipe scraper and meal planning calendar
Domain (pure Dart):
- Add Recipe, MacroProfile, and RecipeIngredient entities (quantity/unit/
  item parsing: decimals, ASCII/unicode/mixed fractions, known-unit
  vocabulary) plus MealPlanSlot reusing the nutrition MealType enum
- Define IRecipeRepository and IMealPlanRepository contracts
- Add ScrapeRecipeFromUrl use case: package:http download with injectable
  fetcher, package:html DOM parse, JSON-LD @graph/root/array/list-@type
  navigation, HowToStep/HowToSection flattening, ISO-8601 prep/cook times,
  recipeYield, regex nutrition parsing, OpenGraph fallback, and typed
  RecipeScrapeException
- Add core/utils/uuid.dart RFC-4122 v4 generator (no new dependency)

Data:
- Bump AppDatabase to schema v3 (migration for v1/v2 installs) with
  RecipesTable (UUID PK, JSON ingredient/instruction columns, macros) and
  MealPlanSlotsTable (UUID PK, nullable recipeId FK ON DELETE SET NULL)
- Implement DriftRecipeRepository (URL-deduplicated importRecipe, createdAt
  preserved on re-import) and DriftMealPlanRepository (half-open [from, to)
  window, upsert by id)

Presentation:
- Add RecipeImportController (URL submit -> loading -> preview modal state,
  live catalog watch stream, confirm-save/dismiss/delete) and
  MealPlanCalendarController (Monday-Sunday window navigation, slot
  assign/upsert/clear, prepared toggles, per-day protein rollup against the
  100 g target, deleted-recipe slot normalization)
- Rework MealPlannerScreen: URL import bar with spinner, RecipePreviewDialog
  (macros, ingredients, steps, save with error surface), draggable
  SavedRecipesTray with protein badges, responsive WeeklyCalendarView
  (>=900 dp: 7-column grid, below: tabbed pager) with DragTarget slots and
  daily protein indicators, AssignSlotDialog quick-assign modal

Fixes:
- Eliminate desktop resize overflow (RenderFlex hatched stripes): wide-mode
  day columns scroll vertically, and a LayoutBuilder collapses the page
  into a single scrollable column with a fixed-height calendar when the
  window body is shorter than ~520 dp

Testing:
- 29 new tests: recipe scraper (JSON-LD variants, macro/ingredient/
  instruction/duration parsing, OG fallback, execute error mapping),
  meal-plan repositories (window filter, upsert, dedup), calendar
  controller (rollup, navigation, prepared/custom entries), and screen
  widget tests pumping short-compact/short-wide/tall/extremely-short
  window sizes to prove no overflow; full suite green (89/89)
- BUILD_STATE.md: mark Phase 4 complete and record all verification gates
2026-10-16 09:07:41 -04:00
Mike Kell 094cf08f25 feat(nutrition,health_sync): complete Phase 3 nutrition tracking and HealthGateway sync
- Domain: FoodLogEntry/MealType, HydrationLogEntry, BiometricEntry/BiometricSource entities plus INutritionRepository and IHealthSyncRepository contracts (pure Dart).

- Data: AppDatabase schema v2 (FoodLogs/HydrationLogs/BiometricLogs tables, v1 migration, range-filtered CRUD) with drift-backed NutritionRepository and HealthSyncRepository; startOfDay/endOfDay date utils.

- Platform: HealthGateway conditional-import factory - mobile impl wires package:health (WEIGHT kg->lb, ACTIVE_ENERGY_BURNED J->kcal, permissions, write-back) with guarded fallbacks; web/desktop impl never touches platform channels (manual entry + drift cache).

- Presentation: NutritionController and BiometricController AsyncNotifiers (100g protein / 96oz water targets, meal grouping, 7/30-day weight deltas, idempotent 90-day native sync with per-day active-energy aggregation); LogFoodDialog and LogWeightDialog.

- UI: NutritionScreen progress meters, +8/+16/+24oz quick-add, meal-grouped food diary, weight tracker card with Log Weight / Sync (native sync on Android/iOS, manual dialog on Windows/Web) and status snackbars.

- Tests: 23 new tests (in-memory drift repos, controller fakes, fake health gateway covering fallback, permission denial, and duplicate-free re-sync); 60/60 passing, flutter analyze clean, flutter build web succeeds. BUILD_STATE.md updated to Phase 4.
2026-10-16 07:02:28 -04:00
Mike Kell 7682d8317a feat(medication): complete Phase 2 GLP-1 tracking, reminders, and dev bypass
Domain (pure Dart):
- Add 12-quadrant InjectionSite anatomical model with a least-recently-used
  rotation policy, DoseEntry entity, and Glp1Compound value object
  (Semaglutide 168h, Tirzepatide 120h, Liraglutide 13h)
- Add CalculateSerumLevel use case implementing
  C(t) = sum(D_i * 0.5^((t - t_i) / t_half))
- Define IMedicationRepository contract; retire superseded MedicationDose

Data:
- Add injection-history CRUD to AppDatabase (watch/get/insert/update/delete/
  latest-administered), ordered newest first
- Implement drift-backed MedicationRepository with record-to-entity mapping

Presentation and local alerts:
- Add MedicationController (StateNotifier) managing serum estimation, site
  rotation suggestions, and best-effort reminder scheduling
- Rework MedicationScreen: serum card, suggested-site card, interactive log
  dialog (compound, dosage, timestamp, quadrant), history with delete, and a
  60s decay ticker
- Add INotificationService / PlatformNotificationService wrapping
  flutter_local_notifications (one-shot scheduling on Android/iOS/macOS,
  graceful no-op on Web/Windows/Linux), Android boot receivers for reminder
  restoration, and the timezone dependency

Development tooling:
- Add debug-only "Continue Without Signing In" bypass on the login screen,
  backed by an in-memory devAuthBypassProvider; the router guard honors the
  flag and the shell shows a dev-mode banner while active

Fixes:
- Resolve the Supabase client lazily in SupabaseAuthService so widget tests
  and pre-initialization builds no longer throw

Testing:
- 36 new tests covering PK decay math, rotation determinism, repository CRUD
  on an in-memory drift database, controller behavior with fakes, and the
  end-to-end dev bypass flow; full suite green (37/37)
- BUILD_STATE.md: mark Phase 2 complete and record all verification gates
2026-10-16 06:20:23 -04:00
Mike Kell 7b6b646d2c feat(auth): complete Phase 1 passwordless identity and encrypted DB foundation 2026-10-16 05:01:54 -04:00
Mike Kell 98e0d318f3 feat(init): baseline multiplatform architecture with clean arch shell, clinerules, and docs 2026-10-15 21:01:52 -04:00