Unraid deployment fixes and generator improvements

- Add Next.js rewrites to proxy API calls through same origin (fixes login/media on werkout.treytartt.com)
- Fix mediaUrl() in DayCard and ExerciseRow to use relative paths in production
- Add proxyTimeout for long-running workout generation endpoints
- Add CSRF trusted origin for treytartt.com
- Split docker-compose into production (Unraid) and dev configs
- Show display_name and descriptions on workout type cards
- Generator: rules engine improvements, movement enforcement, exercise selector updates
- Add new test files for rules drift, workout research generation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-23 10:25:45 -06:00
parent 1c61b80731
commit 03681c532d
21 changed files with 2366 additions and 138 deletions

View File

@@ -0,0 +1,123 @@
from django.core.management import call_command
from django.db import migrations
WORKOUT_TYPE_CALIBRATION = {
'functional_strength_training': {
'typical_rest_between_sets': 60,
'typical_intensity': 'medium',
'rep_range_min': 8,
'rep_range_max': 15,
'round_range_min': 3,
'round_range_max': 4,
'duration_bias': 0.15,
'superset_size_min': 2,
'superset_size_max': 4,
},
'traditional_strength_training': {
'typical_rest_between_sets': 120,
'typical_intensity': 'high',
'rep_range_min': 4,
'rep_range_max': 8,
'round_range_min': 3,
'round_range_max': 5,
'duration_bias': 0.1,
'superset_size_min': 1,
'superset_size_max': 3,
},
'high_intensity_interval_training': {
'typical_rest_between_sets': 30,
'typical_intensity': 'high',
'rep_range_min': 10,
'rep_range_max': 20,
'round_range_min': 3,
'round_range_max': 5,
'duration_bias': 0.7,
'superset_size_min': 3,
'superset_size_max': 6,
},
'cross_training': {
'typical_rest_between_sets': 45,
'typical_intensity': 'high',
'rep_range_min': 8,
'rep_range_max': 15,
'round_range_min': 3,
'round_range_max': 5,
'duration_bias': 0.4,
'superset_size_min': 3,
'superset_size_max': 5,
},
'core_training': {
'typical_rest_between_sets': 30,
'typical_intensity': 'medium',
'rep_range_min': 10,
'rep_range_max': 20,
'round_range_min': 2,
'round_range_max': 4,
'duration_bias': 0.5,
'superset_size_min': 3,
'superset_size_max': 5,
},
'flexibility': {
'typical_rest_between_sets': 15,
'typical_intensity': 'low',
'rep_range_min': 1,
'rep_range_max': 5,
'round_range_min': 1,
'round_range_max': 2,
'duration_bias': 0.9,
'superset_size_min': 3,
'superset_size_max': 6,
},
'cardio': {
'typical_rest_between_sets': 30,
'typical_intensity': 'medium',
'rep_range_min': 1,
'rep_range_max': 1,
'round_range_min': 1,
'round_range_max': 3,
'duration_bias': 1.0,
'superset_size_min': 1,
'superset_size_max': 3,
},
'hypertrophy': {
'typical_rest_between_sets': 90,
'typical_intensity': 'high',
'rep_range_min': 8,
'rep_range_max': 15,
'round_range_min': 3,
'round_range_max': 4,
'duration_bias': 0.2,
'superset_size_min': 2,
'superset_size_max': 4,
},
}
def apply_calibration(apps, schema_editor):
WorkoutType = apps.get_model('generator', 'WorkoutType')
for type_name, fields in WORKOUT_TYPE_CALIBRATION.items():
defaults = dict(fields)
defaults.setdefault('display_name', type_name.replace('_', ' ').title())
defaults.setdefault('description', '')
WorkoutType.objects.update_or_create(name=type_name, defaults=defaults)
# Ensure the full 8 x 5 x 3 = 120 structure-rule matrix is present and calibrated.
call_command('calibrate_structure_rules')
def noop_reverse(apps, schema_editor):
# Intentionally no-op: this migration normalizes live calibration data.
pass
class Migration(migrations.Migration):
dependencies = [
('generator', '0005_add_periodization_fields'),
]
operations = [
migrations.RunPython(apply_calibration, noop_reverse),
]

View File

@@ -0,0 +1,121 @@
from django.core.management import call_command
from django.db import migrations
WORKOUT_TYPE_CALIBRATION = {
'functional_strength_training': {
'typical_rest_between_sets': 60,
'typical_intensity': 'medium',
'rep_range_min': 8,
'rep_range_max': 15,
'round_range_min': 3,
'round_range_max': 4,
'duration_bias': 0.15,
'superset_size_min': 2,
'superset_size_max': 4,
},
'traditional_strength_training': {
'typical_rest_between_sets': 120,
'typical_intensity': 'high',
'rep_range_min': 4,
'rep_range_max': 8,
'round_range_min': 3,
'round_range_max': 5,
'duration_bias': 0.1,
'superset_size_min': 1,
'superset_size_max': 3,
},
'high_intensity_interval_training': {
'typical_rest_between_sets': 30,
'typical_intensity': 'high',
'rep_range_min': 10,
'rep_range_max': 20,
'round_range_min': 3,
'round_range_max': 5,
'duration_bias': 0.7,
'superset_size_min': 3,
'superset_size_max': 6,
},
'cross_training': {
'typical_rest_between_sets': 45,
'typical_intensity': 'high',
'rep_range_min': 8,
'rep_range_max': 15,
'round_range_min': 3,
'round_range_max': 5,
'duration_bias': 0.4,
'superset_size_min': 3,
'superset_size_max': 5,
},
'core_training': {
'typical_rest_between_sets': 30,
'typical_intensity': 'medium',
'rep_range_min': 10,
'rep_range_max': 20,
'round_range_min': 2,
'round_range_max': 4,
'duration_bias': 0.5,
'superset_size_min': 3,
'superset_size_max': 5,
},
'flexibility': {
'typical_rest_between_sets': 15,
'typical_intensity': 'low',
'rep_range_min': 1,
'rep_range_max': 5,
'round_range_min': 1,
'round_range_max': 2,
'duration_bias': 0.9,
'superset_size_min': 3,
'superset_size_max': 6,
},
'cardio': {
'typical_rest_between_sets': 30,
'typical_intensity': 'medium',
'rep_range_min': 1,
'rep_range_max': 1,
'round_range_min': 1,
'round_range_max': 3,
'duration_bias': 1.0,
'superset_size_min': 1,
'superset_size_max': 3,
},
'hypertrophy': {
'typical_rest_between_sets': 90,
'typical_intensity': 'high',
'rep_range_min': 8,
'rep_range_max': 15,
'round_range_min': 3,
'round_range_max': 4,
'duration_bias': 0.2,
'superset_size_min': 2,
'superset_size_max': 4,
},
}
def apply_calibration(apps, schema_editor):
WorkoutType = apps.get_model('generator', 'WorkoutType')
for type_name, fields in WORKOUT_TYPE_CALIBRATION.items():
defaults = dict(fields)
defaults.setdefault('display_name', type_name.replace('_', ' ').title())
defaults.setdefault('description', '')
WorkoutType.objects.update_or_create(name=type_name, defaults=defaults)
call_command('calibrate_structure_rules')
def noop_reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('generator', '0006_calibrate_workout_types_and_structure_rules'),
]
operations = [
migrations.RunPython(apply_calibration, noop_reverse),
]