Skip to content
Merged
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
4 changes: 4 additions & 0 deletions lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,10 @@
"investigateLnRmssd": "ln RMSSD",
"investigateBaselineRmssd": "Deine RMSSD-Baseline",
"investigateStabilityCv": "Stabilität (VK)",
"investigateStressIndexTitle": "Stressindex",
"investigateStressIndexSi": "Index (SI)",
"investigateStressLevel": "Stufe",
"investigateStressScore": "Wert (0–100)",
"investigateFrequencyDomain": "Frequenzbereich",
"investigateUlfPower": "ULF-Leistung",
"investigateVlfPower": "VLF-Leistung",
Expand Down
16 changes: 16 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -4696,6 +4696,22 @@
"@investigateStabilityCv": {
"description": "HRV metric label: coefficient of variation stability"
},
"investigateStressIndexTitle": "Stress index",
"@investigateStressIndexTitle": {
"description": "Stress Nerd Stats table title: the Baevsky index behind the score"
},
"investigateStressIndexSi": "Index (SI)",
"@investigateStressIndexSi": {
"description": "Stress Nerd Stats row: the Baevsky stress index for the night"
},
"investigateStressLevel": "Level",
"@investigateStressLevel": {
"description": "Stress Nerd Stats row: low / normal / elevated / high band of the index"
},
"investigateStressScore": "Score (0–100)",
"@investigateStressScore": {
"description": "Stress Nerd Stats row: the 0-100 score derived from the index"
},
"investigateFrequencyDomain": "Frequency domain",
"@investigateFrequencyDomain": {
"description": "HRV table title: frequency-domain metrics"
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@
"investigateLnRmssd": "ln RMSSD",
"investigateBaselineRmssd": "Tu RMSSD de referencia",
"investigateStabilityCv": "Estabilidad (CV)",
"investigateStressIndexTitle": "Índice de estrés",
"investigateStressIndexSi": "Índice (SI)",
"investigateStressLevel": "Nivel",
"investigateStressScore": "Puntuación (0–100)",
"investigateFrequencyDomain": "Dominio de la frecuencia",
"investigateUlfPower": "Potencia ULF",
"investigateVlfPower": "Potencia VLF",
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@
"investigateLnRmssd": "ln RMSSD",
"investigateBaselineRmssd": "Votre RMSSD de référence",
"investigateStabilityCv": "Stabilité (CV)",
"investigateStressIndexTitle": "Indice de stress",
"investigateStressIndexSi": "Indice (SI)",
"investigateStressLevel": "Niveau",
"investigateStressScore": "Score (0–100)",
"investigateFrequencyDomain": "Domaine fréquentiel",
"investigateUlfPower": "Puissance ULF",
"investigateVlfPower": "Puissance VLF",
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_hi.arb
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@
"investigateLnRmssd": "ln RMSSD",
"investigateBaselineRmssd": "आपका बेसलाइन RMSSD",
"investigateStabilityCv": "स्थिरता (CV)",
"investigateStressIndexTitle": "तनाव सूचकांक",
"investigateStressIndexSi": "सूचकांक (SI)",
"investigateStressLevel": "स्तर",
"investigateStressScore": "स्कोर (0–100)",
"investigateFrequencyDomain": "फ़्रीक्वेंसी डोमेन",
"investigateUlfPower": "ULF पावर",
"investigateVlfPower": "VLF पावर",
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -2045,6 +2045,10 @@
"investigateLnRmssd": "ln RMSSD",
"investigateBaselineRmssd": "Ваш базовый RMSSD",
"investigateStabilityCv": "Стабильность (CV)",
"investigateStressIndexTitle": "Индекс стресса",
"investigateStressIndexSi": "Индекс (SI)",
"investigateStressLevel": "Уровень",
"investigateStressScore": "Оценка (0–100)",
"investigateFrequencyDomain": "Частотная область",
"investigateUlfPower": "Мощность ULF",
"investigateVlfPower": "Мощность VLF",
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,10 @@
"investigateLnRmssd": "ln RMSSD",
"investigateBaselineRmssd": "你的 RMSSD 基线",
"investigateStabilityCv": "稳定性(变异系数)",
"investigateStressIndexTitle": "压力指数",
"investigateStressIndexSi": "指数 (SI)",
"investigateStressLevel": "等级",
"investigateStressScore": "得分(0–100)",
"investigateFrequencyDomain": "频域指标",
"investigateUlfPower": "ULF 功率",
"investigateVlfPower": "VLF 功率",
Expand Down
87 changes: 86 additions & 1 deletion lib/ui2/screens/investigate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import '../../l10n/date_text.dart';
// "locked" — Beats, Vitals, Sleep, Readiness and every metric drill-down each
// carry a plain row down to it.

import '../../l10n/decimal_text.dart';
import '../../l10n/display_text.dart';
import 'dart:convert' show jsonDecode;
import 'dart:isolate' show Isolate;
import 'dart:math' show sqrt;

import 'package:flutter/material.dart';
Expand All @@ -37,6 +39,26 @@ import '../ui2.dart';
import 'day_timeline.dart';
import 'home_screen.dart';
import 'metric_detail.dart';
import 'wellness_screen.dart' show stressLevelLabel;

/// The `stress` block of a day bundle, or empty. Its own values only: no other
/// block stands in for a missing field.
Map<String, dynamic> stressBlockOf(Object? bundle) {
final blk = bundle is Map ? bundle['stress'] : null;
// A plain copy, not a cast view: it is returned across an isolate boundary.
return blk is Map ? Map<String, dynamic>.from(blk) : const {};
}

/// Everything the stress screen reads from a day bundle, in one decode. Plain
/// maps and scalars only: it is returned across an isolate boundary.
Map<String, dynamic> _stressFacts(String payload) {
final b = jsonDecode(payload);
return {
'stress': stressBlockOf(b),
'imported': b is Map && b['imported'] == true,
'source': b is Map ? b['source']?.toString() : null,
};
}

class InvestigateData {
final String? day;
Expand Down Expand Up @@ -87,6 +109,11 @@ class InvestigateData {
final int? coveragePct;
final int? windowStart, windowEnd;
final List<double> series;
/// The viewed day's own stress block: `si`, `level`, `score`. Read
/// from that day's payload, never `getDayStress`, which serves the latest
/// complete day for a today with no bundle yet and substitutes the nightly
/// RMSSD for a missing one. Only loaded for the stress key.
final Map<String, dynamic> stress;

const InvestigateData({
this.day,
Expand All @@ -106,6 +133,7 @@ class InvestigateData {
this.windowStart,
this.windowEnd,
this.series = const [],
this.stress = const {},
});

static Future<InvestigateData> load(LocalRepository repo, String key,
Expand Down Expand Up @@ -137,10 +165,30 @@ class InvestigateData {
// onto the bundle itself; a day derived here has neither.
String? importedFrom;
var steps = const <String, dynamic>{};
var stress = const <String, dynamic>{};
final payload = row?['payload_json'];
// The day's own stress block, decoded off the UI isolate: the bundle holds
// the 1 Hz curves and is far larger than the few values read from it. The
// same decode answers "is this day imported", so the stress key decodes
// the bundle once, not twice.
final isStress = key == 'stress';
if (isStress && payload is String) {
// An optional side table: a payload that will not decode is an absent
// table, not a screen that fails to load.
try {
final f = await Isolate.run(() => _stressFacts(payload));
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
stress = (f['stress'] as Map).cast<String, dynamic>();
if (f['imported'] == true) {
importedFrom = f['source']?.toString() ?? 'an import';
}
} on FormatException {
// fall through with no table
}
}
// The `contains` guard is what keeps every other key off the decode; the
// steps split needs the bundle, so that key pays for it deliberately.
if (payload is String &&
if (!isStress &&
payload is String &&
(key == 'steps' || payload.contains('"imported"'))) {
final b = jsonDecode(payload);
if (b is Map) {
Expand Down Expand Up @@ -171,6 +219,7 @@ class InvestigateData {
windowStart: win is Map ? (win['start'] as num?)?.toInt() : null,
windowEnd: win is Map ? (win['end'] as num?)?.toInt() : null,
series: series,
stress: stress,
);
}

Expand Down Expand Up @@ -289,6 +338,7 @@ class _InvestigateState extends State<Investigate> {
const Center(child: CircularProgressIndicator()),
] else ...[
if (hrvish) ..._hrvPanels(c, d) else ..._genericPanels(c, spec, d),
if (widget.metricKey == 'stress') ..._stressPanels(c, d),
if (widget.metricKey == 'resp_rate') ..._restingBreathPanels(d),
if (widget.metricKey == 'resp_rate') ..._cvhrPanels(d),
if (widget.metricKey == 'sleep') ..._stagePanels(d),
Expand Down Expand Up @@ -984,6 +1034,41 @@ class _InvestigateState extends State<Investigate> {
];
}

// ── stress: the index behind the score, for the day being viewed ──
//
// The 0–100 score is a log mapping of the Baevsky index clamped at both ends,
// so on its own it cannot say WHY it reads 0. The index and its level are the
// unclamped facts. Read from the day bundle, not `metric_series`: the SI is
// not a stored series.
List<Widget> _stressPanels(BuildContext c, InvestigateData d) {
final l = AppLocalizations.of(c);
final b = d.stress;
// The level and the score are readings of the index; with no index there
// is nothing for them to explain.
final si = b['si'];
if (si is! num || !si.isFinite) return const [];
String num2(Object? v, int digits) =>
v is num && v.isFinite ? displayFixed(v, digits) : '—';
final rawLevel = b['level'];
// Only the four bands the labels know. An unknown token is absent, not
// an untranslated English word in a localized table.
final level = rawLevel is String &&
const {'low', 'normal', 'elevated', 'high'}.contains(rawLevel)
? rawLevel
: null;
return [
const SizedBox(height: S.x3),
MonoTable(l?.investigateStressIndexTitle ?? 'Stress index', [
(l?.investigateStressIndexSi ?? 'Index (SI)', displayFixed(si, 2)),
(
l?.investigateStressLevel ?? 'Level',
level == null ? '—' : stressLevelLabel(l, level) ?? level,
),
(l?.investigateStressScore ?? 'Score (0–100)', num2(b['score'], 1)),
]),
];
}

// ── anything else: what the series itself looks like ──
List<Widget> _genericPanels(
BuildContext c, MetricSpec spec, InvestigateData d) {
Expand Down
Loading
Loading