Repository navigation
Opt-in setting to log clear naps automatically #699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0cb3d8d
b8c0e85
b4639f2
6b904b4
c9fdc73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -459,6 +459,7 @@ class ActivityStore { | |
| required int endTs, | ||
| Map<String, dynamic>? session, | ||
| bool edited = false, | ||
| bool auto = false, | ||
| }) => db.transaction((tx) async { | ||
| final row = await _check(tx, expected, 'confirmed'); | ||
| if (row == null) return; | ||
|
|
@@ -520,6 +521,8 @@ class ActivityStore { | |
| 'start': startTs, | ||
| 'end': endTs, | ||
| 'source': 'confirmed', | ||
| // Logged by the opt-in setting, not by a tap (#625). | ||
| if (auto) 'auto_logged': true, | ||
| }), | ||
| }); | ||
| await _changed(tx, day); | ||
|
|
@@ -533,6 +536,83 @@ class ActivityStore { | |
| await _changed(tx, row['day_id'] as String); | ||
| }); | ||
|
|
||
| /// The bar for logging a nap without asking (#625). The detector's | ||
| /// confidence is a composite of HR coverage, HR dip, stillness and wear, | ||
| /// in [0.2, 0.85]; it is not a probability and 0.8 is not a validated cut. | ||
| /// ponytail: one fixed bar, the value the issue asked for. Make it a user | ||
| /// choice if people want it looser or stricter. | ||
| static const double napAutoLogMinConfidence = 0.8; | ||
|
|
||
| /// Whether the opt-in setting would log [s] without asking. [sinceSec] is | ||
| /// when the setting was turned on (null = off): a nap that ended before | ||
| /// that is never logged behind the user's back. | ||
| static bool napAutoLoggable(ActivitySuggestion s, int? sinceSec) { | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| final c = s.details['confidence']; | ||
| return sinceSec != null && | ||
| s.kind == ActivityKind.nap && | ||
| s.status == ActivityReviewStatus.pending && | ||
| c is num && | ||
| c >= napAutoLogMinConfidence && | ||
| // Strictly after: the switch is stored in whole seconds, and a nap | ||
| // that ended in that same second ended before it was turned on. | ||
| s.endTs > sinceSec; | ||
| } | ||
|
|
||
| /// Confirms every [napAutoLoggable] pending nap exactly as detected, once | ||
| /// the data edge is [settleSec] past its end, so sleep that resumes after a | ||
| /// short arousal can still be bridged into it first. A nap that cannot be | ||
| /// confirmed (it overlaps saved sleep, or changed underneath) stays pending. | ||
| /// Returns how many were logged. | ||
| /// | ||
| /// [current] is the nap candidates this pass detected. Only a pending row | ||
| /// that overlaps one of them is a candidate: [reconcile] only retires rows | ||
| /// that overlap a re-detection, so a nap a later pass no longer finds stays | ||
| /// pending, and must not be confirmed behind the user's back. | ||
| Future<int> autoLogNaps({ | ||
| required int? sinceSec, | ||
| required int dataEdgeSec, | ||
| required int settleSec, | ||
| required List<Map<String, dynamic>> current, | ||
| }) async { | ||
| if (sinceSec == null) return 0; | ||
| final spans = <(int, int)>[ | ||
| for (final c in current) | ||
| ( | ||
| ((c['start_ts'] ?? c['start']) as num).toInt(), | ||
| ((c['end_ts'] ?? c['end']) as num).toInt(), | ||
| ), | ||
| ]; | ||
| if (spans.isEmpty) return 0; | ||
| await migrateLegacy(); | ||
| // Imported history is never logged for the user: the same line | ||
| // [mayNotifyNew] draws for prompts. | ||
| final importedThrough = | ||
| (await db.query('activity_review_meta')).single['alert_after_ts'] | ||
| as int; | ||
| // Not [pending]: its seven-day floor is for what the review list shows. A | ||
| // band that syncs after a longer gap still has naps to log. | ||
| final rows = await db.query( | ||
| 'activity_suggestions', | ||
| where: "status = 'pending' AND kind = 'nap' AND end_ts > ?", | ||
| whereArgs: [math.max(sinceSec, importedThrough)], | ||
| orderBy: 'start_ts ASC, id ASC', | ||
| ); | ||
| var logged = 0; | ||
| for (final s in rows.map(ActivitySuggestion.fromRow)) { | ||
| if (!napAutoLoggable(s, sinceSec) || s.endTs + settleSec > dataEdgeSec) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| continue; | ||
| } | ||
| if (!spans.any((c) => s.startTs < c.$2 && s.endTs > c.$1)) continue; | ||
| try { | ||
| await confirm(s, startTs: s.startTs, endTs: s.endTs, auto: true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Retire pending naps that a later detection pass no longer returns.
The retirement fix needs a day scope.
|
||
| logged++; | ||
| } on ActivityReviewException { | ||
| // Left for the user to review. | ||
|
Comment on lines
+609
to
+610
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| } | ||
| return logged; | ||
| } | ||
|
|
||
| static Future<void> _changed(DatabaseExecutor tx, String day) async { | ||
| await _ensureMeta(tx); | ||
| await tx.rawUpdate( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -132,6 +132,12 @@ class DeriveCoordinator { | |
| } catch (e) { | ||
| _log('[derive] session rescore failed: $e'); | ||
| } | ||
| // Naps the opt-in auto-log confirmed during this pass: their review job | ||
| // is durable but only runs on a resume or an edit, so the day's nap | ||
| // credit would otherwise wait for one of those. | ||
| if (DerivationEngine.takeAutoLoggedNaps() > 0) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Read the auto-logged nap counter after the heavy rescan.
Move the check below the heavy rescan block: Suggested fix
|
||
| unawaited(refreshActivityReviews(retry: true)); | ||
| } | ||
| bumpInsights(); | ||
| _notify(); // screens re-fetch from the derived store | ||
| // Same signal, for the surfaces that can't listen: home/lock-screen | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,6 +80,16 @@ class Prefs { | |
| } | ||
| } | ||
|
|
||
| /// [setBoolAcked] for an int: false when there is no storage or the platform | ||
| /// refused the write, never a throw. | ||
| static Future<bool> setIntAcked(String key, int value) async { | ||
| try { | ||
| return await _sp?.setInt(key, value) ?? false; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gate auto-logging on the acknowledged enable write. The derivation reads the enable timestamp from the SharedPreferences cache. That cache updates before the platform confirms the write. If the write is refused, Derivation should use the enable timestamp only after
|
||
| } catch (_) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // ── selection keys (one namespace; keep them disjoint) ────────────────────── | ||
| /// The five-tab bar's index (Home, Health, Nutrition, Workout, Wellness). | ||
| /// Read once to migrate, never written. | ||
|
|
@@ -101,6 +111,11 @@ class Prefs { | |
| /// list and nothing reads it except the surfaces it reveals. | ||
| static const String devMode = 'dev.mode'; | ||
|
|
||
| /// "Log clear naps automatically" (#625). Absent = off. Holds WHEN it was | ||
| /// turned on, in epoch seconds, so only naps that end after that are logged | ||
| /// without asking: switching it on never rewrites days already past. | ||
| static const String napAutoLogSinceSec = 'activity.nap_auto_log_since_sec'; | ||
|
sourcery-ai[bot] marked this conversation as resolved.
Resolved
|
||
|
|
||
| /// Per-metric range toggle on the shared MetricScreen (Today/Week/Month/3M). | ||
| /// Keyed by the metric id so Sleep / Heart / Body each remember independently. | ||
| static String metricTab(String metric) => 'ui.metric_tab.$metric'; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve the refresh signal after partial batch success.
If one nap confirmation commits and a later confirmation throws a database exception,
autoLogNapsexits without returning its accumulated count. This addition never runs. The catch logs the error, butafterDraincan then see zero and leave the committed nap's credit unapplied until a resume or edit.Record successful confirmations as they commit, or preserve the completed count when a later row fails.