Dashboard
This commit is contained in:
6
.idea/data_source_mapping.xml
generated
Normal file
6
.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourcePerFileMappings">
|
||||
<file url="file://$PROJECT_DIR$/peakup-backend/src/main/resources/db/migration/V40__rename_goal_metric_aggregate_table.sql" value="146146a8-fc52-45aa-b5e3-48b7a37a3a5f" />
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/sqldialects.xml
generated
1
.idea/sqldialects.xml
generated
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/peakup-backend/src/main/resources/db/migration/V40__rename_goal_metric_aggregate_table.sql" dialect="PostgreSQL" />
|
||||
<file url="file://$PROJECT_DIR$/peakup-backend/src/test/resources/skripts.sql" dialect="GenericSQL" />
|
||||
<file url="PROJECT" dialect="PostgreSQL" />
|
||||
</component>
|
||||
|
||||
@@ -23,6 +23,20 @@ public class IntervalsActivityDAO extends AbstractDAO<IntervalsActivity> {
|
||||
return IntervalsActivity.class;
|
||||
}
|
||||
|
||||
public @Nullable IntervalsActivity getEarliestActivity(final Long athleteId) {
|
||||
final CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
final CriteriaQuery<IntervalsActivity> cq = cb.createQuery(IntervalsActivity.class);
|
||||
final Root<IntervalsActivity> root = cq.from(IntervalsActivity.class);
|
||||
cq.select(root);
|
||||
cq.where(cb.equal(root.get(AbstractActivity_.athleteId), athleteId));
|
||||
cq.orderBy(cb.asc(root.get(AbstractActivity_.startTime)));
|
||||
return em.createQuery(cq)
|
||||
.setMaxResults(1)
|
||||
.getResultStream()
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public @Nullable IntervalsActivity getLatestActivity(final Long athleteId) {
|
||||
final CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
final CriteriaQuery<IntervalsActivity> cq = cb.createQuery(IntervalsActivity.class);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.peakup.activity.service;
|
||||
import com.peakup.activity.dao.IntervalsActivityDAO;
|
||||
import com.peakup.activity.model.IntervalsActivity;
|
||||
import com.peakup.common.service.AbstractService;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
@@ -30,6 +31,11 @@ public class IntervalsActivityService extends AbstractService<IntervalsActivity,
|
||||
return super.getById(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public @Nullable IntervalsActivity getEarliestActivity(final Long athleteId) {
|
||||
return intervalsActivityDAO.getEarliestActivity(athleteId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<IntervalsActivity> getAllByAthleteId(final Long athleteId) {
|
||||
return intervalsActivityDAO.getAllByAthleteId(athleteId);
|
||||
|
||||
@@ -40,6 +40,23 @@ public class GoalMetricPeriodTotalDAO extends AbstractDAO<GoalMetricPeriodTotal>
|
||||
return em.createQuery(cq).getResultStream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public @Nullable GoalMetricPeriodTotal findLastWeeklyByAthlete(final long athleteId) {
|
||||
final CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
final CriteriaQuery<GoalMetricPeriodTotal> cq = cb.createQuery(GoalMetricPeriodTotal.class);
|
||||
final Root<GoalMetricPeriodTotal> root = cq.from(GoalMetricPeriodTotal.class);
|
||||
cq.select(root)
|
||||
.where(
|
||||
cb.equal(root.get(GoalMetricPeriodTotal_.athleteId), athleteId),
|
||||
cb.equal(root.get(GoalMetricPeriodTotal_.period), GoalPeriodEnum.WEEKLY),
|
||||
cb.equal(root.get(GoalMetricPeriodTotal_.metric), GoalMetricEnum.DISTANCE)
|
||||
)
|
||||
.orderBy(
|
||||
cb.desc(root.get(GoalMetricPeriodTotal_.periodYear)),
|
||||
cb.desc(root.get(GoalMetricPeriodTotal_.periodIndex))
|
||||
);
|
||||
return em.createQuery(cq).setMaxResults(1).getResultStream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public List<GoalMetricPeriodTotal> findByAthleteAndMetricAndPeriod(
|
||||
final long athleteId,
|
||||
final GoalMetricEnum metric,
|
||||
|
||||
@@ -75,11 +75,38 @@ public class GoalMetricPeriodTotalService extends AbstractService<GoalMetricPeri
|
||||
return intervalsActivityService.getByAthleteIdBetween(athleteId, weekStart.toString(), weekEnd.toString());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void recalculateUpToCurrentWeek(final long athleteId) {
|
||||
final LocalDate today = LocalDate.now(ZoneOffset.UTC);
|
||||
final GoalMetricPeriodTotal lastWeekly = goalMetricAggregateDAO.findLastWeeklyByAthlete(athleteId);
|
||||
final LocalDate startDate;
|
||||
if (lastWeekly != null) {
|
||||
startDate = LocalDate.of(lastWeekly.getPeriodYear(), 1, 4)
|
||||
.with(WeekFields.ISO.weekOfWeekBasedYear(), lastWeekly.getPeriodIndex())
|
||||
.with(WeekFields.ISO.dayOfWeek(), 1);
|
||||
} else {
|
||||
final IntervalsActivity earliestActivity = intervalsActivityService.getEarliestActivity(athleteId);
|
||||
if (earliestActivity == null) {
|
||||
return;
|
||||
}
|
||||
startDate = earliestActivity.getStartTime().atZone(ZoneOffset.UTC).toLocalDate()
|
||||
.with(WeekFields.ISO.dayOfWeek(), 1);
|
||||
}
|
||||
LocalDate weekStart = startDate;
|
||||
while (!weekStart.isAfter(today)) {
|
||||
recalculateForAthlete(athleteId,
|
||||
weekStart.get(WeekFields.ISO.weekBasedYear()),
|
||||
weekStart.get(WeekFields.ISO.weekOfWeekBasedYear()));
|
||||
weekStart = weekStart.plusWeeks(1);
|
||||
}
|
||||
}
|
||||
|
||||
private void recalculateMetricForWeek(
|
||||
final long athleteId,
|
||||
final GoalMetricEnum metric,
|
||||
final List<IntervalsActivity> weekActivities) {
|
||||
// WEEKLY: always recompute fresh from this week's activities
|
||||
// WEEKLY: recompute fresh from activities; apply delta to MONTHLY/YEARLY to keep them idempotent.
|
||||
// MONTHLY/YEARLY are attributed to the week's Monday — a slight simplification at month boundaries.
|
||||
final Map<PeriodKey, Double> weeklySums = new HashMap<>();
|
||||
for (final IntervalsActivity activity : weekActivities) {
|
||||
final LocalDate date = activity.getStartTime().atZone(ZoneOffset.UTC).toLocalDate();
|
||||
@@ -89,26 +116,29 @@ public class GoalMetricPeriodTotalService extends AbstractService<GoalMetricPeri
|
||||
weeklySums.merge(key, metric.extractValue(activity), Double::sum);
|
||||
}
|
||||
for (final Map.Entry<PeriodKey, Double> entry : weeklySums.entrySet()) {
|
||||
upsertAggregate(athleteId, metric, GoalPeriodEnum.WEEKLY,
|
||||
entry.getKey().year(), entry.getKey().index(), entry.getValue());
|
||||
}
|
||||
final int weekYear = entry.getKey().year();
|
||||
final int weekIndex = entry.getKey().index();
|
||||
final double newWeeklyValue = entry.getValue();
|
||||
|
||||
// MONTHLY, YEARLY: reload existing aggregate (base from before this week) and add this week's contribution.
|
||||
// Activities are grouped by period key — if the week spans a month or year boundary, each side is
|
||||
// handled as a separate key and its own aggregate is updated independently.
|
||||
for (final GoalPeriodEnum period : List.of(GoalPeriodEnum.MONTHLY, GoalPeriodEnum.YEARLY)) {
|
||||
final Map<PeriodKey, Double> periodSums = new HashMap<>();
|
||||
for (final IntervalsActivity activity : weekActivities) {
|
||||
final LocalDate date = activity.getStartTime().atZone(ZoneOffset.UTC).toLocalDate();
|
||||
final PeriodKey key = new PeriodKey(period.getPeriodYear(date), period.getPeriodIndex(date));
|
||||
periodSums.merge(key, metric.extractValue(activity), Double::sum);
|
||||
}
|
||||
for (final Map.Entry<PeriodKey, Double> entry : periodSums.entrySet()) {
|
||||
final GoalMetricPeriodTotal existing = goalMetricAggregateDAO.findByKey(
|
||||
athleteId, metric, period, entry.getKey().year(), entry.getKey().index());
|
||||
final double baseValue = existing != null ? existing.getAggregatedValue() : 0.0;
|
||||
upsertAggregate(athleteId, metric, period,
|
||||
entry.getKey().year(), entry.getKey().index(), baseValue + entry.getValue());
|
||||
final GoalMetricPeriodTotal previousWeekly = goalMetricAggregateDAO.findByKey(
|
||||
athleteId, metric, GoalPeriodEnum.WEEKLY, weekYear, weekIndex);
|
||||
final double previousWeeklyValue = previousWeekly != null ? previousWeekly.getAggregatedValue() : 0.0;
|
||||
final double weeklyDelta = newWeeklyValue - previousWeeklyValue;
|
||||
|
||||
upsertAggregate(athleteId, metric, GoalPeriodEnum.WEEKLY, weekYear, weekIndex, newWeeklyValue);
|
||||
|
||||
if (weeklyDelta != 0.0) {
|
||||
final LocalDate weekStartDate = LocalDate.of(weekYear, 1, 4)
|
||||
.with(WeekFields.ISO.weekOfWeekBasedYear(), weekIndex)
|
||||
.with(WeekFields.ISO.dayOfWeek(), 1);
|
||||
for (final GoalPeriodEnum period : List.of(GoalPeriodEnum.MONTHLY, GoalPeriodEnum.YEARLY)) {
|
||||
final int periodYear = period.getPeriodYear(weekStartDate);
|
||||
final int periodIndex = period.getPeriodIndex(weekStartDate);
|
||||
final GoalMetricPeriodTotal existing = goalMetricAggregateDAO.findByKey(
|
||||
athleteId, metric, period, periodYear, periodIndex);
|
||||
final double baseValue = existing != null ? existing.getAggregatedValue() : 0.0;
|
||||
upsertAggregate(athleteId, metric, period, periodYear, periodIndex, baseValue + weeklyDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.peakup.connector.intervals.model.IntervalsConnector;
|
||||
import com.peakup.connector.intervals.service.IntervalsConnectorService;
|
||||
import com.peakup.connector.strava.model.StravaConnector;
|
||||
import com.peakup.connector.strava.service.StravaConnectorService;
|
||||
import com.peakup.goal.service.GoalMetricPeriodTotalService;
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
import io.smallrye.common.annotation.RunOnVirtualThread;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
@@ -34,6 +35,9 @@ public class ActivityImportScheduler {
|
||||
@Inject
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
IntervalsContext intervalsContext;
|
||||
@Inject
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
GoalMetricPeriodTotalService goalMetricPeriodTotalService;
|
||||
|
||||
@Scheduled(every = "20m")
|
||||
@RunOnVirtualThread
|
||||
@@ -74,6 +78,14 @@ public class ActivityImportScheduler {
|
||||
intervalsContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
for (final IntervalsConnector connector : intervalsConnectorService.getIntervalsConnectors()) {
|
||||
try {
|
||||
goalMetricPeriodTotalService.recalculateUpToCurrentWeek(connector.getAthleteId());
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to recalculate goal metric period totals for athleteId {}.", connector.getAthleteId(), e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
running.set(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user