- 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>
72 lines
2.5 KiB
Go
72 lines
2.5 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
// WorkoutTypePace is a workout kind's user-declared target pace range and HR
|
|
// range (percent of heart rate reserve). Informational only -- never read by
|
|
// the classification rule engine. No history: fields are overwritten in
|
|
// place.
|
|
type WorkoutTypePace struct {
|
|
WorkoutKindID int64
|
|
PaceMinSecPerKm *float64
|
|
PaceMaxSecPerKm *float64
|
|
HRMinPctHRR *float64
|
|
HRMaxPctHRR *float64
|
|
}
|
|
|
|
func scanWorkoutTypePace(row interface{ Scan(...any) error }) (WorkoutTypePace, error) {
|
|
var p WorkoutTypePace
|
|
err := row.Scan(&p.WorkoutKindID, &p.PaceMinSecPerKm, &p.PaceMaxSecPerKm, &p.HRMinPctHRR, &p.HRMaxPctHRR)
|
|
return p, err
|
|
}
|
|
|
|
const workoutTypePaceColumns = `workout_kind_id, pace_min_sec_per_km, pace_max_sec_per_km, hr_min_pct_hrr, hr_max_pct_hrr`
|
|
|
|
// GetWorkoutTypePace fetches the pace/zone row for one workout kind.
|
|
func (db *DB) GetWorkoutTypePace(ctx context.Context, workoutKindID int64) (WorkoutTypePace, error) {
|
|
row := db.QueryRowContext(ctx, `SELECT `+workoutTypePaceColumns+` FROM workout_type_paces WHERE workout_kind_id = ?`, workoutKindID)
|
|
p, err := scanWorkoutTypePace(row)
|
|
if err == sql.ErrNoRows {
|
|
return WorkoutTypePace{WorkoutKindID: workoutKindID}, nil
|
|
}
|
|
if err != nil {
|
|
return WorkoutTypePace{}, fmt.Errorf("get workout type pace for kind %d: %w", workoutKindID, err)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// UpdateWorkoutTypePace overwrites the pace/zone row for one workout kind.
|
|
func (db *DB) UpdateWorkoutTypePace(ctx context.Context, p WorkoutTypePace) error {
|
|
_, err := db.ExecContext(ctx, `
|
|
UPDATE workout_type_paces SET pace_min_sec_per_km=?, pace_max_sec_per_km=?, hr_min_pct_hrr=?, hr_max_pct_hrr=?
|
|
WHERE workout_kind_id=?`,
|
|
p.PaceMinSecPerKm, p.PaceMaxSecPerKm, p.HRMinPctHRR, p.HRMaxPctHRR, p.WorkoutKindID)
|
|
if err != nil {
|
|
return fmt.Errorf("update workout type pace for kind %d: %w", p.WorkoutKindID, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListWorkoutTypePaces returns every workout kind's pace/zone row.
|
|
func (db *DB) ListWorkoutTypePaces(ctx context.Context) ([]WorkoutTypePace, error) {
|
|
rows, err := db.QueryContext(ctx, `SELECT `+workoutTypePaceColumns+` FROM workout_type_paces ORDER BY workout_kind_id`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list workout type paces: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
paces := []WorkoutTypePace{}
|
|
for rows.Next() {
|
|
p, err := scanWorkoutTypePace(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan workout type pace row: %w", err)
|
|
}
|
|
paces = append(paces, p)
|
|
}
|
|
return paces, rows.Err()
|
|
}
|