Files
geniusrun/backend/internal/api/display_fields.go
Christophe Vila 518bccf5ab Dedup Garmin data storage, configurable chart colors, taxonomy fixes, sync/progression fixes
- 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>
2026-07-20 06:33:47 +02:00

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
}