Files
rideaware-api/internal/templates/templates.go

305 lines
13 KiB
Go

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
}