package api import ( "encoding/json" "errors" "net/http" "geniusrun/backend/internal/store" ) // validateProfile checks the HR zones are ascending and gap-free once they // start, but deliberately does NOT require zone 1 to start at 0% or zone 5 // to end at 100%: real Karvonen HR training zones (e.g. the seeded defaults, // 50-60/60-70/70-80/80-90/90-100) don't cover the 0-50% range at all -- // below zone 1 simply isn't a named training zone. func validateProfile(p store.Profile) error { if p.RestingHeartRate != nil && p.MaxHeartRate != nil && *p.RestingHeartRate >= *p.MaxHeartRate { return errors.New("resting heart rate must be less than max heart rate") } zones := [][2]float64{ {p.HRZone1MinPct, p.HRZone1MaxPct}, {p.HRZone2MinPct, p.HRZone2MaxPct}, {p.HRZone3MinPct, p.HRZone3MaxPct}, {p.HRZone4MinPct, p.HRZone4MaxPct}, {p.HRZone5MinPct, p.HRZone5MaxPct}, } for i, z := range zones { if z[0] >= z[1] { return errors.New("each HR zone's min must be less than its max") } if i > 0 && z[0] != zones[i-1][1] { return errors.New("HR zones must be contiguous and non-overlapping") } } return nil } func (s *Server) handleGetProfile(w http.ResponseWriter, r *http.Request) { p, err := s.DB.GetProfile(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, p) } func (s *Server) handleUpdateProfile(w http.ResponseWriter, r *http.Request) { var p store.Profile if err := json.NewDecoder(r.Body).Decode(&p); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if err := validateProfile(p); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if err := s.DB.UpdateProfile(r.Context(), p); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.Garmin.UpdateCredentials(p.GarminEmail, p.GarminPassword) updated, err := s.DB.GetProfile(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, updated) }