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),
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}")

View File

@ -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>
)}