Initial commit: smartrun MVP
Garmin run classification and progression tracker. Go backend (MCP client to mcp-garmin, SQLite store, deterministic rule engine, REST API) and React/TS frontend (Dashboard, Review Queue, Workout Kinds). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
66
backend/internal/store/samples.go
Normal file
66
backend/internal/store/samples.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Sample is one ~1-second telemetry reading for an activity.
|
||||
type Sample struct {
|
||||
ElapsedSeconds float64
|
||||
TimestampMs int64
|
||||
HeartRate *float64
|
||||
SpeedMps *float64
|
||||
DistanceM *float64
|
||||
ElevationM *float64
|
||||
}
|
||||
|
||||
// ReplaceActivitySamples deletes any existing samples for activityID and
|
||||
// bulk-inserts the given set, so re-syncing an activity's details is idempotent.
|
||||
func (db *DB) ReplaceActivitySamples(ctx context.Context, activityID int64, samples []Sample) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin replace samples tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM activity_samples WHERE activity_id = ?`, activityID); err != nil {
|
||||
return fmt.Errorf("delete existing samples for activity %d: %w", activityID, err)
|
||||
}
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, `
|
||||
INSERT INTO activity_samples (activity_id, elapsed_seconds, timestamp_ms, heart_rate, speed_mps, distance_m, elevation_m)
|
||||
VALUES (?,?,?,?,?,?,?)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare insert sample: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, s := range samples {
|
||||
if _, err := stmt.ExecContext(ctx, activityID, s.ElapsedSeconds, s.TimestampMs, s.HeartRate, s.SpeedMps, s.DistanceM, s.ElevationM); err != nil {
|
||||
return fmt.Errorf("insert sample for activity %d: %w", activityID, err)
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// SamplesForActivity returns all samples for an activity, ordered by elapsed_seconds.
|
||||
func (db *DB) SamplesForActivity(ctx context.Context, activityID int64) ([]Sample, error) {
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT elapsed_seconds, timestamp_ms, heart_rate, speed_mps, distance_m, elevation_m
|
||||
FROM activity_samples WHERE activity_id = ? ORDER BY elapsed_seconds`, activityID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("samples for activity %d: %w", activityID, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
samples := []Sample{}
|
||||
for rows.Next() {
|
||||
var s Sample
|
||||
if err := rows.Scan(&s.ElapsedSeconds, &s.TimestampMs, &s.HeartRate, &s.SpeedMps, &s.DistanceM, &s.ElevationM); err != nil {
|
||||
return nil, fmt.Errorf("scan sample row: %w", err)
|
||||
}
|
||||
samples = append(samples, s)
|
||||
}
|
||||
return samples, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user