feat: Auto-populate goal start_value from historical data
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 15s

**Problem:** Goals created today had start_value = current_value,
showing 0% progress even after months of tracking.

**Solution:**
1. Added start_date and start_value to GoalCreate/GoalUpdate models
2. New function _get_historical_value_for_goal_type():
   - Queries source table for value on specific date
   - ±7 day window for closest match
   - Works with all goal types via goal_type_definitions
3. create_goal() logic:
   - If start_date < today → auto-populate from historical data
   - If start_date = today → use current value
   - User can override start_value manually
4. update_goal() logic:
   - Changing start_date recalculates start_value
   - Can manually override start_value

**Example:**
- Goal created today with start_date = 3 months ago
- System finds weight on that date (88 kg)
- Current weight: 85.2 kg, Target: 82 kg
- Progress: (85.2 - 88) / (82 - 88) = 47% ✓

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-28 13:14:33 +01:00
parent a6701bf7b2
commit efde158dd4

View File

@ -54,6 +54,8 @@ class GoalCreate(BaseModel):
target_value: float
unit: str # kg, %, ml/kg/min, bpm, mmHg, cm, reps
target_date: Optional[date] = None
start_date: Optional[date] = None # When goal started (defaults to today, can be historical)
start_value: Optional[float] = None # Auto-populated from start_date if not provided
category: Optional[str] = 'other' # body, training, nutrition, recovery, health, other
priority: Optional[int] = 2 # 1=high, 2=medium, 3=low
name: Optional[str] = None
@ -64,6 +66,8 @@ class GoalUpdate(BaseModel):
"""Update existing goal"""
target_value: Optional[float] = None
target_date: Optional[date] = None
start_date: Optional[date] = None # Change start date (recalculates start_value)
start_value: Optional[float] = None # Manually override start value
status: Optional[str] = None # active, reached, abandoned, expired
is_primary: Optional[bool] = None # Kept for backward compatibility
category: Optional[str] = None # body, training, nutrition, recovery, health, other
@ -382,18 +386,36 @@ def create_goal(data: GoalCreate, session: dict = Depends(require_auth)):
# Get current value for this goal type
current_value = _get_current_value_for_goal_type(conn, pid, data.goal_type)
# Determine start_date (default to today if not provided)
start_date = data.start_date if data.start_date else date.today()
# Determine start_value
if data.start_value is not None:
# User explicitly provided start_value
start_value = data.start_value
elif start_date < date.today():
# Historical start date - try to get historical value
start_value = _get_historical_value_for_goal_type(conn, pid, data.goal_type, start_date)
if start_value is None:
# No data on that date, fall back to current value
start_value = current_value
print(f"[WARN] No historical data for {data.goal_type} on {start_date}, using current value")
else:
# Start date is today, use current value
start_value = current_value
# Insert goal
cur.execute("""
INSERT INTO goals (
profile_id, goal_type, is_primary,
target_value, current_value, start_value, unit,
target_date, category, priority, name, description
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
start_date, target_date, category, priority, name, description
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (
pid, data.goal_type, data.is_primary,
data.target_value, current_value, current_value, data.unit,
data.target_date, data.category, data.priority, data.name, data.description
data.target_value, current_value, start_value, data.unit,
start_date, data.target_date, data.category, data.priority, data.name, data.description
))
goal_id = cur.fetchone()['id']
@ -472,6 +494,28 @@ def update_goal(goal_id: str, data: GoalUpdate, session: dict = Depends(require_
updates.append("description = %s")
params.append(data.description)
# Handle start_date and start_value
if data.start_date is not None:
updates.append("start_date = %s")
params.append(data.start_date)
# If start_value not explicitly provided, recalculate from historical data
if data.start_value is None:
# Get goal_type for historical lookup
cur.execute("SELECT goal_type FROM goals WHERE id = %s", (goal_id,))
goal_row = cur.fetchone()
if goal_row:
goal_type = goal_row['goal_type']
historical_value = _get_historical_value_for_goal_type(conn, pid, goal_type, data.start_date)
if historical_value is not None:
updates.append("start_value = %s")
params.append(historical_value)
print(f"[INFO] Auto-populated start_value from {data.start_date}: {historical_value}")
if data.start_value is not None:
updates.append("start_value = %s")
params.append(data.start_value)
# Handle focus_contributions separately (can be updated even if no other changes)
if data.focus_contributions is not None:
# Delete existing contributions
@ -625,6 +669,70 @@ def _get_current_value_for_goal_type(conn, profile_id: str, goal_type: str) -> O
# Delegate to universal fetcher (Phase 1.5)
return get_current_value_for_goal(conn, profile_id, goal_type)
def _get_historical_value_for_goal_type(conn, profile_id: str, goal_type: str, target_date: date) -> Optional[float]:
"""
Get historical value for a goal type on a specific date.
Looks for closest value within ±7 days window.
Args:
conn: Database connection
profile_id: User's profile ID
goal_type: Goal type key (e.g., 'weight', 'body_fat')
target_date: Date to query (can be historical)
Returns:
Historical value or None if not found
"""
from goal_utils import get_goal_type_config, get_cursor
# Get goal type configuration
config = get_goal_type_config(conn, goal_type)
if not config:
return None
source_table = config.get('source_table')
source_column = config.get('source_column')
if not source_table or not source_column:
return None
# Query for value closest to target_date (±7 days window)
cur = get_cursor(conn)
try:
# Special handling for different tables
if source_table == 'vitals_baseline':
date_col = 'date'
elif source_table == 'blood_pressure_log':
date_col = 'recorded_at::date'
else:
date_col = 'date'
cur.execute(f"""
SELECT {source_column}
FROM {source_table}
WHERE profile_id = %s
AND {date_col} BETWEEN %s AND %s
ORDER BY ABS(EXTRACT(EPOCH FROM ({date_col} - %s::date)))
LIMIT 1
""", (
profile_id,
target_date - timedelta(days=7),
target_date + timedelta(days=7),
target_date
))
row = cur.fetchone()
if row:
value = row[source_column]
# Convert Decimal to float
return float(value) if value is not None else None
return None
except Exception as e:
print(f"[ERROR] Failed to get historical value for {goal_type} on {target_date}: {e}")
return None
def _update_goal_progress(conn, profile_id: str, goal: dict):
"""Update goal progress (modifies goal dict in-place)"""
# Get current value