Repository navigation
Preserve repeated corrupt-DB quarantines instead of dropping the newest (#661) - #681
Merged
Merged
Conversation
…st (#661) The #1014 corruption handler kept a single fixed `.corrupt` slot: the FIRST corrupt copy was preserved and every later one deleted (the `else` branch). But a later corruption is a rebuilt store that has banked new, non-resendable strap history since — so the copy it discarded was the most recovery-worthy one, silently defeating the handler's whole purpose. Quarantine under a TIMESTAMPED name (`whoop.db.corrupt.<epochMillis>`) and keep the newest MAX_CORRUPT_QUARANTINES (3), pruning older ones. This caps disk (a crash loop can't multiply 100 MB files — the original concern) AND preserves the latest data plus a couple of priors for diagnosing a recurring corruption path. The naming/pruning is a pure, unit-tested function. Also move the WAL/SHM sidecars ALONGSIDE the quarantine rather than deleting them: the WAL can hold the most recent un-checkpointed writes. The live path is still cleared (rename, or copy+delete on the cross-storage fallback), so the fresh rebuild can never inherit a stale write-ahead log — a stale live WAL could otherwise be applied to the new DB by salt and re-inject corrupt pages. Corrected the class-doc, which claimed the factory 'deletes NOTHING' — the old else-branch deleted the original and the sidecars were always dropped. Android-only: iOS/GRDB has no delete-on-corruption default (this handler exists to neutralise Android's), and already quarantines with timestamped names. Reported-by: tigercraft4 (static data-integrity audit). Verified: compileFullDebugKotlin + CorruptDbQuarantineTest + com.noop.data.*. Author: ryanbr <mp3geek@gmail.com>
ryanbr
added a commit
that referenced
this pull request
Jul 20, 2026
Marketing version stays 9.0.3; Android versionCode 296->297 and iOS CURRENT_PROJECT_VERSION 206->207 so the new staging build installs over the previous one. Content since the last testing build: Health Connect writeback failure surfacing (#672), SpO2/respiration empty-track explanation (#673), WHOOP 5/MG raw-IMU offload persist (#675), hide stale sleep recompute entries (#679), Oura activity MET research corpus + cap/rotation (#676/#680), trend chart date alignment (#528), and corrupt-DB quarantine preservation (#681).
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…st (ryanbr#661) (ryanbr#681) Timestamped .corrupt.<epoch> quarantine + keep-newest-3 pruning replaces the single fixed .corrupt slot that deleted every later corrupt copy (the most recovery-worthy one). WAL/SHM moved alongside the quarantine; live path always cleared so the rebuild can't inherit a stale WAL (ryanbr#1014 crash-loop invariant preserved). Pure planCorruptQuarantine + unit test. Android-only; iOS/GRDB has no delete-on-corruption default. Reported-by: tigercraft4.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
Marketing version stays 9.0.3; Android versionCode 296->297 and iOS CURRENT_PROJECT_VERSION 206->207 so the new staging build installs over the previous one. Content since the last testing build: Health Connect writeback failure surfacing (ryanbr#672), SpO2/respiration empty-track explanation (ryanbr#673), WHOOP 5/MG raw-IMU offload persist (ryanbr#675), hide stale sleep recompute entries (ryanbr#679), Oura activity MET research corpus + cap/rotation (ryanbr#676/ryanbr#680), trend chart date alignment (ryanbr#528), and corrupt-DB quarantine preservation (ryanbr#681).
brendankowitz
pushed a commit
to brendankowitz/noop
that referenced
this pull request
Aug 9, 2026
Rebuilds the in-exercise screen (ryanbr#238) to the Hearth mockup's §1e heart-rate-led take. It stops being a stack of cream cards on a light scenic backdrop and becomes one full-bleed moment: a dark-plum sky, a recording pill, HR leading at 96pt, Effort second, the HR zone rail, a frosted Avg/Peak/Effort strip, and a floating control bar pinned to the bottom. Visual only — every number is the same live value the card layout showed, from the same sources: smoothed `AppModel.bpm`, `HRZones` off the user's HR-max, `ActiveWorkout.liveStrain`/avgHr/peakHr, and the TimelineView elapsed tick. The LiveState isolation (`SensorRowIfPresent` owning its own observation so a 1 Hz packet doesn't invalidate the hero), the ryanbr#681 realtime-HR arm/disarm balance, the ryanbr#703 keep-awake hold and the ryanbr#517 end-confirm are all preserved verbatim. Notes: - Drops the last live `StrainGauge` call site in the app (the layered bevel gauge that the rest of the retheme already flattened elsewhere). The Effort numeral replaces it; the band word it used to draw (LIGHT / MODERATE / STRENUOUS / HIGH / ALL-OUT, `showsLabel` defaulted true) is kept on the SAME thresholds, so the screen says exactly what it said before — no new classifier was invented for it. - No pause/resume exists in the recorder (AppModel has start/end only), so the bar's right-hand control is a close-and-keep-recording chevron, not the mockup's pause glyph. Both controls are 56pt, clear of the 44pt minimum. - Zone rail: inactive rungs are a tint of THAT zone's own colour; the active rung's label picks ink or off-white by the fill's luminance, since the Hearth zone ramp runs light sage to dark maroon. - Colours route through the override layer: two additions to HearthTheme (the mockup's live-workout sky ramp, and an on-sky rose — metricRose is authored for warm paper and is below AA on the plum). No hex literals in the view. - DEBUG `--demo-workout` renders one fixed synthetic reading so the populated hero can be screenshotted at all (the Simulator has no BLE); same in-file launch-flag idiom as `--demo-reveal`, stripped from Release. Verified: iOS NOOPiOS and macOS Strand both build; screenshotted the populated hero and the honest no-reading state in the Simulator. Not strap-tested (no BLE path was touched). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GPcFU4xvYzxovzd4aQ7uGB
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…st (ryanbr#661) (ryanbr#681) Timestamped .corrupt.<epoch> quarantine + keep-newest-3 pruning replaces the single fixed .corrupt slot that deleted every later corrupt copy (the most recovery-worthy one). WAL/SHM moved alongside the quarantine; live path always cleared so the rebuild can't inherit a stale WAL (ryanbr#1014 crash-loop invariant preserved). Pure planCorruptQuarantine + unit test. Android-only; iOS/GRDB has no delete-on-corruption default. Reported-by: tigercraft4.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
Marketing version stays 9.0.3; Android versionCode 296->297 and iOS CURRENT_PROJECT_VERSION 206->207 so the new staging build installs over the previous one. Content since the last testing build: Health Connect writeback failure surfacing (ryanbr#672), SpO2/respiration empty-track explanation (ryanbr#673), WHOOP 5/MG raw-IMU offload persist (ryanbr#675), hide stale sleep recompute entries (ryanbr#679), Oura activity MET research corpus + cap/rotation (ryanbr#676/ryanbr#680), trend chart date alignment (ryanbr#528), and corrupt-DB quarantine preservation (ryanbr#681).
xxblyxx
added a commit
to xxblyxx/noop
that referenced
this pull request
Aug 21, 2026
startWorkout() took no realtime-HR reference; every existing caller (LiveWorkoutView's sheet, the Live tab) was screen-scoped. Dismissing the sheet right after Start dropped the ref-count to zero and silently stopped capturing samples on a WHOOP 5/MG, since live HR there only flows while the puffin realtime stream is armed (ryanbr#681-shaped) — captureWorkoutSample bailed on every sample and endWorkout discarded the session with nothing to save. startWorkout() and rehydrateActiveWorkout() now take a realtime-HR reference for as long as activeWorkout is non-nil, released in endWorkout(). Ref-counted and clamped at 0, so it composes with LiveWorkoutView's own start/stop pair without either disarming the other. Android's requestRealtimeHr/releaseRealtimeHr stays screen-scoped (ui/LiveWorkoutScreen.kt:79-80) — a known, deliberate divergence, not a formula or stored value, so no Kotlin twin is required for this behavior change.
ryanbr
pushed a commit
that referenced
this pull request
Sep 10, 2026
The Breathe screen reads live beats but never asked for them. On a WHOOP 5 or MG, which only streams while armed, that left the BPM hero and the RMSSD tile on a dash and the coherence card on "No data" unless the wearer happened to have opened Live first, which is why it looked intermittent rather than broken. Both platforms consume the stream: Swift ingests R-R through `onRRPackets` and Kotlin collects `rrPackets()` into the buffer behind `Hrv.rmssd`. Neither armed it. The screen now takes the stream while it is visible, through the ref-counted request that Live and the in-exercise view already use. Because it is ref-counted, moving between Breathe and Live never stops the stream out from under whichever screen stays. Arming on visible rather than on a running session is deliberate. The BPM hero and the RMSSD tile are on screen before a session starts, so gating on the session would leave a wearer looking at two dashes and deciding the feature is broken, which is the report this came from. The cost is bounded the same way Live's is, by a foreground screen the wearer opened, and the lightweight HR recording is unchanged either way. The two sides were verified against each other rather than trusted for looking alike: both hold the same counter, both arm on the zero to one edge, both blank the stale smoothing window there and only there, and both clamp the release at zero. Nothing else on this screen touches the count, so the appear and disappear pair is balanced. Relates to #681, which is where the same only-streams-while-armed behaviour was first pinned down. Thanks to @kiesstein.
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.
Fixes #661.
The bug
The #1014 corruption handler (
CorruptionPreservingOpenHelperFactory.onCorruption) kept a single fixed.corruptslot: the first corrupt copy was preserved and every later one deleted (theelse { original.delete() }branch). But a later corruption is a rebuilt store that has banked new, non-resendable strap history since — so the copy it discarded was the most recovery-worthy one, silently defeating the handler's whole purpose. tigercraft4 flagged this in a static data-integrity audit.The behaviour was a deliberate disk bound ("repeated failed opens can't multiply 100 MB files") — but it picked disk-safety over the newest data.
The fix
whoop.db.corrupt.<epochMillis>+ keep newestMAX_CORRUPT_QUARANTINES(3), pruning older ones. Caps disk (a crash loop can't multiply files) and preserves the latest data plus a couple of priors for diagnosing a recurring corruption path. Naming/pruning is a pure, unit-tested function (planCorruptQuarantine).Scope / parity
Android-only. iOS/GRDB has no delete-on-corruption default (this Android handler exists to neutralise the platform default), and
WhoopStore.quarantineIncompatibleDatabasealready uses timestamped names — so this converges the two rather than diverging. No analytics/stored-data change → parity contract untouched.Verification
./gradlew compileFullDebugKotlin✓./gradlew testFullDebugUnitTest --tests com.noop.data.CorruptDbQuarantineTest✓ (first/under-cap/keep-newest-N/eviction-order/never-evict-new/dedup)./gradlew testFullDebugUnitTest --tests com.noop.data.*✓ (no existing regressions)The filesystem move/prune is I/O and validated by inspection; the decision logic is unit-tested.
Reported-by: tigercraft4