diff --git a/backend/internal/store/migrations/0006_global_phase_minutes.sql b/backend/internal/store/migrations/0006_global_phase_minutes.sql new file mode 100644 index 0000000..b866419 --- /dev/null +++ b/backend/internal/store/migrations/0006_global_phase_minutes.sql @@ -0,0 +1,22 @@ +-- Phase-detection warm-up/cool-down was originally one setting per workout +-- type (14 columns: 6 types x 2, plus an unused Interval pair -- Interval +-- detects phases from lap data directly, never from a fixed duration). That +-- turned out to be more granularity than wanted: replaced with a single +-- global warm-up/cool-down pair applied to every fixed-duration workout type. +ALTER TABLE profile ADD COLUMN warmup_minutes REAL NOT NULL DEFAULT 10; +ALTER TABLE profile ADD COLUMN cooldown_minutes REAL NOT NULL DEFAULT 5; + +ALTER TABLE profile DROP COLUMN easy_warmup_minutes; +ALTER TABLE profile DROP COLUMN easy_cooldown_minutes; +ALTER TABLE profile DROP COLUMN long_warmup_minutes; +ALTER TABLE profile DROP COLUMN long_cooldown_minutes; +ALTER TABLE profile DROP COLUMN tempo_warmup_minutes; +ALTER TABLE profile DROP COLUMN tempo_cooldown_minutes; +ALTER TABLE profile DROP COLUMN threshold30_warmup_minutes; +ALTER TABLE profile DROP COLUMN threshold30_cooldown_minutes; +ALTER TABLE profile DROP COLUMN threshold60_warmup_minutes; +ALTER TABLE profile DROP COLUMN threshold60_cooldown_minutes; +ALTER TABLE profile DROP COLUMN mas_test_warmup_minutes; +ALTER TABLE profile DROP COLUMN mas_test_cooldown_minutes; +ALTER TABLE profile DROP COLUMN interval_warmup_minutes; +ALTER TABLE profile DROP COLUMN interval_cooldown_minutes; diff --git a/backend/internal/store/profile.go b/backend/internal/store/profile.go index 83d0330..1a4af9f 100644 --- a/backend/internal/store/profile.go +++ b/backend/internal/store/profile.go @@ -20,17 +20,11 @@ type Profile struct { 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 persisted to dedicated - // columns (DEFAULT 0) but not yet used by phase-detection logic; reserved - // for future Interval fixed-duration strategy if needed. - IntervalWarmupMinutes, IntervalCooldownMinutes 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 } @@ -40,12 +34,7 @@ const profileColumns = ` 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, + warmup_minutes, cooldown_minutes, created_at, updated_at ` @@ -57,12 +46,7 @@ func (db *DB) GetProfile(ctx context.Context) (Profile, error) { &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.WarmupMinutes, &p.CooldownMinutes, &p.CreatedAt, &p.UpdatedAt, ) if err != nil { @@ -81,24 +65,14 @@ func (db *DB) UpdateProfile(ctx context.Context, p Profile) error { 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=?, + warmup_minutes=?, 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, + p.WarmupMinutes, p.CooldownMinutes, ) if err != nil { return fmt.Errorf("update profile: %w", err) diff --git a/backend/internal/store/profile_test.go b/backend/internal/store/profile_test.go index cd2f171..5042017 100644 --- a/backend/internal/store/profile_test.go +++ b/backend/internal/store/profile_test.go @@ -19,6 +19,9 @@ func TestProfile_DefaultsThenUpdate(t *testing.T) { if p.HRZone1MinPct != 50 || p.HRZone5MaxPct != 100 { t.Errorf("zone defaults = %+v, want Z1 min=50, Z5 max=100", p) } + if p.WarmupMinutes != 10 || p.CooldownMinutes != 5 { + t.Errorf("phase-minute defaults = %+v, want warmup=10, cooldown=5", p) + } maxHR, restingHR := 190.0, 50.0 p.GarminEmail = "runner@example.com" @@ -26,7 +29,7 @@ func TestProfile_DefaultsThenUpdate(t *testing.T) { p.RollingWindowDays = 120 p.MaxHeartRate = &maxHR p.RestingHeartRate = &restingHR - p.IntervalWarmupMinutes = 8 + p.WarmupMinutes = 8 if err := db.UpdateProfile(ctx, p); err != nil { t.Fatalf("UpdateProfile: %v", err) @@ -42,7 +45,7 @@ func TestProfile_DefaultsThenUpdate(t *testing.T) { if got.MaxHeartRate == nil || *got.MaxHeartRate != 190 { t.Errorf("MaxHeartRate = %v, want 190", got.MaxHeartRate) } - if got.IntervalWarmupMinutes != 8 { - t.Errorf("IntervalWarmupMinutes = %v, want 8", got.IntervalWarmupMinutes) + if got.WarmupMinutes != 8 { + t.Errorf("WarmupMinutes = %v, want 8", got.WarmupMinutes) } } diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 0826f74..ab4fa3d 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -146,6 +146,25 @@ export function Profile() { ))} +
+ ); diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index f9205d2..4d5d9d8 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -78,20 +78,8 @@ export interface Profile { HRZone4MaxPct: number; HRZone5MinPct: number; HRZone5MaxPct: number; - EasyWarmupMinutes: number; - EasyCooldownMinutes: number; - LongWarmupMinutes: number; - LongCooldownMinutes: number; - TempoWarmupMinutes: number; - TempoCooldownMinutes: number; - Threshold30WarmupMinutes: number; - Threshold30CooldownMinutes: number; - Threshold60WarmupMinutes: number; - Threshold60CooldownMinutes: number; - MASTestWarmupMinutes: number; - MASTestCooldownMinutes: number; - IntervalWarmupMinutes: number; - IntervalCooldownMinutes: number; + WarmupMinutes: number; + CooldownMinutes: number; CreatedAt: string; UpdatedAt: string; }