Merge pull request 'Goalsystem V1' (#50) from develop into main
Reviewed-on: #50
This commit is contained in:
commit
9d22e7e8af
118
CLAUDE.md
118
CLAUDE.md
|
|
@ -76,7 +76,46 @@ frontend/src/
|
|||
└── technical/ # MEMBERSHIP_SYSTEM.md
|
||||
```
|
||||
|
||||
## Aktuelle Version: v9e (Issue #28, #47 Complete) 🚀 Ready for Production 26.03.2026
|
||||
## Aktuelle Version: v9e+ (Phase 1 Goal System Fixes) 🎯 Ready for Phase 0b - 27.03.2026
|
||||
|
||||
### Letzte Updates (27.03.2026 - Phase 1 Complete) 🆕
|
||||
- ✅ **Custom Goals Page (Capture/Eigene Ziele):**
|
||||
- Neue Seite für tägliche Werterfassung individueller Ziele
|
||||
- Dedizierte UI für custom goals (ohne automatische Datenquelle)
|
||||
- Verhindert Verwechslung mit automatischem Tracking (Gewicht, Aktivität, etc.)
|
||||
- Clean UX: Zielauswahl → Schnellerfassung → Verlauf (letzte 5 Einträge)
|
||||
- Navigation: Capture Hub + direkter Link
|
||||
- ✅ **UX-Improvements Progress Modal:**
|
||||
- Volle Breite Eingabefelder, Labels als Überschriften, linksbündiger Text
|
||||
- Progress-Button nur bei custom goals sichtbar (source_table IS NULL)
|
||||
- ✅ **Architektur-Klarstellung:**
|
||||
- Analysis/Goals → Strategisch (Ziele definieren, Prioritäten setzen)
|
||||
- Capture/Custom Goals → Taktisch (tägliche Ist-Wert-Erfassung)
|
||||
- History → Auswertung (Zielerreichungs-Analysen)
|
||||
|
||||
### Updates (27.03.2026 - Phase 1 Fixes)
|
||||
- ✅ **Abstraction Layer:** goal_utils.py für zukunftssichere Phase 0b Platzhalter
|
||||
- ✅ **Primary Goal Toggle Fix:** is_primary Update funktioniert korrekt
|
||||
- ✅ **Lean Mass Berechnung:** Magermasse current_value wird berechnet
|
||||
- ✅ **VO2Max Fix:** Spaltenname vo2_max (statt vo2max) korrigiert
|
||||
- ✅ **Keine Doppelarbeit:** Phase 0b Platzhalter (120+) müssen bei v2.0 nicht umgeschrieben werden
|
||||
|
||||
### Phase 0a Completion (26.03.2026) 🎯
|
||||
- ✅ **Phase 0a: Minimal Goal System:** Strategic + Tactical Layers implementiert
|
||||
- ✅ **Migration 022:** goal_mode, goals, training_phases, fitness_tests tables
|
||||
- ✅ **Backend Router:** goals.py mit vollständigem CRUD (490 Zeilen)
|
||||
- ✅ **Frontend:** GoalsPage mit mobile-friendly Design (570 Zeilen)
|
||||
- ✅ **Navigation:** Goals Preview (Dashboard) + Ziele Button (Analysis)
|
||||
- ✅ **Basis geschaffen:** Für 120+ goal-aware Platzhalter (Phase 0b)
|
||||
- ✅ **Dokumentation:** issue-50, NEXT_STEPS_2026-03-26.md, GOALS_SYSTEM_UNIFIED_ANALYSIS.md
|
||||
|
||||
### Frühere Updates (26.03.2026 - Vormittag)
|
||||
- ✅ **circ_summary erweitert:** Best-of-Each Strategie mit Altersangaben
|
||||
- ✅ **Stage Outputs Fix:** Debug-Info für Experten-Modus
|
||||
- ✅ **Collapsible JSON:** Stage-Rohdaten aufklappbar
|
||||
- ✅ **Gitea #28 geschlossen:** AI-Prompts Flexibilisierung
|
||||
- ✅ **Gitea #44 geschlossen:** Analysen löschen behoben
|
||||
- ✅ **Gitea #47 erstellt:** Wertetabelle Optimierung
|
||||
|
||||
### Implementiert ✅
|
||||
- Login (E-Mail + bcrypt), Auth-Middleware alle Endpoints, Rate Limiting
|
||||
|
|
@ -208,7 +247,8 @@ frontend/src/
|
|||
|
||||
📚 Details: `.claude/docs/technical/MEMBERSHIP_SYSTEM.md` · `.claude/docs/architecture/FEATURE_ENFORCEMENT.md`
|
||||
|
||||
### Issue #28: Unified Prompt System ✅ (Completed 26.03.2026)
|
||||
### Feature: Unified Prompt System ✅ (Completed 26.03.2026)
|
||||
> **Gitea:** Issue #28 (AI-Prompts Flexibilisierung) - CLOSED
|
||||
|
||||
**AI-Prompts Flexibilisierung - Komplett überarbeitet:**
|
||||
|
||||
|
|
@ -316,14 +356,15 @@ frontend/src/
|
|||
📚 Details: `.claude/docs/functional/AI_PROMPTS.md`
|
||||
|
||||
**Related Gitea Issues:**
|
||||
- #28: Unified Prompt System - ✅ CLOSED (26.03.2026)
|
||||
- #43: Enhanced Debug UI - 🔲 OPEN (Future enhancement)
|
||||
- #44: BUG - Analysen löschen - 🔲 OPEN (High priority)
|
||||
- #45: KI Prompt-Optimierer - 🔲 OPEN (Future feature)
|
||||
- #46: KI Prompt-Ersteller - 🔲 OPEN (Future feature)
|
||||
- #47: Value Table - ✅ CLOSED (26.03.2026)
|
||||
- Gitea #28: AI-Prompts Flexibilisierung - ✅ CLOSED (26.03.2026)
|
||||
- Gitea #42, #43: Enhanced Debug UI - 🔲 OPEN (Future enhancement)
|
||||
- Gitea #44: BUG - Analysen löschen - ✅ CLOSED (26.03.2026)
|
||||
- Gitea #45: KI Prompt-Optimierer - 🔲 OPEN (Future feature)
|
||||
- Gitea #46: KI Prompt-Ersteller - 🔲 OPEN (Future feature)
|
||||
- Gitea #47: Wertetabelle Optimierung - 🔲 OPEN (Refinement, siehe docs/issues/issue-50)
|
||||
|
||||
### Issue #47: Comprehensive Value Table ✅ (Completed 26.03.2026)
|
||||
### Feature: Comprehensive Value Table ✅ (Completed 26.03.2026)
|
||||
> **Gitea:** Basis-Implementierung abgeschlossen. Issue #47 (Wertetabelle Optimierung) für Refinement offen.
|
||||
|
||||
**AI-Analyse Transparenz - Vollständige Platzhalter-Anzeige:**
|
||||
|
||||
|
|
@ -371,6 +412,65 @@ frontend/src/
|
|||
|
||||
📚 Details: `.claude/docs/functional/AI_PROMPTS.md`
|
||||
|
||||
### Phase 0a: Minimal Goal System ✅ (Completed 26.03.2026)
|
||||
> **Gitea:** Issue #50 (zu erstellen) - COMPLETED
|
||||
> **Dokumentation:** `docs/issues/issue-50-phase-0a-goal-system.md`, `docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md`
|
||||
|
||||
**Zwei-Ebenen-Ziel-Architektur für goal-aware KI-Analysen:**
|
||||
|
||||
- ✅ **Strategic Layer (Goal Modes):**
|
||||
- `goal_mode` in profiles table (weight_loss, strength, endurance, recomposition, health)
|
||||
- Bestimmt Score-Gewichtung für alle KI-Analysen
|
||||
- UI: 5 Goal Mode Cards mit Icons und Beschreibungen
|
||||
|
||||
- ✅ **Tactical Layer (Concrete Goals):**
|
||||
- `goals` table mit vollständigem Progress-Tracking
|
||||
- 8 Goal-Typen: weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr
|
||||
- Auto-calculated progress percentage
|
||||
- Linear projection für target_date
|
||||
- Primary/Secondary goal concept
|
||||
- UI: Goal CRUD mit Fortschrittsbalken, mobile-friendly
|
||||
|
||||
- ✅ **Training Phases Framework:**
|
||||
- `training_phases` table (Auto-Detection vorbereitet)
|
||||
- 5 Phase-Typen: calorie_deficit, calorie_surplus, deload, maintenance, periodization
|
||||
- Status-Flow: suggested → accepted → active → completed
|
||||
- Confidence scoring für KI-basierte Erkennung
|
||||
|
||||
- ✅ **Fitness Tests:**
|
||||
- `fitness_tests` table für standardisierte Tests
|
||||
- 8 Test-Typen: Cooper, Step Test, Pushups, Plank, VO2Max, Strength (Squat/Bench)
|
||||
- Norm-Kategorisierung vorbereitet
|
||||
|
||||
**Backend:**
|
||||
- Migration 022: goal_mode, goals, training_phases, fitness_tests tables
|
||||
- Router: `routers/goals.py` (490 Zeilen) - vollständiges CRUD
|
||||
- API Endpoints: `/api/goals/*` (mode, list, create, update, delete, phases, tests)
|
||||
|
||||
**Frontend:**
|
||||
- GoalsPage: `frontend/src/pages/GoalsPage.jsx` (570 Zeilen)
|
||||
- Mobile-friendly Design (full-width inputs, labels above)
|
||||
- Navigation: Dashboard (Goals Preview Card) + Analysis (🎯 Ziele Button)
|
||||
- api.js: 15+ neue Goal-Funktionen
|
||||
|
||||
**Commits:**
|
||||
- `337667f` - feat: Phase 0a - Minimal Goal System
|
||||
- `906a3b7` - fix: Migration 022 tracking
|
||||
- `75f0a5d` - refactor: mobile-friendly design
|
||||
- `5be52bc` - feat: goals navigation + UX
|
||||
|
||||
**Basis für Phase 0b:**
|
||||
- Foundation für 120+ goal-aware Platzhalter
|
||||
- Score-Berechnungen abhängig von goal_mode
|
||||
- Intelligente Coaching-Funktionen
|
||||
- Automatische Trainingsphasen-Erkennung
|
||||
|
||||
**Nächste Schritte:**
|
||||
- Option A: Issue #49 - Prompt Page Assignment (6-8h, Quick Win)
|
||||
- Option B: Phase 0b - Goal-Aware Placeholders (16-20h, Strategic)
|
||||
|
||||
📚 Details: `docs/NEXT_STEPS_2026-03-26.md`
|
||||
|
||||
## Feature-Roadmap
|
||||
|
||||
> 📋 **Detaillierte Roadmap:** `.claude/docs/ROADMAP.md` (Phasen 0-3, Timeline, Abhängigkeiten)
|
||||
|
|
|
|||
181
backend/check_migration_024.py
Normal file
181
backend/check_migration_024.py
Normal 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()
|
||||
215
backend/fix_seed_goal_types.py
Normal file
215
backend/fix_seed_goal_types.py
Normal 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
504
backend/goal_utils.py
Normal 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
|
||||
}
|
||||
"""
|
||||
|
|
@ -23,6 +23,7 @@ from routers import user_restrictions, access_grants, training_types, admin_trai
|
|||
from routers import admin_activity_mappings, sleep, rest_days
|
||||
from routers import vitals_baseline, blood_pressure # v9d Phase 2d Refactored
|
||||
from routers import evaluation # v9d/v9e Training Type Profiles (#15)
|
||||
from routers import goals # v9e Goal System (Strategic + Tactical)
|
||||
|
||||
# ── App Configuration ─────────────────────────────────────────────────────────
|
||||
DATA_DIR = Path(os.getenv("DATA_DIR", "./data"))
|
||||
|
|
@ -97,6 +98,7 @@ app.include_router(rest_days.router) # /api/rest-days/* (v9d Phase 2a
|
|||
app.include_router(vitals_baseline.router) # /api/vitals/baseline/* (v9d Phase 2d Refactored)
|
||||
app.include_router(blood_pressure.router) # /api/blood-pressure/* (v9d Phase 2d Refactored)
|
||||
app.include_router(evaluation.router) # /api/evaluation/* (v9d/v9e Training Profiles #15)
|
||||
app.include_router(goals.router) # /api/goals/* (v9e Goal System Strategic + Tactical)
|
||||
|
||||
# ── Health Check ──────────────────────────────────────────────────────────────
|
||||
@app.get("/")
|
||||
|
|
|
|||
135
backend/migrations/022_goal_system.sql
Normal file
135
backend/migrations/022_goal_system.sql
Normal 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';
|
||||
185
backend/migrations/024_goal_type_registry.sql
Normal file
185
backend/migrations/024_goal_type_registry.sql
Normal 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
|
||||
);
|
||||
*/
|
||||
103
backend/migrations/025_cleanup_goal_type_definitions.sql
Normal file
103
backend/migrations/025_cleanup_goal_type_definitions.sql
Normal 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;
|
||||
40
backend/migrations/026_goal_type_filters.sql
Normal file
40
backend/migrations/026_goal_type_filters.sql
Normal 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;
|
||||
*/
|
||||
125
backend/migrations/027_focus_areas_system.sql
Normal file
125
backend/migrations/027_focus_areas_system.sql
Normal 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;
|
||||
57
backend/migrations/028_goal_categories_priorities.sql
Normal file
57
backend/migrations/028_goal_categories_priorities.sql
Normal 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';
|
||||
74
backend/migrations/029_fix_missing_goal_types.sql
Normal file
74
backend/migrations/029_fix_missing_goal_types.sql
Normal 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)';
|
||||
64
backend/migrations/030_goal_progress_log.sql
Normal file
64
backend/migrations/030_goal_progress_log.sql
Normal 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
1252
backend/routers/goals.py
Normal file
File diff suppressed because it is too large
Load Diff
116
backend/run_migration_024.py
Normal file
116
backend/run_migration_024.py
Normal 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()
|
||||
595
docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md
Normal file
595
docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md
Normal 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
|
||||
538
docs/GOAL_SYSTEM_PRIORITY_ANALYSIS.md
Normal file
538
docs/GOAL_SYSTEM_PRIORITY_ANALYSIS.md
Normal 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?**
|
||||
729
docs/GOAL_SYSTEM_REDESIGN_v2.md
Normal file
729
docs/GOAL_SYSTEM_REDESIGN_v2.md
Normal 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
|
||||
458
docs/KONZEPT_ANALYSE_2026-03-26.md
Normal file
458
docs/KONZEPT_ANALYSE_2026-03-26.md
Normal 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)
|
||||
460
docs/NEXT_STEPS_2026-03-26.md
Normal file
460
docs/NEXT_STEPS_2026-03-26.md
Normal 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
|
||||
194
docs/STATUS_REPORT_2026-03-26.md
Normal file
194
docs/STATUS_REPORT_2026-03-26.md
Normal 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
284
docs/TODO_GOAL_SYSTEM.md
Normal 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 🚀
|
||||
245
docs/issues/issue-50-phase-0a-goal-system.md
Normal file
245
docs/issues/issue-50-phase-0a-goal-system.md
Normal 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)
|
||||
425
docs/issues/issue-51-prompt-page-assignment.md
Normal file
425
docs/issues/issue-51-prompt-page-assignment.md
Normal 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)
|
||||
|
|
@ -31,10 +31,13 @@ import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage'
|
|||
import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage'
|
||||
import AdminTrainingProfiles from './pages/AdminTrainingProfiles'
|
||||
import AdminPromptsPage from './pages/AdminPromptsPage'
|
||||
import AdminGoalTypesPage from './pages/AdminGoalTypesPage'
|
||||
import SubscriptionPage from './pages/SubscriptionPage'
|
||||
import SleepPage from './pages/SleepPage'
|
||||
import RestDaysPage from './pages/RestDaysPage'
|
||||
import VitalsPage from './pages/VitalsPage'
|
||||
import GoalsPage from './pages/GoalsPage'
|
||||
import CustomGoalsPage from './pages/CustomGoalsPage'
|
||||
import './app.css'
|
||||
|
||||
function Nav() {
|
||||
|
|
@ -172,6 +175,8 @@ function AppShell() {
|
|||
<Route path="/sleep" element={<SleepPage/>}/>
|
||||
<Route path="/rest-days" element={<RestDaysPage/>}/>
|
||||
<Route path="/vitals" element={<VitalsPage/>}/>
|
||||
<Route path="/goals" element={<GoalsPage/>}/>
|
||||
<Route path="/custom-goals" element={<CustomGoalsPage/>}/>
|
||||
<Route path="/nutrition" element={<NutritionPage/>}/>
|
||||
<Route path="/activity" element={<ActivityPage/>}/>
|
||||
<Route path="/analysis" element={<Analysis/>}/>
|
||||
|
|
@ -186,6 +191,7 @@ function AppShell() {
|
|||
<Route path="/admin/activity-mappings" element={<AdminActivityMappingsPage/>}/>
|
||||
<Route path="/admin/training-profiles" element={<AdminTrainingProfiles/>}/>
|
||||
<Route path="/admin/prompts" element={<AdminPromptsPage/>}/>
|
||||
<Route path="/admin/goal-types" element={<AdminGoalTypesPage/>}/>
|
||||
<Route path="/subscription" element={<SubscriptionPage/>}/>
|
||||
</Routes>
|
||||
</main>
|
||||
|
|
|
|||
520
frontend/src/pages/AdminGoalTypesPage.jsx
Normal file
520
frontend/src/pages/AdminGoalTypesPage.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -453,7 +453,7 @@ export default function AdminPanel() {
|
|||
</div>
|
||||
|
||||
{/* 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}}>
|
||||
<Settings size={16} color="var(--accent)"/> KI-Prompts (v9f)
|
||||
</div>
|
||||
|
|
@ -468,6 +468,23 @@ export default function AdminPanel() {
|
|||
</Link>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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 { useAuth } from '../context/AuthContext'
|
||||
import Markdown from '../utils/Markdown'
|
||||
|
|
@ -277,6 +278,7 @@ function InsightCard({ ins, onDelete, defaultOpen=false, prompts=[] }) {
|
|||
|
||||
export default function Analysis() {
|
||||
const { canUseAI } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const [prompts, setPrompts] = useState([])
|
||||
const [allInsights, setAllInsights] = useState([])
|
||||
const [loading, setLoading] = useState(null)
|
||||
|
|
@ -386,7 +388,16 @@ export default function Analysis() {
|
|||
|
||||
return (
|
||||
<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">
|
||||
<button className={'tab'+(tab==='run'?' active':'')} onClick={()=>setTab('run')}>Analysen starten</button>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,13 @@ const ENTRIES = [
|
|||
to: '/vitals',
|
||||
color: '#E74C3C',
|
||||
},
|
||||
{
|
||||
icon: '🎯',
|
||||
label: 'Eigene Ziele',
|
||||
sub: 'Fortschritte für individuelle Ziele erfassen',
|
||||
to: '/custom-goals',
|
||||
color: '#1D9E75',
|
||||
},
|
||||
{
|
||||
icon: '📖',
|
||||
label: 'Messanleitung',
|
||||
|
|
|
|||
370
frontend/src/pages/CustomGoalsPage.jsx
Normal file
370
frontend/src/pages/CustomGoalsPage.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -497,6 +497,21 @@ export default function Dashboard() {
|
|||
</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 */}
|
||||
<div className="card section-gap">
|
||||
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}>
|
||||
|
|
|
|||
1230
frontend/src/pages/GoalsPage.jsx
Normal file
1230
frontend/src/pages/GoalsPage.jsx
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -330,4 +330,39 @@ export const api = {
|
|||
|
||||
// Placeholder Export
|
||||
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)),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user