Merge pull request 'Goalsystem V1' (#50) from develop into main
All checks were successful
Deploy Production / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

Reviewed-on: #50
This commit is contained in:
Lars 2026-03-27 17:40:50 +01:00
commit 9d22e7e8af
33 changed files with 9304 additions and 12 deletions

118
CLAUDE.md
View File

@ -76,7 +76,46 @@ frontend/src/
└── technical/ # MEMBERSHIP_SYSTEM.md └── 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 ✅ ### Implementiert ✅
- Login (E-Mail + bcrypt), Auth-Middleware alle Endpoints, Rate Limiting - 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` 📚 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:** **AI-Prompts Flexibilisierung - Komplett überarbeitet:**
@ -316,14 +356,15 @@ frontend/src/
📚 Details: `.claude/docs/functional/AI_PROMPTS.md` 📚 Details: `.claude/docs/functional/AI_PROMPTS.md`
**Related Gitea Issues:** **Related Gitea Issues:**
- #28: Unified Prompt System - ✅ CLOSED (26.03.2026) - Gitea #28: AI-Prompts Flexibilisierung - ✅ CLOSED (26.03.2026)
- #43: Enhanced Debug UI - 🔲 OPEN (Future enhancement) - Gitea #42, #43: Enhanced Debug UI - 🔲 OPEN (Future enhancement)
- #44: BUG - Analysen löschen - 🔲 OPEN (High priority) - Gitea #44: BUG - Analysen löschen - ✅ CLOSED (26.03.2026)
- #45: KI Prompt-Optimierer - 🔲 OPEN (Future feature) - Gitea #45: KI Prompt-Optimierer - 🔲 OPEN (Future feature)
- #46: KI Prompt-Ersteller - 🔲 OPEN (Future feature) - Gitea #46: KI Prompt-Ersteller - 🔲 OPEN (Future feature)
- #47: Value Table - ✅ CLOSED (26.03.2026) - 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:** **AI-Analyse Transparenz - Vollständige Platzhalter-Anzeige:**
@ -371,6 +412,65 @@ frontend/src/
📚 Details: `.claude/docs/functional/AI_PROMPTS.md` 📚 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 ## Feature-Roadmap
> 📋 **Detaillierte Roadmap:** `.claude/docs/ROADMAP.md` (Phasen 0-3, Timeline, Abhängigkeiten) > 📋 **Detaillierte Roadmap:** `.claude/docs/ROADMAP.md` (Phasen 0-3, Timeline, Abhängigkeiten)

View File

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

View File

@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""
Quick Fix: Insert seed data for goal_type_definitions
This script ONLY inserts the 8 standard goal types.
Safe to run multiple times (uses ON CONFLICT DO NOTHING).
Run inside backend container:
docker exec bodytrack-dev-backend-1 python fix_seed_goal_types.py
"""
import psycopg2
import os
from psycopg2.extras import RealDictCursor
# Database connection
DB_HOST = os.getenv('DB_HOST', 'db')
DB_PORT = os.getenv('DB_PORT', '5432')
DB_NAME = os.getenv('DB_NAME', 'bodytrack')
DB_USER = os.getenv('DB_USER', 'bodytrack')
DB_PASS = os.getenv('DB_PASSWORD', '')
SEED_DATA = [
{
'type_key': 'weight',
'label_de': 'Gewicht',
'label_en': 'Weight',
'unit': 'kg',
'icon': '⚖️',
'category': 'body',
'source_table': 'weight_log',
'source_column': 'weight',
'aggregation_method': 'latest',
'description': 'Aktuelles Körpergewicht',
'is_system': True
},
{
'type_key': 'body_fat',
'label_de': 'Körperfett',
'label_en': 'Body Fat',
'unit': '%',
'icon': '📊',
'category': 'body',
'source_table': 'caliper_log',
'source_column': 'body_fat_pct',
'aggregation_method': 'latest',
'description': 'Körperfettanteil aus Caliper-Messung',
'is_system': True
},
{
'type_key': 'lean_mass',
'label_de': 'Muskelmasse',
'label_en': 'Lean Mass',
'unit': 'kg',
'icon': '💪',
'category': 'body',
'calculation_formula': '{"type": "lean_mass", "dependencies": ["weight_log.weight", "caliper_log.body_fat_pct"], "formula": "weight - (weight * body_fat_pct / 100)"}',
'description': 'Fettfreie Körpermasse (berechnet aus Gewicht und Körperfett)',
'is_system': True
},
{
'type_key': 'vo2max',
'label_de': 'VO2Max',
'label_en': 'VO2Max',
'unit': 'ml/kg/min',
'icon': '🫁',
'category': 'recovery',
'source_table': 'vitals_baseline',
'source_column': 'vo2_max',
'aggregation_method': 'latest',
'description': 'Maximale Sauerstoffaufnahme (geschätzt oder gemessen)',
'is_system': True
},
{
'type_key': 'rhr',
'label_de': 'Ruhepuls',
'label_en': 'Resting Heart Rate',
'unit': 'bpm',
'icon': '💓',
'category': 'recovery',
'source_table': 'vitals_baseline',
'source_column': 'resting_hr',
'aggregation_method': 'latest',
'description': 'Ruhepuls morgens vor dem Aufstehen',
'is_system': True
},
{
'type_key': 'bp',
'label_de': 'Blutdruck',
'label_en': 'Blood Pressure',
'unit': 'mmHg',
'icon': '❤️',
'category': 'recovery',
'source_table': 'blood_pressure_log',
'source_column': 'systolic',
'aggregation_method': 'latest',
'description': 'Blutdruck (aktuell nur systolisch, v2.0: beide Werte)',
'is_system': True
},
{
'type_key': 'strength',
'label_de': 'Kraft',
'label_en': 'Strength',
'unit': 'kg',
'icon': '🏋️',
'category': 'activity',
'description': 'Maximalkraft (Platzhalter, Datenquelle in v2.0)',
'is_system': True,
'is_active': False
},
{
'type_key': 'flexibility',
'label_de': 'Beweglichkeit',
'label_en': 'Flexibility',
'unit': 'cm',
'icon': '🤸',
'category': 'activity',
'description': 'Beweglichkeit (Platzhalter, Datenquelle in v2.0)',
'is_system': True,
'is_active': False
}
]
def main():
print("=" * 70)
print("Goal Type Definitions - Seed Data Fix")
print("=" * 70)
# Connect to database
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASS
)
conn.autocommit = False
cur = conn.cursor(cursor_factory=RealDictCursor)
try:
# Check current state
cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions")
before_count = cur.fetchone()['count']
print(f"\nBefore: {before_count} goal types in database")
# Insert seed data
print(f"\nInserting {len(SEED_DATA)} standard goal types...")
inserted = 0
skipped = 0
for data in SEED_DATA:
columns = list(data.keys())
values = [data[col] for col in columns]
placeholders = ', '.join(['%s'] * len(values))
cols_str = ', '.join(columns)
sql = f"""
INSERT INTO goal_type_definitions ({cols_str})
VALUES ({placeholders})
ON CONFLICT (type_key) DO NOTHING
RETURNING id
"""
cur.execute(sql, values)
result = cur.fetchone()
if result:
inserted += 1
print(f"{data['type_key']}: {data['label_de']}")
else:
skipped += 1
print(f" - {data['type_key']}: already exists (skipped)")
conn.commit()
# Check final state
cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions")
after_count = cur.fetchone()['count']
print(f"\nAfter: {after_count} goal types in database")
print(f" Inserted: {inserted}")
print(f" Skipped: {skipped}")
# Show summary
cur.execute("""
SELECT type_key, label_de, is_active, is_system
FROM goal_type_definitions
ORDER BY is_system DESC, type_key
""")
print("\n" + "=" * 70)
print("Current Goal Types:")
print("=" * 70)
print(f"\n{'Type Key':<20} {'Label':<20} {'System':<8} {'Active':<8}")
print("-" * 70)
for row in cur.fetchall():
status = "YES" if row['is_system'] else "NO"
active = "YES" if row['is_active'] else "NO"
print(f"{row['type_key']:<20} {row['label_de']:<20} {status:<8} {active:<8}")
print("\n✅ DONE! Goal types seeded successfully.")
print("\nNext step: Reload frontend to see the changes.")
except Exception as e:
conn.rollback()
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
finally:
cur.close()
conn.close()
if __name__ == '__main__':
main()

504
backend/goal_utils.py Normal file
View File

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

View File

@ -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 admin_activity_mappings, sleep, rest_days
from routers import vitals_baseline, blood_pressure # v9d Phase 2d Refactored from routers import vitals_baseline, blood_pressure # v9d Phase 2d Refactored
from routers import evaluation # v9d/v9e Training Type Profiles (#15) from routers import evaluation # v9d/v9e Training Type Profiles (#15)
from routers import goals # v9e Goal System (Strategic + Tactical)
# ── App Configuration ───────────────────────────────────────────────────────── # ── App Configuration ─────────────────────────────────────────────────────────
DATA_DIR = Path(os.getenv("DATA_DIR", "./data")) 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(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(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(evaluation.router) # /api/evaluation/* (v9d/v9e Training Profiles #15)
app.include_router(goals.router) # /api/goals/* (v9e Goal System Strategic + Tactical)
# ── Health Check ────────────────────────────────────────────────────────────── # ── Health Check ──────────────────────────────────────────────────────────────
@app.get("/") @app.get("/")

View File

@ -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';

View File

@ -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
);
*/

View File

@ -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;

View File

@ -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;
*/

View File

@ -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;

View File

@ -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';

View File

@ -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)';

View File

@ -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';

1252
backend/routers/goals.py Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -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 (
<div className="card">
<h2>🎯 Trainingsziel</h2>
<div className="form-row">
<label>Hauptziel</label>
<select value={selectedMode} onChange={e => setSelectedMode(e.target.value)}>
{Object.entries(goalModes).map(([key, config]) => (
<option key={key} value={key}>
{config.label}
</option>
))}
</select>
<p style={{fontSize: 12, color: 'var(--text3)'}}>
{goalModes[selectedMode]?.description}
</p>
</div>
{(selectedMode === 'weight_loss' || selectedMode === 'recomposition') && (
<div className="form-row">
<label>Zielgewicht (optional)</label>
<input type="number" step="0.1" value={goalWeight} onChange={...} />
</div>
)}
<button onClick={saveGoal}>Ziel speichern</button>
</div>
)
}
```
### 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?**

View File

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

View File

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

View File

@ -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
<PagePrompts pageSlug="weight_history" />
// 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

View File

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

284
docs/TODO_GOAL_SYSTEM.md Normal file
View File

@ -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 🚀

View File

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

View File

@ -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:
<div className="form-row">
<label>Verfügbar auf Seiten</label>
<div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
{PAGE_OPTIONS.map(opt => (
<label key={opt.value} style={{display: 'flex', gap: 8, alignItems: 'center'}}>
<input
type="checkbox"
checked={availableOn.includes(opt.value)}
onChange={e => {
if (e.target.checked) {
setAvailableOn([...availableOn, opt.value])
} else {
// Don't allow unchecking all
if (availableOn.length > 1) {
setAvailableOn(availableOn.filter(v => v !== opt.value))
}
}
}}
/>
{opt.label}
</label>
))}
</div>
</div>
```
### 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 (
<div className="page">
<h1>Gewicht - Verlauf</h1>
{/* Chart */}
<WeightChart data={data} />
{/* AI Prompts Widget */}
{prompts.length > 0 && (
<div className="ai-prompts-widget">
<h3>🤖 KI-Analysen</h3>
{prompts.map(p => (
<button
key={p.slug}
onClick={() => runAnalysis(p.slug)}
disabled={runningAnalysis === p.slug}
>
{p.name} {runningAnalysis === p.slug ? '⏳' : '▶'}
</button>
))}
</div>
)}
{/* Analysis Result Modal */}
{analysisResult && (
<AnalysisResultModal
result={analysisResult}
onClose={() => setAnalysisResult(null)}
/>
)}
{/* Data Table */}
<DataTable entries={entries} />
</div>
)
}
```
**Wiederverwendbare Komponente:**
```javascript
// components/PagePrompts.jsx
export function PagePrompts({ pageSlug }) {
// ... logic ...
return (
<div className="page-prompts">
<h3>🤖 KI-Analysen</h3>
{prompts.map(p => (
<PromptButton key={p.slug} prompt={p} onRun={runAnalysis} />
))}
</div>
)
}
// Usage in any page:
<PagePrompts pageSlug="weight_history" />
```
## 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)

View File

@ -31,10 +31,13 @@ import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage'
import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage' import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage'
import AdminTrainingProfiles from './pages/AdminTrainingProfiles' import AdminTrainingProfiles from './pages/AdminTrainingProfiles'
import AdminPromptsPage from './pages/AdminPromptsPage' import AdminPromptsPage from './pages/AdminPromptsPage'
import AdminGoalTypesPage from './pages/AdminGoalTypesPage'
import SubscriptionPage from './pages/SubscriptionPage' import SubscriptionPage from './pages/SubscriptionPage'
import SleepPage from './pages/SleepPage' import SleepPage from './pages/SleepPage'
import RestDaysPage from './pages/RestDaysPage' import RestDaysPage from './pages/RestDaysPage'
import VitalsPage from './pages/VitalsPage' import VitalsPage from './pages/VitalsPage'
import GoalsPage from './pages/GoalsPage'
import CustomGoalsPage from './pages/CustomGoalsPage'
import './app.css' import './app.css'
function Nav() { function Nav() {
@ -172,6 +175,8 @@ function AppShell() {
<Route path="/sleep" element={<SleepPage/>}/> <Route path="/sleep" element={<SleepPage/>}/>
<Route path="/rest-days" element={<RestDaysPage/>}/> <Route path="/rest-days" element={<RestDaysPage/>}/>
<Route path="/vitals" element={<VitalsPage/>}/> <Route path="/vitals" element={<VitalsPage/>}/>
<Route path="/goals" element={<GoalsPage/>}/>
<Route path="/custom-goals" element={<CustomGoalsPage/>}/>
<Route path="/nutrition" element={<NutritionPage/>}/> <Route path="/nutrition" element={<NutritionPage/>}/>
<Route path="/activity" element={<ActivityPage/>}/> <Route path="/activity" element={<ActivityPage/>}/>
<Route path="/analysis" element={<Analysis/>}/> <Route path="/analysis" element={<Analysis/>}/>
@ -186,6 +191,7 @@ function AppShell() {
<Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/> <Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/>
<Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/> <Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/>
<Route path="/admin/prompts" element={<AdminPromptsPage/>}/> <Route path="/admin/prompts" element={<AdminPromptsPage/>}/>
<Route path="/admin/goal-types" element={<AdminGoalTypesPage/>}/>
<Route path="/subscription" element={<SubscriptionPage/>}/> <Route path="/subscription" element={<SubscriptionPage/>}/>
</Routes> </Routes>
</main> </main>

View File

@ -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 (
<div className="page">
<div style={{ textAlign: 'center', padding: 40 }}>
<div className="spinner"></div>
</div>
</div>
)
}
return (
<div className="page">
<div className="page-header">
<h1><Database size={24} /> Goal Type Verwaltung</h1>
</div>
{error && (
<div className="card" style={{ background: '#FEF2F2', border: '1px solid #FCA5A5', marginBottom: 16 }}>
<p style={{ color: '#DC2626', margin: 0 }}>{error}</p>
</div>
)}
{toast && (
<div style={{
position: 'fixed',
top: 16,
right: 16,
background: 'var(--accent)',
color: 'white',
padding: '12px 20px',
borderRadius: 8,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
zIndex: 1000
}}>
{toast}
</div>
)}
<div className="card" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div>
<h2 style={{ margin: 0, marginBottom: 4 }}>Verfügbare Goal Types</h2>
<p style={{ fontSize: 13, color: 'var(--text2)', margin: 0 }}>
{goalTypes.length} Types registriert ({goalTypes.filter(t => t.is_system).length} System, {goalTypes.filter(t => !t.is_system).length} Custom)
</p>
</div>
<button className="btn-primary" onClick={handleCreate}>
<Plus size={16} /> Neuer Type
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{goalTypes.map(type => (
<div
key={type.id}
className="card"
style={{
background: 'var(--surface2)',
padding: 12,
border: type.is_system ? '2px solid var(--accent)' : '1px solid var(--border)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 20 }}>{type.icon || '📊'}</span>
<span style={{ fontWeight: 600 }}>{type.label_de}</span>
<span style={{
fontSize: 11,
padding: '2px 8px',
borderRadius: 4,
background: 'var(--surface)',
color: 'var(--text2)'
}}>
{type.unit}
</span>
{type.is_system && (
<span style={{
fontSize: 11,
padding: '2px 8px',
borderRadius: 4,
background: 'var(--accent)',
color: 'white'
}}>
SYSTEM
</span>
)}
{!type.is_active && (
<span style={{
fontSize: 11,
padding: '2px 8px',
borderRadius: 4,
background: '#F59E0B',
color: 'white'
}}>
INAKTIV
</span>
)}
</div>
<div style={{ fontSize: 12, color: 'var(--text2)' }}>
<strong>Key:</strong> {type.type_key}
{type.source_table && (
<>
{' | '}<strong>Quelle:</strong> {type.source_table}.{type.source_column}
{' | '}<strong>Methode:</strong> {type.aggregation_method}
</>
)}
{type.calculation_formula && (
<>
{' | '}<strong>Formel:</strong> Komplex (JSON)
</>
)}
</div>
{type.description && (
<div style={{ fontSize: 12, color: 'var(--text2)', marginTop: 4, fontStyle: 'italic' }}>
{type.description}
</div>
)}
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button
className="btn-secondary"
onClick={() => handleEdit(type)}
style={{ padding: '6px 12px' }}
>
<Pencil size={14} />
</button>
<button
className="btn-secondary"
onClick={() => handleDelete(type.id, type.label_de, type.is_system)}
style={{ padding: '6px 12px', color: type.is_system ? '#F59E0B' : '#DC2626' }}
>
<Trash2 size={14} />
</button>
</div>
</div>
</div>
))}
</div>
</div>
{/* Form Modal */}
{showForm && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
zIndex: 1000,
padding: 16,
paddingTop: 40,
overflowY: 'auto'
}}>
<div className="card" style={{ maxWidth: 600, width: '100%', marginBottom: 40 }}>
<div className="card-title">
{editingType ? 'Goal Type bearbeiten' : 'Neuer Goal Type'}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Type Key (nur bei Create) */}
{!editingType && (
<div>
<label className="form-label">
Eindeutiger Key * (z.B. meditation_minutes)
</label>
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.type_key}
onChange={e => setFormData(f => ({ ...f, type_key: e.target.value }))}
placeholder="snake_case verwenden"
/>
</div>
)}
{/* Label */}
<div>
<label className="form-label">Label (Deutsch) *</label>
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.label_de}
onChange={e => setFormData(f => ({ ...f, label_de: e.target.value }))}
placeholder="z.B. Meditation"
/>
</div>
{/* Unit & Icon */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<label className="form-label">Einheit *</label>
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.unit}
onChange={e => setFormData(f => ({ ...f, unit: e.target.value }))}
placeholder="z.B. min/Tag"
/>
</div>
<div>
<label className="form-label">Icon (Emoji)</label>
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.icon}
onChange={e => setFormData(f => ({ ...f, icon: e.target.value }))}
placeholder="🧘"
/>
</div>
</div>
{/* Category */}
<div>
<label className="form-label">Kategorie</label>
<select
className="form-input"
style={{ width: '100%' }}
value={formData.category}
onChange={e => setFormData(f => ({ ...f, category: e.target.value }))}
>
{CATEGORIES.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
{/* Data Source */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<label className="form-label">Tabelle</label>
{schemaInfo ? (
<select
className="form-input"
style={{ width: '100%' }}
value={formData.source_table}
onChange={e => setFormData(f => ({ ...f, source_table: e.target.value, source_column: '' }))}
>
<option value="">-- Optional --</option>
{Object.entries(schemaInfo).map(([table, info]) => (
<option key={table} value={table} title={info.description}>
{table} - {info.description}
</option>
))}
</select>
) : (
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.source_table}
onChange={e => setFormData(f => ({ ...f, source_table: e.target.value }))}
placeholder="Lade Schema..."
disabled
/>
)}
</div>
<div>
<label className="form-label">Spalte</label>
{schemaInfo && formData.source_table && schemaInfo[formData.source_table] ? (
<select
className="form-input"
style={{ width: '100%' }}
value={formData.source_column}
onChange={e => setFormData(f => ({ ...f, source_column: e.target.value }))}
>
<option value="">-- Wählen --</option>
{Object.entries(schemaInfo[formData.source_table].columns).map(([col, info]) => (
<option key={col} value={col} title={info.description}>
{col} - {info.description}
</option>
))}
</select>
) : (
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.source_column}
onChange={e => setFormData(f => ({ ...f, source_column: e.target.value }))}
placeholder={formData.source_table ? "Spalte wählen..." : "Erst Tabelle wählen"}
disabled={!formData.source_table}
/>
)}
</div>
</div>
{/* Aggregation Method */}
<div>
<label className="form-label">Aggregationsmethode</label>
<select
className="form-input"
style={{ width: '100%' }}
value={formData.aggregation_method}
onChange={e => setFormData(f => ({ ...f, aggregation_method: e.target.value }))}
>
{AGGREGATION_METHODS.map(method => (
<option key={method.value} value={method.value}>{method.label}</option>
))}
</select>
</div>
{/* Filter Conditions */}
<div>
<label className="form-label">Filter (optional, JSON)</label>
<textarea
className="form-input"
style={{ width: '100%', minHeight: 80, fontFamily: 'monospace', fontSize: 13 }}
value={formData.filter_conditions}
onChange={e => setFormData(f => ({ ...f, filter_conditions: e.target.value }))}
placeholder={'Beispiel:\n{\n "training_type": "strength"\n}\n\nOder mehrere Werte:\n{\n "training_type": ["strength", "hiit"]\n}'}
/>
<div style={{ fontSize: 12, color: 'var(--text2)', marginTop: 4 }}>
💡 Filtert Einträge nach Spalten. Beispiel: <code>{`{"training_type": "strength"}`}</code> zählt nur Krafttraining
</div>
</div>
{/* Description */}
<div>
<label className="form-label">Beschreibung (optional)</label>
<textarea
className="form-input"
style={{ width: '100%', minHeight: 60 }}
value={formData.description}
onChange={e => setFormData(f => ({ ...f, description: e.target.value }))}
placeholder="Kurze Erklärung..."
/>
</div>
{/* Buttons */}
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
<button className="btn-primary" onClick={handleSave} style={{ flex: 1 }}>
Speichern
</button>
<button
className="btn-secondary"
onClick={() => {
setShowForm(false)
setError(null)
}}
style={{ flex: 1 }}
>
Abbrechen
</button>
</div>
</div>
</div>
</div>
)}
</div>
)
}

