From bafa8c93770108547d55ea3ccf663a416056977b Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sat, 18 Jul 2026 00:17:11 +0530 Subject: [PATCH 01/14] App-store-readiness prep (Phase 1, legal-review-driven) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non-contested items from the App Store/Play Store legal review: naming hygiene, disclaimers, privacy policy draft, license attribution, OTA gating. Does NOT touch the contract/ToS exposure item — that's separately pending attorney review. - Rename internal-only WHOOP-referencing identifiers to code-hygiene-neutral names; the rendered UI text was already "Fitness age"/"FITNESS AGE" and is unchanged: - crossday_pipeline.dart / derivation_engine.dart: `whoop_age` bundle key -> `fitness_age` - coach_cards.dart: WhoopAgeCard/_WhoopAgeCardState -> FitnessAgeCard/ _FitnessAgeCardState; screens.dart usage site updated to match. Left pairing_screen.dart / profile_screen.dart's import-source label / import_screen.dart untouched — those are legitimate nominative/factual WHOOP references (device name, import source), not branding. - pubspec.yaml description: dropped "WHOOP 4.0 companion" wording, replaced with an affiliation-neutral description ending in "Not affiliated with WHOOP, Inc." - New AffiliationDisclaimer widget (welcome_screen.dart), attorney-reviewed wording, shown once on first-run onboarding (right under the welcome hero) and reused verbatim in the new Settings -> About screen. - New Settings -> About screen (lib/ui/profile/about_screen.dart), wired in from profile_screen.dart's new "About" section: - the affiliation/health-claim disclaimer - "Privacy policy" link (placeholder hosted URL - see TODO below) - "Notice & attribution" link (points at NOTICE.md on GitHub) - "Open-source licenses" -> Flutter's built-in showLicensePage - app name + version (via package_info_plus, already a dependency) - PRIVACY.md at repo root: attorney-review-draft privacy policy per the legal review's exact text. [DATE], [CONTACT EMAIL], and the data-deletion mechanism are left as explicit placeholders for a human to fill in. Flagged in-file and in about_screen.dart: both app stores require a *hosted* URL, not a repo file — a GitHub Pages page (or a page on whatever domain backs wtf.openstrap.*) still needs to be stood up before store submission; kPrivacyPolicyUrl is a placeholder pointing nowhere real yet. - NOTICE.md at repo root: consolidates the affiliation/no-copied-code/ methodology disclaimer already scattered across the three READMEs. No new claims introduced beyond what protocol/README.md and edge/README.md already say; deliberately says nothing about decompiling. - OTA self-update now gated behind a compile-time flag, off by default: - update_service.dart: new `kSideloadOtaEnabled` const (`bool.fromEnvironment('ENABLE_SIDELOAD_OTA', defaultValue: false)`), mirroring the build-time-URL convention already used by backend_client.dart / companion_client.dart. `UpdateService.supported` now requires the flag in addition to Platform.isAndroid. - app_state.dart: `updateAvailable` / `updateMandatory` now also require the flag, so the "Update available" banner never appears at all on a store build (not just falls back to a browser link) - a Play Store or future App Store build gets zero self-update surface unless built with --dart-define=ENABLE_SIDELOAD_OTA=true. License check: ran `flutter pub get` + `flutter pub deps --style=compact`, then scanned every resolved package's LICENSE file (218 packages) for GPL/ AGPL. Zero real hits. The only "GPL" string matches were false positives: MPL-2.0's boilerplate "Secondary Licenses" clause in bluez/dbus/nm/upower (Linux-desktop-only D-Bus bindings, transitive via flutter_blue_plus, unused on iOS/Android) and Flutter's own bundled third-party-notices file (sky_engine, standard for every Flutter app, not specific to this project). flutter analyze: 7 pre-existing info/warnings, none introduced by this change (same deprecated-glassBoxReadiness item already tracked separately). flutter test: all 457 tests pass. --- NOTICE.md | 11 +++ PRIVACY.md | 57 +++++++++++++ lib/compute/crossday_pipeline.dart | 6 +- lib/compute/derivation_engine.dart | 4 +- lib/state/app_state.dart | 10 ++- lib/sync/update_service.dart | 37 +++++++-- lib/ui/insights/coach_cards.dart | 14 ++-- lib/ui/onboarding/welcome_screen.dart | 20 +++++ lib/ui/profile/about_screen.dart | 115 ++++++++++++++++++++++++++ lib/ui/profile/profile_screen.dart | 15 ++++ lib/ui/screens/screens.dart | 2 +- pubspec.yaml | 2 +- 12 files changed, 270 insertions(+), 23 deletions(-) create mode 100644 NOTICE.md create mode 100644 PRIVACY.md create mode 100644 lib/ui/profile/about_screen.dart diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 00000000..97c3ad5d --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,11 @@ +# Notice + +Edge is an independent, open-source project (MIT License — see `LICENSE`). It +is not affiliated with, sponsored by, or endorsed by WHOOP, Inc. or any of its +trademarks. + +No WHOOP source code, binaries, firmware, or copyrighted assets are included +in this repository. The Bluetooth protocol support in this project was +independently developed by observing the band's own Bluetooth communications; +see [the protocol repo's README](https://github.com/OpenStrap/protocol) for +methodology notes. diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 00000000..0b6d3377 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,57 @@ +> **ATTORNEY-REVIEW DRAFT — not final.** This has not been reviewed or approved +> by a licensed attorney and must not be treated as the live policy until it +> has been. `[DATE]`, `[CONTACT EMAIL]`, and the data-deletion mechanism below +> are placeholders a human must fill in before this goes live. Both the App +> Store and Play Store require a **hosted** URL for the privacy policy (not a +> link to this repo file) — this still needs a GitHub Pages page (or a page on +> whatever domain backs `wtf.openstrap.*`) before store submission. See +> `lib/ui/profile/about_screen.dart`'s `kPrivacyPolicyUrl` TODO for the +> in-app link that needs updating once that page exists. + +# Privacy Policy — Edge / OpenStrap + +_Last updated: [DATE]_ + +Edge ("the App") is an independent, open-source project. It is not affiliated +with, sponsored by, or endorsed by WHOOP, Inc. + +**Data we collect** +- Biometric/health data from your paired WHOOP 4.0 band over Bluetooth: heart + rate, heart rate variability (RR intervals), motion (accelerometer), raw + skin-temperature/blood-oxygen sensor channels, and (if you record workouts) + GPS location during those workouts. +- Basic app usage/crash telemetry, only if you opt in. + +**Where your data lives** +By default, all of the above is stored locally on your device only. We do not +upload it anywhere unless you explicitly opt in to one of the following: +- **Cloud backup** — a one-time or periodic encrypted upload of your local + database to our own backend, only if you turn this on in Settings. +- **AI Coach** — if you enable this feature and supply your own API key, + summaries of your data are sent to the AI provider you configure (by + default, OpenAI) to generate coaching responses. This is off by default and + requires your own API key. +- **Health app integration** — if you enable it, the App can write derived + daily metrics to Apple Health or Google Health Connect, which are controlled + by your device's own OS-level health app, not by us. + +**What we don't do** +We do not sell your data. We do not send your health data to WHOOP, Inc. or +any advertising network. We do not require a WHOOP account or credentials to +use the App. + +**Your controls** +You can disable cloud backup, telemetry, or AI Coach at any time in Settings. +[Describe how a user deletes their local data / requests deletion of any +uploaded backup — INSERT MECHANISM]. + +**Children** +This App is not directed to children under 13 (or the relevant age of digital +consent in your jurisdiction) and we do not knowingly collect data from them. + +**Changes** +We may update this policy; material changes will be reflected here with an +updated date. + +**Contact** +Questions about this policy: [CONTACT EMAIL]. diff --git a/lib/compute/crossday_pipeline.dart b/lib/compute/crossday_pipeline.dart index 5c70e3cb..e3931c95 100644 --- a/lib/compute/crossday_pipeline.dart +++ b/lib/compute/crossday_pipeline.dart @@ -222,8 +222,8 @@ Map buildCrossDayBundle( tsb: ls?.tsb, ); - // ── VO₂max + WHOOP-Age (physiological age). Resting VO₂max (Uth HRmax:RHR) - // over the baseline RHR; WHOOP-Age composites VO₂max + RHR + HRV + sleep + + // ── VO₂max + Fitness Age (physiological age). Resting VO₂max (Uth HRmax:RHR) + // over the baseline RHR; Fitness Age composites VO₂max + RHR + HRV + sleep + // steps vs age-norms. All ESTIMATE, absent on missing inputs. final age = _numOrNull(profile['age']); final sexStr = (profile['sex'] as String?)?.toLowerCase(); @@ -301,7 +301,7 @@ Map buildCrossDayBundle( }, 'strain_coach': strainTgt.toJson((v) => v.toJson()), 'vo2max': vo2.toJson(), - 'whoop_age': physAge.toJson((v) => v.toJson()), + 'fitness_age': physAge.toJson((v) => v.toJson()), 'percentiles': percentiles, 'recent': recent, }; diff --git a/lib/compute/derivation_engine.dart b/lib/compute/derivation_engine.dart index 511e337b..de34b758 100644 --- a/lib/compute/derivation_engine.dart +++ b/lib/compute/derivation_engine.dart @@ -101,7 +101,7 @@ import 'substrate.dart'; /// also tunes this estimate. Bumping re-derives non-finalized days so they fill. // v20: principled nap detection (van Hees immobility + HR-dip) → `naps` block + // `nap_min` scalar; cross-day Sleep Coach (need/bedtime/cycle-wake/performance), -// Strain Coach (recovery-gated target), VO₂max + WHOOP-Age, all in the crossday +// Strain Coach (recovery-gated target), VO₂max + Fitness Age, all in the crossday // bundle. // v21: all-day HRV line (`series.hrv_day`, epoch rolling RMSSD over 24/7 RR). // v22: all-day RESP line (`series.resp_day`, rolling RSA br/min) + relative @@ -1978,7 +1978,7 @@ class DerivationEngine { 'skin_temp_z': sc('skin_temp_z'), 'trimp': sc('trimp'), // Headline 0–21 strain, daily steps, nap minutes — feed Sleep/Strain Coach - // + VO₂max/WHOOP-Age in the cross-day rollup. + // + VO₂max/Fitness Age in the cross-day rollup. 'strain': sc('strain'), 'steps': sc('steps'), 'nap_min': sc('nap_min'), diff --git a/lib/state/app_state.dart b/lib/state/app_state.dart index 4746fac7..c855fa98 100644 --- a/lib/state/app_state.dart +++ b/lib/state/app_state.dart @@ -517,14 +517,22 @@ class AppState extends ChangeNotifier { UpdateInfo? get _update => appStatus?.update; /// A newer build is published (we're behind latest_build). + /// + /// Gated on [kSideloadOtaEnabled] regardless of platform: store builds + /// (Play Store / App Store) must never surface a self-update prompt at + /// all, not just fall back to a browser link — see update_service.dart. bool get updateAvailable => + kSideloadOtaEnabled && _update != null && _currentBuild > 0 && _update!.latestBuild > _currentBuild; /// We're below the mandatory floor — the prompt can't be dismissed. bool get updateMandatory => - _update != null && _currentBuild > 0 && _currentBuild < _update!.minBuild; + kSideloadOtaEnabled && + _update != null && + _currentBuild > 0 && + _currentBuild < _update!.minBuild; UpdateInfo? get update => _update; diff --git a/lib/sync/update_service.dart b/lib/sync/update_service.dart index c9ff0e9c..60a063ea 100644 --- a/lib/sync/update_service.dart +++ b/lib/sync/update_service.dart @@ -1,10 +1,15 @@ -// UpdateService — the Android OTA mechanics. There's no app store in the loop -// (the app is sideloaded), so we ARE the update channel: download the signed APK -// from the backend's update pointer and hand it to the system installer. The new -// APK must be signed with the same release key or Android refuses the update — -// which is already true for our CI-built GitHub releases. +// UpdateService — the Android OTA mechanics. On a direct-distribution/sideload +// build there's no app store in the loop, so we ARE the update channel: +// download the signed APK from the backend's update pointer and hand it to the +// system installer. The new APK must be signed with the same release key or +// Android refuses the update — which is already true for our CI-built GitHub +// releases. // -// iOS can't sideload-install, so [supported] is false there and the UI hides OTA. +// iOS can't sideload-install, so [supported] is false there and the UI hides +// in-app install (falls back to a browser link) regardless of the flag below. +// +// Store builds (Play Store, and any future App Store build) must NOT offer +// self-update at all — see [kSideloadOtaEnabled]. import 'dart:convert'; import 'dart:io'; @@ -16,6 +21,21 @@ import 'package:url_launcher/url_launcher.dart'; import '../cloud/companion_client.dart'; import '../models/app_status.dart'; +/// Compile-time gate for the whole self-update feature (the "Update +/// available" banner AND the in-app APK download/install flow). Defaults OFF +/// so the open-source repo — and any store submission built from it without +/// extra flags — never bakes in self-update: app-store review does not allow +/// an app to silently install another build of itself outside the store. +/// +/// A direct-distribution/sideload build opts in explicitly: +/// flutter build apk --dart-define=ENABLE_SIDELOAD_OTA=true +/// +/// Mirrors the build-time-URL convention in lib/cloud/backend_client.dart / +/// lib/cloud/companion_client.dart — the public repo must not bake a specific +/// distribution channel in by default. +const bool kSideloadOtaEnabled = + bool.fromEnvironment('ENABLE_SIDELOAD_OTA', defaultValue: false); + /// A coarse progress event the UI renders. class OtaProgress { final String phase; // 'downloading' | 'installing' | 'error' @@ -25,8 +45,9 @@ class OtaProgress { } class UpdateService { - /// Only Android can install an APK in-app. - static bool get supported => Platform.isAndroid; + /// Only Android can install an APK in-app, and only on a sideload build + /// that opted into [kSideloadOtaEnabled]. + static bool get supported => Platform.isAndroid && kSideloadOtaEnabled; /// Fetch the OTA update pointer + admin alert banner from the companion /// backend's public, UNAUTHENTICATED `GET /app/status` — the single URL the app diff --git a/lib/ui/insights/coach_cards.dart b/lib/ui/insights/coach_cards.dart index 7add2d61..3053fd83 100644 --- a/lib/ui/insights/coach_cards.dart +++ b/lib/ui/insights/coach_cards.dart @@ -1,5 +1,5 @@ // Coach + fitness cards — surface the cross-day coaching/fitness analytics -// (Sleep Coach, Strain Coach, WHOOP-Age) and the journal correlation engine +// (Sleep Coach, Strain Coach, Fitness Age) and the journal correlation engine // ("Performance Assessment"). All read the precomputed `crossday` bundle via // repo.getInsights() (or getJournalInsights), so they do ZERO heavy compute on // read. Honest: every value traces to a tested analytics Metric; absent → a @@ -276,16 +276,16 @@ class _StrainCoachCardState extends State { } } -// ── WHOOP-AGE / fitness ────────────────────────────────────────────────────── +// ── FITNESS AGE ────────────────────────────────────────────────────────────── /// Physiological "age" vs chronological + VO₂max estimate. -class WhoopAgeCard extends StatefulWidget { - const WhoopAgeCard({super.key}); +class FitnessAgeCard extends StatefulWidget { + const FitnessAgeCard({super.key}); @override - State createState() => _WhoopAgeCardState(); + State createState() => _FitnessAgeCardState(); } -class _WhoopAgeCardState extends State { +class _FitnessAgeCardState extends State { Map? _age; num? _vo2; bool _loading = true; @@ -301,7 +301,7 @@ class _WhoopAgeCardState extends State { final cd = await context.read().repo?.getInsights(); if (!mounted) return; setState(() { - _age = _val(cd?['whoop_age']); + _age = _val(cd?['fitness_age']); final v = cd?['vo2max']; _vo2 = v is Map ? v['value'] as num? : null; _loading = false; diff --git a/lib/ui/onboarding/welcome_screen.dart b/lib/ui/onboarding/welcome_screen.dart index 4005cbce..3ba3f51a 100644 --- a/lib/ui/onboarding/welcome_screen.dart +++ b/lib/ui/onboarding/welcome_screen.dart @@ -154,6 +154,8 @@ class _WelcomeScreenState extends State { physics: const BouncingScrollPhysics(), children: dsStaggered([ const WelcomeHero(), + const SizedBox(height: Sp.x4), + const AffiliationDisclaimer(), const SizedBox(height: Sp.x6), Text('How do you want to start?', style: AppText.h2), const SizedBox(height: Sp.x4), @@ -375,6 +377,24 @@ class WelcomeHero extends StatelessWidget { } } +/// Legal/affiliation disclaimer shown once at first run (and mirrored in +/// Settings → About — see [AboutScreen]). Attorney-reviewed wording; don't +/// paraphrase without re-running it past the same review. +class AffiliationDisclaimer extends StatelessWidget { + const AffiliationDisclaimer({super.key}); + + static const String text = + 'Edge is an independent, open-source project. It is not affiliated ' + 'with, sponsored by, or endorsed by WHOOP, Inc. You must own a genuine ' + 'WHOOP 4.0 band to use this app. Health metrics are wellness estimates ' + 'from published research methods — not medical diagnoses.'; + + @override + Widget build(BuildContext context) { + return Text(text, style: AppText.captionMuted); + } +} + /// One onboarding choice — a SurfaceCard row: icon-in-tile, title, one quiet /// line, chevron. [accent] marks the recommended path with the ember tint. class WelcomeOptionCard extends StatelessWidget { diff --git a/lib/ui/profile/about_screen.dart b/lib/ui/profile/about_screen.dart new file mode 100644 index 00000000..ef90841c --- /dev/null +++ b/lib/ui/profile/about_screen.dart @@ -0,0 +1,115 @@ +// about_screen.dart — Settings → About. Surfaces the affiliation/health-claim +// disclaimer (same attorney-reviewed wording shown once at first run — see +// AffiliationDisclaimer in lib/ui/onboarding/welcome_screen.dart), links to the +// Privacy Policy and NOTICE, the open-source license attribution page, and the +// installed app version. Added as part of app-store-readiness prep; the +// disclaimer wording must not be edited without re-running it past legal +// review. + +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../design/design.dart'; +import '../onboarding/welcome_screen.dart' show AffiliationDisclaimer; + +/// TODO(legal): both app stores require a *hosted* URL, not a repo file. Point +/// this at the GitHub Pages (or wtf.openstrap.* domain) page once PRIVACY.md +/// is published there — see PRIVACY.md's own TODO for the same gap. +const String kPrivacyPolicyUrl = 'https://openstrap.github.io/edge/PRIVACY.md'; + +const String kNoticeUrl = + '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/OpenStrap/edge/blob/main/NOTICE.md'; + +class AboutScreen extends StatefulWidget { + const AboutScreen({super.key}); + + @override + State createState() => _AboutScreenState(); +} + +class _AboutScreenState extends State { + PackageInfo? _pkg; + + @override + void initState() { + super.initState(); + PackageInfo.fromPlatform().then((p) { + if (mounted) setState(() => _pkg = p); + }); + } + + static Future _openUrl(String url) async { + try { + await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + } catch (_) {} + } + + @override + Widget build(BuildContext context) { + final versionLabel = _pkg == null + ? null + : '${_pkg!.version}+${_pkg!.buildNumber}'; + + return AppScaffold( + title: 'About', + children: [ + const SizedBox(height: Sp.x4), + SurfaceCard( + padding: const EdgeInsets.all(Sp.x4), + child: const AffiliationDisclaimer(), + ), + const SizedBox(height: Sp.x6), + const SectionHeader('Legal'), + _SettingsCard(rows: [ + ListRow( + icon: OsIcon.activity, + title: 'Privacy policy', + divider: true, + onTap: () => _openUrl(kPrivacyPolicyUrl), + ), + ListRow( + icon: OsIcon.activity, + title: 'Notice & attribution', + subtitle: 'Trademark + reverse-engineering disclosure', + divider: true, + onTap: () => _openUrl(kNoticeUrl), + ), + ListRow( + icon: OsIcon.activity, + title: 'Open-source licenses', + onTap: () => showLicensePage( + context: context, + applicationName: 'Edge', + applicationVersion: versionLabel, + ), + ), + ]), + const SizedBox(height: Sp.x6), + const SectionHeader('Version'), + _SettingsCard(rows: [ + ListRow( + icon: OsIcon.activity, + title: 'Edge', + value: versionLabel ?? '…', + ), + ]), + const SizedBox(height: Sp.x8), + ], + ); + } +} + +/// Local copy of ProfileScreen's private `_SettingsCard` shape (a SurfaceCard +/// wrapping stacked ListRows) — kept tiny and duplicated rather than exporting +/// the private widget across files for one screen. +class _SettingsCard extends StatelessWidget { + final List rows; + const _SettingsCard({required this.rows}); + + @override + Widget build(BuildContext context) => SurfaceCard( + padding: const EdgeInsets.symmetric(horizontal: Sp.x4, vertical: Sp.x1), + child: Column(children: rows), + ); +} diff --git a/lib/ui/profile/profile_screen.dart b/lib/ui/profile/profile_screen.dart index 0431efe5..6dfe330c 100644 --- a/lib/ui/profile/profile_screen.dart +++ b/lib/ui/profile/profile_screen.dart @@ -25,6 +25,7 @@ import '../design/design.dart'; import '../design/gallery_screen.dart'; import '../import/import_screen.dart'; import '../today/step_goal_screen.dart'; +import 'about_screen.dart'; import 'advanced_data_screen.dart'; import 'data_history_screen.dart'; import 'gesture_section.dart'; @@ -422,6 +423,20 @@ class ProfileScreen extends StatelessWidget { const SizedBox(height: Sp.x6), + // ── About ──────────────────────────────────────────────────── + const SectionHeader('About'), + _SettingsCard(rows: [ + ListRow( + icon: OsIcon.activity, + title: 'About Edge', + subtitle: 'Affiliation, privacy policy, licenses', + onTap: () => Navigator.of(context).push( + themedRoute((_) => const AboutScreen(), name: 'AboutScreen')), + ), + ]), + + const SizedBox(height: Sp.x6), + // ── Reset ──────────────────────────────────────────────────── const SectionHeader('Reset'), _SettingsCard(rows: [ diff --git a/lib/ui/screens/screens.dart b/lib/ui/screens/screens.dart index 362f98fc..93acdeab 100644 --- a/lib/ui/screens/screens.dart +++ b/lib/ui/screens/screens.dart @@ -148,7 +148,7 @@ class BodyScreen extends StatelessWidget { const SizedBox(height: Sp.x3), StrainDetailScreen(date: todayLabel(), embedded: true), const SizedBox(height: Sp.x3), - const WhoopAgeCard(), + const FitnessAgeCard(), const SizedBox(height: Sp.x3), const PerformanceAssessmentCard(), const CycleEntryCard(), diff --git a/pubspec.yaml b/pubspec.yaml index 8db63312..1114dc96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: openstrap_edge -description: "OpenStrap — open-source WHOOP 4.0 companion. BLE drain → raw-first local store → cloud sync." +description: "Open-source, local-first fitness tracking companion for WHOOP 4.0 hardware. Not affiliated with WHOOP, Inc." publish_to: 'none' version: 0.9.14+47 From a6bf39ecb900692e778ebca744b0431c9f582a7e Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sat, 18 Jul 2026 12:21:18 +0530 Subject: [PATCH 02/14] Point kPrivacyPolicyUrl at the live hosted Privacy Policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub Pages is now live for docs/legal-site (maintainer fixed the Settings -> Pages source branch). Updates the placeholder from Phase 1: - about_screen.dart: kPrivacyPolicyUrl -> https://openstrap.github.io/edge/privacy.html (was the placeholder .../edge/PRIVACY.md, which pointed at a repo file, not a hosted page). Removed the now-resolved TODO(legal) comment on the constant. - PRIVACY.md: removed the "still needs a GitHub Pages page" note (resolved) and pointed at the live URL as the version to keep current going forward. The DATE/CONTACT EMAIL/data-deletion-mechanism placeholders in THIS repo copy are untouched and still open — only the hosted-URL gap is resolved here; the docs/legal-site branch's privacy.html already has those filled in separately. --- PRIVACY.md | 12 ++++++------ lib/ui/profile/about_screen.dart | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 0b6d3377..132635ed 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,12 +1,12 @@ > **ATTORNEY-REVIEW DRAFT — not final.** This has not been reviewed or approved > by a licensed attorney and must not be treated as the live policy until it > has been. `[DATE]`, `[CONTACT EMAIL]`, and the data-deletion mechanism below -> are placeholders a human must fill in before this goes live. Both the App -> Store and Play Store require a **hosted** URL for the privacy policy (not a -> link to this repo file) — this still needs a GitHub Pages page (or a page on -> whatever domain backs `wtf.openstrap.*`) before store submission. See -> `lib/ui/profile/about_screen.dart`'s `kPrivacyPolicyUrl` TODO for the -> in-app link that needs updating once that page exists. +> are placeholders a human must fill in before this goes live. This is the +> repo copy for reference; the live, store-linked policy is hosted at +> https://openstrap.github.io/edge/privacy.html (GitHub Pages, `docs/` +> folder on the `docs/legal-site` branch) and is the version that should be +> kept current — `lib/ui/profile/about_screen.dart`'s `kPrivacyPolicyUrl` +> already points at it. # Privacy Policy — Edge / OpenStrap diff --git a/lib/ui/profile/about_screen.dart b/lib/ui/profile/about_screen.dart index ef90841c..59acfe2a 100644 --- a/lib/ui/profile/about_screen.dart +++ b/lib/ui/profile/about_screen.dart @@ -13,10 +13,9 @@ import 'package:url_launcher/url_launcher.dart'; import '../design/design.dart'; import '../onboarding/welcome_screen.dart' show AffiliationDisclaimer; -/// TODO(legal): both app stores require a *hosted* URL, not a repo file. Point -/// this at the GitHub Pages (or wtf.openstrap.* domain) page once PRIVACY.md -/// is published there — see PRIVACY.md's own TODO for the same gap. -const String kPrivacyPolicyUrl = 'https://openstrap.github.io/edge/PRIVACY.md'; +/// Hosted Privacy Policy (GitHub Pages, docs/legal-site). Both app stores +/// require a live hosted URL, not a repo file — this is that URL. +const String kPrivacyPolicyUrl = 'https://openstrap.github.io/edge/privacy.html'; const String kNoticeUrl = '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/OpenStrap/edge/blob/main/NOTICE.md'; From 9f8b88e591a9fb2b35ce82dc2af6519e242ea502 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sat, 18 Jul 2026 13:04:57 +0530 Subject: [PATCH 03/14] iOS submission prep: scrub hardcoded Team ID + sync Widget/Watch versions Three fixes from the readiness audit: 1. Scrub the hardcoded personal Team ID (2U62X3RF3R) from the "OpenStrapWatch Watch App" target's Debug/Release/Profile build configs in project.pbxproj -> replaced with $(APPLE_DEVELOPMENT_TEAM), matching every other target (Runner/RunnerTests/OpenStrapWidgetExtension already used the variable). Confirmed via `git show HEAD` this was committed, not a local artifact. 2. AccessorySetupKit entitlement - researched via Apple's current docs + an independent developer writeup (both converge): NO special entitlement (e.g. com.apple.developer.accessory-setup-kit) is required for AccessorySetupKit. It's driven entirely by Info.plist keys (NSAccessorySetupKitSupports, NSAccessorySetupBluetoothServices), which Runner/Info.plist already has correctly configured. No entitlement change made - the existing Runner.entitlements (App Groups + HealthKit only) is correct as-is. 3. Synced OpenStrapWidget(Extension) and "OpenStrapWatch Watch App" target versions (previously stuck at 1.0/1 and 0.7.0/17 respectively) to 0.9.14/47, matching the current pubspec.yaml version. These two targets aren't wired to Generated.xcconfig (only Runner's target-level Debug/ Release configs include it), so $(FLUTTER_BUILD_NAME)/$(FLUTTER_BUILD_NUMBER) aren't available to them - wiring that in would require adding a new baseConfigurationReference to targets that also inherit Flutter-runner- specific settings (arch exclusions, framework search paths) I can't verify are safe without an actual Xcode build. Took the safer path instead: hardcode-bumped to match, and documented the manual-sync requirement in guides/IOS_INSTALLATION.md ("Version Numbers" section) + a NOTE comment next to pubspec.yaml's version: field, so the next release bump doesn't silently let these drift again. RunnerTests' own CURRENT_PROJECT_VERSION/MARKETING_VERSION (1/1.0) intentionally left untouched - it's a test bundle, never shipped, out of scope here. --- guides/IOS_INSTALLATION.md | 16 +++++++++++++++ ios/Runner.xcodeproj/project.pbxproj | 30 ++++++++++++++-------------- pubspec.yaml | 5 +++++ 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/guides/IOS_INSTALLATION.md b/guides/IOS_INSTALLATION.md index 8bb1ed47..8fccac7f 100644 --- a/guides/IOS_INSTALLATION.md +++ b/guides/IOS_INSTALLATION.md @@ -126,6 +126,22 @@ For a signed release-style device install: flutter run --release -d --dart-define-from-file=.env ``` +## Version Numbers + +`Runner`'s `MARKETING_VERSION`/`CURRENT_PROJECT_VERSION` track `pubspec.yaml`'s +`version:` automatically via `$(FLUTTER_BUILD_NAME)`/`$(FLUTTER_BUILD_NUMBER)` +(Flutter writes these into `ios/Flutter/Generated.xcconfig` on every build). + +The **`OpenStrapWidget`/`OpenStrapWidgetExtension`** and **`OpenStrapWatch Watch +App`** targets are NOT wired to that mechanism — they don't include +`Generated.xcconfig`, so those Flutter variables aren't available to them, and +`MARKETING_VERSION`/`CURRENT_PROJECT_VERSION` are hardcoded per-target in +`project.pbxproj` instead. **Bump these by hand to match `pubspec.yaml`'s +`version:` every time it changes** — App Store Connect validates that a host +app's embedded extensions and companion Watch app carry compatible version +numbers, so letting these drift is a real upload-time risk, not just cosmetic +inconsistency. + ## Debug Builds and Home-Screen Relaunch Flutter debug builds on iOS must be launched by Flutter tooling or Xcode. If you diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4329a129..a0bdb4b2 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -833,7 +833,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 47; DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -849,7 +849,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.9.14; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)"; @@ -880,7 +880,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 47; DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -896,7 +896,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.9.14; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -924,7 +924,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = OpenStrapWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 47; DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -940,7 +940,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 0.9.14; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -971,8 +971,8 @@ CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; - DEVELOPMENT_TEAM = 2U62X3RF3R; + CURRENT_PROJECT_VERSION = 47; + DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)"; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -987,7 +987,7 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 0.7.0; + MARKETING_VERSION = 0.9.14; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp"; @@ -1026,8 +1026,8 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; - DEVELOPMENT_TEAM = 2U62X3RF3R; + CURRENT_PROJECT_VERSION = 47; + DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)"; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1042,7 +1042,7 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 0.7.0; + MARKETING_VERSION = 0.9.14; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1077,8 +1077,8 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "OpenStrapWatch Watch App/OpenStrapWatch Watch App.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 17; - DEVELOPMENT_TEAM = 2U62X3RF3R; + CURRENT_PROJECT_VERSION = 47; + DEVELOPMENT_TEAM = "$(APPLE_DEVELOPMENT_TEAM)"; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -1093,7 +1093,7 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 0.7.0; + MARKETING_VERSION = 0.9.14; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER).watchkitapp"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/pubspec.yaml b/pubspec.yaml index 1114dc96..5a5ab5b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,11 @@ name: openstrap_edge description: "Open-source, local-first fitness tracking companion for WHOOP 4.0 hardware. Not affiliated with WHOOP, Inc." publish_to: 'none' +# NOTE: bumping this also requires manually bumping MARKETING_VERSION/ +# CURRENT_PROJECT_VERSION for the OpenStrapWidget(Extension) and "OpenStrapWatch +# Watch App" targets in ios/Runner.xcodeproj/project.pbxproj — they aren't wired +# to FLUTTER_BUILD_NAME/FLUTTER_BUILD_NUMBER. See guides/IOS_INSTALLATION.md +# "Version Numbers". version: 0.9.14+47 environment: From 7c6adc7e1cd15ffba1a907aa687f7f2a80f05470 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sat, 18 Jul 2026 19:37:52 +0530 Subject: [PATCH 04/14] Offload derive second half to isolate; fix 3 fatal crashes; readiness bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Freeze fix: the entire second half of _derivePreparedDay (all-day HRV/RSA Timeline curves, naps, workout detection, wake features, steps/energy) ran on the CALLING isolate — the UI isolate for the foreground light pass fired on every sync — hanging the main thread for seconds. Confirmed via Crashlytics: iOS 6.1s jank_watchdog + Android __kernel_rem_pio2/cos main-thread ANRs, fresh in v0.9.13. Moved it into a second Isolate.run (pure methods made static; DB reads done first, DB writes/notifications applied after) under _perDayTimeout, and made it NON-FATAL so a failure persists the headline day (readiness intact) instead of skipping it — the "readiness randomly goes -" path. Crash fixes (Crashlytics FATAL triage): - db.dart: LocalDb.instance reopens when the cached handle isn't open. Android closes the SQLiteDatabase on background teardown without nulling _db, so every write threw "attempt to re-open an already-closed object" (91-event burst). - route_map.dart: drop non-finite GPS coords + only bounds-fit a non-zero-area box (else fixed-zoom center) — degenerate/NaN routes crashed flutter_map's tile layer (NaN.toInt in _clampToNativeZoom). - coach_settings_screen.dart: capture navigator before the await + guard pop with canPop() — bare Navigator.pop() after await threw "Bad state: No element". Readiness: bump kAlgoVersion 42->43 for the analytics MAD/SD-z fallback so previously-blanked days recompute; add a tightly-gated queryable Crashlytics non-fatal for any residual readiness_absent (non-cold-start) case. --- lib/compute/derivation_engine.dart | 618 +++++++++++++++--------- lib/data/db.dart | 11 +- lib/data/local_repository_impl.dart | 146 +++--- lib/ui/coach/coach_settings_screen.dart | 16 +- lib/ui/kit/route_map.dart | 24 +- 5 files changed, 512 insertions(+), 303 deletions(-) diff --git a/lib/compute/derivation_engine.dart b/lib/compute/derivation_engine.dart index de34b758..466fc7f3 100644 --- a/lib/compute/derivation_engine.dart +++ b/lib/compute/derivation_engine.dart @@ -210,7 +210,12 @@ import 'substrate.dart'; // over time while per-night-local always leads. Deep stays a low-confidence // NREM sub-split (deep_low_confidence). The profile self-seeds across this // re-derivation sweep; no explicit migration. Bump so every day re-stages. -const int kAlgoVersion = 42; +// v43: readinessComposite now falls back to a mean/SD z when the robust (median +// +MAD) z is degenerate (MAD==0 on a tightly-clustered quantized baseline — +// whole-bpm RHR / integer skin-temp ADC), which was intermittently blanking the +// whole readiness score to "—" on nights that had valid sleep. Bump so days that +// were previously absent-for-that-reason recompute a real score. +const int kAlgoVersion = 43; /// Raw is kept this many days past derivation, then pruned (derived stays). const int rawRetentionDays = 3; @@ -1397,6 +1402,28 @@ class DerivationEngine { message: 'readiness_absent', context: flat, ); + // Also surface the SURPRISING case — readiness absent when it should NOT + // be (adequate inputs, not the honest cold-start `need_baseline` note) — + // as a queryable Crashlytics non-fatal, so a residual "readiness '—' even + // with sleep present" is diagnosable from the per-input flags WITHOUT GA4 + // access. Cold-start (need_baseline) absences stay Analytics-only so this + // stays low-noise (and, post the MAD/SD-z fallback, rare). + final note = (diag['note'] as String?) ?? ''; + if (!note.startsWith('need_baseline')) { + final summary = StringBuffer('readiness_absent'); + for (final key in ['hrv', 'rhr', 'resp', 'temp']) { + final v = (diag[key] as Map?)?.cast(); + if (v == null) continue; + summary.write( + ' $key=${v['value'] == true ? 'Y' : 'n'}/${v['baseline_n']}'); + } + summary.write(' | $note'); + TelemetryService.instance.recordNonFatal( + StateError(summary.toString()), + StackTrace.current, + reason: 'readiness_absent', + ); + } } // Where this day's sleep window came from (auto / auto_fallback / manual / @@ -1405,142 +1432,94 @@ class DerivationEngine { bundle['sleep_source'] = day.sleepSource; final scMap = (bundle['scalars'] as Map?)?.cast(); - final wake = _buildWakeDayFeatures( - daySub, - profile, - sleepOnsetSec: day.sleepOnsetSec, - sleepOffsetSec: day.sleepOffsetSec, - restingHr: (scMap?['rhr'] as num?)?.toDouble(), - ); - _applyWakeDayFeatures(bundle, scMap, wake); - - // ── STEPS (real 100 Hz + 1 Hz estimate) + TOTAL DAILY ENERGY (TDEE) ─────── - // Pull the day's 100 Hz coverage windows (device-time sec) + their real step - // count so _stepsAndEnergy can prefer them and estimate only the rest. - final dayLo = daySub.length == 0 ? 0 : daySub.tsSec.first; - final dayHi = daySub.length == 0 ? 0 : daySub.tsSec.last + 60; - final coverageWindows = await LocalDb.coverageWindowsOverlapping( - dayLo, - dayHi, - ); - final liveStepsReal = await LocalDb.liveStepsForDay(day.date); - final stepCalib = await LocalDb.getStepCalibration(); - _stepsAndEnergy( - bundle, - scMap, - daySub, - profile, - coverageWindows, - liveStepsReal, - stepCalib, - ); - // ── More substrate-derived detail blocks (computed here, where the full - // sliced substrate lives — same pattern as activeMin; these are fresh - // blocks so ints are safe, unlike the double? scalars). ─ - bundle['daytime_hrv'] = _daytimeHrv( - daySub, - day.sleepOnsetSec, - day.sleepOffsetSec, - ); // waking RMSSD - // ALL-DAY HRV / resp / skin-temp lines for the Timeline graph. Epoch-stamped - // (rolling RMSSD, rolling RSA, relative skin-temp) over the day-wide 24/7 - // substrate RR/ADC — context lines, movement-confounded, not the recovery - // values. Computed here where the day substrate lives. - final sersMap = (bundle['series'] as Map?)?.cast(); - sersMap?['hrv_day'] = _dayHrvCurve(daySub); - sersMap?['resp_day'] = _dayRespCurve(daySub); - sersMap?['skin_temp_day'] = _daySkinTempCurve(daySub); - bundle['restlessness'] = _restlessness(sleepSub); // nocturnal movement - bundle['sleep_periods'] = _sleepPeriods( - daySub, - day.sleepOnsetSec, - day.sleepOffsetSec, - ); // naps - // Principled daytime NAPS (van Hees immobility + HR-dip): rich per-nap block - // + a `nap_min` scalar feeding the Sleep Coach's nap credit + the Timeline. - _attachNaps(bundle, scMap, daySub, day.sleepOnsetSec, day.sleepOffsetSec); - - // Per-5-min movement-level curve for the "Your day" Movement view ([{t,v}], - // v = fraction of moving seconds 0..1). Fresh top-level list — no typed-map. - bundle['activity_curve'] = _activityCurve(daySub); - - // ── AUTO-DETECTED WORKOUTS (WorkoutDetector, 1 Hz HR + gravity) ────────── - // Retroactive bout detection over the day's HR + gravity (lives in daySub, - // not the bundle input). Per-bout strain/zones/calories via the analytics - // family. OVERLAP-DEDUP: any manual/live session for this day is passed as a - // saved span so a detected bout overlapping it is dropped (manual wins). The - // sport seam stays default ("detected") — OpenStrap's HAR typer can be wired - // in once high-rate accel features exist. - bundle['detected_workouts'] = await _detectedWorkouts(daySub, day, profile); - - // ── AUTO-WORKOUT SUGGESTIONS + HRR (opt-in detector over day HR+motion) ── - // One pass of the opt-in detector serves two features: (1) persisted "did you - // work out?" suggestions + a recent-day notification, and (2) heart-rate - // recovery (HRR) per bout → `hrr_bpm` scalar. scMap writes through to - // bundle['scalars'] (CastMap view), so hrr_bpm reaches the series block below. - await _attachWorkoutSuggestionsAndHrr(bundle, scMap, daySub, day, dataNowSec); - - // ── WRIST ORIENTATION during sleep (low-confidence; NOT body position) ─── - _attachWristOrientation( - bundle, daySub, day.sleepOnsetSec, day.sleepOffsetSec); - - // ── ADVANCED SLEEP (4-class Cole–Kripke + DoG/percentile stager) ───────── - // A richer, AASM-style sleep read (SOL / REM-latency / disturbances + a - // 4-class hypnogram) computed over the day substrate (needs accel, which - // lives here, not in the bundle input). ADDITIVE: the canonical single-source - // `sleep` block (from segmentSleep) is the headline; this is a parallel - // ESTIMATE detail. Best-effort — never throws. - bundle['advanced_sleep'] = await _advancedSleep(daySub); - - // Feature 6: Restlessness Map (Heatmap of Sleep) - if (sleepSub.length > 0) { - final bucketSec = 300; // 5 min - final moveSum = {}; - final moveCount = {}; - for (var i = 0; i < sleepSub.length; i++) { - final b = sleepSub.tsSec[i] ~/ bucketSec; - final ax = sleepSub.ax[i]; - final ay = sleepSub.ay[i]; - final az = sleepSub.az[i]; - final mag = math.sqrt(ax * ax + ay * ay + az * az); - final enmo = (mag - 1.0).abs(); - moveSum[b] = (moveSum[b] ?? 0.0) + enmo; - moveCount[b] = (moveCount[b] ?? 0) + 1; + // ── SECOND HALF — OFFLOADED to a background isolate ────────────────────── + // Everything that turns the isolate-1 bundle into the full day result (wake + // features, hybrid steps + TDEE, all-day HRV/RSA/skin-temp Timeline lines, + // naps, workout detection + HRR, wrist orientation, restlessness map, fit + // quality) used to run on the CALLING isolate — the UI isolate for the + // foreground light pass that fires on every sync — hanging the main thread + // for seconds (the rolling-RSA Lomb-Scargle over the 24 h day + nap + // re-staging + workout detection are the trig/CPU hogs). It is all PURE + // compute over the two substrates + a few scalars, so it now runs in + // Isolate.run. DB reads that it needs are done HERE (this is the DB-owning + // isolate); the DB writes + notification it produces are returned as + // descriptors and applied below. Same _perDayTimeout guard as isolate 1. + // NON-FATAL: a failure OR timeout anywhere in the offloaded second half must + // never skip the whole day. Isolate 1 already computed the headline scalars + // (readiness / RHR / RMSSD) into bundle['scalars']; we persist those and just + // drop the optional detail blocks. (Previously an exception here threw out of + // _derivePreparedDay → the day was marked skipped → readiness rendered "-" + // even though it had been computed fine — the "readiness randomly goes -" bug.) + try { + final dayLo = daySub.length == 0 ? 0 : daySub.tsSec.first; + final dayHi = daySub.length == 0 ? 0 : daySub.tsSec.last + 60; + final coverageWindows = + await LocalDb.coverageWindowsOverlapping(dayLo, dayHi); + final liveStepsReal = await LocalDb.liveStepsForDay(day.date); + final stepCalib = await LocalDb.getStepCalibration(); + final savedSessions = await LocalDb.sessionsInRange(dayLo, dayHi); + + // Built on THIS isolate so the Isolate.run closure captures only this plain + // sendable object (never `this`, `day`, or `bundle`). + final blocksInput = _DayBlocksInput( + daySub: daySub, + sleepSub: sleepSub, + profile: profile, + onsetSec: day.sleepOnsetSec, + offsetSec: day.sleepOffsetSec, + rhr: (scMap?['rhr'] as num?)?.toDouble(), + maxHrUsed: (bundle['max_hr_used'] as num?)?.round(), + coverageWindows: coverageWindows, + liveStepsReal: liveStepsReal, + stepCalib: stepCalib, + savedSessions: savedSessions, + date: day.date, + dayEndSec: day.endSec, + dataNowSec: dataNowSec, + ); + final blocks = await Isolate.run(() => _computeDayBlocks(blocksInput)) + .timeout(_perDayTimeout); + + // Merge the computed blocks back into the isolate-1 bundle. scMap is the + // CastMap view over bundle['scalars'], so addAll writes through — nap_min / + // hrr_bpm reach the persisted series map below. + bundle.addAll(blocks.bundlePatch); + (bundle['series'] as Map?)?.cast().addAll( + blocks.seriesPatch, + ); + scMap?.addAll(blocks.scalarPatch); + + // DB writes + notification the pure compute deferred to us (DB-owning isolate). + for (final w in blocks.sessionHrrWrites) { + await LocalDb.setSessionHrr(w.$1, w.$2); } - final out = >[]; - final keys = moveSum.keys.toList()..sort(); - for (final b in keys) { - final avgEnmo = moveSum[b]! / moveCount[b]!; - // 0.1g ENMO is quite restless. Scale to 0-1 for heatmap UI. - final density = math.min(1.0, avgEnmo * 10.0); - out.add({ - 't': b * bucketSec, - 'density': double.parse(density.toStringAsFixed(3)), + for (final sug in blocks.suggestionsToPersist) { + await LocalDb.putWorkoutSuggestion({ + ...sug, + 'created_at': DateTime.now().millisecondsSinceEpoch, }); } - bundle['restlessness_map'] = out; - } - - // Feature 2: Fit Quality Diagnostics (Band Too Loose) - // Analyze skinContact during active periods (HR > 100) - var activeContactSum = 0; - var activeContactN = 0; - for (var i = 0; i < daySub.length; i++) { - if (daySub.hr[i] > 100 && daySub.skinContact[i] > 0) { - activeContactSum += daySub.skinContact[i]; - activeContactN++; - } - } - if (activeContactN > 60) { // at least 1 minute of activity - final avgContact = activeContactSum / activeContactN; - if (avgContact < 100) { - bundle['fit_quality'] = 'poor'; - bundle['fit_warning'] = 'Band is worn too loosely during high activity. Tighten for accurate HR.'; + final nb = blocks.notifBout; + if (nb != null) { + await NotificationCenter.instance.emit(NotificationEvent( + dedupeKey: '${day.date}:auto_workout', + category: NotifCategory.recovery, + priority: NotifPriority.normal, + title: 'Did you work out?', + body: 'We spotted ~${nb.durationMin} min of elevated activity. ' + 'Tap to log it.', + date: day.date, + route: '/workouts', + )); } - } - await _persistWakeDayFeatures(dayId: day.date, wake: wake); + await _persistWakeDayFeatures(dayId: day.date, wake: blocks.wake); + } catch (e, st) { + _log('day-blocks (offloaded second half) failed for ${day.date} — ' + 'persisting headline day: $e'); + TelemetryService.instance.recordNonFatal(e, st, reason: 'day_blocks_failed'); + } // Finalize once the DATA EDGE has moved >48 h past the day's wake — i.e. we // have continuous drained data well beyond it, so no more flash can land for @@ -2024,7 +2003,7 @@ class DerivationEngine { } } - List _perMinuteMeanWake( + static List _perMinuteMeanWake( Substrate s, int sleepOnsetSec, int sleepOffsetSec, @@ -2044,7 +2023,7 @@ class DerivationEngine { return [for (final k in keys) _meanWake(buckets[k]!)!]; } - Map _wakeZoneMinutes( + static Map _wakeZoneMinutes( Substrate s, int sleepOnsetSec, int sleepOffsetSec, @@ -2066,7 +2045,7 @@ class DerivationEngine { return ana.HeartRateZones.timeInZone(samples, zoneSet).toRoundedMinuteMap(); } - double _keytelCaloriesWake( + static double _keytelCaloriesWake( List perMin, double age, double weight, @@ -2084,7 +2063,7 @@ class DerivationEngine { return kcal; } - double? _meanWake(List xs) { + static double? _meanWake(List xs) { if (xs.isEmpty) return null; var s = 0.0; for (final x in xs) { @@ -2093,7 +2072,7 @@ class DerivationEngine { return s / xs.length; } - void _applyWakeDayFeatures( + static void _applyWakeDayFeatures( Map bundle, Map? scMap, Map wake, @@ -2123,7 +2102,7 @@ class DerivationEngine { /// NOT cover ([coverageWindows], device-time sec). So a minute is counted by /// 100 Hz OR estimated by 1 Hz, never both. TDEE = HR-flex (Mifflin BMR floor + /// active Keytel surplus). Best-effort. - void _stepsAndEnergy( + static void _stepsAndEnergy( Map bundle, Map? scMap, Substrate daySub, @@ -2216,7 +2195,7 @@ class DerivationEngine { } } } catch (e) { - _log('steps/energy skipped: $e'); + if (kDebugMode) debugPrint('[derive] steps/energy skipped: $e'); } } @@ -2232,7 +2211,7 @@ class DerivationEngine { ); } - Map _buildWakeDayFeatures( + static Map _buildWakeDayFeatures( Substrate daySub, Profile profile, { required int sleepOnsetSec, @@ -2343,7 +2322,7 @@ class DerivationEngine { } /// Active minutes over the WAKE span — a coarse 1 Hz movement proxy. - int _activeMinutes(Substrate s, int sleepOnsetSec, int sleepOffsetSec) { + static int _activeMinutes(Substrate s, int sleepOnsetSec, int sleepOffsetSec) { final n = s.length; if (n < 60) return 0; final ang = List.filled(n, 0); @@ -2374,7 +2353,7 @@ class DerivationEngine { return active; } - List _motionMinutes(Substrate s) { + static List _motionMinutes(Substrate s) { final samples = [ for (var i = 0; i < s.length; i++) ana.AccelSample( @@ -2388,7 +2367,7 @@ class DerivationEngine { return ana.enmoSeries(samples).minutes; } - List _hrPerMinuteAligned(List motion, Substrate s) { + static List _hrPerMinuteAligned(List motion, Substrate s) { final buckets = >{}; for (var i = 0; i < s.hr.length && i < s.tsSec.length; i++) { if (s.hr[i] <= 0) continue; @@ -2401,7 +2380,7 @@ class DerivationEngine { ]; } - String _workoutSex(String? sex) { + static String _workoutSex(String? sex) { switch ((sex ?? '').toLowerCase()) { case 'm': case 'male': @@ -2414,37 +2393,17 @@ class DerivationEngine { } } - /// Auto-detected workouts for the day via [ana.WorkoutDetector] over the - /// day's 1 Hz HR + gravity. Returns a list of per-bout maps (each bout's - /// avg/peak HR, duration, zone time-%, strain, calories, sport). Manual/live - /// sessions overlapping the day are passed as saved spans so an overlapping - /// detected bout is dropped (OVERLAP-DEDUP; manual wins). Empty list when no - /// bout qualifies. Never throws — best-effort, like the other detail blocks. - Future>> _detectedWorkouts( - Substrate s, - PreparedDerivationDay day, - Profile profile, - ) async => const []; - // NOTE: sport typing (ana.RuleSportClassifier) lived here, attached to detected - // workouts. PR#25 relocated workout detection out of _deriveDay, so the per-bout - // classifier was removed; re-home it where the new flow surfaces bouts. - - /// Advanced 4-class sleep over the day substrate via [ana.AdvancedSleepStager] - /// (Cole–Kripke sleep/wake spine + DoG HR-variability + percentile-band - /// classifier + median/physiology smoothing). Returns the MAIN sleep session's - /// AASM metrics (TST/SOL/REM-latency/WASO/disturbances + per-stage minutes) and - /// a 4-class hypnogram. ADDITIVE — the canonical `sleep` block stays the - /// single source; this is a parallel ESTIMATE. `{present:false}` when no - /// qualifying sleep / insufficient data. Never throws (best-effort detail). - Future> _advancedSleep(Substrate s) async => const { - 'present': false, - }; + // NOTE: `detected_workouts` (`const []`) and `advanced_sleep` + // (`{present:false}`) are currently constant stubs — they are now emitted + // directly inside [_computeDayBlocks] (the offloaded second half). When the + // real WorkoutDetector / AdvancedSleepStager passes are re-homed, put them + // back there so they stay OFF the calling isolate. /// Per-5-min movement-level curve over the whole day ([{t, v}], v = fraction /// of seconds in the bucket with a ≥5° wrist-orientation change, 0..1). The /// honest 1 Hz movement signal (same basis as active-minutes) for the "Your /// day" Movement view. Sleep is NOT excluded — the curve naturally dips there. - List> _activityCurve(Substrate s) { + static List> _activityCurve(Substrate s) { final n = s.length; if (n < 60) return const []; final ang = List.filled(n, 0); @@ -2482,7 +2441,7 @@ class DerivationEngine { /// CAVEAT: this assumes the band does NOT keep logging while off-wrist. If a /// future firmware streams off-wrist records, add a skin-temp/motion on-body /// gate here (the substrate carries accel + skinTemp). - Map _wearBlock(Substrate s) { + static Map _wearBlock(Substrate s) { final n = s.length; if (n == 0) { return { @@ -2547,7 +2506,7 @@ class DerivationEngine { /// Timeline graph. 5-min sliding window, emitted ~each minute. Inline artifact /// gate (plausible RR 300–2000 ms) — daytime RR is noisier/motion-confounded, /// so this is a context line, not the nocturnal recovery RMSSD. - List> _dayHrvCurve(Substrate s) { + static List> _dayHrvCurve(Substrate s) { final ts = [], rr = []; for (var i = 0; i < s.rrMs.length; i++) { final v = s.rrMs[i]; @@ -2596,7 +2555,7 @@ class DerivationEngine { /// 24/7 RR. 3-min window emitted ~every 5 min; absent windows (too few/too /// noisy beats) are skipped — never fabricated. Daytime RSA is movement- /// confounded, so it's a context line. - List> _dayRespCurve(Substrate s) { + static List> _dayRespCurve(Substrate s) { final ts = [], rr = []; for (var i = 0; i < s.rrMs.length; i++) { final v = s.rrMs[i]; @@ -2636,7 +2595,7 @@ class DerivationEngine { /// All-day RELATIVE skin-temperature trend (epoch {t,v}). Per-5-min mean ADC /// expressed as a delta from the day's median — RELATIVE only, no absolute °C /// (the band has no calibrated temperature). A slow context line. - List> _daySkinTempCurve(Substrate s) { + static List> _daySkinTempCurve(Substrate s) { final bins = >{}; for (var i = 0; i < s.skinTemp.length && i < s.tsSec.length; i++) { final v = s.skinTemp[i]; @@ -2656,7 +2615,7 @@ class DerivationEngine { ]; } - Map _daytimeHrv(Substrate s, int onsetSec, int offsetSec) { + static Map _daytimeHrv(Substrate s, int onsetSec, int offsetSec) { const binSec = 300; final bins = >{}; double? prev; @@ -2699,7 +2658,7 @@ class DerivationEngine { /// Nocturnal restlessness from sleep-window orientation change: minutes with /// movement, number of distinct movement bouts, longest still stretch (min). - Map _restlessness(Substrate s) { + static Map _restlessness(Substrate s) { final n = s.length; if (n < 60) { return { @@ -2743,7 +2702,7 @@ class DerivationEngine { /// Sleep periods: the main sleep + any NAPS (still, on-wrist minute-runs ≥20 /// min OUTSIDE the main window). Conservative — naps need sustained stillness. - Map _sleepPeriods(Substrate s, int onsetSec, int offsetSec) { + static Map _sleepPeriods(Substrate s, int onsetSec, int offsetSec) { final periods = >[]; var totalAsleep = 0; if (offsetSec > onsetSec) { @@ -2813,7 +2772,7 @@ class DerivationEngine { /// + HR-dip over the WAKE span, the main nocturnal window carved out). Writes a /// rich `naps` block (per-nap start/end epoch-sec + duration + confidence) and a /// `nap_min` scalar (total nap minutes) used by the Sleep Coach + Timeline. - void _attachNaps( + static void _attachNaps( Map bundle, Map? scMap, Substrate s, @@ -2860,27 +2819,154 @@ class DerivationEngine { final napMin = naps.fold(0, (a, nap) => a + (nap.durationSec ~/ 60)); scMap?['nap_min'] = napMin.toDouble(); } catch (e) { - _log('naps FAILED/skipped: $e'); + if (kDebugMode) debugPrint('[derive] naps FAILED/skipped: $e'); } } - /// Opt-in workout SUGGESTIONS (`autoDetectWorkouts`) + HEART-RATE RECOVERY. + /// The full PURE second half of per-day derivation, run OFF the calling isolate + /// via `Isolate.run` (see [_derivePreparedDay]). Previously ALL of this ran on + /// whatever isolate drove the engine — the UI isolate for the foreground light + /// pass fired on every sync — producing multi-second main-thread hangs (rolling + /// RSA Lomb-Scargle over the 24 h day, nap re-staging, workout detection, wake + /// features, steps/energy). DB reads are performed by the caller and passed in; + /// DB writes + notifications are returned as descriptors for the caller to apply. + static _DayBlocksOutput _computeDayBlocks(_DayBlocksInput inp) { + final daySub = inp.daySub; + final sleepSub = inp.sleepSub; + final onset = inp.onsetSec; + final offset = inp.offsetSec; + final bundlePatch = {}; + final seriesPatch = {}; + // Working scalars — seeded with the nightly RHR the pure helpers read + // (steps/energy + wake features gate on it). The seed is removed from the + // returned patch so we only write back the NEWLY computed scalars. + final scMap = {'rhr': inp.rhr}; + + // Wake-day features (active min / strain / calories / steps / zones / wear), + // then the hybrid 100 Hz + 1 Hz steps + TDEE override (order preserved). + final wake = _buildWakeDayFeatures( + daySub, + inp.profile, + sleepOnsetSec: onset, + sleepOffsetSec: offset, + restingHr: inp.rhr, + ); + _applyWakeDayFeatures(bundlePatch, scMap, wake); + _stepsAndEnergy( + bundlePatch, + scMap, + daySub, + inp.profile, + inp.coverageWindows, + inp.liveStepsReal, + inp.stepCalib, + ); + + bundlePatch['daytime_hrv'] = _daytimeHrv(daySub, onset, offset); + seriesPatch['hrv_day'] = _dayHrvCurve(daySub); + seriesPatch['resp_day'] = _dayRespCurve(daySub); + seriesPatch['skin_temp_day'] = _daySkinTempCurve(daySub); + bundlePatch['restlessness'] = _restlessness(sleepSub); + bundlePatch['sleep_periods'] = _sleepPeriods(daySub, onset, offset); + _attachNaps(bundlePatch, scMap, daySub, onset, offset); + // Overrides wake's activity_curve (same value, computed once here). + bundlePatch['activity_curve'] = _activityCurve(daySub); + bundlePatch['detected_workouts'] = const >[]; + + final wc = _computeWorkouts( + s: daySub, + maxHr: inp.maxHrUsed, + rhrScalar: inp.rhr, + saved: inp.savedSessions, + date: inp.date, + dayEndSec: inp.dayEndSec, + dataNowSec: inp.dataNowSec, + ); + bundlePatch['workout_suggestions'] = wc.boutJson; + if (wc.hrrBpm != null) scMap['hrr_bpm'] = wc.hrrBpm; + + _attachWristOrientation(bundlePatch, daySub, onset, offset); + bundlePatch['advanced_sleep'] = const {'present': false}; + + // Feature 6: Restlessness Map (5-min ENMO heatmap of the sleep window). + if (sleepSub.length > 0) { + const bucketSec = 300; // 5 min + final moveSum = {}; + final moveCount = {}; + for (var i = 0; i < sleepSub.length; i++) { + final b = sleepSub.tsSec[i] ~/ bucketSec; + final ax = sleepSub.ax[i]; + final ay = sleepSub.ay[i]; + final az = sleepSub.az[i]; + final mag = math.sqrt(ax * ax + ay * ay + az * az); + final enmo = (mag - 1.0).abs(); + moveSum[b] = (moveSum[b] ?? 0.0) + enmo; + moveCount[b] = (moveCount[b] ?? 0) + 1; + } + final out = >[]; + final keys = moveSum.keys.toList()..sort(); + for (final b in keys) { + final avgEnmo = moveSum[b]! / moveCount[b]!; + final density = math.min(1.0, avgEnmo * 10.0); + out.add({ + 't': b * bucketSec, + 'density': double.parse(density.toStringAsFixed(3)), + }); + } + bundlePatch['restlessness_map'] = out; + } + + // Feature 2: Fit-quality diagnostic (band too loose during high activity). + var activeContactSum = 0; + var activeContactN = 0; + for (var i = 0; i < daySub.length; i++) { + if (daySub.hr[i] > 100 && daySub.skinContact[i] > 0) { + activeContactSum += daySub.skinContact[i]; + activeContactN++; + } + } + if (activeContactN > 60) { + final avgContact = activeContactSum / activeContactN; + if (avgContact < 100) { + bundlePatch['fit_quality'] = 'poor'; + bundlePatch['fit_warning'] = + 'Band is worn too loosely during high activity. Tighten for accurate HR.'; + } + } + + scMap.remove('rhr'); // seed only; the real rhr scalar already lives in the bundle + return _DayBlocksOutput( + bundlePatch: bundlePatch, + seriesPatch: seriesPatch, + scalarPatch: scMap, + wake: wake, + suggestionsToPersist: wc.suggestionsToPersist, + sessionHrrWrites: wc.sessionHrrWrites, + notifBout: wc.notifBout, + ); + } + + /// PURE compute half of workout SUGGESTIONS (`autoDetectWorkouts`) + HRR. /// - /// One detector pass over the day's 1 Hz HR (+ gravity motion) serves both: the - /// detected bouts become persisted "did you work out?" suggestions (excluding - /// any already-saved manual/live session), and each bout's HR tail yields an - /// HRR-60s drop whose daily mean → `hrr_bpm`. Suggestions + the notification are - /// gated to RECENT days so a re-analyze / import never spams. Best-effort. - Future _attachWorkoutSuggestionsAndHrr( - Map bundle, - Map? scMap, - Substrate s, - PreparedDerivationDay day, - int dataNowSec, - ) async { + /// Runs inside the day-blocks isolate: one detector pass over the day's 1 Hz HR + /// (+ gravity motion) yields the detected bouts (excluding any already-saved + /// manual/live session, passed in via [saved] which the caller read from the DB + /// on the DB-owning isolate) and each bout's HR-tail HRR-60s drop. Returns the + /// bout JSON, the mean `hrr_bpm`, the retrospective per-session HRR writes, the + /// recent-day suggestions to persist, and the freshly-ended notif candidate — + /// the DB writes + notification are performed by the caller on the main isolate. + static _WorkoutCompute _computeWorkouts({ + required Substrate s, + required int? maxHr, + required double? rhrScalar, + required List> saved, + required String date, + required int dayEndSec, + required int dataNowSec, + }) { try { final n = s.length; - if (n < 60) return; + if (n < 60) return const _WorkoutCompute.empty(); final hrTs = []; final hrBpm = []; for (var i = 0; i < n; i++) { @@ -2889,22 +2975,16 @@ class DerivationEngine { hrBpm.add(s.hr[i]); } } - if (hrBpm.length < 60) return; + if (hrBpm.length < 60) return const _WorkoutCompute.empty(); final motion = ana.AutoWorkoutDetector.motionPoints(s.tsSec, s.ax, s.ay, s.az); // Exclude windows the user has already logged (manual/live wins). - final dayLo = s.tsSec.first; - final dayHi = s.tsSec.last + 60; - final saved = await LocalDb.sessionsInRange(dayLo, dayHi); final savedSpans = [ for (final r in saved) if (r['start_ts'] is int && r['end_ts'] is int) ana.SavedWorkoutSpan(r['start_ts'] as int, r['end_ts'] as int), ]; - final rhr = (scMap?['rhr'] as num?)?.round(); - // Age-predicted HRmax (Tanaka) computed by the pipeline — drives the %HRR - // gate in the detector. Null when age is unknown (detector falls back). - final maxHr = (bundle['max_hr_used'] as num?)?.round(); + final rhr = rhrScalar?.round(); // Auto-detection needs a real resting-HR baseline. Without one the detector // can't compute a trustworthy %HRR floor and ordinary daytime HR reads as a // workout. If we don't have a nightly RHR for this day yet, skip detection @@ -2940,6 +3020,7 @@ class DerivationEngine { // Also fill HRR for already-saved sessions (manual/live) retrospectively // from the substrate around each session's end — so the workout detail // screen shows HRR without buffering 60 s after a live stop. + final sessionHrr = <(String, double)>[]; for (final r in saved) { final id = r['id']; final endTs = r['end_ts']; @@ -2947,23 +3028,24 @@ class DerivationEngine { final m = _hrrForBout(s, endTs); if (m != null) { drops.add(m); - await LocalDb.setSessionHrr(id, double.parse(m.toStringAsFixed(1))); + sessionHrr.add((id, double.parse(m.toStringAsFixed(1)))); } } - if (drops.isNotEmpty) { - final mean = drops.reduce((a, c) => a + c) / drops.length; - scMap?['hrr_bpm'] = double.parse(mean.toStringAsFixed(1)); - } - bundle['workout_suggestions'] = boutJson; + final hrrBpm = drops.isEmpty + ? null + : double.parse( + (drops.reduce((a, c) => a + c) / drops.length).toStringAsFixed(1)); // Persist + notify only for RECENT days (≤ ~36 h old) so imports/re-analyze // don't resurface 90 days of prompts. - final recent = (dataNowSec - day.endSec) < 36 * 3600; + final recent = (dataNowSec - dayEndSec) < 36 * 3600; + final toPersist = >[]; + ({int endSec, int durationMin})? notif; if (recent && bouts.isNotEmpty) { for (final b in bouts) { - await LocalDb.putWorkoutSuggestion({ - 'id': '${day.date}:${b.startSec}', - 'date': day.date, + toPersist.add({ + 'id': '$date:${b.startSec}', + 'date': date, 'start_ts': b.startSec, 'end_ts': b.endSec, 'avg_bpm': b.avgBpm, @@ -2971,7 +3053,6 @@ class DerivationEngine { 'duration_min': b.durationMin, 'sport': b.sport, 'dismissed': 0, - 'created_at': DateTime.now().millisecondsSinceEpoch, }); } // Notify ONLY for a bout that ended in the last ~2 h (a near-real-time @@ -2979,30 +3060,27 @@ class DerivationEngine { // day at once; without this every hours-old bout would fire a "did you work // out?" prompt → a wall of notifications. Suggestions are still persisted // above so they surface in the Workouts screen; we just don't ping for them. - final newest = - bouts.reduce((a, b) => a.endSec >= b.endSec ? a : b); - final freshlyEnded = (dataNowSec - newest.endSec) < 2 * 3600; - if (freshlyEnded) { - await NotificationCenter.instance.emit(NotificationEvent( - dedupeKey: '${day.date}:auto_workout', - category: NotifCategory.recovery, - priority: NotifPriority.normal, - title: 'Did you work out?', - body: 'We spotted ~${newest.durationMin} min of elevated activity. ' - 'Tap to log it.', - date: day.date, - route: '/workouts', - )); + final newest = bouts.reduce((a, b) => a.endSec >= b.endSec ? a : b); + if ((dataNowSec - newest.endSec) < 2 * 3600) { + notif = (endSec: newest.endSec, durationMin: newest.durationMin); } } + return _WorkoutCompute( + boutJson: boutJson, + hrrBpm: hrrBpm, + sessionHrrWrites: sessionHrr, + suggestionsToPersist: toPersist, + notifBout: notif, + ); } catch (e) { - _log('auto-workout/HRR FAILED/skipped: $e'); + if (kDebugMode) debugPrint('[derive] auto-workout/HRR FAILED/skipped: $e'); + return const _WorkoutCompute.empty(); } } /// HRR-60s for a bout ending at [endSec]: build the per-second HR tail around /// the end index and delegate to [ana.hrRecovery]. Returns the drop (bpm) or null. - double? _hrrForBout(Substrate s, int endSec) { + static double? _hrrForBout(Substrate s, int endSec) { final n = s.length; if (n == 0) return null; // Find the index nearest the bout end. @@ -3026,7 +3104,7 @@ class DerivationEngine { /// Explicitly a WRIST measure (body-position PROXY), never claimed as the /// sleeper's supine/side/prone body position. Emits a dominant-orientation /// summary + per-position minutes + an orientation-change count. Best-effort. - void _attachWristOrientation( + static void _attachWristOrientation( Map bundle, Substrate s, int onsetSec, @@ -3071,7 +3149,7 @@ class DerivationEngine { 'independently of the torso.', }; } catch (e) { - _log('wrist-orientation FAILED/skipped: $e'); + if (kDebugMode) debugPrint('[derive] wrist-orientation FAILED/skipped: $e'); } } @@ -3116,6 +3194,88 @@ class DerivationEngine { } } +/// Sendable input for [DerivationEngine._computeDayBlocks] — crosses the +/// `Isolate.run` boundary, so every field is plain data (Substrate is int/double +/// lists; Profile/StepCalibration are primitive data classes). DB reads that the +/// pure compute needs are performed by the caller and passed in here. +class _DayBlocksInput { + final Substrate daySub; + final Substrate sleepSub; + final Profile profile; + final int onsetSec; + final int offsetSec; + final double? rhr; + final int? maxHrUsed; + final List> coverageWindows; + final int liveStepsReal; + final ana.StepCalibration? stepCalib; + final List> savedSessions; + final String date; + final int dayEndSec; + final int dataNowSec; + const _DayBlocksInput({ + required this.daySub, + required this.sleepSub, + required this.profile, + required this.onsetSec, + required this.offsetSec, + required this.rhr, + required this.maxHrUsed, + required this.coverageWindows, + required this.liveStepsReal, + required this.stepCalib, + required this.savedSessions, + required this.date, + required this.dayEndSec, + required this.dataNowSec, + }); +} + +/// Sendable output of [DerivationEngine._computeDayBlocks]. [bundlePatch] / +/// [seriesPatch] / [scalarPatch] are merged into the isolate-1 bundle on the main +/// isolate; [wake] is persisted; [suggestionsToPersist] / [sessionHrrWrites] / +/// [notifBout] are the DB writes + notification the caller applies. +class _DayBlocksOutput { + final Map bundlePatch; + final Map seriesPatch; + final Map scalarPatch; + final Map wake; + final List> suggestionsToPersist; + final List<(String, double)> sessionHrrWrites; + final ({int endSec, int durationMin})? notifBout; + const _DayBlocksOutput({ + required this.bundlePatch, + required this.seriesPatch, + required this.scalarPatch, + required this.wake, + required this.suggestionsToPersist, + required this.sessionHrrWrites, + required this.notifBout, + }); +} + +/// Result of the pure workout compute ([DerivationEngine._computeWorkouts]). +class _WorkoutCompute { + final List> boutJson; + final double? hrrBpm; + final List<(String, double)> sessionHrrWrites; + final List> suggestionsToPersist; + final ({int endSec, int durationMin})? notifBout; + const _WorkoutCompute({ + required this.boutJson, + required this.hrrBpm, + required this.sessionHrrWrites, + required this.suggestionsToPersist, + required this.notifBout, + }); + const _WorkoutCompute.empty() + : boutJson = const [], + hrrBpm = null, + sessionHrrWrites = const [], + suggestionsToPersist = const [], + notifBout = null; +} + double? _median(List xs) { if (xs.isEmpty) return null; final vs = List.from(xs)..sort(); diff --git a/lib/data/db.dart b/lib/data/db.dart index 89bcf719..ce6590cf 100644 --- a/lib/data/db.dart +++ b/lib/data/db.dart @@ -25,8 +25,15 @@ class LocalDb { static String dbName = 'openstrap.db'; static Future get instance async { - _db ??= await _open(); - return _db!; + final db = _db; + // `_db != null` is NOT enough: Android can close the underlying + // SQLiteDatabase on background teardown without our close() ever nulling + // `_db`. A plain `_db ??=` then keeps handing back that dead handle, and + // every write throws `DatabaseException(attempt to re-open an already-closed + // object)` — a sustained crash burst (seen in the wild on background event + // ingest). Reopen whenever the cached handle isn't actually open. + if (db != null && db.isOpen) return db; + return _db = await _open(); } static Future close() async { diff --git a/lib/data/local_repository_impl.dart b/lib/data/local_repository_impl.dart index d0af2450..3ebda36b 100644 --- a/lib/data/local_repository_impl.dart +++ b/lib/data/local_repository_impl.dart @@ -14,6 +14,7 @@ // Profile-gated metrics are null when the profile field is missing. import 'dart:convert'; +import 'dart:isolate'; import 'dart:math' as math; import '../compute/derivation_engine.dart'; @@ -2299,38 +2300,10 @@ class LocalRepositoryImpl extends LocalRepository { @override Future> spotCheck(List records) async { - // Decode RR from the live RR-bearing frames (0x28 / R10), clean, compute HRV. - final rrMs = []; - final hrs = []; - for (final hex in records) { - final rr = proto.realtimeRr(hex); - if (rr != null) { - for (final v in rr.rrMs) { - if (v > 0) rrMs.add(v.toDouble()); - } - } - try { - final s = proto.decodeRecord(hex); - if (s != null && s.hr > 0) hrs.add(s.hr.toDouble()); - } catch (_) {} - } - if (rrMs.length < 20) { - return {'ok': false, 'n_beats': rrMs.length}; - } - final cleaned = ana.correctRr(rrMs); - final hrv = ana.hrvTime(cleaned.nn, nnTimesMs: cleaned.nnTimesMs); - if (!hrv.present) return {'ok': false, 'n_beats': cleaned.nn.length}; - final meanHr = hrs.isEmpty - ? null - : hrs.reduce((a, b) => a + b) / hrs.length; - return { - 'ok': true, - 'rmssd': hrv.value!.rmssd?.round(), - 'sdnn': hrv.value!.sdnn?.round(), - 'mean_hr': meanHr?.round(), - 'n_beats': cleaned.nn.length, - 'confidence': hrv.confidence, - }; + // Decode + HRV run OFF the UI isolate. The spot-check buffer grows over the + // multi-minute measurement, so decoding every frame + RR correction + HRV on + // the main isolate was real per-tick work that hung the UI on slower phones. + return Isolate.run(() => _spotCheckCompute(records)); } @override @@ -2338,39 +2311,10 @@ class LocalRepositoryImpl extends LocalRepository { List records, { double? pacedHz, }) async { - // Decode RR from the live RR-bearing frames (0x28 / R10) — same seam - // spotCheck uses — then run McCraty & Zayas 2014 cardiac coherence. - final rrMs = []; - for (final hex in records) { - final rr = proto.realtimeRr(hex); - if (rr != null) { - for (final v in rr.rrMs) { - if (v > 0) rrMs.add(v.toDouble()); - } - } - } - if (rrMs.length < 20) { - return {'ok': false, 'n_beats': rrMs.length}; - } - final cleaned = ana.correctRr(rrMs); - final m = ana.cardiacCoherence(cleaned.nn, cleaned.nnTimesMs, pacedHz: pacedHz); - if (!m.present) { - return { - 'ok': false, - 'n_beats': cleaned.nn.length, - 'note': m.note, - }; - } - return { - 'ok': true, - 'ratio': m.value!.ratio, - 'score': m.value!.score.round(), - 'peak_hz': m.value!.peakHz, - 'n_beats': cleaned.nn.length, - 'confidence': m.confidence, - 'tier': m.tier, - 'note': m.note, - }; + // Offloaded: cardiac coherence is a 400-point Lomb-Scargle PSD recomputed + // over the FULL (growing) session buffer every 20 s — pure sin/cos work that + // was running on the UI isolate and is a confirmed foreground-hang source. + return Isolate.run(() => _breathingCoherenceCompute(records, pacedHz)); } // ── small series helpers ───────────────────────────────────────────────────── @@ -2421,3 +2365,75 @@ Map? stressSummaryForToday( 'level': score < 34 ? 'low' : (score < 67 ? 'moderate' : 'high'), }; } + +// ── Live spot-check / breathing compute (run under Isolate.run, off the UI) ──── +// Top-level (no `this` capture) + only file-scoped `proto`/`ana` top-level +// functions + a List of hex frames in, a plain Map out — all sendable. + +Map _spotCheckCompute(List records) { + // Decode RR from the live RR-bearing frames (0x28 / R10), clean, compute HRV. + final rrMs = []; + final hrs = []; + for (final hex in records) { + final rr = proto.realtimeRr(hex); + if (rr != null) { + for (final v in rr.rrMs) { + if (v > 0) rrMs.add(v.toDouble()); + } + } + try { + final s = proto.decodeRecord(hex); + if (s != null && s.hr > 0) hrs.add(s.hr.toDouble()); + } catch (_) {} + } + if (rrMs.length < 20) { + return {'ok': false, 'n_beats': rrMs.length}; + } + final cleaned = ana.correctRr(rrMs); + final hrv = ana.hrvTime(cleaned.nn, nnTimesMs: cleaned.nnTimesMs); + if (!hrv.present) return {'ok': false, 'n_beats': cleaned.nn.length}; + final meanHr = hrs.isEmpty ? null : hrs.reduce((a, b) => a + b) / hrs.length; + return { + 'ok': true, + 'rmssd': hrv.value!.rmssd?.round(), + 'sdnn': hrv.value!.sdnn?.round(), + 'mean_hr': meanHr?.round(), + 'n_beats': cleaned.nn.length, + 'confidence': hrv.confidence, + }; +} + +Map _breathingCoherenceCompute( + List records, + double? pacedHz, +) { + // Decode RR from the live RR-bearing frames (0x28 / R10) — same seam + // spotCheck uses — then run McCraty & Zayas 2014 cardiac coherence. + final rrMs = []; + for (final hex in records) { + final rr = proto.realtimeRr(hex); + if (rr != null) { + for (final v in rr.rrMs) { + if (v > 0) rrMs.add(v.toDouble()); + } + } + } + if (rrMs.length < 20) { + return {'ok': false, 'n_beats': rrMs.length}; + } + final cleaned = ana.correctRr(rrMs); + final m = ana.cardiacCoherence(cleaned.nn, cleaned.nnTimesMs, pacedHz: pacedHz); + if (!m.present) { + return {'ok': false, 'n_beats': cleaned.nn.length, 'note': m.note}; + } + return { + 'ok': true, + 'ratio': m.value!.ratio, + 'score': m.value!.score.round(), + 'peak_hz': m.value!.peakHz, + 'n_beats': cleaned.nn.length, + 'confidence': m.confidence, + 'tier': m.tier, + 'note': m.note, + }; +} diff --git a/lib/ui/coach/coach_settings_screen.dart b/lib/ui/coach/coach_settings_screen.dart index 38910b4e..881706ad 100644 --- a/lib/ui/coach/coach_settings_screen.dart +++ b/lib/ui/coach/coach_settings_screen.dart @@ -73,11 +73,19 @@ class _CoachSettingsScreenState extends State { setState(() => _msg = 'Pick or type a model first.'); return; } + // Capture the messenger + navigator BEFORE the await (no BuildContext across + // an async gap), then guard pop() with canPop(): a bare Navigator.pop() after + // the await crashed with "Bad state: No element" (NavigatorState.pop's + // internal lastWhere) when the route was no longer poppable by the time save + // completed — being `mounted` doesn't guarantee a poppable route. + final messenger = ScaffoldMessenger.of(context); + final navigator = Navigator.of(context); await cfg.save(baseUrl: _base.text, apiKey: _key.text, model: _chosen); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('AI Coach settings saved.'))); - Navigator.of(context).pop(); - } + if (!mounted) return; + messenger.showSnackBar( + const SnackBar(content: Text('AI Coach settings saved.')), + ); + if (navigator.canPop()) navigator.pop(); } // Searchable list of fetched models + a "use what I typed" custom row. The row diff --git a/lib/ui/kit/route_map.dart b/lib/ui/kit/route_map.dart index 43b1f0cb..a4c69a47 100644 --- a/lib/ui/kit/route_map.dart +++ b/lib/ui/kit/route_map.dart @@ -162,7 +162,13 @@ class RouteMapView extends StatelessWidget { @override Widget build(BuildContext context) { - final pts = _points; + // Drop any non-finite GPS coordinate (a bad fix can carry NaN/Inf lat/lng). + // Left in, it makes LatLngBounds + camera-fit NaN and crashes the tile layer + // at build (NaN.toInt in flutter_map's _clampToNativeZoom) — a real FATAL. + final pts = [ + for (final p in _points) + if (p.latitude.isFinite && p.longitude.isFinite) p, + ]; if (pts.isEmpty) return const SizedBox.shrink(); // Detect gesture-driven camera moves so a live map can stop auto-following. @@ -170,10 +176,22 @@ class RouteMapView extends StatelessWidget { if (hasGesture) onUserPan?.call(); } - final options = pts.length >= 2 + // Only bounds-fit when the box has real area. A zero-area box (every point + // identical — a stationary or aborted route) makes CameraFit.bounds emit a + // NaN/Infinite zoom that the tile layer then crashes on; fall back to a + // fixed-zoom center view in that case. + LatLngBounds? fitBounds; + if (pts.length >= 2) { + final b = LatLngBounds.fromPoints(pts); + if ((b.north - b.south).abs() > 1e-7 || + (b.east - b.west).abs() > 1e-7) { + fitBounds = b; + } + } + final options = fitBounds != null ? MapOptions( initialCameraFit: CameraFit.bounds( - bounds: LatLngBounds.fromPoints(pts), + bounds: fitBounds, padding: const EdgeInsets.all(28), // Cap how far bounds-fit will zoom in. Without this, a route // whose points are all still close together (early in a From e5691e37319e55a4f4b2a41e8877740de4be280c Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sat, 18 Jul 2026 23:43:25 +0530 Subject: [PATCH 05/14] Fix CodeRabbit findings on PR #93 (app-store-readiness prep) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IOS_INSTALLATION.md: correct the version-tracking mechanism doc — Runner has no MARKETING_VERSION setting; it flows through Info.plist's CFBundleShortVersionString/CFBundleVersion instead. - derivation_engine.dart: run the offloaded day-blocks second half in an explicitly spawned, killable isolate instead of Isolate.run().timeout(), so a timed-out day's isolate is actually killed instead of continuing to burn CPU in the background. - derivation_engine.dart + db.dart: track second-half success as `partial` on day_result (new additive column, db v24->v25, same pattern as the existing `skipped` column) so a headline-only result from a failed/timed- out second half never locks in as finalized or counts as "derived" for the raw-pruning guard — closes a real data-loss path where naps/workouts/ HRR/wear/curves for an old day could become permanently unrecoverable. - derivation_engine.dart: disable permission prompts on the headless auto-workout notification (allowPermissionPrompt: false). - derivation_engine.dart: copy hybrid-corrected steps/calories_total back into the wake artifact after _stepsAndEnergy runs, so the Today repo's early-read path doesn't serve stale 1Hz-only step estimates. - app_state.dart: gate updateAvailable/updateMandatory on UpdateService. supported (Android + kSideloadOtaEnabled) instead of the OTA flag alone. - route_map.dart: filter NaN/Inf vertices out of _polylines(), breaking the segment there instead of letting them reach PolylineLayer. - about_screen.dart: surface a snackbar when a legal-link launch fails instead of swallowing it silently. PRIVACY.md vs. shipped consent-default mismatch was investigated but left unfixed — needs a human/legal decision (see PR discussion). --- guides/IOS_INSTALLATION.md | 10 +- lib/compute/derivation_engine.dart | 156 ++++++++++++++++++++++++----- lib/data/db.dart | 58 +++++++++-- lib/state/app_state.dart | 11 +- lib/ui/kit/route_map.dart | 21 +++- lib/ui/profile/about_screen.dart | 25 ++++- 6 files changed, 235 insertions(+), 46 deletions(-) diff --git a/guides/IOS_INSTALLATION.md b/guides/IOS_INSTALLATION.md index 8fccac7f..f3b663fe 100644 --- a/guides/IOS_INSTALLATION.md +++ b/guides/IOS_INSTALLATION.md @@ -128,9 +128,13 @@ flutter run --release -d --dart-define-from-file=.env ## Version Numbers -`Runner`'s `MARKETING_VERSION`/`CURRENT_PROJECT_VERSION` track `pubspec.yaml`'s -`version:` automatically via `$(FLUTTER_BUILD_NAME)`/`$(FLUTTER_BUILD_NUMBER)` -(Flutter writes these into `ios/Flutter/Generated.xcconfig` on every build). +`Runner` tracks `pubspec.yaml`'s `version:` automatically via `Info.plist`'s +`CFBundleShortVersionString`/`CFBundleVersion` keys, which point at +`$(FLUTTER_BUILD_NAME)`/`$(FLUTTER_BUILD_NUMBER)` (Flutter writes these into +`ios/Flutter/Generated.xcconfig` on every build). Runner's own +`CURRENT_PROJECT_VERSION` build setting also resolves to `$(FLUTTER_BUILD_NUMBER)`; +it does not set a `MARKETING_VERSION` build setting at all — the marketing +version comes solely from the `Info.plist` key above. The **`OpenStrapWidget`/`OpenStrapWidgetExtension`** and **`OpenStrapWatch Watch App`** targets are NOT wired to that mechanism — they don't include diff --git a/lib/compute/derivation_engine.dart b/lib/compute/derivation_engine.dart index 466fc7f3..c5b1dd70 100644 --- a/lib/compute/derivation_engine.dart +++ b/lib/compute/derivation_engine.dart @@ -1451,6 +1451,13 @@ class DerivationEngine { // drop the optional detail blocks. (Previously an exception here threw out of // _derivePreparedDay → the day was marked skipped → readiness rendered "-" // even though it had been computed fine — the "readiness randomly goes -" bug.) + // `secondHalfOk` tracks whether this actually completed: a headline-only + // row must be marked `partial` below so it never locks as finalized and + // never counts as "derived" for the raw-pruning guard (see + // LocalDb.dayResultIds) — otherwise a transient failure here permanently + // loses the ability to ever back-fill naps/workouts/HRR/wear/curves for + // this day once its raw substrate is pruned. + var secondHalfOk = true; try { final dayLo = daySub.length == 0 ? 0 : daySub.tsSec.first; final dayHi = daySub.length == 0 ? 0 : daySub.tsSec.last + 60; @@ -1478,8 +1485,8 @@ class DerivationEngine { dayEndSec: day.endSec, dataNowSec: dataNowSec, ); - final blocks = await Isolate.run(() => _computeDayBlocks(blocksInput)) - .timeout(_perDayTimeout); + final blocks = + await _runDayBlocksCancellable(blocksInput, _perDayTimeout); // Merge the computed blocks back into the isolate-1 bundle. scMap is the // CastMap view over bundle['scalars'], so addAll writes through — nap_min / @@ -1502,31 +1509,43 @@ class DerivationEngine { } final nb = blocks.notifBout; if (nb != null) { - await NotificationCenter.instance.emit(NotificationEvent( - dedupeKey: '${day.date}:auto_workout', - category: NotifCategory.recovery, - priority: NotifPriority.normal, - title: 'Did you work out?', - body: 'We spotted ~${nb.durationMin} min of elevated activity. ' - 'Tap to log it.', - date: day.date, - route: '/workouts', - )); + await NotificationCenter.instance.emit( + NotificationEvent( + dedupeKey: '${day.date}:auto_workout', + category: NotifCategory.recovery, + priority: NotifPriority.normal, + title: 'Did you work out?', + body: 'We spotted ~${nb.durationMin} min of elevated activity. ' + 'Tap to log it.', + date: day.date, + route: '/workouts', + ), + // This runs from headless background derivation too — never prompt + // for permission from a background context (violates the OS + // background contract and can incorrectly cache permission=denied). + allowPermissionPrompt: false, + ); } await _persistWakeDayFeatures(dayId: day.date, wake: blocks.wake); } catch (e, st) { + secondHalfOk = false; _log('day-blocks (offloaded second half) failed for ${day.date} — ' - 'persisting headline day: $e'); + 'persisting headline day (partial): $e'); TelemetryService.instance.recordNonFatal(e, st, reason: 'day_blocks_failed'); } // Finalize once the DATA EDGE has moved >48 h past the day's wake — i.e. we // have continuous drained data well beyond it, so no more flash can land for // this day. (Anchored on the last record ts, NOT the wall clock.) Imports - // force-finalize: there is no stored raw to ever recompute them from. - final finalized = - forceFinalize || (day.endSec + _finalizationSec) < dataNowSec; + // force-finalize: there is no stored raw to ever recompute them from, so + // forceFinalize wins even for a partial (headline-only) result — there's + // nothing left to retry regardless. Outside of that, never let a partial + // result lock in as finalized purely by age, or its missing naps/ + // workouts/HRR/wear/curves would never get a chance to be filled in by a + // later retry. + final ageFinalized = (day.endSec + _finalizationSec) < dataNowSec; + final finalized = forceFinalize || (ageFinalized && secondHalfOk); final scalars = (bundle['scalars'] as Map?)?.cast() ?? const {}; @@ -1539,6 +1558,7 @@ class DerivationEngine { ((day.sleepJson['window'] as Map?) ?? const {}).cast(), ), finalized: finalized, + partial: !secondHalfOk, rhr: sc('rhr'), rmssd: sc('rmssd'), readiness: sc('readiness'), @@ -2823,13 +2843,92 @@ class DerivationEngine { } } - /// The full PURE second half of per-day derivation, run OFF the calling isolate - /// via `Isolate.run` (see [_derivePreparedDay]). Previously ALL of this ran on - /// whatever isolate drove the engine — the UI isolate for the foreground light - /// pass fired on every sync — producing multi-second main-thread hangs (rolling - /// RSA Lomb-Scargle over the 24 h day, nap re-staging, workout detection, wake - /// features, steps/energy). DB reads are performed by the caller and passed in; - /// DB writes + notifications are returned as descriptors for the caller to apply. + /// Runs [_computeDayBlocks] in an explicitly spawned, killable isolate and + /// enforces [timeout] on the isolate itself — not just on the caller's wait. + /// + /// `Isolate.run(...).timeout(...)` (the previous approach) only stops the + /// CALLER from awaiting the result; the spawned isolate keeps executing to + /// completion in the background regardless. Under a multi-day backlog with + /// a bounded worker pool ([_deriveConcurrency]), a slow/hung day's abandoned + /// isolate can keep burning CPU well after its caller moved on to the next + /// day — silently exceeding the intended concurrency budget. Spawning the + /// isolate ourselves gives us a handle to actually `kill()` it on timeout. + static Future<_DayBlocksOutput> _runDayBlocksCancellable( + _DayBlocksInput input, + Duration timeout, + ) async { + final port = ReceivePort(); + final isolate = await Isolate.spawn( + _dayBlocksIsolateEntry, + (port.sendPort, input), + onError: port.sendPort, + onExit: port.sendPort, + ); + final completer = Completer<_DayBlocksOutput>(); + late final StreamSubscription sub; + sub = port.listen((message) { + if (completer.isCompleted) return; + if (message is _DayBlocksOutput) { + completer.complete(message); + } else if (message is List) { + // Either our own caught-exception report (`[error, stack]`) or the + // `onError` port's uncaught-error format — both are 2-element lists + // of strings. `onExit` fires with `null`, which we treat as "the + // isolate ended without ever sending a result" below. + completer.completeError( + StateError( + message.isNotEmpty + ? 'day-blocks isolate failed: ${message.first}' + : 'day-blocks isolate failed with no error detail', + ), + ); + } else if (message == null) { + if (!completer.isCompleted) { + completer.completeError( + StateError('day-blocks isolate exited without a result'), + ); + } + } + }); + try { + return await completer.future.timeout( + timeout, + onTimeout: () { + isolate.kill(priority: Isolate.immediate); + throw TimeoutException( + 'day-blocks computation timed out after $timeout', + ); + }, + ); + } finally { + await sub.cancel(); + port.close(); + // No-op if the isolate already exited normally; guarantees a hung or + // still-running isolate never outlives this call. + isolate.kill(priority: Isolate.immediate); + } + } + + /// `Isolate.spawn` entry point for [_runDayBlocksCancellable]. Must be a + /// static/top-level function taking exactly one (sendable) argument. + static void _dayBlocksIsolateEntry((SendPort, _DayBlocksInput) args) { + final (sendPort, input) = args; + try { + sendPort.send(_computeDayBlocks(input)); + } catch (e, st) { + sendPort.send([e.toString(), st.toString()]); + } + } + + /// The full PURE second half of per-day derivation, run OFF the calling + /// isolate via a cancellable spawned isolate (see [_runDayBlocksCancellable], + /// [_derivePreparedDay]). Previously ALL of this ran on whatever isolate + /// drove the engine — the UI isolate for the foreground light pass fired on + /// every sync — producing multi-second main-thread hangs (rolling RSA + /// Lomb-Scargle over the 24 h day, nap re-staging, workout detection, wake + /// features, steps/energy). DB reads are performed by the caller and passed + /// in; DB writes + notifications are returned as descriptors for the caller + /// to apply. static _DayBlocksOutput _computeDayBlocks(_DayBlocksInput inp) { final daySub = inp.daySub; final sleepSub = inp.sleepSub; @@ -2861,6 +2960,17 @@ class DerivationEngine { inp.liveStepsReal, inp.stepCalib, ); + // _stepsAndEnergy just corrected `steps`/`calories_total` in bundlePatch + + // scMap using the hybrid real-100Hz + 1Hz-estimate count, but `wake` (built + // above by _buildWakeDayFeatures, before this correction ran) still holds + // the earlier 1Hz-only estimate. `wake` is what _persistWakeDayFeatures + // stores and what the Today repository reads while the full day result + // isn't ready yet, so copy the corrected values back in to avoid serving + // stale steps/calories from that early-read path. + for (final key in const ['steps', 'calories_total']) { + final value = scMap[key]; + if (value != null) wake[key] = value; + } bundlePatch['daytime_hrv'] = _daytimeHrv(daySub, onset, offset); seriesPatch['hrv_day'] = _dayHrvCurve(daySub); diff --git a/lib/data/db.dart b/lib/data/db.dart index ce6590cf..aa9bf979 100644 --- a/lib/data/db.dart +++ b/lib/data/db.dart @@ -287,11 +287,27 @@ class LocalDb { // this is right. await _ensureDayResultSkippedColumn(db); } + if (oldV < 25) { + // Same class of bug as `skipped` above, different failure mode: the + // offloaded second-half day-blocks compute (naps/workouts/HRR/wear/ + // curves/wake-features) can throw or time out AFTER the first-half + // headline scalars (readiness/RHR/RMSSD) already succeeded. That + // path is non-fatal by design (the day still gets a real, + // non-skipped day_result row so headline scalars display), but the + // raw-pruning guard only excluded `skipped` rows - a headline-only + // partial row still counted as "derived" and let the raw substrate + // it would need to fill in those missing blocks get pruned for + // good. This column lets the guard exclude partial rows too. + // Existing rows default to 0 (not partial) - can't know in + // hindsight which old rows were partial, but going forward this is + // right. + await _ensureDayResultPartialColumn(db); + } }, onOpen: (db) async { await _repairOpenSchema(db); }, - version: 24, + version: 25, ); } @@ -339,6 +355,7 @@ class LocalDb { await _createWorkoutRoute(db); await _ensureWorkoutRouteSpeed(db); await _ensureDayResultSkippedColumn(db); + await _ensureDayResultPartialColumn(db); // Views LAST — they depend on metric_series / day_result / baselines / sessions // / notifications all existing. DROP+CREATE so a shape change takes effect. await _ensureCoachViews(db); @@ -359,6 +376,20 @@ class LocalDb { } } + static Future _ensureDayResultPartialColumn(Database db) async { + final info = await db.rawQuery('PRAGMA table_info(day_result)'); + final has = info.any((c) => c['name'] == 'partial'); + if (!has) { + try { + await db.execute( + 'ALTER TABLE day_result ADD COLUMN partial INTEGER NOT NULL DEFAULT 0', + ); + } catch (_) { + /* another opener won the race — column now exists */ + } + } + } + // ── MENSTRUAL SYMPTOM LOG ────────────────────────────────────────────────── static Future _createCycleSymptom(Database db) async { await db.execute(''' @@ -746,6 +777,7 @@ class LocalDb { rmssd REAL, readiness REAL, skipped INTEGER NOT NULL DEFAULT 0, + partial INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (day_id, algo_version) ) '''); @@ -2154,6 +2186,15 @@ class LocalDb { required String windowJson, bool finalized = false, bool skipped = false, + // A day whose offloaded second-half compute (naps/workouts/HRR/wear/ + // curves/wake-features) failed or timed out AFTER the headline scalars + // already succeeded — the row is real (not a skip marker) so headline + // scalars still display, but it must never count as "derived" for the + // raw-pruning guard (see dayResultIds). Callers should also avoid passing + // `finalized: true` alongside `partial: true` unless there genuinely is + // no raw substrate to ever retry from (e.g. the force-finalized import + // path), since a finalized row is never revisited on a version bump. + bool partial = false, double? rhr, double? rmssd, double? readiness, @@ -2170,6 +2211,7 @@ class LocalDb { 'computed_at': now, 'finalized': finalized ? 1 : 0, 'skipped': skipped ? 1 : 0, + 'partial': partial ? 1 : 0, 'rhr': rhr, 'rmssd': rmssd, 'readiness': readiness, @@ -2218,17 +2260,19 @@ class LocalDb { return [for (final r in rows) _withDate(r)]; } - /// The set of day_id labels that already have a REAL (non-skipped) result - /// at [algoVersion]. Used by the raw-pruning guard to decide what's safe - /// to prune - a day that only ever got a skip-marker (its derivation - /// threw) must NOT count as "derived" here, or its raw substrate gets - /// pruned with no way left to ever compute it correctly. + /// The set of day_id labels that already have a REAL, COMPLETE result at + /// [algoVersion]. Used by the raw-pruning guard to decide what's safe to + /// prune - a day that only ever got a skip-marker (its derivation threw) + /// or a partial row (headline scalars only; the offloaded second-half + /// blocks failed/timed out) must NOT count as "derived" here, or its raw + /// substrate gets pruned with no way left to ever fill in the missing + /// data correctly. static Future> dayResultIds(int algoVersion) async { final db = await instance; final rows = await db.query( 'day_result', columns: ['day_id'], - where: 'algo_version = ? AND skipped = 0', + where: 'algo_version = ? AND skipped = 0 AND partial = 0', whereArgs: [algoVersion], ); return {for (final r in rows) r['day_id'] as String}; diff --git a/lib/state/app_state.dart b/lib/state/app_state.dart index c855fa98..50eddd5f 100644 --- a/lib/state/app_state.dart +++ b/lib/state/app_state.dart @@ -518,18 +518,19 @@ class AppState extends ChangeNotifier { /// A newer build is published (we're behind latest_build). /// - /// Gated on [kSideloadOtaEnabled] regardless of platform: store builds - /// (Play Store / App Store) must never surface a self-update prompt at - /// all, not just fall back to a browser link — see update_service.dart. + /// Gated on [UpdateService.supported] (Android + [kSideloadOtaEnabled]): + /// store builds (Play Store / App Store) and any non-Android build must + /// never surface a self-update prompt at all, not just fall back to a + /// browser link — see update_service.dart. bool get updateAvailable => - kSideloadOtaEnabled && + UpdateService.supported && _update != null && _currentBuild > 0 && _update!.latestBuild > _currentBuild; /// We're below the mandatory floor — the prompt can't be dismissed. bool get updateMandatory => - kSideloadOtaEnabled && + UpdateService.supported && _update != null && _currentBuild > 0 && _currentBuild < _update!.minBuild; diff --git a/lib/ui/kit/route_map.dart b/lib/ui/kit/route_map.dart index a4c69a47..e03dafee 100644 --- a/lib/ui/kit/route_map.dart +++ b/lib/ui/kit/route_map.dart @@ -129,6 +129,16 @@ class RouteMapView extends StatelessWidget { /// look under the crisp line — the only colour against the monochrome /// basemap, so it needs to read as unmistakably "the route", not a thin /// GPS-app line lost against a busy street atlas. + // A bad GPS fix can carry NaN/Inf lat/lng (see the finite-filter in build() + // below for the camera/bounds path). _points there is filtered, but + // _polylines() reads straight from `vertices`, so it needs its own check — + // an unfiltered NaN/Inf vertex reaching PolylineLayer can crash or corrupt + // rendering. + bool _validPos(List v, int i) { + final p = v[i].pos; + return p.latitude.isFinite && p.longitude.isFinite; + } + List _polylines({bool glow = false}) { final v = vertices; if (v.length < 2) return const []; @@ -137,14 +147,19 @@ class RouteMapView extends StatelessWidget { final width = interactive ? 5.0 : 4.0; var i = 0; while (i < v.length - 1) { - if (v[i + 1].gapBefore) { - i++; // segment break — no edge across the gap + if (v[i + 1].gapBefore || !_validPos(v, i) || !_validPos(v, i + 1)) { + // Segment break — no edge across a recording gap OR an invalid + // (NaN/Inf) vertex; either way we never draw an edge touching it. + i++; continue; } final c = edgeColor(i); final pts = [v[i].pos]; var j = i; - while (j < v.length - 1 && edgeColor(j) == c && !v[j + 1].gapBefore) { + while (j < v.length - 1 && + edgeColor(j) == c && + !v[j + 1].gapBefore && + _validPos(v, j + 1)) { pts.add(v[j + 1].pos); j++; } diff --git a/lib/ui/profile/about_screen.dart b/lib/ui/profile/about_screen.dart index 59acfe2a..8bbab5d5 100644 --- a/lib/ui/profile/about_screen.dart +++ b/lib/ui/profile/about_screen.dart @@ -38,10 +38,25 @@ class _AboutScreenState extends State { }); } - static Future _openUrl(String url) async { + /// Returns whether the launch actually succeeded. `launchUrl` can return + /// `false` without throwing (e.g. no handler for the URL) — callers here + /// (Privacy Policy / Notice links) are legal-disclosure surfaces, so a + /// failed launch must be surfaced to the user rather than swallowed. + static Future _openUrl(String url) async { try { - await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); - } catch (_) {} + return await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + } catch (_) { + return false; + } + } + + Future _openUrlWithFeedback(String url) async { + final ok = await _openUrl(url); + if (!ok && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Couldn't open the link.")), + ); + } } @override @@ -65,14 +80,14 @@ class _AboutScreenState extends State { icon: OsIcon.activity, title: 'Privacy policy', divider: true, - onTap: () => _openUrl(kPrivacyPolicyUrl), + onTap: () => _openUrlWithFeedback(kPrivacyPolicyUrl), ), ListRow( icon: OsIcon.activity, title: 'Notice & attribution', subtitle: 'Trademark + reverse-engineering disclosure', divider: true, - onTap: () => _openUrl(kNoticeUrl), + onTap: () => _openUrlWithFeedback(kNoticeUrl), ), ListRow( icon: OsIcon.activity, From 46da0fc583d366cf767a0e8ddb35b5eba7c99b33 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sun, 19 Jul 2026 00:10:28 +0530 Subject: [PATCH 06/14] PRIVACY.md: finalize placeholders, document health data contribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add a real disclosure for the "Contribute my health data" upload (HealthUploader): full local DB, gzipped, Wi-Fi+charging gated, once/24h max, latest-only retention (no restore/backup capability). - Extend the same anonymous-install-ID reasoning already used for telemetry to health data contributions in "Your controls" — same deviceId mechanism, same inability to service individual deletion requests. - Fill in the last-updated date and contact email; remove the attorney-review-draft banner now that the placeholders are resolved. --- PRIVACY.md | 48 +++++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 132635ed..15cac4df 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,16 +1,6 @@ -> **ATTORNEY-REVIEW DRAFT — not final.** This has not been reviewed or approved -> by a licensed attorney and must not be treated as the live policy until it -> has been. `[DATE]`, `[CONTACT EMAIL]`, and the data-deletion mechanism below -> are placeholders a human must fill in before this goes live. This is the -> repo copy for reference; the live, store-linked policy is hosted at -> https://openstrap.github.io/edge/privacy.html (GitHub Pages, `docs/` -> folder on the `docs/legal-site` branch) and is the version that should be -> kept current — `lib/ui/profile/about_screen.dart`'s `kPrivacyPolicyUrl` -> already points at it. - # Privacy Policy — Edge / OpenStrap -_Last updated: [DATE]_ +_Last updated: July 18, 2026_ Edge ("the App") is an independent, open-source project. It is not affiliated with, sponsored by, or endorsed by WHOOP, Inc. @@ -25,8 +15,14 @@ with, sponsored by, or endorsed by WHOOP, Inc. **Where your data lives** By default, all of the above is stored locally on your device only. We do not upload it anywhere unless you explicitly opt in to one of the following: -- **Cloud backup** — a one-time or periodic encrypted upload of your local - database to our own backend, only if you turn this on in Settings. +- **Health data contribution** — if you enable "Contribute my health data" in + Settings, the App periodically uploads a full copy of your on-device + database (the same biometric/health data listed above) to our backend, to + help improve the algorithms. This only happens while your phone is on Wi-Fi + and charging, at most once every 24 hours. We keep only your most recent + upload — each new one replaces the last — and there is no restore/backup + feature; it is a one-way contribution, not a backup you can recover from. + Off by default. - **AI Coach** — if you enable this feature and supply your own API key, summaries of your data are sent to the AI provider you configure (by default, OpenAI) to generate coaching responses. This is off by default and @@ -41,9 +37,27 @@ any advertising network. We do not require a WHOOP account or credentials to use the App. **Your controls** -You can disable cloud backup, telemetry, or AI Coach at any time in Settings. -[Describe how a user deletes their local data / requests deletion of any -uploaded backup — INSERT MECHANISM]. +You can disable telemetry, health data contribution, or AI Coach at any time +in Settings — this stops any further collection or upload immediately. + +*Local data:* everything the App stores lives in the App's own private storage +on your device. Uninstalling the App deletes all of it immediately; nothing +else needs to happen, since nothing left your device in the first place unless +you separately opted in to one of the uploads above. + +*Previously-sent anonymous diagnostics and health data contributions:* both of +these are submitted under a randomly-generated install identifier that is +created on your device, is never linked to your name, email, or any account, +and is not shown anywhere in the App. Because we have no way to verify that a +deletion request actually corresponds to a particular person's submissions, we +are not able to fulfill individual requests to delete previously-sent +telemetry or a previously-uploaded health data contribution. Turning off the +"Send anonymous diagnostics" or "Contribute my health data" toggle in Settings +stops any further collection/upload immediately — it does not retroactively +remove what was already sent. (For the health data contribution specifically: +since only your most recent upload is retained and each new one replaces it, +turning the toggle off also means no further upload will ever replace that +last one.) **Children** This App is not directed to children under 13 (or the relevant age of digital @@ -54,4 +68,4 @@ We may update this policy; material changes will be reflected here with an updated date. **Contact** -Questions about this policy: [CONTACT EMAIL]. +Questions about this policy: abdulsaheel81@gmail.com. From 1d9f4150a99fc5ed6078b5b6aca206e60c40ccfd Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sun, 19 Jul 2026 00:39:43 +0530 Subject: [PATCH 07/14] Rewrite privacy/terms: no data collection, Firebase-only diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We do not operate a backend that receives user health data, on any distribution channel. The only thing that leaves the device is optional, on-by-default anonymous crash/performance monitoring via Firebase (Crashlytics/Performance/Analytics) — governed by Firebase's own privacy and security practices, linked directly, rather than us documenting a bespoke backend's security posture. Removes the "Cloud backup" feature description (privacy.html + terms.html) since no such backend exists or is planned; AI Coach and Health app integration remain unchanged as the only user-initiated, user-configured integrations. --- docs/privacy.html | 56 +++++++++++++++++++++++++---------------------- docs/terms.html | 7 ++++-- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/docs/privacy.html b/docs/privacy.html index c4791acb..f07bfa38 100644 --- a/docs/privacy.html +++ b/docs/privacy.html @@ -26,26 +26,34 @@

