incyclist-services 1.7.89 → 1.7.91

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.
@@ -48,6 +48,7 @@ const incyclist_devices_1 = require("incyclist-devices");
48
48
  const devices_1 = require("../../devices");
49
49
  const base_1 = require("../base");
50
50
  const uuid_1 = require("uuid");
51
+ const workouts_1 = require("../../workouts");
51
52
  const route_1 = require("../../routes/base/utils/route");
52
53
  const route_2 = require("../../routes/base/model/route");
53
54
  const stats_1 = require("./stats");
@@ -425,14 +426,15 @@ let ActivityRideService = (() => {
425
426
  const route = this.getRouteList().getSelected();
426
427
  const startSettings = this.getRouteList().getStartSettings();
427
428
  const isFreeRide = startSettings?.type === 'Free-Ride';
428
- const isWorkout = this.activity?.routeType === 'None';
429
+ const showWorkoutSummary = this.activity?.routeType === 'None';
429
430
  const showSave = this.activity !== undefined && !this.isSaveDone;
430
431
  const showContinue = this.state !== 'completed';
431
432
  const hasGPX = this.activity?.logs?.some((log) => !!log.lat && !!log.lng);
432
- const showMap = hasGPX || isFreeRide || isWorkout || (route?.description?.hasGpx);
433
+ const showMap = hasGPX || isFreeRide || (route?.description?.hasGpx);
433
434
  const preview = showMap ? undefined : route?.description?.previewUrl;
435
+ const workoutGraph = showWorkoutSummary ? this.buildWorkoutSummaryGraph(this.activity) : undefined;
434
436
  if (!this.isSummaryShown) {
435
- this.logEvent({ message: 'activity summary shown', showSave, showContinue, showMap, fileLink: this.activity?.fitFileName });
437
+ this.logEvent({ message: 'activity summary shown', showSave, showContinue, showMap, showWorkoutSummary, fileLink: this.activity?.fitFileName });
436
438
  }
437
439
  this.isSummaryShown = true;
438
440
  const props = {
@@ -440,6 +442,8 @@ let ActivityRideService = (() => {
440
442
  showSave,
441
443
  showContinue,
442
444
  showMap,
445
+ showWorkoutSummary,
446
+ workoutGraph,
443
447
  showDonate: this.canShowDonate(),
444
448
  preview,
445
449
  units: this.getUnitConverter().getDefaultUnits()
@@ -451,6 +455,34 @@ let ActivityRideService = (() => {
451
455
  return {};
452
456
  }
453
457
  }
458
+ buildWorkoutSummaryGraph(activity) {
459
+ const workout = activity?.workout;
460
+ if (!workout)
461
+ return undefined;
462
+ const ftp = activity.user?.ftp ?? 0;
463
+ const bars = (0, workouts_1.getWorkoutGraphSeries)(workout, { ftp, absValues: true });
464
+ const lastBarX = bars.length ? bars.at(-1).x : 0;
465
+ const lastLogTime = activity.logs?.length ? activity.logs.at(-1).time : 0;
466
+ const maxXCandidates = [lastLogTime, lastBarX, workout.duration ?? 0].filter(Number.isFinite);
467
+ const maxX = maxXCandidates.length ? Math.max(...maxXCandidates) : 0;
468
+ const maxBarPower = bars.length ? Math.max(...bars.map(b => b.y)) : 0;
469
+ const plan = {
470
+ bars,
471
+ ftp,
472
+ ftpLine: ftp,
473
+ domain: { x: [0, maxX], y: [0, maxBarPower * 1.1] }
474
+ };
475
+ const actuals = {
476
+ power: (activity.logs ?? [])
477
+ .filter(log => Number.isFinite(log.time) && Number.isFinite(log.power))
478
+ .map(log => ({ x: log.time, y: log.power })),
479
+ heartrate: (activity.logs ?? [])
480
+ .filter(log => Number.isFinite(log.time) && Number.isFinite(log.heartrate))
481
+ .map(log => ({ x: log.time, y: log.heartrate })),
482
+ position: -1
483
+ };
484
+ return { plan, actuals };
485
+ }
454
486
  canShowDonate() {
455
487
  try {
456
488
  if (this.isDonateShown) {
@@ -222,8 +222,8 @@ let DeviceConfigurationService = (() => {
222
222
  if (c.capability === 'bike')
223
223
  return;
224
224
  const capability = c.capability;
225
- if (!capabilties.includes(capability) && c.selected !== udid) {
226
- this.delete(udid, capability);
225
+ if (!capabilties.includes(capability)) {
226
+ this.delete(udid, capability, true);
227
227
  }
228
228
  });
229
229
  this.updateUserSettings();
@@ -208,7 +208,7 @@ let UserInterfaceServcie = (() => {
208
208
  try {
209
209
  this.startQueueWorker();
210
210
  this.startHeartbeatWorker();
211
- const sent = false;
211
+ let sent = false;
212
212
  const user = this.getUserSettings().getValue('user', {});
213
213
  const id = this.getUserSettings().getValue('uuid', undefined);
214
214
  this.getFeatureToggleSync().start(id);
@@ -223,7 +223,7 @@ let UserInterfaceServcie = (() => {
223
223
  started: new Date().toISOString()
224
224
  };
225
225
  if (this.isMobile() || this.isOnline()) {
226
- this.sendMessage(topic, payload);
226
+ sent = this.sendMessage(topic, payload);
227
227
  }
228
228
  if (!sent) {
229
229
  this.queueMessage(topic, payload);
@@ -364,7 +364,8 @@ let UserInterfaceServcie = (() => {
364
364
  version: this.version,
365
365
  appVersion: appInfo.getAppVersion(),
366
366
  uuid: this.getUserSettings().get('uuid', undefined),
367
- session: this.session
367
+ session: this.session,
368
+ 'app-channel': this.platform
368
369
  };
369
370
  this.logger.logEvent({ message: 'setting globals', ...globals });
370
371
  this.logger.setGlobal(globals);
@@ -93,7 +93,11 @@ let WorkoutListPageService = (() => {
93
93
  openPage() {
94
94
  try {
95
95
  super.openPage();
96
- this.logEvent({ message: 'page shown', page: 'Workouts' });
96
+ let pageType;
97
+ if (!this.getAppState().hasFeature('MOBILE_WORKOUTS')) {
98
+ pageType = 'placeholder';
99
+ }
100
+ this.logEvent({ message: 'page shown', page: 'Workouts', pageType });
97
101
  const { observer } = this.getWorkoutList().open();
98
102
  this.listObserver = observer;
99
103
  this.listObserver?.on('started', this.listUpdateHandler);
@@ -323,10 +327,12 @@ let WorkoutListPageService = (() => {
323
327
  this.importPhase = 'importing';
324
328
  this.importingFileName = file?.name;
325
329
  this.emitImportUpdate();
330
+ this.logEvent({ message: 'workout import attempted', file: file?.name });
326
331
  this.getWorkoutList().import(file, { showImportCards: false })
327
332
  .then(([card]) => {
328
333
  const group = this.getLastUsedImportGroup();
329
334
  observer.emit('success');
335
+ this.logEvent({ message: 'workout import succeeded', file: file?.name, workoutName: card.getTitle(), group });
330
336
  if (generation !== this.importGeneration)
331
337
  return;
332
338
  if (group !== DEFAULT_IMPORT_GROUP)
@@ -337,6 +343,7 @@ let WorkoutListPageService = (() => {
337
343
  })
338
344
  .catch((err) => {
339
345
  observer.emit('error', err);
346
+ this.logEvent({ message: 'workout import failed', file: file?.name, error: err?.message });
340
347
  if (generation !== this.importGeneration)
341
348
  return;
342
349
  this.importPhase = 'error';
@@ -91,8 +91,8 @@ let WorkoutRidePageService = (() => {
91
91
  this.workoutEventHandler['update'] = this.onWorkoutUpdate.bind(this);
92
92
  this.workoutEventHandler['forward'] = this.onWorkoutUpdate.bind(this);
93
93
  this.workoutEventHandler['backward'] = this.onWorkoutUpdate.bind(this);
94
- this.workoutEventHandler['completed'] = this.onWorkoutFinished.bind(this);
95
- this.workoutEventHandler['stopped'] = this.onWorkoutFinished.bind(this);
94
+ this.workoutEventHandler['completed'] = this.onFinished.bind(this);
95
+ this.workoutEventHandler['stopped'] = this.onFinished.bind(this);
96
96
  }
97
97
  openPage(simulate) {
98
98
  try {
@@ -119,7 +119,7 @@ let WorkoutRidePageService = (() => {
119
119
  try {
120
120
  gd_eventlog_1.EventLogger.setGlobalConfig('page', null);
121
121
  this.logEvent({ message: 'page closed', page: 'WorkoutRide' });
122
- this.getRideDisplay().stop(true);
122
+ this.getRideDisplay().stop();
123
123
  this.unregisterRideEventHandlers();
124
124
  this.unsubscribeFromWorkoutObserver();
125
125
  this.menuProps = null;
@@ -243,8 +243,7 @@ let WorkoutRidePageService = (() => {
243
243
  }
244
244
  onStop() {
245
245
  try {
246
- this.getRideDisplay().stop(true);
247
- this.emitNavigateBack();
246
+ this.finishRide();
248
247
  }
249
248
  catch (err) {
250
249
  this.logError(err, 'onStop');
@@ -353,7 +352,17 @@ let WorkoutRidePageService = (() => {
353
352
  onWorkoutUpdate() {
354
353
  this.updatePageDisplay();
355
354
  }
356
- onWorkoutFinished() {
355
+ onFinished() {
356
+ try {
357
+ this.menuProps = { showResume: false, finished: true, canStepBack: false, canStepForward: false };
358
+ this.updatePageDisplay();
359
+ }
360
+ catch (err) {
361
+ this.logError(err, 'onFinished');
362
+ }
363
+ }
364
+ finishRide() {
365
+ this.getRideDisplay().stop(true);
357
366
  this.emitNavigateBack();
358
367
  }
359
368
  subscribeToWorkoutObserver() {
@@ -42,6 +42,7 @@ import { IncyclistCapability } from "incyclist-devices";
42
42
  import { useDeviceConfiguration, useDeviceRide } from "../../devices";
43
43
  import { ActivitiesRepository, ActivityConverter, ActivityConverterFactory, DB_VERSION, DEFAULT_SPORT_ACTIVITY_TITLE } from "../base";
44
44
  import { v4 as generateUUID } from 'uuid';
45
+ import { getWorkoutGraphSeries } from "../../workouts";
45
46
  import { addDetails, checkIsLoop, getElevationGainAt, getNextPosition, getPosition, getRouteHash, validateRoute } from "../../routes/base/utils/route";
46
47
  import { Route } from "../../routes/base/model/route";
47
48
  import { ActivityStatsCalculator } from "./stats";
@@ -419,14 +420,15 @@ let ActivityRideService = (() => {
419
420
  const route = this.getRouteList().getSelected();
420
421
  const startSettings = this.getRouteList().getStartSettings();
421
422
  const isFreeRide = startSettings?.type === 'Free-Ride';
422
- const isWorkout = this.activity?.routeType === 'None';
423
+ const showWorkoutSummary = this.activity?.routeType === 'None';
423
424
  const showSave = this.activity !== undefined && !this.isSaveDone;
424
425
  const showContinue = this.state !== 'completed';
425
426
  const hasGPX = this.activity?.logs?.some((log) => !!log.lat && !!log.lng);
426
- const showMap = hasGPX || isFreeRide || isWorkout || (route?.description?.hasGpx);
427
+ const showMap = hasGPX || isFreeRide || (route?.description?.hasGpx);
427
428
  const preview = showMap ? undefined : route?.description?.previewUrl;
429
+ const workoutGraph = showWorkoutSummary ? this.buildWorkoutSummaryGraph(this.activity) : undefined;
428
430
  if (!this.isSummaryShown) {
429
- this.logEvent({ message: 'activity summary shown', showSave, showContinue, showMap, fileLink: this.activity?.fitFileName });
431
+ this.logEvent({ message: 'activity summary shown', showSave, showContinue, showMap, showWorkoutSummary, fileLink: this.activity?.fitFileName });
430
432
  }
431
433
  this.isSummaryShown = true;
432
434
  const props = {
@@ -434,6 +436,8 @@ let ActivityRideService = (() => {
434
436
  showSave,
435
437
  showContinue,
436
438
  showMap,
439
+ showWorkoutSummary,
440
+ workoutGraph,
437
441
  showDonate: this.canShowDonate(),
438
442
  preview,
439
443
  units: this.getUnitConverter().getDefaultUnits()
@@ -445,6 +449,34 @@ let ActivityRideService = (() => {
445
449
  return {};
446
450
  }
447
451
  }
452
+ buildWorkoutSummaryGraph(activity) {
453
+ const workout = activity?.workout;
454
+ if (!workout)
455
+ return undefined;
456
+ const ftp = activity.user?.ftp ?? 0;
457
+ const bars = getWorkoutGraphSeries(workout, { ftp, absValues: true });
458
+ const lastBarX = bars.length ? bars.at(-1).x : 0;
459
+ const lastLogTime = activity.logs?.length ? activity.logs.at(-1).time : 0;
460
+ const maxXCandidates = [lastLogTime, lastBarX, workout.duration ?? 0].filter(Number.isFinite);
461
+ const maxX = maxXCandidates.length ? Math.max(...maxXCandidates) : 0;
462
+ const maxBarPower = bars.length ? Math.max(...bars.map(b => b.y)) : 0;
463
+ const plan = {
464
+ bars,
465
+ ftp,
466
+ ftpLine: ftp,
467
+ domain: { x: [0, maxX], y: [0, maxBarPower * 1.1] }
468
+ };
469
+ const actuals = {
470
+ power: (activity.logs ?? [])
471
+ .filter(log => Number.isFinite(log.time) && Number.isFinite(log.power))
472
+ .map(log => ({ x: log.time, y: log.power })),
473
+ heartrate: (activity.logs ?? [])
474
+ .filter(log => Number.isFinite(log.time) && Number.isFinite(log.heartrate))
475
+ .map(log => ({ x: log.time, y: log.heartrate })),
476
+ position: -1
477
+ };
478
+ return { plan, actuals };
479
+ }
448
480
  canShowDonate() {
449
481
  try {
450
482
  if (this.isDonateShown) {
@@ -216,8 +216,8 @@ let DeviceConfigurationService = (() => {
216
216
  if (c.capability === 'bike')
217
217
  return;
218
218
  const capability = c.capability;
219
- if (!capabilties.includes(capability) && c.selected !== udid) {
220
- this.delete(udid, capability);
219
+ if (!capabilties.includes(capability)) {
220
+ this.delete(udid, capability, true);
221
221
  }
222
222
  });
223
223
  this.updateUserSettings();
@@ -205,7 +205,7 @@ let UserInterfaceServcie = (() => {
205
205
  try {
206
206
  this.startQueueWorker();
207
207
  this.startHeartbeatWorker();
208
- const sent = false;
208
+ let sent = false;
209
209
  const user = this.getUserSettings().getValue('user', {});
210
210
  const id = this.getUserSettings().getValue('uuid', undefined);
211
211
  this.getFeatureToggleSync().start(id);
@@ -220,7 +220,7 @@ let UserInterfaceServcie = (() => {
220
220
  started: new Date().toISOString()
221
221
  };
222
222
  if (this.isMobile() || this.isOnline()) {
223
- this.sendMessage(topic, payload);
223
+ sent = this.sendMessage(topic, payload);
224
224
  }
225
225
  if (!sent) {
226
226
  this.queueMessage(topic, payload);
@@ -361,7 +361,8 @@ let UserInterfaceServcie = (() => {
361
361
  version: this.version,
362
362
  appVersion: appInfo.getAppVersion(),
363
363
  uuid: this.getUserSettings().get('uuid', undefined),
364
- session: this.session
364
+ session: this.session,
365
+ 'app-channel': this.platform
365
366
  };
366
367
  this.logger.logEvent({ message: 'setting globals', ...globals });
367
368
  this.logger.setGlobal(globals);
@@ -90,7 +90,11 @@ let WorkoutListPageService = (() => {
90
90
  openPage() {
91
91
  try {
92
92
  super.openPage();
93
- this.logEvent({ message: 'page shown', page: 'Workouts' });
93
+ let pageType;
94
+ if (!this.getAppState().hasFeature('MOBILE_WORKOUTS')) {
95
+ pageType = 'placeholder';
96
+ }
97
+ this.logEvent({ message: 'page shown', page: 'Workouts', pageType });
94
98
  const { observer } = this.getWorkoutList().open();
95
99
  this.listObserver = observer;
96
100
  this.listObserver?.on('started', this.listUpdateHandler);
@@ -320,10 +324,12 @@ let WorkoutListPageService = (() => {
320
324
  this.importPhase = 'importing';
321
325
  this.importingFileName = file?.name;
322
326
  this.emitImportUpdate();
327
+ this.logEvent({ message: 'workout import attempted', file: file?.name });
323
328
  this.getWorkoutList().import(file, { showImportCards: false })
324
329
  .then(([card]) => {
325
330
  const group = this.getLastUsedImportGroup();
326
331
  observer.emit('success');
332
+ this.logEvent({ message: 'workout import succeeded', file: file?.name, workoutName: card.getTitle(), group });
327
333
  if (generation !== this.importGeneration)
328
334
  return;
329
335
  if (group !== DEFAULT_IMPORT_GROUP)
@@ -334,6 +340,7 @@ let WorkoutListPageService = (() => {
334
340
  })
335
341
  .catch((err) => {
336
342
  observer.emit('error', err);
343
+ this.logEvent({ message: 'workout import failed', file: file?.name, error: err?.message });
337
344
  if (generation !== this.importGeneration)
338
345
  return;
339
346
  this.importPhase = 'error';
@@ -88,8 +88,8 @@ let WorkoutRidePageService = (() => {
88
88
  this.workoutEventHandler['update'] = this.onWorkoutUpdate.bind(this);
89
89
  this.workoutEventHandler['forward'] = this.onWorkoutUpdate.bind(this);
90
90
  this.workoutEventHandler['backward'] = this.onWorkoutUpdate.bind(this);
91
- this.workoutEventHandler['completed'] = this.onWorkoutFinished.bind(this);
92
- this.workoutEventHandler['stopped'] = this.onWorkoutFinished.bind(this);
91
+ this.workoutEventHandler['completed'] = this.onFinished.bind(this);
92
+ this.workoutEventHandler['stopped'] = this.onFinished.bind(this);
93
93
  }
94
94
  openPage(simulate) {
95
95
  try {
@@ -116,7 +116,7 @@ let WorkoutRidePageService = (() => {
116
116
  try {
117
117
  EventLogger.setGlobalConfig('page', null);
118
118
  this.logEvent({ message: 'page closed', page: 'WorkoutRide' });
119
- this.getRideDisplay().stop(true);
119
+ this.getRideDisplay().stop();
120
120
  this.unregisterRideEventHandlers();
121
121
  this.unsubscribeFromWorkoutObserver();
122
122
  this.menuProps = null;
@@ -240,8 +240,7 @@ let WorkoutRidePageService = (() => {
240
240
  }
241
241
  onStop() {
242
242
  try {
243
- this.getRideDisplay().stop(true);
244
- this.emitNavigateBack();
243
+ this.finishRide();
245
244
  }
246
245
  catch (err) {
247
246
  this.logError(err, 'onStop');
@@ -350,7 +349,17 @@ let WorkoutRidePageService = (() => {
350
349
  onWorkoutUpdate() {
351
350
  this.updatePageDisplay();
352
351
  }
353
- onWorkoutFinished() {
352
+ onFinished() {
353
+ try {
354
+ this.menuProps = { showResume: false, finished: true, canStepBack: false, canStepForward: false };
355
+ this.updatePageDisplay();
356
+ }
357
+ catch (err) {
358
+ this.logError(err, 'onFinished');
359
+ }
360
+ }
361
+ finishRide() {
362
+ this.getRideDisplay().stop(true);
354
363
  this.emitNavigateBack();
355
364
  }
356
365
  subscribeToWorkoutObserver() {
@@ -6,7 +6,7 @@ import { ActivitiesRepository, ActivityConverterFactory, ActivityDetails, Activi
6
6
  import { FreeRideStartSettings } from "../../routes/list/types";
7
7
  import { RouteSettings } from "../../routes/list/cards/types";
8
8
  import { RoutePoint } from "../../routes/base/types";
9
- import { ActivityDashboardDataItem, ActivityState, ActivitySummaryDisplayProperties } from "./types";
9
+ import { ActivityDashboardDataItem, ActivityState, ActivitySummaryDisplayProperties, ActivityWorkoutSummaryGraph } from "./types";
10
10
  import { Route } from "../../routes/base/model/route";
11
11
  import { ActivityStatsCalculator } from "./stats";
12
12
  import { ActivityDuration } from "./duration";
@@ -146,6 +146,7 @@ export declare class ActivityRideService extends IncyclistService {
146
146
  elevationGain?: undefined;
147
147
  };
148
148
  getActivitySummaryDisplayProperties(): ActivitySummaryDisplayProperties;
149
+ protected buildWorkoutSummaryGraph(activity?: ActivityDetails): ActivityWorkoutSummaryGraph | undefined;
149
150
  protected canShowDonate(): boolean;
150
151
  onDonateClicked(): void;
151
152
  getActivity(): ActivityDetails;
@@ -2,12 +2,19 @@ import { HealthStatus } from "../../devices";
2
2
  import { Dimension, Unit } from "../../i18n";
3
3
  import { RoutePoint } from "../../types";
4
4
  import { ActivityDetailsUI } from "../base";
5
+ import { WorkoutGraphActuals, WorkoutGraphPlan } from "../../workouts";
5
6
  export type ActivityState = 'ininitalized' | 'active' | 'paused' | 'completed' | 'idle';
7
+ export type ActivityWorkoutSummaryGraph = {
8
+ plan: WorkoutGraphPlan;
9
+ actuals: WorkoutGraphActuals;
10
+ };
6
11
  export type ActivitySummaryDisplayProperties = {
7
12
  activity?: ActivityDetailsUI;
8
13
  showSave?: boolean;
9
14
  showContinue?: boolean;
10
15
  showMap?: boolean;
16
+ showWorkoutSummary?: boolean;
17
+ workoutGraph?: ActivityWorkoutSummaryGraph;
11
18
  preview?: string;
12
19
  units?: Record<Dimension, Unit>;
13
20
  };
@@ -40,7 +40,8 @@ export declare class WorkoutRidePageService extends IncyclistPageService impleme
40
40
  adjustLoad(deltaPct: number): PowerAdjustmentResult | undefined;
41
41
  protected onRideStateUpdate(state: CurrentRideState): void;
42
42
  protected onWorkoutUpdate(): void;
43
- protected onWorkoutFinished(): void;
43
+ protected onFinished(): void;
44
+ protected finishRide(): void;
44
45
  protected subscribeToWorkoutObserver(): void;
45
46
  protected unsubscribeFromWorkoutObserver(): void;
46
47
  protected registerRideEventHandlers(): void;
@@ -1,6 +1,6 @@
1
1
  import type { IObserver } from "../../../base/typedefs";
2
2
  import type { IPageService } from "../../../base/pages";
3
- import type { RidePageDisplayProps } from "../../../ride/page/types";
3
+ import type { RideMenuProps, RidePageDisplayProps } from "../../../ride/page/types";
4
4
  import type { WorkoutGraphPlanBar } from "../../base/graph/types";
5
5
  export interface WorkoutGraphPoint {
6
6
  x: number;
@@ -37,8 +37,7 @@ export interface WorkoutDashboardLine {
37
37
  text: string;
38
38
  mode: string | null;
39
39
  }
40
- export interface WorkoutRideMenuProps {
41
- showResume: boolean;
40
+ export interface WorkoutRideMenuProps extends RideMenuProps {
42
41
  canStepBack: boolean;
43
42
  canStepForward: boolean;
44
43
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "incyclist-services",
3
- "version": "1.7.89",
3
+ "version": "1.7.91",
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.25",
10
+ "incyclist-devices": "^3.0.27",
11
11
  "promise.any": "^2.0.6",
12
12
  "semver": "^7.7.4",
13
13
  "tcx-builder": "^1.1.1",