Skip to content

Fix #521: stop the repeated "New data added" notification when nothing new arrived#522

Closed
ryanbr wants to merge 119 commits into
NoopApp:mainfrom
ryanbr:fix-repeated-new-data-notification-81
Closed

Fix #521: stop the repeated "New data added" notification when nothing new arrived#522
ryanbr wants to merge 119 commits into
NoopApp:mainfrom
ryanbr:fix-repeated-new-data-notification-81

Conversation

@ryanbr

@ryanbr ryanbr commented Jun 20, 2026

Copy link
Copy Markdown

Fixes #521.

The Today "N new days added" announcement compared repo.days.count against a baseline held in @State (iOS) / remember (Android) — both non-durable. repo.days / days fills async from empty, so on every relaunch / view re-create the baseline was captured at a transient 0, and the next (full) load saw 0 → N and false-posted "N new days of history landed" — repeating on each reopen (the reporter's "every hour").

Fix: persist the baseline (iOS @AppStorage, Android SharedPreferences) so the "announce genuine growth once" invariant survives relaunch, and guard out the not-yet-loaded state (count <= 0 = "not loaded yet", not "zero days") so 0 never becomes the baseline. First real load still sets the baseline silently; only a genuine increase vs the durable baseline posts. Both platforms.

Notes:

  • Upgrade-clean: on the first launch after this ships the key is absent → silent baseline, no spurious post.
  • Couldn't run CI locally — flagging for the Android/Swift checks.

ujix and others added 30 commits June 17, 2026 21:56
…t top bar

Rebuild the iOS Today hero to match WHOOP's polish:

- New GlowRing component (StrandDesign): a crisp full-circle arc over a clearly
  visible track, a bold count-up number, and a tight colour-matched glow (no
  fuzzy bloom), with a spring sweep-in that re-animates on day nav. Replaces the
  240 deg bevel gauge in the three-up hero — Charge centred + enlarged, Rest /
  Effort flanking — floating on the page rather than boxed in a card.
- Compact top bar: profile/settings button (left, opens Settings sheet), centred
  bold day-nav with chevrons (the full date shows only when navigated off today),
  and the strap-battery badge (right). Drops the big "Today" title and the
  duplicated date.
- Move the greeting + SOLID/CALIBRATING data-confidence pill onto the Synthesis
  card's top-right, and remove the "At a glance / Today's Synthesis" header so the
  rings lead the screen.
- ScreenScaffold.title is now optional (iOS Today supplies its own top bar; macOS
  keeps its header). DEBUG --demo-seed sets a demo strap battery so the badge
  renders without a connected strap.
…ild 122)

The .xcodeproj is generated by xcodegen from project.yml; the release build
now regenerates it so the bundle version always tracks the bump.
v4.6.2 only swapped the ring component into Android's old layout, so it kept
the big gold RecoveryRing hero + 'At a glance' header and didn't match the
iOS/macOS WHOOP-style hero. Restructure Today to parity:

- Three GlowRings as the hero, Charge centred + enlarged, Rest/Effort flanking,
  floating on the scenic backdrop with tappable label+chevron beneath each.
- Drop the big RecoveryRing hero; recovery reads as the enlarged Charge ring.
- Move the Support heart into the scaffold's compact top bar (new trailing slot).
- Re-home the Synthesis card (greeting + SOLID/CALIBRATING pill) and the
  HRV/Resting HR/Respiratory rows below the hero, mirroring TodayView.heroSection.

Verified on emulator (demo data): Rest/Charge(enlarged)/Effort render correctly.
Android-only; iOS/macOS were already on this layout in 4.6.2.
The new three-ring Today hero sized the centre ring off (width/2.3) without
allowing for the two inter-ring gaps, so centre + 2×side + 2×gap overflowed the
row and the Row squeezed the last (Effort) column horizontally — its GlowRing box
became taller-than-wide and the arc drew as an ellipse.

