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:
2026-07-19 11:55:13 +02:00
parent 0610897541
commit af41aa0f7f
16 changed files with 666 additions and 47 deletions

View File

@@ -164,6 +164,12 @@ func TestReviewQueueResolve(t *testing.T) {
}); err != nil {
t.Fatalf("InsertKindAssignment: %v", err)
}
targetLow, targetHigh := 3.0, 3.5
if err := db.ReplaceLaps(ctx, activityID, []store.Lap{
{LapIndex: 1, TargetPaceLowMps: &targetLow, TargetPaceHighMps: &targetHigh},
}); err != nil {
t.Fatalf("ReplaceLaps: %v", err)
}
router := s.Router()
rec := doJSON(t, router, http.MethodGet, "/api/review-queue/", nil)
@@ -175,6 +181,10 @@ func TestReviewQueueResolve(t *testing.T) {
if len(queue) != 1 {
t.Fatalf("expected 1 item in review queue, got %d: %s", len(queue), rec.Body.String())
}
laps, _ := queue[0]["laps"].([]any)
if len(laps) != 1 {
t.Fatalf("expected 1 lap in review queue item, got %d: %s", len(laps), rec.Body.String())
}
rec = doJSON(t, router, http.MethodPost, "/api/review-queue/"+itoa(activityID)+"/resolve", map[string]any{"workout_kind_id": kindID})
if rec.Code != http.StatusOK {

View File

@@ -21,6 +21,7 @@ func (s *Server) handleReviewQueue(w http.ResponseWriter, r *http.Request) {
type item struct {
store.KindAssignment
Activity store.Activity `json:"activity"`
Laps []store.Lap `json:"laps"`
}
items := make([]item, 0, len(queue))
for _, a := range queue {
@@ -32,7 +33,12 @@ func (s *Server) handleReviewQueue(w http.ResponseWriter, r *http.Request) {
if !ok {
continue
}
items = append(items, item{KindAssignment: a, Activity: activity})
laps, err := s.DB.LapsForActivity(r.Context(), a.ActivityID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
items = append(items, item{KindAssignment: a, Activity: activity, Laps: laps})
}
// Most recent run first, by when the activity actually happened (not