Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.git
.gradle
.idea
build/
bin/
ui/node_modules/
ui/dist/
*.sqlite
10 changes: 9 additions & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
ko_fi: freekode
# GitHub Sponsors - your GitHub username
github: rodrigobrancaglion

# Ko-fi - your Ko-fi username
ko_fi: rodrigobrancaglion

# Custom links (up to 4 URLs)
custom:
- https://paypal.me/rodrigobrancaglion
19 changes: 15 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# --- Stage 1: Build do Backend ---
FROM amazoncorretto:21-alpine AS backend-builder
WORKDIR /build-app
RUN apk add --no-cache findutils dos2unix
COPY . .

WORKDIR /build-app/boot
RUN dos2unix gradlew && chmod +x gradlew
RUN ./gradlew :bootJar --no-daemon

# --- Stage 2: Runner ---
FROM amazoncorretto:21-alpine
EXPOSE 8080
WORKDIR /app
COPY --from=backend-builder /build-app/boot/build/libs/*.jar app.jar

ARG JAR_PATH='boot/build/libs/tp2intervals.jar'
COPY $JAR_PATH /app/app.jar
ENTRYPOINT java -jar /app/app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
20 changes: 20 additions & 0 deletions README_Run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Third Party to Intervals.icu

## - WEB Browser (Localhost)

### Intellij
Run Configuraction 'Application'

View: http://localhost:4200
### Docker
Docker image also built for every release
```shell
docker-compose up --build
```

## - Desktop (App Electron)
Run script
```shell
./app_rebuild_electron.sh
```
Run the file: electron/dist/mac-arm64/tp2intervals.app
17 changes: 17 additions & 0 deletions app_rebuild_electron.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash

# 1. Compilar o Backend (Kotlin)
echo "Compilando Backend..."
./gradlew :boot:assemble

# 2. Mover e renomear o JAR automaticamente
echo "Movendo JAR para a pasta do Electron..."
mkdir -p electron/artifact
cp boot/build/libs/*.jar electron/artifact/boot.jar

# 3. Gerar o App Electron
echo "Gerando aplicativo descompactado..."
cd electron
npm run build:unpack

echo "Pronto! O app atualizado está em: electron/dist/mac-arm64/tp2intervals.app"
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.freekode.tp2intervals.app.wellness

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<TrainingType>,
val skipSynced: Boolean,
val sourcePlatform: Platform,
val targetPlatform: Platform
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.freekode.tp2intervals.app.wellness

import org.freekode.tp2intervals.domain.ExternalData
import java.time.LocalDate

data class CopyWellnessResponse(
val copied: Int,
val startDate: LocalDate,
val endDate: LocalDate,
val externalData: ExternalData
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.freekode.tp2intervals.app.wellness

import org.freekode.tp2intervals.domain.ExternalData
import org.freekode.tp2intervals.domain.wellness.WellnessRepository
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service

@Service
class WellnessService(
repositories: List<WellnessRepository>,
) {
private val log = LoggerFactory.getLogger(this.javaClass)
private val repositoryMap = repositories.associateBy { it.platform() }

fun copyWellnessC2C(request: CopyFromCalendarToCalendarRequest): CopyWellnessResponse {
log.info("Received request for copy calendar to calendar: $request")
val sourceRepository = repositoryMap[request.sourcePlatform]!!
val targetRepository = repositoryMap[request.targetPlatform]!!

val allToSync = sourceRepository.getFromCalendar(request.startDate, request.endDate)

val response = CopyWellnessResponse(
1,
request.startDate,
request.endDate,
ExternalData.empty()
)
targetRepository.saveToCalendar(allToSync, request.startDate, request.endDate)
log.info("Saved Wellness (Metrics) to calendar successfully: $response")
return response
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.freekode.tp2intervals.app.wellness.schedule

import org.freekode.tp2intervals.app.wellness.CopyFromCalendarToCalendarRequest
import org.freekode.tp2intervals.domain.Platform
import org.freekode.tp2intervals.domain.TrainingType
import java.time.LocalDate

data class C2CTodayScheduledRequest(
val types: List<TrainingType>,
val skipSynced: Boolean,
val sourcePlatform: Platform,
val targetPlatform: Platform
) : Schedulable {
fun forToday() = CopyFromCalendarToCalendarRequest(
LocalDate.now(),
LocalDate.now(),
types,
skipSynced,
sourcePlatform,
targetPlatform
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.freekode.tp2intervals.app.wellness.schedule


interface Schedulable
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.freekode.tp2intervals.app.wellness.schedule

import com.fasterxml.jackson.databind.ObjectMapper
import org.freekode.tp2intervals.app.wellness.WellnessService
import org.freekode.tp2intervals.domain.TrainingType
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 ScheduledJobService(
private val wellnessService: WellnessService,
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(): List<ScheduleRequestEntity> {
return scheduleRequestRepository.findAll().filter { record ->
try {
// Internal conversion just to check the types inside the JSON
val request = objectMapper.readValue(record.requestJson, C2CTodayScheduledRequest::class.java)
request.types.contains(TrainingType.WEIGHT)
} catch (e: Exception) {
// If JSON is incompatible, exclude it from the wellness list
false
}
}
}

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) {
wellnessService.copyWellnessC2C(request.forToday())
}

private fun ScheduleRequestEntity.toSchedulable(): C2CTodayScheduledRequest {
return objectMapper.readValue(requestJson, C2CTodayScheduledRequest::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.freekode.tp2intervals.app.workout.schedule

import com.fasterxml.jackson.databind.ObjectMapper
import org.freekode.tp2intervals.app.workout.WorkoutService
import org.freekode.tp2intervals.domain.TrainingType
import org.freekode.tp2intervals.infrastructure.schedule.ScheduleRequestEntity
import org.freekode.tp2intervals.infrastructure.schedule.ScheduleRequestRepository
import org.slf4j.LoggerFactory
Expand All @@ -24,7 +25,32 @@ class WorkoutScheduledJob(
}

fun getRequests() =
scheduleRequestRepository.findAll().toList()
scheduleRequestRepository.findAll()
.filter { record ->
try {
// Internal conversion just to check the types inside the JSON
val request = objectMapper.readValue(
record.requestJson,
org.freekode.tp2intervals.app.wellness.schedule.C2CTodayScheduledRequest::class.java
)
request.types.contains(TrainingType.SWIM)
|| request.types.contains(TrainingType.BIKE)
|| request.types.contains(TrainingType.RUN)
|| request.types.contains(TrainingType.BRICK)
|| request.types.contains(TrainingType.RACE)
|| request.types.contains(TrainingType.DAY_OFF)
|| request.types.contains(TrainingType.STRENGTH)
|| request.types.contains(TrainingType.MTB)
|| request.types.contains(TrainingType.VIRTUAL_BIKE)
|| request.types.contains(TrainingType.WALK)
|| request.types.contains(TrainingType.NOTE)
|| request.types.contains(TrainingType.UNKNOWN)
} catch (e: Exception) {
// If JSON is incompatible, exclude it from the wellness list
false
}
}
.toList()

fun deleteRequest(id: Int) {
scheduleRequestRepository.deleteById(id)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.freekode.tp2intervals.domain

enum class CategoryType {
WORKOUT, RACE_A, RACE_B, RACE_C, NOTE, PLAN, HOLIDAY, SICK, INJURED, SET_EFTP, FITNESS_DAYS, SEASON_START, TARGET, SET_FITNESS, WELLNESS
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package org.freekode.tp2intervals.domain

enum class TrainingType(val title: String) {
BIKE("Ride"),
MTB("MTB"),
VIRTUAL_BIKE("Virtual Ride"),
RUN("Run"),
SWIM("Swim"),
WALK("Walk"),
WEIGHT("Weight"),
NOTE("Note"),
UNKNOWN("Unknown");
enum class TrainingType(val title: String, val category: CategoryType) {
SWIM("Swim", CategoryType.WORKOUT),
BIKE("Ride", CategoryType.WORKOUT),
RUN("Run", CategoryType.WORKOUT),
BRICK("Brick", CategoryType.NOTE),
//crosstrain
RACE("Race", CategoryType.WORKOUT),
DAY_OFF("Day-off", CategoryType.NOTE),
STRENGTH("Strength", CategoryType.WORKOUT),

MTB("MTB", CategoryType.WORKOUT),
VIRTUAL_BIKE("Virtual Ride", CategoryType.WORKOUT),
WALK("Walk", CategoryType.WORKOUT),
NOTE("Note", CategoryType.NOTE),
UNKNOWN("Unknown", CategoryType.WORKOUT),

//WELLNESS (Metrics)
WEIGHT("Weight Wellness", CategoryType.WELLNESS),
;

companion object {
val DEFAULT_LIST = listOf(BIKE, VIRTUAL_BIKE, MTB, RUN, SWIM)
val DEFAULT_LIST = listOf(BIKE, VIRTUAL_BIKE, MTB, RUN, SWIM, DAY_OFF, NOTE, BRICK)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.freekode.tp2intervals.domain.wellness

class Wellness(
val date: String?,
val type: String?,
val weight: Double?,
) {
companion object {
const val DATE_FORMAT = "yyyy-MM-dd"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.freekode.tp2intervals.domain.wellness

import org.freekode.tp2intervals.domain.Platform
import java.time.LocalDate

interface WellnessRepository {
fun platform(): Platform

fun getFromCalendar(startDate: LocalDate, endDate: LocalDate): List<Wellness>

fun saveToCalendar(wellnesses: List<Wellness?>, startDate: LocalDate, endDate: LocalDate)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package org.freekode.tp2intervals.domain.workout

import java.io.Serializable
import java.time.Duration
import java.time.LocalDate
import org.freekode.tp2intervals.domain.ExternalData
import org.freekode.tp2intervals.domain.TrainingType
import java.io.Serializable
import java.time.Duration
import java.time.temporal.ChronoUnit

data class WorkoutDetails(
val type: TrainingType,
Expand All @@ -22,8 +22,9 @@ data class WorkoutDetails(

other as WorkoutDetails

if (name != other.name) return false
if (duration != other.duration) return false
if (name.uppercase() != other.name.uppercase()
&& !name.uppercase().startsWith(other.name.uppercase())) return false
if (duration?.truncatedTo(ChronoUnit.MINUTES) != other.duration?.truncatedTo(ChronoUnit.MINUTES)) return false
if (externalData != other.externalData) return false

return true
Expand Down
Loading