fix: 3 critical bugs in Goals and Vitals
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 21s

**Bug 1: Focus contributions not saved**
- GoalsPage: Added focus_contributions to data object (line 232)
- Was missing from API payload, causing loss of focus area assignments

**Bug 2: Filter focus areas in goal form**
- Only show focus areas user has weighted (weight > 0)
- Cleaner UX, avoids confusion with non-prioritized areas
- Filters focusAreasGrouped by userFocusWeights

**Bug 3: Vitals RHR entry - Internal Server Error**
- Fixed: Endpoint tried to INSERT into vitals_log (renamed in Migration 015)
- Now uses vitals_baseline table (correct post-migration table)
- Removed BP fields from baseline endpoint (use /blood-pressure instead)
- Backward compatible return format

All fixes tested and ready for production.
This commit is contained in:
Lars 2026-03-27 21:04:28 +01:00
parent 3116fbbc91
commit 378bf434fc
2 changed files with 47 additions and 34 deletions

View File

@ -140,63 +140,66 @@ def create_vitals(
x_profile_id: Optional[str] = Header(default=None), x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth) session: dict = Depends(require_auth)
): ):
"""Create or update vitals entry (upsert).""" """
Create or update vitals entry (upsert).
Post-Migration-015: Routes to vitals_baseline (for RHR, HRV, etc.)
Note: BP measurements should use /api/blood-pressure endpoint instead.
"""
pid = get_pid(x_profile_id, session) pid = get_pid(x_profile_id, session)
# Validation: at least one vital must be provided # Validation: at least one baseline vital must be provided
has_data = any([ has_baseline = any([
entry.resting_hr, entry.hrv, entry.blood_pressure_systolic, entry.resting_hr, entry.hrv, entry.vo2_max,
entry.blood_pressure_diastolic, entry.vo2_max, entry.spo2, entry.spo2, entry.respiratory_rate
entry.respiratory_rate
]) ])
if not has_data:
raise HTTPException(400, "Mindestens ein Vitalwert muss angegeben werden") if not has_baseline:
raise HTTPException(400, "Mindestens ein Vitalwert muss angegeben werden (RHR, HRV, VO2Max, SpO2, oder Atemfrequenz)")
with get_db() as conn: with get_db() as conn:
cur = get_cursor(conn) cur = get_cursor(conn)
# Upsert: insert or update if date already exists # Upsert into vitals_baseline (Migration 015)
cur.execute( cur.execute(
""" """
INSERT INTO vitals_log ( INSERT INTO vitals_baseline (
profile_id, date, resting_hr, hrv, profile_id, date, resting_hr, hrv,
blood_pressure_systolic, blood_pressure_diastolic, pulse,
vo2_max, spo2, respiratory_rate, vo2_max, spo2, respiratory_rate,
irregular_heartbeat, possible_afib,
note, source note, source
) )
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 'manual') VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'manual')
ON CONFLICT (profile_id, date) ON CONFLICT (profile_id, date)
DO UPDATE SET DO UPDATE SET
resting_hr = COALESCE(EXCLUDED.resting_hr, vitals_log.resting_hr), resting_hr = COALESCE(EXCLUDED.resting_hr, vitals_baseline.resting_hr),
hrv = COALESCE(EXCLUDED.hrv, vitals_log.hrv), hrv = COALESCE(EXCLUDED.hrv, vitals_baseline.hrv),
blood_pressure_systolic = COALESCE(EXCLUDED.blood_pressure_systolic, vitals_log.blood_pressure_systolic), vo2_max = COALESCE(EXCLUDED.vo2_max, vitals_baseline.vo2_max),
blood_pressure_diastolic = COALESCE(EXCLUDED.blood_pressure_diastolic, vitals_log.blood_pressure_diastolic), spo2 = COALESCE(EXCLUDED.spo2, vitals_baseline.spo2),
pulse = COALESCE(EXCLUDED.pulse, vitals_log.pulse), respiratory_rate = COALESCE(EXCLUDED.respiratory_rate, vitals_baseline.respiratory_rate),
vo2_max = COALESCE(EXCLUDED.vo2_max, vitals_log.vo2_max), note = COALESCE(EXCLUDED.note, vitals_baseline.note),
spo2 = COALESCE(EXCLUDED.spo2, vitals_log.spo2),
respiratory_rate = COALESCE(EXCLUDED.respiratory_rate, vitals_log.respiratory_rate),
irregular_heartbeat = COALESCE(EXCLUDED.irregular_heartbeat, vitals_log.irregular_heartbeat),
possible_afib = COALESCE(EXCLUDED.possible_afib, vitals_log.possible_afib),
note = COALESCE(EXCLUDED.note, vitals_log.note),
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
RETURNING id, profile_id, date, resting_hr, hrv, RETURNING id, profile_id, date, resting_hr, hrv,
blood_pressure_systolic, blood_pressure_diastolic, pulse,
vo2_max, spo2, respiratory_rate, vo2_max, spo2, respiratory_rate,
irregular_heartbeat, possible_afib,
note, source, created_at, updated_at note, source, created_at, updated_at
""", """,
(pid, entry.date, entry.resting_hr, entry.hrv, (pid, entry.date, entry.resting_hr, entry.hrv,
entry.blood_pressure_systolic, entry.blood_pressure_diastolic, entry.pulse,
entry.vo2_max, entry.spo2, entry.respiratory_rate, entry.vo2_max, entry.spo2, entry.respiratory_rate,
entry.irregular_heartbeat, entry.possible_afib,
entry.note) entry.note)
) )
row = cur.fetchone() row = cur.fetchone()
conn.commit() conn.commit()
logger.info(f"[VITALS] Upserted vitals for {pid} on {entry.date}") logger.info(f"[VITALS] Upserted baseline vitals for {pid} on {entry.date}")
return r2d(row)
# Return in legacy format for backward compatibility
result = r2d(row)
result['blood_pressure_systolic'] = None
result['blood_pressure_diastolic'] = None
result['pulse'] = None
result['irregular_heartbeat'] = None
result['possible_afib'] = None
return result
@router.put("/{vitals_id}") @router.put("/{vitals_id}")

