diff --git a/CLAUDE.md b/CLAUDE.md index 04004e4..886f3a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,7 +76,46 @@ frontend/src/ └── technical/ # MEMBERSHIP_SYSTEM.md ``` -## Aktuelle Version: v9e (Issue #28, #47 Complete) 🚀 Ready for Production 26.03.2026 +## Aktuelle Version: v9e+ (Phase 1 Goal System Fixes) 🎯 Ready for Phase 0b - 27.03.2026 + +### Letzte Updates (27.03.2026 - Phase 1 Complete) 🆕 +- ✅ **Custom Goals Page (Capture/Eigene Ziele):** + - Neue Seite für tägliche Werterfassung individueller Ziele + - Dedizierte UI für custom goals (ohne automatische Datenquelle) + - Verhindert Verwechslung mit automatischem Tracking (Gewicht, Aktivität, etc.) + - Clean UX: Zielauswahl → Schnellerfassung → Verlauf (letzte 5 Einträge) + - Navigation: Capture Hub + direkter Link +- ✅ **UX-Improvements Progress Modal:** + - Volle Breite Eingabefelder, Labels als Überschriften, linksbündiger Text + - Progress-Button nur bei custom goals sichtbar (source_table IS NULL) +- ✅ **Architektur-Klarstellung:** + - Analysis/Goals → Strategisch (Ziele definieren, Prioritäten setzen) + - Capture/Custom Goals → Taktisch (tägliche Ist-Wert-Erfassung) + - History → Auswertung (Zielerreichungs-Analysen) + +### Updates (27.03.2026 - Phase 1 Fixes) +- ✅ **Abstraction Layer:** goal_utils.py für zukunftssichere Phase 0b Platzhalter +- ✅ **Primary Goal Toggle Fix:** is_primary Update funktioniert korrekt +- ✅ **Lean Mass Berechnung:** Magermasse current_value wird berechnet +- ✅ **VO2Max Fix:** Spaltenname vo2_max (statt vo2max) korrigiert +- ✅ **Keine Doppelarbeit:** Phase 0b Platzhalter (120+) müssen bei v2.0 nicht umgeschrieben werden + +### Phase 0a Completion (26.03.2026) 🎯 +- ✅ **Phase 0a: Minimal Goal System:** Strategic + Tactical Layers implementiert +- ✅ **Migration 022:** goal_mode, goals, training_phases, fitness_tests tables +- ✅ **Backend Router:** goals.py mit vollständigem CRUD (490 Zeilen) +- ✅ **Frontend:** GoalsPage mit mobile-friendly Design (570 Zeilen) +- ✅ **Navigation:** Goals Preview (Dashboard) + Ziele Button (Analysis) +- ✅ **Basis geschaffen:** Für 120+ goal-aware Platzhalter (Phase 0b) +- ✅ **Dokumentation:** issue-50, NEXT_STEPS_2026-03-26.md, GOALS_SYSTEM_UNIFIED_ANALYSIS.md + +### Frühere Updates (26.03.2026 - Vormittag) +- ✅ **circ_summary erweitert:** Best-of-Each Strategie mit Altersangaben +- ✅ **Stage Outputs Fix:** Debug-Info für Experten-Modus +- ✅ **Collapsible JSON:** Stage-Rohdaten aufklappbar +- ✅ **Gitea #28 geschlossen:** AI-Prompts Flexibilisierung +- ✅ **Gitea #44 geschlossen:** Analysen löschen behoben +- ✅ **Gitea #47 erstellt:** Wertetabelle Optimierung ### Implementiert ✅ - Login (E-Mail + bcrypt), Auth-Middleware alle Endpoints, Rate Limiting @@ -208,7 +247,8 @@ frontend/src/ 📚 Details: `.claude/docs/technical/MEMBERSHIP_SYSTEM.md` · `.claude/docs/architecture/FEATURE_ENFORCEMENT.md` -### Issue #28: Unified Prompt System ✅ (Completed 26.03.2026) +### Feature: Unified Prompt System ✅ (Completed 26.03.2026) +> **Gitea:** Issue #28 (AI-Prompts Flexibilisierung) - CLOSED **AI-Prompts Flexibilisierung - Komplett überarbeitet:** @@ -316,14 +356,15 @@ frontend/src/ 📚 Details: `.claude/docs/functional/AI_PROMPTS.md` **Related Gitea Issues:** -- #28: Unified Prompt System - ✅ CLOSED (26.03.2026) -- #43: Enhanced Debug UI - 🔲 OPEN (Future enhancement) -- #44: BUG - Analysen löschen - 🔲 OPEN (High priority) -- #45: KI Prompt-Optimierer - 🔲 OPEN (Future feature) -- #46: KI Prompt-Ersteller - 🔲 OPEN (Future feature) -- #47: Value Table - ✅ CLOSED (26.03.2026) +- Gitea #28: AI-Prompts Flexibilisierung - ✅ CLOSED (26.03.2026) +- Gitea #42, #43: Enhanced Debug UI - 🔲 OPEN (Future enhancement) +- Gitea #44: BUG - Analysen löschen - ✅ CLOSED (26.03.2026) +- Gitea #45: KI Prompt-Optimierer - 🔲 OPEN (Future feature) +- Gitea #46: KI Prompt-Ersteller - 🔲 OPEN (Future feature) +- Gitea #47: Wertetabelle Optimierung - 🔲 OPEN (Refinement, siehe docs/issues/issue-50) -### Issue #47: Comprehensive Value Table ✅ (Completed 26.03.2026) +### Feature: Comprehensive Value Table ✅ (Completed 26.03.2026) +> **Gitea:** Basis-Implementierung abgeschlossen. Issue #47 (Wertetabelle Optimierung) für Refinement offen. **AI-Analyse Transparenz - Vollständige Platzhalter-Anzeige:** @@ -371,6 +412,65 @@ frontend/src/ 📚 Details: `.claude/docs/functional/AI_PROMPTS.md` +### Phase 0a: Minimal Goal System ✅ (Completed 26.03.2026) +> **Gitea:** Issue #50 (zu erstellen) - COMPLETED +> **Dokumentation:** `docs/issues/issue-50-phase-0a-goal-system.md`, `docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md` + +**Zwei-Ebenen-Ziel-Architektur für goal-aware KI-Analysen:** + +- ✅ **Strategic Layer (Goal Modes):** + - `goal_mode` in profiles table (weight_loss, strength, endurance, recomposition, health) + - Bestimmt Score-Gewichtung für alle KI-Analysen + - UI: 5 Goal Mode Cards mit Icons und Beschreibungen + +- ✅ **Tactical Layer (Concrete Goals):** + - `goals` table mit vollständigem Progress-Tracking + - 8 Goal-Typen: weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr + - Auto-calculated progress percentage + - Linear projection für target_date + - Primary/Secondary goal concept + - UI: Goal CRUD mit Fortschrittsbalken, mobile-friendly + +- ✅ **Training Phases Framework:** + - `training_phases` table (Auto-Detection vorbereitet) + - 5 Phase-Typen: calorie_deficit, calorie_surplus, deload, maintenance, periodization + - Status-Flow: suggested → accepted → active → completed + - Confidence scoring für KI-basierte Erkennung + +- ✅ **Fitness Tests:** + - `fitness_tests` table für standardisierte Tests + - 8 Test-Typen: Cooper, Step Test, Pushups, Plank, VO2Max, Strength (Squat/Bench) + - Norm-Kategorisierung vorbereitet + +**Backend:** +- Migration 022: goal_mode, goals, training_phases, fitness_tests tables +- Router: `routers/goals.py` (490 Zeilen) - vollständiges CRUD +- API Endpoints: `/api/goals/*` (mode, list, create, update, delete, phases, tests) + +**Frontend:** +- GoalsPage: `frontend/src/pages/GoalsPage.jsx` (570 Zeilen) +- Mobile-friendly Design (full-width inputs, labels above) +- Navigation: Dashboard (Goals Preview Card) + Analysis (🎯 Ziele Button) +- api.js: 15+ neue Goal-Funktionen + +**Commits:** +- `337667f` - feat: Phase 0a - Minimal Goal System +- `906a3b7` - fix: Migration 022 tracking +- `75f0a5d` - refactor: mobile-friendly design +- `5be52bc` - feat: goals navigation + UX + +**Basis für Phase 0b:** +- Foundation für 120+ goal-aware Platzhalter +- Score-Berechnungen abhängig von goal_mode +- Intelligente Coaching-Funktionen +- Automatische Trainingsphasen-Erkennung + +**Nächste Schritte:** +- Option A: Issue #49 - Prompt Page Assignment (6-8h, Quick Win) +- Option B: Phase 0b - Goal-Aware Placeholders (16-20h, Strategic) + +📚 Details: `docs/NEXT_STEPS_2026-03-26.md` + ## Feature-Roadmap > 📋 **Detaillierte Roadmap:** `.claude/docs/ROADMAP.md` (Phasen 0-3, Timeline, Abhängigkeiten) diff --git a/backend/check_migration_024.py b/backend/check_migration_024.py new file mode 100644 index 0000000..c31cf1d --- /dev/null +++ b/backend/check_migration_024.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +Quick diagnostic: Check Migration 024 state + +Run this inside the backend container: +docker exec bodytrack-dev-backend-1 python check_migration_024.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', '') + +def main(): + print("=" * 70) + print("Migration 024 Diagnostic") + print("=" * 70) + + # Connect to database + conn = psycopg2.connect( + host=DB_HOST, + port=DB_PORT, + dbname=DB_NAME, + user=DB_USER, + password=DB_PASS + ) + cur = conn.cursor(cursor_factory=RealDictCursor) + + # 1. Check if table exists + print("\n1. Checking if goal_type_definitions table exists...") + cur.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'goal_type_definitions' + ) + """) + exists = cur.fetchone()['exists'] + print(f" ✓ Table exists: {exists}") + + if not exists: + print("\n❌ TABLE DOES NOT EXIST - Migration 024 did not run!") + print("\nRECOMMENDED ACTION:") + print(" 1. Restart backend container: docker restart bodytrack-dev-backend-1") + print(" 2. Check logs: docker logs bodytrack-dev-backend-1 | grep 'Migration'") + cur.close() + conn.close() + return + + # 2. Check row count + print("\n2. Checking row count...") + cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions") + count = cur.fetchone()['count'] + print(f" Row count: {count}") + + if count == 0: + print("\n❌ TABLE IS EMPTY - Seed data was not inserted!") + print("\nPOSSIBLE CAUSES:") + print(" - INSERT statements failed (constraint violation?)") + print(" - Migration ran partially") + print("\nRECOMMENDED ACTION:") + print(" Run the seed statements manually (see below)") + else: + print(f" ✓ Table has {count} entries") + + # 3. Show all entries + print("\n3. Current goal type definitions:") + cur.execute(""" + SELECT type_key, label_de, unit, is_system, is_active, created_at + FROM goal_type_definitions + ORDER BY is_system DESC, type_key + """) + + entries = cur.fetchall() + if entries: + print(f"\n {'Type Key':<20} {'Label':<20} {'Unit':<10} {'System':<8} {'Active':<8}") + print(" " + "-" * 70) + for row in entries: + status = "SYSTEM" if row['is_system'] else "CUSTOM" + active = "YES" if row['is_active'] else "NO" + print(f" {row['type_key']:<20} {row['label_de']:<20} {row['unit']:<10} {status:<8} {active:<8}") + else: + print(" (empty)") + + # 4. Check schema_migrations + print("\n4. Checking schema_migrations tracking...") + cur.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'schema_migrations' + ) + """) + sm_exists = cur.fetchone()['exists'] + + if sm_exists: + cur.execute(""" + SELECT filename, executed_at + FROM schema_migrations + WHERE filename = '024_goal_type_registry.sql' + """) + tracked = cur.fetchone() + if tracked: + print(f" ✓ Migration 024 is tracked (executed: {tracked['executed_at']})") + else: + print(" ❌ Migration 024 is NOT tracked in schema_migrations") + else: + print(" ⚠️ schema_migrations table does not exist") + + # 5. Check for errors + print("\n5. Potential issues:") + issues = [] + + if count == 0: + issues.append("No seed data - INSERTs failed") + + if count > 0 and count < 6: + issues.append(f"Only {count} types (expected 8) - partial seed") + + cur.execute(""" + SELECT COUNT(*) as inactive_count + FROM goal_type_definitions + WHERE is_active = false + """) + inactive = cur.fetchone()['inactive_count'] + if inactive > 2: + issues.append(f"{inactive} inactive types (expected 2)") + + if not issues: + print(" ✓ No issues detected") + else: + for issue in issues: + print(f" ❌ {issue}") + + # 6. Test query that frontend uses + print("\n6. Testing frontend query (WHERE is_active = true)...") + cur.execute(""" + SELECT COUNT(*) as active_count + FROM goal_type_definitions + WHERE is_active = true + """) + active_count = cur.fetchone()['active_count'] + print(f" Active types returned: {active_count}") + + if active_count == 0: + print(" ❌ This is why frontend shows empty list!") + + print("\n" + "=" * 70) + print("SUMMARY") + print("=" * 70) + + if count == 0: + print("\n🔴 PROBLEM: Table exists but has no data") + print("\nQUICK FIX: Run these SQL commands manually:") + print("\n```sql") + print("-- Connect to database:") + print("docker exec -it bodytrack-dev-db-1 psql -U bodytrack -d bodytrack") + print("\n-- Then paste migration content:") + print("-- (copy from backend/migrations/024_goal_type_registry.sql)") + print("-- Skip CREATE TABLE (already exists), run INSERT statements only") + print("```") + elif active_count >= 6: + print("\n🟢 EVERYTHING LOOKS GOOD") + print(f" {active_count} active goal types available") + print("\nIf frontend still shows error, check:") + print(" 1. Backend logs: docker logs bodytrack-dev-backend-1 -f") + print(" 2. Network tab in browser DevTools") + print(" 3. API endpoint: curl -H 'X-Auth-Token: YOUR_TOKEN' http://localhost:8099/api/goals/goal-types") + else: + print(f"\n🟡 PARTIAL DATA: {active_count} active types (expected 6)") + print(" Some INSERTs might have failed") + + cur.close() + conn.close() + +if __name__ == '__main__': + main() 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/goal_utils.py b/backend/goal_utils.py new file mode 100644 index 0000000..09bd9cc --- /dev/null +++ b/backend/goal_utils.py @@ -0,0 +1,504 @@ +""" +Goal Utilities - Abstraction Layer for Focus Weights & Universal Value Fetcher + +This module provides: +1. Abstraction layer between goal modes and focus weights (Phase 1) +2. Universal value fetcher for dynamic goal types (Phase 1.5) + +Version History: +- V1 (Phase 1): Maps goal_mode to predefined weights +- V1.5 (Phase 1.5): Universal value fetcher for DB-registry goal types +- V2 (future): Reads from focus_areas table with custom user weights + +Part of Phase 1 + Phase 1.5: Flexible Goal System +""" + +from typing import Dict, Optional, Any +from datetime import date, timedelta +from decimal import Decimal +import json +from db import get_cursor + + +def get_focus_weights(conn, profile_id: str) -> Dict[str, float]: + """ + Get focus area weights for a profile. + + V2 (Goal System v2.0): Reads from focus_areas table with custom user weights. + Falls back to goal_mode mapping if focus_areas not set. + + Args: + conn: Database connection + profile_id: User's profile ID + + Returns: + Dict with focus weights (sum = 1.0): + { + 'weight_loss': 0.3, # Fat loss priority + 'muscle_gain': 0.2, # Muscle gain priority + 'strength': 0.25, # Strength training priority + 'endurance': 0.25, # Cardio/endurance priority + 'flexibility': 0.0, # Mobility priority + 'health': 0.0 # General health maintenance + } + + Example Usage in Phase 0b: + weights = get_focus_weights(conn, profile_id) + + # Score calculation considers user's focus + overall_score = ( + body_score * weights['weight_loss'] + + strength_score * weights['strength'] + + cardio_score * weights['endurance'] + ) + """ + cur = get_cursor(conn) + + # V2: Try to fetch from focus_areas table + cur.execute(""" + SELECT weight_loss_pct, muscle_gain_pct, strength_pct, + endurance_pct, flexibility_pct, health_pct + FROM focus_areas + WHERE profile_id = %s AND active = true + LIMIT 1 + """, (profile_id,)) + + row = cur.fetchone() + + if row: + # Convert percentages to weights (0-1 range) + return { + 'weight_loss': row['weight_loss_pct'] / 100.0, + 'muscle_gain': row['muscle_gain_pct'] / 100.0, + 'strength': row['strength_pct'] / 100.0, + 'endurance': row['endurance_pct'] / 100.0, + 'flexibility': row['flexibility_pct'] / 100.0, + 'health': row['health_pct'] / 100.0 + } + + # V1 Fallback: Use goal_mode if focus_areas not set + cur.execute( + "SELECT goal_mode FROM profiles WHERE id = %s", + (profile_id,) + ) + row = cur.fetchone() + + if not row: + # Ultimate fallback: balanced health focus + return { + 'weight_loss': 0.0, + 'muscle_gain': 0.0, + 'strength': 0.10, + 'endurance': 0.20, + 'flexibility': 0.15, + 'health': 0.55 + } + + goal_mode = row['goal_mode'] + + if not goal_mode: + return { + 'weight_loss': 0.0, + 'muscle_gain': 0.0, + 'strength': 0.10, + 'endurance': 0.20, + 'flexibility': 0.15, + 'health': 0.55 + } + + # V1: Predefined weight mappings per goal_mode (fallback) + WEIGHT_MAPPINGS = { + 'weight_loss': { + 'weight_loss': 0.60, + 'endurance': 0.20, + 'muscle_gain': 0.0, + 'strength': 0.10, + 'flexibility': 0.05, + 'health': 0.05 + }, + 'strength': { + 'strength': 0.50, + 'muscle_gain': 0.40, + 'endurance': 0.10, + 'weight_loss': 0.0, + 'flexibility': 0.0, + 'health': 0.0 + }, + 'endurance': { + 'endurance': 0.70, + 'health': 0.20, + 'flexibility': 0.10, + 'weight_loss': 0.0, + 'muscle_gain': 0.0, + 'strength': 0.0 + }, + 'recomposition': { + 'weight_loss': 0.30, + 'muscle_gain': 0.30, + 'strength': 0.25, + 'endurance': 0.10, + 'flexibility': 0.05, + 'health': 0.0 + }, + 'health': { + 'health': 0.50, + 'endurance': 0.20, + 'flexibility': 0.15, + 'strength': 0.10, + 'weight_loss': 0.05, + 'muscle_gain': 0.0 + } + } + + return WEIGHT_MAPPINGS.get(goal_mode, WEIGHT_MAPPINGS['health']) + + +def get_primary_focus(conn, profile_id: str) -> str: + """ + Get the primary focus area for a profile. + + Returns the focus area with the highest weight. + Useful for UI labels and simple decision logic. + + Args: + conn: Database connection + profile_id: User's profile ID + + Returns: + Primary focus area name (e.g., 'weight_loss', 'strength') + """ + weights = get_focus_weights(conn, profile_id) + return max(weights.items(), key=lambda x: x[1])[0] + + +def get_focus_description(focus_area: str) -> str: + """ + Get human-readable description for a focus area. + + Args: + focus_area: Focus area key (e.g., 'weight_loss') + + Returns: + German description for UI display + """ + descriptions = { + 'weight_loss': 'Gewichtsreduktion & Fettabbau', + 'muscle_gain': 'Muskelaufbau & Hypertrophie', + 'strength': 'Kraftsteigerung & Performance', + 'endurance': 'Ausdauer & aerobe Kapazität', + 'flexibility': 'Beweglichkeit & Mobilität', + 'health': 'Allgemeine Gesundheit & Erhaltung' + } + return descriptions.get(focus_area, focus_area) + + +# ============================================================================ +# Phase 1.5: Universal Value Fetcher for Dynamic Goal Types +# ============================================================================ + +def get_goal_type_config(conn, type_key: str) -> Optional[Dict[str, Any]]: + """ + Get goal type configuration from database registry. + + Args: + conn: Database connection + type_key: Goal type key (e.g., 'weight', 'meditation_minutes') + + Returns: + Dict with config or None if not found/inactive + """ + cur = get_cursor(conn) + + cur.execute(""" + SELECT type_key, source_table, source_column, aggregation_method, + calculation_formula, filter_conditions, label_de, unit, icon, category + FROM goal_type_definitions + WHERE type_key = %s AND is_active = true + LIMIT 1 + """, (type_key,)) + + return cur.fetchone() + + +def get_current_value_for_goal(conn, profile_id: str, goal_type: str) -> Optional[float]: + """ + Universal value fetcher for any goal type. + + Reads configuration from goal_type_definitions table and executes + appropriate query based on aggregation_method or calculation_formula. + + Args: + conn: Database connection + profile_id: User's profile ID + goal_type: Goal type key (e.g., 'weight', 'meditation_minutes') + + Returns: + Current value as float or None if not available + """ + config = get_goal_type_config(conn, goal_type) + + if not config: + print(f"[WARNING] Goal type '{goal_type}' not found or inactive") + return None + + # Complex calculation (e.g., lean_mass) + if config['calculation_formula']: + return _execute_calculation_formula(conn, profile_id, config['calculation_formula']) + + # Simple aggregation + return _fetch_by_aggregation_method( + conn, + profile_id, + config['source_table'], + config['source_column'], + config['aggregation_method'], + config.get('filter_conditions') + ) + + +def _fetch_by_aggregation_method( + conn, + profile_id: str, + table: str, + column: str, + method: str, + filter_conditions: Optional[Any] = None +) -> Optional[float]: + """ + Fetch value using specified aggregation method. + + Supported methods: + - latest: Most recent value + - avg_7d: 7-day average + - avg_30d: 30-day average + - sum_30d: 30-day sum + - count_7d: Count of entries in last 7 days + - count_30d: Count of entries in last 30 days + - min_30d: Minimum value in last 30 days + - max_30d: Maximum value in last 30 days + + Args: + filter_conditions: Optional JSON filters (e.g., {"training_type": "strength"}) + """ + # Guard: source_table/column required for simple aggregation + if not table or not column: + print(f"[WARNING] Missing source_table or source_column for aggregation") + return None + + # Table-specific date column mapping (some tables use different column names) + DATE_COLUMN_MAP = { + 'blood_pressure_log': 'measured_at', + 'activity_log': 'date', + 'weight_log': 'date', + 'circumference_log': 'date', + 'caliper_log': 'date', + 'nutrition_log': 'date', + 'sleep_log': 'date', + 'vitals_baseline': 'date', + 'rest_days': 'date', + 'fitness_tests': 'test_date' + } + date_col = DATE_COLUMN_MAP.get(table, 'date') + + # Build filter SQL from JSON conditions + filter_sql = "" + filter_params = [] + + if filter_conditions: + try: + if isinstance(filter_conditions, str): + filters = json.loads(filter_conditions) + else: + filters = filter_conditions + + for filter_col, filter_val in filters.items(): + if isinstance(filter_val, list): + # IN clause for multiple values + placeholders = ', '.join(['%s'] * len(filter_val)) + filter_sql += f" AND {filter_col} IN ({placeholders})" + filter_params.extend(filter_val) + else: + # Single value equality + filter_sql += f" AND {filter_col} = %s" + filter_params.append(filter_val) + except (json.JSONDecodeError, TypeError, AttributeError) as e: + print(f"[WARNING] Invalid filter_conditions: {e}, ignoring filters") + + cur = get_cursor(conn) + + try: + if method == 'latest': + params = [profile_id] + filter_params + cur.execute(f""" + SELECT {column} FROM {table} + WHERE profile_id = %s AND {column} IS NOT NULL{filter_sql} + ORDER BY {date_col} DESC LIMIT 1 + """, params) + row = cur.fetchone() + return float(row[column]) if row else None + + elif method == 'avg_7d': + days_ago = date.today() - timedelta(days=7) + params = [profile_id, days_ago] + filter_params + cur.execute(f""" + SELECT AVG({column}) as avg_value FROM {table} + WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql} + """, params) + row = cur.fetchone() + return float(row['avg_value']) if row and row['avg_value'] is not None else None + + elif method == 'avg_30d': + days_ago = date.today() - timedelta(days=30) + params = [profile_id, days_ago] + filter_params + cur.execute(f""" + SELECT AVG({column}) as avg_value FROM {table} + WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql} + """, params) + row = cur.fetchone() + return float(row['avg_value']) if row and row['avg_value'] is not None else None + + elif method == 'sum_30d': + days_ago = date.today() - timedelta(days=30) + params = [profile_id, days_ago] + filter_params + cur.execute(f""" + SELECT SUM({column}) as sum_value FROM {table} + WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql} + """, params) + row = cur.fetchone() + return float(row['sum_value']) if row and row['sum_value'] is not None else None + + elif method == 'count_7d': + days_ago = date.today() - timedelta(days=7) + params = [profile_id, days_ago] + filter_params + cur.execute(f""" + SELECT COUNT(*) as count_value FROM {table} + WHERE profile_id = %s AND {date_col} >= %s{filter_sql} + """, params) + row = cur.fetchone() + return float(row['count_value']) if row else 0.0 + + elif method == 'count_30d': + days_ago = date.today() - timedelta(days=30) + params = [profile_id, days_ago] + filter_params + cur.execute(f""" + SELECT COUNT(*) as count_value FROM {table} + WHERE profile_id = %s AND {date_col} >= %s{filter_sql} + """, params) + row = cur.fetchone() + return float(row['count_value']) if row else 0.0 + + elif method == 'min_30d': + days_ago = date.today() - timedelta(days=30) + params = [profile_id, days_ago] + filter_params + cur.execute(f""" + SELECT MIN({column}) as min_value FROM {table} + WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql} + """, params) + row = cur.fetchone() + return float(row['min_value']) if row and row['min_value'] is not None else None + + elif method == 'max_30d': + days_ago = date.today() - timedelta(days=30) + params = [profile_id, days_ago] + filter_params + cur.execute(f""" + SELECT MAX({column}) as max_value FROM {table} + WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql} + """, params) + row = cur.fetchone() + return float(row['max_value']) if row and row['max_value'] is not None else None + + else: + print(f"[WARNING] Unknown aggregation method: {method}") + return None + + except Exception as e: + print(f"[ERROR] Failed to fetch value from {table}.{column} using {method}: {e}") + return None + + +def _execute_calculation_formula(conn, profile_id: str, formula_json: str) -> Optional[float]: + """ + Execute complex calculation formula. + + Currently supports: + - lean_mass: weight - (weight * body_fat_pct / 100) + + Future: Parse JSON formula and execute dynamically. + + Args: + conn: Database connection + profile_id: User's profile ID + formula_json: JSON string with calculation config + + Returns: + Calculated value or None + """ + try: + formula = json.loads(formula_json) + calc_type = formula.get('type') + + if calc_type == 'lean_mass': + # Get dependencies + cur = get_cursor(conn) + + cur.execute(""" + SELECT weight FROM weight_log + WHERE profile_id = %s + ORDER BY date DESC LIMIT 1 + """, (profile_id,)) + weight_row = cur.fetchone() + + cur.execute(""" + SELECT body_fat_pct FROM caliper_log + WHERE profile_id = %s + ORDER BY date DESC LIMIT 1 + """, (profile_id,)) + bf_row = cur.fetchone() + + if weight_row and bf_row: + weight = float(weight_row['weight']) + bf_pct = float(bf_row['body_fat_pct']) + lean_mass = weight - (weight * bf_pct / 100.0) + return round(lean_mass, 2) + + return None + + else: + print(f"[WARNING] Unknown calculation type: {calc_type}") + return None + + except (json.JSONDecodeError, KeyError, ValueError, TypeError) as e: + print(f"[ERROR] Formula execution failed: {e}, formula={formula_json}") + return None + + +# Future V2 Implementation (commented out for reference): +""" +def get_focus_weights_v2(conn, profile_id: str) -> Dict[str, float]: + '''V2: Read from focus_areas table with custom user weights''' + cur = get_cursor(conn) + + cur.execute(''' + SELECT weight_loss_pct, muscle_gain_pct, endurance_pct, + strength_pct, flexibility_pct, health_pct + FROM focus_areas + WHERE profile_id = %s AND active = true + LIMIT 1 + ''', (profile_id,)) + + row = cur.fetchone() + + if not row: + # Fallback to V1 behavior + return get_focus_weights(conn, profile_id) + + # Convert percentages to weights (0-1 range) + return { + 'weight_loss': row['weight_loss_pct'] / 100.0, + 'muscle_gain': row['muscle_gain_pct'] / 100.0, + 'endurance': row['endurance_pct'] / 100.0, + 'strength': row['strength_pct'] / 100.0, + 'flexibility': row['flexibility_pct'] / 100.0, + 'health': row['health_pct'] / 100.0 + } +""" diff --git a/backend/main.py b/backend/main.py index 63faff3..738f07e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -23,6 +23,7 @@ from routers import user_restrictions, access_grants, training_types, admin_trai from routers import admin_activity_mappings, sleep, rest_days from routers import vitals_baseline, blood_pressure # v9d Phase 2d Refactored from routers import evaluation # v9d/v9e Training Type Profiles (#15) +from routers import goals # v9e Goal System (Strategic + Tactical) # ── App Configuration ───────────────────────────────────────────────────────── DATA_DIR = Path(os.getenv("DATA_DIR", "./data")) @@ -97,6 +98,7 @@ app.include_router(rest_days.router) # /api/rest-days/* (v9d Phase 2a app.include_router(vitals_baseline.router) # /api/vitals/baseline/* (v9d Phase 2d Refactored) app.include_router(blood_pressure.router) # /api/blood-pressure/* (v9d Phase 2d Refactored) app.include_router(evaluation.router) # /api/evaluation/* (v9d/v9e Training Profiles #15) +app.include_router(goals.router) # /api/goals/* (v9e Goal System Strategic + Tactical) # ── Health Check ────────────────────────────────────────────────────────────── @app.get("/") diff --git a/backend/migrations/022_goal_system.sql b/backend/migrations/022_goal_system.sql new file mode 100644 index 0000000..925433d --- /dev/null +++ b/backend/migrations/022_goal_system.sql @@ -0,0 +1,135 @@ +-- Migration 022: Goal System (Strategic + Tactical) +-- Date: 2026-03-26 +-- Purpose: Two-level goal architecture for AI-driven coaching + +-- ============================================================================ +-- STRATEGIC LAYER: Goal Modes +-- ============================================================================ + +-- Add goal_mode to profiles (strategic training direction) +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS goal_mode VARCHAR(50) DEFAULT 'health'; + +COMMENT ON COLUMN profiles.goal_mode IS + 'Strategic goal mode: weight_loss, strength, endurance, recomposition, health. + Determines score weights and interpretation context for all analyses.'; + +-- ============================================================================ +-- TACTICAL LAYER: Concrete Goal Targets +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS goals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Goal Classification + goal_type VARCHAR(50) NOT NULL, -- weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr + is_primary BOOLEAN DEFAULT false, + status VARCHAR(20) DEFAULT 'active', -- draft, active, reached, abandoned, expired + + -- Target Values + target_value DECIMAL(10,2), + current_value DECIMAL(10,2), + start_value DECIMAL(10,2), + unit VARCHAR(20), -- kg, %, ml/kg/min, bpm, mmHg, cm, reps + + -- Timeline + start_date DATE DEFAULT CURRENT_DATE, + target_date DATE, + reached_date DATE, + + -- Metadata + name VARCHAR(100), -- e.g., "Sommerfigur 2026" + description TEXT, + + -- Progress Tracking + progress_pct DECIMAL(5,2), -- Auto-calculated: (current - start) / (target - start) * 100 + projection_date DATE, -- Prognose wann Ziel erreicht wird + on_track BOOLEAN, -- true wenn Prognose <= target_date + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_goals_profile ON goals(profile_id); +CREATE INDEX IF NOT EXISTS idx_goals_status ON goals(profile_id, status); +CREATE INDEX IF NOT EXISTS idx_goals_primary ON goals(profile_id, is_primary) WHERE is_primary = true; + +COMMENT ON TABLE goals IS 'Concrete user goals (tactical targets)'; +COMMENT ON COLUMN goals.goal_type IS 'Type of goal: weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr'; +COMMENT ON COLUMN goals.is_primary IS 'Primary goal gets highest priority in scoring and charts'; +COMMENT ON COLUMN goals.status IS 'draft = not yet started, active = in progress, reached = successfully completed, abandoned = given up, expired = deadline passed'; +COMMENT ON COLUMN goals.progress_pct IS 'Percentage progress: (current_value - start_value) / (target_value - start_value) * 100'; +COMMENT ON COLUMN goals.projection_date IS 'Projected date when goal will be reached based on current trend'; +COMMENT ON COLUMN goals.on_track IS 'true if projection_date <= target_date (goal reachable on time)'; + +-- ============================================================================ +-- TRAINING PHASES (Auto-Detection) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS training_phases ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Phase Classification + phase_type VARCHAR(50) NOT NULL, -- calorie_deficit, calorie_surplus, deload, maintenance, periodization + detected_automatically BOOLEAN DEFAULT false, + confidence_score DECIMAL(3,2), -- 0.00 - 1.00 (Wie sicher ist die Erkennung?) + status VARCHAR(20) DEFAULT 'suggested', -- suggested, accepted, active, completed, rejected + + -- Timeframe + start_date DATE NOT NULL, + end_date DATE, + duration_days INT, + + -- Detection Criteria (JSONB für Flexibilität) + detection_params JSONB, -- { "avg_calories": 1800, "weight_trend": -0.3, ... } + + -- User Notes + notes TEXT, + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_training_phases_profile ON training_phases(profile_id); +CREATE INDEX IF NOT EXISTS idx_training_phases_status ON training_phases(profile_id, status); +CREATE INDEX IF NOT EXISTS idx_training_phases_dates ON training_phases(profile_id, start_date, end_date); + +COMMENT ON TABLE training_phases IS 'Training phases detected from data patterns or manually defined'; +COMMENT ON COLUMN training_phases.phase_type IS 'calorie_deficit, calorie_surplus, deload, maintenance, periodization'; +COMMENT ON COLUMN training_phases.detected_automatically IS 'true if AI detected this phase from data patterns'; +COMMENT ON COLUMN training_phases.confidence_score IS 'AI confidence in detection (0.0 - 1.0)'; +COMMENT ON COLUMN training_phases.status IS 'suggested = AI proposed, accepted = user confirmed, active = currently running, completed = finished, rejected = user dismissed'; +COMMENT ON COLUMN training_phases.detection_params IS 'JSON with detection criteria: avg_calories, weight_trend, activity_volume, etc.'; + +-- ============================================================================ +-- FITNESS TESTS (Standardized Performance Tests) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS fitness_tests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Test Type + test_type VARCHAR(50) NOT NULL, -- cooper_12min, step_test, pushups_max, plank_max, flexibility_sit_reach, vo2max_est, strength_1rm_squat, strength_1rm_bench + result_value DECIMAL(10,2) NOT NULL, + result_unit VARCHAR(20) NOT NULL, -- meters, bpm, reps, seconds, cm, ml/kg/min, kg + + -- Test Metadata + test_date DATE NOT NULL, + test_conditions TEXT, -- Optional: Notizen zu Bedingungen + norm_category VARCHAR(30), -- sehr gut, gut, durchschnitt, unterdurchschnitt, schlecht + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_fitness_tests_profile ON fitness_tests(profile_id); +CREATE INDEX IF NOT EXISTS idx_fitness_tests_type ON fitness_tests(profile_id, test_type); +CREATE INDEX IF NOT EXISTS idx_fitness_tests_date ON fitness_tests(profile_id, test_date); + +COMMENT ON TABLE fitness_tests IS 'Standardized fitness tests (Cooper, step test, strength tests, etc.)'; +COMMENT ON COLUMN fitness_tests.test_type IS 'cooper_12min, step_test, pushups_max, plank_max, flexibility_sit_reach, vo2max_est, strength_1rm_squat, strength_1rm_bench'; +COMMENT ON COLUMN fitness_tests.norm_category IS 'Performance category based on age/gender norms'; diff --git a/backend/migrations/024_goal_type_registry.sql b/backend/migrations/024_goal_type_registry.sql new file mode 100644 index 0000000..704fc95 --- /dev/null +++ b/backend/migrations/024_goal_type_registry.sql @@ -0,0 +1,185 @@ +-- Migration 024: Goal Type Registry (Flexible Goal System) +-- Date: 2026-03-27 +-- Purpose: Enable dynamic goal types without code changes + +-- ============================================================================ +-- Goal Type Definitions +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS goal_type_definitions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + + -- Unique identifier (used in code) + type_key VARCHAR(50) UNIQUE NOT NULL, + + -- Display metadata + label_de VARCHAR(100) NOT NULL, + label_en VARCHAR(100), + unit VARCHAR(20) NOT NULL, + icon VARCHAR(10), + category VARCHAR(50), -- body, mind, activity, nutrition, recovery, custom + + -- Data source configuration + source_table VARCHAR(50), -- Which table to query + source_column VARCHAR(50), -- Which column to fetch + aggregation_method VARCHAR(20), -- How to aggregate: latest, avg_7d, avg_30d, sum_30d, count_7d, count_30d, min_30d, max_30d + + -- Complex calculations (optional) + -- For types like lean_mass that need custom logic + -- JSON format: {"type": "formula", "dependencies": ["weight", "body_fat"], "expression": "..."} + calculation_formula TEXT, + + -- Metadata + description TEXT, + is_active BOOLEAN DEFAULT true, + is_system BOOLEAN DEFAULT false, -- System types cannot be deleted + + -- Audit + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_goal_type_definitions_active ON goal_type_definitions(is_active) WHERE is_active = true; +CREATE INDEX IF NOT EXISTS idx_goal_type_definitions_category ON goal_type_definitions(category); + +COMMENT ON TABLE goal_type_definitions IS 'Registry of available goal types - allows dynamic goal creation without code changes'; +COMMENT ON COLUMN goal_type_definitions.type_key IS 'Unique key used in code (e.g., weight, meditation_minutes)'; +COMMENT ON COLUMN goal_type_definitions.aggregation_method IS 'latest = most recent value, avg_7d = 7-day average, count_7d = count in last 7 days, etc.'; +COMMENT ON COLUMN goal_type_definitions.calculation_formula IS 'JSON for complex calculations like lean_mass = weight - (weight * bf_pct / 100)'; +COMMENT ON COLUMN goal_type_definitions.is_system IS 'System types are protected from deletion (core functionality)'; + +-- ============================================================================ +-- Seed Data: Migrate existing 8 goal types +-- ============================================================================ + +-- 1. Weight (simple - latest value) +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + '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 ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + '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 ( + type_key, label_de, label_en, unit, icon, category, + calculation_formula, + description, is_system +) VALUES ( + '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 ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + '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 ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + '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 +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + '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 ( + type_key, label_de, label_en, unit, icon, category, + description, is_system, is_active +) 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 ( + type_key, label_de, label_en, unit, icon, category, + description, is_system, is_active +) 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) +-- ============================================================================ + +/* +-- Meditation Minutes (avg last 7 days) +INSERT INTO goal_type_definitions ( + type_key, label_de, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + 'meditation_minutes', 'Meditation', 'min/Tag', '🧘', 'mind', + 'meditation_log', 'duration_minutes', 'avg_7d', + 'Durchschnittliche Meditationsdauer pro Tag (7 Tage)', false +); + +-- Training Frequency (count last 7 days) +INSERT INTO goal_type_definitions ( + type_key, label_de, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + 'training_frequency', 'Trainingshäufigkeit', 'x/Woche', '📅', 'activity', + 'activity_log', 'id', 'count_7d', + 'Anzahl Trainingseinheiten pro Woche', false +); + +-- Sleep Quality (avg last 7 days) +INSERT INTO goal_type_definitions ( + type_key, label_de, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + 'sleep_quality', 'Schlafqualität', '%', '💤', 'recovery', + 'sleep_log', 'quality_score', 'avg_7d', + 'Durchschnittliche Schlafqualität (Deep+REM Anteil)', false +); +*/ diff --git a/backend/migrations/025_cleanup_goal_type_definitions.sql b/backend/migrations/025_cleanup_goal_type_definitions.sql new file mode 100644 index 0000000..db97771 --- /dev/null +++ b/backend/migrations/025_cleanup_goal_type_definitions.sql @@ -0,0 +1,103 @@ +-- Migration 025: Cleanup goal_type_definitions +-- Date: 2026-03-27 +-- Purpose: Remove problematic FK columns and ensure seed data + +-- Remove created_by/updated_by columns if they exist +-- (May have been created by failed Migration 024) +ALTER TABLE goal_type_definitions DROP COLUMN IF EXISTS created_by; +ALTER TABLE goal_type_definitions DROP COLUMN IF EXISTS updated_by; + +-- Re-insert seed data (ON CONFLICT ensures idempotency) +-- This fixes cases where Migration 024 created table but failed to seed + +-- 1. Weight +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + 'weight', 'Gewicht', 'Weight', 'kg', '⚖️', 'body', + 'weight_log', 'weight', 'latest', + 'Aktuelles Körpergewicht', true +) +ON CONFLICT (type_key) DO NOTHING; + +-- 2. Body Fat +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + '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 +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + calculation_formula, + description, is_system +) VALUES ( + '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 +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + '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 +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + '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 +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + description, is_system +) VALUES ( + '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 (inactive placeholder) +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + description, is_system, is_active +) VALUES ( + 'strength', 'Kraft', 'Strength', 'kg', '🏋️', 'activity', + 'Maximalkraft (Platzhalter, Datenquelle in v2.0)', true, false +) +ON CONFLICT (type_key) DO NOTHING; + +-- 8. Flexibility (inactive placeholder) +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + description, is_system, is_active +) VALUES ( + 'flexibility', 'Beweglichkeit', 'Flexibility', 'cm', '🤸', 'activity', + 'Beweglichkeit (Platzhalter, Datenquelle in v2.0)', true, false +) +ON CONFLICT (type_key) DO NOTHING; diff --git a/backend/migrations/026_goal_type_filters.sql b/backend/migrations/026_goal_type_filters.sql new file mode 100644 index 0000000..46a70fa --- /dev/null +++ b/backend/migrations/026_goal_type_filters.sql @@ -0,0 +1,40 @@ +-- Migration 026: Goal Type Filters +-- Date: 2026-03-27 +-- Purpose: Enable filtered counting/aggregation (e.g., count only strength training) + +-- Add filter_conditions column for flexible filtering +ALTER TABLE goal_type_definitions +ADD COLUMN IF NOT EXISTS filter_conditions JSONB; + +COMMENT ON COLUMN goal_type_definitions.filter_conditions IS +'Optional filter conditions as JSON. Example: {"training_type": "strength"} to count only strength training sessions. +Supports any column in the source table. Format: {"column_name": "value"} or {"column_name": ["value1", "value2"]} for IN clause.'; + +-- Example usage (commented out): +/* +-- Count only strength training sessions per week +INSERT INTO goal_type_definitions ( + type_key, label_de, unit, icon, category, + source_table, source_column, aggregation_method, + filter_conditions, + description, is_system +) VALUES ( + 'strength_frequency', 'Krafttraining Häufigkeit', 'x/Woche', '🏋️', 'activity', + 'activity_log', 'id', 'count_7d', + '{"training_type": "strength"}', + 'Anzahl Krafttraining-Einheiten pro Woche', false +) ON CONFLICT (type_key) DO NOTHING; + +-- Count only cardio sessions per week +INSERT INTO goal_type_definitions ( + type_key, label_de, unit, icon, category, + source_table, source_column, aggregation_method, + filter_conditions, + description, is_system +) VALUES ( + 'cardio_frequency', 'Cardio Häufigkeit', 'x/Woche', '🏃', 'activity', + 'activity_log', 'id', 'count_7d', + '{"training_type": "cardio"}', + 'Anzahl Cardio-Einheiten pro Woche', false +) ON CONFLICT (type_key) DO NOTHING; +*/ diff --git a/backend/migrations/027_focus_areas_system.sql b/backend/migrations/027_focus_areas_system.sql new file mode 100644 index 0000000..4e7fac8 --- /dev/null +++ b/backend/migrations/027_focus_areas_system.sql @@ -0,0 +1,125 @@ +-- Migration 027: Focus Areas System (Goal System v2.0) +-- Date: 2026-03-27 +-- Purpose: Replace single primary goal with weighted multi-goal system + +-- ============================================================================ +-- Focus Areas Table +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS focus_areas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Six focus dimensions (percentages, sum = 100) + weight_loss_pct INTEGER DEFAULT 0 CHECK (weight_loss_pct >= 0 AND weight_loss_pct <= 100), + muscle_gain_pct INTEGER DEFAULT 0 CHECK (muscle_gain_pct >= 0 AND muscle_gain_pct <= 100), + strength_pct INTEGER DEFAULT 0 CHECK (strength_pct >= 0 AND strength_pct <= 100), + endurance_pct INTEGER DEFAULT 0 CHECK (endurance_pct >= 0 AND endurance_pct <= 100), + flexibility_pct INTEGER DEFAULT 0 CHECK (flexibility_pct >= 0 AND flexibility_pct <= 100), + health_pct INTEGER DEFAULT 0 CHECK (health_pct >= 0 AND health_pct <= 100), + + -- Status + active BOOLEAN DEFAULT true, + + -- Audit + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + -- Constraints + CONSTRAINT sum_equals_100 CHECK ( + weight_loss_pct + muscle_gain_pct + strength_pct + + endurance_pct + flexibility_pct + health_pct = 100 + ) +); + +-- Only one active focus_areas per profile +CREATE UNIQUE INDEX IF NOT EXISTS idx_focus_areas_profile_active +ON focus_areas(profile_id) WHERE active = true; + +COMMENT ON TABLE focus_areas IS 'User-defined focus area weights (replaces simple goal_mode). Enables multi-goal prioritization with custom percentages.'; +COMMENT ON COLUMN focus_areas.weight_loss_pct IS 'Focus on fat loss (0-100%)'; +COMMENT ON COLUMN focus_areas.muscle_gain_pct IS 'Focus on muscle growth (0-100%)'; +COMMENT ON COLUMN focus_areas.strength_pct IS 'Focus on strength gains (0-100%)'; +COMMENT ON COLUMN focus_areas.endurance_pct IS 'Focus on aerobic capacity (0-100%)'; +COMMENT ON COLUMN focus_areas.flexibility_pct IS 'Focus on mobility/flexibility (0-100%)'; +COMMENT ON COLUMN focus_areas.health_pct IS 'Focus on general health (0-100%)'; + +-- ============================================================================ +-- Migrate existing goal_mode to focus_areas +-- ============================================================================ + +-- For each profile with a goal_mode, create initial focus_areas +INSERT INTO focus_areas ( + profile_id, + weight_loss_pct, muscle_gain_pct, strength_pct, + endurance_pct, flexibility_pct, health_pct +) +SELECT + id AS profile_id, + CASE goal_mode + WHEN 'weight_loss' THEN 60 + WHEN 'recomposition' THEN 30 + WHEN 'health' THEN 5 + ELSE 0 + END AS weight_loss_pct, + + CASE goal_mode + WHEN 'strength' THEN 40 ELSE 0 + END + + CASE goal_mode + WHEN 'recomposition' THEN 30 ELSE 0 + END AS muscle_gain_pct, + + CASE goal_mode + WHEN 'strength' THEN 50 + WHEN 'recomposition' THEN 25 + WHEN 'weight_loss' THEN 10 + WHEN 'health' THEN 10 + ELSE 0 + END AS strength_pct, + + CASE goal_mode + WHEN 'endurance' THEN 70 + WHEN 'recomposition' THEN 10 + WHEN 'weight_loss' THEN 20 + WHEN 'health' THEN 20 + ELSE 0 + END AS endurance_pct, + + CASE goal_mode + WHEN 'endurance' THEN 10 ELSE 0 + END + + CASE goal_mode + WHEN 'health' THEN 15 ELSE 0 + END + + CASE goal_mode + WHEN 'recomposition' THEN 5 ELSE 0 + END + + CASE goal_mode + WHEN 'weight_loss' THEN 5 ELSE 0 + END AS flexibility_pct, + + CASE goal_mode + WHEN 'health' THEN 50 + WHEN 'endurance' THEN 20 + WHEN 'strength' THEN 10 + WHEN 'weight_loss' THEN 5 + ELSE 0 + END AS health_pct +FROM profiles +WHERE goal_mode IS NOT NULL +ON CONFLICT DO NOTHING; + +-- For profiles without goal_mode, use balanced health focus +INSERT INTO focus_areas ( + profile_id, + weight_loss_pct, muscle_gain_pct, strength_pct, + endurance_pct, flexibility_pct, health_pct +) +SELECT + id AS profile_id, + 0, 0, 10, 20, 15, 55 +FROM profiles +WHERE goal_mode IS NULL + AND id NOT IN (SELECT profile_id FROM focus_areas WHERE active = true) +ON CONFLICT DO NOTHING; diff --git a/backend/migrations/028_goal_categories_priorities.sql b/backend/migrations/028_goal_categories_priorities.sql new file mode 100644 index 0000000..43a2a7f --- /dev/null +++ b/backend/migrations/028_goal_categories_priorities.sql @@ -0,0 +1,57 @@ +-- Migration 028: Goal Categories and Priorities +-- Date: 2026-03-27 +-- Purpose: Multi-dimensional goal priorities (one primary goal per category) + +-- ============================================================================ +-- Add category and priority columns +-- ============================================================================ + +ALTER TABLE goals +ADD COLUMN category VARCHAR(50), +ADD COLUMN priority INTEGER DEFAULT 2 CHECK (priority >= 1 AND priority <= 3); + +COMMENT ON COLUMN goals.category IS 'Goal category: body, training, nutrition, recovery, health, other'; +COMMENT ON COLUMN goals.priority IS 'Priority level: 1=high, 2=medium, 3=low'; + +-- ============================================================================ +-- Migrate existing goals to categories based on goal_type +-- ============================================================================ + +UPDATE goals SET category = CASE + -- Body composition goals + WHEN goal_type IN ('weight', 'body_fat', 'lean_mass') THEN 'body' + + -- Training goals + WHEN goal_type IN ('strength', 'flexibility', 'training_frequency') THEN 'training' + + -- Health/cardio goals + WHEN goal_type IN ('vo2max', 'rhr', 'bp', 'hrv') THEN 'health' + + -- Recovery goals + WHEN goal_type IN ('sleep_quality', 'sleep_duration', 'rest_days') THEN 'recovery' + + -- Nutrition goals + WHEN goal_type IN ('calories', 'protein', 'healthy_eating') THEN 'nutrition' + + -- Default + ELSE 'other' +END +WHERE category IS NULL; + +-- ============================================================================ +-- Set priority based on is_primary +-- ============================================================================ + +UPDATE goals SET priority = CASE + WHEN is_primary = true THEN 1 -- Primary goals get priority 1 + ELSE 2 -- Others get priority 2 (medium) +END; + +-- ============================================================================ +-- Create index for category-based queries +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_goals_category_priority +ON goals(profile_id, category, priority); + +COMMENT ON INDEX idx_goals_category_priority IS 'Fast lookup for category-grouped goals sorted by priority'; diff --git a/backend/migrations/029_fix_missing_goal_types.sql b/backend/migrations/029_fix_missing_goal_types.sql new file mode 100644 index 0000000..b40f5e8 --- /dev/null +++ b/backend/migrations/029_fix_missing_goal_types.sql @@ -0,0 +1,74 @@ +-- Migration 029: Fix Missing Goal Types (flexibility, strength) +-- Date: 2026-03-27 +-- Purpose: Ensure flexibility and strength goal types are active and properly configured + +-- These types were created earlier but are inactive or misconfigured +-- This migration fixes them without breaking if they don't exist + +-- ============================================================================ +-- Upsert flexibility goal type +-- ============================================================================ + +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + calculation_formula, filter_conditions, description, is_active +) VALUES ( + 'flexibility', + 'Beweglichkeit', + 'Flexibility', + 'cm', + '🤸', + 'training', + NULL, -- No automatic data source + NULL, + 'latest', + NULL, + NULL, + 'Beweglichkeit und Mobilität - manuelle Erfassung', + true +) +ON CONFLICT (type_key) +DO UPDATE SET + label_de = 'Beweglichkeit', + label_en = 'Flexibility', + unit = 'cm', + icon = '🤸', + category = 'training', + is_active = true, + description = 'Beweglichkeit und Mobilität - manuelle Erfassung'; + +-- ============================================================================ +-- Upsert strength goal type +-- ============================================================================ + +INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + calculation_formula, filter_conditions, description, is_active +) VALUES ( + 'strength', + 'Kraftniveau', + 'Strength', + 'Punkte', + '💪', + 'training', + NULL, -- No automatic data source + NULL, + 'latest', + NULL, + NULL, + 'Allgemeines Kraftniveau - manuelle Erfassung', + true +) +ON CONFLICT (type_key) +DO UPDATE SET + label_de = 'Kraftniveau', + label_en = 'Strength', + unit = 'Punkte', + icon = '💪', + category = 'training', + is_active = true, + description = 'Allgemeines Kraftniveau - manuelle Erfassung'; + +COMMENT ON TABLE goal_type_definitions IS 'Goal type registry - defines all available goal types (v1.5: DB-driven, flexible system)'; diff --git a/backend/migrations/030_goal_progress_log.sql b/backend/migrations/030_goal_progress_log.sql new file mode 100644 index 0000000..48080d4 --- /dev/null +++ b/backend/migrations/030_goal_progress_log.sql @@ -0,0 +1,64 @@ +-- Migration 030: Goal Progress Log +-- Date: 2026-03-27 +-- Purpose: Track progress history for all goals (especially custom goals without data source) + +-- ============================================================================ +-- Goal Progress Log Table +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS goal_progress_log ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + goal_id UUID NOT NULL REFERENCES goals(id) ON DELETE CASCADE, + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Progress data + date DATE NOT NULL, + value DECIMAL(10,2) NOT NULL, + note TEXT, + + -- Metadata + source VARCHAR(20) DEFAULT 'manual' CHECK (source IN ('manual', 'automatic', 'import')), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + -- Constraints + CONSTRAINT unique_progress_per_day UNIQUE(goal_id, date) +); + +CREATE INDEX idx_goal_progress_goal_date ON goal_progress_log(goal_id, date DESC); +CREATE INDEX idx_goal_progress_profile ON goal_progress_log(profile_id); + +COMMENT ON TABLE goal_progress_log IS 'Progress history for goals - enables manual tracking for custom goals and charts'; +COMMENT ON COLUMN goal_progress_log.value IS 'Progress value in goal unit (e.g., kg, cm, points)'; +COMMENT ON COLUMN goal_progress_log.source IS 'manual: user entered, automatic: computed from data source, import: CSV/API'; + +-- ============================================================================ +-- Function: Update goal current_value from latest progress +-- ============================================================================ + +CREATE OR REPLACE FUNCTION update_goal_current_value() +RETURNS TRIGGER AS $$ +BEGIN + -- Update current_value in goals table with latest progress entry + UPDATE goals + SET current_value = ( + SELECT value + FROM goal_progress_log + WHERE goal_id = NEW.goal_id + ORDER BY date DESC + LIMIT 1 + ), + updated_at = NOW() + WHERE id = NEW.goal_id; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Trigger: Auto-update current_value when progress is added/updated +CREATE TRIGGER trigger_update_goal_current_value +AFTER INSERT OR UPDATE ON goal_progress_log +FOR EACH ROW +EXECUTE FUNCTION update_goal_current_value(); + +COMMENT ON FUNCTION update_goal_current_value IS 'Auto-update goal.current_value when new progress is logged'; diff --git a/backend/routers/goals.py b/backend/routers/goals.py new file mode 100644 index 0000000..be2af28 --- /dev/null +++ b/backend/routers/goals.py @@ -0,0 +1,1252 @@ +""" +Goals Router - Goal System (Strategic + Tactical) + +Endpoints for managing: +- Strategic goal modes (weight_loss, strength, etc.) +- Tactical goal targets (concrete values with deadlines) +- Training phase detection +- Fitness tests + +Part of v9e Goal System implementation. +""" +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from typing import Optional, List +from datetime import date, datetime, timedelta +from decimal import Decimal +import traceback + +from db import get_db, get_cursor, r2d +from auth import require_auth +from goal_utils import get_current_value_for_goal + +router = APIRouter(prefix="/api/goals", tags=["goals"]) + +# ============================================================================ +# Pydantic Models +# ============================================================================ + +class GoalModeUpdate(BaseModel): + """Update strategic goal mode (deprecated - use FocusAreasUpdate)""" + goal_mode: str # weight_loss, strength, endurance, recomposition, health + +class FocusAreasUpdate(BaseModel): + """Update focus area weights (v2.0)""" + weight_loss_pct: int + muscle_gain_pct: int + strength_pct: int + endurance_pct: int + flexibility_pct: int + health_pct: int + +class GoalCreate(BaseModel): + """Create or update a concrete goal""" + goal_type: str # weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr + is_primary: bool = False # Kept for backward compatibility + target_value: float + unit: str # kg, %, ml/kg/min, bpm, mmHg, cm, reps + target_date: Optional[date] = None + category: Optional[str] = 'other' # body, training, nutrition, recovery, health, other + priority: Optional[int] = 2 # 1=high, 2=medium, 3=low + name: Optional[str] = None + description: Optional[str] = None + +class GoalUpdate(BaseModel): + """Update existing goal""" + target_value: Optional[float] = None + target_date: Optional[date] = None + status: Optional[str] = None # active, reached, abandoned, expired + is_primary: Optional[bool] = None # Kept for backward compatibility + category: Optional[str] = None # body, training, nutrition, recovery, health, other + priority: Optional[int] = None # 1=high, 2=medium, 3=low + name: Optional[str] = None + description: Optional[str] = None + +class TrainingPhaseCreate(BaseModel): + """Create training phase (manual or auto-detected)""" + phase_type: str # calorie_deficit, calorie_surplus, deload, maintenance, periodization + start_date: date + end_date: Optional[date] = None + notes: Optional[str] = None + +class FitnessTestCreate(BaseModel): + """Record fitness test result""" + test_type: str + result_value: float + result_unit: str + test_date: date + test_conditions: Optional[str] = None + +class GoalProgressCreate(BaseModel): + """Log progress for a goal""" + date: date + value: float + note: Optional[str] = None + +class GoalProgressUpdate(BaseModel): + """Update progress entry""" + value: Optional[float] = None + note: Optional[str] = None + +class GoalTypeCreate(BaseModel): + """Create custom goal type definition""" + type_key: str + label_de: str + label_en: Optional[str] = None + unit: str + icon: Optional[str] = None + category: Optional[str] = 'custom' + source_table: Optional[str] = None + source_column: Optional[str] = None + aggregation_method: Optional[str] = 'latest' + calculation_formula: Optional[str] = None + filter_conditions: Optional[dict] = None + description: Optional[str] = None + +class GoalTypeUpdate(BaseModel): + """Update goal type definition""" + label_de: Optional[str] = None + label_en: Optional[str] = None + unit: Optional[str] = None + icon: Optional[str] = None + category: Optional[str] = None + source_table: Optional[str] = None + source_column: Optional[str] = None + aggregation_method: Optional[str] = None + calculation_formula: Optional[str] = None + filter_conditions: Optional[dict] = None + description: Optional[str] = None + is_active: Optional[bool] = None + +# ============================================================================ +# Strategic Layer: Goal Modes +# ============================================================================ + +@router.get("/mode") +def get_goal_mode(session: dict = Depends(require_auth)): + """Get user's current strategic goal mode""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "SELECT goal_mode FROM profiles WHERE id = %s", + (pid,) + ) + row = cur.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Profil nicht gefunden") + + return { + "goal_mode": row['goal_mode'] or 'health', + "description": _get_goal_mode_description(row['goal_mode'] or 'health') + } + +@router.put("/mode") +def update_goal_mode(data: GoalModeUpdate, session: dict = Depends(require_auth)): + """Update user's strategic goal mode""" + pid = session['profile_id'] + + # Validate goal mode + valid_modes = ['weight_loss', 'strength', 'endurance', 'recomposition', 'health'] + if data.goal_mode not in valid_modes: + raise HTTPException( + status_code=400, + detail=f"Ungültiger Goal Mode. Erlaubt: {', '.join(valid_modes)}" + ) + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "UPDATE profiles SET goal_mode = %s WHERE id = %s", + (data.goal_mode, pid) + ) + + return { + "goal_mode": data.goal_mode, + "description": _get_goal_mode_description(data.goal_mode) + } + +def _get_goal_mode_description(mode: str) -> str: + """Get description for goal mode""" + descriptions = { + 'weight_loss': 'Gewichtsreduktion (Kaloriendefizit, Fettabbau)', + 'strength': 'Kraftaufbau (Muskelwachstum, progressive Belastung)', + 'endurance': 'Ausdauer (VO2Max, aerobe Kapazität)', + 'recomposition': 'Körperkomposition (gleichzeitig Fett ab- und Muskeln aufbauen)', + 'health': 'Allgemeine Gesundheit (ausgewogen, präventiv)' + } + return descriptions.get(mode, 'Unbekannt') + +# ============================================================================ +# Focus Areas (v2.0): Weighted Multi-Goal System +# ============================================================================ + +@router.get("/focus-areas") +def get_focus_areas(session: dict = Depends(require_auth)): + """ + Get current focus area weights. + + Returns custom weights if set, otherwise derives from goal_mode. + """ + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Try to get custom focus areas + cur.execute(""" + SELECT weight_loss_pct, muscle_gain_pct, strength_pct, + endurance_pct, flexibility_pct, health_pct, + created_at, updated_at + FROM focus_areas + WHERE profile_id = %s AND active = true + LIMIT 1 + """, (pid,)) + + row = cur.fetchone() + + if row: + return { + "custom": True, + "weight_loss_pct": row['weight_loss_pct'], + "muscle_gain_pct": row['muscle_gain_pct'], + "strength_pct": row['strength_pct'], + "endurance_pct": row['endurance_pct'], + "flexibility_pct": row['flexibility_pct'], + "health_pct": row['health_pct'], + "updated_at": row['updated_at'] + } + + # Fallback: Derive from goal_mode + cur.execute("SELECT goal_mode FROM profiles WHERE id = %s", (pid,)) + profile = cur.fetchone() + + if not profile or not profile['goal_mode']: + # Default balanced health + return { + "custom": False, + "weight_loss_pct": 0, + "muscle_gain_pct": 0, + "strength_pct": 10, + "endurance_pct": 20, + "flexibility_pct": 15, + "health_pct": 55, + "source": "default" + } + + # Derive from goal_mode (using same logic as migration) + mode = profile['goal_mode'] + mode_mappings = { + 'weight_loss': { + 'weight_loss_pct': 60, + 'muscle_gain_pct': 0, + 'strength_pct': 10, + 'endurance_pct': 20, + 'flexibility_pct': 5, + 'health_pct': 5 + }, + 'strength': { + 'weight_loss_pct': 0, + 'muscle_gain_pct': 40, + 'strength_pct': 50, + 'endurance_pct': 10, + 'flexibility_pct': 0, + 'health_pct': 0 + }, + 'endurance': { + 'weight_loss_pct': 0, + 'muscle_gain_pct': 0, + 'strength_pct': 0, + 'endurance_pct': 70, + 'flexibility_pct': 10, + 'health_pct': 20 + }, + 'recomposition': { + 'weight_loss_pct': 30, + 'muscle_gain_pct': 30, + 'strength_pct': 25, + 'endurance_pct': 10, + 'flexibility_pct': 5, + 'health_pct': 0 + }, + 'health': { + 'weight_loss_pct': 0, + 'muscle_gain_pct': 0, + 'strength_pct': 10, + 'endurance_pct': 20, + 'flexibility_pct': 15, + 'health_pct': 55 + } + } + + mapping = mode_mappings.get(mode, mode_mappings['health']) + mapping['custom'] = False + mapping['source'] = f"goal_mode:{mode}" + return mapping + +@router.put("/focus-areas") +def update_focus_areas(data: FocusAreasUpdate, session: dict = Depends(require_auth)): + """ + Update focus area weights (upsert). + + Validates that sum = 100 and all values are 0-100. + """ + pid = session['profile_id'] + + # Validate sum = 100 + total = ( + data.weight_loss_pct + data.muscle_gain_pct + data.strength_pct + + data.endurance_pct + data.flexibility_pct + data.health_pct + ) + + if total != 100: + raise HTTPException( + status_code=400, + detail=f"Summe muss 100% sein (aktuell: {total}%)" + ) + + # Validate range 0-100 + values = [ + data.weight_loss_pct, data.muscle_gain_pct, data.strength_pct, + data.endurance_pct, data.flexibility_pct, data.health_pct + ] + + if any(v < 0 or v > 100 for v in values): + raise HTTPException( + status_code=400, + detail="Alle Werte müssen zwischen 0 und 100 liegen" + ) + + with get_db() as conn: + cur = get_cursor(conn) + + # Deactivate old focus_areas + cur.execute( + "UPDATE focus_areas SET active = false WHERE profile_id = %s", + (pid,) + ) + + # Insert new focus_areas + cur.execute(""" + INSERT INTO focus_areas ( + profile_id, weight_loss_pct, muscle_gain_pct, strength_pct, + endurance_pct, flexibility_pct, health_pct + ) VALUES (%s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, ( + pid, data.weight_loss_pct, data.muscle_gain_pct, data.strength_pct, + data.endurance_pct, data.flexibility_pct, data.health_pct + )) + + return { + "message": "Fokus-Bereiche aktualisiert", + "weight_loss_pct": data.weight_loss_pct, + "muscle_gain_pct": data.muscle_gain_pct, + "strength_pct": data.strength_pct, + "endurance_pct": data.endurance_pct, + "flexibility_pct": data.flexibility_pct, + "health_pct": data.health_pct + } + +# ============================================================================ +# Tactical Layer: Concrete Goals +# ============================================================================ + +@router.get("/list") +def list_goals(session: dict = Depends(require_auth)): + """List all goals for current user""" + pid = session['profile_id'] + + try: + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT id, goal_type, is_primary, status, + target_value, current_value, start_value, unit, + start_date, target_date, reached_date, + name, description, + progress_pct, projection_date, on_track, + created_at, updated_at + FROM goals + WHERE profile_id = %s + ORDER BY is_primary DESC, created_at DESC + """, (pid,)) + + goals = [r2d(row) for row in cur.fetchall()] + print(f"[DEBUG] Loaded {len(goals)} goals for profile {pid}") + + # Update current values for each goal + for goal in goals: + try: + _update_goal_progress(conn, pid, goal) + except Exception as e: + print(f"[ERROR] Failed to update progress for goal {goal.get('id')}: {e}") + # Continue with other goals even if one fails + + return goals + + except Exception as e: + print(f"[ERROR] list_goals failed: {e}") + import traceback + traceback.print_exc() + raise HTTPException( + status_code=500, + detail=f"Fehler beim Laden der Ziele: {str(e)}" + ) + +@router.post("/create") +def create_goal(data: GoalCreate, session: dict = Depends(require_auth)): + """Create new goal""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # If this is set as primary, unset other primary goals + if data.is_primary: + cur.execute( + "UPDATE goals SET is_primary = false WHERE profile_id = %s", + (pid,) + ) + + # Get current value for this goal type + current_value = _get_current_value_for_goal_type(conn, pid, data.goal_type) + + # Insert goal + cur.execute(""" + INSERT INTO goals ( + profile_id, goal_type, is_primary, + target_value, current_value, start_value, unit, + target_date, category, priority, name, description + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, ( + pid, data.goal_type, data.is_primary, + data.target_value, current_value, current_value, data.unit, + data.target_date, data.category, data.priority, data.name, data.description + )) + + goal_id = cur.fetchone()['id'] + + return {"id": goal_id, "message": "Ziel erstellt"} + +@router.put("/{goal_id}") +def update_goal(goal_id: str, data: GoalUpdate, session: dict = Depends(require_auth)): + """Update existing goal""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Verify ownership + cur.execute( + "SELECT id FROM goals WHERE id = %s AND profile_id = %s", + (goal_id, pid) + ) + if not cur.fetchone(): + raise HTTPException(status_code=404, detail="Ziel nicht gefunden") + + # If setting this goal as primary, unset all other primary goals + if data.is_primary is True: + cur.execute( + "UPDATE goals SET is_primary = false WHERE profile_id = %s AND id != %s", + (pid, goal_id) + ) + + # Build update query dynamically + updates = [] + params = [] + + if data.target_value is not None: + updates.append("target_value = %s") + params.append(data.target_value) + + if data.target_date is not None: + updates.append("target_date = %s") + params.append(data.target_date) + + if data.status is not None: + updates.append("status = %s") + params.append(data.status) + if data.status == 'reached': + updates.append("reached_date = CURRENT_DATE") + + if data.is_primary is not None: + updates.append("is_primary = %s") + params.append(data.is_primary) + + if data.category is not None: + updates.append("category = %s") + params.append(data.category) + + if data.priority is not None: + updates.append("priority = %s") + params.append(data.priority) + + if data.name is not None: + updates.append("name = %s") + params.append(data.name) + + if data.description is not None: + updates.append("description = %s") + params.append(data.description) + + if not updates: + raise HTTPException(status_code=400, detail="Keine Änderungen angegeben") + + updates.append("updated_at = NOW()") + params.extend([goal_id, pid]) + + cur.execute( + f"UPDATE goals SET {', '.join(updates)} WHERE id = %s AND profile_id = %s", + tuple(params) + ) + + return {"message": "Ziel aktualisiert"} + +@router.delete("/{goal_id}") +def delete_goal(goal_id: str, session: dict = Depends(require_auth)): + """Delete goal""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "DELETE FROM goals WHERE id = %s AND profile_id = %s", + (goal_id, pid) + ) + + if cur.rowcount == 0: + raise HTTPException(status_code=404, detail="Ziel nicht gefunden") + + return {"message": "Ziel gelöscht"} + +# ============================================================================ +# Goal Progress Endpoints +# ============================================================================ + +@router.get("/{goal_id}/progress") +def get_goal_progress(goal_id: str, session: dict = Depends(require_auth)): + """Get progress history for a goal""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Verify ownership + cur.execute( + "SELECT id FROM goals WHERE id = %s AND profile_id = %s", + (goal_id, pid) + ) + if not cur.fetchone(): + raise HTTPException(status_code=404, detail="Ziel nicht gefunden") + + # Get progress entries + cur.execute(""" + SELECT id, date, value, note, source, created_at + FROM goal_progress_log + WHERE goal_id = %s + ORDER BY date DESC + """, (goal_id,)) + + entries = cur.fetchall() + return [r2d(e) for e in entries] + +@router.post("/{goal_id}/progress") +def create_goal_progress(goal_id: str, data: GoalProgressCreate, session: dict = Depends(require_auth)): + """Log new progress for a goal""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Verify ownership and check if manual entry is allowed + cur.execute(""" + SELECT g.id, g.unit, gt.source_table + FROM goals g + LEFT JOIN goal_type_definitions gt ON g.goal_type = gt.type_key + WHERE g.id = %s AND g.profile_id = %s + """, (goal_id, pid)) + goal = cur.fetchone() + if not goal: + raise HTTPException(status_code=404, detail="Ziel nicht gefunden") + + # Prevent manual entries for goals with automatic data sources + if goal['source_table']: + raise HTTPException( + status_code=400, + detail=f"Manuelle Einträge nicht erlaubt für automatisch erfasste Ziele. " + f"Bitte nutze die entsprechende Erfassungsseite (z.B. Gewicht, Aktivität)." + ) + + # Insert progress entry + try: + cur.execute(""" + INSERT INTO goal_progress_log (goal_id, profile_id, date, value, note, source) + VALUES (%s, %s, %s, %s, %s, 'manual') + RETURNING id + """, (goal_id, pid, data.date, data.value, data.note)) + + progress_id = cur.fetchone()['id'] + + # Trigger will auto-update goals.current_value + return { + "id": progress_id, + "message": f"Fortschritt erfasst: {data.value} {goal['unit']}" + } + + except Exception as e: + if "unique_progress_per_day" in str(e): + raise HTTPException( + status_code=400, + detail=f"Für {data.date} existiert bereits ein Eintrag. Bitte bearbeite den existierenden Eintrag." + ) + raise HTTPException(status_code=500, detail=f"Fehler beim Speichern: {str(e)}") + +@router.delete("/{goal_id}/progress/{progress_id}") +def delete_goal_progress(goal_id: str, progress_id: str, session: dict = Depends(require_auth)): + """Delete progress entry""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Verify ownership + cur.execute( + "SELECT id FROM goals WHERE id = %s AND profile_id = %s", + (goal_id, pid) + ) + if not cur.fetchone(): + raise HTTPException(status_code=404, detail="Ziel nicht gefunden") + + # Delete progress entry + cur.execute( + "DELETE FROM goal_progress_log WHERE id = %s AND goal_id = %s AND profile_id = %s", + (progress_id, goal_id, pid) + ) + + if cur.rowcount == 0: + raise HTTPException(status_code=404, detail="Progress-Eintrag nicht gefunden") + + # After deletion, recalculate current_value from remaining entries + cur.execute(""" + UPDATE goals + SET current_value = ( + SELECT value FROM goal_progress_log + WHERE goal_id = %s + ORDER BY date DESC + LIMIT 1 + ) + WHERE id = %s + """, (goal_id, goal_id)) + + return {"message": "Progress-Eintrag gelöscht"} + +@router.get("/grouped") +def get_goals_grouped(session: dict = Depends(require_auth)): + """ + Get goals grouped by category, sorted by priority. + + Returns structure: + { + "body": [{"id": "...", "goal_type": "weight", "priority": 1, ...}, ...], + "training": [...], + "nutrition": [...], + "recovery": [...], + "health": [...], + "other": [...] + } + """ + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Get all active goals with type definitions + cur.execute(""" + SELECT + g.id, g.goal_type, g.target_value, g.current_value, g.start_value, + g.unit, g.target_date, g.status, g.is_primary, g.category, g.priority, + g.name, g.description, g.progress_pct, g.on_track, g.projection_date, + g.created_at, g.updated_at, + gt.label_de, gt.icon, gt.category as type_category, + gt.source_table, gt.source_column + FROM goals g + LEFT JOIN goal_type_definitions gt ON g.goal_type = gt.type_key + WHERE g.profile_id = %s + ORDER BY g.category, g.priority ASC, g.created_at DESC + """, (pid,)) + + goals = cur.fetchall() + + # Group by category + grouped = {} + for goal in goals: + cat = goal['category'] or 'other' + if cat not in grouped: + grouped[cat] = [] + grouped[cat].append(r2d(goal)) + + return grouped + +# ============================================================================ +# Training Phases +# ============================================================================ + +@router.get("/phases") +def list_training_phases(session: dict = Depends(require_auth)): + """List training phases""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT id, phase_type, detected_automatically, confidence_score, + status, start_date, end_date, duration_days, + detection_params, notes, created_at + FROM training_phases + WHERE profile_id = %s + ORDER BY start_date DESC + """, (pid,)) + + return [r2d(row) for row in cur.fetchall()] + +@router.post("/phases") +def create_training_phase(data: TrainingPhaseCreate, session: dict = Depends(require_auth)): + """Create training phase (manual)""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + duration = None + if data.end_date: + duration = (data.end_date - data.start_date).days + + cur.execute(""" + INSERT INTO training_phases ( + profile_id, phase_type, detected_automatically, + status, start_date, end_date, duration_days, notes + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, ( + pid, data.phase_type, False, + 'active', data.start_date, data.end_date, duration, data.notes + )) + + phase_id = cur.fetchone()['id'] + + return {"id": phase_id, "message": "Trainingsphase erstellt"} + +@router.put("/phases/{phase_id}/status") +def update_phase_status( + phase_id: str, + status: str, + session: dict = Depends(require_auth) +): + """Update training phase status (accept/reject auto-detected phases)""" + pid = session['profile_id'] + + valid_statuses = ['suggested', 'accepted', 'active', 'completed', 'rejected'] + if status not in valid_statuses: + raise HTTPException( + status_code=400, + detail=f"Ungültiger Status. Erlaubt: {', '.join(valid_statuses)}" + ) + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "UPDATE training_phases SET status = %s WHERE id = %s AND profile_id = %s", + (status, phase_id, pid) + ) + + if cur.rowcount == 0: + raise HTTPException(status_code=404, detail="Trainingsphase nicht gefunden") + + return {"message": "Status aktualisiert"} + +# ============================================================================ +# Fitness Tests +# ============================================================================ + +@router.get("/tests") +def list_fitness_tests(session: dict = Depends(require_auth)): + """List all fitness tests""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT id, test_type, result_value, result_unit, + test_date, test_conditions, norm_category, created_at + FROM fitness_tests + WHERE profile_id = %s + ORDER BY test_date DESC + """, (pid,)) + + return [r2d(row) for row in cur.fetchall()] + +@router.post("/tests") +def create_fitness_test(data: FitnessTestCreate, session: dict = Depends(require_auth)): + """Record fitness test result""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Calculate norm category (simplified for now) + norm_category = _calculate_norm_category( + data.test_type, + data.result_value, + data.result_unit + ) + + cur.execute(""" + INSERT INTO fitness_tests ( + profile_id, test_type, result_value, result_unit, + test_date, test_conditions, norm_category + ) VALUES (%s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, ( + pid, data.test_type, data.result_value, data.result_unit, + data.test_date, data.test_conditions, norm_category + )) + + test_id = cur.fetchone()['id'] + + return {"id": test_id, "norm_category": norm_category} + +# ============================================================================ +# Helper Functions +# ============================================================================ + +def _get_current_value_for_goal_type(conn, profile_id: str, goal_type: str) -> Optional[float]: + """ + Get current value for a goal type. + + DEPRECATED: This function now delegates to the universal fetcher in goal_utils.py. + Phase 1.5: All goal types are now defined in goal_type_definitions table. + + Args: + conn: Database connection + profile_id: User's profile ID + goal_type: Goal type key (e.g., 'weight', 'meditation_minutes') + + Returns: + Current value or None + """ + # Delegate to universal fetcher (Phase 1.5) + return get_current_value_for_goal(conn, profile_id, goal_type) + +def _update_goal_progress(conn, profile_id: str, goal: dict): + """Update goal progress (modifies goal dict in-place)""" + # Get current value + current = _get_current_value_for_goal_type(conn, profile_id, goal['goal_type']) + + if current is not None and goal['start_value'] is not None and goal['target_value'] is not None: + goal['current_value'] = current + + # Calculate progress percentage + total_delta = float(goal['target_value']) - float(goal['start_value']) + current_delta = current - float(goal['start_value']) + + if total_delta != 0: + progress_pct = (current_delta / total_delta) * 100 + goal['progress_pct'] = round(progress_pct, 2) + + # Simple linear projection + if goal['start_date'] and current_delta != 0: + days_elapsed = (date.today() - goal['start_date']).days + if days_elapsed > 0: + days_per_unit = days_elapsed / current_delta + remaining_units = float(goal['target_value']) - current + remaining_days = int(days_per_unit * remaining_units) + goal['projection_date'] = date.today() + timedelta(days=remaining_days) + + # Check if on track + if goal['target_date'] and goal['projection_date']: + goal['on_track'] = goal['projection_date'] <= goal['target_date'] + +def _calculate_norm_category(test_type: str, value: float, unit: str) -> Optional[str]: + """ + Calculate norm category for fitness test + (Simplified - would need age/gender-specific norms) + """ + # Placeholder - should use proper norm tables + return None + +# ============================================================================ +# Goal Type Definitions (Phase 1.5 - Flexible Goal System) +# ============================================================================ + +@router.get("/schema-info") +def get_schema_info(session: dict = Depends(require_auth)): + """ + Get available tables and columns for goal type creation. + + Admin-only endpoint for building custom goal types. + Returns structure with descriptions for UX guidance. + """ + pid = session['profile_id'] + + # Check admin role + with get_db() as conn: + cur = get_cursor(conn) + cur.execute("SELECT role FROM profiles WHERE id = %s", (pid,)) + profile = cur.fetchone() + + if not profile or profile['role'] != 'admin': + raise HTTPException(status_code=403, detail="Admin-Zugriff erforderlich") + + # Define relevant tables with descriptions + # Only include tables that make sense for goal tracking + schema = { + "weight_log": { + "description": "Gewichtsverlauf", + "columns": { + "weight": {"type": "DECIMAL", "description": "Körpergewicht in kg"} + } + }, + "caliper_log": { + "description": "Caliper-Messungen (Hautfalten)", + "columns": { + "body_fat_pct": {"type": "DECIMAL", "description": "Körperfettanteil in %"}, + "sum_mm": {"type": "DECIMAL", "description": "Summe Hautfalten in mm"} + } + }, + "circumference_log": { + "description": "Umfangsmessungen", + "columns": { + "c_neck": {"type": "DECIMAL", "description": "Nackenumfang in cm"}, + "c_chest": {"type": "DECIMAL", "description": "Brustumfang in cm"}, + "c_waist": {"type": "DECIMAL", "description": "Taillenumfang in cm"}, + "c_hips": {"type": "DECIMAL", "description": "Hüftumfang in cm"}, + "c_thigh_l": {"type": "DECIMAL", "description": "Oberschenkel links in cm"}, + "c_thigh_r": {"type": "DECIMAL", "description": "Oberschenkel rechts in cm"}, + "c_calf_l": {"type": "DECIMAL", "description": "Wade links in cm"}, + "c_calf_r": {"type": "DECIMAL", "description": "Wade rechts in cm"}, + "c_bicep_l": {"type": "DECIMAL", "description": "Bizeps links in cm"}, + "c_bicep_r": {"type": "DECIMAL", "description": "Bizeps rechts in cm"} + } + }, + "activity_log": { + "description": "Trainingseinheiten", + "columns": { + "id": {"type": "UUID", "description": "ID (für Zählung von Einheiten)"}, + "duration_minutes": {"type": "INTEGER", "description": "Trainingsdauer in Minuten"}, + "perceived_exertion": {"type": "INTEGER", "description": "Belastungsempfinden (1-10)"}, + "quality_rating": {"type": "INTEGER", "description": "Qualitätsbewertung (1-10)"} + } + }, + "nutrition_log": { + "description": "Ernährungstagebuch", + "columns": { + "calories": {"type": "INTEGER", "description": "Kalorien in kcal"}, + "protein_g": {"type": "DECIMAL", "description": "Protein in g"}, + "carbs_g": {"type": "DECIMAL", "description": "Kohlenhydrate in g"}, + "fat_g": {"type": "DECIMAL", "description": "Fett in g"} + } + }, + "sleep_log": { + "description": "Schlafprotokoll", + "columns": { + "total_minutes": {"type": "INTEGER", "description": "Gesamtschlafdauer in Minuten"} + } + }, + "vitals_baseline": { + "description": "Vitalwerte (morgens)", + "columns": { + "resting_hr": {"type": "INTEGER", "description": "Ruhepuls in bpm"}, + "hrv_rmssd": {"type": "INTEGER", "description": "Herzratenvariabilität (RMSSD) in ms"}, + "vo2_max": {"type": "DECIMAL", "description": "VO2 Max in ml/kg/min"}, + "spo2": {"type": "INTEGER", "description": "Sauerstoffsättigung in %"}, + "respiratory_rate": {"type": "INTEGER", "description": "Atemfrequenz pro Minute"} + } + }, + "blood_pressure_log": { + "description": "Blutdruckmessungen", + "columns": { + "systolic": {"type": "INTEGER", "description": "Systolischer Blutdruck in mmHg"}, + "diastolic": {"type": "INTEGER", "description": "Diastolischer Blutdruck in mmHg"}, + "pulse": {"type": "INTEGER", "description": "Puls in bpm"} + } + }, + "rest_days": { + "description": "Ruhetage", + "columns": { + "id": {"type": "UUID", "description": "ID (für Zählung von Ruhetagen)"} + } + } + } + + return schema + +@router.get("/goal-types") +def list_goal_type_definitions(session: dict = Depends(require_auth)): + """ + Get all active goal type definitions. + + Public endpoint - returns all available goal types for dropdown. + """ + try: + with get_db() as conn: + cur = get_cursor(conn) + + cur.execute(""" + SELECT id, type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + calculation_formula, description, is_system, is_active, + created_at, updated_at + FROM goal_type_definitions + WHERE is_active = true + ORDER BY + CASE + WHEN is_system = true THEN 0 + ELSE 1 + END, + label_de + """) + + results = [r2d(row) for row in cur.fetchall()] + print(f"[DEBUG] Loaded {len(results)} goal types") + return results + + except Exception as e: + print(f"[ERROR] list_goal_type_definitions failed: {e}") + print(traceback.format_exc()) + raise HTTPException( + status_code=500, + detail=f"Fehler beim Laden der Goal Types: {str(e)}" + ) + +@router.post("/goal-types") +def create_goal_type_definition( + data: GoalTypeCreate, + session: dict = Depends(require_auth) +): + """ + Create custom goal type definition. + + Admin-only endpoint for creating new goal types. + Users with admin role can define custom metrics. + """ + pid = session['profile_id'] + + # Check admin role + with get_db() as conn: + cur = get_cursor(conn) + cur.execute("SELECT role FROM profiles WHERE id = %s", (pid,)) + profile = cur.fetchone() + + if not profile or profile['role'] != 'admin': + raise HTTPException( + status_code=403, + detail="Admin-Zugriff erforderlich" + ) + + # Validate type_key is unique + cur.execute( + "SELECT id FROM goal_type_definitions WHERE type_key = %s", + (data.type_key,) + ) + if cur.fetchone(): + raise HTTPException( + status_code=400, + detail=f"Goal Type '{data.type_key}' existiert bereits" + ) + + # Insert new goal type + import json as json_lib + filter_json = json_lib.dumps(data.filter_conditions) if data.filter_conditions else None + + cur.execute(""" + INSERT INTO goal_type_definitions ( + type_key, label_de, label_en, unit, icon, category, + source_table, source_column, aggregation_method, + calculation_formula, filter_conditions, description, is_active, is_system + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, ( + data.type_key, data.label_de, data.label_en, data.unit, data.icon, + data.category, data.source_table, data.source_column, + data.aggregation_method, data.calculation_formula, filter_json, data.description, + True, False # is_active=True, is_system=False + )) + + goal_type_id = cur.fetchone()['id'] + + return { + "id": goal_type_id, + "message": f"Goal Type '{data.label_de}' erstellt" + } + +@router.put("/goal-types/{goal_type_id}") +def update_goal_type_definition( + goal_type_id: str, + data: GoalTypeUpdate, + session: dict = Depends(require_auth) +): + """ + Update goal type definition. + + Admin-only. System goal types can be updated but not deleted. + """ + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Check admin role + cur.execute("SELECT role FROM profiles WHERE id = %s", (pid,)) + profile = cur.fetchone() + + if not profile or profile['role'] != 'admin': + raise HTTPException( + status_code=403, + detail="Admin-Zugriff erforderlich" + ) + + # Check goal type exists + cur.execute( + "SELECT id FROM goal_type_definitions WHERE id = %s", + (goal_type_id,) + ) + if not cur.fetchone(): + raise HTTPException(status_code=404, detail="Goal Type nicht gefunden") + + # Build update query + updates = [] + params = [] + + if data.label_de is not None: + updates.append("label_de = %s") + params.append(data.label_de) + + if data.label_en is not None: + updates.append("label_en = %s") + params.append(data.label_en) + + if data.unit is not None: + updates.append("unit = %s") + params.append(data.unit) + + if data.icon is not None: + updates.append("icon = %s") + params.append(data.icon) + + if data.category is not None: + updates.append("category = %s") + params.append(data.category) + + if data.source_table is not None: + updates.append("source_table = %s") + params.append(data.source_table) + + if data.source_column is not None: + updates.append("source_column = %s") + params.append(data.source_column) + + if data.aggregation_method is not None: + updates.append("aggregation_method = %s") + params.append(data.aggregation_method) + + if data.calculation_formula is not None: + updates.append("calculation_formula = %s") + params.append(data.calculation_formula) + + if data.filter_conditions is not None: + import json as json_lib + filter_json = json_lib.dumps(data.filter_conditions) if data.filter_conditions else None + updates.append("filter_conditions = %s") + params.append(filter_json) + + if data.description is not None: + updates.append("description = %s") + params.append(data.description) + + if data.is_active is not None: + updates.append("is_active = %s") + params.append(data.is_active) + + if not updates: + raise HTTPException(status_code=400, detail="Keine Änderungen angegeben") + + updates.append("updated_at = NOW()") + params.append(goal_type_id) + + cur.execute( + f"UPDATE goal_type_definitions SET {', '.join(updates)} WHERE id = %s", + tuple(params) + ) + + return {"message": "Goal Type aktualisiert"} + +@router.delete("/goal-types/{goal_type_id}") +def delete_goal_type_definition( + goal_type_id: str, + session: dict = Depends(require_auth) +): + """ + Delete (deactivate) goal type definition. + + Admin-only. System goal types cannot be deleted, only deactivated. + Custom goal types can be fully deleted if no goals reference them. + """ + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Check admin role + cur.execute("SELECT role FROM profiles WHERE id = %s", (pid,)) + profile = cur.fetchone() + + if not profile or profile['role'] != 'admin': + raise HTTPException( + status_code=403, + detail="Admin-Zugriff erforderlich" + ) + + # Get goal type info + cur.execute( + "SELECT id, type_key, is_system FROM goal_type_definitions WHERE id = %s", + (goal_type_id,) + ) + goal_type = cur.fetchone() + + if not goal_type: + raise HTTPException(status_code=404, detail="Goal Type nicht gefunden") + + # Check if any goals use this type + cur.execute( + "SELECT COUNT(*) as count FROM goals WHERE goal_type = %s", + (goal_type['type_key'],) + ) + count = cur.fetchone()['count'] + + if count > 0: + # Deactivate instead of delete + cur.execute( + "UPDATE goal_type_definitions SET is_active = false WHERE id = %s", + (goal_type_id,) + ) + return { + "message": f"Goal Type deaktiviert ({count} Ziele nutzen diesen Typ)" + } + else: + if goal_type['is_system']: + # System types: only deactivate + cur.execute( + "UPDATE goal_type_definitions SET is_active = false WHERE id = %s", + (goal_type_id,) + ) + return {"message": "System Goal Type deaktiviert"} + else: + # Custom types: delete + cur.execute( + "DELETE FROM goal_type_definitions WHERE id = %s", + (goal_type_id,) + ) + return {"message": "Goal Type gelöscht"} diff --git a/backend/run_migration_024.py b/backend/run_migration_024.py new file mode 100644 index 0000000..b3cc132 --- /dev/null +++ b/backend/run_migration_024.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Manual Migration 024 Runner + +Run this to manually execute Migration 024 if it didn't run automatically. +""" + +import psycopg2 +import os +from psycopg2.extras import RealDictCursor + +# Database connection +DB_HOST = os.getenv('DB_HOST', 'localhost') +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', '') + +def main(): + print("🔧 Manual Migration 024 Runner") + print("=" * 60) + + # 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 if table exists + cur.execute(""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'goal_type_definitions' + ) + """) + exists = cur.fetchone()['exists'] + + if exists: + print("✓ goal_type_definitions table already exists") + + # Check if it has data + cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions") + count = cur.fetchone()['count'] + print(f"✓ Table has {count} entries") + + if count > 0: + print("\n📊 Existing Goal Types:") + cur.execute(""" + SELECT type_key, label_de, unit, is_system, is_active + FROM goal_type_definitions + ORDER BY is_system DESC, label_de + """) + for row in cur.fetchall(): + status = "SYSTEM" if row['is_system'] else "CUSTOM" + active = "ACTIVE" if row['is_active'] else "INACTIVE" + print(f" - {row['type_key']}: {row['label_de']} ({row['unit']}) [{status}] [{active}]") + + print("\n✅ Migration 024 is already complete!") + return + + # Run migration + print("\n🚀 Running Migration 024...") + + with open('migrations/024_goal_type_registry.sql', 'r', encoding='utf-8') as f: + migration_sql = f.read() + + cur.execute(migration_sql) + conn.commit() + + print("✅ Migration 024 executed successfully!") + + # Verify + cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions") + count = cur.fetchone()['count'] + print(f"✓ {count} goal types seeded") + + # Show created types + cur.execute(""" + SELECT type_key, label_de, unit, is_system + FROM goal_type_definitions + WHERE is_active = true + ORDER BY is_system DESC, label_de + """) + + print("\n📊 Created Goal Types:") + for row in cur.fetchall(): + status = "SYSTEM" if row['is_system'] else "CUSTOM" + print(f" - {row['type_key']}: {row['label_de']} ({row['unit']}) [{status}]") + + # Update schema_migrations + cur.execute(""" + INSERT INTO schema_migrations (filename, executed_at) + VALUES ('024_goal_type_registry.sql', NOW()) + ON CONFLICT (filename) DO NOTHING + """) + conn.commit() + + print("\n✅ Migration 024 complete!") + + 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/docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md b/docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md new file mode 100644 index 0000000..f69134a --- /dev/null +++ b/docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md @@ -0,0 +1,595 @@ +# Zielesystem: Vereinheitlichte Analyse beider Fachkonzepte + +**Datum:** 26. März 2026 +**Basis:** +- `.claude/docs/functional/GOALS_VITALS.md` (v9e Spec) +- `.claude/docs/functional/mitai_jinkendo_konzept_diagramme_auswertungen_v2.md` + +--- + +## 1. Wichtige Erkenntnis: BEIDE Konzepte sind komplementär! + +### GOALS_VITALS.md definiert: +- **Konkrete Zielwerte** (z.B. "82kg bis 30.06.2026") +- 8 Zieltypen (Gewicht, KF%, VO2Max, etc.) +- Primär-/Nebenziel-Konzept +- Trainingsphasen (automatische Erkennung) +- Aktive Tests (Cooper, Liegestütze, etc.) +- 13 neue KI-Platzhalter + +### Konzept v2 definiert: +- **Goal Modes** (strategische Ausrichtung: weight_loss, strength, etc.) +- Score-Gewichtung je Goal Mode +- Chart-Priorisierung je Goal Mode +- Regelbasierte Interpretationen + +### Zusammenspiel: +``` +Goal MODE (v2) → "weight_loss" (strategische Ausrichtung) + ↓ +Primary GOAL (v9e) → "82kg bis 30.06.2026" (konkretes Ziel) +Secondary GOAL → "16% Körperfett" + ↓ +Training PHASE (v9e) → "Kaloriendefizit" (automatisch erkannt) + ↓ +Score Weights (v2) → body_progress: 0.30, nutrition: 0.25, ... + ↓ +Charts (v2) → Zeigen gewichtete Scores + Fortschritt zu Zielen +``` + +--- + +## 2. Zwei-Ebenen-Architektur + +### Ebene 1: STRATEGIC (Goal Modes aus v2) +**Was:** Grundsätzliche Trainingsausrichtung +**Werte:** weight_loss, strength, endurance, recomposition, health +**Zweck:** Bestimmt Score-Gewichtung und Interpretations-Kontext +**Beispiel:** "Ich will Kraft aufbauen" → mode: strength + +### Ebene 2: TACTICAL (Goal Targets aus v9e) +**Was:** Konkrete messbare Ziele +**Werte:** "82kg bis 30.06.2026", "VO2Max 55 ml/kg/min", "50 Liegestütze" +**Zweck:** Fortschritts-Tracking, Prognosen, Motivation +**Beispiel:** "Ich will 82kg wiegen" → target: Gewichtsziel + +### Beide zusammen = Vollständiges Zielesystem + +--- + +## 3. Überarbeitetes Datenmodell + +### Tabelle: `profiles` (erweitern) +```sql +-- Strategic Goal Mode (aus v2) +ALTER TABLE profiles ADD COLUMN goal_mode VARCHAR(50) DEFAULT 'health'; + +COMMENT ON COLUMN profiles.goal_mode IS + 'Strategic goal mode: weight_loss, strength, endurance, recomposition, health. + Determines score weights and interpretation context.'; +``` + +### Tabelle: `goals` (NEU, aus v9e) +```sql +CREATE TABLE goals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Goal Classification + goal_type VARCHAR(50) NOT NULL, -- weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr + is_primary BOOLEAN DEFAULT false, + status VARCHAR(20) DEFAULT 'active', -- draft, active, reached, abandoned, expired + + -- Target Values + target_value DECIMAL(10,2), + current_value DECIMAL(10,2), + start_value DECIMAL(10,2), + unit VARCHAR(20), -- kg, %, ml/kg/min, bpm, mmHg, cm, reps + + -- Timeline + start_date DATE DEFAULT CURRENT_DATE, + target_date DATE, + reached_date DATE, + + -- Metadata + name VARCHAR(100), -- z.B. "Sommerfigur 2026" + description TEXT, + + -- Progress Tracking + progress_pct DECIMAL(5,2), -- Auto-calculated: (current - start) / (target - start) * 100 + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + -- Constraints + CHECK (progress_pct >= 0 AND progress_pct <= 100), + CHECK (status IN ('draft', 'active', 'reached', 'abandoned', 'expired')) +); + +-- Only one primary goal per profile +CREATE UNIQUE INDEX idx_goals_primary ON goals(profile_id, is_primary) WHERE is_primary = true; + +-- Index for active goals lookup +CREATE INDEX idx_goals_active ON goals(profile_id, status) WHERE status = 'active'; +``` + +### Tabelle: `training_phases` (NEU, aus v9e) +```sql +CREATE TABLE training_phases ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Phase Type + phase_type VARCHAR(50) NOT NULL, + -- Werte: calorie_deficit, calorie_maintenance, calorie_surplus, + -- conditioning, hiit, max_strength, regeneration, competition_prep + + -- Detection + detected_automatically BOOLEAN DEFAULT false, + confidence_score DECIMAL(3,2), -- 0.00-1.00 + + -- Status + status VARCHAR(20) DEFAULT 'suggested', -- suggested, confirmed, active, ended + + -- Timeline + start_date DATE, + end_date DATE, + + -- Metadata + detection_reason TEXT, -- Why was this phase detected? + user_notes TEXT, + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Only one active phase per profile +CREATE UNIQUE INDEX idx_phases_active ON training_phases(profile_id, status) WHERE status = 'active'; +``` + +### Tabelle: `fitness_tests` (NEU, aus v9e) +```sql +CREATE TABLE fitness_tests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Test Type + test_type VARCHAR(50) NOT NULL, + -- Standard: cooper, step_test, pushups, squats, sit_reach, balance, grip_strength + -- Custom: user_defined + + -- Result + result_value DECIMAL(10,2) NOT NULL, + result_unit VARCHAR(20) NOT NULL, -- meters, bpm, reps, cm, seconds, kg + + -- Test Date + test_date DATE NOT NULL, + + -- Evaluation + norm_category VARCHAR(30), -- very_good, good, average, needs_improvement + percentile DECIMAL(5,2), -- Where user ranks vs. norm (0-100) + + -- Trend + improvement_vs_last DECIMAL(10,2), -- % change from previous test + + -- Metadata + notes TEXT, + conditions TEXT, -- e.g., "Nach 3h Schlaf, erkältet" + + -- Next Test Recommendation + recommended_retest_date DATE, + + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_fitness_tests_profile_type ON fitness_tests(profile_id, test_type, test_date DESC); +``` + +--- + +## 4. Vereinheitlichte API-Struktur + +### Goal Modes (Strategic) +```python +# routers/goals.py + +@router.get("/modes") +def get_goal_modes(): + """Get all strategic goal modes with score weights.""" + return GOAL_MODES # From v2 concept + +@router.post("/set-mode") +def set_goal_mode(goal_mode: str, session=Depends(require_auth)): + """Set user's strategic goal mode.""" + # Updates profiles.goal_mode +``` + +### Goal Targets (Tactical) +```python +@router.get("/targets") +def get_goal_targets(session=Depends(require_auth)): + """Get all active goal targets.""" + profile_id = session['profile_id'] + # Returns list from goals table + # Includes: primary + all secondary goals + +@router.post("/targets") +def create_goal_target(goal: GoalCreate, session=Depends(require_auth)): + """Create a new goal target.""" + # Inserts into goals table + # Auto-calculates progress_pct + +@router.get("/targets/{goal_id}") +def get_goal_detail(goal_id: str, session=Depends(require_auth)): + """Get detailed goal info with history.""" + # Returns goal + progress history + prognosis + +@router.put("/targets/{goal_id}/progress") +def update_goal_progress(goal_id: str, session=Depends(require_auth)): + """Recalculate goal progress.""" + # Auto-called after new measurements + # Updates current_value, progress_pct + +@router.post("/targets/{goal_id}/reach") +def mark_goal_reached(goal_id: str, session=Depends(require_auth)): + """Mark goal as reached.""" + # Sets status='reached', reached_date=today +``` + +### Training Phases +```python +@router.get("/phases/current") +def get_current_phase(session=Depends(require_auth)): + """Get active training phase.""" + +@router.get("/phases/detect") +def detect_phase(session=Depends(require_auth)): + """Run phase detection algorithm.""" + # Analyzes last 14 days + # Returns suggested phase + confidence + reasoning + +@router.post("/phases/confirm") +def confirm_phase(phase_id: str, session=Depends(require_auth)): + """Confirm detected phase.""" + # Sets status='active' +``` + +### Fitness Tests +```python +@router.get("/tests/types") +def get_test_types(): + """Get all available fitness tests.""" + +@router.post("/tests/{test_type}/execute") +def record_test_result( + test_type: str, + result_value: float, + result_unit: str, + session=Depends(require_auth) +): + """Record a fitness test result.""" + # Inserts into fitness_tests + # Auto-calculates norm_category, percentile, improvement + +@router.get("/tests/due") +def get_due_tests(session=Depends(require_auth)): + """Get tests that are due for retesting.""" +``` + +--- + +## 5. Neue KI-Platzhalter (kombiniert aus beiden Konzepten) + +### Strategic (aus v2) +```python +{{goal_mode}} # "weight_loss" +{{goal_mode_label}} # "Gewichtsreduktion" +{{goal_mode_description}} # "Fettabbau bei Erhalt der Magermasse" +``` + +### Tactical - Primary Goal (aus v9e) +```python +{{primary_goal_type}} # "weight" +{{primary_goal_name}} # "Sommerfigur 2026" +{{primary_goal_target}} # "82 kg bis 30.06.2026" +{{primary_goal_current}} # "85.2 kg" +{{primary_goal_start}} # "86.1 kg" +{{primary_goal_progress_pct}} # "72%" +{{primary_goal_progress_text}} # "72% erreicht (4 kg von 5,5 kg)" +{{primary_goal_days_remaining}} # "45 Tage" +{{primary_goal_prognosis}} # "Ziel voraussichtlich in 6 Wochen erreicht (3 Wochen früher!)" +{{primary_goal_on_track}} # "true" +``` + +### Tactical - Secondary Goals (aus v9e) +```python +{{secondary_goals_count}} # "2" +{{secondary_goals_list}} # "16% Körperfett, VO2Max 55 ml/kg/min" +{{secondary_goal_1_type}} # "body_fat" +{{secondary_goal_1_progress}} # "45%" +``` + +### Training Phase (aus v9e) +```python +{{current_phase}} # "calorie_deficit" +{{current_phase_label}} # "Kaloriendefizit" +{{phase_since}} # "seit 14 Tagen" +{{phase_confidence}} # "0.92" +{{phase_recommendation}} # "Krafttraining erhalten, Cardio moderat, Proteinzufuhr 2g/kg" +{{phase_detected_automatically}} # "true" +``` + +### Fitness Tests (aus v9e) +```python +{{test_last_cooper}} # "2.800m (VO2Max ~52) vor 3 Wochen" +{{test_last_cooper_date}} # "2026-03-05" +{{test_last_cooper_result}} # "2800" +{{test_last_cooper_vo2max}} # "52.3" +{{test_last_cooper_category}} # "good" +{{test_due_list}} # "Sit & Reach (seit 5 Wochen), Liegestütze (seit 4 Wochen)" +{{test_next_recommended}} # "Cooper-Test (in 2 Wochen fällig)" +{{fitness_score_overall}} # "72/100" +{{fitness_score_endurance}} # "good" +{{fitness_score_strength}} # "average" +{{fitness_score_flexibility}} # "needs_improvement" +``` + +### GESAMT: 35+ neue Platzhalter aus v9e +Plus die 84 aus v2 = **120+ neue Platzhalter total** + +--- + +## 6. Überarbeitete Implementierungs-Roadmap + +### Phase 0a: Minimal Goal System (3-4h) ⭐ **JETZT** + +**Strategic Layer:** +- DB: `goal_mode` in profiles +- Backend: GOAL_MODES aus v2 +- API: GET/SET goal mode +- UI: Goal Mode Selector (5 Modi) + +**Tactical Layer:** +- DB: `goals` table +- API: CRUD für goal targets +- UI: Goal Management Page (minimal) + - Liste aktiver Ziele + - Fortschrittsbalken + - "+ Neues Ziel" Button + +**Aufwand:** 3-4h (erweitert wegen Tactical Layer) + +--- + +### Phase 0b: Goal-Aware Placeholders (16-20h) + +**Strategic Placeholders:** +```python +{{goal_mode}} # Aus profiles.goal_mode +{{goal_mode_label}} # Aus GOAL_MODES mapping +``` + +**Tactical Placeholders:** +```python +{{primary_goal_type}} # Aus goals WHERE is_primary=true +{{primary_goal_target}} +{{primary_goal_progress_pct}} +{{primary_goal_prognosis}} # Berechnet aus Trend +``` + +**Score Calculations (goal-aware):** +```python +def get_body_progress_score(profile_id: str) -> str: + profile = get_profile_data(profile_id) + goal_mode = profile.get('goal_mode', 'health') + + # Get weights from v2 concept + weights = GOAL_MODES[goal_mode]['score_weights'] + + # Calculate sub-scores + fm_score = calculate_fm_progress(profile_id) + lbm_score = calculate_lbm_progress(profile_id) + + # Weight according to goal mode + if goal_mode == 'weight_loss': + total = 0.50 * fm_score + 0.30 * weight_score + 0.20 * lbm_score + elif goal_mode == 'strength': + total = 0.60 * lbm_score + 0.30 * fm_score + 0.10 * weight_score + # ... + + return f"{int(total)}/100" +``` + +--- + +### Phase 0c: Training Phases (4-6h) **PARALLEL** + +**DB:** +- `training_phases` table + +**Detection Algorithm:** +```python +def detect_current_phase(profile_id: str) -> dict: + """Detects training phase from last 14 days of data.""" + + # Analyze data + kcal_balance = get_kcal_balance_14d(profile_id) + training_dist = get_training_distribution_14d(profile_id) + weight_trend = get_weight_trend_14d(profile_id) + hrv_avg = get_hrv_avg_14d(profile_id) + volume_change = get_volume_change_14d(profile_id) + + # Phase Detection Rules + if kcal_balance < -300 and weight_trend < 0: + return { + 'phase': 'calorie_deficit', + 'confidence': 0.85, + 'reason': f'Avg kcal balance {kcal_balance}/day, weight -0.5kg/week' + } + + if training_dist['endurance'] > 60 and vo2max_trend > 0: + return { + 'phase': 'conditioning', + 'confidence': 0.78, + 'reason': f'{training_dist["endurance"]}% cardio, VO2max improving' + } + + if volume_change < -40 and hrv_avg < hrv_baseline * 0.85: + return { + 'phase': 'regeneration', + 'confidence': 0.92, + 'reason': f'Volume -40%, HRV below baseline, recovery needed' + } + + # Default + return { + 'phase': 'maintenance', + 'confidence': 0.50, + 'reason': 'No clear pattern detected' + } +``` + +**API:** +- GET /phases/current +- GET /phases/detect +- POST /phases/confirm + +**UI:** +- Dashboard Badge: "📊 Phase: Kaloriendefizit" +- Phase Detection Banner: "Wir haben erkannt: Kaloriendefizit-Phase. Stimmt das?" + +--- + +### Phase 0d: Fitness Tests (4-6h) **SPÄTER** + +**DB:** +- `fitness_tests` table + +**Test Definitions:** +```python +FITNESS_TESTS = { + 'cooper': { + 'name': 'Cooper-Test', + 'description': '12 Minuten laufen, maximale Distanz', + 'unit': 'meters', + 'interval_weeks': 6, + 'norm_tables': { # Simplified + 'male_30-39': {'very_good': 2800, 'good': 2500, 'average': 2200}, + 'female_30-39': {'very_good': 2500, 'good': 2200, 'average': 1900} + }, + 'calculate_vo2max': lambda distance: (distance - 504.9) / 44.73 + }, + 'pushups': { + 'name': 'Liegestütze-Test', + 'description': 'Maximale Anzahl ohne Pause', + 'unit': 'reps', + 'interval_weeks': 4, + 'norm_tables': { ... } + }, + # ... weitere Tests +} +``` + +**UI:** +- Tests Page mit Testliste +- Test Execution Flow (Anleitung → Eingabe → Auswertung) +- Test History mit Trend-Chart + +--- + +## 7. Priorisierte Reihenfolge + +### SOFORT (3-4h) +**Phase 0a:** Minimal Goal System (Strategic + Tactical) +- Basis für alles andere +- User kann Ziele setzen +- Score-Berechnungen können goal_mode nutzen + +### DIESE WOCHE (16-20h) +**Phase 0b:** Goal-Aware Placeholders +- 84 Platzhalter aus v2 +- 35+ Platzhalter aus v9e +- **TOTAL: 120+ Platzhalter** + +### PARALLEL (4-6h) +**Phase 0c:** Training Phases +- Automatische Erkennung +- Phase-aware Recommendations + +### SPÄTER (4-6h) +**Phase 0d:** Fitness Tests +- Enhancement, nicht kritisch für Charts + +--- + +## 8. Kritische Erkenntnisse + +### 1. GOALS_VITALS.md ist detaillierter +- Konkrete Implementierungs-Specs +- DB-Schema-Vorschläge +- 13 definierte KI-Platzhalter +- **ABER:** Fehlt Score-Gewichtung (das hat v2) + +### 2. Konzept v2 ist strategischer +- Goal Modes mit Score-Gewichtung +- Chart-Interpretationen +- Regelbasierte Logik +- **ABER:** Fehlt konkrete Ziel-Tracking (das hat v9e) + +### 3. Beide zusammen = Vollständig +- v2 (Goal Modes) + v9e (Goal Targets) = Komplettes Zielesystem +- v2 (Scores) + v9e (Tests) = Vollständiges Assessment +- v2 (Charts) + v9e (Phases) = Kontext-aware Visualisierung + +### 4. Meine ursprüngliche Analyse war incomplete +- Ich hatte nur v2 betrachtet +- v9e fügt kritische Details hinzu +- **Neue Gesamt-Schätzung:** 120+ Platzhalter (statt 84) + +--- + +## 9. Aktualisierte Empfehlung + +**JA zu Phase 0a (Minimal Goal System), ABER erweitert:** + +### Was Phase 0a umfassen muss (3-4h): + +1. **Strategic Layer (aus v2):** + - goal_mode in profiles + - GOAL_MODES Definition + - GET/SET endpoints + +2. **Tactical Layer (aus v9e):** + - goals Tabelle + - CRUD für Ziele + - Fortschritts-Berechnung + +3. **UI:** + - Goal Mode Selector (Settings) + - Goal Management Page (Basic) + - Dashboard Goal Widget + +### Was kann warten: +- Training Phases → Phase 0c (parallel) +- Fitness Tests → Phase 0d (später) +- Vollständige Test-Integration → v9f + +--- + +## 10. Nächste Schritte + +**JETZT:** +1. Phase 0a implementieren (3-4h) + - Strategic + Tactical Goal System +2. Dann Phase 0b (Goal-Aware Placeholders, 16-20h) +3. Parallel Phase 0c (Training Phases, 4-6h) + +**Soll ich mit Phase 0a (erweitert) starten?** +- Beide Goal-Konzepte integriert +- Ready für 120+ Platzhalter +- Basis für intelligentes Coach-System + +**Commit:** ae93b9d (muss aktualisiert werden) +**Neue Analyse:** GOALS_SYSTEM_UNIFIED_ANALYSIS.md diff --git a/docs/GOAL_SYSTEM_PRIORITY_ANALYSIS.md b/docs/GOAL_SYSTEM_PRIORITY_ANALYSIS.md new file mode 100644 index 0000000..e9cac76 --- /dev/null +++ b/docs/GOAL_SYSTEM_PRIORITY_ANALYSIS.md @@ -0,0 +1,538 @@ +# Zielesystem: Prioritäts-Analyse + +**Datum:** 26. März 2026 +**Frage:** Zielesystem vor oder nach Platzhaltern/Charts? +**Antwort:** **Minimales Zielesystem VOR Platzhaltern, volles System parallel** + +--- + +## 1. Kritische Erkenntnis aus Fachkonzept + +### Zitat Fachkonzept (Zeile 20-28): +> **Wichtig ist, dass das System zielabhängig interpretiert:** +> - Gewichtsreduktion +> - Muskel-/Kraftaufbau +> - Konditions-/Ausdaueraufbau +> - Körperrekomposition +> - allgemeine Gesundheit +> +> **Dasselbe Rohsignal kann je nach Ziel anders bewertet werden.** +> Ein Kaloriendefizit ist z. B. bei Gewichtsreduktion oft positiv, +> bei Kraftaufbau aber potenziell hinderlich. + +### Konsequenz +❌ **Charts OHNE Zielesystem = falsche Interpretationen** +✅ **Charts MIT Zielesystem = korrekte, zielspezifische Aussagen** + +--- + +## 2. Abhängigkeits-Matrix + +### Was hängt vom Zielesystem ab? + +| Komponente | Zielabhängig? | Beispiel | +|------------|---------------|----------| +| **Rohdaten-Charts** | ❌ Nein | Gewichtsverlauf, Umfänge-Trend | +| **Score-Gewichtung** | ✅ JA | Body Progress Score: 30% bei weight_loss, 20% bei strength | +| **Interpretationen** | ✅ JA | Kaloriendefizit: "gut" bei weight_loss, "kritisch" bei strength | +| **Hinweise** | ✅ JA | "Gewicht stagniert" → bei weight_loss: Warnung, bei strength: egal | +| **Platzhalter (Berechnungen)** | ⚠️ TEILWEISE | Trends: Nein, Scores: JA | +| **KI-Prompts** | ✅ JA | Analyse-Kontext ändert sich komplett | + +### Fachkonzept: Score-Gewichtung (Zeile 185-216) + +```yaml +score_weights: + weight_loss: + body_progress: 0.30 # Körper wichtig + nutrition: 0.25 + activity: 0.20 + recovery: 0.15 + health_risk: 0.10 + + strength: + body_progress: 0.20 + nutrition: 0.25 + activity: 0.30 # Training wichtiger + recovery: 0.20 + health_risk: 0.05 # Weniger kritisch + + endurance: + body_progress: 0.10 # Körper unwichtiger + activity: 0.35 # Training am wichtigsten + recovery: 0.25 # Recovery sehr wichtig +``` + +### Beispiel: Body Progress Score + +**OHNE Zielesystem:** +```python +def calculate_body_progress_score(): + # Generisch, für niemanden wirklich passend + fm_delta_score = calculate_fm_change() # -5kg + lbm_delta_score = calculate_lbm_change() # -2kg + return (fm_delta_score + lbm_delta_score) / 2 + # Score: 50/100 (FM gut runter, aber LBM auch runter) +``` + +**MIT Zielesystem:** +```python +def calculate_body_progress_score(goal_mode): + fm_delta_score = calculate_fm_change() # -5kg + lbm_delta_score = calculate_lbm_change() # -2kg + + if goal_mode == "weight_loss": + # FM runter: sehr gut, LBM runter: tolerierbar wenn nicht zu viel + return 0.70 * fm_delta_score + 0.30 * lbm_delta_score + # Score: 78/100 (FM wichtiger, LBM-Verlust weniger kritisch) + + elif goal_mode == "strength": + # FM runter: ok, LBM runter: SEHR SCHLECHT + return 0.30 * fm_delta_score + 0.70 * lbm_delta_score + # Score: 32/100 (LBM-Verlust ist Hauptproblem!) + + elif goal_mode == "recomposition": + # FM runter: gut, LBM runter: schlecht + return 0.50 * fm_delta_score + 0.50 * lbm_delta_score + # Score: 50/100 (ausgewogen bewertet) +``` + +**Ergebnis:** +- Gleiche Daten (-5kg FM, -2kg LBM) +- ABER: 78/100 bei weight_loss, 32/100 bei strength +- **Ohne Ziel: völlig falsche Bewertung!** + +--- + +## 3. Ziel-Erkennung aus Daten + +### Fachkonzept erwähnt dies NICHT explizit, aber logisch ableitbar: + +**Pattern-Erkennung:** +```python +def suggest_goal_from_data(profile_id): + """Schlägt Ziel basierend auf Daten-Mustern vor.""" + + # Analyse der letzten 28 Tage + training_types = get_training_distribution_28d(profile_id) + nutrition = get_nutrition_pattern_28d(profile_id) + body_changes = get_body_changes_28d(profile_id) + + # Pattern 1: Viel Kraft + viel Protein + LBM steigt + if (training_types['strength'] > 60% and + nutrition['protein_g_per_kg'] > 1.8 and + body_changes['lbm_trend'] > 0): + return { + 'suggested_goal': 'strength', + 'confidence': 'high', + 'reasoning': 'Krafttraining dominant + hohe Proteinzufuhr + Muskelaufbau erkennbar' + } + + # Pattern 2: Viel Cardio + Kaloriendefizit + Gewicht sinkt + if (training_types['endurance'] > 50% and + nutrition['kcal_balance_avg'] < -300 and + body_changes['weight_trend'] < 0): + return { + 'suggested_goal': 'weight_loss', + 'confidence': 'high', + 'reasoning': 'Ausdauertraining + Kaloriendefizit + Gewichtsverlust' + } + + # Pattern 3: Mixed Training + Protein hoch + Gewicht stabil + Rekomposition + if (training_types['mixed'] == True and + nutrition['protein_g_per_kg'] > 1.6 and + abs(body_changes['weight_trend']) < 0.05 and + body_changes['fm_trend'] < 0 and + body_changes['lbm_trend'] > 0): + return { + 'suggested_goal': 'recomposition', + 'confidence': 'medium', + 'reasoning': 'Gemischtes Training + Rekomposition sichtbar (FM↓, LBM↑)' + } + + # Default: Nicht genug Muster erkennbar + return { + 'suggested_goal': 'health', + 'confidence': 'low', + 'reasoning': 'Keine klaren Muster erkennbar, gesundheitsorientiertes Training angenommen' + } +``` + +### Voraussetzungen für Ziel-Erkennung: +1. ✅ Mindestens 21-28 Tage Daten +2. ✅ Training-Type Distribution +3. ✅ Ernährungs-Pattern +4. ✅ Körper-Trends (FM, LBM, Gewicht) +5. ✅ Berechnet → **braucht Platzhalter!** + +**ABER:** Ziel-Erkennung ist **nachgelagert**, nicht Voraussetzung. + +--- + +## 4. Empfohlene Implementierungs-Strategie + +### Hybrid-Ansatz: Minimal-Ziele SOFORT, Voll-System parallel + +## Phase 0a: Minimal-Zielesystem (2-3h) ⭐ **START HIER** + +### Ziel +User kann manuell Ziel setzen, System nutzt es für Berechnungen. + +### Implementierung + +**1. DB-Schema erweitern:** +```sql +-- Migration 023 +ALTER TABLE profiles ADD COLUMN goal_mode VARCHAR(50) DEFAULT 'health'; +ALTER TABLE profiles ADD COLUMN goal_weight DECIMAL(5,2); +ALTER TABLE profiles ADD COLUMN goal_bf_pct DECIMAL(4,1); +ALTER TABLE profiles ADD COLUMN goal_set_date DATE; +ALTER TABLE profiles ADD COLUMN goal_target_date DATE; + +COMMENT ON COLUMN profiles.goal_mode IS + 'Primary goal: weight_loss, strength, endurance, recomposition, health'; +``` + +**2. Goal-Mode Konstanten:** +```python +# backend/goals.py (NEU) +GOAL_MODES = { + 'weight_loss': { + 'label': 'Gewichtsreduktion', + 'description': 'Fettabbau bei Erhalt der Magermasse', + 'score_weights': { + 'body_progress': 0.30, + 'nutrition': 0.25, + 'activity': 0.20, + 'recovery': 0.15, + 'health_risk': 0.10 + }, + 'focus_areas': ['fettmasse', 'gewichtstrend', 'kalorienbilanz', 'protein_sicherung'] + }, + 'strength': { + 'label': 'Kraftaufbau', + 'description': 'Muskelaufbau und Kraftsteigerung', + 'score_weights': { + 'body_progress': 0.20, + 'nutrition': 0.25, + 'activity': 0.30, + 'recovery': 0.20, + 'health_risk': 0.05 + }, + 'focus_areas': ['trainingsqualitaet', 'protein', 'lbm', 'recovery'] + }, + 'endurance': { + 'label': 'Ausdaueraufbau', + 'description': 'Kondition und VO2max verbessern', + 'score_weights': { + 'body_progress': 0.10, + 'nutrition': 0.20, + 'activity': 0.35, + 'recovery': 0.25, + 'health_risk': 0.10 + }, + 'focus_areas': ['trainingsvolumen', 'intensitaetsverteilung', 'vo2max', 'recovery'] + }, + 'recomposition': { + 'label': 'Körperrekomposition', + 'description': 'Fettabbau bei gleichzeitigem Muskelaufbau', + 'score_weights': { + 'body_progress': 0.30, + 'nutrition': 0.25, + 'activity': 0.25, + 'recovery': 0.15, + 'health_risk': 0.05 + }, + 'focus_areas': ['lbm', 'fettmasse', 'protein', 'trainingsqualitaet'] + }, + 'health': { + 'label': 'Allgemeine Gesundheit', + 'description': 'Ausgeglichenes Gesundheits- und Fitnesstraining', + 'score_weights': { + 'body_progress': 0.20, + 'nutrition': 0.20, + 'activity': 0.20, + 'recovery': 0.20, + 'health_risk': 0.20 + }, + 'focus_areas': ['bewegung', 'blutdruck', 'schlaf', 'gewicht', 'regelmaessigkeit'] + } +} +``` + +**3. API-Endpoint:** +```python +# routers/goals.py (NEU) +from fastapi import APIRouter, Depends +from auth import require_auth +from goals import GOAL_MODES + +router = APIRouter(prefix="/api/goals", tags=["goals"]) + +@router.get("/modes") +def get_goal_modes(): + """Return all available goal modes with descriptions.""" + return GOAL_MODES + +@router.get("/current") +def get_current_goal(session: dict = Depends(require_auth)): + """Get user's current goal settings.""" + profile_id = session['profile_id'] + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + """SELECT goal_mode, goal_weight, goal_bf_pct, + goal_set_date, goal_target_date + FROM profiles WHERE id=%s""", + (profile_id,) + ) + row = r2d(cur.fetchone()) + return { + **row, + 'mode_config': GOAL_MODES.get(row['goal_mode'], GOAL_MODES['health']) + } + +@router.post("/set") +def set_goal( + goal_mode: str, + goal_weight: Optional[float] = None, + goal_bf_pct: Optional[float] = None, + target_date: Optional[str] = None, + session: dict = Depends(require_auth) +): + """Set user's goal.""" + if goal_mode not in GOAL_MODES: + raise HTTPException(400, f"Invalid goal_mode. Must be one of: {list(GOAL_MODES.keys())}") + + profile_id = session['profile_id'] + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + """UPDATE profiles + SET goal_mode=%s, goal_weight=%s, goal_bf_pct=%s, + goal_set_date=CURRENT_DATE, goal_target_date=%s + WHERE id=%s""", + (goal_mode, goal_weight, goal_bf_pct, target_date, profile_id) + ) + conn.commit() + + return {"success": True, "goal_mode": goal_mode} +``` + +**4. Frontend UI (Settings.jsx):** +```jsx +// Minimal Goal Selector +function GoalSettings() { + const [goalModes, setGoalModes] = useState({}) + const [currentGoal, setCurrentGoal] = useState(null) + const [selectedMode, setSelectedMode] = useState('health') + + useEffect(() => { + loadGoalModes() + loadCurrentGoal() + }, []) + + const loadGoalModes = async () => { + const modes = await api.getGoalModes() + setGoalModes(modes) + } + + const loadCurrentGoal = async () => { + const goal = await api.getCurrentGoal() + setCurrentGoal(goal) + setSelectedMode(goal.goal_mode || 'health') + } + + const saveGoal = async () => { + await api.setGoal({ + goal_mode: selectedMode, + goal_weight: goalWeight, + goal_bf_pct: goalBfPct, + target_date: targetDate + }) + loadCurrentGoal() + } + + return ( +
+

