Deep audit identified 106 findings; 102 fixed, 4 deferred. Covers 8 areas: - Settings & deploy: env-gated DEBUG/SECRET_KEY, HTTPS headers, gunicorn, celery worker - Auth (registered_user): password write_only, request.data fixes, transaction safety, proper HTTP status codes - Workout app: IDOR protection, get_object_or_404, prefetch_related N+1 fixes, transaction.atomic - Video/scripts: path traversal sanitization, HLS trigger guard, auth on cache wipe - Models (exercise/equipment/muscle/superset): null-safe __str__, stable IDs, prefetch support - Generator views: helper for registered_user lookup, logger.exception, bulk_update, transaction wrapping - Generator core (rules/selector/generator): push-pull ratio, type affinity normalization, modality checks, side-pair exact match, word-boundary regex, equipment cache clearing - Generator services (plan_builder/analyzer/normalizer): transaction.atomic, muscle cache, bulk_update, glutes classification fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
26 KiB
Hardening Audit Report — Werkout API (Django/Python)
Audit Sources
- 5 mapper agents (100% file coverage)
- 8 specialized domain auditors (parallel)
- 1 cross-cutting deep audit (parallel)
- Total source files: 75
CRITICAL — Will crash or lose data (18 findings)
1. werkout_api/settings.py:16 | DEBUG=True hardcoded, never disabled in production
- What:
DEBUG = Trueset at module level. Production branch (whenDATABASE_URLset) never overrides toFalse— the code is commented out (lines 142-157).CORS_ALLOW_ALL_ORIGINSon line 226 depends on DEBUG, so it's alwaysTrue. - Impact: Full stack traces, SQL queries, internal paths exposed to end users. CORS allows any origin with credentials.
- Source: Security, Config, Cross-cutting
2. werkout_api/settings.py:160 | SECRET_KEY falls back to 'secret'
- What:
SECRET_KEY = os.environ.get("SECRET_KEY", 'secret'). Neitherdocker-compose.ymlnor any env file sets SECRET_KEY. - Impact: Session cookies, CSRF tokens, password hashes use a publicly known key. Complete auth bypass.
- Source: Security, Config
3. werkout_api/settings.py:226 | CORS allows all origins with credentials in production
- What:
CORS_ALLOW_ALL_ORIGINS = True if DEBUG else Falseis alwaysTrue(DEBUG never False). Combined withCORS_ALLOW_CREDENTIALS = True(line 231). - Impact: Any website can make authenticated cross-origin requests and steal data.
- Source: Security, Config
4. registered_user/serializers.py:31 | Password hash exposed in API responses
- What:
write_only_fields = ('password',)is NOT a valid DRF Meta option. Correct:extra_kwargs = {'password': {'write_only': True}}. Password field is readable. - Impact: Hashed password returned in registration responses. Enables offline brute-force.
- Source: Security, API, Cross-cutting
5. registered_user/views.py:83-90 | update_registered_user uses request.POST — JSON requests wipe user data
- What:
request.POST.get(...)only works for form-encoded data. JSON requests returnNonefor all fields. Lines 88-90 setfirst_name=None,email=None, etc. and save. - Impact: Any JSON profile update silently corrupts user data. Email set to None breaks login.
- Source: Security, Logic, Cross-cutting
6. registered_user/views.py:108-114 | Password update broken for JSON clients, can lock out user
- What:
request.POST.get("new_password")returnsNonefor JSON.set_password(None)makes password uncheckable, permanently locking user out. - Impact: Password endpoint non-functional for JSON clients. Potential permanent account lockout.
- Source: Security, Cross-cutting
7. registered_user/serializers.py:46 | Registration creates RegisteredUser with non-existent phone_number field
- What:
RegisteredUser.objects.create(phone_number=self.context.get("phone_number"), ...)— model has nophone_numberfield (removed in migration 0002). - Impact: User registration crashes with TypeError if phone_number is passed in context.
- Source: Cross-cutting, Logic
8. scripts/views.py:43-45 | Anonymous cache wipe endpoint — no authentication
- What:
clear_redisview has no auth decorators. Active inscripts/urls.py. Any anonymous request wipes entire Redis cache. - Impact: Denial of service — any internet user can flush all cached data at will.
- Source: Security
9. video/views.py:50-59 | Path traversal vulnerability in hls_videos
- What:
video_nameandvideo_typefrom GET params concatenated directly into file paths without sanitization.../../etc/passwdsequences can access arbitrary files. - Impact: Arbitrary file read on the server. Route commented out in urls.py but view exists.
- Source: Security
10. video/views.py:74 | Celery task called with zero arguments but requires filename
- What:
create_hls_tasks.delay()called with no args. Task signaturecreate_hls_tasks(filename)requires one. - Impact: Every call to
/videos/create_hls/crashes the Celery worker with TypeError. - Source: Celery, Cross-cutting
11. supervisord.conf:13 | Production runs Django dev server (runserver) instead of WSGI
- What:
python manage.py runserver 0.0.0.0:8000in production.uwsgi.iniexists but is unused. - Impact: Single-threaded, no request timeouts, not designed for production. Memory leaks.
- Source: Config
12. supervisord.conf | No Celery worker process configured
- What: Only
djangoandnextjsprograms defined. No[program:celery]entry. - Impact: All
.delay()calls queue tasks in Redis that are never consumed. Entire async task system non-functional. - Source: Celery, Config
13. supervisord.conf:13 | Auto-migrate on every container start
- What:
python manage.py migratein startup command runs migrations automatically without review. - Impact: Destructive migrations run silently. Race conditions if multiple containers start simultaneously.
- Source: Config
14. docker-compose.yml:8-10,26 | Database credentials hardcoded as postgres/postgres
- What:
POSTGRES_USER=postgres,POSTGRES_PASSWORD=postgresin compose file and DATABASE_URL. No.envoverride. - Impact: Trivial unauthorized access if database port exposed. Credentials in git history permanently.
- Source: Security, Config
15. AI/workouts.py + AI/cho/workouts.py | 86K lines of PII data committed to git
- What: Two files totaling 86,000+ lines of user workout data from Future Fitness API with user IDs, S3 URLs, timestamps.
- Impact: PII permanently in git history. Potential GDPR/privacy liability.
- Source: Security, Config
16. generator/views.py:1032-1160 | save_plan has no transaction wrapping
- What: Creates GeneratedWeeklyPlan, then loops creating Workout, Superset, SupersetExercise, GeneratedWorkout, PlannedWorkout objects. No
transaction.atomic(). - Impact: Mid-loop failure (e.g., date parsing) leaves orphaned plan records. Partially saved plans with missing days.
- Source: Data Integrity, Cross-cutting
17. generator/views.py:789-803 | confirm_plan has no transaction wrapping
- What: Loops through generated workouts, saves status, deletes/creates PlannedWorkouts individually.
- Impact: Partial plan confirmation — some days accepted, others not, on any mid-loop error.
- Source: Data Integrity
18. registered_user/serializers.py:34-51 | User + RegisteredUser creation has no transaction
- What:
User.objects.create(),set_password(),RegisteredUser.objects.create(),Token.objects.create()— four DB ops with notransaction.atomic(). - Impact: Orphaned User records if RegisteredUser creation fails. Ghost users block re-registration.
- Source: Data Integrity, Cross-cutting
BUG — Incorrect behavior (28 findings)
1. registered_user/views.py:30,47 | Validation errors return HTTP 500 instead of 400
- Impact: Clients can't distinguish server errors from bad input.
2. registered_user/views.py:74 | Failed login returns 404 instead of 401
- Impact: Wrong HTTP semantics for auth failures.
3. registered_user/models.py:20 | __str__ concatenates nullable last_name — TypeError
- Impact: Admin, logging crash for users with null last_name.
4. registered_user/admin.py:11 | Token.objects.get crashes if no token exists
- Impact: Admin list page crashes if any user lacks a Token.
5. equipment/models.py:13 | __str__ concatenates nullable category/name — TypeError
- Impact: Admin crashes for Equipment with null fields.
6. muscle/models.py:11 | __str__ returns None when name is null
- Impact: Violates
__str__contract. Admin/template crashes.
7. workout/views.py:45 | Workout.objects.get with no DoesNotExist handling
- Impact: Missing workouts return 500 instead of 404.
8. workout/views.py:60,143,165 | Validation errors return HTTP 500 instead of 400
- Impact: Three views misreport client errors as server errors.
9. workout/views.py:69 | GET endpoint returns 201 Created instead of 200
- Impact: Incorrect HTTP semantics for read operation.
10. workout/views.py:76 | Unreachable None check — .get() raises exception, never returns None
- Impact: Dead code; actual DoesNotExist is unhandled (500 error).
11. workout/views.py:124 | estimated_rep_duration None multiplication crashes
- What:
exercise["reps"] * exercise_obj.estimated_rep_durationwhere field can be null.int * None= TypeError. - Impact: Workout creation crashes mid-loop, orphaning partial records (no transaction).
12. workout/serializers.py:37 | KeyError if 'notes' not in validated_data
- Impact: Completing a workout without notes crashes with 500.
13. workout/serializers.py:40 | Wrong attribute name — health_kit UUID never persisted
- What: Sets
completed_workout.workout_uuidbut model field ishealth_kit_workout_uuid. - Impact: HealthKit UUIDs silently discarded forever.
14. workout/tasks.py:85 | estimated_rep_duration None multiplication in Celery task
- Impact: Bulk import crashes mid-way, leaving partial data.
15. workout/tasks.py:73 | Exercise.objects.get with no DoesNotExist handling
- Impact: One missing exercise aborts entire import.
16. workout/urls.py:14 | Duplicate URL name 'plan workout' on two paths
- Impact:
reverse('plan workout')resolves to wrong URL.
17. scripts/views.py:37 | NameError: MuscleGroup is not defined
- What: Catches
MuscleGroup.DoesNotExistbut onlyMuscleis imported. - Impact: NameError crashes endpoint instead of catching intended exception.
18. scripts/views.py:15 | equipment_required.split() crashes on None
- Impact: sync_equipment crashes for any exercise with null equipment_required.
19. video/models.py:24 | save() missing *args in signature
- Impact: Callers passing positional args (force_insert) get TypeError.
20. video/models.py:24-27 | HLS transcoding triggered on EVERY save, not just file changes
- Impact: Redundant expensive ffmpeg jobs on metadata-only edits.
21. video/serializers.py:13 | video_file can be None — AttributeError
- Impact: Video listing crashes if any Video has no file.
22. video/tasks.py:10 | Existence check uses wrong filename pattern — never matches
- Impact: Guard clause never short-circuits; re-encodes every time.
23. generator/views.py:70 | RegisteredUser.objects.get repeated ~17 times with no DoesNotExist handling
- Impact: Any user without RegisteredUser gets unhandled 500 on every generator endpoint.
24. superset/helpers.py:16 | Exercise.objects.get("First Up") with no error handling
- Impact: Workout detail crashes if "First Up" exercise is missing.
25. superset/serializers.py:20 | get_unique_id returns random UUID per serialization
- Impact: Frontend can't use unique_id as stable key. Breaks diffing/caching.
26. workout/models.py:51 | settings not imported — NameError on duration_audio()/weight_audio()
- What: Relies on
from exercise.models import *transitive import of settings. - Impact: NameError if transitive chain breaks.
27. workout_generator.py:909 | None multiplication when duration is None
- Impact: Plan generation crashes if preferences have no duration set.
28. workout_generator.py:802 | sum(c.difficulty) crashes if any difficulty is None
- Impact: Plan generation crashes for users with incomplete completion records.
SILENT FAILURE — Error swallowed or ignored (5 findings)
1. generator/views.py:193,491,874,989,1156 | Broad except Exception catches all errors, leaks str(e)
- Impact: Bugs masked. Internal details leaked to clients.
2. superset/helpers.py:19-23 | In-memory mutations on Exercise ORM object never saved
- Impact: Changes silently lost. Risk of corrupting shared Exercise if accidentally saved.
3. workout/helpers.py:41 | ser_data.mutable = True is a no-op
- Impact: No effect. Indicates confusion about data type.
4. audit_exercise_data.py:168-170 | except Exception: pass silently swallows all errors
- Impact: Database errors during field checks silently ignored.
5. workout/views.py:32 | Infinite cache with incomplete invalidation
- Impact: Generated workouts never appear in all_workouts until manual cache clear.
RACE CONDITION — Concurrency issue (1 finding)
1. registered_user/views.py:34 | Email uniqueness check is a race condition
- What:
User.objects.filter(email=email)check followed byserializer.save(). No DB unique constraint visible. - Impact: Concurrent registrations can create duplicate email accounts.
LOGIC ERROR — Code doesn't match intent (12 findings)
1. rules_engine.py:650 | Push/pull ratio check skipped when either count is zero
- What: Condition requires both counts > 0. A workout with 2 push, 0 pull passes silently.
- Impact: Unbalanced push-heavy workouts pass validation.
2. rules_engine.py:858-860 | Workout type match is a no-op for non-strength types
- What: Non-strength branch unconditionally counts every exercise as matching (100% always).
- Impact: HIIT/cardio/core workouts can contain arbitrary exercises without violations.
3. workout_generator.py:1459 | Workout type affinity matching NEVER works
- What:
SPLIT_TYPE_WORKOUT_AFFINITYuses underscore names like'traditional_strength_training'but comparison useswt.name.strip().lower()which yields space-separated names. - Impact: All workout type assignments fall through to round-robin fallback. Push splits get assigned random types.
4. workout_generator.py:2070 | Modality check counts exercise capability, not actual assignment
- What: Checks
ex.is_duration(capability flag) not whether the entry was actually given duration. - Impact: False modality calculations for dual-modality exercises.
5. workout_generator.py:1404 | Diversify type count wrong on replacement
- What: Doesn't subtract from the removed type count when replacing, only adds to candidate count.
- Impact: Valid replacements rejected. Invalid ones accepted in edge cases.
6. workout_generator.py:2898 | Final conformance treats all warnings as blocking
- What:
_is_blocking_final_violationreturns True forseverity in {'error', 'warning'}. - Impact: Workouts crash with ValueError for minor advisory issues (cooldown missing, duration bias slightly off).
7. workout_generator.py:1209 | Recursive retry destroys cross-day dedup state
- What: Failed attempt's exercises already recorded in week state via
accumulate_week_state. Retry with different exercises creates ghost entries. - Impact: Later days in the week have artificially smaller exercise pools.
8. entry_rules.py:19 | Volume floor can violate workout type rep ranges
- What: With
min_volume=12androunds=1, forces 12 reps. Strength (3-6 rep range) gets 12 reps. - Impact: Strength workouts get inflated reps contradicting their character.
9. rules_engine.py:441 | Push/pull counting double-counts dual-pattern exercises
- What: Exercise with
'upper push, upper pull'counted in BOTH push AND pull totals. - Impact: Inaccurate push:pull ratio calculations.
10. exercise_selector.py:631 | No-equipment path restricts to bodyweight only (contradicts docs)
- What: MEMORY.md says "no equipment set = all exercises available." Code excludes all exercises with equipment entries.
- Impact: Users without equipment config get dramatically reduced pool.
11. muscle_normalizer.py:163 | Glutes in both lower_push and lower_pull categories
- Impact: Glute-dominant workouts get incorrect split classification, cascading into wrong type assignments.
12. exercise_selector.py:1274 | Substring partner matching causes false positives
- What:
if base_name.lower() in partner.name.lower()— "Curl" matches "Barbell Curl Right", "Hammer Curl Right", etc. - Impact: Wrong exercises paired as L/R counterparts.
PERFORMANCE — Unnecessary cost (18 findings)
1. exercise/serializers.py:30,35 | N+1 per exercise for muscles + equipment (~3400+ queries on cache miss)
- Impact:
/exercise/all/cold cache: 1133 exercises × 3 queries each.
2. workout/serializers.py:56-77 | Triple N+1 on WorkoutSerializer (~5000+ queries)
- Impact:
all_workoutscache miss: 633 workouts × (muscles + equipment + exercise_count).
3. superset/serializers.py:32 | N+1 per superset for exercises, cascading through ExerciseSerializer
- Impact: Each workout detail triggers O(supersets × exercises × 3) queries.
4. workout/helpers.py:14-71 | Cascade of N+1 queries in exercise list builder
- Impact: ~80+ queries per workout detail (supersets + exercises + serializer chain).
5. generator/serializers.py:338 | N+1 for supersets in GeneratedWorkoutDetailSerializer
- Impact: Plan detail views trigger dozens of cascading queries per day.
6. generator/views.py:1106 | Exercise.objects.get in triple-nested loop in save_plan
- Impact: 5-day plan with 5 supersets × 3 exercises = 75 individual SELECT queries.
7. muscle_normalizer.py:218 | ExerciseMuscle query per exercise in analyzer (~19,000 queries)
- Impact:
analyze_workoutscommand fires ~19,000 queries for 633 workouts.
8. workout_analyzer.py:1332-1337 | 120 exists() checks in _step7
- Impact: 8 types × 3 sections × 5 goals = 120 individual queries.
9. recalculate_workout_times.py:53-58 | Triple-nested N+1 with no prefetch (~18,000 queries)
- Impact: Command takes orders of magnitude longer than necessary.
10. exercise_selector.py:593,629 | M2M querysets not cached (excluded_exercises + available_equipment)
- Impact: 15-30 redundant identical queries per workout generation.
11. populate_exercise_fields.py:1006 | Individual save() per exercise (1133 UPDATE queries)
- Impact: Command takes minutes instead of seconds. No bulk_update.
12. plan_builder.py:64,82 | Redundant save() after create() on Workout and Superset
- Impact: 2 unnecessary DB writes per superset creation.
13. Various views | Infinite cache with no invalidation strategy
- What: equipment, exercise, muscle, workout, video views all use
cache.set(key, data, timeout=None)with no invalidation. - Impact: New/edited data never appears until manual cache clear or restart.
14. workout/serializers.py:109 | Redundant re-fetch of registered_user
- Impact: Extra query per workout detail for no reason.
15. generator/views.py:570-572,604-607 | N+1 save pattern for re-ordering after delete
- Impact: Up to N individual UPDATEs instead of 1 bulk_update.
16. generator/views.py:423-429,964-976 | N+1 for sibling exercise exclusion
- Impact: N queries instead of 1 IN query for sibling workout exercises.
17. generator/views.py:70 | RegisteredUser.objects.get repeated 17x with no caching
- Impact: 1 unnecessary query per API request across all generator endpoints.
18. exercise_selector.py:1063 | Potentially large retry loop in _weighted_pick
- What:
max_attempts = len(pool) * 3with weighted pools of 500+ entries = 1500+ iterations. - Impact: CPU-bound stall risk in constrained pools.
SECURITY — Vulnerability or exposure (6 additional findings)
1. werkout_api/settings.py:140 | ALLOWED_HOSTS=['*'] in production
- Impact: HTTP Host header injection, cache poisoning, password reset URL manipulation.
2. werkout_api/settings.py:1-231 | Missing all HTTPS/security hardening settings
- What: No SECURE_SSL_REDIRECT, SECURE_HSTS_SECONDS, SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURE, etc.
- Impact: Cookies sent over plaintext HTTP. No HSTS protection.
3. werkout_api/settings.py:31 | Django Debug Toolbar enabled unconditionally
- Impact: Exposes SQL queries, settings, request data at
/__debug__/in production.
4. workout/views.py:24-33 | all_workouts returns ALL users' workouts (IDOR)
- What:
Workout.objects.all()with no ownership filter. - Impact: Any authenticated user sees every user's workout data.
5. workout/views.py:39-49 | workout_details has no ownership check (IDOR)
- What: Any authenticated user can view any workout by guessing IDs.
- Impact: Insecure Direct Object Reference.
6. workout/views.py:170-172 | GET endpoint triggers data mutation — bulk import
- What: GET triggers Celery task importing workouts for hardcoded user IDs. Any authenticated user can trigger.
- Impact: Data corruption via idempotent-violating GET.
DATA INTEGRITY — Database/model consistency issues (5 findings)
1. workout/views.py:94-138 | add_workout has no transaction wrapping
- Impact: Partial Workout/Superset records on mid-loop failure.
2. plan_builder.py:59-149 | create_workout_from_spec has no transaction wrapping
- Impact: Core builder used by all generation paths creates orphaned records on error.
3. workout_analyzer.py:249-252 | _clear_existing_patterns deletes without transaction
- Impact: If analysis crashes mid-way, ML pattern tables are empty with no recovery.
4. workout/tasks.py:11-101 | Bulk import has no transaction or idempotency
- Impact: Partial imports, duplicate records on re-run.
5. workout/views.py:150 | datetime.now() without timezone in USE_TZ=True project
- Impact: Incorrect PlannedWorkout filtering near midnight due to timezone mismatch.
MODERNIZATION — Legacy pattern to update (4 findings)
1. Dockerfile:13 | Python 3.9.13 base image (EOL October 2025)
- Impact: No further security patches.
2. requirements.txt | All dependencies pinned to mid-2023 versions
- Impact: Django 4.2.2 has had multiple security releases since.
3. supervisord.conf:24 | Next.js runs next dev in production
- Impact: No production optimizations, source maps exposed.
4. Various models | max_length on IntegerField/FloatField (no-op parameters)
- What: 10+ fields across superset, workout, exercise models use meaningless
max_lengthon numeric fields. - Impact: Misleading — suggests validation that doesn't exist.
DEAD CODE / UNREACHABLE (4 findings)
1. exercise/serializers.py:5 | Import shadowed by local class definition
- What: Imports
ExerciseMuscleSerializerthen redefines it locally.
2. exercise/models.py:4 | from random import randrange — imported but never used
3. audit_exercise_data.py:88-89 | Dead .exclude() clause — logically impossible condition
4. workout/views.py:76 | Unreachable None check after .get()
FRAGILE — Works now but will break easily (5 findings)
1. exercise_selector.py:613 | Hard exclude to soft penalty conversion mutates instance state permanently
- What:
_warned_small_poolguard useshasattrwhich survivesreset(). - Impact: Once triggered, ALL subsequent selections treat hard-excluded exercises with soft penalty only.
2. exercise_selector.py:645 | Equipment map cache survives reset() — stale data possible
- Impact: Low risk per-request but dangerous in long-running processes.
3. workout_generator.py:1046 | Working superset detection relies on name prefix 'Working'
- Impact: Any naming inconsistency silently breaks trimming, padding, modality validation, compound ordering, rebalancing.
4. workout/models.py:51 | settings import via wildcard chain from exercise.models
- Impact: Transitive dependency breaks if
*re-export chain changes.
5. exercise_selector.py:260 | Working set exclusion icontains 'stretch' catches valid exercises
- Impact: Exercises like "Stiff Leg Deadlift Stretch Position" incorrectly excluded from working sets.
Summary
Summary by Category
| Category | Count |
|---|---|
| Critical | 18 |
| Bug | 28 |
| Silent Failure | 5 |
| Race Condition | 1 |
| Logic Error | 12 |
| Performance | 18 |
| Security | 6 |
| Data Integrity | 5 |
| Modernization | 4 |
| Dead Code | 4 |
| Fragile | 5 |
| Total | 106 |
Summary by Source
| Source | Findings |
|---|---|
| Security Auditor | 34 |
| Data Integrity/ORM Auditor | 64 |
| Logic Errors Auditor | 42 |
| Performance Auditor | 41 |
| Generator Logic Auditor | 22 |
| API Correctness Auditor | 43 |
| Celery/Async Auditor | 24 |
| Config/Deployment Auditor | 30 |
| Cross-cutting Deep Audit | 35 |
| (after dedup) | 106 unique |
Top 10 Priorities
-
[CRITICAL] settings.py — DEBUG=True + SECRET_KEY='secret' + CORS wide open in production — Three compounding security misconfigurations that enable session forgery, CSRF bypass, and full API data theft from any website.
-
[CRITICAL] registered_user/views.py:83-90 — request.POST wipes user data on JSON update — Any JSON profile update sets email, name, image all to None. Active, reachable endpoint.
-
[CRITICAL] registered_user/serializers.py:31 — Password hash exposed in API — Invalid DRF Meta option means hashed password is readable in registration responses.
-
[CRITICAL] scripts/views.py:43 — Anonymous cache wipe — Unauthenticated endpoint wipes entire Redis cache. Active route, no auth required.
-
[CRITICAL] supervisord.conf — No Celery worker + dev server in production — All async tasks (HLS transcoding, imports) silently queue and never execute. Django dev server handles all production traffic.
-
[CRITICAL] generator/views.py — No transaction.atomic() on save_plan/confirm_plan — Multi-object creation loops with no transaction wrapping leave orphaned records on any failure.
-
[BUG] workout/serializers.py:40 — HealthKit UUID silently discarded — Sets wrong attribute name (
workout_uuidvshealth_kit_workout_uuid). Data permanently lost. -
[BUG] workout/views.py:124 + tasks.py:85 — None multiplication on estimated_rep_duration — Nullable field multiplied without null check. Crashes workout creation and bulk import.
-
[LOGIC] workout_generator.py:1459 — Workout type affinity matching NEVER works — Space vs underscore comparison means all type assignments fall through to random round-robin.
-
[PERFORMANCE] Serializer N+1 queries — 5000+ queries on cache miss — WorkoutSerializer, ExerciseSerializer, and SupersetSerializer each trigger per-object queries with no prefetching. Mitigated by infinite caching but devastating on any cache clear.