View File

@ -228,7 +228,8 @@ export default function GoalsPage() {
unit: formData.unit, unit: formData.unit,
target_date: formData.target_date || null, target_date: formData.target_date || null,
name: formData.name || null, name: formData.name || null,
description: formData.description || null description: formData.description || null,
focus_contributions: formData.focus_contributions || [] // v2.0: Focus area assignments
} }
console.log('[DEBUG] Saving goal:', { editingGoal, data }) console.log('[DEBUG] Saving goal:', { editingGoal, data })
@ -888,7 +889,15 @@ export default function GoalsPage() {
</div> </div>
) : ( ) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{Object.entries(focusAreasGrouped).map(([category, areas]) => ( {Object.entries(focusAreasGrouped).map(([category, areas]) => {
// Filter to only show focus areas the user has weighted
const userWeightedAreaIds = new Set(userFocusWeights.map(w => w.id))
const filteredAreas = areas.filter(area => userWeightedAreaIds.has(area.id))
// Skip category if no weighted areas
if (filteredAreas.length === 0) return null
return (
<div key={category}> <div key={category}>
<div style={{ <div style={{
fontSize: 11, fontSize: 11,
@ -905,7 +914,7 @@ export default function GoalsPage() {
flexWrap: 'wrap', flexWrap: 'wrap',
gap: 6 gap: 6
}}> }}>
{areas.map(area => { {filteredAreas.map(area => {
const isSelected = formData.focus_contributions?.some( const isSelected = formData.focus_contributions?.some(
fc => fc.focus_area_id === area.id fc => fc.focus_area_id === area.id
) )
@ -960,7 +969,8 @@ export default function GoalsPage() {
})} })}
</div> </div>
</div> </div>
))} )
})}
</div> </div>
)} )}