feat: Find my band, Broadcast HR (phone as BLE HR monitor), Strava integration#20
feat: Find my band, Broadcast HR (phone as BLE HR monitor), Strava integration#20shoshiiiin wants to merge 67 commits into
Conversation
Added deployed backend URL to the README.
… honest upload count iOS only ever synced on app-open: pauseForBackground disconnected the band, so iOS dropped the Bluetooth assertion and suspended the app in ~34s, and the restore central never armed. Now we KEEP the live connection in the background (iOS resumes us per BLE notification under bluetooth-central) so the drain + upload continue. - Add one connection-independent flusher (~15s) that uploads queued records and retries anything a prior tick failed to send (uploader retains rows on any non-200). Removes the per-record flushing that was tripping the backend rate limit. No cooldown. - Show the honest uploaded count: read `received` from the ingest response (the client was reading a non-existent `processed` field, so it always showed 0/N). - BleRestoreManager is recovery-only: event-driven re-arm via setOwnsBand, and the 50-min cooldown timer is gone (replaced by an idle-after-sync flag). - Remove the 15-min WorkManager periodic task (iOS + Android) and all its plumbing; Android keep-alive is the Edge Tracking foreground service, iOS is the kept-alive connection with CB state-restoration as recovery only. - Ensure the flusher + Android foreground service start on every session-establish path (init/login/reconnect/resume), not just openSession. - Cleanup: drop the workmanager dependency, the BGTask/processing/fetch Info.plist entries, and RECEIVE_BOOT_COMPLETED.
…palive Fix background sync: hold BLE connection alive + persistent flusher + honest upload count
…sh, full metric surfacing Reworks the app around per-concern screens reached from a 5-tab shell (Today · Sleep · Heart · Body · Workouts, shoe icon) + Today gauges. - ConcernScreen: one reusable screen per concern with Today/Week/Month/3M, a GlowCard hero (display number + delta + tappable bars inside, readable labels), inline drill (month→week→day) keyed by date, SectionHeader rhythm — matches the hand-written look. - Sleep tab = the rich SleepDetailScreen embedded; Body tab = StrainDetailScreen embedded (added `embedded` mode to both); Heart = composed card (recovery/RHR GlowCard hero → HR timeline → full HRV RMSSD/SDNN/LF-HF → stress → zones w/ legend → nocturnal → respiratory → illness 3-signal breakdown). - MetricRow + kMetricInfo: every metric shows a one-line "what this is" on its own full-width line; proper padding (fixes the cramped vertical-only cards). - "What affected this" is display-only (no navigation loop); per-concern Records + journal Patterns resurfaced on Today tabs. - Today de-duped: gauges open the concern screens; removed duplicate Sleep/Day-strain tiles, Stats/Activity tabs, Lungs tab (resp/SpO2 → Sleep+Heart). Profile via gear icon (wrapped in a Scaffold when pushed). Back buttons on pushed concern screens. Numbers on bar charts. Removed the orphaned MetricExplorer. - api_client: /trend, /day/heart, /day/lungs, workouts endpoints. flutter analyze clean (pre-existing info lints only).
…, detail, auto-detect tags)
Merge pull request OpenStrap#3 from OpenStrap/fix/background-sync-keepalive
Metrics hrv explorer
…eens, always-show illness watch - lib/ui/concern → lib/ui/screens; ConcernScreen→MetricScreen, *ConcernScreen→Sleep/Heart/BodyScreen, ConcernExtras→SectionExtras (section: param) - delete orphan screens: sleep_screen, recovery_screen, trends_screen; drop dead SleepDayCard/LungsDayCard - Illness watch now always visible in Heart screen: active-signal / all-clear / building-baseline states
…/off detail - WearScreen (MetricScreen, metric 'wear', minutes→hours) + WearDayCard (/day/wear) - home 'Wear time' tile now navigates to it - MetricScreen header: title on its own row, time-scale filter below (long titles like 'Wear time' no longer shrink)
…reakdown - list: GlowCard summary (active time, count/kcal/avg-strain, zone distribution + legend), richer tiles (type icon, when, duration, avg HR, strain) - breakdown: hero (duration + strain ring + avg/max/min HR + kcal), HR chart with drift + time-to-peak, zone legend (bpm ranges + minutes + %), HR-recovery curve, output (steps/cadence/calories/coverage) - live screen notes server auto-stop; MetricScreen-style left-aligned filter
…etired) - widget_service pushes hrv + hrv_baseline instead of readiness - TodayData.hrv exposes baseline; /today now carries it - OpenStrapWidget.swift rebuilt: TripleRings (small + medium), HRV ring scaled to your baseline + colour-coded, accessories updated to strain/HRV
…ow, BaselineDeltaChip, FormChart, CalendarHeatmap (shared components, built once)
…dy·Strain·Sleep·HRV) - TodayData.readiness/vo2max/fitness/form getters - Today: ReadinessHero above the gauges (abstains until HRV exists) - widget: push readiness; OpenStrapWidget small=2x2 grid, medium=readiness row + trio, accessories lead with Readiness
… rows; Irregular-beat watch card
…rm chart, ACWR + monotony trends)
… SRI as tappable trend rows) + metric info entries
…er + streaks + milestones + playful lines + Home live banner - picker now starts the LOCAL live engine (live HR UI + iOS Live Activity + global state) alongside the backend session, opens the rich LiveSessionScreen - LiveSessionScreen rebuilt: ember core beats at real HR, ember particle field scales with effort, zone ladder with 'X bpm to Z4' nudge, in-the-red streak, zone-up + milestone callouts with hand-drawn confetti, playful line engine, haptics; hold-to-finish → backend end + breakdown - app.dart: persistent _LiveBanner above navbar (live HR + elapsed, tap to resume) shown on any tab while a workout runs - app_state: workout carries backend id + type + maxHrSeen - removed the old plain LiveWorkoutScreen
…n the breakdown, with confirm
Android installs the signed APK in-app (ota_update) with a browser fallback; iOS falls back to browser. App polls public GET /app/status on launch + every foreground resume. New StatusBanner on Today shows an update card (build-number gated, min_build forces it) and an admin-pushed banner (info/warn/critical; critical non-dismissible; dismissals persist). Banner respects an optional action_url — tappable 'Open →' when set, plain notice when not. Bump version 0.2.0+3 → 0.3.0+4 (build number must exceed installed builds for update detection). Adds package_info_plus, ota_update, url_launcher; REQUEST_INSTALL_PACKAGES on Android.
…aring v0.3.0's release build failed at ota_update:verifyReleaseResources (android:attr/lStar not found — plugin compiled against an old SDK) and then checkReleaseAarMetadata (desugaring required). Bump ota_update 6→7.1.0 (modern compileSdk) and enable isCoreLibraryDesugaringEnabled + desugar_jdk_libs. Debug build hid this — verifyReleaseResources is release-only. Verified: flutter build apk --release succeeds locally (58MB).
- Step goal: setting screen (progress ring) + Today ring entry + profile row (PATCH /profile) - Sleep v2: per-period sleep screen (naps as cards) + /sleep/v2 client methods - Minute-detail gating: hypnogram / 24h HR / timeline / wear / stress / workout-HR show for the last 7 days, daily summary after (Body/strain curve kept — stored) - kit: detailedAvailable() + DetailRetentionNote helper
… at 100% download (Couldn't find meta-data for provider ...ota_update_provider); ota_update 7.x needs the host app to declare it. Bump 0.4.1+6.
Adds a System/Light/Dark appearance system that preserves the warm "Ember on Paper" identity — the paper inverts to warm charcoal, the coral ember stays constant. Never cold black. - Palette/AppColors.active token system: light + dark resolved at runtime, all existing AppColors.x call sites unchanged; dark elevation via border. - ThemeController: System/Light/Dark, persisted, follows OS by default so login/signup already match the phone; status-bar icons flip. - Foolproof rebuilds: home stack + all tabs (incl. kept-alive) and every pushed route (themedRoute) re-colour live; cross-fade dissolve transition. - Appearance picker in onboarding (with sex/age) and Profile. - Fixes SegToggle selected-label contrast in dark. - iOS widget + Live Activity follow the app's mode via an App Group flag. Bump 0.4.2+7.
Adds a macos-latest `ios` job that builds the app with --no-codesign, packages it as an unsigned .ipa, and attaches it to the same tagged release as the APK. For sideloading (AltStore / Sideloadly / TrollStore) — not App Store signed; users re-sign with their own Apple ID.
- Heart screen: LiveHrTile as the first tile — live BLE HR with a BPM-paced lub-dub heartbeat + pulse ring; honesty-gated (off-wrist/stale/disconnected → "—"). - Notification relay (Android only; invisible on iOS): user-selected installed apps buzz the strap on notification. Built on notification_listener_service + installed_apps; settings live in Profile. Self-heal: re-arm the stream + rebind the system listener on resume and on a timer. - Background survival: a cached long-lived FlutterEngine (EdgeApplication) so the Dart VM + BLE + relay survive the Activity being destroyed on swipe-away; platform channels moved into NativeChannels. NOTE: suspected to regress the band buzz — under test on a release build. deps: notification_listener_service, installed_apps
…-pod-lock Make iOS signing setup contributor-friendly
The band is a raw pipe — upload is raw hex, cloud decodes sensors. Slim R24
+ Sample to {ts,counter,hr}; drop the dead decoded sensor cache (samples
table read by nothing) and the prior fabricated spo2(@72)/temp(@70/4). DB v4
migration. analyze clean; 7/7 protocol tests pass.
A 'Cycles' card directly below Stages: count + mean duration + the overnight z-RMSSD wave with diamond markers at each detected cycle boundary (the cardiac analog of the fractal-cycle figure). Renders only for recent nights when /day/sleep returns cycles.
Share.shareXFiles threw PlatformException(sharePositionOrigin: argument must be set) on iOS because the share sheet is presented as a popover and requires a source rect. Capture the screen render box before the async gap and pass its global rect as the anchor.
…skin-temp & irregular UI, units toggle - AI Coach: bring-your-own OpenAI-compatible key (stored in keychain), agentic tool-calling over the user's data, AI-built charts rendered natively (animated), read+action tools (writes need confirmation), markdown answers, dynamic model picker (search + custom), strict health-only scope, multi-session chat history persisted on disk. Entry: AI Coach button on the Body screen. - Menstrual cycle: opt-in CycleScreen (phase, next-period, fertile window, body overlay) gated behind a Profile toggle. - Live HRV spot-check: 60s wrist-gated optical scan → HRV result. - Skin-temperature section + always-on irregular-rhythm card on Heart. - Units toggle (metric/imperial), local display preference. - Fix: recap share sheet sets sharePositionOrigin (iOS crash).
# Conflicts: # ios/Podfile.lock
feat: pull-to-refresh on all screens + per-nap hypnograms
- Profile 'Community' section: GitHub / Discord / Reddit / X (url_launcher). - Brand icons added to the kit.
v0.5.0: community links + release bump
The Stress detail screen read the old top-level score/buckets/worn_min keys, but
/day/stress now nests the HRV stress under 'stress' ({score,si,lf_hf,rmssd,level}).
It saw null and showed 'No stress data' while Today (daily.stress.score) showed the
real value. Rewrote it to render the HRV score + SI/LF-HF/RMSSD + HR context +
overnight arousal + drivers.
v0.5.1: fix Stress detail screen (HRV-stress shape)
Drop zero-caller query methods superseded by the day-detail / getTrend endpoints: fetchMetrics (404 /metrics), fetchSleep, fetchDaily, fetchTrends, getSleepV2, getTrends.
Collapse the six upload() triggers (15s periodic + 5 event-driven) into one always-running flusher: it kicks once on start, then uploads every 60s. All record sources already store locally to raw_records (hex PK, retain-until-200); this makes the 60s flusher the SINGLE thing that POSTs, so behaviour is uniform everywhere. Delete-by-hex on confirmed 200 is unchanged (the no-data-loss invariant); the iOS headless-recovery one-shot keeps its direct uploadPending (separate isolate, same primitive). Steady-state 1Hz trickle drops from ~5,760 POSTs/day to ~1,440 (~4x fewer R2 puts + D1 writes + Workers requests), with <=60s upload latency that is invisible to cloud metrics (computed at the wake-close).
Address Copilot review nits (comment accuracy only, no logic change): - drop the stale stacked '~15s cadence' doc block superseded by the 60s one - _startFlusher: the kick fires on COLD start only; on reconnect/foreground the already-running timer carries it on the next tick (not 'without waiting') - correct 'stopped only by endSession' → session teardown (logout/signOut/ unpair/endSession) - foreground-reclaim comment: no explicit flush anymore; the always-running flusher keeps uploads current
perf(upload): single 60s flusher as the sole upload trigger
…+ classifier accuracy; surface daytime HRV, restlessness, desaturation - Workouts: DETECTED/LOGGED/LIVE tags, phase count, pencil → type picker → POST /workout/:id/type (calibration), classifier-accuracy chip in training hero - Stress screen: richer restlessness (shifts + longest-still) + Daytime HRV section (/day/hrv) - Lungs view (detail_cards): overnight desaturation dips (ODI, screening-only) - api_client: setWorkoutType, getDayHrv - dart analyze: no new issues
The 1 Hz trickle and the morning drain of a full night the band buffered while disconnected are different workloads. When the local backlog exceeds ~1000 records, upload in 1500-record batches and re-drain after ~2s (gated on a successful, progress-making pass) instead of waiting the 60s trickle cadence; revert to 300/60s automatically once caught up. The backend rate limit counts per POST, not per record, so bigger batches lift the effective ceiling ~5x (30x300 -> 30x1500/min) without changing the request budget, and the day-packed minute store does the same D1 work either way. Shared batch policy in uploader.dart; applied in both the foreground flusher (app_state) and the headless background isolate (background_sync).
…g isConnected On iOS the app can resume from suspension with the peripheral still flagged "connected" while its GATT notifications silently died — the UI shows connected but no events arrive, and only a kill+reopen (full reconnect, which re-runs the only setNotifyValue path) recovers. openSession's foreground-reclaim branch trusted engine.isConnected and early-returned, never re-subscribing. Track the wall-clock of the last received notification (_lastRx, set on connect and on every onValueReceived). On reclaim, if data arrived within 30s the link is genuinely live → keep the fast path; otherwise it's stale → disconnect and fall through to the full connect → subscribe → drain path. Brief glances stay instant; a real lock/suspension self-heals on unlock.
…tion
The GATT-owning central (flutter_blue_plus — the one subscribed to the band's
HR/event characteristics) was never opted into CoreBluetooth state restoration;
the only central with a restore identifier (BleRestoreManager) holds no
subscriptions by design. Apple preserves a connection AND its subscriptions only
for a central created with a restore id, so on suspension the subscribed central's
notifications weren't preserved — the app resumed "connected" with dead
notifications until a full reconnect ("connected, no events"). Add
FlutterBluePlus.setOptions(restoreState: true) before any BLE work so FBP's
central carries a restore identifier and iOS preserves the live subscriptions.
Root cause from an iOS/CoreBluetooth audit vs Apple docs; the prior _lastRx
resume-revalidation only treated the symptom.
No APNs registration or silent-push handler exists anywhere in the app, so the remote-notification UIBackgroundMode was a dead/misleading capability (audit finding OpenStrap#6). Keep only bluetooth-central.
The fast-reclaim path (resume with a still-live link) returned without re-polling the band, so battery %/charging/strap-name/alarm stayed frozen until a cold restart (the full connect path is the only place they were fetched). Re-poll them in the background (non-blocking) on a healthy reclaim so the charge UI stays current without forcing a reconnect.
feat(workouts): detected-vs-selected + phases + type confirm/correct + classifier accuracy; daytime HRV / restlessness / desaturation
…tching Ships the background-BLE fix (flutter_blue_plus State Preservation & Restoration via setOptions(restoreState:true)), the stale-link resume revalidation, battery re-poll on reclaim, adaptive upload batching/backfill, and the workout HAR/honest metrics surfaced from the v2/v3 backend.
- Find my band: a button (Profile -> device) that vibrates the band via the existing safe haptic command, to locate it. - Broadcast HR: re-broadcast the band's live HR as a standard BLE Heart Rate Service (0x180D) via ble_peripheral, so a bike computer / gym machine can pair with the phone like a chest strap. Adds the bluetooth-peripheral background mode. (Best with the app foregrounded — iOS limits background BLE peripheral.) - Strava: a Profile -> Integrations -> Strava screen (status, connect via the backend OAuth flow, sync, disconnect, and the pulled-activity list), plus the ApiClient methods. Pairs with the backend Strava endpoints.
|
hey, i'm really sorry about this one, and thank you for the PR. i should've said something way earlier about the direction. for a while now i've been moving the whole project to a local-first approach , no backend, all the compute and storage happens on the device and i never actually announced it. and you ended up building against the old cloud setup without knowing, that's my fault what changed on the repo: the old cloud version is now on the i really don't want to just close good work. your changes are genuinely valuable , find my band, the HR broadcast, the strava bits, i'd love to get them in. the find-band and HR broadcast especially don't really depend on the backend, so they should port over pretty cleanly. could i ask you to close this one and open a fresh PR off the current main? that way it's against the local-first base and we skip the messy conflicts. i know that's a bit annoying and i'm sorry for it, more than happy to help with the rebase or point you to where stuff moved if that helps. again, sorry for the wasted effort , and thank you for contributing - really appreciate it. |
Three app-side features (happy to split if you prefer).
Find my band
A button under Profile → device that buzzes the band (via the existing safe haptic command) so you can locate it. ~no new code beyond the engine call.
Broadcast HR → bike computer / gym
lib/ble/hr_broadcast.dartmakes the phone a standard BLE Heart Rate Monitor (service 0x180D, characteristic 0x2A37) viable_peripheral, re-broadcasting the band's live HR. A Wahoo/Garmin or gym machine pairs with "OpenStrap HR" like a chest strap. Adds thebluetooth-peripheralbackground mode to Info.plist.Strava integration (UI)
Profile → Integrations → Strava: connection status, Connect (launches the backend OAuth flow in a browser, re-checks on resume), Sync now, Disconnect, and the pulled-activity list. Adds the
strava*methods toApiClient. Pairs with the backend Strava endpoints (OpenStrap/backend#12).Dependency
ble_peripheral(for the HR broadcast).flutter pub get+pod install.