Repository navigation
fix(android): give every notifier a distinct notification id (#297) - #304
Merged
Merged
Conversation
notify() is tagless, so notifiers sharing an id silently replace each other's undismissed notifications. On main, id 4203 was posted by InactivityNotifier, BatteryAlertNotifier.NOTIF_ID_LOW and ScheduledReportNotifier.MORNING; id 4204 by SmartAlarmNotifier, BatteryAlertNotifier.NOTIF_ID_FULL and .WORKOUT — so e.g. a low-battery warning wiped the morning recap. Reassign to a clean, fully-distinct map: 4201 connection, 4202 illness, 4203 inactivity, 4204 smart alarm, 4205/4206/4207 battery (runtime/low/full), 4208/4209 scheduled report (morning/workout). (Corrects #289, which moved NOTIF_ID_LOW to 4205 — already taken by the battery NOTIF_ID_RUNTIME in the same file — and bundled an unrelated AUDIT.md. Battery needs THREE ids, not two.)
This was referenced Jul 12, 2026
ryanbr
added a commit
that referenced
this pull request
Jul 18, 2026
…he calendar day (#567) (#589) The morning recap re-fired at midnight for anyone up late. ScheduledReportNotifier.onMorning deduped on LocalDate.now() (the phone's CALENDAR day, rolls at midnight), but the recap reports a banked NIGHT and _today resolves via the logical day (rolls at 04:00, #304 carve-out). For a late-nighter crossing midnight without sleeping, the new calendar day has no banked night, so resolveTodayRow falls back to LAST night's row (totalSleepMin != null) — while the dedup key had already advanced to the new calendar day, so `lastNotifiedDay != today` went true again and the recap re-fired with last night's data ("sent at the start of a new day"). Fix: dedup on the REPORTED NIGHT's day (the resolved today-row's `day`), not the calendar day. - onMorning gains a reportDay param; AppViewModel passes todayRow.day. - Policy param renamed today -> reportDay with a doc note (prevents a regression back to LocalDate.now()). Logic unchanged: enabled && present && lastNotifiedDay != reportDay. At midnight the row still resolves to last night (reportDay unchanged) → already notified → no re-fire. The real morning's new-night row fires normally. No pref migration: reportMorningDay stays an ISO yyyy-MM-dd string; a calendar day and a row day compare the same, so an upgrading user simply fires normally the next morning. Android-only (the morning recap is an Android feature; no iOS twin). Strap-agnostic (day-key logic). Tested: ./gradlew compileFullDebugKotlin + testFullDebugUnitTest --tests ScheduledReportPolicyTest -> 16/16 (added a midnight-rollover regression: reportDay = last night's day → suppressed).
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
) (ryanbr#304) notify() is tagless, so notifiers sharing an id silently replace each other's undismissed notifications. On main, id 4203 was posted by InactivityNotifier, BatteryAlertNotifier.NOTIF_ID_LOW and ScheduledReportNotifier.MORNING; id 4204 by SmartAlarmNotifier, BatteryAlertNotifier.NOTIF_ID_FULL and .WORKOUT — so e.g. a low-battery warning wiped the morning recap. Reassign to a clean, fully-distinct map: 4201 connection, 4202 illness, 4203 inactivity, 4204 smart alarm, 4205/4206/4207 battery (runtime/low/full), 4208/4209 scheduled report (morning/workout). (Corrects ryanbr#289, which moved NOTIF_ID_LOW to 4205 — already taken by the battery NOTIF_ID_RUNTIME in the same file — and bundled an unrelated AUDIT.md. Battery needs THREE ids, not two.)
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…he calendar day (ryanbr#567) (ryanbr#589) The morning recap re-fired at midnight for anyone up late. ScheduledReportNotifier.onMorning deduped on LocalDate.now() (the phone's CALENDAR day, rolls at midnight), but the recap reports a banked NIGHT and _today resolves via the logical day (rolls at 04:00, ryanbr#304 carve-out). For a late-nighter crossing midnight without sleeping, the new calendar day has no banked night, so resolveTodayRow falls back to LAST night's row (totalSleepMin != null) — while the dedup key had already advanced to the new calendar day, so `lastNotifiedDay != today` went true again and the recap re-fired with last night's data ("sent at the start of a new day"). Fix: dedup on the REPORTED NIGHT's day (the resolved today-row's `day`), not the calendar day. - onMorning gains a reportDay param; AppViewModel passes todayRow.day. - Policy param renamed today -> reportDay with a doc note (prevents a regression back to LocalDate.now()). Logic unchanged: enabled && present && lastNotifiedDay != reportDay. At midnight the row still resolves to last night (reportDay unchanged) → already notified → no re-fire. The real morning's new-night row fires normally. No pref migration: reportMorningDay stays an ISO yyyy-MM-dd string; a calendar day and a row day compare the same, so an upgrading user simply fires normally the next morning. Android-only (the morning recap is an Android feature; no iOS twin). Strap-agnostic (day-key logic). Tested: ./gradlew compileFullDebugKotlin + testFullDebugUnitTest --tests ScheduledReportPolicyTest -> 16/16 (added a midnight-rollover regression: reportDay = last night's day → suppressed).
DX23876
added a commit
to DX23876/noop
that referenced
this pull request
Aug 1, 2026
…dditively Three correctness follow-ups to the reviewed revision. Day keying. applyingEditedSleepSessions grouped its sessions with a single current-zone offset applied across all history, while userEditedDays — which decides where the debt correction lands — resolves the offset at each session's own end instant. The two could therefore disagree about which day a night belongs to across a DST boundary, leaving the rebuilt sleep fields on one day and the debt correction on another. The keyer existed in three copies, one of them wrong; it is now Repository.sleepEndDayKey and userEditedDays, mergeSleep and the edited-day overlay all go through it. This is the ryanbr#406 (Swift) / ryanbr#304 (Android) local wake-day rule that MergeSleepLocalDayTest pins. Owner probe order. sleepOwnerIds was computedReadIds + importedReadIds, putting the CANONICAL computed namespace ahead of the ACTIVE imported one. That demotes the active imported source relative to the previous computed-then-imported chain and lets a coincidental same-detectedStartTs row capture an edit — the failure the single fallback was written to avoid. The order is now strictly additive over the pre-union behaviour: the active strap's two namespaces keep their exact previous order, and the canonical pair is appended as a fallback that was not probed at all before, so no case that worked before can regress. The doc comment also claimed computed rows outrank imported ones, which SleepMerge.merge documents the other way round; corrected. Android edited-day set. It tried a UTC key first and fell back to UTC, which re-opens ryanbr#304 for a UTC+ user who wakes after local midnight but before UTC midnight. It now uses localDayString, the helper this same file already uses eight lines above to group sleeps by local day. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8 of 12 tasks
DX23876
added a commit
to DX23876/noop
that referenced
this pull request
Aug 1, 2026
…dditively Three correctness follow-ups to the reviewed revision. Day keying. applyingEditedSleepSessions grouped its sessions with a single current-zone offset applied across all history, while userEditedDays — which decides where the debt correction lands — resolves the offset at each session's own end instant. The two could therefore disagree about which day a night belongs to across a DST boundary, leaving the rebuilt sleep fields on one day and the debt correction on another. The keyer existed in three copies, one of them wrong; it is now Repository.sleepEndDayKey and userEditedDays, mergeSleep and the edited-day overlay all go through it. This is the ryanbr#406 (Swift) / ryanbr#304 (Android) local wake-day rule that MergeSleepLocalDayTest pins. Owner probe order. sleepOwnerIds was computedReadIds + importedReadIds, putting the CANONICAL computed namespace ahead of the ACTIVE imported one. That demotes the active imported source relative to the previous computed-then-imported chain and lets a coincidental same-detectedStartTs row capture an edit — the failure the single fallback was written to avoid. The order is now strictly additive over the pre-union behaviour: the active strap's two namespaces keep their exact previous order, and the canonical pair is appended as a fallback that was not probed at all before, so no case that worked before can regress. The doc comment also claimed computed rows outrank imported ones, which SleepMerge.merge documents the other way round; corrected. Android edited-day set. It tried a UTC key first and fell back to UTC, which re-opens ryanbr#304 for a UTC+ user who wakes after local midnight but before UTC midnight. It now uses localDayString, the helper this same file already uses eight lines above to group sleeps by local day.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
) (ryanbr#304) notify() is tagless, so notifiers sharing an id silently replace each other's undismissed notifications. On main, id 4203 was posted by InactivityNotifier, BatteryAlertNotifier.NOTIF_ID_LOW and ScheduledReportNotifier.MORNING; id 4204 by SmartAlarmNotifier, BatteryAlertNotifier.NOTIF_ID_FULL and .WORKOUT — so e.g. a low-battery warning wiped the morning recap. Reassign to a clean, fully-distinct map: 4201 connection, 4202 illness, 4203 inactivity, 4204 smart alarm, 4205/4206/4207 battery (runtime/low/full), 4208/4209 scheduled report (morning/workout). (Corrects ryanbr#289, which moved NOTIF_ID_LOW to 4205 — already taken by the battery NOTIF_ID_RUNTIME in the same file — and bundled an unrelated AUDIT.md. Battery needs THREE ids, not two.)
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…he calendar day (ryanbr#567) (ryanbr#589) The morning recap re-fired at midnight for anyone up late. ScheduledReportNotifier.onMorning deduped on LocalDate.now() (the phone's CALENDAR day, rolls at midnight), but the recap reports a banked NIGHT and _today resolves via the logical day (rolls at 04:00, ryanbr#304 carve-out). For a late-nighter crossing midnight without sleeping, the new calendar day has no banked night, so resolveTodayRow falls back to LAST night's row (totalSleepMin != null) — while the dedup key had already advanced to the new calendar day, so `lastNotifiedDay != today` went true again and the recap re-fired with last night's data ("sent at the start of a new day"). Fix: dedup on the REPORTED NIGHT's day (the resolved today-row's `day`), not the calendar day. - onMorning gains a reportDay param; AppViewModel passes todayRow.day. - Policy param renamed today -> reportDay with a doc note (prevents a regression back to LocalDate.now()). Logic unchanged: enabled && present && lastNotifiedDay != reportDay. At midnight the row still resolves to last night (reportDay unchanged) → already notified → no re-fire. The real morning's new-night row fires normally. No pref migration: reportMorningDay stays an ISO yyyy-MM-dd string; a calendar day and a row day compare the same, so an upgrading user simply fires normally the next morning. Android-only (the morning recap is an Android feature; no iOS twin). Strap-agnostic (day-key logic). Tested: ./gradlew compileFullDebugKotlin + testFullDebugUnitTest --tests ScheduledReportPolicyTest -> 16/16 (added a midnight-rollover regression: reportDay = last night's day → suppressed).
xxblyxx
added a commit
to xxblyxx/noop
that referenced
this pull request
Aug 21, 2026
The Home Screen widget showed heart rate, battery and the connection dot
but blanked Charge, Effort, Rest, HRV and Resting HR — while Today showed
four of those five as real numbers.
`WidgetSnapshot.publish` resolved ONE recovery-gated anchor day
(`Repository.widgetAnchor`) and read Charge, Effort, HRV and Resting HR off
it, with the `sleep_performance` read nested inside `if let day`. That
anchor returns non-nil only when some row carries a non-null `recovery`, so
a store with nothing scored blanked all five at once. Only bpm/battery/
bonded survived, because they come off `model.live` and never touch the
anchor. Today does not have this problem: it resolves the same five stats
through four independent, mostly recovery-INDEPENDENT selectors, so one
null recovery column blanked five widget fields but only one app field.
Adds `Repository.glanceFields`, a pure injectable resolver that gives each
stat the selector its Today call site uses. It invents no new selector — it
composes four that already exist:
Charge widgetAnchor (the one field the gate belongs on)
Effort today's own row never carries; yesterday's strain
shown as today's is a false statement
Rest TodayView.freshRestScore the shared ryanbr#977 staleness gate that
WatchSessionBridge already adopted;
no longer nested behind the anchor
HRV / RHR today-first per-field carry over lastVitalsDay, which is
explicitly recovery-independent
`todayKey` is pinned to `todayRow?.day ?? logicalKey` (byte-identical to
widgetAnchor's own carriedKey), NOT the 2-arg lastVitalsDay overload's
`max(logicalKey, localKey)` — those diverge in the ryanbr#304 pre-04:00 window.
Unreachable today, but it is the near-miss class that produced ryanbr#911.
Charge stays blank on this store because no row carries a recovery score at
all — the same reason Today's Charge hero is blank. That is a separate
data gap, deliberately not addressed here.
Render side unchanged: NOOPWidget already renders every field honest-null,
and the wire schema is untouched, so no App-Group migration.
`exploreSeries` is now unconditional on the full publish. It was already
paid whenever an anchor existed; `publishLive` still bypasses it, so the
high-frequency HR/battery path is unaffected.
Verification: `xcodegen generate` + `xcodebuild` BUILD SUCCEEDED for both
the macOS `Strand` scheme and the iOS `NOOPiOS` scheme (which compiles the
embedded NOOPiOSWidgets extension) — CI does not build the app targets in
this repo. New `StrandTests/WidgetGlanceFieldsTests` (14 tests) passes
14/14 and `WidgetAnchorTests` still passes 9/9 on `platform=macOS`; the new
suite is pure — no clock, no AppModel, no store, no strap. Not yet
confirmed on device.
Kotlin twin written but NOT compiled locally (no JDK, no Android SDK).
Both Android producers carried the identical coupling and both already had
`resolveTodayRow` in scope; `restPct`/`effortPct` and `widgetRest`/
`widgetEffort` now read today's row while recovery keeps the anchor.
Android's WidgetSnapshot has no HRV or resting-HR field, so `lastVitalsRow`
does not enter the twin. `StrainScorer.effectiveEffort` deliberately left
alone — Android's Today hero uses it and iOS's does not, a pre-existing
divergence out of scope here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfTpUwbM2d7TmwyGneB7mx
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
Consume the approved day-aware Home route handoff and align the Strain target marker across platforms. Refs ryanbr#304
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
Refs ryanbr#304 # Conflicts: # Strand/Screens/HomeDashboardContent.swift # android/app/src/main/java/com/noop/ui/TodayScreen.kt
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
Route selected Sleep through the shell path. Token changes are the D1 owner handoff from e2ed08e; Sleep hook lines are from 59eac66. Refs ryanbr#304
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
Use the selected activity window separately from a carried score day, keep recorded unscored sleep honest, and correct the merged Insights source guard. The named source argument follows Plan owner 2dc0456. Refs ryanbr#304
TrapstarKS
added a commit
to TrapstarKS/troop
that referenced
this pull request
Oct 4, 2026
…al label on one line Previous day skipped the local-calendar day when the ryanbr#304 carve-out surfaced it before 04:00. Shared homeDayAnchor on Swift and Kotlin with a parity test. Android compact ScoreDial label now auto-shrinks like iOS lineLimit(1) + minimumScaleFactor.
stammi922
added a commit
to stammi922/noop
that referenced
this pull request
Nov 1, 2026
Sessions were keyed by the logical day (04:00 rollover) while Today reads the Rest credit for repo.today?.day, which carries the ryanbr#304 pre-04:00 carve-out. After a 22:00-02:45 sleep, a 03:15 session was credited to yesterday while Today showed the wake day, so the credit never appeared. The key now goes through the same resolveToday, falling back to the logical day like Today's selectedDayKey. NSDRSessionDayKeyTests pins the 03:15 case and the fallbacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Closes #297. Reimplements #289 with the collision corrected.
The bug
notify()is called tagless, so notifiers sharing an id silently replace each other's undismissed notifications. On main:NOTIF_ID_LOW, ScheduledReportNotifier.MORNING(3-way)NOTIF_ID_FULL, ScheduledReportNotifier.WORKOUT(3-way)A low-battery warning could wipe the morning recap, a move reminder wipe the low-battery warning, etc.
Fix
Fully-distinct map:
4201connection ·4202illness ·4203inactivity ·4204smart alarm ·4205/4206/4207battery (runtime/low/full) ·4208/4209scheduled report (morning/workout). Verified all nine ids are unique.Why not #289
#289 moved
NOTIF_ID_LOWto 4205 — butNOTIF_ID_RUNTIMEin the same file is already 4205, so it traded one collision for another (battery has three posters, not two). It also bundled an unrelated 200-lineAUDIT.md. This drops both problems.Android-only;
compileFullDebugKotlinclean; ids verified distinct by grep.