feat: extend equipment and workout models with service tracking
This commit is contained in:
304
internal/templates/templates.go
Normal file
304
internal/templates/templates.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package templates
|
||||
|
||||
import "rideaware/internal/workout"
|
||||
|
||||
// Template represents a predefined workout template.
|
||||
type Template struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Duration int `json:"duration"`
|
||||
Difficulty string `json:"difficulty"` // "easy", "moderate", "hard", "very_hard"
|
||||
Category string `json:"category"` // "endurance", "threshold", "vo2max", "recovery", "sprint", "sweet_spot"
|
||||
Segments []workout.WorkoutSegment `json:"segments"`
|
||||
}
|
||||
|
||||
// All returns the full list of workout templates.
|
||||
var All = []Template{
|
||||
{
|
||||
ID: "recovery-spin",
|
||||
Name: "Recovery Spin",
|
||||
Description: "Easy spin to promote blood flow and recovery. Keep it light and conversational.",
|
||||
Type: "Recovery",
|
||||
Duration: 1800, // 30 min
|
||||
Difficulty: "easy",
|
||||
Category: "recovery",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 300, PowerLow: 0.30, PowerHigh: 0.45},
|
||||
{Type: "steadystate", Duration: 1200, Power: 0.45, Cadence: 90},
|
||||
{Type: "cooldown", Duration: 300, PowerLow: 0.45, PowerHigh: 0.30},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "endurance-60",
|
||||
Name: "Endurance Base",
|
||||
Description: "Steady zone 2 ride to build aerobic base. Maintain a comfortable, sustainable effort.",
|
||||
Type: "Endurance",
|
||||
Duration: 3600, // 60 min
|
||||
Difficulty: "easy",
|
||||
Category: "endurance",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.65},
|
||||
{Type: "steadystate", Duration: 2400, Power: 0.65, Cadence: 85},
|
||||
{Type: "cooldown", Duration: 600, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "endurance-90",
|
||||
Name: "Long Endurance",
|
||||
Description: "Extended zone 2 ride for deep aerobic adaptation. Pack snacks.",
|
||||
Type: "Endurance",
|
||||
Duration: 5400, // 90 min
|
||||
Difficulty: "moderate",
|
||||
Category: "endurance",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.65},
|
||||
{Type: "steadystate", Duration: 4200, Power: 0.68, Cadence: 85},
|
||||
{Type: "cooldown", Duration: 600, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "tempo-45",
|
||||
Name: "Tempo Ride",
|
||||
Description: "Sustained zone 3 effort. Comfortably hard - you can talk in short sentences.",
|
||||
Type: "Tempo",
|
||||
Duration: 2700, // 45 min
|
||||
Difficulty: "moderate",
|
||||
Category: "endurance",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.70},
|
||||
{Type: "steadystate", Duration: 1200, Power: 0.78, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.55},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.78, Cadence: 90},
|
||||
{Type: "cooldown", Duration: 300, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "sweet-spot-60",
|
||||
Name: "Sweet Spot",
|
||||
Description: "The most time-efficient training zone. 88-94% FTP intervals with short recovery.",
|
||||
Type: "Threshold",
|
||||
Duration: 3600, // 60 min
|
||||
Difficulty: "moderate",
|
||||
Category: "sweet_spot",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
{Type: "steadystate", Duration: 600, Power: 0.90, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.55},
|
||||
{Type: "steadystate", Duration: 600, Power: 0.92, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.55},
|
||||
{Type: "steadystate", Duration: 600, Power: 0.90, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.55},
|
||||
{Type: "cooldown", Duration: 660, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "threshold-intervals",
|
||||
Name: "Threshold Intervals",
|
||||
Description: "Classic 2x20 at FTP. The gold standard for raising your threshold.",
|
||||
Type: "Threshold",
|
||||
Duration: 3600, // 60 min
|
||||
Difficulty: "hard",
|
||||
Category: "threshold",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.80, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 1200, Power: 1.00, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.50},
|
||||
{Type: "steadystate", Duration: 1200, Power: 1.00, Cadence: 95},
|
||||
{Type: "cooldown", Duration: 600, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "over-unders",
|
||||
Name: "Over-Under Intervals",
|
||||
Description: "Alternating between just below and just above FTP. Builds lactate tolerance.",
|
||||
Type: "Threshold",
|
||||
Duration: 3600, // 60 min
|
||||
Difficulty: "hard",
|
||||
Category: "threshold",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
// Set 1
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.50},
|
||||
// Set 2
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.50},
|
||||
// Set 3
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.95, Cadence: 90},
|
||||
{Type: "steadystate", Duration: 60, Power: 1.05, Cadence: 95},
|
||||
{Type: "cooldown", Duration: 600, PowerLow: 0.65, PowerHigh: 0.40},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "vo2max-intervals",
|
||||
Name: "VO2max Intervals",
|
||||
Description: "5x3min at 115% FTP with equal rest. Expands your aerobic ceiling.",
|
||||
Type: "VO2 Max",
|
||||
Duration: 2700, // 45 min
|
||||
Difficulty: "very_hard",
|
||||
Category: "vo2max",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.45},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.45},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.45},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "steadystate", Duration: 180, Power: 0.45},
|
||||
{Type: "steadystate", Duration: 180, Power: 1.15, Cadence: 100},
|
||||
{Type: "cooldown", Duration: 420, PowerLow: 0.55, PowerHigh: 0.35},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "vo2max-short",
|
||||
Name: "VO2max Short-Shorts",
|
||||
Description: "30/30s at 120% FTP. Accumulate VO2max time in manageable chunks.",
|
||||
Type: "VO2 Max",
|
||||
Duration: 2400, // 40 min
|
||||
Difficulty: "very_hard",
|
||||
Category: "vo2max",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
// Set 1: 10x 30/30
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.40},
|
||||
// Set 2: 10x 30/30
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "steadystate", Duration: 30, Power: 0.40},
|
||||
{Type: "interval", Duration: 30, PowerLow: 1.20, PowerHigh: 1.20},
|
||||
{Type: "cooldown", Duration: 300, PowerLow: 0.50, PowerHigh: 0.35},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "sprint-intervals",
|
||||
Name: "Sprint Power",
|
||||
Description: "Short maximal efforts to build neuromuscular power and sprint capacity.",
|
||||
Type: "VO2 Max",
|
||||
Duration: 2700, // 45 min
|
||||
Difficulty: "very_hard",
|
||||
Category: "sprint",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 600, PowerLow: 0.40, PowerHigh: 0.75},
|
||||
{Type: "steadystate", Duration: 300, Power: 0.85, Cadence: 95},
|
||||
{Type: "steadystate", Duration: 120, Power: 0.50},
|
||||
// Sprints: 8x 15s all-out / 105s recovery
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "steadystate", Duration: 105, Power: 0.40},
|
||||
{Type: "interval", Duration: 15, PowerLow: 1.50, PowerHigh: 2.00},
|
||||
{Type: "cooldown", Duration: 480, PowerLow: 0.55, PowerHigh: 0.35},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "ramp-test",
|
||||
Name: "FTP Ramp Test",
|
||||
Description: "Progressive ramp to estimate your FTP. Ride until you can't hold the target.",
|
||||
Type: "Threshold",
|
||||
Duration: 1500, // ~25 min (most people fail around 19-22 min)
|
||||
Difficulty: "very_hard",
|
||||
Category: "threshold",
|
||||
Segments: []workout.WorkoutSegment{
|
||||
{Type: "warmup", Duration: 300, PowerLow: 0.40, PowerHigh: 0.46},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.46, PowerHigh: 0.52},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.52, PowerHigh: 0.58},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.58, PowerHigh: 0.64},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.64, PowerHigh: 0.70},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.70, PowerHigh: 0.76},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.76, PowerHigh: 0.82},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.82, PowerHigh: 0.88},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.88, PowerHigh: 0.94},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 0.94, PowerHigh: 1.00},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.00, PowerHigh: 1.06},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.06, PowerHigh: 1.12},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.12, PowerHigh: 1.18},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.18, PowerHigh: 1.24},
|
||||
{Type: "ramp", Duration: 60, PowerLow: 1.24, PowerHigh: 1.30},
|
||||
{Type: "cooldown", Duration: 300, PowerLow: 0.50, PowerHigh: 0.30},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// GetByID returns a template by its ID, or nil if not found.
|
||||
func GetByID(id string) *Template {
|
||||
for i := range All {
|
||||
if All[i].ID == id {
|
||||
return &All[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByCategory returns all templates matching a category.
|
||||
func GetByCategory(category string) []Template {
|
||||
var result []Template
|
||||
for _, t := range All {
|
||||
if t.Category == category {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user