- ScoreHeroRow: size the centre ring off (maxWidth - 2×gap) / 2.4 so all three fit.
- GlowRing: draw the arc off min(width,height), centred, so it stays a circle even
  in a non-square box (defense in depth).

Verified on emulator (zoomed): Rest / Charge / Effort all render as equal-radius
circles, Charge enlarged. Android-only.
Flagship + adopted PRs (reimplemented as NoopApp, credited):
- NoopApp#35 (matt): Xiaomi Mi Band import — Mi Fitness on-device DB, fully offline;
  StrandImport parser + GRDB, dedicated iOS/Mac page + Android Data Sources card,
  Explore/Compare/Correlations, sleep hypnogram. (+ NoopApp#36 chart perf folded in.)
- NoopApp#30 (ryanbr): WHOOP 4.0 sleep — sparse-gravity gate median->largest gap so a
  clumped night with a long dropout is bridged, not shredded. Fixes NoopApp#28, NoopApp#33.
- NoopApp#34 (ryanbr): device/toggle-aware raw-export empty-state message. Fixes NoopApp#32.

Issue fixes (cross-platform):
- NoopApp#41: TrendChip suppresses the direction glyph for plain magnitudes ("874 kcal"
  no longer renders a leading dash that read as -874). Swift + Kotlin.
- NoopApp#38: iOS Explore rows push detail via a direct closure NavigationLink (no more
  value+destination double-push).
- NoopApp#22: hide 5/MG-only Settings controls on a 4.0, gated on the auto-detected model
  (keeps the generic CSV export visible; never hides from a real 5/MG owner).
- NoopApp#25: auto-continue backfill also fires on HISTORY_COMPLETE, so a strap slicing a
  deep offload drains back-to-back; cap engages. Swift + Kotlin + tests.
- NoopApp#27: pruneRaw bounds unsynced raw by maxUnsyncedBytes + Puffin capture dir cap.
- NoopApp#20: iOS reads Apple Health body composition (weight/body-fat/lean/BMI), read-only.

Verified: macOS/iOS/Android builds green; StrandAnalytics 410 + WhoopStore 122 +
StrandImport 103 package tests + 722 Android unit tests pass; kcal fix render-confirmed.
Lockstep 4.7.0 (Apple 154/123, Android vc 211).
…ort, steps explainer

- NoopApp#127 Manual HRV snapshot: a "HRV reading" button on Live captures ~60s of live R-R and
  reports RMSSD/SDNN/mean-HR (reuses HRVAnalyzer + the proven Breathe ingest path). All platforms.
- NoopApp#460 Haptic Clock: the strap buzzes the current time (pure HapticClock encoder + buzzTimeNow
  trigger). Apple wires it as a double-tap Automation action (MacActionKind.hapticClock); Android
  exposes a "Buzz the time" Settings button. Pulse.isLong keeps the timing table module-internal.
- NoopApp#461 Phase 1 sleep marks: "Going to sleep" / "I'm awake" buttons on the Sleep screen log a
  timestamped, persisted mark (sleep_mark series) + a greppable strap-log line.
- NoopApp#510 scheduled debug export (Android): bounded 24h rolling strap-log buffer + a daily
  WorkManager export at a chosen time with timestamped filenames; Settings card + app-start reschedule.
- NoopApp#37 steps "no motion synced yet" explainer on the calibration sheet (Swift + Android).

Lockstep: mac CFBundle 155, iOS 124, Android versionCode 212. Verified: mac+iOS xcodebuild,
Android assembleFullRelease (cert 3b1716ab…), 173 WhoopProtocol + 410 StrandAnalytics + Android JVM tests.
…opApp#33/PR#46), step-calibration phone-source fix (NoopApp#37), brew docs (NoopApp#44)

- NoopApp#39/PR#45 (ryanbr): route every live-HR display (Health hero, macOS menu bar) through the
  spike-filtered AppModel.bpm/AppViewModel.bpm median instead of raw live HR, so a one-off optical
  spike (real 91 shown as 170+) no longer leaks to the hero/menu-bar. Disconnect clears the median
  to "—" on both platforms (iOS ingest guard + Android collect-block parity).