🎯 Trainingsziel

+ +
+ + +

+ {goalModes[selectedMode]?.description} +

+
+ + {(selectedMode === 'weight_loss' || selectedMode === 'recomposition') && ( +
+ + +
+ )} + + +
+ ) +} +``` + +### Aufwand: 2-3h +- 1h: DB + Backend +- 1h: Frontend UI +- 0.5h: Testing + +--- + +## Phase 0b: Goal-Aware Platzhalter (16-20h) + +**Alle 84 Platzhalter implementieren, ABER:** +- Score-Berechnungen nutzen `goal_mode` von Anfang an +- Beispiel: + +```python +def get_body_progress_score(profile_id: str) -> str: + """Body Progress Score (0-100, goal-dependent).""" + profile = get_profile_data(profile_id) + goal_mode = profile.get('goal_mode', 'health') + + # Hole Gewichte aus goals.GOAL_MODES + weights = GOAL_MODES[goal_mode]['score_weights'] + + # Berechne Sub-Scores + fm_score = calculate_fm_progress(profile_id) + lbm_score = calculate_lbm_progress(profile_id) + weight_score = calculate_weight_progress(profile_id, goal_mode) + + # Gewichte nach Ziel + if goal_mode == 'weight_loss': + total = (0.50 * fm_score + 0.30 * weight_score + 0.20 * lbm_score) + elif goal_mode == 'strength': + total = (0.60 * lbm_score + 0.30 * fm_score + 0.10 * weight_score) + elif goal_mode == 'recomposition': + total = (0.45 * fm_score + 0.45 * lbm_score + 0.10 * weight_score) + else: # health, endurance + total = (0.40 * weight_score + 0.30 * fm_score + 0.30 * lbm_score) + + return f"{int(total)}/100" +``` + +**Resultat:** +- Charts bekommen von Anfang an **korrekte** Scores +- Keine Umarbeitung nötig später +- System ist "smart" ab Tag 1 + +--- + +## Phase 2+: Vollständiges Zielesystem (6-8h) + +**Features:** +1. **Ziel-Erkennung aus Daten** + - Pattern-Analyse (wie oben) + - Vorschlag mit Confidence + - "Passt dein Ziel noch?" Check + +2. **Sekundäre Ziele** + - `goal_mode` = primary + - `secondary_goals[]` = weitere Schwerpunkte + - Gewichtung: 70% primary, 30% secondary + +3. **Ziel-Progression Tracking** + - Fortschritt zum Ziel (%) + - Geschätzte Erreichung (Datum) + - Anpassungs-Vorschläge + +4. **Goal-Aware Charts** + - Priorisierung nach goal_relevance + - Dashboard zeigt ziel-spezifische Charts zuerst + +5. **Goal-Aware KI** + - Prompt-Kontext enthält goal_mode + - KI interpretiert zielspezifisch + +--- + +## 5. Entscheidungs-Matrix + +### Option A: Zielesystem komplett ZUERST +**Aufwand:** 10-12h +**Pro:** +- Alles konsistent von Anfang an +- Keine Umarbeitung +**Contra:** +- Verzögert Platzhalter-Start +- Ziel-Erkennung braucht Platzhalter (Henne-Ei) + +### Option B: Platzhalter ZUERST, dann Ziele +**Aufwand:** 16-20h + später Rework +**Pro:** +- Schneller Start +**Contra:** +- ALLE Scores falsch gewichtet +- Komplette Umarbeitung nötig +- User sehen falsche Werte + +### Option C: HYBRID ⭐ **EMPFOHLEN** +**Aufwand:** 2-3h (Minimal-Ziele) + 16-20h (Goal-Aware Platzhalter) + später 6-8h (Voll-System) +**Pro:** +- ✅ Beste aus beiden Welten +- ✅ Korrekte Scores von Anfang an +- ✅ Keine Umarbeitung +- ✅ Ziel-Erkennung später als Enhancement +**Contra:** +- Keinen signifikanten Nachteil + +--- + +## 6. Empfehlung + +### JA, Zielesystem VOR Platzhaltern – aber minimal! + +**Reihenfolge:** + +1. **Phase 0a (2-3h):** Minimal-Zielesystem + - DB: goal_mode field + - API: Get/Set Goal + - UI: Goal Selector (Settings) + - Default: "health" + +2. **Phase 0b (16-20h):** Goal-Aware Platzhalter + - 84 Platzhalter implementieren + - Scores nutzen goal_mode + - Berechnungen goal-abhängig + +3. **Phase 1 (12-16h):** Charts + - Nutzen goal-aware Platzhalter + - Zeigen korrekte Interpretationen + +4. **Phase 2+ (6-8h):** Vollständiges Zielesystem + - Ziel-Erkennung + - Sekundäre Ziele + - Goal Progression Tracking + +--- + +## 7. Fazit + +**Deine Intuition war 100% richtig!** + +✅ **Ohne Zielesystem:** +- Charts zeigen falsche Interpretationen +- Scores sind generisch und für niemanden passend +- System bleibt "dummer Datensammler" + +✅ **Mit Zielesystem:** +- Charts interpretieren zielspezifisch +- Scores sind individuell gewichtet +- System wird "intelligenter Coach" + +**Nächster Schritt:** Phase 0a implementieren (2-3h), dann Phase 0b mit goal-aware Platzhaltern. + +**Soll ich mit Phase 0a (Minimal-Zielesystem) starten?** diff --git a/docs/GOAL_SYSTEM_REDESIGN_v2.md b/docs/GOAL_SYSTEM_REDESIGN_v2.md new file mode 100644 index 0000000..be6aab1 --- /dev/null +++ b/docs/GOAL_SYSTEM_REDESIGN_v2.md @@ -0,0 +1,729 @@ +# Goal System Redesign v2.0 + +**Datum:** 26. März 2026 +**Status:** 📋 KONZEPTION +**Anlass:** Fundamentale Design-Probleme in Phase 0a identifiziert + +--- + +## 1. Probleme der aktuellen Implementierung (Phase 0a) + +### 1.1 Primärziel zu simplistisch +**Problem:** +- Nur EIN Primärziel erlaubt +- Binäres System (primär/nicht-primär) +- Toggle funktioniert nicht richtig beim Update + +**Realität:** +- User hat MEHRERE Ziele gleichzeitig mit unterschiedlichen Prioritäten +- Beispiel: 30% Abnehmen, 25% Kraft, 25% Ausdauer, 20% Beweglichkeit + +**Lösung:** +→ **Gewichtungssystem** (0-100%, Summe = 100%) + +--- + +### 1.2 Ein Goal Mode zu simpel +**Problem:** +- User muss sich für EINEN Modus entscheiden (weight_loss ODER strength) +- In Realität: Kombinierte Ziele (Abnehmen + Kraft + Ausdauer gleichzeitig) + +**Realität (User-Zitat):** +> "Ich versuche nach einer Operation Kraft und Ausdauer aufzubauen, gleichzeitig Abzunehmen und meine Beweglichkeit und Koordination wieder zu steigern." + +**Lösung:** +→ **Multi-Mode mit Gewichtung** statt Single-Mode + +--- + +### 1.3 Fehlende Current Values +**Problem:** +- `lean_mass` current value = "-" (nicht implementiert) +- `strength`, `flexibility` haben keine Datenquellen +- VO2Max wirft Internal Server Error + +**Lösung:** +→ Alle Goal-Typen mit korrekten Datenquellen verbinden + +--- + +### 1.4 Abstrakte Zieltypen +**Problem:** +- "Kraft" - was bedeutet das? Bankdrücken? Kniebeuge? Gesamt? +- "Beweglichkeit" - welcher Test? Sit-and-Reach? Hüftbeugung? +- Zu unspezifisch für konkrete Messung + +**Lösung:** +→ **Konkrete, messbare Zieltypen** mit standardisierten Tests + +--- + +### 1.5 Blutdruck als einzelner Wert +**Problem:** +- BP braucht ZWEI Werte (systolisch/diastolisch) +- Aktuelles Schema: nur ein `target_value` + +**Lösung:** +→ **Compound Goals** (Ziele mit mehreren Werten) + +--- + +### 1.6 Keine Guidance für User +**Problem:** +- User muss konkrete Zahlen eingeben ohne Kontext +- Was ist ein guter VO2Max Wert? Was ist realistisch? + +**Lösung:** +→ **Richtwerte, Normen, Beispiele** in UI + +--- + +## 2. Redesign-Konzept v2.0 + +### 2.1 Kern-Prinzipien + +**Prinzip 1: Gewichtung statt Priorisierung** +- Alle Ziele haben eine Gewichtung (0-100%) +- Summe aller Gewichtungen = 100% +- KI berücksichtigt Gewichtung in Analysen + +**Prinzip 2: Multi-dimensional statt Singular** +- Kein einzelner "Goal Mode" +- Stattdessen: Gewichtete Kombination von Fokus-Bereichen +- Realitätsnah: User hat mehrere Ziele gleichzeitig + +**Prinzip 3: Konkret statt Abstrakt** +- Jedes Ziel hat klare Messbarkeit +- Standardisierte Tests wo möglich +- Datenquellen eindeutig definiert + +**Prinzip 4: Guidance statt Ratlosigkeit** +- Richtwerte für jedes Ziel +- Alters-/Geschlechts-spezifische Normen +- Beispiele und Erklärungen + +--- + +## 3. Neues Datenmodell + +### 3.1 Fokus-Bereiche (statt Goal Modes) + +**Tabelle: `focus_areas` (NEU)** +```sql +CREATE TABLE focus_areas ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Gewichtete Fokus-Bereiche + weight_loss_pct INT DEFAULT 0, -- 0-100% + muscle_gain_pct INT DEFAULT 0, -- 0-100% + endurance_pct INT DEFAULT 0, -- 0-100% + strength_pct INT DEFAULT 0, -- 0-100% + flexibility_pct INT DEFAULT 0, -- 0-100% + health_pct INT DEFAULT 0, -- 0-100% (Erhaltung, kein spezifisches Ziel) + + -- Constraint: Summe muss 100 sein + CONSTRAINT sum_equals_100 CHECK ( + weight_loss_pct + muscle_gain_pct + endurance_pct + + strength_pct + flexibility_pct + health_pct = 100 + ), + + active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + -- Nur ein aktiver Fokus-Mix pro User + UNIQUE(profile_id, active) WHERE active = true +); + +COMMENT ON TABLE focus_areas IS + 'Weighted focus distribution - replaces single goal_mode. + Example: 30% weight loss + 25% strength + 25% endurance + 20% flexibility = 100%'; +``` + +**Beispiel-Daten:** +```json +// User nach Operation (wie im Feedback beschrieben): +{ + "weight_loss_pct": 30, + "muscle_gain_pct": 20, + "endurance_pct": 25, + "strength_pct": 15, + "flexibility_pct": 10, + "health_pct": 0 +} + +// User reiner Kraftfokus: +{ + "weight_loss_pct": 0, + "muscle_gain_pct": 50, + "strength_pct": 40, + "endurance_pct": 10, + "flexibility_pct": 0, + "health_pct": 0 +} + +// User Gewichtsverlust primär: +{ + "weight_loss_pct": 60, + "muscle_gain_pct": 0, + "endurance_pct": 20, + "strength_pct": 10, + "flexibility_pct": 5, + "health_pct": 5 +} +``` + +--- + +### 3.2 Überarbeitete Goal-Typen + +**Tabelle: `goals` (ÜBERARBEITET)** + +**A) Simple Goals (ein Wert):** +```sql +goal_type: +- 'weight' → kg (aus weight_log) +- 'body_fat_pct' → % (aus caliper_log) +- 'lean_mass' → kg (berechnet: weight - (weight * bf_pct)) +- 'vo2max' → ml/kg/min (aus vitals_baseline) +- 'rhr' → bpm (aus vitals_baseline) +- 'hrv' → ms (aus vitals_baseline) +``` + +**B) Test-based Goals (standardisierte Tests):** +```sql +goal_type: +- 'cooper_test' → Meter (12min Lauf) +- 'pushups_max' → Anzahl +- 'plank_max' → Sekunden +- 'sit_reach' → cm (Beweglichkeit) +- 'squat_1rm' → kg (Kraft Unterkörper) +- 'bench_1rm' → kg (Kraft Oberkörper) +- 'deadlift_1rm' → kg (Kraft Rücken) +``` + +**C) Compound Goals (mehrere Werte):** +```sql +goal_type: +- 'blood_pressure' → systolic/diastolic (mmHg) + → Braucht: target_value_secondary +``` + +**Schema-Erweiterung:** +```sql +ALTER TABLE goals ADD COLUMN goal_weight INT DEFAULT 100; + -- Gewichtung dieses Ziels (0-100%) + -- Summe aller goal_weight für einen User sollte ~100% sein + +ALTER TABLE goals ADD COLUMN target_value_secondary DECIMAL(10,2); + -- Für Compound Goals (z.B. BP diastolisch) + +ALTER TABLE goals ADD COLUMN current_value_secondary DECIMAL(10,2); + -- Aktueller Wert für sekundären Target + +ALTER TABLE goals DROP COLUMN is_primary; + -- Nicht mehr nötig (wird durch goal_weight ersetzt) + +COMMENT ON COLUMN goals.goal_weight IS + 'Weight/priority of this goal (0-100%). + Higher weight = more important in AI scoring. + Sum of all goal_weight should be ~100% per user.'; +``` + +--- + +### 3.3 Datenquellen-Mapping + +**Korrekte Current-Value Extraktion:** + +```python +# backend/routers/goals.py - _get_current_value_for_goal_type() + +GOAL_TYPE_SOURCES = { + # Simple values from existing tables + 'weight': { + 'table': 'weight_log', + 'column': 'weight', + 'order': 'date DESC' + }, + 'body_fat_pct': { + 'table': 'caliper_log', + 'column': 'body_fat_pct', + 'order': 'date DESC' + }, + 'lean_mass': { + 'calculation': 'weight - (weight * body_fat_pct / 100)', + 'requires': ['weight_log', 'caliper_log'] + }, + 'vo2max': { + 'table': 'vitals_baseline', + 'column': 'vo2_max', + 'order': 'date DESC' + }, + 'rhr': { + 'table': 'vitals_baseline', + 'column': 'resting_hr', + 'order': 'date DESC' + }, + 'hrv': { + 'table': 'vitals_baseline', + 'column': 'hrv', + 'order': 'date DESC' + }, + + # Test-based values from fitness_tests + 'cooper_test': { + 'table': 'fitness_tests', + 'filter': "test_type = 'cooper_12min'", + 'column': 'result_value', + 'order': 'test_date DESC' + }, + 'pushups_max': { + 'table': 'fitness_tests', + 'filter': "test_type = 'pushups_max'", + 'column': 'result_value', + 'order': 'test_date DESC' + }, + # ... weitere Tests + + # Compound goals + 'blood_pressure': { + 'table': 'blood_pressure_log', + 'columns': ['systolic', 'diastolic'], # Beide Werte + 'order': 'measured_at DESC' + } +} +``` + +--- + +## 4. UI/UX Redesign + +### 4.1 Fokus-Bereiche Konfigurator + +**Statt 5 einzelne Cards → Slider-Interface:** + +``` +┌─────────────────────────────────────────────────────┐ +│ 🎯 Mein Trainings-Fokus │ +├─────────────────────────────────────────────────────┤ +│ Verschiebe die Regler um deine Prioritäten zu │ +│ setzen. Die Summe muss 100% ergeben. │ +│ │ +│ 📉 Gewichtsverlust [====] 30% │ +│ Schwerpunkt auf Kaloriendefizit & Fettabbau │ +│ │ +│ 💪 Muskelaufbau [===] 20% │ +│ Magermasse steigern, Körperkomposition │ +│ │ +│ 🏃 Ausdauer [====] 25% │ +│ VO2Max, aerobe Kapazität, Pace │ +│ │ +│ 🏋️ Maximalkraft [==] 15% │ +│ 1RM Steigerung, progressive Belastung │ +│ │ +│ 🤸 Beweglichkeit [=] 10% │ +│ Mobilität, Flexibilität, Koordination │ +│ │ +│ ❤️ Allgemeine Gesundheit [ ] 0% │ +│ Erhaltung, präventiv │ +│ │ +│ ────────────────────────────────────────────────── │ +│ Gesamt: 100% ✓ │ +│ │ +│ [Speichern] [Zurücksetzen] │ +└─────────────────────────────────────────────────────┘ +``` + +**Technisch:** +- HTML Range Slider (0-100) +- Live-Update der Summe +- Validierung: Summe muss 100% sein +- Auto-Adjust: Wenn User einen Slider erhöht, andere proportional reduzieren + +--- + +### 4.2 Ziele mit Gewichtung + +**Goal-List mit Gewichtungs-Indikator:** + +``` +┌─────────────────────────────────────────────────────┐ +│ 🎯 Konkrete Ziele │ +├─────────────────────────────────────────────────────┤ +│ ┌───────────────────────────────────────────────┐ │ +│ │ ⚖️ Zielgewicht: 82 kg [30%]│ │ +│ │ Start: 95 kg → Aktuell: 89 kg → Ziel: 82 kg │ │ +│ │ ████████████░░░░░░░░░░ 65% │ │ +│ │ ✓ Voraussichtlich: 15.05.2026 (on track) │ │ +│ │ [✏️] [🗑️] │ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────┐ │ +│ │ 💪 Magermasse: 72 kg [20%]│ │ +│ │ Start: 68 kg → Aktuell: 70.5 kg → Ziel: 72 kg│ │ +│ │ ██████████░░░░░░░░░░░░ 63% │ │ +│ │ ⚠ Prognose: 20.06.2026 (5 Tage später) │ │ +│ │ [✏️] [🗑️] │ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +│ [+ Neues Ziel] │ +└─────────────────────────────────────────────────────┘ + +Summe Gewichtungen: 50% (noch 50% verfügbar) +``` + +**Änderungen:** +- Gewichtung in `[30%]` Badge angezeigt +- Summe unten angezeigt +- Warnung wenn Summe > 100% + +--- + +### 4.3 Ziel-Editor mit Guidance + +**Beispiel: VO2Max Ziel erstellen:** + +``` +┌─────────────────────────────────────────────────────┐ +│ Neues Ziel erstellen │ +├─────────────────────────────────────────────────────┤ +│ Zieltyp │ +│ [VO2 Max ▼] │ +│ │ +│ ℹ️ VO2 Max (ml/kg/min) - Maximale Sauerstoffauf- │ +│ nahme. Misst die aerobe Leistungsfähigkeit. │ +│ │ +│ 📊 Richtwerte (Männer, 35 Jahre): │ +│ Sehr gut: > 48 ml/kg/min │ +│ Gut: 44-48 ml/kg/min │ +│ Durchschn.: 40-44 ml/kg/min │ +│ Unterdurch.: 35-40 ml/kg/min │ +│ │ +│ 🎯 Zielwert │ +│ ┌──────────┬──────────┐ │ +│ │ [ 46 ] │ ml/kg/min│ │ +│ └──────────┴──────────┘ │ +│ Dein aktueller Wert: 42 ml/kg/min (Durchschnitt) │ +│ → Ziel liegt in "Gut"-Bereich ✓ │ +│ │ +│ 📅 Zieldatum (optional) │ +│ [2026-06-30] │ +│ │ +│ ⚖️ Gewichtung │ +│ [==== ] 25% │ +│ Wie wichtig ist dir dieses Ziel? │ +│ │ +│ 💡 Name (optional) │ +│ [Ausdauer für Bergwandern ] │ +│ │ +│ [Ziel erstellen] [Abbrechen] │ +└─────────────────────────────────────────────────────┘ +``` + +**Features:** +- Info-Box mit Erklärung +- Alters-/geschlechtsspezifische Richtwerte +- Live-Feedback zum eingegebenen Wert +- Aktueller Wert automatisch geladen +- Gewichtungs-Slider mit Live-Preview + +--- + +### 4.4 Compound Goals (Blutdruck) + +**Spezial-UI für Blutdruck:** + +``` +┌─────────────────────────────────────────────────────┐ +│ Zieltyp: Blutdruck │ +├─────────────────────────────────────────────────────┤ +│ 🎯 Zielwerte │ +│ │ +│ Systolisch (oberer Wert) │ +│ [ 120 ] mmHg │ +│ │ +│ Diastolisch (unterer Wert) │ +│ [ 80 ] mmHg │ +│ │ +│ ℹ️ WHO/ISH Klassifikation: │ +│ Optimal: < 120/80 mmHg │ +│ Normal: 120-129 / 80-84 mmHg │ +│ Hoch-norm.: 130-139 / 85-89 mmHg │ +│ Hypertonie: ≥ 140/90 mmHg │ +│ │ +│ Dein aktueller Wert: 135/88 mmHg (Hoch-normal) │ +│ Dein Ziel: 120/80 mmHg (Optimal) ✓ │ +│ │ +│ [Ziel erstellen] [Abbrechen] │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +## 5. Scoring-System mit Gewichtung + +### 5.1 Score-Berechnung v2.0 + +**Aktuell (Phase 0a):** +```python +# Feste Gewichtung per goal_mode +SCORE_WEIGHTS = { + "strength": { + "body_progress": 0.35, + "nutrition": 0.30, + # ... + } +} +``` + +**Neu (v2.0):** +```python +def calculate_weighted_score(profile_id): + """ + Berechnet Score basierend auf: + 1. Focus Areas (Multi-dimensional statt single mode) + 2. Goal Weights (individuelle Ziel-Gewichtungen) + """ + + # 1. Hole Focus Areas + focus = get_focus_areas(profile_id) + # → {weight_loss: 30%, muscle_gain: 20%, endurance: 25%, ...} + + # 2. Hole alle Ziele mit Gewichtung + goals = get_goals_with_weights(profile_id) + # → [{type: 'weight', weight: 30%}, {type: 'lean_mass', weight: 20%}, ...] + + # 3. Berechne Basis-Scores + base_scores = { + 'body_composition': calculate_body_score(profile_id), + 'nutrition': calculate_nutrition_score(profile_id), + 'training': calculate_training_score(profile_id), + 'recovery': calculate_recovery_score(profile_id) + } + + # 4. Gewichte Scores nach Focus Areas + weighted_score = 0 + + # Weight Loss Focus → Body Composition + Nutrition wichtiger + if focus['weight_loss_pct'] > 0: + weighted_score += ( + base_scores['body_composition'] * 0.4 + + base_scores['nutrition'] * 0.4 + + base_scores['training'] * 0.1 + + base_scores['recovery'] * 0.1 + ) * (focus['weight_loss_pct'] / 100) + + # Muscle Gain Focus → Body + Nutrition + Training + if focus['muscle_gain_pct'] > 0: + weighted_score += ( + base_scores['body_composition'] * 0.35 + + base_scores['nutrition'] * 0.35 + + base_scores['training'] * 0.25 + + base_scores['recovery'] * 0.05 + ) * (focus['muscle_gain_pct'] / 100) + + # Endurance Focus → Training + Recovery + if focus['endurance_pct'] > 0: + weighted_score += ( + base_scores['training'] * 0.50 + + base_scores['recovery'] * 0.30 + + base_scores['body_composition'] * 0.10 + + base_scores['nutrition'] * 0.10 + ) * (focus['endurance_pct'] / 100) + + # ... weitere Focus Areas + + return { + 'overall_score': round(weighted_score, 1), + 'base_scores': base_scores, + 'focus_weights': focus, + 'goal_weights': [g['weight'] for g in goals] + } +``` + +**Beispiel:** +```python +User: 30% Weight Loss + 25% Endurance + 20% Muscle Gain + 25% Strength + +Base Scores: +- Body Composition: 75/100 +- Nutrition: 80/100 +- Training: 70/100 +- Recovery: 65/100 + +Calculation: +Weight Loss (30%): + = (75*0.4 + 80*0.4 + 70*0.1 + 65*0.1) * 0.30 + = 69.5 * 0.30 = 20.85 + +Endurance (25%): + = (70*0.50 + 65*0.30 + 75*0.10 + 80*0.10) * 0.25 + = 69.0 * 0.25 = 17.25 + +Muscle Gain (20%): + = (75*0.35 + 80*0.35 + 70*0.25 + 65*0.05) * 0.20 + = 74.0 * 0.20 = 14.80 + +Strength (25%): + = (70*0.40 + 80*0.30 + 75*0.20 + 65*0.10) * 0.25 + = 72.5 * 0.25 = 18.13 + +Overall Score = 20.85 + 17.25 + 14.80 + 18.13 = 71.03/100 +``` + +--- + +## 6. Migration-Strategie + +### 6.1 Daten-Migration von Phase 0a + +**Bestehende Daten:** +- `profiles.goal_mode` (single mode) +- `goals` mit `is_primary` + +**Migrations-Logik:** +```sql +-- Migration 023: Goal System Redesign v2.0 + +-- 1. Erstelle focus_areas Tabelle +CREATE TABLE focus_areas (...); + +-- 2. Migriere bestehende goal_mode → focus_areas +INSERT INTO focus_areas (profile_id, weight_loss_pct, muscle_gain_pct, ...) +SELECT + id, + CASE goal_mode + WHEN 'weight_loss' THEN 70 -- 70% Weight Loss + 15% Health + 15% Endurance + WHEN 'strength' THEN 0 + -- ... + END as weight_loss_pct, + CASE goal_mode + WHEN 'strength' THEN 60 + WHEN 'recomposition' THEN 30 + -- ... + END as muscle_gain_pct, + -- ... weitere +FROM profiles +WHERE goal_mode IS NOT NULL; + +-- 3. Erweitere goals Tabelle +ALTER TABLE goals ADD COLUMN goal_weight INT DEFAULT 100; +ALTER TABLE goals ADD COLUMN target_value_secondary DECIMAL(10,2); +ALTER TABLE goals ADD COLUMN current_value_secondary DECIMAL(10,2); + +-- 4. Migriere is_primary → goal_weight +UPDATE goals SET goal_weight = 100 WHERE is_primary = true; +UPDATE goals SET goal_weight = 50 WHERE is_primary = false; + +-- 5. Cleanup (später) +-- ALTER TABLE profiles DROP COLUMN goal_mode; -- nach Verifikation +-- ALTER TABLE goals DROP COLUMN is_primary; -- nach Verifikation +``` + +--- + +## 7. Implementierungs-Phasen + +### Phase 1: Konzeption ✅ (DIESES DOKUMENT) +**Dauer:** - +**Ziel:** Vollständiges Redesign-Konzept + +### Phase 2: Backend Redesign (6-8h) +- Migration 023 erstellen +- `focus_areas` Tabelle + CRUD +- `goals` erweitern (weight, secondary values) +- Datenquellen-Mapping korrigieren (lean_mass, VO2Max fix, etc.) +- Scoring-System v2.0 implementieren + +### Phase 3: Frontend Redesign (8-10h) +- Fokus-Bereiche Slider-UI +- Ziel-Editor mit Guidance (Richtwerte, Normen) +- Gewichtungs-System in Goal-Liste +- Compound Goals UI (Blutdruck zwei Werte) +- Neue Goal-Typen (Tests) integrieren + +### Phase 4: Testing & Refinement (2-3h) +- Migration testen (Phase 0a → v2.0) +- Scoring-Logik verifizieren +- UI/UX Testing +- Edge Cases (Summe ≠ 100%, keine Ziele, etc.) + +**Total: 16-21h** + +--- + +## 8. Offene Fragen / Entscheidungen + +### 8.1 Focus Areas vs Goals Weight +**Frage:** Brauchen wir BEIDE Gewichtungssysteme? +- Focus Areas (Weight Loss 30%, Strength 25%, ...) +- Goal Weights (Ziel "82kg" = 30%, Ziel "VO2Max 46" = 25%, ...) + +**Option A:** NUR Focus Areas +- Einfacher +- Weniger Redundanz +- Aber: Weniger granular + +**Option B:** BEIDE Systeme +- Focus Areas = Strategisch (Richtung) +- Goal Weights = Taktisch (konkrete Prioritäten) +- Komplexer, aber flexibler + +**Empfehlung:** Option B - beide Systeme ergänzen sich + +--- + +### 8.2 Konkrete vs Abstrakte Tests +**Frage:** Wie konkret sollen Strength-Goals sein? + +**Option A:** Sehr konkret +- `bench_press_1rm`, `squat_1rm`, `deadlift_1rm` +- Vorteil: Präzise, messbar +- Nachteil: Viele Goal-Typen + +**Option B:** Abstrakt mit Kontext +- `strength` mit Sub-Type (Bench/Squat/Deadlift) +- Vorteil: Flexibler +- Nachteil: Komplizierteres Schema + +**Empfehlung:** Option A - konkrete Typen, dafür klare Messbarkeit + +--- + +### 8.3 Auto-Update von Current Values +**Frage:** Wie oft sollen current_value aktualisiert werden? + +**Option A:** On-Demand (beim Laden der Goals-Seite) +- Vorteil: Keine Background-Jobs +- Nachteil: Kann verzögert sein + +**Option B:** Trigger-basiert (bei neuem Messwert) +- Vorteil: Immer aktuell +- Nachteil: Mehr Komplexität + +**Empfehlung:** Option A für MVP, Option B später + +--- + +## 9. Nächste Schritte + +### User-Feedback einholen: +1. ✅ Löst das Redesign alle genannten Probleme? +2. ✅ Ist die Fokus-Bereiche UI verständlich? +3. ✅ Sind die konkreten Goal-Typen sinnvoll? +4. ✅ Brauchen wir beide Gewichtungssysteme? +5. ✅ Fehlt noch etwas? + +### Nach Freigabe: +1. Migration 023 schreiben +2. Backend implementieren +3. Frontend implementieren +4. Testing + +--- + +**Erstellt:** 26. März 2026 +**Status:** 📋 WARTET AUF FEEDBACK +**Nächster Schritt:** User-Review & Freigabe diff --git a/docs/KONZEPT_ANALYSE_2026-03-26.md b/docs/KONZEPT_ANALYSE_2026-03-26.md new file mode 100644 index 0000000..23b5798 --- /dev/null +++ b/docs/KONZEPT_ANALYSE_2026-03-26.md @@ -0,0 +1,458 @@ +# Konzept-Analyse: Fachkonzept vs. Gitea Issues + +**Datum:** 26. März 2026 +**Analyst:** Claude Code +**Basis:** `.claude/docs/functional/mitai_jinkendo_konzept_diagramme_auswertungen_v2.md` +**Geprüfte Issues:** #26, #27, alle offenen + +--- + +## 1. Executive Summary + +### Kernerkenntnis +Das Fachkonzept ist **wesentlich umfassender** als die aktuellen Gitea Issues #26 und #27. Es definiert ein 3-stufiges Analyse-System (Deskriptiv → Diagnostisch → Präskriptiv), das weit über einfache Charts und Korrelationen hinausgeht. + +### Strategische Empfehlung +**NICHT** Issues #26 und #27 einzeln implementieren, sondern: +1. **Neu-Strukturierung:** Konzept-basierte Phasen-Issues erstellen +2. **Platzhalter-First:** Erst Berechnungs-Platzhalter implementieren +3. **Dann Visualisierung:** Charts nutzen die Platzhalter +4. **Dann KI-Integration:** KI nutzt regelbasierte Scores + Rohdaten + +--- + +## 2. Analyse: Issue #26 vs. Fachkonzept + +### Issue #26: Charts & Visualisierungen erweitern +**Status:** OPEN +**Priority:** Medium-High +**Aufwand:** 8-10h + +**Definierte Charts:** +- Gewicht-Trends (Line-Chart + Trendlinie) +- Umfänge-Verlauf (Multi-Line) +- Vitalwerte-Trends (RHR, HRV, BP) +- Schlaf-Analyse (Dauer, Phasen) +- Ernährungs-Charts (Kalorien, Makros) + +### Fachkonzept: Diagrammkatalog + +**KÖRPER (K1-K5):** +- K1: Gewichtstrend + Trendkanal + Zielprojektion + - 7d Rolling Median, 28d/90d Trend-Slope + - Prozentuale Zielannäherung + - Regelbasierte Hinweise (zu schnell/langsam) +- K2: Körperzusammensetzung (Gewicht/FM/LBM) + - FM = Gewicht × BF%, LBM = Gewicht × (1-BF%) + - 28d/90d Änderung von FM und LBM +- K3: Umfangs-Panel (8 Mini-Charts) + - Links-Rechts Asymmetrie + - Taille/Hüfte, Taille/Körpergröße +- K4: Rekompositions-Detektor (Quadranten) +- K5: Body Progress Score (0-100) + +**ERNÄHRUNG (E1-E5):** +- E1: Energieaufnahme vs. Verbrauch vs. Gewichtstrend +- E2: Protein adequacy (g/Tag, g/kg, g/kg LBM) +- E3: Makroverteilung + Wochenkonsistenz +- E4: Ernährungs-Adhärenz-Score (0-100) +- E5: Energieverfügbarkeits-Warnung + +**AKTIVITÄT (A1-A8):** +- A1: Trainingsvolumen pro Woche +- A2: Intensitätsverteilung / Zonenbild +- A3: Trainingsqualitäts-Matrix +- A4: Fähigkeiten-Balance / Ability Radar +- A5: Load-Monitoring (interne Last, Monotony, Strain) +- A6: Aktivitäts-Goal-Alignment-Score (0-100) +- A7: Ruhetags-/Recovery-Compliance +- A8: VO2max-Entwicklung + +### Bewertung +❌ **Issue #26 ist zu eng gefasst** +- Fokus nur auf Basis-Visualisierung +- Keine Scores, keine Baselines, keine Confidence +- Keine regelbasierten Hinweise +- Keine Ziel-Abhängigkeit + +✅ **Fachkonzept bietet:** +- 18 dedizierte Charts (K1-K5, E1-E5, A1-A8) +- Scores als eigenständige Visualisierungen +- Regelbasierte Aussagen ohne KI +- Ziel-Modi Steuerung + +--- + +## 3. Analyse: Issue #27 vs. Fachkonzept + +### Issue #27: Korrelationen & Insights erweitern +**Status:** OPEN +**Priority:** High +**Aufwand:** 6-8h + +**Definierte Korrelationen:** +- Schlaf ↔ Erholung (Schlafdauer → RHR, Qualität → HRV) +- Training ↔ Vitalwerte (Load → RHR-Anstieg, HRV-Abfall) +- Ernährung ↔ Performance (Defizit → Intensität) +- Blutdruck ↔ Lifestyle (Stress → BP, Training → BP) +- Multi-Faktor Analyse (KI-Insights) + +### Fachkonzept: Korrelationen (C1-C6) + +**KORRELATIONEN (C1-C6):** +- C1: Energie-Balance vs. Gewichtsveränderung (lagged) + - Lags: 0, 3, 7, 10, 14 Tage + - Bestes Lag ermitteln, Effektstärke, Confidence +- C2: Protein adequacy vs. LBM-Trend + - 28d Fenstervergleich, Training als Moderator +- C3: Trainingslast vs. HRV/RHR (1-3 Tage verzögert) + - Duale Lag-Auswertung, individuelle Ermüdungsreaktion +- C4: Schlafdauer + Schlafregularität vs. Recovery + - Bubble-Chart, Sleep Regularity Index +- C5: Blutdruck-Kontextmatrix (Kontext-abhängig) + - Messkontext, Schlaf Vor-Nacht, Training +- C6: Plateau-Detektor (Ereignis-Karte) + - Ziel-spezifische Plateau-Definitionen + +### Zusätzlich: Lag-Analyse Prinzipien + +**Zwingend im Fachkonzept:** +- **NIE nur lag=0 prüfen** +- Kalorienbilanz → Gewicht: 2-14 Tage Verzögerung +- Protein/Krafttraining → LBM: 2-6 Wochen Verzögerung +- Trainingslast → HRV/RHR: 1-3 Tage Verzögerung +- Schlafdefizit → Recovery: 1-3 Tage Verzögerung + +**Mindestdatenmenge:** +- Korrelationen: mind. 21 gepaarte Tageswerte +- Lag-basiert: mind. 28 gepaarte Tage +- Confidence-Klassen (hoch/mittel/niedrig/nicht auswertbar) + +### Bewertung +❌ **Issue #27 ist zu oberflächlich** +- Keine Lag-Analyse +- Keine Confidence-Bewertung +- Keine Mindestdatenmenge-Checks +- Keine Ziel-Abhängigkeit + +✅ **Fachkonzept bietet:** +- 6 dedizierte Korrelations-Charts mit Lag-Analyse +- Explizite Confidence-Bewertung +- Medizinischer Sicherheitsmodus +- Plateau-Detektion (regelbasiert) + +--- + +## 4. Konflikt-Analyse + +### Gibt es Widersprüche zwischen #26 und #27? +**NEIN** – Sie sind komplementär: +- #26: Deskriptive Ebene (Charts) +- #27: Diagnostische Ebene (Korrelationen) + +### Aber: Beide sind zu isoliert +Das Fachkonzept zeigt: **Charts und Korrelationen müssen verzahnt sein** + +**Beispiel:** +``` +Fachkonzept C1: Energie-Balance vs. Gewichtsveränderung +├─ Visualisierung: Lag-Heatmap (diagnostisch) +├─ Berechnung: Cross-Correlation (0, 3, 7, 10, 14 Tage Lags) +├─ Input-Daten: Tägliche Kalorienbilanz (E-Chart) +├─ Input-Daten: 7d Gewichtsänderung (K-Chart) +└─ Regelbasierte Aussage: "Energiebilanz zeigt sich bei dir nach ~7 Tagen im Gewicht" +``` + +**Fazit:** Charts (K, E, A) liefern Basis-Daten für Korrelationen (C) + +--- + +## 5. Neue Platzhalter aus Fachkonzept + +### 5.1 KÖRPER (18 neue Platzhalter) + +**Gewicht & Trends:** +```python +{{weight_7d_rolling_median}} # 7-Tage gleitender Median +{{weight_28d_trend_slope}} # 28-Tage Trend-Steigung (kg/Tag) +{{weight_90d_trend_slope}} # 90-Tage Trend-Steigung +{{weight_goal_progress_pct}} # Prozentuale Zielannäherung +{{weight_projection_days}} # Geschätzte Tage bis Zielgewicht +{{weight_loss_rate_weekly}} # kg/Woche (28d Mittel) +``` + +**Körperzusammensetzung:** +```python +{{fm_current}} # Fettmasse aktuell (kg) +{{lbm_current}} # Magermasse aktuell (kg) +{{fm_28d_delta}} # FM Änderung 28 Tage (kg) +{{lbm_28d_delta}} # LBM Änderung 28 Tage (kg) +{{fm_90d_delta}} # FM Änderung 90 Tage +{{lbm_90d_delta}} # LBM Änderung 90 Tage +{{recomposition_score}} # 0-100 (FM↓ + LBM↑ = ideal) +``` + +**Umfänge:** +```python +{{waist_to_hip_ratio}} # Taille/Hüfte Verhältnis +{{waist_to_height_ratio}} # Taille/Körpergröße (Gesundheitsmarker) +{{arm_asymmetry_pct}} # Links-Rechts Differenz % +{{leg_asymmetry_pct}} # Oberschenkel L-R Differenz +{{waist_28d_delta}} # Taillenumfang Änderung 28d +``` + +**Body Progress Score:** +```python +{{body_progress_score}} # 0-100 (zielabhängig gewichtet) +``` + +### 5.2 ERNÄHRUNG (15 neue Platzhalter) + +**Energie & Bilanz:** +```python +{{kcal_7d_avg}} # Bereits vorhanden? Prüfen +{{kcal_28d_avg}} # 28-Tage Durchschnitt +{{kcal_estimated_tdee}} # Geschätzter Gesamtumsatz +{{kcal_balance_7d_avg}} # Durchschnittliche Bilanz 7d +{{kcal_balance_28d_avg}} # Durchschnittliche Bilanz 28d +{{energy_availability_status}} # "adequate" | "low" | "critical" +``` + +**Protein:** +```python +{{protein_g_per_kg}} # Protein g/kg Körpergewicht +{{protein_g_per_kg_lbm}} # Protein g/kg Magermasse +{{protein_adequacy_score}} # 0-100 (Ziel: 1.6-2.2 g/kg) +``` + +**Makros & Adhärenz:** +```python +{{carb_pct_7d_avg}} # % der Gesamtkalorien +{{fat_pct_7d_avg}} # % der Gesamtkalorien +{{macro_consistency_score}} # 0-100 (Regelmäßigkeit) +{{nutrition_adherence_score}} # 0-100 (Gesamtscore) +{{nutrition_days_7d}} # Erfasste Tage letzte 7d +{{nutrition_days_28d}} # Erfasste Tage letzte 28d +``` + +### 5.3 AKTIVITÄT (25 neue Platzhalter) + +**Volumen:** +```python +{{activity_volume_7d_min}} # Gesamtminuten 7 Tage +{{activity_volume_28d_min}} # Gesamtminuten 28 Tage +{{activity_frequency_7d}} # Anzahl Sessions 7d +{{activity_frequency_28d}} # Anzahl Sessions 28d +{{activity_avg_duration_28d}} # Durchschn. Dauer pro Session +``` + +**Intensität:** +```python +{{activity_z1_pct}} # % Zeit in Zone 1 (7d) +{{activity_z2_pct}} # % Zeit in Zone 2 +{{activity_z3_pct}} # % Zeit in Zone 3 +{{activity_z4_pct}} # % Zeit in Zone 4 +{{activity_z5_pct}} # % Zeit in Zone 5 +{{activity_polarization_index}} # Polarisierung (Z1+Z2 vs Z4+Z5) +``` + +**Qualität & Load:** +```python +{{activity_quality_avg_28d}} # Durchschn. Quality-Score +{{activity_load_7d}} # Interne Last (7d Summe) +{{activity_load_28d}} # Interne Last (28d Summe) +{{activity_monotony_28d}} # Last-Variabilität +{{activity_strain_28d}} # Load × Monotony +{{activity_acwr}} # Acute:Chronic Workload Ratio +``` + +**Fähigkeiten:** +```python +{{ability_strength_score}} # 0-100 (aus Training Types) +{{ability_endurance_score}} # 0-100 +{{ability_mobility_score}} # 0-100 +{{ability_skills_score}} # 0-100 +{{ability_mindfulness_score}} # 0-100 +{{ability_balance_score}} # 0-100 (wie ausgewogen?) +``` + +**Goal Alignment:** +```python +{{activity_goal_alignment_score}} # 0-100 (zielabhängig) +{{rest_days_compliance}} # 0-100 (geplant vs. tatsächlich) +``` + +### 5.4 RECOVERY & GESUNDHEIT (12 neue Platzhalter) + +**Baselines:** +```python +{{rhr_7d_baseline}} # 7-Tage Baseline Ruhepuls +{{rhr_28d_baseline}} # 28-Tage Baseline +{{hrv_7d_baseline}} # 7-Tage Baseline HRV +{{hrv_28d_baseline}} # 28-Tage Baseline +``` + +**Deltas & Trends:** +```python +{{rhr_vs_baseline_7d}} # Abweichung von Baseline (bpm) +{{hrv_vs_baseline_7d}} # Abweichung von Baseline (ms) +{{vo2max_trend_28d}} # VO2max Entwicklung +``` + +**Scores:** +```python +{{recovery_score}} # 0-100 (HRV, RHR, Schlaf) +{{recovery_score_confidence}} # 0-100 (Datenqualität) +{{sleep_regularity_index}} # Schlafregelmäßigkeit +{{sleep_debt_hours}} # Akkumulierte Schlafschuld +{{health_risk_score}} # 0-100 (Blutdruck, etc.) +``` + +### 5.5 KORRELATIONEN (8 neue Platzhalter) + +```python +{{corr_energy_weight_lag}} # Bestes Lag Energie→Gewicht (Tage) +{{corr_energy_weight_r}} # Korrelationskoeffizient +{{corr_protein_lbm_r}} # Protein ↔ LBM Korrelation +{{corr_load_hrv_lag}} # Bestes Lag Load→HRV +{{corr_load_hrv_r}} # Korrelation +{{corr_sleep_rhr_r}} # Schlaf ↔ RHR Korrelation +{{plateau_detected}} # true|false (regelbasiert) +{{plateau_type}} # "weight_loss" | "strength" | etc. +``` + +### 5.6 META-PLATZHALTER (6 neue) + +```python +{{goal_mode}} # "weight_loss" | "strength" | etc. +{{training_age_weeks}} # Trainingserfahrung +{{data_quality_score}} # 0-100 (Gesamtdatenqualität) +{{measurement_consistency}} # 0-100 (Messzeit-Konsistenz) +{{analysis_confidence}} # "high" | "medium" | "low" +{{analysis_timeframe}} # "7d" | "28d" | "90d" +``` + +--- + +## 6. Gesamt-Übersicht: Neue Platzhalter + +| Kategorie | Anzahl | Beispiele | +|-----------|--------|-----------| +| KÖRPER | 18 | weight_28d_trend_slope, fm_28d_delta, recomposition_score | +| ERNÄHRUNG | 15 | protein_g_per_kg_lbm, nutrition_adherence_score, energy_availability_status | +| AKTIVITÄT | 25 | activity_quality_avg_28d, activity_strain_28d, ability_balance_score | +| RECOVERY | 12 | recovery_score, sleep_regularity_index, sleep_debt_hours | +| KORRELATIONEN | 8 | corr_energy_weight_lag, plateau_detected, corr_load_hrv_r | +| META | 6 | goal_mode, data_quality_score, analysis_confidence | +| **GESAMT** | **84** | **Neue Platzhalter aus Fachkonzept** | + +--- + +## 7. Strategische Roadmap-Empfehlung + +### Phase 0: Fundament (JETZT) +**Ziel:** Berechnungs-Platzhalter implementieren +**Aufwand:** 16-20h +**Deliverables:** +- 84 neue Platzhalter in `placeholder_resolver.py` +- Baseline-Berechnungen (7d, 28d, 90d) +- Score-Algorithmen (Body Progress, Nutrition Adherence, Activity Goal Alignment, Recovery) +- Lag-Korrelations-Funktionen +- Confidence-Berechnung + +**Issues zu erstellen:** +- #52: Baseline & Trend Calculations (Körper, Ernährung, Aktivität) +- #53: Score Algorithms (4 Haupt-Scores) +- #54: Correlation & Lag Analysis +- #55: Confidence & Data Quality Metrics + +### Phase 1: Visualisierung (DANN) +**Ziel:** Charts nutzen die neuen Platzhalter +**Aufwand:** 12-16h +**Deliverables:** +- K1-K5 Charts (Körper) +- E1-E5 Charts (Ernährung) +- A1-A8 Charts (Aktivität) +- C1-C6 Charts (Korrelationen) + +**Issues zu konsolidieren:** +- #26 erweitern zu "Comprehensive Chart System (K, E, A, C)" +- #27 erweitern zu "Correlation & Lag Analysis Charts" + +### Phase 2: Regelbasierte Insights (DANACH) +**Ziel:** System wird Coach (nicht nur Datensammler) +**Aufwand:** 8-12h +**Deliverables:** +- Regelbasierte Hinweise ohne KI +- Plateau-Detektion +- Ziel-abhängige Interpretationen +- Warnungen (Gesundheit, Übertraining, Energieverfügbarkeit) + +**Neue Issues:** +- #56: Rule-Based Recommendations Engine +- #57: Goal-Mode System & Interpretation +- #58: Health & Safety Warnings + +### Phase 3: KI-Integration (SPÄTER) +**Ziel:** KI nutzt Scores + Rohdaten + Regeln +**Aufwand:** 6-8h +**Deliverables:** +- KI-Prompts nutzen neue Platzhalter +- Contextual AI Analysis (nutzt goal_mode) +- Multi-Faktor Insights + +--- + +## 8. Aktions-Empfehlungen + +### SOFORT (heute) +1. ✅ **Issues #26 und #27 NICHT einzeln implementieren** +2. ✅ **Neues Issue #52 erstellen:** Baseline & Trend Calculations +3. ✅ **Neues Issue #53 erstellen:** Score Algorithms +4. ✅ **Issue #26 umbennen/erweitern:** "Comprehensive Chart System (based on Fachkonzept)" +5. ✅ **Issue #27 umbennen/erweitern:** "Correlation & Lag Analysis (based on Fachkonzept)" + +### DIESE WOCHE +6. ✅ **Implementierung starten:** Phase 0 - Platzhalter +7. ✅ **Dokumentation:** Mapping Fachkonzept → Code +8. ✅ **KI-Prompts vorbereiten:** Nutzen neue Platzhalter + +### NÄCHSTE WOCHE +9. ✅ **Implementierung:** Phase 1 - Charts +10. ✅ **Testing:** Alle Scores & Berechnungen +11. ✅ **Production:** Deployment vorbereiten + +--- + +## 9. Zusammenfassung: Transformation Data Collector → Active Coach + +### Aktueller Stand +**Data Collector:** +- Daten werden erfasst +- Einfache Listen +- Basis-Statistiken +- KI-Analysen manuell angestoßen + +### Ziel (nach Fachkonzept) +**Active Coach:** +- Daten werden **interpretiert** +- Trends & Baselines +- Scores & Confidence +- Regelbasierte Hinweise +- Ziel-abhängige Bewertung +- Proaktive Warnungen +- KI nutzt strukturierte Insights + +--- + +## 10. Nächste Schritte + +1. **Issues neu strukturieren** (heute) +2. **Platzhalter implementieren** (Phase 0, diese Woche) +3. **Charts implementieren** (Phase 1, nächste Woche) +4. **Regelbasierte Insights** (Phase 2, Woche danach) +5. **KI-Integration** (Phase 3, dann) + +**Commit:** cd2609d +**Analysiert von:** Claude Code +**Basis:** Fachkonzept v2 (2086 Zeilen, 24.03.2026) diff --git a/docs/NEXT_STEPS_2026-03-26.md b/docs/NEXT_STEPS_2026-03-26.md new file mode 100644 index 0000000..3528257 --- /dev/null +++ b/docs/NEXT_STEPS_2026-03-26.md @@ -0,0 +1,460 @@ +# Nächste Schritte nach Phase 0a + +**Stand:** 26. März 2026, nach Completion von Phase 0a (Goal System) +**Aktueller Branch:** `develop` +**Deployed:** `dev.mitai.jinkendo.de` + +--- + +## Aktueller Stand ✅ + +### Abgeschlossen +- ✅ **Phase 0a:** Minimal Goal System (Strategic + Tactical) + - Migration 022, goals.py Router, GoalsPage UI + - Navigation von Dashboard + Analysis + - Mobile-friendly Design + - **Basis vorhanden für 120+ goal-aware Platzhalter** + +### Offene Gitea Issues +- 🔲 **#49:** Prompt-Zuordnung zu Verlaufsseiten (6-8h) +- 🔲 **#47:** Wertetabelle Optimierung (4-6h) +- 🔲 **#46:** KI Prompt-Ersteller (später) +- 🔲 **#45:** KI Prompt-Optimierer (später) +- 🔲 **#43, #42:** Enhanced Debug UI (später) + +--- + +## Option A: Issue #49 - Prompt Page Assignment ⚡ + +**Aufwand:** 6-8 Stunden +**Priorität:** Medium +**Typ:** UX Enhancement +**Labels:** feature, ux, enhancement + +### Beschreibung +KI-Prompts flexibel auf verschiedenen Verlaufsseiten verfügbar machen. Jeder Prompt kann auf mehreren Seiten gleichzeitig angeboten werden (Mehrfachauswahl). + +### Problem +**Aktuell:** +- Prompts nur über zentrale Analyse-Seite verfügbar +- Kein kontextbezogener Zugriff auf relevante Analysen +- User muss immer zur Analyse-Seite navigieren + +**Beispiel-Szenario:** +``` +User ist auf: Gewicht → Verlauf +Will: Gewichtstrend analysieren +Muss: Zur Analyse-Seite → Prompt auswählen → Zurück +``` + +**Wünschenswert:** +``` +User ist auf: Gewicht → Verlauf +Sieht: "🤖 KI-Analyse" Widget mit relevanten Prompts +Kann: Direkt "Gewichtstrend-Analyse" starten +``` + +### Technische Umsetzung + +**Backend (2h):** +```sql +-- Migration 023 +ALTER TABLE ai_prompts ADD COLUMN available_on JSONB DEFAULT '["analysis"]'; + +-- Beispiel: +{ + "slug": "weight_trend", + "available_on": ["analysis", "weight_history"] +} +``` + +**API:** +```python +# Neuer Endpoint +GET /api/prompts/for-page/{page_slug} + → Returns: List[Prompt] where available_on contains page_slug + +# CRUD erweitern +PUT /api/prompts/unified/{id} + → Body: {..., "available_on": ["analysis", "weight_history"]} +``` + +**Frontend (4h):** +```javascript +// Wiederverwendbare Komponente + + +// UnifiedPromptModal erweitern +const PAGE_OPTIONS = [ + { value: 'analysis', label: '📊 Analyse (Hauptseite)', default: true }, + { value: 'weight_history', label: '⚖️ Gewicht → Verlauf' }, + { value: 'nutrition_history', label: '🍎 Ernährung → Verlauf' }, + // ... 9 Optionen total +] + +// Multi-select checkboxes in Prompt-Editor +``` + +**Integration in Verlaufsseiten (2h):** +- WeightPage, NutritionPage, ActivityPage erweitern +- Widget unterhalb Charts einfügen +- Modal für Inline-Analyse + +### Vorteile +- ✅ Schneller Nutzen (UX-Verbesserung sofort sichtbar) +- ✅ Nutzt bestehendes Unified Prompt System (Issue #28) +- ✅ Relativ einfache Implementierung +- ✅ Bereitet vor für Phase 0b (neue Platzhalter dann sofort auf allen Seiten nutzbar) + +### Nachteile +- ⚠️ Verzögert strategische Tiefe (goal-aware Analysen) +- ⚠️ Erst sinnvoll wenn mehr Prompts existieren + +**Dokumentation:** Siehe `docs/issues/issue-51-prompt-page-assignment.md` + +--- + +## Option B: Phase 0b - Goal-Aware Placeholders 🎯 + +**Aufwand:** 16-20 Stunden +**Priorität:** High (strategisch kritisch) +**Typ:** Core Feature +**Labels:** feature, ai, goal-system + +### Beschreibung +Implementierung von 120+ neuen KI-Platzhaltern die `goal_mode` berücksichtigen. Verwandelt System von "Datensammler" zu "intelligentem Coach". + +### Problem +**Aktuell:** +- Ziele existieren, aber KI-Analysen ignorieren sie +- Gleiche Daten werden für alle goal_modes gleich interpretiert +- Keine goal-spezifischen Score-Berechnungen + +**Beispiel:** +```python +# Gleiche Messung: -5kg FM, -2kg LBM +# Aktuell: Generischer Score (z.B. 50/100) + +# Mit Phase 0b: +goal_mode = "weight_loss" → 78/100 (FM↓ gut!) +goal_mode = "strength" → 32/100 (LBM↓ Katastrophe!) +goal_mode = "recomposition" → 65/100 (beides relevant) +``` + +### Technische Umsetzung + +**1. Placeholder Functions (8-10h):** + +**Kategorie: KÖRPER (18 neue):** +```python +def weight_7d_rolling_median(profile_id, goal_mode): + """Rolling median statt avg für Stabilität""" + +def weight_28d_trend_slope(profile_id, goal_mode): + """Linear regression slope - kg/Woche""" + +def fm_28d_delta(profile_id, goal_mode): + """Fettmasse-Veränderung 28 Tage""" + +def lbm_28d_delta(profile_id, goal_mode): + """Magermasse-Veränderung 28 Tage""" + +def recomposition_score(profile_id, goal_mode): + """FM↓ + LBM↑ Balance-Score""" + # Nur relevant wenn goal_mode = "recomposition" + +def waist_to_hip_ratio(profile_id): + """WHR - Bauchfettverteilung""" + +def waist_to_height_ratio(profile_id): + """WHtR - Gesundheitsrisiko""" +``` + +**Kategorie: ERNÄHRUNG (15 neue):** +```python +def protein_g_per_kg(profile_id, goal_mode): + """Protein pro kg Körpergewicht""" + # Target abhängig von goal_mode: + # strength: 2.0-2.2g/kg + # weight_loss: 1.8-2.0g/kg + # endurance: 1.4-1.6g/kg + +def protein_g_per_kg_lbm(profile_id): + """Protein pro kg Magermasse (präziser)""" + +def nutrition_adherence_score(profile_id, goal_mode): + """Wie gut hält User seine Makro-Ziele ein?""" + # Ziele abhängig von goal_mode + +def energy_availability_status(profile_id): + """kcal - activity_kcal - BMR = verfügbare Energie""" + # RED-S Warnung wenn < 30 kcal/kg LBM +``` + +**Kategorie: AKTIVITÄT (25 neue):** +```python +def activity_quality_avg_28d(profile_id): + """Durchschnittliche Trainingsqualität""" + +def activity_strain_28d(profile_id): + """Kumulierte Belastung (Monotonie-Detektion)""" + +def activity_monotony_28d(profile_id): + """Variation im Training (Plateaus erkennen)""" + +def ability_balance_score(profile_id, goal_mode): + """Balance zwischen Fähigkeiten (Strength/Cardio/Mobility)""" + # Gewichtung abhängig von goal_mode +``` + +**Kategorie: RECOVERY (12 neue):** +```python +def recovery_score(profile_id): + """ + Kombiniert: RHR + HRV + Sleep Quality + Rest Days + Score: 0-100 + """ + +def sleep_regularity_index(profile_id): + """Wie regelmäßig sind Schlafzeiten? (0-100)""" + +def sleep_debt_hours(profile_id): + """Kumulierte Schlafdifferenz zu Ziel""" +``` + +**Kategorie: KORRELATIONEN (8 neue):** +```python +def corr_energy_weight_lag(profile_id): + """ + Korrelation Kaloriendefizit → Gewicht + Mit Lag-Analysis (verzögerte Effekte) + Confidence-Score basierend auf Datenmenge + """ + +def plateau_detected(profile_id): + """ + Boolean: Gewicht stagniert trotz Defizit? + Trigger für Interventionen + """ +``` + +**Kategorie: META (6 neue):** +```python +def goal_mode(profile_id): + """Aktueller goal_mode (für Prompts verfügbar)""" + +def data_quality_score(profile_id): + """Wie vollständig/konsistent sind Daten? (0-100)""" + +def profile_age_years(profile_id): + """Alter für altersabhängige Normen""" +``` + +**2. Score-Gewichtung (4-6h):** + +```python +# backend/score_calculator.py (NEU) + +SCORE_WEIGHTS = { + "weight_loss": { + "body_progress": 0.30, # FM↓ wichtig + "nutrition": 0.25, # Defizit wichtig + "training_quality": 0.15, # Moderat wichtig + "recovery": 0.15, # Moderat wichtig + "adherence": 0.15 # Konsistenz wichtig + }, + "strength": { + "body_progress": 0.35, # LBM↑ KRITISCH + "nutrition": 0.30, # Surplus + Protein + "training_quality": 0.25, # Progressive Overload + "recovery": 0.10 # Weniger wichtig + }, + "endurance": { + "training_quality": 0.40, # VO2Max, Pace wichtig + "recovery": 0.25, # Übertraining vermeiden + "body_progress": 0.15, # Gewicht sekundär + "nutrition": 0.20 # Energie-Verfügbarkeit + }, + # ... recomposition, health +} + +def calculate_overall_score(profile_id, goal_mode): + """Berechnet Gesamt-Score basierend auf goal_mode Gewichtung""" + weights = SCORE_WEIGHTS[goal_mode] + + scores = { + "body_progress": calculate_body_progress_score(profile_id, goal_mode), + "nutrition": calculate_nutrition_score(profile_id, goal_mode), + "training_quality": calculate_training_score(profile_id, goal_mode), + "recovery": calculate_recovery_score(profile_id), + "adherence": calculate_adherence_score(profile_id, goal_mode) + } + + overall = sum(scores[key] * weights[key] for key in weights) + return { + "overall": round(overall, 1), + "breakdown": scores, + "weights": weights + } +``` + +**3. Baseline-Berechnungen (2-3h):** + +```python +def calculate_baselines(profile_id): + """ + Berechnet persönliche Referenzwerte: + - 7d baseline (kurzfristig) + - 28d baseline (mittelfristig) + - 90d baseline (langfristig) + + Für: Gewicht, RHR, HRV, Kalorien, Protein, etc. + """ + +def detect_anomalies(profile_id, metric, value): + """ + Ist Wert außerhalb von ±2 SD vom Baseline? + → Warnung für User + """ +``` + +**4. Integration in Prompts (1-2h):** + +```python +# Beispiel Prompt-Template: +""" +Du bist ein KI-Coach für {{goal_mode}} Training. + +Aktueller Status: +- Gewichtstrend: {{weight_28d_trend_slope}} kg/Woche +- Fettmasse Δ28d: {{fm_28d_delta}} kg +- Magermasse Δ28d: {{lbm_28d_delta}} kg +- Rekompositions-Score: {{recomposition_score}}/100 + +Ernährung: +- Protein/kg: {{protein_g_per_kg}} g/kg (Ziel: {{protein_target_for_mode}}) +- Adherence: {{nutrition_adherence_score}}/100 + +Training: +- Qualität (28d): {{activity_quality_avg_28d}}/5.0 +- Monotonie: {{activity_monotony_28d}} (Warnung bei >2.0) + +Recovery: +- Recovery Score: {{recovery_score}}/100 +- Schlafschuld: {{sleep_debt_hours}}h + +Gesamt-Score ({{goal_mode}}-optimiert): {{overall_score}}/100 + +Analyse den Fortschritt aus Sicht eines {{goal_mode}} Ziels... +""" +``` + +### Vorteile +- ✅ Größter strategischer Impact (System wird intelligent) +- ✅ Ziele werden tatsächlich genutzt (nicht nur Display) +- ✅ Basis für alle zukünftigen Features +- ✅ Automatische Trainingsphasen-Erkennung möglich + +### Nachteile +- ⚠️ Hoher Aufwand (16-20h) +- ⚠️ Komplexe Logik (viel Testing nötig) +- ⚠️ Erfordert mehr Daten für sinnvolle Scores + +--- + +## Option C: Issue #47 - Value Table Refinement 🔬 + +**Aufwand:** 4-6 Stunden +**Priorität:** Low (Polishing) +**Typ:** Enhancement + +### Beschreibung +Wertetabelle übersichtlicher gestalten - Normal-Modus nur Einzelwerte, Experten-Modus mit Stage-Rohdaten. + +### Vorteile +- ✅ Bessere UX für Value Table +- ✅ Weniger Überforderung im Normal-Modus + +### Nachteile +- ⚠️ Kosmetisch, kein funktionaler Impact +- ⚠️ Besser warten bis Phase 0b (dann 120+ Platzhalter) + +**Empfehlung:** Später (nach Phase 0b) + +--- + +## Empfehlung 🎯 + +### Szenario 1: "Quick Wins first" +``` +1. Issue #49 - Prompt Assignment (6-8h) + → Bessere UX sofort + +2. Phase 0b - Goal-Aware Placeholders (16-20h) + → Neue Platzhalter profitieren von Page Assignment + → Volle Power mit beiden Features + +Total: 22-28h +``` + +### Szenario 2: "Strategic Depth first" +``` +1. Phase 0b - Goal-Aware Placeholders (16-20h) + → System wird intelligent + +2. Issue #49 - Prompt Assignment (6-8h) + → Intelligente Prompts dann auf allen Seiten + +Total: 22-28h +``` + +### Persönliche Empfehlung: **Szenario 1** + +**Begründung:** +- Issue #49 ist relativ einfach und bringt sofort UX-Nutzen +- Nutzt bestehendes Unified Prompt System optimal +- Phase 0b profitiert dann von besserer Navigation +- User kann neue Platzhalter (Phase 0b) direkt auf relevanten Seiten nutzen +- Psychologisch: Zwei Erfolgserlebnisse statt einem großen + +--- + +## Nächste Session: Action Items + +**Falls Issue #49 gewählt:** +1. [ ] Migration 023 erstellen (available_on JSONB) +2. [ ] Backend: `/api/prompts/for-page/{slug}` Endpoint +3. [ ] Backend: CRUD erweitern (available_on in PUT) +4. [ ] Frontend: PAGE_OPTIONS in UnifiedPromptModal +5. [ ] Frontend: PagePrompts Komponente (wiederverwendbar) +6. [ ] Integration: WeightPage, NutritionPage, ActivityPage +7. [ ] Testing: Multi-select, Modal-Inline-Analyse + +**Falls Phase 0b gewählt:** +1. [ ] Placeholder-Funktionen kategorieweise implementieren (KÖRPER → ERNÄHRUNG → AKTIVITÄT → RECOVERY → KORRELATIONEN → META) +2. [ ] Score-Gewichtung pro goal_mode definieren +3. [ ] Backend: score_calculator.py erstellen +4. [ ] Baseline-Berechnungen implementieren +5. [ ] Integration in bestehende Prompts +6. [ ] Testing mit verschiedenen goal_modes + +--- + +## Metriken & Timeline + +**Geschätzte Timeline (bei 4h/Tag Entwicklung):** + +| Szenario | Dauer | Fertig bis | +|----------|-------|------------| +| Issue #49 | 1.5-2 Tage | ~28.03.2026 | +| Phase 0b | 4-5 Tage | ~31.03.2026 | +| Szenario 1 (Quick Wins first) | 5.5-7 Tage | ~02.04.2026 | +| Szenario 2 (Strategic first) | 5.5-7 Tage | ~02.04.2026 | + +**Bei 8h/Tag Entwicklung:** Timeline halbiert sich (~01.04.2026) + +--- + +**Erstellt:** 26. März 2026 +**Status:** Aktiv - Wartet auf Entscheidung +**Nächste Aktualisierung:** Nach Completion von gewähltem Path diff --git a/docs/STATUS_REPORT_2026-03-26.md b/docs/STATUS_REPORT_2026-03-26.md new file mode 100644 index 0000000..84eab33 --- /dev/null +++ b/docs/STATUS_REPORT_2026-03-26.md @@ -0,0 +1,194 @@ +# Status Report: 26. März 2026 + +## Audit & Synchronisation + +Vollständige Überprüfung aller Dokumente und Gitea Issues durchgeführt. + +--- + +## ✅ Abgeschlossene Arbeiten + +### 1. Gitea Issue #28: AI-Prompts Flexibilisierung +**Status:** ✅ CLOSED (26.03.2026) + +**Implementierte Features:** +- Unified Prompt System (4 Phasen) +- DB-Migration zu einheitlichem Schema (base + pipeline) +- Universeller Executor (prompt_executor.py) +- Frontend UI Consolidation (UnifiedPromptModal) +- Debug & Development Tools (Test-Button, Export/Import) +- 32 aktive Platzhalter mit Kategorisierung +- `{{placeholder|d}}` Modifier + +**Commits:** 20+ commits (2e0838c bis ae6bd0d) +**Dokumentation:** CLAUDE.md "Feature: Unified Prompt System" + +**Gitea Aktion:** Issue geschlossen mit Completion-Kommentar + +--- + +### 2. Gitea Issue #44: BUG - Analysen löschen +**Status:** ✅ CLOSED (26.03.2026) + +**Fix:** +- Delete-Button in InsightCard hinzugefügt +- `api.deleteInsight(id)` Funktion implementiert +- Auth-Token wird korrekt übergeben +- Liste aktualisiert sich nach Löschen + +**Commit:** c56d2b2 +**Dokumentation:** Gitea-Kommentar mit Code-Beispiel + +**Gitea Aktion:** Issue geschlossen mit Fix-Details + +--- + +### 3. Feature: Comprehensive Value Table +**Status:** ✅ Basis-Implementierung COMPLETE (26.03.2026) + +**Implementierte Features:** +- Metadata Collection System (alle Platzhalter mit Werten) +- Expert Mode Toggle (🔬 Experten-Modus) +- Stage Output Extraction (Einzelwerte aus JSON) +- Category Grouping (PROFIL, KÖRPER, ERNÄHRUNG, etc.) +- Collapsible JSON für Stage-Rohdaten +- Best-of-Each circ_summary mit Altersangaben + +**Commits:** 10+ commits (c0a50de bis 6e651b5, 159fcab) +**Dokumentation:** CLAUDE.md "Feature: Comprehensive Value Table" + +**Gitea:** Basis abgeschlossen, Issue #47 für Refinement erstellt + +--- + +### 4. Placeholder System Enhancements +**Status:** ✅ COMPLETE + +**Fixes & Verbesserungen:** +- `circ_summary`: Alle 8 Umfangspunkte (statt nur 3) +- `circ_summary`: Best-of-Each mit Altersangaben ("heute", "vor 2 Wochen") +- `sleep_avg_quality`: Lowercase stage names fix +- `calculate_age`: PostgreSQL DATE object handling +- Stage outputs in debug info für Value Table + +**Commits:** 7daa2e4, a43a9f1, 3ad1a19, d06d3d8, 159fcab, 6e651b5 + +--- + +## 🔲 Neue/Offene Issues + +### Gitea Issue #47: Wertetabelle Optimierung +**Status:** 🔲 OPEN (neu erstellt 26.03.2026) +**Priority:** Medium +**Aufwand:** 4-6 Stunden + +**Ziel:** Value Table übersichtlicher gestalten + +**Kernpunkte:** +- Normal-Modus: Nur Einzelwerte (~24 statt 32) +- Experten-Modus: Zusätzlich Stage-Rohdaten +- Beschreibungen für alle 32 Platzhalter vervollständigen +- Schema-basierte Beschreibungen für extrahierte Werte + +**Dokumentation:** `docs/issues/issue-50-value-table-refinement.md` + +--- + +## 📊 Gitea Issue Übersicht + +### Geschlossen (heute) +- ✅ #28: AI-Prompts Flexibilisierung +- ✅ #44: BUG - Analysen löschen + +### Neu erstellt (heute) +- 🆕 #47: Wertetabelle Optimierung + +### Weiterhin offen (Backlog) +- 🔲 #25: Ziele-System (Goals) +- 🔲 #26: Charts erweitern +- 🔲 #27: Korrelationen & Insights +- 🔲 #29: Abilities-Matrix UI +- 🔲 #30: Responsive UI +- 🔲 #42, #43: Enhanced Debug UI +- 🔲 #45: KI Prompt-Optimierer +- 🔲 #46: KI Prompt-Ersteller + +### Bereits geschlossen (früher) +- ✅ #24: Quality-Filter für KI-Auswertungen + +--- + +## 📝 Dokumentations-Updates + +### CLAUDE.md +- ✅ "Letzte Updates (26.03.2026)" Sektion hinzugefügt +- ✅ Gitea Issue-Referenzen klargestellt (Prefix "Gitea #") +- ✅ Feature-Sections umbenannt (nicht "Issue #28/47") +- ✅ "Claude Code Verantwortlichkeiten" Sektion +- ✅ Issue-Management via Gitea API dokumentiert + +### docs/issues/ +- ✅ issue-50-value-table-refinement.md erstellt +- ℹ️ Weitere Files in .claude/issues/ (nicht versioniert) + +### Gitea Kommentare +- ✅ Issue #28: Completion-Details mit Features & Commits +- ✅ Issue #44: Fix-Details mit Code-Beispiel + +--- + +## 🔄 Nächste Schritte + +### Empfohlen (Kurzfristig) +1. **Testing auf dev.mitai.jinkendo.de:** + - Value Table im Experten-Modus testen + - Stage-Outputs JSON Anzeige prüfen + - circ_summary mit Altersangaben verifizieren + +2. **Production Deployment:** + - Develop → Main Merge (wenn Tests OK) + - Alle Features (Unified Prompts + Value Table) deployen + +3. **Issue #47 Refinement:** + - Wertetabelle im Normal-Modus optimieren + - Beschreibungen vervollständigen + +### Optional (Mittelfristig) +4. **Weitere offene Issues priorisieren:** + - #25: Ziele-System (Phase 1) + - #27: Korrelationen (Phase 2) + - #30: Responsive UI (Phase 0) + +--- + +## 📈 Metriken + +**Commits (heute):** 12 +**Issues geschlossen:** 2 (#28, #44) +**Issues erstellt:** 1 (#47) +**Dokumentations-Updates:** 3 (CLAUDE.md, STATUS_REPORT, issue-50) +**Gitea Kommentare:** 2 + +**Entwicklungszeit (geschätzt):** ~6-8 Stunden +- circ_summary Enhancement: 1h +- Stage Outputs Fix: 1h +- Value Table Collapsible JSON: 1h +- Issue-Management System: 1h +- Dokumentation & Sync: 2-4h + +--- + +## ✅ Verifizierung + +- [x] Alle Gitea Issues überprüft (47 Issues total) +- [x] Abgeschlossene Arbeiten identifiziert (#28, #44) +- [x] Issues in Gitea geschlossen +- [x] Completion-Kommentare hinzugefügt +- [x] CLAUDE.md aktualisiert +- [x] Status Report erstellt +- [x] Entwicklungs-Dokumentation aktuell + +**Audit durchgeführt von:** Claude Code +**Datum:** 26. März 2026, 14:55 Uhr +**Branch:** develop +**Letzter Commit:** 582f125 diff --git a/docs/TODO_GOAL_SYSTEM.md b/docs/TODO_GOAL_SYSTEM.md new file mode 100644 index 0000000..202a93b --- /dev/null +++ b/docs/TODO_GOAL_SYSTEM.md @@ -0,0 +1,284 @@ +# Goal System - TODO & Offene Punkte + +**Erstellt:** 27. März 2026 +**Status:** Aktiv +**Zweck:** Zentrale Tracking-Liste für Goal System Entwicklung + +--- + +## ✅ Erledigt (27.03.2026) + +### Phase 0a: Minimal Goal System (26.03.2026) +- ✅ Migration 022 (goal_mode, goals, training_phases, fitness_tests) +- ✅ Backend Router goals.py (490 Zeilen) +- ✅ Frontend GoalsPage (570 Zeilen) +- ✅ Navigation Integration (Dashboard + Analysis) + +### Phase 1: Quick Fixes (27.03.2026) +- ✅ goal_utils.py Abstraction Layer +- ✅ Primary Goal Toggle Fix +- ✅ Lean Mass Berechnung +- ✅ VO2Max Spaltenname Fix + +--- + +## 🔲 Nächste Schritte (Priorität) + +### Phase 1.5: Flexibles Goal System - DB-Registry ✅ KOMPLETT (27.03.2026) + +**Status:** ✅ ABGESCHLOSSEN +**Priorität:** CRITICAL (blockt Phase 0b) +**Aufwand:** 8h (geplant 8-12h) +**Entscheidung:** 27.03.2026 - Option B gewählt + +**Problem:** +- Aktuelles System: Hardcoded goal types (nur 8 Typen möglich) +- Jedes neue Ziel braucht Code-Änderung + Deploy +- Zukünftige Ziele (Meditation, Rituale, Planabweichung) nicht möglich + +**Lösung: DB-Registry** +- Goal Types in Datenbank definiert +- Admin UI: Neue Ziele ohne Code erstellen +- Universal Value Fetcher (konfigurierbar) +- User kann eigene Custom-Metriken definieren + +**Tasks:** +- ✅ Migration 024: goal_type_definitions Tabelle +- ✅ Backend: Universal Value Fetcher (_fetch_latest, _fetch_avg, _fetch_count) +- ✅ Backend: CRUD API für Goal Type Definitions +- ✅ Frontend: Dynamisches Goal Types Dropdown +- ✅ Admin UI: Goal Type Management Page +- ✅ Seed Data: 8 existierende Typen migriert +- 🔲 Testing: Alle Goals + Custom Goal erstellen (NEXT) + +**Warum JETZT (vor Phase 0b)?** +- Phase 0b Platzhalter nutzen Goals für Score-Berechnungen +- Flexible Goals → automatisch in Platzhaltern verfügbar +- Später umbauen = 120+ Platzhalter anpassen (Doppelarbeit) + +**Dokumentation:** Siehe unten "Flexibles Goal System Details" + +--- + +### Phase 0b: Goal-Aware Placeholders (NACH 1.5 - 16-20h) + +**Status:** 🔲 BEREIT ZUM START (Phase 1.5 ✅) +**Priorität:** HIGH (strategisch kritisch) +**Aufwand:** 16-20h +**Blockt:** Intelligente KI-Analysen + +**Tasks:** +- [ ] 18 KÖRPER Platzhalter (weight_7d_rolling_median, fm_28d_delta, lbm_28d_delta, recomposition_score, etc.) +- [ ] 15 ERNÄHRUNG Platzhalter (protein_g_per_kg, nutrition_adherence_score, energy_availability_status, etc.) +- [ ] 25 AKTIVITÄT Platzhalter (activity_quality_avg_28d, activity_strain_28d, ability_balance_score, etc.) +- [ ] 12 RECOVERY Platzhalter (recovery_score, sleep_regularity_index, sleep_debt_hours, etc.) +- [ ] 8 KORRELATIONEN Platzhalter (corr_energy_weight_lag, plateau_detected, etc.) +- [ ] 6 META Platzhalter (goal_mode, data_quality_score, profile_age_years, etc.) +- [ ] Score-Gewichtung pro goal_mode (SCORE_WEIGHTS Dictionary) +- [ ] Baseline-Berechnungen (7d/28d/90d Referenzwerte) +- [ ] Integration in bestehende Prompts + +**Vorteile:** +- System wird "intelligent" (kein Datensammler mehr) +- Ziele werden tatsächlich genutzt +- Basis für automatische Trainingsphasen-Erkennung + +**Dokumentation:** `docs/NEXT_STEPS_2026-03-26.md` (Zeile 116-300) + +--- + +### v2.0 Redesign (SPÄTER - 8-10h) + +**Status:** 📋 KONZEPTION +**Priorität:** MEDIUM (nach Phase 0b & User-Feedback) +**Aufwand:** 8-10h (dank Abstraction Layer) + +**Probleme zu lösen:** +1. ❌ Primärziel zu simplistisch (nur 1 erlaubt) +2. ❌ Goal Mode zu simpel (nur 1 Modus wählbar) +3. ✅ Fehlende Current Values (ERLEDIGT in Phase 1) +4. ❌ Abstrakte Zieltypen (strength, flexibility) +5. ❌ Blutdruck braucht 2 Werte (systolisch/diastolisch) +6. ❌ Keine Guidance für User (Richtwerte fehlen) + +**Lösung:** +- Migration 023: focus_areas Tabelle mit Gewichtungssystem +- UI: Slider für 6 Fokus-Bereiche (Summe = 100%) +- Backend: `get_focus_weights()` V2 Implementierung (eine Funktion!) +- Compound Goals für BP +- Konkrete Test-basierte Goals (Cooper, Plank, etc.) +- Richtwerte & Normen in UI + +**Dokumentation:** `docs/GOAL_SYSTEM_REDESIGN_v2.md` + +**Entscheidung:** ⏳ Wartet auf User-Feedback nach Phase 0b + +--- + +## 🔗 Verwandte Issues + +### Gitea (http://192.168.2.144:3000/Lars/mitai-jinkendo/issues) +- **#49:** Prompt-Zuordnung zu Verlaufsseiten (6-8h, Quick Win) +- **#47:** Wertetabelle Optimierung (4-6h, Polishing) +- **#50:** Phase 0a Goal System (✅ CLOSED) + +### Interne Docs +- `docs/issues/issue-50-phase-0a-goal-system.md` (✅ Completed) +- `docs/issues/issue-51-prompt-page-assignment.md` (#49 Spec) + +--- + +## 📊 Roadmap-Übersicht + +| Phase | Was | Status | Aufwand | +|-------|-----|--------|---------| +| **Phase 0a** | Minimal Goal System | ✅ DONE | 3-4h | +| **Phase 1** | Quick Fixes + Abstraction | ✅ DONE | 4-6h | +| **Phase 1.5** | 🆕 **Flexibles Goal System (DB-Registry)** | ✅ **DONE** | 8h | +| **Phase 0b** | Goal-Aware Placeholders | 🔲 READY | 16-20h | +| **Issue #49** | Prompt Page Assignment | 🔲 OPEN | 6-8h | +| **v2.0** | Redesign (Focus Areas) | 📋 LATER | 8-10h | + +**Total Roadmap:** ~45-60h bis vollständiges intelligentes Goal System + +**KRITISCH:** Phase 1.5 MUSS vor Phase 0b abgeschlossen sein, sonst Doppelarbeit! + +--- + +## 💡 Wichtige Notizen + +### Abstraction Layer (Keine Doppelarbeit!) +**Datei:** `backend/goal_utils.py` + +```python +get_focus_weights(conn, profile_id) +``` + +- **V1 (jetzt):** Mappt goal_mode → Gewichte +- **V2 (v2.0):** Liest focus_areas Tabelle +- **Vorteil:** 120+ Phase 0b Platzhalter müssen NICHT umgeschrieben werden + +### Testing Checklist (nach jedem Deploy) +- [ ] Goal Mode ändern → Gewichtung korrekt? +- [ ] Primäres Ziel setzen → Andere auf false? +- [ ] Lean Mass Ziel → Current Value berechnet? +- [ ] VO2Max Ziel → Kein Server Error? +- [ ] Mehrere Ziele → Progress korrekt? + +--- + +## 📅 Timeline + +| Datum | Event | +|-------|-------| +| 26.03.2026 | Phase 0a Complete | +| 27.03.2026 | Phase 1 Complete (Quick Fixes) | +| 28.03.2026 | **Phase 0b Start (geplant)** | +| 02.04.2026 | Phase 0b Complete (geschätzt bei 4h/Tag) | +| 04.04.2026 | v2.0 Redesign (wenn validiert) | + +--- + +## 🔧 Flexibles Goal System - Technische Details + +### Architektur: DB-Registry Pattern + +**Vorher (Phase 0a/1):** +```javascript +// Frontend: Hardcoded +const GOAL_TYPES = { + weight: { label: 'Gewicht', unit: 'kg', icon: '⚖️' } +} + +// Backend: Hardcoded if/elif +if goal_type == 'weight': + cur.execute("SELECT weight FROM weight_log...") +elif goal_type == 'body_fat': + cur.execute("SELECT body_fat_pct FROM caliper_log...") +``` + +**Nachher (Phase 1.5):** +```sql +-- Datenbank: Konfigurierbare Goal Types +CREATE TABLE goal_type_definitions ( + type_key VARCHAR(50) UNIQUE, + label_de VARCHAR(100), + unit VARCHAR(20), + icon VARCHAR(10), + category VARCHAR(50), + source_table VARCHAR(50), + source_column VARCHAR(50), + aggregation_method VARCHAR(20), -- latest, avg_7d, count_7d, etc. + calculation_formula TEXT, -- JSON für komplexe Berechnungen + is_system BOOLEAN -- System-Typen nicht löschbar +); +``` + +```python +# Backend: Universal Fetcher +def get_current_value_for_goal(conn, profile_id, goal_type): + """Liest Config aus DB, führt Query aus""" + config = get_goal_type_config(conn, goal_type) + + if config['calculation_formula']: + return execute_formula(conn, profile_id, config['calculation_formula']) + else: + return fetch_by_method( + conn, profile_id, + config['source_table'], + config['source_column'], + config['aggregation_method'] + ) +``` + +```javascript +// Frontend: Dynamisch +const goalTypes = await api.getGoalTypeDefinitions() +// Lädt aktuell verfügbare Typen von API +``` + +### Vorteile: + +**Flexibilität:** +- ✅ Neue Ziele via Admin UI (KEIN Code-Deploy) +- ✅ User kann Custom-Metriken definieren +- ✅ Zukünftige Module automatisch integriert + +**Beispiele neuer Ziele:** +- 🧘 Meditation (min/Tag) → `meditation_log.duration_minutes`, avg_7d +- 📅 Trainingshäufigkeit (x/Woche) → `activity_log.id`, count_7d +- 📊 Planabweichung (%) → `activity_log.planned_vs_actual`, avg_30d +- 🎯 Ritual-Adherence (%) → `rituals_log.completed`, avg_30d +- 💤 Schlafqualität (%) → `sleep_log.quality_score`, avg_7d + +**Integration mit Phase 0b:** +- Platzhalter nutzen `get_current_value_for_goal()` → automatisch alle Typen verfügbar +- Neue Ziele → sofort in KI-Analysen nutzbar +- Keine Platzhalter-Anpassungen nötig + +--- + +**Letzte Aktualisierung:** 27. März 2026 (Phase 1.5 ✅ ABGESCHLOSSEN) +**Nächste Aktualisierung:** Nach Phase 0b Completion + +--- + +## 🎉 Phase 1.5 Completion Report (27.03.2026) + +**Commits:** +- `65ee5f8` - Phase 1.5 Part 1/2 (Backend, Migration, Universal Fetcher) +- `640ef81` - Phase 1.5 Part 2/2 (Frontend Dynamic, Admin UI) - **COMPLETE** + +**Implementiert:** +1. ✅ DB-Registry für Goal Types (8 System Types seeded) +2. ✅ Universal Value Fetcher (8 Aggregationsmethoden) +3. ✅ CRUD API (admin-only, System Types geschützt) +4. ✅ Dynamic Frontend (keine hardcoded Types mehr) +5. ✅ Admin UI (vollständiges CRUD Interface) + +**System ist jetzt flexibel:** +- Neue Goal Types via UI ohne Code-Deploy +- Phase 0b Platzhalter nutzen automatisch alle Types +- Custom Metrics möglich (Meditation, Rituale, etc.) + +**Ready für Phase 0b:** 120+ Goal-Aware Placeholders 🚀 diff --git a/docs/issues/issue-50-phase-0a-goal-system.md b/docs/issues/issue-50-phase-0a-goal-system.md new file mode 100644 index 0000000..11cf137 --- /dev/null +++ b/docs/issues/issue-50-phase-0a-goal-system.md @@ -0,0 +1,245 @@ +# Phase 0a: Minimal Goal System (Strategic + Tactical) + +**Status:** ✅ ABGESCHLOSSEN (26.03.2026) +**Labels:** feature, enhancement, goal-system +**Priority:** High (Foundation for Phase 0b) +**Aufwand:** 3-4h (geschätzt) / ~4h (tatsächlich) + +--- + +## Beschreibung + +Implementierung des minimalen Zielsystems als Basis für goal-aware KI-Analysen. Zwei-Ebenen-Architektur: +- **Strategic Layer:** Goal Modes (beeinflusst Score-Gewichtung) +- **Tactical Layer:** Konkrete Zielwerte mit Progress-Tracking + +--- + +## Implementiert ✅ + +### Strategic Layer (Goal Modes) +- `goal_mode` in `profiles` table +- 5 Modi: `weight_loss`, `strength`, `endurance`, `recomposition`, `health` +- Bestimmt Score-Gewichtung für alle KI-Analysen +- **UI:** 5 Goal Mode Cards mit Beschreibungen und Icons + +### Tactical Layer (Concrete Goals) +- `goals` table mit vollständigem Tracking: + - Target/Current/Start values + - Progress percentage (auto-calculated) + - Projection date & on-track status + - Primary/Secondary goal concept + - 8 Goal-Typen: weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr +- **UI:** + - Goal CRUD mit Fortschrittsbalken + - Mobile-friendly Design (full-width inputs, labels above fields) + - Inline editing vorbereitet + +### Training Phases Framework +- `training_phases` table (Auto-Detection vorbereitet für Phase 2) +- 5 Phase-Typen: calorie_deficit, calorie_surplus, deload, maintenance, periodization +- Status-Flow: suggested → accepted → active → completed → rejected +- Confidence scoring für KI-basierte Erkennung +- JSONB detection_params für Flexibilität + +### Fitness Tests +- `fitness_tests` table für standardisierte Tests +- 8 Test-Typen: cooper_12min, step_test, pushups_max, plank_max, flexibility_sit_reach, vo2max_est, strength_1rm_squat, strength_1rm_bench +- Norm-Kategorisierung vorbereitet (age/gender-spezifisch) +- Baseline-Tracking für Fortschrittsmessung + +--- + +## Technische Umsetzung + +### Backend + +**Migration 022:** `backend/migrations/022_goal_system.sql` +```sql +-- Strategic Layer +ALTER TABLE profiles ADD COLUMN goal_mode VARCHAR(50) DEFAULT 'health'; + +-- Tactical Layer +CREATE TABLE goals (...); +CREATE TABLE training_phases (...); +CREATE TABLE fitness_tests (...); +``` + +**Router:** `backend/routers/goals.py` (490 Zeilen) +- Vollständiges CRUD für alle 3 Ebenen +- Progress calculation (auto-update current values) +- Linear projection für target_date +- Helper functions für goal-type spezifische Current-Values + +**API Endpoints:** `/api/goals/*` +- `GET/PUT /mode` - Strategic goal mode +- `GET /list` - All goals with progress +- `POST /create` - Create goal +- `PUT /{id}` - Update goal +- `DELETE /{id}` - Delete goal +- `GET/POST /phases` - Training phases +- `PUT /phases/{id}/status` - Accept/reject auto-detected phases +- `GET/POST /tests` - Fitness tests + +### Frontend + +**GoalsPage:** `frontend/src/pages/GoalsPage.jsx` (570 Zeilen) +- **Goal Mode Selector:** 5 Karten mit Icons, Farben, Beschreibungen +- **Goal List:** Cards mit Progress-Balken, Projection-Display, Edit/Delete +- **Goal Form:** Mobile-optimiertes Modal + - Full-width inputs + - Labels above fields (not beside) + - Section headers with emoji (🎯 Zielwert) + - Unit display as styled badge + - Primary goal checkbox in highlighted section + - Text-align: left für Text-Felder, right für Zahlen +- **Empty State:** Placeholder mit CTA + +**Navigation Integration:** +- **Dashboard:** Goals Preview Card mit "Verwalten →" Link +- **Analysis Page:** 🎯 Ziele Button neben Titel (direkter Zugang) +- **Route:** `/goals` in App.jsx registriert + +**api.js:** 15+ neue API-Funktionen +```javascript +// Goal Modes +getGoalMode(), updateGoalMode(mode) + +// Goals CRUD +listGoals(), createGoal(data), updateGoal(id, data), deleteGoal(id) + +// Training Phases +listTrainingPhases(), createTrainingPhase(data), updatePhaseStatus(id, status) + +// Fitness Tests +listFitnessTests(), createFitnessTest(data) +``` + +--- + +## Commits + +| Commit | Beschreibung | +|--------|-------------| +| `337667f` | feat: Phase 0a - Minimal Goal System (Strategic + Tactical) | +| `906a3b7` | fix: Migration 022 - remove invalid schema_migrations tracking | +| `75f0a5d` | refactor: mobile-friendly goal form design | +| `5be52bc` | feat: goals navigation + UX improvements | + +**Branch:** `develop` +**Deployed to:** `dev.mitai.jinkendo.de` ✅ + +--- + +## Dokumentation + +- ✅ `docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md` (538 Zeilen) + - Analyse beider Fachkonzepte (Konzept v2 + GOALS_VITALS.md) + - Zwei-Ebenen-Architektur erklärt + - 120+ Placeholder-Kategorisierung für Phase 0b +- ✅ Migration 022 mit vollständigen COMMENT ON statements +- ✅ API-Dokumentation in Router-Docstrings +- ✅ Dieses Issue-Dokument + +--- + +## Basis für Phase 0b + +Phase 0a bietet die Foundation für: + +### Phase 0b: Goal-Aware Placeholders (16-20h) +- ✅ 120+ neue Platzhalter die `goal_mode` berücksichtigen +- ✅ Score-Berechnungen abhängig von Strategic Layer +- ✅ Baseline-Berechnungen (7d/28d/90d Trends) +- ✅ Lag-basierte Korrelationen +- ✅ Confidence Scoring + +**Beispiel Goal-Mode Impact:** +```python +# Gleiche Daten, unterschiedliche Interpretation: +Δ: -5kg FM, -2kg LBM + +goal_mode = "weight_loss" + → body_progress_score = 78/100 (FM↓ gut, LBM↓ tolerierbar) + +goal_mode = "strength" + → body_progress_score = 32/100 (LBM↓ ist KATASTROPHE!) + +goal_mode = "health" + → body_progress_score = 50/100 (neutral, ohne Bias) +``` + +--- + +## Testing + +✅ Migration erfolgreich auf dev.mitai.jinkendo.de +✅ Goal Mode wechselbar +✅ Goal CRUD funktioniert +✅ Progress calculation korrekt +✅ Mobile UI responsive +✅ Navigation von Dashboard + Analysis + +**Manuelle Tests durchgeführt:** +- [x] Goal Mode ändern +- [x] Ziel erstellen (alle 8 Typen) +- [x] Ziel bearbeiten +- [x] Ziel löschen +- [x] Primary Goal setzen +- [x] Progress-Balken korrekt +- [x] Mobile UI full-width +- [x] Text-Align korrekt + +--- + +## Akzeptanzkriterien + +- [x] Migration 022 erfolgreich +- [x] Goal Mode in profiles funktioniert +- [x] Goals CRUD vollständig +- [x] Progress-Tracking funktioniert +- [x] Primary Goal Konzept implementiert +- [x] Mobile-friendly UI +- [x] Navigation von 2+ Stellen +- [x] API-Dokumentation vollständig +- [x] Frontend form validation +- [x] Error handling korrekt + +--- + +## Nächste Schritte + +**Empfohlen:** + +1. **Option A: Issue #49 - Prompt Page Assignment (6-8h)** + - Prompts auf Verlaufsseiten zuordnen + - Quick Win für bessere UX + - Nutzt bestehendes Unified Prompt System + +2. **Option B: Phase 0b - Goal-Aware Placeholders (16-20h)** + - 120+ neue Platzhalter + - Score-Berechnungen mit goal_mode + - Größter strategischer Impact + +**Siehe:** `docs/NEXT_STEPS_2026-03-26.md` für detaillierte Planung + +--- + +## Lessons Learned + +### Was gut lief: +- ✅ Zwei-Ebenen-Architektur (Strategic + Tactical) macht Sinn +- ✅ Mobile-first Design von Anfang an +- ✅ Unified Analysis vor Implementierung (beide Fachkonzepte) +- ✅ Migration-System funktioniert einwandfrei + +### Was zu beachten ist: +- ⚠️ Schema_migrations verwendet `filename`, nicht `version` +- ⚠️ Unnötige DO-Blocks in Migrationen vermeiden +- ⚠️ Text-align: right als Default in form-input (für Textfelder überschreiben) + +--- + +**Erstellt:** 26. März 2026 +**Status:** ✅ COMPLETE - Ready for Phase 0b +**Related Issues:** #49 (Prompt Assignment), #47 (Value Table Refinement) diff --git a/docs/issues/issue-51-prompt-page-assignment.md b/docs/issues/issue-51-prompt-page-assignment.md new file mode 100644 index 0000000..4fc7c72 --- /dev/null +++ b/docs/issues/issue-51-prompt-page-assignment.md @@ -0,0 +1,425 @@ +# Feature: Prompt-Zuordnung zu Verlaufsseiten + +**Labels:** feature, ux, enhancement +**Priority:** Medium (Phase 1-2) +**Related:** Issue #28 (Unified Prompt System - Complete) + +## Beschreibung +KI-Prompts sollen flexibel auf verschiedenen Verlaufsseiten verfügbar gemacht werden können. Jeder Prompt kann auf mehreren Seiten gleichzeitig angeboten werden (Mehrfachauswahl). + +## Problem (aktueller Stand) + +**Aktuell:** +- Prompts sind nur über die zentrale Analyse-Seite (📊 Analyse) verfügbar +- Kein kontextbezogener Zugriff auf relevante Analysen +- User muss immer zur Analyse-Seite navigieren + +**Beispiel-Szenario:** +``` +User ist auf: Gewicht → Verlauf +Will: Gewichtstrend analysieren +Muss: Zur Analyse-Seite → Prompt auswählen → Zurück +``` + +**Wünschenswert:** +``` +User ist auf: Gewicht → Verlauf +Sieht: "🤖 KI-Analyse" Button mit relevanten Prompts +Kann: Direkt "Gewichtstrend-Analyse" starten +``` + +## Gewünschtes Verhalten + +### 1. Prompt-Konfiguration erweitern + +**Admin → KI-Prompts → Prompt bearbeiten:** + +``` +┌─────────────────────────────────────────────┐ +│ Prompt bearbeiten: Gewichtstrend-Analyse │ +├─────────────────────────────────────────────┤ +│ Name: Gewichtstrend-Analyse │ +│ Slug: weight_trend │ +│ Type: Pipeline │ +│ │ +│ 📍 Verfügbar auf Seiten: │ +│ ┌─────────────────────────────────────────┐ │ +│ │ ☑ Analyse (Hauptseite) │ │ +│ │ ☑ Gewicht → Verlauf │ │ +│ │ ☐ Umfänge → Verlauf │ │ +│ │ ☐ Caliper → Verlauf │ │ +│ │ ☐ Aktivität → Verlauf │ │ +│ │ ☐ Ernährung → Verlauf │ │ +│ │ ☐ Schlaf → Verlauf │ │ +│ │ ☐ Vitalwerte → Verlauf │ │ +│ │ ☐ Dashboard │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ [Speichern] [Abbrechen] │ +└─────────────────────────────────────────────┘ +``` + +**Mehrfachauswahl:** +- Ein Prompt kann auf mehreren Seiten gleichzeitig verfügbar sein +- Mindestens eine Seite muss ausgewählt sein +- Default: "Analyse (Hauptseite)" ist immer vorausgewählt + +### 2. UI auf Verlaufsseiten + +**Gewicht → Verlauf:** + +``` +┌─────────────────────────────────────────────┐ +│ 📊 Gewicht - Verlauf │ +│ [Filter: 7d] [30d] [90d] [Alle] │ +├─────────────────────────────────────────────┤ +│ │ +│ [Chart: Gewichtsverlauf] │ +│ │ +├─────────────────────────────────────────────┤ +│ 🤖 KI-Analysen │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Gewichtstrend-Analyse [▶ Starten]│ │ +│ │ Körperkomposition-Check [▶ Starten]│ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ [Einträge-Tabelle...] │ +└─────────────────────────────────────────────┘ +``` + +**Features:** +- Kompaktes Widget unterhalb des Charts +- Nur relevante Prompts werden angezeigt +- Button startet Analyse inline (Modal oder expandierend) +- Ergebnis wird direkt auf der Seite angezeigt + +### 3. Inline-Analyse Anzeige + +**Option A: Modal (empfohlen für MVP):** +``` +Click auf [▶ Starten] + ↓ +┌─────────────────────────────────────────────┐ +│ ✕ Gewichtstrend-Analyse │ +├─────────────────────────────────────────────┤ +│ [Spinner] Analysiere Gewichtsdaten... │ +│ │ +│ [Nach Abschluss:] │ +│ Analyse-Text... │ +│ │ +│ 📊 Verwendete Werte (12) [🔬 Experten] │ +│ [Value Table...] │ +│ │ +│ [Schließen] [In Verlauf speichern] │ +└─────────────────────────────────────────────┘ +``` + +**Option B: Expandierend (später):** +``` +Click auf [▶ Starten] + ↓ +Widget expandiert nach unten +Zeigt Analyse-Ergebnis inline +[△ Einklappen] Button +``` + +## Technische Umsetzung + +### 1. Datenbankschema erweitern + +**Tabelle: `ai_prompts`** +```sql +ALTER TABLE ai_prompts ADD COLUMN available_on JSONB DEFAULT '["analysis"]'; + +COMMENT ON COLUMN ai_prompts.available_on IS + 'Array of page slugs where prompt is available. + Values: analysis, weight_history, circ_history, caliper_history, + activity_history, nutrition_history, sleep_history, vitals_history, dashboard'; + +-- Migration 022 +``` + +**Beispiel-Werte:** +```json +{ + "slug": "weight_trend", + "name": "Gewichtstrend-Analyse", + "available_on": ["analysis", "weight_history"] +} + +{ + "slug": "pipeline_master", + "name": "Vollständige Analyse", + "available_on": ["analysis", "dashboard"] +} + +{ + "slug": "nutrition_check", + "name": "Ernährungs-Check", + "available_on": ["analysis", "nutrition_history", "activity_history"] +} +``` + +### 2. Backend API erweitern + +**Neuer Endpoint: GET /api/prompts/for-page/{page_slug}** + +```python +@router.get("/for-page/{page_slug}") +def get_prompts_for_page(page_slug: str, session: dict = Depends(require_auth)): + """Get all prompts available for a specific page. + + Args: + page_slug: Page identifier (e.g., 'weight_history', 'analysis') + + Returns: + List of prompts with available_on containing page_slug + """ + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + """SELECT id, name, slug, type, description, available_on + FROM ai_prompts + WHERE available_on @> %s + ORDER BY name""", + (json.dumps([page_slug]),) + ) + return [r2d(row) for row in cur.fetchall()] +``` + +**Beispiel-Aufruf:** +```javascript +// In WeightPage.jsx +const prompts = await api.getPromptsForPage('weight_history') +// Returns: [{slug: 'weight_trend', name: 'Gewichtstrend-Analyse', ...}] +``` + +**Prompt CRUD erweitern:** +```python +@router.put("/unified/{id}") +def update_unified_prompt(id: str, p: UnifiedPromptCreate, session=Depends(require_admin)): + # ... existing code ... + cur.execute( + """UPDATE ai_prompts + SET name=%s, slug=%s, template=%s, ..., available_on=%s + WHERE id=%s""", + (..., json.dumps(p.available_on), id) + ) +``` + +### 3. Frontend: Prompt-Editor erweitern + +**UnifiedPromptModal.jsx:** + +```javascript +const PAGE_OPTIONS = [ + { value: 'analysis', label: '📊 Analyse (Hauptseite)', default: true }, + { value: 'weight_history', label: '⚖️ Gewicht → Verlauf' }, + { value: 'circ_history', label: '📏 Umfänge → Verlauf' }, + { value: 'caliper_history', label: '📐 Caliper → Verlauf' }, + { value: 'activity_history', label: '🏃 Aktivität → Verlauf' }, + { value: 'nutrition_history', label: '🍎 Ernährung → Verlauf' }, + { value: 'sleep_history', label: '😴 Schlaf → Verlauf' }, + { value: 'vitals_history', label: '❤️ Vitalwerte → Verlauf' }, + { value: 'dashboard', label: '🏠 Dashboard' }, +] + +// In form: +
+ +
+ {PAGE_OPTIONS.map(opt => ( + + ))} +
+
+``` + +### 4. Frontend: Verlaufsseiten erweitern + +**WeightPage.jsx (Beispiel):** + +```javascript +function WeightPage() { + const [prompts, setPrompts] = useState([]) + const [runningAnalysis, setRunningAnalysis] = useState(null) + const [analysisResult, setAnalysisResult] = useState(null) + + useEffect(() => { + loadPrompts() + }, []) + + const loadPrompts = async () => { + try { + const data = await api.getPromptsForPage('weight_history') + setPrompts(data) + } catch(e) { + console.error('Failed to load prompts:', e) + } + } + + const runAnalysis = async (promptSlug) => { + setRunningAnalysis(promptSlug) + try { + const result = await api.executePrompt(promptSlug, {save: true}) + setAnalysisResult(result) + } catch(e) { + setError(e.message) + } finally { + setRunningAnalysis(null) + } + } + + return ( +
+

Gewicht - Verlauf

+ + {/* Chart */} + + + {/* AI Prompts Widget */} + {prompts.length > 0 && ( +
+

🤖 KI-Analysen

+ {prompts.map(p => ( + + ))} +
+ )} + + {/* Analysis Result Modal */} + {analysisResult && ( + setAnalysisResult(null)} + /> + )} + + {/* Data Table */} + +
+ ) +} +``` + +**Wiederverwendbare Komponente:** +```javascript +// components/PagePrompts.jsx +export function PagePrompts({ pageSlug }) { + // ... logic ... + return ( +
+

