2026-07-17 18:33:06 +02:00
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"net/http"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// detailFillBatchSize bounds how many activities' details/splits are fetched
|
|
|
|
|
// per sync trigger, matching the sequential rate-limited fetch in
|
|
|
|
|
// internal/sync.Service.FillPendingDetails.
|
|
|
|
|
const detailFillBatchSize = 50
|
|
|
|
|
|
2026-07-19 12:41:38 +02:00
|
|
|
// handleSyncRun does a full sync pass: Backfill first (resumes from the
|
|
|
|
|
// watermark, so widening the configured history horizon between clicks is
|
|
|
|
|
// picked up automatically), then IncrementalSync (catches anything new since
|
|
|
|
|
// the latest known activity), then fills in details for whatever's still
|
|
|
|
|
// missing them. Activities already fully processed are left untouched --
|
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
|
|
|
// see internal/sync.Service.FillPendingDetails. Recorded as a single
|
|
|
|
|
// FullSync run so "last sync" reports the combined activity count, not just
|
|
|
|
|
// whichever of Backfill/IncrementalSync happened to finish last.
|
2026-07-17 18:33:06 +02:00
|
|
|
func (s *Server) handleSyncRun(w http.ResponseWriter, r *http.Request) {
|
2026-07-25 18:28:02 +02:00
|
|
|
userID := userIDFromContext(r.Context())
|
|
|
|
|
svc, err := s.syncFor(r.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ok := s.backgroundSync(userID, func(ctx context.Context) error {
|
|
|
|
|
return svc.FullSync(ctx, detailFillBatchSize)
|
2026-07-17 18:33:06 +02:00
|
|
|
})
|
|
|
|
|
if !ok {
|
|
|
|
|
writeError(w, http.StatusConflict, "a sync is already in progress")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusAccepted, map[string]string{"status": "started"})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 12:41:38 +02:00
|
|
|
// handleSyncReset wipes every synced activity (and its laps/samples/kind
|
|
|
|
|
// assignments) and rewinds the backfill watermark, so the next Sync Now
|
|
|
|
|
// performs a genuinely fresh pull from Garmin. Destructive -- the frontend
|
|
|
|
|
// gates this behind a confirmation.
|
|
|
|
|
func (s *Server) handleSyncReset(w http.ResponseWriter, r *http.Request) {
|
2026-07-25 18:28:02 +02:00
|
|
|
userID := userIDFromContext(r.Context())
|
|
|
|
|
svc, err := s.syncFor(r.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ok := s.backgroundSync(userID, func(ctx context.Context) error {
|
|
|
|
|
return svc.ResetAll(ctx)
|
2026-07-17 18:33:06 +02:00
|
|
|
})
|
|
|
|
|
if !ok {
|
|
|
|
|
writeError(w, http.StatusConflict, "a sync is already in progress")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusAccepted, map[string]string{"status": "started"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleSyncRuns(w http.ResponseWriter, r *http.Request) {
|
2026-07-25 18:28:02 +02:00
|
|
|
userID := userIDFromContext(r.Context())
|
|
|
|
|
runs, err := s.DB.ListSyncRuns(r.Context(), userID, 20)
|
2026-07-17 18:33:06 +02:00
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusOK, runs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleSyncStatus(w http.ResponseWriter, r *http.Request) {
|
2026-07-25 18:28:02 +02:00
|
|
|
userID := userIDFromContext(r.Context())
|
|
|
|
|
run, ok, err := s.DB.LatestSyncRun(r.Context(), userID)
|
2026-07-17 18:33:06 +02:00
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-07-27 08:27:34 +02:00
|
|
|
pendingDetails, err := s.DB.CountActivitiesMissingDetails(r.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
pendingWorkouts, err := s.DB.CountActivitiesMissingWorkout(r.Context(), userID)
|
2026-07-17 18:33:06 +02:00
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.mu.Lock()
|
2026-07-25 18:28:02 +02:00
|
|
|
inProgress := s.userSyncRunning[userID]
|
2026-07-17 18:33:06 +02:00
|
|
|
s.mu.Unlock()
|
|
|
|
|
|
2026-07-25 18:28:02 +02:00
|
|
|
svc, err := s.syncFor(r.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
progress := svc.Progress()
|
2026-07-17 18:33:06 +02:00
|
|
|
|
|
|
|
|
resp := map[string]any{
|
|
|
|
|
"in_progress": inProgress,
|
2026-07-27 08:27:34 +02:00
|
|
|
"progress": progress,
|
|
|
|
|
"activities_pending_details": pendingDetails,
|
|
|
|
|
"workouts_pending": pendingWorkouts,
|
2026-07-17 18:33:06 +02:00
|
|
|
}
|
|
|
|
|
if ok {
|
|
|
|
|
resp["last_run"] = run
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
|
|
|
}
|