- NoopApp#33/PR#46 (ryanbr): durable DismissedSleep tombstone (Room v9→v10) mirroring DismissedWorkout so a
  deleted night stays deleted across on-device recompute; OR'd into the sleepKept overlap filter. +4 tests.
- NoopApp#37 (pikapik487): step calibration now reads phone steps from appleDaily (the table the Android
  Apple-Health/Health-Connect importers actually write steps to) and UNIONS Health Connect — fixes
  the "Not calibrated" stuck state for HC and Apple-Health Android users (engine read was hitting the
  empty dailyMetrics.steps table).
- NoopApp#44 (tonyjacked): corrected the Homebrew install commands in README + HOMEBREW.md to the explicit
  self-hosted tap URL (the bare shorthand resolved to a dead host).

Lockstep: mac CFBundle 156, iOS 125, Android versionCode 213. Verified: mac+iOS xcodebuild exit 0,
Android assembleFullRelease + JVM tests (incl. new SleepEditDurability + Room v9→v10 migration), cert 3b1716ab…
…(caused 'given data not valid JSON')

The GitHub host is gone, so the old raw.githubusercontent.com URLs return an HTML error page
instead of JSON/PNG — sideloaders reported 'given data not valid JSON' when adding the source, and
the source icon was blank. Point both at the self-hosted noop.fans raw URL. (Recurring iOS-install
complaint on the subreddit.)
…day fix, honest smart-alarm copy

From the r/NOOPApp bug-hunt:
- Calories too high: estimateDayCalories applied the gross-exercise Keytel rate to ordinary daytime
  HR (active gate at 0.30 HRR ≈ 94 bpm). Day path now gates at 0.50 HRR and floors the active rate at
  BMR, so light activity isn't credited exercise-intensity burn. Bout (detected/manual workout)
  calories unchanged. Cross-platform (StrandAnalytics + Kotlin twin + tests).
- NoopApp#34/NoopApp#53 (Android): Today HR-graph workout markers queried only "my-whoop"; Health-Connect (and
  apple-health) imported workouts were excluded. Widened to the same source union the Last-Workouts
  feed uses, via a shared workoutsAllSources() seam.
- Android WHOOP-export import keyed the sleep→DailyMetric fold off sleep ONSET (prev evening) instead
  of WAKE-day, landing a night a day early + splitting it; now keys to wake-day to match the cycle
  row + macOS + AppleHealth. +JVM test.
- Smart-alarm Automations card (Apple) over-promised a strap-driven wake; now leads with the
  experimental/keep-a-backup caveat, mirroring Android. Copy only, no alarm-logic change.
Also shipped earlier this session: iOS sideload source URL fix (AltStore "given data not valid JSON").

Lockstep: mac CFBundle 157, iOS 126, Android versionCode 214. Verified: mac+iOS xcodebuild exit 0,
Android assembleFullRelease + JVM tests, 412 StrandAnalytics tests, cert 3b1716ab…
NOOP becomes a raw-signal platform: it reasons from the strap's beat-to-beat R-R, PPG,
motion and skin temperature, computes everything on-device (private, free), and can act on
the body through the strap's haptic motor.

Seven pillars + Lab Book (all opt-in, non-clinical, local-only):
- Haptic biofeedback — personal resonance breathing paced by the wrist motor (screen-off),
  a Calm-me buzz-below-HR down-regulation, and a passive stress check-in nudge.
- Insights 'What Moves You' — n-of-1 lag-aware ranking of what actually moves your recovery,
  plus a personal alcohol/caffeine dose-response with an evening damage forecast.
- Skin-temperature suite — opt-in cycle-phase awareness, a Body Clock / jet-lag helper, and a
  confounder-suppressed illness Heads-Up.
- Your Data, Fused — best-source-wins arbitration across WHOOP + a band + Apple Health/Health
  Connect, on-device, with provenance + honest conflict compare (never a silent average).
