Files
geniusrun/backend/internal/store/profile.go
Christophe Vila 4748917a25 feat: add single-profile settings table and store layer
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>
2026-07-17 18:41:37 +02:00

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
}