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),
|
||||
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)
|
||||
|
||||
# Validation: at least one vital must be provided
|
||||
has_data = any([
|
||||
entry.resting_hr, entry.hrv, entry.blood_pressure_systolic,
|
||||
entry.blood_pressure_diastolic, entry.vo2_max, entry.spo2,
|
||||
entry.respiratory_rate
|
||||
# Validation: at least one baseline vital must be provided
|
||||
has_baseline = any([
|
||||
entry.resting_hr, entry.hrv, entry.vo2_max,
|
||||
entry.spo2, 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:
|
||||
cur = get_cursor(conn)
|
||||
|
||||
# Upsert: insert or update if date already exists
|
||||
# Upsert into vitals_baseline (Migration 015)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO vitals_log (
|
||||
INSERT INTO vitals_baseline (
|
||||
profile_id, date, resting_hr, hrv,
|
||||
blood_pressure_systolic, blood_pressure_diastolic, pulse,
|
||||
vo2_max, spo2, respiratory_rate,
|
||||
irregular_heartbeat, possible_afib,
|
||||
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)
|
||||
DO UPDATE SET
|
||||
resting_hr = COALESCE(EXCLUDED.resting_hr, vitals_log.resting_hr),
|
||||
hrv = COALESCE(EXCLUDED.hrv, vitals_log.hrv),
|
||||
blood_pressure_systolic = COALESCE(EXCLUDED.blood_pressure_systolic, vitals_log.blood_pressure_systolic),
|
||||
blood_pressure_diastolic = COALESCE(EXCLUDED.blood_pressure_diastolic, vitals_log.blood_pressure_diastolic),
|
||||
pulse = COALESCE(EXCLUDED.pulse, vitals_log.pulse),
|
||||
vo2_max = COALESCE(EXCLUDED.vo2_max, vitals_log.vo2_max),
|
||||
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),
|
||||
resting_hr = COALESCE(EXCLUDED.resting_hr, vitals_baseline.resting_hr),
|
||||
hrv = COALESCE(EXCLUDED.hrv, vitals_baseline.hrv),
|
||||
vo2_max = COALESCE(EXCLUDED.vo2_max, vitals_baseline.vo2_max),
|
||||
spo2 = COALESCE(EXCLUDED.spo2, vitals_baseline.spo2),
|
||||
respiratory_rate = COALESCE(EXCLUDED.respiratory_rate, vitals_baseline.respiratory_rate),
|
||||
note = COALESCE(EXCLUDED.note, vitals_baseline.note),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING id, profile_id, date, resting_hr, hrv,
|
||||
blood_pressure_systolic, blood_pressure_diastolic, pulse,
|
||||
vo2_max, spo2, respiratory_rate,
|
||||
irregular_heartbeat, possible_afib,
|
||||
note, source, created_at, updated_at
|
||||
""",
|
||||
(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.irregular_heartbeat, entry.possible_afib,
|
||||
entry.note)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"[VITALS] Upserted vitals for {pid} on {entry.date}")
|
||||
return r2d(row)
|
||||
logger.info(f"[VITALS] Upserted baseline vitals for {pid} on {entry.date}")
|
||||
|
||||
# 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}")
|
||||
|
|
|
|||
|
|
@ -228,7 +228,8 @@ export default function GoalsPage() {
|
|||
unit: formData.unit,
|
||||
target_date: formData.target_date || 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 })
|
||||
|
|
@ -888,7 +889,15 @@ export default function GoalsPage() {
|
|||
</div>
|
||||
) : (
|
||||
<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 style={{
|
||||
fontSize: 11,
|
||||
|
|
@ -905,7 +914,7 @@ export default function GoalsPage() {
|
|||
flexWrap: 'wrap',
|
||||
gap: 6
|
||||
}}>
|
||||
{areas.map(area => {
|
||||
{filteredAreas.map(area => {
|
||||
const isSelected = formData.focus_contributions?.some(
|
||||
fc => fc.focus_area_id === area.id
|
||||
)
|
||||
|
|
@ -960,7 +969,8 @@ export default function GoalsPage() {
|
|||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user