- Lab Book — your own private logbook for bloods / BP / scans / notes; trend each marker and
  line it up against a wearable signal. Ships no reference ranges; not medical advice.
- Rhythm (experimental) — a beat-to-beat regularity Poincaré visualization behind a consent
  gate. Descriptive only: no diagnosis, no condition names.
- Smarter BYO-key AI Coach — can optionally fold in your on-device patterns + Lab Book markers,
  summaries only, nothing raw leaves the device.

Architecture: pure on-device engines in the shared Swift packages with value-for-value Kotlin
twins, pinned by golden-vector parity tests (533 StrandAnalytics + 132 WhoopStore Swift, 892
Android JVM, all green). UI per pillar across macOS/iOS/Android, wired into the analytics pass,
Settings opt-ins and consent gates. Builds verified on all three platforms.

Lockstep: macOS CFBundle 158, iOS project 127, Android versionCode 215.

(Also scrubs the maintainer's first name out of a handful of code comments + design specs that
the content gate flagged — the pre-push hook only guards commit metadata, not file contents.)
The three Lab Book sheets sized themselves with .frame(minWidth:minHeight:) on macOS. A macOS
sheet hosting a ScrollView (ScreenScaffold) needs a DEFINITE height — with only a minimum, the
scaffold's height stayed ambiguous and every row collapsed to the top, so the title, fields and
the full marker catalog rendered on top of each other (the 'Add a reading' overlap). Switched all
three to a fixed .frame(width:height:), matching every other working editor sheet (FusedRecord,
AddDeviceWizard, ManualWorkout, KeyMetricsEditor, Sleep edit). macOS build SUCCEEDED.
…issues

After the Lab Book 'Add a reading' overlap, audited every v5 page (SwiftUI + Compose). Fixes:

macOS sheet collapse (ScrollView sheet needs a FIXED frame, not minWidth/none):
- Breathe trainer sheet (StressView), Your-Data-Fused conflict-compare (FusedRecordView),
  Trends report (pre-existing), Workout detail (pre-existing) — all given fixed frames.
- HealthView Fitness-Age: consolidated 2 stacked sheets into one enum-driven .sheet(item:) + frame.

Clipping / layout:
- Lab Book marker picker now SCROLLS (was clipping ~24 of 30 markers under a maxHeight cap).
- Breathe pace pills scroll horizontally instead of truncating on a narrow iPhone.
- Insights hub: moved the outcome/dose segmented controls onto their own row (no more crowding).
- Compose: Fused conflict dialog scrolls (Done no longer clips); illness chips wrap (FlowRow);
  cycle headline + fused value no longer crowd; Lab Book value ellipsizes.

