Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions lib/compute/derivation_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6989,12 +6989,17 @@ class DerivationEngine {
bool carrySleep = true,
}) {
var carried = false;
var wearCarried = false;
for (final e in prev.entries) {
if (e.key == 'scalars' || e.key == 'series') continue;
// Row metadata, not detail: the persist stamps the fresh edge, and the
// wear edge is stamped below only when wear itself came across.
if (e.key == 'data_edge_sec' || e.key == 'wear_edge_sec') continue;
if (!carrySleep && kSleepOwnedDetailKeys.contains(e.key)) continue;
if (next.containsKey(e.key) || e.value == null) continue;
next[e.key] = e.value;
carried = true;
if (e.key == 'wear') wearCarried = true;
}
// `scalars` and `series` are flat maps the second half patches INTO rather
// than owning, so they merge per key instead of wholesale.
Expand Down Expand Up @@ -7025,6 +7030,17 @@ class DerivationEngine {
}
if (merged != null) next[sub] = merged;
}
// #450. A carried wear block was computed against the PREVIOUS row's
// edge, not this one's, and a second half that keeps failing would
// otherwise pass that morning off as the current day. Stamped only when
// WEAR was carried: a second half can fail after writing fresh wear (a
// later step throws) while other detail is carried, and fresh wear must
// not be labelled old. A chain of failures keeps the oldest edge: that is
// when wear was last actually computed.
if (wearCarried) {
final edge = prev['wear_edge_sec'] ?? prev['data_edge_sec'];
if (edge is num) next['wear_edge_sec'] = edge.toInt();
}
return carried;
}

