wip(watch): sauvegarde travail en cours lot critique (#157 #163 #164 #174 #175 #178)

This commit is contained in:
2026-07-29 12:34:07 +02:00
parent f523a5a8b5
commit 74d6000606
26 changed files with 516 additions and 62 deletions

View File

@ -63,6 +63,10 @@ enum WatchManualScoreScope { series, step }
enum WatchStepType { time, reps }
enum WatchAlertKind { countdownTick, countdownFinished }
enum WatchAlertPattern { countdownTick, timerFinished }
final class WatchCommandEnvelope {
const WatchCommandEnvelope({
this.schemaVersion = watchBridgeSchemaVersion,
@ -134,6 +138,133 @@ final class WatchCommandEnvelope {
}
}
final class WatchAlertEnvelope {
const WatchAlertEnvelope({
this.schemaVersion = watchBridgeSchemaVersion,
required this.alertId,
required this.sessionId,
required this.revision,
required this.kind,
required this.timerKind,
this.programIndex,
this.exerciseIndex,
this.setIndex,
this.passageIndex,
this.stepIndex,
required this.scheduledForEpochMs,
required this.expiresAtEpochMs,
required this.pattern,
});
factory WatchAlertEnvelope.fromJson(Map<String, Object?> json) {
return WatchAlertEnvelope(
schemaVersion: _intFromJson(
json['schemaVersion'],
watchBridgeSchemaVersion,
),
alertId: _stringFromJson(json['alertId']),
sessionId: _stringFromJson(json['sessionId']),
revision: _intFromJson(json['revision'], 0),
kind: _enumFromJson(
json['kind'],
WatchAlertKind.values,
WatchAlertKind.countdownTick,
),
timerKind: _enumFromJson(
json['timerKind'],
WatchTimerKind.values,
WatchTimerKind.step,
),
programIndex: _nullableIntFromJson(json['programIndex']),
exerciseIndex: _nullableIntFromJson(json['exerciseIndex']),
setIndex: _nullableIntFromJson(json['setIndex']),
passageIndex: _nullableIntFromJson(json['passageIndex']),
stepIndex: _nullableIntFromJson(json['stepIndex']),
scheduledForEpochMs: _intFromJson(json['scheduledForEpochMs'], 0),
expiresAtEpochMs: _intFromJson(json['expiresAtEpochMs'], 0),
pattern: _enumFromJson(
json['pattern'],
WatchAlertPattern.values,
WatchAlertPattern.countdownTick,
),
);
}
final int schemaVersion;
final String alertId;
final String sessionId;
final int revision;
final WatchAlertKind kind;
final WatchTimerKind timerKind;
final int? programIndex;
final int? exerciseIndex;
final int? setIndex;
final int? passageIndex;
final int? stepIndex;
final int scheduledForEpochMs;
final int expiresAtEpochMs;
final WatchAlertPattern pattern;
Map<String, Object?> toJson() {
return {
'schemaVersion': schemaVersion,
'alertId': alertId,
'sessionId': sessionId,
'revision': revision,
'kind': kind.name,
'timerKind': timerKind.name,
'programIndex': programIndex,
'exerciseIndex': exerciseIndex,
'setIndex': setIndex,
'passageIndex': passageIndex,
'stepIndex': stepIndex,
'scheduledForEpochMs': scheduledForEpochMs,
'expiresAtEpochMs': expiresAtEpochMs,
'pattern': pattern.name,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is WatchAlertEnvelope &&
schemaVersion == other.schemaVersion &&
alertId == other.alertId &&
sessionId == other.sessionId &&
revision == other.revision &&
kind == other.kind &&
timerKind == other.timerKind &&
programIndex == other.programIndex &&
exerciseIndex == other.exerciseIndex &&
setIndex == other.setIndex &&
passageIndex == other.passageIndex &&
stepIndex == other.stepIndex &&
scheduledForEpochMs == other.scheduledForEpochMs &&
expiresAtEpochMs == other.expiresAtEpochMs &&
pattern == other.pattern;
}
@override
int get hashCode {
return Object.hash(
schemaVersion,
alertId,
sessionId,
revision,
kind,
timerKind,
programIndex,
exerciseIndex,
setIndex,
passageIndex,
stepIndex,
scheduledForEpochMs,
expiresAtEpochMs,
pattern,
);
}
}
final class WatchSessionProjection {
const WatchSessionProjection({
this.schemaVersion = watchBridgeSchemaVersion,

View File

@ -52,6 +52,55 @@ void main() {
});
});
group('WatchAlertEnvelope', () {
test('round-trips every alert kind through JSON', () {
for (final kind in WatchAlertKind.values) {
final alert = WatchAlertEnvelope(
alertId: 'alert-${kind.name}',
sessionId: 'session-1',
revision: 7,
kind: kind,
timerKind: WatchTimerKind.step,
programIndex: 0,
exerciseIndex: 1,
setIndex: 2,
passageIndex: 3,
stepIndex: 4,
scheduledForEpochMs: 1710000000700,
expiresAtEpochMs: 1710000002700,
pattern: kind == WatchAlertKind.countdownTick
? WatchAlertPattern.countdownTick
: WatchAlertPattern.timerFinished,
);
final decoded = WatchAlertEnvelope.fromJson(
jsonDecode(jsonEncode(alert.toJson())) as Map<String, Object?>,
);
expect(decoded, alert);
}
});
test('falls back safely for absent fields', () {
final alert = WatchAlertEnvelope.fromJson({});
expect(alert.schemaVersion, watchBridgeSchemaVersion);
expect(alert.alertId, '');
expect(alert.sessionId, '');
expect(alert.revision, 0);
expect(alert.kind, WatchAlertKind.countdownTick);
expect(alert.timerKind, WatchTimerKind.step);
expect(alert.programIndex, isNull);
expect(alert.exerciseIndex, isNull);
expect(alert.setIndex, isNull);
expect(alert.passageIndex, isNull);
expect(alert.stepIndex, isNull);
expect(alert.scheduledForEpochMs, 0);
expect(alert.expiresAtEpochMs, 0);
expect(alert.pattern, WatchAlertPattern.countdownTick);
});
});
group('WatchSensorSummary', () {
test('round-trips heart rate summary through JSON', () {
const summary = WatchSensorSummary(