- Remove duplicated Garmin fields from storage; decode display-only fields (activity name/type, lap duration/HR, structured workout raw JSON) from RawJSON at API-response time instead of storing redundant columns - Add a fully configurable chart color system (pace/HR main-line colors, 4 effort-kind colors, tint/darken/brighten intensity knobs) under Profile > Chart colors - Rename training types and fix their display order (Easy, Long, 60'/30' Threshold, Tempo, Intervals, MAS Test, Race) everywhere they're listed - Add an Efficiency Factor progression metric; fix Progression chart axes to use tight non-zero-based domains, m:ss/km pace formatting, and rounded ticks instead of raw floating-point labels - Expose the raw get_workout_by_id() payload in the raw-data viewer alongside activity/lap/detail JSON; enlarge the modal and shrink array indentation for readability - Fix "last sync" reporting a meaningless activity count: record one combined sync run per manual "Sync now" and count genuinely new activities instead of re-listing whatever Garmin returned for the queried window - Let a Review Queue activity be manually cleared back to Unclassified, and make "Reset all" available even while disconnected from Garmin Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
76 lines
2.2 KiB
Go
76 lines
2.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"smartrun/backend/internal/store"
|
|
)
|
|
|
|
// activityResponse adds back ActivityName/ActivityType as fields decoded
|
|
// from RawJSON. They're no longer stored as their own columns (Phase 2 of
|
|
// the 2026-07 duplication cleanup -- see store.Activity's doc comment):
|
|
// Garmin's raw JSON is the only place they live now, decoded fresh for each
|
|
// API response instead of kept as a redundant copy in the database.
|
|
type activityResponse struct {
|
|
store.Activity
|
|
ActivityName string `json:"ActivityName"`
|
|
ActivityType string `json:"ActivityType"`
|
|
}
|
|
|
|
func toActivityResponse(a store.Activity) activityResponse {
|
|
name, typeKey := decodeActivityDisplayFields(a.RawJSON)
|
|
return activityResponse{Activity: a, ActivityName: name, ActivityType: typeKey}
|
|
}
|
|
|
|
type rawActivityDisplayFields struct {
|
|
ActivityName string `json:"activityName"`
|
|
ActivityType struct {
|
|
TypeKey string `json:"typeKey"`
|
|
} `json:"activityType"`
|
|
}
|
|
|
|
func decodeActivityDisplayFields(rawJSON string) (name, typeKey string) {
|
|
var f rawActivityDisplayFields
|
|
_ = json.Unmarshal([]byte(rawJSON), &f) // best-effort -- zero values are fine if rawJSON is empty/malformed
|
|
return f.ActivityName, f.ActivityType.TypeKey
|
|
}
|
|
|
|
// lapResponse adds back DurationSeconds/AvgHR as fields decoded from
|
|
// RawJSON, same rationale as activityResponse -- the frontend chart still
|
|
// needs them (x-axis timing, dashed average-HR line) even though nothing on
|
|
// the backend does.
|
|
type lapResponse struct {
|
|
store.Lap
|
|
DurationSeconds float64 `json:"DurationSeconds"`
|
|
AvgHR *float64 `json:"AvgHR"`
|
|
}
|
|
|
|
func toLapResponse(l store.Lap) lapResponse {
|
|
duration, avgHR := decodeLapDisplayFields(l.RawJSON)
|
|
return lapResponse{Lap: l, DurationSeconds: duration, AvgHR: avgHR}
|
|
}
|
|
|
|
func toLapResponses(laps []store.Lap) []lapResponse {
|
|
out := make([]lapResponse, len(laps))
|
|
for i, l := range laps {
|
|
out[i] = toLapResponse(l)
|
|
}
|
|
return out
|
|
}
|
|
|
|
type rawLapDisplayFields struct {
|
|
Duration float64 `json:"duration"`
|
|
AverageHR float64 `json:"averageHR"`
|
|
}
|
|
|
|
func decodeLapDisplayFields(rawJSON string) (durationSeconds float64, avgHR *float64) {
|
|
var f rawLapDisplayFields
|
|
_ = json.Unmarshal([]byte(rawJSON), &f)
|
|
durationSeconds = f.Duration
|
|
if f.AverageHR > 0 {
|
|
v := f.AverageHR
|
|
avgHR = &v
|
|
}
|
|
return
|
|
}
|