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:
Tizian 「ティツィアーン」 2026-10-28 19:53:12 +01:00
commit 5250f32329
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 4079 additions and 28 deletions

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:halkyon_train_flutter/src/feature/day/model/day.dart';
import 'package:halkyon_train_flutter/src/feature/loop/data/loop_platform.dart';
import 'package:halkyon_train_flutter/src/feature/loop/model/loop_figures.dart';
import 'package:halkyon_train_flutter/src/feature/recording/data/recorder_platform.dart';
@ -19,6 +20,16 @@ class FakeLoopPlatform({
/// What was asked, in order: `connect a`, `setUp a`, `setClock a`, …
final calls = <String>[];
/// What each read of the days gives, after its `read` line, one read's
/// at a time; none left, nothing but the `read` line.
final days = <List<Map<String, Object?>>>[];
/// The day reads happen on, as the band's calendar has it.
DateTime today = DateTime.utc(2026, 10, 28);
/// The background job's config, as last given; null where none runs.
LoopBackground? backgroundConfig;
/// The figures each band was set up with.
final setUpWith = <String, LoopFigures>{};
@ -90,6 +101,36 @@ class FakeLoopPlatform({
calls.add('setClock $id');
}
@override
Future<List<Object?>> readDays(
String id, {
required DateTime from,
DateTime? keptUntil,
}) async {
calls.add(
'readDays $id ${dayKey(from)} '
'${keptUntil == null ? '-' : dayKey(keptUntil)}',
);
if (!inReach.contains(id)) {
throw const LoopFailed('not in reach');
}
return [
{
'kind': 'read',
'on': dayKey(today),
'from': dayKey(from),
'to': dayKey(today),
},
if (days.isNotEmpty) ...days.removeAt(0),
];
}
@override
Future<void> background(LoopBackground? config) async {
calls.add('background ${config == null ? 'off' : 'on'}');
backgroundConfig = config;
}
@override
Future<void> reset(String id) async {
calls.add('reset $id');

View file

@ -0,0 +1,66 @@
import 'dart:async';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_train_flutter/src/feature/day/data/day_gateway.dart';
/// [DayGateway] in memory, whose rows a test changes as an instance could;
/// it records the envelopes written, so a test sees what reached it.
class MemoryDayGateway() implements DayGateway {
final dayRows = <String, Envelope>{};
final seriesRows = <String, DaySeriesRow>{};
final nightRows = <String, Envelope>{};
/// Every envelope ever written, in order.
final written = <Envelope>[];
final _remote = StreamController<void>.broadcast();
/// As a sync bringing others' changes.
void bringRemoteChanges() => _remote.add(null);
@override
Future<List<DayRow>> days() async => [
for (final MapEntry(:key, :value) in dayRows.entries)
(id: key, envelope: value),
];
@override
Future<Envelope?> day(String id) async => dayRows[id];
@override
Future<List<DaySeriesRow>> seriesOf(String dayId) async => [
for (final row in seriesRows.values)
if (row.dayId == dayId) row,
];
@override
Future<List<DayRow>> nights() async => [
for (final MapEntry(:key, :value) in nightRows.entries)
(id: key, envelope: value),
];
@override
Future<Envelope?> night(String id) async => nightRows[id];
@override
Future<void> writeDay(DayRow day, List<DaySeriesRow>? series) async {
dayRows[day.id] = day.envelope;
written.add(day.envelope);
if (series != null) {
seriesRows.removeWhere((_, row) => row.dayId == day.id);
for (final row in series) {
seriesRows[row.id] = row;
written.add(row.envelope);
}
}
}
@override
Future<void> writeNight(DayRow night) async {
nightRows[night.id] = night.envelope;
written.add(night.envelope);
}
@override
Stream<void> get remoteChanges => _remote.stream;
}