Files
geniusrun/backend/internal/api/review.go
Christophe Vila 7f4877df57 feat: review queue polish (pace, sort, type filter), drop unused phase-detection UI
Review Queue: show pace alongside distance/duration/HR, sort by activity
date (most recent first) instead of classification timestamp, and add a
filter by workout kind with a special "Unsorted" option for runs where
the rule engine found zero candidates at all (distinct from ambiguous
multi-candidate runs).

Profile: remove the phase-detection warm-up/cool-down section from the
UI -- not used by anything yet (phase segmentation is future work) and
was adding noise. The underlying fields are untouched so no data is lost
and the settings screen still round-trips them on save.
2026-07-18 08:39:23 +02:00

87 lines
2.3 KiB
Go

package api
import (
"encoding/json"
"net/http"
"sort"
"strconv"
"github.com/go-chi/chi/v5"
"smartrun/backend/internal/store"
)
func (s *Server) handleReviewQueue(w http.ResponseWriter, r *http.Request) {
queue, err := s.DB.ReviewQueue(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
type item struct {
store.KindAssignment
Activity store.Activity `json:"activity"`
}
items := make([]item, 0, len(queue))
for _, a := range queue {
activity, ok, err := s.DB.GetActivity(r.Context(), a.ActivityID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !ok {
continue
}
items = append(items, item{KindAssignment: a, Activity: activity})
}
// Most recent run first, by when the activity actually happened (not
// when the rule engine flagged it), so the queue reads like a log.
sort.Slice(items, func(i, j int) bool {
return items[i].Activity.StartTimeUTC > items[j].Activity.StartTimeUTC
})
writeJSON(w, http.StatusOK, items)
}
func (s *Server) handleResolveReview(w http.ResponseWriter, r *http.Request) {
activityID, err := strconv.ParseInt(chi.URLParam(r, "activityID"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid activity id")
return
}
var body struct {
WorkoutKindID int64 `json:"workout_kind_id"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if body.WorkoutKindID == 0 {
writeError(w, http.StatusBadRequest, "workout_kind_id is required")
return
}
if _, ok, err := s.DB.GetWorkoutKind(r.Context(), body.WorkoutKindID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
} else if !ok {
writeError(w, http.StatusBadRequest, "workout kind not found")
return
}
kindID := body.WorkoutKindID
if _, err := s.DB.InsertKindAssignment(r.Context(), store.KindAssignment{
ActivityID: activityID,
WorkoutKindID: &kindID,
AssignmentSource: store.AssignmentSourceManual,
Status: store.AssignmentStatusAssigned,
CandidateKindsJSON: "[]",
}); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "resolved"})
}