@swimedge/metrics 1.0.0

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/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @swimedge/metrics
2
+
3
+ Shared metrics registry for SwimEdge - used by frontend, backend, and watch app.
4
+
5
+ ## Installation
6
+
7
+ ### Frontend (React Native)
8
+ ```bash
9
+ cd SwimEdge
10
+ npm install ../shared/metrics
11
+ ```
12
+
13
+ ### Backend (Node.js)
14
+ ```bash
15
+ cd backend-api
16
+ npm install ../shared/metrics
17
+ ```
18
+
19
+ ### Watch App (Swift - via JSON export)
20
+ ```bash
21
+ cd shared/metrics
22
+ npm run export-json
23
+ # Copy metrics.json to watch app
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```typescript
29
+ import { METRICS_REGISTRY, MetricsHelper, MetricCategory } from '@swimedge/metrics';
30
+
31
+ // Get metric definition
32
+ const metric = MetricsHelper.getMetric('totalStrokeCount');
33
+ console.log(metric.label); // "Total Strokes"
34
+ console.log(metric.validationMin); // 0
35
+ console.log(metric.validationMax); // 50000
36
+
37
+ // Get all session metrics
38
+ const sessionMetrics = MetricsHelper.getSessionMetrics();
39
+
40
+ // Get metrics by category
41
+ const techniqueMetrics = MetricsHelper.getMetricsByCategory(MetricCategory.TECHNIQUE);
42
+ ```
43
+
44
+ ## Benefits
45
+
46
+ - ✅ Single source of truth
47
+ - ✅ Type-safe across all platforms
48
+ - ✅ Validation rules included
49
+ - ✅ Icons and descriptions embedded
50
+ - ✅ Easy to maintain
51
+
52
+ ## Metrics Count
53
+
54
+ - Session-level: 18 metrics
55
+ - Lap-level: 3 metrics
56
+ - Aggregated: 2 metrics
57
+ - Total: 23+ core metrics
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @swimedge/metrics
3
+ * Shared metrics registry for frontend, backend, and watch app
4
+ */
5
+ export declare enum SwimType {
6
+ INDOOR = "indoor",
7
+ OPEN_WATER = "open_water"
8
+ }
9
+ export interface SwimSessionLaps {
10
+ lapSplitTimes?: number[];
11
+ lapTurnTimes?: number[];
12
+ lapStrokeTypes?: string[];
13
+ lapHeartRates?: number[];
14
+ lapStrokeCounts?: number[];
15
+ lapPaces?: number[];
16
+ lapCalories?: number[];
17
+ lapDistances?: number[];
18
+ lapSwolfScores?: number[];
19
+ lapStrokeRates?: number[];
20
+ lapDistancesPerStroke?: number[];
21
+ }
22
+ export interface SwimSession extends SwimSessionLaps {
23
+ id: string;
24
+ watchSessionId?: string;
25
+ date: string;
26
+ distance: number;
27
+ laps: number;
28
+ time: number;
29
+ activeTime?: number;
30
+ restTime?: number;
31
+ calories: number;
32
+ avgHeartRate?: number;
33
+ maxHeartRate?: number;
34
+ minHeartRate?: number;
35
+ avgPace: string;
36
+ totalStrokeCount?: number;
37
+ avgStrokeRate?: number;
38
+ avgSwolfScore?: number;
39
+ poolLength: number;
40
+ source: string;
41
+ swimType?: SwimType;
42
+ primaryStrokeType?: string;
43
+ waterTemp?: number;
44
+ poolLocation?: {
45
+ name: string;
46
+ address: string;
47
+ coordinates?: {
48
+ lat: number;
49
+ lng: number;
50
+ };
51
+ };
52
+ notes?: string;
53
+ trainingTypes?: string[];
54
+ perceivedEffort?: string;
55
+ equipment?: string[];
56
+ avgDistancePerStroke?: number;
57
+ }
58
+ export interface AggregatedSwimMetrics {
59
+ totalDistance: number;
60
+ longestDistance: number;
61
+ avgPace: string;
62
+ bestPace: string;
63
+ avgDistance: number;
64
+ avgHeartRate: number;
65
+ avgRestTime: number;
66
+ avgLaps: number;
67
+ sessionCount: number;
68
+ maxHeartRate: number;
69
+ avgRecoveryHeartRate: number;
70
+ totalCalories: number;
71
+ totalStrokeCount: number;
72
+ avgStrokeRate: number;
73
+ avgDistancePerStroke: number;
74
+ avgSwolfScore: number;
75
+ minHeartRate: number;
76
+ primaryStrokeType: string;
77
+ totalWorkoutTime: number;
78
+ swimTypeDistribution: {
79
+ counts: Record<string, number>;
80
+ percentages: Record<string, number>;
81
+ preferred: string;
82
+ total: number;
83
+ };
84
+ }
85
+ export declare enum MetricCategory {
86
+ PERFORMANCE = "performance",
87
+ HEALTH = "health",
88
+ TECHNIQUE = "technique",
89
+ ENERGY = "energy"
90
+ }
91
+ export interface MetricDefinition {
92
+ key: string;
93
+ label: string;
94
+ unit?: string;
95
+ category: MetricCategory;
96
+ description: string;
97
+ icon?: string;
98
+ isPerSession?: boolean;
99
+ isPerLap?: boolean;
100
+ formula?: string;
101
+ source?: string;
102
+ healthKitSource?: string;
103
+ availableFrom: {
104
+ watch: boolean;
105
+ manual: boolean;
106
+ };
107
+ }
108
+ export declare const METRICS_REGISTRY: Record<keyof SwimSession, MetricDefinition>;
109
+ export declare const MetricsHelper: {
110
+ getMetric: (key: keyof SwimSession) => MetricDefinition;
111
+ getSessionMetrics: () => MetricDefinition[];
112
+ getLapMetrics: () => MetricDefinition[];
113
+ getMetricsByCategory: (category: MetricCategory) => MetricDefinition[];
114
+ getAllMetricKeys: () => string[];
115
+ };
package/dist/index.js ADDED
@@ -0,0 +1,412 @@
1
+ "use strict";
2
+ /**
3
+ * @swimedge/metrics
4
+ * Shared metrics registry for frontend, backend, and watch app
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.MetricsHelper = exports.METRICS_REGISTRY = exports.MetricCategory = exports.SwimType = void 0;
8
+ var SwimType;
9
+ (function (SwimType) {
10
+ SwimType["INDOOR"] = "indoor";
11
+ SwimType["OPEN_WATER"] = "open_water";
12
+ })(SwimType || (exports.SwimType = SwimType = {}));
13
+ var MetricCategory;
14
+ (function (MetricCategory) {
15
+ MetricCategory["PERFORMANCE"] = "performance";
16
+ MetricCategory["HEALTH"] = "health";
17
+ MetricCategory["TECHNIQUE"] = "technique";
18
+ MetricCategory["ENERGY"] = "energy";
19
+ })(MetricCategory || (exports.MetricCategory = MetricCategory = {}));
20
+ exports.METRICS_REGISTRY = {
21
+ distance: {
22
+ key: 'distance',
23
+ label: 'Distance',
24
+ unit: 'm',
25
+ category: MetricCategory.PERFORMANCE,
26
+ description: 'Total distance swum',
27
+ icon: 'resize-outline',
28
+ isPerSession: true,
29
+ source: 'HealthKit: HKWorkout distance',
30
+ healthKitSource: 'HKWorkout.totalDistance',
31
+ availableFrom: { watch: true, manual: true },
32
+ },
33
+ time: {
34
+ key: 'time',
35
+ label: 'Duration',
36
+ unit: 's',
37
+ category: MetricCategory.PERFORMANCE,
38
+ description: 'Total workout duration',
39
+ icon: 'time-outline',
40
+ isPerSession: true,
41
+ source: 'HealthKit: HKWorkout duration',
42
+ healthKitSource: 'HKWorkout.duration',
43
+ availableFrom: { watch: true, manual: true },
44
+ },
45
+ activeTime: {
46
+ key: 'activeTime',
47
+ label: 'Active Time',
48
+ unit: 's',
49
+ category: MetricCategory.PERFORMANCE,
50
+ description: 'Time actively swimming',
51
+ icon: 'play-outline',
52
+ isPerSession: true,
53
+ source: 'Calculated from stroke detection with 5-second timeout',
54
+ availableFrom: { watch: true, manual: false },
55
+ },
56
+ restTime: {
57
+ key: 'restTime',
58
+ label: 'Rest Time',
59
+ unit: 's',
60
+ category: MetricCategory.PERFORMANCE,
61
+ description: 'Time resting',
62
+ icon: 'pause-outline',
63
+ isPerSession: true,
64
+ formula: 'Total Duration - Active Time',
65
+ availableFrom: { watch: true, manual: false },
66
+ },
67
+ laps: {
68
+ key: 'laps',
69
+ label: 'Laps',
70
+ unit: '',
71
+ category: MetricCategory.PERFORMANCE,
72
+ description: 'Pool lengths',
73
+ icon: 'layers-outline',
74
+ isPerSession: true,
75
+ formula: 'Distance ÷ Pool Length',
76
+ availableFrom: { watch: true, manual: true },
77
+ },
78
+ avgPace: {
79
+ key: 'avgPace',
80
+ label: 'Pace',
81
+ unit: '/100m',
82
+ category: MetricCategory.PERFORMANCE,
83
+ description: 'Average pace',
84
+ icon: 'speedometer-outline',
85
+ isPerSession: true,
86
+ formula: '(Active Time ÷ Distance) × 100',
87
+ availableFrom: { watch: true, manual: true },
88
+ },
89
+ poolLength: {
90
+ key: 'poolLength',
91
+ label: 'Pool Length',
92
+ unit: 'm',
93
+ category: MetricCategory.PERFORMANCE,
94
+ description: 'Pool length',
95
+ icon: 'resize-outline',
96
+ isPerSession: true,
97
+ source: 'User configuration setting',
98
+ availableFrom: { watch: true, manual: true },
99
+ },
100
+ swimType: {
101
+ key: 'swimType',
102
+ label: 'Swim Type',
103
+ unit: '',
104
+ category: MetricCategory.PERFORMANCE,
105
+ description: 'Indoor/Open Water',
106
+ icon: 'water-outline',
107
+ isPerSession: true,
108
+ source: 'User selection or workout location type',
109
+ availableFrom: { watch: true, manual: true },
110
+ },
111
+ waterTemp: {
112
+ key: 'waterTemp',
113
+ label: 'Water Temp',
114
+ unit: '°C',
115
+ category: MetricCategory.PERFORMANCE,
116
+ description: 'Water temperature',
117
+ icon: 'thermometer-outline',
118
+ isPerSession: true,
119
+ source: 'Apple Watch water temperature sensor',
120
+ availableFrom: { watch: true, manual: false },
121
+ },
122
+ avgHeartRate: {
123
+ key: 'avgHeartRate',
124
+ label: 'Avg Heart Rate',
125
+ unit: 'bpm',
126
+ category: MetricCategory.HEALTH,
127
+ description: 'Average heart rate',
128
+ icon: 'heart-outline',
129
+ isPerSession: true,
130
+ source: 'HealthKit: HKQuantityTypeIdentifierHeartRate',
131
+ healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
132
+ availableFrom: { watch: true, manual: false },
133
+ },
134
+ maxHeartRate: {
135
+ key: 'maxHeartRate',
136
+ label: 'Max Heart Rate',
137
+ unit: 'bpm',
138
+ category: MetricCategory.HEALTH,
139
+ description: 'Maximum heart rate',
140
+ icon: 'trending-up-outline',
141
+ isPerSession: true,
142
+ source: 'HealthKit: HKQuantityTypeIdentifierHeartRate',
143
+ healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
144
+ availableFrom: { watch: true, manual: false },
145
+ },
146
+ minHeartRate: {
147
+ key: 'minHeartRate',
148
+ label: 'Min Heart Rate',
149
+ unit: 'bpm',
150
+ category: MetricCategory.HEALTH,
151
+ description: 'Minimum heart rate',
152
+ icon: 'trending-down-outline',
153
+ isPerSession: true,
154
+ source: 'HealthKit: HKQuantityTypeIdentifierHeartRate',
155
+ healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
156
+ availableFrom: { watch: true, manual: false },
157
+ },
158
+ calories: {
159
+ key: 'calories',
160
+ label: 'Calories',
161
+ unit: 'kcal',
162
+ category: MetricCategory.ENERGY,
163
+ description: 'Total calories',
164
+ icon: 'flame-outline',
165
+ isPerSession: true,
166
+ source: 'HealthKit: HKQuantityTypeIdentifierActiveEnergyBurned',
167
+ healthKitSource: 'HKQuantityTypeIdentifierActiveEnergyBurned',
168
+ availableFrom: { watch: true, manual: true },
169
+ },
170
+ totalStrokeCount: {
171
+ key: 'totalStrokeCount',
172
+ label: 'Total Strokes',
173
+ unit: '',
174
+ category: MetricCategory.TECHNIQUE,
175
+ description: 'Total strokes',
176
+ icon: 'hand-left-outline',
177
+ isPerSession: true,
178
+ source: 'Apple Watch accelerometer and gyroscope stroke detection',
179
+ availableFrom: { watch: true, manual: false },
180
+ },
181
+ avgStrokeRate: {
182
+ key: 'avgStrokeRate',
183
+ label: 'Stroke Rate',
184
+ unit: 'SPM',
185
+ category: MetricCategory.TECHNIQUE,
186
+ description: 'Strokes per minute',
187
+ icon: 'repeat-outline',
188
+ isPerSession: true,
189
+ formula: 'Total Strokes ÷ (Active Time in Minutes)',
190
+ availableFrom: { watch: true, manual: true },
191
+ },
192
+ avgDistancePerStroke: {
193
+ key: 'avgDistancePerStroke',
194
+ label: 'Distance/Stroke',
195
+ unit: 'm',
196
+ category: MetricCategory.TECHNIQUE,
197
+ description: 'Distance per stroke',
198
+ icon: 'arrow-forward-outline',
199
+ isPerSession: true,
200
+ formula: 'Distance ÷ Total Strokes',
201
+ availableFrom: { watch: true, manual: true },
202
+ },
203
+ avgSwolfScore: {
204
+ key: 'avgSwolfScore',
205
+ label: 'SWOLF',
206
+ unit: '',
207
+ category: MetricCategory.TECHNIQUE,
208
+ description: 'Efficiency score',
209
+ icon: 'trophy-outline',
210
+ isPerSession: true,
211
+ formula: 'Average of (Lap Time in Seconds + Lap Stroke Count) for each lap',
212
+ availableFrom: { watch: true, manual: true },
213
+ },
214
+ primaryStrokeType: {
215
+ key: 'primaryStrokeType',
216
+ label: 'Primary Stroke',
217
+ unit: '',
218
+ category: MetricCategory.TECHNIQUE,
219
+ description: 'Main stroke',
220
+ icon: 'hand-right-outline',
221
+ isPerSession: true,
222
+ source: 'Apple Watch motion pattern recognition',
223
+ availableFrom: { watch: true, manual: true },
224
+ },
225
+ lapSplitTimes: {
226
+ key: 'lapSplitTimes',
227
+ label: 'Split Times',
228
+ unit: 's',
229
+ category: MetricCategory.PERFORMANCE,
230
+ description: 'Lap times',
231
+ icon: 'stopwatch-outline',
232
+ isPerLap: true,
233
+ source: 'Tracked per lap completion',
234
+ availableFrom: { watch: true, manual: false },
235
+ },
236
+ lapHeartRates: {
237
+ key: 'lapHeartRates',
238
+ label: 'Lap HR',
239
+ unit: 'bpm',
240
+ category: MetricCategory.HEALTH,
241
+ description: 'HR per lap',
242
+ icon: 'heart-outline',
243
+ isPerLap: true,
244
+ source: 'HealthKit: HKQuantityTypeIdentifierHeartRate sampled at lap completion',
245
+ healthKitSource: 'HKQuantityTypeIdentifierHeartRate',
246
+ availableFrom: { watch: true, manual: false },
247
+ },
248
+ lapStrokeCounts: {
249
+ key: 'lapStrokeCounts',
250
+ label: 'Lap Strokes',
251
+ unit: '',
252
+ category: MetricCategory.TECHNIQUE,
253
+ description: 'Strokes per lap',
254
+ icon: 'hand-left-outline',
255
+ isPerLap: true,
256
+ source: 'Apple Watch accelerometer stroke detection per lap',
257
+ availableFrom: { watch: true, manual: false },
258
+ },
259
+ id: {
260
+ key: 'id',
261
+ label: 'ID',
262
+ category: MetricCategory.PERFORMANCE,
263
+ description: 'Session identifier',
264
+ isPerSession: true,
265
+ availableFrom: { watch: false, manual: false },
266
+ },
267
+ watchSessionId: {
268
+ key: 'watchSessionId',
269
+ label: 'Watch Session ID',
270
+ category: MetricCategory.PERFORMANCE,
271
+ description: 'Watch session identifier',
272
+ isPerSession: true,
273
+ availableFrom: { watch: true, manual: false },
274
+ },
275
+ date: {
276
+ key: 'date',
277
+ label: 'Date',
278
+ category: MetricCategory.PERFORMANCE,
279
+ description: 'Session date',
280
+ isPerSession: true,
281
+ availableFrom: { watch: true, manual: true },
282
+ },
283
+ source: {
284
+ key: 'source',
285
+ label: 'Source',
286
+ category: MetricCategory.PERFORMANCE,
287
+ description: 'Data source',
288
+ isPerSession: true,
289
+ availableFrom: { watch: false, manual: false },
290
+ },
291
+ lapTurnTimes: {
292
+ key: 'lapTurnTimes',
293
+ label: 'Turn Times',
294
+ unit: 's',
295
+ category: MetricCategory.TECHNIQUE,
296
+ description: 'Turn times per lap',
297
+ isPerLap: true,
298
+ availableFrom: { watch: true, manual: false },
299
+ },
300
+ lapStrokeTypes: {
301
+ key: 'lapStrokeTypes',
302
+ label: 'Stroke Types',
303
+ category: MetricCategory.TECHNIQUE,
304
+ description: 'Stroke types per lap',
305
+ isPerLap: true,
306
+ availableFrom: { watch: true, manual: false },
307
+ },
308
+ lapPaces: {
309
+ key: 'lapPaces',
310
+ label: 'Lap Paces',
311
+ unit: '/100m',
312
+ category: MetricCategory.PERFORMANCE,
313
+ description: 'Pace per lap',
314
+ isPerLap: true,
315
+ availableFrom: { watch: true, manual: false },
316
+ },
317
+ lapCalories: {
318
+ key: 'lapCalories',
319
+ label: 'Lap Calories',
320
+ unit: 'kcal',
321
+ category: MetricCategory.ENERGY,
322
+ description: 'Calories per lap',
323
+ isPerLap: true,
324
+ availableFrom: { watch: true, manual: false },
325
+ },
326
+ lapDistances: {
327
+ key: 'lapDistances',
328
+ label: 'Lap Distances',
329
+ unit: 'm',
330
+ category: MetricCategory.PERFORMANCE,
331
+ description: 'Distance per lap',
332
+ isPerLap: true,
333
+ availableFrom: { watch: true, manual: false },
334
+ },
335
+ lapSwolfScores: {
336
+ key: 'lapSwolfScores',
337
+ label: 'Lap SWOLF',
338
+ category: MetricCategory.TECHNIQUE,
339
+ description: 'SWOLF per lap',
340
+ isPerLap: true,
341
+ availableFrom: { watch: true, manual: false },
342
+ },
343
+ lapStrokeRates: {
344
+ key: 'lapStrokeRates',
345
+ label: 'Lap Stroke Rate',
346
+ unit: 'SPM',
347
+ category: MetricCategory.TECHNIQUE,
348
+ description: 'Strokes per minute per lap',
349
+ icon: 'repeat-outline',
350
+ isPerLap: true,
351
+ formula: 'Lap Strokes ÷ (Lap Time in Minutes)',
352
+ availableFrom: { watch: true, manual: false },
353
+ },
354
+ lapDistancesPerStroke: {
355
+ key: 'lapDistancesPerStroke',
356
+ label: 'Lap Distance/Stroke',
357
+ unit: 'm',
358
+ category: MetricCategory.TECHNIQUE,
359
+ description: 'Distance covered per stroke per lap',
360
+ icon: 'arrow-forward-outline',
361
+ isPerLap: true,
362
+ formula: 'Lap Distance ÷ Lap Strokes',
363
+ availableFrom: { watch: true, manual: false },
364
+ },
365
+ poolLocation: {
366
+ key: 'poolLocation',
367
+ label: 'Pool Location',
368
+ category: MetricCategory.PERFORMANCE,
369
+ description: 'Pool location details',
370
+ isPerSession: true,
371
+ availableFrom: { watch: false, manual: true },
372
+ },
373
+ notes: {
374
+ key: 'notes',
375
+ label: 'Notes',
376
+ category: MetricCategory.PERFORMANCE,
377
+ description: 'Session notes',
378
+ isPerSession: true,
379
+ availableFrom: { watch: false, manual: true },
380
+ },
381
+ trainingTypes: {
382
+ key: 'trainingTypes',
383
+ label: 'Training Types',
384
+ category: MetricCategory.PERFORMANCE,
385
+ description: 'Training types',
386
+ isPerSession: true,
387
+ availableFrom: { watch: false, manual: true },
388
+ },
389
+ perceivedEffort: {
390
+ key: 'perceivedEffort',
391
+ label: 'Perceived Effort',
392
+ category: MetricCategory.PERFORMANCE,
393
+ description: 'Effort rating',
394
+ isPerSession: true,
395
+ availableFrom: { watch: false, manual: true },
396
+ },
397
+ equipment: {
398
+ key: 'equipment',
399
+ label: 'Equipment',
400
+ category: MetricCategory.PERFORMANCE,
401
+ description: 'Equipment used',
402
+ isPerSession: true,
403
+ availableFrom: { watch: false, manual: true },
404
+ },
405
+ };
406
+ exports.MetricsHelper = {
407
+ getMetric: (key) => exports.METRICS_REGISTRY[key],
408
+ getSessionMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerSession),
409
+ getLapMetrics: () => Object.values(exports.METRICS_REGISTRY).filter((m) => m.isPerLap),
410
+ getMetricsByCategory: (category) => Object.values(exports.METRICS_REGISTRY).filter((m) => m.category === category),
411
+ getAllMetricKeys: () => Object.keys(exports.METRICS_REGISTRY),
412
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@swimedge/metrics",
3
+ "version": "1.0.0",
4
+ "description": "Shared metrics registry for SwimEdge (frontend, backend, watch)",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "private": false,
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "prepublishOnly": "npm run build",
17
+ "build": "tsc",
18
+ "watch": "tsc --watch",
19
+ "export-json": "node scripts/export-json.js"
20
+ },
21
+ "keywords": [
22
+ "metrics",
23
+ "swimming",
24
+ "shared"
25
+ ],
26
+ "author": "SwimEdge",
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "esbuild-register": "^3.6.0",
33
+ "typescript": "^5.0.0"
34
+ }
35
+ }