incyclist-services 1.7.71 → 1.7.72

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.
@@ -1362,8 +1362,15 @@ let DeviceRideService = (() => {
1362
1362
  canEnforceSimulator() {
1363
1363
  const selected = (0, routes_1.getRouteList)().getSelected();
1364
1364
  const personalApiKey = this.getGoogleMaps().hasPersonalApiKey();
1365
+ if (!selected?.description)
1366
+ return false;
1365
1367
  if (selected?.description?.hasVideo || personalApiKey)
1366
1368
  return true;
1369
+ if (!selected.description.hasVideo) {
1370
+ const rideView = (0, settings_1.useUserSettings)().getValue('preferences.rideView', null);
1371
+ if (rideView === 'map')
1372
+ return true;
1373
+ }
1367
1374
  return (0, settings_1.useUserSettings)().isNewUser();
1368
1375
  }
1369
1376
  logEvent(event) {
@@ -377,7 +377,7 @@ let RideDisplayService = (() => {
377
377
  return { ...props, ...childProps };
378
378
  }
379
379
  catch (err) {
380
- this.logError(err, 'getDisplayProperties');
380
+ this.logError(err, 'getDisplayProperties', { state: this.state });
381
381
  return { state: this.state };
382
382
  }
383
383
  }
@@ -425,7 +425,7 @@ let RideDisplayService = (() => {
425
425
  async stopRide(props = {}) {
426
426
  try {
427
427
  if (this.state !== 'Starting' && this.state !== 'Idle') {
428
- this.logEvent({ message: "activity stopped", activity: this.activity?.id });
428
+ this.logEvent({ message: "activity stopped", activity: this.activity?.id, lastState: this.state });
429
429
  }
430
430
  const prevState = this.state;
431
431
  this.state = prevState === 'Starting' ? 'Closing' : 'Finished';
@@ -449,7 +449,6 @@ let RideDisplayService = (() => {
449
449
  }
450
450
  if (prevState !== 'Starting')
451
451
  this.enableScreensaver();
452
- this.state = 'Idle';
453
452
  }
454
453
  catch (err) {
455
454
  this.logError(err, 'stopRide');
@@ -458,9 +457,10 @@ let RideDisplayService = (() => {
458
457
  }
459
458
  async closePrevRide() {
460
459
  try {
461
- if (this.observer || (this.state !== 'Idle')) {
460
+ if (this.observer || (this.state !== 'Idle' && this.state !== 'Finished')) {
462
461
  await this.stopRide({ noStateUpdates: true });
463
462
  }
463
+ this.state = 'Idle';
464
464
  }
465
465
  catch (err) {
466
466
  this.logError(err, 'init');
@@ -196,6 +196,12 @@ let RidePageService = (() => {
196
196
  }
197
197
  onMenuClose() {
198
198
  try {
199
+ const state = this.getRideDisplay().getState();
200
+ if (state === 'Finished' || this.menuProps.finished) {
201
+ this.moveToPreviousPage();
202
+ this.closePage();
203
+ return;
204
+ }
199
205
  this.menuProps = null;
200
206
  this.updatePageDisplay();
201
207
  }
@@ -223,6 +229,15 @@ let RidePageService = (() => {
223
229
  this.logError(err, 'onResume');
224
230
  }
225
231
  }
232
+ onFinished() {
233
+ try {
234
+ this.menuProps = { showResume: false, finished: true };
235
+ this.updatePageDisplay();
236
+ }
237
+ catch (err) {
238
+ this.logError(err, 'onResume');
239
+ }
240
+ }
226
241
  onEndRide() {
227
242
  try {
228
243
  this.getRideDisplay().stop();
@@ -336,6 +351,9 @@ let RidePageService = (() => {
336
351
  case 'Paused':
337
352
  this.menuProps = { showResume: true };
338
353
  break;
354
+ case 'Finished':
355
+ this.menuProps = { showResume: false, finished: true };
356
+ break;
339
357
  case 'Active':
340
358
  this.menuProps = null;
341
359
  break;
@@ -276,7 +276,7 @@ let RouteDisplayService = (() => {
276
276
  const mode = this.getDeviceRide().getCyclingMode();
277
277
  const isSIM = mode?.isSIM();
278
278
  const realityFactor = this.startSettings?.realityFactor ?? 100;
279
- const targetSlope = (this.position.slope ?? 0) * realityFactor / 100;
279
+ const targetSlope = (this.position?.slope ?? 0) * realityFactor / 100;
280
280
  if (props?.limits && !isSIM) {
281
281
  delete this.prevRequestSlope;
282
282
  return { ...props?.limits, slope: targetSlope };
@@ -0,0 +1,123 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ import { Injectable } from '../../base/decorators/Injection';
36
+ import { IncyclistService } from '../../base/service';
37
+ import { Singleton } from '../../base/types';
38
+ import { TrainingPeaksAppConnection } from '../../apps/trainingpeaks/TrainingPeaksAppConnection';
39
+ import { ActivityUploadFactory } from './factory';
40
+ let TrainingPeaksUpload = (() => {
41
+ let _classDecorators = [Singleton];
42
+ let _classDescriptor;
43
+ let _classExtraInitializers = [];
44
+ let _classThis;
45
+ let _classSuper = IncyclistService;
46
+ let _instanceExtraInitializers = [];
47
+ let _getConnection_decorators;
48
+ var TrainingPeaksUpload = class extends _classSuper {
49
+ static { _classThis = this; }
50
+ static {
51
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
52
+ _getConnection_decorators = [Injectable];
53
+ __esDecorate(this, null, _getConnection_decorators, { kind: "method", name: "getConnection", static: false, private: false, access: { has: obj => "getConnection" in obj, get: obj => obj.getConnection }, metadata: _metadata }, null, _instanceExtraInitializers);
54
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
55
+ TrainingPeaksUpload = _classThis = _classDescriptor.value;
56
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
57
+ __runInitializers(_classThis, _classExtraInitializers);
58
+ }
59
+ isInitialized = __runInitializers(this, _instanceExtraInitializers);
60
+ connection = new TrainingPeaksAppConnection();
61
+ constructor() {
62
+ super('TrainingPeaksUpload');
63
+ this.isInitialized = false;
64
+ this.init();
65
+ }
66
+ init() {
67
+ if (this.isInitialized)
68
+ return true;
69
+ this.isInitialized = this.getConnection().init();
70
+ return this.isInitialized;
71
+ }
72
+ isConnected() {
73
+ return this.getConnection().isConnected();
74
+ }
75
+ async upload(activity, format = 'TCX') {
76
+ try {
77
+ const ok = this.ensureInitialized();
78
+ if (!ok) {
79
+ this.logEvent({ message: 'TrainingPeaks upload skipped', reason: 'not initialized' });
80
+ return false;
81
+ }
82
+ if (!this.isConnected())
83
+ return false;
84
+ const lcFormat = format.toLowerCase();
85
+ const ucFormat = format.toUpperCase();
86
+ this.logEvent({ message: 'TrainingPeaks upload', format: ucFormat });
87
+ if (!activity.links)
88
+ activity.links = {};
89
+ const fileName = activity[`${lcFormat}FileName`];
90
+ const res = await this.getApi().upload(fileName, { name: activity.title, description: '' });
91
+ this.logEvent({ message: 'TrainingPeaks upload success', activityId: res.trainingPeaksId });
92
+ activity.links.trainingpeaks = {
93
+ activity_id: String(res.trainingPeaksId),
94
+ url: this.getUrl(res.trainingPeaksId)
95
+ };
96
+ return true;
97
+ }
98
+ catch (err) {
99
+ this.logEvent({ message: 'TrainingPeaks upload failure', error: err.message });
100
+ activity.links.trainingpeaks = { error: err.message };
101
+ return false;
102
+ }
103
+ }
104
+ getUrl(activityId) {
105
+ return `https://www.trainingpeaks.com/athlete/workouts/${activityId}`;
106
+ }
107
+ ensureInitialized() {
108
+ if (!this.isInitialized)
109
+ this.init();
110
+ return this.isInitialized;
111
+ }
112
+ getConnection() {
113
+ return this.connection;
114
+ }
115
+ getApi() {
116
+ return this.getConnection().getApi();
117
+ }
118
+ };
119
+ return TrainingPeaksUpload = _classThis;
120
+ })();
121
+ export { TrainingPeaksUpload };
122
+ const factory = new ActivityUploadFactory();
123
+ factory.add('trainingpeaks', new TrainingPeaksUpload());
@@ -0,0 +1,143 @@
1
+ import { EventLogger } from 'gd-eventlog';
2
+ import { Observer } from '../../../../base/types/observer';
3
+ import { valid } from '../../../../utils/valid';
4
+ import { sleep } from '../../../../utils/sleep';
5
+ import { AppApiBase } from '../base';
6
+ const UPLOAD_POLL_INTERVAL_MS = 2000;
7
+ const UPLOAD_TIMEOUT_MS = 60000;
8
+ export class TrainingPeaksApi extends AppApiBase {
9
+ config;
10
+ logger;
11
+ observer;
12
+ constructor() {
13
+ super();
14
+ this.logger = new EventLogger('TrainingPeaksApi');
15
+ this.observer = new Observer();
16
+ }
17
+ getBaseUrl() {
18
+ return process.env.TRAININGPEAKS_API || 'https://api.sandbox.trainingpeaks.com';
19
+ }
20
+ getOauthBaseUrl() {
21
+ return process.env.TRAININGPEAKS_OAUTH_URL || 'https://oauth.sandbox.trainingpeaks.com';
22
+ }
23
+ init(config) {
24
+ this.config = config;
25
+ return this.observer;
26
+ }
27
+ update(config) {
28
+ if (!this.config)
29
+ throw new Error('update not allowed before init');
30
+ this.config = { ...this.config, ...config };
31
+ }
32
+ isAuthenticated() {
33
+ return valid(this.config?.accessToken);
34
+ }
35
+ async getLoggedInAthlete() {
36
+ await this.verifyAuthentication();
37
+ const response = await this.get('/v1/athlete/profile');
38
+ return response.data;
39
+ }
40
+ async getCalendarWorkouts(opts) {
41
+ await this.verifyAuthentication();
42
+ const oldest = opts?.oldest ?? new Date();
43
+ const newest = opts?.newest ?? new Date(oldest.valueOf() + 30 * 24 * 60 * 60 * 1000);
44
+ const fmt = (d) => d.toISOString().slice(0, 10);
45
+ const response = await this.get(`/v2/workouts/${fmt(oldest)}/${fmt(newest)}`);
46
+ return response.data;
47
+ }
48
+ async getWorkoutFile(id, format = 'zwo') {
49
+ await this.verifyAuthentication();
50
+ const response = await this.get(`/v2/workouts/wod/file/${id}?format=${format}`);
51
+ return response.data;
52
+ }
53
+ async upload(fileName, props) {
54
+ await this.verifyAuthentication();
55
+ const reqOpts = {
56
+ headers: { Authorization: `bearer ${this.config.accessToken}` }
57
+ };
58
+ const form = await this.createForm('/v3/file', { file: { type: 'file', fileName } }, reqOpts);
59
+ const response = await this.postForm(form);
60
+ const location = response.headers?.['location'] ?? '';
61
+ const trackingId = location.split('/').filter(Boolean).pop();
62
+ if (!trackingId) {
63
+ throw new Error('TrainingPeaks upload failed: no tracking ID returned');
64
+ }
65
+ return this.waitForUploadComplete(trackingId, props?.externalId);
66
+ }
67
+ async getUploadStatus(trackingId) {
68
+ await this.verifyAuthentication();
69
+ const response = await this.get(`/v3/status/${trackingId}`);
70
+ return response.data;
71
+ }
72
+ async verifyAuthentication() {
73
+ if (!this.isAuthenticated())
74
+ throw new Error('not authenticated');
75
+ await this.verifyToken();
76
+ }
77
+ async verifyToken() {
78
+ if (!this.isTokenStillValid()) {
79
+ try {
80
+ await this.refreshToken();
81
+ }
82
+ catch {
83
+ throw new Error('token refresh failed');
84
+ }
85
+ }
86
+ }
87
+ isTokenStillValid() {
88
+ if (!this.config?.expiration)
89
+ return false;
90
+ return Date.now() < this.config.expiration.valueOf();
91
+ }
92
+ async refreshToken() {
93
+ this.logger.logEvent({ message: 'TrainingPeaks refresh token start' });
94
+ try {
95
+ const response = await this.getApi().request({
96
+ method: 'post',
97
+ url: `${this.getOauthBaseUrl()}/oauth/token`,
98
+ data: new URLSearchParams({
99
+ grant_type: 'refresh_token',
100
+ refresh_token: this.config.refreshToken,
101
+ client_id: this.config.clientId,
102
+ client_secret: this.config.clientSecret,
103
+ }).toString(),
104
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
105
+ });
106
+ const data = response.data;
107
+ this.config.accessToken = data.access_token;
108
+ this.config.refreshToken = data.refresh_token;
109
+ this.config.expiration = new Date(Date.now() + data.expires_in * 1000);
110
+ this.logger.logEvent({ message: 'TrainingPeaks refresh token success' });
111
+ this.observer.emit('token.updated', this.config);
112
+ }
113
+ catch (err) {
114
+ this.logger.logEvent({ message: 'TrainingPeaks refresh token error', error: err.message });
115
+ throw err;
116
+ }
117
+ }
118
+ async waitForUploadComplete(trackingId, externalId) {
119
+ const deadline = Date.now() + UPLOAD_TIMEOUT_MS;
120
+ while (Date.now() < deadline) {
121
+ const status = await this.getUploadStatus(trackingId);
122
+ if (status.Completed) {
123
+ if (!status.WorkoutIds?.length) {
124
+ throw new Error(`TrainingPeaks upload failed: ${status.Status}`);
125
+ }
126
+ return { trainingPeaksId: status.WorkoutIds[0], externalId };
127
+ }
128
+ await sleep(UPLOAD_POLL_INTERVAL_MS);
129
+ }
130
+ throw new Error('TrainingPeaks upload timed out');
131
+ }
132
+ async get(url, config) {
133
+ const props = config ?? {};
134
+ const response = await this.getApi().request({
135
+ method: 'get',
136
+ url: this.getBaseUrl() + url,
137
+ headers: { Authorization: `bearer ${this.config.accessToken}` },
138
+ validateStatus: (status) => (status >= 200 && status < 300) || status === 403,
139
+ ...props
140
+ });
141
+ return response;
142
+ }
143
+ }
@@ -0,0 +1 @@
1
+ export { TrainingPeaksApi } from './api';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,189 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ import { getBindings } from '../../api';
36
+ import { Injectable, Singleton } from '../../base/decorators';
37
+ import { valid } from '../../utils/valid';
38
+ import { TrainingPeaksApi } from '../base/api/trainingpeaks';
39
+ import { ConnectedAppService } from '../base/app';
40
+ let TrainingPeaksAppConnection = (() => {
41
+ let _classDecorators = [Singleton];
42
+ let _classDescriptor;
43
+ let _classExtraInitializers = [];
44
+ let _classThis;
45
+ let _classSuper = ConnectedAppService;
46
+ let _instanceExtraInitializers = [];
47
+ let _getSecretBindings_decorators;
48
+ var TrainingPeaksAppConnection = class extends _classSuper {
49
+ static { _classThis = this; }
50
+ static {
51
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
52
+ _getSecretBindings_decorators = [Injectable];
53
+ __esDecorate(this, null, _getSecretBindings_decorators, { kind: "method", name: "getSecretBindings", static: false, private: false, access: { has: obj => "getSecretBindings" in obj, get: obj => obj.getSecretBindings }, metadata: _metadata }, null, _instanceExtraInitializers);
54
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
55
+ TrainingPeaksAppConnection = _classThis = _classDescriptor.value;
56
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
57
+ __runInitializers(_classThis, _classExtraInitializers);
58
+ }
59
+ config = __runInitializers(this, _instanceExtraInitializers);
60
+ api;
61
+ tokenUpdateHandler = this.updateConfig.bind(this);
62
+ constructor() {
63
+ super('TrainingPeaksAppConnection', 'trainingpeaks');
64
+ }
65
+ async connect(credentials) {
66
+ this.ensureInitialized();
67
+ this.logEvent({ message: 'Connect with TrainingPeaks' });
68
+ try {
69
+ const isConnected = this.isConnected();
70
+ const config = this.buildConfigFromCredentials(credentials);
71
+ if (isConnected) {
72
+ this.getApi().update(config);
73
+ }
74
+ else {
75
+ this.initApi(config);
76
+ this.saveCredentials(config);
77
+ }
78
+ this.logEvent({ message: 'Connect with TrainingPeaks success' });
79
+ return true;
80
+ }
81
+ catch (err) {
82
+ this.logError(err, 'connect');
83
+ throw err;
84
+ }
85
+ }
86
+ isConnecting() {
87
+ return false;
88
+ }
89
+ disconnect() {
90
+ try {
91
+ this.config = undefined;
92
+ this.getApi().init(undefined);
93
+ this.saveCredentials();
94
+ }
95
+ catch (err) {
96
+ this.logError(err, 'disconnect');
97
+ }
98
+ }
99
+ isConnected() {
100
+ if (!this.isInitialized) {
101
+ this.init();
102
+ }
103
+ return valid(this.config);
104
+ }
105
+ getCredentials() {
106
+ try {
107
+ return this.getUserSettings().get('user.auth.trainingpeaks', null);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ getApi() {
114
+ if (!this.api)
115
+ this.api = new TrainingPeaksApi();
116
+ return this.api;
117
+ }
118
+ initAuth() {
119
+ let isInitialized = false;
120
+ try {
121
+ const auth = this.getCredentials();
122
+ const clientId = this.getSecret('TRAININGPEAKS_CLIENT_ID');
123
+ const clientSecret = this.getSecret('TRAININGPEAKS_CLIENT_SECRET');
124
+ if (clientId && clientSecret && auth) {
125
+ this.logEvent({ message: 'TrainingPeaks init done', hasCredentials: true });
126
+ this.config = this.buildConfigFromCredentials(auth);
127
+ this.initApi(this.config);
128
+ }
129
+ else {
130
+ this.logEvent({ message: 'TrainingPeaks init done', hasCredentials: false });
131
+ }
132
+ isInitialized = true;
133
+ }
134
+ catch (err) {
135
+ this.logEvent({ message: 'error', error: err.message, fn: 'init', stack: err.stack });
136
+ isInitialized = false;
137
+ delete this.config;
138
+ }
139
+ return isInitialized;
140
+ }
141
+ updateConfig(config) {
142
+ const { accessToken, refreshToken, expiration } = config;
143
+ this.config = { ...this.config, accessToken, refreshToken, expiration };
144
+ this.saveCredentials();
145
+ }
146
+ saveCredentials(config) {
147
+ if (config)
148
+ this.config = config;
149
+ try {
150
+ if (!this.isConnected()) {
151
+ this.logEvent({ message: 'TrainingPeaks delete credentials' });
152
+ this.getUserSettings().set('user.auth.trainingpeaks', null);
153
+ return;
154
+ }
155
+ this.logEvent({ message: 'TrainingPeaks save credentials' });
156
+ this.getUserSettings().set('user.auth.trainingpeaks', {
157
+ accesstoken: this.config.accessToken,
158
+ refreshtoken: this.config.refreshToken,
159
+ expiration: this.config.expiration?.valueOf().toString()
160
+ });
161
+ }
162
+ catch (err) {
163
+ this.logEvent({ message: 'error', fn: 'saveCredentials', error: err.message, stack: err.stack });
164
+ }
165
+ this.logEvent({ message: 'TrainingPeaks save credentials done' });
166
+ }
167
+ buildConfigFromCredentials(credentials) {
168
+ return {
169
+ clientId: this.getSecret('TRAININGPEAKS_CLIENT_ID'),
170
+ clientSecret: this.getSecret('TRAININGPEAKS_CLIENT_SECRET'),
171
+ accessToken: credentials?.accesstoken,
172
+ refreshToken: credentials?.refreshtoken,
173
+ expiration: credentials?.expiration ? new Date(Number(credentials.expiration)) : undefined
174
+ };
175
+ }
176
+ initApi(config) {
177
+ const observer = this.getApi().init(config);
178
+ observer.on('token.updated', this.tokenUpdateHandler);
179
+ }
180
+ getSecret(key) {
181
+ return this.getSecretBindings()?.getSecret(key);
182
+ }
183
+ getSecretBindings() {
184
+ return getBindings()?.secret;
185
+ }
186
+ };
187
+ return TrainingPeaksAppConnection = _classThis;
188
+ })();
189
+ export { TrainingPeaksAppConnection };
@@ -0,0 +1 @@
1
+ export {};
@@ -1356,8 +1356,15 @@ let DeviceRideService = (() => {
1356
1356
  canEnforceSimulator() {
1357
1357
  const selected = getRouteList().getSelected();
1358
1358
  const personalApiKey = this.getGoogleMaps().hasPersonalApiKey();
1359
+ if (!selected?.description)
1360
+ return false;
1359
1361
  if (selected?.description?.hasVideo || personalApiKey)
1360
1362
  return true;
1363
+ if (!selected.description.hasVideo) {
1364
+ const rideView = useUserSettings().getValue('preferences.rideView', null);
1365
+ if (rideView === 'map')
1366
+ return true;
1367
+ }
1361
1368
  return useUserSettings().isNewUser();
1362
1369
  }
1363
1370
  logEvent(event) {
@@ -374,7 +374,7 @@ let RideDisplayService = (() => {
374
374
  return { ...props, ...childProps };
375
375
  }
376
376
  catch (err) {
377
- this.logError(err, 'getDisplayProperties');
377
+ this.logError(err, 'getDisplayProperties', { state: this.state });
378
378
  return { state: this.state };
379
379
  }
380
380
  }
@@ -422,7 +422,7 @@ let RideDisplayService = (() => {
422
422
  async stopRide(props = {}) {
423
423
  try {
424
424
  if (this.state !== 'Starting' && this.state !== 'Idle') {
425
- this.logEvent({ message: "activity stopped", activity: this.activity?.id });
425
+ this.logEvent({ message: "activity stopped", activity: this.activity?.id, lastState: this.state });
426
426
  }
427
427
  const prevState = this.state;
428
428
  this.state = prevState === 'Starting' ? 'Closing' : 'Finished';
@@ -446,7 +446,6 @@ let RideDisplayService = (() => {
446
446
  }
447
447
  if (prevState !== 'Starting')
448
448
  this.enableScreensaver();
449
- this.state = 'Idle';
450
449
  }
451
450
  catch (err) {
452
451
  this.logError(err, 'stopRide');
@@ -455,9 +454,10 @@ let RideDisplayService = (() => {
455
454
  }
456
455
  async closePrevRide() {
457
456
  try {
458
- if (this.observer || (this.state !== 'Idle')) {
457
+ if (this.observer || (this.state !== 'Idle' && this.state !== 'Finished')) {
459
458
  await this.stopRide({ noStateUpdates: true });
460
459
  }
460
+ this.state = 'Idle';
461
461
  }
462
462
  catch (err) {
463
463
  this.logError(err, 'init');
@@ -193,6 +193,12 @@ let RidePageService = (() => {
193
193
  }
194
194
  onMenuClose() {
195
195
  try {
196
+ const state = this.getRideDisplay().getState();
197
+ if (state === 'Finished' || this.menuProps.finished) {
198
+ this.moveToPreviousPage();
199
+ this.closePage();
200
+ return;
201
+ }
196
202
  this.menuProps = null;
197
203
  this.updatePageDisplay();
198
204
  }
@@ -220,6 +226,15 @@ let RidePageService = (() => {
220
226
  this.logError(err, 'onResume');
221
227
  }
222
228
  }
229
+ onFinished() {
230
+ try {
231
+ this.menuProps = { showResume: false, finished: true };
232
+ this.updatePageDisplay();
233
+ }
234
+ catch (err) {
235
+ this.logError(err, 'onResume');
236
+ }
237
+ }
223
238
  onEndRide() {
224
239
  try {
225
240
  this.getRideDisplay().stop();
@@ -333,6 +348,9 @@ let RidePageService = (() => {
333
348
  case 'Paused':
334
349
  this.menuProps = { showResume: true };
335
350
  break;
351
+ case 'Finished':
352
+ this.menuProps = { showResume: false, finished: true };
353
+ break;
336
354
  case 'Active':
337
355
  this.menuProps = null;
338
356
  break;
@@ -270,7 +270,7 @@ let RouteDisplayService = (() => {
270
270
  const mode = this.getDeviceRide().getCyclingMode();
271
271
  const isSIM = mode?.isSIM();
272
272
  const realityFactor = this.startSettings?.realityFactor ?? 100;
273
- const targetSlope = (this.position.slope ?? 0) * realityFactor / 100;
273
+ const targetSlope = (this.position?.slope ?? 0) * realityFactor / 100;
274
274
  if (props?.limits && !isSIM) {
275
275
  delete this.prevRequestSlope;
276
276
  return { ...props?.limits, slope: targetSlope };
@@ -0,0 +1,201 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ import { useAppsService } from '../../../../apps';
36
+ import { TrainingPeaksAppConnection } from '../../../../apps/trainingpeaks/TrainingPeaksAppConnection';
37
+ import { Injectable } from '../../../../base/decorators';
38
+ import { IncyclistService } from '../../../../base/service';
39
+ import { Observer } from '../../../../base/types';
40
+ import { getFirstDayOfCurrentWeek, waitNextTick } from '../../../../utils';
41
+ import { ZwoParser } from '../../../base/parsers/zwo/zwo';
42
+ import { WorkoutSyncFactory } from '../factory';
43
+ const SYNC_DAYS = 30;
44
+ let TrainingPeaksCalendarSyncProvider = (() => {
45
+ let _classSuper = IncyclistService;
46
+ let _instanceExtraInitializers = [];
47
+ let _getApi_decorators;
48
+ let _getConnection_decorators;
49
+ let _getAppsService_decorators;
50
+ return class TrainingPeaksCalendarSyncProvider extends _classSuper {
51
+ static {
52
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
53
+ _getApi_decorators = [Injectable];
54
+ _getConnection_decorators = [Injectable];
55
+ _getAppsService_decorators = [Injectable];
56
+ __esDecorate(this, null, _getApi_decorators, { kind: "method", name: "getApi", static: false, private: false, access: { has: obj => "getApi" in obj, get: obj => obj.getApi }, metadata: _metadata }, null, _instanceExtraInitializers);
57
+ __esDecorate(this, null, _getConnection_decorators, { kind: "method", name: "getConnection", static: false, private: false, access: { has: obj => "getConnection" in obj, get: obj => obj.getConnection }, metadata: _metadata }, null, _instanceExtraInitializers);
58
+ __esDecorate(this, null, _getAppsService_decorators, { kind: "method", name: "getAppsService", static: false, private: false, access: { has: obj => "getAppsService" in obj, get: obj => obj.getAppsService }, metadata: _metadata }, null, _instanceExtraInitializers);
59
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
60
+ }
61
+ lastSyncTS = __runInitializers(this, _instanceExtraInitializers);
62
+ observer;
63
+ stopRequested;
64
+ workouts;
65
+ constructor() {
66
+ super('TrainingPeaksCalendarSync');
67
+ this.lastSyncTS = 0;
68
+ this.workouts = [];
69
+ }
70
+ sync() {
71
+ if (this.observer)
72
+ return this.observer;
73
+ this.observer = new Observer();
74
+ const onDone = () => {
75
+ waitNextTick().then(() => {
76
+ this.stopRequested = false;
77
+ delete this.observer;
78
+ });
79
+ };
80
+ this.observer.once('done', onDone);
81
+ this.observer.once('loaded', onDone);
82
+ this.observer.once('stop', () => { this.stopSync(); });
83
+ this.loadWorkouts();
84
+ return this.observer;
85
+ }
86
+ isConnected() {
87
+ try {
88
+ if (this.getApi().isAuthenticated())
89
+ return true;
90
+ return this.getConnection().isConnected();
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ reset() {
97
+ this.lastSyncTS = 0;
98
+ this.workouts = [];
99
+ }
100
+ stopSync() {
101
+ this.stopRequested = true;
102
+ }
103
+ async loadWorkouts() {
104
+ const isFirst = this.lastSyncTS === 0;
105
+ const event = isFirst ? 'loaded' : 'done';
106
+ try {
107
+ if (!this.isConnected()) {
108
+ this.observer.emit(event, [], 'trainingpeaks');
109
+ return [];
110
+ }
111
+ if (!this.getAppsService().isEnabled('trainingpeaks', 'WorkoutDownload')) {
112
+ await waitNextTick();
113
+ this.observer.emit(event, [], 'trainingpeaks');
114
+ return [];
115
+ }
116
+ const oldest = getFirstDayOfCurrentWeek();
117
+ const newest = new Date(oldest.valueOf() + SYNC_DAYS * 24 * 60 * 60 * 1000);
118
+ const all = await this.getApi().getCalendarWorkouts({ oldest, newest }) ?? [];
119
+ const bike = all.filter(w => w.WorkoutType === 'Bike');
120
+ await this.parseWorkouts(bike);
121
+ this.observer.emit(event, this.workouts, 'trainingpeaks');
122
+ this.lastSyncTS = Date.now();
123
+ return this.workouts;
124
+ }
125
+ catch {
126
+ this.observer.emit(event, [], 'trainingpeaks');
127
+ return [];
128
+ }
129
+ }
130
+ async parseWorkouts(workouts) {
131
+ const zwoParser = new ZwoParser();
132
+ const promises = [];
133
+ const transform = async (w) => {
134
+ try {
135
+ const day = new Date(w.WorkoutDay);
136
+ const updated = w.LastModifiedDate ? new Date(w.LastModifiedDate) : undefined;
137
+ if (!w.WorkoutFileFormats?.includes('zwo')) {
138
+ this.logEvent({
139
+ message: 'workout skipped - no ZWO format available',
140
+ workoutId: w.Id,
141
+ title: w.Title,
142
+ availableFormats: w.WorkoutFileFormats ?? []
143
+ });
144
+ return;
145
+ }
146
+ const fileContent = await this.getApi().getWorkoutFile(w.Id, 'zwo');
147
+ const workout = await zwoParser.fromStr(fileContent, w.Title);
148
+ const entry = {
149
+ day,
150
+ updated,
151
+ workoutId: String(w.Id),
152
+ workout
153
+ };
154
+ const existingIdx = this.workouts.findIndex(e => e.workoutId === String(w.Id));
155
+ const isExisting = existingIdx !== -1;
156
+ if (this.lastSyncTS > 0) {
157
+ if (!isExisting) {
158
+ this.observer.emit('added', entry, 'trainingpeaks');
159
+ }
160
+ else if (updated && updated.valueOf() > this.lastSyncTS) {
161
+ this.observer.emit('updated', entry, 'trainingpeaks');
162
+ }
163
+ }
164
+ if (isExisting) {
165
+ this.workouts[existingIdx] = entry;
166
+ }
167
+ else {
168
+ this.workouts.push(entry);
169
+ }
170
+ }
171
+ catch (err) {
172
+ this.logEvent({ message: 'error parsing workout', error: err.message, workoutId: w.Id, title: w.Title });
173
+ }
174
+ };
175
+ if (this.lastSyncTS > 0) {
176
+ const deleted = this.workouts.filter(e => !workouts.some(w => String(w.Id) === e.workoutId));
177
+ for (const e of deleted) {
178
+ this.observer.emit('deleted', e, 'trainingpeaks');
179
+ }
180
+ this.workouts = this.workouts.filter(e => workouts.some(w => String(w.Id) === e.workoutId));
181
+ }
182
+ for (const w of workouts) {
183
+ promises.push(transform(w));
184
+ }
185
+ await Promise.all(promises);
186
+ return this.workouts;
187
+ }
188
+ getApi() {
189
+ return this.getConnection().getApi();
190
+ }
191
+ getConnection() {
192
+ return new TrainingPeaksAppConnection();
193
+ }
194
+ getAppsService() {
195
+ return useAppsService();
196
+ }
197
+ };
198
+ })();
199
+ export { TrainingPeaksCalendarSyncProvider };
200
+ const factory = new WorkoutSyncFactory();
201
+ factory.add('trainingpeaks', new TrainingPeaksCalendarSyncProvider());
@@ -0,0 +1,16 @@
1
+ import { IncyclistService } from '../../base/service';
2
+ import { TrainingPeaksAppConnection } from '../../apps/trainingpeaks/TrainingPeaksAppConnection';
3
+ import { ActivityDetails } from '../base';
4
+ import { IActivityUpload } from './types';
5
+ export declare class TrainingPeaksUpload extends IncyclistService implements IActivityUpload {
6
+ protected isInitialized: boolean;
7
+ protected connection: TrainingPeaksAppConnection;
8
+ constructor();
9
+ init(): boolean;
10
+ isConnected(): boolean;
11
+ upload(activity: ActivityDetails, format?: string): Promise<boolean>;
12
+ getUrl(activityId: number | string): string;
13
+ protected ensureInitialized(): boolean;
14
+ protected getConnection(): TrainingPeaksAppConnection;
15
+ protected getApi(): import("../../apps/base/api/trainingpeaks").TrainingPeaksApi;
16
+ }
@@ -0,0 +1,30 @@
1
+ import { AxiosResponse } from 'axios';
2
+ import { EventLogger } from 'gd-eventlog';
3
+ import { Observer } from '../../../../base/types/observer';
4
+ import { AppApiBase } from '../base';
5
+ import { TrainingPeaksAthlete, TrainingPeaksConfig, TrainingPeaksUploadProps, TrainingPeaksUploadResult, TrainingPeaksUploadStatus, TrainingPeaksWorkout } from './types';
6
+ export declare class TrainingPeaksApi extends AppApiBase {
7
+ protected config: TrainingPeaksConfig;
8
+ protected logger: EventLogger;
9
+ protected observer: Observer;
10
+ constructor();
11
+ protected getBaseUrl(): string;
12
+ protected getOauthBaseUrl(): string;
13
+ init(config: TrainingPeaksConfig): Observer;
14
+ update(config: Partial<TrainingPeaksConfig>): void;
15
+ isAuthenticated(): boolean;
16
+ getLoggedInAthlete(): Promise<TrainingPeaksAthlete>;
17
+ getCalendarWorkouts(opts?: {
18
+ oldest?: Date;
19
+ newest?: Date;
20
+ }): Promise<TrainingPeaksWorkout[]>;
21
+ getWorkoutFile(id: number, format?: string): Promise<string>;
22
+ upload(fileName: string, props?: TrainingPeaksUploadProps): Promise<TrainingPeaksUploadResult>;
23
+ getUploadStatus(trackingId: string): Promise<TrainingPeaksUploadStatus>;
24
+ protected verifyAuthentication(): Promise<void>;
25
+ protected verifyToken(): Promise<void>;
26
+ protected isTokenStillValid(): boolean;
27
+ protected refreshToken(): Promise<void>;
28
+ protected waitForUploadComplete(trackingId: string, externalId?: string): Promise<TrainingPeaksUploadResult>;
29
+ protected get(url: string, config?: object): Promise<AxiosResponse>;
30
+ }
@@ -0,0 +1,2 @@
1
+ export { TrainingPeaksApi } from './api';
2
+ export type { TrainingPeaksConfig, TrainingPeaksWorkout, TrainingPeaksUploadProps, TrainingPeaksUploadResult, TrainingPeaksUploadStatus, TrainingPeaksAthlete } from './types';
@@ -0,0 +1,55 @@
1
+ export type TrainingPeaksConfig = {
2
+ accessToken: string;
3
+ refreshToken: string;
4
+ clientId: string;
5
+ clientSecret: string;
6
+ expiration?: Date;
7
+ };
8
+ export type TrainingPeaksRefreshTokenResponse = {
9
+ access_token: string;
10
+ token_type: string;
11
+ expires_in: number;
12
+ refresh_token: string;
13
+ scope: string;
14
+ };
15
+ export type TrainingPeaksAthlete = {
16
+ Id: number;
17
+ Username: string;
18
+ FirstName: string;
19
+ LastName: string;
20
+ Email: string;
21
+ };
22
+ export type TrainingPeaksWorkout = {
23
+ Id: number;
24
+ AthleteId: number;
25
+ Title: string;
26
+ WorkoutType: string;
27
+ WorkoutDay: string;
28
+ StartTimePlanned?: string;
29
+ TotalTimePlanned?: number;
30
+ TotalTime?: number;
31
+ DistancePlanned?: number;
32
+ Distance?: number;
33
+ TssPlanned?: number;
34
+ TssActual?: number;
35
+ IFPlanned?: number;
36
+ Completed: boolean;
37
+ LastModifiedDate: string;
38
+ WorkoutFileFormats?: string[];
39
+ Description?: string;
40
+ };
41
+ export type TrainingPeaksUploadStatus = {
42
+ Completed: boolean;
43
+ Status: string;
44
+ WorkoutIds: number[];
45
+ };
46
+ export type TrainingPeaksUploadProps = {
47
+ name?: string;
48
+ description?: string;
49
+ format?: string;
50
+ externalId?: string;
51
+ };
52
+ export type TrainingPeaksUploadResult = {
53
+ trainingPeaksId: number;
54
+ externalId?: string;
55
+ };
@@ -0,0 +1,22 @@
1
+ import { TrainingPeaksApi, TrainingPeaksConfig } from '../base/api/trainingpeaks';
2
+ import { ConnectedAppService } from '../base/app';
3
+ import { TrainingPeaksCredentials } from './types';
4
+ export declare class TrainingPeaksAppConnection extends ConnectedAppService<TrainingPeaksCredentials> {
5
+ protected config: TrainingPeaksConfig;
6
+ protected api: TrainingPeaksApi;
7
+ protected tokenUpdateHandler: any;
8
+ constructor();
9
+ connect(credentials: TrainingPeaksCredentials): Promise<boolean>;
10
+ isConnecting(): boolean;
11
+ disconnect(): void;
12
+ isConnected(): boolean;
13
+ getCredentials(): TrainingPeaksCredentials;
14
+ getApi(): TrainingPeaksApi;
15
+ protected initAuth(): boolean;
16
+ protected updateConfig(config: TrainingPeaksConfig): void;
17
+ protected saveCredentials(config?: TrainingPeaksConfig): void;
18
+ protected buildConfigFromCredentials(credentials: TrainingPeaksCredentials): TrainingPeaksConfig;
19
+ protected initApi(config: TrainingPeaksConfig): void;
20
+ protected getSecret(key: string): string;
21
+ protected getSecretBindings(): import("../../api").ISecretBinding;
22
+ }
@@ -0,0 +1,6 @@
1
+ import { AppCredentials } from '../base/types';
2
+ export interface TrainingPeaksCredentials extends AppCredentials {
3
+ accesstoken: string;
4
+ refreshtoken: string;
5
+ expiration?: string;
6
+ }
@@ -23,6 +23,7 @@ export declare class RidePageService extends IncyclistPageService implements IRi
23
23
  onMenuClose(): void;
24
24
  onPause(): void;
25
25
  onResume(): void;
26
+ onFinished(): void;
26
27
  onEndRide(): void;
27
28
  onRetryStart(): void;
28
29
  onIgnoreStart(): void;
@@ -24,7 +24,8 @@ export interface GPXRidePageDisplayProps extends RidePageDisplayProps {
24
24
  }
25
25
  export type AnyRidePageDisplayProps = VideoRidePageDisplayProps | RidePageDisplayProps;
26
26
  export interface RideMenuProps {
27
- showResume: boolean;
27
+ showResume?: boolean;
28
+ finished?: boolean;
28
29
  }
29
30
  interface RidePageCallbacks {
30
31
  onMenuOpen(): void;
@@ -0,0 +1,22 @@
1
+ import { TrainingPeaksAppConnection } from '../../../../apps/trainingpeaks/TrainingPeaksAppConnection';
2
+ import { TrainingPeaksWorkout } from '../../../../apps/base/api/trainingpeaks/types';
3
+ import { IncyclistService } from '../../../../base/service';
4
+ import { Observer } from '../../../../base/types';
5
+ import { WorkoutCalendarEntry } from '../../types';
6
+ import { IWorkoutSyncProvider } from '../types';
7
+ export declare class TrainingPeaksCalendarSyncProvider extends IncyclistService implements IWorkoutSyncProvider {
8
+ protected lastSyncTS: number;
9
+ protected observer: Observer;
10
+ protected stopRequested: boolean;
11
+ protected workouts: WorkoutCalendarEntry[];
12
+ constructor();
13
+ sync(): Observer;
14
+ isConnected(): boolean;
15
+ reset(): void;
16
+ protected stopSync(): void;
17
+ protected loadWorkouts(): Promise<WorkoutCalendarEntry[]>;
18
+ protected parseWorkouts(workouts: TrainingPeaksWorkout[]): Promise<WorkoutCalendarEntry[]>;
19
+ protected getApi(): import("../../../../apps/base/api/trainingpeaks").TrainingPeaksApi;
20
+ protected getConnection(): TrainingPeaksAppConnection;
21
+ protected getAppsService(): import("../../../../apps").AppsService;
22
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "incyclist-services",
3
- "version": "1.7.71",
3
+ "version": "1.7.72",
4
4
  "peerDependencies": {
5
5
  "gd-eventlog": "^0.1.27"
6
6
  },
7
7
  "dependencies": {
8
8
  "@garmin/fitsdk": "^21.200.0",
9
9
  "axios": "^1.15.2",
10
- "incyclist-devices": "^3.0.21",
10
+ "incyclist-devices": "^3.0.22",
11
11
  "promise.any": "^2.0.6",
12
12
  "semver": "^7.7.4",
13
13
  "tcx-builder": "^1.1.1",