From e4a2b63a482825f68ad98a0b911bae2c37a8b2af Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 27 Mar 2026 22:09:52 +0100 Subject: [PATCH] fix: vitals baseline parameter sync + goal utils transaction rollback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1 Fix (Ruhepuls): - Completely rewrote vitals_baseline POST endpoint - Clear separation: param_values array contains ALL values (pid, date, ...) - Synchronized insert_cols, insert_placeholders, and param_values - Added debug logging - Simplified UPDATE logic (EXCLUDED.col instead of COALESCE) Bug 2 Fix (Custom Goal Type Transaction Error): - Added transaction rollback in goal_utils._fetch_by_aggregation_method() - When SQL query fails (e.g., invalid column name), rollback transaction - Prevents 'InFailedSqlTransaction' errors on subsequent queries - Enhanced error logging (shows filter conditions, SQL, params) - Returns None gracefully so goal creation can continue User Action Required for Bug 2: - Edit goal type 'Trainingshäufigkeit Krafttraining' - Change filter from {"training_type": "strength"} to {"training_category": "strength"} - activity_log has training_category, NOT training_type column --- backend/goal_utils.py | 16 +++++++- backend/routers/vitals_baseline.py | 59 +++++++++++++++++++----------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/backend/goal_utils.py b/backend/goal_utils.py index 09bd9cc..421450c 100644 --- a/backend/goal_utils.py +++ b/backend/goal_utils.py @@ -278,7 +278,7 @@ def _fetch_by_aggregation_method( - max_30d: Maximum value in last 30 days Args: - filter_conditions: Optional JSON filters (e.g., {"training_type": "strength"}) + filter_conditions: Optional JSON filters (e.g., {"training_category": "strength"}) """ # Guard: source_table/column required for simple aggregation if not table or not column: @@ -412,7 +412,21 @@ def _fetch_by_aggregation_method( return None except Exception as e: + # Log detailed error for debugging print(f"[ERROR] Failed to fetch value from {table}.{column} using {method}: {e}") + print(f"[ERROR] Filter conditions: {filter_conditions}") + print(f"[ERROR] Filter SQL: {filter_sql}") + print(f"[ERROR] Filter params: {filter_params}") + + # CRITICAL: Rollback transaction to avoid InFailedSqlTransaction errors + try: + conn.rollback() + print(f"[INFO] Transaction rolled back after query error") + except Exception as rollback_err: + print(f"[WARNING] Rollback failed: {rollback_err}") + + # Return None so goal creation can continue without current_value + # (current_value will be NULL in the goal record) return None diff --git a/backend/routers/vitals_baseline.py b/backend/routers/vitals_baseline.py index 39aae6c..8651ef8 100644 --- a/backend/routers/vitals_baseline.py +++ b/backend/routers/vitals_baseline.py @@ -99,67 +99,72 @@ def create_or_update_baseline( """Create or update baseline entry (upsert on date).""" pid = get_pid(x_profile_id) - # Build dynamic INSERT columns and UPDATE fields + # Build dynamic INSERT columns, placeholders, UPDATE fields, and values list + # All arrays must stay synchronized insert_cols = [] insert_placeholders = [] update_fields = [] - values = [pid, entry.date] - param_idx = 3 # Start after $1 (pid) and $2 (date) + param_values = [] # Will contain ALL values including pid and date + + # Always include profile_id and date + param_values.append(pid) + param_values.append(entry.date) + param_idx = 3 # Next parameter starts at $3 if entry.resting_hr is not None: insert_cols.append("resting_hr") insert_placeholders.append(f"${param_idx}") - update_fields.append(f"resting_hr = COALESCE(EXCLUDED.resting_hr, vitals_baseline.resting_hr)") - values.append(entry.resting_hr) + update_fields.append(f"resting_hr = EXCLUDED.resting_hr") + param_values.append(entry.resting_hr) param_idx += 1 if entry.hrv is not None: insert_cols.append("hrv") insert_placeholders.append(f"${param_idx}") - update_fields.append(f"hrv = COALESCE(EXCLUDED.hrv, vitals_baseline.hrv)") - values.append(entry.hrv) + update_fields.append(f"hrv = EXCLUDED.hrv") + param_values.append(entry.hrv) param_idx += 1 if entry.vo2_max is not None: insert_cols.append("vo2_max") insert_placeholders.append(f"${param_idx}") - update_fields.append(f"vo2_max = COALESCE(EXCLUDED.vo2_max, vitals_baseline.vo2_max)") - values.append(entry.vo2_max) + update_fields.append(f"vo2_max = EXCLUDED.vo2_max") + param_values.append(entry.vo2_max) param_idx += 1 if entry.spo2 is not None: insert_cols.append("spo2") insert_placeholders.append(f"${param_idx}") - update_fields.append(f"spo2 = COALESCE(EXCLUDED.spo2, vitals_baseline.spo2)") - values.append(entry.spo2) + update_fields.append(f"spo2 = EXCLUDED.spo2") + param_values.append(entry.spo2) param_idx += 1 if entry.respiratory_rate is not None: insert_cols.append("respiratory_rate") insert_placeholders.append(f"${param_idx}") - update_fields.append(f"respiratory_rate = COALESCE(EXCLUDED.respiratory_rate, vitals_baseline.respiratory_rate)") - values.append(entry.respiratory_rate) + update_fields.append(f"respiratory_rate = EXCLUDED.respiratory_rate") + param_values.append(entry.respiratory_rate) param_idx += 1 if entry.body_temperature is not None: insert_cols.append("body_temperature") insert_placeholders.append(f"${param_idx}") - update_fields.append(f"body_temperature = COALESCE(EXCLUDED.body_temperature, vitals_baseline.body_temperature)") - values.append(entry.body_temperature) + update_fields.append(f"body_temperature = EXCLUDED.body_temperature") + param_values.append(entry.body_temperature) param_idx += 1 if entry.resting_metabolic_rate is not None: insert_cols.append("resting_metabolic_rate") insert_placeholders.append(f"${param_idx}") - update_fields.append(f"resting_metabolic_rate = COALESCE(EXCLUDED.resting_metabolic_rate, vitals_baseline.resting_metabolic_rate)") - values.append(entry.resting_metabolic_rate) + update_fields.append(f"resting_metabolic_rate = EXCLUDED.resting_metabolic_rate") + param_values.append(entry.resting_metabolic_rate) param_idx += 1 if entry.note: insert_cols.append("note") insert_placeholders.append(f"${param_idx}") - update_fields.append(f"note = COALESCE(EXCLUDED.note, vitals_baseline.note)") - values.append(entry.note) + update_fields.append(f"note = EXCLUDED.note") + param_values.append(entry.note) param_idx += 1 # At least one field must be provided @@ -168,14 +173,24 @@ def create_or_update_baseline( with get_db() as conn: cur = get_cursor(conn) + + # Build complete column list and placeholder list + all_cols = f"profile_id, date, {', '.join(insert_cols)}" + all_placeholders = f"$1, $2, {', '.join(insert_placeholders)}" + query = f""" - INSERT INTO vitals_baseline (profile_id, date, {', '.join(insert_cols)}) - VALUES ($1, $2, {', '.join(insert_placeholders)}) + INSERT INTO vitals_baseline ({all_cols}) + VALUES ({all_placeholders}) ON CONFLICT (profile_id, date) DO UPDATE SET {', '.join(update_fields)}, updated_at = NOW() RETURNING * """ - cur.execute(query, tuple(values)) + + # Debug logging + print(f"[DEBUG] Vitals baseline query: {query}") + print(f"[DEBUG] Param values ({len(param_values)}): {param_values}") + + cur.execute(query, tuple(param_values)) return r2d(cur.fetchone())