Repository navigation
Import v8.2.2 snapshot (upstream catch-up + board wave; ports PRs #1035/#1036 + BLE/readiness fixes) - #1
Merged
Conversation
…scrub a name leak PRs reimplemented under our identity (cross-platform parity, Hevy CLOUD sync declined, on-device only): - #827 dismissible "building your baseline" calibrating note (+ iOS parity) - #831 strap firmware version on Devices (+ iOS/mac parity) - #846 Teams notifications moved to Messaging (+ iOS parity) - #854 O(1) Android frame reassembler (offset, no per-frame copy) - #855 ranged CRC validation, no per-frame sub-array (Swift + Kotlin) - #835 Apple Health WORKOUT read only (the Hevy cloud sync is out of scope) - #830 Spanish + #837 Italian string-catalog completion Bug fixes: - #845 active-workout stat bubbles no longer clipped (Android) - #821 alarm screen consistency (strap label, explainer position) - #849 Today no longer re-runs the full history reload on every re-mount (the Apple-Health-import lag) - #857 Oura export import accepts the real export shape with an honest error - #823 imported-only days now compute a Charge/Rest from their metrics (source-only scoring) Hardening: - anonymize-macos-app.sh now scrubs the WHOLE bundle (embedded extensions/frameworks) and re-signs inside-out - removed the maintainer first name from 16 source/doc comments (anonymity) macOS widget (PR #828) deferred: App Groups need real signing, which the unsigned macOS distribution lacks, so it would not populate. Parked pending a signing-free IPC.
New issues folded in: - #859 iOS Backup folder picker: open-in-place (asCopy false) so a folder is selectable - #860 UX: Today defaults to the current day after an update (not the first-ever day); the More tab remembers which groups are expanded; Insights Q&A rolls over by date instead of pinning stale answers under "Today" - #861 sleep: merge a single overnight that was being split into a nap plus a sleep Hardening: - build-v7-artifacts.sh now does an anonymity source-scan and refuses to build if a maintainer name reaches tracked source - scrubbed three stray em-dashes off comment lines edited during the name scrub
…em 1) Retire the launch "auto-land on your most recent day with data" (#605/#739): a fresh launch now lands Today on today (offset 0) unconditionally, even when today has no data yet and the only banked data is days back (the calibrating-user-after- update case the v7.6.0 snap-to-today did not cover, since the auto-land overrode it). In-session day memory (#739/#614) is preserved. Extracted a pure launchDayOffset(isFreshLaunch, savedOffset, hasTodayData, latestDataDayBack) helper on both platforms and pinned it with a deterministic test (testFreshLaunchLandsOnTodayEvenWithOnlyOldData: fresh + no-today + data 5 days back -> 0, not 5) that runs and passes on Swift (StrandTests) and Kotlin (JVM). Lockstep bump: iOS 163 / Mac CFBundle 194 / Android vc 251.
… + Android) A user supplied the actual per-file schema of a real Oura account export. Correcting the parser against it, evidence-based: - HEADLINE: real Oura export CSVs are semicolon-delimited; the reader split only on commas, so every Oura row parsed as one unsplit field and extracted nothing. Added a quote-aware per-file delimiter sniff (; / , / tab), defaulting to comma so the WHOOP/Fitbit comma path is byte-for-byte unchanged. - SpO2: read Oura's real nested spo2_percentage.average (was a flat key that never hit). - VO2max: now ingested (vo2_max from the vo2max file) -> vo2max metric series (feeds Fitness Age, same as Apple Health). - Daily activity distance (equivalent_walking_distance), calorie alias order. - Per-category bare `score` columns disambiguated (readiness vs sleep) instead of mislabelled; activity-score rows left out. - Robustness: per-category single files recognised on their own columns; `deleted` sleep periods skipped; unknown file types/columns skipped cleanly, never an error. Tests (Swift 13 + Android 15, all green): real-schema semicolon per-category CSVs, nested spo2/vo2 from JSON, semicolon delimiter detection.
…oid) A 5/MG owner enabled the strap wake-alarm; iOS said "Armed on the strap itself" but the strap never buzzed. Their export showed NOOP's own log: "5/MG firmware alarm needs the Experimental toggle (unconfirmed), not armed" , armStrapAlarm only arms a 5/MG when PuffinExperiment is on, otherwise it returns without arming, yet the UI still claimed it was armed. Honest-data violation. Mirror Android's honest surface on iOS: when a 5/MG is detected and Experimental is off, the card now says the wake time is saved but the strap is NOT armed until Experimental mode is on (Settings, Experimental), and that even then a 5/MG strap wake is unconfirmed so keep a backup, instead of implying it is armed. WHOOP 4.0 wording unchanged. Adds AppModel.whoop5Detected (forwards BLEManager.isWhoop5).
…both platforms) The day-scale read already uses off-main SQL buckets, but the RAW (zoomed-in) branch of Repository.timelineSeries dedup'd + sorted + mapped up to 200k HR points SYNCHRONOUSLY on the @mainactor, a real beach-ball for a large-history user inspecting the Deep Timeline. Moved that post-processing into a Task.detached(.utility), mirroring restageFromRaw, via two nonisolated static helpers (dedupSortRawHr / dedupSortDownsampleRaw); the store reads stay on the WhoopStore actor. Output byte-identical (first-id-wins dedup, ascending sort, same isRaw/bucketSeconds). Android parity: FullDayChartScreen.readTimeline ran the same 200k-row map + downsample on Dispatchers.Main (called from a LaunchedEffect with no withContext). Wrapped its body in withContext(Dispatchers.Default); Room queries still run on Room's executor. Tests: testRawHrDedupSortIsOffMainPure / testRawNonHrDedupSortIsOffMainPure assert dedup (first wins on overlapping ts) + ascending order off the actor. NOTE: this is the zoom-case freeze. phsycology's ON-OPEN macOS freeze (#833) is a separate, not-yet-pinned main-thread stall; a hang watchdog in the diagnostic export is next so the reporter's capture pinpoints it. Does not claim #833 closed.
…ce Keychain write failures (#872) #873 (real, low): refreshModels() is @mainactor with one suspension point (the model fetch). It re-read provider.modelOptions FRESH after the await while the fetched ids were the OLD provider's, so switching provider mid-fetch wrote a stale/mixed list to the new provider. Now it captures the provider before the await and `guard provider == captured else return` on resume, building the merge from the captured snapshot. DEBUG-only fetch seam + a test (switch mid-fetch, assert only the new provider's models survive). #872 (defensive): AIKeyStore.save discarded the SecItemAdd OSStatus and wrote the owner marker unconditionally. The described broken-state impact does NOT occur (isConfigured/ hasKey read the live Keychain, not the marker), but a discarded credential-write status is worth handling: save now returns Bool, skips the marker on failure, and setKey surfaces it. Android already had both guards (captured-provider check; atomic key+owner write), so this brings iOS to parity. Reimplemented under our identity; the external PRs are not merged.
…auses The Deep-Timeline raw fix (acf8f77) handled the zoom case; these are the on-OPEN and Insights stalls the audit pinned: 1) On open: Repository.reconcileWorkoutHrWithTrace ran a sequential per-row loop (cap 300), each awaiting store.hrSamples(limit: 8000) one at a time, then reduce/max over up to 8000 ints SYNCHRONOUSLY on the @mainactor. Rewritten in 3 phases: eligibility in original order, bounded-concurrency reads (withTaskGroup, chunks of 8) with the reduction OFF the main actor (nonisolated reduceWorkoutHr returning Sendable scalars), then in-order reassembly. Byte-identical output. Test: WorkoutHrReduceTests (arithmetic + asserts it runs off-main). 2) Insights: the macOS sidebar's .id(selection) cold-mounts each screen on every switch, and Insights (unlike Today) had no remount cache, so it re-read full history each time. Added a Repository-level insightsCache/insightsLoadedSeq mirroring Today's pattern; load(allowCache:) restores the snapshot when refreshSeq + dayKey are unchanged. Crossfade kept. Android already safe on both (SQL-aggregate hrWindowStats off the IO executor; Compose nav has no .id teardown). Does not over-claim , phsycology to confirm on the next build.
…t a partial top-up inflate days-left #7: the Swift "~X days left" badge read an in-memory buffer fed only by live BLE events, so after a reconnect it restarted empty (Android seeds from its persisted battery table). Now bootstrapStore seeds LiveState.batterySamples from store.batterySamples over a 14-day window (new seedBatterySamples setter, de-duped against live points), so the estimate is measured immediately after reconnect. #8: BatteryEstimator reset the discharge fit on ANY SoC rise >1pp, so a 40->55% top-up discarded the long clean discharge run and inflated days-left exactly like a full charge. Now a rise only anchors a new run when it lands near full (>=90%); a partial top-up is stepped over and the fit uses the longer pre-top-up segment. Swift + Kotlin identical. Tests on both platforms (incl. the [(0,100),(60h,40), (61h,55),(64h,53)] scenario -> honest ~53h, not inflated). #713 near-full reset behaviour preserved.
…arity) + Swift steps-trace parity #9: HealthConnectImporter flat-summed every StepsRecord/calorie record across ALL data sources, so a user with two HC sources (phone + watch logging the same walk) got ~2x steps and calories. Ported the #589 per-source-MAX de-overlap already on iOS/mac + the Android XML importer: DayAcc now keys steps/ totalKcal/activeKcal by dataOrigin.packageName (sum within a source, MAX across sources) in both the import and the live refreshTodaySteps top-up. Test: two sources -> MAX, not sum. #12: the Swift steps-trace rawCounterTrace lacked the zero-counter noRawCounter branch its Kotlin twin has, so the two platforms emitted different text on a counter-less day. Added the matching branch (byte-identical wording) + parity tests.
…aches the daily rollup (#17) #0: SleepWindowReclip was START-BLIND (reclip(stagesJSON, sessionStart, oldEnd, newEnd)), so a bed/onset edit on an imported or not-yet-synced night left stages BEFORE the corrected onset and the displayed "time in bed" / hypnogram no longer matched (wake - onset). Android was worse: it always reclipped against the OLD effectiveStartTs. Made reclip START-AWARE on both platforms (adds newStart): reclipSegments now drops segments wholly before newStart and clips the straddling one; reclipMinutes derives the delta from BOTH boundaries ((newEnd-newStart) - (oldEnd-sessionStart)) so a later onset trims the front. Callers (Repository.editSleepTimes, WhoopRepository.updateSleep- SessionTimes, SleepScreen optimistic copy) now pass the new onset. Tests on both platforms cover a bed-only edit on a segment night and an imported minute-dict night. #17: imported Oura/Garmin respiratory rate was parsed onto the sleep SESSION but the day-rollup type had no resp field, so DailyMetric.respRateBpm stayed nil and the resp term dropped out of imported Charge. Added WearableDailyRow.respRateBpm, folded the night session's resp into the day row in the Oura JSON+CSV and Garmin parsers (and the Android twin), and wired the WearableImporter mappers so it actually reaches DailyMetric. Tests on both platforms.
…arbiter (#5) #4/#6 (iOS): the iOS smart alarm only armed the strap firmware buzz and posted a local notification AFTER the strap reported it fired, so a failed strap buzz or a suspended phone past day one meant NO wake at all. applySmartAlarm() now also schedules a REPEATING daily UNCalendarNotificationTrigger (the WindDownNudge idiom: lives in the notification center, survives relaunch) at the wake time per selected weekday, gated on the wrist-alerts master + notification auth, replaced on every re-arm and cancelled on disarm. HONEST: it is a best-effort backup, not a guaranteed loud alarm (a sideloaded build has no critical-alert entitlement, so Focus/silent can still mute it) , the honesty card now says so and still tells users to keep their Clock alarm. #5 (Android): "Buzz WHOOP 4/5" and the "Strap wake-alarm" both drove the single firmware-alarm slot with no coordination, so toggling one off ran disableStrapAlarm() and killed the other. Introduced one reconcileStrapAlarm() as the SOLE caller of arm/disable: it computes both requested times and arms the slot to the EARLIEST when either is on, else disarms. All six call sites routed through it. Test: both on, turn one off, slot stays armed to the other.
…gs silent write-stalls (#13) #1 (both platforms): in finishChunk the trim==0xFFFFFFFF "no banked history, fully charge it" gate read sessionRowsPersisted at the TOP of the function, but this chunk's rows are tallied LATER (after store.insert). So a bad-clock/flash strap that emits records on its no-cursor END logged the alarming false "no history" line even though the END just delivered rows. Relocated the gate to AFTER `sessionRowsPersisted += tally.rows` (and before setCursor); the genuinely-empty session path is preserved (empty frames skip the persist block, gate still sees 0 rows). End-to-end test on Swift (real v25 record frames + a 0xFFFFFFFF END) + a Kotlin ordering-contract test. #13 (Android only): two failure returns in Backfiller.kt (decoded-insert catch, cursor-write catch) returned silently with no log(), while the Swift twin logs both. Added the matching log lines so a #601-class write-stall (history stalls while live HR still works) is falsifiable in the Android strap log. The third candidate point already logs and was left untouched.
Between 00:00 and 04:00 (the logical-day rollover window) three top-bar paths showed a date one day ahead of the data: #14 (macOS): DayNavBar dated its label from raw Date(); added a `today: Date` logical anchor to the init and the macOS call site now passes Repository.logicalDay(Date()), so the navigator date and the shown data share one source of truth (the picker setter + range cap moved to the same anchor). #15 (iOS): the Today date label at offset 0 now anchors to the resolver's surfaced row (repo.today?.day), matching Android, instead of selectedLogicalDay. #16 (iOS): the day-picker getter/setter + the nav label used raw Date(), so the graphical picker pre-highlighted the wrong day (and the a11y label mismatched at offset >= 2). All three now use the logical anchor via a new pure pickedDayOffset(pickedDate:anchorLogicalDay:) helper, with rollover tests.
On Apple the route polyline lives only in RouteStore, a side-store keyed by "<startTs>|<sport>". The edit save path (Repository.saveManualWorkout) deleted+upserted the DB row on a natural-key change but left the route under the OLD key, so editing a GPS workout's sport or start time orphaned its route + distance from the detail view. Now, in that branch, the route is re-keyed: load under the old key, and only if present, store under the new key + remove the old. Guarded so a route-less session is a clean no-op. Android needs no change , its route is a column on the row (WorkoutRow.routePolyline) and preservingCaptured carries it through the edit. Tests cover re-key and the no-route no-op.
…zones/Effort (both platforms) Editing a workout's Avg HR applies the typed number but preservingCaptured keeps the original strain/zones/HR-graph, so the detail view showed an edited average over an unchanged curve with no indication. We do NOT fabricate a re-score from a single number, instead we disclose it honestly: the edit sheet shows "Avg HR is shown as typed. The HR graph, zones and Effort stay from the recorded session." when the edit changes avg on a captured-strain/zones row, and the detail view adds a caption under the HR card when the shown average diverges from the trace mean by more than 3 bpm. Mirrored on Android via a pure WorkoutEditing.avgHrEdited helper (5 tests).
…tops getting missed (#897) Two users in a row (#897 + a Discord report) could not find the Oura ring in Add a device because it sat LAST under Experimental, below Amazfit, Mi Band and Garmin, so it fell below the fold and they never scrolled to it. Oura is a flagship beta we are actively promoting, so it should not be buried. Pure reorder (no behaviour change) on both platforms: the Experimental order is now Oura ring, Amazfit / Zepp, Xiaomi Mi Band, Garmin. The four standard options above the heading are unchanged.
…plements #896, thanks ryanbr) WhoopBleClient's _state is a MutableStateFlow over an immutable LiveState data class, mutated with ~52 non-atomic _state.value = _state.value.copy(...) writes from THREE threads (the GATT binder thread, the main Looper, and the IO dispatcher). A concurrent write could clobber a sibling field; the sharpest case was parseStandardHr re-asserting bonded=true on every HR tick while a main-thread teardown set it false, leaving a phantom-connected strap with frozen HR (5/MG especially). Converted every write to an atomic _state.update { it.copy(...) } CAS (the helpers are pure, so the retry loop is safe), handling the charging-site `it` shadow with a named param. Pure reads untouched, output byte-identical. iOS/macOS LiveState is @mainactor + per-field @published, so it is structurally immune. Reimplemented under our identity (PR not merged).
) + honest Rest tile (#898) #863: a calibrating 4.0 banks sparse history fine, but two surfaces showed empty. (a) The Deep Timeline auto-landed only when a scored DailyMetric existed, stranding a calibrating user on an empty today; Android now falls back to the latest day with raw HR (latestHrSampleTs) when no scored day exists (pure landTargetDayStart helper + test). iOS already landed on the raw-HR day, no change. (b) The Today heart-rate card did `if (buckets.size < 2) return` and rendered nothing on a sparse/empty day (read as "the graph froze"); both platforms now show an explicit calibrating/empty state instead of vanishing (the past-day-swap was declined in #605). #898: an aggregate-import user (Health Connect HRV/RHR, no in-bed session) gets Charge from WatchRecovery but no Rest, because that path writes recovery and never sleep_performance. We do not fabricate a Rest number; instead the Rest tile now shows "needs a tracked night" when there is a recovery but no scored sleep, on both platforms, instead of a bare "No data". (A real Rest for HC-only users needs Health Connect sleep ingest, tracked separately.)
…ve (#839, reimplements #879) When a workout is active, Today now shows an indicator card with the sport + a live elapsed clock; tapping it opens the in-exercise Live screen. Detection reads the existing activeWorkout source of truth (survives an app kill), so the card auto-appears and clears with no new lifecycle wiring. The card is a perf-isolated leaf with its own clock (TimelineView on iOS, a delay-tick on Android) so the per-second tick never re-renders Today's body. Elapsed shows M:SS, then H:MM:SS past an hour. Reimplemented under our identity from mvanhorn's PR with three fixes (resolve against the new DayNavBar(today:) signature, drop a redundant monospacedDigit, add the >1h format) AND the full Android port the PR skipped (the contributor only did iOS). Tests on both platforms.
…#895, thanks dpguglielmi) Folded the Italian gap-fill from dpguglielmi's PR (the contributor's helper script was NOT taken in, only the strings). 1265 new it values, placeholder integrity validated (205 with format specifiers, zero mismatches), de/es/fr/ru and our existing Italian untouched, catalog now 1730 keys. Em-dashes stripped from the new Italian values per our no-em-dash house rule (clause-break dashes -> commas; the legit no-data placeholder kept). The English SOURCE strings still carry their existing em-dashes (they regenerate from Swift source), tracked separately for an all-languages source sweep. A native Italian skim of the longest onboarding/disclaimer strings is a recommended follow-up quality step.
…#829) The Deep Timeline already had a full zoom/pan engine (both platforms), so the only iOS gap was the Today HR chart, which used the same OverviewHRChart but passed no zoom binding. Wired a hrZoomDomain window into it (pinch zooms about centre, drag pans, both clamped to the loaded day with a 60s floor), added a shared double-tap-to-reset + reduce-motion gating to the chart gesture modifier, and a reclamp rule so a day step opens full-scale while a same-day live extension keeps your zoom. No per-frame DB read (Today's buckets are already loaded). 6 tests on the reclamp math. Android's Today HR chart is the remaining mirror (its chart has no reusable zoom binding), tracked as a follow-up.
…STREAMING pill (#903) #900 (critical, both platforms): live Oura HR never streamed. The enable triplet (dhr_read, enable, subscribe) stalled at step 0 because handleSecureFrame only recognized enable-ACK sub-ops 0x23/0x27, but the first command's ACK is sub-op 0x21 (OURA_PROTOCOL s5.6 step 1). Unrecognized, it returned .unhandled, the sequencer never advanced, and no live HR ever arrived. Added 0x21 to the enable-ACK set on Swift and the identical gap on Android, with a decode test each. Also resolves discussion #858. Reimplemented from pipiche38's report, thanks for the find. #903: the menu-bar pill never showed STREAMING for a live Oura ring because OuraLiveSource set no positive signal. Added a dedicated streamingLiveHR flag to LiveState (NOT bonded, which carries WHOOP encrypted-bond + buzz semantics), set on the Oura streaming branch and cleared on teardown, and drove the green pill off it (bonded kept as the WHOOP fallback). macOS-only by nature (no Android menu bar).
…e tree (#902, thanks pipiche38) These v2.8.2..2.8.8 macOS zips were committed before dist/ was gitignored and lingered as tracked blobs. dist/ is gitignored, nothing in the tree references them (the only NOOP-v2.8 hits are iOS .ipa release URLs in altstore-source.json), and they are already published as GitHub release assets, so removing them loses nothing. Leaner tree, fewer old binaries. History is unchanged (a filter-repo purge is a separate, heavier call).
IntelligenceEngine.analyzeRecent opened with `guard !computing else { return }` and silently dropped a
force:true post-sync recompute that landed while a 15-min idle pass was mid-flight. On a WHOOP 5/MG
segmented offload that window is wide enough to intermittently strand a freshly-synced night un-rescored,
so Today falls back to the last scored day (frazzle28 #899). Now a forced call that hits the guard sets a
pendingForcedRescore flag; the defer clears the lock and, if set, re-invokes once (single re-arm, no loop).
5 tests on the (computing, force) -> drop|rearm|rerun state machine. Android needs no change: its
IntelligenceEngine is a stateless object with no shared computing lock, so a forced rescore is never
dropped there (verified, not assumed). The stale carry-over bounds (B/C) await frazzle28's export.
…@63 (clean-room from #901) The @63 activity_class (0=still,1=walk,2=run, #316/j0b-dev) was decoded into the StepSample model on both platforms but DROPPED at the storage boundary, so it could never be read or shown. Plumbed it end-to-end: Apple GRDB migration v18->v19 adds a nullable stepSample.activityClass, the StreamStore INSERT now writes it, and Reads.swift maps it back; Android Room migration 12->13 + the WhoopRepository insert that was dropping it. Then a subtle still/walk/run glyph on a REAL (measured) Steps tile, both platforms, tinted as part of the tile, self-hiding when the class is unknown (never a fabricated 0/still). Migrations are additive/nullable so existing rows read absent. Round-trip + migration tests on both platforms. (The tigercraft4 PR botched this on Apple, SELECTing a column it never created; this is the correct chain.)
…from #901, no duplicate sheet) Lifted only the good parts of the #901 coach work, inline in the EXISTING CoachView (not a second settings sheet that duplicated the provider/key/model UI). An editable system prompt (collapsed by default, TextEditor + Reset, persisted in UserDefaults / NoopPrefs, read fresh per send, blank clears it), and a single derived Baevsky stress-index line added to the coach context under the SAME dataConsent gate as the HRV/RHR already sent, computed the same way StressView does, no raw R-R egress, absent when there are too few clean beats. Both platforms, BYOK untouched, tests on both.
ryanbr
added a commit
that referenced
this pull request
Jul 16, 2026
…description typos (#492) (#496) * Today (Android): unclip the day title + drop the bogus Strain % (#492) Two Android-only visual bugs from #492 (iOS already handles both correctly): - #1: the tappable day-title Column had .clip(RoundedCornerShape(cornerSm)) with indication = null — no ripple to shape, but the rounded corners clipped the title/date text's bottom-left (the 'W' of 'Wednesday'). Drop the clip; the clickable still defines the tap area. iOS uses contentShape(Rectangle()), no clip. - #8: the Strain Key-Metrics tile hardcoded unit = "%". Strain/Effort is a load index (0–21 WHOOP / 0–100 NOOP), not a percentage, so the % was wrong — most visibly on the 0–21 scale ('5.7%'). Recovery/Rest are genuine 0–100 % and keep it. iOS shows the strain axis as an 'of 21'/'of 100' caption with no %. compileFullDebugKotlin: BUILD SUCCESSFUL. Reported by bartmuskala. * Trends (Android): fix two metric-description punctuation typos (#492 follow-up) Spotted in the #492 screenshots (not in the reporter's list): the Charge/Sleep metric-explorer descriptions had a space before the comma, and the Sleep one also used a comma where iOS uses a colon. These are documented to 'match the macOS MetricCatalog entries exactly' (TrendsExploreScreen :190), so this restores that parity: - 'you are , led' -> 'you are, led' - 'sleep was , dura...' -> 'sleep was: dura...' (matches MetricCatalog.swift) String-literal only; no logic.
This was referenced Jul 16, 2026
This was referenced Aug 7, 2026
Merged
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…8.2.2 elsewhere) Resolves PR ryanbr#1 (ryanbr/noop) so main can adopt the v8.2.2 snapshot while retaining the fork-only pieces. The 9 conflicting files were all cases where the fork's perf (ranged CRC, O(1) Reassembler drain) and firmware-version-display work had ALREADY been reimplemented independently in v8.2.2 — same features, different variable names and comments. Taking v8.2.2 for those keeps a single upstream implementation instead of a divergent fork copy; the features are retained (TodayScreen v8.2.2 is even a superset: ryanbr#827 calibrating-dismissal plus a new carried-sleep card). Kept (non-conflicting, fork-unique): the unified fork testing-build CI workflow, fork debug keystore, fork appId/identity (build.gradle.kts, project.yml), and the fork update-source plumbing (UpdateCheck.kt / UpdateChecker.swift).
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
Import v8.2.2 snapshot (upstream catch-up + board wave; ports PRs ryanbr#1035/ryanbr#1036 + BLE/readiness fixes)
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…to the notes Each release's GitHub body is now: the curated docs/releases/<TAG>.md notes + GitHub's auto-generated "What's Changed" breakdown (every merged PR since the previous RELEASE, with authors/links + New Contributors + full-changelog compare link) + the downloads footer. Uses the generate-notes REST API (deterministic; not dependent on gh flag-combining) diffed from the most recent NON-prerelease release (so testing-latest snapshots are ignored). Skipped on the very first release (no prior tag would diff to repo start). Best-effort — a failure is ignored. Verified: the API returns the PR list (ryanbr#1..ryanbr#9); first-release PREV detection returns empty as expected.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ck skew Rounds out the Alarm block to cover the full alarm-failure matrix, not just 'didn't stick': - ryanbr#1 last fired: persist STRAP_DRIVEN_ALARM_EXECUTED → 'Last fired: Nh ago / never observed' (the 'did the strap actually buzz?' leg of the armed→reports→fired sequence). - ryanbr#3 model + 5/MG experimental gate → 'Model: 5.0/MG · experimental: off → NOT armed', the ryanbr#1 '5/MG alarm never fires' cause (silently gated off). - ryanbr#4 strap clock skew: persist the newest banked record → 'Strap clock: 446d behind (reset)', the actual ryanbr#34 root cause, surfaced instead of buried in the CLOCK-WARNING log line. Now the block triages: didn't arm (model/experimental/connected), didn't stick (sent-vs- reports + clock), didn't fire (last-fired). Both platforms; persists at low-frequency points (fired event, GET_DATA_RANGE). Android compiles clean; iOS gated by CI.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…description typos (ryanbr#492) (ryanbr#496) * Today (Android): unclip the day title + drop the bogus Strain % (ryanbr#492) Two Android-only visual bugs from ryanbr#492 (iOS already handles both correctly): - ryanbr#1: the tappable day-title Column had .clip(RoundedCornerShape(cornerSm)) with indication = null — no ripple to shape, but the rounded corners clipped the title/date text's bottom-left (the 'W' of 'Wednesday'). Drop the clip; the clickable still defines the tap area. iOS uses contentShape(Rectangle()), no clip. - ryanbr#8: the Strain Key-Metrics tile hardcoded unit = "%". Strain/Effort is a load index (0–21 WHOOP / 0–100 NOOP), not a percentage, so the % was wrong — most visibly on the 0–21 scale ('5.7%'). Recovery/Rest are genuine 0–100 % and keep it. iOS shows the strain axis as an 'of 21'/'of 100' caption with no %. compileFullDebugKotlin: BUILD SUCCESSFUL. Reported by bartmuskala. * Trends (Android): fix two metric-description punctuation typos (ryanbr#492 follow-up) Spotted in the ryanbr#492 screenshots (not in the reporter's list): the Charge/Sleep metric-explorer descriptions had a space before the comma, and the Sleep one also used a comma where iOS uses a colon. These are documented to 'match the macOS MetricCatalog entries exactly' (TrendsExploreScreen :190), so this restores that parity: - 'you are , led' -> 'you are, led' - 'sleep was , dura...' -> 'sleep was: dura...' (matches MetricCatalog.swift) String-literal only; no logic.
UtkuDenizAltiok
added a commit
to UtkuDenizAltiok/noop
that referenced
this pull request
Sep 9, 2026
The store reads rows, LiftMetrics computes — now invariant ryanbr#1, because both of this feature's metric bugs were second implementations of one figure. Records three real upstream findings and why none is worth a local diff: every line changed in upstream code is a line re-merged on every sync. They are PR candidates for later. Includes one that was checked and is NOT a bug, so nobody investigates it twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 13, 2026
ryanbr
added a commit
that referenced
this pull request
Sep 13, 2026
Requested by @paulinyrobert-del, who runs with background connection off and wanted a sync without leaving Today. There was no way to start a scan from Today at all: the quick-actions menu is four navigations, the header's other controls go to Settings, Devices and that sheet, and there is no pull to refresh on Today or in its scaffold. Two affordances, one action, and neither takes width from anything. A disc on the leading edge of the wordmark row, which was a spacer. Not decoration: #2110 put it there to mirror the trailing Customize control exactly, keeping the wordmark optically centred after the two had previously overlapped. The new disc is the same HeaderClusterControl size as the one it mirrors, so that invariant holds and the wordmark does not move. Deliberately not in the right-hand cluster, where #2110 measured that a fifth control leaves the 28sp day title about 117dp against the 139dp "Yesterday" needs. And the sync chip takes a tap, being the one header element that already reports sync state and had none. It carries an onClickLabel rather than a contentDescription, so the action is named alongside the state the icon describes instead of replacing it. Both route through rememberRequestScan, the wrapper Settings' Re-scan and Live's Connect already use, which requests the runtime Bluetooth permission first. Calling connect() directly silently does nothing on Android 12+ when that permission was denied or revoked, which was #1, so a third entry point had to go through the gate rather than around it. The disc greys and refuses taps while a scan runs, matching the Settings and Live buttons. The chip keeps its tap during a sync, a backfill in flight not being a scan in flight. Android only. The disc stays visible when a strap is already connected, which matches Settings' Re-scan rather than inventing a rule for the same action. The new label is in all eight values files. SyncChipStateTest and BackupSyncTest, 26 tests, 0 failures; i18n gate, doc lint and release lint clean. Two things found while reviewing and filed rather than folded in. Today lost its recording-status chip in the 7.0.0 redesign and the composable is still present with no call site (#2179), which is the status dot the report remembers. And a permanently denied permission still ends in a silent tap on every entry point including these, which wants deciding alongside #2179, since Today having no connection status is the same gap from the other side.
5 tasks done
ryanbr
added a commit
that referenced
this pull request
Sep 22, 2026
Color.init(light:dark:) built a UIColor/NSColor dynamic provider whose closure called sRGBComponents(hex:). That closure runs once per RESOLUTION, not once per token, and the liquid layer resolves every frame: liquidComponents() asks for NSColor(self).usingColorSpace(.sRGB), which re-invokes the provider. So a trimmingCharacters plus a Scanner over a string ran at 60 fps, which is how a reporter's macOS profile came to show Color.sRGBComponents(hex:) and closure #1 in Color.init(light:dark:) among its hot leaves. Parses both hexes once in the initialiser, into one always-read pair, and lets the provider pick between the two ready tuples. One declaration rather than two because watchOS resolves straight to the dark hex, so a separate light constant would be unused there and warn. The tokens are static let, so this is now once per token for the life of the process. Deliberately not a cache. A cache needs a key, a lock (a dynamic provider can resolve off the main thread) and an eviction story, all to re-derive per frame a value that cannot change after the initialiser runs. A dynamic Color cannot be read back, as BrandSleepRampTests already notes, so the resolved value is not assertable. The pin is a source census in the same style as QuietMotionCoverageTests: exactly two parses in the initialiser, both appearances reading the hoisted tuples. The regression is silent otherwise, since a parse put back inside a closure renders identically. No behaviour change: the same components reach the same provider, only earlier.
ryanbr
added a commit
that referenced
this pull request
Sep 22, 2026
Color.init(light:dark:) built a UIColor/NSColor dynamic provider whose closure called sRGBComponents(hex:). That closure runs once per RESOLUTION, not once per token, and the liquid layer resolves every frame: liquidComponents() asks for NSColor(self).usingColorSpace(.sRGB), which re-invokes the provider. So a trimmingCharacters plus a Scanner over a string ran at 60 fps, which is how a reporter's macOS profile came to show Color.sRGBComponents(hex:) and closure #1 in Color.init(light:dark:) among its hot leaves. Parses both hexes once in the initialiser, into one always-read pair, and lets the provider pick between the two ready tuples. One declaration rather than two because watchOS resolves straight to the dark hex, so a separate light constant would be unused there and warn. The tokens are static let, so this is now once per token for the life of the process. Deliberately not a cache. A cache needs a key, a lock (a dynamic provider can resolve off the main thread) and an eviction story, all to re-derive per frame a value that cannot change after the initialiser runs. A dynamic Color cannot be read back, as BrandSleepRampTests already notes, so the resolved value is not assertable. The pin is a source census in the same style as QuietMotionCoverageTests: exactly two parses in the initialiser, both appearances reading the hoisted tuples. The regression is silent otherwise, since a parse put back inside a closure renders identically. No behaviour change: the same components reach the same provider, only earlier.
ryanbr
added a commit
that referenced
this pull request
Sep 22, 2026
…me (#2395) Color.init(light:dark:) built a UIColor/NSColor dynamic provider whose closure called sRGBComponents(hex:). That closure runs once per RESOLUTION, not once per token, and the liquid layer resolves every frame: liquidComponents() asks for NSColor(self).usingColorSpace(.sRGB), which re-invokes the provider. So a trimmingCharacters plus a Scanner over a string ran at 60 fps, which is how a reporter's macOS profile came to show Color.sRGBComponents(hex:) and closure #1 in Color.init(light:dark:) among its hot leaves. Parses both hexes once in the initialiser, into one always-read pair, and lets the provider pick between the two ready tuples. One declaration rather than two because watchOS resolves straight to the dark hex, so a separate light constant would be unused there and warn. The tokens are static let, so this is now once per token for the life of the process. Deliberately not a cache. A cache needs a key, a lock (a dynamic provider can resolve off the main thread) and an eviction story, all to re-derive per frame a value that cannot change after the initialiser runs. A dynamic Color cannot be read back, as BrandSleepRampTests already notes, so the resolved value is not assertable. The pin is a source census in the same style as QuietMotionCoverageTests: exactly two parses in the initialiser, both appearances reading the hoisted tuples. The regression is silent otherwise, since a parse put back inside a closure renders identically. No behaviour change: the same components reach the same provider, only earlier.
nathantttttt
added a commit
to nathantttttt/noop
that referenced
this pull request
Oct 14, 2026
Build ryanbr#1 shipped CFBundleVersion 1 over an installed NOOP at 435, which SideStore treats as a downgrade and stalls on. Builds are now 10000 + run number (checked after packaging), and each release carries nathan-source.json so SideStore offers the build as an update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rzp1nid1YA7KA8uvHqAibR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Brings
ryanbr/noopup to the v8.2.2 snapshot taken fromsilentestshadows/noop@f099af09(current upstreammain+ the 2026-07-06 board fix wave).Why
ryanbr/noop:mainwas 158 commits behind upstream. The v8.2.2 snapshot catches the fork up and lands, among other things, faithful Android parity ports of the 8 PRs from this cycle — each credited by PR number in-code:#1035HrvAnalyzer.rollingRmssd minBeatsPerWindow=8 gate (drop 2-beat noisy spikes)#1036HrvAnalyzer.rollingRmssd stepSec thinning stride (day-scale flood guard,window/8)@Volatile+ RR rounding + cancel-stale-reconnect, SourceCoordinator serialize (Android thread-safety / BLE reliability)Plus the wider v8.2.2 wave: i18n, UI/sleep-visual fixes, corrupt-DB quarantine handler, terms-gate clickwrap, AltStore build 172.
Notes
snapshot-v8.2.2points at the exact v8.2.2 head (f099af09); no local edits on top — it's a clean import of the published snapshot.maincurrently carries 10 fork-only commits (the unified fork CI workflows + a few earlier merged testing PRs). Review the merge result for those before merging — the CI workflow files may need re-applying on top after this lands.