package api import ( "net/http" "sort" "strconv" "github.com/go-chi/chi/v5" "geniusrun/backend/internal/store" ) type progressionPoint struct { Date string `json:"date"` ActivityID int64 `json:"activity_id"` Value float64 `json:"value"` } func metricValue(metric string, a store.Activity) (float64, bool) { switch metric { case "pace": if a.AvgSpeedMps == nil || *a.AvgSpeedMps <= 0 { return 0, false } return 1000 / *a.AvgSpeedMps, true case "hr": if a.AvgHR == nil { return 0, false } return *a.AvgHR, true case "vo2max": if a.VO2MaxValue == nil { return 0, false } return *a.VO2MaxValue, true case "aerobic_te": if a.AerobicTrainingEffect == nil { return 0, false } return *a.AerobicTrainingEffect, true case "anaerobic_te": if a.AnaerobicTrainingEffect == nil { return 0, false } return *a.AnaerobicTrainingEffect, true case "efficiency_factor": // Speed per heartbeat: how much ground each beat covers, on average. // A rising trend at similar effort/HR over time is a classic sign of // improving aerobic fitness, independent of pace/HR shown alone. // Scaled by 1000 (m/s per bpm is otherwise a tiny fraction like // 0.02, hard to read/compare at a glance). if a.AvgSpeedMps == nil || a.AvgHR == nil || *a.AvgHR <= 0 { return 0, false } return (*a.AvgSpeedMps / *a.AvgHR) * 1000, true default: return 0, false } } // handleProgression returns a time series of the requested metric for every // activity currently assigned to a workout kind, for progression charts. func (s *Server) handleProgression(w http.ResponseWriter, r *http.Request) { kindID, err := strconv.ParseInt(chi.URLParam(r, "kindID"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid workout kind id") return } metric := r.URL.Query().Get("metric") if metric == "" { metric = "pace" } from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") assignments, err := s.DB.AssignmentsForKind(r.Context(), kindID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } points := []progressionPoint{} for _, a := range assignments { activity, ok, err := s.DB.GetActivity(r.Context(), a.ActivityID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if !ok { continue } if from != "" && activity.StartTimeUTC < from { continue } if to != "" && activity.StartTimeUTC > to+" 23:59:59" { continue } value, ok := metricValue(metric, activity) if !ok { continue } points = append(points, progressionPoint{Date: activity.StartTimeUTC, ActivityID: activity.ID, Value: value}) } sort.Slice(points, func(i, j int) bool { return points[i].Date < points[j].Date }) writeJSON(w, http.StatusOK, points) }