feat: hybrid goal tracking - with/without target_date
All checks were successful
Deploy Development / deploy (push) Successful in 52s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 15s

Implements requested hybrid approach:

WITH target_date:
  - Time-based deviation (actual vs. expected progress)
  - Format: 'Zielgewicht (41%, +7% voraus)'

WITHOUT target_date:
  - Simple progress percentage
  - Format: 'Ruhepuls (100% erreicht)' or 'VO2max (0% erreicht)'

Sorting:
  behind_schedule:
    1. Goals with negative deviation (behind timeline)
    2. Goals without date with progress < 50%

  on_track:
    1. Goals with positive deviation (ahead of timeline)
    2. Goals without date with progress >= 50%

Kept debug logging for new hybrid logic validation.
This commit is contained in:
Lars 2026-03-28 17:22:18 +01:00
parent 0e89850df8
commit dd395180a3

View File

@ -898,25 +898,82 @@ def _format_goals_behind(profile_id: str, n: int = 3) -> str:
print(f"[DEBUG] → Exception: {type(e).__name__}: {e}") print(f"[DEBUG] → Exception: {type(e).__name__}: {e}")
continue continue
if not goals_with_deviation: # Also process goals WITHOUT target_date (simple progress)
return 'Keine Ziele mit Zeitvorgabe' goals_without_date = []
print(f"[DEBUG] Processing goals without target_date for simple progress")
# Sort by deviation ascending (most negative first = most behind) for g in goals:
# Only include goals that are actually behind (deviation < 0) if g.get('target_date'):
behind_goals = [g for g in goals_with_deviation if g['_deviation'] < 0] continue # Already processed above
if not behind_goals: goal_name = g.get('name') or g.get('goal_type', 'Unknown')
return 'Alle Ziele im Zeitplan' current = g.get('current_value')
target = g.get('target_value')
start = g.get('start_value')
sorted_goals = sorted(behind_goals, key=lambda x: x['_deviation'])[:n] if None in [current, target, start]:
print(f"[DEBUG] Goal '{goal_name}' (no date): Skipped - missing values")
continue
try:
current = float(current)
target = float(target)
start = float(start)
if target == start:
progress_pct = 100 if current == target else 0
else:
progress_pct = ((current - start) / (target - start)) * 100
progress_pct = max(0, min(100, progress_pct))
g['_simple_progress'] = int(progress_pct)
goals_without_date.append(g)
print(f"[DEBUG] Goal '{goal_name}' (no date): Added with {int(progress_pct)}% progress")
except (ValueError, ZeroDivisionError, TypeError) as e:
print(f"[DEBUG] Goal '{goal_name}' (no date): Exception - {e}")
continue
# Combine: Goals with negative deviation + Goals without date with low progress
behind_with_date = [g for g in goals_with_deviation if g['_deviation'] < 0]
behind_without_date = [g for g in goals_without_date if g['_simple_progress'] < 50]
print(f"[DEBUG] Behind with date: {len(behind_with_date)}, Behind without date: {len(behind_without_date)}")
# Create combined list with sort keys
combined = []
for g in behind_with_date:
combined.append({
'goal': g,
'sort_key': g['_deviation'], # Negative deviation (worst first)
'has_date': True
})
for g in behind_without_date:
# Map progress to deviation-like scale: 0% = -100, 50% = -50
combined.append({
'goal': g,
'sort_key': g['_simple_progress'] - 100, # Convert to negative scale
'has_date': False
})
if not combined:
return 'Alle Ziele im Zeitplan oder erreicht'
# Sort by sort_key (most negative first)
sorted_combined = sorted(combined, key=lambda x: x['sort_key'])[:n]
lines = [] lines = []
for goal in sorted_goals: for item in sorted_combined:
name = goal.get('name') or goal.get('goal_type', 'Unbekannt') g = item['goal']
actual = goal['_actual_progress'] name = g.get('name') or g.get('goal_type', 'Unbekannt')
expected = goal['_expected_progress']
deviation = goal['_deviation'] if item['has_date']:
lines.append(f"{name} ({actual}% statt {expected}%, {deviation}%)") actual = g['_actual_progress']
expected = g['_expected_progress']
deviation = g['_deviation']
lines.append(f"{name} ({actual}% statt {expected}%, {deviation}%)")
else:
progress = g['_simple_progress']
lines.append(f"{name} ({progress}% erreicht)")
return ', '.join(lines) return ', '.join(lines)
except Exception as e: except Exception as e:
@ -1014,25 +1071,81 @@ def _format_goals_on_track(profile_id: str, n: int = 3) -> str:
print(f"[DEBUG] → Exception: {type(e).__name__}: {e}") print(f"[DEBUG] → Exception: {type(e).__name__}: {e}")
continue continue
if not goals_with_deviation: # Also process goals WITHOUT target_date (simple progress)
return 'Keine Ziele mit Zeitvorgabe' goals_without_date = []
print(f"[DEBUG] Processing goals without target_date for simple progress")
# Sort by deviation descending (most positive first = most ahead) for g in goals:
# Only include goals that are ahead or on track (deviation >= 0) if g.get('target_date'):
ahead_goals = [g for g in goals_with_deviation if g['_deviation'] >= 0] continue # Already processed above
if not ahead_goals: goal_name = g.get('name') or g.get('goal_type', 'Unknown')
current = g.get('current_value')
target = g.get('target_value')
start = g.get('start_value')
if None in [current, target, start]:
print(f"[DEBUG] Goal '{goal_name}' (no date): Skipped - missing values")
continue
try:
current = float(current)
target = float(target)
start = float(start)
if target == start:
progress_pct = 100 if current == target else 0
else:
progress_pct = ((current - start) / (target - start)) * 100
progress_pct = max(0, min(100, progress_pct))
g['_simple_progress'] = int(progress_pct)
goals_without_date.append(g)
print(f"[DEBUG] Goal '{goal_name}' (no date): Added with {int(progress_pct)}% progress")
except (ValueError, ZeroDivisionError, TypeError) as e:
print(f"[DEBUG] Goal '{goal_name}' (no date): Exception - {e}")
continue
# Combine: Goals with positive deviation + Goals without date with high progress
ahead_with_date = [g for g in goals_with_deviation if g['_deviation'] >= 0]
ahead_without_date = [g for g in goals_without_date if g['_simple_progress'] >= 50]
print(f"[DEBUG] Ahead with date: {len(ahead_with_date)}, Ahead without date: {len(ahead_without_date)}")
# Create combined list with sort keys
combined = []
for g in ahead_with_date:
combined.append({
'goal': g,
'sort_key': g['_deviation'], # Positive deviation (best first)
'has_date': True
})
for g in ahead_without_date:
# Map progress to deviation-like scale: 50% = 0, 100% = +50
combined.append({
'goal': g,
'sort_key': g['_simple_progress'] - 50, # Convert to positive scale
'has_date': False
})
if not combined:
return 'Keine Ziele im Zeitplan' return 'Keine Ziele im Zeitplan'
sorted_goals = sorted(ahead_goals, key=lambda x: x['_deviation'], reverse=True)[:n] # Sort by sort_key descending (most positive first)
sorted_combined = sorted(combined, key=lambda x: x['sort_key'], reverse=True)[:n]
lines = [] lines = []
for goal in sorted_goals: for item in sorted_combined:
name = goal.get('name') or goal.get('goal_type', 'Unbekannt') g = item['goal']
actual = goal['_actual_progress'] name = g.get('name') or g.get('goal_type', 'Unbekannt')
expected = goal['_expected_progress']
deviation = goal['_deviation'] if item['has_date']:
lines.append(f"{name} ({actual}%, +{deviation}% voraus)") actual = g['_actual_progress']
deviation = g['_deviation']
lines.append(f"{name} ({actual}%, +{deviation}% voraus)")
else:
progress = g['_simple_progress']
lines.append(f"{name} ({progress}% erreicht)")
return ', '.join(lines) return ', '.join(lines)
except Exception as e: except Exception as e: