Add Profile table to store Garmin credentials and engine configuration parameters. Implements GetProfile() and UpdateProfile() store methods with comprehensive test coverage. The profile singleton row is automatically initialized on migration and persists all configuration state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
111 lines
4.7 KiB
Go
111 lines
4.7 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
// Profile is the single active user's Garmin credentials plus every
|
|
// tunable analysis-engine parameter. Always exactly one row (id=1).
|
|
type Profile struct {
|
|
GarminEmail string
|
|
GarminPassword string
|
|
RollingWindowDays int
|
|
MaxHeartRate *float64
|
|
RestingHeartRate *float64
|
|
|
|
HRZone1MinPct, HRZone1MaxPct float64
|
|
HRZone2MinPct, HRZone2MaxPct float64
|
|
HRZone3MinPct, HRZone3MaxPct float64
|
|
HRZone4MinPct, HRZone4MaxPct float64
|
|
HRZone5MinPct, HRZone5MaxPct float64
|
|
|
|
EasyWarmupMinutes, EasyCooldownMinutes float64
|
|
LongWarmupMinutes, LongCooldownMinutes float64
|
|
TempoWarmupMinutes, TempoCooldownMinutes float64
|
|
Threshold30WarmupMinutes, Threshold30CooldownMinutes float64
|
|
Threshold60WarmupMinutes, Threshold60CooldownMinutes float64
|
|
MASTestWarmupMinutes, MASTestCooldownMinutes float64
|
|
|
|
// IntervalWarmupMinutes/IntervalCooldownMinutes are not backed by
|
|
// dedicated columns: Interval phase detection uses the lap_intensity
|
|
// strategy (existing ACTIVE/REST lap tagging), not a fixed-duration
|
|
// guess. Kept here as a convenience zero-value for callers that don't
|
|
// yet distinguish strategies; always 0 until a future migration adds
|
|
// real columns if a fixed fallback is ever needed.
|
|
IntervalWarmupMinutes, IntervalCooldownMinutes float64
|
|
|
|
CreatedAt, UpdatedAt string
|
|
}
|
|
|
|
const profileColumns = `
|
|
garmin_email, garmin_password, rolling_window_days, max_heart_rate, resting_heart_rate,
|
|
hr_zone1_min_pct, hr_zone1_max_pct, hr_zone2_min_pct, hr_zone2_max_pct,
|
|
hr_zone3_min_pct, hr_zone3_max_pct, hr_zone4_min_pct, hr_zone4_max_pct,
|
|
hr_zone5_min_pct, hr_zone5_max_pct,
|
|
easy_warmup_minutes, easy_cooldown_minutes, long_warmup_minutes, long_cooldown_minutes,
|
|
tempo_warmup_minutes, tempo_cooldown_minutes,
|
|
threshold30_warmup_minutes, threshold30_cooldown_minutes,
|
|
threshold60_warmup_minutes, threshold60_cooldown_minutes,
|
|
mas_test_warmup_minutes, mas_test_cooldown_minutes,
|
|
interval_warmup_minutes, interval_cooldown_minutes,
|
|
created_at, updated_at
|
|
`
|
|
|
|
// GetProfile returns the single profile row.
|
|
func (db *DB) GetProfile(ctx context.Context) (Profile, error) {
|
|
var p Profile
|
|
err := db.QueryRowContext(ctx, `SELECT `+profileColumns+` FROM profile WHERE id = 1`).Scan(
|
|
&p.GarminEmail, &p.GarminPassword, &p.RollingWindowDays, &p.MaxHeartRate, &p.RestingHeartRate,
|
|
&p.HRZone1MinPct, &p.HRZone1MaxPct, &p.HRZone2MinPct, &p.HRZone2MaxPct,
|
|
&p.HRZone3MinPct, &p.HRZone3MaxPct, &p.HRZone4MinPct, &p.HRZone4MaxPct,
|
|
&p.HRZone5MinPct, &p.HRZone5MaxPct,
|
|
&p.EasyWarmupMinutes, &p.EasyCooldownMinutes, &p.LongWarmupMinutes, &p.LongCooldownMinutes,
|
|
&p.TempoWarmupMinutes, &p.TempoCooldownMinutes,
|
|
&p.Threshold30WarmupMinutes, &p.Threshold30CooldownMinutes,
|
|
&p.Threshold60WarmupMinutes, &p.Threshold60CooldownMinutes,
|
|
&p.MASTestWarmupMinutes, &p.MASTestCooldownMinutes,
|
|
&p.IntervalWarmupMinutes, &p.IntervalCooldownMinutes,
|
|
&p.CreatedAt, &p.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return Profile{}, fmt.Errorf("get profile: %w", err)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// UpdateProfile overwrites the single profile row. Callers should read via
|
|
// GetProfile first and modify the fields they intend to change, since this
|
|
// replaces every column.
|
|
func (db *DB) UpdateProfile(ctx context.Context, p Profile) error {
|
|
_, err := db.ExecContext(ctx, `
|
|
UPDATE profile SET
|
|
garmin_email=?, garmin_password=?, rolling_window_days=?, max_heart_rate=?, resting_heart_rate=?,
|
|
hr_zone1_min_pct=?, hr_zone1_max_pct=?, hr_zone2_min_pct=?, hr_zone2_max_pct=?,
|
|
hr_zone3_min_pct=?, hr_zone3_max_pct=?, hr_zone4_min_pct=?, hr_zone4_max_pct=?,
|
|
hr_zone5_min_pct=?, hr_zone5_max_pct=?,
|
|
easy_warmup_minutes=?, easy_cooldown_minutes=?, long_warmup_minutes=?, long_cooldown_minutes=?,
|
|
tempo_warmup_minutes=?, tempo_cooldown_minutes=?,
|
|
threshold30_warmup_minutes=?, threshold30_cooldown_minutes=?,
|
|
threshold60_warmup_minutes=?, threshold60_cooldown_minutes=?,
|
|
mas_test_warmup_minutes=?, mas_test_cooldown_minutes=?,
|
|
interval_warmup_minutes=?, interval_cooldown_minutes=?,
|
|
updated_at=datetime('now')
|
|
WHERE id = 1`,
|
|
p.GarminEmail, p.GarminPassword, p.RollingWindowDays, p.MaxHeartRate, p.RestingHeartRate,
|
|
p.HRZone1MinPct, p.HRZone1MaxPct, p.HRZone2MinPct, p.HRZone2MaxPct,
|
|
p.HRZone3MinPct, p.HRZone3MaxPct, p.HRZone4MinPct, p.HRZone4MaxPct,
|
|
p.HRZone5MinPct, p.HRZone5MaxPct,
|
|
p.EasyWarmupMinutes, p.EasyCooldownMinutes, p.LongWarmupMinutes, p.LongCooldownMinutes,
|
|
p.TempoWarmupMinutes, p.TempoCooldownMinutes,
|
|
p.Threshold30WarmupMinutes, p.Threshold30CooldownMinutes,
|
|
p.Threshold60WarmupMinutes, p.Threshold60CooldownMinutes,
|
|
p.MASTestWarmupMinutes, p.MASTestCooldownMinutes,
|
|
p.IntervalWarmupMinutes, p.IntervalCooldownMinutes,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update profile: %w", err)
|
|
}
|
|
return nil
|
|
}
|