52 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 4B — Private Recipe Sources & Mealie Integration (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 4B: Private Recipe Sources & Mealie Integration (architecture §4.9 — self-hosted Mealie sync)
- 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.
Phase 4B Deliverables (2026-10-17)
- Security:
CryptoManagergainsmealie_base_url/mealie_api_tokenvault keys pluswriteMealieCredentials()/readMealieCredentials()/deleteMealieCredentials()— the pairing persists only in the platform keystore (Keychain / EncryptedSharedPreferences / DPAPI) and never enters controller state;cryptoManagerProviderexported fromcore/security/. - Data:
MealieApiClient(lib/features/recipes/data/datasources/mealie_api_client.dart) —testConnectionprobesGET {base}/api/app/aboutwith aGET {base}/api/recipes?perPage=1fallback (HTTP 200 = connected);fetchAllRecipeslistsGET {base}/api/recipes?perPage=-1(bare-array and{items}shapes) and enriches every summary withGET {base}/api/recipes/{slug}(detail failure degrades to the summary), mappingrecipeIngredient(structured{quantity, unit, food, note}maps + legacy strings),recipeInstructions({name, text}section flattening),nutrition("512 kcal"/"30.5 g"string parsing →MacroProfile), ISO-8601prepTime/cookTime→ minutes,recipeYield/recipeYieldQuantity→ servings; image{base}/api/media/recipes/{id}/images/min-original.webp, source{base}/recipe/{slug}; the Mealie UUID is reused asRecipe.idfor idempotent upserts (local UUID v4 fallback); injectablehttp.Client, 15 s timeout,MealieApiException, staticisMealieSourceUrlmatcher (prefix + host fallback for scheme drift). - Data:
AppDatabase.upsertRecipe(insertOnConflictUpdate) +DriftRecipeRepository.syncFromMealie(baseUrl, token)callingMealieApiClient.fetchAllRecipesand upserting normalized rows while preservingcreatedAt(returns the synced row count). - Presentation:
MealieIntegrationController(lib/features/recipes/presentation/controllers/mealie_integration_controller.dart) — async vaulted-credential load,testAndSave(URL/token validation + connectivity-gated vault write returning inline errors),syncNow(spinner state, status/error outcome),disconnect,clearMessages,isMealieRecipebadge/filter helper; providersmealieApiClientProvider,mealieSyncRepositoryProvider(null when tests override the catalog with a fake),mealieIntegrationControllerProvider. - Settings: "Private Recipe Integrations" card — "Mealie Self-Hosted Instance" row with Connected/Disconnected status badge; disconnected → "Connect Mealie" modal dialog (Server URL field hint
https://mealie.kellfamilyhome.com, obscured API Bearer Token field hint "Generate in Mealie under User Profile -> API Tokens", inline error text, "Test & Save" verifies connectivity before persisting credentials); connected → active endpoint display, "Sync Now" (progress spinner while syncing), "Disconnect"; screen-level listener surfaces status/error snackbars. - Recipe Vault:
RecipeSourceFilterchips [All / Web Scraped / Mealie] above the macro chips, subtle Mealierestaurant_menubadge with tooltip ("Synced from your Mealie server") on synced cards, and an AppBar "Sync Private Recipes" icon action (spinner while syncing) rendered only when Mealie is configured. - Regression fix:
_SettingsCardtitle Row wraps itsTextinExpanded— the new, longer section title overflowed by 91 px at 390 dp (caught by the mobile navigation widget test, which renders with the Ahem test font where every glyph advance equals the font size); all card titles now shrink/wrap instead of overflowing narrow layouts. - Tests:
test/features/recipes/mealie_parser_test.dart— 14 tests: identity/media/times/yield mapping, structured + string ingredient mapping, ingredient JSON serialization round-trip (toMap/fromMap), instruction flattening, nutrition string parsing, UUID-v4 fallback, null-title rejection, listing/detail request shape + Bearer auth header, summary degradation on detail failure, 401 →MealieApiException, connection probes (about / fallback / unreachable / empty input),isMealieSourceUrlmatching incl. scheme drift.
Phase 4B Verification Gates (2026-10-17)
flutter analyze— No issues found.- Codegen:
dart run build_runner build(flutter pub runis retired on current Flutter;--delete-conflicting-outputsis now ignored by build_runner) — drift re-emitsapp_database.g.dartbyte-identically (no schema change). flutter test— 184/184 green (170 pre-existing + 14 new Mealie tests), including the 390 dp mobile navigation suite.- Manual Mealie round trip on a reachable instance: Settings → Private Recipe Integrations → Connect Mealie (URL + API token) → Test & Save → badge flips to Connected → Sync Now (or the Recipe Vault AppBar sync action) → vault shows the Mealie badge and the Mealie/Web Scraped filter chips isolate each source.