SMARTRUN_BACKFILL_HORIZON_DAYS was a server-startup-only env var with no UI, defaulting to 3 years -- so editing the unrelated "Rolling window" profile field (for classification, not sync) had no effect on how far back Sync Now reached. Backfill horizon is now Profile.BackfillHorizonDays, read fresh on every Backfill call, with its own field on the Profile page.
88 lines
3.3 KiB
Go
88 lines
3.3 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
|
|
// BackfillHorizonDays bounds how far back "Sync now" reaches when
|
|
// walking backward from today; it's read fresh on every sync (not fixed
|
|
// at server startup), so changing it here takes effect on the next
|
|
// click. Unrelated to RollingWindowDays, which is for the (not yet
|
|
// implemented) relative classification window.
|
|
BackfillHorizonDays int
|
|
MaxHeartRate *float64
|
|
RestingHeartRate *float64
|
|
|
|
HRZone1MinPct, HRZone1MaxPct float64
|
|
HRZone2MinPct, HRZone2MaxPct float64
|
|
HRZone3MinPct, HRZone3MaxPct float64
|
|
HRZone4MinPct, HRZone4MaxPct float64
|
|
HRZone5MinPct, HRZone5MaxPct float64
|
|
|
|
// WarmupMinutes/CooldownMinutes apply uniformly to every fixed-duration
|
|
// workout type's phase detection (Easy, Long, Tempo, Threshold 30'/60',
|
|
// MAS Test). Interval workouts detect phases from lap data directly and
|
|
// don't use these.
|
|
WarmupMinutes, CooldownMinutes float64
|
|
|
|
CreatedAt, UpdatedAt string
|
|
}
|
|
|
|
const profileColumns = `
|
|
garmin_email, garmin_password, rolling_window_days, backfill_horizon_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,
|
|
warmup_minutes, 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.BackfillHorizonDays, &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.WarmupMinutes, &p.CooldownMinutes,
|
|
&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=?, backfill_horizon_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=?,
|
|
warmup_minutes=?, cooldown_minutes=?,
|
|
updated_at=datetime('now')
|
|
WHERE id = 1`,
|
|
p.GarminEmail, p.GarminPassword, p.RollingWindowDays, p.BackfillHorizonDays, 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.WarmupMinutes, p.CooldownMinutes,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update profile: %w", err)
|
|
}
|
|
return nil
|
|
}
|