@swimedge/metrics 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -3,12 +3,11 @@
3
3
  * Shared metrics registry for frontend, backend, and watch app
4
4
  */
5
5
  export declare enum SwimType {
6
- INDOOR = "indoor",
7
- OPEN_WATER = "open_water"
6
+ INDOOR = "indoorPool",
7
+ OPEN_WATER = "openWater"
8
8
  }
9
9
  export interface SwimSessionLaps {
10
10
  lapSplitTimes?: number[];
11
- lapTurnTimes?: number[];
12
11
  lapStrokeTypes?: string[];
13
12
  lapHeartRates?: number[];
14
13
  lapStrokeCounts?: number[];
@@ -54,6 +53,11 @@ export interface SwimSession extends SwimSessionLaps {
54
53
  perceivedEffort?: string;
55
54
  equipment?: string[];
56
55
  avgDistancePerStroke?: number;
56
+ routeCoordinates?: number[][];
57
+ routeMapImage?: string;
58
+ deviceType?: string;
59
+ osVersion?: string;
60
+ healthKitWorkoutId?: string;
57
61
  }
58
62
  export interface AggregatedSwimMetrics {
59
63
  totalDistance: number;
@@ -93,7 +97,7 @@ export declare const formatPace: (pace: string | number) => string;
93
97
  export declare const formatTime: (seconds: number) => string;
94
98
  export declare const formatHeartRate: (bpm: number) => string;
95
99
  export declare const formatDistance: (meters: number) => string;
96
- export declare const formatAverageTurnTime: (session: SwimSession) => string;
100
+ export declare const formatSwimType: (swimType: SwimType | string | undefined) => string;
97
101
  export interface MetricDefinition {
98
102
  key: string;
99
103
  label: string;
@@ -112,6 +116,8 @@ export interface MetricDefinition {
112
116
  watch: boolean;
113
117
  manual: boolean;
114
118
  };
119
+ swimTypes: SwimType[];
120
+ hasChart?: boolean;
115
121
  formatter?: (session: SwimSession) => string | number | null;
116
122
  lapFormatter?: (value: any) => string;
117
123
  condition?: (session: SwimSession) => boolean;
@@ -122,5 +128,8 @@ export declare const MetricsHelper: {
122
128
  getSessionMetrics: () => MetricDefinition[];
123
129
  getLapMetrics: () => MetricDefinition[];
124
130
  getMetricsByCategory: (category: MetricCategory) => MetricDefinition[];
131
+ getMetricsBySwimType: (swimType: SwimType) => MetricDefinition[];
132
+ getIndoorMetrics: () => MetricDefinition[];
133
+ getOpenWaterMetrics: () => MetricDefinition[];
125
134
  getAllMetricKeys: () => string[];
126
135
  };
package/dist/index.js CHANGED
@@ -4,11 +4,11 @@
4
4
  * Shared metrics registry for frontend, backend, and watch app
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.MetricsHelper = exports.METRICS_REGISTRY = exports.formatAverageTurnTime = exports.formatDistance = exports.formatHeartRate = exports.formatTime = exports.formatPace = exports.formatNumber = exports.MetricCategory = exports.SwimType = void 0;
7
+ exports.MetricsHelper = exports.METRICS_REGISTRY = exports.formatSwimType = exports.formatDistance = exports.formatHeartRate = exports.formatTime = exports.formatPace = exports.formatNumber = exports.MetricCategory = exports.SwimType = void 0;
8
8
  var SwimType;
9
9
  (function (SwimType) {
10
- SwimType["INDOOR"] = "indoor";
11
- SwimType["OPEN_WATER"] = "open_water";
10
+ SwimType["INDOOR"] = "indoorPool";
11
+ SwimType["OPEN_WATER"] = "openWater";
12
12
  })(SwimType || (exports.SwimType = SwimType = {}));
13
13
  var MetricCategory;
14
14
  (function (MetricCategory) {
@@ -32,7 +32,12 @@ const formatPace = (pace) => {
32
32
  exports.formatPace = formatPace;
33
33
  const formatTime = (seconds) => {
34
34
  const mins = Math.floor(seconds / 60);
35
- const secs = Math.floor(seconds % 60);
35
+ const secs = seconds % 60;
36
+ if (mins >= 60) {
37
+ const hours = Math.floor(mins / 60);
38
+ const remainingMins = mins % 60;
39
+ return `${hours}:${remainingMins.toString().padStart(2, '0')}`;
40
+ }
36
41
  return `${mins}:${secs.toString().padStart(2, '0')}`;
37
42
  };
38
43
  exports.formatTime = formatTime;
@@ -46,16 +51,15 @@ const formatDistance = (meters) => {
46
51
  : `${Math.round(meters)}m`;
47
52
  };
48
53
  exports.formatDistance = formatDistance;
49
- const formatAverageTurnTime = (session) => {
50
- if (!session.lapTurnTimes?.length)
51
- return '';
52
- const validTurnTimes = session.lapTurnTimes.filter(t => !isNaN(t) && t > 0);
53
- if (!validTurnTimes.length)
54
- return '';
55
- const avgTime = validTurnTimes.reduce((a, b) => a + b, 0) / validTurnTimes.length;
56
- return `${(0, exports.formatNumber)(avgTime, 1)}s`;
54
+ const formatSwimType = (swimType) => {
55
+ if (!swimType)
56
+ return 'Unknown';
57
+ return ({
58
+ [SwimType.INDOOR]: 'Indoor Pool',
59
+ [SwimType.OPEN_WATER]: 'Open Water',
60
+ }[swimType] || swimType);
57
61
  };
58
- exports.formatAverageTurnTime = formatAverageTurnTime;
62
+ exports.formatSwimType = formatSwimType;
59
63
  exports.METRICS_REGISTRY = {
60
64
  distance: {
61
65
  key: 'distance',
@@ -69,6 +73,8 @@ exports.METRICS_REGISTRY = {
69
73
  healthKitSource: 'HKWorkout.totalDistance',
70
74
  isHealthKit: true,
71
75
  availableFrom: { watch: true, manual: true },
76
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
77
+ hasChart: true,
72
78
  formatter: (s) => (0, exports.formatDistance)(s.distance),
73
79
  },
74
80
  time: {
@@ -83,6 +89,8 @@ exports.METRICS_REGISTRY = {
83
89
  healthKitSource: 'HKWorkout.duration',
84
90
  isHealthKit: true,
85
91
  availableFrom: { watch: true, manual: true },
92
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
93
+ hasChart: true,
86
94
  formatter: (s) => (0, exports.formatTime)(s.time),
87
95
  },
88
96
  activeTime: {
@@ -97,6 +105,8 @@ exports.METRICS_REGISTRY = {
97
105
  healthKitSource: 'HKWorkoutBuilder.elapsedTime',
98
106
  isHealthKit: true,
99
107
  availableFrom: { watch: true, manual: false },
108
+ hasChart: true,
109
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
100
110
  },
101
111
  restTime: {
102
112
  key: 'restTime',
@@ -108,6 +118,8 @@ exports.METRICS_REGISTRY = {
108
118
  isPerSession: true,
109
119
  formula: 'Total Duration - Active Time',
110
120
  availableFrom: { watch: true, manual: false },
121
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
122
+ hasChart: true,
111
123
  formatter: (s) => (0, exports.formatTime)(s.restTime),
112
124
  condition: (s) => !!s.restTime,
113
125
  },
@@ -121,6 +133,8 @@ exports.METRICS_REGISTRY = {
121
133
  isPerSession: true,
122
134
  formula: 'Distance ÷ Pool Length',
123
135
  availableFrom: { watch: true, manual: true },
136
+ swimTypes: [SwimType.INDOOR],
137
+ hasChart: true,
124
138
  formatter: (s) => String(s.laps),
125
139
  condition: (s) => !!s.laps,
126
140
  },
@@ -134,6 +148,8 @@ exports.METRICS_REGISTRY = {
134
148
  isPerSession: true,
135
149
  formula: '(Active Time ÷ Distance) × 100',
136
150
  availableFrom: { watch: true, manual: true },
151
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
152
+ hasChart: true,
137
153
  formatter: (s) => (0, exports.formatPace)(s.avgPace),
138
154
  condition: (s) => !!s.avgPace,
139
155
  },
@@ -147,6 +163,7 @@ exports.METRICS_REGISTRY = {
147
163
  isPerSession: true,
148
164
  source: 'User configuration setting',
149
165
  availableFrom: { watch: true, manual: true },
166
+ swimTypes: [SwimType.INDOOR],
150
167
  formatter: (s) => `${s.poolLength}m`,
151
168
  condition: (s) => !!s.poolLength,
152
169
  },
@@ -160,7 +177,8 @@ exports.METRICS_REGISTRY = {
160
177
  isPerSession: true,
161
178
  source: 'User selection or workout location type',
162
179
  availableFrom: { watch: true, manual: true },
163
- formatter: (s) => s.swimType,
180
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
181
+ formatter: (s) => (0, exports.formatSwimType)(s.swimType),
164
182
  condition: (s) => !!s.swimType,
165
183
  },
166
184
  waterTemp: {
@@ -174,6 +192,7 @@ exports.METRICS_REGISTRY = {
174
192
  source: 'Apple Watch water temperature sensor',
175
193
  isHealthKit: true,
176
194
  availableFrom: { watch: true, manual: false },
195
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
177
196
  formatter: (s) => `${(0, exports.formatNumber)(s.waterTemp, 1)}°C`,
178
197
  condition: (s) => !!s.waterTemp,
179
198
  },
@@ -189,6 +208,7 @@ exports.METRICS_REGISTRY = {
189
208
  healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
190
209
  isHealthKit: true,
191
210
  availableFrom: { watch: true, manual: false },
211
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
192
212
  formatter: (s) => (0, exports.formatHeartRate)(s.avgHeartRate),
193
213
  condition: (s) => !!s.avgHeartRate,
194
214
  },
@@ -204,6 +224,7 @@ exports.METRICS_REGISTRY = {
204
224
  healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
205
225
  isHealthKit: true,
206
226
  availableFrom: { watch: true, manual: false },
227
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
207
228
  formatter: (s) => (0, exports.formatHeartRate)(s.maxHeartRate),
208
229
  condition: (s) => !!s.maxHeartRate,
209
230
  },
@@ -219,6 +240,7 @@ exports.METRICS_REGISTRY = {
219
240
  healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
220
241
  isHealthKit: true,
221
242
  availableFrom: { watch: true, manual: false },
243
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
222
244
  formatter: (s) => (0, exports.formatHeartRate)(s.minHeartRate),
223
245
  condition: (s) => !!s.minHeartRate,
224
246
  },
@@ -234,6 +256,8 @@ exports.METRICS_REGISTRY = {
234
256
  healthKitSource: 'HKQuantityTypeIdentifierActiveEnergyBurned',
235
257
  isHealthKit: true,
236
258
  availableFrom: { watch: true, manual: true },
259
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
260
+ hasChart: true,
237
261
  formatter: (s) => String(s.calories),
238
262
  condition: (s) => !!s.calories,
239
263
  },
@@ -249,6 +273,8 @@ exports.METRICS_REGISTRY = {
249
273
  healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
250
274
  isHealthKit: true,
251
275
  availableFrom: { watch: true, manual: false },
276
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
277
+ hasChart: true,
252
278
  formatter: (s) => String(s.totalStrokeCount || 0),
253
279
  condition: (s) => !!s.totalStrokeCount,
254
280
  },
@@ -262,6 +288,8 @@ exports.METRICS_REGISTRY = {
262
288
  isPerSession: true,
263
289
  formula: 'Total Strokes ÷ (Active Time in Minutes)',
264
290
  availableFrom: { watch: true, manual: true },
291
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
292
+ hasChart: true,
265
293
  formatter: (s) => `${(0, exports.formatNumber)(s.avgStrokeRate, 1)} SPM`,
266
294
  condition: (s) => !!s.avgStrokeRate,
267
295
  },
@@ -275,6 +303,8 @@ exports.METRICS_REGISTRY = {
275
303
  isPerSession: true,
276
304
  formula: 'Distance ÷ Total Strokes',
277
305
  availableFrom: { watch: true, manual: true },
306
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
307
+ hasChart: true,
278
308
  formatter: (s) => `${(0, exports.formatNumber)(s.avgDistancePerStroke, 2)}m`,
279
309
  condition: (s) => !!s.avgDistancePerStroke,
280
310
  },
@@ -288,6 +318,8 @@ exports.METRICS_REGISTRY = {
288
318
  isPerSession: true,
289
319
  formula: 'Average of (Lap Time in Seconds + Lap Stroke Count) for each lap',
290
320
  availableFrom: { watch: true, manual: true },
321
+ swimTypes: [SwimType.INDOOR],
322
+ hasChart: true,
291
323
  formatter: (s) => (0, exports.formatNumber)(s.avgSwolfScore, 0),
292
324
  condition: (s) => !!s.avgSwolfScore,
293
325
  },
@@ -303,6 +335,7 @@ exports.METRICS_REGISTRY = {
303
335
  healthKitSource: 'HKWorkoutEvent.metadata[HKMetadataKeySwimmingStrokeStyle]',
304
336
  isHealthKit: true,
305
337
  availableFrom: { watch: true, manual: true },
338
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
306
339
  formatter: (s) => s.primaryStrokeType,
307
340
  condition: (s) => !!s.primaryStrokeType,
308
341
  },
@@ -316,6 +349,7 @@ exports.METRICS_REGISTRY = {
316
349
  isPerLap: true,
317
350
  source: 'Tracked per lap completion',
318
351
  availableFrom: { watch: true, manual: false },
352
+ swimTypes: [SwimType.INDOOR],
319
353
  formatter: (s) => `${s.lapSplitTimes.length} splits`,
320
354
  lapFormatter: (v) => (0, exports.formatTime)(v),
321
355
  condition: (s) => !!s.lapSplitTimes?.length,
@@ -333,6 +367,7 @@ exports.METRICS_REGISTRY = {
333
367
  healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
334
368
  isHealthKit: true,
335
369
  availableFrom: { watch: true, manual: false },
370
+ swimTypes: [SwimType.INDOOR],
336
371
  },
337
372
  lapStrokeCounts: {
338
373
  key: 'lapStrokeCounts',
@@ -347,6 +382,7 @@ exports.METRICS_REGISTRY = {
347
382
  healthKitSource: 'HKQuantityTypeIdentifierSwimmingStrokeCount',
348
383
  isHealthKit: true,
349
384
  availableFrom: { watch: true, manual: false },
385
+ swimTypes: [SwimType.INDOOR],
350
386
  },
351
387
  id: {
352
388
  key: 'id',
@@ -355,6 +391,7 @@ exports.METRICS_REGISTRY = {
355
391
  description: 'Session identifier',
356
392
  isPerSession: true,
357
393
  availableFrom: { watch: false, manual: false },
394
+ swimTypes: [SwimType.INDOOR],
358
395
  },
359
396
  watchSessionId: {
360
397
  key: 'watchSessionId',
@@ -363,6 +400,7 @@ exports.METRICS_REGISTRY = {
363
400
  description: 'Watch session identifier',
364
401
  isPerSession: true,
365
402
  availableFrom: { watch: true, manual: false },
403
+ swimTypes: [SwimType.INDOOR],
366
404
  },
367
405
  date: {
368
406
  key: 'date',
@@ -371,6 +409,7 @@ exports.METRICS_REGISTRY = {
371
409
  description: 'Session date',
372
410
  isPerSession: true,
373
411
  availableFrom: { watch: true, manual: true },
412
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
374
413
  },
375
414
  source: {
376
415
  key: 'source',
@@ -379,20 +418,23 @@ exports.METRICS_REGISTRY = {
379
418
  description: 'Data source',
380
419
  isPerSession: true,
381
420
  availableFrom: { watch: false, manual: false },
382
- },
383
- lapTurnTimes: {
384
- key: 'lapTurnTimes',
385
- label: 'Turn',
386
- unit: 's',
387
- category: MetricCategory.TECHNIQUE,
388
- description: 'Time spent turning at pool wall per lap',
389
- icon: 'refresh-circle-outline',
390
- isPerLap: true,
391
- decimals: 2,
392
- availableFrom: { watch: true, manual: false },
393
- formatter: exports.formatAverageTurnTime,
394
- condition: (s) => !!s.lapTurnTimes?.length,
395
- },
421
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
422
+ },
423
+ // HealthKit doesn't track turn times
424
+ /*lapTurnTimes: {
425
+ key: 'lapTurnTimes',
426
+ label: 'Turn',
427
+ unit: 's',
428
+ category: MetricCategory.TECHNIQUE,
429
+ description: 'Time spent turning at pool wall per lap',
430
+ icon: 'refresh-circle-outline',
431
+ isPerLap: true,
432
+ decimals: 2,
433
+ availableFrom: { watch: true, manual: false },
434
+ swimTypes: [SwimType.INDOOR],
435
+ formatter: formatAverageTurnTime,
436
+ condition: (s) => !!s.lapTurnTimes?.length,
437
+ },*/
396
438
  lapStrokeTypes: {
397
439
  key: 'lapStrokeTypes',
398
440
  label: 'Stroke',
@@ -401,6 +443,7 @@ exports.METRICS_REGISTRY = {
401
443
  icon: 'list-outline',
402
444
  isPerLap: true,
403
445
  availableFrom: { watch: true, manual: false },
446
+ swimTypes: [SwimType.INDOOR],
404
447
  formatter: (s) => `${s.lapStrokeTypes.length} types`,
405
448
  condition: (s) => !!s.lapStrokeTypes?.length,
406
449
  },
@@ -413,6 +456,7 @@ exports.METRICS_REGISTRY = {
413
456
  isPerLap: true,
414
457
  decimals: 1,
415
458
  availableFrom: { watch: true, manual: false },
459
+ swimTypes: [SwimType.INDOOR],
416
460
  },
417
461
  lapCalories: {
418
462
  key: 'lapCalories',
@@ -423,6 +467,7 @@ exports.METRICS_REGISTRY = {
423
467
  isPerLap: true,
424
468
  decimals: 0,
425
469
  availableFrom: { watch: true, manual: false },
470
+ swimTypes: [SwimType.INDOOR],
426
471
  },
427
472
  lapDistances: {
428
473
  key: 'lapDistances',
@@ -432,6 +477,7 @@ exports.METRICS_REGISTRY = {
432
477
  description: 'Distance per lap',
433
478
  isPerLap: true,
434
479
  availableFrom: { watch: true, manual: false },
480
+ swimTypes: [SwimType.INDOOR],
435
481
  },
436
482
  lapSwolfScores: {
437
483
  key: 'lapSwolfScores',
@@ -441,6 +487,7 @@ exports.METRICS_REGISTRY = {
441
487
  isPerLap: true,
442
488
  decimals: 0,
443
489
  availableFrom: { watch: true, manual: false },
490
+ swimTypes: [SwimType.INDOOR],
444
491
  },
445
492
  lapStrokeRates: {
446
493
  key: 'lapStrokeRates',
@@ -453,6 +500,7 @@ exports.METRICS_REGISTRY = {
453
500
  decimals: 1,
454
501
  formula: 'Lap Strokes ÷ (Lap Time in Minutes)',
455
502
  availableFrom: { watch: true, manual: false },
503
+ swimTypes: [SwimType.INDOOR],
456
504
  },
457
505
  lapDistancesPerStroke: {
458
506
  key: 'lapDistancesPerStroke',
@@ -465,6 +513,7 @@ exports.METRICS_REGISTRY = {
465
513
  decimals: 2,
466
514
  formula: 'Lap Distance ÷ Lap Strokes',
467
515
  availableFrom: { watch: true, manual: false },
516
+ swimTypes: [SwimType.INDOOR],
468
517
  },
469
518
  poolLocation: {
470
519
  key: 'poolLocation',
@@ -473,6 +522,7 @@ exports.METRICS_REGISTRY = {
473
522
  description: 'Pool location details',
474
523
  isPerSession: true,
475
524
  availableFrom: { watch: false, manual: true },
525
+ swimTypes: [SwimType.INDOOR],
476
526
  },
477
527
  notes: {
478
528
  key: 'notes',
@@ -481,6 +531,7 @@ exports.METRICS_REGISTRY = {
481
531
  description: 'Session notes',
482
532
  isPerSession: true,
483
533
  availableFrom: { watch: false, manual: true },
534
+ swimTypes: [SwimType.INDOOR],
484
535
  },
485
536
  trainingTypes: {
486
537
  key: 'trainingTypes',
@@ -492,6 +543,7 @@ exports.METRICS_REGISTRY = {
492
543
  availableFrom: { watch: false, manual: true },
493
544
  formatter: (s) => s.trainingTypes?.join(', ') || '',
494
545
  condition: (s) => !!s.trainingTypes?.length,
546
+ swimTypes: [SwimType.INDOOR],
495
547
  },
496
548
  perceivedEffort: {
497
549
  key: 'perceivedEffort',
@@ -503,6 +555,7 @@ exports.METRICS_REGISTRY = {
503
555
  availableFrom: { watch: false, manual: true },
504
556
  formatter: (s) => s.perceivedEffort,
505
557
  condition: (s) => !!s.perceivedEffort,
558
+ swimTypes: [SwimType.INDOOR],
506
559
  },
507
560
  equipment: {
508
561
  key: 'equipment',
@@ -514,6 +567,56 @@ exports.METRICS_REGISTRY = {
514
567
  availableFrom: { watch: false, manual: true },
515
568
  formatter: (s) => s.equipment?.join(', ') || '',
516
569
  condition: (s) => !!s.equipment?.length,
570
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
571
+ },
572
+ routeCoordinates: {
573
+ key: 'routeCoordinates',
574
+ label: 'GPS Route',
575
+ category: MetricCategory.PERFORMANCE,
576
+ description: 'GPS coordinates of the swimming route',
577
+ icon: 'map-outline',
578
+ isPerSession: true,
579
+ availableFrom: { watch: true, manual: false },
580
+ swimTypes: [SwimType.OPEN_WATER],
581
+ formatter: (s) => s.routeCoordinates ? `${s.routeCoordinates.length} points` : '',
582
+ condition: (s) => !!s.routeCoordinates?.length,
583
+ },
584
+ deviceType: {
585
+ key: 'deviceType',
586
+ label: 'Device',
587
+ category: MetricCategory.PERFORMANCE,
588
+ description: 'Device used to record the session',
589
+ isPerSession: true,
590
+ availableFrom: { watch: true, manual: false },
591
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
592
+ },
593
+ osVersion: {
594
+ key: 'osVersion',
595
+ label: 'OS Version',
596
+ category: MetricCategory.PERFORMANCE,
597
+ description: 'Operating system version of the recording device',
598
+ isPerSession: true,
599
+ availableFrom: { watch: true, manual: false },
600
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
601
+ },
602
+ healthKitWorkoutId: {
603
+ key: 'healthKitWorkoutId',
604
+ label: 'HealthKit ID',
605
+ category: MetricCategory.PERFORMANCE,
606
+ description: 'HealthKit workout identifier',
607
+ isPerSession: true,
608
+ availableFrom: { watch: true, manual: false },
609
+ swimTypes: [SwimType.INDOOR, SwimType.OPEN_WATER],
610
+ },
611
+ routeMapImage: {
612
+ key: 'routeMapImage',
613
+ label: 'Route Map',
614
+ category: MetricCategory.PERFORMANCE,
615
+ description: 'Base64 encoded route map image for open water sessions',
616
+ isPerSession: true,
617
+ availableFrom: { watch: false, manual: false },
618
+ swimTypes: [SwimType.OPEN_WATER],
619
+ condition: (s) => !!s.routeMapImage,
517
620
  },
518
621
  };
519
622
  exports.MetricsHelper = {
@@ -521,5 +624,8 @@ exports.MetricsHelper = {
521
624
  getSessionMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerSession),
522
625
  getLapMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerLap),
523
626
  getMetricsByCategory: (category) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.category === category),
627
+ getMetricsBySwimType: (swimType) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(swimType)),
628
+ getIndoorMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(SwimType.INDOOR)),
629
+ getOpenWaterMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.swimTypes?.includes(SwimType.OPEN_WATER)),
524
630
  getAllMetricKeys: () => Object.keys(exports.METRICS_REGISTRY),
525
631
  };
@@ -72,34 +72,4 @@ describe('Metrics Formatting Functions', () => {
72
72
  expect((0, index_1.formatDistance)(500.7)).toBe('501m');
73
73
  });
74
74
  });
75
- describe('formatAverageTurnTime', () => {
76
- const createSession = (lapTurnTimes) => ({
77
- id: '1',
78
- date: '2024-01-01',
79
- distance: 1000,
80
- laps: 20,
81
- time: 1200,
82
- calories: 300,
83
- avgPace: '2:00',
84
- poolLength: 50,
85
- source: 'watch',
86
- lapTurnTimes,
87
- });
88
- it('should return empty string when no lap turn times', () => {
89
- expect((0, index_1.formatAverageTurnTime)(createSession())).toBe('');
90
- expect((0, index_1.formatAverageTurnTime)(createSession([]))).toBe('');
91
- });
92
- it('should calculate average turn time', () => {
93
- expect((0, index_1.formatAverageTurnTime)(createSession([2.5, 3.0, 2.5]))).toBe('2.7s');
94
- });
95
- it('should filter out invalid turn times', () => {
96
- expect((0, index_1.formatAverageTurnTime)(createSession([2.5, NaN, 3.0, 0, 2.5]))).toBe('2.7s');
97
- });
98
- it('should return empty string when all turn times are invalid', () => {
99
- expect((0, index_1.formatAverageTurnTime)(createSession([NaN, 0, -1]))).toBe('');
100
- });
101
- it('should format with 1 decimal place', () => {
102
- expect((0, index_1.formatAverageTurnTime)(createSession([2.456]))).toBe('2.5s');
103
- });
104
- });
105
75
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimedge/metrics",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Shared metrics registry for SwimEdge (frontend, backend, watch)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,7 +13,6 @@
13
13
  "README.md"
14
14
  ],
15
15
  "scripts": {
16
- "prepublishOnly": "npm run build",
17
16
  "build": "tsc",
18
17
  "watch": "tsc --watch",
19
18
  "export-json": "node scripts/export-json.js",