From c5da5a4e01e7cf23432d5066950a75270a3e0014 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:51:45 +0100 Subject: [PATCH 01/35] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a8be08a0..6b32e1f3 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,10 @@ services: ## FAQ ### General -* Ramp steps in TrainerRoad are not supported -* **MacOS** app is not signed. Usually you need to open it twice. After opening it, be patient, it takes some time to - start +* Ramp steps in TrainerRoad are not supported +* **MacOS arm64** Error: `"tp2intervals" is damaged and can’t be opened.` + Run command in terminal `xattr -d com.apple.quarantine /Applications/tp2intervals.app` and then open app again +* **MacOS** app is not signed. Usually you need to open it twice * **Windows** The app will ask to access local network and Internet, you need to allow it. After all it makes HTTP requests * More info you can find on the forum https://forum.intervals.icu/t/tp2intervals-copy-trainingpeaks-and-trainerroad-workouts-plans-to-intervals/63375 From f321c8bd6b3598d35d3ab90888ec8a92a0022900 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Sat, 18 Jan 2025 19:01:51 +0100 Subject: [PATCH 02/35] Scheduled job for workout sync (#93) --- .../org/freekode/tp2intervals/Application.kt | 2 + .../CopyFromCalendarToCalendarRequest.kt | 14 -- .../app/workout/WorkoutService.kt | 11 +- ...yFromCalendarToCalendarScheduledRequest.kt | 23 +++ .../app/workout/scheduled/Schedulable.kt | 4 + .../workout/scheduled/WorkoutJobScheduler.kt | 39 +++++ .../tp2intervals/domain/ExternalData.kt | 2 +- .../workout/FromIntervalsWorkoutConverter.kt | 8 +- .../workout/ToIntervalsWorkoutConverter.kt | 3 +- .../workout/TPToWorkoutConverter.kt | 2 +- .../configuration/ConfigurationController.kt | 12 +- .../rest/library/LibraryController.kt | 4 +- .../rest/workout/WorkoutController.kt | 14 +- .../workout/WorkoutJobSchedulerController.kt | 21 +++ .../workout/TrainingPeaksWorkoutServiceIT.kt | 5 +- .../app/workout/WorkoutJobSchedulerIT.kt | 28 ++++ .../tp2intervals/domain/ExternalDataTest.kt | 6 +- .../copy-calendar-to-calendar.component.html | 109 +++++++++++++ .../copy-calendar-to-calendar.component.scss | 5 + .../copy-calendar-to-calendar.component.ts | 147 ++++++++++++++++++ ...r-copy-calendar-to-calendar.component.html | 83 +--------- ...r-copy-calendar-to-calendar.component.scss | 5 - .../tr-copy-calendar-to-calendar.component.ts | 96 ++---------- .../tr-copy-calendar-to-library.component.ts | 22 +-- .../tr-copy-library-to-library.component.ts | 2 - .../trainer-road-training-types.ts | 7 - ...p-copy-calendar-to-calendar.component.html | 86 +--------- ...p-copy-calendar-to-calendar.component.scss | 5 - .../tp-copy-calendar-to-calendar.component.ts | 97 +++--------- .../tp-copy-calendar-to-library.component.ts | 27 ++-- .../training-peaks-training-types.ts | 12 -- ui/src/app/training-types.ts | 16 ++ .../infrastructure/client/workout.client.ts | 14 ++ ui/src/infrastructure/platform.ts | 7 + 34 files changed, 524 insertions(+), 414 deletions(-) delete mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToCalendarRequest.kt create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/CopyFromCalendarToCalendarScheduledRequest.kt create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/Schedulable.kt create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutJobSchedulerController.kt create mode 100644 boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt create mode 100644 ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html create mode 100644 ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.scss create mode 100644 ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts delete mode 100644 ui/src/app/trainer-road/trainer-road-training-types.ts delete mode 100644 ui/src/app/training-peaks/training-peaks-training-types.ts create mode 100644 ui/src/app/training-types.ts diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/Application.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/Application.kt index eb6a2abc..6b06d600 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/Application.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/Application.kt @@ -6,10 +6,12 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.runApplication import org.springframework.cache.annotation.EnableCaching import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.scheduling.annotation.EnableScheduling @SpringBootApplication @EnableFeignClients @EnableCaching +@EnableScheduling @EnableConfigurationProperties(DefaultConfiguration::class) class Application diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToCalendarRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToCalendarRequest.kt deleted file mode 100644 index 97391230..00000000 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToCalendarRequest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.freekode.tp2intervals.app.workout - -import org.freekode.tp2intervals.domain.Platform -import org.freekode.tp2intervals.domain.TrainingType -import java.time.LocalDate - -data class CopyFromCalendarToCalendarRequest( - val startDate: LocalDate, - val endDate: LocalDate, - val types: List, - val skipSynced: Boolean, - val sourcePlatform: Platform, - val targetPlatform: Platform -) diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt index 54f60c4b..16dc0f8f 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt @@ -1,11 +1,13 @@ package org.freekode.tp2intervals.app.workout +import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest import org.freekode.tp2intervals.domain.ExternalData import org.freekode.tp2intervals.domain.Platform import org.freekode.tp2intervals.domain.librarycontainer.LibraryContainerRepository import org.freekode.tp2intervals.domain.workout.WorkoutDetails import org.freekode.tp2intervals.domain.workout.WorkoutRepository import org.freekode.tp2intervals.rest.workout.DeleteWorkoutRequestDTO +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.time.LocalDate @@ -14,10 +16,12 @@ class WorkoutService( workoutRepositories: List, planRepositories: List, ) { + private val log = LoggerFactory.getLogger(this.javaClass) private val workoutRepositoryMap = workoutRepositories.associateBy { it.platform() } private val planRepositoryMap = planRepositories.associateBy { it.platform() } - fun copyWorkoutsFromCalendarToCalendar(request: CopyFromCalendarToCalendarRequest): CopyWorkoutsResponse { + fun copyWorkoutsFromCalendarToCalendar(request: CopyFromCalendarToCalendarScheduledRequest): CopyWorkoutsResponse { + log.info("Received request for copy calendar to calendar: $request") val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!! val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!! @@ -36,10 +40,12 @@ class WorkoutService( ExternalData.empty() // TODO figure smth better ) targetWorkoutRepository.saveWorkoutsToCalendar(filteredWorkoutsToSync) + log.info("Saved workouts to calendar successfully: $response") return response } fun copyWorkoutsFromCalendarToLibrary(request: CopyFromCalendarToLibraryRequest): CopyWorkoutsResponse { + log.info("Received request for copy calendar to library: $request") val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!! val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!! val targetPlanRepository = planRepositoryMap[request.targetPlatform]!! @@ -59,6 +65,7 @@ class WorkoutService( } fun copyWorkoutFromLibraryToLibrary(request: CopyFromLibraryToLibraryRequest): CopyWorkoutsResponse { + log.info("Received request for copy library to library: $request") val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!! val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!! @@ -68,10 +75,12 @@ class WorkoutService( } fun findWorkoutsByName(platform: Platform, name: String): List { + log.info("Received request for find workouts by name, platform: $platform, name: $name") return workoutRepositoryMap[platform]!!.findWorkoutsFromLibraryByName(name) } fun deleteWorkoutsFromCalendar(request: DeleteWorkoutRequestDTO) { + log.info("Received request to delete workouts from calendar: $request") val workoutRepository = workoutRepositoryMap[request.platform]!! workoutRepository.deleteWorkoutsFromCalendar(request.startDate, request.endDate) } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/CopyFromCalendarToCalendarScheduledRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/CopyFromCalendarToCalendarScheduledRequest.kt new file mode 100644 index 00000000..5586885d --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/CopyFromCalendarToCalendarScheduledRequest.kt @@ -0,0 +1,23 @@ +package org.freekode.tp2intervals.app.workout.scheduled + +import org.freekode.tp2intervals.domain.Platform +import org.freekode.tp2intervals.domain.TrainingType +import java.time.LocalDate + +data class CopyFromCalendarToCalendarScheduledRequest( + val startDate: LocalDate, + val endDate: LocalDate, + val types: List, + val skipSynced: Boolean, + val sourcePlatform: Platform, + val targetPlatform: Platform +) : Schedulable { + fun forToday() = CopyFromCalendarToCalendarScheduledRequest( + LocalDate.now(), + LocalDate.now(), + types, + skipSynced, + sourcePlatform, + targetPlatform + ) +} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/Schedulable.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/Schedulable.kt new file mode 100644 index 00000000..24588bdb --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/Schedulable.kt @@ -0,0 +1,4 @@ +package org.freekode.tp2intervals.app.workout.scheduled + + +interface Schedulable diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt new file mode 100644 index 00000000..5fe80624 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt @@ -0,0 +1,39 @@ +package org.freekode.tp2intervals.app.workout.scheduled + +import org.freekode.tp2intervals.app.workout.WorkoutService +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.util.concurrent.TimeUnit + +@Service +class WorkoutJobScheduler( + private val workoutService: WorkoutService +) { + private val log = LoggerFactory.getLogger(this.javaClass) + private val scheduledRequests = mutableListOf() + + fun addRequest(schedulable: Schedulable) = + scheduledRequests.add(schedulable) + + fun getRequests() = + scheduledRequests.toList() + + @Scheduled(fixedRate = 20, timeUnit = TimeUnit.MINUTES) + fun job() { + val requests = getRequests() + log.info("Starting processing scheduled requests. There are ${requests.size} requests") + + for (request in requests) { + if (request is CopyFromCalendarToCalendarScheduledRequest) { + handleCopyCalendarToCalendarRequest(request) + } + } + + log.info("Finished processing scheduled requests"); + } + + private fun handleCopyCalendarToCalendarRequest(request: CopyFromCalendarToCalendarScheduledRequest) { + workoutService.copyWorkoutsFromCalendarToCalendar(request.forToday()) + } +} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/domain/ExternalData.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/domain/ExternalData.kt index 89d4fe4a..451fb56e 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/domain/ExternalData.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/domain/ExternalData.kt @@ -19,7 +19,7 @@ data class ExternalData( fun withTrainerRoad(trainerRoadId: String) = ExternalData(trainingPeaksId, intervalsId, trainerRoadId) - fun withSimpleString(string: String): ExternalData { + fun fromSimpleString(string: String): ExternalData { val split = string.split(externalDataDescriptionSeparator) if (split.size != 2) { return this diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/FromIntervalsWorkoutConverter.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/FromIntervalsWorkoutConverter.kt index ce984998..63db0029 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/FromIntervalsWorkoutConverter.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/FromIntervalsWorkoutConverter.kt @@ -3,7 +3,11 @@ package org.freekode.tp2intervals.infrastructure.platform.intervalsicu.workout import org.freekode.tp2intervals.domain.ExternalData import org.freekode.tp2intervals.domain.workout.Workout import org.freekode.tp2intervals.domain.workout.WorkoutDetails -import org.freekode.tp2intervals.domain.workout.structure.* +import org.freekode.tp2intervals.domain.workout.structure.MultiStep +import org.freekode.tp2intervals.domain.workout.structure.SingleStep +import org.freekode.tp2intervals.domain.workout.structure.StepLength +import org.freekode.tp2intervals.domain.workout.structure.WorkoutStep +import org.freekode.tp2intervals.domain.workout.structure.WorkoutStructure class FromIntervalsWorkoutConverter( private val eventDTO: IntervalsEventDTO @@ -20,7 +24,7 @@ class FromIntervalsWorkoutConverter( eventDTO.description, eventDTO.mapDuration(), eventDTO.icu_training_load, - ExternalData.empty().withIntervals(eventDTO.id.toString()) + ExternalData.empty().withIntervals(eventDTO.id.toString()).fromSimpleString(eventDTO.description ?: "") ), eventDTO.start_date_local.toLocalDate(), workoutsStructure, diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/ToIntervalsWorkoutConverter.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/ToIntervalsWorkoutConverter.kt index 23ebdd9a..82f3988f 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/ToIntervalsWorkoutConverter.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/intervalsicu/workout/ToIntervalsWorkoutConverter.kt @@ -1,10 +1,10 @@ package org.freekode.tp2intervals.infrastructure.platform.intervalsicu.workout -import java.time.LocalDate import org.freekode.tp2intervals.domain.librarycontainer.LibraryContainer import org.freekode.tp2intervals.domain.workout.Workout import org.freekode.tp2intervals.infrastructure.Signature import org.freekode.tp2intervals.infrastructure.utils.Date +import java.time.LocalDate class ToIntervalsWorkoutConverter { private val unwantedStepRegex = "^[-*]".toRegex(RegexOption.MULTILINE) @@ -53,6 +53,7 @@ class ToIntervalsWorkoutConverter { description += workoutString ?.let { "\n\n- - - -\n$it" } .orEmpty() + description += "\n\n${workout.details.externalData.toSimpleString()}" return description } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/trainingpeaks/workout/TPToWorkoutConverter.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/trainingpeaks/workout/TPToWorkoutConverter.kt index 7d4eae4a..f00d7a9e 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/trainingpeaks/workout/TPToWorkoutConverter.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/platform/trainingpeaks/workout/TPToWorkoutConverter.kt @@ -65,6 +65,6 @@ class TPToWorkoutConverter { } private fun getWorkoutExternalData(tpWorkout: TPBaseWorkoutResponseDTO): ExternalData { - return ExternalData.empty().withTrainingPeaks(tpWorkout.id).withSimpleString(tpWorkout.description ?: "") + return ExternalData.empty().withTrainingPeaks(tpWorkout.id).fromSimpleString(tpWorkout.description ?: "") } } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt index 298dcb59..4eadac25 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt @@ -23,14 +23,14 @@ class ConfigurationController( @GetMapping("/api/configuration") fun getConfigurations(): AppConfigurationDTO { - log.info("Received request for getting all configurations") + log.debug("Received request for getting all configurations") val configurations = configurationService.getConfigurations() return AppConfigurationDTO(configurations.configMap) } @PutMapping("/api/configuration") fun updateConfiguration(@RequestBody requestDTO: UpdateConfigurationRequestDTO): ResponseEntity { - log.info("Received request for updating configuration: $requestDTO") + log.debug("Received request for updating configuration: {}", requestDTO) val errors = configurationService.updateConfiguration(UpdateConfigurationRequest(requestDTO.config)) if (errors.isNotEmpty()) { return ResponseEntity.badRequest().body(ErrorResponseDTO(errors.joinToString())) @@ -38,12 +38,6 @@ class ConfigurationController( return ResponseEntity.ok().build() } - @Deprecated("all config on ui") - @GetMapping("/api/configuration/training-types") - fun getTrainingTypes(): List { - return TrainingType.entries.map { TrainingTypeDTO(it) } - } - @GetMapping("/api/configuration/intervals-step-modifiers") fun getIntervalsStepModifiers(): List { return StepModifier.entries @@ -51,7 +45,7 @@ class ConfigurationController( @GetMapping("/api/configuration/{platform}") fun getConfigurations(@PathVariable platform: Platform): PlatformInfo { - log.info("Received request for getting configurations for platform: $platform") + log.debug("Received request for getting configurations for platform: {}", platform) return configurationService.platformInfo(platform) } } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/library/LibraryController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/library/LibraryController.kt index b8f6a8e9..379afbe8 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/library/LibraryController.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/library/LibraryController.kt @@ -20,13 +20,13 @@ class LibraryController( @GetMapping("/api/library-container") fun getLibraryContainers(@RequestParam platform: Platform): List { - log.info("Received request for getting library containers: $platform") + log.debug("Received request for getting library containers: {}", platform) return libraryService.findByPlatform(platform) } @PostMapping("/api/library-container/copy") fun copyLibraryContainer(@RequestBody request: CopyLibraryRequest): CopyPlanResponse { - log.info("Received request to copy the library container: $request") + log.debug("Received request to copy the library container: {}", request) return libraryService.copyLibrary(request) } } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt index b73f3e3e..66dbfe8e 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt @@ -1,12 +1,11 @@ package org.freekode.tp2intervals.rest.workout -import org.freekode.tp2intervals.app.workout.CopyFromCalendarToCalendarRequest +import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest import org.freekode.tp2intervals.app.workout.CopyFromCalendarToLibraryRequest import org.freekode.tp2intervals.app.workout.CopyFromLibraryToLibraryRequest import org.freekode.tp2intervals.app.workout.CopyWorkoutsResponse import org.freekode.tp2intervals.app.workout.WorkoutService import org.freekode.tp2intervals.domain.Platform -import org.slf4j.LoggerFactory import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -16,31 +15,25 @@ import org.springframework.web.bind.annotation.RestController @RestController class WorkoutController( - private val workoutService: WorkoutService + private val workoutService: WorkoutService, ) { - private val log = LoggerFactory.getLogger(this.javaClass) - @PostMapping("/api/workout/copy-calendar-to-calendar") - fun copyWorkoutsFromCalendarToCalendar(@RequestBody request: CopyFromCalendarToCalendarRequest): CopyWorkoutsResponse { - log.info("Received request for copy calendar to calendar: $request") + fun copyWorkoutsFromCalendarToCalendar(@RequestBody request: CopyFromCalendarToCalendarScheduledRequest): CopyWorkoutsResponse { return workoutService.copyWorkoutsFromCalendarToCalendar(request) } @PostMapping("/api/workout/copy-calendar-to-library") fun copyWorkoutsFromCalendarToLibrary(@RequestBody request: CopyFromCalendarToLibraryRequest): CopyWorkoutsResponse { - log.info("Received request for copy calendar to library: $request") return workoutService.copyWorkoutsFromCalendarToLibrary(request) } @PostMapping("/api/workout/copy-library-to-library") fun copyWorkoutFromLibraryToLibrary(@RequestBody request: CopyFromLibraryToLibraryRequest): CopyWorkoutsResponse { - log.info("Received request for copy library to library: $request") return workoutService.copyWorkoutFromLibraryToLibrary(request) } @GetMapping("/api/workout/find") fun findWorkoutsByName(@RequestParam platform: Platform, @RequestParam name: String): List { - log.info("Received request for find workouts by name, platform: $platform, name: $name") return workoutService.findWorkoutsByName(platform, name) .map { workoutDetails -> WorkoutDetailsDTO( @@ -54,7 +47,6 @@ class WorkoutController( @DeleteMapping("/api/workout") fun deleteWorkoutsFromCalendar(@RequestBody request: DeleteWorkoutRequestDTO) { - log.info("Received request to delete workouts from calendar: $request") workoutService.deleteWorkoutsFromCalendar(request) } } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutJobSchedulerController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutJobSchedulerController.kt new file mode 100644 index 00000000..f74b8526 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutJobSchedulerController.kt @@ -0,0 +1,21 @@ +package org.freekode.tp2intervals.rest.workout + +import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest +import org.freekode.tp2intervals.app.workout.scheduled.WorkoutJobScheduler +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class WorkoutJobSchedulerController( + private val workoutJobScheduler: WorkoutJobScheduler +) { + @PostMapping("/api/workout/copy-calendar-to-calendar/schedule") + fun scheduleCopyWorkoutsFromCalendarToCalendar(@RequestBody request: CopyFromCalendarToCalendarScheduledRequest) = + workoutJobScheduler.addRequest(request) + + @GetMapping("/api/workout/copy-calendar-to-calendar/schedule") + fun getScheduledJobsCopyWorkoutsFromCalendarToCalendar() = + workoutJobScheduler.getRequests() +} diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt index 5a2ab0c2..430929a5 100644 --- a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt @@ -4,6 +4,7 @@ import config.BaseSpringITConfig import org.freekode.tp2intervals.app.plan.CopyLibraryRequest import org.freekode.tp2intervals.app.plan.DeleteLibraryRequest import org.freekode.tp2intervals.app.plan.LibraryService +import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest import org.freekode.tp2intervals.domain.Platform import org.freekode.tp2intervals.domain.TrainingType import org.freekode.tp2intervals.domain.workout.structure.StepModifier @@ -30,9 +31,9 @@ class TrainingPeaksWorkoutServiceIT : BaseSpringITConfig() { val deleteRequest = DeleteWorkoutRequestDTO(startDate, endDate, platform) workoutService.deleteWorkoutsFromCalendar(deleteRequest) - val copyRequest = CopyFromCalendarToCalendarRequest( + val copyRequest = CopyFromCalendarToCalendarScheduledRequest( startDate, endDate, - TrainingType.Companion.DEFAULT_LIST, + TrainingType.DEFAULT_LIST, true, Platform.INTERVALS, platform diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt new file mode 100644 index 00000000..804e02d4 --- /dev/null +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt @@ -0,0 +1,28 @@ +package org.freekode.tp2intervals.app.workout + +import config.BaseSpringITConfig +import org.assertj.core.api.Assertions.assertThat +import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest +import org.freekode.tp2intervals.app.workout.scheduled.WorkoutJobScheduler +import org.freekode.tp2intervals.domain.Platform +import org.freekode.tp2intervals.domain.TrainingType +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDate + +class WorkoutJobSchedulerIT : BaseSpringITConfig() { + @Autowired + lateinit var workoutJobScheduler: WorkoutJobScheduler + + @Test + fun test() { + val request = CopyFromCalendarToCalendarScheduledRequest(LocalDate.now(), LocalDate.now(), listOf(TrainingType.BIKE), true, Platform.INTERVALS, Platform.TRAINING_PEAKS) + + workoutJobScheduler.addRequest(request) + + val requests = workoutJobScheduler.getRequests() + + assertThat(requests.isNotEmpty()).isTrue() + assertThat(requests[0] is CopyFromCalendarToCalendarScheduledRequest).isTrue() + } +} diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/domain/ExternalDataTest.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/domain/ExternalDataTest.kt index a50a14a8..181e70cc 100644 --- a/boot/src/test/kotlin/org/freekode/tp2intervals/domain/ExternalDataTest.kt +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/domain/ExternalDataTest.kt @@ -11,7 +11,7 @@ class ExternalDataTest { // when var data = ExternalData(null, null, null) - data = data.withSimpleString(string) + data = data.fromSimpleString(string) // then Assertions.assertNotNull(data) @@ -28,9 +28,9 @@ class ExternalDataTest { // when var data = ExternalData(null, null, null) - data = data.withSimpleString(string) + data = data.fromSimpleString(string) var data1 = ExternalData(null, null, null) - data1 = data1.withSimpleString(string1) + data1 = data1.fromSimpleString(string1) // then Assertions.assertNull(data.trainingPeaksId) diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html new file mode 100644 index 00000000..20414ff4 --- /dev/null +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html @@ -0,0 +1,109 @@ +
+
+ + Direction + + @for (item of directions; track item) { + {{ item.title }} + } + + +
+ +
+ + Workout Types + + @for (type of trainingTypes; track type) { + {{ type.title }} + } + + +
+ +
+ + Date Range + + + + + + + +
+ +
+ + Skip already synced workouts + +
+ +
+ + + +
+ +
+ + + Scheduled jobs + + + @for (job of scheduledJobs; track job) { + + Types: {{ mapTrainingTypesToTitles(job.types) }}, + Skip synced: {{ job.skipSynced }}, + {{ Platform.getTitle(job.sourcePlatform) }} -> {{ Platform.getTitle(job.targetPlatform) }} + + } + +
+ +
+ +
+ + +
diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.scss b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.scss new file mode 100644 index 00000000..1b1e613c --- /dev/null +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.scss @@ -0,0 +1,5 @@ +.date-range-text { + padding-top: 10px; + padding-bottom: 10px; + padding-left: 10px; +} diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts new file mode 100644 index 00000000..2bba94db --- /dev/null +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts @@ -0,0 +1,147 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; +import {MatButtonModule} from "@angular/material/button"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {MatInputModule} from "@angular/material/input"; +import {MatProgressBarModule} from "@angular/material/progress-bar"; +import {MatDatepickerModule} from "@angular/material/datepicker"; +import {MatNativeDateModule} from "@angular/material/core"; +import {MatSnackBarModule} from "@angular/material/snack-bar"; +import {MatSelectModule} from "@angular/material/select"; +import {MatCheckboxModule} from "@angular/material/checkbox"; +import {Platform} from "infrastructure/platform"; +import {formatDate} from "utils/date-formatter"; +import {MatDividerModule} from "@angular/material/divider"; +import {MatListModule} from "@angular/material/list"; +import {NgIf} from "@angular/common"; +import {ConfigurationClient} from "infrastructure/client/configuration.client"; +import {finalize, switchMap, tap} from "rxjs"; +import {WorkoutClient} from "infrastructure/client/workout.client"; +import {NotificationService} from "infrastructure/notification.service"; +import {MatTooltipModule} from "@angular/material/tooltip"; +import {TrainingTypes} from "app/training-types"; + +@Component({ + selector: 'copy-calendar-to-calendar', + standalone: true, + imports: [ + FormsModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + MatProgressBarModule, + MatDatepickerModule, + MatNativeDateModule, + MatSnackBarModule, + MatSelectModule, + MatCheckboxModule, + MatDividerModule, + MatListModule, + NgIf, + MatTooltipModule, + ], + templateUrl: './copy-calendar-to-calendar.component.html', + styleUrl: './copy-calendar-to-calendar.component.scss' +}) +export class CopyCalendarToCalendarComponent implements OnInit { + readonly Platform = Platform; + readonly todayDate = new Date() + readonly tomorrowDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000) + + @Input() platform: any + @Input() trainingTypes: any[] = [] + @Input() selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE'] + @Input() directions: any[] = [] + @Input() inProgress = false + + formGroup: FormGroup + platformInfo: any + scheduledJobs: any[] = [] + + constructor( + private formBuilder: FormBuilder, + private configurationClient: ConfigurationClient, + private workoutClient: WorkoutClient, + private notificationService: NotificationService + ) { + } + + ngOnInit(): void { + this.configurationClient.platformInfo(this.platform.key).subscribe(value => { + this.platformInfo = value + }) + this.formGroup = this.getFormGroup(); + this.loadScheduledJobs().subscribe() + } + + submit() { + let startDate = formatDate(this.formGroup.controls['startDate'].value) + let endDate = formatDate(this.formGroup.controls['endDate'].value) + this.copyWorkouts(startDate, endDate); + } + + today() { + this.copyWorkoutsForOneDay(formatDate(this.todayDate)); + } + + tomorrow() { + this.copyWorkoutsForOneDay(formatDate(this.tomorrowDate)); + } + + scheduleToday() { + let startDate = formatDate(this.formGroup.controls['startDate'].value) + let endDate = formatDate(this.formGroup.controls['endDate'].value) + let direction = this.formGroup.value.direction + let trainingTypes = this.formGroup.value.trainingTypes + let skipSynced = this.formGroup.value.skipSynced + + this.inProgress = true + this.workoutClient.scheduleCopyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe( + switchMap(() => this.loadScheduledJobs()), + finalize(() => this.inProgress = false) + ).subscribe(() => { + this.notificationService.success(`Scheduled sync job`) + }) + } + + mapTrainingTypesToTitles(values) { + return values.map(value => TrainingTypes.getTitle(value)) + } + + private copyWorkoutsForOneDay(date) { + this.copyWorkouts(date, date) + } + + private copyWorkouts(startDate, endDate) { + let direction = this.formGroup.value.direction + let trainingTypes = this.formGroup.value.trainingTypes + let skipSynced = this.formGroup.value.skipSynced + + this.inProgress = true + this.workoutClient.copyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe( + finalize(() => this.inProgress = false) + ).subscribe((response) => { + this.notificationService.success( + `Planned: ${response.copied}\n Filtered out: ${response.filteredOut}\n From ${response.startDate} to ${response.endDate}`) + }) + } + + private getFormGroup() { + return this.formBuilder.group({ + direction: [this.directions[0].value, Validators.required], + trainingTypes: [this.selectedTrainingTypes, Validators.required], + startDate: [this.todayDate, Validators.required], + endDate: [this.tomorrowDate, Validators.required], + skipSynced: [true, Validators.required], + }) + } + + private loadScheduledJobs() { + return this.workoutClient.getScheduledJobsCopyCalendarToCalendar().pipe( + tap(values => { + this.scheduledJobs = values + }) + ) + } +} diff --git a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html index c1f14f3b..4248b14a 100644 --- a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html +++ b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html @@ -1,78 +1,5 @@ -
-
- - Direction - - @for (item of directions; track item) { - {{ item.title }} - } - - -
- -
- - Workout Types - - @for (type of trainingTypes; track type) { - {{ type.title }} - } - - -
- -
- - Date Range - - - - - - - -
- -
- - Skip already synced workouts - -
- -
- - - -
- - -
+ + diff --git a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.scss b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.scss index 1b1e613c..e69de29b 100644 --- a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.scss +++ b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.scss @@ -1,5 +0,0 @@ -.date-range-text { - padding-top: 10px; - padding-bottom: 10px; - padding-left: 10px; -} diff --git a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.ts b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.ts index d8b98b2b..67e7cde6 100644 --- a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.ts +++ b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.ts @@ -1,23 +1,20 @@ import {Component, OnInit} from '@angular/core'; -import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {MatButtonModule} from "@angular/material/button"; import {MatFormFieldModule} from "@angular/material/form-field"; import {MatInputModule} from "@angular/material/input"; import {MatProgressBarModule} from "@angular/material/progress-bar"; -import {NgIf} from "@angular/common"; import {MatDatepickerModule} from "@angular/material/datepicker"; import {MatNativeDateModule} from "@angular/material/core"; import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; -import {WorkoutClient} from "infrastructure/client/workout.client"; -import {NotificationService} from "infrastructure/notification.service"; -import {finalize} from "rxjs"; import {Platform} from "infrastructure/platform"; -import {formatDate} from "utils/date-formatter"; -import {TrainingPeaksTrainingTypes} from "app/training-peaks/training-peaks-training-types"; -import {TrainerRoadTrainingTypes} from "app/trainer-road/trainer-road-training-types"; -import {ConfigurationClient} from "infrastructure/client/configuration.client"; +import {MatDividerModule} from "@angular/material/divider"; +import {MatListModule} from "@angular/material/list"; +import { + CopyCalendarToCalendarComponent +} from "app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component"; @Component({ selector: 'tr-copy-calendar-to-calendar', @@ -29,94 +26,33 @@ import {ConfigurationClient} from "infrastructure/client/configuration.client"; MatInputModule, ReactiveFormsModule, MatProgressBarModule, - NgIf, MatDatepickerModule, MatNativeDateModule, MatSnackBarModule, MatSelectModule, MatCheckboxModule, + MatDividerModule, + MatListModule, + CopyCalendarToCalendarComponent, ], templateUrl: './tr-copy-calendar-to-calendar.component.html', styleUrl: './tr-copy-calendar-to-calendar.component.scss' }) export class TrCopyCalendarToCalendarComponent implements OnInit { - readonly trainingTypes = TrainerRoadTrainingTypes.trainingTypes; - readonly todayDate = new Date() - readonly tomorrowDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000) + readonly Platform = Platform; readonly directions = [ {title: "TrainerRoad -> TrainingPeaks", value: Platform.DIRECTION_TR_TP}, {title: "TrainerRoad -> Intervals.icu", value: Platform.DIRECTION_TR_INT}, ] - readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE']; + readonly trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Unknown", value: "UNKNOWN"}, + ]; - formGroup: FormGroup = this.formBuilder.group({ - direction: [this.directions[0].value, Validators.required], - trainingTypes: [this.selectedTrainingTypes, Validators.required], - startDate: [this.todayDate, Validators.required], - endDate: [this.tomorrowDate, Validators.required], - skipSynced: [true, Validators.required], - }); - - inProgress = false - tpPlatformInfo: any = null - - constructor( - private formBuilder: FormBuilder, - private workoutClient: WorkoutClient, - private configurationClient: ConfigurationClient, - private notificationService: NotificationService - ) { + constructor() { } ngOnInit(): void { - this.configurationClient.platformInfo(Platform.TRAINING_PEAKS.key).subscribe(value => { - this.tpPlatformInfo = value - }) - this.listenDirectionChange(); - } - - submit() { - let startDate = formatDate(this.formGroup.controls['startDate'].value) - let endDate = formatDate(this.formGroup.controls['endDate'].value) - this.copyWorkouts(startDate, endDate); - } - - today() { - this.copyWorkoutsForOneDay(formatDate(this.todayDate)); - } - - tomorrow() { - this.copyWorkoutsForOneDay(formatDate(this.tomorrowDate)); } - - private copyWorkoutsForOneDay(date) { - this.copyWorkouts(date, date) - } - - private copyWorkouts(startDate, endDate) { - this.inProgress = true - let direction = this.formGroup.value.direction - let trainingTypes = this.formGroup.value.trainingTypes - let skipSynced = this.formGroup.value.skipSynced - this.workoutClient.copyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe( - finalize(() => this.inProgress = false) - ).subscribe((response) => { - this.notificationService.success( - `Planned: ${response.copied}\n Filtered out: ${response.filteredOut}\n From ${response.startDate} to ${response.endDate}`) - }) - } - - private listenDirectionChange() { - this.formGroup.controls['direction'].valueChanges.subscribe(value => { - if (value === Platform.DIRECTION_TR_INT) { - this.formGroup.controls['skipSynced'].disable() - this.formGroup.controls['skipSynced'].setValue(false) - } else { - this.formGroup.controls['skipSynced'].enable() - this.formGroup.controls['skipSynced'].setValue(true) - } - }) - } - - protected readonly Platform = Platform; } diff --git a/ui/src/app/trainer-road/tr-copy-calendar-to-library/tr-copy-calendar-to-library.component.ts b/ui/src/app/trainer-road/tr-copy-calendar-to-library/tr-copy-calendar-to-library.component.ts index 83013a93..b4f7f01c 100644 --- a/ui/src/app/trainer-road/tr-copy-calendar-to-library/tr-copy-calendar-to-library.component.ts +++ b/ui/src/app/trainer-road/tr-copy-calendar-to-library/tr-copy-calendar-to-library.component.ts @@ -16,7 +16,6 @@ import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; import {Platform} from "infrastructure/platform"; -import {TrainerRoadTrainingTypes} from "app/trainer-road/trainer-road-training-types"; @Component({ selector: 'tr-copy-calendar-to-library', @@ -40,8 +39,18 @@ import {TrainerRoadTrainingTypes} from "app/trainer-road/trainer-road-training-t styleUrl: './tr-copy-calendar-to-library.component.scss' }) export class TrCopyCalendarToLibraryComponent implements OnInit { - private readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; - private readonly direction = Platform.DIRECTION_TR_INT + readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; + readonly direction = Platform.DIRECTION_TR_INT + readonly planType = [ + {name: 'Plan', value: true}, + {name: 'Folder', value: false} + ] + + trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Unknown", value: "UNKNOWN"}, + ] formGroup: FormGroup = this.formBuilder.group({ name: ['My New Library', Validators.required], @@ -53,13 +62,6 @@ export class TrCopyCalendarToLibraryComponent implements OnInit { inProgress = false - trainingTypes = TrainerRoadTrainingTypes.trainingTypes; - - readonly planType = [ - {name: 'Plan', value: true}, - {name: 'Folder', value: false} - ] - constructor( private formBuilder: FormBuilder, private workoutClient: WorkoutClient, diff --git a/ui/src/app/trainer-road/tr-copy-library-to-library/tr-copy-library-to-library.component.ts b/ui/src/app/trainer-road/tr-copy-library-to-library/tr-copy-library-to-library.component.ts index 0b600e74..f01a3725 100644 --- a/ui/src/app/trainer-road/tr-copy-library-to-library/tr-copy-library-to-library.component.ts +++ b/ui/src/app/trainer-road/tr-copy-library-to-library/tr-copy-library-to-library.component.ts @@ -12,7 +12,6 @@ import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; import {WorkoutClient} from "infrastructure/client/workout.client"; -import {ConfigurationClient} from "infrastructure/client/configuration.client"; import {NotificationService} from "infrastructure/notification.service"; import {debounceTime, filter, finalize, map, Observable, switchMap, tap} from "rxjs"; import {LibraryClient} from "infrastructure/client/library-client.service"; @@ -61,7 +60,6 @@ export class TrCopyLibraryToLibraryComponent implements OnInit { private formBuilder: FormBuilder, private workoutClient: WorkoutClient, private planClient: LibraryClient, - private configurationClient: ConfigurationClient, private notificationService: NotificationService ) { } diff --git a/ui/src/app/trainer-road/trainer-road-training-types.ts b/ui/src/app/trainer-road/trainer-road-training-types.ts deleted file mode 100644 index 80d703b8..00000000 --- a/ui/src/app/trainer-road/trainer-road-training-types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class TrainerRoadTrainingTypes { - static trainingTypes = [ - {"title": "Ride", "value": "BIKE"}, - {"title": "Virtual Ride", "value": "VIRTUAL_BIKE"}, - {"title": "Unknown", "value": "UNKNOWN"}, - ] -} diff --git a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html index b565718d..7f677c7c 100644 --- a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html +++ b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html @@ -1,80 +1,6 @@ -
-
- - Direction - - @for (item of directions; track item) { - {{ item.title }} - } - - -
- -
- - Workout Types - - @for (type of trainingTypes; track type) { - {{ type.title }} - } - - -
- -
- - Date Range - - - - - - - -
- -
- - Skip already synced workouts - -
- -
- - - -
- - -
+ + diff --git a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.scss b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.scss index 1b1e613c..e69de29b 100644 --- a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.scss +++ b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.scss @@ -1,5 +0,0 @@ -.date-range-text { - padding-top: 10px; - padding-bottom: 10px; - padding-left: 10px; -} diff --git a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.ts b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.ts index d9852c42..c09afafe 100644 --- a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.ts +++ b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.ts @@ -1,22 +1,19 @@ import {Component, OnInit} from '@angular/core'; -import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators} from "@angular/forms"; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {MatButtonModule} from "@angular/material/button"; import {MatFormFieldModule} from "@angular/material/form-field"; import {MatInputModule} from "@angular/material/input"; import {MatProgressBarModule} from "@angular/material/progress-bar"; -import {NgIf} from "@angular/common"; import {MatDatepickerModule} from "@angular/material/datepicker"; import {MatNativeDateModule} from "@angular/material/core"; import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; -import {WorkoutClient} from "infrastructure/client/workout.client"; -import {NotificationService} from "infrastructure/notification.service"; -import {finalize} from "rxjs"; import {Platform} from "infrastructure/platform"; -import {formatDate} from "utils/date-formatter"; -import {TrainingPeaksTrainingTypes} from "app/training-peaks/training-peaks-training-types"; -import {ConfigurationClient} from "infrastructure/client/configuration.client"; +import {MatListModule} from "@angular/material/list"; +import { + CopyCalendarToCalendarComponent +} from "app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component"; @Component({ selector: 'tp-copy-calendar-to-calendar', @@ -28,94 +25,38 @@ import {ConfigurationClient} from "infrastructure/client/configuration.client"; MatInputModule, ReactiveFormsModule, MatProgressBarModule, - NgIf, MatDatepickerModule, MatNativeDateModule, MatSnackBarModule, MatSelectModule, MatCheckboxModule, + MatListModule, + CopyCalendarToCalendarComponent, ], templateUrl: './tp-copy-calendar-to-calendar.component.html', styleUrl: './tp-copy-calendar-to-calendar.component.scss' }) export class TpCopyCalendarToCalendarComponent implements OnInit { - readonly trainingTypes = TrainingPeaksTrainingTypes.trainingTypes; - readonly todayDate = new Date() - readonly tomorrowDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000) + readonly Platform = Platform; readonly directions = [ {title: "Intervals.icu -> TrainingPeaks", value: Platform.DIRECTION_INT_TP}, {title: "TrainingPeaks -> Intervals.icu", value: Platform.DIRECTION_TP_INT}, ] + readonly trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "MTB", value: "MTB"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Run", value: "RUN"}, + {title: "Swim", value: "SWIM"}, + {title: "Walk", value: "WALK"}, + {title: "Weight Training", value: "WEIGHT"}, + {title: "Any other", value: "UNKNOWN"}, + ] readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; - formGroup: FormGroup = this.formBuilder.group({ - direction: [this.directions[0].value, Validators.required], - trainingTypes: [this.selectedTrainingTypes, Validators.required], - startDate: [this.todayDate, Validators.required], - endDate: [this.tomorrowDate, Validators.required], - skipSynced: [true, Validators.required], - }); - - inProgress = false - platformInfo: any = null - - constructor( - private formBuilder: FormBuilder, - private workoutClient: WorkoutClient, - private configurationClient: ConfigurationClient, - private notificationService: NotificationService - ) { + constructor() { } ngOnInit(): void { - this.configurationClient.platformInfo(Platform.TRAINING_PEAKS.key).subscribe(value => { - this.platformInfo = value - }) - this.listenDirectionChange() - } - - submit() { - let startDate = formatDate(this.formGroup.controls['startDate'].value) - let endDate = formatDate(this.formGroup.controls['endDate'].value) - this.copyWorkouts(startDate, endDate); - } - - today() { - this.copyWorkoutsForOneDay(formatDate(this.todayDate)); - } - - tomorrow() { - this.copyWorkoutsForOneDay(formatDate(this.tomorrowDate)); - } - - private copyWorkoutsForOneDay(date) { - this.copyWorkouts(date, date) - } - - private copyWorkouts(startDate, endDate) { - this.inProgress = true - let direction = this.formGroup.value.direction - let trainingTypes = this.formGroup.value.trainingTypes - let skipSynced = this.formGroup.value.skipSynced - this.workoutClient.copyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe( - finalize(() => this.inProgress = false) - ).subscribe((response) => { - this.notificationService.success( - `Planned: ${response.copied}\n Filtered out: ${response.filteredOut}\n From ${response.startDate} to ${response.endDate}`) - }) - } - - private listenDirectionChange() { - this.formGroup.controls['direction'].valueChanges.subscribe(value => { - if (value === Platform.DIRECTION_INT_TP) { - this.formGroup.controls['skipSynced'].enable() - this.formGroup.controls['skipSynced'].setValue(true) - } else { - this.formGroup.controls['skipSynced'].disable() - this.formGroup.controls['skipSynced'].setValue(false) - } - }) } - - protected readonly Platform = Platform; } diff --git a/ui/src/app/training-peaks/tp-copy-calendar-to-library/tp-copy-calendar-to-library.component.ts b/ui/src/app/training-peaks/tp-copy-calendar-to-library/tp-copy-calendar-to-library.component.ts index d79e9c94..e9d58801 100644 --- a/ui/src/app/training-peaks/tp-copy-calendar-to-library/tp-copy-calendar-to-library.component.ts +++ b/ui/src/app/training-peaks/tp-copy-calendar-to-library/tp-copy-calendar-to-library.component.ts @@ -16,7 +16,6 @@ import {MatSnackBarModule} from "@angular/material/snack-bar"; import {MatSelectModule} from "@angular/material/select"; import {MatCheckboxModule} from "@angular/material/checkbox"; import {Platform} from "infrastructure/platform"; -import {TrainingPeaksTrainingTypes} from "app/training-peaks/training-peaks-training-types"; @Component({ selector: 'tp-copy-calendar-to-library', @@ -40,8 +39,23 @@ import {TrainingPeaksTrainingTypes} from "app/training-peaks/training-peaks-trai styleUrl: './tp-copy-calendar-to-library.component.scss' }) export class TpCopyCalendarToLibraryComponent implements OnInit { - private readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; - private readonly direction = Platform.DIRECTION_TP_INT + readonly selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE', 'MTB', 'RUN']; + readonly direction = Platform.DIRECTION_TP_INT + readonly planType = [ + {name: 'Plan', value: true}, + {name: 'Folder', value: false} + ] + + trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "MTB", value: "MTB"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Run", value: "RUN"}, + {title: "Swim", value: "SWIM"}, + {title: "Walk", value: "WALK"}, + {title: "Weight Training", value: "WEIGHT"}, + {title: "Any other", value: "UNKNOWN"}, + ] formGroup: FormGroup = this.formBuilder.group({ name: ['My New Library', Validators.required], @@ -50,15 +64,8 @@ export class TpCopyCalendarToLibraryComponent implements OnInit { endDate: [null, Validators.required], isPlan: [true, Validators.required], }); - inProgress = false - trainingTypes = TrainingPeaksTrainingTypes.trainingTypes; - readonly planType = [ - {name: 'Plan', value: true}, - {name: 'Folder', value: false} - ] - constructor( private formBuilder: FormBuilder, private workoutClient: WorkoutClient, diff --git a/ui/src/app/training-peaks/training-peaks-training-types.ts b/ui/src/app/training-peaks/training-peaks-training-types.ts deleted file mode 100644 index 170933c4..00000000 --- a/ui/src/app/training-peaks/training-peaks-training-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class TrainingPeaksTrainingTypes { - static trainingTypes = [ - {"title": "Ride", "value": "BIKE"}, - {"title": "MTB", "value": "MTB"}, - {"title": "Virtual Ride", "value": "VIRTUAL_BIKE"}, - {"title": "Run", "value": "RUN"}, - {"title": "Swim", "value": "SWIM"}, - {"title": "Walk", "value": "WALK"}, - {"title": "Weight Training", "value": "WEIGHT"}, - {"title": "Any other", "value": "UNKNOWN"}, - ] -} diff --git a/ui/src/app/training-types.ts b/ui/src/app/training-types.ts new file mode 100644 index 00000000..02d4711e --- /dev/null +++ b/ui/src/app/training-types.ts @@ -0,0 +1,16 @@ +export class TrainingTypes { + static trainingTypes = [ + {title: "Ride", value: "BIKE"}, + {title: "MTB", value: "MTB"}, + {title: "Virtual Ride", value: "VIRTUAL_BIKE"}, + {title: "Run", value: "RUN"}, + {title: "Swim", value: "SWIM"}, + {title: "Walk", value: "WALK"}, + {title: "Weight Training", value: "WEIGHT"}, + {title: "Any other", value: "UNKNOWN"}, + ] + + static getTitle(value: string) { + return this.trainingTypes.find(type => type.value === value)?.title + } +} diff --git a/ui/src/infrastructure/client/workout.client.ts b/ui/src/infrastructure/client/workout.client.ts index 6481a8f7..537f44e6 100644 --- a/ui/src/infrastructure/client/workout.client.ts +++ b/ui/src/infrastructure/client/workout.client.ts @@ -33,4 +33,18 @@ export class WorkoutClient { return this.httpClient.get(`/api/workout/find`, {params: {platform, name}}) } + scheduleCopyCalendarToCalendar(startDate, endDate, types, skipSynced, platformDirection): Observable { + return this.httpClient + .post(`/api/workout/copy-calendar-to-calendar/schedule`, { + startDate, + endDate, + types, + skipSynced, + ...platformDirection + }) + } + + getScheduledJobsCopyCalendarToCalendar(): Observable { + return this.httpClient.get(`/api/workout/copy-calendar-to-calendar/schedule`) + } } diff --git a/ui/src/infrastructure/platform.ts b/ui/src/infrastructure/platform.ts index 0525a325..40eab2a7 100644 --- a/ui/src/infrastructure/platform.ts +++ b/ui/src/infrastructure/platform.ts @@ -4,6 +4,9 @@ export class Platform { static INTERVALS = {key: 'INTERVALS', title: 'Intervals.icu'} static TRAINING_PEAKS = {key: 'TRAINING_PEAKS', title: 'TrainingPeaks'} static TRAINER_ROAD = {key: 'TRAINER_ROAD', title: 'TrainerRoad'} + static platforms = [ + this.INTERVALS, this.TRAINING_PEAKS, this.TRAINER_ROAD + ] static DIRECTION_TP_INT = { sourcePlatform: this.TRAINING_PEAKS.key, targetPlatform: this.INTERVALS.key @@ -17,4 +20,8 @@ export class Platform { static DIRECTION_TR_TP = { sourcePlatform: this.TRAINER_ROAD.key, targetPlatform: this.TRAINING_PEAKS.key } + + static getTitle(key) { + return this.platforms.find(platform => platform.key === key)?.title + } } From 747038e621ddf658513665ecaf697845afb58af4 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 19 Jan 2025 10:57:05 +0100 Subject: [PATCH 03/35] scheduled workouts readme --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6ecac0ce..ce3c83ed 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Runs on MacOS (DMG), Windows (EXE installer), Linux (AppImage). Alternatively th All files are available for download on [Release page](https://github.com/freekode/tp2intervals/releases/latest). +**Only for educational purposes** + * [List of features](#list-of-features) @@ -37,7 +39,7 @@ To fix issues I can only relay on logs and HAR files from you. ## List of features -### TrainingPeaks features +### TrainingPeaks **Athlete account** * Sync planned workouts in calendar between Intervals.icu and TrainingPeaks (for today and tomorrow with free TP account) * Copy whole training plan from TrainingPeaks @@ -46,12 +48,13 @@ To fix issues I can only relay on logs and HAR files from you. **Coach account** * Copy whole training plan and workout library from TrainingPeaks -### TrainerRoad features +### TrainerRoad * Sync planned workouts in calendar from TrainerRoad to TrainingPeaks or Intervals.icu * Copy workouts from TrainerRoad library to Intervals * Create training plan or workout folder on Intervals.icu from planned workouts on TrainerRoad -**Only for educational purposes** +Automatically schedule workouts for today, by checking your calendar every 20 minutes. + ## Configuration From a95a36b304cf2dd7ac8879de3ff59cfd5c676c6f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 19 Jan 2025 11:01:01 +0100 Subject: [PATCH 04/35] scheduled job duplication fix --- README.md | 1 + .../tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt | 2 +- .../copy-calendar-to-calendar.component.html | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce3c83ed..691a18d8 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ To fix issues I can only relay on logs and HAR files from you. * Create training plan or workout folder on Intervals.icu from planned workouts on TrainerRoad Automatically schedule workouts for today, by checking your calendar every 20 minutes. +To clear up scheduled jobs just restart the application. ## Configuration diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt index 5fe80624..5b7d6d30 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt @@ -11,7 +11,7 @@ class WorkoutJobScheduler( private val workoutService: WorkoutService ) { private val log = LoggerFactory.getLogger(this.javaClass) - private val scheduledRequests = mutableListOf() + private val scheduledRequests = mutableSetOf() fun addRequest(schedulable: Schedulable) = scheduledRequests.add(schedulable) diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html index 20414ff4..192d9431 100644 --- a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html @@ -100,7 +100,7 @@ > Schedule for today From 1f679139ed1cf9521277da38370c1ebd4ca09507 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 19 Jan 2025 11:06:08 +0100 Subject: [PATCH 05/35] scheduled job duplication fix --- .github/workflows/branch.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index ebf6a56f..30b07cc4 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -40,7 +40,6 @@ jobs: jar-artifact-name: tp2intervals-jar image-name: tp2intervals image-tag: ${{ needs.extract_branch_name.outputs.branch_name }} - dry-run: false electron: needs: From 3e08b9018f1a21f490e710cb4b4fd9b8154709c7 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 19 Jan 2025 11:12:20 +0100 Subject: [PATCH 06/35] readme upd --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 691a18d8..8052ee12 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,11 @@ If everything is fine, you will be redirected to the home page. If your configuration is wrong. You will see an error that there is no access to particular platform. Check all your values and save configuration again. + ### Intervals.icu Copy API key and Athlete Id from [Settings page](https://intervals.icu/settings) in Developer Settings section on Intervals.icu web page. + ### TrainingPeaks To use TrainingPeaks copy all cookies from request `https://tpapi.trainingpeaks.com/users/v3/token` and put it on Configuration page. The app automatically will remove redundant parts and only require cookie will remain. Follow guide below how to do that. @@ -80,6 +82,7 @@ Another guide is [available here](https://forum.intervals.icu/t/implemented-push + ### TrainerRoad Configuration is very similar to TrainingPeaks. Copy all cookies from request `https://tpapi.trainingpeaks.com/users/v3/token` and put it on Configuration page. The app automatically will remove redundant parts and only require cookie will remain. Follow guide below how to do that. From 2fb7b7586c16bb33eef466f53a6c02f871b02ab4 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:14:14 +0100 Subject: [PATCH 07/35] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 8052ee12..b5537051 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,7 @@ Check all your values and save configuration again. ### Intervals.icu Copy API key and Athlete Id from [Settings page](https://intervals.icu/settings) in Developer Settings section on Intervals.icu web page. - -### TrainingPeaks +### TrainingPeaks To use TrainingPeaks copy all cookies from request `https://tpapi.trainingpeaks.com/users/v3/token` and put it on Configuration page. The app automatically will remove redundant parts and only require cookie will remain. Follow guide below how to do that. From 4de0a8edf3b4666ce0c26de28851ad656d52c874 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:19:29 +0100 Subject: [PATCH 08/35] Update README.md --- README.md | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index b5537051..19cdb11f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ All files are available for download on [Release page](https://github.com/freeko + [Docker](#docker) * [FAQ](#faq) + [General](#general) - + [Sync automatically planned workouts to TrainingPeaks](#sync-automatically-planned-workouts-to-trainingpeaks) + [Info regarding scheduling for the next day with TrainingPeaks free account](#info-regarding-scheduling-for-the-next-day-with-trainingpeaks-free-account) * [Troubleshooting](#troubleshooting) + [How to get logs](#how-to-get-logs) @@ -39,16 +38,17 @@ To fix issues I can only relay on logs and HAR files from you. ## List of features -### TrainingPeaks -**Athlete account** +**TrainingPeaks** + +Athlete account * Sync planned workouts in calendar between Intervals.icu and TrainingPeaks (for today and tomorrow with free TP account) * Copy whole training plan from TrainingPeaks * Create training plan or workout folder on Intervals.icu from planned workouts on TrainingPeaks -**Coach account** +Coach account * Copy whole training plan and workout library from TrainingPeaks -### TrainerRoad +**TrainerRoad** * Sync planned workouts in calendar from TrainerRoad to TrainingPeaks or Intervals.icu * Copy workouts from TrainerRoad library to Intervals * Create training plan or workout folder on Intervals.icu from planned workouts on TrainerRoad @@ -58,7 +58,6 @@ To clear up scheduled jobs just restart the application. ## Configuration - Before using the application you need to configure access to platforms. Access to Intervals.icu is required, access to other platforms is optional. @@ -68,11 +67,10 @@ If everything is fine, you will be redirected to the home page. If your configuration is wrong. You will see an error that there is no access to particular platform. Check all your values and save configuration again. - ### Intervals.icu Copy API key and Athlete Id from [Settings page](https://intervals.icu/settings) in Developer Settings section on Intervals.icu web page. -### TrainingPeaks +### TrainingPeaks To use TrainingPeaks copy all cookies from request `https://tpapi.trainingpeaks.com/users/v3/token` and put it on Configuration page. The app automatically will remove redundant parts and only require cookie will remain. Follow guide below how to do that. @@ -81,7 +79,6 @@ Another guide is [available here](https://forum.intervals.icu/t/implemented-push - ### TrainerRoad Configuration is very similar to TrainingPeaks. Copy all cookies from request `https://tpapi.trainingpeaks.com/users/v3/token` and put it on Configuration page. The app automatically will remove redundant parts and only require cookie will remain. Follow guide below how to do that. @@ -93,7 +90,6 @@ Cookie `SharedTrainerRoadAuth` (key and value, smth like `SharedTrainerRoadAuth= Be aware, Firefox cuts long strings in Dev Tool window. Copy cookie value with right click -> Copy Value. ## Other ways to run the app - ### Executable JAR The project has executable jar with web UI. It requires JDK 21. To run jar: ```shell @@ -135,27 +131,6 @@ services: * **Windows** The app will ask to access local network and Internet, you need to allow it. After all it makes HTTP requests * More info you can find on the forum https://forum.intervals.icu/t/tp2intervals-copy-trainingpeaks-and-trainerroad-workouts-plans-to-intervals/63375 - -### Bash script to sync planned workouts -To sync workouts without clicking buttons on UI there is a script [sync-planned-workouts.sh](scripts/sync-planned-workouts.sh). - -```sh -./sync-planned-workouts.sh -``` - -Example, sync workouts from TrainerRoad to TrainingPeaks for tomorrow in standalone app: -```sh -./sync-planned-workouts.sh tomorrow TRAINER_ROAD TRAINING_PEAKS standalone -``` - -Example, sync workouts from Intervals.icu to TrainingPeaks for 2025-01-05 in docker: -```sh -./sync-planned-workouts.sh 2025-01-05 INTERVALS TRAINING_PEAKS -``` - -Docker image has build in cron, you can edit its configuration and add script to run it on schedule - - ### Info regarding scheduling for the next day with TrainingPeaks free account Officially if you have a free TP account, you can't plan workouts for future dates, but practically you can. You can plan a workout for the next day relative to TrainingPeaks server local time. The server is in UTC-6 time zone. Let's check some examples: From 56b3f2ef3ceabd89ef68f882248f9d30aaf4e392 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:52:31 +0100 Subject: [PATCH 09/35] fix tp premium detection (#94) --- .../app/confguration/ConfigurationService.kt | 3 +++ .../rest/configuration/ConfigurationController.kt | 12 ++++++------ .../copy-calendar-to-calendar.component.html | 2 +- .../copy-calendar-to-calendar.component.ts | 9 ++++----- .../tr-copy-calendar-to-calendar.component.html | 1 - .../tp-copy-calendar-to-calendar.component.html | 1 - ui/src/infrastructure/client/configuration.client.ts | 9 +++++++++ ui/src/{app => infrastructure}/training-types.ts | 0 8 files changed, 23 insertions(+), 14 deletions(-) rename ui/src/{app => infrastructure}/training-types.ts (100%) diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/confguration/ConfigurationService.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/confguration/ConfigurationService.kt index 3b33ce35..1a0e1b7a 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/confguration/ConfigurationService.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/confguration/ConfigurationService.kt @@ -31,6 +31,9 @@ class ConfigurationService( return errors } + fun platformInfo() = + platformInfoRepositoryMap.entries.associate { it.key to it.value.platformInfo() } + fun platformInfo(platform: Platform): PlatformInfo { return platformInfoRepositoryMap[platform]!!.platformInfo() } diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt index 4eadac25..6e1388cf 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/configuration/ConfigurationController.kt @@ -2,8 +2,6 @@ package org.freekode.tp2intervals.rest.configuration import org.freekode.tp2intervals.app.confguration.ConfigurationService import org.freekode.tp2intervals.domain.Platform -import org.freekode.tp2intervals.domain.TrainingType -import org.freekode.tp2intervals.domain.config.PlatformInfo import org.freekode.tp2intervals.domain.config.UpdateConfigurationRequest import org.freekode.tp2intervals.domain.workout.structure.StepModifier import org.freekode.tp2intervals.rest.ErrorResponseDTO @@ -43,9 +41,11 @@ class ConfigurationController( return StepModifier.entries } + @GetMapping("/api/configuration/platform") + fun getAllPlatformInfo() = + configurationService.platformInfo() + @GetMapping("/api/configuration/{platform}") - fun getConfigurations(@PathVariable platform: Platform): PlatformInfo { - log.debug("Received request for getting configurations for platform: {}", platform) - return configurationService.platformInfo(platform) - } + fun getConfigurations(@PathVariable platform: Platform) = + configurationService.platformInfo(platform) } diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html index 192d9431..dd4ffb21 100644 --- a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html @@ -25,7 +25,7 @@ Date Range diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts index 2bba94db..36d1537d 100644 --- a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts @@ -19,7 +19,7 @@ import {finalize, switchMap, tap} from "rxjs"; import {WorkoutClient} from "infrastructure/client/workout.client"; import {NotificationService} from "infrastructure/notification.service"; import {MatTooltipModule} from "@angular/material/tooltip"; -import {TrainingTypes} from "app/training-types"; +import {TrainingTypes} from "infrastructure/training-types"; @Component({ selector: 'copy-calendar-to-calendar', @@ -49,14 +49,13 @@ export class CopyCalendarToCalendarComponent implements OnInit { readonly todayDate = new Date() readonly tomorrowDate = new Date(new Date().getTime() + 24 * 60 * 60 * 1000) - @Input() platform: any @Input() trainingTypes: any[] = [] @Input() selectedTrainingTypes = ['BIKE', 'VIRTUAL_BIKE'] @Input() directions: any[] = [] @Input() inProgress = false formGroup: FormGroup - platformInfo: any + platformsInfo: any scheduledJobs: any[] = [] constructor( @@ -68,8 +67,8 @@ export class CopyCalendarToCalendarComponent implements OnInit { } ngOnInit(): void { - this.configurationClient.platformInfo(this.platform.key).subscribe(value => { - this.platformInfo = value + this.configurationClient.getAllPlatformInfo().subscribe(value => { + this.platformsInfo = value }) this.formGroup = this.getFormGroup(); this.loadScheduledJobs().subscribe() diff --git a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html index 4248b14a..9911d0a4 100644 --- a/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html +++ b/ui/src/app/trainer-road/tr-copy-calendar-to-calendar/tr-copy-calendar-to-calendar.component.html @@ -1,5 +1,4 @@ diff --git a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html index 7f677c7c..ab077869 100644 --- a/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html +++ b/ui/src/app/training-peaks/tp-copy-calendar-to-calendar/tp-copy-calendar-to-calendar.component.html @@ -1,5 +1,4 @@ diff --git a/ui/src/infrastructure/client/configuration.client.ts b/ui/src/infrastructure/client/configuration.client.ts index 8ec6e60f..d6916bb6 100644 --- a/ui/src/infrastructure/client/configuration.client.ts +++ b/ui/src/infrastructure/client/configuration.client.ts @@ -24,6 +24,15 @@ export class ConfigurationClient { .put(`/api/configuration`, configData) } + getAllPlatformInfo(): Observable { + return this.httpClient.get(`/api/configuration/platform`).pipe( + map(response => { + Object.keys(response).forEach(key => response[key] = response[key].infoMap) + return response + }) + ) + } + platformInfo(platform): Observable { return this.httpClient.get(`/api/configuration/${platform}`).pipe( map(response => (response).infoMap) diff --git a/ui/src/app/training-types.ts b/ui/src/infrastructure/training-types.ts similarity index 100% rename from ui/src/app/training-types.ts rename to ui/src/infrastructure/training-types.ts From 9ab5b94e6c42ab39b164ed8671f013976349e60a Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 19 Jan 2025 11:54:19 +0100 Subject: [PATCH 10/35] changelog --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d30c3d2..1496e1f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ - +### 0.11.0 +- Added scheduled job to sync planned workouts in calendars +- Added support arm64 for Mac +- Added support to skip already synced workouts when syncing to Intervals.icu +- Fixed null workout name in TrainingPeaks + ### 0.10.0 - Added calendar sync between TrainerRoad, TrainingPeaks and Intervals - Added bash script for automatic calendar sync with cron @@ -11,4 +16,4 @@ ### 0.9.0 -- Added support distance based steps in workouts \ No newline at end of file +- Added support distance based steps in workouts From f3834f5764d6ef7104b7c8df5befad6bcaba5360 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 19 Jan 2025 10:59:18 +0000 Subject: [PATCH 11/35] version 0.11.0 --- boot/version | 2 +- electron/package-lock.json | 4 ++-- electron/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/boot/version b/boot/version index 78bc1abd..d9df1bbc 100644 --- a/boot/version +++ b/boot/version @@ -1 +1 @@ -0.10.0 +0.11.0 diff --git a/electron/package-lock.json b/electron/package-lock.json index fb1c64e1..99e9949c 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "tp2intervals", - "version": "0.10.0", + "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tp2intervals", - "version": "0.10.0", + "version": "0.11.0", "license": "GNU GPLv3", "dependencies": { "electron-log": "^5.1.1", diff --git a/electron/package.json b/electron/package.json index eb6114b8..63a5f355 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,7 +1,7 @@ { "name": "tp2intervals", "productName": "tp2intervals", - "version": "0.10.0", + "version": "0.11.0", "description": "Third Party synchronization with Intervals.icu", "keywords": [ "trainingpeaks", From 9cea0fe0a79281c2c5bdea89c1e571b46b0be8de Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Tue, 11 Feb 2025 18:34:23 +0100 Subject: [PATCH 12/35] fix #96 (#98) --- ui/src/infrastructure/environment.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/src/infrastructure/environment.service.ts b/ui/src/infrastructure/environment.service.ts index f0484deb..45a6bdab 100644 --- a/ui/src/infrastructure/environment.service.ts +++ b/ui/src/infrastructure/environment.service.ts @@ -7,15 +7,17 @@ import {map, Observable} from 'rxjs'; providedIn: 'root' }) export class EnvironmentService { - readonly defaultPort = 8080; readonly electronPort = 44864; constructor(private httpClient: HttpClient) { } getAddress() { - let port = window.electron ? this.electronPort : this.defaultPort - return `http://localhost:${port}` + if (window.electron) { + return `http://localhost:${this.electronPort}` + } else { + return '' + } } getVersion(): Observable { From c687ef261215398aa8b64429325c4a0e82d265c6 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 11 Feb 2025 18:35:17 +0100 Subject: [PATCH 13/35] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1496e1f7..62e9f5ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### 0.11.1 +- Fix docker image + ### 0.11.0 - Added scheduled job to sync planned workouts in calendars - Added support arm64 for Mac From 25b05dbb0f47db2a794daab32d1cbb5abc767a36 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 11 Feb 2025 17:35:52 +0000 Subject: [PATCH 14/35] version 0.11.1 --- boot/version | 2 +- electron/package-lock.json | 4 ++-- electron/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/boot/version b/boot/version index d9df1bbc..af88ba82 100644 --- a/boot/version +++ b/boot/version @@ -1 +1 @@ -0.11.0 +0.11.1 diff --git a/electron/package-lock.json b/electron/package-lock.json index 99e9949c..bd5ca99a 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "tp2intervals", - "version": "0.11.0", + "version": "0.11.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tp2intervals", - "version": "0.11.0", + "version": "0.11.1", "license": "GNU GPLv3", "dependencies": { "electron-log": "^5.1.1", diff --git a/electron/package.json b/electron/package.json index 63a5f355..727d8e85 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,7 +1,7 @@ { "name": "tp2intervals", "productName": "tp2intervals", - "version": "0.11.0", + "version": "0.11.1", "description": "Third Party synchronization with Intervals.icu", "keywords": [ "trainingpeaks", From c905de1224dbe12b9bbdc6f231fd4d23bc9e1308 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:12:19 +0100 Subject: [PATCH 15/35] Persistent scheduling (#101) * Persist scheduled requests * Persist scheduled requests * Persist scheduled requests --- .../app/workout/CopyC2CRequest.kt | 14 +++++ ...rToLibraryRequest.kt => CopyC2LRequest.kt} | 0 ...yToLibraryRequest.kt => CopyL2LRequest.kt} | 0 .../app/workout/WorkoutService.kt | 7 ++- .../C2CTodayScheduledRequest.kt} | 11 ++-- .../app/workout/schedule/Schedulable.kt | 4 ++ .../workout/schedule/WorkoutScheduledJob.kt | 52 +++++++++++++++++++ .../app/workout/scheduled/Schedulable.kt | 4 -- .../workout/scheduled/WorkoutJobScheduler.kt | 39 -------------- .../schedule/ScheduleRequestEntity.kt | 22 ++++++++ .../schedule/ScheduleRequestRepository.kt | 9 ++++ .../rest/workout/WorkoutController.kt | 10 ++-- .../workout/WorkoutJobSchedulerController.kt | 21 -------- .../workout/WorkoutScheduledJobController.kt | 28 ++++++++++ .../schemas/0008-add-scheduled-requests.yaml | 23 ++++++++ boot/src/main/resources/ehcache.xml | 2 +- .../workout/TrainerRoadWorkoutServiceIT.kt | 4 +- .../workout/TrainingPeaksWorkoutServiceIT.kt | 7 ++- .../app/workout/WorkoutJobSchedulerIT.kt | 18 +++---- .../copy-calendar-to-calendar.component.html | 26 ++++++---- .../copy-calendar-to-calendar.component.ts | 32 ++++++++---- .../infrastructure/client/workout.client.ts | 6 ++- ui/src/styles.scss | 2 +- 23 files changed, 226 insertions(+), 115 deletions(-) create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2CRequest.kt rename boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/{CopyFromCalendarToLibraryRequest.kt => CopyC2LRequest.kt} (100%) rename boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/{CopyFromLibraryToLibraryRequest.kt => CopyL2LRequest.kt} (100%) rename boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/{scheduled/CopyFromCalendarToCalendarScheduledRequest.kt => schedule/C2CTodayScheduledRequest.kt} (63%) create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/Schedulable.kt create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/WorkoutScheduledJob.kt delete mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/Schedulable.kt delete mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestEntity.kt create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestRepository.kt delete mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutJobSchedulerController.kt create mode 100644 boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutScheduledJobController.kt create mode 100644 boot/src/main/resources/db/changelog/schemas/0008-add-scheduled-requests.yaml diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2CRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2CRequest.kt new file mode 100644 index 00000000..6c5c4983 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2CRequest.kt @@ -0,0 +1,14 @@ +package org.freekode.tp2intervals.app.workout + +import org.freekode.tp2intervals.domain.Platform +import org.freekode.tp2intervals.domain.TrainingType +import java.time.LocalDate + +data class CopyFromCalendarToCalendarRequest( + val startDate: LocalDate, + val endDate: LocalDate, + val types: List, + val skipSynced: Boolean, + val sourcePlatform: Platform, + val targetPlatform: Platform +) \ No newline at end of file diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToLibraryRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2LRequest.kt similarity index 100% rename from boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromCalendarToLibraryRequest.kt rename to boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyC2LRequest.kt diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromLibraryToLibraryRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyL2LRequest.kt similarity index 100% rename from boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyFromLibraryToLibraryRequest.kt rename to boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/CopyL2LRequest.kt diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt index 16dc0f8f..1b3eb6e0 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/WorkoutService.kt @@ -1,6 +1,5 @@ package org.freekode.tp2intervals.app.workout -import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest import org.freekode.tp2intervals.domain.ExternalData import org.freekode.tp2intervals.domain.Platform import org.freekode.tp2intervals.domain.librarycontainer.LibraryContainerRepository @@ -20,7 +19,7 @@ class WorkoutService( private val workoutRepositoryMap = workoutRepositories.associateBy { it.platform() } private val planRepositoryMap = planRepositories.associateBy { it.platform() } - fun copyWorkoutsFromCalendarToCalendar(request: CopyFromCalendarToCalendarScheduledRequest): CopyWorkoutsResponse { + fun copyWorkoutsC2C(request: CopyFromCalendarToCalendarRequest): CopyWorkoutsResponse { log.info("Received request for copy calendar to calendar: $request") val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!! val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!! @@ -44,7 +43,7 @@ class WorkoutService( return response } - fun copyWorkoutsFromCalendarToLibrary(request: CopyFromCalendarToLibraryRequest): CopyWorkoutsResponse { + fun copyWorkoutsC2L(request: CopyFromCalendarToLibraryRequest): CopyWorkoutsResponse { log.info("Received request for copy calendar to library: $request") val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!! val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!! @@ -64,7 +63,7 @@ class WorkoutService( ) } - fun copyWorkoutFromLibraryToLibrary(request: CopyFromLibraryToLibraryRequest): CopyWorkoutsResponse { + fun copyWorkoutL2L(request: CopyFromLibraryToLibraryRequest): CopyWorkoutsResponse { log.info("Received request for copy library to library: $request") val sourceWorkoutRepository = workoutRepositoryMap[request.sourcePlatform]!! val targetWorkoutRepository = workoutRepositoryMap[request.targetPlatform]!! diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/CopyFromCalendarToCalendarScheduledRequest.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/C2CTodayScheduledRequest.kt similarity index 63% rename from boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/CopyFromCalendarToCalendarScheduledRequest.kt rename to boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/C2CTodayScheduledRequest.kt index 5586885d..74e92c09 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/CopyFromCalendarToCalendarScheduledRequest.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/C2CTodayScheduledRequest.kt @@ -1,18 +1,17 @@ -package org.freekode.tp2intervals.app.workout.scheduled +package org.freekode.tp2intervals.app.workout.schedule +import org.freekode.tp2intervals.app.workout.CopyFromCalendarToCalendarRequest import org.freekode.tp2intervals.domain.Platform import org.freekode.tp2intervals.domain.TrainingType import java.time.LocalDate -data class CopyFromCalendarToCalendarScheduledRequest( - val startDate: LocalDate, - val endDate: LocalDate, +data class C2CTodayScheduledRequest( val types: List, val skipSynced: Boolean, val sourcePlatform: Platform, val targetPlatform: Platform ) : Schedulable { - fun forToday() = CopyFromCalendarToCalendarScheduledRequest( + fun forToday() = CopyFromCalendarToCalendarRequest( LocalDate.now(), LocalDate.now(), types, @@ -20,4 +19,4 @@ data class CopyFromCalendarToCalendarScheduledRequest( sourcePlatform, targetPlatform ) -} +} \ No newline at end of file diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/Schedulable.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/Schedulable.kt new file mode 100644 index 00000000..3d99df24 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/Schedulable.kt @@ -0,0 +1,4 @@ +package org.freekode.tp2intervals.app.workout.schedule + + +interface Schedulable diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/WorkoutScheduledJob.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/WorkoutScheduledJob.kt new file mode 100644 index 00000000..e310b107 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/schedule/WorkoutScheduledJob.kt @@ -0,0 +1,52 @@ +package org.freekode.tp2intervals.app.workout.schedule + +import com.fasterxml.jackson.databind.ObjectMapper +import org.freekode.tp2intervals.app.workout.WorkoutService +import org.freekode.tp2intervals.infrastructure.schedule.ScheduleRequestEntity +import org.freekode.tp2intervals.infrastructure.schedule.ScheduleRequestRepository +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.util.concurrent.TimeUnit + +@Service +class WorkoutScheduledJob( + private val workoutService: WorkoutService, + private val scheduleRequestRepository: ScheduleRequestRepository, + private val objectMapper: ObjectMapper +) { + private val log = LoggerFactory.getLogger(this.javaClass) + + fun addRequest(schedulable: Schedulable) { + val requestJson = objectMapper.writeValueAsString(schedulable) + if (scheduleRequestRepository.findByRequestJson(requestJson) != null) throw IllegalArgumentException("Request already exists") + scheduleRequestRepository.save(ScheduleRequestEntity(requestJson)) + } + + fun getRequests() = + scheduleRequestRepository.findAll().toList() + + fun deleteRequest(id: Int) { + scheduleRequestRepository.deleteById(id) + } + + @Scheduled(fixedRate = 3, timeUnit = TimeUnit.MINUTES) + fun job() { + val requests = getRequests().map { it.toSchedulable() } + log.info("Starting processing scheduled requests. There are ${requests.size} requests") + + for (request in requests) { + handleCopyCalendarToCalendarRequest(request) + } + + log.info("Finished processing scheduled requests"); + } + + private fun handleCopyCalendarToCalendarRequest(request: C2CTodayScheduledRequest) { + workoutService.copyWorkoutsC2C(request.forToday()) + } + + private fun ScheduleRequestEntity.toSchedulable(): C2CTodayScheduledRequest { + return objectMapper.readValue(requestJson, C2CTodayScheduledRequest::class.java) + } +} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/Schedulable.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/Schedulable.kt deleted file mode 100644 index 24588bdb..00000000 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/Schedulable.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.freekode.tp2intervals.app.workout.scheduled - - -interface Schedulable diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt deleted file mode 100644 index 5b7d6d30..00000000 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/app/workout/scheduled/WorkoutJobScheduler.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.freekode.tp2intervals.app.workout.scheduled - -import org.freekode.tp2intervals.app.workout.WorkoutService -import org.slf4j.LoggerFactory -import org.springframework.scheduling.annotation.Scheduled -import org.springframework.stereotype.Service -import java.util.concurrent.TimeUnit - -@Service -class WorkoutJobScheduler( - private val workoutService: WorkoutService -) { - private val log = LoggerFactory.getLogger(this.javaClass) - private val scheduledRequests = mutableSetOf() - - fun addRequest(schedulable: Schedulable) = - scheduledRequests.add(schedulable) - - fun getRequests() = - scheduledRequests.toList() - - @Scheduled(fixedRate = 20, timeUnit = TimeUnit.MINUTES) - fun job() { - val requests = getRequests() - log.info("Starting processing scheduled requests. There are ${requests.size} requests") - - for (request in requests) { - if (request is CopyFromCalendarToCalendarScheduledRequest) { - handleCopyCalendarToCalendarRequest(request) - } - } - - log.info("Finished processing scheduled requests"); - } - - private fun handleCopyCalendarToCalendarRequest(request: CopyFromCalendarToCalendarScheduledRequest) { - workoutService.copyWorkoutsFromCalendarToCalendar(request.forToday()) - } -} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestEntity.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestEntity.kt new file mode 100644 index 00000000..98a6474c --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestEntity.kt @@ -0,0 +1,22 @@ +package org.freekode.tp2intervals.infrastructure.schedule + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table + +@Table(name = "schedule_requests") +@Entity +data class ScheduleRequestEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Int?, + + @Column + var requestJson: String?, +) { + constructor() : this(null, null) + constructor(requestJson: String) : this(null, requestJson) +} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestRepository.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestRepository.kt new file mode 100644 index 00000000..241de0b5 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/infrastructure/schedule/ScheduleRequestRepository.kt @@ -0,0 +1,9 @@ +package org.freekode.tp2intervals.infrastructure.schedule + +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface ScheduleRequestRepository : CrudRepository { + fun findByRequestJson(requestJson: String): ScheduleRequestEntity? +} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt index 66dbfe8e..01fdb40b 100644 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutController.kt @@ -1,6 +1,6 @@ package org.freekode.tp2intervals.rest.workout -import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest +import org.freekode.tp2intervals.app.workout.CopyFromCalendarToCalendarRequest import org.freekode.tp2intervals.app.workout.CopyFromCalendarToLibraryRequest import org.freekode.tp2intervals.app.workout.CopyFromLibraryToLibraryRequest import org.freekode.tp2intervals.app.workout.CopyWorkoutsResponse @@ -18,18 +18,18 @@ class WorkoutController( private val workoutService: WorkoutService, ) { @PostMapping("/api/workout/copy-calendar-to-calendar") - fun copyWorkoutsFromCalendarToCalendar(@RequestBody request: CopyFromCalendarToCalendarScheduledRequest): CopyWorkoutsResponse { - return workoutService.copyWorkoutsFromCalendarToCalendar(request) + fun copyWorkoutsFromCalendarToCalendar(@RequestBody request: CopyFromCalendarToCalendarRequest): CopyWorkoutsResponse { + return workoutService.copyWorkoutsC2C(request) } @PostMapping("/api/workout/copy-calendar-to-library") fun copyWorkoutsFromCalendarToLibrary(@RequestBody request: CopyFromCalendarToLibraryRequest): CopyWorkoutsResponse { - return workoutService.copyWorkoutsFromCalendarToLibrary(request) + return workoutService.copyWorkoutsC2L(request) } @PostMapping("/api/workout/copy-library-to-library") fun copyWorkoutFromLibraryToLibrary(@RequestBody request: CopyFromLibraryToLibraryRequest): CopyWorkoutsResponse { - return workoutService.copyWorkoutFromLibraryToLibrary(request) + return workoutService.copyWorkoutL2L(request) } @GetMapping("/api/workout/find") diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutJobSchedulerController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutJobSchedulerController.kt deleted file mode 100644 index f74b8526..00000000 --- a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutJobSchedulerController.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.freekode.tp2intervals.rest.workout - -import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest -import org.freekode.tp2intervals.app.workout.scheduled.WorkoutJobScheduler -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RestController - -@RestController -class WorkoutJobSchedulerController( - private val workoutJobScheduler: WorkoutJobScheduler -) { - @PostMapping("/api/workout/copy-calendar-to-calendar/schedule") - fun scheduleCopyWorkoutsFromCalendarToCalendar(@RequestBody request: CopyFromCalendarToCalendarScheduledRequest) = - workoutJobScheduler.addRequest(request) - - @GetMapping("/api/workout/copy-calendar-to-calendar/schedule") - fun getScheduledJobsCopyWorkoutsFromCalendarToCalendar() = - workoutJobScheduler.getRequests() -} diff --git a/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutScheduledJobController.kt b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutScheduledJobController.kt new file mode 100644 index 00000000..7fb0fd14 --- /dev/null +++ b/boot/src/main/kotlin/org/freekode/tp2intervals/rest/workout/WorkoutScheduledJobController.kt @@ -0,0 +1,28 @@ +package org.freekode.tp2intervals.rest.workout + +import org.freekode.tp2intervals.app.workout.schedule.C2CTodayScheduledRequest +import org.freekode.tp2intervals.app.workout.schedule.WorkoutScheduledJob +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class WorkoutScheduledJobController( + private val workoutScheduledJob: WorkoutScheduledJob +) { + @PostMapping("/api/workout/copy-calendar-to-calendar/schedule") + fun scheduleC2CTodayRequest(@RequestBody request: C2CTodayScheduledRequest) { + workoutScheduledJob.addRequest(request) + } + + @GetMapping("/api/workout/copy-calendar-to-calendar/schedule") + fun getScheduleRequests() = + workoutScheduledJob.getRequests() + + @DeleteMapping("/api/workout/copy-calendar-to-calendar/schedule/{id}") + fun deleteScheduleRequest(@PathVariable id: Int) = + workoutScheduledJob.deleteRequest(id) +} diff --git a/boot/src/main/resources/db/changelog/schemas/0008-add-scheduled-requests.yaml b/boot/src/main/resources/db/changelog/schemas/0008-add-scheduled-requests.yaml new file mode 100644 index 00000000..95bf1686 --- /dev/null +++ b/boot/src/main/resources/db/changelog/schemas/0008-add-scheduled-requests.yaml @@ -0,0 +1,23 @@ +databaseChangeLog: + - changeSet: + id: _ + author: _ + changes: + - dropTable: + tableName: schedule_requests + - createTable: + tableName: schedule_requests + columns: + - column: + name: id + type: int + autoIncrement: true + constraints: + primaryKey: true + unique: true + + - column: + name: request_json + type: varchar(5000) + constraints: + nullable: false diff --git a/boot/src/main/resources/ehcache.xml b/boot/src/main/resources/ehcache.xml index 2b465a65..5647f253 100644 --- a/boot/src/main/resources/ehcache.xml +++ b/boot/src/main/resources/ehcache.xml @@ -6,7 +6,7 @@ java.lang.String java.lang.String - 1 + 50 1 diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainerRoadWorkoutServiceIT.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainerRoadWorkoutServiceIT.kt index 6e465e7e..2a10c6a0 100644 --- a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainerRoadWorkoutServiceIT.kt +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainerRoadWorkoutServiceIT.kt @@ -38,7 +38,7 @@ class TrainerRoadWorkoutServiceIT : BaseSpringITConfig() { platform, Platform.INTERVALS ) - val response = workoutService.copyWorkoutFromLibraryToLibrary(copyRequest) + val response = workoutService.copyWorkoutL2L(copyRequest) libraryService.deleteLibrary(DeleteLibraryRequest(libraryContainer.externalData, Platform.INTERVALS)) assertEquals(response.copied, 1) } @@ -46,7 +46,7 @@ class TrainerRoadWorkoutServiceIT : BaseSpringITConfig() { @Test @Disabled("don't have example response for calendar") fun `should copy planned workouts to library`() { - val response = workoutService.copyWorkoutsFromCalendarToLibrary( + val response = workoutService.copyWorkoutsC2L( CopyFromCalendarToLibraryRequest( LocalDate.parse("2024-03-04"), LocalDate.parse("2024-03-10"), diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt index 430929a5..4f10d188 100644 --- a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/TrainingPeaksWorkoutServiceIT.kt @@ -4,7 +4,6 @@ import config.BaseSpringITConfig import org.freekode.tp2intervals.app.plan.CopyLibraryRequest import org.freekode.tp2intervals.app.plan.DeleteLibraryRequest import org.freekode.tp2intervals.app.plan.LibraryService -import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest import org.freekode.tp2intervals.domain.Platform import org.freekode.tp2intervals.domain.TrainingType import org.freekode.tp2intervals.domain.workout.structure.StepModifier @@ -31,14 +30,14 @@ class TrainingPeaksWorkoutServiceIT : BaseSpringITConfig() { val deleteRequest = DeleteWorkoutRequestDTO(startDate, endDate, platform) workoutService.deleteWorkoutsFromCalendar(deleteRequest) - val copyRequest = CopyFromCalendarToCalendarScheduledRequest( + val copyRequest = CopyFromCalendarToCalendarRequest( startDate, endDate, TrainingType.DEFAULT_LIST, true, Platform.INTERVALS, platform ) - val response = workoutService.copyWorkoutsFromCalendarToCalendar(copyRequest) + val response = workoutService.copyWorkoutsC2C(copyRequest) workoutService.deleteWorkoutsFromCalendar(deleteRequest) @@ -66,7 +65,7 @@ class TrainingPeaksWorkoutServiceIT : BaseSpringITConfig() { @Test fun `should copy planned workouts to library`() { - val response = workoutService.copyWorkoutsFromCalendarToLibrary( + val response = workoutService.copyWorkoutsC2L( CopyFromCalendarToLibraryRequest( LocalDate.parse("2024-03-04"), LocalDate.parse("2024-03-10"), diff --git a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt index 804e02d4..0c77af31 100644 --- a/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt +++ b/boot/src/test/kotlin/org/freekode/tp2intervals/app/workout/WorkoutJobSchedulerIT.kt @@ -2,27 +2,27 @@ package org.freekode.tp2intervals.app.workout import config.BaseSpringITConfig import org.assertj.core.api.Assertions.assertThat -import org.freekode.tp2intervals.app.workout.scheduled.CopyFromCalendarToCalendarScheduledRequest -import org.freekode.tp2intervals.app.workout.scheduled.WorkoutJobScheduler +import org.freekode.tp2intervals.app.workout.schedule.C2CTodayScheduledRequest +import org.freekode.tp2intervals.app.workout.schedule.WorkoutScheduledJob import org.freekode.tp2intervals.domain.Platform import org.freekode.tp2intervals.domain.TrainingType +import org.freekode.tp2intervals.infrastructure.schedule.ScheduleRequestEntity import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired -import java.time.LocalDate class WorkoutJobSchedulerIT : BaseSpringITConfig() { @Autowired - lateinit var workoutJobScheduler: WorkoutJobScheduler + lateinit var workoutScheduledJob: WorkoutScheduledJob @Test fun test() { - val request = CopyFromCalendarToCalendarScheduledRequest(LocalDate.now(), LocalDate.now(), listOf(TrainingType.BIKE), true, Platform.INTERVALS, Platform.TRAINING_PEAKS) + val request = + C2CTodayScheduledRequest(listOf(TrainingType.BIKE), true, Platform.INTERVALS, Platform.TRAINING_PEAKS) + workoutScheduledJob.addRequest(request) - workoutJobScheduler.addRequest(request) - - val requests = workoutJobScheduler.getRequests() + val requests = workoutScheduledJob.getRequests() assertThat(requests.isNotEmpty()).isTrue() - assertThat(requests[0] is CopyFromCalendarToCalendarScheduledRequest).isTrue() + assertThat(requests[0] is ScheduleRequestEntity).isTrue() } } diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html index dd4ffb21..4cae9d64 100644 --- a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html @@ -74,17 +74,24 @@ -
+
Scheduled jobs - @for (job of scheduledJobs; track job) { + @for (request of scheduleRequests; track request) { - Types: {{ mapTrainingTypesToTitles(job.types) }}, - Skip synced: {{ job.skipSynced }}, - {{ Platform.getTitle(job.sourcePlatform) }} -> {{ Platform.getTitle(job.targetPlatform) }} + Types: {{ mapTrainingTypesToTitles(request.request.types) }}, + Skip synced: {{ request.request.skipSynced }}, + {{ Platform.getTitle(request.request.sourcePlatform) }} -> {{ Platform.getTitle(request.request.targetPlatform) }} + } @@ -95,15 +102,16 @@ mat-raised-button color="primary" type="button" + matTooltip="Scheduled job runs every 20 minutes and will sync workouts for today. Jobs won't persist after app restart" [disabled]="formGroup.invalid || inProgress" (click)="scheduleToday()" > Schedule for today - +
- + diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts index 36d1537d..e875c2e6 100644 --- a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.ts @@ -56,7 +56,7 @@ export class CopyCalendarToCalendarComponent implements OnInit { formGroup: FormGroup platformsInfo: any - scheduledJobs: any[] = [] + scheduleRequests: any[] = [] constructor( private formBuilder: FormBuilder, @@ -71,7 +71,7 @@ export class CopyCalendarToCalendarComponent implements OnInit { this.platformsInfo = value }) this.formGroup = this.getFormGroup(); - this.loadScheduledJobs().subscribe() + this.loadScheduleRequests().subscribe() } submit() { @@ -89,15 +89,15 @@ export class CopyCalendarToCalendarComponent implements OnInit { } scheduleToday() { - let startDate = formatDate(this.formGroup.controls['startDate'].value) - let endDate = formatDate(this.formGroup.controls['endDate'].value) + let startDate = null + let endDate = null let direction = this.formGroup.value.direction let trainingTypes = this.formGroup.value.trainingTypes let skipSynced = this.formGroup.value.skipSynced this.inProgress = true this.workoutClient.scheduleCopyCalendarToCalendar(startDate, endDate, trainingTypes, skipSynced, direction).pipe( - switchMap(() => this.loadScheduledJobs()), + switchMap(() => this.loadScheduleRequests()), finalize(() => this.inProgress = false) ).subscribe(() => { this.notificationService.success(`Scheduled sync job`) @@ -136,11 +136,25 @@ export class CopyCalendarToCalendarComponent implements OnInit { }) } - private loadScheduledJobs() { - return this.workoutClient.getScheduledJobsCopyCalendarToCalendar().pipe( + private loadScheduleRequests() { + return this.workoutClient.getScheduleRequests().pipe( tap(values => { - this.scheduledJobs = values - }) + this.scheduleRequests = values.map(value => { + return {id: value.id, request: JSON.parse(value.requestJson)} + }) + console.log(this.scheduleRequests) + } + ) ) } + + deleteJob(jobId: any) { + this.inProgress = true + this.workoutClient.deleteScheduleRequest(jobId).pipe( + switchMap(() => this.loadScheduleRequests()), + finalize(() => this.inProgress = false) + ).subscribe(() => { + this.notificationService.success(`Deleted job`) + }) + } } diff --git a/ui/src/infrastructure/client/workout.client.ts b/ui/src/infrastructure/client/workout.client.ts index 537f44e6..1da51edd 100644 --- a/ui/src/infrastructure/client/workout.client.ts +++ b/ui/src/infrastructure/client/workout.client.ts @@ -44,7 +44,11 @@ export class WorkoutClient { }) } - getScheduledJobsCopyCalendarToCalendar(): Observable { + getScheduleRequests(): Observable { return this.httpClient.get(`/api/workout/copy-calendar-to-calendar/schedule`) } + + deleteScheduleRequest(id: any) { + return this.httpClient.delete(`/api/workout/copy-calendar-to-calendar/schedule/${id}`) + } } diff --git a/ui/src/styles.scss b/ui/src/styles.scss index 8a461aab..d30dbdc0 100644 --- a/ui/src/styles.scss +++ b/ui/src/styles.scss @@ -34,7 +34,7 @@ body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } } .action-button-section { - margin-bottom: 5px; + margin-top: 10px; } .beta-icon { From 8aac5a1ff85bf9f5d66332f3d7cd220a43da8c12 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:40:08 +0100 Subject: [PATCH 16/35] Docker multi arch (#102) * Docker multi arch * Docker multi arch * Docker multi arch * Docker multi arch * Docker multi arch * Docker multi arch * Docker multi arch --- .github/workflows/branch.yml | 10 ++ .github/workflows/pr.yml | 3 +- .github/workflows/release.yml | 10 ++ .../workflows/workflow-docker-image-new.yml | 108 ++++++++++++++++++ 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/workflow-docker-image-new.yml diff --git a/.github/workflows/branch.yml b/.github/workflows/branch.yml index 30b07cc4..23a3facc 100644 --- a/.github/workflows/branch.yml +++ b/.github/workflows/branch.yml @@ -41,6 +41,16 @@ jobs: image-name: tp2intervals image-tag: ${{ needs.extract_branch_name.outputs.branch_name }} + docker-image-new: + needs: + - jar + uses: ./.github/workflows/workflow-docker-image-new.yml + secrets: inherit + with: + jar-name: tp2intervals + jar-artifact-name: tp2intervals-jar + dry-run: false + electron: needs: - jar diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d781753b..2d57c09d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -19,11 +19,10 @@ jobs: docker-image: needs: - jar - uses: ./.github/workflows/workflow-docker-image.yml + uses: ./.github/workflows/workflow-docker-image-new.yml with: jar-name: tp2intervals jar-artifact-name: tp2intervals-jar - image-name: tp2intervals electron: needs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ff96bd9..497d58a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,16 @@ jobs: image-tag: latest dry-run: false + docker-image-new: + needs: + - jar + uses: ./.github/workflows/workflow-docker-image-new.yml + secrets: inherit + with: + jar-name: tp2intervals + jar-artifact-name: tp2intervals-jar + dry-run: false + electron: needs: - jar diff --git a/.github/workflows/workflow-docker-image-new.yml b/.github/workflows/workflow-docker-image-new.yml new file mode 100644 index 00000000..4ded4add --- /dev/null +++ b/.github/workflows/workflow-docker-image-new.yml @@ -0,0 +1,108 @@ +name: Docker Image + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +on: + workflow_call: + inputs: + jar-name: + required: true + type: string + jar-artifact-name: + required: true + type: string + dry-run: + required: false + type: boolean + default: true + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@master + with: + name: ${{ inputs.jar-artifact-name }} + path: artifact + + # Install the cosign tool except on PR + # https://github.com/sigstore/cosign-installer + - name: Install cosign + if: ${{ inputs.dry-run == false }} + uses: sigstore/cosign-installer@v3.8.1 + with: + cosign-release: 'v2.2.4' + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + if: ${{ inputs.dry-run == false }} + uses: docker/login-action@v3 # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 # v5.0.0 + with: + context: . + push: ${{ inputs.dry-run == false }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: "JAR_PATH=artifact/${{ inputs.jar-name }}.jar" + + # Sign the resulting Docker image digest except on PRs. + # This will only write to the public Rekor transparency log when the Docker + # repository is public to avoid leaking data. If you would like to publish + # transparency data even for private images, pass --force to cosign below. + # https://github.com/sigstore/cosign + - name: Sign the published Docker image + if: ${{ inputs.dry-run == false }} + env: + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} + # This step uses the identity token to provision an ephemeral certificate + # against the sigstore community Fulcio instance. + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} From b0b64ba50d54478ee772600de0352f99ecbccb70 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:09:18 +0100 Subject: [PATCH 17/35] Update workflow-docker-image-new.yml --- .github/workflows/workflow-docker-image-new.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/workflow-docker-image-new.yml b/.github/workflows/workflow-docker-image-new.yml index 4ded4add..2e4dabda 100644 --- a/.github/workflows/workflow-docker-image-new.yml +++ b/.github/workflows/workflow-docker-image-new.yml @@ -54,6 +54,9 @@ jobs: with: cosign-release: 'v2.2.4' + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + # Set up BuildKit Docker container builder to be able to build # multi-platform images and export cache # https://github.com/docker/setup-buildx-action From b3a34ddc55317c5148e88b81f8831d93d67abc95 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 11:20:04 +0100 Subject: [PATCH 18/35] Update workflow-docker-image-new.yml --- .github/workflows/workflow-docker-image-new.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/workflow-docker-image-new.yml b/.github/workflows/workflow-docker-image-new.yml index 2e4dabda..ad4095e2 100644 --- a/.github/workflows/workflow-docker-image-new.yml +++ b/.github/workflows/workflow-docker-image-new.yml @@ -93,6 +93,7 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 build-args: "JAR_PATH=artifact/${{ inputs.jar-name }}.jar" # Sign the resulting Docker image digest except on PRs. From 0f8d6a9810101d28cd8f008bb59766dc51fcfdc8 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 12:23:25 +0100 Subject: [PATCH 19/35] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 19cdb11f..ff40e6fa 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ services: app: image: ghcr.io/freekode/tp2intervals/tp2intervals:latest container_name: tp2intervals + volumes: + - ./tp2intervals.sqlite:/tp2intervals.sqlite ports: - '8080:8080' ``` From 2b94c5fd49132aafa0086538c2bda824ea73d08b Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:22:21 +0100 Subject: [PATCH 20/35] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ff40e6fa..15e0bc98 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Docker image also built for every release To run docker execute: ```shell -docker run --rm --name tp2intervals -p 8080:8080 ghcr.io/freekode/tp2intervals/tp2intervals:latest +docker run --rm --name tp2intervals -p 8080:8080 ghcr.io/freekode/tp2intervals:latest ``` Or with `docker compose` @@ -115,7 +115,7 @@ Or with `docker compose` ```yaml services: app: - image: ghcr.io/freekode/tp2intervals/tp2intervals:latest + image: ghcr.io/freekode/tp2intervals:latest container_name: tp2intervals volumes: - ./tp2intervals.sqlite:/tp2intervals.sqlite From b651464aa01d851008367cfc4bb14d47c4a1d6db Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:33:36 +0100 Subject: [PATCH 21/35] Update workflow-docker-image-new.yml --- .github/workflows/workflow-docker-image-new.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/workflow-docker-image-new.yml b/.github/workflows/workflow-docker-image-new.yml index ad4095e2..37dfb098 100644 --- a/.github/workflows/workflow-docker-image-new.yml +++ b/.github/workflows/workflow-docker-image-new.yml @@ -80,6 +80,8 @@ jobs: uses: docker/metadata-action@v5 # v5.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action From f6876f77d8507737a7765a22a458b63a59806737 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:42:28 +0100 Subject: [PATCH 22/35] Update workflow-docker-image-new.yml --- .github/workflows/workflow-docker-image-new.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/workflow-docker-image-new.yml b/.github/workflows/workflow-docker-image-new.yml index 37dfb098..31703f5d 100644 --- a/.github/workflows/workflow-docker-image-new.yml +++ b/.github/workflows/workflow-docker-image-new.yml @@ -82,6 +82,7 @@ jobs: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=tag # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action From 0d9288b7604b09362afec49c38ddea5722f2f7e0 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:48:41 +0100 Subject: [PATCH 23/35] Update README.md --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 15e0bc98..2cf4a4f0 100644 --- a/README.md +++ b/README.md @@ -104,14 +104,6 @@ java -Dserver.port=9090 -jar tp2intervals.jar ### Docker Docker image also built for every release -To run docker execute: - -```shell -docker run --rm --name tp2intervals -p 8080:8080 ghcr.io/freekode/tp2intervals:latest -``` - -Or with `docker compose` - ```yaml services: app: From 916e2b0541c5faa33cb0dfb22e70ccafe7b5b0a2 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:52:02 +0100 Subject: [PATCH 24/35] Update README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2cf4a4f0..e5161493 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,14 @@ All files are available for download on [Release page](https://github.com/freeko + [How to record HAR file](#how-to-record-har-file) -**TrainerRoad Updates ⚠️** + +**New Docker image location ⚠️** + +**New image url: `ghcr.io/freekode/tp2intervals`** + +Old image url: `ghcr.io/freekode/tp2intervals/tp2intervals` + +**TrainerRoad Updates** I don't have access to TrainerRoad anymore. Account, which I used, cancelled subscription. I don't use the platform and it's too expensive to have it for occasional fixes. To fix issues I can only relay on logs and HAR files from you. From 40daf24f67c8b7fc1c84e8aee275f6f09e8ad4f1 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:53:00 +0100 Subject: [PATCH 25/35] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62e9f5ab..18544c36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 0.12.0 +- Multiarch docker image +- Persistent scheduled job + ### 0.11.1 - Fix docker image From c3d0d1a7c455153d26dd70ebc86900f77ba34427 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:53:10 +0100 Subject: [PATCH 26/35] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18544c36..b2e8e74f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,5 @@ - Added TrainerRoad configuration tutorial - Removed Strava support - ### 0.9.0 - Added support distance based steps in workouts From 2a9a8d150216efb5e9882b0039fba53def5d23f0 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Thu, 13 Mar 2025 14:53:49 +0000 Subject: [PATCH 27/35] version 0.12.0 --- boot/version | 2 +- electron/package-lock.json | 4 ++-- electron/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/boot/version b/boot/version index af88ba82..ac454c6a 100644 --- a/boot/version +++ b/boot/version @@ -1 +1 @@ -0.11.1 +0.12.0 diff --git a/electron/package-lock.json b/electron/package-lock.json index bd5ca99a..5f4e23ed 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "tp2intervals", - "version": "0.11.1", + "version": "0.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tp2intervals", - "version": "0.11.1", + "version": "0.12.0", "license": "GNU GPLv3", "dependencies": { "electron-log": "^5.1.1", diff --git a/electron/package.json b/electron/package.json index 727d8e85..fae2c033 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,7 +1,7 @@ { "name": "tp2intervals", "productName": "tp2intervals", - "version": "0.11.1", + "version": "0.12.0", "description": "Third Party synchronization with Intervals.icu", "keywords": [ "trainingpeaks", From 62847e81a1acf75e562aeb8938f551e68d6b2669 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:58:18 +0100 Subject: [PATCH 28/35] Delete scripts directory --- scripts/sync-planned-workouts.sh | 42 -------------------------------- 1 file changed, 42 deletions(-) delete mode 100644 scripts/sync-planned-workouts.sh diff --git a/scripts/sync-planned-workouts.sh b/scripts/sync-planned-workouts.sh deleted file mode 100644 index ffdced15..00000000 --- a/scripts/sync-planned-workouts.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh - -# How to run -# ./sync-planned-workouts.sh (tomorrow|) (INTERVALS|TRAINING_PEAKS|TRAINER_ROAD) (INTERVALS|TRAINING_PEAKS|TRAINER_ROAD) [standalone] -# -# Examples: -# ./sync-planned-workouts.sh tomorrow TRAINER_ROAD TRAINING_PEAKS standalone -# ./sync-planned-workouts.sh 2025-01-05 INTERVALS TRAINING_PEAKS - - -syncDate= -sourcePlatform=$2 # INTERVALS, TRAINING_PEAKS, TRAINER_ROAD -targetPlatform=$3 # INTERVALS, TRAINING_PEAKS, TRAINER_ROAD -port=8080 # 8080, 44864 - - -tomorrow_date() { - platform=$(uname) - if [ "$platform" == "Darwin" ]; then - echo $(date -v+1d +"%Y-%m-%d") - elif [ "$platform" == "Linux" ]; then - echo $(date -d @$(( $(date +%s) + 86400 )) +%F) - fi - return 0 -} - -if [ "$1" = "tomorrow" ]; then - syncDate=$(tomorrow_date) -else - syncDate=$1 -fi - -if [ "$4" = "standalone" ]; then - port=44864 -fi - -postData='{"startDate":"'$syncDate'","endDate":"'$syncDate'","types":["BIKE","VIRTUAL_BIKE","MTB","RUN"],"skipSynced":true,"sourcePlatform":"'$sourcePlatform'","targetPlatform":"'targetPlatform'"}' - -wget -O - \ - --header="Content-Type: application/json" \ - --post-data=$postData \ - http://localhost:$port/api/workout/copy-calendar-to-calendar From 4daaf9190f198082254d75056d3b0416d01eed21 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:16:10 +0100 Subject: [PATCH 29/35] Update Dockerfile --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1d488804..0877f447 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,5 +3,4 @@ EXPOSE 8080 ARG JAR_PATH='boot/build/libs/tp2intervals.jar' COPY $JAR_PATH /app/app.jar -COPY scripts /scripts ENTRYPOINT java -jar /app/app.jar From 60938d0dfbbb486e96cf85e83dcbbb523d40d907 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 19 Mar 2025 20:55:07 +0100 Subject: [PATCH 30/35] Fix remove button position --- docker-compose.yml | 6 +++--- .../copy-calendar-to-calendar.component.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c61e1bb3..3852229e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,9 @@ -version: '3.1' - services: app: - image: ghcr.io/freekode/tp2intervals/tp2intervals:latest + image: ghcr.io/freekode/tp2intervals:latest container_name: tp2intervals restart: unless-stopped + volumes: + - ./tp2intervals.sqlite:/tp2intervals.sqlite ports: - '8080:8080' diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html index 4cae9d64..cb64e0e6 100644 --- a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html @@ -82,9 +82,6 @@ @for (request of scheduleRequests; track request) { - Types: {{ mapTrainingTypesToTitles(request.request.types) }}, - Skip synced: {{ request.request.skipSynced }}, - {{ Platform.getTitle(request.request.sourcePlatform) }} -> {{ Platform.getTitle(request.request.targetPlatform) }} + Types: {{ mapTrainingTypesToTitles(request.request.types) }}, + Skip synced: {{ request.request.skipSynced }}, + {{ Platform.getTitle(request.request.sourcePlatform) }} -> {{ Platform.getTitle(request.request.targetPlatform) }} } From f6f4f207e8eb8e257b63ff23a361a284eecc2689 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 19 Mar 2025 19:57:42 +0000 Subject: [PATCH 31/35] version 0.12.1 --- boot/version | 2 +- electron/package-lock.json | 4 ++-- electron/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/boot/version b/boot/version index ac454c6a..34a83616 100644 --- a/boot/version +++ b/boot/version @@ -1 +1 @@ -0.12.0 +0.12.1 diff --git a/electron/package-lock.json b/electron/package-lock.json index 5f4e23ed..6368a974 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "tp2intervals", - "version": "0.12.0", + "version": "0.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tp2intervals", - "version": "0.12.0", + "version": "0.12.1", "license": "GNU GPLv3", "dependencies": { "electron-log": "^5.1.1", diff --git a/electron/package.json b/electron/package.json index fae2c033..24aafd2b 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,7 +1,7 @@ { "name": "tp2intervals", "productName": "tp2intervals", - "version": "0.12.0", + "version": "0.12.1", "description": "Third Party synchronization with Intervals.icu", "keywords": [ "trainingpeaks", From 3c7573f504edd516718155675a724b083318008a Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Wed, 19 Mar 2025 20:58:43 +0100 Subject: [PATCH 32/35] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2e8e74f..07684b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### 0.12.1 +- Fixed invisible remove button for scheduled job + ### 0.12.0 - Multiarch docker image - Persistent scheduled job From 6f28d366708ae66d71a74393a0d58cdbee8b13e7 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 15 Apr 2025 19:55:55 +0200 Subject: [PATCH 33/35] remove premium check, add warn sign --- .../copy-calendar-to-calendar.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html index cb64e0e6..6c870b63 100644 --- a/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html +++ b/ui/src/app/components/copy-calendar-to-calendar/copy-calendar-to-calendar.component.html @@ -25,7 +25,6 @@ Date Range @@ -33,6 +32,11 @@ + +
From 62ec1050319a1253582579e2ff1d9995021eadb0 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 15 Apr 2025 18:05:27 +0000 Subject: [PATCH 34/35] version 0.12.2 --- boot/version | 2 +- electron/package-lock.json | 4 ++-- electron/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/boot/version b/boot/version index 34a83616..26acbf08 100644 --- a/boot/version +++ b/boot/version @@ -1 +1 @@ -0.12.1 +0.12.2 diff --git a/electron/package-lock.json b/electron/package-lock.json index 6368a974..ee376643 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "tp2intervals", - "version": "0.12.1", + "version": "0.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tp2intervals", - "version": "0.12.1", + "version": "0.12.2", "license": "GNU GPLv3", "dependencies": { "electron-log": "^5.1.1", diff --git a/electron/package.json b/electron/package.json index 24aafd2b..dd5618c9 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,7 +1,7 @@ { "name": "tp2intervals", "productName": "tp2intervals", - "version": "0.12.1", + "version": "0.12.2", "description": "Third Party synchronization with Intervals.icu", "keywords": [ "trainingpeaks", From af73acd35f0e6127a2e8cebfe55e9759daaac024 Mon Sep 17 00:00:00 2001 From: Evgeny <940893+freekode@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:06:47 +0200 Subject: [PATCH 35/35] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07684b37..455bb6c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### 0.12.2 +- Fixed TP Premium users coulndt select future dates to sync workouts + ### 0.12.1 - Fixed invisible remove button for scheduled job