feat(train): Days, the Loop's days and nights kept sealed (ADR 0030, D37, D38)
What the band read brings, from the bridge or the background job's file (`LoopLines`), is kept by `DayRepository`: one sealed `day` and `night` a date and the day's heart rate in chunks of three hours, every ID made from the person and the date, a later read replacing what it brought and keeping the rest, nothing rewritten unchanged; versioned, gzipped JSON (`day_payload.dart`). `PolarLoop` reads the band as each connection is ready (every half hour at most, or when asked), takes in the background file as the app opens, and remembers up to which day the days are sealed, which the next read then deletes on the band; it keeps the background job on the band and from the right day, and `Let the band go` stops it. Home gains a Days tab, each day with its steps, active time, sleep and Nightly Recharge, pulled to read the band; a day's page shows its figures, its time in each class, its heart rate, the night's phases and rating, Nightly Recharge and skin temperature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
88e8693025
commit
5250f32329
1 changed files with 4079 additions and 28 deletions
|
|
@ -9,6 +9,8 @@ import 'package:halkyon_train_client/halkyon_train_client.dart' show MapSource;
|
|||
import 'package:halkyon_train_flutter/src/app/train_app.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/core/privacy/train_keys.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/data/day_repository.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/data/loop_lines.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/data/health_connect.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/data/health_platform.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/data/health_store.dart';
|
||||
|
|
@ -32,6 +34,7 @@ import '../support/fake_health_platform.dart';
|
|||
import '../support/fake_loop_platform.dart';
|
||||
import '../support/fake_recorder_platform.dart';
|
||||
import '../support/memory_content_key_gateway.dart';
|
||||
import '../support/memory_day_gateway.dart';
|
||||
import '../support/memory_profile_gateway.dart';
|
||||
import '../support/memory_workout_gateway.dart';
|
||||
|
||||
|
|
@ -63,6 +66,7 @@ void main() {
|
|||
bool mapReachable = true,
|
||||
HealthConnect? health,
|
||||
ProfileRepository? profile,
|
||||
DayRepository? days,
|
||||
PolarLoop? loop,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(412, 900);
|
||||
|
|
@ -143,6 +147,17 @@ void main() {
|
|||
),
|
||||
person: person.authUserId,
|
||||
),
|
||||
days:
|
||||
days ??
|
||||
DayRepository(
|
||||
MemoryDayGateway(),
|
||||
TrainKeys(
|
||||
MemoryContentKeyGateway(),
|
||||
FakeContentCipher(),
|
||||
() async => null,
|
||||
),
|
||||
person: person.authUserId,
|
||||
),
|
||||
recording: recording,
|
||||
health: health,
|
||||
loop: loop,
|
||||
|
|
@ -451,6 +466,7 @@ void main() {
|
|||
late Directory folder;
|
||||
late TrainKeys keys;
|
||||
late ProfileRepository profile;
|
||||
late DayRepository days;
|
||||
late FakeLoopPlatform platform;
|
||||
late PolarLoop loop;
|
||||
|
||||
|
|
@ -466,6 +482,7 @@ void main() {
|
|||
keys,
|
||||
person: 'alice',
|
||||
);
|
||||
days = DayRepository(MemoryDayGateway(), keys, person: 'alice');
|
||||
platform = FakeLoopPlatform()
|
||||
..nearby.add(band)
|
||||
..inReach.add('A1');
|
||||
|
|
@ -499,8 +516,16 @@ void main() {
|
|||
platform,
|
||||
LoopStore(File('${folder.path}/loop.json')),
|
||||
profile,
|
||||
days: days,
|
||||
backgroundLines: File('${folder.path}/loop_days.txt'),
|
||||
);
|
||||
await start(
|
||||
tester,
|
||||
usedBefore: true,
|
||||
profile: profile,
|
||||
days: days,
|
||||
loop: loop,
|
||||
);
|
||||
await start(tester, usedBefore: true, profile: profile, loop: loop);
|
||||
await tester.runAsync(() async => loop.start());
|
||||
await openAccount(tester);
|
||||
await tester.tap(find.text('Polar Loop'));
|
||||
|
|
@ -1347,6 +1372,108 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('days (ADR 0030, D37)', () {
|
||||
late TrainKeys keys;
|
||||
late DayRepository days;
|
||||
|
||||
setUp(() {
|
||||
keys = TrainKeys(
|
||||
MemoryContentKeyGateway(),
|
||||
FakeContentCipher(),
|
||||
() async => FakeAccountCrypto().keysNumbered(1),
|
||||
);
|
||||
days = DayRepository(MemoryDayGateway(), keys, person: 'alice');
|
||||
});
|
||||
|
||||
tearDown(() => keys.dispose());
|
||||
|
||||
Future<void> keepTuesday(WidgetTester tester) => tester.runAsync(
|
||||
() => days.take(
|
||||
LoopLines()
|
||||
..add({
|
||||
'kind': 'summary',
|
||||
'date': '2026-10-27',
|
||||
'steps': 9876,
|
||||
'times': {'light': 5400, 'continuousModerate': 1200},
|
||||
})
|
||||
..add({
|
||||
'kind': 'heart',
|
||||
'date': '2026-10-27',
|
||||
'runs': [
|
||||
{
|
||||
'at': 600,
|
||||
'trigger': 'timed',
|
||||
'bpm': [58, 60],
|
||||
},
|
||||
{
|
||||
'at': 40000,
|
||||
'trigger': 'timed',
|
||||
'bpm': [92],
|
||||
},
|
||||
],
|
||||
})
|
||||
..add({
|
||||
'kind': 'sleep',
|
||||
'date': '2026-10-27',
|
||||
'start': '2026-10-26T22:00:00Z',
|
||||
'end': '2026-10-27T06:00:00Z',
|
||||
'phases': [
|
||||
[0, 'wake'],
|
||||
[600, 'nonrem12'],
|
||||
[7200, 'nonrem3'],
|
||||
[10800, 'rem'],
|
||||
],
|
||||
'rating': 'well',
|
||||
})
|
||||
..add({
|
||||
'kind': 'recharge',
|
||||
'date': '2026-10-27',
|
||||
'recovery': 4,
|
||||
'rmssd': 48,
|
||||
'baselineRmssd': 45,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('without a band, the days tab says where days come from', (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true, days: days);
|
||||
|
||||
await tester.tap(find.text('Days'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('No days yet'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a day kept lists its figures and opens its page: heart '
|
||||
'rate, the night and its Nightly Recharge', (tester) async {
|
||||
await keepTuesday(tester);
|
||||
await start(tester, usedBefore: true, days: days);
|
||||
|
||||
await tester.tap(find.text('Days'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Tuesday, October 27'), findsOneWidget);
|
||||
expect(find.text('9876 steps'), findsOneWidget);
|
||||
expect(find.text('1 h 50 min active'), findsOneWidget);
|
||||
expect(find.text('7 h 50 min asleep'), findsOneWidget);
|
||||
expect(find.text('Recharge 4 of 6'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Tuesday, October 27'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Heart rate'), findsOneWidget);
|
||||
expect(find.text('The night'), findsOneWidget);
|
||||
expect(find.text('You slept well'), findsOneWidget);
|
||||
await tester.scrollUntilVisible(find.text('Nightly Recharge'), 200);
|
||||
expect(
|
||||
find.text('Heart rate variability 48 ms, your usual 45 ms'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('workouts (ADR 0030, D24)', () {
|
||||
late MemoryWorkoutGateway rows;
|
||||
late TrainKeys keys;
|
||||
|
|
|
|||
Loading…
Reference in a new issue