fix: Apple Health import - German names + duplicate detection
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 13s

Issue 1: Automatic training type mapping didn't work
- Root cause: Only English workout names were mapped
- Solution: Added 20+ German workout type mappings:
  - "Traditionelles Krafttraining" → hypertrophy
  - "Outdoor Spaziergang" → walk
  - "Innenräume Spaziergang" → walk
  - "Matrial Arts" → technique (handles typo)
  - "Cardio Dance" → dance
  - "Geist & Körper" → yoga
  - Plus: Laufen, Gehen, Radfahren, Schwimmen, etc.

Issue 2: Reimporting CSV created duplicates without training types
- Root cause: Import always did INSERT with new UUID, no duplicate check
- Solution: Check if entry exists (profile_id + date + start_time)
  - If exists: UPDATE with new data + training type mapping
  - If new: INSERT as before
- Handles multiple workouts per day (different start times)
- "Skipped" count now includes updated entries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-21 19:16:09 +01:00
parent 4d9ef5b33b
commit a4bd738e6f

View File

@ -122,7 +122,9 @@ def get_training_type_for_apple_health(workout_type: str):
cur = get_cursor(conn) cur = get_cursor(conn)
# Mapping: Apple Health Workout Type → training_type subcategory # Mapping: Apple Health Workout Type → training_type subcategory
# Supports English and German workout names
mapping = { mapping = {
# English
'running': 'running', 'running': 'running',
'walking': 'walk', 'walking': 'walk',
'hiking': 'walk', 'hiking': 'walk',
@ -141,6 +143,33 @@ def get_training_type_for_apple_health(workout_type: str):
'cooldown': 'regeneration', 'cooldown': 'regeneration',
'meditation': 'meditation', 'meditation': 'meditation',
'mindfulness': 'mindfulness', 'mindfulness': 'mindfulness',
# German (Deutsch)
'laufen': 'running',
'gehen': 'walk',
'wandern': 'walk',
'outdoor spaziergang': 'walk',
'innenräume spaziergang': 'walk',
'spaziergang': 'walk',
'radfahren': 'cycling',
'schwimmen': 'swimming',
'traditionelles krafttraining': 'hypertrophy',
'funktionelles krafttraining': 'functional',
'hochintensives intervalltraining': 'hiit',
'yoga': 'yoga',
'kampfsport': 'technique',
'matrial arts': 'technique', # Common typo in Apple Health
'boxen': 'sparring',
'rudern': 'rowing',
'tanzen': 'dance',
'cardio dance': 'dance',
'core training': 'functional',
'flexibilität': 'static',
'abwärmen': 'regeneration',
'cooldown': 'regeneration',
'meditation': 'meditation',
'achtsamkeit': 'mindfulness',
'geist & körper': 'yoga', # Mind & Body → Yoga category
} }
subcategory = mapping.get(workout_type.lower()) subcategory = mapping.get(workout_type.lower())
@ -255,16 +284,54 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional
training_type_id, training_category, training_subcategory = get_training_type_for_apple_health(wtype) training_type_id, training_category, training_subcategory = get_training_type_for_apple_health(wtype)
try: try:
cur.execute("""INSERT INTO activity_log # Check if entry already exists (duplicate detection by date + start_time)
(id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting, cur.execute("""
hr_avg,hr_max,distance_km,source,training_type_id,training_category,training_subcategory,created) SELECT id FROM activity_log
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'apple_health',%s,%s,%s,CURRENT_TIMESTAMP)""", WHERE profile_id = %s AND date = %s AND start_time = %s
(str(uuid.uuid4()),pid,date,start,row.get('End',''),wtype,duration_min, """, (pid, date, start))
kj(row.get('Aktive Energie (kJ)','')),kj(row.get('Ruheeinträge (kJ)','')), existing = cur.fetchone()
tf(row.get('Durchschn. Herzfrequenz (count/min)','')),
tf(row.get('Max. Herzfrequenz (count/min)','')), if existing:
tf(row.get('Distanz (km)','')), # Update existing entry (e.g., to add training type mapping)
training_type_id,training_category,training_subcategory)) cur.execute("""
inserted+=1 UPDATE activity_log
except: skipped+=1 SET end_time = %s,
activity_type = %s,
duration_min = %s,
kcal_active = %s,
kcal_resting = %s,
hr_avg = %s,
hr_max = %s,
distance_km = %s,
training_type_id = %s,
training_category = %s,
training_subcategory = %s
WHERE id = %s
""", (
row.get('End',''), wtype, duration_min,
kj(row.get('Aktive Energie (kJ)','')),
kj(row.get('Ruheeinträge (kJ)','')),
tf(row.get('Durchschn. Herzfrequenz (count/min)','')),
tf(row.get('Max. Herzfrequenz (count/min)','')),
tf(row.get('Distanz (km)','')),
training_type_id, training_category, training_subcategory,
existing['id']
))
skipped += 1 # Count as skipped (not newly inserted)
else:
# Insert new entry
cur.execute("""INSERT INTO activity_log
(id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting,
hr_avg,hr_max,distance_km,source,training_type_id,training_category,training_subcategory,created)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'apple_health',%s,%s,%s,CURRENT_TIMESTAMP)""",
(str(uuid.uuid4()),pid,date,start,row.get('End',''),wtype,duration_min,
kj(row.get('Aktive Energie (kJ)','')),kj(row.get('Ruheeinträge (kJ)','')),
tf(row.get('Durchschn. Herzfrequenz (count/min)','')),
tf(row.get('Max. Herzfrequenz (count/min)','')),
tf(row.get('Distanz (km)','')),
training_type_id,training_category,training_subcategory))
inserted+=1
except Exception as e:
logger.warning(f"Import row failed: {e}")
skipped+=1
return {"inserted":inserted,"skipped":skipped,"message":f"{inserted} Trainings importiert"} return {"inserted":inserted,"skipped":skipped,"message":f"{inserted} Trainings importiert"}