2026-07-17 18:53:55 +02:00
|
|
|
package store
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"fmt"
|
|
|
|
|
)
|
|
|
|
|
|
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
|
|
|
// 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
|
2026-07-25 13:44:06 +02:00
|
|
|
// place. Has no user_id column of its own -- ownership is checked via a
|
|
|
|
|
// join to workout_kinds.user_id, since it's always accessed 1:1 through a
|
|
|
|
|
// specific workout kind.
|
2026-07-17 18:53:55 +02:00
|
|
|
type WorkoutTypePace struct {
|
|
|
|
|
WorkoutKindID int64
|
|
|
|
|
PaceMinSecPerKm *float64
|
|
|
|
|
PaceMaxSecPerKm *float64
|
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
|
|
|
HRMinPctHRR *float64
|
|
|
|
|
HRMaxPctHRR *float64
|
2026-07-17 18:53:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func scanWorkoutTypePace(row interface{ Scan(...any) error }) (WorkoutTypePace, error) {
|
|
|
|
|
var p WorkoutTypePace
|
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
|
|
|
err := row.Scan(&p.WorkoutKindID, &p.PaceMinSecPerKm, &p.PaceMaxSecPerKm, &p.HRMinPctHRR, &p.HRMaxPctHRR)
|
2026-07-17 18:53:55 +02:00
|
|
|
return p, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 13:44:06 +02:00
|
|
|
const workoutTypePaceColumns = `wtp.workout_kind_id, wtp.pace_min_sec_per_km, wtp.pace_max_sec_per_km, wtp.hr_min_pct_hrr, wtp.hr_max_pct_hrr`
|
2026-07-17 18:53:55 +02:00
|
|
|
|
2026-07-25 13:44:06 +02:00
|
|
|
// GetWorkoutTypePace fetches the pace/zone row for one workout kind, scoped
|
|
|
|
|
// to userID via a join to workout_kinds.
|
|
|
|
|
func (db *DB) GetWorkoutTypePace(ctx context.Context, userID, workoutKindID int64) (WorkoutTypePace, error) {
|
|
|
|
|
row := db.QueryRowContext(ctx, `
|
|
|
|
|
SELECT `+workoutTypePaceColumns+`
|
|
|
|
|
FROM workout_type_paces wtp
|
|
|
|
|
JOIN workout_kinds wk ON wk.id = wtp.workout_kind_id
|
|
|
|
|
WHERE wtp.workout_kind_id = ? AND wk.user_id = ?`, workoutKindID, userID)
|
2026-07-17 18:53:55 +02:00
|
|
|
p, err := scanWorkoutTypePace(row)
|
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
|
return WorkoutTypePace{WorkoutKindID: workoutKindID}, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
2026-07-25 13:44:06 +02:00
|
|
|
return WorkoutTypePace{}, fmt.Errorf("get workout type pace for kind %d (user %d): %w", workoutKindID, userID, err)
|
2026-07-17 18:53:55 +02:00
|
|
|
}
|
|
|
|
|
return p, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 13:44:06 +02:00
|
|
|
// UpdateWorkoutTypePace overwrites the pace/zone row for one workout kind,
|
|
|
|
|
// scoped so it can only ever affect a kind owned by userID.
|
|
|
|
|
func (db *DB) UpdateWorkoutTypePace(ctx context.Context, userID int64, p WorkoutTypePace) error {
|
2026-07-17 18:53:55 +02:00
|
|
|
_, err := db.ExecContext(ctx, `
|
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
|
|
|
UPDATE workout_type_paces SET pace_min_sec_per_km=?, pace_max_sec_per_km=?, hr_min_pct_hrr=?, hr_max_pct_hrr=?
|
2026-07-25 13:44:06 +02:00
|
|
|
WHERE workout_kind_id=? AND workout_kind_id IN (SELECT id FROM workout_kinds WHERE user_id=?)`,
|
|
|
|
|
p.PaceMinSecPerKm, p.PaceMaxSecPerKm, p.HRMinPctHRR, p.HRMaxPctHRR, p.WorkoutKindID, userID)
|
2026-07-17 18:53:55 +02:00
|
|
|
if err != nil {
|
2026-07-25 13:44:06 +02:00
|
|
|
return fmt.Errorf("update workout type pace for kind %d (user %d): %w", p.WorkoutKindID, userID, err)
|
2026-07-17 18:53:55 +02:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 13:44:06 +02:00
|
|
|
// ListWorkoutTypePaces returns every one of userID's workout kinds' pace/zone rows.
|
|
|
|
|
func (db *DB) ListWorkoutTypePaces(ctx context.Context, userID int64) ([]WorkoutTypePace, error) {
|
|
|
|
|
rows, err := db.QueryContext(ctx, `
|
|
|
|
|
SELECT `+workoutTypePaceColumns+`
|
|
|
|
|
FROM workout_type_paces wtp
|
|
|
|
|
JOIN workout_kinds wk ON wk.id = wtp.workout_kind_id
|
|
|
|
|
WHERE wk.user_id = ?
|
|
|
|
|
ORDER BY wtp.workout_kind_id`, userID)
|
2026-07-17 18:53:55 +02:00
|
|
|
if err != nil {
|
2026-07-25 13:44:06 +02:00
|
|
|
return nil, fmt.Errorf("list workout type paces for user %d: %w", userID, err)
|
2026-07-17 18:53:55 +02:00
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
|
}
|