fix: make Migration 024 idempotent + add seed data fix script
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 14s

This commit is contained in:
Lars 2026-03-27 07:40:42 +01:00
parent c9e4b6aa02
commit b3cc588293
2 changed files with 231 additions and 8 deletions

View File

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

View File

@ -63,7 +63,8 @@ INSERT INTO goal_type_definitions (
'weight', 'Gewicht', 'Weight', 'kg', '⚖️', 'body', 'weight', 'Gewicht', 'Weight', 'kg', '⚖️', 'body',
'weight_log', 'weight', 'latest', 'weight_log', 'weight', 'latest',
'Aktuelles Körpergewicht', true 'Aktuelles Körpergewicht', true
); )
ON CONFLICT (type_key) DO NOTHING;
-- 2. Body Fat (simple - latest value) -- 2. Body Fat (simple - latest value)
INSERT INTO goal_type_definitions ( INSERT INTO goal_type_definitions (
@ -74,7 +75,8 @@ INSERT INTO goal_type_definitions (
'body_fat', 'Körperfett', 'Body Fat', '%', '📊', 'body', 'body_fat', 'Körperfett', 'Body Fat', '%', '📊', 'body',
'caliper_log', 'body_fat_pct', 'latest', 'caliper_log', 'body_fat_pct', 'latest',
'Körperfettanteil aus Caliper-Messung', true 'Körperfettanteil aus Caliper-Messung', true
); )
ON CONFLICT (type_key) DO NOTHING;
-- 3. Lean Mass (complex - calculation formula) -- 3. Lean Mass (complex - calculation formula)
INSERT INTO goal_type_definitions ( INSERT INTO goal_type_definitions (
@ -85,7 +87,8 @@ INSERT INTO goal_type_definitions (
'lean_mass', 'Muskelmasse', 'Lean Mass', 'kg', '💪', 'body', '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)"}', '{"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 'Fettfreie Körpermasse (berechnet aus Gewicht und Körperfett)', true
); )
ON CONFLICT (type_key) DO NOTHING;
-- 4. VO2 Max (simple - latest value) -- 4. VO2 Max (simple - latest value)
INSERT INTO goal_type_definitions ( INSERT INTO goal_type_definitions (
@ -96,7 +99,8 @@ INSERT INTO goal_type_definitions (
'vo2max', 'VO2Max', 'VO2Max', 'ml/kg/min', '🫁', 'recovery', 'vo2max', 'VO2Max', 'VO2Max', 'ml/kg/min', '🫁', 'recovery',
'vitals_baseline', 'vo2_max', 'latest', 'vitals_baseline', 'vo2_max', 'latest',
'Maximale Sauerstoffaufnahme (geschätzt oder gemessen)', true 'Maximale Sauerstoffaufnahme (geschätzt oder gemessen)', true
); )
ON CONFLICT (type_key) DO NOTHING;
-- 5. Resting Heart Rate (simple - latest value) -- 5. Resting Heart Rate (simple - latest value)
INSERT INTO goal_type_definitions ( INSERT INTO goal_type_definitions (
@ -107,7 +111,8 @@ INSERT INTO goal_type_definitions (
'rhr', 'Ruhepuls', 'Resting Heart Rate', 'bpm', '💓', 'recovery', 'rhr', 'Ruhepuls', 'Resting Heart Rate', 'bpm', '💓', 'recovery',
'vitals_baseline', 'resting_hr', 'latest', 'vitals_baseline', 'resting_hr', 'latest',
'Ruhepuls morgens vor dem Aufstehen', true 'Ruhepuls morgens vor dem Aufstehen', true
); )
ON CONFLICT (type_key) DO NOTHING;
-- 6. Blood Pressure (placeholder - compound goal for v2.0) -- 6. Blood Pressure (placeholder - compound goal for v2.0)
-- Currently limited to single value, v2.0 will support systolic/diastolic -- 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', 'bp', 'Blutdruck', 'Blood Pressure', 'mmHg', '❤️', 'recovery',
'blood_pressure_log', 'systolic', 'latest', 'blood_pressure_log', 'systolic', 'latest',
'Blutdruck (aktuell nur systolisch, v2.0: beide Werte)', true 'Blutdruck (aktuell nur systolisch, v2.0: beide Werte)', true
); )
ON CONFLICT (type_key) DO NOTHING;
-- 7. Strength (placeholder - no data source yet) -- 7. Strength (placeholder - no data source yet)
INSERT INTO goal_type_definitions ( INSERT INTO goal_type_definitions (
@ -128,7 +134,8 @@ INSERT INTO goal_type_definitions (
) VALUES ( ) VALUES (
'strength', 'Kraft', 'Strength', 'kg', '🏋️', 'activity', 'strength', 'Kraft', 'Strength', 'kg', '🏋️', 'activity',
'Maximalkraft (Platzhalter, Datenquelle in v2.0)', true, false 'Maximalkraft (Platzhalter, Datenquelle in v2.0)', true, false
); )
ON CONFLICT (type_key) DO NOTHING;
-- 8. Flexibility (placeholder - no data source yet) -- 8. Flexibility (placeholder - no data source yet)
INSERT INTO goal_type_definitions ( INSERT INTO goal_type_definitions (
@ -137,7 +144,8 @@ INSERT INTO goal_type_definitions (
) VALUES ( ) VALUES (
'flexibility', 'Beweglichkeit', 'Flexibility', 'cm', '🤸', 'activity', 'flexibility', 'Beweglichkeit', 'Flexibility', 'cm', '🤸', 'activity',
'Beweglichkeit (Platzhalter, Datenquelle in v2.0)', true, false 'Beweglichkeit (Platzhalter, Datenquelle in v2.0)', true, false
); )
ON CONFLICT (type_key) DO NOTHING;
-- ============================================================================ -- ============================================================================
-- Example: Future custom goal types (commented out, for reference) -- Example: Future custom goal types (commented out, for reference)