incyclist-services 1.7.138 → 1.7.139

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.
@@ -66,6 +66,10 @@ let Route = (() => {
66
66
  const points = this._details?.points ?? this._description?.points;
67
67
  return points;
68
68
  }
69
+ replace(update) {
70
+ this._description = { ...update._description };
71
+ this._details = { ...update._details };
72
+ }
69
73
  addDetails(details) {
70
74
  this._details = details;
71
75
  }
@@ -123,6 +123,7 @@ let RouteLibraryScannerService = (() => {
123
123
  scan(folderInfo) {
124
124
  if (!this.importProps)
125
125
  this.prepare();
126
+ this.logEvent({ message: 'import route library', folder: folderInfo.displayName, uri: folderInfo.uri });
126
127
  this.isCancelled = false;
127
128
  this.scanResult = [];
128
129
  this.importProps.phase = 'scanning';
@@ -137,6 +138,8 @@ let RouteLibraryScannerService = (() => {
137
138
  this.importProps.scanProgress = progress;
138
139
  })
139
140
  .on('scan-complete', () => {
141
+ const cntScanned = this.importProps.scanProgress.scannedFolders;
142
+ this.logEvent({ message: 'import route library: scan success', folder: folderInfo.displayName, cntScanned });
140
143
  this.importProps.phase = 'parsing';
141
144
  });
142
145
  return observer;
@@ -145,6 +148,7 @@ let RouteLibraryScannerService = (() => {
145
148
  const observer = new types_1.Observer();
146
149
  if (!this.importProps)
147
150
  this.prepare();
151
+ this.logEvent({ message: 'import route library: parse start', folder: scannedRoutes?.[0].folderName });
148
152
  this._parse(scannedRoutes, observer).catch(err => {
149
153
  this.logError(err, 'parse');
150
154
  observer.emit('error', err.message);
@@ -165,6 +169,13 @@ let RouteLibraryScannerService = (() => {
165
169
  }
166
170
  });
167
171
  observer.on('parse-complete', () => {
172
+ const parseSumamry = {
173
+ cntTotal: this.importProps.routes.length,
174
+ cntSuccess: this.importProps.routes.filter(r => !r.errorReason).length,
175
+ cntError: this.importProps.routes.filter(r => r.errorReason != null).length,
176
+ cntExisting: this.importProps.routes.filter(r => r.alreadyImported).length,
177
+ };
178
+ this.logEvent({ message: 'import route library: parse completed', folder: scannedRoutes?.[0].folderName, parseSumamry });
168
179
  this.importProps.phase = 'selecting';
169
180
  });
170
181
  return observer;
@@ -215,23 +226,36 @@ let RouteLibraryScannerService = (() => {
215
226
  }
216
227
  }
217
228
  async importSingleGpxRoute(fileInfo, observer) {
218
- this.logEvent({ message: 'import single GPX route' });
219
- const list = this.getRouteList();
229
+ const name = fileInfo.url ?? fileInfo.filename ?? fileInfo.name;
230
+ this.logEvent({ message: 'import single route file', file: name, type: fileInfo.ext });
231
+ const service = this.getRouteList();
220
232
  const db = this.getRoutesDBLoader();
221
233
  try {
222
234
  const { data, details } = await parsers_1.RouteParser.parse(fileInfo);
223
235
  const route = new route_1.Route(data, details);
236
+ this.logEvent({ message: 'import single route file success', file: name });
237
+ const existing = service.findCard(route);
238
+ if (existing) {
239
+ this.logEvent({ message: 'route updated (import)', route: route.title });
240
+ }
224
241
  route.description.tsImported = Date.now();
225
242
  await db.save(route, true);
226
- list.addRoute(route, 'user');
243
+ service.addRoute(route, existing ? 'import' : 'user');
244
+ const cardInfo = service.findCard(route);
245
+ cardInfo?.card?.verify();
246
+ if (existing) {
247
+ existing.card.emitUpdate();
248
+ }
227
249
  observer.emit('success', route);
228
250
  }
229
251
  catch (err) {
252
+ this.logEvent({ message: 'import single route file failed', file: name, reason: err.message, stack: err.stack });
230
253
  observer.emit('error', err.message);
231
254
  }
232
255
  }
233
256
  async importSingleVideoRoute(fileInfo, observer) {
234
- this.logEvent({ message: 'import single video route', fileInfo });
257
+ const name = fileInfo.url ?? fileInfo.filename ?? fileInfo.name;
258
+ this.logEvent({ message: 'import single route file', file: name, type: fileInfo.ext });
235
259
  const parsers = this.getParsers();
236
260
  const { dir, delimiter } = fileInfo;
237
261
  let { ext } = fileInfo;
@@ -240,6 +264,7 @@ let RouteLibraryScannerService = (() => {
240
264
  const folderUri = dir.endsWith(delimiter ?? '/') ? dir.slice(0, -delimiter.length) : dir;
241
265
  try {
242
266
  if (!parsers.isPrimaryExtension(ext)) {
267
+ this.logEvent({ message: 'import single route file failed', file: name, reason: 'not a route control file' });
243
268
  observer.emit('error', 'not a route control file', ext);
244
269
  return;
245
270
  }
@@ -249,9 +274,11 @@ let RouteLibraryScannerService = (() => {
249
274
  this.scanResult = [];
250
275
  scanObserver.stop();
251
276
  if (!files.length) {
277
+ this.logEvent({ message: 'import single route file failed', file: name, reason: 'no file found' });
252
278
  observer.emit('error', 'no file found');
253
279
  }
254
280
  if (files[0].scanError) {
281
+ this.logEvent({ message: 'import single route file failed', file: name, reason: files[0].scanError });
255
282
  observer.emit('error', files[0].scanError);
256
283
  return;
257
284
  }
@@ -261,6 +288,7 @@ let RouteLibraryScannerService = (() => {
261
288
  parseObserver.on('parse-result', (result) => {
262
289
  parseObserver.stop();
263
290
  if (result.parseError) {
291
+ this.logEvent({ message: 'import single route file failed', file: name, reason: result.parseError });
264
292
  observer.emit('error', result.parseError);
265
293
  return;
266
294
  }
@@ -268,15 +296,18 @@ let RouteLibraryScannerService = (() => {
268
296
  let ingestError;
269
297
  ingest.once('ingest-error', (_, reason) => {
270
298
  ingest.stop();
299
+ this.logEvent({ message: 'import single route file failed', file: name, reason });
271
300
  observer.emit('error', reason);
272
301
  ingestError = reason;
273
302
  });
274
303
  ingest.once('ingest-complete', (summary) => {
275
304
  ingest.stop();
276
305
  if (!ingestError && summary.imported > 0) {
306
+ this.logEvent({ message: 'import single route file success', file: name });
277
307
  observer.emit('success', summary.importedRoutes?.[0]?.title);
278
308
  }
279
309
  else if (!ingestError && summary.imported === 0) {
310
+ this.logEvent({ message: 'import single route file failed', file: name, reason: 'not imported' });
280
311
  observer.emit('error', 'not imported');
281
312
  }
282
313
  });
@@ -284,6 +315,7 @@ let RouteLibraryScannerService = (() => {
284
315
  }
285
316
  }
286
317
  catch (err) {
318
+ this.logEvent({ message: 'import single route file failed', file: name, reason: err.message, stack: err.stack });
287
319
  observer.emit('error', err.message);
288
320
  }
289
321
  }
@@ -299,7 +331,6 @@ let RouteLibraryScannerService = (() => {
299
331
  }
300
332
  async scanFolder(uri, folderName, observer, parsers, progress, discoveredCount, recursive = true) {
301
333
  const fs = this.getBindings().fs;
302
- this.logEvent({ message: 'scanFolder', uri, folderName });
303
334
  let entries;
304
335
  try {
305
336
  entries = await fs.readdir(uri, { recursive: false, extended: true });
@@ -393,7 +424,6 @@ let RouteLibraryScannerService = (() => {
393
424
  fileName = info.base;
394
425
  }
395
426
  catch { }
396
- this.logEvent({ message: 'parsing route', fileName });
397
427
  await this._parseTarget(target, service, observer);
398
428
  this.logEvent({ message: 'parsing route done', fileName });
399
429
  }
@@ -446,7 +476,6 @@ let RouteLibraryScannerService = (() => {
446
476
  validateVideoUrl(route, folderUri, folderFiles) {
447
477
  const { video } = route?.details ?? {};
448
478
  const { file, url, format } = video ?? {};
449
- this.logEvent({ message: 'validateVideoUrl', route: { file, url, format }, folderUri, folderFiles });
450
479
  let videoFormat = format;
451
480
  try {
452
481
  if (videoFormat === undefined) {
@@ -511,7 +540,7 @@ let RouteLibraryScannerService = (() => {
511
540
  await (0, utils_1.waitNextTick)();
512
541
  const service = this.getRouteList();
513
542
  const db = this.getRoutesDBLoader();
514
- const target = routes.filter(r => !r.alreadyImported && !r.parseError);
543
+ const target = routes.filter(r => !r.parseError);
515
544
  const total = target.length;
516
545
  let errors = 0;
517
546
  const failedRoutes = [];
@@ -520,10 +549,25 @@ let RouteLibraryScannerService = (() => {
520
549
  if (this.isCancelled)
521
550
  continue;
522
551
  const { route } = target[i] ?? {};
552
+ const isExisting = target[i].alreadyImported;
553
+ const existing = isExisting ? service.findCard(route) : null;
523
554
  try {
524
555
  observer.emit('ingest-progress', { current: i + 1, total, currentName: route.title });
556
+ if (existing) {
557
+ this.logEvent({ message: 'route updated (library import)', route: route.title });
558
+ }
559
+ route.description.tsImported = Date.now();
525
560
  await db.save(route, true);
526
- service.addRoute(route, 'user');
561
+ service.addRoute(route, existing ? 'import' : 'user');
562
+ try {
563
+ const cardInfo = service.findCard(route);
564
+ cardInfo?.card?.verify();
565
+ }
566
+ catch {
567
+ }
568
+ if (existing) {
569
+ existing.card.emitUpdate();
570
+ }
527
571
  importedRoutes.push(route);
528
572
  }
529
573
  catch (err) {
@@ -760,6 +760,7 @@ let RouteListService = (() => {
760
760
  return;
761
761
  const importCard = importId ? null : importCards[idx];
762
762
  const name = file.url ?? file.filename ?? file.name;
763
+ this.logEvent({ message: 'import single route file', file: name, type: file.ext });
763
764
  try {
764
765
  if (importId && observer)
765
766
  observer?.emit('parsing', importId);
@@ -768,12 +769,15 @@ let RouteListService = (() => {
768
769
  observer?.emit('success', importId);
769
770
  const route = new route_1.Route(data, details);
770
771
  route.description.tsImported = Date.now();
772
+ this.logEvent({ message: 'import single route file success', file: name });
771
773
  const existing = this.findCard(route);
772
774
  if (existing) {
773
775
  existing.list.remove(existing.card);
776
+ this.logEvent({ message: 'route updated (import)', route: route.title });
774
777
  }
775
778
  else {
776
779
  this.routes.push(route);
780
+ this.logEvent({ message: 'route added', route: route.title });
777
781
  }
778
782
  const card = new RouteCard_1.RouteCard(route, { list: this.myRoutes });
779
783
  card.verify();
@@ -789,7 +793,7 @@ let RouteListService = (() => {
789
793
  this.verifyPoints(card, route);
790
794
  }
791
795
  catch (err) {
792
- this.logEvent({ message: 'import failed', name, reason: err.message, stack: err.stack });
796
+ this.logEvent({ message: 'import single route file failed', file: name, reason: err.message, stack: err.stack });
793
797
  if (importId && observer)
794
798
  observer?.emit('error', importId, err.message);
795
799
  if (importCard)
@@ -911,7 +915,10 @@ let RouteListService = (() => {
911
915
  }
912
916
  }
913
917
  addRoute(route, source = 'system') {
914
- if (this.getRoute(route.description.id)) {
918
+ const existing = source === 'system' ? this.getRoute(route.description.id) :
919
+ this.routes.find(r => r.description?.id === route.description.id);
920
+ if (existing) {
921
+ existing.replace(route);
915
922
  return;
916
923
  }
917
924
  this.routes.push(route);
@@ -929,6 +936,8 @@ let RouteListService = (() => {
929
936
  if (updated) {
930
937
  this.logEvent({ message: 'route updated (country)', route: route.title, country: route.description?.country });
931
938
  this.db.save(route, false);
939
+ const cardInfo = this.findCard(route);
940
+ cardInfo?.card?.emitUpdate();
932
941
  }
933
942
  });
934
943
  }
@@ -940,7 +949,7 @@ let RouteListService = (() => {
940
949
  list.add(card);
941
950
  if (list.getId() === 'myRoutes')
942
951
  card.enableDelete(true);
943
- this.emitLists('updated', { source });
952
+ this.emitLists('updated', { source: source === 'user' ? 'user' : 'system' });
944
953
  }
945
954
  async verifyPoints(card, route) {
946
955
  const updated = await route.updateCountryFromPoints();
@@ -88,7 +88,6 @@ let IntervalsJsonParser = (() => {
88
88
  workout.addStep(step);
89
89
  }
90
90
  });
91
- this.logger.logEvent({ message: 'parse success', file: fileName });
92
91
  return workout;
93
92
  }
94
93
  supportsExtension(extension) {
@@ -120,7 +120,11 @@ let IntervalsCalendarSyncProvider = (() => {
120
120
  }
121
121
  const oldest = (0, utils_1.getFirstDayOfCurrentWeek)();
122
122
  const cw = await this.getIntervalsApi().getCalendarWorkouts({ oldest, days: 30, ext: 'zwo' }) ?? [];
123
- const cycling = cw.filter(w => w.type === 'Ride');
123
+ const cycling = cw.filter(w => w.type === 'Ride' || w.type === 'VirtualRide');
124
+ this.logger.logEvent({ message: 'workout sync', externalApp: 'intervals',
125
+ oldest: oldest.toLocaleDateString(), cntTotal: cw.length, cntCycling: cycling.length,
126
+ workouts: cw.map(cw => `${cw.type}:${cw.name}`).join('|')
127
+ });
124
128
  await this.parseWorkouts(cycling);
125
129
  this.observer.emit(event, this.workouts, 'intervals');
126
130
  this.lastSyncTS = Date.now();
@@ -166,9 +170,11 @@ let IntervalsCalendarSyncProvider = (() => {
166
170
  if (this.lastSyncTS > 0) {
167
171
  if (!isExisting) {
168
172
  this.observer.emit('added', w, 'intervals');
173
+ this.logEvent({ message: 'workout added', externalApp: 'intervals', workout: w.workout?.name });
169
174
  }
170
175
  else if (updated && updated.valueOf() > this.lastSyncTS) {
171
176
  this.observer.emit('updated', w, 'intervals');
177
+ this.logEvent({ message: 'workout updated', externalApp: 'intervals', workout: w.workout?.name });
172
178
  }
173
179
  }
174
180
  if (isExisting) {
@@ -186,6 +192,7 @@ let IntervalsCalendarSyncProvider = (() => {
186
192
  const deletedWorkouts = this.workouts.filter(w => !events.some(e => e.id.toString() === w.workoutId));
187
193
  for (const w of deletedWorkouts) {
188
194
  this.observer.emit('deleted', w, 'intervals');
195
+ this.logEvent({ message: 'workout deleted', externalApp: 'intervals', workout: w.workout?.name });
189
196
  }
190
197
  this.workouts = this.workouts.filter(w => events.find(e => e.id.toString() === w.workoutId));
191
198
  }
@@ -60,6 +60,10 @@ let Route = (() => {
60
60
  const points = this._details?.points ?? this._description?.points;
61
61
  return points;
62
62
  }
63
+ replace(update) {
64
+ this._description = { ...update._description };
65
+ this._details = { ...update._details };
66
+ }
63
67
  addDetails(details) {
64
68
  this._details = details;
65
69
  }
@@ -120,6 +120,7 @@ let RouteLibraryScannerService = (() => {
120
120
  scan(folderInfo) {
121
121
  if (!this.importProps)
122
122
  this.prepare();
123
+ this.logEvent({ message: 'import route library', folder: folderInfo.displayName, uri: folderInfo.uri });
123
124
  this.isCancelled = false;
124
125
  this.scanResult = [];
125
126
  this.importProps.phase = 'scanning';
@@ -134,6 +135,8 @@ let RouteLibraryScannerService = (() => {
134
135
  this.importProps.scanProgress = progress;
135
136
  })
136
137
  .on('scan-complete', () => {
138
+ const cntScanned = this.importProps.scanProgress.scannedFolders;
139
+ this.logEvent({ message: 'import route library: scan success', folder: folderInfo.displayName, cntScanned });
137
140
  this.importProps.phase = 'parsing';
138
141
  });
139
142
  return observer;
@@ -142,6 +145,7 @@ let RouteLibraryScannerService = (() => {
142
145
  const observer = new Observer();
143
146
  if (!this.importProps)
144
147
  this.prepare();
148
+ this.logEvent({ message: 'import route library: parse start', folder: scannedRoutes?.[0].folderName });
145
149
  this._parse(scannedRoutes, observer).catch(err => {
146
150
  this.logError(err, 'parse');
147
151
  observer.emit('error', err.message);
@@ -162,6 +166,13 @@ let RouteLibraryScannerService = (() => {
162
166
  }
163
167
  });
164
168
  observer.on('parse-complete', () => {
169
+ const parseSumamry = {
170
+ cntTotal: this.importProps.routes.length,
171
+ cntSuccess: this.importProps.routes.filter(r => !r.errorReason).length,
172
+ cntError: this.importProps.routes.filter(r => r.errorReason != null).length,
173
+ cntExisting: this.importProps.routes.filter(r => r.alreadyImported).length,
174
+ };
175
+ this.logEvent({ message: 'import route library: parse completed', folder: scannedRoutes?.[0].folderName, parseSumamry });
165
176
  this.importProps.phase = 'selecting';
166
177
  });
167
178
  return observer;
@@ -212,23 +223,36 @@ let RouteLibraryScannerService = (() => {
212
223
  }
213
224
  }
214
225
  async importSingleGpxRoute(fileInfo, observer) {
215
- this.logEvent({ message: 'import single GPX route' });
216
- const list = this.getRouteList();
226
+ const name = fileInfo.url ?? fileInfo.filename ?? fileInfo.name;
227
+ this.logEvent({ message: 'import single route file', file: name, type: fileInfo.ext });
228
+ const service = this.getRouteList();
217
229
  const db = this.getRoutesDBLoader();
218
230
  try {
219
231
  const { data, details } = await RouteParser.parse(fileInfo);
220
232
  const route = new Route(data, details);
233
+ this.logEvent({ message: 'import single route file success', file: name });
234
+ const existing = service.findCard(route);
235
+ if (existing) {
236
+ this.logEvent({ message: 'route updated (import)', route: route.title });
237
+ }
221
238
  route.description.tsImported = Date.now();
222
239
  await db.save(route, true);
223
- list.addRoute(route, 'user');
240
+ service.addRoute(route, existing ? 'import' : 'user');
241
+ const cardInfo = service.findCard(route);
242
+ cardInfo?.card?.verify();
243
+ if (existing) {
244
+ existing.card.emitUpdate();
245
+ }
224
246
  observer.emit('success', route);
225
247
  }
226
248
  catch (err) {
249
+ this.logEvent({ message: 'import single route file failed', file: name, reason: err.message, stack: err.stack });
227
250
  observer.emit('error', err.message);
228
251
  }
229
252
  }
230
253
  async importSingleVideoRoute(fileInfo, observer) {
231
- this.logEvent({ message: 'import single video route', fileInfo });
254
+ const name = fileInfo.url ?? fileInfo.filename ?? fileInfo.name;
255
+ this.logEvent({ message: 'import single route file', file: name, type: fileInfo.ext });
232
256
  const parsers = this.getParsers();
233
257
  const { dir, delimiter } = fileInfo;
234
258
  let { ext } = fileInfo;
@@ -237,6 +261,7 @@ let RouteLibraryScannerService = (() => {
237
261
  const folderUri = dir.endsWith(delimiter ?? '/') ? dir.slice(0, -delimiter.length) : dir;
238
262
  try {
239
263
  if (!parsers.isPrimaryExtension(ext)) {
264
+ this.logEvent({ message: 'import single route file failed', file: name, reason: 'not a route control file' });
240
265
  observer.emit('error', 'not a route control file', ext);
241
266
  return;
242
267
  }
@@ -246,9 +271,11 @@ let RouteLibraryScannerService = (() => {
246
271
  this.scanResult = [];
247
272
  scanObserver.stop();
248
273
  if (!files.length) {
274
+ this.logEvent({ message: 'import single route file failed', file: name, reason: 'no file found' });
249
275
  observer.emit('error', 'no file found');
250
276
  }
251
277
  if (files[0].scanError) {
278
+ this.logEvent({ message: 'import single route file failed', file: name, reason: files[0].scanError });
252
279
  observer.emit('error', files[0].scanError);
253
280
  return;
254
281
  }
@@ -258,6 +285,7 @@ let RouteLibraryScannerService = (() => {
258
285
  parseObserver.on('parse-result', (result) => {
259
286
  parseObserver.stop();
260
287
  if (result.parseError) {
288
+ this.logEvent({ message: 'import single route file failed', file: name, reason: result.parseError });
261
289
  observer.emit('error', result.parseError);
262
290
  return;
263
291
  }
@@ -265,15 +293,18 @@ let RouteLibraryScannerService = (() => {
265
293
  let ingestError;
266
294
  ingest.once('ingest-error', (_, reason) => {
267
295
  ingest.stop();
296
+ this.logEvent({ message: 'import single route file failed', file: name, reason });
268
297
  observer.emit('error', reason);
269
298
  ingestError = reason;
270
299
  });
271
300
  ingest.once('ingest-complete', (summary) => {
272
301
  ingest.stop();
273
302
  if (!ingestError && summary.imported > 0) {
303
+ this.logEvent({ message: 'import single route file success', file: name });
274
304
  observer.emit('success', summary.importedRoutes?.[0]?.title);
275
305
  }
276
306
  else if (!ingestError && summary.imported === 0) {
307
+ this.logEvent({ message: 'import single route file failed', file: name, reason: 'not imported' });
277
308
  observer.emit('error', 'not imported');
278
309
  }
279
310
  });
@@ -281,6 +312,7 @@ let RouteLibraryScannerService = (() => {
281
312
  }
282
313
  }
283
314
  catch (err) {
315
+ this.logEvent({ message: 'import single route file failed', file: name, reason: err.message, stack: err.stack });
284
316
  observer.emit('error', err.message);
285
317
  }
286
318
  }
@@ -296,7 +328,6 @@ let RouteLibraryScannerService = (() => {
296
328
  }
297
329
  async scanFolder(uri, folderName, observer, parsers, progress, discoveredCount, recursive = true) {
298
330
  const fs = this.getBindings().fs;
299
- this.logEvent({ message: 'scanFolder', uri, folderName });
300
331
  let entries;
301
332
  try {
302
333
  entries = await fs.readdir(uri, { recursive: false, extended: true });
@@ -390,7 +421,6 @@ let RouteLibraryScannerService = (() => {
390
421
  fileName = info.base;
391
422
  }
392
423
  catch { }
393
- this.logEvent({ message: 'parsing route', fileName });
394
424
  await this._parseTarget(target, service, observer);
395
425
  this.logEvent({ message: 'parsing route done', fileName });
396
426
  }
@@ -443,7 +473,6 @@ let RouteLibraryScannerService = (() => {
443
473
  validateVideoUrl(route, folderUri, folderFiles) {
444
474
  const { video } = route?.details ?? {};
445
475
  const { file, url, format } = video ?? {};
446
- this.logEvent({ message: 'validateVideoUrl', route: { file, url, format }, folderUri, folderFiles });
447
476
  let videoFormat = format;
448
477
  try {
449
478
  if (videoFormat === undefined) {
@@ -508,7 +537,7 @@ let RouteLibraryScannerService = (() => {
508
537
  await waitNextTick();
509
538
  const service = this.getRouteList();
510
539
  const db = this.getRoutesDBLoader();
511
- const target = routes.filter(r => !r.alreadyImported && !r.parseError);
540
+ const target = routes.filter(r => !r.parseError);
512
541
  const total = target.length;
513
542
  let errors = 0;
514
543
  const failedRoutes = [];
@@ -517,10 +546,25 @@ let RouteLibraryScannerService = (() => {
517
546
  if (this.isCancelled)
518
547
  continue;
519
548
  const { route } = target[i] ?? {};
549
+ const isExisting = target[i].alreadyImported;
550
+ const existing = isExisting ? service.findCard(route) : null;
520
551
  try {
521
552
  observer.emit('ingest-progress', { current: i + 1, total, currentName: route.title });
553
+ if (existing) {
554
+ this.logEvent({ message: 'route updated (library import)', route: route.title });
555
+ }
556
+ route.description.tsImported = Date.now();
522
557
  await db.save(route, true);
523
- service.addRoute(route, 'user');
558
+ service.addRoute(route, existing ? 'import' : 'user');
559
+ try {
560
+ const cardInfo = service.findCard(route);
561
+ cardInfo?.card?.verify();
562
+ }
563
+ catch {
564
+ }
565
+ if (existing) {
566
+ existing.card.emitUpdate();
567
+ }
524
568
  importedRoutes.push(route);
525
569
  }
526
570
  catch (err) {
@@ -754,6 +754,7 @@ let RouteListService = (() => {
754
754
  return;
755
755
  const importCard = importId ? null : importCards[idx];
756
756
  const name = file.url ?? file.filename ?? file.name;
757
+ this.logEvent({ message: 'import single route file', file: name, type: file.ext });
757
758
  try {
758
759
  if (importId && observer)
759
760
  observer?.emit('parsing', importId);
@@ -762,12 +763,15 @@ let RouteListService = (() => {
762
763
  observer?.emit('success', importId);
763
764
  const route = new Route(data, details);
764
765
  route.description.tsImported = Date.now();
766
+ this.logEvent({ message: 'import single route file success', file: name });
765
767
  const existing = this.findCard(route);
766
768
  if (existing) {
767
769
  existing.list.remove(existing.card);
770
+ this.logEvent({ message: 'route updated (import)', route: route.title });
768
771
  }
769
772
  else {
770
773
  this.routes.push(route);
774
+ this.logEvent({ message: 'route added', route: route.title });
771
775
  }
772
776
  const card = new RouteCard(route, { list: this.myRoutes });
773
777
  card.verify();
@@ -783,7 +787,7 @@ let RouteListService = (() => {
783
787
  this.verifyPoints(card, route);
784
788
  }
785
789
  catch (err) {
786
- this.logEvent({ message: 'import failed', name, reason: err.message, stack: err.stack });
790
+ this.logEvent({ message: 'import single route file failed', file: name, reason: err.message, stack: err.stack });
787
791
  if (importId && observer)
788
792
  observer?.emit('error', importId, err.message);
789
793
  if (importCard)
@@ -905,7 +909,10 @@ let RouteListService = (() => {
905
909
  }
906
910
  }
907
911
  addRoute(route, source = 'system') {
908
- if (this.getRoute(route.description.id)) {
912
+ const existing = source === 'system' ? this.getRoute(route.description.id) :
913
+ this.routes.find(r => r.description?.id === route.description.id);
914
+ if (existing) {
915
+ existing.replace(route);
909
916
  return;
910
917
  }
911
918
  this.routes.push(route);
@@ -923,6 +930,8 @@ let RouteListService = (() => {
923
930
  if (updated) {
924
931
  this.logEvent({ message: 'route updated (country)', route: route.title, country: route.description?.country });
925
932
  this.db.save(route, false);
933
+ const cardInfo = this.findCard(route);
934
+ cardInfo?.card?.emitUpdate();
926
935
  }
927
936
  });
928
937
  }
@@ -934,7 +943,7 @@ let RouteListService = (() => {
934
943
  list.add(card);
935
944
  if (list.getId() === 'myRoutes')
936
945
  card.enableDelete(true);
937
- this.emitLists('updated', { source });
946
+ this.emitLists('updated', { source: source === 'user' ? 'user' : 'system' });
938
947
  }
939
948
  async verifyPoints(card, route) {
940
949
  const updated = await route.updateCountryFromPoints();
@@ -85,7 +85,6 @@ let IntervalsJsonParser = (() => {
85
85
  workout.addStep(step);
86
86
  }
87
87
  });
88
- this.logger.logEvent({ message: 'parse success', file: fileName });
89
88
  return workout;
90
89
  }
91
90
  supportsExtension(extension) {
@@ -117,7 +117,11 @@ let IntervalsCalendarSyncProvider = (() => {
117
117
  }
118
118
  const oldest = getFirstDayOfCurrentWeek();
119
119
  const cw = await this.getIntervalsApi().getCalendarWorkouts({ oldest, days: 30, ext: 'zwo' }) ?? [];
120
- const cycling = cw.filter(w => w.type === 'Ride');
120
+ const cycling = cw.filter(w => w.type === 'Ride' || w.type === 'VirtualRide');
121
+ this.logger.logEvent({ message: 'workout sync', externalApp: 'intervals',
122
+ oldest: oldest.toLocaleDateString(), cntTotal: cw.length, cntCycling: cycling.length,
123
+ workouts: cw.map(cw => `${cw.type}:${cw.name}`).join('|')
124
+ });
121
125
  await this.parseWorkouts(cycling);
122
126
  this.observer.emit(event, this.workouts, 'intervals');
123
127
  this.lastSyncTS = Date.now();
@@ -163,9 +167,11 @@ let IntervalsCalendarSyncProvider = (() => {
163
167
  if (this.lastSyncTS > 0) {
164
168
  if (!isExisting) {
165
169
  this.observer.emit('added', w, 'intervals');
170
+ this.logEvent({ message: 'workout added', externalApp: 'intervals', workout: w.workout?.name });
166
171
  }
167
172
  else if (updated && updated.valueOf() > this.lastSyncTS) {
168
173
  this.observer.emit('updated', w, 'intervals');
174
+ this.logEvent({ message: 'workout updated', externalApp: 'intervals', workout: w.workout?.name });
169
175
  }
170
176
  }
171
177
  if (isExisting) {
@@ -183,6 +189,7 @@ let IntervalsCalendarSyncProvider = (() => {
183
189
  const deletedWorkouts = this.workouts.filter(w => !events.some(e => e.id.toString() === w.workoutId));
184
190
  for (const w of deletedWorkouts) {
185
191
  this.observer.emit('deleted', w, 'intervals');
192
+ this.logEvent({ message: 'workout deleted', externalApp: 'intervals', workout: w.workout?.name });
186
193
  }
187
194
  this.workouts = this.workouts.filter(w => events.find(e => e.id.toString() === w.workoutId));
188
195
  }
@@ -53,7 +53,7 @@ export type IntervalsCalendarEvent = {
53
53
  icu_training_load: number;
54
54
  start_date_local: string;
55
55
  end_date_local: string;
56
- type: 'Ride' | 'Run' | 'Swim' | 'Walk' | 'Weight Training';
56
+ type: 'Ride' | 'Run' | 'Swim' | 'Walk' | 'Weight Training' | 'VirtualRide' | 'MountainBikeRide' | 'GravelRide';
57
57
  calendar_id: number;
58
58
  uid: string;
59
59
  athlete_id: string;
@@ -2,11 +2,12 @@ import { Countries } from "../../../i18n/countries";
2
2
  import { RouteApiDetail } from "../api/types";
3
3
  import { RouteInfo, RoutePoint } from "../types";
4
4
  export declare class Route {
5
- private readonly _description;
5
+ private _description;
6
6
  private _details;
7
7
  constructor(info: RouteInfo, raw?: RouteApiDetail);
8
8
  get description(): RouteInfo;
9
9
  get points(): Array<RoutePoint>;
10
+ replace(update: Route): void;
10
11
  addDetails(details: RouteApiDetail): void;
11
12
  get details(): RouteApiDetail;
12
13
  set details(details: RouteApiDetail);
@@ -138,7 +138,7 @@ export declare class RouteListService extends IncyclistService implements IRoute
138
138
  protected getFilterCountries(): Array<string>;
139
139
  existsBySourceUri(uri: string): boolean;
140
140
  getBySourceUri(uri: string): Route;
141
- addRoute(route: Route, source?: 'user' | 'system'): void;
141
+ addRoute(route: Route, source?: 'user' | 'system' | 'import'): void;
142
142
  protected verifyPoints(card: RouteCard, route: Route): Promise<void>;
143
143
  protected addFromApi(route: Route): Promise<void>;
144
144
  protected update(route: Route, source?: 'user' | 'system'): Promise<void>;
@@ -168,7 +168,7 @@ export declare class RouteListService extends IncyclistService implements IRoute
168
168
  protected checkExistingPreviewFiles(descr: RouteInfo): Promise<string | undefined>;
169
169
  protected doCreatePreview(descr: RouteInfo): Promise<string>;
170
170
  protected processPreviewQueue(): PromiseObserver<void>;
171
- protected findCard(target: Route | string): {
171
+ findCard(target: Route | string): {
172
172
  card: RouteCard;
173
173
  list: CardList<Route>;
174
174
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "incyclist-services",
3
- "version": "1.7.138",
3
+ "version": "1.7.139",
4
4
  "peerDependencies": {
5
5
  "gd-eventlog": "^0.1.27"
6
6
  },