Privacy Policy — Edge / OpenStrap

Edge ("the App") is an independent, open-source project. It is not affiliated with, sponsored by, or endorsed by WHOOP, Inc.

-

Data we collect

-
    -
  • Biometric/health data from your paired WHOOP 4.0 band over Bluetooth: heart - rate, heart rate variability (RR intervals), motion (accelerometer), raw - skin-temperature/blood-oxygen sensor channels, and (if you record workouts) GPS - location during those workouts.
  • -
  • Basic app usage/crash telemetry, only if you opt in.
  • -
+

We do not collect your data

+

Your biometric/health data from your paired WHOOP 4.0 band — heart rate, heart + rate variability (RR intervals), motion (accelerometer), raw + skin-temperature/blood-oxygen sensor channels, and (if you record workouts) GPS + location during those workouts — is processed and stored entirely on your + device. We do not upload it, we do not operate a backend that receives it, and + we never see it.

+ +

Anonymous diagnostics (on by default)

+

The only thing the App sends off your device is basic crash/error and + performance monitoring, via Firebase (Google) — Crashlytics, Performance + Monitoring, and Analytics. This is on by default; you can turn it off at any + time in Settings ("Send anonymous diagnostics"), which stops any further + collection immediately. It never includes your health data — only crash + reports, basic device info (OS/model/app version), and coarse performance + timing. This data is handled under Firebase's own privacy and security + practices, not a system we built or operate ourselves — see Google's Firebase + privacy & security documentation: + firebase.google.com/support/privacy.

