fix: behind_schedule now uses time-based deviation, not just lowest progress
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

OLD: Showed 3 goals with lowest progress %
NEW: Calculates expected progress based on elapsed time vs. total time
     Shows goals with largest negative deviation (behind schedule)

Example Weight Goal:
- Total time: 98 days (22.02 - 31.05)
- Elapsed: 34 days (35%)
- Actual progress: 41%
- Deviation: +7% (AHEAD, not behind)

Also updated on_track to show goals with positive deviation (ahead of schedule).

Note: Linear progress is a simplification. Real-world progress curves vary
by goal type (weight loss, muscle gain, VO2max, etc). Future: AI-based
projection models for more realistic expectations.
This commit is contained in:
Lars 2026-03-28 14:58:50 +01:00
parent d7aa0eb3af
commit 8e67175ed2

View File

@ -806,111 +806,218 @@ def _format_top_focus_areas(profile_id: str, n: int = 3) -> str:
def _format_goals_behind(profile_id: str, n: int = 3) -> str:
"""Format top N goals behind schedule"""
"""
Format top N goals behind schedule (based on time deviation).
Compares actual progress vs. expected progress based on elapsed time.
Negative deviation = behind schedule.
"""
try:
from goal_utils import get_active_goals
from datetime import date
goals = get_active_goals(profile_id)
if not goals:
return 'Keine Ziele definiert'
# Calculate progress for each goal
goals_with_progress = []
today = date.today()
goals_with_deviation = []
for g in goals:
current = g.get('current_value')
target = g.get('target_value')
start = g.get('start_value')
start_date = g.get('start_date')
target_date = g.get('target_date')
# Skip if missing required values
if None in [current, target, start]:
continue
# Calculate progress percentage
# Skip if no target_date (can't calculate time-based progress)
if not target_date:
continue
try:
current = float(current)
target = float(target)
start = float(start)
# Calculate actual progress percentage
if target == start:
progress_pct = 100 if current == target else 0
actual_progress_pct = 100 if current == target else 0
else:
progress_pct = ((current - start) / (target - start)) * 100
progress_pct = max(0, min(100, progress_pct))
actual_progress_pct = ((current - start) / (target - start)) * 100
actual_progress_pct = max(0, min(100, actual_progress_pct))
g['_calc_progress_pct'] = int(progress_pct)
goals_with_progress.append(g)
except (ValueError, ZeroDivisionError):
# Calculate expected progress based on time
if start_date:
# Use start_date if available
start_dt = start_date if isinstance(start_date, date) else date.fromisoformat(str(start_date))
else:
# Fallback: assume start date = created_at date
created_at = g.get('created_at')
if created_at:
start_dt = date.fromisoformat(str(created_at).split('T')[0])
else:
continue # Can't calculate without start date
target_dt = target_date if isinstance(target_date, date) else date.fromisoformat(str(target_date))
# Calculate time progress
total_days = (target_dt - start_dt).days
elapsed_days = (today - start_dt).days
if total_days <= 0:
continue # Invalid date range
expected_progress_pct = (elapsed_days / total_days) * 100
expected_progress_pct = max(0, min(100, expected_progress_pct))
# Calculate deviation (negative = behind schedule)
deviation = actual_progress_pct - expected_progress_pct
g['_actual_progress'] = int(actual_progress_pct)
g['_expected_progress'] = int(expected_progress_pct)
g['_deviation'] = int(deviation)
goals_with_deviation.append(g)
except (ValueError, ZeroDivisionError, TypeError):
continue
if not goals_with_progress:
return 'Keine Ziele mit Fortschritt'
if not goals_with_deviation:
return 'Keine Ziele mit Zeitvorgabe'
# Sort by progress ascending (lowest first) and take top N
sorted_goals = sorted(goals_with_progress, key=lambda x: x.get('_calc_progress_pct', 0))[:n]
# Sort by deviation ascending (most negative first = most behind)
# Only include goals that are actually behind (deviation < 0)
behind_goals = [g for g in goals_with_deviation if g['_deviation'] < 0]
if not behind_goals:
return 'Alle Ziele im Zeitplan'
sorted_goals = sorted(behind_goals, key=lambda x: x['_deviation'])[:n]
lines = []
for goal in sorted_goals:
name = goal.get('name') or goal.get('goal_type', 'Unbekannt')
progress = goal.get('_calc_progress_pct', 0)
lines.append(f"{name} ({progress}%)")
actual = goal['_actual_progress']
expected = goal['_expected_progress']
deviation = goal['_deviation']
lines.append(f"{name} ({actual}% statt {expected}%, {deviation}%)")
return ', '.join(lines)
except Exception:
except Exception as e:
print(f"[ERROR] _format_goals_behind: {e}")
import traceback
traceback.print_exc()
return 'nicht verfügbar'
def _format_goals_on_track(profile_id: str, n: int = 3) -> str:
"""Format top N goals on track"""
"""
Format top N goals ahead of schedule (based on time deviation).
Compares actual progress vs. expected progress based on elapsed time.
Positive deviation = ahead of schedule / on track.
"""
try:
from goal_utils import get_active_goals
from datetime import date
goals = get_active_goals(profile_id)
if not goals:
return 'Keine Ziele definiert'
# Calculate progress for each goal
goals_with_progress = []
today = date.today()
goals_with_deviation = []
for g in goals:
current = g.get('current_value')
target = g.get('target_value')
start = g.get('start_value')
start_date = g.get('start_date')
target_date = g.get('target_date')
# Skip if missing required values
if None in [current, target, start]:
continue
# Calculate progress percentage
# Skip if no target_date
if not target_date:
continue
try:
current = float(current)
target = float(target)
start = float(start)
# Calculate actual progress percentage
if target == start:
progress_pct = 100 if current == target else 0
actual_progress_pct = 100 if current == target else 0
else:
progress_pct = ((current - start) / (target - start)) * 100
progress_pct = max(0, min(100, progress_pct))
actual_progress_pct = ((current - start) / (target - start)) * 100
actual_progress_pct = max(0, min(100, actual_progress_pct))
g['_calc_progress_pct'] = int(progress_pct)
goals_with_progress.append(g)
except (ValueError, ZeroDivisionError):
# Calculate expected progress based on time
if start_date:
start_dt = start_date if isinstance(start_date, date) else date.fromisoformat(str(start_date))
else:
created_at = g.get('created_at')
if created_at:
start_dt = date.fromisoformat(str(created_at).split('T')[0])
else:
continue
# Filter goals with progress >= 50%
goals_on_track = [g for g in goals_with_progress if g.get('_calc_progress_pct', 0) >= 50]
target_dt = target_date if isinstance(target_date, date) else date.fromisoformat(str(target_date))
if not goals_on_track:
return 'Keine Ziele auf gutem Weg'
# Calculate time progress
total_days = (target_dt - start_dt).days
elapsed_days = (today - start_dt).days
# Sort by progress descending (highest first) and take top N
sorted_goals = sorted(goals_on_track, key=lambda x: x.get('_calc_progress_pct', 0), reverse=True)[:n]
if total_days <= 0:
continue
expected_progress_pct = (elapsed_days / total_days) * 100
expected_progress_pct = max(0, min(100, expected_progress_pct))
# Calculate deviation (positive = ahead of schedule)
deviation = actual_progress_pct - expected_progress_pct
g['_actual_progress'] = int(actual_progress_pct)
g['_expected_progress'] = int(expected_progress_pct)
g['_deviation'] = int(deviation)
goals_with_deviation.append(g)
except (ValueError, ZeroDivisionError, TypeError):
continue
if not goals_with_deviation:
return 'Keine Ziele mit Zeitvorgabe'
# Sort by deviation descending (most positive first = most ahead)
# Only include goals that are ahead or on track (deviation >= 0)
ahead_goals = [g for g in goals_with_deviation if g['_deviation'] >= 0]
if not ahead_goals:
return 'Keine Ziele im Zeitplan'
sorted_goals = sorted(ahead_goals, key=lambda x: x['_deviation'], reverse=True)[:n]
lines = []
for goal in sorted_goals:
name = goal.get('name') or goal.get('goal_type', 'Unbekannt')
progress = goal.get('_calc_progress_pct', 0)
lines.append(f"{name} ({progress}%)")
actual = goal['_actual_progress']
expected = goal['_expected_progress']
deviation = goal['_deviation']
lines.append(f"{name} ({actual}%, +{deviation}% voraus)")
return ', '.join(lines)
except Exception:
except Exception as e:
print(f"[ERROR] _format_goals_on_track: {e}")
import traceback
traceback.print_exc()
return 'nicht verfügbar'