Expand Down
8 changes: 8 additions & 0 deletions lib/data/local_repository_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,14 @@ class LocalRepositoryImpl extends LocalRepository {
'first_on': w?['first_on'],
'last_on': w?['last_on'],
'longest_off_min': w?['longest_off_min'],
// #450. The data edge this wear was computed at, when it is older than
// the row it sits in: the derive's second half failed and the engine
// carried the previous wear forward. Absent on a complete derive, so a
// screen never shows an age that is not there. Not gated on the block:
// the stamp exists only when wear was carried, whichever field above
// the figure ends up read from.
if (b['wear_edge_sec'] is num)
'as_of': (b['wear_edge_sec'] as num).toInt(),
'hourly': const [],
};
}
Expand Down
4 changes: 3 additions & 1 deletion lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3685,5 +3685,7 @@
"stepCalDoneHousehold": "Gespeichert. Dein Smartphone hat {count} der {band} Schritte gezählt, die das Band erkannt hat. Bewegung im Haushalt wird jetzt auf {gain} skaliert.",
"whoopFirmwareWarningTitle": "WHOOP-Nutzer: Verbinde dich nicht wieder mit der WHOOP-App",
"whoopFirmwareWarningAction": "Wenn du OpenStrap oder eine andere Open-Source-App nutzt oder später zu einer wechseln möchtest, verbinde dein Band nicht wieder mit der offiziellen WHOOP-App. Sie kann Firmware-Updates automatisch installieren. Deinstalliere oder deaktiviere die WHOOP-App, damit sie dein Band nicht aktualisiert.",
"whoopFirmwareWarningDetails": "Berichte zur WHOOP 5.0 / MG Firmware 50.43.1.0 und 50.43.2.0 beschreiben einen blockierten Datenzugriff. Bluetooth kann sich weiterhin verbinden. OpenStrap kann das von WHOOP signierte Mitgliedschafts-Token nicht ausstellen, das betroffene Bänder entsperrt. Wir arbeiten an einer Lösung, aber es gibt noch keine Abhilfe."
"whoopFirmwareWarningDetails": "Berichte zur WHOOP 5.0 / MG Firmware 50.43.1.0 und 50.43.2.0 beschreiben einen blockierten Datenzugriff. Bluetooth kann sich weiterhin verbinden. OpenStrap kann das von WHOOP signierte Mitgliedschafts-Token nicht ausstellen, das betroffene Bänder entsperrt. Wir arbeiten an einer Lösung, aber es gibt noch keine Abhilfe.",
"healthWearNotUpdatedSince": "nicht aktualisiert seit {time}",
"dayStrainWornUntil": "Getragen bis {time}"
}
20 changes: 19 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -16805,5 +16805,23 @@
"@strengthSaveFailed": {"description": "Strength set editor copy."},
"napAutoLogTitle": "Log clear naps automatically",
"napAutoLogSub": "A detected nap with strong evidence is added without asking. You can still edit or remove it",
"napAutoLogSaveFailed": "That setting could not be saved. Try again."
"napAutoLogSaveFailed": "That setting could not be saved. Try again.",
"healthWearNotUpdatedSince": "not updated since {time}",
"@healthWearNotUpdatedSince": {
"description": "Wear-time row: the figure is from an earlier derive that the latest one could not refresh. time is a clock time, e.g. 11:05 AM",
"placeholders": {
"time": {
"type": "String"
}
}
},
"dayStrainWornUntil": "Worn until {time}",
"@dayStrainWornUntil": {
"description": "Day strain: label over a wear figure from an earlier derive that the latest one could not refresh. time is a clock time, e.g. 11:05 AM",
"placeholders": {
"time": {
"type": "String"
}
}
}
}
4 changes: 3 additions & 1 deletion lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3385,5 +3385,7 @@
"stepCalDoneHousehold": "Guardado. Tu teléfono contó {count} de los {band} pasos que vio la banda. El movimiento en casa ahora se escala a {gain}.",
"whoopFirmwareWarningTitle": "Usuarios de WHOOP: no vuelvas a conectarte a la app de WHOOP",
"whoopFirmwareWarningAction": "Si usas OpenStrap u otra app de código abierto, o piensas cambiarte a una más adelante, no vuelvas a conectar tu banda a la app oficial de WHOOP. Puede instalar actualizaciones de firmware automáticamente. Desinstala o desactiva la app de WHOOP para evitar que actualice tu banda.",
"whoopFirmwareWarningDetails": "Hay informes de que el firmware 50.43.1.0 y 50.43.2.0 de WHOOP 5.0 / MG bloquea el acceso a los datos. Es posible que el Bluetooth siga conectándose. OpenStrap no puede emitir el token de membresía firmado por WHOOP que se necesita para desbloquear las bandas afectadas. Estamos trabajando en una solución, pero todavía no hay ningún arreglo disponible."
"whoopFirmwareWarningDetails": "Hay informes de que el firmware 50.43.1.0 y 50.43.2.0 de WHOOP 5.0 / MG bloquea el acceso a los datos. Es posible que el Bluetooth siga conectándose. OpenStrap no puede emitir el token de membresía firmado por WHOOP que se necesita para desbloquear las bandas afectadas. Estamos trabajando en una solución, pero todavía no hay ningún arreglo disponible.",
"healthWearNotUpdatedSince": "sin actualizar desde {time}",
"dayStrainWornUntil": "Llevada hasta {time}"
}
4 changes: 3 additions & 1 deletion lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3385,5 +3385,7 @@
"pairOuraExistingKeyGuidance": "Vous utilisez déjà l’application Oura ? Gardez l’anneau associé dans cette application et associez-le avec sa clé actuelle. Passez les étapes de réinitialisation ci-dessous. Réinitialisez-le uniquement si vous voulez une nouvelle clé.",
"whoopFirmwareWarningTitle": "Utilisateurs WHOOP : ne vous reconnectez pas à l’application WHOOP",
"whoopFirmwareWarningAction": "Si vous utilisez OpenStrap ou une autre application open source, ou si vous comptez y passer plus tard, ne reconnectez pas votre bracelet à l’application WHOOP officielle. Elle peut installer des mises à jour du firmware automatiquement. Désinstallez ou désactivez l’application WHOOP pour l’empêcher de mettre à jour votre bracelet.",
"whoopFirmwareWarningDetails": "Des signalements concernant les firmwares 50.43.1.0 et 50.43.2.0 des WHOOP 5.0 / MG décrivent un accès aux données bloqué. Le Bluetooth peut encore se connecter. OpenStrap ne peut pas émettre le jeton d’abonnement signé par WHOOP nécessaire pour déverrouiller les bracelets concernés. Nous travaillons sur une solution, mais aucun correctif n’est encore disponible."
"whoopFirmwareWarningDetails": "Des signalements concernant les firmwares 50.43.1.0 et 50.43.2.0 des WHOOP 5.0 / MG décrivent un accès aux données bloqué. Le Bluetooth peut encore se connecter. OpenStrap ne peut pas émettre le jeton d’abonnement signé par WHOOP nécessaire pour déverrouiller les bracelets concernés. Nous travaillons sur une solution, mais aucun correctif n’est encore disponible.",
"healthWearNotUpdatedSince": "pas mis à jour depuis {time}",
"dayStrainWornUntil": "Porté jusqu’à {time}"
}
4 changes: 3 additions & 1 deletion lib/l10n/app_hi.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3386,5 +3386,7 @@
"stepCalRoutineTooShort": "वह अभ्यास टाइमर ख़त्म होने से पहले रुक गया। पूरा होने तक चलते रहें।",
"stepCalSaveFailed": "वह कैलिब्रेशन सहेजा नहीं जा सका।",
"stepCalDoneGait": "सहेजा गया। आपने {count} कदम गिने और बैंड ने {band} कदम देखे। अब से इस तरह की चाल {pct}% से ठीक की जाएगी।",
"stepCalDoneHousehold": "सहेजा गया। बैंड द्वारा देखे गए {band} कदमों में से आपके फ़ोन ने {count} कदम गिने। घर के कामों की हलचल अब {gain} के अनुपात में समायोजित होगी।"
"stepCalDoneHousehold": "सहेजा गया। बैंड द्वारा देखे गए {band} कदमों में से आपके फ़ोन ने {count} कदम गिने। घर के कामों की हलचल अब {gain} के अनुपात में समायोजित होगी।",
"healthWearNotUpdatedSince": "{time} से अपडेट नहीं हुआ",
"dayStrainWornUntil": "{time} तक पहना"
}
4 changes: 3 additions & 1 deletion lib/l10n/app_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -8159,5 +8159,7 @@
"pairTroubleDiscordSub": "Сообщество OpenStrap",
"pairModeTitle": "Переведите в режим сопряжения",
"pairModeNoSite": "Это универсальное устройство без единого производителя, поэтому инструкции производителя нет.",
"pairOuraExistingKeyGuidance": "Уже пользуетесь приложением Oura? Оставьте кольцо сопряжённым в этом приложении и выполните сопряжение с его текущим ключом. Пропустите шаги сброса ниже. Сбрасывайте кольцо только для получения нового ключа."
"pairOuraExistingKeyGuidance": "Уже пользуетесь приложением Oura? Оставьте кольцо сопряжённым в этом приложении и выполните сопряжение с его текущим ключом. Пропустите шаги сброса ниже. Сбрасывайте кольцо только для получения нового ключа.",
"healthWearNotUpdatedSince": "не обновлялось с {time}",
"dayStrainWornUntil": "Ношение до {time}"
}
4 changes: 3 additions & 1 deletion lib/l10n/app_th.arb
Original file line number Diff line number Diff line change
Expand Up @@ -16627,5 +16627,7 @@
"@pairModeNoSite": {
"description": "Shown instead of steps for a generic device with no maker’s page."
},
"pairOuraExistingKeyGuidance": "ใช้แอป Oura อยู่แล้วใช่ไหม คงการจับคู่แหวนในแอปนั้นไว้ แล้วจับคู่ด้วยคีย์เดิมของแหวน ข้ามขั้นตอนรีเซ็ตด้านล่าง รีเซ็ตเฉพาะเมื่อต้องการคีย์ใหม่เท่านั้น"
"pairOuraExistingKeyGuidance": "ใช้แอป Oura อยู่แล้วใช่ไหม คงการจับคู่แหวนในแอปนั้นไว้ แล้วจับคู่ด้วยคีย์เดิมของแหวน ข้ามขั้นตอนรีเซ็ตด้านล่าง รีเซ็ตเฉพาะเมื่อต้องการคีย์ใหม่เท่านั้น",
"healthWearNotUpdatedSince": "ไม่ได้อัปเดตตั้งแต่ {time}",
"dayStrainWornUntil": "สวมถึง {time}"
}
4 changes: 3 additions & 1 deletion lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3385,5 +3385,7 @@
"stepCalRoutineTooShort": "这项测试在计时结束前就停止了。请坚持到它完成。",
"stepCalSaveFailed": "无法保存此次校准。",
"stepCalDoneGait": "已保存。你数了 {count} 步,腕带记录了 {band} 步。今后这类走路将按 {pct}% 进行校正。",
"stepCalDoneHousehold": "已保存。在腕带记录的 {band} 步中,你的手机记录了 {count} 步。居家活动现在将按 {gain} 进行缩放。"
"stepCalDoneHousehold": "已保存。在腕带记录的 {band} 步中,你的手机记录了 {count} 步。居家活动现在将按 {gain} 进行缩放。",
"healthWearNotUpdatedSince": "自 {time} 起未更新",
"dayStrainWornUntil": "佩戴至 {time}"
}
23 changes: 21 additions & 2 deletions lib/ui2/activity/day_strain.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import '../../l10n/app_localizations.dart';
import '../../l10n/date_text.dart';
import '../../l10n/decimal_text.dart';
import '../../models/metric.dart' show whyFromNote;
import '../screens/home_screen.dart' show repoOf;
import '../screens/home_screen.dart' show asOfText, repoOf;
import '../screens/metric_detail.dart' show detailScaffold;
import '../ui2.dart';
import 'catalogue.dart' show zonesWhy;
Expand Down Expand Up @@ -72,6 +72,10 @@ class DayStrainData {

final int? peakHr, wornMin, coveragePct;

/// When [wornMin] and [coveragePct] were last computed, if an earlier
/// derive's wear was carried forward (#450). Null when they are current.
final int? wearAsOf;

/// WHY the day has no strain, as the bundle said it — never a sentence
/// written on this screen. Null means nothing came back with the absence, and
/// the screen then says exactly that.
Expand All @@ -89,11 +93,15 @@ class DayStrainData {
this.peakHr,
this.wornMin,
this.coveragePct,
this.wearAsOf,
this.note,
});

