From 8da577fe5890ba6826b60ee428b0612c7c214d48 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 28 Mar 2026 12:34:24 +0100 Subject: [PATCH] fix: Phase 0b - body_progress_score + placeholder formatting Fixed remaining placeholder calculation issues: 1. body_progress_score returning 0: - When start_value is NULL, query oldest weight from last 90 days - Prevents progress = 0% when start equals current 2. focus_areas_weighted_json empty: - Changed from goal_utils.get_focus_weights_v2() to scores.get_user_focus_weights() - Now uses same function as focus_area_weights_json 3. Implemented 5 TODO markdown formatting functions: - _format_goals_as_markdown() - table with progress bars - _format_focus_areas_as_markdown() - weighted list - _format_top_focus_areas() - top N by weight - _format_goals_behind() - lowest progress goals - _format_goals_on_track() - goals >= 50% progress All placeholders should now return proper values. Co-Authored-By: Claude Opus 4.6 --- backend/calculations/body_metrics.py | 19 +++- backend/placeholder_resolver.py | 125 ++++++++++++++++++++++++--- 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/backend/calculations/body_metrics.py b/backend/calculations/body_metrics.py index 8a8d942..8a20d9a 100644 --- a/backend/calculations/body_metrics.py +++ b/backend/calculations/body_metrics.py @@ -395,11 +395,26 @@ def _score_weight_trend(profile_id: str) -> Optional[int]: current = goal.get('current_value') target = goal.get('target_value') - start = goal.get('start_value', current) + start = goal.get('start_value') - if None in [current, target, start]: + if None in [current, target]: return None + # If no start_value, use oldest weight in last 90 days + if start is None: + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT weight + FROM weight_log + WHERE profile_id = %s + AND date >= CURRENT_DATE - INTERVAL '90 days' + ORDER BY date ASC + LIMIT 1 + """, (profile_id,)) + row = cur.fetchone() + start = float(row['weight']) if row else current + # Progress percentage progress_pct = calculate_goal_progress_pct(current, target, start) diff --git a/backend/placeholder_resolver.py b/backend/placeholder_resolver.py index 985b522..5f194b4 100644 --- a/backend/placeholder_resolver.py +++ b/backend/placeholder_resolver.py @@ -677,9 +677,10 @@ def _get_focus_areas_weighted_json(profile_id: str) -> str: """Get focus areas with weights as JSON string""" import json try: - from goal_utils import get_focus_weights_v2, get_db, get_cursor + from calculations.scores import get_user_focus_weights + from goal_utils import get_db, get_cursor - weights = get_focus_weights_v2(get_db(), profile_id) + weights = get_user_focus_weights(profile_id) # Get focus area details with get_db() as conn: @@ -713,32 +714,132 @@ def _get_focus_areas_weighted_json(profile_id: str) -> str: def _format_goals_as_markdown(profile_id: str) -> str: """Format goals as markdown table""" - # TODO: Implement - return 'Keine Ziele definiert' + try: + from goal_utils import get_active_goals + + goals = get_active_goals(profile_id) + if not goals: + return 'Keine Ziele definiert' + + # Build markdown table + lines = ['| Ziel | Aktuell | Ziel | Progress | Status |', '|------|---------|------|----------|--------|'] + + for goal in goals: + name = goal.get('name') or goal.get('goal_type', 'Unbekannt') + current = f"{goal.get('current_value', '-')}" + target = f"{goal.get('target_value', '-')}" + progress = f"{goal.get('progress_pct', '-')}%" if goal.get('progress_pct') else '-' + status = '🎯' if goal.get('is_primary') else '○' + + lines.append(f"| {name} | {current} | {target} | {progress} | {status} |") + + return '\n'.join(lines) + except Exception: + return 'Keine Ziele definiert' def _format_focus_areas_as_markdown(profile_id: str) -> str: """Format focus areas as markdown""" - # TODO: Implement - return 'Keine Focus Areas aktiv' + try: + import json + weighted_json = _get_focus_areas_weighted_json(profile_id) + areas = json.loads(weighted_json) + + if not areas: + return 'Keine Focus Areas aktiv' + + # Build markdown list + lines = [] + for area in areas: + name = area.get('name', 'Unbekannt') + weight = area.get('weight', 0) + lines.append(f"- **{name}**: {weight}%") + + return '\n'.join(lines) + except Exception: + return 'Keine Focus Areas aktiv' def _format_top_focus_areas(profile_id: str, n: int = 3) -> str: """Format top N focus areas as text""" - # TODO: Implement - return 'nicht verfügbar' + try: + import json + weighted_json = _get_focus_areas_weighted_json(profile_id) + areas = json.loads(weighted_json) + + if not areas: + return 'Keine Focus Areas definiert' + + # Sort by weight descending and take top N + sorted_areas = sorted(areas, key=lambda x: x.get('weight', 0), reverse=True)[:n] + + lines = [] + for i, area in enumerate(sorted_areas, 1): + name = area.get('name', 'Unbekannt') + weight = area.get('weight', 0) + lines.append(f"{i}. {name} ({weight}%)") + + return ', '.join(lines) + except Exception: + return 'nicht verfügbar' def _format_goals_behind(profile_id: str, n: int = 3) -> str: """Format top N goals behind schedule""" - # TODO: Implement - return 'nicht verfügbar' + try: + from goal_utils import get_active_goals + goals = get_active_goals(profile_id) + + if not goals: + return 'Keine Ziele definiert' + + # Filter goals with progress_pct available, sort by lowest progress + goals_with_progress = [g for g in goals if g.get('progress_pct') is not None] + + if not goals_with_progress: + return 'Keine Ziele mit Fortschritt' + + # Sort by progress ascending (lowest first) and take top N + sorted_goals = sorted(goals_with_progress, key=lambda x: x.get('progress_pct', 0))[:n] + + lines = [] + for goal in sorted_goals: + name = goal.get('name') or goal.get('goal_type', 'Unbekannt') + progress = goal.get('progress_pct', 0) + lines.append(f"{name} ({progress}%)") + + return ', '.join(lines) + except Exception: + return 'nicht verfügbar' def _format_goals_on_track(profile_id: str, n: int = 3) -> str: """Format top N goals on track""" - # TODO: Implement - return 'nicht verfügbar' + try: + from goal_utils import get_active_goals + goals = get_active_goals(profile_id) + + if not goals: + return 'Keine Ziele definiert' + + # Filter goals with progress >= 50%, sort by highest progress + goals_on_track = [g for g in goals if g.get('progress_pct') is not None and g.get('progress_pct', 0) >= 50] + + if not goals_on_track: + return 'Keine Ziele auf gutem Weg' + + # Sort by progress descending (highest first) and take top N + sorted_goals = sorted(goals_on_track, key=lambda x: x.get('progress_pct', 0), reverse=True)[:n] + + lines = [] + for goal in sorted_goals: + name = goal.get('name') or goal.get('goal_type', 'Unbekannt') + progress = goal.get('progress_pct', 0) + lines.append(f"{name} ({progress}%)") + + return ', '.join(lines) + except Exception: + return 'nicht verfügbar' # ── Placeholder Registry ──────────────────────────────────────────────────────