From b3cc588293630feccd29d74648862f43f0988542 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 27 Mar 2026 07:40:42 +0100 Subject: [PATCH] fix: make Migration 024 idempotent + add seed data fix script --- backend/fix_seed_goal_types.py | 215 ++++++++++++++++++ backend/migrations/024_goal_type_registry.sql | 24 +- 2 files changed, 231 insertions(+), 8 deletions(-) create mode 100644 backend/fix_seed_goal_types.py diff --git a/backend/fix_seed_goal_types.py b/backend/fix_seed_goal_types.py new file mode 100644 index 0000000..59cc0e6 --- /dev/null +++ b/backend/fix_seed_goal_types.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" +Quick Fix: Insert seed data for goal_type_definitions + +This script ONLY inserts the 8 standard goal types. +Safe to run multiple times (uses ON CONFLICT DO NOTHING). + +Run inside backend container: +docker exec bodytrack-dev-backend-1 python fix_seed_goal_types.py +""" + +import psycopg2 +import os +from psycopg2.extras import RealDictCursor + +# Database connection +DB_HOST = os.getenv('DB_HOST', 'db') +DB_PORT = os.getenv('DB_PORT', '5432') +DB_NAME = os.getenv('DB_NAME', 'bodytrack') +DB_USER = os.getenv('DB_USER', 'bodytrack') +DB_PASS = os.getenv('DB_PASSWORD', '') + +SEED_DATA = [ + { + 'type_key': 'weight', + 'label_de': 'Gewicht', + 'label_en': 'Weight', + 'unit': 'kg', + 'icon': '⚖️', + 'category': 'body', + 'source_table': 'weight_log', + 'source_column': 'weight', + 'aggregation_method': 'latest', + 'description': 'Aktuelles Körpergewicht', + 'is_system': True + }, + { + 'type_key': 'body_fat', + 'label_de': 'Körperfett', + 'label_en': 'Body Fat', + 'unit': '%', + 'icon': '📊', + 'category': 'body', + 'source_table': 'caliper_log', + 'source_column': 'body_fat_pct', + 'aggregation_method': 'latest', + 'description': 'Körperfettanteil aus Caliper-Messung', + 'is_system': True + }, + { + 'type_key': 'lean_mass', + 'label_de': 'Muskelmasse', + 'label_en': 'Lean Mass', + 'unit': 'kg', + 'icon': '💪', + 'category': 'body', + 'calculation_formula': '{"type": "lean_mass", "dependencies": ["weight_log.weight", "caliper_log.body_fat_pct"], "formula": "weight - (weight * body_fat_pct / 100)"}', + 'description': 'Fettfreie Körpermasse (berechnet aus Gewicht und Körperfett)', + 'is_system': True + }, + { + 'type_key': 'vo2max', + 'label_de': 'VO2Max', + 'label_en': 'VO2Max', + 'unit': 'ml/kg/min', + 'icon': '🫁', + 'category': 'recovery', + 'source_table': 'vitals_baseline', + 'source_column': 'vo2_max', + 'aggregation_method': 'latest', + 'description': 'Maximale Sauerstoffaufnahme (geschätzt oder gemessen)', + 'is_system': True + }, + { + 'type_key': 'rhr', + 'label_de': 'Ruhepuls', + 'label_en': 'Resting Heart Rate', + 'unit': 'bpm', + 'icon': '💓', + 'category': 'recovery', + 'source_table': 'vitals_baseline', + 'source_column': 'resting_hr', + 'aggregation_method': 'latest', + 'description': 'Ruhepuls morgens vor dem Aufstehen', + 'is_system': True + }, + { + 'type_key': 'bp', + 'label_de': 'Blutdruck', + 'label_en': 'Blood Pressure', + 'unit': 'mmHg', + 'icon': '❤️', + 'category': 'recovery', + 'source_table': 'blood_pressure_log', + 'source_column': 'systolic', + 'aggregation_method': 'latest', + 'description': 'Blutdruck (aktuell nur systolisch, v2.0: beide Werte)', + 'is_system': True + }, + { + 'type_key': 'strength', + 'label_de': 'Kraft', + 'label_en': 'Strength', + 'unit': 'kg', + 'icon': '🏋️', + 'category': 'activity', + 'description': 'Maximalkraft (Platzhalter, Datenquelle in v2.0)', + 'is_system': True, + 'is_active': False + }, + { + 'type_key': 'flexibility', + 'label_de': 'Beweglichkeit', + 'label_en': 'Flexibility', + 'unit': 'cm', + 'icon': '🤸', + 'category': 'activity', + 'description': 'Beweglichkeit (Platzhalter, Datenquelle in v2.0)', + 'is_system': True, + 'is_active': False + } +] + +def main(): + print("=" * 70) + print("Goal Type Definitions - Seed Data Fix") + print("=" * 70) + + # Connect to database + conn = psycopg2.connect( + host=DB_HOST, + port=DB_PORT, + dbname=DB_NAME, + user=DB_USER, + password=DB_PASS + ) + conn.autocommit = False + cur = conn.cursor(cursor_factory=RealDictCursor) + + try: + # Check current state + cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions") + before_count = cur.fetchone()['count'] + print(f"\nBefore: {before_count} goal types in database") + + # Insert seed data + print(f"\nInserting {len(SEED_DATA)} standard goal types...") + inserted = 0 + skipped = 0 + + for data in SEED_DATA: + columns = list(data.keys()) + values = [data[col] for col in columns] + placeholders = ', '.join(['%s'] * len(values)) + cols_str = ', '.join(columns) + + sql = f""" + INSERT INTO goal_type_definitions ({cols_str}) + VALUES ({placeholders}) + ON CONFLICT (type_key) DO NOTHING + RETURNING id + """ + + cur.execute(sql, values) + result = cur.fetchone() + + if result: + inserted += 1 + print(f" ✓ {data['type_key']}: {data['label_de']}") + else: + skipped += 1 + print(f" - {data['type_key']}: already exists (skipped)") + + conn.commit() + + # Check final state + cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions") + after_count = cur.fetchone()['count'] + + print(f"\nAfter: {after_count} goal types in database") + print(f" Inserted: {inserted}") + print(f" Skipped: {skipped}") + + # Show summary + cur.execute(""" + SELECT type_key, label_de, is_active, is_system + FROM goal_type_definitions + ORDER BY is_system DESC, type_key + """) + + print("\n" + "=" * 70) + print("Current Goal Types:") + print("=" * 70) + print(f"\n{'Type Key':<20} {'Label':<20} {'System':<8} {'Active':<8}") + print("-" * 70) + + for row in cur.fetchall(): + status = "YES" if row['is_system'] else "NO" + active = "YES" if row['is_active'] else "NO" + print(f"{row['type_key']:<20} {row['label_de']:<20} {status:<8} {active:<8}") + + print("\n✅ DONE! Goal types seeded successfully.") + print("\nNext step: Reload frontend to see the changes.") + + except Exception as e: + conn.rollback() + print(f"\n❌ Error: {e}") + import traceback + traceback.print_exc() + finally: + cur.close() + conn.close() + +if __name__ == '__main__': + main() diff --git a/backend/migrations/024_goal_type_registry.sql b/backend/migrations/024_goal_type_registry.sql index a2182e9..2d40d23 100644 --- a/backend/migrations/024_goal_type_registry.sql +++ b/backend/migrations/024_goal_type_registry.sql @@ -63,7 +63,8 @@ INSERT INTO goal_type_definitions ( 'weight', 'Gewicht', 'Weight', 'kg', '⚖️', 'body', 'weight_log', 'weight', 'latest', 'Aktuelles Körpergewicht', true -); +) +ON CONFLICT (type_key) DO NOTHING; -- 2. Body Fat (simple - latest value) INSERT INTO goal_type_definitions ( @@ -74,7 +75,8 @@ INSERT INTO goal_type_definitions ( 'body_fat', 'Körperfett', 'Body Fat', '%', '📊', 'body', 'caliper_log', 'body_fat_pct', 'latest', 'Körperfettanteil aus Caliper-Messung', true -); +) +ON CONFLICT (type_key) DO NOTHING; -- 3. Lean Mass (complex - calculation formula) INSERT INTO goal_type_definitions ( @@ -85,7 +87,8 @@ INSERT INTO goal_type_definitions ( 'lean_mass', 'Muskelmasse', 'Lean Mass', 'kg', '💪', 'body', '{"type": "lean_mass", "dependencies": ["weight_log.weight", "caliper_log.body_fat_pct"], "formula": "weight - (weight * body_fat_pct / 100)"}', 'Fettfreie Körpermasse (berechnet aus Gewicht und Körperfett)', true -); +) +ON CONFLICT (type_key) DO NOTHING; -- 4. VO2 Max (simple - latest value) INSERT INTO goal_type_definitions ( @@ -96,7 +99,8 @@ INSERT INTO goal_type_definitions ( 'vo2max', 'VO2Max', 'VO2Max', 'ml/kg/min', '🫁', 'recovery', 'vitals_baseline', 'vo2_max', 'latest', 'Maximale Sauerstoffaufnahme (geschätzt oder gemessen)', true -); +) +ON CONFLICT (type_key) DO NOTHING; -- 5. Resting Heart Rate (simple - latest value) INSERT INTO goal_type_definitions ( @@ -107,7 +111,8 @@ INSERT INTO goal_type_definitions ( 'rhr', 'Ruhepuls', 'Resting Heart Rate', 'bpm', '💓', 'recovery', 'vitals_baseline', 'resting_hr', 'latest', 'Ruhepuls morgens vor dem Aufstehen', true -); +) +ON CONFLICT (type_key) DO NOTHING; -- 6. Blood Pressure (placeholder - compound goal for v2.0) -- Currently limited to single value, v2.0 will support systolic/diastolic @@ -119,7 +124,8 @@ INSERT INTO goal_type_definitions ( 'bp', 'Blutdruck', 'Blood Pressure', 'mmHg', '❤️', 'recovery', 'blood_pressure_log', 'systolic', 'latest', 'Blutdruck (aktuell nur systolisch, v2.0: beide Werte)', true -); +) +ON CONFLICT (type_key) DO NOTHING; -- 7. Strength (placeholder - no data source yet) INSERT INTO goal_type_definitions ( @@ -128,7 +134,8 @@ INSERT INTO goal_type_definitions ( ) VALUES ( 'strength', 'Kraft', 'Strength', 'kg', '🏋️', 'activity', 'Maximalkraft (Platzhalter, Datenquelle in v2.0)', true, false -); +) +ON CONFLICT (type_key) DO NOTHING; -- 8. Flexibility (placeholder - no data source yet) INSERT INTO goal_type_definitions ( @@ -137,7 +144,8 @@ INSERT INTO goal_type_definitions ( ) VALUES ( 'flexibility', 'Beweglichkeit', 'Flexibility', 'cm', '🤸', 'activity', 'Beweglichkeit (Platzhalter, Datenquelle in v2.0)', true, false -); +) +ON CONFLICT (type_key) DO NOTHING; -- ============================================================================ -- Example: Future custom goal types (commented out, for reference)