bool get hasCurve => curve.any((v) => v != null);

/// The local label of the day this screen shows (today when [day] is null).
String get shownDay => day == null ? todayLabel() : dayLabelOf(day!);

/// Lays the curve's epoch-second points onto the fixed 00:00–24:00 axis,
/// one slot per WALL-CLOCK minute of the first point's local day.
///
Expand Down Expand Up @@ -174,6 +182,7 @@ class DayStrainData {
peakHr: hr is Map ? (hr['max'] as num?)?.toInt() : null,
wornMin: (wear['worn_min'] as num?)?.toInt(),
coveragePct: (wear['coverage_pct'] as num?)?.toInt(),
wearAsOf: (wear['as_of'] as num?)?.toInt(),
// The headline absence's reason, at the top level of the payload — the
// same string as `absent.strain`.
note: s['note'] as String?,
Expand Down Expand Up @@ -328,7 +337,17 @@ class _DayStrainDetailState extends State<DayStrainDetail> {
if (d.peakHr != null)
(l?.dayStrainPeakHr ?? 'Peak HR', '${d.peakHr} bpm', C.red),
if (d.wornMin != null)
(l?.dayStrainWorn ?? 'Worn', '${d.wornMin} min', C.teal),
// #450: a carried figure says where it stops, or a frozen
// morning reads as the whole day so far.
(
d.wearAsOf == null
? (l?.dayStrainWorn ?? 'Worn')
: (l?.dayStrainWornUntil(
asOfText(d.wearAsOf!, d.shownDay, l)) ??
'Worn until ${asOfText(d.wearAsOf!, d.shownDay, l)}'),
'${d.wornMin} min',
C.teal,
),
]),
],
]),
Expand Down
22 changes: 16 additions & 6 deletions lib/ui2/screens/health_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,11 @@ class _HealthScreenState extends State<HealthScreen> with RevisionReload {
final resp = respBlock is Map ? respBlock['value'] as num? : null;
final worn = v.wear['worn_min'] as num?;
final coverage = v.wear['coverage_pct'] as num?;
// #450: the latest derive could not refresh wear, so this is an earlier
// one's figure. Say so rather than let a frozen morning read as now.
final wearAsOf = v.wear['as_of'] as num?;
final wearAsOfText =
wearAsOf == null ? '' : asOfText(wearAsOf, v.day ?? todayLabel(), l);
final skinTemp = metricOf(d.today['skin_temp']);
// WHOOP 5 reads in real °C, so its row says degrees, not SDs.
final tempDevC = ((d.today['skin_temp'] as Map?)?['dev_c'] as num?)
Expand Down Expand Up @@ -1220,12 +1225,17 @@ class _HealthScreenState extends State<HealthScreen> with RevisionReload {
MetricRow(LucideIcons.watch, C.green, l?.healthRowWearTime ?? 'Wear time',
hm(worn),
// `83.33333333333333% of the day` shipped. It is a percentage.
sub: coverage == null
? dayWord
: (l?.healthCoverageOf(coverage.round(),
isToday ? (l.healthTheDay) : dayWord) ??
'${coverage.round()}% of '
'${isToday ? 'the day' : dayWord}'),
sub: [
coverage == null
? dayWord
: (l?.healthCoverageOf(coverage.round(),
isToday ? (l.healthTheDay) : dayWord) ??
'${coverage.round()}% of '
'${isToday ? 'the day' : dayWord}'),
if (wearAsOf != null)
l?.healthWearNotUpdatedSince(wearAsOfText) ??
'not updated since $wearAsOfText',
].join(' · '),
onTap: () => go(c, const MetricDetail('wear'))),
];

Expand Down
14 changes: 13 additions & 1 deletion lib/ui2/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ import '../../compute/findings.dart'
kReadinessEasyBelow,
kReadinessGoodFrom,
kReadinessRestBelow;
import '../../data/day_label.dart' show todayLabel, calendarDaysBetween;
import '../../data/day_label.dart'
show calendarDaysBetween, dayLabelOf, todayLabel;
import '../../compute/onehz_pipeline.dart'
show readinessInputShortfallNote, readinessUnstableBaselineNote;
import '../../data/db.dart' show DbRebuild, LocalDb;
Expand Down Expand Up @@ -685,6 +686,17 @@ String clock(num? minOfDay) =>
minOfDay == null ? '' : formatMinuteOfDay(minOfDay.round());

/// Epoch seconds → "11:08 PM" / "23:08" in the device zone.
/// When a carried wear figure was last computed (#450), as seen from the day
/// on screen ([shownDay], a local label): a clock alone when it fell on that
/// day, otherwise the date too. A past day's carried wear can date from a
/// later day's sync, and "08:00" under that past day would name the wrong one.
String asOfText(num ts, String shownDay, [AppLocalizations? l]) {
final day = dayLabelOf(DateTime.fromMillisecondsSinceEpoch(ts.round() * 1000));
return day == shownDay
? clockOfTs(ts)
: '${prettyDay(day, l)} ${clockOfTs(ts)}';
}

String clockOfTs(num? ts) {
if (ts == null) return '';
return formatClockOf(DateTime.fromMillisecondsSinceEpoch(ts.round() * 1000));
Expand Down
Loading