-

Where your data lives

-

By default, all of the above is stored locally on your device only. We do not - upload it anywhere unless you explicitly opt in to one of the following:

+

Optional, user-initiated integrations

+

If you choose to enable them, the App can also send data to services + you configure:

    -
  • Cloud backup — a one-time or periodic encrypted upload of - your local database to our own backend, only if you turn this on in - Settings.
  • AI Coach — if you enable this feature and supply your own API key, summaries of your data are sent to the AI provider you configure (by - default, OpenAI) to generate coaching responses. This is off by default and - requires your own API key.
  • + default, OpenAI) to generate coaching responses. Off by default and requires + your own API key.
  • Health app integration — if you enable it, the App can write derived daily metrics to Apple Health or Google Health Connect, which are controlled by your device's own OS-level health app, not by us.
  • @@ -54,18 +62,14 @@

    Where your data lives

    What we don't do

    We do not sell your data. We do not send your health data to WHOOP, Inc. or any advertising network. We do not require a WHOOP account or credentials to use the - App.

    + App. We do not operate a backend that stores your health data.

    Your controls

    -

    You can disable cloud backup, telemetry, or AI Coach at any time in Settings. - Uninstalling the App deletes all of its local data from your device — there is - no separate local-data export left behind. In-app, Settings → "Reset profile" - clears your local profile (name, age, body metrics) and forgets your paired - band, but does not delete your previously stored band data on the device; full - local deletion currently requires uninstalling the App. If you opted in to - cloud backup, we do not yet have a self-serve deletion flow for that uploaded - copy — email us at abdulsaheel81@gmail.com to - request its deletion and we will process it manually.

    +

    Turn off "Send anonymous diagnostics" at any time in Settings — this stops any + further collection immediately. Uninstalling the App deletes all of your + locally stored data immediately, since nothing else is retained anywhere except + the anonymous Firebase diagnostics described above (governed by Firebase's own + practices, linked above, not a system we operate).

    Children

    This App is not directed to children under 13 (or the relevant age of digital diff --git a/docs/terms.html b/docs/terms.html index be56a716..9d548cc4 100644 --- a/docs/terms.html +++ b/docs/terms.html @@ -62,10 +62,13 @@

    5. Your Responsibilities

6. Third-Party Services

-

Certain optional features rely on third-party services you separately +

The App uses Firebase (Google) for anonymous crash/error and performance + monitoring (Crashlytics, Performance Monitoring, Analytics). This is on by + default and can be turned off at any time in Settings — see the Privacy + Policy for details. It never includes your health data.

+

Certain other, optional features rely on third-party services you separately configure or consent to, including:

    -
  • An optional cloud backup feature, self-hosted by the App's maintainers.
  • An optional AI Coach feature, which sends data you choose to share to a third-party AI provider (by default, OpenAI) using an API key you supply.
  • From 54d51d9587811f46bfd4d02bdf6e8071aab93302 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sun, 19 Jul 2026 00:40:57 +0530 Subject: [PATCH 08/14] No data collection: gate health-data upload off, Firebase-only diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add kHealthDataContributionEnabled (defaults OFF, same convention as kSideloadOtaEnabled): compile-time gate for the whole health-data- contribution feature (HealthUploader's full on-device DB upload + the "Contribute my health data" toggle in Settings and onboarding). Hidden, not just disabled, when off. CI no longer opts any distribution channel into it — we do not collect/receive user health data anywhere. - Rewrite PRIVACY.md: no bespoke backend collects health data; the only thing that leaves the device is optional, on-by-default anonymous diagnostics via Firebase (Crashlytics/Performance/Analytics), governed by Firebase's own published privacy/security practices (linked directly) rather than us documenting a backend security posture we don't have. Drops the now-inapplicable anonymous-vs-pseudonymous/deletion-mechanism discussion that only applied to the removed upload path. - Matching rewrite pushed to docs/legal-site (privacy.html + terms.html — the actual hosted, store-linked copies) in a separate commit there, since GitHub Pages serves from that branch, not this one. --- .github/workflows/build.yml | 10 ++++ PRIVACY.md | 70 ++++++++++++---------------- lib/telemetry/health_uploader.dart | 25 ++++++++++ lib/ui/profile/profile_screen.dart | 30 +++++++----- lib/ui/profile_setup_screen.dart | 41 +++++++++++----- test/flow_screens_redesign_test.dart | 13 +++++- 6 files changed, 123 insertions(+), 66 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf3be87d..ea8158aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,11 @@ jobs: run: | echo "COMPANION_URL=${{ secrets.COMPANION_URL }}" > .env echo "BACKEND_URL=${{ secrets.BACKEND_URL }}" >> .env + # Health-data contribution (full on-device DB upload) stays OFF + # here too (see kHealthDataContributionEnabled, defaults false) — + # we don't collect/receive user health data on ANY distribution + # channel, sideload included, matching PRIVACY.md's "we do not + # collect your data" — not just the future store build. # Decode the release keystore from a secret. The gradle config picks these # env vars up (ANDROID_KEYSTORE_PATH etc.) and signs the release build. @@ -127,6 +132,11 @@ jobs: run: | echo "COMPANION_URL=${{ secrets.COMPANION_URL }}" > .env echo "BACKEND_URL=${{ secrets.BACKEND_URL }}" >> .env + # Health-data contribution (full on-device DB upload) stays OFF + # here too (see kHealthDataContributionEnabled, defaults false) — + # we don't collect/receive user health data on ANY distribution + # channel, sideload included, matching PRIVACY.md's "we do not + # collect your data" — not just the future store build. - name: Install flutterfire (required by Crashlytics Xcode build phase) run: dart pub global activate flutterfire_cli 1.4.0 diff --git a/PRIVACY.md b/PRIVACY.md index 15cac4df..5e90963d 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -5,27 +5,31 @@ _Last updated: July 18, 2026_ Edge ("the App") is an independent, open-source project. It is not affiliated with, sponsored by, or endorsed by WHOOP, Inc. -**Data we collect** -- Biometric/health data from your paired WHOOP 4.0 band over Bluetooth: heart - rate, heart rate variability (RR intervals), motion (accelerometer), raw - skin-temperature/blood-oxygen sensor channels, and (if you record workouts) - GPS location during those workouts. -- Basic app usage/crash telemetry, only if you opt in. - -**Where your data lives** -By default, all of the above is stored locally on your device only. We do not -upload it anywhere unless you explicitly opt in to one of the following: -- **Health data contribution** — if you enable "Contribute my health data" in - Settings, the App periodically uploads a full copy of your on-device - database (the same biometric/health data listed above) to our backend, to - help improve the algorithms. This only happens while your phone is on Wi-Fi - and charging, at most once every 24 hours. We keep only your most recent - upload — each new one replaces the last — and there is no restore/backup - feature; it is a one-way contribution, not a backup you can recover from. - Off by default. +**We do not collect your data** +Your biometric/health data from your paired WHOOP 4.0 band — heart rate, heart +rate variability (RR intervals), motion (accelerometer), raw +skin-temperature/blood-oxygen sensor channels, and (if you record workouts) +GPS location during those workouts — is processed and stored entirely on your +device. We do not upload it, we do not operate a backend that receives it, and +we never see it. + +**Anonymous diagnostics (on by default)** +The only thing the App sends off your device is basic crash/error and +performance monitoring, via Firebase (Google) — Crashlytics, Performance +Monitoring, and Analytics. This is on by default; you can turn it off at any +time in Settings ("Send anonymous diagnostics"), which stops any further +collection immediately. It never includes your health data — only crash +reports, basic device info (OS/model/app version), and coarse performance +timing. This data is handled under Firebase's own privacy and security +practices, not a system we built or operate ourselves — see Google's Firebase +privacy & security documentation: https://firebase.google.com/support/privacy. + +**Optional, user-initiated integrations** +If you choose to enable them, the App can also send data to services *you* +configure: - **AI Coach** — if you enable this feature and supply your own API key, summaries of your data are sent to the AI provider you configure (by - default, OpenAI) to generate coaching responses. This is off by default and + default, OpenAI) to generate coaching responses. Off by default and requires your own API key. - **Health app integration** — if you enable it, the App can write derived daily metrics to Apple Health or Google Health Connect, which are controlled @@ -34,30 +38,14 @@ upload it anywhere unless you explicitly opt in to one of the following: **What we don't do** We do not sell your data. We do not send your health data to WHOOP, Inc. or any advertising network. We do not require a WHOOP account or credentials to -use the App. +use the App. We do not operate a backend that stores your health data. **Your controls** -You can disable telemetry, health data contribution, or AI Coach at any time -in Settings — this stops any further collection or upload immediately. - -*Local data:* everything the App stores lives in the App's own private storage -on your device. Uninstalling the App deletes all of it immediately; nothing -else needs to happen, since nothing left your device in the first place unless -you separately opted in to one of the uploads above. - -*Previously-sent anonymous diagnostics and health data contributions:* both of -these are submitted under a randomly-generated install identifier that is -created on your device, is never linked to your name, email, or any account, -and is not shown anywhere in the App. Because we have no way to verify that a -deletion request actually corresponds to a particular person's submissions, we -are not able to fulfill individual requests to delete previously-sent -telemetry or a previously-uploaded health data contribution. Turning off the -"Send anonymous diagnostics" or "Contribute my health data" toggle in Settings -stops any further collection/upload immediately — it does not retroactively -remove what was already sent. (For the health data contribution specifically: -since only your most recent upload is retained and each new one replaces it, -turning the toggle off also means no further upload will ever replace that -last one.) +Turn off "Send anonymous diagnostics" at any time in Settings — this stops any +further collection immediately. Uninstalling the App deletes all of your +locally stored data immediately, since nothing else is retained anywhere +except the anonymous Firebase diagnostics described above (governed by +Firebase's own practices, linked above, not a system we operate). **Children** This App is not directed to children under 13 (or the relevant age of digital diff --git a/lib/telemetry/health_uploader.dart b/lib/telemetry/health_uploader.dart index dd28972f..00710a01 100644 --- a/lib/telemetry/health_uploader.dart +++ b/lib/telemetry/health_uploader.dart @@ -16,6 +16,24 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../cloud/companion_client.dart'; import '../data/db.dart'; +/// Compile-time gate for the whole health-data-contribution feature (this +/// upload path + the "Contribute my health data" toggle in Settings and +/// onboarding). Defaults OFF, same convention as [kSideloadOtaEnabled] in +/// update_service.dart — the open-source repo, and any App Store/Play Store +/// submission built from it without extra flags, never offers uploading a +/// full copy of the local health database anywhere. Uploading someone's +/// entire raw + derived health history to a self-hosted backend is by far +/// the biggest privacy/compliance surface this app has; store-review-facing +/// builds should not carry it at all, only Firebase-based crash/diagnostics +/// telemetry (gated separately by the user's telemetry consent). +/// +/// A direct-distribution/sideload build opts in explicitly: +/// flutter build apk --dart-define=ENABLE_HEALTH_DATA_CONTRIBUTION=true +const bool kHealthDataContributionEnabled = bool.fromEnvironment( + 'ENABLE_HEALTH_DATA_CONTRIBUTION', + defaultValue: false, +); + class HealthUploader { HealthUploader._(); static final HealthUploader instance = HealthUploader._(); @@ -32,7 +50,14 @@ class HealthUploader { /// Attempt an upload if [consented] AND on Wi-Fi AND charging AND >24h since the /// last one. Returns true if an upload actually happened. + /// + /// Defense in depth: even though the Settings/onboarding toggle that sets + /// [consented] is itself hidden when [kHealthDataContributionEnabled] is + /// false, gate the actual network call on the same flag directly — no + /// build without the flag should ever attempt this upload regardless of + /// how `consented` ended up true (e.g. a stale local pref). Future maybeUpload({required bool consented}) async { + if (!kHealthDataContributionEnabled) return false; if (!consented || !CompanionClient.configured || _running) return false; if (deviceId == null) return false; _running = true; diff --git a/lib/ui/profile/profile_screen.dart b/lib/ui/profile/profile_screen.dart index 6dfe330c..a6343000 100644 --- a/lib/ui/profile/profile_screen.dart +++ b/lib/ui/profile/profile_screen.dart @@ -19,6 +19,7 @@ import '../../health/health_export.dart'; import '../../state/app_state.dart'; import '../../state/units_controller.dart'; import '../../debug/debug_mode.dart'; +import '../../telemetry/health_uploader.dart' show kHealthDataContributionEnabled; import '../../theme/theme_switcher.dart'; import '../ai/ai_settings_screen.dart'; import '../design/design.dart'; @@ -324,17 +325,24 @@ class ProfileScreen extends StatelessWidget { value: app.telemetryConsent, onChanged: (v) => app.setTelemetryConsent(v), ), - const SizedBox(height: Sp.x3), - _ToggleCard( - icon: OsIcon.activity, - title: 'Contribute my health data', - subtitle: - 'Periodically upload your on-device database (over Wi-Fi, ' - 'while charging) to improve the algorithms. On by default — ' - 'switch off anytime.', - value: app.healthShareConsent, - onChanged: (v) => app.setHealthShareConsent(v), - ), + // Health-data contribution (full on-device DB upload) is a + // compile-time-gated feature (see kHealthDataContributionEnabled) — + // never offered in a store-review-facing build, only in a + // sideload/direct-distribution build that opted in. Hide the + // toggle entirely rather than leave it visible-but-inert. + if (kHealthDataContributionEnabled) ...[ + const SizedBox(height: Sp.x3), + _ToggleCard( + icon: OsIcon.activity, + title: 'Contribute my health data', + subtitle: + 'Periodically upload your on-device database (over Wi-Fi, ' + 'while charging) to improve the algorithms. On by default — ' + 'switch off anytime.', + value: app.healthShareConsent, + onChanged: (v) => app.setHealthShareConsent(v), + ), + ], const SizedBox(height: Sp.x6), diff --git a/lib/ui/profile_setup_screen.dart b/lib/ui/profile_setup_screen.dart index 1037099e..105a4025 100644 --- a/lib/ui/profile_setup_screen.dart +++ b/lib/ui/profile_setup_screen.dart @@ -16,6 +16,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../state/app_state.dart'; +import '../telemetry/health_uploader.dart' show kHealthDataContributionEnabled; import 'design/design.dart'; class ProfileSetupScreen extends StatefulWidget { @@ -30,7 +31,10 @@ class _ProfileSetupScreenState extends State { // either off here). Recorded + sent on Continue. When re-editing an existing // profile we reflect the saved choice instead. bool _telemetry = true; - bool _healthShare = true; + // Health-data contribution is a compile-time-gated feature (see + // kHealthDataContributionEnabled) — never pre-enabled (or even offered) in + // a build that doesn't carry it. + bool _healthShare = kHealthDataContributionEnabled; @override void initState() { @@ -43,6 +47,10 @@ class _ProfileSetupScreenState extends State { _telemetry = app.telemetryConsent; _healthShare = app.healthShareConsent; } + // Never true in a build without the feature — even a stale saved `true` + // (e.g. carried over from a different build's local prefs) must not + // resubmit consent for an upload this build will never attempt. + if (!kHealthDataContributionEnabled) _healthShare = false; } Future _submit( @@ -84,7 +92,9 @@ class ProfileSetupForm extends StatefulWidget { super.key, this.initial = const {}, this.telemetryInitial = true, - this.healthShareInitial = true, + // Gated default: never pre-enabled (or shown) in a build compiled + // without kHealthDataContributionEnabled. + this.healthShareInitial = kHealthDataContributionEnabled, required this.onSubmit, }); @@ -207,18 +217,25 @@ class _ProfileSetupFormState extends State { value: _telemetry, onChanged: (v) => setState(() => _telemetry = v), ), - const SizedBox(height: Sp.x3), - ConsentTile( - title: 'Contribute my health data', - subtitle: - 'Uploads your on-device database over Wi-Fi while charging, to ' - 'improve the algorithms.', - value: _healthShare, - onChanged: (v) => setState(() => _healthShare = v), - ), + // Health-data contribution (full on-device DB upload) is a + // compile-time-gated feature (see kHealthDataContributionEnabled) — + // never offered in a store-review-facing build. + if (kHealthDataContributionEnabled) ...[ + const SizedBox(height: Sp.x3), + ConsentTile( + title: 'Contribute my health data', + subtitle: + 'Uploads your on-device database over Wi-Fi while charging, ' + 'to improve the algorithms.', + value: _healthShare, + onChanged: (v) => setState(() => _healthShare = v), + ), + ], const SizedBox(height: Sp.x2), Text( - 'Switch either off here or anytime in Settings.', + kHealthDataContributionEnabled + ? 'Switch either off here or anytime in Settings.' + : 'Switch it off here or anytime in Settings.', style: AppText.caption.copyWith(color: AppColors.inkMuted), ), const SizedBox(height: Sp.x7), diff --git a/test/flow_screens_redesign_test.dart b/test/flow_screens_redesign_test.dart index f4ca135f..9c0ebf08 100644 --- a/test/flow_screens_redesign_test.dart +++ b/test/flow_screens_redesign_test.dart @@ -14,6 +14,8 @@ import 'package:openstrap_edge/ui/onboarding/welcome_screen.dart' show WelcomeHero, WelcomeOptionCard; import 'package:openstrap_edge/ui/pairing_screen.dart' show PairPhase, PairingInstructionContent, PairingStateView; +import 'package:openstrap_edge/telemetry/health_uploader.dart' + show kHealthDataContributionEnabled; import 'package:openstrap_edge/ui/profile/profile_screen.dart' show DeviceTile; import 'package:openstrap_edge/ui/profile_setup_screen.dart' show ConsentTile, ProfileSetupForm; @@ -166,7 +168,12 @@ void main() { expect(find.text('About you'), findsOneWidget); expect(find.text('Male'), findsOneWidget); expect(find.text('Female'), findsOneWidget); - expect(find.byType(ConsentTile), findsNWidgets(2)); + // The health-data-contribution tile only renders in a build compiled + // with kHealthDataContributionEnabled (never in CI/default test runs). + expect( + find.byType(ConsentTile), + findsNWidgets(kHealthDataContributionEnabled ? 2 : 1), + ); // No sex chosen yet → Continue disabled (onSubmit not called on tap). await t.ensureVisible(find.text('Continue')); @@ -186,7 +193,9 @@ void main() { expect(submitted!['age'], 30); expect(submitted!['sex'], 'f'); expect(telemetry, isTrue); - expect(healthShare, isTrue); + // Gated default: only pre-enabled in a build compiled with + // kHealthDataContributionEnabled. + expect(healthShare, kHealthDataContributionEnabled); }); } From 2f156e9c696b8fad6136f3ca4295cbe5e8ff1888 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sun, 19 Jul 2026 00:49:50 +0530 Subject: [PATCH 09/14] Scope the "no data collection" claim to builds we actually distribute An unqualified "we do not collect your data" is an overbroad claim for open-source code: the flag that gates health-data upload exists in source and could be enabled by an independent developer's own separately-built copy. Scope the claim explicitly to every build we ourselves publish (GitHub Releases APK/IPA, any future store submission) and disclose the capability + who's responsible for a self-built variant that enables it. Same scoping applied to the Firebase-only diagnostics claim. --- docs/privacy.html | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/privacy.html b/docs/privacy.html index f07bfa38..abf1d9bb 100644 --- a/docs/privacy.html +++ b/docs/privacy.html @@ -27,19 +27,32 @@

    Privacy Policy — Edge / OpenStrap

    with, sponsored by, or endorsed by WHOOP, Inc.

    We do not collect your data

    -

    Your biometric/health data from your paired WHOOP 4.0 band — heart rate, heart - rate variability (RR intervals), motion (accelerometer), raw - skin-temperature/blood-oxygen sensor channels, and (if you record workouts) GPS - location during those workouts — is processed and stored entirely on your - device. We do not upload it, we do not operate a backend that receives it, and - we never see it.

    +

    This section describes every build we ourselves distribute — the signed + Android APK and iOS IPA published on GitHub Releases, and any future App + Store/Play Store submission. Your biometric/health data from your paired + WHOOP 4.0 band — heart rate, heart rate variability (RR intervals), motion + (accelerometer), raw skin-temperature/blood-oxygen sensor channels, and (if + you record workouts) GPS location during those workouts — is processed and + stored entirely on your device in every one of those builds. We do not + upload it, we do not operate a backend that receives it, and we never see + it.

    + +

    Edge is open source. The underlying code contains an off-by-default, + compile-time flag (kHealthDataContributionEnabled) that an + independent developer could enable in their own, separately-built + and separately-distributed copy of the app, pointed at a backend of their + own choosing. No build we publish enables it — our CI explicitly forces it + off for every release rather than relying on the flag's default. A + self-built copy compiled with that flag on is that builder's own software + and their own responsibility; it is not covered by this policy.

    Anonymous diagnostics (on by default)

    -

    The only thing the App sends off your device is basic crash/error and - performance monitoring, via Firebase (Google) — Crashlytics, Performance - Monitoring, and Analytics. This is on by default; you can turn it off at any - time in Settings ("Send anonymous diagnostics"), which stops any further - collection immediately. It never includes your health data — only crash +

    In every build we distribute, the only thing the App sends off your device + is basic crash/error and performance monitoring, via Firebase (Google) — + Crashlytics, Performance Monitoring, and Analytics. This is on by default; + you can turn it off at any time in Settings ("Send anonymous diagnostics"), + which stops any further collection immediately. It never includes your + health data — only crash reports, basic device info (OS/model/app version), and coarse performance timing. This data is handled under Firebase's own privacy and security practices, not a system we built or operate ourselves — see Google's Firebase From 7c074fe59369a119fa6f63c3ddc6485b9c5a6226 Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sun, 19 Jul 2026 00:50:35 +0530 Subject: [PATCH 10/14] Address CodeRabbit: make the off-by-default health-data gate explicit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CI now explicitly passes ENABLE_HEALTH_DATA_CONTRIBUTION=false for both the Android and iOS release builds, instead of relying on kHealthDataContributionEnabled's Dart-side default — true by construction of the CI config, not contingent on the default never changing later. - Updated the flag's doc comment: no build we distribute opts in anymore (the sideload channel no longer does either); it exists solely for an independent developer's own separately-built copy. - PRIVACY.md: scoped the "we do not collect your data" and "only Firebase" claims explicitly to builds we ourselves distribute, and disclosed the open-source capability + who's responsible if someone else compiles their own build with it enabled. - Matching scoping pushed to docs/legal-site's privacy.html in a separate commit there. --- .github/workflows/build.yml | 24 +++++++++++++---------- PRIVACY.md | 31 +++++++++++++++++++++--------- lib/telemetry/health_uploader.dart | 22 +++++++++++++-------- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea8158aa..ea0a240a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,11 +36,13 @@ jobs: run: | echo "COMPANION_URL=${{ secrets.COMPANION_URL }}" > .env echo "BACKEND_URL=${{ secrets.BACKEND_URL }}" >> .env - # Health-data contribution (full on-device DB upload) stays OFF - # here too (see kHealthDataContributionEnabled, defaults false) — - # we don't collect/receive user health data on ANY distribution - # channel, sideload included, matching PRIVACY.md's "we do not - # collect your data" — not just the future store build. + # Health-data contribution (full on-device DB upload) is OFF for + # every build we distribute, sideload included — matching + # PRIVACY.md's "we do not collect your data". Passed explicitly + # (not just relying on kHealthDataContributionEnabled's default) + # so this is true by construction of the CI config itself, not + # contingent on nobody ever changing that default later. + echo "ENABLE_HEALTH_DATA_CONTRIBUTION=false" >> .env # Decode the release keystore from a secret. The gradle config picks these # env vars up (ANDROID_KEYSTORE_PATH etc.) and signs the release build. @@ -132,11 +134,13 @@ jobs: run: | echo "COMPANION_URL=${{ secrets.COMPANION_URL }}" > .env echo "BACKEND_URL=${{ secrets.BACKEND_URL }}" >> .env - # Health-data contribution (full on-device DB upload) stays OFF - # here too (see kHealthDataContributionEnabled, defaults false) — - # we don't collect/receive user health data on ANY distribution - # channel, sideload included, matching PRIVACY.md's "we do not - # collect your data" — not just the future store build. + # Health-data contribution (full on-device DB upload) is OFF for + # every build we distribute, sideload included — matching + # PRIVACY.md's "we do not collect your data". Passed explicitly + # (not just relying on kHealthDataContributionEnabled's default) + # so this is true by construction of the CI config itself, not + # contingent on nobody ever changing that default later. + echo "ENABLE_HEALTH_DATA_CONTRIBUTION=false" >> .env - name: Install flutterfire (required by Crashlytics Xcode build phase) run: dart pub global activate flutterfire_cli 1.4.0 diff --git a/PRIVACY.md b/PRIVACY.md index 5e90963d..80dcf91a 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -6,17 +6,30 @@ Edge ("the App") is an independent, open-source project. It is not affiliated with, sponsored by, or endorsed by WHOOP, Inc. **We do not collect your data** -Your biometric/health data from your paired WHOOP 4.0 band — heart rate, heart -rate variability (RR intervals), motion (accelerometer), raw -skin-temperature/blood-oxygen sensor channels, and (if you record workouts) -GPS location during those workouts — is processed and stored entirely on your -device. We do not upload it, we do not operate a backend that receives it, and -we never see it. +This section describes every build we ourselves distribute — the signed +Android APK and iOS IPA published on GitHub Releases, and any future App +Store/Play Store submission. Your biometric/health data from your paired +WHOOP 4.0 band — heart rate, heart rate variability (RR intervals), motion +(accelerometer), raw skin-temperature/blood-oxygen sensor channels, and (if +you record workouts) GPS location during those workouts — is processed and +stored entirely on your device in every one of those builds. We do not +upload it, we do not operate a backend that receives it, and we never see it. + +Edge is open source. The underlying code contains an off-by-default, +compile-time flag (`kHealthDataContributionEnabled`, see +`lib/telemetry/health_uploader.dart`) that an independent developer could +enable in their *own*, separately-built and separately-distributed copy of +the app, pointed at a backend of their own choosing. No build we publish +enables it — our CI explicitly forces it off for every release (see +`.github/workflows/build.yml`) rather than relying on the flag's default. A +self-built copy compiled with that flag on is that builder's own software +and their own responsibility; it is not covered by this policy. **Anonymous diagnostics (on by default)** -The only thing the App sends off your device is basic crash/error and -performance monitoring, via Firebase (Google) — Crashlytics, Performance -Monitoring, and Analytics. This is on by default; you can turn it off at any +In every build we distribute, the only thing the App sends off your device is +basic crash/error and performance monitoring, via Firebase (Google) — +Crashlytics, Performance Monitoring, and Analytics. This is on by default; you +can turn it off at any time in Settings ("Send anonymous diagnostics"), which stops any further collection immediately. It never includes your health data — only crash reports, basic device info (OS/model/app version), and coarse performance diff --git a/lib/telemetry/health_uploader.dart b/lib/telemetry/health_uploader.dart index 00710a01..89aca362 100644 --- a/lib/telemetry/health_uploader.dart +++ b/lib/telemetry/health_uploader.dart @@ -19,15 +19,21 @@ import '../data/db.dart'; /// Compile-time gate for the whole health-data-contribution feature (this /// upload path + the "Contribute my health data" toggle in Settings and /// onboarding). Defaults OFF, same convention as [kSideloadOtaEnabled] in -/// update_service.dart — the open-source repo, and any App Store/Play Store -/// submission built from it without extra flags, never offers uploading a -/// full copy of the local health database anywhere. Uploading someone's -/// entire raw + derived health history to a self-hosted backend is by far -/// the biggest privacy/compliance surface this app has; store-review-facing -/// builds should not carry it at all, only Firebase-based crash/diagnostics -/// telemetry (gated separately by the user's telemetry consent). +/// update_service.dart. /// -/// A direct-distribution/sideload build opts in explicitly: +/// NOT enabled by ANY build we distribute — not the GitHub Releases sideload +/// APK/IPA, and not any future App Store/Play Store submission (CI passes +/// `ENABLE_HEALTH_DATA_CONTRIBUTION=false` explicitly for both; see +/// .github/workflows/build.yml). Uploading someone's entire raw + derived +/// health history to a backend is by far the biggest privacy/compliance +/// surface this app could have, so PRIVACY.md's "we do not collect your +/// data" claim is scoped to — and must remain true of — every build we +/// ourselves ship. +/// +/// This flag exists purely because the code is open source: an independent +/// developer compiling their OWN build from this repo can opt in and point +/// it at a backend of their own choosing. That is their build and their +/// responsibility, not something our privacy policy governs. /// flutter build apk --dart-define=ENABLE_HEALTH_DATA_CONTRIBUTION=true const bool kHealthDataContributionEnabled = bool.fromEnvironment( 'ENABLE_HEALTH_DATA_CONTRIBUTION', From 6f773592a74a73076006d997094ee1f2a5a01fcb Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sun, 19 Jul 2026 07:44:35 +0530 Subject: [PATCH 11/14] Carve out AI Coach / Health app integration from the absolute claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "We do not collect your data", the Firebase-only diagnostics claim, and "nothing else is retained" all read as unqualified, but AI Coach and Health app integration do send data off-device when a user opts into them. Add explicit carve-outs, and clarify that uninstalling only removes app-local data — third-party retention/deletion for those two opt-in integrations follows each service's own policies, not ours. --- docs/privacy.html | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/privacy.html b/docs/privacy.html index abf1d9bb..2196718c 100644 --- a/docs/privacy.html +++ b/docs/privacy.html @@ -35,7 +35,9 @@

    We do not collect your data

    you record workouts) GPS location during those workouts — is processed and stored entirely on your device in every one of those builds. We do not upload it, we do not operate a backend that receives it, and we never see - it.

    + it — unless you separately choose to enable AI Coach or Health app + integration, described below, which send specific data to services you + configure.

    Edge is open source. The underlying code contains an off-by-default, compile-time flag (kHealthDataContributionEnabled) that an @@ -48,9 +50,11 @@

    We do not collect your data

    Anonymous diagnostics (on by default)

    In every build we distribute, the only thing the App sends off your device - is basic crash/error and performance monitoring, via Firebase (Google) — - Crashlytics, Performance Monitoring, and Analytics. This is on by default; - you can turn it off at any time in Settings ("Send anonymous diagnostics"), + automatically — aside from the optional, user-initiated + integrations described next — is basic crash/error and performance + monitoring, via Firebase (Google) — Crashlytics, Performance Monitoring, and + Analytics. This is on by default; you can turn it off at any time in + Settings ("Send anonymous diagnostics"), which stops any further collection immediately. It never includes your health data — only crash reports, basic device info (OS/model/app version), and coarse performance @@ -79,10 +83,24 @@

    What we don't do

    Your controls

    Turn off "Send anonymous diagnostics" at any time in Settings — this stops any - further collection immediately. Uninstalling the App deletes all of your - locally stored data immediately, since nothing else is retained anywhere except - the anonymous Firebase diagnostics described above (governed by Firebase's own - practices, linked above, not a system we operate).

    + further collection immediately. You can also disable AI Coach or Health app + integration at any time in Settings if you'd previously turned them on.

    + +

    Uninstalling the App deletes all of your locally stored data immediately. + That's the whole picture unless you had separately enabled one of + the optional integrations above — in that case, uninstalling stops the App + from sending anything further, but does not reach back and delete data + already sent:

    +
      +
    • Anonymous diagnostics already sent to Firebase are retained and + governed by Firebase's own practices (linked above), not by us.
    • +
    • Data already sent to your configured AI Coach provider (e.g. OpenAI) + is retained and governed by that provider's own policies, not by + us.
    • +
    • Metrics already written to Apple Health or Google Health Connect are + retained and governed by that platform's own data controls, not by us + — manage or delete them from that app directly.
    • +

    Children

    This App is not directed to children under 13 (or the relevant age of digital From 1c13e21a8ea6b67adf31a72587e59ce76b3a667e Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sun, 19 Jul 2026 07:44:57 +0530 Subject: [PATCH 12/14] Carve out AI Coach / Health app integration from the absolute claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "We do not collect your data", the Firebase-only diagnostics claim, and "nothing else is retained" all read as unqualified, but AI Coach and Health app integration do send data off-device when a user opts into them. Add explicit carve-outs, and clarify that uninstalling only removes app-local data — third-party retention/deletion for those two opt-in integrations follows each service's own policies, not ours. Matching fix pushed to docs/legal-site's privacy.html in a separate commit there. --- PRIVACY.md | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/PRIVACY.md b/PRIVACY.md index 80dcf91a..f1430749 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -13,7 +13,10 @@ WHOOP 4.0 band — heart rate, heart rate variability (RR intervals), motion (accelerometer), raw skin-temperature/blood-oxygen sensor channels, and (if you record workouts) GPS location during those workouts — is processed and stored entirely on your device in every one of those builds. We do not -upload it, we do not operate a backend that receives it, and we never see it. +upload it, we do not operate a backend that receives it, and we never see it +— *unless you separately choose to enable AI Coach or Health app +integration, described below, which send specific data to services you +configure.* Edge is open source. The underlying code contains an off-by-default, compile-time flag (`kHealthDataContributionEnabled`, see @@ -26,10 +29,11 @@ self-built copy compiled with that flag on is that builder's own software and their own responsibility; it is not covered by this policy. **Anonymous diagnostics (on by default)** -In every build we distribute, the only thing the App sends off your device is -basic crash/error and performance monitoring, via Firebase (Google) — -Crashlytics, Performance Monitoring, and Analytics. This is on by default; you -can turn it off at any +In every build we distribute, the only thing the App sends off your device +*automatically* — aside from the optional, user-initiated integrations +described next — is basic crash/error and performance monitoring, via +Firebase (Google) — Crashlytics, Performance Monitoring, and Analytics. This +is on by default; you can turn it off at any time in Settings ("Send anonymous diagnostics"), which stops any further collection immediately. It never includes your health data — only crash reports, basic device info (OS/model/app version), and coarse performance @@ -55,10 +59,21 @@ use the App. We do not operate a backend that stores your health data. **Your controls** Turn off "Send anonymous diagnostics" at any time in Settings — this stops any -further collection immediately. Uninstalling the App deletes all of your -locally stored data immediately, since nothing else is retained anywhere -except the anonymous Firebase diagnostics described above (governed by -Firebase's own practices, linked above, not a system we operate). +further collection immediately. You can also disable AI Coach or Health app +integration at any time in Settings if you'd previously turned them on. + +Uninstalling the App deletes all of your locally stored data immediately. +That's the whole picture *unless* you had separately enabled one of the +optional integrations above — in that case, uninstalling stops the App from +sending anything further, but does not reach back and delete data already +sent: +- Anonymous diagnostics already sent to Firebase are retained and governed by + Firebase's own practices (linked above), not by us. +- Data already sent to your configured AI Coach provider (e.g. OpenAI) is + retained and governed by that provider's own policies, not by us. +- Metrics already written to Apple Health or Google Health Connect are + retained and governed by that platform's own data controls, not by us — + manage or delete them from that app directly. **Children** This App is not directed to children under 13 (or the relevant age of digital From 1ec89b9f39c6bb40a5cbfcc32cfcaa875931143a Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sun, 19 Jul 2026 08:33:32 +0530 Subject: [PATCH 13/14] Bump kAlgoVersion 43 -> 44 for the partial-day + wake-steps fixes Out of caution: neither fix changes a scalar a previously-finalized day_result already had right, but bumping forces non-finalized days to re-derive with both fixes (partial-day pruning guard, corrected early-read wake steps/calories) in effect going forward. --- lib/compute/derivation_engine.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/compute/derivation_engine.dart b/lib/compute/derivation_engine.dart index c5b1dd70..c8549ac4 100644 --- a/lib/compute/derivation_engine.dart +++ b/lib/compute/derivation_engine.dart @@ -215,7 +215,23 @@ import 'substrate.dart'; // whole-bpm RHR / integer skin-temp ADC), which was intermittently blanking the // whole readiness score to "—" on nights that had valid sleep. Bump so days that // were previously absent-for-that-reason recompute a real score. -const int kAlgoVersion = 43; +// v44: two consistency fixes; neither changes a scalar that a previously +// FINALIZED day_result already had right, but both affect data availability/ +// consistency going forward. (1) A day whose offloaded second-half compute +// (naps/workouts/HRR/wear/curves/wake-features) failed or timed out — but +// whose headline scalars (readiness/RHR/RMSSD) already succeeded — could get +// marked finalized and treated as fully "derived" by the raw-pruning guard, +// permanently losing the raw substrate needed to ever fill in those missing +// fields on retry. Now tracked via a new `partial` day_result column and +// excluded from both age-based finalization and the pruning guard until the +// second half actually completes. (2) The wake_day_features early-read +// artifact (what the Today repo shows before the full day result is ready) +// was copying the pre-hybrid-correction 1Hz-only step/calorie estimate +// instead of the corrected real-100Hz+1Hz hybrid value computed moments +// later in the same pass — the final day_result was always correct, only +// this transient early read was stale. Bump so any day currently sitting +// non-finalized re-derives with both fixes in effect. +const int kAlgoVersion = 44; /// Raw is kept this many days past derivation, then pruned (derived stays). const int rawRetentionDays = 3; From c0df0616fc2ecd7839dc3cf5fd573a95290262fd Mon Sep 17 00:00:00 2001 From: abdulsaheel Date: Sun, 19 Jul 2026 08:37:56 +0530 Subject: [PATCH 14/14] iOS App Store prep: usage-description strings + real launch screen - Info.plist: add NSLocationAlwaysAndWhenInUseUsageDescription and NSPhotoLibraryUsageDescription. Neither permission is requested by app code (gps_source.dart is WhenInUse-only; import_screen.dart's FilePicker only ever uses FileType.any, never .image/.media) but geolocator_apple and file_picker's iOS podspecs unconditionally link the corresponding APIs, which trips App Store Connect's static binary scan (ITMS-90683) at submission without these strings present. - Launch screen: replace the literal 1x1 placeholder LaunchImage PNGs (1x/2x/3x) with a real 96x96 logo, and the hardcoded white background with a new LaunchBackground color asset (light/dark-mode aware). - project.pbxproj: cosmetic alphabetical reorder of two build-file entries only; the Crashlytics symbol-upload script's `|| true` (kept non-fatal so a Crashlytics upload hiccup never fails the build for a contributor without matching Firebase/signing setup) is unchanged. --- ios/Runner.xcodeproj/project.pbxproj | 4 +- .../LaunchBackground.colorset/Contents.json | 38 ++++++++++++++++++ .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 12359 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 43205 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 92976 bytes ios/Runner/Base.lproj/LaunchScreen.storyboard | 4 +- ios/Runner/Info.plist | 15 +++++++ 7 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 ios/Runner/Assets.xcassets/LaunchBackground.colorset/Contents.json diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a0bdb4b2..bee5ce53 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 5348973F2FDC19C80033A4D9 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5348973E2FDC19C80033A4D9 /* SwiftUI.framework */; }; 5348974E2FDC19C90033A4D9 /* OpenStrapWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5348973B2FDC19C80033A4D9 /* OpenStrapWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 534897A72FDC2B310033A4D9 /* LiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 534897A62FDC2B310033A4D9 /* LiveActivityBridge.swift */; }; - BEEF00000000000000000002 /* BreathingLiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEEF00000000000000000001 /* BreathingLiveActivityBridge.swift */; }; 53962E962FF6EE120061A61B /* WatchBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53962E952FF6EE120061A61B /* WatchBridge.swift */; }; 53962E972FF6EE120061A61B /* OpenStrapIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53962E942FF6EE120061A61B /* OpenStrapIntents.swift */; }; 630172CC9317145AD5F8F3B7 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C721EA0C8D31A3834E66203 /* Pods_RunnerTests.framework */; }; @@ -28,6 +27,7 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; ACCE55E7000000000000B11D /* AccessorySetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACCE55E7000000000000F11E /* AccessorySetup.swift */; }; + BEEF00000000000000000002 /* BreathingLiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEEF00000000000000000001 /* BreathingLiveActivityBridge.swift */; }; FADE0001FADE0001FADE0001 /* OpenStrapWatch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 53962EBE2FF6EF790061A61B /* OpenStrapWatch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -105,7 +105,6 @@ 534897562FDC1A450033A4D9 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 534897572FDC1A610033A4D9 /* OpenStrapWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenStrapWidgetExtension.entitlements; sourceTree = ""; }; 534897A62FDC2B310033A4D9 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = ""; }; - BEEF00000000000000000001 /* BreathingLiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreathingLiveActivityBridge.swift; sourceTree = ""; }; 53962E942FF6EE120061A61B /* OpenStrapIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenStrapIntents.swift; sourceTree = ""; }; 53962E952FF6EE120061A61B /* WatchBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchBridge.swift; sourceTree = ""; }; 53962EBE2FF6EF790061A61B /* OpenStrapWatch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "OpenStrapWatch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -128,6 +127,7 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; ACCE55E7000000000000F11E /* AccessorySetup.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccessorySetup.swift; sourceTree = ""; }; B0B95B9F07C8083A2DB92078 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + BEEF00000000000000000001 /* BreathingLiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BreathingLiveActivityBridge.swift; sourceTree = ""; }; DE1829A4D85DB1C3222C2CB5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; EC2DC396945E3B2DAA52EB3F /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; FE8EE10565E740E87BE6E423 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.colorset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.colorset/Contents.json new file mode 100644 index 00000000..19292716 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEC", + "green" : "0xF1", + "red" : "0xF4" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x0D", + "green" : "0x11", + "red" : "0x14" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..bf1b93a1c0b52301c2644efc0e2931d3073a7439 100644 GIT binary patch literal 12359 zcmV-NFu2c&P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91V4wp41ONa40RR91U;qFB0I4%yP5=Neu}MThRCocke0h)^$9Z4R%zN|B zy)P_w0pcVG5ClPx07dZtbrngOF%^lDMMV-tV*mik$5zI@Lk#GJJ##LG^s07g;L zc~old8Y{Y*jNEXE;F|w(g){hIr7Y(bQAPM5X=>jpQI|B6h;5uOjvjH2 zGI30k$8EWTNMO68#>3SRSP2nR4PenmVpJ7TX-LE&f=3X`b%i@{B8E}5CR`4W5On6F z`J*z#LlP^Fohw$aFlhG#a>QsYnI;s?wFPmih!t@9FDD$ zT-S9Re42I3a#3B}oG5QH%N+zh7ZbhLMMb+xy(WwP01 zGAU|B;$t-bpR;J2TCG+p6&L0UGqW>ub90q)#q~VjPr_f#=5DG%^O~iW9N=`q5(jB) z?Hz;BLM4Spqfstby1Tpf-*e|DKXLDEdvD#iaYI*EXC{+McnP$enfzNQSJWdeLh>2_ z2S+MsCM;@D3)9vD5zuV)dc9n(&dknTxG?hep|@Xn;iW@|4oyu@`+h2s@M0u!hDPvm zDNZ(iTFiAtsN(AvRAAARI&U;Xm-?bjHBO#9`{WOw{ENrGKQc0! z$)wp2O^PxoN4jaTM)(p3k-gAH*oG5swOSo@zw_uL|Mszeb={^-s#*xkrLfV^7?9ee z(StnW!05s%%~-G{>tNL@VD@Nao8*EcRT_vI%g6YL=yo7zjt7=bMg=k#J=}zo$tth? z^61E){K+@I^XK1dG=gL@>N=uCQ}~jia&p;RTy?P{Qk8mWsg+8#zpv+;-}s|1J@Q3W zEeFL#M6*>}P@-nhA~c!F%PkHErR_%A4r`^DBwhfzkd0~8O1i5VH@o)d{o%iSQP$XX@=+8PZq zezP|Xbi72E$$j=WzQ`b2I_n$y1btHNUQpLYg2}7S^psjGsCo_0yKFgl1Rp=8+B(9X0cUW7 zTDwv8460;?p2Ek7vr_4%rfSI9{%7@wIZ4`B8|+}c;wHRDe(MXrdf{blUW`EOQem{9 zM3t2%Pc-fA`A_e^OI0!SNEI!!dT2x`MXG3-hKDH#sqr7k-OzJXVLCj2T%CMRT|B2| zro(zeVH<$vnZc1BN9Ur=uzJjKPK1avgn9p3wQaYvV{h0s;2@HMX!fBVAK6Tz2JO{f znjuNEN}0}iDk9QsA_qBe_Zqbq4$3nq%S+PbxCHf_4@@ViHSKXMQ;b&5Nv3B~ef z(}s2Ve7=DeZb^|0qmSGo-QuXi4SmXURlVZ8|9be!bK!-Hs$4^WC;;IKn1ZPQRw5*L zW9ZT&5)dpxVLHfhJn7MmP>oNiSAM0AzZ2fJU+wusm}z&gkP$v2PZ~>WfpVmm_Pf>2 zV6vJEyJRlAb@Rrz-hR(lN$WpWY4k*si>1dMbnAx(u`pLl9b}OxyD6VgDq;CjIjO}w zF>y)#?T^F5hg6}Y*QE|3M|Rdg;xKkd?JycF6VSq6NN62I8j5rzeCp!a*r_?{+En=b z)6V%5YX9fdx@}O>3I=r~!HA$ZZ~;@W)j$I1LUKcbj~F`=cao)g(&>Z-`9PJbs@71i{!&#d;XVIQ zC9s1*?c}4?(@H}U|5iz1!oz~um}i}UB2;f~xw0&L4I4p^=bOxJyAnmvaFaXThJnIW zum3W9`iIU87Em(o!f?x{kwUrV97u{x=Z25C)U>4?e{gF+t= zYalg`CtGyFgsYCe9{%(%)${_5H2gSCNtBTcFoTcPM6F&fRw|`(y;{T6sh5BrX01fR zPy3m+te;IgJ{|?Mqh*kS8yon-P1s-tLnlNK6)Mh~FREOpy6saOvq&Jan_2@X5L<&g zlg83(^r&nt1t7kXMiKHX7h)rsqP2_=@sva?4b>8a6uolnLioR*P!rQ!7NWP{%bP|6 zCP^7*K?tYJ&lMJm<&tuPw!G8d?PfEf?1W<5T@_2t+(Ke>G8;77yV~0NyS=o}{uh0y zD%Vl$lHgxc;q0Pw@E59kjoQ3Zw9%?jJ^QuV(k`T^$a)lPie+P6%+@yV+Av`sk$@HR z;vHgY`*8!*QKf=<_OH~%QR%!E4I~o4umT@6Z?#gHo|+$@D^|L@61%qg1ATsb-bwl# zm1(f>1nYXeQ7F!iU7bC9KKtQ?zW&bk{%!~R3VOZ+BBPT1ks$-=wNOn=sRK`|?g7== zCyLT?)1W|sb6XON_KRwrh{vzCsP$39;9FFU1@W4%u!#5wlBq#G;P|}xV)*Xc9B~1W z`I{`H#6Ag2!Vqt)My}1w=h{+tek`|cFpiik!@Byxmykukuu(ZFlB?c~P zvx-g@6jl&L_?mpQlGYf8gWL$Q89G1W0k}=92Xp%T>WF&rS*KW$&ZU(GJ$pW#1CKQt zGqZ(r$VwVp(50f;MMkMv#2wPc(Y$Z zY!cH~>tYmVbk+FeKnn4#h@(URSb>B`$Jd_^M@Jb`81R6G3XT{2gA!N)PtO!iPZkm% z+uO5em!I!Q+rkE;-eiHtYScOugbCnlYD4k+vapxasni9>x`lh6?asoG(tzKI$|P!pta^oqpb zH*XQkCWty*z7W23P%m~tC{}8fC>GYvFO)9M6#aYe>e#)@Z}0TmIuek znS&p?M;wdjfRBQPSFZ=v3Xl`od^J7m?Z2y#^he%!ZDYbsi5s)6xekg0>38b7bX04_krD-93G>dc)zxd17_u3o zq|L>9>XGp|XWzcAeS4E_T`6>~Y|hm~Pr`Ty?;7YHj?)m!ff!s}cnL3T{Zs?UskE0$ zc?)Up-u-ix>gD$iUAJYO@{@eB!$T~R&?1g#_2jfV@w^;i~BEA0*OK1KjhZ;3l1 ztu?Bk$R&!PCP;EdSNcfvHF8)fW3#k@CYqU4@4m_BBR)?GI_F;zi3f~&aBX6~wsBL> z-5>Wmdy?&)$!wn1`WThrog@wyz%>|u6yCtdH1E_JPNm9a5x6CiA;<4z0L&!teJ|;U zpZV0n#Q5~p@t)xUc$4}-9G=w}NpLE{HdMzCt2;ie2G)stsHk;`y)z250TD_J%hp8C zVisWT95}R*I*e#9okbSZCqhTXnd8pr2-_VsR=QAeMOVO7?fIRb-ku*!GcgzIEd;p7$d=Br@r?7K_#4kn5fLnA1Om9ZV|Q)R-knqAd;6l`CKXv}B=ZNTZuHia11D zE?42H9fje!9v(fciWRI-;2l{R>hX>m*BbSi`C{t!J8~N~Ci88^I~bKQ2jd(_0!Kj* zJql}ORay+b{jK7MrywEo_8ZB6{#cmncJUa2M+(=)Xhclug}_HkS*YnV=9j4vHM zUaB_mDV{hL9QdWeF(aLTh8~FUL6`I~7-cYo`MG@dP7K(oTB*V?6LB1Xkl{)a5ao=V zQ`6%FicVsuC{F`)5^r+&f*&l*R2f@WagZTo6wp0_3_`=Bc?;FpB{ew#1a^B{uhqq3 z56=^+?Kh-{hP-r^b5=5i6*FDKV?<_LhF=kejdu=JY8-Vif|M&k?VY!s(mWebYd~Lm zE`yFE5!RNN*D}NFoEvtO3yYKs;h;srWDj@9AkRJT(F=S^CW5GkYn0Jc+8x$F;~aCN zovS#g=@X8CBAG;@Z7Z{pv#MBR9R#EGD4y#emKs6bb#piG@-U-kvh)sy9!x73P;`GG zztF(|G(Ay0eX54$fPwIm<5Wh+)YwI>9cMSR5B@YfJCmQ{k*7#zQI; zfOr!`@N`*?p2svH2-Hz0qKhfKAnBo1GE%0Ml0nUC4zf6~G{#-@5N#x0u(MZFmo6}d zb%M9?fhDpBk5x>vr#rQAqnAvhXE}PUOz((;Xs~GF`{9|Bm5E6Z%5xSwYk6iNICTOK zyzI+J;vI1-XcIUb1$QHqQd_px+d8mDN7RT-@R6)DIO@pm%H@lyQkKOBy<$^5Z(-JC zDT}3&gF!1{Ybv6dS`lw}*?g$*wr_ll>LUvBX}7P#8|v|fy50VEC+lOj$*o_P>gtgh z9j6588ZHHFL})&E7t}aDmB2&%BBS`TU`PN+r&9fYQeC*_$%;t8xg4^B^X zc6)0EgIditd8D@hKsPn178jgo7_vjBSlw#4_AnW=z}e!)?)Z&+b;!XxZZ7$#sf_ z>hWXPK}|MlJ@?(%wRdB(Uc;nSK6O^jPC>qj4&BWdo)UvH38LqEYlkrI=;6njEh{5x z9ICn5aA6kx76{VVVh6G)n6?)PRf?xb!_`*O!raPZf&BqoY(NRKP!;AK95r*@@vE~R zjLk2=E_%Vqd6OH5+lROJIGcxEo7|ZTC9YNS0>vpoL75>xTz{#mvk_POyFk*BYIhKV1RvAYN!>fIGq9${S136A(Uj` zz=^2ikjkVTMQ%~i)CefbujU}SLY58@GtkLk2KlB-V{7Kb(YXs_XXmQp*o(=ev+1Od zzD6a(shL`OWTF3yLt!rCR57+D;5^Mhbb`mBM5z2k!eaAViQ(kpQCZLaUJe;^of+&3kAq=;^OY7LxJcCL=9 z9eZr}kX#tWJ!SL~SK-q14hpM0Y5<2(hYu2SAj452^n59^G#uv*z zjon0t^F=pSIPO?}=Mef=)(gBX zo5QvNCzVQU81~LyEh(>b^jKlukKmY_@rBHU*&*(}n4& z0uB~n$ZJn0yHcM2;&X|08`KTE9UO?jsv6ryINWXC_dy+LDcVOPmnS%g&RAAK1)46sSFoN?k#d2`v z+2_}H_q$*FMNM8NseH=Wv^DhG@HpmezcKOhJD4F0m&Q6S zo^y8G7RnL;U5h=(v>kNfv=7~#3^&^*CgwjpH*sMBhY4JqN5Ot%#tCy6$IngnocY6! zn{N)+uE&(O==$9^@9EvK)1J@OO=dM)c87zSdPlW-6l0NoqE2{3ymsu=*wer0%y?mO z-VN)?uKcEL!+itoIUH6g6vL6xpxSWL$NwnW{>-%#6QwYj9BxbGGpS5hF4K|4!n0JYmKV$81z$&+Q}%)NeS;0se>t_S?_qK9u#MJ+ZP za#%BzX3w1-KUWCz`Ak0H4&A!BYs(rReGf6r&Hr+7YSG)Z+r565GqhE;ce|N3gd2V; zAjU}-6NW2YAV5EhX_>7k1}3otWiVzpu1+#q*GYs9ypzk;pZ)9Ub0ZwzQ(j{0 zmQW?IrxD=5Niv<<89;Y=veKg=bc}gc5|bxH^fl`%w@GwqqHc~93zLBRTHZq zXO$!oL=^2K+?d5#xg-vLxt)DZ&tP)LO-??Cqp*AqqA>X)k44|eR7axTzjwP^sTLc~ z=$WzEZ~d`z;5klZ?QIwu`J{+t=Z&+W@0O7pnmruX4OC*W;HJkXEdP z6aVY4ouh}Hjy9FX9sm@;gy*WZELhc^{rR<%qt!-;>1)$x_jJ@N!H1W^nPRwDA}Y`pljH2@DE}dKZEe$BQGB&28L8u z7oWx0rf?6N7|0IH#*lIDYVZAbl_usV&W@B){^+SIi{JQ0=dNEQ`UmP`V>9o6I5Stq zpyY=^cJul$--8I^ViHX1HD~Q`+q!|7(^t^}$H(WA-}vL+-~L_q_Wdf?Ml}`}!()dR ze)06>*WQ{d)!asX!vp&|>($1kDQ?=Mv*WcrdruhTa>>DAFV~@b9OaWal>L-SuAanF zsfJ0bIw+zC5P=m~OdLw(%mQS_EuJ}pqO$mJ(_0Dc6$DKF9z|e#I0xjrR+vRTE-cVkM-Bt8o>`r*euu)6xzB&Jk z7cjWd3wXYttxU|EcHcV*AsBzLzl-s+QL6Os-nf2qZ*c0WD%H5=)=Nb2#*9&K)O**Y z@N*`!i$HI%0knpYjy%j^=ZGij6{S_iAjScy1-D6Wj6xM;$rEF+enSmErva|)oD+fD z4MK+#Jy^?56|-B0H+<=SUv_(78^-Zk5O6dP!gNsU*t;ui?+;z^4j&*AbQ2u5<%Z51 zHiuOpV%!g_N?}qfIf=64)f^A&(OR+GwSB{u-9zg9L|7;*yv#sk%5E97!%>(j$kts4 z?@0K-QiKmQr8&?u=7yGn*hUn5k>U<&Is!;|rer2TEcgQA%ai z;4pT)@f0G9C~=LooC-5pgLKJ&fN8uDImna^8YKwQGH}F)fr+;f?d_H8Heu-w3LQ8i zknKZ(oW6|dNR(|TQ>B4c1_hPu8@TR~&koPdHWn86y18$#(X}q{GH{3N zG-^wz4*bPg62|q~X>y?VzGd%d4U4xZH{N@oe0d~J|{w^4wRf{F36oiWV(Bb=#~7z-Sk=4p_D3EbGc;%%jMko5own8%ka?bY(p4#IoUL?W?!&-}qx zdKc$mr%=;KD#iVK6wURn%KAsrf>9c^Y#ZpWQ%*W}(g07F#ZJ+4j_)Tk(J`oG}1_A-{S; zM@}oLV@T{Fr7=;;7TEkLf~JxyFKh{oY26S0ma!sg-o};M`R=bBw=F+4q2(#p!QMN0{k68JoF;v$G6!=J+MV0)(&dCL`(8GaP5C z#o*A3YHT#TG!>L_)JFEw^llnzNf6G?Eml5xXZPA6zDJRLUI}Nd4Tc2hWD-s5Q@4%G#yb*3z44`0AlNTc%O%D`OKYn&Aj_=V)Ck&l!0G2D6T*c z9PGk}$4BsH3vLf@{fuhw!ybSzf*?*Y=$7CoT0$;{HmjxMW8Yc@GFHxxtG8cNCrigPbmJ^@|~9yzCe{cCmL$Ij>nIA^Tz zGd8|NeNNEV0af<`iV6ru=f!G%E6Ji-BZQMG9-N)sc{L>PvW zNw1d7@49K~wvR2m@oIZ63pDPM3L!h0v>tYu)v*iibU{r{Iyc-DZoN(Qu2-0oRFAQ*kZDG&8-aUKpT9M}? z7_iM1kTC%_U{erpyla!#@VYE_dx=hWv{pj_ur@4lN3AVD8+LZ^hdB_B&Vh%60B<`J zwQBx>`=&09B(7Y_W-`E%bqPBw81RA-1L&6~+=)5&>}6-`akYMv8rrOS)~S3Kwt=`% z03NozYh|@Kqb5hyrBmwMnegJ3V004uwrn@C8BF=MN1FlWIQ_U#9Pc0Mc;Ep)-;Q_F z<_UwEi~p>_1n_G}1uQrMVx>vM0a84IVzm~l$tcprSSkaes2g%6k6kXjGS6hXd#hi3 z_}X{Bx3*lsFRn?X38hESAk4YgcEBUZ$x-LxHMjS)GuWegyHrPq%JQr$(YYPHpl0XP z)J$-7Dx6wC_XHTys5Rzwj#*q!P)&g97?H|jsa)paFJ$`%aSi~(BL8QGURasXATAS# zXy`Iy8NX3NR+z&yz{KiMjgpLZeXT^FBK;Cw+7#tSvd#OUt6jKkh3v77E1?obUhQ z!<{$XoXWQ0U?|=u+GVHK-|88ehm3ZJTww|REs2H2k(iIVSZf8lwEWV-_=u+Htidxa zU|UEeVol^=qg0xsedkT{zvEqc@+SjxQyn?{3l?cJDg;(~-xEQ^mgMMU6s7OkWZ239 zgkIH_2$^nWPT6FFgb8z|P#Vv*XCD517rfJk<7*5bdA0za1fxbLiB6<8^0ZLtSPO*$ zO2XM9+WRUFa-4CTgJg@x2sKAbNroI9am75XZkI<>k^NCW#Qm!7hih0r?l9f&vL;7%QZVt>h%Or*`GM>N=UJq zUmzoc&5~xE;W-q`pvsh+RQG~pUsxYC%|R&4&dr@UcM&Exp|PBv#hR|s zRXBP&I(GHU+4KAM-irM^Y4N5I;IK*P7zqW|;3L6=#+GIW1V1cb%i~!D2gNsUO%1Ir zo;*GN`WrLn&gbi;YzqJJ07o5o6a)Q1ybNw|47u{clk|XNBn~Roda+V3B$Bm_Tm8Ma zb>FZp)z*RI#6HeFrZU)C(!0*mtr$4`Fhv>3Q$>QPtW9vx*>mSFja^yQL4qlSq!T^C z#c>uEi!UBHc>CTxL?GC72CcLaL5+}ThEUU9SY|cAOG1Z7H8K2DcpxmXYghgHt$ZhO z?D*`-Q|{!1UoE8^zK*A>@XP^bIMWJYcxu28$Yd+Jl=N%Z1K6@9y>olpx^+0dnMh^f z9dQj$TI**nc5vew@e6G-YBD31MQZDor8E`3{OW7-3yax|{$D~sF;1Xm;L-X&%ruZ! z9QnHC#_NCn!|$|p^`et4A=551{vkRWpBH}69XRrF9_Q~Z@TL{7&#U;=1zz~pDy0VU z^&0-6Lv?bpHg>f(b|sviR`~x8R)BT^EnjE@tK zICzhfir53e!x|%0URX!ofjY)D)-E<&+wm>pq()X zFH|c|auXMje>QL=rOejZV- zc!<7$Py#DN1fJb|9B)eDS7S+xDL5XEOeT{|=aQLRGRqI18evK!bgju?fMgqN7`$mwOj-RImfA2zxTuszx$(Sd>n$XX2crbz*dYP>u>DoC%kX| z`Csk1Y5QIK?}d+~0xFmgGB;smXpm=4K)3)GLoR`cA`DY#YJ@nG#ET3H9JBDchV!{i zVkD{IS(wTzF3gzTBqBtJ$Y5OKPdG5wOj=n}CW@}cTh}*UfBirG*$*&zS#*0h)`1y` zSB#)0e<-%FSbF#9iMwvy(c9O@IU7{?$TSdVNt&jKGO1Bhen`Pp5-zk*n=M!DCBbs; z+90y9kYOj1o+w33PaJ>+0=dY8kq!+!)=C~isg4$;)sB=B;Vo*3BESzCCqFp)dyoD5 z^P^XNS>Fq#CXBAg(4Hj@idXAA|HHtQiK)ZyAK!P=^?iN4+DFqQoaQQcFx32pt4CJ!Po$BN_3v5W|+Wq@f7SkQto>YTyPG29@%w zue|bmkNt;tKRBJn(NBwNgJ09KbJ?n;5v1X$ZenNb_p5;TXd8jTs$u}BD=EPu?Q18OMhg)@C5P|1)4{t{}-#E2o5N!m8yFA$x> zP=fa;6IVw6?7KgB>_7hL$d!q7Dw@|p8+kVf3 zeQN)j!6BTY=L-p3YJKIA{q05j3v#5lgbY6mGe0 zc>f)H?!JA`4O=(&_4nbQ#o=Eva^)hHi?0|ROqisQlZIwv5g4(h%XDXq!HFf5BGzUN z#8T+SkSJpekdLs!Y8?OQqF5}BPfVOVeeTsa-#PI5;nNpJfdlVYDWni7FbcROR1c)g{ zc1UeRLR5<{cZ3PlqlT2nx2kpQz|PJW#wTa4j89I@%vWl4OjU>*D+Kq*B4of)7i0Cz zYB&xkxBNfpB}h?GGm47HL^AY#D4GokOvOOsp*X(jQr0?KWrGZ5qmkTJRN2zB-VsPm zU8omI@EYbniDn(ptt2b~%Luf#EXM(4>8s}zk%(tG#t{Hyk~s(M6Kf)kF15{UGba^) zzl*kKC7S#qpNsSJ!VjE+8?{&1^HboaUqI1bcNi{9j3%(tsLa zgQ5NaqBD`+8e+&I2LWk$inCj>RwG1c%W;-KfVm>FmcC*YIB0mJ;SwehRg_sGG~$3v z#3Ta7AsV3t5;+KUOVUTk1E%H@@%m`wC7f3(l2gQO3BnS1RMbMFVw8!K0|%ewRZD=n zxEyV@*FF@n0=5Bzo5?N7=;B9Qg(ohC!$)9#bk!=ltw6LI<`VdlVv~^^E0J1U#EoW7 z&_Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..664f151ccb79e83b18390acd0ee3b38ba5e6d600 100644 GIT binary patch literal 43205 zcmV)HK)t_-P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91z@P&F1ONa40RR91zyJUM08KkN$^ZaB07*naRCocDy=Sm($#vhiPrUiv zmnXdU=1s!PUDUGP4m@7A|E*3iqI$;;uf=YITxa(Due@INzSmYijAtX zti%(HD(iWbk)2pcfaGfSuJkT%LRTtfPKq{1(3L}kS=nn&$=cu$Pelq75y~2K2nv%_ zIiapj0^lQKu1c>(v@zP$D>d1`4D)0|#e@i!WG5KO+NDH7DH%CY_Enifm}J1Q(qKt_ zVH}ZZ)Z*e&v{StMHH5-_^lZWYeT~g3qdwZUpZ7mgu0c&XgLuWNDbtm5R~Dq(h~`?W z|ErMytC+83lxYsCx|9D)VBsN!l(X7gNw8{MNL0K(h#wHa)a|Kcu7vN(rlUt1vXM`etSB7`xfuH-%`W52)|*~c zIWxtX8eujSYf6(8o!hKT_M}v4n95tsbj5<`Rh?i0%IMBkl@eiY)J{qC9IX2ogU5o( z@*kAQc#EWZlbZ z>YCb#Sh4PtkKS;tYcMugwavjro=QGZMUAtXC7i!}6Ot7>nZOA2kc#MIYi#ps3FT07 zDMVyX;&Dr?tkPR$!X#Ci-(2uBj~UD5oD*c~keMY|2&Hw7tqM3rcAy3VRYFMY+D-H3 z06e)d*9E`W#E|~kDQEFi73J8vimbt>cqS(UYK%e7GDgm_iCAY2gX`ORBM?y6(hQG4 z_E&(!%BkY5AallIVntSy0H0vC{Vo~E8|sh?(fT%tSXlI^vq-ErC|`u@`Nq1yW+E?9Zx`T^LZQ%ccq>v015Q_sN+z$=Z32`#=a* z8J~sN=d4&#RdkRm!iI{c%G3da7~83~0IA`_Q2AMkYR-&eMU&D@(uC0Jbh?=!`cz3m z5wxYV5h?@`dX-5`nb{IEo2;}>l;R3DpsMQNuNa`)v}|h@4F1c8x_w~NHYa}X_Avo% zV??fNSKcw^z)(X&(98k~=6Xg|3Qh5{4FaksGbiV`6;WcYU|>(!}Qel ziaB_GfJnm3I6?^ddc!K&Dig*=F>P~QR-encU6fOm9iH#s*6>|_P zp8rH<-zW%YK>a5~g69XRsM`>n`x<3e%_!L?g4AUqGRHF1%*fVNP!ZKtMq)o8?L(m$ zpTd^{!+^w7w`AD7`q8K=JXH1VcbJW?$^9i(S z8^nrk4O-YhNvx4iy0xoBYBZ=puG54;swMCS}d*di8HPx&tRrTi5BRw^)Nmq z?yF5as?W))LJeD$h(eELbt52Cr&|GBQ0WFx5h~)V&j#Wb&rD@Buc7Y_3geSldPDUs`QYe}w*bSbQR|u5?fn*)7y;ASi z;K-W2LMl9krJ{BjO+w^T5m*FO zihT^Pp?9`MQqd6-7m9V)m_ljbXBOM<8?j-Iq-2Uxa$r;hsQNx%10z~06!Xg!vo_aF zvZJQp#+QXwmzB%r%5Fv-9iF}!==O+xR%K$b`k76%A;D6k0Lm7)ZhGJ+MDr#w<@F!> zd{{&cv|1Z+kgcycBNIGqmCPey@XAR!nT*Hd30u3S(QGzbt!BH`Znm1-u8XrkYt9$9 zlTWe17K9maOzp86tAeU7vFTxa08Tk-y_+Q1T>+{BsaIR8@M|J9OVpyMsu3YbLYKj# zH|xSV!Z-xvm&s^2l5ZxHh&(*fYPInXDf$j!-~^ARsx49wCehr`ok+2>Ps~$o%%x2V z1H%b}auN!}kbv$HD1~NlB%N+xMS)n)n@9SsAohYxA(f+NpOg@?jB5e>Ql!b?`&_s`s$g7AAb0o-~86szW$BJ9(#OiYrEs5)*x0{T5n!bn2z)8 z4OmIkrD_*LJ}6NYJh7dk`*|RU0bQ)3P~X6l5hEII}f%adA=IQ8mfD-UO)*9r=CqslfhRctb+vUcW!z?SANkANae! z^mp$0@Q0QT9M;e>8IMcE9Q*C9TwZazI}DV#g_ zz7L}+Vt1YZ+=S#E@*q`Q87l9MT`7?LVopewQe`k0aoRq!!cuFpN|qPA3PK=+kZhLh zfF_1o%v|k8lWC+&ZGUg;D_{A_zxmhy_KSb=XFI#S`T4o}krVa^E+wl-j>N30m`$vC z%Mw8+8=M)%Pb5~PDpT=6iB!o~RY6Z8VV{`n{K|^Ka0uxm_x$ib`tN_`gCF`p(Vj2L z0n5T-JTAt=LZeQaq~Z!e)y}%U&=iWJ4=ems<&MonVaSeJO6BNMAYvq~7%X0uY8`oH zzt)@*NYa&+s#5Bbe(fRzvq2h|6pgtY{6+|?YZXG8CYx!BR=FvjidL&2hMJv5i!fph zF#OiN_x_7t`~^~WF*6({$O;^dtaZd}vbVJB9jG(9Wm9(O1D?GpLzz&dN$AIc|ol-nUhSnQKs41^Zb8r^Q+z%07~I zZn92Ol?l^aS8Zl#*#L<~Ub!l{MH-V@t+Lf_w7ZRFw`g#b_P_A?FZ}91|6jlRz55py z7C2Lp#6Cl9PDYBlAGECt&)8SpObx4MY^xa@xL4qb3gDw_o z-S2;2gLoPC%fVhT>=(pPL{G9kV*r1p!=&C!SrDGjDl3aYi4>NkVqkk~SuNl6AHEFI z^c=OWDx;;ncGfjDF-3OX$pw`nxTU3@vZc1KRk0Z4beanm4Xj1K-#>il;Q#q+|I0u7pZ|aSpR>^769lo?QZiX=D&Sg{^vsbpFiR%O0xKj1<+|8JRgK`IL{0crY7MY>#rw~0@#-itacmL35KJ{LIJP#$m>KIr0JP5#_R$z#*m?q00(0gjr^Vj^wpvYU8XH&vQRza_p*;Kk>_SDB)W~?SMo8b*8-(oE4&3bFBm{oHv=E&~e>vulJ$9 z_TK;S+yDB+_18`MtaXMSc;d`F84-}PKn9HdN|;$Ed{9XH1hVHvR=e0I4ziA9$|h#A zQOxL^Q6)L0SItR)sV?*Io_NBlR(PGu|p`uG) z4t>kQGE=TMFJQ!GGcB2N7;UIjMZQ)H&ug0+FdeJRDAoXtT<{qpRSC_0s10%<#MYWa zKG7HqovSs|*;z$dS=be#rdc@g)^MOyM6rYpGEm8@PB~^Y7DmmoyRh={kKOacV~;=b z=;IyFWx>F9ic^ynNM*3-0SnolbGa5kNu$u5LV#>k)T*f=xn9|_;C{{Rx4q#rzw;X> zPG9Hf;ar_-${I-(;E_T~!C^_kl5$CK#v~J4^*pL3y z3opLh?Q}evRA|WrDYeULz+_Wxm7(lKbkHjh@dZg+^^mt-Rt8fRP$M~QEPwrf`{y6~ z15jqB>)~1S2zY zt?b5dZSy~U1v2AMx)=X970xGMIlD1ieG*+@I z>`#gx3m7W$K8S0K=BcnVH14gQ5|OZv`6?W13oq*8Glg_$*fMBSXSuau4wcC==d#iS zk%~4~LYArERqKD%MAd$l;GLBdb1hAUDT6n1BNuH*08=2V z52~}7FEL_V?d=?u8fxo!$v;)Ko1{5pG#Xxi>cn4s?o%hOzkV`g?BQZhtePv8@T!q2 z$C9XqlCq(VEu2;GNkISFd4IVPVwgIxHUflL-U@XJqkO_kC+brOYgc88j*L*>hZ$j+ z4qSmh99Ea0s#_R3qj3Dd0zt~3ojLuTq)66@vtY=Qx%^im)Zuc*2<(n zQ6i}PZxI?7Q8~3s>=4yoVx38-y|QI;lHqGo=9~^G(YG}K)x)k`0-Z8vtubQ>bZREd z@@`F2mUpBXP7Lu0qzcmIu_{)uwLeRZx`nLIsWGLBHF3Zc4PW(X}cwX;4F zkLBwe%BvAGZ47Jc4KP#oL|s0PYopA72v!Pnnkj&+R)|H4b6ws`#s?h)vn3E0p_mV8 zF32QH#Sni(wOUB`ZX@H?23Dq){;RXuC1R9px4~eU8H@Zq) zXBiE+hyRcN!LO{Y9^lFA6sOackrT2ENu!)hG+wGAU#%j^+YZ;~{620MeFXOVgZI7b z?H_pmdw9yw&-8>A$<9Q4Yr zAXZfssL%SEjbHj(Khs@Y)bOK)b8529MMP3H&qy|LC^P+3ppi+g4R0B19|J{qqFhVL znC@?nD{6!5et@*a%#a|2!NI5FPahiB;6z3Dk8~G_2W`2nt*wZzElPaW3=blbhF;@l z@Gvn*VW{(*K(mLC<1q2t)S#(BYK%x8!R)KVUxTvbl{N^*CpQl!<0ob?RE&QnGHw_& z8^8Pue`|hzJ~$*7`c)1rww3Onh$XnqXW)<6CdK{=+}~zH&Iq0W@tR zxGq#J$g(n{ZVK38ZCK~3%Fe9#@;!!U-?A5Y(k;By;s_!uhFkR5x?mYGnxB9f^^5*a zv9sCO-YB=%8@pTO-mdNe^DHIfJhFNixm|F&JGaP%xN_-0ao|w7e6UzpDY|n-3m>9b z{hloeJ<>ofl&e6=7Rn9cRTp@ZDnlNch#@8k3u-@wodVa;jRLTHw#J(x(MYz(@CpDu z_`x9b?)TpPo_D|VD_{Nk9L+IcpsGa)Kj11*sEMKnL^4wt$~FnvAZwg49mfuTx!e-6icfm&y~W%kS1ta}QL`JJ6$56c z)+8a-Kw)Dm$*EwEM24-^h)FD72^&fMBEZBzbNFJ=!pcwoFEZd~^7`QZDH96q$eQKSd= zodU;&8dy-10aUi5J3}OI10kf6tVovC{B`5$x83=saQD>is>NBwG!zg{Fj<556)j8P zjGRp(iGcq-q)!cVJ?$3*iVPKa=8=C$Y{gRSZ5OXTQ$G7h@%l^U#(J^0R}OsXAzy$9 z)-c$Z7K9okw)vT2;YzJ8$Fb!LHf+o0q!?~AHrC1~AJU|D)yeXfI~%v$RUW=dcCw&C z$|*8oTt(5rCcYFs)Jko{nww1lW*?IwRNn@e1Vjx5QJ?ZvP|AzO4iT6{(6_0EgBx!+ zb;s?u-utcl<~m(f+oFK+Hsh-xkc<=GOr0`Xrx+?7A?d9vhr`j^-g3vmLkIb?ZD^V? zI?#onv78iLz|N4Wm};wXG$WJqzN%9u*G|R57pk)Wo8Sp`sujH~)?Y84ez17vq2j{X zqCYJAqeg#F?s4Wg!6FVex=E8TS2OgLE1AoUYa6ao;OLkfXY*65SXe7A%3*E~PaL=T zo=vM5j*FL`E1!F+_{NuuTkkCHyt{GD4NPrx6hSBC=ts%PYB$8ztE25ctFQN9B2;F{ zwEt9sF@uRsCcCUzRc=WM6)NEYX0fHk_uT!CuYCQRU46$jiDbro`+!K+coA&{<}%d` znHGFeQl84U-FbV_ZWjYRun{&)Wh-5|3iIlXQGFOtajLV!X12L5)Y_lgSc+QN5uqqf zm7+>MG9T(hgYm+d;*oC^&pcdSxW*PzS1NE)LN2Etu;ZE^zV$7 zh!QE)SrQ7xoK?MA9cA&&)TlaAcz40pOItk{~<5=B=}NLjYB} zgO;?XX+g$Jg=sVQO>V#S4Z7}}EXhdIy{(E7S;T3V2-@(W!(AO>fZ4545j1Uc*?S+X znPRK5(<&r=zolF|TRimj;;9G9_03|tS8VU%9s4&tNQ~I@+yYvv3XelV6(}E_xlf$~ zF*HMiPLTnNQA{>Q1$%h(^%EA_4Hi`~Ch<(^(~lOW0GN zgs8}AOV(I%8gF{qL`|?}ZdFMh>gs{zQzuW94B$=~M}?3H6Wu0N+AqP&HY#(&EM$_{ z#VyxD;3KfKBOjxf`DKJxx+MTr;G3^O*$5kr;6DouZmc*@Zh2r)r5eKb2i8ka*NX;KHmmNXZXT`q!J>&LP25d^>Y~HCW(`t0z|7O%;LMoHk+)+ zHh5(kJ3t0HxQijD{T;s5UOseR@%)p;yY4C8{GOt{U^{|a?Cx)A5?uTnOB_zAPCJ{D zh^V(ZTaj4PjwVqzt=Jc02>`PvjhCM(-}mFik*m4ltw+2n zXXQ-j3^Tz^MygiYvwO3RDyz||bTX53Q)1+TgM2-yKil2TwO1Wuj1YdbhFoF{Ja4@a zw-bbL`DuJoC~r)yNXig9cI1%07h-FxmWcBTDX0lhjg~dZq8fV=&f08S#hIz)!@gmNI)xhnWs0X&XJI_9fq; z&Xrahe$z-1R(LEjj(L=$f@_Tki^GiuR*Nv*^C^eJ#{68dI9ERRSn=A6#d|+d-2P4; zHI$3 z4Goe_R!0+I)T5vlqH4O5fipa-*--aDG{BgT4g-;k1-pE|dsHsYHOk%MOTSy5f30}; zj}+adtU#Da!lV!uC9IV7k*Z8pOuQ=NO^YNWL)tzLvDC@{O-PLcE4-qq1`@z|>zK>6 z#^)d-fGllgnX}r98#|^pm&M8wuY(cUy@5~2XZ?|r!|;6&vp(CE_>zf~lCYVkoPsCq z<91O@sHF4wy7bpRS3GzxE1vSgh8D{VJaH`t^>k`z2{Y(9g2c84fXMoX)q4G|I5euH zBtnzGk`~k1>-&|APF%s~8*^jD(9%Nj-LDkq zUu%5iXN%RNRsml^LNJai;Rz)O&gTu>QJD$aLm_UgSr8--$cm+amJW;xVKa!S;BxvR zTM^R#6x-1wo)A*Tf_e6JA>_nGN)X=XGPcdn&4J38@2mahl)J3WP z``p}r;mP82|F%5;5;6jpS=lGjQBTxFni@*keQ8Elt61^LBtT*$0$oYMv70iAK0ogl zB`8Y@-pEHG2?;+}>O{mWp)iw0%3R5mR5i!hMAay0{#3$2Kv+&of`SSzBUZxF0bi|L zNr!n9l`z+bH(8@@UMN5JTg9W_FV3%*YulyHz3o9dlt5x`B{D3i&KvX9Oos^L3L&Ma zvNh`0bO@x-CQ$s;hD5^<4Mm>s0ztBR@KP2dYh9s7>Lj8>-mP_LDxd{IeN1=?G6$}3 zI8WsE?WSUgiLCM2-zv^LtuM5*j0+JU?Xu7UMn(}{=yVloGmAxRV5X=aYA7|%%#kqA z+ikb-#$_zhk@>4Xro9bOpJ~cCPc@9(8d}+sDi>E6yXu#iVU0K9ur6yU@JYa*&jzVJK6D^KzMB;B7fQZ_dkEgGMh1bS$a8kwH!6R%w|BT*}aX_-*+>Q~SLDB}dU zu@fm2h>L$;VM{h4DXJ4pn|)twkK#1Ijn_#lsN@T456X>4hd zF;OsH?)Y%5{Nff!4`US<5J1S{%1CJp9=1dqg&yjx)H5ayK!Ff6G)jsYjXNz|^V}Qh zdc>c7x;XP}e7#P_i#0x=$+X#2Q6fXLq_vjROecp$ts=5@bo7WhA~1_t`#!ONk|O>Y zfpU{5i-;(@DL^6!B%B@$Dw8DP1l4|?HS{v1X$!4E8%x@VloOt>>KR^LrO4aeEI$9+ z#S2e2E^amU2C9!?K+}YH0trTOGp=fc$kw?MT0MT@7pFR%opi?5%8~=9P@}MmwW5Pg z1Gd;@DngC`MWk({!XE*~&?F|!p%KcmKvt#5NSdTqquNfd*cldEo5h#@V{z`KhQ=X* zNE81uPWf7LDM(dCJ)`{ezXGZuq^w&=9)%II)|Nyo#Hk$i1~Q~}>Tt1z7q1{-GqSvo zNf#1lfLSJ72HHnoG_wWZIwwV9E}@1U_=`xmx{mo{qm!Kvp9HF(TJPlVj;mD z{tM@GI^A7ltUj@&DPP_9AlEJuCNJrO9$rXlmr7aS`kL86EU>n^sNjT zo+Y&pv7l7>%zCmb5l;Ou&fI)=esQj|IMn^L1vRi?>#$^T-qURAg;;x(6Q zQbr9FeU?E*F_@{^InyZW&qODNiY3VGH2WS8VN+o7)LRZlTGETLmRi$r;r?wdXRESPMztD-(a|Jo zJpu)rwiUMVgrV+d7gZ|L22}=wDsnQH3S>f;3gIE;NQp%l@>o?&1_?7sb4d+H?raoa z`u$>aquAIsv)ykg>&SqjBx(ewifXJ9dHbcoiyub)!Cr6B8;o`a!@a?9#7nH{Y+i74 z)xq|$!<|D18Vd`Jjvg$Gn-~TK0|5OK9F4(nveO%_Z}!h#Dlc4YZ|!!Pjh(&z{MI&? zahH!AXfH0L6%xWpESi;DXH$K?=T1^rggd>1i@^!ASX1-7?6 zR&;uvnMJje@o?1d^>_CMTYH1u-hj+xZoYluWc#`kb4ONN^SoYJuUgjK2LTyNJN*I9 zKO|lASS*?c4>nF-Q{G;VcY5Qq7kbaX+&g=&+uPah4R}BN>gw{s>PmCLZ;8h}k`0?H zqjT2^GIf4<0eD3QTTU=}reKRllqSV}UoP(X8NHXjGv}W=01&}Q<|bPa5;;^RrYr2S z|AViRR?{y7Gob>6Lp_RL*kVcqP*ok`u1ZisBDlVHgt_EXV_tKgB_ zVyp;>IGKFx`^5v_D7Jb$ikjs(?W#A&eFLNqbd6}zB%-4zDP~>FGG}LRKnHK_>}_rJ zCrb<6J8oOP>2&+Rvcd(`TGy~5)~^WYo!q{m4;D2F0Nub+D-6w!-(cdNf7xE`whyj$ zZ@yu4@zU_A=ljpS(BIhE8;+K?dj}64m^-whhZ3`#TNcj9TQ$=Gsye|+iL{H1POejH zPJHR<;>iaZcfLn4+(}x3nkQhxvs;(SuYQ4H zoHN_x57$r`Ski-+{bvILD|w$v`Gva$ea4$TMxC`C-iFibw>sUo+`jsT)9qzOl2${% zN5?~<^OD$SCS4@>G8Hv{*)CzON2o!nAR6Lhw zR80y)sW!Ye#tMFuE74%r*4*+{)LE4L3@1HBS(8_z9F^bti^jRvN*)m7u(X%O3j+tO zk7{*6YN5Lr$Z6y486I-?gtf6dxUjLi)f;qApIUv}9dpMHshTwo;nL}}x^u0$Io|!o z>6OMIdMIMZgAgVWh8i5nVTugUe5hZ?WBJ5)%l$?+zg%nFI(m5Nhu=MT!%aKiy}y6q zoX+e9!-GdxJ4=f|2uc>pZZ(~~o?A);t$SRhC)yMWZUR|ubnNkCBOAp7_ZC0=6TDEa z(UDk2QLAB%BJ2i9_E!FJ#w=oR(!muvD5}cU1_%aiE0L4|SFH&1vQ&Jr5Xq{d0uZq( zOaY^5RI2s+ID#O0>RbTKkH6IFjF&mGWfG7}^-?e<(jhxFg+Z|@BCO=}=nQL~sWC$ZD3LstT zQ5e~1+$Ehd)h*QsM46L(o$!wsgmevQtcytF{@!??Umhx2xgW)0c&k zklSpjB$-Tg$v&nsd=#vPcQ*A;xZSK$7V~E?)V9>y7O9fKQ4EH7BGJ{!U1F*U*RqMs zE=#9SKup>GJKWu_ym$Gw1iOq z#CmzVKfJKLcX4fNw0h{+2j9JT;%d%f_$?CV6henlhp~rXQn+~FQRp~^yb56!F=h%K zBhvyBaiax3)0wv;#-S1GaYh?5V~V`uxY=BO|GW0CKEC~xdnfC=@E#kFyQ>F;ED-E< zn~4~XF4jqEooaNfk%>dlO+;CJ4i{$Xt9|gY7vbtIV$AEGs96qgh@WXwMubWtAtHK(l}MM z!2(^`K#n#=IfxPhT~|=b;<+cwXCC#})HSRJg9X#3)*@S^sR1rbVb2W#&0L#%!}D9c z3u{}=lP8XT@b2#FK~7=Xb6ntAXc9NfT#P*$iaHI(p4!q!0jMa{p{TZzH)^s&{fd!s zt(v9uh)-iQYR=8K7?!vmIp~kty~co_mFX|M@s`$s1G|6v=jHkah8~%e3x`(;Rz^p_ z(G&hutK5EVq{hzY$YFRgqaK=OB*sooNV&Rr``h)V2f|%Jh^QJr6|{JUN>;}%w&JdL zKy;3QjO4l~h9!03l%IiD0Le#TL)t>KNl75(;et;VOjFoDEr*;95MyL2tjo9)nz{;i z@{HA>SA6@g8hd+_oj%OD?1Wb#{uCshc4ZI!fq9c?8gy-=k=1973;N zFgh6yG#hrhob^qZ)b@JaQ`a{C`j2n_(VvXhE)eF&+s*ma73MOD#F$~@u^#)?Ah(X3 z_8y;fDG1(CsaqWM6@$>}TeK3=D%<@w6|^uYc1;2_s%#cpcr=lqC`l-SD2fsmhD$jc zC4oSxajP7s@yg{?=gzdWw>G)aSXs$FCUoRL36&0NH8(NV>`S&{wZ>{bd;yQg^LWgP z1HB3sjWJmm47Whg&bZ1Z@o3Fpv^yMM-0rWf?{sdy{_uz1+ge;|&o8zX*v_{Yfjs;$ z>NHzk_eAK#dB0GyF&OyYEmp-4-sZmTmGce*16jCPwC zY92ef@)Muf{OlLjH`m(D=Fw)ev$Cu!X}n)o>I^7dx?UyMIaZ|;MKFLL#tcV|L92Z2 zrGnx4&Ucl>5U~}(;{JB0*h}GcI~)wTDYLnt-pMSgvM#q+)~FOB2{n_VB2aT)djcq< z39HHn&wdKk92j-^E9EQuvL!It&cIjJ7&wi+x`e&H;=fmZ=UzY6!0;0l+Vc-jTpFgB zp~x5|p!k-0f3&+dT;Cg9;_DeFPaON`2bzm38g&#s3vGTzw9DC6j6I&b6g`}@d+gEh zlcI~>nA-Y*oHk*^w5XYp;f;xMw7b(der)w8e{A#j zKD)NDg+fjO+l!09OI*jhpY_tJ7N2i3$Fi z!=X4CMV_;K9_~^VMIGh(7qFCxA3aRn%0VDmHfpE}H{@cHZ~z5&2)L^%LgF@>5rM(e zNdbOU{s$ymVORHwi*cls$A&y=5ah!I=*rsq%QY<-1k@_V}#^oLk6voc0`!z1IpwauwSW)ZL zj7S=1a4F&uq856Y%NB09e)ypeZhzs6b35EP+g-qzxVI%-FX>Vr(Ku@b9%T_BWTL_S zp(5(h@0X`eJUK4B+XZ5IKw z52;o^%_hL)T&a;KMO70bIYX6Qj}=vo8s`Q&Bj%7v*5dt37m6nz)H^h|`;$^S_!#R2 zBqvsi%;82+GZ>9``s1~&J*Kb&A9&y5_1E$HtlT2klG*DXepRo@2|T|oqLX%p0QGB* z9?f21Fd6k1!+3l&saXfDR3DcuMUUbn9myC5g~dCMw@zBj!u)Ok*HTy}SKwqAhlr%n z&i2aN-ZFmu?9PMVU)$L2@a2S+B}b)Is6n(Y)M={%cV(@@TQs4an<||KE5-A#mnRnd zTf4HDDg`tR39!i9N#0CV1c%GgKyOaDoC`{g3NIxwM}5vDxp*S6s)JhP$?&NpP1iyo zlJ+S-V{VAUraMz1&rYEhHK8H36q4pIO68M}G&a{Jy@4J@6?E1k&s2`dI@M+3qLO9o zU^w0xjJCFVt($H<^v<_7=NFr-dlnXT($2ucwPt?SlV0>CA9byJlujitMG7H@4n{L( zMD%o84Svg!0R~$L8OBAv??Hs9v8;P!Kr~Qh$Y$CHmPY8ZIp*Ud4HX{I#Kgm5y_I`@ zc=Od)x7RN$Z0;QFa>~YaIJHF40Ymb(y(2g~j#{}$Ou9tGDgE|l`SfEA?rgNjvew?s zoguC&Pu619PL)o{y;2~vosv(Zpsm`P6jKVW3PB8zuJk;RFjegog_@W`RMj=S6e)F~ zTD&Bnw3{-?<+7RKhacm8^n2WYZu0hPDNIeW?wacGXE+-;I&s3)yHRg2+}hn6&Cefw z|J_>gEG{>_^zk`YZRTRsVR+G&7*f}2v=K=gI$E89X4a;2{Cq^Ce2N48tR8wldaQBl zo65OX+1sOtm><2*2Z1Is`9mTMKh*0cg-{im?5PUmvh1!FOZR-J|C#@|x!a%L-d$W> z78@V6a;Bwj9*-i~)wv^e{PF~TyIDT@aN})v^WINgSgKrVf9XKQQL{wZC&cJB?NDh) zk`JP`GA2@VsY;SFEw-sX?KmN@NMZs(eH0+lQgLUJxrI~_61+{W_{k)30cKu=Y<3uw zm^J)N&b?N=^t>*Y`J`3&KEgF@StcX|WZ1-@!E3_&wc8){di|yMzGva&35FlfTwE~Z zq`k}OdbMiyNaImQ-RVA#;Q_aiAo{6iqNm*5DSz+(Ena?Bi&oYle1G)*8ycVZ8|C3c z`ZfqaguRTke>D0qlF?9R974;lnc{|?AQ>^_GX80h;(a_`xa|$2+uziCrt;gl&cau{E?M z5s-t$R7uM2L`o*4)@CTP0OJuz>LDAMt$xSnK z#)w8}K~a>?JlWXY*8AsVhk7&GCvmDZlsg$PAZ37_svv+SJG*<0Lx&H%I`S8^iB@R@hRELo|CJOQT6+6k-NT35 zoVPRZ=!7exhjot@uzB_4Fk=3S$t#ARuo1O5laAnd!f$@9{QkH1&TZ`N_E}vGcs66F zw|n-|`02-suYb9y{m8ZFC7Q?|AHZ2Z`4lXcSz2}Ja-yzxvxIMVx%<}k&HwpRr#f$Y z8_!1W?(T64sq+Ip^=3y^+c;3dpYmC_QL2&%uVLh+s+_=P_qyNgUP(AQmWU26wI*0aPa@u%LnQtrF9Dw>Dl?lxEVdG}=(L!au`pUC6BMsX!R0*!5nZb) zF>CQDqVg(g0d(;K7dBX*ikiH{F7cQW^bm1)8HT}&`HRax?W2cR?z&SKdo=v$q+PcZ z;#nUS&Ki4s>drPE*l-NVXa*gPL>$y!0I~k%g73Y5IbwyvLp?lo&+T@tn01|LxV~H5 z_qF1}Yr6P~4<(;II(Sr0qgt9D<3x{3K@KBK;vs6S=;sy)sl~hBIa*xev7rI?D59}) zfG>B^FjUG>L9#12(vs6y7-^_$Ts&J`I?t_R`7k?9e%_yi9<*tWvS|6Atn8OVpe1g~ z^{k3&w`I(13M0^rgo)w-NwMZ6)M+9gV|A*8$=atJ%2`-dR+zyINNWYCB@Em0T?l4s1a~>udyUhp_)?HW1OlwHNrEJr4oO#N!9!Vr=612kiv7ub-CKm ziIsA*27jPrwRRqC!lfo-0|K`{uA-d4M6(ckxke6po^ONe*&3;~lF*?TQZ9qiUfax; z$%~|KbRjiAzxd`iDOS?t<)_1l7)@T$g#chcpT7|_S~cs9R3y(m5kuB#Wb`mdJpR~Z zYm0koj6E{lH43EBWH1;Hc<W+59x3pj50rK&S8>f)iNFm-{T1tjL|1KKMmsz%|XEz^qn zbv$?7skv*f)gnW;x>)*X;L*9d#{e}@LyjjY_kr>@JlP2*x^N4pP(1$k$o)ee5%Vo} z>5w7<-8)_{#`iX#f3C5$rdR#=aCel;E`^IsR|C9fE7dl@#wezJ4^A=ULFk4Xi)&8s zy`m9%rOU0;-p!YK&UDQPG9*aX- zsKN^OCAK7-$cZ7oL%Q(B+nf3>j-I2?%?cmu3Lq9A(SMv{d&tpN8T-zN>V8`#BZ<6= z7bY(~(;qOpSv@>(IIp%fWajw%`Qpq=J`Q8vQPz-dqL=r{J8HOA)(nKcjOhh6^>J2>6wgm^bbl`Pd81Nn>eo?v^*iIgTHX_uVcHIb4`w>q!JH9v0|H z$|-)_o(fXSUrw){eR8tC#&jAfR%4n(=q>Y)=xiPieCly7_F(UHfV2iFiQu#%>#W6) zPtD?jxMGNKZ?@)de0k`XiTYE2D$rF zlj_onNq3;zKz4r$8M$BWaBY%EoRoshmVEHDSH|Oc5WO`KdS()Y@~L;p1ymveV!OpQ zO>(Y3A|(caSTrNcArp>fy_1`V^i(G|NkkoQPelP59~hv^%a+u!Saz z^h7hgLPn?4aO&t+1C`gzm6OY&R+zql#r!qe?d!bDfk&@+dFzoT%^^H#I4+-gh9^&S z!O$n)7^j}MLohP1r8EkmhAoz0k0&yn+iNK5@Wr(9*m0i0Rg;jguk0n8nu^AkseIfI z@{;Ft;^vi?w2n5XZJS?zWkeLEvb!?LWHz%#z|E_;ICL0+bgof7?xP;yQhomLZMtU3S4Fu2%0(KMDH?rJ z;%(RqH{R4*TqJ0Gtwqo2xu3M=Fq^|Lhlwc|QrYMx$mmAN+1d6^`Sg?i4Hg{|`lpJ> zj-K_2kse!R<^J-kjf>|z`BUpzCL>jQA3!MbC&dsE>>*G_@^rQWNYA^SI_*mZQY2Ft zB1swyC~}DL48Lp%42^}hiy}_bICSAG-we?@+D8?s1O-BiH2nIhH$&!^rQGGUsc6xbmftx6p7!=&u(vH+5w zJRx!2U&91nQ0EpSKSaGSS1j|H=cTyCVy9T>HoDAEP0ow8s?j$ATixqV`=p%%I!@8O zT*WFBJBFlA#q@ns^^a?9c(_CUF8JERD=$vYzBVM7)ioO4h%5h)`IzWZgWY)yPos^^ z;>8yg&29zOE28L0a zdeyIj>yMh;Tj0|hwz=idS!fa@jgniX~Fyt74s)m}=ZW*_9Q#p9iwKDWYI%QlKvUIRr>T*K=#Y~qegA$Sw?uh4 z_X=;IEz7N5xxUln$u0c9yjmPNQnIQbn)tE{{ef-jp$HnCbETo*ZBxTj3iA{_q7i9> zarxLoT$W^jfrb9-j)u&Q3%~NSlNZnSe*f#`@~W3SY6HJCH-7rLHqTVdAJ&0FbMUIL z?On;&{O1Z$bQd2!&0k(xkzqJ(H4hzXEH4*>oxlkj&5&?J7d(6EFnHwrI+)&G$w3%b z)bJ{!WrC1B!A--X!4l7%5SYQBt=k>V5L=Z}n-Y|0Pq2l#z`BU-)iWY^rtFYF2 zw@0R{2?iuLA^1n*%{6_eku@dHR@fTx_+NN7z;u;DRvE!2UnHrhwPb|7vKF$KgajAR zWMxVG;G73J)g*3nnmb%xiHN2axgqI@ma+9@-D%i#W2YJuo5b~;i^g8Q%)mjzc~iT| ztp}Joy;U;`3!?RzGwyKZ-JUaH)Co>B-sc z#=+Ir?Kign;sIS)Vo=nr&$7SMTYU1V;!St?+AuBQfKiOhAXAv?imoCG98YjY7`58m zzndIBqMuSEp70D)5KhN@WbHn6nFk2NYk>yvv`OT*Hsnv)PC)tMg85|(e0uD5^J}}_cyH-Fv zw_47zL^*N1IC!koul}@)n{McS^HJ721AgI|wQ7SGs!qmFJkjQ-GkGdm*I>{FlCI~93L}dUpj7ZRRAZt$4J$b)f8d)v% zsmznO2LJ@s#(fEbRb_H2cxNk;M4d14QXA^Ag8-uFHXF%>jCqB<23V384v5XLs|bN) z6;lJ`IXBkTFzGnr8vL_Q@(uRAJzn=Ty13apvppEi%^f;C&y>bktScry5fLC=Gtp{i zetEgqY@WIQ)vMoqs`EqF6?1d^9FzUUBZaI>5%3yFcA?)LCiz+a$w-YC>Q2_|m)0jw zJ;rh?qMEU6j*cRLy370&frhlJ54Em7(s^w|XVkQfNaDAS&z$L;JyY^sV>(qAguvsE z$ENj1PD-Rhx)E_!;X}m8$daJvQ69xn$98eV;$VEqDDt~v`M|8B%-Zw zmXcfL`Kx&F;o;eH{bA~v4%Z`j8UJ-B8m;+~%LjDY@?ztLQ|&Vs7%T#fi`BjL&G{!D zEl%I0$C%VKcOJ~>Nt&|hR*4`6Dr7~*jWkWmAVnTe{_8Y?ul1|%^;nV~-`0Dq z*J`qrs*tG|1dyq!j+AmVU2X^f)u@n6)-ILIG$SccMp$N>ryd&o-lr~}TYGJ7=c zcDH-*;0o`nVlO{^U#6f>Jw#izL;NXEF5U?Tcve*S9#ABxv|u zIzH`UzEdpeSD~7`I*s9)H-T>K>|Wg5UEAty?DV&KeE(l(j4@er9f&90E}fIO!b{4@ z#Gb4gtDSVKR8^R&O_c~=@*W7IIj(`0Ff!v5LL-oxZ62;|51x6o_w1{zZriI%JgrU`fzA2GUVn1_^-JyYf#uKq zZt;ses@&J3%6Qmj+9@6xvQgoK+z`bTzSR=y{s#uHo*C$dfldrrd@%$R*PkvJesmd= zl?r?lep;}3>SX(gmvo=Zk<7E$y-OSOPd-tcx`Bt3!xdB|H)$UwwkgRgQB6){jX!c6 zHzK(BuJMDI)`j?CufKVA{k2!up1QdEDpv}c&6O@wT9esQZ*iTBTib(`_2SUn?!sJq zsogv{*IQZp*5U&X%-?ZG^DRGATytu2;9$A1)R4$~bcByKG_f z;Ix>U5JsD+Ru@79kXj}B{<=!cqhfGW5nAyFm*|#fPAJNkzWMdX|KN|>yIZ{a*c2UmPa(r zF`cs7VP9$@{YO-A#Ixr8?l*Zmrrz=E(--c!bynuf)7No&!EZp)bp#CGoG!ZajazPQ zKmKf^=+c8Um(Ly-9v^-FA;ds^sU3 zIdV&ur?|L3;qlS-%JJ0$*Bw7}(^Ur!F0^?w@^jA@o7>FyJRqa~U>qa37P||lj-C36 zA6mcbme+s#&$>M0J~u~y(*v+D^;~>y?f9SlVf*mG#)m#aj22u@i*ZQ3<zR(`Q#yn$v%#k8e$BxNSU3F>$CT595vx2mmeKm7cK zSGLD1{CXhkGrmgJAB3@`_y&_plwYt~rc`q5|LqKbw#Ny^<>^iq3 zl#*8Qseq&8ghVx{U;+N2zyFU8EUrv;xB0c})~G+)TJP*`a;>M^nk>$9UAaAXbanOW zBa8E`4mY+gT$()l!eno#i9kzP{fv!z4J8Z`yzO>#w|x2B>Rq>V{@#zj_8VW&Loi*2 z0ht5)_U^Ft^7*5m{Z#AdRgF8}su+T*DMcJ^o1wzKFqkC)*9qnp8sGbJ@6}hije*%5 zn#AjTF>Ks)L(y5}%udmxDa=s(fcJH$I|o-gXEzxrAQ5srg|T~aZSleHH?F&0kEN!- zMGn)#OEAh#@T;=C9c~jod8`={(1VMcm(FiK{`&S>vwQgHiZRN$W|JR?MA3%b&nQq^lNXM3}eHY{_R6GaDL5v* z?V~rJJo+zwO+TnM8J2_HV*66Dey-8mDn@(7+S$qDj|?7qw0U8jbux|D@k+z^&?l@D z=}yfsUIOY6)nLz(wY>L{#kbse;uk*p`fvUjuO-xL~Wte)xBPvpDpR z8z)beJ3DF!Fg( zXz(7LQ#Y53hc12TtMeDvmX_wVj>LRTA`T;uExJG=gSlU5i@8s_GbtZ@cH!;KtN!MP zU-{R6%w-ZT8~Q{|=Rg}9z3wy5t$gbLp8VoJERGy3_*D&FEl47a>?YY39^hU$!_VS! z;~QV?J^ch{ZyN9DN^ZvyL&#opLhop1#?VDSjHA<7sc4RCzUhwn?|#44XF5hg_I?TMGs*mRjKRs~^7 z65nF^?KwCG%*Lm{nni)XJ`&+98&(H1Ajm^1KeO z>E~M$9QP{+Co6|0b4#t`SG8}to*SrIN>q~`lJ=^ug1M z2jBPB#`+c!qL|j}lqWlT;|piEdJlZR@jL%Vv9{4zSWfDdu+u z)-{c%o-% z{<>fM=%JGb%ln@wo`0>uLoB^v$&d81WYqjM(jmvqm%(Vb-5VZ1-a2v&mllgF#nMVK z&%vXITQHtjw*TlKuPY;1Y!sWz)07BjiiDt5+n`RPM2qOlfuj{>9?|DnIe_<;}P9oDCO?H7o#wC0rCK=8G?W zdhq4Xt*`I;>tAXO@SR3EcVMBo;bxu3Y3vGdOx@N^w{#wTu4J@v;Bsv9 zmfuSkw&%a_+3roZ7Pr5}t9W=hMNe%BQ4KthFa)2l$j33Ag~=a%Y4FO%PP?aU>HsOa`JfXwK^eKUTrQPY|JmQ+|e5p zBl>gi$;0Y6?cEr)+Bvl>rJBK+l=o99a-`S_iN?6wGUv>m`Wk)`%mEeZ@_=SJ6=Y#b zaO&enESS~#(iT@!^se1ywRY)1^YCGR;g?f;iAj)uItHZy6xyTK(-}{{Xw7ndxAEY^ zSN*-8S;SxZYOri!9O3T5c7JsKrAyo2`F`=Kf7$%l}2!VZR&3?IfPM!{~Yv9ih)vf_9D{ovkjZA@CbJ#Ode zYdV}&z;(mVe}o6jCXc_Q=wZC+Gvw%eop4d}xdO&)PM(R^!%FMu(bmc;FSXHS!}xNX zhadV?evw;AIuKG;)F4i`t`yZw3W>~Zo_%33mD4RID(#FKHDv*;B(q8r#BUjK(7zRN zY8jy;5e_sKyM!rMS}D>AoEktan=C1g94i;*c|)Dl=~x#-)+35db5o$I#*4=xGf}tk z#4DY*oAveIIPVM)SelI^|G_~o_L&EE34bKQr((YWE(a^*;a3*wi~ zmd`w~``pXxuWxMa@I_M)ImOf3oxWaTsdUTDJSVFkG|}*bIn-i#sKyL_>3y$p(@hIk z9iBV4Gh!g%5+G}9tStFCyng4=rw{+u|K9k~pDW+~eyyGQd%XTPJH#hkOy!&n@yoj3 z`BvlJKi_!h!B=0p&|_92b~#Pw+pv56oBqzn4j-NyKm44choIq-7}0~Jv|h^O2>v`L zYRKZdt&M|+TC6D-m_YTdCY?Xp_~gCttHkVJwTEP{dPY!5p3qMD;38XRSB2|^X0}YI zorRfH--uDRnwH_RbOmGr8wjE)o+6i^kSD;{auCxodftHpXv8;>TSt#i78Xl>O;^=w zgHE?&l$8Ykfk8&N0_HoMymX1nZiBtacfP;)b3c9jlOKKk5C3GuibtWW^CNv3rz{7T zdK>Gnth{i3;m~8;>!vTqd!yYAT?}Vk&Ug7V;b}V5nuPP_IWTWd^Ox4n%wo3Ljlm`^ zH|WQg%7X{Gx8Bx${aZVFa7ZQ*)C^?+UO2lp>OXpP^Z#k{tK@gRzc_um!J>o}7?H=* z(7b(|@$Z=@%kO@z_xK}gYg-#@TfACCcR{#9NTBZSUHkEOU3KH(Nz7afGsMpnY@ZYQYP_K zWOa}w*#(d|u!3ULQJhI06e<{3LCPpuMhvnbI=%u3Ik?GatG9P)eabP-Dv0BTcML4A z40+q=_6955^xb%YTLT$=bl=M)H%C3@+p6f|^2RG`g%{(fO~jN zczNmE_?4IX=Pzyk|AoD2u%*X&-*?X4x0yTpj=?tA32*@jZX!s61Vvg2MVpdn$#AU5 zmCA=C<>ZShRY{f09}<0WmCIJ-RHDd|T~Z=ZG9#N-WQ&q13LpuAAV81=2!PmTu+4Js z+_`i2?HvDq|L5s`-*ack^gXxV?q~0(pSQc;eR289WxY{tOO3EmShKmbcHsUyPCs-< z@A+4C({~O$+;+t9! z&u{<3-?*}}+#BWFO47}%JGkjcci(q;xpKJDk1R_+;;n?{xi+O&8s2?JXIyI!?NMH| zF^j1{cgT2Fzg$1jTkbYGx8FXwZ_n68y|y{`Qkldt0#eSb6p0*vS0E%WsWN zKFcwK?#kWT(kt}!#k-Z&O`Ww;EGX7>C!>VpnDzCslZS5k`~%%r-|WpTch+@A-d9aE zP0CzF53R!QgprbX^e2lpwssC2)=T(V`e>ijEsn&EESIAt8^Va1Bzq!L51lx-pzdF| zJa*&$qrdXSiS>2umeK(;A>tFraXwNnknxq0vOlB2uV?7b4DC#uxv_ilMxMOkmTG>o zkckYNmFFpGPE4z;I|x4VQ`36b$PL$siXuPitcAP=7!4c>MO}T-W!KHl(1un>ovy0t zZbcHKBZ`^wGzw{*edh1~^q%#rJBps=bzZ_s^eBvc!lLmBQyAT8pWqUG#8WG*BS*t# z9GjsN15Y?yy&&uSpBo!pyR-IEP@#fk_e(d>l7vw;@TQ=MN8B-iEwn#&Yj$=8Qs}Jw z-~h=ub)XYekr;a@5s8ezv6l=D@87>Qdr%LDyG7=qhl@#YxGINPG`W6qzm_w4nnyQ& z=oHZK03^ZD^w&+Yh0N%)XLevAnAj84URebirA(AuS| z8!^sbnEly@PJQmvqx!&{+Y^l8{$i{LHIBX6n{`rVcBD6b^Qr!w_w+XTQay4J1CoSjV)GE?c(CEom~L;U zyS8=azxw$T$A)*Fdy_XcaNJ(b_rw6jgeOb0@#$r`#Sks3Hc#Hbt8JLPVz-o+9Ad?p z_L|uPLU&bXW|s}XT7Ir*M+PdTxr|Bf2xP_-=B_e>X`mE+un63ssrVFHfMku;M~hTW zHf^X3GO7xYE+I#b)))$-TW(#Kzqso%cuYY5LYKLP36J5{!t+4->50_~7Z@tW^w#(B z-aGHie*R;p|Fcg{uC3~37|9lSxQ-t2r8f3wd0SDelZPXnLsOlRM?TRV)%7o3aa7#! zt7A_NNSeh6qhXzG^58biN9-SbVEM3Ci%q;78*;2->2Z zosrF*+y2`}Pu?@T^ThL=@o8PA;Omx}{6v^Rqf&HsjJ~uTGt?&et*856c66pd7fRU= zbNTE&lLoulxJn|liRsV2i_ESDU6N_t3?RW-a0zh}DmIaFy%I(RfM)tvtAUQH^Bm>G zkg-s3DFOj0q>&pStAUJhj33&EPo3J-gTwM#%EN2+xx2vRLg7BkivoxJ%X;EaZ`D}r zU%IF(wf)ga-F>C+P%( zwmJi#9^UCKUY$L4>Yji2Umv`2Waq2j?4Muk9^B8`dChC*3^(_gh7L8ENU|d$dLma4 zuB>gHx?yDhzIZ7cldsJ-t=)qez|xN)r0voc#AGrT<86HlH^6VnDI|NS9Y9qZs*7fm zhd^@uhnAo*w#h;PT}@I=rK|*q9G|*r>K;#ILqmFVyUxThi+T8A+|3ZeW9)jCR{Rkn zb#hskskzR-c)2@&siRYiV>+Ebt^4777v{!}@4w+!|NcFX{*BY8X7_bBC%4x}w>HLi zHm7$skB#n}xn=*!M?X9K=}-4Jblk3+9~3>TpgDh-t5No8>(%b7GV>Z%@f@w*1fuFl z|MFGcA#}rk_bVq4O^t5&3$Jb;9`gJgT>?x8TZ>CNw!h^IkKFS={o+h_Vduqj{TJWt z96QVix45e7>WZ!ye!P3x58;gnn#`@B*IU`#8ov83y=zgYf4qp6h2b%}fWD2uQv+y& z9hT)%O$rMHR%ie?8IZgJP)$(aBiA~zCZVIxBM-Row23q*pk0eH#D&@0QZc$zAR^lN zu}H~TDYLZ#GcVCO(2rIQp|7-NN;JqyRzBcJI;s3bYrrD3H_S;^x(a2 zH}8#o;J%aZz2o>hm$u(Nzdd(ZHwo*5fukpmbWa@bP3-Ng@lPksd|Krh zL9!6YMlw}U3@PY!hm!^&y%TLxcS8={ar?>t@xPz`v*`p8tp7`naP0bGV&b_W%So^PB;Hi%T2l@*u9o^aE`769c?BFU6#ERdfJok8+ zqQ`kxr)S4*zC|a}xbj8ND`1s--^Q_`($E3Omx~J>;e7bb!q)sz z7_6Bpu7#9f$2oe)@Wbqbcud+LX(%*&XS&kO*k~PIQLLZ1aop+BT!efnC`IOnd~nuO4Yt! z`uyytKD6@vmzG|BW9`y{rWl!_L!_}Ihi2b(X3w3c$24os&F{RvM$fLSbuV7*oI0v2 z?fr#io;OfC6>T)m12o-C+cCpk?$&}5Y;9?^f8YIjGe7TT(;*l!Ewdvp?uoq(d;JqX z7-IwI-jM8;M*%^el%@dSf>|*m0Hx|{VL<^>q=lg=fFPMCq(T>j)Cs5}(3Fm4bJ)BH zxJHX;{gVjxXI^YL{OY6M@`Cy0$sAyfiX7GxPBe&OH1fU1QP7M$9GZw3gR%0gJthx+JGp z$>?ye+k5)C?#yI&a=QQaLYEWP8VNYmB4ni8MNjAGPH&`(xhQ;9Gwyx&`facIF6?YF z4x*K?*>}`nG%zI;3VRRf1sYDkVX4LWg*#P7-_Vp>gnForL+1 ziXL52(HfY!Kry5!acd%%7FnKUg%G!-xO4pVIUQFGkM$(2rxQ`bJDY36E6ZJd_g-g$ z=m4ES%zAL@7s&x+zb(6@%C!L0&O9{^flzDV@ zd41#LslBI9>#fsV!H`MZ9jjlM9V~V;#s|N0`hrdQH!fgSVQeB@MlFTPr$UG|x@7I| zg%wy~a$Q0i5lD=h+M13okcK-T)=p>R5Rx|v0I}K-%_u|X@Du z6&^lmvR8a862XKgLmC2Yjv)F*_&f8RTTXP&zt%r;ynFgC6pZLZ@#fITxSng^nHx^Y zm{%SbN>42CK^9Fzx<^Mh0C5d9R>~GDGi|PvQP3~cD!deg9)xuFphS7qr5Ug{JgTv1 zOOc~5&Gq!)zT!p`6E``>ETid0_qX@wFLqvese9~@?yl@zTGUY_?oe`$8hMK{t!HqM z1MG>Ik@TF`)s^+(`|cZ=p3z&8IQ^s0!94vOd8J#hmzV*CyeJ2t>H~`&HZY(`8$?w( z)QlgD&bsVyq#@!TIg%iAFIR-ucRIC;p~FJ!orl-oiLS{V!mNQl9L>gzsBvj=AH zzLU^VD-^*V$3p^=(yJ+ycra9%+d*s85|>qoszEq8+K3q{S!QHGZq^V3Hh=?DSeqKF zO;Slq0Zr-*A|MSxmVinsU~!rq@-h#usMtKkk>_k8C0?35v!{Rm`>%fGj}OdFvv_dp zY=qkrNgqi#2S@tf|7QOKpXpA{5JS4q&tM>P<8-i8bFUAs zSLm?%Vd>+Z@DhiHU>VI&OS7@=2HXKV%@#|zM`n5^mWdq4>BcTCxwiF?0HkZV zS^(K*InX0D=E0^Mrd)Eng6&32olK1$3y&I;S#ZK@Kp_llK{j>)*FI#Ci47JIy90rS z0;g+TlEV_wD8K3-B`Wk*H;2!j@9(*_vpCmz_OZ?fKJEDhQyf6PBSLx!4vxkZ zIz9|$-6KfD)-xkyxuE9uoE z-+rop>8tLQe<~jd;;buy;cby7ZT78IZIu^j25aS7r~)Jn&vV>cHpp zIEHVgFxnM{V=+n*ffDC{tVbK*rU)|AMiyYVG=zw`@|6O5OpZKzttFuptqU?2f+b*T z7LE%-+2T~HDY~i!U5Q;zdI9yL=VAcheki^2SucD0;0JZP9_vo=a%77MQB{;MjKpLF zT0Kga0^w^)7itpz=>!H>`I=!;-_gE*gV)e|(=Jf`yW{O&UaQWU!2hkG~En4)IxA>2-WWG zwrYN|8zf5@Rup<-`bX#bcj|$mm$}gR{zr7cB?%s=kfcJmnPw!P$8Y!T%Mdgv|$#~vCz zuz$!OJj7irfUp9C8#D1o2nAdbhLQ=AO)Ac;0l}^X9+J{y>F^@o0)i%s?I}zW%~`2K z90Cg?yo|o=%Qdwc5dCYdg)Ip!qR27K1X_F`l7o59t7;I^s|oMDck`}yU0qt^)(ibP zLRC^pAX``-I>RzfaP^rKfABYKcyGfQt{bc~>eWM-hUmIZ}D znxjj|U2+CTI|(?z3@*lg6|!%!9=NiAlo~)1A*MuY`FA-s&h6|1H9iHynG_*JHG7bK zD14!P+dJ`yzUKFt&n%8juJH!K@Qzq>PO)t$r!H&bjvGX4e^mvmxOL+EMOX!p&sv4io`OY-;r_vh-_vTg6f+4!3uuIj&{d|S@P-*Ikgkf|HTK^s3z_;? zJ0gjqQDesyjJ8P(B#CT5p-?xLU@|DQNFr+6f(v(0MOSbVMfx-~#2}=7cX(vthLgh| z|HS2`bzWX*8P=c>p~3<$0$tV^jpMOGRsR!9z25v{=dYjZygkotbI<-o_gjD5y>db7 zcyNd7SvHeE@?mg^5s7U791;a0*{v92!b^1Vt2meD```FV=dpjMTmQSdLHhaEdv9LU z$I0WC?65p+4&Rg=Az{&sXpLH+$!mUTZRjH(n!Mo#9lG=5Xd}*;+i#K*mP_^Z5+*C* zl_*7yn=&;e0aQUyc)$cI2CxMKk!*UbCs+amvs0jhxeY1i76YOmtu!PlN>em!kSUl` z8XGh)34&EcjVISmtWMU!uFl->O+T%s_kQ%lb1%KP`0C3e`=&?Xl3+wBez;g9CRJUR zgelI9xi7*6X3RAPS0CKXh zz*ZRYlYy=I1z}0wU0>?I{A~BRCwqDgk1mpE+IszM-Sv!Ec4-r5q zI_a#}FkZR3wszx9hd%n@k@3k9eUqBKE3Te;`7fp6z_hs8NJuf1RyHY`+_VS6#Dfa^ z#9C6)w?%-e!hIB0h}{yQsw;Q1iBQ9Ym7v&ysxnj!M32#ku$Z9cT#h(BfH@o_%oj8? zN{HLbQe76%!LVLEu=hXznZ^J8H^)~u_D+uJfSce61!79M79CkMHO#?8bqE{Xyf^g9 zTiwe`ox5-DoH*3|(No=XFLZ9XuYcp+ojnI>tPFBn$l$TZ>7bB>TTO%A)c_N@#R9Ed z>AwD4=cT7ROA9*x-C5h|zVLEyZcz^lxUH72;Y!jaC^V=cZcB+<$wb4PZ)P#)WFVFx5g?2pu*D(ZO`y?$Wg zefq z+&jkcBWKIB@b|nQ%MW`bc1R3UirmE-IEg~LEeLGQt78kVBUw!56A)VmlOTAyJpu(? zhydMUt6q~t2%?V)4KaF6f?c$r!aPKJ)D8`1(BQ){E0$^@BB?q)(x;sC&Jw<2IlQwq z`+@h*z5Vv(#~vHmKQpO!M$;sOC`7PH9CU$)%+wP7f?fk`n%;Y%nBCeLI(KpC!ew2{ z?A&s)e`Kih#`B%CKj`Si9lCsWM4twQhtc55&&I?J?nN=Tq})*d#eJwyyE;jBWSvPfA20R_Qz zP@UV=qnUcds=x0upSt+gJM(8>J-lybg0~Pf^3@1MX>^FASlrQN5D88_T@4va@C}90 z`R+THy8HKVOP4N;53hF5|ETlU3wnpXUMkq1o>ds>4V9eH(bWlAw4){PW@lrqv%1(> zS?sJY>sl!huTTHzoca0r&f6FJSC$xcXpT*wK)g7#WeH{qV-oqYpb{1a`ZEga8i3h=g>X;#1&sTN<%TuRZ<>5+8ZGBX0Z(@<>WGV&4k1{(@?Mk z$9i<4a)Lf8aG8>QSZ_n7nJ^g|1(j=|WskubjTTYa;0_ZoCCrgf^lINjH`1-p1^kIk z!1Sl73yTENg|V2$db+I5RrI0W&dAjC{x5!E;kW4^!wWeoN-+M?l(lS2|4 z>ZMtVV`asJDGg~DVZ?KZ7Z-=-7P}K?y9f4k4)5^@{%%M*ll+!;n)G5L#%mYy1f&KrMboE+%q-FROZeK1KH12N*hv*!;`YRQ5%U5 zls6O=EeeI9we`+h^PC@=7}Mt|_y(C)GrF3qr5Kj!a88qyo>Jb@%dGT@!oJ>;zrN{P zZyhfwG&I=_f)rgyz&K*jCODwnUo9sI$g3LlVxxtXjrq~Z{a^Up#L=T8<5L=bblsFI zU#xrL;urBk55!htOA9HItR8Q=p(u7DN>r%zfZ9-CG$Y-JwFQnap~04v)#%U&pfKdYK~#BUOveEuDBRcWtev@WEFZmK?d;$QtE?z2uof%K zQh2mlUaTqBn$`3Uk3{PxgMgV#Mg5RhfN(__!!08ZJ)ml(hM^)-vfdM*9Whcv)sz|` zl@m=t(10pgc7QP6*S(ZheCnEmo>t+4gLXb&<<`sEM`rXe&ZB>Oer$41A5G8)PKh?j z=A2=4jHJ*V!2ClE+PU^2YF}weR2XG4k+346g_1>4;UikDgNY@=Nx*1^qM9+O2rg=j zg%Ns7ut1v4fAohD03-k_Od`=cI$~eh+F4j#*P}@@KmYle+ixEkn;?38@Woj%xuUsr zTHJ{5YKpRVY8Bar;!02o*9V)ut5;9!yaNgQv}tQF{8~_~NiCJEh`*NGvLtqWeLY3R z26HnE2H>i|0y;~kq#STY;BqKo5bToVLkv*tcB8m&OgBhOXemGp-F5ry-~Gk;iJ6Of z*O(4=qi5Z#;+EA+ROA+xAVom-r~ML{Bg12jkjcTBM;Pg0EU~GEgw1eff=uSZNyuoE zI157rF{d9iIr6|7hMUtRwb*6qPO z#@h6mTRVUE7Z?8E%bkU}?dkEE(GfjzDvNAYx}vrwl*lgkBJ#t{l^k^lk6te15+)HC zE!Gs4ED+O9L4{EnB5&wfRSYqSirLH!)6jqUcjmwJ<@MKJ+uk#^Z(P^$wSGbw z^7#`PqC%Z=+JM7=rgx%Ef3ytcMDt zkEs<#dbh|%PbZId^sUd0W5*Bt+~+1v9M>p2Iz1B?O?}sPJawv*Qe~)))+v|dl+R{& zqQNr5C}n81l>vb|s>z4(z;xMPACI<&z#xO>rG_?(g+deeL??v-jp#N*py-~t`2`(q zsxGvh*NAT%jOGEM$sdNpX{@s77a*)oVWTzL&-`vzomWH)P0=N67a`JTdM1t@IrQik zFaPORFFpNTeTr@G_{b#Rw2@o!tZ1wRi>MJ>5la{neGDXO0YI!Z2&^);6zlZlkYWfy zb~K3Kv~wj`f+7T^*EB#ZU33`JSl~AX=_5MY-8Xl1wRro^+Yf*4vlDt%@7ToXR78(% zIAreT-m$#fKZYM#XQ?H6!c=qtc4OUS`v?=2(ic&=Zf-u=zq2zpH=m7>^`QigXo=h0 z7GS=3urR=)t3*-8XVnoR(lNlL`OA9apFb;Vi$l0@!3tjmwY9a%1C~Ww4MLX^f+$I` z#uX9}Av|Q&#{ha0`}Q6C`Ohz$I(6~ie|7EZ+WwjGneovveNPONiyv5`SeC?wT@n@= zisTp<#6mEG1RbSpjfNJ5U>YV0i%@228*ICY9rqs+B*tZjo#X-1f>}o)+r3r2WMOq{ zVW&I!$&Vj?=tG{cCUm+>1>I6tqDOPL?pTsdaiv-6>s4C&qLZ?c`v2_94 zT`?=r!Gz~^nqu?q3m0`)1Vo5biU3GfGpn4q*v4UeP*Uc5BBZ@-R;!$wTDil{~pG(5G0uxwOFS0#{E zL)nTg&mfIJ+Vz<}gyIfnQ0#{2z;(da*Jf_LdE!?dUHr?(=N|u-o?O^FJ+VihB-240 zuCQgIE5TCC0WL6I5YA4@jB0RTGPe|uX67e+;(CIkBbLV5I0TZ|2%%xeUz^vBO{?3w zyJUU2*ByJ{fg=w;G=5;8o~P8QLfxkk7uz}O!zK5)V_ynl>`^SZr8M7$kn=AWpd{=J z`_Zgqcvv4SM4I649F-^!BvH-H&tJGWrx7USHTZ0i0qrt?_6Q^)rS+*wdO}(h@bu!< z<=4-iyW{rTv<^yP#hTr+(}D^P>l3O?MNoF7X_-AHI7ei@*yxjPmbJr^U9xpZS(Bw8=E836WXbVG#D{xG5Nw9X{gObZ3S+rc77HFtE%6kIcwF3RhL&}%)tz;64>>c;ko<9k2(v6(wK)5B>VJ;I@D zGh;g4tg(kPUU9u$Ip2@f^3nL(CL!q zOls3$K_f1hGNDnHC|0Vh%mYXp9Y7E%q7*keJ*cDf&G-`3#^&1e-tmtecunR5EZ?kfOrYz#q`jk&*8`|I*yt+~K3g zq6Hz__K6EQr59k1eP*}`jV(-w9&nL4?O(P_IvML`R@0Zo_=<1er`)ll8fp=$wpAM~2yX1lj_=+NX(-FM{PyA(Ql0f|<} ziWpWrUhC*N%I1K}7b_G%eo#bOWEuvdhqeZUijyD)gK{d{fVNq4beS>UhL{D+*orQX< z%huSTLn9A=Z2JB8Z@l{Y@-yFCdF}PFtE+l1{rH%=uB%CngEjS-bDsA?s(z*Y)VN8K zEl7N(AIIV*QqU-v6;Hfeoa1x7E@4~WAluZz)Qz`Hy!)QnJI;*l-AickQ;6EY;2q+kOo|z zHcF>=DjLXTRc`vy1$>=3mtMV^USI9iH+Ogo{rGtQ*opDm&+NVZ%*4^dS{*BN6dXhh z3!QkX+M|xV7C(ip<}4z|Ln)J&RA=9K%it(y-~;il7UXY()q>RYR8>r)dGC<%>C0Jhacxq)~Ds?v(CVijBcCq7i`bj--eUp)Tg`Sb4_ z(~GWSzey7F5ez9H>Wwyw1B+0bDTubjP2mX$0z-_v>EJXQO!Bh3G<4YE3LHKD>sMqt zhV%+A9n@$L+R-a}c63h>UxA-Gd1B)Dk={oh+?>C>e)f&6vv01SztCT}(p_8ERW^l@ z`jH@{+p&!wJEWHH^r+NvZ;}qHc6Kyd@jhrR&h+T}?EdbtBV#w7-gElY=)nWTO~}v~ zGdiNNN6Q^zh#`j)JPIj7Tvurr^f=m*J{CRF>G2fBB=qB#O!h*;abHk`6QWuuy-gC+ z`>&AtFa=$9f)$g@?Nx1VJniM`@XA$*R;Iwib$Y6t(dVz+O+H=SbGeZVFsf z#(EIq7@yr&*TVQL*O0!u6Vn-QCEM1g)OWV^CPUs(#!_Wl&%F*!o;axsPn{1wxV@oc z>nocV=e90f+FrP_wYa#WZzrtl%gEd><`drxZ_-E4YA(}*p;Nl)Y;r{R3LZKzcKFcf zkwatq_l-gtCmiG+fX}`P7!jxheUMtLAVDKp8c?^lzVYN!KYZn^qCNB$B3N6g$`wrI5J&)- zo=pD%Z57}G=$;H4sc7>5qp$q=&ph(+sUfW?;y5UEC3DJeY{fy=LbDwL)FR;&N@#(f zE(@(d?I)N^sGCxYYI$E!tRd=@IBBGZ1~n;Y3>sF@^wfX)s41&ljT?H>Z&SB3>+TNS z?<41Nt#Wd5^7skKfM#1?x!>w-Z1B^&J~hg4&yhM;QK83{_4)^`WAthjjVLT%N=M2B z7GXg~lz4SKtOco7&Fo+vbu=`JOu5X%)WS{+96c1mCA5jw(RMPYh=nNIg9%S;lH$22 ziarJu0ZPm@(x6fN=&ID(^3osv+pp?QevJ!~l_sDe1rLVYBE|ECWfTcB#@&dn+`4;Z=+2sQaOh< zq$>s0WmlxJ97&$M}+1#EPTJmmN^Yx5aeZ&3Laf zqmLg(YpPF>Urbft(gX*rjsim?A;uYGZvJ9@qcW4b$_f;rn5-x$$bvJ223d$xLg=pK zCet#=qGI}8WV@<1)b_H&iyMIUc;xHfa)8T8J9@9wo>sxbE;YI)&}F#dWek zScget4HO+vp(2WM zE}YurdIUqEC@gI0EpPCP7!*&kgjn-=zEbmyrdKPd+gKPe*l^*cr3ot&q{f_Pei z59!d-PH)>)VFgnIm6Krw01T{j<^p0|)G*bh>QU8zSc*~Va6NstvA(vn_&ZcbA1o5T~S=Z#4P?HE_1PW*sFhEpHGq$qoe)qAluYK$3uRZp}XFvVP?GZinHZIN9 z4ZHs7;cQmMy{g!ji6R7{Ew%;k?kj}p4oM6!Y9efq5SoZ4uU3d8b&%&WedmdDkldY0 z42e{;l43=xMF1Hz9G7b}g23W}GDN#Q*nzRJn<)kuNHBpwiP43E$C!vJ0*HY}fl?@A z$Voxo4P2HK#7C!*yE&i&Q{4l+L7P`>g6>L&&=Mu*VR_aC z>re1z+#nz{P0>h>YG?`OqTC81fP%|`D;lR3Ux12ou`v?R)mu9||LnKF^uGJSQAaP)}RQuYzV0D6w-M{!NCfMgP7bDMglrm zIC-q~3}4EPc*2VjMnMpR5WIvSI*^{K&=gH#Lrf4rsMAJj97({9YJvu0(J*Z(w-7Q5 z^W_)pEf6Gz`67k}8xrdx5F?-q`ygwQt(60UiEeTW%v0#0A$~OaT)uSS*MH|rIw=$L z7jh75DO8fL6u=cQk}2!F;HciVr!8s?xK0*`;Gr!0ywUlKp*PQ8)EV9f-+i|n#HrQr zEO-ntp)Gd8KvN3ta`m8lTF-ZS zfx^r}L2DOQEgU+j4ZaauZ-5VoPuplEuHz{Yu!7dxEm<0KV8t!lnigS1LM(c68B_)& zCU-BN8I7WuHmul|AUDVDb<16Ksrn$Br;Qp|{1YUS1%w@_Vv-N2l9`Ot;esB#Utir` zx%xYQ@c;hb|Mt%{{2(zURdrEUH3FqpD*;mVU*68)*&(F_4d`HwJfZE45PAv1Z++=c z9(>O|KlPypIquLcQMs!zNM(p%UbVyfMUm3BOB7Wlc;g&>V2Y^PRJlWQmc@d{cNpyb zF6ceYvC|@!aV1V5i5fCas>4m$BTgx}f&n2sY?laB*ru=yK+I5H))>z)<7IMELvOC& zL#R@#62builm*_V8nI%6FkajZa*EV!CZLQW38=zzv`W#ID@M#oTbrBfD~s>E`NpsQ+W+?Ixp&6&v8;iV zj5bGQrK&l~02}Y?+`XnSKtZh1KoT@skv)7}Lb>$(rTKR*T>RLB_iFNxNrN5w;vz=@3K$pxl2{C`Y0~=K*2uq6CWW7f{{L1ee4FQ2Z1? z5lmHq~ix2ENpJBEni)_eCdDrXTSB;$Df)Q_v%G`%kUA>s}2f=RjH+5Otq#N zfm-&3p&|_9TurtDv1z3teVqt;;nj0%Yil2P&z5D8TR<(=-2au%%ciSLCL|T8z1KjdTCnH_M z{L?>pN#}X*f7cnIS%k{(c9r#;p=DgCa}&q{9l(@c)g;s;s1mh*MMOf70o){PN?qVW zZ+ujfAjS;XG@J;{_~T4@gb_efDO_A2hT?||C#tB)aZclb%GQfXGBj;*4GgZdUogg; zF1QlyA{TG)(|C3ED~`st1cfgc%~x|Fr>B8KH1$7gVm*XnW0Z-;>WiuRC(yd$(dpNqon+V5s-A<01CRKFi*QIu472uR{vs}MWYxM$?Y;A@Yx=dWZ4p= z*7O7Ns%{%%0m#&^Nsbp)8H$W!k59C=M2z(~Xb7FqggOw)xPK7_3wcR1DY^5^&3I$W)3H@WEW#Fw%h%awL54=SGNx=2*2&R4+Q_ z5G@M+9gL<>HMUi^iI9|dxcp~+RcHl7-eji!2^vV7)GilFd4VPK*7nBQ>c-0A8?V3i zkN(MT{{EkQ%?IsfP$n@gHQ~7;HZ@z5)q;T0%s{FUO#soJU8(ZI7~0wtE&5VKus+K- zGV;>vZ$AF*XHOiOJ$39Lr+@|Z)LPh#!;f&{IYInFk1oyk&@VIn7jgkiA6|QP$h&BTGT^eO9~_^zyeM^*2Si5fWSc} zqzJ;Yc2c$J8qQ%cZS8DrY;3G9Z7eVT)#Fe6>aYFAH=llPLMIH(ilFLGAR$3{{6ct? z-5Ug&sx_(HXwZ1ho?N(c<;)F7bhcXe zR?vVLa?=3XVxO~Z3l*K4X^F-Z(LrT?NL*#x#;Y8XZINgJ1f@$66le?);4lL*mgGDv ztDK32Ca58X({(c@!Z3>hrDO}j4A+C-Z3t}9p4fs3$zEPX5iL%;rwceZ&!b6eOV>zO zmbX@}zWv6z-}vp{|FwVqyXWT?bg=^cZIi`;F#Urmxuuh(qOhz~gR)>`ETvUB zS&+DpD5jr~QY(^mib-dB`~9cB|Kiu5cxI&AKXrI^M0b7ZiZ`O5M5mjSYyl2S$YUrn zP0^X^ZF&enPQeZoaR~zzScI8K1#6XBO5ltoSls2tT72M45Hd_t=mKsT!IxqRy|^1G z7F8e^VD&`=LR2;puxuPtmXol6gg1!>1;FKC-emHs?w->r=JmCeO)Z$`FaEnf``Z8f zkN=N9{rZ#Q;zl2{l37>CwZPDZ$U>s6egQXOR>f+PfK`U6>FGgm#s9cMo4l&Q0#X8k zCMx>0GG8Tr-#xee($9YS;g3Fe=3IKQ6X1(sQ@{6b{_M%`{!mo9ygks`)QY0P zMpw%qcLjpl4)ZlEh9^tc0TdE3+oD5HnCzwiaJ5m)z?w3GJRB8N2^rYv?N?HK_nDji z#={@}#D^ca<)#~^rg_A!T))&Yzb_7?Gua?N)MUkwBGqokn*qqJ=+jiXqqH)Fris{C z?cCjllwR>kX)Hy?ZA>9739Z+!2?SG8Ig)BQCzF09N}S-gE=Yl~e$>40U;7MJmh z)etqdKqQg5)6&SPscB2BD%yo?cndTrEiI_{mLg!+vg;L#+y{4HcF()-I`h#7-+lkR zcinjE`0VW7Q9bX%HIO_7%tF;kM>hi@Bn@G|Wj6}&D~(5~Bg@)rPywUk%1+(*#11o^ zjW51%6xLCuw(AHavE=G^c2*!irK1tbLQj0h7JNj515aF}f9HG8KmO#i-~Hjsa|??)+}%Cuq$Q23Fga5j7-c!C?vfCNdraOCM+}9G5FnNLPSs&7GIFJe+mRry#%AD0ZCoHXL|C6<45ki_4MsG-EjKUvEzpi z9@w{cW{+N?Il-I9^qf+hxX|+tj3*YZJXB@#ED;mI>R^w0NurhZ2t_lc!H%bB7CSYv zy>G!+%yn286VgHo9#tI{Zvv+r$z~N2R8%C~D$8i#Mqp}iqrS9MZBx16vTa1txRB+I(XCI=5!1BW)J zplcWkJOxxb!z&pA9Y6x7rpmah5atZ98&ugEq1SMF*H36mP6*>!VEG4sX{|RoJ~lly zxo2i-dTK(?C{0d`>mfjm#ySHKK7rr+VAWD3mVR*e;X-$YZv}QYrKw_ZC4p_ zDJz;lO`@o{ORWx|hr$@fV2B<9VLfJY@a7(j08kDUl&Y@7f1RN*2-#L25g*1?p1^1~ z)4lF`1?R@*=JM*=>iWjg%G&bE+S?2>}%18o7nJLP9cRYJ>YBl}4Y`b`{3ZTI|6fC%ZC=VswYebsiYR z8M2ZzH?%TQZSCo)q6SpM=Z?%Ow^J6XhN3Bj{HA{WQk7o|yXdY954|nJb}^Nf74LzT zdn3iX?obAvcE+ZigKVd;#H!^_8qXf$6x*C&Kw*SCYT*)61{K=sgp zI4bPH0WqnurI)?;i8<5;KH0NUTV3nyCia>j;^Ty)sve9{BM3^PfN~&BG$f)?V)_uC zT)WyY;|!6mR2+kwrC#HS(t2AGVoQ!))p7x)j&?UG2&jurX#i;m{G}~mO@Y~&EWZ&W zeAQv7@hR85*Ue&Lm`V$C^d5snZZ!HYJj}}u~~dcY!YS0M%GxbQsH8jaf)r$ zfSLkB^k41CQmuQhNeSABt)i&d@RV7ps0X58@U|ey;P$qiT5JvBO)Vs*LNlihPt#Rj z0opvQa%vPb7DrecW3mR5EQPxSkO^#%=qojaK`TRAA)vWYZgYvL5TMO00}!B2GE#!NJfVx9rROCvLg8{KJH@mm#H zq$jr+;3-nfg)aRP7L{TL2>q0Vrr=6I3J9oCHA@UE0}Vk=q>#`ygC%3}w$+G4O)m^8 zMfVm!;u?asV9eZ&vAED{qoOHLg<9(?(!C90%Q0D%*$hL_g(8Tc_!i*B?kSv6~>DCRw6Ntt!s~14F^ZR(Z=-4>>I+%T)&Lpgj<^%}^Cl03oIj z%vsAQ50=Qm6d_SoUV$pK$pQl;C=@`=NIfVh*bK{T7bMgexC;Y)$XB6-%VIEAf}z2rq2LzT)BQXolDb%d6vR*eJnr9Ubq#7OnF)dUfgl*J@6G&)`oaIiLQH~z*OIq2p57NyFVc9Jjg=0gZsw_jD8DbX@lgXieg+;i!^Lbf>Kab zz(Ovl04cfwT1#aN3!AFxq8fQ+R6&r)RS4|0?FoIs1#zz005k&I?Hv@cOLDL%uX;ru z!h;fmR52*XWI-7$A!;{K)=kK3m`kdqTzR#Ew73+~Dzx3Au&mV&DYOz}vRS3Yiold| zifs%??(4R*9xXwXEM*j}5+uD{ZYZl{6NGu~JVv1F@&-`EA-vLra8>KCr3Bz0eZv=` zU^h%vrN{$dk2UnhlSR*jSgI9K*o{iCHX$=84NV^gyp3&1qF6;_p`g7cGhbN)feAqw zHfmE9Tu{MY7s4W`a#f(*VhNOTCY#`_2DvKQN?r*DWx!wwg&!79G6a3FS21gvKTuM$aTl4utxSxV;Efce zre5HdJqV^t(gh9GHKhT8MO6hxgR9)OSpx2gF++AM5U{|tC1KNUyXrnuXiA%-UUS=% zG)0z$UO#J57J>?@g@XA>+aPwXELmeyeAcih8bGD2R1LD0v~E#Hg{vDWX5J0V4lrwb z1S)L44^vv%)eEc^3{Wk81>ksRwCS@}Q6<2$*4Z;?QE+%y& zq>Ks;y-7-S_n6TZQp&bE_Z(W}dNm|925}LaK+iS}Or^r!a78J^1wk_8CMl^6LMmVY z)uJiIkevn>U`uQk*$8|6h6S!g*#;m}|M9EkuooPuA}N&=MRlMH(HHS(GlQ2Xt6RjO zvdChR_K?*GGCYLVkZYzXfp#@Y8iCZ#579-w$SVe+1I!Kpp#$jHvq=RKHez~MDt_z? zgKbEz*ow_b=_;0@8jeWK{(%IcTG%21pqmXFcD1aJwpV$^W;+`#K%L62Q~;P`n+k#T zWK#=~aJD1}-JD8ZZ~)R*ZJZe;Cqy;v8K5z1lCTLmE>ybc0t9qB{|UE?)|vZ|nFs&? N002ovPDHLkV1geR7GwYb literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..89f9b6b7faaa6e650bae749fc64a228c9cde1aec 100644 GIT binary patch literal 92976 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

    I*|h>eysFSQt+i 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 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard index f2e259c7..ad5c6556 100644 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -19,7 +19,7 @@ - + @@ -32,6 +32,6 @@ - + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 524c5da4..2fcc9d28 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -44,6 +44,21 @@ OpenStrap connects to your WHOOP band over Bluetooth to sync your health data. NSLocationWhenInUseUsageDescription OpenStrap records your route on a map during a run, ride or walk so you can see it, colored by heart-rate zone, when the workout ends. Your location stays on this device and is never uploaded. + + NSLocationAlwaysAndWhenInUseUsageDescription + OpenStrap does not track your location in the background. This permission is linked by a dependency but unused — the app only ever asks for location access while you're actively viewing a workout route. + + NSPhotoLibraryUsageDescription + OpenStrap does not access your photo library. This permission is linked by a dependency but unused — importing your own exported WHOOP data uses the Files app, not Photos. NSSupportsLiveActivities OpenStrapAppGroupIdentifier