Correctness / parity:
- Rhythm 'Extra/Skipped' tile was the app's CRITICAL RED — recoloured to the calm Rest tone
  (the screen's 'never red/alarm' contract), both platforms.
- Android Breathe now offers the locked Resonance pace; coherence bar/tile use Rest colours.
- Android Health gains the 'Records & sources' deep-links (Lab Book / Your Data Fused) for parity.

Verified: macOS + iOS xcodebuild SUCCEEDED, Android compileFullDebugKotlin SUCCEEDED — 0 errors.
- MARKETING 5.0.1, iOS CURRENT_PROJECT_VERSION 128, macOS CFBundle 159, Android versionCode 216
- The v5 UI/UX sweep fixes (macOS sheet-collapse class, Lab Book catalog scroll, never-red Rhythm,
  Compose clipping + parity) shipped in 1624e06; this release-bumps them.
- Regression gate: 533 StrandAnalytics + 892 Android JVM tests green; mac/iOS/Android builds SUCCEEDED.
Replaces the native tab bar + floating FAB (which overlapped + carried a gold glow) with a single
frosted bar: Today · Trends · [add] · Sleep · More. The add is a small contained gold disc inline
(same gold language, differentiated, no longer hogging space). Real iOS 26 Liquid Glass with a
.ultraThinMaterial fallback below; cleaner flat icons; soft neutral shadow only. Native bar hidden
via .toolbar(.hidden, for: .tabBar). Verified on the iPhone 17 Pro simulator.
The gold blooms/glows read as 'too much' against the cleaner design. Toned down at the source so
it propagates app-wide (mac+iOS): primary gold button now a soft neutral lift (was gold cast-glow);
selected segmented-pill loses its gold/accent glow (crisp flat fill); the global additive bloom
(ring/hero/sparkline halos on dark) dialed to 0.55; the Breathe orb glow radius 26→12 @0.18.
Verified crisper on the iPhone 17 Pro sim in dark mode. (Android parity in the parity pass.)
…ndroid bar/glow/pills

- SegmentedPillControl (shared): selected pill now fills the track height so its inset is EQUAL
  on all sides (was more vertical margin than horizontal).
- iOS onboarding: new 'Make it yours' appearance step (System/Light/Dark, re-themes live) so new
  users discover they can change the look — points to Settings → Appearance.
- Android (Compose) parity: unified glass bottom bar (Today·Trends·[add]·Sleep·More, small contained
  gold disc, no overlap/glow), de-glow (neutral CTA elevation, restrained ring bloom, halved orb),
  equal-margin pills, matching onboarding appearance step.

Verified: iOS-sim + macOS xcodebuild SUCCEEDED, Android compileFullDebugKotlin SUCCEEDED — 0 errors.
The + button moves off the bottom bar (which is now four even tabs: Today·Trends·Sleep·More) into
the top-right of the Today header, balancing the profile avatar on the left. It routes to the shell's
quick-action sheet via a new NavRouter.requestQuickActions() signal. Matches the mockup. Verified on
the iPhone 17 Pro sim.
…ottom bar

Mirrors the iOS change — drops the AddDisc from GlassBottomBar (now four even tabs Today·Trends·
Sleep·More) and adds a 34dp gold quick-action disc to the Today header's trailing edge, wired to the
existing quick-action sheet. Verified on the emulator.
A new UpdateStore (persisted) + Updates inbox sheet, with a bell + unread badge in the Today header
(between the day-nav and the +). The Today info-cards (Live-now, New-here) get an × that tucks them
into the inbox; the inbox offers Restore-to-Today, Mark-all-read, Clear-all, and deep-links tapped
items (e.g. a 'new data landed' reading → Trends via NavRouter.trends). Seeds What's New once per
version. Verified on the iPhone 17 Pro sim (bell + badge + card × render). Android mirror + profile
picture (C) next.
…heet

Hand-port of the iOS Updates inbox: UpdateStore.kt (org.json-persisted, observable, idempotent
What's-New seed), an UpdatesInboxScreen presented as a ModalBottomSheet from a bell in the Today
header (gold unread badge), the two Today info-cards made dismissible (× → tuck into inbox, per-card
prefs flag), Restore-to-Today, Mark-all-read/Clear-all, and a 'new data landed' reading poster that
deep-links to Trends. Verified on the emulator.
NoopApp and others added 25 commits June 19, 2026 20:44
…P-4 pairing fix (NoopApp#50, Android)

- NoopApp#69 (iPhone & Mac): the Today Synthesis card's greeting + recovery/calibration pill no longer crowd the card headline on narrow iPhones — overlay width-capped + greeting line-limited; InsightCard gains an opt-in titleTrailingInset so its title rows reserve room.
- NoopApp#50 (Android): OnePlus BT stack fires onMtuChanged twice with the same value, wedging CCCD subscription so the WHOOP 4.0 bond never finishes (infinite watchdog bounce). Dedupe a same-value MTU callback within 1s (device-agnostic, always spurious) + a 450ms CCCD settle on OnePlus before the first descriptor write. Watchdog intact.
- Lockstep bump 5.2.4 (iOS build 135, mac CFBundle 166, Android versionCode 223).
…opApp#78)

Fixes a 5.2.3 regression: the 'put the strap in pairing mode' hint was keyed off
lastBondedPeripheralUUID, so a strap that had bonded in a PRIOR session but now refuses
the encrypted bond (held by the WHOOP app, or a stale iOS pairing after a Restored-
CONNECTED-peripheral) got the wrong 'transient reconnect race' message and NO actionable
guidance — exactly NoopApp#78. Now: a bondRefusalStreak counter (reset only on a genuine bond,
not on disconnect, so it accumulates across the reconnect loop) surfaces the guidance from
the 2nd consecutive refusal, while a single transient refusal right after a good bond (NoopApp#74)
stays quiet. Guidance also now tells the user to Forget This Device in iOS Bluetooth.
Unreleased on main — rides the next build; the user's immediate fix is the workaround.
…usal guidance (NoopApp#78)

- forgetDevice(): removing a WHOOP from Devices now stops auto-reconnect, drops the BLE
  link, and clears the targeted-connect pin + iOS state-restoration ref — so NOOP lets go
  instead of re-grabbing it, freeing a stuck 5/MG to enter pairing mode and re-pair.
- bondRefusalStreak guidance (already on main): surface the real pairing-mode + forget-device
  steps on a persistent 5/MG bond refusal (fixes the 5.2.3 'transient race' regression).
- Apple-only (shared Swift BLE engine). Lockstep bump 5.2.5 (iOS build 136, mac CFBundle 167).
… the mirror

- AltStore (altstore-source.json + update-altstore-source.sh): IPA downloadURL → GitHub release assets
- Homebrew (update-homebrew-cask.sh): cask url → GitHub release asset; dual-push tap to GitHub + forge mirror; simpler default-host tap command
- README + docs + issue templates + community files: Releases/Issues/Wiki/badge URLs → github.com/NoopApp/noop; brew install simplified; noop.fans noted as mirror
- Badges (refresh-stats-badges.py): read from GitHub API, write docs/stats/*.json locally (multi-push distributes them) — ends the noop.fans-via-API divergence
- New Tools/release.sh: publishes a release to BOTH GitHub (canonical) + Forgejo (mirror)
- Stats refreshed from GitHub (latest v5.2.5)
…ain)

- UpdateChecker.swift + UpdateCheck.kt: endpoint noop.fans/api/v1 → api.github.com/repos/NoopApp/noop/releases/latest (drop-in; same tag_name/html_url/body fields). User-initiated, on-device, nothing sent.
- Settings → About 'project home & source' link + donation URL → github.com (noop.fans kept as mirror).
- AltStore + Homebrew already pull from GitHub releases (channel configs repointed).
- Lockstep 5.2.6 (iOS build 137, mac CFBundle 168, Android versionCode 224).
… nothing new arrived

The Today "New data — N days added" announcement compared repo.days.count to a baseline held in
@State (iOS) / remember (Android) — both NON-durable. repo.days / days fills ASYNC from EMPTY, so on
every relaunch / view re-create the baseline was captured at a transient 0, and the next (full) load
saw "0 -> N" and false-posted "N new days of history landed" — repeating on each reopen.

Persist the baseline (iOS @AppStorage, Android SharedPreferences) so the "announce genuine growth
once" invariant survives relaunch, and guard out the transient-empty load (count <= 0 = "not loaded
yet", not "zero days") so 0 never becomes the baseline. First real load still sets the baseline
silently; only a genuine increase vs the durable baseline posts. Both platforms.
@ryanbr ryanbr force-pushed the fix-repeated-new-data-notification-81 branch from b0e6d95 to 434d513 Compare June 20, 2026 07:39
@ryanbr ryanbr changed the title Fix #81: stop the repeated "New data added" notification when nothing new arrived Fix #521: stop the repeated "New data added" notification when nothing new arrived Jun 20, 2026
@ryanbr

ryanbr commented Jun 20, 2026

Copy link
Copy Markdown
Author

Superseded — this is already fixed in v5.3.0 (#521: the "New data added" inbox spam). The shipped fix persists the last announced day key rather than a day-count, which dedups more cleanly than this approach, so closing. Thanks!

@ryanbr ryanbr closed this Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Repeated “new data added” notifications

3 participants