Show expected vs actual pace/HR per lap in the Review Queue
Activities recorded from a structured Garmin workout carry a workoutId; when its steps line up 1:1 with the recorded laps, the per-step target pace/HR zone is resolved (via a Karvonen lookup for named zones) and stored on each lap. The Review Queue plots it against the actual per-lap pace/HR so the user can eyeball whether a run matches its plan while sorting it.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -233,6 +234,10 @@ func (s *Service) FillPendingDetails(ctx context.Context, limit int) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
profile, err := s.db.GetProfile(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load profile: %w", err)
|
||||
}
|
||||
|
||||
s.setProgress(0, len(pending))
|
||||
defer s.setProgress(0, 0)
|
||||
@@ -245,7 +250,7 @@ func (s *Service) FillPendingDetails(ctx context.Context, limit int) error {
|
||||
case <-time.After(s.cfg.InterCallDelay):
|
||||
}
|
||||
}
|
||||
if err := s.fillActivityDetails(ctx, a); err != nil {
|
||||
if err := s.fillActivityDetails(ctx, a, profile); err != nil {
|
||||
return fmt.Errorf("fill details for activity %d: %w", a.GarminActivityID, err)
|
||||
}
|
||||
if err := s.ClassifyActivity(ctx, a.ID); err != nil {
|
||||
@@ -256,7 +261,7 @@ func (s *Service) FillPendingDetails(ctx context.Context, limit int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) fillActivityDetails(ctx context.Context, a store.Activity) error {
|
||||
func (s *Service) fillActivityDetails(ctx context.Context, a store.Activity, profile store.Profile) error {
|
||||
splits, err := s.garmin.GetActivitySplits(ctx, a.GarminActivityID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get_activity_splits: %w", err)
|
||||
@@ -266,11 +271,21 @@ func (s *Service) fillActivityDetails(ctx context.Context, a store.Activity) err
|
||||
return fmt.Errorf("get_activity_details: %w", err)
|
||||
}
|
||||
|
||||
targets := make([]*garmin.WorkoutStep, len(splits.Laps))
|
||||
if a.WorkoutID != nil {
|
||||
workout, err := s.garmin.GetWorkoutByID(ctx, *a.WorkoutID)
|
||||
if err != nil {
|
||||
log.Printf("sync: get_workout_by_id(%d) for activity %d failed, continuing without target zones: %v", *a.WorkoutID, a.GarminActivityID, err)
|
||||
} else {
|
||||
targets = alignWorkoutTargets(splits.Laps, workout)
|
||||
}
|
||||
}
|
||||
|
||||
samples := garmin.ExtractSamples(details)
|
||||
if err := s.db.ReplaceActivitySamples(ctx, a.ID, toSampleRows(samples)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.db.ReplaceLaps(ctx, a.ID, toLapRows(splits.Laps, samples)); err != nil {
|
||||
if err := s.db.ReplaceLaps(ctx, a.ID, toLapRows(splits.Laps, samples, targets, profile)); err != nil {
|
||||
return err
|
||||
}
|
||||
detailsRaw, _ := json.Marshal(details)
|
||||
|
||||
Reference in New Issue
Block a user