View File

@ -453,7 +453,7 @@ export default function AdminPanel() {
</div> </div>
{/* KI-Prompts Section */} {/* KI-Prompts Section */}
<div className="card"> <div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}> <div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> KI-Prompts (v9f) <Settings size={16} color="var(--accent)"/> KI-Prompts (v9f)
</div> </div>
@ -468,6 +468,23 @@ export default function AdminPanel() {
</Link> </Link>
</div> </div>
</div> </div>
{/* Goal Types Section */}
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> Ziel-Typen (v9e)
</div>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
Verwalte Goal-Type-Definitionen: Erstelle custom goal types mit oder ohne automatische Datenquelle.
</div>
<div style={{display:'grid',gap:8}}>
<Link to="/admin/goal-types">
<button className="btn btn-secondary btn-full">
🎯 Ziel-Typen verwalten
</button>
</Link>
</div>
</div>
</div> </div>
) )
} }

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { Brain, Trash2, ChevronDown, ChevronUp } from 'lucide-react' import { Brain, Trash2, ChevronDown, ChevronUp, Target } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { api } from '../utils/api' import { api } from '../utils/api'
import { useAuth } from '../context/AuthContext' import { useAuth } from '../context/AuthContext'
import Markdown from '../utils/Markdown' import Markdown from '../utils/Markdown'
@ -277,6 +278,7 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
export default function Analysis() { export default function Analysis() {
const { canUseAI } = useAuth() const { canUseAI } = useAuth()
const navigate = useNavigate()
const [prompts, setPrompts] = useState([]) const [prompts, setPrompts] = useState([])
const [allInsights, setAllInsights] = useState([]) const [allInsights, setAllInsights] = useState([])
const [loading, setLoading] = useState(null) const [loading, setLoading] = useState(null)
@ -386,7 +388,16 @@ export default function Analysis() {
return ( return (
<div> <div>
<h1 className="page-title">KI-Analyse</h1> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<h1 className="page-title" style={{ margin: 0 }}>KI-Analyse</h1>
<button
className="btn btn-secondary"
onClick={() => navigate('/goals')}
style={{ fontSize: 13, padding: '6px 12px' }}
>
<Target size={14} /> Ziele
</button>
</div>
<div className="tabs"> <div className="tabs">
<button className={'tab'+(tab==='run'?' active':'')} onClick={()=>setTab('run')}>Analysen starten</button> <button className={'tab'+(tab==='run'?' active':'')} onClick={()=>setTab('run')}>Analysen starten</button>

View File

@ -66,6 +66,13 @@ const ENTRIES = [
to: '/vitals', to: '/vitals',
color: '#E74C3C', color: '#E74C3C',
}, },
{
icon: '🎯',
label: 'Eigene Ziele',
sub: 'Fortschritte für individuelle Ziele erfassen',
to: '/custom-goals',
color: '#1D9E75',
},
{ {
icon: '📖', icon: '📖',
label: 'Messanleitung', label: 'Messanleitung',

View File

@ -0,0 +1,370 @@
import { useState, useEffect } from 'react'
import { api } from '../utils/api'
import { Target, TrendingUp, Calendar, CheckCircle2, AlertCircle } from 'lucide-react'
import dayjs from 'dayjs'
export default function CustomGoalsPage() {
const [customGoals, setCustomGoals] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [selectedGoal, setSelectedGoal] = useState(null)
const [formData, setFormData] = useState({
date: new Date().toISOString().split('T')[0],
value: '',
note: ''
})
const [recentProgress, setRecentProgress] = useState([])
useEffect(() => {
loadCustomGoals()
}, [])
const loadCustomGoals = async () => {
try {
setLoading(true)
const grouped = await api.listGoalsGrouped()
// Extract all goals and filter for custom only (no source_table)
const allGoals = Object.values(grouped).flat()
const custom = allGoals.filter(g => !g.source_table)
setCustomGoals(custom)
setError(null)
} catch (err) {
console.error('Failed to load custom goals:', err)
setError(err.message || 'Fehler beim Laden')
} finally {
setLoading(false)
}
}
const loadRecentProgress = async (goalId) => {
try {
const entries = await api.listGoalProgress(goalId)
setRecentProgress(entries.slice(0, 5)) // Last 5 entries
} catch (err) {
console.error('Failed to load progress:', err)
setRecentProgress([])
}
}
const handleSelectGoal = async (goal) => {
setSelectedGoal(goal)
setFormData({
date: new Date().toISOString().split('T')[0],
value: goal.current_value || '',
note: ''
})
await loadRecentProgress(goal.id)
}
const handleSaveProgress = async () => {
if (!formData.value || !formData.date) {
setError('Bitte Datum und Wert eingeben')
return
}
try {
const data = {
date: formData.date,
value: parseFloat(formData.value),
note: formData.note || null
}
await api.createGoalProgress(selectedGoal.id, data)
// Reset form and reload
setFormData({
date: new Date().toISOString().split('T')[0],
value: '',
note: ''
})
await loadCustomGoals()
await loadRecentProgress(selectedGoal.id)
// Update selected goal with new current_value
const updated = customGoals.find(g => g.id === selectedGoal.id)
if (updated) setSelectedGoal(updated)
setError(null)
} catch (err) {
console.error('Failed to save progress:', err)
setError(err.message || 'Fehler beim Speichern')
}
}
const getProgressPercentage = (goal) => {
if (!goal.current_value || !goal.target_value) return 0
const current = parseFloat(goal.current_value)
const target = parseFloat(goal.target_value)
const start = parseFloat(goal.start_value) || 0
if (goal.direction === 'decrease') {
return Math.min(100, Math.max(0, ((start - current) / (start - target)) * 100))
} else {
return Math.min(100, Math.max(0, ((current - start) / (target - start)) * 100))
}
}
if (loading) {
return (
<div style={{ padding: 20, textAlign: 'center' }}>
<div className="spinner" />
</div>
)
}
return (
<div style={{ paddingBottom: 80 }}>
{/* Header */}
<div style={{
background: 'linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%)',
color: 'white',
padding: '24px 16px',
marginBottom: 16
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
<Target size={28} />
<h1 style={{ fontSize: 24, fontWeight: 700, margin: 0 }}>Eigene Ziele</h1>
</div>
<div style={{ fontSize: 14, opacity: 0.9 }}>
Erfasse Fortschritte für deine individuellen Ziele
</div>
</div>
{error && (
<div style={{
margin: '0 16px 16px',
padding: 12,
background: '#FEE2E2',
color: '#991B1B',
borderRadius: 8,
fontSize: 14
}}>
{error}
</div>
)}
{customGoals.length === 0 ? (
<div className="card" style={{ margin: 16, textAlign: 'center', padding: 40 }}>
<Target size={48} style={{ color: 'var(--text3)', margin: '0 auto 16px' }} />
<div style={{ fontSize: 16, color: 'var(--text2)', marginBottom: 8 }}>
Keine eigenen Ziele vorhanden
</div>
<div style={{ fontSize: 14, color: 'var(--text3)' }}>
Erstelle eigene Ziele über die Ziele-Seite in der Analyse
</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, padding: 16 }}>
{/* Goal Selection */}
<div className="card">
<h2 style={{ fontSize: 16, marginBottom: 12, fontWeight: 600 }}>
Ziel auswählen ({customGoals.length})
</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{customGoals.map(goal => {
const progress = getProgressPercentage(goal)
const isSelected = selectedGoal?.id === goal.id
return (
<button
key={goal.id}
onClick={() => handleSelectGoal(goal)}
style={{
width: '100%',
padding: 12,
background: isSelected ? 'var(--accent)' : 'var(--surface2)',
color: isSelected ? 'white' : 'var(--text1)',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
textAlign: 'left',
transition: 'all 0.2s'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<span style={{ fontWeight: 600 }}>
{goal.name || goal.label_de || goal.goal_type}
</span>
{goal.current_value && (
<span style={{
fontSize: 18,
fontWeight: 700,
opacity: isSelected ? 1 : 0.8
}}>
{goal.current_value} {goal.unit}
</span>
)}
</div>
{goal.target_value && (
<>
<div style={{ fontSize: 13, opacity: isSelected ? 0.9 : 0.7, marginBottom: 6 }}>
Ziel: {goal.target_value} {goal.unit}
</div>
<div style={{
width: '100%',
height: 6,
background: isSelected ? 'rgba(255,255,255,0.2)' : 'var(--surface)',
borderRadius: 3,
overflow: 'hidden'
}}>
<div style={{
width: `${progress}%`,
height: '100%',
background: isSelected ? 'white' : 'var(--accent)',
transition: 'width 0.3s'
}} />
</div>
</>
)}
</button>
)
})}
</div>
</div>
{/* Progress Entry Form */}
{selectedGoal && (
<div className="card">
<h2 style={{ fontSize: 16, marginBottom: 16, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8 }}>
<TrendingUp size={20} style={{ color: 'var(--accent)' }} />
Fortschritt erfassen
</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<div style={{
fontSize: 14,
fontWeight: 600,
marginBottom: 8,
color: 'var(--text1)'
}}>
Datum
</div>
<input
type="date"
className="form-input"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
max={new Date().toISOString().split('T')[0]}
style={{ width: '100%', textAlign: 'left' }}
/>
</div>
<div>
<div style={{
fontSize: 14,
fontWeight: 600,
marginBottom: 8,
color: 'var(--text1)'
}}>
Wert ({selectedGoal.unit})
</div>
<input
type="number"
step="0.01"
className="form-input"
value={formData.value}
onChange={(e) => setFormData({ ...formData, value: e.target.value })}
placeholder={`Aktueller Wert in ${selectedGoal.unit}`}
style={{ width: '100%', textAlign: 'left' }}
/>
</div>
<div>
<div style={{
fontSize: 14,
fontWeight: 600,
marginBottom: 8,
color: 'var(--text1)'
}}>
Notiz (optional)
</div>
<textarea
className="form-input"
value={formData.note}
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
placeholder="Optionale Notiz zu dieser Messung..."
rows={2}
style={{ width: '100%', textAlign: 'left' }}
/>
</div>
<button
className="btn-primary"
onClick={handleSaveProgress}
disabled={!formData.value}
style={{ width: '100%' }}
>
<CheckCircle2 size={18} style={{ marginRight: 8 }} />
Wert speichern
</button>
</div>
</div>
)}
{/* Recent Progress */}
{selectedGoal && recentProgress.length > 0 && (
<div className="card">
<h2 style={{ fontSize: 16, marginBottom: 12, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 8 }}>
<Calendar size={20} style={{ color: 'var(--accent)' }} />
Letzte Einträge
</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{recentProgress.map(entry => (
<div key={entry.id} style={{
padding: 12,
background: 'var(--surface2)',
borderRadius: 8
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontSize: 14, color: 'var(--text2)' }}>
{dayjs(entry.date).format('DD.MM.YYYY')}
</span>
<span style={{ fontSize: 18, fontWeight: 600, color: 'var(--accent)' }}>
{entry.value} {selectedGoal.unit}
</span>
</div>
{entry.note && (
<div style={{ fontSize: 13, color: 'var(--text3)' }}>
{entry.note}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Help Card */}
{!selectedGoal && (
<div style={{
padding: 16,
background: 'var(--surface2)',
borderRadius: 8,
border: '1px solid var(--border)'
}}>
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
<AlertCircle size={20} style={{ color: 'var(--accent)', flexShrink: 0, marginTop: 2 }} />
<div style={{ fontSize: 14, color: 'var(--text2)' }}>
<div style={{ fontWeight: 600, marginBottom: 4, color: 'var(--text1)' }}>
Eigene Ziele erfassen
</div>
Wähle ein Ziel aus und erfasse regelmäßig deine Fortschritte.
Die Werte werden automatisch in deine Zielverfolgung übernommen.
</div>
</div>
</div>
)}
</div>
)}
</div>
)
}

View File

@ -497,6 +497,21 @@ export default function Dashboard() {
</div> </div>
)} )}
{/* Goals Preview */}
<div className="card section-gap" style={{marginBottom:16,cursor:'pointer'}}
onClick={()=>nav('/goals')}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
<div style={{fontWeight:600,fontSize:13}}>🎯 Ziele</div>
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
onClick={(e)=>{e.stopPropagation();nav('/goals')}}>
Verwalten
</button>
</div>
<div style={{fontSize:12,color:'var(--text2)',padding:'8px 0'}}>
Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen
</div>
</div>
{/* Latest AI insight */} {/* Latest AI insight */}
<div className="card section-gap"> <div className="card section-gap">
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}> <div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}>

File diff suppressed because it is too large Load Diff

View File

@ -330,4 +330,39 @@ export const api = {
// Placeholder Export // Placeholder Export
exportPlaceholderValues: () => req('/prompts/placeholders/export-values'), exportPlaceholderValues: () => req('/prompts/placeholders/export-values'),
// v9e: Goals System (Strategic + Tactical)
getGoalMode: () => req('/goals/mode'),
updateGoalMode: (mode) => req('/goals/mode', jput({goal_mode: mode})),
// Focus Areas (v2.0)
getFocusAreas: () => req('/goals/focus-areas'),
updateFocusAreas: (d) => req('/goals/focus-areas', jput(d)),
listGoals: () => req('/goals/list'),
listGoalsGrouped: () => req('/goals/grouped'),
createGoal: (d) => req('/goals/create', json(d)),
updateGoal: (id,d) => req(`/goals/${id}`, jput(d)),
deleteGoal: (id) => req(`/goals/${id}`, {method:'DELETE'}),
// Goal Progress (v2.1)
listGoalProgress: (id) => req(`/goals/${id}/progress`),
createGoalProgress: (id,d) => req(`/goals/${id}/progress`, json(d)),
deleteGoalProgress: (gid,pid) => req(`/goals/${gid}/progress/${pid}`, {method:'DELETE'}),
// Goal Type Definitions (Phase 1.5)
listGoalTypeDefinitions: () => req('/goals/goal-types'),
createGoalType: (d) => req('/goals/goal-types', json(d)),
updateGoalType: (id,d) => req(`/goals/goal-types/${id}`, jput(d)),
deleteGoalType: (id) => req(`/goals/goal-types/${id}`, {method:'DELETE'}),
getSchemaInfo: () => req('/goals/schema-info'),
// Training Phases
listTrainingPhases: () => req('/goals/phases'),
createTrainingPhase: (d) => req('/goals/phases', json(d)),
updatePhaseStatus: (id,status) => req(`/goals/phases/${id}/status?status=${status}`, jput({})),
// Fitness Tests
listFitnessTests: () => req('/goals/tests'),
createFitnessTest: (d) => req('/goals/tests', json(d)),
} }