From 7dcab1d7a305b3746174b731a3c2a135d25fea8d Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 16:35:07 +0100 Subject: [PATCH 1/6] fix: correct import skipped count when manual entries exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Import reported all entries as "updated" even when skipped due to WHERE clause (source != 'manual') Root cause: RETURNING returns NULL when WHERE clause prevents update, but code counted NULL as "updated" instead of "skipped" Fix: - Check if result is None → skipped (WHERE prevented update) - Check if xmax = 0 → inserted (new row) - Otherwise → updated (existing row modified) Affects: - vitals_baseline.py: Apple Health import - blood_pressure.py: Omron import Co-Authored-By: Claude Opus 4.6 --- backend/routers/blood_pressure.py | 5 ++++- backend/routers/vitals_baseline.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/routers/blood_pressure.py b/backend/routers/blood_pressure.py index 92bb28d..b613d3b 100644 --- a/backend/routers/blood_pressure.py +++ b/backend/routers/blood_pressure.py @@ -376,7 +376,10 @@ async def import_omron_csv( )) result = cur.fetchone() - if result and result['inserted']: + if result is None: + # WHERE clause prevented update (manual entry exists) + skipped += 1 + elif result['inserted']: inserted += 1 else: updated += 1 diff --git a/backend/routers/vitals_baseline.py b/backend/routers/vitals_baseline.py index 95b1d5d..dd8f02a 100644 --- a/backend/routers/vitals_baseline.py +++ b/backend/routers/vitals_baseline.py @@ -357,7 +357,10 @@ async def import_apple_health_baseline( )) result = cur.fetchone() - if result and result['inserted']: + if result is None: + # WHERE clause prevented update (manual entry exists) + skipped += 1 + elif result['inserted']: inserted += 1 else: updated += 1 From 6a7b78c3ebd74959a4321a8338b6f3803c10449d Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 16:38:18 +0100 Subject: [PATCH 2/6] debug: add logging to Apple Health import to diagnose skipped rows Logs: - CSV column names from first row - Rows skipped due to missing date - Rows skipped due to no vitals data - Shows which fields were found/missing Helps diagnose CSV format mismatches. Co-Authored-By: Claude Opus 4.6 --- backend/routers/vitals_baseline.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/routers/vitals_baseline.py b/backend/routers/vitals_baseline.py index dd8f02a..8815e64 100644 --- a/backend/routers/vitals_baseline.py +++ b/backend/routers/vitals_baseline.py @@ -311,10 +311,18 @@ async def import_apple_health_baseline( with get_db() as conn: cur = get_cursor(conn) + # Log available columns for debugging + first_row = True + for row in reader: try: + if first_row: + logger.info(f"CSV Columns: {list(row.keys())}") + first_row = False + date = row.get('Start')[:10] if row.get('Start') else None if not date: + logger.warning(f"Skipped row (no date): Start='{row.get('Start')}'") skipped += 1 continue @@ -327,6 +335,7 @@ async def import_apple_health_baseline( # Skip if no baseline vitals if not any([rhr, hrv, vo2, spo2, resp_rate]): + logger.warning(f"Skipped row {date} (no vitals): RHR={rhr}, HRV={hrv}, VO2={vo2}, SpO2={spo2}, RespRate={resp_rate}") skipped += 1 continue From f506a55d7b73acb24c1bed932207086fb9e5e114 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 16:40:49 +0100 Subject: [PATCH 3/6] fix: support German column names in CSV imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Import expected English column names, but German Apple Health/Omron exports use German names with units. Fixed: - Apple Health: Support both English and German column names - "Start" OR "Datum/Uhrzeit" - "Resting Heart Rate" OR "Ruhepuls (count/min)" - "Heart Rate Variability" OR "Herzfrequenzvariabilität (ms)" - "VO2 Max" OR "VO2 max (ml/(kg·min))" - "Oxygen Saturation" OR "Blutsauerstoffsättigung (%)" - "Respiratory Rate" OR "Atemfrequenz (count/min)" - Omron: Support column names with/without units - "Systolisch (mmHg)" OR "Systolisch" - "Diastolisch (mmHg)" OR "Diastolisch" - "Puls (bpm)" OR "Puls" - "Unregelmäßiger Herzschlag festgestellt" OR "Unregelmäßiger Herzschlag" - "Mögliches AFib" OR "Vorhofflimmern" Added debug logging for both imports to show detected columns. Co-Authored-By: Claude Opus 4.6 --- backend/routers/blood_pressure.py | 30 +++++++++++++++++++++++------- backend/routers/vitals_baseline.py | 18 ++++++++++-------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/backend/routers/blood_pressure.py b/backend/routers/blood_pressure.py index b613d3b..e22c790 100644 --- a/backend/routers/blood_pressure.py +++ b/backend/routers/blood_pressure.py @@ -310,8 +310,15 @@ async def import_omron_csv( with get_db() as conn: cur = get_cursor(conn) + # Log available columns for debugging + first_row = True + for row in reader: try: + if first_row: + logger.info(f"Omron CSV Columns: {list(row.keys())}") + first_row = False + # Parse Omron German date format date_str = row.get('Datum', row.get('Date')) time_str = row.get('Zeit', row.get('Time', '08:00')) @@ -325,18 +332,27 @@ async def import_omron_csv( errors += 1 continue - # Extract measurements - systolic = row.get('Systolisch', row.get('Systolic')) - diastolic = row.get('Diastolisch', row.get('Diastolic')) - pulse = row.get('Puls', row.get('Pulse')) + # Extract measurements (support column names with/without units) + systolic = (row.get('Systolisch (mmHg)') or row.get('Systolisch') or + row.get('Systolic (mmHg)') or row.get('Systolic')) + diastolic = (row.get('Diastolisch (mmHg)') or row.get('Diastolisch') or + row.get('Diastolic (mmHg)') or row.get('Diastolic')) + pulse = (row.get('Puls (bpm)') or row.get('Puls') or + row.get('Pulse (bpm)') or row.get('Pulse')) if not systolic or not diastolic: + logger.warning(f"Skipped row {date_str} {time_str}: Missing BP values (sys={systolic}, dia={diastolic})") skipped += 1 continue - # Parse warning flags - irregular = row.get('Unregelmäßiger Herzschlag', row.get('Irregular Heartbeat', '')) - afib = row.get('Vorhofflimmern', row.get('AFib', '')) + # Parse warning flags (support various column names) + irregular = (row.get('Unregelmäßiger Herzschlag festgestellt') or + row.get('Unregelmäßiger Herzschlag') or + row.get('Irregular Heartbeat') or '') + afib = (row.get('Mögliches AFib') or + row.get('Vorhofflimmern') or + row.get('Possible AFib') or + row.get('AFib') or '') irregular_heartbeat = irregular.lower() in ['ja', 'yes', 'true', '1'] possible_afib = afib.lower() in ['ja', 'yes', 'true', '1'] diff --git a/backend/routers/vitals_baseline.py b/backend/routers/vitals_baseline.py index 8815e64..affa695 100644 --- a/backend/routers/vitals_baseline.py +++ b/backend/routers/vitals_baseline.py @@ -320,18 +320,20 @@ async def import_apple_health_baseline( logger.info(f"CSV Columns: {list(row.keys())}") first_row = False - date = row.get('Start')[:10] if row.get('Start') else None + # Support both English and German column names + date_raw = row.get('Start') or row.get('Datum/Uhrzeit') + date = date_raw[:10] if date_raw else None if not date: - logger.warning(f"Skipped row (no date): Start='{row.get('Start')}'") + logger.warning(f"Skipped row (no date): Start='{row.get('Start')}', Datum/Uhrzeit='{row.get('Datum/Uhrzeit')}'") skipped += 1 continue - # Extract baseline vitals from Apple Health export - rhr = row.get('Resting Heart Rate') - hrv = row.get('Heart Rate Variability') - vo2 = row.get('VO2 Max') - spo2 = row.get('Oxygen Saturation') - resp_rate = row.get('Respiratory Rate') + # Extract baseline vitals (support English + German column names) + rhr = row.get('Resting Heart Rate') or row.get('Ruhepuls (count/min)') + hrv = row.get('Heart Rate Variability') or row.get('Herzfrequenzvariabilität (ms)') + vo2 = row.get('VO2 Max') or row.get('VO2 max (ml/(kg·min))') + spo2 = row.get('Oxygen Saturation') or row.get('Blutsauerstoffsättigung (%)') + resp_rate = row.get('Respiratory Rate') or row.get('Atemfrequenz (count/min)') # Skip if no baseline vitals if not any([rhr, hrv, vo2, spo2, resp_rate]): From 4b024e6d0f84310fc9ba80f3b55c55101000324a Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 16:44:16 +0100 Subject: [PATCH 4/6] debug: add detailed error logging with traceback for import failures --- backend/routers/vitals_baseline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/routers/vitals_baseline.py b/backend/routers/vitals_baseline.py index affa695..88b58ff 100644 --- a/backend/routers/vitals_baseline.py +++ b/backend/routers/vitals_baseline.py @@ -377,7 +377,10 @@ async def import_apple_health_baseline( updated += 1 except Exception as e: - logger.error(f"Error importing row: {e}") + import traceback + error_detail = f"Row error: {str(e)}\nTraceback: {traceback.format_exc()}" + logger.error(error_detail) + print(error_detail) # Force output to Docker logs errors += 1 return { From 6b64cf31c4d6fe73e898a790696259292820c087 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 16:47:36 +0100 Subject: [PATCH 5/6] fix: return error details in import response for debugging Problem: Errors during import were logged but not visible to user. Changes: - Backend: Collect error messages and return in response (first 10 errors) - Frontend: Display error details in import result box - UI: Red background when errors > 0, shows detailed error messages Now users can see exactly which rows failed and why. Co-Authored-By: Claude Opus 4.6 --- backend/routers/vitals_baseline.py | 10 ++++++---- frontend/src/pages/VitalsPage.jsx | 12 ++++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/routers/vitals_baseline.py b/backend/routers/vitals_baseline.py index 88b58ff..ca17db9 100644 --- a/backend/routers/vitals_baseline.py +++ b/backend/routers/vitals_baseline.py @@ -307,6 +307,7 @@ async def import_apple_health_baseline( updated = 0 skipped = 0 errors = 0 + error_details = [] # Collect error messages with get_db() as conn: cur = get_cursor(conn) @@ -378,14 +379,15 @@ async def import_apple_health_baseline( except Exception as e: import traceback - error_detail = f"Row error: {str(e)}\nTraceback: {traceback.format_exc()}" - logger.error(error_detail) - print(error_detail) # Force output to Docker logs + error_msg = f"Row {date if 'date' in locals() else 'unknown'}: {str(e)}" + error_details.append(error_msg) + logger.error(f"{error_msg}\n{traceback.format_exc()}") errors += 1 return { "inserted": inserted, "updated": updated, "skipped": skipped, - "errors": errors + "errors": errors, + "error_details": error_details[:10] # Return first 10 errors } diff --git a/frontend/src/pages/VitalsPage.jsx b/frontend/src/pages/VitalsPage.jsx index d6fda55..6e94708 100644 --- a/frontend/src/pages/VitalsPage.jsx +++ b/frontend/src/pages/VitalsPage.jsx @@ -947,10 +947,18 @@ function ImportTab({ onImportComplete }) { {error &&
{error}
} {result && ( -
- Import erfolgreich ({result.type === 'omron' ? 'Omron' : 'Apple Health'}):
+
0 ? '#FCEBEB' : '#E8F7F0', border: `1px solid ${result.errors > 0 ? '#D85A30' : '#1D9E75'}`, borderRadius: 8, fontSize: 13, color: result.errors > 0 ? '#D85A30' : '#085041', marginBottom: 12 }}> + Import {result.errors > 0 ? 'mit Fehlern' : 'erfolgreich'} ({result.type === 'omron' ? 'Omron' : 'Apple Health'}):
{result.inserted} neu · {result.updated} aktualisiert · {result.skipped} übersprungen {result.errors > 0 && <> · {result.errors} Fehler} + {result.error_details && result.error_details.length > 0 && ( +
+ Fehler-Details: + {result.error_details.map((err, i) => ( +
• {err}
+ ))} +
+ )}
)} From 6f035e3706476f6f7f77ef4a1feac3b204253427 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 16:50:08 +0100 Subject: [PATCH 6/6] fix: handle decimal values in Apple Health vitals import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Import failed with "invalid literal for int() with base 10: '37.95'" because Apple Health exports HRV and other vitals with decimal values. Root cause: Code used int() directly on string values with decimals. Fix: - Added safe_int(): parses decimals as float first, then rounds to int - Added safe_float(): robust float parsing with error handling - Applied to all vital value parsing: RHR, HRV, VO2 Max, SpO2, resp rate Example: '37.95' → float(37.95) → int(38) ✓ Co-Authored-By: Claude Opus 4.6 --- backend/routers/vitals_baseline.py | 31 +++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/backend/routers/vitals_baseline.py b/backend/routers/vitals_baseline.py index ca17db9..cd460ee 100644 --- a/backend/routers/vitals_baseline.py +++ b/backend/routers/vitals_baseline.py @@ -290,6 +290,27 @@ def get_baseline_stats( # Import: Apple Health CSV # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +def safe_int(value): + """Safely parse string to int, handling decimals.""" + if not value or value == '': + return None + try: + # If it has a decimal point, parse as float first then round to int + if '.' in str(value): + return int(float(value)) + return int(value) + except (ValueError, TypeError): + return None + +def safe_float(value): + """Safely parse string to float.""" + if not value or value == '': + return None + try: + return float(value) + except (ValueError, TypeError): + return None + @router.post("/import/apple-health") async def import_apple_health_baseline( file: UploadFile = File(...), @@ -361,11 +382,11 @@ async def import_apple_health_baseline( RETURNING (xmax = 0) AS inserted """, ( pid, date, - int(rhr) if rhr else None, - int(hrv) if hrv else None, - float(vo2) if vo2 else None, - int(spo2) if spo2 else None, - float(resp_rate) if resp_rate else None + safe_int(rhr), + safe_int(hrv), + safe_float(vo2), + safe_int(spo2), + safe_float(resp_rate) )) result = cur.fetchone()