fix: robust error handling in goal value fetcher
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 13s

Prevents crashes when:
- Goal types have NULL source_table/column (lean_mass, inactive placeholders)
- Old goals reference inactive goal types
- SQL queries fail for any reason

Changes:
- Guard clause checks table/column before SQL
- try-catch wraps all aggregation queries
- Returns None gracefully instead of crashing endpoint
- Logs warnings for debugging

Fixes: Goals page not loading due to /api/goals/list crash
This commit is contained in:
Lars 2026-03-27 07:55:19 +01:00
parent 1e758696fd
commit 1f4ee5021e

View File

@ -245,82 +245,92 @@ def _fetch_by_aggregation_method(
- min_30d: Minimum value in last 30 days - min_30d: Minimum value in last 30 days
- max_30d: Maximum value in last 30 days - max_30d: Maximum value in last 30 days
""" """
# Guard: source_table/column required for simple aggregation
if not table or not column:
print(f"[WARNING] Missing source_table or source_column for aggregation")
return None
cur = get_cursor(conn) cur = get_cursor(conn)
if method == 'latest': try:
cur.execute(f""" if method == 'latest':
SELECT {column} FROM {table} cur.execute(f"""
WHERE profile_id = %s AND {column} IS NOT NULL SELECT {column} FROM {table}
ORDER BY date DESC LIMIT 1 WHERE profile_id = %s AND {column} IS NOT NULL
""", (profile_id,)) ORDER BY date DESC LIMIT 1
row = cur.fetchone() """, (profile_id,))
return float(row[column]) if row else None row = cur.fetchone()
return float(row[column]) if row else None
elif method == 'avg_7d': elif method == 'avg_7d':
days_ago = date.today() - timedelta(days=7) days_ago = date.today() - timedelta(days=7)
cur.execute(f""" cur.execute(f"""
SELECT AVG({column}) as avg_value FROM {table} SELECT AVG({column}) as avg_value FROM {table}
WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL
""", (profile_id, days_ago)) """, (profile_id, days_ago))
row = cur.fetchone() row = cur.fetchone()
return float(row['avg_value']) if row and row['avg_value'] is not None else None return float(row['avg_value']) if row and row['avg_value'] is not None else None
elif method == 'avg_30d': elif method == 'avg_30d':
days_ago = date.today() - timedelta(days=30) days_ago = date.today() - timedelta(days=30)
cur.execute(f""" cur.execute(f"""
SELECT AVG({column}) as avg_value FROM {table} SELECT AVG({column}) as avg_value FROM {table}
WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL
""", (profile_id, days_ago)) """, (profile_id, days_ago))
row = cur.fetchone() row = cur.fetchone()
return float(row['avg_value']) if row and row['avg_value'] is not None else None return float(row['avg_value']) if row and row['avg_value'] is not None else None
elif method == 'sum_30d': elif method == 'sum_30d':
days_ago = date.today() - timedelta(days=30) days_ago = date.today() - timedelta(days=30)
cur.execute(f""" cur.execute(f"""
SELECT SUM({column}) as sum_value FROM {table} SELECT SUM({column}) as sum_value FROM {table}
WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL
""", (profile_id, days_ago)) """, (profile_id, days_ago))
row = cur.fetchone() row = cur.fetchone()
return float(row['sum_value']) if row and row['sum_value'] is not None else None return float(row['sum_value']) if row and row['sum_value'] is not None else None
elif method == 'count_7d': elif method == 'count_7d':
days_ago = date.today() - timedelta(days=7) days_ago = date.today() - timedelta(days=7)
cur.execute(f""" cur.execute(f"""
SELECT COUNT(*) as count_value FROM {table} SELECT COUNT(*) as count_value FROM {table}
WHERE profile_id = %s AND date >= %s WHERE profile_id = %s AND date >= %s
""", (profile_id, days_ago)) """, (profile_id, days_ago))
row = cur.fetchone() row = cur.fetchone()
return float(row['count_value']) if row else 0.0 return float(row['count_value']) if row else 0.0
elif method == 'count_30d': elif method == 'count_30d':
days_ago = date.today() - timedelta(days=30) days_ago = date.today() - timedelta(days=30)
cur.execute(f""" cur.execute(f"""
SELECT COUNT(*) as count_value FROM {table} SELECT COUNT(*) as count_value FROM {table}
WHERE profile_id = %s AND date >= %s WHERE profile_id = %s AND date >= %s
""", (profile_id, days_ago)) """, (profile_id, days_ago))
row = cur.fetchone() row = cur.fetchone()
return float(row['count_value']) if row else 0.0 return float(row['count_value']) if row else 0.0
elif method == 'min_30d': elif method == 'min_30d':
days_ago = date.today() - timedelta(days=30) days_ago = date.today() - timedelta(days=30)
cur.execute(f""" cur.execute(f"""
SELECT MIN({column}) as min_value FROM {table} SELECT MIN({column}) as min_value FROM {table}
WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL
""", (profile_id, days_ago)) """, (profile_id, days_ago))
row = cur.fetchone() row = cur.fetchone()
return float(row['min_value']) if row and row['min_value'] is not None else None return float(row['min_value']) if row and row['min_value'] is not None else None
elif method == 'max_30d': elif method == 'max_30d':
days_ago = date.today() - timedelta(days=30) days_ago = date.today() - timedelta(days=30)
cur.execute(f""" cur.execute(f"""
SELECT MAX({column}) as max_value FROM {table} SELECT MAX({column}) as max_value FROM {table}
WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL WHERE profile_id = %s AND date >= %s AND {column} IS NOT NULL
""", (profile_id, days_ago)) """, (profile_id, days_ago))
row = cur.fetchone() row = cur.fetchone()
return float(row['max_value']) if row and row['max_value'] is not None else None return float(row['max_value']) if row and row['max_value'] is not None else None
else: else:
print(f"[WARNING] Unknown aggregation method: {method}") print(f"[WARNING] Unknown aggregation method: {method}")
return None
except Exception as e:
print(f"[ERROR] Failed to fetch value from {table}.{column} using {method}: {e}")
return None return None