Repository navigation
Privacy: add retention for scheduled debug exports - #960
Conversation
…r#650) Scheduled debug exports (strap log + optional raw 5/MG capture) accumulated on disk indefinitely with no cleanup and no visible control: - Android wrote noop-straplog-<stamp>.txt/.bin into cache/logs on every daily WorkManager run. - iOS/macOS wrote noop-strap-log-<stamp>.txt / noop-raw-capture-<stamp>.json into the Files-visible Documents folder on every scheduled/"Run now" drop. Both platforms now prune to a keep-count of generations (a log+raw pair counts once) after every write, mirroring the existing BackupSync keep-count pattern (pure stamp-extraction + prune-list helpers, unit tested). Default keeps 14 generations; the Test Centre export card exposes a keep-count picker (3/7/14/30/60) plus a manual "Clear scheduled exports" action with a confirmation dialog, for an immediate wipe independent of the daily cap. New user-facing strings are translated for de/es/fr/pt-PT (plus zh/zh-Hans, it, ru to satisfy the i18n audit's non-regression ratchet).
ryanbr
left a comment
There was a problem hiding this comment.
Checked the deletion scoping first, since that's the part that can hurt someone. It's tight on both
platforms: every delete is gated on the filename yielding a stamp, so nothing else in the folder is
reachable. The Apple side is actually stricter — it needs the extension to match too, so a file someone
dropped in Documents called noop-strap-log-notes.md survives. And noop-strap-log- doesn't match
noop-straplog-, so Android's interactive shares are safe. Fragile-looking, correct, and you tested it.
Two notes on your own caveats:
Your Kotlin tests did run. Android CI executed testFullDebugUnitTest and passed — Gradle just
doesn't name passing classes. So LogExportRetentionTest is covered.
Your Swift test has never run anywhere. StrandTests isn't in CI at all, and you ran build not
test, so ScheduledDebugExportTests is unexecuted by you and by us. Worth one xcodebuild test before
this lands — it's the only thing covering the Apple retention math.
Small thing: the two new clear-result toasts are raw English, so "new user-facing strings are translated"
isn't quite complete. LogExport.kt already has six raw toasts and no uiString, so you're matching the
file — not asking you to change it, just flagging that the audit can't see them because they go through a
variable.
…yanbr#676) Append each anchored Oura 0x50 activity/MET record to a diagnostic JSONL sidecar for offline protocol RE — instrumentation only, never scored, never a datastore row (Tier-B honest-data invariant). Both platforms with a byte-pinned line encoder; append-only, crash-safe, deduped by a persisted ring-time high-water. Rebuild of ryanbr#447 off current main. Related ryanbr#960.
…yanbr#676) Append each anchored Oura 0x50 activity/MET record to a diagnostic JSONL sidecar for offline protocol RE — instrumentation only, never scored, never a datastore row (Tier-B honest-data invariant). Both platforms with a byte-pinned line encoder; append-only, crash-safe, deduped by a persisted ring-time high-water. Rebuild of ryanbr#447 off current main. Related ryanbr#960.
…r#650) (ryanbr#960) Scheduled debug exports (strap log + optional raw 5/MG capture) accumulated on disk indefinitely with no cleanup and no visible control: - Android wrote noop-straplog-<stamp>.txt/.bin into cache/logs on every daily WorkManager run. - iOS/macOS wrote noop-strap-log-<stamp>.txt / noop-raw-capture-<stamp>.json into the Files-visible Documents folder on every scheduled/"Run now" drop. Both platforms now prune to a keep-count of generations (a log+raw pair counts once) after every write, mirroring the existing BackupSync keep-count pattern (pure stamp-extraction + prune-list helpers, unit tested). Default keeps 14 generations; the Test Centre export card exposes a keep-count picker (3/7/14/30/60) plus a manual "Clear scheduled exports" action with a confirmation dialog, for an immediate wipe independent of the daily cap. New user-facing strings are translated for de/es/fr/pt-PT (plus zh/zh-Hans, it, ru to satisfy the i18n audit's non-regression ratchet). Co-authored-by: tigercraft4 <>
Thanks to @pipiche38, who captured the evidence and wrote the correction. Commands.swift claimed real_steps is server-flag-gated off and so "never emitted for an offline NOOP-only ring". On-device captures show the status read (2f 02 20 0b) coming back status=1 (enabled) identically from an authenticated Oura-app session and from NOOP's own fully offline, unauthenticated connection to the same ring, so the gate is ring-side state rather than something tied to which client asks. The wording gets the important things right. It replaces an absolute with an observation and hedges it ("Do not assume 'off for NOOP' without checking a live read"), and it keeps the separate safety claim intact - the command is the 0x20 READ verb, never an enable or set-mode write. Those two claims were entangled in one comment and only one of them was wrong. Comment-only; no behaviour change. OuraProtocol tests green. FOLLOW-UP OWED, and not this PR's job. The same claim is asserted in four more places that this does not touch: com/noop/oura/Commands.kt:30 and :172 are word-for-word twins of the two lines corrected here, OuraLiveSource.kt:1812 says it again, and OURA_PROTOCOL.md:989 is the source all of them cite. The doc needs more care than a find-and-replace. It does not merely state the gate - it uses it as an EXPLANATION: "This explains 0x7E/0x7F never once appearing across the PR #960 live sessions - the ring isn't sending them, it is not a NOOP decode gap." If the gate reads enabled, that explanation no longer holds, which reopens a question that sentence had closed. Note that status=1 is NOT evidence the ring emits those tags, so the honest correction is "gating no longer explains the absence; the cause is unknown", not "therefore it is a decode gap".
…d a test (#1687) Follow-up to **@pipiche38's #1629**, which corrected the claim in the two Swift files. The same assertion lived in **four more places** — and the Kotlin ones are *word-for-word copies* of the lines that PR changed, so the tree was left saying opposite things in twin files: | Location | What it said | |---|---| | `com/noop/oura/Commands.kt:30` | identical to the corrected Swift line | | `com/noop/oura/Commands.kt:172` | identical to the corrected Swift doc | | `ble/OuraLiveSource.kt:1812` | used it to dismiss the tags' absence | | `docs/OURA_PROTOCOL.md` §7.3 | the source all of them cite | ## The spec needed more than a find-and-replace This is the part worth reading. §7.3 didn't merely *state* the gate — it **reasoned from it**: > *"This explains `0x7E`/`0x7F` never once appearing across the PR #960 live sessions - the ring isn't sending them, it is not a NOOP decode gap."* If the ring's own status read comes back **enabled**, that explanation no longer holds. The observation stands — those tags still never appeared — but the cause attributed to it doesn't. **And that sentence is what closed the investigation.** So the correction says what is actually known and no more: - `status=1` removes *gating* as the explanation - it is **not** evidence the ring emits those tags - the reason for their absence is now **unknown** - a decode gap is back on the table as a possibility rather than ruled out The old text is quoted in place, so anyone who cited that paragraph can see exactly what changed and why. ## A test gap the sweep exposed The sweep found a test comment reading an all-zero fixture as *"server-gated off"* — describing the fixture as though it were a ring's answer. Reworded; and the gap it hid is now covered. `OuraFeatureStatusTests` only ever exercised the **all-zero** reply. So the one reading anybody has actually observed on hardware — `status=1` — was the one nothing pinned. That case is now a test, asserting the **decode only**, with the same caution written into it: it says the ring reports the feature enabled, not that the ring emits `0x7E`/`0x7F`. ## Verification Comment, doc and test only; no behaviour change. `OuraProtocol` tests green (7), Android **4590 green**, compile clean, `doc_comment_lint` clean. No app-target Swift touched, so no app-build needed.
Fixes #642, #650
Summary
Scheduled debug exports (the daily strap-log + optional raw 5/MG capture) accumulated on disk indefinitely with no cleanup and no visible cap:
DebugExportScheduler.kt/LogExport.kt): the daily WorkManager job wrotenoop-straplog-<stamp>.txt(+.binwhen a raw 5/MG capture exists) into the app-privatecache/logsdir on every run, forever.ScheduledDebugExport.swift): the scheduled/"Run now" export wrotenoop-strap-log-<stamp>.txt(+noop-raw-capture-<stamp>.json) into the app's Documents directory, which is Files-visible (UIFileSharingEnabled), also forever.Both platforms now mirror the existing
BackupSynckeep-count retention pattern:scheduledExportStamp/scheduledExportStampsToPrune(Android) andexportStamp(fromFilename:)/exportStampsToPrune(Swift) pair extracts the timestamp embedded in each filename and computes which generations to delete beyond a keep-count — a day's log+raw pair shares one stamp and counts as one generation, not two. Unit tested on both platforms.writeScheduledExport; Apple: end ofperformExport, so it also covers the "Run now" button).BackupSync's 7 is reasonable).noop-strap-log-interactive prefix on Android is spelled without the internal dash vs. the schedulednoop-straplog-prefix; on Apple, interactive saves go through the save panel / share sheet, never Documents) are never touched by retention or the clear action — only their own distinct filename prefixes are scanned.Localization
New user-facing strings (retention picker label/blurb, clear button, confirm dialog title/message, clear-result toast/alert copy) are translated for the focus set de/es/fr/pt-PT, plus zh (Android
values-zh) and zh-Hans/zh-Hant/it/ru (Apple catalog) to keepTools/i18n_audit.py --ci's non-focus-locale ratchet from regressing.python3 Tools/i18n_audit.py --ci origin/mainpasses clean (exit 0).Verification
cd android && ./gradlew :app:compileFullDebugKotlin→BUILD SUCCESSFUL. New unit tests added (LogExportRetentionTest.kt) covering stamp extraction, keep-N pruning (pair-counts-once), no-op within budget, and that interactive-share/unrelated files are never pruned. Not run against the JVM test task in this pass — only compile was verified per the build-verification checklist; the pure logic has no Android framework dependency so it should run undertestFullDebugUnitTestwithout issue, but I have not executed that task here.xcodegen generate && xcodebuild -project Strand.xcodeproj -scheme Strand -destination 'platform=macOS' build→BUILD SUCCEEDED.Strand/System/ScheduledDebugExport.swiftandStrand/Screens/TestCentreView.swiftare shared between theStrand(macOS) andNOOPiOStargets (not excluded inproject.yml), so the macOS build exercises the same source the iOS target compiles. New XCTest coverage added (ScheduledDebugExportTests.swift,@MainActorto match the type's isolation) butxcodebuild testwas not run in this pass — onlybuild.Scope
One concern (retention for scheduled debug exports), both platforms, per the repo's cross-platform-parity rule. No schema/DB migration involved — this is pure filesystem pruning. UI additions use only existing design-system components (
NoopButton/Palette/NoopTypeon Android,NoopButton/StrandPalette/StrandFont/Pickeron Apple) — no hardcoded colors/fonts/spacing.