mitai-jinkendo/backend/goal_utils.py
Lars 87464ff138
All checks were successful
Deploy Development / deploy (push) Successful in 53s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
fix: Phase 1 - Goal System Quick Fixes + Abstraction Layer
Behebt 4 kritische Bugs in Phase 0a und schafft Basis für Phase 0b
ohne spätere Doppelarbeit.

Backend:
- NEW: goal_utils.py mit get_focus_weights() Abstraction Layer
  → V1: Mappt goal_mode zu Gewichten
  → V2 (später): Liest aus focus_areas Tabelle
  → Phase 0b Platzhalter (120+) müssen NICHT umgeschrieben werden
- FIX: Primary goal toggle in goals.py (is_primary im GoalUpdate Model)
  → Beim Update auf primary werden andere Goals korrekt auf false gesetzt
- FIX: lean_mass current_value Berechnung implementiert
  → weight - (weight * body_fat_pct / 100)
- FIX: VO2Max Spaltenname vo2_max (statt vo2max)
  → Internal Server Error behoben

CLAUDE.md:
- Version Update: Phase 1 Fixes (27.03.2026)

Keine Doppelarbeit:
- Alle zukünftigen Phase 0b Platzhalter nutzen get_focus_weights()
- v2.0 Redesign = nur eine Funktion ändern, nicht 120+ Platzhalter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 06:13:47 +01:00

191 lines
6.2 KiB
Python

"""
Goal Utilities - Abstraction Layer for Focus Weights
This module provides an abstraction layer between goal modes and focus weights.
This allows Phase 0b placeholders to work with the current simple goal_mode system,
while enabling future v2.0 redesign (focus_areas table) without rewriting 120+ placeholders.
Version History:
- V1 (current): Maps goal_mode to predefined weights
- V2 (future): Reads from focus_areas table with custom user weights
Part of Phase 1: Quick Fixes + Abstraction Layer
"""
from typing import Dict
from db import get_cursor
def get_focus_weights(conn, profile_id: str) -> Dict[str, float]:
"""
Get focus area weights for a profile.
This is an abstraction layer that will evolve:
- V1 (now): Maps goal_mode → predefined weights
- V2 (later): Reads from focus_areas table → custom user weights
Args:
conn: Database connection
profile_id: User's profile ID
Returns:
Dict with focus weights (sum = 1.0):
{
'weight_loss': 0.3, # Fat loss priority
'muscle_gain': 0.2, # Muscle gain priority
'strength': 0.25, # Strength training priority
'endurance': 0.25, # Cardio/endurance priority
'flexibility': 0.0, # Mobility priority
'health': 0.0 # General health maintenance
}
Example Usage in Phase 0b:
weights = get_focus_weights(conn, profile_id)
# Score calculation considers user's focus
overall_score = (
body_score * weights['weight_loss'] +
strength_score * weights['strength'] +
cardio_score * weights['endurance']
)
"""
cur = get_cursor(conn)
# Fetch current goal_mode
cur.execute(
"SELECT goal_mode FROM profiles WHERE id = %s",
(profile_id,)
)
row = cur.fetchone()
if not row:
# Fallback: balanced health focus
return {
'weight_loss': 0.0,
'muscle_gain': 0.0,
'strength': 0.0,
'endurance': 0.0,
'flexibility': 0.0,
'health': 1.0
}
goal_mode = row['goal_mode'] or 'health'
# V1: Predefined weight mappings per goal_mode
# These represent "typical" focus distributions for each mode
WEIGHT_MAPPINGS = {
'weight_loss': {
'weight_loss': 0.60, # Primary: fat loss
'endurance': 0.20, # Support: cardio for calorie burn
'muscle_gain': 0.0, # Not compatible
'strength': 0.10, # Maintain muscle during deficit
'flexibility': 0.05, # Minor: mobility work
'health': 0.05 # Minor: general wellness
},
'strength': {
'strength': 0.50, # Primary: strength gains
'muscle_gain': 0.40, # Support: hypertrophy
'endurance': 0.10, # Minor: work capacity
'weight_loss': 0.0, # Not compatible with strength focus
'flexibility': 0.0,
'health': 0.0
},
'endurance': {
'endurance': 0.70, # Primary: aerobic capacity
'health': 0.20, # Support: cardiovascular health
'flexibility': 0.10, # Support: mobility for running
'weight_loss': 0.0,
'muscle_gain': 0.0,
'strength': 0.0
},
'recomposition': {
'weight_loss': 0.30, # Equal: lose fat
'muscle_gain': 0.30, # Equal: gain muscle
'strength': 0.25, # Support: progressive overload
'endurance': 0.10, # Minor: conditioning
'flexibility': 0.05, # Minor: mobility
'health': 0.0
},
'health': {
'health': 0.50, # Primary: general wellness
'endurance': 0.20, # Support: cardio health
'flexibility': 0.15, # Support: mobility
'strength': 0.10, # Support: functional strength
'weight_loss': 0.05, # Minor: maintain healthy weight
'muscle_gain': 0.0
}
}
return WEIGHT_MAPPINGS.get(goal_mode, WEIGHT_MAPPINGS['health'])
def get_primary_focus(conn, profile_id: str) -> str:
"""
Get the primary focus area for a profile.
Returns the focus area with the highest weight.
Useful for UI labels and simple decision logic.
Args:
conn: Database connection
profile_id: User's profile ID
Returns:
Primary focus area name (e.g., 'weight_loss', 'strength')
"""
weights = get_focus_weights(conn, profile_id)
return max(weights.items(), key=lambda x: x[1])[0]
def get_focus_description(focus_area: str) -> str:
"""
Get human-readable description for a focus area.
Args:
focus_area: Focus area key (e.g., 'weight_loss')
Returns:
German description for UI display
"""
descriptions = {
'weight_loss': 'Gewichtsreduktion & Fettabbau',
'muscle_gain': 'Muskelaufbau & Hypertrophie',
'strength': 'Kraftsteigerung & Performance',
'endurance': 'Ausdauer & aerobe Kapazität',
'flexibility': 'Beweglichkeit & Mobilität',
'health': 'Allgemeine Gesundheit & Erhaltung'
}
return descriptions.get(focus_area, focus_area)
# Future V2 Implementation (commented out for reference):
"""
def get_focus_weights_v2(conn, profile_id: str) -> Dict[str, float]:
'''V2: Read from focus_areas table with custom user weights'''
cur = get_cursor(conn)
cur.execute('''
SELECT weight_loss_pct, muscle_gain_pct, endurance_pct,
strength_pct, flexibility_pct, health_pct
FROM focus_areas
WHERE profile_id = %s AND active = true
LIMIT 1
''', (profile_id,))
row = cur.fetchone()
if not row:
# Fallback to V1 behavior
return get_focus_weights(conn, profile_id)
# Convert percentages to weights (0-1 range)
return {
'weight_loss': row['weight_loss_pct'] / 100.0,
'muscle_gain': row['muscle_gain_pct'] / 100.0,
'endurance': row['endurance_pct'] / 100.0,
'strength': row['strength_pct'] / 100.0,
'flexibility': row['flexibility_pct'] / 100.0,
'health': row['health_pct'] / 100.0
}
"""