fix: 3 critical bugs in Goals and Vitals
**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:
parent
3116fbbc91
commit
378bf434fc
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user