🤖 KI-Analysen

+ {prompts.map(p => ( + + ))} +
+ ) +} + +// Usage in any page: + +``` + +## Akzeptanzkriterien + +- [ ] DB-Migration: `available_on` JSONB column in ai_prompts +- [ ] Backend: `GET /api/prompts/for-page/{page_slug}` Endpoint +- [ ] Backend: CRUD operations unterstützen available_on +- [ ] Frontend: Prompt-Editor zeigt Page-Auswahl (Mehrfachauswahl) +- [ ] Frontend: Mindestens 1 Page muss ausgewählt sein +- [ ] Frontend: Wiederverwendbare PagePrompts Komponente +- [ ] Frontend: Integration in mind. 2 Verlaufsseiten (Weight, Nutrition) +- [ ] UI: Inline-Analyse via Modal mit Value Table +- [ ] UI: Loading-State während Analyse läuft +- [ ] Dokumentation: API-Dokumentation aktualisiert + +## Abschätzung + +**Aufwand:** 6-8 Stunden +- 1h: DB-Migration + Backend Endpoint +- 2h: Prompt-Editor erweitern (Page-Auswahl) +- 2h: PagePrompts Komponente + Modal +- 2h: Integration in Verlaufsseiten (2-3 Seiten) +- 1h: Testing + Feintuning + +**Priorität:** Medium +- Verbessert UX erheblich (kontextbezogene Analysen) +- Nutzt bestehendes Prompt-System (Issue #28) +- Relativ einfach zu implementieren (kein neues Backend-System) + +## Use Cases + +### UC1: Gewichtstrend auf Gewicht-Seite +``` +User: Navigiert zu "Gewicht → Verlauf" +System: Zeigt Gewichts-Chart + verfügbare Prompts +User: Click "Gewichtstrend-Analyse ▶" +System: Startet Analyse, zeigt Modal mit Ergebnis +User: Click "In Verlauf speichern" +System: Speichert in ai_insights, zeigt in Analyse-Verlauf +``` + +### UC2: Ernährungs-Check auf Ernährung-Seite +``` +User: Navigiert zu "Ernährung → Verlauf" +System: Zeigt Ernährungs-Charts + verfügbare Prompts +User: Click "Ernährungs-Check ▶" +System: Analysiert Makros + Kalorien der letzten 7 Tage +User: Sieht Empfehlungen direkt auf Ernährungs-Seite +``` + +### UC3: Multi-Page Prompt (z.B. "Vollständige Analyse") +``` +Admin: Konfiguriert "Vollständige Analyse" + - Verfügbar auf: [Analyse, Dashboard, Gewicht, Ernährung] +User: Sieht denselben Prompt auf 4 verschiedenen Seiten +User: Kann von überall die gleiche umfassende Analyse starten +``` + +## Notizen + +- **Rückwärtskompatibilität:** Bestehende Prompts ohne `available_on` → Default `["analysis"]` +- **Migration:** Alle existierenden Prompts bekommen `["analysis"]` gesetzt +- **Permissions:** Prompts respektieren weiterhin Feature-Enforcement (ai_calls) +- **Caching:** Prompts könnten gecacht werden (selten geändert) +- **Mobile:** PagePrompts sollte auch auf Mobile gut aussehen (Stack-Layout) +- **Performance:** Lazy-Loading der Prompts (nur laden wenn Seite besucht) + +## Erweiterungen (Future) + +- **Conditional Display:** Prompts nur anzeigen wenn Daten vorhanden + - Beispiel: "Gewichtstrend" nur wenn min. 3 Gewichts-Einträge +- **Quick Actions:** Direkt-Buttons im Chart (ohne separates Widget) +- **Page-spezifische Variablen:** Automatisch aktuelle Filter übergeben + - Beispiel: Wenn "30d" Filter aktiv → `{{timeframe}}` = 30 +- **Prompt-Templates pro Page:** Vordefinierte Vorlagen für jede Seite +- **Favoriten:** User kann Prompts auf Seiten favorisieren (User-spezifisch) + +## Verwandte Issues + +- #28: Unified Prompt System (Basis für dieses Feature) +- #45: KI Prompt-Optimierer (könnte Page-Kontext nutzen) +- #46: KI Prompt-Ersteller (sollte Page-Auswahl anbieten) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f1374a1..bab73a1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -31,10 +31,13 @@ import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage' import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage' import AdminTrainingProfiles from './pages/AdminTrainingProfiles' import AdminPromptsPage from './pages/AdminPromptsPage' +import AdminGoalTypesPage from './pages/AdminGoalTypesPage' import SubscriptionPage from './pages/SubscriptionPage' import SleepPage from './pages/SleepPage' import RestDaysPage from './pages/RestDaysPage' import VitalsPage from './pages/VitalsPage' +import GoalsPage from './pages/GoalsPage' +import CustomGoalsPage from './pages/CustomGoalsPage' import './app.css' function Nav() { @@ -172,6 +175,8 @@ function AppShell() { }/> }/> }/> + }/> + }/> }/> }/> }/> @@ -186,6 +191,7 @@ function AppShell() { }/> }/> }/> + }/> }/> diff --git a/frontend/src/pages/AdminGoalTypesPage.jsx b/frontend/src/pages/AdminGoalTypesPage.jsx new file mode 100644 index 0000000..edc4173 --- /dev/null +++ b/frontend/src/pages/AdminGoalTypesPage.jsx @@ -0,0 +1,520 @@ +import { useState, useEffect } from 'react' +import { Settings, Plus, Pencil, Trash2, Database } from 'lucide-react' +import { api } from '../utils/api' + +export default function AdminGoalTypesPage() { + const [goalTypes, setGoalTypes] = useState([]) + const [schemaInfo, setSchemaInfo] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [showForm, setShowForm] = useState(false) + const [editingType, setEditingType] = useState(null) + const [toast, setToast] = useState(null) + + const [formData, setFormData] = useState({ + type_key: '', + label_de: '', + unit: '', + icon: '', + category: 'custom', + source_table: '', + source_column: '', + aggregation_method: 'latest', + filter_conditions: '', + description: '' + }) + + const CATEGORIES = ['body', 'mind', 'activity', 'nutrition', 'recovery', 'custom'] + const AGGREGATION_METHODS = [ + { value: 'latest', label: 'Letzter Wert' }, + { value: 'avg_7d', label: 'Durchschnitt 7 Tage' }, + { value: 'avg_30d', label: 'Durchschnitt 30 Tage' }, + { value: 'sum_30d', label: 'Summe 30 Tage' }, + { value: 'count_7d', label: 'Anzahl 7 Tage' }, + { value: 'count_30d', label: 'Anzahl 30 Tage' }, + { value: 'min_30d', label: 'Minimum 30 Tage' }, + { value: 'max_30d', label: 'Maximum 30 Tage' } + ] + + useEffect(() => { + loadGoalTypes() + }, []) + + const loadGoalTypes = async () => { + setLoading(true) + setError(null) + try { + const [typesData, schema] = await Promise.all([ + api.listGoalTypeDefinitions(), + api.getSchemaInfo() + ]) + console.log('[DEBUG] Loaded goal types:', typesData) + console.log('[DEBUG] Loaded schema info:', schema) + setGoalTypes(typesData || []) + setSchemaInfo(schema || {}) + } catch (err) { + console.error('[ERROR] Failed to load goal types:', err) + setError(`Fehler beim Laden der Goal Types: ${err.message || err.toString()}`) + } finally { + setLoading(false) + } + } + + const showToast = (message) => { + setToast(message) + setTimeout(() => setToast(null), 2000) + } + + const handleCreate = () => { + setEditingType(null) + setFormData({ + type_key: '', + label_de: '', + unit: '', + icon: '', + category: 'custom', + source_table: '', + source_column: '', + aggregation_method: 'latest', + filter_conditions: '', + description: '' + }) + setShowForm(true) + } + + const handleEdit = (type) => { + setEditingType(type.id) + setFormData({ + type_key: type.type_key, + label_de: type.label_de, + unit: type.unit, + icon: type.icon || '', + category: type.category || 'custom', + source_table: type.source_table || '', + source_column: type.source_column || '', + aggregation_method: type.aggregation_method || 'latest', + filter_conditions: type.filter_conditions ? JSON.stringify(type.filter_conditions, null, 2) : '', + description: type.description || '' + }) + setShowForm(true) + } + + const handleSave = async () => { + if (!formData.label_de || !formData.unit) { + setError('Bitte Label und Einheit ausfüllen') + return + } + + // Parse filter_conditions from string to JSON + let payload = { ...formData } + if (formData.filter_conditions && formData.filter_conditions.trim()) { + try { + payload.filter_conditions = JSON.parse(formData.filter_conditions) + } catch (e) { + setError('Ungültiges JSON in Filter-Bedingungen') + return + } + } else { + payload.filter_conditions = null + } + + try { + if (editingType) { + await api.updateGoalType(editingType, payload) + showToast('✓ Goal Type aktualisiert') + } else { + if (!formData.type_key) { + setError('Bitte eindeutigen Key angeben (z.B. meditation_minutes)') + return + } + await api.createGoalType(payload) + showToast('✓ Goal Type erstellt') + } + + await loadGoalTypes() + setShowForm(false) + setError(null) + } catch (err) { + setError(err.message || 'Fehler beim Speichern') + } + } + + const handleDelete = async (typeId, typeName, isSystem) => { + if (isSystem) { + if (!confirm(`System Goal Type "${typeName}" deaktivieren? (Nicht löschbar)`)) return + } else { + if (!confirm(`Goal Type "${typeName}" wirklich löschen?`)) return + } + + try { + await api.deleteGoalType(typeId) + showToast('✓ Goal Type gelöscht/deaktiviert') + await loadGoalTypes() + } catch (err) { + setError(err.message || 'Fehler beim Löschen') + } + } + + if (loading) { + return ( +
+
+
+
+
+ ) + } + + return ( +
+
+

Goal Type Verwaltung

+
+ + {error && ( +
+

{error}

+
+ )} + + {toast && ( +
+ {toast} +
+ )} + +
+
+
+

Verfügbare Goal Types

+

+ {goalTypes.length} Types registriert ({goalTypes.filter(t => t.is_system).length} System, {goalTypes.filter(t => !t.is_system).length} Custom) +

+
+ +
+ +
+ {goalTypes.map(type => ( +
+
+
+
+ {type.icon || '📊'} + {type.label_de} + + {type.unit} + + {type.is_system && ( + + SYSTEM + + )} + {!type.is_active && ( + + INAKTIV + + )} +
+ +
+ Key: {type.type_key} + {type.source_table && ( + <> + {' | '}Quelle: {type.source_table}.{type.source_column} + {' | '}Methode: {type.aggregation_method} + + )} + {type.calculation_formula && ( + <> + {' | '}Formel: Komplex (JSON) + + )} +
+ + {type.description && ( +
+ {type.description} +
+ )} +
+ +
+ + +
+
+
+ ))} +
+
+ + {/* Form Modal */} + {showForm && ( +
+
+
+ {editingType ? 'Goal Type bearbeiten' : 'Neuer Goal Type'} +
+ +
+ {/* Type Key (nur bei Create) */} + {!editingType && ( +
+ + setFormData(f => ({ ...f, type_key: e.target.value }))} + placeholder="snake_case verwenden" + /> +
+ )} + + {/* Label */} +
+ + setFormData(f => ({ ...f, label_de: e.target.value }))} + placeholder="z.B. Meditation" + /> +
+ + {/* Unit & Icon */} +
+
+ + setFormData(f => ({ ...f, unit: e.target.value }))} + placeholder="z.B. min/Tag" + /> +
+
+ + setFormData(f => ({ ...f, icon: e.target.value }))} + placeholder="🧘" + /> +
+
+ + {/* Category */} +
+ + +
+ + {/* Data Source */} +
+
+ + {schemaInfo ? ( + + ) : ( + setFormData(f => ({ ...f, source_table: e.target.value }))} + placeholder="Lade Schema..." + disabled + /> + )} +
+
+ + {schemaInfo && formData.source_table && schemaInfo[formData.source_table] ? ( + + ) : ( + setFormData(f => ({ ...f, source_column: e.target.value }))} + placeholder={formData.source_table ? "Spalte wählen..." : "Erst Tabelle wählen"} + disabled={!formData.source_table} + /> + )} +
+
+ + {/* Aggregation Method */} +
+ + +
+ + {/* Filter Conditions */} +
+ +