Compare commits

...

52 Commits

Author SHA1 Message Date
9d22e7e8af Merge pull request 'Goalsystem V1' (#50) from develop into main
All checks were successful
Deploy Production / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Reviewed-on: #50
2026-03-27 17:40:50 +01:00
0a1da37197 fix: Remove g.direction from SELECT - column does not exist
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
2026-03-27 17:08:30 +01:00
495f218f9a feat: Add Goal Types admin link to Settings/AdminPanel
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
2026-03-27 17:05:42 +01:00
fac8820208 fix: SQL error - direction is in goals table, not goal_type_definitions
Some checks failed
Build Test / lint-backend (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Deploy Development / deploy (push) Has been cancelled
2026-03-27 17:05:14 +01:00
217990d417 fix: Prevent manual progress entries for automatic goals
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
**Backend Safeguards:**
- get_goals_grouped: Added source_table, source_column, direction to SELECT
- create_goal_progress: Check source_table before allowing manual entry
- Returns HTTP 400 if user tries to log progress for automatic goals (weight, activity, etc.)

**Prevents:**
- Data confusion: Manual entries in goal_progress_log for weight/activity/etc.
- Dual tracking: Same data in multiple tables
- User error: Wrong data entry location

**Result:**
- Frontend filter (!goal.source_table) now works correctly
- CustomGoalsPage shows ONLY custom goals (flexibility, strength, etc.)
- Clear error message if manual entry attempted via API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 17:00:53 +01:00
1960ae4924 docs: Update CLAUDE.md - Custom Goals Page
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
2026-03-27 15:32:44 +01:00
bcb867da69 refactor: Separate goal tracking - strategic vs tactical
Some checks failed
Build Test / lint-backend (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Deploy Development / deploy (push) Has been cancelled
**UX Improvements:**
- Progress modal: full-width inputs, label-as-heading, left-aligned text
- Progress button only visible for custom goals (no source_table)
- Prevents confusion with automatic tracking (Weight, Activity, etc.)

**New Page: Custom Goals (Capture/Eigene Ziele):**
- Dedicated page for daily custom goal value entry
- Clean goal selection with progress bars
- Quick entry form (date, value, note)
- Recent progress history (last 5 entries)
- Mobile-optimized for daily use

**Architecture:**
- Analysis/Goals → Strategic (define goals, set priorities)
- Capture/Custom Goals → Tactical (daily value entry)
- History → Evaluation (goal achievement analysis)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 15:32:15 +01:00
398c645a98 feat: Goal Progress Log UI - complete frontend
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
- Added Progress button (TrendingUp icon) to each goal card
- Created Progress Modal with:
  • Form to add new progress entry (date, value, note)
  • Historical entries list with delete option
  • Category-colored goal info header
  • Auto-disables manual delete for non-manual entries
- Integration complete: handlers → API → backend

Completes Phase 0a Progress Tracking (Migration 030 + Backend + Frontend)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 14:02:24 +01:00
7db98a4fa6 feat: Goal Progress Log - backend + API (v2.1)
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Implemented progress tracking system for all goals.

**Backend:**
- Migration 030: goal_progress_log table with unique constraint per day
- Trigger: Auto-update goal.current_value from latest progress
- Endpoints: GET/POST/DELETE /api/goals/{id}/progress
- Pydantic Models: GoalProgressCreate, GoalProgressUpdate

**Features:**
- Manual progress tracking for custom goals (flexibility, strength, etc.)
- Full history with date, value, note
- current_value always reflects latest progress entry
- One entry per day per goal (unique constraint)
- Cascade delete when goal is deleted

**API:**
- GET /api/goals/{goal_id}/progress - List all entries
- POST /api/goals/{goal_id}/progress - Log new progress
- DELETE /api/goals/{goal_id}/progress/{progress_id} - Delete entry

**Next:** Frontend UI (progress button, modal, history list)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 13:58:14 +01:00
ce37afb2bb fix: Migration 029 - activate missing goal types (flexibility, strength)
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
These goal types existed but were inactive or misconfigured.

Uses UPSERT (INSERT ... ON CONFLICT DO UPDATE):
- If exists → activate + fix labels/icons/category
- If not exists → create properly

Idempotent: Safe to run multiple times, works on dev + prod.

Both types have no automatic data source (source_table = NULL),
so current_value must be updated manually.

Fixes: flexibility and strength goals not visible in admin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 13:53:47 +01:00
db90f397e8 feat: auto-assign goal category based on goal type
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Added intelligent category assignment:
- Weight, body_fat, lean_mass → Körper
- Strength, flexibility → Training
- BP, VO2Max, RHR, HRV → Gesundheit
- Sleep goals → Erholung
- Nutrition goals → Ernährung
- Unknown types → Sonstiges

Changes:
1. getCategoryForGoalType() mapping function
2. Auto-set category in handleGoalTypeChange()
3. Auto-set category in handleCreateGoal()

User can still manually change category if needed.

Fixes: Blood pressure goals wrongly categorized as 'Training'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 13:09:34 +01:00
498ad7a47f fix: show goal type label when name is empty
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 13s
Enhanced fallback chain for goal display:
1. goal.name (custom name if set)
2. goal.label_de (from backend JOIN)
3. typeInfo.label_de (from goalTypesMap)
4. goal.goal_type (raw key as last resort)

Also use goal.icon from backend if available.

Fixes: Empty goal names showing blank in list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 12:51:24 +01:00
9e95fd8416 fix: get_goals_grouped - remove is_active check (column doesn't exist)
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
goals table doesn't have is_active column.
Removed AND g.is_active = true from WHERE clause.

Fixes: psycopg2.errors.UndefinedColumn: column g.is_active does not exist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 12:45:03 +01:00
ca4f722b47 fix: goal_utils - support different date column names
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Fixed: column 'date' does not exist in blood_pressure_log

blood_pressure_log uses 'measured_at' instead of 'date'.
Added DATE_COLUMN_MAP for table-specific date columns:
- blood_pressure_log → measured_at
- fitness_tests → test_date
- all others → date

Replaced all hardcoded 'date' with dynamic date_col variable.

Fixes error: [ERROR] Failed to fetch value from blood_pressure_log.systolic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 12:42:56 +01:00
1c00238414 fix: get_goals_grouped - remove non-existent linear_projection column
All checks were successful
Deploy Development / deploy (push) Successful in 52s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 15s
Fixed SQL error: column g.linear_projection does not exist
Replaced with: g.on_track, g.projection_date (actual columns)

This was causing Internal Server Error on /api/goals/grouped

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 12:41:06 +01:00
448d19b840 fix: Migration 028 - remove is_active from index (column doesn't exist yet)
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Migration 028 failed because goals table doesn't have is_active column yet.
Removed WHERE clause from index definition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 12:36:58 +01:00
caebc37da0 feat: goal categories UI - complete rebuild
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Completed frontend for multi-dimensional goal priorities.

**UI Changes:**
- Category-grouped goal display with color-coded headers
- Each category shows: icon, name, description, goal count
- Priority stars (//) replace "PRIMÄR" badge
- Goals have category-colored left border
- Form fields: Category dropdown + Priority selector
- Removed "Gewichtung gesamt" display (useless UX)

**Categories:**
- 📉 Körper (body): Gewicht, Körperfett, Muskelmasse
- 🏋️ Training: Kraft, Frequenz, Performance
- 🍎 Ernährung: Kalorien, Makros, Essgewohnheiten
- 😴 Erholung: Schlaf, Regeneration, Ruhetage
- ❤️ Gesundheit: Vitalwerte, Blutdruck, HRV
- 📌 Sonstiges: Weitere Ziele

**Priority Levels:**
-  Hoch (1)
-  Mittel (2)
-  Niedrig (3)

**Implementation:**
- Load groupedGoals via api.listGoalsGrouped()
- GOAL_CATEGORIES + PRIORITY_LEVELS constants
- handleEditGoal/handleSaveGoal/handleCreateGoal extended
- Backward compatible (is_primary still exists)

Next: Test migration + UI, then update Dashboard to show top-1 per category

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 12:33:17 +01:00
6a3a782bff feat: goal categories and priorities - backend + API
Implemented multi-dimensional goal priorities (Option B).

**Backend Changes:**
- Migration 028: Added `category` + `priority` columns to goals table
- Auto-migration of existing goals to categories based on goal_type
- GoalCreate/GoalUpdate models extended with category + priority
- New endpoint: GET /api/goals/grouped (returns goals by category)
- Categories: body, training, nutrition, recovery, health, other
- Priorities: 1=high (), 2=medium (), 3=low ()

**API Changes:**
- Added api.listGoalsGrouped() binding

**Frontend (partial):**
- Added GOAL_CATEGORIES + PRIORITY_LEVELS constants
- Extended formData with category + priority fields
- Removed "Gewichtung gesamt" display (useless)
- Load groupedGoals in addition to flat goals list

Next: Complete frontend UI rebuild for category grouping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 12:30:59 +01:00
2f51b26418 fix: focus areas slider NaN values and validation
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Fixed multiple issues with relative weight sliders:
1. Sanitize focusData on load (ensure all 6 fields are numeric)
2. Sync focusTemp when clicking "Anpassen" button
3. Robust sum calculation filtering only *_pct fields
4. Convert NaN/undefined to 0 in all calculations
5. Safe Number() coercion before normalization

Fixes errors:
- "Gewichtung gesamt: NaN"
- "Input should be a valid integer, input: null"
- Prozent always showing 0%

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 12:20:01 +01:00
92cc309489 feat: relative weight sliders for focus areas
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Improved UX for focus area configuration:
- Sliders now use relative weights (0-10) instead of percentages
- System automatically normalizes to percentages (sum=100%)
- Live preview shows "weight → percent%" (e.g., "5 → 50%")
- No more manual balancing required from user

User sets: Kraft=5, Ausdauer=3, Flexibilität=2
System calculates: 50%, 30%, 20%

Addresses user feedback: "Summe muss 100% sein" not user-friendly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 12:10:56 +01:00
1fdf91cb50 fix: Migration 027 - health mode missing dimensions
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Fixed health mode calculation to include all 6 dimensions.
Simplified CASE statements (single CASE instead of multiple additions).

Before: health mode only set flexibility (15%) + health (55%) = 70% 
After:  health mode sets all dimensions = 100% 
  - weight_loss: 5%
  - muscle_gain: 0%
  - strength: 10%
  - endurance: 20%
  - flexibility: 15%
  - health: 50%

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 10:56:53 +01:00
80d57918ae fix: Migration 027 constraint violation - health mode sum
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Fixed health mode calculation in focus_areas migration.
Changed health_pct from 50 to 55 to ensure sum equals 100%.

Before: 0+0+10+20+15+50 = 95% (constraint violation)
After:  0+0+10+20+15+55 = 100% (valid)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 10:53:39 +01:00
d97925d5a1 feat: Focus Areas Slider UI (Goal System v2.0 complete)
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Replaces single goal mode cards with weighted multi-focus system

UI Features:
- 6 sliders for focus dimensions (5% increments)
- Live sum calculation with visual feedback
- Validation: Sum must equal 100%
- Color-coded sliders per dimension
- Edit/Display mode toggle
- Shows derived values if not customized

UX Flow:
1. Default: Shows focus distribution (bars)
2. Click 'Anpassen': Shows sliders
3. Adjust percentages (sum = 100%)
4. Save → Updates backend + reloads

Visual:
- Active dimensions shown as colored cards (display mode)
- Gradient sliders with percentage labels (edit mode)
- Green box when sum = 100%, red when != 100%
- Info message if derived from old goal_mode

Complete v2.0:
 Backend (Migration 027, API, get_focus_weights V2)
 Frontend (Slider UI, state management, validation)
 Auto-migration (goal_mode → focus_areas)

Ready for: KI-Integration with weighted scoring
2026-03-27 10:36:42 +01:00
4a11d20c4d feat: Goal System v2.0 - Focus Areas with weighted priorities
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
BREAKING: Replaces single 'primary goal' with weighted multi-goal system

Migration 027:
- New table: focus_areas (6 dimensions with percentages)
- Constraint: Sum must equal 100%
- Auto-migration: goal_mode → focus_areas for existing users
- Unique constraint: One active focus_areas per profile

Backend:
- get_focus_weights() V2: Reads from focus_areas table
- Fallback: Uses goal_mode if focus_areas not set
- New endpoints: GET/PUT /api/goals/focus-areas
- Validation: Sum=100, range 0-100

API:
- getFocusAreas() - Get current weights
- updateFocusAreas(data) - Update weights (upsert)

Focus dimensions:
1. weight_loss_pct   (Fettabbau)
2. muscle_gain_pct   (Muskelaufbau)
3. strength_pct      (Kraftsteigerung)
4. endurance_pct     (Ausdauer)
5. flexibility_pct   (Beweglichkeit)
6. health_pct        (Allgemeine Gesundheit)

Benefits:
- Multiple goals with custom priorities
- More flexible than single primary goal
- KI can use weighted scores
- Ready for Phase 0b placeholder integration

UI: Coming in next commit (slider interface)
2026-03-27 08:38:03 +01:00
2303c04123 feat: filtered goal types - count specific training types
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
NEW FEATURE: Filter conditions for goal types
Enables counting/aggregating specific subsets of data.

Example use case: Count only strength training sessions per week
- Create goal type with filter: {"training_type": "strength"}
- count_7d now counts only strength training, not all activities

Implementation:
- Migration 026: filter_conditions JSONB column
- Backend: Dynamic WHERE clause building from JSON filters
- Supports single value: {"training_type": "strength"}
- Supports multiple values: {"training_type": ["strength", "hiit"]}
- Works with all 8 aggregation methods (count, avg, sum, min, max)
- Frontend: JSON textarea with example + validation
- Pydantic models: filter_conditions field added

Technical details:
- SQL injection safe (parameterized queries)
- Graceful degradation (invalid JSON ignored with warning)
- Backward compatible (NULL filters = no filtering)

Answers user question: 'Kann ich Trainingstypen wie Krafttraining separat zählen?'
Answer: YES! 🎯
2026-03-27 08:14:22 +01:00
2c978bf948 feat: dynamic schema dropdowns for goal type admin UI
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Admin can now easily create custom goal types:
- New endpoint /api/goals/schema-info with table/column metadata
- 9 tables documented (weight, caliper, activity, nutrition, sleep, vitals, BP, rest_days, circumference)
- Table dropdown with descriptions (e.g., 'activity_log - Trainingseinheiten')
- Column dropdown dependent on selected table
- All columns documented in German with data types
- Fields optional (for complex calculation formulas)

UX improvements:
- No need to guess table/column names
- Clear descriptions for each field
- Type-safe selection (no typos)
- Cascading dropdowns (column depends on table)

Closes user feedback: 'Admin weiß nicht welche Tabellen/Spalten verfügbar sind'
2026-03-27 08:05:45 +01:00
210671059a debug: comprehensive error handling and logging for list_goals
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
- try-catch around entire endpoint
- try-catch for each goal progress update
- Detailed error logging with traceback
- Continue processing other goals if one fails
- Clear error message to frontend

This will show exact error location in logs.
2026-03-27 07:58:56 +01:00
1f4ee5021e fix: robust error handling in goal value fetcher
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 13s
Prevents crashes when:
- Goal types have NULL source_table/column (lean_mass, inactive placeholders)
- Old goals reference inactive goal types
- SQL queries fail for any reason

Changes:
- Guard clause checks table/column before SQL
- try-catch wraps all aggregation queries
- Returns None gracefully instead of crashing endpoint
- Logs warnings for debugging

Fixes: Goals page not loading due to /api/goals/list crash
2026-03-27 07:55:19 +01:00
1e758696fd feat: Migration 025 - automatic cleanup and seed for goal_type_definitions
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Fixes cases where Migration 024 partially ran:
- Removes created_by/updated_by columns if they exist
- Re-inserts seed data with ON CONFLICT DO NOTHING
- Fully automated, no manual intervention needed
- Production-safe (idempotent)

This ensures clean deployment to production without manual DB changes.
2026-03-27 07:49:09 +01:00
a039a0fad3 fix: Migration 024 - remove problematic FK constraints created_by/updated_by
Some checks failed
Build Test / lint-backend (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Deploy Development / deploy (push) Has been cancelled
Goal type definitions are global system entities, not user-specific.
System types seeded in migration cannot have created_by FK.

Changes:
- Remove created_by/updated_by columns from goal_type_definitions
- Update CREATE/UPDATE endpoints to not use these fields
- Migration now runs cleanly on container start
- No manual intervention needed for production deployment
2026-03-27 07:48:23 +01:00
b3cc588293 fix: make Migration 024 idempotent + add seed data fix script
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 14s
2026-03-27 07:40:42 +01:00
c9e4b6aa02 debug: diagnostic script for Migration 024 state 2026-03-27 07:39:18 +01:00
8be87bfdfb fix: Remove broken table_exists check
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Removed faulty EXISTS check that was causing "0" error.
Added debug logging and better error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 07:34:29 +01:00
484c25575d feat: manual migration 024 runner script
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Allows running Migration 024 manually if auto-migration failed.

Usage: python backend/run_migration_024.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 07:28:43 +01:00
bbee44ecdc fix: Better error handling for goal types loading
Some checks failed
Build Test / lint-backend (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Deploy Development / deploy (push) Has been cancelled
- Check if goal_type_definitions table exists
- Detailed error messages
- Fallback if goalTypes is empty
- Prevent form opening without types

Helps debugging Migration 024 issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 07:28:14 +01:00
043bed4323 docs: Phase 1.5 complete - update roadmap
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Phase 1.5 (Flexible Goal System) erfolgreich abgeschlossen:
- 8h Aufwand (geplant 8-12h)
- Alle Tasks 
- System vollständig flexibel
- Phase 0b ready to start

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 06:52:18 +01:00
640ef81257 feat: Phase 1.5 - Flexible Goal System (DB-Registry) Part 2/2 - COMPLETE
Some checks failed
Build Test / lint-backend (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Deploy Development / deploy (push) Has been cancelled
Frontend dynamic goal types + Admin UI komplett implementiert.

Frontend GoalsPage:
- HARDCODED GOAL_TYPES entfernt
- Dynamic loading von goal_type_definitions via API
- goalTypes state + goalTypesMap für quick lookup
- Dropdown zeigt alle aktiven Types aus DB
- Vollständig flexibel - neue Types sofort verfügbar

Admin UI:
- AdminGoalTypesPage.jsx (400+ Zeilen)
  → Übersicht aller Goal Types (System + Custom)
  → Create/Edit/Delete Forms
  → CRUD via api.js (admin-only)
  → Validierung: System Types nur deaktivierbar, nicht löschbar
  → 8 Aggregationsmethoden im Dropdown
  → Category-Auswahl (body, mind, activity, nutrition, recovery, custom)
- Route registriert: /admin/goal-types
- Import in App.jsx

Phase 1.5 KOMPLETT:
 Migration 024 (goal_type_definitions)
 Universal Value Fetcher (goal_utils.py)
 CRUD API (goals.py)
 Frontend Dynamic Dropdown (GoalsPage.jsx)
 Admin UI (AdminGoalTypesPage.jsx)

System ist jetzt VOLLSTÄNDIG FLEXIBEL:
- Neue Goal Types via Admin UI ohne Code-Deploy
- Beispiele: Meditation, Trainingshäufigkeit, Planabweichung
- Phase 0b Platzhalter können alle Types nutzen
- Keine Doppelarbeit bei v2.0 Redesign

Nächster Schritt: Testing + Phase 0b (120+ Platzhalter)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 06:51:46 +01:00
65ee5f898f feat: Phase 1.5 - Flexible Goal System (DB-Registry) Part 1/2
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 13s
KRITISCHE ARCHITEKTUR-ÄNDERUNG vor Phase 0b:
Ermöglicht dynamische Goal Types ohne Code-Änderungen.

Backend:
- Migration 024: goal_type_definitions Tabelle
  → 8 existierende Typen als Seed-Data migriert
  → Flexible Schema: source_table, aggregation_method, calculation_formula
  → System vs. Custom Types (is_system flag)
- goal_utils.py: Universal Value Fetcher
  → get_current_value_for_goal() ersetzt hardcoded if/elif chain
  → Unterstützt: latest, avg_7d, avg_30d, sum_30d, count_7d, etc.
  → Komplexe Formeln (lean_mass) via calculation_formula JSON
- goals.py: CRUD API für Goal Type Definitions
  → GET /goals/goal-types (public)
  → POST/PUT/DELETE /goals/goal-types (admin-only)
  → Schutz für System-Types (nicht löschbar)
- goals.py: _get_current_value_for_goal_type() delegiert zu Universal Fetcher

Frontend:
- api.js: 4 neue Funktionen (listGoalTypeDefinitions, create, update, delete)

Dokumentation:
- TODO_GOAL_SYSTEM.md: Phase 1.5 hinzugefügt, Roadmap aktualisiert

Part 2/2 (nächster Commit):
- Frontend: Dynamic Goal Types Dropdown
- Admin UI: Goal Type Management Page
- Testing

Warum JETZT (vor Phase 0b)?
- Phase 0b Platzhalter (120+) nutzen Goals für Score-Berechnungen
- Flexible Goals → automatisch in Platzhaltern verfügbar
- Später umbauen = Doppelarbeit (alle Platzhalter anpassen)

Zukünftige Custom Goals möglich:
- 🧘 Meditation (min/Tag)
- 📅 Trainingshäufigkeit (x/Woche)
- 📊 Planabweichung (%)
- 🎯 Ritual-Adherence (%)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 06:45:05 +01:00
27a8af7008 debug: Add logging and warnings for Goal System issues
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Based on test feedback - 3 issues addressed:

1. Primary Toggle (Frontend Debug):
   - Add console.log in handleSaveGoal
   - Shows what data is sent to backend
   - Helps debug if checkbox state is correct

2. Lean Mass Display (Backend Debug):
   - Add error handling in lean_mass calculation
   - Log why calculation fails (missing weight/bf data)
   - Try-catch for value conversion errors

3. BP/Strength/Flexibility Warning (UI):
   - Yellow warning box for incomplete goal types
   - BP: "benötigt 2 Werte (geplant für v2.0)"
   - Strength/Flexibility: "Keine Datenquelle"
   - Transparent about limitations

Next: User re-tests with debug output to identify root cause.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 06:24:40 +01:00
14d80fc903 docs: zentrale TODO-Liste für Goal System erstellt
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Tracking-Dokument für alle offenen Punkte:
- Phase 0b Tasks (120+ Platzhalter)
- v2.0 Redesign Probleme
- Gitea Issues Referenzen
- Timeline & Roadmap

Verhindert dass wichtige Punkte vergessen werden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 06:17:57 +01:00
87464ff138 fix: Phase 1 - Goal System Quick Fixes + Abstraction Layer
All checks were successful
Deploy Development / deploy (push) Successful in 53s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Behebt 4 kritische Bugs in Phase 0a und schafft Basis für Phase 0b
ohne spätere Doppelarbeit.

Backend:
- NEW: goal_utils.py mit get_focus_weights() Abstraction Layer
  → V1: Mappt goal_mode zu Gewichten
  → V2 (später): Liest aus focus_areas Tabelle
  → Phase 0b Platzhalter (120+) müssen NICHT umgeschrieben werden
- FIX: Primary goal toggle in goals.py (is_primary im GoalUpdate Model)
  → Beim Update auf primary werden andere Goals korrekt auf false gesetzt
- FIX: lean_mass current_value Berechnung implementiert
  → weight - (weight * body_fat_pct / 100)
- FIX: VO2Max Spaltenname vo2_max (statt vo2max)
  → Internal Server Error behoben

CLAUDE.md:
- Version Update: Phase 1 Fixes (27.03.2026)

Keine Doppelarbeit:
- Alle zukünftigen Phase 0b Platzhalter nutzen get_focus_weights()
- v2.0 Redesign = nur eine Funktion ändern, nicht 120+ Platzhalter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 06:13:47 +01:00
e3f1e399c2 docs: Goal System Redesign v2.0 - comprehensive concept
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Created comprehensive redesign document addressing all identified issues:

Problems addressed:
1. Primary goal too simplistic → Weight system (0-100%)
2. Single goal mode too simple → Multi-mode with weights
3. Missing current values → All goal types with data sources
4. Abstract goal types → Concrete, measurable goals
5. Blood pressure single value → Compound goals (systolic/diastolic)
6. No user guidance → Norms, examples, age-specific values

New Concept:
- Focus Areas: Weighted distribution (30% weight loss + 25% endurance + ...)
- Goal Weights: Each goal has individual weight (not binary primary/not)
- Concrete Goal Types: cooper_test, pushups_max, squat_1rm, etc.
- Compound Goals: Support for multi-value targets (BP: 120/80)
- Guidance System: Age/gender-specific norms and examples

Schema Changes:
- New table: focus_areas (replaces single goal_mode)
- goals: Add goal_weight, target_value_secondary, current_value_secondary
- goals: Remove is_primary (replaced by weight)

UI/UX Redesign:
- Slider interface for focus areas (must sum to 100%)
- Goal editor with guidance and norms
- Weight indicators on all goals
- Special UI for compound goals

Implementation Phases: 16-21h total
- Phase 2: Backend Redesign (6-8h)
- Phase 3: Frontend Redesign (8-10h)
- Phase 4: Testing & Refinement (2-3h)

Status: WAITING FOR USER FEEDBACK & APPROVAL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 22:05:05 +01:00
3dd10d3dc7 docs: Phase 0a completion - comprehensive documentation
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
CLAUDE.md:
- Version updated to v9e+ (Phase 0a Goal System Complete)
- Added Phase 0a feature section with full details
- Updated 'Letzte Updates' with Phase 0a completion
- Links to new documentation files

docs/issues/issue-50-phase-0a-goal-system.md (NEW):
- Complete Phase 0a implementation documentation
- Technical details: Migration 022, goals.py, GoalsPage
- 4 commits documented (337667f to 5be52bc)
- Lessons learned section
- Basis for Phase 0b documented
- Testing checklist + acceptance criteria

docs/NEXT_STEPS_2026-03-26.md (NEW):
- Comprehensive planning document
- Option A: Issue #49 - Prompt Page Assignment (6-8h)
- Option B: Phase 0b - Goal-Aware Placeholders (16-20h)
- Option C: Issue #47 - Value Table Refinement (4-6h)
- Recommendation: Szenario 1 (Quick Wins first)
- Detailed technical breakdown for both options
- Timeline estimates (4h/day vs 8h/day)
- 120+ placeholder categorization for Phase 0b

All documentation reflects current state post Phase 0a.
Next decision: Choose between Issue #49 or Phase 0b.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 21:49:07 +01:00
5be52bcfeb feat: goals navigation + UX improvements
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Analysis Page:
- Add 'Ziele' button next to page title
- Direct navigation to /goals from analysis page
- Thematic link: goals influence AI analysis weighting

Goals Page:
- Fix text-align for text inputs (name, date, description)
- Text fields now left-aligned (numbers remain right-aligned)
- Better UX for non-numeric inputs

Navigation strategy: Goals accessible from Analysis page where
goal_mode directly impacts score calculation and interpretation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 16:50:22 +01:00
75f0a5dd6e refactor: mobile-friendly goal form design
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
- Full-width inputs throughout the form
- Labels above inputs (mobile best practice)
- Section headers with emoji (🎯 Zielwert)
- Consistent spacing (marginBottom: 16)
- Read-only unit display as styled badge
- Primary goal checkbox in highlighted section
- Full-width buttons (btn-full class)
- Scrollable modal with top padding
- Error display above form

Matches VitalsPage design pattern for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 16:32:37 +01:00
906a3b7cdd fix: Migration 022 - remove invalid schema_migrations tracking
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
The migration system tracks migrations via filename automatically.
Removed manual DO block that used wrong column name (version vs filename).

Also removed unused json import from goals.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 16:26:48 +01:00
337667fc07 feat: Phase 0a - Minimal Goal System (Strategic + Tactical)
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
- Strategic Layer: Goal modes (weight_loss, strength, endurance, recomposition, health)
- Tactical Layer: Concrete goal targets with progress tracking
- Training phases (manual + auto-detection framework)
- Fitness tests (standardized performance tracking)

Backend:
- Migration 022: goal_mode in profiles, goals, training_phases, fitness_tests tables
- New router: routers/goals.py with full CRUD for goals, phases, tests
- API endpoints: /api/goals/* (mode, list, create, update, delete)

Frontend:
- GoalsPage: Goal mode selector + goal management UI
- Dashboard: Goals preview card with link
- API integration: goal mode, CRUD operations, progress calculation

Basis for 120+ placeholders and goal-aware analyses (Phase 0b)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 16:20:35 +01:00
ae93b9d428 docs: goal system priority analysis - hybrid approach
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Key Decision: Minimal Goal System BEFORE Placeholders

Critical Finding:
- Same data = different interpretation per goal
- Example: -5kg FM, -2kg LBM
  - weight_loss: 78/100 (good!)
  - strength: 32/100 (LBM loss critical!)
  - Without goal: 50/100 (generic, wrong for both)

Recommended Approach (Hybrid):
1. Phase 0a (2-3h): Minimal Goal System
   - DB: goal_mode field
   - API: Get/Set Goal
   - UI: Goal Selector
   - Default: health

2. Phase 0b (16-20h): Goal-Aware Placeholders
   - 84 placeholders with goal-dependent calculations
   - Scores use goal_mode from day 1
   - No rework needed later

3. Phase 2+ (6-8h): Full Goal System
   - Goal recognition from patterns
   - Secondary goals
   - Goal progression tracking

Why Hybrid Works:
 Charts show correct interpretations immediately
 No rework of 84 placeholders later
 Goal recognition can come later (needs placeholders anyway)
 System is "smart coach" from day 1

File: docs/GOAL_SYSTEM_PRIORITY_ANALYSIS.md (650 lines)
2026-03-26 16:08:00 +01:00
8398368ed7 docs: comprehensive functional concept analysis
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
Analysis Results:
- 84 new placeholders needed from Fachkonzept
- Issues #26 & #27 too narrow (complementary, not conflicting)
- Recommend 4-phase approach: Placeholders → Charts → Rules → KI
- Transform: Data Collector → Active Coach

Key Findings:
- Fachkonzept defines 3 levels (Deskriptiv, Diagnostisch, Präskriptiv)
- 18 dedicated charts (K1-K5, E1-E5, A1-A8, C1-C6)
- Goal-mode dependent interpretation
- Lag-based correlations mandatory
- Confidence & data quality essential

Recommended Actions:
- Create Issues #52-55 (Baseline, Scores, Correlations, Metrics)
- Expand #26 & #27 based on Fachkonzept
- Start Phase 0: Implement 84 placeholders

File: docs/KONZEPT_ANALYSE_2026-03-26.md (385 lines)
2026-03-26 15:26:12 +01:00
cd2609da7c feat: Feature Request #49 - Prompt-Zuordnung zu Verlaufsseiten
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
UX Enhancement: Kontextbezogene KI-Analysen

Features:
- Prompts auf Verlaufsseiten verfügbar machen
- Mehrfachauswahl: Prompt auf mehreren Seiten
- Inline-Analyse via Modal
- Wiederverwendbare PagePrompts Komponente

Technisch:
- DB: available_on JSONB column
- API: GET /api/prompts/for-page/{page_slug}
- UI: Page-Auswahl im Prompt-Editor

Aufwand: 6-8h, Priority: Medium
Gitea: Issue #49
2026-03-26 15:12:39 +01:00
39db23d417 docs: comprehensive status report 26.03.2026
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
Audit Results:
- 2 Issues closed: #28 (AI-Prompts), #44 (Delete bug)
- 1 Issue created: #47 (Value Table Refinement)
- 12 commits today, 3 major features completed
- All documentation synchronized with Gitea

Next: Testing on dev, then Production deployment
2026-03-26 14:53:45 +01:00
582f125897 docs: comprehensive status update and Gitea sync
Some checks failed
Deploy Development / deploy (push) Successful in 57s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Has been cancelled
Updates:
- Clarify Gitea issue references (prefix with 'Gitea #')
- Mark #28 (AI-Prompts) as CLOSED
- Mark #44 (Delete insights) as CLOSED
- Update #47 reference (Wertetabelle Optimierung)
- Add 'Letzte Updates' section for 26.03.2026
- Document Issue-Management responsibility

Gitea Actions:
- Closed Issue #28 with completion comment
- Closed Issue #44 with fix details
- Created Issue #47 (Wertetabelle Optimierung)
2026-03-26 14:52:44 +01:00
33 changed files with 9304 additions and 12 deletions

118
CLAUDE.md
View File

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

View File

@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
Quick diagnostic: Check Migration 024 state
Run this inside the backend container:
docker exec bodytrack-dev-backend-1 python check_migration_024.py
"""
import psycopg2
import os
from psycopg2.extras import RealDictCursor
# Database connection
DB_HOST = os.getenv('DB_HOST', 'db')
DB_PORT = os.getenv('DB_PORT', '5432')
DB_NAME = os.getenv('DB_NAME', 'bodytrack')
DB_USER = os.getenv('DB_USER', 'bodytrack')
DB_PASS = os.getenv('DB_PASSWORD', '')
def main():
print("=" * 70)
print("Migration 024 Diagnostic")
print("=" * 70)
# Connect to database
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASS
)
cur = conn.cursor(cursor_factory=RealDictCursor)
# 1. Check if table exists
print("\n1. Checking if goal_type_definitions table exists...")
cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'goal_type_definitions'
)
""")
exists = cur.fetchone()['exists']
print(f" ✓ Table exists: {exists}")
if not exists:
print("\n❌ TABLE DOES NOT EXIST - Migration 024 did not run!")
print("\nRECOMMENDED ACTION:")
print(" 1. Restart backend container: docker restart bodytrack-dev-backend-1")
print(" 2. Check logs: docker logs bodytrack-dev-backend-1 | grep 'Migration'")
cur.close()
conn.close()
return
# 2. Check row count
print("\n2. Checking row count...")
cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions")
count = cur.fetchone()['count']
print(f" Row count: {count}")
if count == 0:
print("\n❌ TABLE IS EMPTY - Seed data was not inserted!")
print("\nPOSSIBLE CAUSES:")
print(" - INSERT statements failed (constraint violation?)")
print(" - Migration ran partially")
print("\nRECOMMENDED ACTION:")
print(" Run the seed statements manually (see below)")
else:
print(f" ✓ Table has {count} entries")
# 3. Show all entries
print("\n3. Current goal type definitions:")
cur.execute("""
SELECT type_key, label_de, unit, is_system, is_active, created_at
FROM goal_type_definitions
ORDER BY is_system DESC, type_key
""")
entries = cur.fetchall()
if entries:
print(f"\n {'Type Key':<20} {'Label':<20} {'Unit':<10} {'System':<8} {'Active':<8}")
print(" " + "-" * 70)
for row in entries:
status = "SYSTEM" if row['is_system'] else "CUSTOM"
active = "YES" if row['is_active'] else "NO"
print(f" {row['type_key']:<20} {row['label_de']:<20} {row['unit']:<10} {status:<8} {active:<8}")
else:
print(" (empty)")
# 4. Check schema_migrations
print("\n4. Checking schema_migrations tracking...")
cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'schema_migrations'
)
""")
sm_exists = cur.fetchone()['exists']
if sm_exists:
cur.execute("""
SELECT filename, executed_at
FROM schema_migrations
WHERE filename = '024_goal_type_registry.sql'
""")
tracked = cur.fetchone()
if tracked:
print(f" ✓ Migration 024 is tracked (executed: {tracked['executed_at']})")
else:
print(" ❌ Migration 024 is NOT tracked in schema_migrations")
else:
print(" ⚠️ schema_migrations table does not exist")
# 5. Check for errors
print("\n5. Potential issues:")
issues = []
if count == 0:
issues.append("No seed data - INSERTs failed")
if count > 0 and count < 6:
issues.append(f"Only {count} types (expected 8) - partial seed")
cur.execute("""
SELECT COUNT(*) as inactive_count
FROM goal_type_definitions
WHERE is_active = false
""")
inactive = cur.fetchone()['inactive_count']
if inactive > 2:
issues.append(f"{inactive} inactive types (expected 2)")
if not issues:
print(" ✓ No issues detected")
else:
for issue in issues:
print(f"{issue}")
# 6. Test query that frontend uses
print("\n6. Testing frontend query (WHERE is_active = true)...")
cur.execute("""
SELECT COUNT(*) as active_count
FROM goal_type_definitions
WHERE is_active = true
""")
active_count = cur.fetchone()['active_count']
print(f" Active types returned: {active_count}")
if active_count == 0:
print(" ❌ This is why frontend shows empty list!")
print("\n" + "=" * 70)
print("SUMMARY")
print("=" * 70)
if count == 0:
print("\n🔴 PROBLEM: Table exists but has no data")
print("\nQUICK FIX: Run these SQL commands manually:")
print("\n```sql")
print("-- Connect to database:")
print("docker exec -it bodytrack-dev-db-1 psql -U bodytrack -d bodytrack")
print("\n-- Then paste migration content:")
print("-- (copy from backend/migrations/024_goal_type_registry.sql)")
print("-- Skip CREATE TABLE (already exists), run INSERT statements only")
print("```")
elif active_count >= 6:
print("\n🟢 EVERYTHING LOOKS GOOD")
print(f" {active_count} active goal types available")
print("\nIf frontend still shows error, check:")
print(" 1. Backend logs: docker logs bodytrack-dev-backend-1 -f")
print(" 2. Network tab in browser DevTools")
print(" 3. API endpoint: curl -H 'X-Auth-Token: YOUR_TOKEN' http://localhost:8099/api/goals/goal-types")
else:
print(f"\n🟡 PARTIAL DATA: {active_count} active types (expected 6)")
print(" Some INSERTs might have failed")
cur.close()
conn.close()
if __name__ == '__main__':
main()

View File

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

504
backend/goal_utils.py Normal file
View File

@ -0,0 +1,504 @@
"""
Goal Utilities - Abstraction Layer for Focus Weights & Universal Value Fetcher
This module provides:
1. Abstraction layer between goal modes and focus weights (Phase 1)
2. Universal value fetcher for dynamic goal types (Phase 1.5)
Version History:
- V1 (Phase 1): Maps goal_mode to predefined weights
- V1.5 (Phase 1.5): Universal value fetcher for DB-registry goal types
- V2 (future): Reads from focus_areas table with custom user weights
Part of Phase 1 + Phase 1.5: Flexible Goal System
"""
from typing import Dict, Optional, Any
from datetime import date, timedelta
from decimal import Decimal
import json
from db import get_cursor
def get_focus_weights(conn, profile_id: str) -> Dict[str, float]:
"""
Get focus area weights for a profile.
V2 (Goal System v2.0): Reads from focus_areas table with custom user weights.
Falls back to goal_mode mapping if focus_areas not set.
Args:
conn: Database connection
profile_id: User's profile ID
Returns:
Dict with focus weights (sum = 1.0):
{
'weight_loss': 0.3, # Fat loss priority
'muscle_gain': 0.2, # Muscle gain priority
'strength': 0.25, # Strength training priority
'endurance': 0.25, # Cardio/endurance priority
'flexibility': 0.0, # Mobility priority
'health': 0.0 # General health maintenance
}
Example Usage in Phase 0b:
weights = get_focus_weights(conn, profile_id)
# Score calculation considers user's focus
overall_score = (
body_score * weights['weight_loss'] +
strength_score * weights['strength'] +
cardio_score * weights['endurance']
)
"""
cur = get_cursor(conn)
# V2: Try to fetch from focus_areas table
cur.execute("""
SELECT weight_loss_pct, muscle_gain_pct, strength_pct,
endurance_pct, flexibility_pct, health_pct
FROM focus_areas
WHERE profile_id = %s AND active = true
LIMIT 1
""", (profile_id,))
row = cur.fetchone()
if row:
# Convert percentages to weights (0-1 range)
return {
'weight_loss': row['weight_loss_pct'] / 100.0,
'muscle_gain': row['muscle_gain_pct'] / 100.0,
'strength': row['strength_pct'] / 100.0,
'endurance': row['endurance_pct'] / 100.0,
'flexibility': row['flexibility_pct'] / 100.0,
'health': row['health_pct'] / 100.0
}
# V1 Fallback: Use goal_mode if focus_areas not set
cur.execute(
"SELECT goal_mode FROM profiles WHERE id = %s",
(profile_id,)
)
row = cur.fetchone()
if not row:
# Ultimate fallback: balanced health focus
return {
'weight_loss': 0.0,
'muscle_gain': 0.0,
'strength': 0.10,
'endurance': 0.20,
'flexibility': 0.15,
'health': 0.55
}
goal_mode = row['goal_mode']
if not goal_mode:
return {
'weight_loss': 0.0,
'muscle_gain': 0.0,
'strength': 0.10,
'endurance': 0.20,
'flexibility': 0.15,
'health': 0.55
}
# V1: Predefined weight mappings per goal_mode (fallback)
WEIGHT_MAPPINGS = {
'weight_loss': {
'weight_loss': 0.60,
'endurance': 0.20,
'muscle_gain': 0.0,
'strength': 0.10,
'flexibility': 0.05,
'health': 0.05
},
'strength': {
'strength': 0.50,
'muscle_gain': 0.40,
'endurance': 0.10,
'weight_loss': 0.0,
'flexibility': 0.0,
'health': 0.0
},
'endurance': {
'endurance': 0.70,
'health': 0.20,
'flexibility': 0.10,
'weight_loss': 0.0,
'muscle_gain': 0.0,
'strength': 0.0
},
'recomposition': {
'weight_loss': 0.30,
'muscle_gain': 0.30,
'strength': 0.25,
'endurance': 0.10,
'flexibility': 0.05,
'health': 0.0
},
'health': {
'health': 0.50,
'endurance': 0.20,
'flexibility': 0.15,
'strength': 0.10,
'weight_loss': 0.05,
'muscle_gain': 0.0
}
}
return WEIGHT_MAPPINGS.get(goal_mode, WEIGHT_MAPPINGS['health'])
def get_primary_focus(conn, profile_id: str) -> str:
"""
Get the primary focus area for a profile.
Returns the focus area with the highest weight.
Useful for UI labels and simple decision logic.
Args:
conn: Database connection
profile_id: User's profile ID
Returns:
Primary focus area name (e.g., 'weight_loss', 'strength')
"""
weights = get_focus_weights(conn, profile_id)
return max(weights.items(), key=lambda x: x[1])[0]
def get_focus_description(focus_area: str) -> str:
"""
Get human-readable description for a focus area.
Args:
focus_area: Focus area key (e.g., 'weight_loss')
Returns:
German description for UI display
"""
descriptions = {
'weight_loss': 'Gewichtsreduktion & Fettabbau',
'muscle_gain': 'Muskelaufbau & Hypertrophie',
'strength': 'Kraftsteigerung & Performance',
'endurance': 'Ausdauer & aerobe Kapazität',
'flexibility': 'Beweglichkeit & Mobilität',
'health': 'Allgemeine Gesundheit & Erhaltung'
}
return descriptions.get(focus_area, focus_area)
# ============================================================================
# Phase 1.5: Universal Value Fetcher for Dynamic Goal Types
# ============================================================================
def get_goal_type_config(conn, type_key: str) -> Optional[Dict[str, Any]]:
"""
Get goal type configuration from database registry.
Args:
conn: Database connection
type_key: Goal type key (e.g., 'weight', 'meditation_minutes')
Returns:
Dict with config or None if not found/inactive
"""
cur = get_cursor(conn)
cur.execute("""
SELECT type_key, source_table, source_column, aggregation_method,
calculation_formula, filter_conditions, label_de, unit, icon, category
FROM goal_type_definitions
WHERE type_key = %s AND is_active = true
LIMIT 1
""", (type_key,))
return cur.fetchone()
def get_current_value_for_goal(conn, profile_id: str, goal_type: str) -> Optional[float]:
"""
Universal value fetcher for any goal type.
Reads configuration from goal_type_definitions table and executes
appropriate query based on aggregation_method or calculation_formula.
Args:
conn: Database connection
profile_id: User's profile ID
goal_type: Goal type key (e.g., 'weight', 'meditation_minutes')
Returns:
Current value as float or None if not available
"""
config = get_goal_type_config(conn, goal_type)
if not config:
print(f"[WARNING] Goal type '{goal_type}' not found or inactive")
return None
# Complex calculation (e.g., lean_mass)
if config['calculation_formula']:
return _execute_calculation_formula(conn, profile_id, config['calculation_formula'])
# Simple aggregation
return _fetch_by_aggregation_method(
conn,
profile_id,
config['source_table'],
config['source_column'],
config['aggregation_method'],
config.get('filter_conditions')
)
def _fetch_by_aggregation_method(
conn,
profile_id: str,
table: str,
column: str,
method: str,
filter_conditions: Optional[Any] = None
) -> Optional[float]:
"""
Fetch value using specified aggregation method.
Supported methods:
- latest: Most recent value
- avg_7d: 7-day average
- avg_30d: 30-day average
- sum_30d: 30-day sum
- count_7d: Count of entries in last 7 days
- count_30d: Count of entries in last 30 days
- min_30d: Minimum value in last 30 days
- max_30d: Maximum value in last 30 days
Args:
filter_conditions: Optional JSON filters (e.g., {"training_type": "strength"})
"""
# Guard: source_table/column required for simple aggregation
if not table or not column:
print(f"[WARNING] Missing source_table or source_column for aggregation")
return None
# Table-specific date column mapping (some tables use different column names)
DATE_COLUMN_MAP = {
'blood_pressure_log': 'measured_at',
'activity_log': 'date',
'weight_log': 'date',
'circumference_log': 'date',
'caliper_log': 'date',
'nutrition_log': 'date',
'sleep_log': 'date',
'vitals_baseline': 'date',
'rest_days': 'date',
'fitness_tests': 'test_date'
}
date_col = DATE_COLUMN_MAP.get(table, 'date')
# Build filter SQL from JSON conditions
filter_sql = ""
filter_params = []
if filter_conditions:
try:
if isinstance(filter_conditions, str):
filters = json.loads(filter_conditions)
else:
filters = filter_conditions
for filter_col, filter_val in filters.items():
if isinstance(filter_val, list):
# IN clause for multiple values
placeholders = ', '.join(['%s'] * len(filter_val))
filter_sql += f" AND {filter_col} IN ({placeholders})"
filter_params.extend(filter_val)
else:
# Single value equality
filter_sql += f" AND {filter_col} = %s"
filter_params.append(filter_val)
except (json.JSONDecodeError, TypeError, AttributeError) as e:
print(f"[WARNING] Invalid filter_conditions: {e}, ignoring filters")
cur = get_cursor(conn)
try:
if method == 'latest':
params = [profile_id] + filter_params
cur.execute(f"""
SELECT {column} FROM {table}
WHERE profile_id = %s AND {column} IS NOT NULL{filter_sql}
ORDER BY {date_col} DESC LIMIT 1
""", params)
row = cur.fetchone()
return float(row[column]) if row else None
elif method == 'avg_7d':
days_ago = date.today() - timedelta(days=7)
params = [profile_id, days_ago] + filter_params
cur.execute(f"""
SELECT AVG({column}) as avg_value FROM {table}
WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql}
""", params)
row = cur.fetchone()
return float(row['avg_value']) if row and row['avg_value'] is not None else None
elif method == 'avg_30d':
days_ago = date.today() - timedelta(days=30)
params = [profile_id, days_ago] + filter_params
cur.execute(f"""
SELECT AVG({column}) as avg_value FROM {table}
WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql}
""", params)
row = cur.fetchone()
return float(row['avg_value']) if row and row['avg_value'] is not None else None
elif method == 'sum_30d':
days_ago = date.today() - timedelta(days=30)
params = [profile_id, days_ago] + filter_params
cur.execute(f"""
SELECT SUM({column}) as sum_value FROM {table}
WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql}
""", params)
row = cur.fetchone()
return float(row['sum_value']) if row and row['sum_value'] is not None else None
elif method == 'count_7d':
days_ago = date.today() - timedelta(days=7)
params = [profile_id, days_ago] + filter_params
cur.execute(f"""
SELECT COUNT(*) as count_value FROM {table}
WHERE profile_id = %s AND {date_col} >= %s{filter_sql}
""", params)
row = cur.fetchone()
return float(row['count_value']) if row else 0.0
elif method == 'count_30d':
days_ago = date.today() - timedelta(days=30)
params = [profile_id, days_ago] + filter_params
cur.execute(f"""
SELECT COUNT(*) as count_value FROM {table}
WHERE profile_id = %s AND {date_col} >= %s{filter_sql}
""", params)
row = cur.fetchone()
return float(row['count_value']) if row else 0.0
elif method == 'min_30d':
days_ago = date.today() - timedelta(days=30)
params = [profile_id, days_ago] + filter_params
cur.execute(f"""
SELECT MIN({column}) as min_value FROM {table}
WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql}
""", params)
row = cur.fetchone()
return float(row['min_value']) if row and row['min_value'] is not None else None
elif method == 'max_30d':
days_ago = date.today() - timedelta(days=30)
params = [profile_id, days_ago] + filter_params
cur.execute(f"""
SELECT MAX({column}) as max_value FROM {table}
WHERE profile_id = %s AND {date_col} >= %s AND {column} IS NOT NULL{filter_sql}
""", params)
row = cur.fetchone()
return float(row['max_value']) if row and row['max_value'] is not None else None
else:
print(f"[WARNING] Unknown aggregation method: {method}")
return None
except Exception as e:
print(f"[ERROR] Failed to fetch value from {table}.{column} using {method}: {e}")
return None
def _execute_calculation_formula(conn, profile_id: str, formula_json: str) -> Optional[float]:
"""
Execute complex calculation formula.
Currently supports:
- lean_mass: weight - (weight * body_fat_pct / 100)
Future: Parse JSON formula and execute dynamically.
Args:
conn: Database connection
profile_id: User's profile ID
formula_json: JSON string with calculation config
Returns:
Calculated value or None
"""
try:
formula = json.loads(formula_json)
calc_type = formula.get('type')
if calc_type == 'lean_mass':
# Get dependencies
cur = get_cursor(conn)
cur.execute("""
SELECT weight FROM weight_log
WHERE profile_id = %s
ORDER BY date DESC LIMIT 1
""", (profile_id,))
weight_row = cur.fetchone()
cur.execute("""
SELECT body_fat_pct FROM caliper_log
WHERE profile_id = %s
ORDER BY date DESC LIMIT 1
""", (profile_id,))
bf_row = cur.fetchone()
if weight_row and bf_row:
weight = float(weight_row['weight'])
bf_pct = float(bf_row['body_fat_pct'])
lean_mass = weight - (weight * bf_pct / 100.0)
return round(lean_mass, 2)
return None
else:
print(f"[WARNING] Unknown calculation type: {calc_type}")
return None
except (json.JSONDecodeError, KeyError, ValueError, TypeError) as e:
print(f"[ERROR] Formula execution failed: {e}, formula={formula_json}")
return None
# Future V2 Implementation (commented out for reference):
"""
def get_focus_weights_v2(conn, profile_id: str) -> Dict[str, float]:
'''V2: Read from focus_areas table with custom user weights'''
cur = get_cursor(conn)
cur.execute('''
SELECT weight_loss_pct, muscle_gain_pct, endurance_pct,
strength_pct, flexibility_pct, health_pct
FROM focus_areas
WHERE profile_id = %s AND active = true
LIMIT 1
''', (profile_id,))
row = cur.fetchone()
if not row:
# Fallback to V1 behavior
return get_focus_weights(conn, profile_id)
# Convert percentages to weights (0-1 range)
return {
'weight_loss': row['weight_loss_pct'] / 100.0,
'muscle_gain': row['muscle_gain_pct'] / 100.0,
'endurance': row['endurance_pct'] / 100.0,
'strength': row['strength_pct'] / 100.0,
'flexibility': row['flexibility_pct'] / 100.0,
'health': row['health_pct'] / 100.0
}
"""

View File

@ -23,6 +23,7 @@ from routers import user_restrictions, access_grants, training_types, admin_trai
from routers import admin_activity_mappings, sleep, rest_days
from routers import 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("/")

View File

@ -0,0 +1,135 @@
-- Migration 022: Goal System (Strategic + Tactical)
-- Date: 2026-03-26
-- Purpose: Two-level goal architecture for AI-driven coaching
-- ============================================================================
-- STRATEGIC LAYER: Goal Modes
-- ============================================================================
-- Add goal_mode to profiles (strategic training direction)
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS goal_mode VARCHAR(50) DEFAULT 'health';
COMMENT ON COLUMN profiles.goal_mode IS
'Strategic goal mode: weight_loss, strength, endurance, recomposition, health.
Determines score weights and interpretation context for all analyses.';
-- ============================================================================
-- TACTICAL LAYER: Concrete Goal Targets
-- ============================================================================
CREATE TABLE IF NOT EXISTS goals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Goal Classification
goal_type VARCHAR(50) NOT NULL, -- weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr
is_primary BOOLEAN DEFAULT false,
status VARCHAR(20) DEFAULT 'active', -- draft, active, reached, abandoned, expired
-- Target Values
target_value DECIMAL(10,2),
current_value DECIMAL(10,2),
start_value DECIMAL(10,2),
unit VARCHAR(20), -- kg, %, ml/kg/min, bpm, mmHg, cm, reps
-- Timeline
start_date DATE DEFAULT CURRENT_DATE,
target_date DATE,
reached_date DATE,
-- Metadata
name VARCHAR(100), -- e.g., "Sommerfigur 2026"
description TEXT,
-- Progress Tracking
progress_pct DECIMAL(5,2), -- Auto-calculated: (current - start) / (target - start) * 100
projection_date DATE, -- Prognose wann Ziel erreicht wird
on_track BOOLEAN, -- true wenn Prognose <= target_date
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_goals_profile ON goals(profile_id);
CREATE INDEX IF NOT EXISTS idx_goals_status ON goals(profile_id, status);
CREATE INDEX IF NOT EXISTS idx_goals_primary ON goals(profile_id, is_primary) WHERE is_primary = true;
COMMENT ON TABLE goals IS 'Concrete user goals (tactical targets)';
COMMENT ON COLUMN goals.goal_type IS 'Type of goal: weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr';
COMMENT ON COLUMN goals.is_primary IS 'Primary goal gets highest priority in scoring and charts';
COMMENT ON COLUMN goals.status IS 'draft = not yet started, active = in progress, reached = successfully completed, abandoned = given up, expired = deadline passed';
COMMENT ON COLUMN goals.progress_pct IS 'Percentage progress: (current_value - start_value) / (target_value - start_value) * 100';
COMMENT ON COLUMN goals.projection_date IS 'Projected date when goal will be reached based on current trend';
COMMENT ON COLUMN goals.on_track IS 'true if projection_date <= target_date (goal reachable on time)';
-- ============================================================================
-- TRAINING PHASES (Auto-Detection)
-- ============================================================================
CREATE TABLE IF NOT EXISTS training_phases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Phase Classification
phase_type VARCHAR(50) NOT NULL, -- calorie_deficit, calorie_surplus, deload, maintenance, periodization
detected_automatically BOOLEAN DEFAULT false,
confidence_score DECIMAL(3,2), -- 0.00 - 1.00 (Wie sicher ist die Erkennung?)
status VARCHAR(20) DEFAULT 'suggested', -- suggested, accepted, active, completed, rejected
-- Timeframe
start_date DATE NOT NULL,
end_date DATE,
duration_days INT,
-- Detection Criteria (JSONB für Flexibilität)
detection_params JSONB, -- { "avg_calories": 1800, "weight_trend": -0.3, ... }
-- User Notes
notes TEXT,
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_training_phases_profile ON training_phases(profile_id);
CREATE INDEX IF NOT EXISTS idx_training_phases_status ON training_phases(profile_id, status);
CREATE INDEX IF NOT EXISTS idx_training_phases_dates ON training_phases(profile_id, start_date, end_date);
COMMENT ON TABLE training_phases IS 'Training phases detected from data patterns or manually defined';
COMMENT ON COLUMN training_phases.phase_type IS 'calorie_deficit, calorie_surplus, deload, maintenance, periodization';
COMMENT ON COLUMN training_phases.detected_automatically IS 'true if AI detected this phase from data patterns';
COMMENT ON COLUMN training_phases.confidence_score IS 'AI confidence in detection (0.0 - 1.0)';
COMMENT ON COLUMN training_phases.status IS 'suggested = AI proposed, accepted = user confirmed, active = currently running, completed = finished, rejected = user dismissed';
COMMENT ON COLUMN training_phases.detection_params IS 'JSON with detection criteria: avg_calories, weight_trend, activity_volume, etc.';
-- ============================================================================
-- FITNESS TESTS (Standardized Performance Tests)
-- ============================================================================
CREATE TABLE IF NOT EXISTS fitness_tests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Test Type
test_type VARCHAR(50) NOT NULL, -- cooper_12min, step_test, pushups_max, plank_max, flexibility_sit_reach, vo2max_est, strength_1rm_squat, strength_1rm_bench
result_value DECIMAL(10,2) NOT NULL,
result_unit VARCHAR(20) NOT NULL, -- meters, bpm, reps, seconds, cm, ml/kg/min, kg
-- Test Metadata
test_date DATE NOT NULL,
test_conditions TEXT, -- Optional: Notizen zu Bedingungen
norm_category VARCHAR(30), -- sehr gut, gut, durchschnitt, unterdurchschnitt, schlecht
-- Timestamps
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_fitness_tests_profile ON fitness_tests(profile_id);
CREATE INDEX IF NOT EXISTS idx_fitness_tests_type ON fitness_tests(profile_id, test_type);
CREATE INDEX IF NOT EXISTS idx_fitness_tests_date ON fitness_tests(profile_id, test_date);
COMMENT ON TABLE fitness_tests IS 'Standardized fitness tests (Cooper, step test, strength tests, etc.)';
COMMENT ON COLUMN fitness_tests.test_type IS 'cooper_12min, step_test, pushups_max, plank_max, flexibility_sit_reach, vo2max_est, strength_1rm_squat, strength_1rm_bench';
COMMENT ON COLUMN fitness_tests.norm_category IS 'Performance category based on age/gender norms';

View File

@ -0,0 +1,185 @@
-- Migration 024: Goal Type Registry (Flexible Goal System)
-- Date: 2026-03-27
-- Purpose: Enable dynamic goal types without code changes
-- ============================================================================
-- Goal Type Definitions
-- ============================================================================
CREATE TABLE IF NOT EXISTS goal_type_definitions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Unique identifier (used in code)
type_key VARCHAR(50) UNIQUE NOT NULL,
-- Display metadata
label_de VARCHAR(100) NOT NULL,
label_en VARCHAR(100),
unit VARCHAR(20) NOT NULL,
icon VARCHAR(10),
category VARCHAR(50), -- body, mind, activity, nutrition, recovery, custom
-- Data source configuration
source_table VARCHAR(50), -- Which table to query
source_column VARCHAR(50), -- Which column to fetch
aggregation_method VARCHAR(20), -- How to aggregate: latest, avg_7d, avg_30d, sum_30d, count_7d, count_30d, min_30d, max_30d
-- Complex calculations (optional)
-- For types like lean_mass that need custom logic
-- JSON format: {"type": "formula", "dependencies": ["weight", "body_fat"], "expression": "..."}
calculation_formula TEXT,
-- Metadata
description TEXT,
is_active BOOLEAN DEFAULT true,
is_system BOOLEAN DEFAULT false, -- System types cannot be deleted
-- Audit
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_goal_type_definitions_active ON goal_type_definitions(is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_goal_type_definitions_category ON goal_type_definitions(category);
COMMENT ON TABLE goal_type_definitions IS 'Registry of available goal types - allows dynamic goal creation without code changes';
COMMENT ON COLUMN goal_type_definitions.type_key IS 'Unique key used in code (e.g., weight, meditation_minutes)';
COMMENT ON COLUMN goal_type_definitions.aggregation_method IS 'latest = most recent value, avg_7d = 7-day average, count_7d = count in last 7 days, etc.';
COMMENT ON COLUMN goal_type_definitions.calculation_formula IS 'JSON for complex calculations like lean_mass = weight - (weight * bf_pct / 100)';
COMMENT ON COLUMN goal_type_definitions.is_system IS 'System types are protected from deletion (core functionality)';
-- ============================================================================
-- Seed Data: Migrate existing 8 goal types
-- ============================================================================
-- 1. Weight (simple - latest value)
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'weight', 'Gewicht', 'Weight', 'kg', '⚖️', 'body',
'weight_log', 'weight', 'latest',
'Aktuelles Körpergewicht', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 2. Body Fat (simple - latest value)
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'body_fat', 'Körperfett', 'Body Fat', '%', '📊', 'body',
'caliper_log', 'body_fat_pct', 'latest',
'Körperfettanteil aus Caliper-Messung', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 3. Lean Mass (complex - calculation formula)
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
calculation_formula,
description, is_system
) VALUES (
'lean_mass', 'Muskelmasse', 'Lean Mass', 'kg', '💪', 'body',
'{"type": "lean_mass", "dependencies": ["weight_log.weight", "caliper_log.body_fat_pct"], "formula": "weight - (weight * body_fat_pct / 100)"}',
'Fettfreie Körpermasse (berechnet aus Gewicht und Körperfett)', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 4. VO2 Max (simple - latest value)
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'vo2max', 'VO2Max', 'VO2Max', 'ml/kg/min', '🫁', 'recovery',
'vitals_baseline', 'vo2_max', 'latest',
'Maximale Sauerstoffaufnahme (geschätzt oder gemessen)', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 5. Resting Heart Rate (simple - latest value)
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'rhr', 'Ruhepuls', 'Resting Heart Rate', 'bpm', '💓', 'recovery',
'vitals_baseline', 'resting_hr', 'latest',
'Ruhepuls morgens vor dem Aufstehen', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 6. Blood Pressure (placeholder - compound goal for v2.0)
-- Currently limited to single value, v2.0 will support systolic/diastolic
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'bp', 'Blutdruck', 'Blood Pressure', 'mmHg', '❤️', 'recovery',
'blood_pressure_log', 'systolic', 'latest',
'Blutdruck (aktuell nur systolisch, v2.0: beide Werte)', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 7. Strength (placeholder - no data source yet)
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
description, is_system, is_active
) VALUES (
'strength', 'Kraft', 'Strength', 'kg', '🏋️', 'activity',
'Maximalkraft (Platzhalter, Datenquelle in v2.0)', true, false
)
ON CONFLICT (type_key) DO NOTHING;
-- 8. Flexibility (placeholder - no data source yet)
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
description, is_system, is_active
) VALUES (
'flexibility', 'Beweglichkeit', 'Flexibility', 'cm', '🤸', 'activity',
'Beweglichkeit (Platzhalter, Datenquelle in v2.0)', true, false
)
ON CONFLICT (type_key) DO NOTHING;
-- ============================================================================
-- Example: Future custom goal types (commented out, for reference)
-- ============================================================================
/*
-- Meditation Minutes (avg last 7 days)
INSERT INTO goal_type_definitions (
type_key, label_de, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'meditation_minutes', 'Meditation', 'min/Tag', '🧘', 'mind',
'meditation_log', 'duration_minutes', 'avg_7d',
'Durchschnittliche Meditationsdauer pro Tag (7 Tage)', false
);
-- Training Frequency (count last 7 days)
INSERT INTO goal_type_definitions (
type_key, label_de, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'training_frequency', 'Trainingshäufigkeit', 'x/Woche', '📅', 'activity',
'activity_log', 'id', 'count_7d',
'Anzahl Trainingseinheiten pro Woche', false
);
-- Sleep Quality (avg last 7 days)
INSERT INTO goal_type_definitions (
type_key, label_de, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'sleep_quality', 'Schlafqualität', '%', '💤', 'recovery',
'sleep_log', 'quality_score', 'avg_7d',
'Durchschnittliche Schlafqualität (Deep+REM Anteil)', false
);
*/

View File

@ -0,0 +1,103 @@
-- Migration 025: Cleanup goal_type_definitions
-- Date: 2026-03-27
-- Purpose: Remove problematic FK columns and ensure seed data
-- Remove created_by/updated_by columns if they exist
-- (May have been created by failed Migration 024)
ALTER TABLE goal_type_definitions DROP COLUMN IF EXISTS created_by;
ALTER TABLE goal_type_definitions DROP COLUMN IF EXISTS updated_by;
-- Re-insert seed data (ON CONFLICT ensures idempotency)
-- This fixes cases where Migration 024 created table but failed to seed
-- 1. Weight
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'weight', 'Gewicht', 'Weight', 'kg', '⚖️', 'body',
'weight_log', 'weight', 'latest',
'Aktuelles Körpergewicht', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 2. Body Fat
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'body_fat', 'Körperfett', 'Body Fat', '%', '📊', 'body',
'caliper_log', 'body_fat_pct', 'latest',
'Körperfettanteil aus Caliper-Messung', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 3. Lean Mass
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
calculation_formula,
description, is_system
) VALUES (
'lean_mass', 'Muskelmasse', 'Lean Mass', 'kg', '💪', 'body',
'{"type": "lean_mass", "dependencies": ["weight_log.weight", "caliper_log.body_fat_pct"], "formula": "weight - (weight * body_fat_pct / 100)"}',
'Fettfreie Körpermasse (berechnet aus Gewicht und Körperfett)', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 4. VO2 Max
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'vo2max', 'VO2Max', 'VO2Max', 'ml/kg/min', '🫁', 'recovery',
'vitals_baseline', 'vo2_max', 'latest',
'Maximale Sauerstoffaufnahme (geschätzt oder gemessen)', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 5. Resting Heart Rate
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'rhr', 'Ruhepuls', 'Resting Heart Rate', 'bpm', '💓', 'recovery',
'vitals_baseline', 'resting_hr', 'latest',
'Ruhepuls morgens vor dem Aufstehen', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 6. Blood Pressure
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
description, is_system
) VALUES (
'bp', 'Blutdruck', 'Blood Pressure', 'mmHg', '❤️', 'recovery',
'blood_pressure_log', 'systolic', 'latest',
'Blutdruck (aktuell nur systolisch, v2.0: beide Werte)', true
)
ON CONFLICT (type_key) DO NOTHING;
-- 7. Strength (inactive placeholder)
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
description, is_system, is_active
) VALUES (
'strength', 'Kraft', 'Strength', 'kg', '🏋️', 'activity',
'Maximalkraft (Platzhalter, Datenquelle in v2.0)', true, false
)
ON CONFLICT (type_key) DO NOTHING;
-- 8. Flexibility (inactive placeholder)
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
description, is_system, is_active
) VALUES (
'flexibility', 'Beweglichkeit', 'Flexibility', 'cm', '🤸', 'activity',
'Beweglichkeit (Platzhalter, Datenquelle in v2.0)', true, false
)
ON CONFLICT (type_key) DO NOTHING;

View File

@ -0,0 +1,40 @@
-- Migration 026: Goal Type Filters
-- Date: 2026-03-27
-- Purpose: Enable filtered counting/aggregation (e.g., count only strength training)
-- Add filter_conditions column for flexible filtering
ALTER TABLE goal_type_definitions
ADD COLUMN IF NOT EXISTS filter_conditions JSONB;
COMMENT ON COLUMN goal_type_definitions.filter_conditions IS
'Optional filter conditions as JSON. Example: {"training_type": "strength"} to count only strength training sessions.
Supports any column in the source table. Format: {"column_name": "value"} or {"column_name": ["value1", "value2"]} for IN clause.';
-- Example usage (commented out):
/*
-- Count only strength training sessions per week
INSERT INTO goal_type_definitions (
type_key, label_de, unit, icon, category,
source_table, source_column, aggregation_method,
filter_conditions,
description, is_system
) VALUES (
'strength_frequency', 'Krafttraining Häufigkeit', 'x/Woche', '🏋️', 'activity',
'activity_log', 'id', 'count_7d',
'{"training_type": "strength"}',
'Anzahl Krafttraining-Einheiten pro Woche', false
) ON CONFLICT (type_key) DO NOTHING;
-- Count only cardio sessions per week
INSERT INTO goal_type_definitions (
type_key, label_de, unit, icon, category,
source_table, source_column, aggregation_method,
filter_conditions,
description, is_system
) VALUES (
'cardio_frequency', 'Cardio Häufigkeit', 'x/Woche', '🏃', 'activity',
'activity_log', 'id', 'count_7d',
'{"training_type": "cardio"}',
'Anzahl Cardio-Einheiten pro Woche', false
) ON CONFLICT (type_key) DO NOTHING;
*/

View File

@ -0,0 +1,125 @@
-- Migration 027: Focus Areas System (Goal System v2.0)
-- Date: 2026-03-27
-- Purpose: Replace single primary goal with weighted multi-goal system
-- ============================================================================
-- Focus Areas Table
-- ============================================================================
CREATE TABLE IF NOT EXISTS focus_areas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Six focus dimensions (percentages, sum = 100)
weight_loss_pct INTEGER DEFAULT 0 CHECK (weight_loss_pct >= 0 AND weight_loss_pct <= 100),
muscle_gain_pct INTEGER DEFAULT 0 CHECK (muscle_gain_pct >= 0 AND muscle_gain_pct <= 100),
strength_pct INTEGER DEFAULT 0 CHECK (strength_pct >= 0 AND strength_pct <= 100),
endurance_pct INTEGER DEFAULT 0 CHECK (endurance_pct >= 0 AND endurance_pct <= 100),
flexibility_pct INTEGER DEFAULT 0 CHECK (flexibility_pct >= 0 AND flexibility_pct <= 100),
health_pct INTEGER DEFAULT 0 CHECK (health_pct >= 0 AND health_pct <= 100),
-- Status
active BOOLEAN DEFAULT true,
-- Audit
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
-- Constraints
CONSTRAINT sum_equals_100 CHECK (
weight_loss_pct + muscle_gain_pct + strength_pct +
endurance_pct + flexibility_pct + health_pct = 100
)
);
-- Only one active focus_areas per profile
CREATE UNIQUE INDEX IF NOT EXISTS idx_focus_areas_profile_active
ON focus_areas(profile_id) WHERE active = true;
COMMENT ON TABLE focus_areas IS 'User-defined focus area weights (replaces simple goal_mode). Enables multi-goal prioritization with custom percentages.';
COMMENT ON COLUMN focus_areas.weight_loss_pct IS 'Focus on fat loss (0-100%)';
COMMENT ON COLUMN focus_areas.muscle_gain_pct IS 'Focus on muscle growth (0-100%)';
COMMENT ON COLUMN focus_areas.strength_pct IS 'Focus on strength gains (0-100%)';
COMMENT ON COLUMN focus_areas.endurance_pct IS 'Focus on aerobic capacity (0-100%)';
COMMENT ON COLUMN focus_areas.flexibility_pct IS 'Focus on mobility/flexibility (0-100%)';
COMMENT ON COLUMN focus_areas.health_pct IS 'Focus on general health (0-100%)';
-- ============================================================================
-- Migrate existing goal_mode to focus_areas
-- ============================================================================
-- For each profile with a goal_mode, create initial focus_areas
INSERT INTO focus_areas (
profile_id,
weight_loss_pct, muscle_gain_pct, strength_pct,
endurance_pct, flexibility_pct, health_pct
)
SELECT
id AS profile_id,
CASE goal_mode
WHEN 'weight_loss' THEN 60
WHEN 'recomposition' THEN 30
WHEN 'health' THEN 5
ELSE 0
END AS weight_loss_pct,
CASE goal_mode
WHEN 'strength' THEN 40 ELSE 0
END +
CASE goal_mode
WHEN 'recomposition' THEN 30 ELSE 0
END AS muscle_gain_pct,
CASE goal_mode
WHEN 'strength' THEN 50
WHEN 'recomposition' THEN 25
WHEN 'weight_loss' THEN 10
WHEN 'health' THEN 10
ELSE 0
END AS strength_pct,
CASE goal_mode
WHEN 'endurance' THEN 70
WHEN 'recomposition' THEN 10
WHEN 'weight_loss' THEN 20
WHEN 'health' THEN 20
ELSE 0
END AS endurance_pct,
CASE goal_mode
WHEN 'endurance' THEN 10 ELSE 0
END +
CASE goal_mode
WHEN 'health' THEN 15 ELSE 0
END +
CASE goal_mode
WHEN 'recomposition' THEN 5 ELSE 0
END +
CASE goal_mode
WHEN 'weight_loss' THEN 5 ELSE 0
END AS flexibility_pct,
CASE goal_mode
WHEN 'health' THEN 50
WHEN 'endurance' THEN 20
WHEN 'strength' THEN 10
WHEN 'weight_loss' THEN 5
ELSE 0
END AS health_pct
FROM profiles
WHERE goal_mode IS NOT NULL
ON CONFLICT DO NOTHING;
-- For profiles without goal_mode, use balanced health focus
INSERT INTO focus_areas (
profile_id,
weight_loss_pct, muscle_gain_pct, strength_pct,
endurance_pct, flexibility_pct, health_pct
)
SELECT
id AS profile_id,
0, 0, 10, 20, 15, 55
FROM profiles
WHERE goal_mode IS NULL
AND id NOT IN (SELECT profile_id FROM focus_areas WHERE active = true)
ON CONFLICT DO NOTHING;

View File

@ -0,0 +1,57 @@
-- Migration 028: Goal Categories and Priorities
-- Date: 2026-03-27
-- Purpose: Multi-dimensional goal priorities (one primary goal per category)
-- ============================================================================
-- Add category and priority columns
-- ============================================================================
ALTER TABLE goals
ADD COLUMN category VARCHAR(50),
ADD COLUMN priority INTEGER DEFAULT 2 CHECK (priority >= 1 AND priority <= 3);
COMMENT ON COLUMN goals.category IS 'Goal category: body, training, nutrition, recovery, health, other';
COMMENT ON COLUMN goals.priority IS 'Priority level: 1=high, 2=medium, 3=low';
-- ============================================================================
-- Migrate existing goals to categories based on goal_type
-- ============================================================================
UPDATE goals SET category = CASE
-- Body composition goals
WHEN goal_type IN ('weight', 'body_fat', 'lean_mass') THEN 'body'
-- Training goals
WHEN goal_type IN ('strength', 'flexibility', 'training_frequency') THEN 'training'
-- Health/cardio goals
WHEN goal_type IN ('vo2max', 'rhr', 'bp', 'hrv') THEN 'health'
-- Recovery goals
WHEN goal_type IN ('sleep_quality', 'sleep_duration', 'rest_days') THEN 'recovery'
-- Nutrition goals
WHEN goal_type IN ('calories', 'protein', 'healthy_eating') THEN 'nutrition'
-- Default
ELSE 'other'
END
WHERE category IS NULL;
-- ============================================================================
-- Set priority based on is_primary
-- ============================================================================
UPDATE goals SET priority = CASE
WHEN is_primary = true THEN 1 -- Primary goals get priority 1
ELSE 2 -- Others get priority 2 (medium)
END;
-- ============================================================================
-- Create index for category-based queries
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_goals_category_priority
ON goals(profile_id, category, priority);
COMMENT ON INDEX idx_goals_category_priority IS 'Fast lookup for category-grouped goals sorted by priority';

View File

@ -0,0 +1,74 @@
-- Migration 029: Fix Missing Goal Types (flexibility, strength)
-- Date: 2026-03-27
-- Purpose: Ensure flexibility and strength goal types are active and properly configured
-- These types were created earlier but are inactive or misconfigured
-- This migration fixes them without breaking if they don't exist
-- ============================================================================
-- Upsert flexibility goal type
-- ============================================================================
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
calculation_formula, filter_conditions, description, is_active
) VALUES (
'flexibility',
'Beweglichkeit',
'Flexibility',
'cm',
'🤸',
'training',
NULL, -- No automatic data source
NULL,
'latest',
NULL,
NULL,
'Beweglichkeit und Mobilität - manuelle Erfassung',
true
)
ON CONFLICT (type_key)
DO UPDATE SET
label_de = 'Beweglichkeit',
label_en = 'Flexibility',
unit = 'cm',
icon = '🤸',
category = 'training',
is_active = true,
description = 'Beweglichkeit und Mobilität - manuelle Erfassung';
-- ============================================================================
-- Upsert strength goal type
-- ============================================================================
INSERT INTO goal_type_definitions (
type_key, label_de, label_en, unit, icon, category,
source_table, source_column, aggregation_method,
calculation_formula, filter_conditions, description, is_active
) VALUES (
'strength',
'Kraftniveau',
'Strength',
'Punkte',
'💪',
'training',
NULL, -- No automatic data source
NULL,
'latest',
NULL,
NULL,
'Allgemeines Kraftniveau - manuelle Erfassung',
true
)
ON CONFLICT (type_key)
DO UPDATE SET
label_de = 'Kraftniveau',
label_en = 'Strength',
unit = 'Punkte',
icon = '💪',
category = 'training',
is_active = true,
description = 'Allgemeines Kraftniveau - manuelle Erfassung';
COMMENT ON TABLE goal_type_definitions IS 'Goal type registry - defines all available goal types (v1.5: DB-driven, flexible system)';

View File

@ -0,0 +1,64 @@
-- Migration 030: Goal Progress Log
-- Date: 2026-03-27
-- Purpose: Track progress history for all goals (especially custom goals without data source)
-- ============================================================================
-- Goal Progress Log Table
-- ============================================================================
CREATE TABLE IF NOT EXISTS goal_progress_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
goal_id UUID NOT NULL REFERENCES goals(id) ON DELETE CASCADE,
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Progress data
date DATE NOT NULL,
value DECIMAL(10,2) NOT NULL,
note TEXT,
-- Metadata
source VARCHAR(20) DEFAULT 'manual' CHECK (source IN ('manual', 'automatic', 'import')),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
-- Constraints
CONSTRAINT unique_progress_per_day UNIQUE(goal_id, date)
);
CREATE INDEX idx_goal_progress_goal_date ON goal_progress_log(goal_id, date DESC);
CREATE INDEX idx_goal_progress_profile ON goal_progress_log(profile_id);
COMMENT ON TABLE goal_progress_log IS 'Progress history for goals - enables manual tracking for custom goals and charts';
COMMENT ON COLUMN goal_progress_log.value IS 'Progress value in goal unit (e.g., kg, cm, points)';
COMMENT ON COLUMN goal_progress_log.source IS 'manual: user entered, automatic: computed from data source, import: CSV/API';
-- ============================================================================
-- Function: Update goal current_value from latest progress
-- ============================================================================
CREATE OR REPLACE FUNCTION update_goal_current_value()
RETURNS TRIGGER AS $$
BEGIN
-- Update current_value in goals table with latest progress entry
UPDATE goals
SET current_value = (
SELECT value
FROM goal_progress_log
WHERE goal_id = NEW.goal_id
ORDER BY date DESC
LIMIT 1
),
updated_at = NOW()
WHERE id = NEW.goal_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger: Auto-update current_value when progress is added/updated
CREATE TRIGGER trigger_update_goal_current_value
AFTER INSERT OR UPDATE ON goal_progress_log
FOR EACH ROW
EXECUTE FUNCTION update_goal_current_value();
COMMENT ON FUNCTION update_goal_current_value IS 'Auto-update goal.current_value when new progress is logged';

1252
backend/routers/goals.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""
Manual Migration 024 Runner
Run this to manually execute Migration 024 if it didn't run automatically.
"""
import psycopg2
import os
from psycopg2.extras import RealDictCursor
# Database connection
DB_HOST = os.getenv('DB_HOST', 'localhost')
DB_PORT = os.getenv('DB_PORT', '5432')
DB_NAME = os.getenv('DB_NAME', 'bodytrack')
DB_USER = os.getenv('DB_USER', 'bodytrack')
DB_PASS = os.getenv('DB_PASSWORD', '')
def main():
print("🔧 Manual Migration 024 Runner")
print("=" * 60)
# Connect to database
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASS
)
conn.autocommit = False
cur = conn.cursor(cursor_factory=RealDictCursor)
try:
# Check if table exists
cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'goal_type_definitions'
)
""")
exists = cur.fetchone()['exists']
if exists:
print("✓ goal_type_definitions table already exists")
# Check if it has data
cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions")
count = cur.fetchone()['count']
print(f"✓ Table has {count} entries")
if count > 0:
print("\n📊 Existing Goal Types:")
cur.execute("""
SELECT type_key, label_de, unit, is_system, is_active
FROM goal_type_definitions
ORDER BY is_system DESC, label_de
""")
for row in cur.fetchall():
status = "SYSTEM" if row['is_system'] else "CUSTOM"
active = "ACTIVE" if row['is_active'] else "INACTIVE"
print(f" - {row['type_key']}: {row['label_de']} ({row['unit']}) [{status}] [{active}]")
print("\n✅ Migration 024 is already complete!")
return
# Run migration
print("\n🚀 Running Migration 024...")
with open('migrations/024_goal_type_registry.sql', 'r', encoding='utf-8') as f:
migration_sql = f.read()
cur.execute(migration_sql)
conn.commit()
print("✅ Migration 024 executed successfully!")
# Verify
cur.execute("SELECT COUNT(*) as count FROM goal_type_definitions")
count = cur.fetchone()['count']
print(f"{count} goal types seeded")
# Show created types
cur.execute("""
SELECT type_key, label_de, unit, is_system
FROM goal_type_definitions
WHERE is_active = true
ORDER BY is_system DESC, label_de
""")
print("\n📊 Created Goal Types:")
for row in cur.fetchall():
status = "SYSTEM" if row['is_system'] else "CUSTOM"
print(f" - {row['type_key']}: {row['label_de']} ({row['unit']}) [{status}]")
# Update schema_migrations
cur.execute("""
INSERT INTO schema_migrations (filename, executed_at)
VALUES ('024_goal_type_registry.sql', NOW())
ON CONFLICT (filename) DO NOTHING
""")
conn.commit()
print("\n✅ Migration 024 complete!")
except Exception as e:
conn.rollback()
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
finally:
cur.close()
conn.close()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,595 @@
# Zielesystem: Vereinheitlichte Analyse beider Fachkonzepte
**Datum:** 26. März 2026
**Basis:**
- `.claude/docs/functional/GOALS_VITALS.md` (v9e Spec)
- `.claude/docs/functional/mitai_jinkendo_konzept_diagramme_auswertungen_v2.md`
---
## 1. Wichtige Erkenntnis: BEIDE Konzepte sind komplementär!
### GOALS_VITALS.md definiert:
- **Konkrete Zielwerte** (z.B. "82kg bis 30.06.2026")
- 8 Zieltypen (Gewicht, KF%, VO2Max, etc.)
- Primär-/Nebenziel-Konzept
- Trainingsphasen (automatische Erkennung)
- Aktive Tests (Cooper, Liegestütze, etc.)
- 13 neue KI-Platzhalter
### Konzept v2 definiert:
- **Goal Modes** (strategische Ausrichtung: weight_loss, strength, etc.)
- Score-Gewichtung je Goal Mode
- Chart-Priorisierung je Goal Mode
- Regelbasierte Interpretationen
### Zusammenspiel:
```
Goal MODE (v2) → "weight_loss" (strategische Ausrichtung)
Primary GOAL (v9e) → "82kg bis 30.06.2026" (konkretes Ziel)
Secondary GOAL → "16% Körperfett"
Training PHASE (v9e) → "Kaloriendefizit" (automatisch erkannt)
Score Weights (v2) → body_progress: 0.30, nutrition: 0.25, ...
Charts (v2) → Zeigen gewichtete Scores + Fortschritt zu Zielen
```
---
## 2. Zwei-Ebenen-Architektur
### Ebene 1: STRATEGIC (Goal Modes aus v2)
**Was:** Grundsätzliche Trainingsausrichtung
**Werte:** weight_loss, strength, endurance, recomposition, health
**Zweck:** Bestimmt Score-Gewichtung und Interpretations-Kontext
**Beispiel:** "Ich will Kraft aufbauen" → mode: strength
### Ebene 2: TACTICAL (Goal Targets aus v9e)
**Was:** Konkrete messbare Ziele
**Werte:** "82kg bis 30.06.2026", "VO2Max 55 ml/kg/min", "50 Liegestütze"
**Zweck:** Fortschritts-Tracking, Prognosen, Motivation
**Beispiel:** "Ich will 82kg wiegen" → target: Gewichtsziel
### Beide zusammen = Vollständiges Zielesystem
---
## 3. Überarbeitetes Datenmodell
### Tabelle: `profiles` (erweitern)
```sql
-- Strategic Goal Mode (aus v2)
ALTER TABLE profiles ADD COLUMN goal_mode VARCHAR(50) DEFAULT 'health';
COMMENT ON COLUMN profiles.goal_mode IS
'Strategic goal mode: weight_loss, strength, endurance, recomposition, health.
Determines score weights and interpretation context.';
```
### Tabelle: `goals` (NEU, aus v9e)
```sql
CREATE TABLE goals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Goal Classification
goal_type VARCHAR(50) NOT NULL, -- weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr
is_primary BOOLEAN DEFAULT false,
status VARCHAR(20) DEFAULT 'active', -- draft, active, reached, abandoned, expired
-- Target Values
target_value DECIMAL(10,2),
current_value DECIMAL(10,2),
start_value DECIMAL(10,2),
unit VARCHAR(20), -- kg, %, ml/kg/min, bpm, mmHg, cm, reps
-- Timeline
start_date DATE DEFAULT CURRENT_DATE,
target_date DATE,
reached_date DATE,
-- Metadata
name VARCHAR(100), -- z.B. "Sommerfigur 2026"
description TEXT,
-- Progress Tracking
progress_pct DECIMAL(5,2), -- Auto-calculated: (current - start) / (target - start) * 100
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
-- Constraints
CHECK (progress_pct >= 0 AND progress_pct <= 100),
CHECK (status IN ('draft', 'active', 'reached', 'abandoned', 'expired'))
);
-- Only one primary goal per profile
CREATE UNIQUE INDEX idx_goals_primary ON goals(profile_id, is_primary) WHERE is_primary = true;
-- Index for active goals lookup
CREATE INDEX idx_goals_active ON goals(profile_id, status) WHERE status = 'active';
```
### Tabelle: `training_phases` (NEU, aus v9e)
```sql
CREATE TABLE training_phases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Phase Type
phase_type VARCHAR(50) NOT NULL,
-- Werte: calorie_deficit, calorie_maintenance, calorie_surplus,
-- conditioning, hiit, max_strength, regeneration, competition_prep
-- Detection
detected_automatically BOOLEAN DEFAULT false,
confidence_score DECIMAL(3,2), -- 0.00-1.00
-- Status
status VARCHAR(20) DEFAULT 'suggested', -- suggested, confirmed, active, ended
-- Timeline
start_date DATE,
end_date DATE,
-- Metadata
detection_reason TEXT, -- Why was this phase detected?
user_notes TEXT,
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Only one active phase per profile
CREATE UNIQUE INDEX idx_phases_active ON training_phases(profile_id, status) WHERE status = 'active';
```
### Tabelle: `fitness_tests` (NEU, aus v9e)
```sql
CREATE TABLE fitness_tests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Test Type
test_type VARCHAR(50) NOT NULL,
-- Standard: cooper, step_test, pushups, squats, sit_reach, balance, grip_strength
-- Custom: user_defined
-- Result
result_value DECIMAL(10,2) NOT NULL,
result_unit VARCHAR(20) NOT NULL, -- meters, bpm, reps, cm, seconds, kg
-- Test Date
test_date DATE NOT NULL,
-- Evaluation
norm_category VARCHAR(30), -- very_good, good, average, needs_improvement
percentile DECIMAL(5,2), -- Where user ranks vs. norm (0-100)
-- Trend
improvement_vs_last DECIMAL(10,2), -- % change from previous test
-- Metadata
notes TEXT,
conditions TEXT, -- e.g., "Nach 3h Schlaf, erkältet"
-- Next Test Recommendation
recommended_retest_date DATE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_fitness_tests_profile_type ON fitness_tests(profile_id, test_type, test_date DESC);
```
---
## 4. Vereinheitlichte API-Struktur
### Goal Modes (Strategic)
```python
# routers/goals.py
@router.get("/modes")
def get_goal_modes():
"""Get all strategic goal modes with score weights."""
return GOAL_MODES # From v2 concept
@router.post("/set-mode")
def set_goal_mode(goal_mode: str, session=Depends(require_auth)):
"""Set user's strategic goal mode."""
# Updates profiles.goal_mode
```
### Goal Targets (Tactical)
```python
@router.get("/targets")
def get_goal_targets(session=Depends(require_auth)):
"""Get all active goal targets."""
profile_id = session['profile_id']
# Returns list from goals table
# Includes: primary + all secondary goals
@router.post("/targets")
def create_goal_target(goal: GoalCreate, session=Depends(require_auth)):
"""Create a new goal target."""
# Inserts into goals table
# Auto-calculates progress_pct
@router.get("/targets/{goal_id}")
def get_goal_detail(goal_id: str, session=Depends(require_auth)):
"""Get detailed goal info with history."""
# Returns goal + progress history + prognosis
@router.put("/targets/{goal_id}/progress")
def update_goal_progress(goal_id: str, session=Depends(require_auth)):
"""Recalculate goal progress."""
# Auto-called after new measurements
# Updates current_value, progress_pct
@router.post("/targets/{goal_id}/reach")
def mark_goal_reached(goal_id: str, session=Depends(require_auth)):
"""Mark goal as reached."""
# Sets status='reached', reached_date=today
```
### Training Phases
```python
@router.get("/phases/current")
def get_current_phase(session=Depends(require_auth)):
"""Get active training phase."""
@router.get("/phases/detect")
def detect_phase(session=Depends(require_auth)):
"""Run phase detection algorithm."""
# Analyzes last 14 days
# Returns suggested phase + confidence + reasoning
@router.post("/phases/confirm")
def confirm_phase(phase_id: str, session=Depends(require_auth)):
"""Confirm detected phase."""
# Sets status='active'
```
### Fitness Tests
```python
@router.get("/tests/types")
def get_test_types():
"""Get all available fitness tests."""
@router.post("/tests/{test_type}/execute")
def record_test_result(
test_type: str,
result_value: float,
result_unit: str,
session=Depends(require_auth)
):
"""Record a fitness test result."""
# Inserts into fitness_tests
# Auto-calculates norm_category, percentile, improvement
@router.get("/tests/due")
def get_due_tests(session=Depends(require_auth)):
"""Get tests that are due for retesting."""
```
---
## 5. Neue KI-Platzhalter (kombiniert aus beiden Konzepten)
### Strategic (aus v2)
```python
{{goal_mode}} # "weight_loss"
{{goal_mode_label}} # "Gewichtsreduktion"
{{goal_mode_description}} # "Fettabbau bei Erhalt der Magermasse"
```
### Tactical - Primary Goal (aus v9e)
```python
{{primary_goal_type}} # "weight"
{{primary_goal_name}} # "Sommerfigur 2026"
{{primary_goal_target}} # "82 kg bis 30.06.2026"
{{primary_goal_current}} # "85.2 kg"
{{primary_goal_start}} # "86.1 kg"
{{primary_goal_progress_pct}} # "72%"
{{primary_goal_progress_text}} # "72% erreicht (4 kg von 5,5 kg)"
{{primary_goal_days_remaining}} # "45 Tage"
{{primary_goal_prognosis}} # "Ziel voraussichtlich in 6 Wochen erreicht (3 Wochen früher!)"
{{primary_goal_on_track}} # "true"
```
### Tactical - Secondary Goals (aus v9e)
```python
{{secondary_goals_count}} # "2"
{{secondary_goals_list}} # "16% Körperfett, VO2Max 55 ml/kg/min"
{{secondary_goal_1_type}} # "body_fat"
{{secondary_goal_1_progress}} # "45%"
```
### Training Phase (aus v9e)
```python
{{current_phase}} # "calorie_deficit"
{{current_phase_label}} # "Kaloriendefizit"
{{phase_since}} # "seit 14 Tagen"
{{phase_confidence}} # "0.92"
{{phase_recommendation}} # "Krafttraining erhalten, Cardio moderat, Proteinzufuhr 2g/kg"
{{phase_detected_automatically}} # "true"
```
### Fitness Tests (aus v9e)
```python
{{test_last_cooper}} # "2.800m (VO2Max ~52) vor 3 Wochen"
{{test_last_cooper_date}} # "2026-03-05"
{{test_last_cooper_result}} # "2800"
{{test_last_cooper_vo2max}} # "52.3"
{{test_last_cooper_category}} # "good"
{{test_due_list}} # "Sit & Reach (seit 5 Wochen), Liegestütze (seit 4 Wochen)"
{{test_next_recommended}} # "Cooper-Test (in 2 Wochen fällig)"
{{fitness_score_overall}} # "72/100"
{{fitness_score_endurance}} # "good"
{{fitness_score_strength}} # "average"
{{fitness_score_flexibility}} # "needs_improvement"
```
### GESAMT: 35+ neue Platzhalter aus v9e
Plus die 84 aus v2 = **120+ neue Platzhalter total**
---
## 6. Überarbeitete Implementierungs-Roadmap
### Phase 0a: Minimal Goal System (3-4h) ⭐ **JETZT**
**Strategic Layer:**
- DB: `goal_mode` in profiles
- Backend: GOAL_MODES aus v2
- API: GET/SET goal mode
- UI: Goal Mode Selector (5 Modi)
**Tactical Layer:**
- DB: `goals` table
- API: CRUD für goal targets
- UI: Goal Management Page (minimal)
- Liste aktiver Ziele
- Fortschrittsbalken
- "+ Neues Ziel" Button
**Aufwand:** 3-4h (erweitert wegen Tactical Layer)
---
### Phase 0b: Goal-Aware Placeholders (16-20h)
**Strategic Placeholders:**
```python
{{goal_mode}} # Aus profiles.goal_mode
{{goal_mode_label}} # Aus GOAL_MODES mapping
```
**Tactical Placeholders:**
```python
{{primary_goal_type}} # Aus goals WHERE is_primary=true
{{primary_goal_target}}
{{primary_goal_progress_pct}}
{{primary_goal_prognosis}} # Berechnet aus Trend
```
**Score Calculations (goal-aware):**
```python
def get_body_progress_score(profile_id: str) -> str:
profile = get_profile_data(profile_id)
goal_mode = profile.get('goal_mode', 'health')
# Get weights from v2 concept
weights = GOAL_MODES[goal_mode]['score_weights']
# Calculate sub-scores
fm_score = calculate_fm_progress(profile_id)
lbm_score = calculate_lbm_progress(profile_id)
# Weight according to goal mode
if goal_mode == 'weight_loss':
total = 0.50 * fm_score + 0.30 * weight_score + 0.20 * lbm_score
elif goal_mode == 'strength':
total = 0.60 * lbm_score + 0.30 * fm_score + 0.10 * weight_score
# ...
return f"{int(total)}/100"
```
---
### Phase 0c: Training Phases (4-6h) **PARALLEL**
**DB:**
- `training_phases` table
**Detection Algorithm:**
```python
def detect_current_phase(profile_id: str) -> dict:
"""Detects training phase from last 14 days of data."""
# Analyze data
kcal_balance = get_kcal_balance_14d(profile_id)
training_dist = get_training_distribution_14d(profile_id)
weight_trend = get_weight_trend_14d(profile_id)
hrv_avg = get_hrv_avg_14d(profile_id)
volume_change = get_volume_change_14d(profile_id)
# Phase Detection Rules
if kcal_balance < -300 and weight_trend < 0:
return {
'phase': 'calorie_deficit',
'confidence': 0.85,
'reason': f'Avg kcal balance {kcal_balance}/day, weight -0.5kg/week'
}
if training_dist['endurance'] > 60 and vo2max_trend > 0:
return {
'phase': 'conditioning',
'confidence': 0.78,
'reason': f'{training_dist["endurance"]}% cardio, VO2max improving'
}
if volume_change < -40 and hrv_avg < hrv_baseline * 0.85:
return {
'phase': 'regeneration',
'confidence': 0.92,
'reason': f'Volume -40%, HRV below baseline, recovery needed'
}
# Default
return {
'phase': 'maintenance',
'confidence': 0.50,
'reason': 'No clear pattern detected'
}
```
**API:**
- GET /phases/current
- GET /phases/detect
- POST /phases/confirm
**UI:**
- Dashboard Badge: "📊 Phase: Kaloriendefizit"
- Phase Detection Banner: "Wir haben erkannt: Kaloriendefizit-Phase. Stimmt das?"
---
### Phase 0d: Fitness Tests (4-6h) **SPÄTER**
**DB:**
- `fitness_tests` table
**Test Definitions:**
```python
FITNESS_TESTS = {
'cooper': {
'name': 'Cooper-Test',
'description': '12 Minuten laufen, maximale Distanz',
'unit': 'meters',
'interval_weeks': 6,
'norm_tables': { # Simplified
'male_30-39': {'very_good': 2800, 'good': 2500, 'average': 2200},
'female_30-39': {'very_good': 2500, 'good': 2200, 'average': 1900}
},
'calculate_vo2max': lambda distance: (distance - 504.9) / 44.73
},
'pushups': {
'name': 'Liegestütze-Test',
'description': 'Maximale Anzahl ohne Pause',
'unit': 'reps',
'interval_weeks': 4,
'norm_tables': { ... }
},
# ... weitere Tests
}
```
**UI:**
- Tests Page mit Testliste
- Test Execution Flow (Anleitung → Eingabe → Auswertung)
- Test History mit Trend-Chart
---
## 7. Priorisierte Reihenfolge
### SOFORT (3-4h)
**Phase 0a:** Minimal Goal System (Strategic + Tactical)
- Basis für alles andere
- User kann Ziele setzen
- Score-Berechnungen können goal_mode nutzen
### DIESE WOCHE (16-20h)
**Phase 0b:** Goal-Aware Placeholders
- 84 Platzhalter aus v2
- 35+ Platzhalter aus v9e
- **TOTAL: 120+ Platzhalter**
### PARALLEL (4-6h)
**Phase 0c:** Training Phases
- Automatische Erkennung
- Phase-aware Recommendations
### SPÄTER (4-6h)
**Phase 0d:** Fitness Tests
- Enhancement, nicht kritisch für Charts
---
## 8. Kritische Erkenntnisse
### 1. GOALS_VITALS.md ist detaillierter
- Konkrete Implementierungs-Specs
- DB-Schema-Vorschläge
- 13 definierte KI-Platzhalter
- **ABER:** Fehlt Score-Gewichtung (das hat v2)
### 2. Konzept v2 ist strategischer
- Goal Modes mit Score-Gewichtung
- Chart-Interpretationen
- Regelbasierte Logik
- **ABER:** Fehlt konkrete Ziel-Tracking (das hat v9e)
### 3. Beide zusammen = Vollständig
- v2 (Goal Modes) + v9e (Goal Targets) = Komplettes Zielesystem
- v2 (Scores) + v9e (Tests) = Vollständiges Assessment
- v2 (Charts) + v9e (Phases) = Kontext-aware Visualisierung
### 4. Meine ursprüngliche Analyse war incomplete
- Ich hatte nur v2 betrachtet
- v9e fügt kritische Details hinzu
- **Neue Gesamt-Schätzung:** 120+ Platzhalter (statt 84)
---
## 9. Aktualisierte Empfehlung
**JA zu Phase 0a (Minimal Goal System), ABER erweitert:**
### Was Phase 0a umfassen muss (3-4h):
1. **Strategic Layer (aus v2):**
- goal_mode in profiles
- GOAL_MODES Definition
- GET/SET endpoints
2. **Tactical Layer (aus v9e):**
- goals Tabelle
- CRUD für Ziele
- Fortschritts-Berechnung
3. **UI:**
- Goal Mode Selector (Settings)
- Goal Management Page (Basic)
- Dashboard Goal Widget
### Was kann warten:
- Training Phases → Phase 0c (parallel)
- Fitness Tests → Phase 0d (später)
- Vollständige Test-Integration → v9f
---
## 10. Nächste Schritte
**JETZT:**
1. Phase 0a implementieren (3-4h)
- Strategic + Tactical Goal System
2. Dann Phase 0b (Goal-Aware Placeholders, 16-20h)
3. Parallel Phase 0c (Training Phases, 4-6h)
**Soll ich mit Phase 0a (erweitert) starten?**
- Beide Goal-Konzepte integriert
- Ready für 120+ Platzhalter
- Basis für intelligentes Coach-System
**Commit:** ae93b9d (muss aktualisiert werden)
**Neue Analyse:** GOALS_SYSTEM_UNIFIED_ANALYSIS.md

View File

@ -0,0 +1,538 @@
# Zielesystem: Prioritäts-Analyse
**Datum:** 26. März 2026
**Frage:** Zielesystem vor oder nach Platzhaltern/Charts?
**Antwort:** **Minimales Zielesystem VOR Platzhaltern, volles System parallel**
---
## 1. Kritische Erkenntnis aus Fachkonzept
### Zitat Fachkonzept (Zeile 20-28):
> **Wichtig ist, dass das System zielabhängig interpretiert:**
> - Gewichtsreduktion
> - Muskel-/Kraftaufbau
> - Konditions-/Ausdaueraufbau
> - Körperrekomposition
> - allgemeine Gesundheit
>
> **Dasselbe Rohsignal kann je nach Ziel anders bewertet werden.**
> Ein Kaloriendefizit ist z. B. bei Gewichtsreduktion oft positiv,
> bei Kraftaufbau aber potenziell hinderlich.
### Konsequenz
❌ **Charts OHNE Zielesystem = falsche Interpretationen**
✅ **Charts MIT Zielesystem = korrekte, zielspezifische Aussagen**
---
## 2. Abhängigkeits-Matrix
### Was hängt vom Zielesystem ab?
| Komponente | Zielabhängig? | Beispiel |
|------------|---------------|----------|
| **Rohdaten-Charts** | ❌ Nein | Gewichtsverlauf, Umfänge-Trend |
| **Score-Gewichtung** | ✅ JA | Body Progress Score: 30% bei weight_loss, 20% bei strength |
| **Interpretationen** | ✅ JA | Kaloriendefizit: "gut" bei weight_loss, "kritisch" bei strength |
| **Hinweise** | ✅ JA | "Gewicht stagniert" → bei weight_loss: Warnung, bei strength: egal |
| **Platzhalter (Berechnungen)** | ⚠️ TEILWEISE | Trends: Nein, Scores: JA |
| **KI-Prompts** | ✅ JA | Analyse-Kontext ändert sich komplett |
### Fachkonzept: Score-Gewichtung (Zeile 185-216)
```yaml
score_weights:
weight_loss:
body_progress: 0.30 # Körper wichtig
nutrition: 0.25
activity: 0.20
recovery: 0.15
health_risk: 0.10
strength:
body_progress: 0.20
nutrition: 0.25
activity: 0.30 # Training wichtiger
recovery: 0.20
health_risk: 0.05 # Weniger kritisch
endurance:
body_progress: 0.10 # Körper unwichtiger
activity: 0.35 # Training am wichtigsten
recovery: 0.25 # Recovery sehr wichtig
```
### Beispiel: Body Progress Score
**OHNE Zielesystem:**
```python
def calculate_body_progress_score():
# Generisch, für niemanden wirklich passend
fm_delta_score = calculate_fm_change() # -5kg
lbm_delta_score = calculate_lbm_change() # -2kg
return (fm_delta_score + lbm_delta_score) / 2
# Score: 50/100 (FM gut runter, aber LBM auch runter)
```
**MIT Zielesystem:**
```python
def calculate_body_progress_score(goal_mode):
fm_delta_score = calculate_fm_change() # -5kg
lbm_delta_score = calculate_lbm_change() # -2kg
if goal_mode == "weight_loss":
# FM runter: sehr gut, LBM runter: tolerierbar wenn nicht zu viel
return 0.70 * fm_delta_score + 0.30 * lbm_delta_score
# Score: 78/100 (FM wichtiger, LBM-Verlust weniger kritisch)
elif goal_mode == "strength":
# FM runter: ok, LBM runter: SEHR SCHLECHT
return 0.30 * fm_delta_score + 0.70 * lbm_delta_score
# Score: 32/100 (LBM-Verlust ist Hauptproblem!)
elif goal_mode == "recomposition":
# FM runter: gut, LBM runter: schlecht
return 0.50 * fm_delta_score + 0.50 * lbm_delta_score
# Score: 50/100 (ausgewogen bewertet)
```
**Ergebnis:**
- Gleiche Daten (-5kg FM, -2kg LBM)
- ABER: 78/100 bei weight_loss, 32/100 bei strength
- **Ohne Ziel: völlig falsche Bewertung!**
---
## 3. Ziel-Erkennung aus Daten
### Fachkonzept erwähnt dies NICHT explizit, aber logisch ableitbar:
**Pattern-Erkennung:**
```python
def suggest_goal_from_data(profile_id):
"""Schlägt Ziel basierend auf Daten-Mustern vor."""
# Analyse der letzten 28 Tage
training_types = get_training_distribution_28d(profile_id)
nutrition = get_nutrition_pattern_28d(profile_id)
body_changes = get_body_changes_28d(profile_id)
# Pattern 1: Viel Kraft + viel Protein + LBM steigt
if (training_types['strength'] > 60% and
nutrition['protein_g_per_kg'] > 1.8 and
body_changes['lbm_trend'] > 0):
return {
'suggested_goal': 'strength',
'confidence': 'high',
'reasoning': 'Krafttraining dominant + hohe Proteinzufuhr + Muskelaufbau erkennbar'
}
# Pattern 2: Viel Cardio + Kaloriendefizit + Gewicht sinkt
if (training_types['endurance'] > 50% and
nutrition['kcal_balance_avg'] < -300 and
body_changes['weight_trend'] < 0):
return {
'suggested_goal': 'weight_loss',
'confidence': 'high',
'reasoning': 'Ausdauertraining + Kaloriendefizit + Gewichtsverlust'
}
# Pattern 3: Mixed Training + Protein hoch + Gewicht stabil + Rekomposition
if (training_types['mixed'] == True and
nutrition['protein_g_per_kg'] > 1.6 and
abs(body_changes['weight_trend']) < 0.05 and
body_changes['fm_trend'] < 0 and
body_changes['lbm_trend'] > 0):
return {
'suggested_goal': 'recomposition',
'confidence': 'medium',
'reasoning': 'Gemischtes Training + Rekomposition sichtbar (FM↓, LBM↑)'
}
# Default: Nicht genug Muster erkennbar
return {
'suggested_goal': 'health',
'confidence': 'low',
'reasoning': 'Keine klaren Muster erkennbar, gesundheitsorientiertes Training angenommen'
}
```
### Voraussetzungen für Ziel-Erkennung:
1. ✅ Mindestens 21-28 Tage Daten
2. ✅ Training-Type Distribution
3. ✅ Ernährungs-Pattern
4. ✅ Körper-Trends (FM, LBM, Gewicht)
5. ✅ Berechnet → **braucht Platzhalter!**
**ABER:** Ziel-Erkennung ist **nachgelagert**, nicht Voraussetzung.
---
## 4. Empfohlene Implementierungs-Strategie
### Hybrid-Ansatz: Minimal-Ziele SOFORT, Voll-System parallel
## Phase 0a: Minimal-Zielesystem (2-3h) ⭐ **START HIER**
### Ziel
User kann manuell Ziel setzen, System nutzt es für Berechnungen.
### Implementierung
**1. DB-Schema erweitern:**
```sql
-- Migration 023
ALTER TABLE profiles ADD COLUMN goal_mode VARCHAR(50) DEFAULT 'health';
ALTER TABLE profiles ADD COLUMN goal_weight DECIMAL(5,2);
ALTER TABLE profiles ADD COLUMN goal_bf_pct DECIMAL(4,1);
ALTER TABLE profiles ADD COLUMN goal_set_date DATE;
ALTER TABLE profiles ADD COLUMN goal_target_date DATE;
COMMENT ON COLUMN profiles.goal_mode IS
'Primary goal: weight_loss, strength, endurance, recomposition, health';
```
**2. Goal-Mode Konstanten:**
```python
# backend/goals.py (NEU)
GOAL_MODES = {
'weight_loss': {
'label': 'Gewichtsreduktion',
'description': 'Fettabbau bei Erhalt der Magermasse',
'score_weights': {
'body_progress': 0.30,
'nutrition': 0.25,
'activity': 0.20,
'recovery': 0.15,
'health_risk': 0.10
},
'focus_areas': ['fettmasse', 'gewichtstrend', 'kalorienbilanz', 'protein_sicherung']
},
'strength': {
'label': 'Kraftaufbau',
'description': 'Muskelaufbau und Kraftsteigerung',
'score_weights': {
'body_progress': 0.20,
'nutrition': 0.25,
'activity': 0.30,
'recovery': 0.20,
'health_risk': 0.05
},
'focus_areas': ['trainingsqualitaet', 'protein', 'lbm', 'recovery']
},
'endurance': {
'label': 'Ausdaueraufbau',
'description': 'Kondition und VO2max verbessern',
'score_weights': {
'body_progress': 0.10,
'nutrition': 0.20,
'activity': 0.35,
'recovery': 0.25,
'health_risk': 0.10
},
'focus_areas': ['trainingsvolumen', 'intensitaetsverteilung', 'vo2max', 'recovery']
},
'recomposition': {
'label': 'Körperrekomposition',
'description': 'Fettabbau bei gleichzeitigem Muskelaufbau',
'score_weights': {
'body_progress': 0.30,
'nutrition': 0.25,
'activity': 0.25,
'recovery': 0.15,
'health_risk': 0.05
},
'focus_areas': ['lbm', 'fettmasse', 'protein', 'trainingsqualitaet']
},
'health': {
'label': 'Allgemeine Gesundheit',
'description': 'Ausgeglichenes Gesundheits- und Fitnesstraining',
'score_weights': {
'body_progress': 0.20,
'nutrition': 0.20,
'activity': 0.20,
'recovery': 0.20,
'health_risk': 0.20
},
'focus_areas': ['bewegung', 'blutdruck', 'schlaf', 'gewicht', 'regelmaessigkeit']
}
}
```
**3. API-Endpoint:**
```python
# routers/goals.py (NEU)
from fastapi import APIRouter, Depends
from auth import require_auth
from goals import GOAL_MODES
router = APIRouter(prefix="/api/goals", tags=["goals"])
@router.get("/modes")
def get_goal_modes():
"""Return all available goal modes with descriptions."""
return GOAL_MODES
@router.get("/current")
def get_current_goal(session: dict = Depends(require_auth)):
"""Get user's current goal settings."""
profile_id = session['profile_id']
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"""SELECT goal_mode, goal_weight, goal_bf_pct,
goal_set_date, goal_target_date
FROM profiles WHERE id=%s""",
(profile_id,)
)
row = r2d(cur.fetchone())
return {
**row,
'mode_config': GOAL_MODES.get(row['goal_mode'], GOAL_MODES['health'])
}
@router.post("/set")
def set_goal(
goal_mode: str,
goal_weight: Optional[float] = None,
goal_bf_pct: Optional[float] = None,
target_date: Optional[str] = None,
session: dict = Depends(require_auth)
):
"""Set user's goal."""
if goal_mode not in GOAL_MODES:
raise HTTPException(400, f"Invalid goal_mode. Must be one of: {list(GOAL_MODES.keys())}")
profile_id = session['profile_id']
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"""UPDATE profiles
SET goal_mode=%s, goal_weight=%s, goal_bf_pct=%s,
goal_set_date=CURRENT_DATE, goal_target_date=%s
WHERE id=%s""",
(goal_mode, goal_weight, goal_bf_pct, target_date, profile_id)
)
conn.commit()
return {"success": True, "goal_mode": goal_mode}
```
**4. Frontend UI (Settings.jsx):**
```jsx
// Minimal Goal Selector
function GoalSettings() {
const [goalModes, setGoalModes] = useState({})
const [currentGoal, setCurrentGoal] = useState(null)
const [selectedMode, setSelectedMode] = useState('health')
useEffect(() => {
loadGoalModes()
loadCurrentGoal()
}, [])
const loadGoalModes = async () => {
const modes = await api.getGoalModes()
setGoalModes(modes)
}
const loadCurrentGoal = async () => {
const goal = await api.getCurrentGoal()
setCurrentGoal(goal)
setSelectedMode(goal.goal_mode || 'health')
}
const saveGoal = async () => {
await api.setGoal({
goal_mode: selectedMode,
goal_weight: goalWeight,
goal_bf_pct: goalBfPct,
target_date: targetDate
})
loadCurrentGoal()
}
return (
<div className="card">
<h2>🎯 Trainingsziel</h2>
<div className="form-row">
<label>Hauptziel</label>
<select value={selectedMode} onChange={e => setSelectedMode(e.target.value)}>
{Object.entries(goalModes).map(([key, config]) => (
<option key={key} value={key}>
{config.label}
</option>
))}
</select>
<p style={{fontSize: 12, color: 'var(--text3)'}}>
{goalModes[selectedMode]?.description}
</p>
</div>
{(selectedMode === 'weight_loss' || selectedMode === 'recomposition') && (
<div className="form-row">
<label>Zielgewicht (optional)</label>
<input type="number" step="0.1" value={goalWeight} onChange={...} />
</div>
)}
<button onClick={saveGoal}>Ziel speichern</button>
</div>
)
}
```
### Aufwand: 2-3h
- 1h: DB + Backend
- 1h: Frontend UI
- 0.5h: Testing
---
## Phase 0b: Goal-Aware Platzhalter (16-20h)
**Alle 84 Platzhalter implementieren, ABER:**
- Score-Berechnungen nutzen `goal_mode` von Anfang an
- Beispiel:
```python
def get_body_progress_score(profile_id: str) -> str:
"""Body Progress Score (0-100, goal-dependent)."""
profile = get_profile_data(profile_id)
goal_mode = profile.get('goal_mode', 'health')
# Hole Gewichte aus goals.GOAL_MODES
weights = GOAL_MODES[goal_mode]['score_weights']
# Berechne Sub-Scores
fm_score = calculate_fm_progress(profile_id)
lbm_score = calculate_lbm_progress(profile_id)
weight_score = calculate_weight_progress(profile_id, goal_mode)
# Gewichte nach Ziel
if goal_mode == 'weight_loss':
total = (0.50 * fm_score + 0.30 * weight_score + 0.20 * lbm_score)
elif goal_mode == 'strength':
total = (0.60 * lbm_score + 0.30 * fm_score + 0.10 * weight_score)
elif goal_mode == 'recomposition':
total = (0.45 * fm_score + 0.45 * lbm_score + 0.10 * weight_score)
else: # health, endurance
total = (0.40 * weight_score + 0.30 * fm_score + 0.30 * lbm_score)
return f"{int(total)}/100"
```
**Resultat:**
- Charts bekommen von Anfang an **korrekte** Scores
- Keine Umarbeitung nötig später
- System ist "smart" ab Tag 1
---
## Phase 2+: Vollständiges Zielesystem (6-8h)
**Features:**
1. **Ziel-Erkennung aus Daten**
- Pattern-Analyse (wie oben)
- Vorschlag mit Confidence
- "Passt dein Ziel noch?" Check
2. **Sekundäre Ziele**
- `goal_mode` = primary
- `secondary_goals[]` = weitere Schwerpunkte
- Gewichtung: 70% primary, 30% secondary
3. **Ziel-Progression Tracking**
- Fortschritt zum Ziel (%)
- Geschätzte Erreichung (Datum)
- Anpassungs-Vorschläge
4. **Goal-Aware Charts**
- Priorisierung nach goal_relevance
- Dashboard zeigt ziel-spezifische Charts zuerst
5. **Goal-Aware KI**
- Prompt-Kontext enthält goal_mode
- KI interpretiert zielspezifisch
---
## 5. Entscheidungs-Matrix
### Option A: Zielesystem komplett ZUERST
**Aufwand:** 10-12h
**Pro:**
- Alles konsistent von Anfang an
- Keine Umarbeitung
**Contra:**
- Verzögert Platzhalter-Start
- Ziel-Erkennung braucht Platzhalter (Henne-Ei)
### Option B: Platzhalter ZUERST, dann Ziele
**Aufwand:** 16-20h + später Rework
**Pro:**
- Schneller Start
**Contra:**
- ALLE Scores falsch gewichtet
- Komplette Umarbeitung nötig
- User sehen falsche Werte
### Option C: HYBRID ⭐ **EMPFOHLEN**
**Aufwand:** 2-3h (Minimal-Ziele) + 16-20h (Goal-Aware Platzhalter) + später 6-8h (Voll-System)
**Pro:**
- ✅ Beste aus beiden Welten
- ✅ Korrekte Scores von Anfang an
- ✅ Keine Umarbeitung
- ✅ Ziel-Erkennung später als Enhancement
**Contra:**
- Keinen signifikanten Nachteil
---
## 6. Empfehlung
### JA, Zielesystem VOR Platzhaltern aber minimal!
**Reihenfolge:**
1. **Phase 0a (2-3h):** Minimal-Zielesystem
- DB: goal_mode field
- API: Get/Set Goal
- UI: Goal Selector (Settings)
- Default: "health"
2. **Phase 0b (16-20h):** Goal-Aware Platzhalter
- 84 Platzhalter implementieren
- Scores nutzen goal_mode
- Berechnungen goal-abhängig
3. **Phase 1 (12-16h):** Charts
- Nutzen goal-aware Platzhalter
- Zeigen korrekte Interpretationen
4. **Phase 2+ (6-8h):** Vollständiges Zielesystem
- Ziel-Erkennung
- Sekundäre Ziele
- Goal Progression Tracking
---
## 7. Fazit
**Deine Intuition war 100% richtig!**
✅ **Ohne Zielesystem:**
- Charts zeigen falsche Interpretationen
- Scores sind generisch und für niemanden passend
- System bleibt "dummer Datensammler"
✅ **Mit Zielesystem:**
- Charts interpretieren zielspezifisch
- Scores sind individuell gewichtet
- System wird "intelligenter Coach"
**Nächster Schritt:** Phase 0a implementieren (2-3h), dann Phase 0b mit goal-aware Platzhaltern.
**Soll ich mit Phase 0a (Minimal-Zielesystem) starten?**

View File

@ -0,0 +1,729 @@
# Goal System Redesign v2.0
**Datum:** 26. März 2026
**Status:** 📋 KONZEPTION
**Anlass:** Fundamentale Design-Probleme in Phase 0a identifiziert
---
## 1. Probleme der aktuellen Implementierung (Phase 0a)
### 1.1 Primärziel zu simplistisch
**Problem:**
- Nur EIN Primärziel erlaubt
- Binäres System (primär/nicht-primär)
- Toggle funktioniert nicht richtig beim Update
**Realität:**
- User hat MEHRERE Ziele gleichzeitig mit unterschiedlichen Prioritäten
- Beispiel: 30% Abnehmen, 25% Kraft, 25% Ausdauer, 20% Beweglichkeit
**Lösung:**
**Gewichtungssystem** (0-100%, Summe = 100%)
---
### 1.2 Ein Goal Mode zu simpel
**Problem:**
- User muss sich für EINEN Modus entscheiden (weight_loss ODER strength)
- In Realität: Kombinierte Ziele (Abnehmen + Kraft + Ausdauer gleichzeitig)
**Realität (User-Zitat):**
> "Ich versuche nach einer Operation Kraft und Ausdauer aufzubauen, gleichzeitig Abzunehmen und meine Beweglichkeit und Koordination wieder zu steigern."
**Lösung:**
**Multi-Mode mit Gewichtung** statt Single-Mode
---
### 1.3 Fehlende Current Values
**Problem:**
- `lean_mass` current value = "-" (nicht implementiert)
- `strength`, `flexibility` haben keine Datenquellen
- VO2Max wirft Internal Server Error
**Lösung:**
→ Alle Goal-Typen mit korrekten Datenquellen verbinden
---
### 1.4 Abstrakte Zieltypen
**Problem:**
- "Kraft" - was bedeutet das? Bankdrücken? Kniebeuge? Gesamt?
- "Beweglichkeit" - welcher Test? Sit-and-Reach? Hüftbeugung?
- Zu unspezifisch für konkrete Messung
**Lösung:**
**Konkrete, messbare Zieltypen** mit standardisierten Tests
---
### 1.5 Blutdruck als einzelner Wert
**Problem:**
- BP braucht ZWEI Werte (systolisch/diastolisch)
- Aktuelles Schema: nur ein `target_value`
**Lösung:**
**Compound Goals** (Ziele mit mehreren Werten)
---
### 1.6 Keine Guidance für User
**Problem:**
- User muss konkrete Zahlen eingeben ohne Kontext
- Was ist ein guter VO2Max Wert? Was ist realistisch?
**Lösung:**
**Richtwerte, Normen, Beispiele** in UI
---
## 2. Redesign-Konzept v2.0
### 2.1 Kern-Prinzipien
**Prinzip 1: Gewichtung statt Priorisierung**
- Alle Ziele haben eine Gewichtung (0-100%)
- Summe aller Gewichtungen = 100%
- KI berücksichtigt Gewichtung in Analysen
**Prinzip 2: Multi-dimensional statt Singular**
- Kein einzelner "Goal Mode"
- Stattdessen: Gewichtete Kombination von Fokus-Bereichen
- Realitätsnah: User hat mehrere Ziele gleichzeitig
**Prinzip 3: Konkret statt Abstrakt**
- Jedes Ziel hat klare Messbarkeit
- Standardisierte Tests wo möglich
- Datenquellen eindeutig definiert
**Prinzip 4: Guidance statt Ratlosigkeit**
- Richtwerte für jedes Ziel
- Alters-/Geschlechts-spezifische Normen
- Beispiele und Erklärungen
---
## 3. Neues Datenmodell
### 3.1 Fokus-Bereiche (statt Goal Modes)
**Tabelle: `focus_areas` (NEU)**
```sql
CREATE TABLE focus_areas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
-- Gewichtete Fokus-Bereiche
weight_loss_pct INT DEFAULT 0, -- 0-100%
muscle_gain_pct INT DEFAULT 0, -- 0-100%
endurance_pct INT DEFAULT 0, -- 0-100%
strength_pct INT DEFAULT 0, -- 0-100%
flexibility_pct INT DEFAULT 0, -- 0-100%
health_pct INT DEFAULT 0, -- 0-100% (Erhaltung, kein spezifisches Ziel)
-- Constraint: Summe muss 100 sein
CONSTRAINT sum_equals_100 CHECK (
weight_loss_pct + muscle_gain_pct + endurance_pct +
strength_pct + flexibility_pct + health_pct = 100
),
active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
-- Nur ein aktiver Fokus-Mix pro User
UNIQUE(profile_id, active) WHERE active = true
);
COMMENT ON TABLE focus_areas IS
'Weighted focus distribution - replaces single goal_mode.
Example: 30% weight loss + 25% strength + 25% endurance + 20% flexibility = 100%';
```
**Beispiel-Daten:**
```json
// User nach Operation (wie im Feedback beschrieben):
{
"weight_loss_pct": 30,
"muscle_gain_pct": 20,
"endurance_pct": 25,
"strength_pct": 15,
"flexibility_pct": 10,
"health_pct": 0
}
// User reiner Kraftfokus:
{
"weight_loss_pct": 0,
"muscle_gain_pct": 50,
"strength_pct": 40,
"endurance_pct": 10,
"flexibility_pct": 0,
"health_pct": 0
}
// User Gewichtsverlust primär:
{
"weight_loss_pct": 60,
"muscle_gain_pct": 0,
"endurance_pct": 20,
"strength_pct": 10,
"flexibility_pct": 5,
"health_pct": 5
}
```
---
### 3.2 Überarbeitete Goal-Typen
**Tabelle: `goals` (ÜBERARBEITET)**
**A) Simple Goals (ein Wert):**
```sql
goal_type:
- 'weight' → kg (aus weight_log)
- 'body_fat_pct' → % (aus caliper_log)
- 'lean_mass' → kg (berechnet: weight - (weight * bf_pct))
- 'vo2max' → ml/kg/min (aus vitals_baseline)
- 'rhr' → bpm (aus vitals_baseline)
- 'hrv' → ms (aus vitals_baseline)
```
**B) Test-based Goals (standardisierte Tests):**
```sql
goal_type:
- 'cooper_test' → Meter (12min Lauf)
- 'pushups_max' → Anzahl
- 'plank_max' → Sekunden
- 'sit_reach' → cm (Beweglichkeit)
- 'squat_1rm' → kg (Kraft Unterkörper)
- 'bench_1rm' → kg (Kraft Oberkörper)
- 'deadlift_1rm' → kg (Kraft Rücken)
```
**C) Compound Goals (mehrere Werte):**
```sql
goal_type:
- 'blood_pressure' → systolic/diastolic (mmHg)
→ Braucht: target_value_secondary
```
**Schema-Erweiterung:**
```sql
ALTER TABLE goals ADD COLUMN goal_weight INT DEFAULT 100;
-- Gewichtung dieses Ziels (0-100%)
-- Summe aller goal_weight für einen User sollte ~100% sein
ALTER TABLE goals ADD COLUMN target_value_secondary DECIMAL(10,2);
-- Für Compound Goals (z.B. BP diastolisch)
ALTER TABLE goals ADD COLUMN current_value_secondary DECIMAL(10,2);
-- Aktueller Wert für sekundären Target
ALTER TABLE goals DROP COLUMN is_primary;
-- Nicht mehr nötig (wird durch goal_weight ersetzt)
COMMENT ON COLUMN goals.goal_weight IS
'Weight/priority of this goal (0-100%).
Higher weight = more important in AI scoring.
Sum of all goal_weight should be ~100% per user.';
```
---
### 3.3 Datenquellen-Mapping
**Korrekte Current-Value Extraktion:**
```python
# backend/routers/goals.py - _get_current_value_for_goal_type()
GOAL_TYPE_SOURCES = {
# Simple values from existing tables
'weight': {
'table': 'weight_log',
'column': 'weight',
'order': 'date DESC'
},
'body_fat_pct': {
'table': 'caliper_log',
'column': 'body_fat_pct',
'order': 'date DESC'
},
'lean_mass': {
'calculation': 'weight - (weight * body_fat_pct / 100)',
'requires': ['weight_log', 'caliper_log']
},
'vo2max': {
'table': 'vitals_baseline',
'column': 'vo2_max',
'order': 'date DESC'
},
'rhr': {
'table': 'vitals_baseline',
'column': 'resting_hr',
'order': 'date DESC'
},
'hrv': {
'table': 'vitals_baseline',
'column': 'hrv',
'order': 'date DESC'
},
# Test-based values from fitness_tests
'cooper_test': {
'table': 'fitness_tests',
'filter': "test_type = 'cooper_12min'",
'column': 'result_value',
'order': 'test_date DESC'
},
'pushups_max': {
'table': 'fitness_tests',
'filter': "test_type = 'pushups_max'",
'column': 'result_value',
'order': 'test_date DESC'
},
# ... weitere Tests
# Compound goals
'blood_pressure': {
'table': 'blood_pressure_log',
'columns': ['systolic', 'diastolic'], # Beide Werte
'order': 'measured_at DESC'
}
}
```
---
## 4. UI/UX Redesign
### 4.1 Fokus-Bereiche Konfigurator
**Statt 5 einzelne Cards → Slider-Interface:**
```
┌─────────────────────────────────────────────────────┐
│ 🎯 Mein Trainings-Fokus │
├─────────────────────────────────────────────────────┤
│ Verschiebe die Regler um deine Prioritäten zu │
│ setzen. Die Summe muss 100% ergeben. │
│ │
│ 📉 Gewichtsverlust [====] 30% │
│ Schwerpunkt auf Kaloriendefizit & Fettabbau │
│ │
│ 💪 Muskelaufbau [===] 20% │
│ Magermasse steigern, Körperkomposition │
│ │
│ 🏃 Ausdauer [====] 25% │
│ VO2Max, aerobe Kapazität, Pace │
│ │
│ 🏋️ Maximalkraft [==] 15% │
│ 1RM Steigerung, progressive Belastung │
│ │
│ 🤸 Beweglichkeit [=] 10% │
│ Mobilität, Flexibilität, Koordination │
│ │
│ ❤️ Allgemeine Gesundheit [ ] 0% │
│ Erhaltung, präventiv │
│ │
│ ────────────────────────────────────────────────── │
│ Gesamt: 100% ✓ │
│ │
│ [Speichern] [Zurücksetzen] │
└─────────────────────────────────────────────────────┘
```
**Technisch:**
- HTML Range Slider (0-100)
- Live-Update der Summe
- Validierung: Summe muss 100% sein
- Auto-Adjust: Wenn User einen Slider erhöht, andere proportional reduzieren
---
### 4.2 Ziele mit Gewichtung
**Goal-List mit Gewichtungs-Indikator:**
```
┌─────────────────────────────────────────────────────┐
│ 🎯 Konkrete Ziele │
├─────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────┐ │
│ │ ⚖️ Zielgewicht: 82 kg [30%]│ │
│ │ Start: 95 kg → Aktuell: 89 kg → Ziel: 82 kg │ │
│ │ ████████████░░░░░░░░░░ 65% │ │
│ │ ✓ Voraussichtlich: 15.05.2026 (on track) │ │
│ │ [✏️] [🗑️] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ 💪 Magermasse: 72 kg [20%]│ │
│ │ Start: 68 kg → Aktuell: 70.5 kg → Ziel: 72 kg│ │
│ │ ██████████░░░░░░░░░░░░ 63% │ │
│ │ ⚠ Prognose: 20.06.2026 (5 Tage später) │ │
│ │ [✏️] [🗑️] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ [+ Neues Ziel] │
└─────────────────────────────────────────────────────┘
Summe Gewichtungen: 50% (noch 50% verfügbar)
```
**Änderungen:**
- Gewichtung in `[30%]` Badge angezeigt
- Summe unten angezeigt
- Warnung wenn Summe > 100%
---
### 4.3 Ziel-Editor mit Guidance
**Beispiel: VO2Max Ziel erstellen:**
```
┌─────────────────────────────────────────────────────┐
│ Neues Ziel erstellen │
├─────────────────────────────────────────────────────┤
│ Zieltyp │
│ [VO2 Max ▼] │
│ │
VO2 Max (ml/kg/min) - Maximale Sauerstoffauf- │
│ nahme. Misst die aerobe Leistungsfähigkeit. │
│ │
│ 📊 Richtwerte (Männer, 35 Jahre): │
│ Sehr gut: > 48 ml/kg/min │
│ Gut: 44-48 ml/kg/min │
│ Durchschn.: 40-44 ml/kg/min │
│ Unterdurch.: 35-40 ml/kg/min │
│ │
│ 🎯 Zielwert │
│ ┌──────────┬──────────┐ │
│ │ [ 46 ] │ ml/kg/min│ │
│ └──────────┴──────────┘ │
│ Dein aktueller Wert: 42 ml/kg/min (Durchschnitt) │
│ → Ziel liegt in "Gut"-Bereich ✓ │
│ │
│ 📅 Zieldatum (optional) │
│ [2026-06-30] │
│ │
│ ⚖️ Gewichtung │
│ [==== ] 25% │
│ Wie wichtig ist dir dieses Ziel? │
│ │
│ 💡 Name (optional) │
│ [Ausdauer für Bergwandern ] │
│ │
│ [Ziel erstellen] [Abbrechen] │
└─────────────────────────────────────────────────────┘
```
**Features:**
- Info-Box mit Erklärung
- Alters-/geschlechtsspezifische Richtwerte
- Live-Feedback zum eingegebenen Wert
- Aktueller Wert automatisch geladen
- Gewichtungs-Slider mit Live-Preview
---
### 4.4 Compound Goals (Blutdruck)
**Spezial-UI für Blutdruck:**
```
┌─────────────────────────────────────────────────────┐
│ Zieltyp: Blutdruck │
├─────────────────────────────────────────────────────┤
│ 🎯 Zielwerte │
│ │
│ Systolisch (oberer Wert) │
│ [ 120 ] mmHg │
│ │
│ Diastolisch (unterer Wert) │
│ [ 80 ] mmHg │
│ │
WHO/ISH Klassifikation: │
│ Optimal: < 120/80 mmHg
│ Normal: 120-129 / 80-84 mmHg │
│ Hoch-norm.: 130-139 / 85-89 mmHg │
│ Hypertonie: ≥ 140/90 mmHg │
│ │
│ Dein aktueller Wert: 135/88 mmHg (Hoch-normal) │
│ Dein Ziel: 120/80 mmHg (Optimal) ✓ │
│ │
│ [Ziel erstellen] [Abbrechen] │
└─────────────────────────────────────────────────────┘
```
---
## 5. Scoring-System mit Gewichtung
### 5.1 Score-Berechnung v2.0
**Aktuell (Phase 0a):**
```python
# Feste Gewichtung per goal_mode
SCORE_WEIGHTS = {
"strength": {
"body_progress": 0.35,
"nutrition": 0.30,
# ...
}
}
```
**Neu (v2.0):**
```python
def calculate_weighted_score(profile_id):
"""
Berechnet Score basierend auf:
1. Focus Areas (Multi-dimensional statt single mode)
2. Goal Weights (individuelle Ziel-Gewichtungen)
"""
# 1. Hole Focus Areas
focus = get_focus_areas(profile_id)
# → {weight_loss: 30%, muscle_gain: 20%, endurance: 25%, ...}
# 2. Hole alle Ziele mit Gewichtung
goals = get_goals_with_weights(profile_id)
# → [{type: 'weight', weight: 30%}, {type: 'lean_mass', weight: 20%}, ...]
# 3. Berechne Basis-Scores
base_scores = {
'body_composition': calculate_body_score(profile_id),
'nutrition': calculate_nutrition_score(profile_id),
'training': calculate_training_score(profile_id),
'recovery': calculate_recovery_score(profile_id)
}
# 4. Gewichte Scores nach Focus Areas
weighted_score = 0
# Weight Loss Focus → Body Composition + Nutrition wichtiger
if focus['weight_loss_pct'] > 0:
weighted_score += (
base_scores['body_composition'] * 0.4 +
base_scores['nutrition'] * 0.4 +
base_scores['training'] * 0.1 +
base_scores['recovery'] * 0.1
) * (focus['weight_loss_pct'] / 100)
# Muscle Gain Focus → Body + Nutrition + Training
if focus['muscle_gain_pct'] > 0:
weighted_score += (
base_scores['body_composition'] * 0.35 +
base_scores['nutrition'] * 0.35 +
base_scores['training'] * 0.25 +
base_scores['recovery'] * 0.05
) * (focus['muscle_gain_pct'] / 100)
# Endurance Focus → Training + Recovery
if focus['endurance_pct'] > 0:
weighted_score += (
base_scores['training'] * 0.50 +
base_scores['recovery'] * 0.30 +
base_scores['body_composition'] * 0.10 +
base_scores['nutrition'] * 0.10
) * (focus['endurance_pct'] / 100)
# ... weitere Focus Areas
return {
'overall_score': round(weighted_score, 1),
'base_scores': base_scores,
'focus_weights': focus,
'goal_weights': [g['weight'] for g in goals]
}
```
**Beispiel:**
```python
User: 30% Weight Loss + 25% Endurance + 20% Muscle Gain + 25% Strength
Base Scores:
- Body Composition: 75/100
- Nutrition: 80/100
- Training: 70/100
- Recovery: 65/100
Calculation:
Weight Loss (30%):
= (75*0.4 + 80*0.4 + 70*0.1 + 65*0.1) * 0.30
= 69.5 * 0.30 = 20.85
Endurance (25%):
= (70*0.50 + 65*0.30 + 75*0.10 + 80*0.10) * 0.25
= 69.0 * 0.25 = 17.25
Muscle Gain (20%):
= (75*0.35 + 80*0.35 + 70*0.25 + 65*0.05) * 0.20
= 74.0 * 0.20 = 14.80
Strength (25%):
= (70*0.40 + 80*0.30 + 75*0.20 + 65*0.10) * 0.25
= 72.5 * 0.25 = 18.13
Overall Score = 20.85 + 17.25 + 14.80 + 18.13 = 71.03/100
```
---
## 6. Migration-Strategie
### 6.1 Daten-Migration von Phase 0a
**Bestehende Daten:**
- `profiles.goal_mode` (single mode)
- `goals` mit `is_primary`
**Migrations-Logik:**
```sql
-- Migration 023: Goal System Redesign v2.0
-- 1. Erstelle focus_areas Tabelle
CREATE TABLE focus_areas (...);
-- 2. Migriere bestehende goal_mode → focus_areas
INSERT INTO focus_areas (profile_id, weight_loss_pct, muscle_gain_pct, ...)
SELECT
id,
CASE goal_mode
WHEN 'weight_loss' THEN 70 -- 70% Weight Loss + 15% Health + 15% Endurance
WHEN 'strength' THEN 0
-- ...
END as weight_loss_pct,
CASE goal_mode
WHEN 'strength' THEN 60
WHEN 'recomposition' THEN 30
-- ...
END as muscle_gain_pct,
-- ... weitere
FROM profiles
WHERE goal_mode IS NOT NULL;
-- 3. Erweitere goals Tabelle
ALTER TABLE goals ADD COLUMN goal_weight INT DEFAULT 100;
ALTER TABLE goals ADD COLUMN target_value_secondary DECIMAL(10,2);
ALTER TABLE goals ADD COLUMN current_value_secondary DECIMAL(10,2);
-- 4. Migriere is_primary → goal_weight
UPDATE goals SET goal_weight = 100 WHERE is_primary = true;
UPDATE goals SET goal_weight = 50 WHERE is_primary = false;
-- 5. Cleanup (später)
-- ALTER TABLE profiles DROP COLUMN goal_mode; -- nach Verifikation
-- ALTER TABLE goals DROP COLUMN is_primary; -- nach Verifikation
```
---
## 7. Implementierungs-Phasen
### Phase 1: Konzeption ✅ (DIESES DOKUMENT)
**Dauer:** -
**Ziel:** Vollständiges Redesign-Konzept
### Phase 2: Backend Redesign (6-8h)
- Migration 023 erstellen
- `focus_areas` Tabelle + CRUD
- `goals` erweitern (weight, secondary values)
- Datenquellen-Mapping korrigieren (lean_mass, VO2Max fix, etc.)
- Scoring-System v2.0 implementieren
### Phase 3: Frontend Redesign (8-10h)
- Fokus-Bereiche Slider-UI
- Ziel-Editor mit Guidance (Richtwerte, Normen)
- Gewichtungs-System in Goal-Liste
- Compound Goals UI (Blutdruck zwei Werte)
- Neue Goal-Typen (Tests) integrieren
### Phase 4: Testing & Refinement (2-3h)
- Migration testen (Phase 0a → v2.0)
- Scoring-Logik verifizieren
- UI/UX Testing
- Edge Cases (Summe ≠ 100%, keine Ziele, etc.)
**Total: 16-21h**
---
## 8. Offene Fragen / Entscheidungen
### 8.1 Focus Areas vs Goals Weight
**Frage:** Brauchen wir BEIDE Gewichtungssysteme?
- Focus Areas (Weight Loss 30%, Strength 25%, ...)
- Goal Weights (Ziel "82kg" = 30%, Ziel "VO2Max 46" = 25%, ...)
**Option A:** NUR Focus Areas
- Einfacher
- Weniger Redundanz
- Aber: Weniger granular
**Option B:** BEIDE Systeme
- Focus Areas = Strategisch (Richtung)
- Goal Weights = Taktisch (konkrete Prioritäten)
- Komplexer, aber flexibler
**Empfehlung:** Option B - beide Systeme ergänzen sich
---
### 8.2 Konkrete vs Abstrakte Tests
**Frage:** Wie konkret sollen Strength-Goals sein?
**Option A:** Sehr konkret
- `bench_press_1rm`, `squat_1rm`, `deadlift_1rm`
- Vorteil: Präzise, messbar
- Nachteil: Viele Goal-Typen
**Option B:** Abstrakt mit Kontext
- `strength` mit Sub-Type (Bench/Squat/Deadlift)
- Vorteil: Flexibler
- Nachteil: Komplizierteres Schema
**Empfehlung:** Option A - konkrete Typen, dafür klare Messbarkeit
---
### 8.3 Auto-Update von Current Values
**Frage:** Wie oft sollen current_value aktualisiert werden?
**Option A:** On-Demand (beim Laden der Goals-Seite)
- Vorteil: Keine Background-Jobs
- Nachteil: Kann verzögert sein
**Option B:** Trigger-basiert (bei neuem Messwert)
- Vorteil: Immer aktuell
- Nachteil: Mehr Komplexität
**Empfehlung:** Option A für MVP, Option B später
---
## 9. Nächste Schritte
### User-Feedback einholen:
1. ✅ Löst das Redesign alle genannten Probleme?
2. ✅ Ist die Fokus-Bereiche UI verständlich?
3. ✅ Sind die konkreten Goal-Typen sinnvoll?
4. ✅ Brauchen wir beide Gewichtungssysteme?
5. ✅ Fehlt noch etwas?
### Nach Freigabe:
1. Migration 023 schreiben
2. Backend implementieren
3. Frontend implementieren
4. Testing
---
**Erstellt:** 26. März 2026
**Status:** 📋 WARTET AUF FEEDBACK
**Nächster Schritt:** User-Review & Freigabe

View File

@ -0,0 +1,458 @@
# Konzept-Analyse: Fachkonzept vs. Gitea Issues
**Datum:** 26. März 2026
**Analyst:** Claude Code
**Basis:** `.claude/docs/functional/mitai_jinkendo_konzept_diagramme_auswertungen_v2.md`
**Geprüfte Issues:** #26, #27, alle offenen
---
## 1. Executive Summary
### Kernerkenntnis
Das Fachkonzept ist **wesentlich umfassender** als die aktuellen Gitea Issues #26 und #27. Es definiert ein 3-stufiges Analyse-System (Deskriptiv → Diagnostisch → Präskriptiv), das weit über einfache Charts und Korrelationen hinausgeht.
### Strategische Empfehlung
**NICHT** Issues #26 und #27 einzeln implementieren, sondern:
1. **Neu-Strukturierung:** Konzept-basierte Phasen-Issues erstellen
2. **Platzhalter-First:** Erst Berechnungs-Platzhalter implementieren
3. **Dann Visualisierung:** Charts nutzen die Platzhalter
4. **Dann KI-Integration:** KI nutzt regelbasierte Scores + Rohdaten
---
## 2. Analyse: Issue #26 vs. Fachkonzept
### Issue #26: Charts & Visualisierungen erweitern
**Status:** OPEN
**Priority:** Medium-High
**Aufwand:** 8-10h
**Definierte Charts:**
- Gewicht-Trends (Line-Chart + Trendlinie)
- Umfänge-Verlauf (Multi-Line)
- Vitalwerte-Trends (RHR, HRV, BP)
- Schlaf-Analyse (Dauer, Phasen)
- Ernährungs-Charts (Kalorien, Makros)
### Fachkonzept: Diagrammkatalog
**KÖRPER (K1-K5):**
- K1: Gewichtstrend + Trendkanal + Zielprojektion
- 7d Rolling Median, 28d/90d Trend-Slope
- Prozentuale Zielannäherung
- Regelbasierte Hinweise (zu schnell/langsam)
- K2: Körperzusammensetzung (Gewicht/FM/LBM)
- FM = Gewicht × BF%, LBM = Gewicht × (1-BF%)
- 28d/90d Änderung von FM und LBM
- K3: Umfangs-Panel (8 Mini-Charts)
- Links-Rechts Asymmetrie
- Taille/Hüfte, Taille/Körpergröße
- K4: Rekompositions-Detektor (Quadranten)
- K5: Body Progress Score (0-100)
**ERNÄHRUNG (E1-E5):**
- E1: Energieaufnahme vs. Verbrauch vs. Gewichtstrend
- E2: Protein adequacy (g/Tag, g/kg, g/kg LBM)
- E3: Makroverteilung + Wochenkonsistenz
- E4: Ernährungs-Adhärenz-Score (0-100)
- E5: Energieverfügbarkeits-Warnung
**AKTIVITÄT (A1-A8):**
- A1: Trainingsvolumen pro Woche
- A2: Intensitätsverteilung / Zonenbild
- A3: Trainingsqualitäts-Matrix
- A4: Fähigkeiten-Balance / Ability Radar
- A5: Load-Monitoring (interne Last, Monotony, Strain)
- A6: Aktivitäts-Goal-Alignment-Score (0-100)
- A7: Ruhetags-/Recovery-Compliance
- A8: VO2max-Entwicklung
### Bewertung
❌ **Issue #26 ist zu eng gefasst**
- Fokus nur auf Basis-Visualisierung
- Keine Scores, keine Baselines, keine Confidence
- Keine regelbasierten Hinweise
- Keine Ziel-Abhängigkeit
✅ **Fachkonzept bietet:**
- 18 dedizierte Charts (K1-K5, E1-E5, A1-A8)
- Scores als eigenständige Visualisierungen
- Regelbasierte Aussagen ohne KI
- Ziel-Modi Steuerung
---
## 3. Analyse: Issue #27 vs. Fachkonzept
### Issue #27: Korrelationen & Insights erweitern
**Status:** OPEN
**Priority:** High
**Aufwand:** 6-8h
**Definierte Korrelationen:**
- Schlaf ↔ Erholung (Schlafdauer → RHR, Qualität → HRV)
- Training ↔ Vitalwerte (Load → RHR-Anstieg, HRV-Abfall)
- Ernährung ↔ Performance (Defizit → Intensität)
- Blutdruck ↔ Lifestyle (Stress → BP, Training → BP)
- Multi-Faktor Analyse (KI-Insights)
### Fachkonzept: Korrelationen (C1-C6)
**KORRELATIONEN (C1-C6):**
- C1: Energie-Balance vs. Gewichtsveränderung (lagged)
- Lags: 0, 3, 7, 10, 14 Tage
- Bestes Lag ermitteln, Effektstärke, Confidence
- C2: Protein adequacy vs. LBM-Trend
- 28d Fenstervergleich, Training als Moderator
- C3: Trainingslast vs. HRV/RHR (1-3 Tage verzögert)
- Duale Lag-Auswertung, individuelle Ermüdungsreaktion
- C4: Schlafdauer + Schlafregularität vs. Recovery
- Bubble-Chart, Sleep Regularity Index
- C5: Blutdruck-Kontextmatrix (Kontext-abhängig)
- Messkontext, Schlaf Vor-Nacht, Training
- C6: Plateau-Detektor (Ereignis-Karte)
- Ziel-spezifische Plateau-Definitionen
### Zusätzlich: Lag-Analyse Prinzipien
**Zwingend im Fachkonzept:**
- **NIE nur lag=0 prüfen**
- Kalorienbilanz → Gewicht: 2-14 Tage Verzögerung
- Protein/Krafttraining → LBM: 2-6 Wochen Verzögerung
- Trainingslast → HRV/RHR: 1-3 Tage Verzögerung
- Schlafdefizit → Recovery: 1-3 Tage Verzögerung
**Mindestdatenmenge:**
- Korrelationen: mind. 21 gepaarte Tageswerte
- Lag-basiert: mind. 28 gepaarte Tage
- Confidence-Klassen (hoch/mittel/niedrig/nicht auswertbar)
### Bewertung
❌ **Issue #27 ist zu oberflächlich**
- Keine Lag-Analyse
- Keine Confidence-Bewertung
- Keine Mindestdatenmenge-Checks
- Keine Ziel-Abhängigkeit
✅ **Fachkonzept bietet:**
- 6 dedizierte Korrelations-Charts mit Lag-Analyse
- Explizite Confidence-Bewertung
- Medizinischer Sicherheitsmodus
- Plateau-Detektion (regelbasiert)
---
## 4. Konflikt-Analyse
### Gibt es Widersprüche zwischen #26 und #27?
**NEIN** Sie sind komplementär:
- #26: Deskriptive Ebene (Charts)
- #27: Diagnostische Ebene (Korrelationen)
### Aber: Beide sind zu isoliert
Das Fachkonzept zeigt: **Charts und Korrelationen müssen verzahnt sein**
**Beispiel:**
```
Fachkonzept C1: Energie-Balance vs. Gewichtsveränderung
├─ Visualisierung: Lag-Heatmap (diagnostisch)
├─ Berechnung: Cross-Correlation (0, 3, 7, 10, 14 Tage Lags)
├─ Input-Daten: Tägliche Kalorienbilanz (E-Chart)
├─ Input-Daten: 7d Gewichtsänderung (K-Chart)
└─ Regelbasierte Aussage: "Energiebilanz zeigt sich bei dir nach ~7 Tagen im Gewicht"
```
**Fazit:** Charts (K, E, A) liefern Basis-Daten für Korrelationen (C)
---
## 5. Neue Platzhalter aus Fachkonzept
### 5.1 KÖRPER (18 neue Platzhalter)
**Gewicht & Trends:**
```python
{{weight_7d_rolling_median}} # 7-Tage gleitender Median
{{weight_28d_trend_slope}} # 28-Tage Trend-Steigung (kg/Tag)
{{weight_90d_trend_slope}} # 90-Tage Trend-Steigung
{{weight_goal_progress_pct}} # Prozentuale Zielannäherung
{{weight_projection_days}} # Geschätzte Tage bis Zielgewicht
{{weight_loss_rate_weekly}} # kg/Woche (28d Mittel)
```
**Körperzusammensetzung:**
```python
{{fm_current}} # Fettmasse aktuell (kg)
{{lbm_current}} # Magermasse aktuell (kg)
{{fm_28d_delta}} # FM Änderung 28 Tage (kg)
{{lbm_28d_delta}} # LBM Änderung 28 Tage (kg)
{{fm_90d_delta}} # FM Änderung 90 Tage
{{lbm_90d_delta}} # LBM Änderung 90 Tage
{{recomposition_score}} # 0-100 (FM↓ + LBM↑ = ideal)
```
**Umfänge:**
```python
{{waist_to_hip_ratio}} # Taille/Hüfte Verhältnis
{{waist_to_height_ratio}} # Taille/Körpergröße (Gesundheitsmarker)
{{arm_asymmetry_pct}} # Links-Rechts Differenz %
{{leg_asymmetry_pct}} # Oberschenkel L-R Differenz
{{waist_28d_delta}} # Taillenumfang Änderung 28d
```
**Body Progress Score:**
```python
{{body_progress_score}} # 0-100 (zielabhängig gewichtet)
```
### 5.2 ERNÄHRUNG (15 neue Platzhalter)
**Energie & Bilanz:**
```python
{{kcal_7d_avg}} # Bereits vorhanden? Prüfen
{{kcal_28d_avg}} # 28-Tage Durchschnitt
{{kcal_estimated_tdee}} # Geschätzter Gesamtumsatz
{{kcal_balance_7d_avg}} # Durchschnittliche Bilanz 7d
{{kcal_balance_28d_avg}} # Durchschnittliche Bilanz 28d
{{energy_availability_status}} # "adequate" | "low" | "critical"
```
**Protein:**
```python
{{protein_g_per_kg}} # Protein g/kg Körpergewicht
{{protein_g_per_kg_lbm}} # Protein g/kg Magermasse
{{protein_adequacy_score}} # 0-100 (Ziel: 1.6-2.2 g/kg)
```
**Makros & Adhärenz:**
```python
{{carb_pct_7d_avg}} # % der Gesamtkalorien
{{fat_pct_7d_avg}} # % der Gesamtkalorien
{{macro_consistency_score}} # 0-100 (Regelmäßigkeit)
{{nutrition_adherence_score}} # 0-100 (Gesamtscore)
{{nutrition_days_7d}} # Erfasste Tage letzte 7d
{{nutrition_days_28d}} # Erfasste Tage letzte 28d
```
### 5.3 AKTIVITÄT (25 neue Platzhalter)
**Volumen:**
```python
{{activity_volume_7d_min}} # Gesamtminuten 7 Tage
{{activity_volume_28d_min}} # Gesamtminuten 28 Tage
{{activity_frequency_7d}} # Anzahl Sessions 7d
{{activity_frequency_28d}} # Anzahl Sessions 28d
{{activity_avg_duration_28d}} # Durchschn. Dauer pro Session
```
**Intensität:**
```python
{{activity_z1_pct}} # % Zeit in Zone 1 (7d)
{{activity_z2_pct}} # % Zeit in Zone 2
{{activity_z3_pct}} # % Zeit in Zone 3
{{activity_z4_pct}} # % Zeit in Zone 4
{{activity_z5_pct}} # % Zeit in Zone 5
{{activity_polarization_index}} # Polarisierung (Z1+Z2 vs Z4+Z5)
```
**Qualität & Load:**
```python
{{activity_quality_avg_28d}} # Durchschn. Quality-Score
{{activity_load_7d}} # Interne Last (7d Summe)
{{activity_load_28d}} # Interne Last (28d Summe)
{{activity_monotony_28d}} # Last-Variabilität
{{activity_strain_28d}} # Load × Monotony
{{activity_acwr}} # Acute:Chronic Workload Ratio
```
**Fähigkeiten:**
```python
{{ability_strength_score}} # 0-100 (aus Training Types)
{{ability_endurance_score}} # 0-100
{{ability_mobility_score}} # 0-100
{{ability_skills_score}} # 0-100
{{ability_mindfulness_score}} # 0-100
{{ability_balance_score}} # 0-100 (wie ausgewogen?)
```
**Goal Alignment:**
```python
{{activity_goal_alignment_score}} # 0-100 (zielabhängig)
{{rest_days_compliance}} # 0-100 (geplant vs. tatsächlich)
```
### 5.4 RECOVERY & GESUNDHEIT (12 neue Platzhalter)
**Baselines:**
```python
{{rhr_7d_baseline}} # 7-Tage Baseline Ruhepuls
{{rhr_28d_baseline}} # 28-Tage Baseline
{{hrv_7d_baseline}} # 7-Tage Baseline HRV
{{hrv_28d_baseline}} # 28-Tage Baseline
```
**Deltas & Trends:**
```python
{{rhr_vs_baseline_7d}} # Abweichung von Baseline (bpm)
{{hrv_vs_baseline_7d}} # Abweichung von Baseline (ms)
{{vo2max_trend_28d}} # VO2max Entwicklung
```
**Scores:**
```python
{{recovery_score}} # 0-100 (HRV, RHR, Schlaf)
{{recovery_score_confidence}} # 0-100 (Datenqualität)
{{sleep_regularity_index}} # Schlafregelmäßigkeit
{{sleep_debt_hours}} # Akkumulierte Schlafschuld
{{health_risk_score}} # 0-100 (Blutdruck, etc.)
```
### 5.5 KORRELATIONEN (8 neue Platzhalter)
```python
{{corr_energy_weight_lag}} # Bestes Lag Energie→Gewicht (Tage)
{{corr_energy_weight_r}} # Korrelationskoeffizient
{{corr_protein_lbm_r}} # Protein ↔ LBM Korrelation
{{corr_load_hrv_lag}} # Bestes Lag Load→HRV
{{corr_load_hrv_r}} # Korrelation
{{corr_sleep_rhr_r}} # Schlaf ↔ RHR Korrelation
{{plateau_detected}} # true|false (regelbasiert)
{{plateau_type}} # "weight_loss" | "strength" | etc.
```
### 5.6 META-PLATZHALTER (6 neue)
```python
{{goal_mode}} # "weight_loss" | "strength" | etc.
{{training_age_weeks}} # Trainingserfahrung
{{data_quality_score}} # 0-100 (Gesamtdatenqualität)
{{measurement_consistency}} # 0-100 (Messzeit-Konsistenz)
{{analysis_confidence}} # "high" | "medium" | "low"
{{analysis_timeframe}} # "7d" | "28d" | "90d"
```
---
## 6. Gesamt-Übersicht: Neue Platzhalter
| Kategorie | Anzahl | Beispiele |
|-----------|--------|-----------|
| KÖRPER | 18 | weight_28d_trend_slope, fm_28d_delta, recomposition_score |
| ERNÄHRUNG | 15 | protein_g_per_kg_lbm, nutrition_adherence_score, energy_availability_status |
| AKTIVITÄT | 25 | activity_quality_avg_28d, activity_strain_28d, ability_balance_score |
| RECOVERY | 12 | recovery_score, sleep_regularity_index, sleep_debt_hours |
| KORRELATIONEN | 8 | corr_energy_weight_lag, plateau_detected, corr_load_hrv_r |
| META | 6 | goal_mode, data_quality_score, analysis_confidence |
| **GESAMT** | **84** | **Neue Platzhalter aus Fachkonzept** |
---
## 7. Strategische Roadmap-Empfehlung
### Phase 0: Fundament (JETZT)
**Ziel:** Berechnungs-Platzhalter implementieren
**Aufwand:** 16-20h
**Deliverables:**
- 84 neue Platzhalter in `placeholder_resolver.py`
- Baseline-Berechnungen (7d, 28d, 90d)
- Score-Algorithmen (Body Progress, Nutrition Adherence, Activity Goal Alignment, Recovery)
- Lag-Korrelations-Funktionen
- Confidence-Berechnung
**Issues zu erstellen:**
- #52: Baseline & Trend Calculations (Körper, Ernährung, Aktivität)
- #53: Score Algorithms (4 Haupt-Scores)
- #54: Correlation & Lag Analysis
- #55: Confidence & Data Quality Metrics
### Phase 1: Visualisierung (DANN)
**Ziel:** Charts nutzen die neuen Platzhalter
**Aufwand:** 12-16h
**Deliverables:**
- K1-K5 Charts (Körper)
- E1-E5 Charts (Ernährung)
- A1-A8 Charts (Aktivität)
- C1-C6 Charts (Korrelationen)
**Issues zu konsolidieren:**
- #26 erweitern zu "Comprehensive Chart System (K, E, A, C)"
- #27 erweitern zu "Correlation & Lag Analysis Charts"
### Phase 2: Regelbasierte Insights (DANACH)
**Ziel:** System wird Coach (nicht nur Datensammler)
**Aufwand:** 8-12h
**Deliverables:**
- Regelbasierte Hinweise ohne KI
- Plateau-Detektion
- Ziel-abhängige Interpretationen
- Warnungen (Gesundheit, Übertraining, Energieverfügbarkeit)
**Neue Issues:**
- #56: Rule-Based Recommendations Engine
- #57: Goal-Mode System & Interpretation
- #58: Health & Safety Warnings
### Phase 3: KI-Integration (SPÄTER)
**Ziel:** KI nutzt Scores + Rohdaten + Regeln
**Aufwand:** 6-8h
**Deliverables:**
- KI-Prompts nutzen neue Platzhalter
- Contextual AI Analysis (nutzt goal_mode)
- Multi-Faktor Insights
---
## 8. Aktions-Empfehlungen
### SOFORT (heute)
1. ✅ **Issues #26 und #27 NICHT einzeln implementieren**
2. ✅ **Neues Issue #52 erstellen:** Baseline & Trend Calculations
3. ✅ **Neues Issue #53 erstellen:** Score Algorithms
4. ✅ **Issue #26 umbennen/erweitern:** "Comprehensive Chart System (based on Fachkonzept)"
5. ✅ **Issue #27 umbennen/erweitern:** "Correlation & Lag Analysis (based on Fachkonzept)"
### DIESE WOCHE
6. ✅ **Implementierung starten:** Phase 0 - Platzhalter
7. ✅ **Dokumentation:** Mapping Fachkonzept → Code
8. ✅ **KI-Prompts vorbereiten:** Nutzen neue Platzhalter
### NÄCHSTE WOCHE
9. ✅ **Implementierung:** Phase 1 - Charts
10. ✅ **Testing:** Alle Scores & Berechnungen
11. ✅ **Production:** Deployment vorbereiten
---
## 9. Zusammenfassung: Transformation Data Collector → Active Coach
### Aktueller Stand
**Data Collector:**
- Daten werden erfasst
- Einfache Listen
- Basis-Statistiken
- KI-Analysen manuell angestoßen
### Ziel (nach Fachkonzept)
**Active Coach:**
- Daten werden **interpretiert**
- Trends & Baselines
- Scores & Confidence
- Regelbasierte Hinweise
- Ziel-abhängige Bewertung
- Proaktive Warnungen
- KI nutzt strukturierte Insights
---
## 10. Nächste Schritte
1. **Issues neu strukturieren** (heute)
2. **Platzhalter implementieren** (Phase 0, diese Woche)
3. **Charts implementieren** (Phase 1, nächste Woche)
4. **Regelbasierte Insights** (Phase 2, Woche danach)
5. **KI-Integration** (Phase 3, dann)
**Commit:** cd2609d
**Analysiert von:** Claude Code
**Basis:** Fachkonzept v2 (2086 Zeilen, 24.03.2026)

View File

@ -0,0 +1,460 @@
# Nächste Schritte nach Phase 0a
**Stand:** 26. März 2026, nach Completion von Phase 0a (Goal System)
**Aktueller Branch:** `develop`
**Deployed:** `dev.mitai.jinkendo.de`
---
## Aktueller Stand ✅
### Abgeschlossen
- ✅ **Phase 0a:** Minimal Goal System (Strategic + Tactical)
- Migration 022, goals.py Router, GoalsPage UI
- Navigation von Dashboard + Analysis
- Mobile-friendly Design
- **Basis vorhanden für 120+ goal-aware Platzhalter**
### Offene Gitea Issues
- 🔲 **#49:** Prompt-Zuordnung zu Verlaufsseiten (6-8h)
- 🔲 **#47:** Wertetabelle Optimierung (4-6h)
- 🔲 **#46:** KI Prompt-Ersteller (später)
- 🔲 **#45:** KI Prompt-Optimierer (später)
- 🔲 **#43, #42:** Enhanced Debug UI (später)
---
## Option A: Issue #49 - Prompt Page Assignment ⚡
**Aufwand:** 6-8 Stunden
**Priorität:** Medium
**Typ:** UX Enhancement
**Labels:** feature, ux, enhancement
### Beschreibung
KI-Prompts flexibel auf verschiedenen Verlaufsseiten verfügbar machen. Jeder Prompt kann auf mehreren Seiten gleichzeitig angeboten werden (Mehrfachauswahl).
### Problem
**Aktuell:**
- Prompts nur über zentrale Analyse-Seite verfügbar
- Kein kontextbezogener Zugriff auf relevante Analysen
- User muss immer zur Analyse-Seite navigieren
**Beispiel-Szenario:**
```
User ist auf: Gewicht → Verlauf
Will: Gewichtstrend analysieren
Muss: Zur Analyse-Seite → Prompt auswählen → Zurück
```
**Wünschenswert:**
```
User ist auf: Gewicht → Verlauf
Sieht: "🤖 KI-Analyse" Widget mit relevanten Prompts
Kann: Direkt "Gewichtstrend-Analyse" starten
```
### Technische Umsetzung
**Backend (2h):**
```sql
-- Migration 023
ALTER TABLE ai_prompts ADD COLUMN available_on JSONB DEFAULT '["analysis"]';
-- Beispiel:
{
"slug": "weight_trend",
"available_on": ["analysis", "weight_history"]
}
```
**API:**
```python
# Neuer Endpoint
GET /api/prompts/for-page/{page_slug}
→ Returns: List[Prompt] where available_on contains page_slug
# CRUD erweitern
PUT /api/prompts/unified/{id}
→ Body: {..., "available_on": ["analysis", "weight_history"]}
```
**Frontend (4h):**
```javascript
// Wiederverwendbare Komponente
<PagePrompts pageSlug="weight_history" />
// UnifiedPromptModal erweitern
const PAGE_OPTIONS = [
{ value: 'analysis', label: '📊 Analyse (Hauptseite)', default: true },
{ value: 'weight_history', label: '⚖️ Gewicht → Verlauf' },
{ value: 'nutrition_history', label: '🍎 Ernährung → Verlauf' },
// ... 9 Optionen total
]
// Multi-select checkboxes in Prompt-Editor
```
**Integration in Verlaufsseiten (2h):**
- WeightPage, NutritionPage, ActivityPage erweitern
- Widget unterhalb Charts einfügen
- Modal für Inline-Analyse
### Vorteile
- ✅ Schneller Nutzen (UX-Verbesserung sofort sichtbar)
- ✅ Nutzt bestehendes Unified Prompt System (Issue #28)
- ✅ Relativ einfache Implementierung
- ✅ Bereitet vor für Phase 0b (neue Platzhalter dann sofort auf allen Seiten nutzbar)
### Nachteile
- ⚠️ Verzögert strategische Tiefe (goal-aware Analysen)
- ⚠️ Erst sinnvoll wenn mehr Prompts existieren
**Dokumentation:** Siehe `docs/issues/issue-51-prompt-page-assignment.md`
---
## Option B: Phase 0b - Goal-Aware Placeholders 🎯
**Aufwand:** 16-20 Stunden
**Priorität:** High (strategisch kritisch)
**Typ:** Core Feature
**Labels:** feature, ai, goal-system
### Beschreibung
Implementierung von 120+ neuen KI-Platzhaltern die `goal_mode` berücksichtigen. Verwandelt System von "Datensammler" zu "intelligentem Coach".
### Problem
**Aktuell:**
- Ziele existieren, aber KI-Analysen ignorieren sie
- Gleiche Daten werden für alle goal_modes gleich interpretiert
- Keine goal-spezifischen Score-Berechnungen
**Beispiel:**
```python
# Gleiche Messung: -5kg FM, -2kg LBM
# Aktuell: Generischer Score (z.B. 50/100)
# Mit Phase 0b:
goal_mode = "weight_loss" → 78/100 (FM↓ gut!)
goal_mode = "strength" → 32/100 (LBM↓ Katastrophe!)
goal_mode = "recomposition" → 65/100 (beides relevant)
```
### Technische Umsetzung
**1. Placeholder Functions (8-10h):**
**Kategorie: KÖRPER (18 neue):**
```python
def weight_7d_rolling_median(profile_id, goal_mode):
"""Rolling median statt avg für Stabilität"""
def weight_28d_trend_slope(profile_id, goal_mode):
"""Linear regression slope - kg/Woche"""
def fm_28d_delta(profile_id, goal_mode):
"""Fettmasse-Veränderung 28 Tage"""
def lbm_28d_delta(profile_id, goal_mode):
"""Magermasse-Veränderung 28 Tage"""
def recomposition_score(profile_id, goal_mode):
"""FM↓ + LBM↑ Balance-Score"""
# Nur relevant wenn goal_mode = "recomposition"
def waist_to_hip_ratio(profile_id):
"""WHR - Bauchfettverteilung"""
def waist_to_height_ratio(profile_id):
"""WHtR - Gesundheitsrisiko"""
```
**Kategorie: ERNÄHRUNG (15 neue):**
```python
def protein_g_per_kg(profile_id, goal_mode):
"""Protein pro kg Körpergewicht"""
# Target abhängig von goal_mode:
# strength: 2.0-2.2g/kg
# weight_loss: 1.8-2.0g/kg
# endurance: 1.4-1.6g/kg
def protein_g_per_kg_lbm(profile_id):
"""Protein pro kg Magermasse (präziser)"""
def nutrition_adherence_score(profile_id, goal_mode):
"""Wie gut hält User seine Makro-Ziele ein?"""
# Ziele abhängig von goal_mode
def energy_availability_status(profile_id):
"""kcal - activity_kcal - BMR = verfügbare Energie"""
# RED-S Warnung wenn < 30 kcal/kg LBM
```
**Kategorie: AKTIVITÄT (25 neue):**
```python
def activity_quality_avg_28d(profile_id):
"""Durchschnittliche Trainingsqualität"""
def activity_strain_28d(profile_id):
"""Kumulierte Belastung (Monotonie-Detektion)"""
def activity_monotony_28d(profile_id):
"""Variation im Training (Plateaus erkennen)"""
def ability_balance_score(profile_id, goal_mode):
"""Balance zwischen Fähigkeiten (Strength/Cardio/Mobility)"""
# Gewichtung abhängig von goal_mode
```
**Kategorie: RECOVERY (12 neue):**
```python
def recovery_score(profile_id):
"""
Kombiniert: RHR + HRV + Sleep Quality + Rest Days
Score: 0-100
"""
def sleep_regularity_index(profile_id):
"""Wie regelmäßig sind Schlafzeiten? (0-100)"""
def sleep_debt_hours(profile_id):
"""Kumulierte Schlafdifferenz zu Ziel"""
```
**Kategorie: KORRELATIONEN (8 neue):**
```python
def corr_energy_weight_lag(profile_id):
"""
Korrelation Kaloriendefizit → Gewicht
Mit Lag-Analysis (verzögerte Effekte)
Confidence-Score basierend auf Datenmenge
"""
def plateau_detected(profile_id):
"""
Boolean: Gewicht stagniert trotz Defizit?
Trigger für Interventionen
"""
```
**Kategorie: META (6 neue):**
```python
def goal_mode(profile_id):
"""Aktueller goal_mode (für Prompts verfügbar)"""
def data_quality_score(profile_id):
"""Wie vollständig/konsistent sind Daten? (0-100)"""
def profile_age_years(profile_id):
"""Alter für altersabhängige Normen"""
```
**2. Score-Gewichtung (4-6h):**
```python
# backend/score_calculator.py (NEU)
SCORE_WEIGHTS = {
"weight_loss": {
"body_progress": 0.30, # FM↓ wichtig
"nutrition": 0.25, # Defizit wichtig
"training_quality": 0.15, # Moderat wichtig
"recovery": 0.15, # Moderat wichtig
"adherence": 0.15 # Konsistenz wichtig
},
"strength": {
"body_progress": 0.35, # LBM↑ KRITISCH
"nutrition": 0.30, # Surplus + Protein
"training_quality": 0.25, # Progressive Overload
"recovery": 0.10 # Weniger wichtig
},
"endurance": {
"training_quality": 0.40, # VO2Max, Pace wichtig
"recovery": 0.25, # Übertraining vermeiden
"body_progress": 0.15, # Gewicht sekundär
"nutrition": 0.20 # Energie-Verfügbarkeit
},
# ... recomposition, health
}
def calculate_overall_score(profile_id, goal_mode):
"""Berechnet Gesamt-Score basierend auf goal_mode Gewichtung"""
weights = SCORE_WEIGHTS[goal_mode]
scores = {
"body_progress": calculate_body_progress_score(profile_id, goal_mode),
"nutrition": calculate_nutrition_score(profile_id, goal_mode),
"training_quality": calculate_training_score(profile_id, goal_mode),
"recovery": calculate_recovery_score(profile_id),
"adherence": calculate_adherence_score(profile_id, goal_mode)
}
overall = sum(scores[key] * weights[key] for key in weights)
return {
"overall": round(overall, 1),
"breakdown": scores,
"weights": weights
}
```
**3. Baseline-Berechnungen (2-3h):**
```python
def calculate_baselines(profile_id):
"""
Berechnet persönliche Referenzwerte:
- 7d baseline (kurzfristig)
- 28d baseline (mittelfristig)
- 90d baseline (langfristig)
Für: Gewicht, RHR, HRV, Kalorien, Protein, etc.
"""
def detect_anomalies(profile_id, metric, value):
"""
Ist Wert außerhalb von ±2 SD vom Baseline?
→ Warnung für User
"""
```
**4. Integration in Prompts (1-2h):**
```python
# Beispiel Prompt-Template:
"""
Du bist ein KI-Coach für {{goal_mode}} Training.
Aktueller Status:
- Gewichtstrend: {{weight_28d_trend_slope}} kg/Woche
- Fettmasse Δ28d: {{fm_28d_delta}} kg
- Magermasse Δ28d: {{lbm_28d_delta}} kg
- Rekompositions-Score: {{recomposition_score}}/100
Ernährung:
- Protein/kg: {{protein_g_per_kg}} g/kg (Ziel: {{protein_target_for_mode}})
- Adherence: {{nutrition_adherence_score}}/100
Training:
- Qualität (28d): {{activity_quality_avg_28d}}/5.0
- Monotonie: {{activity_monotony_28d}} (Warnung bei >2.0)
Recovery:
- Recovery Score: {{recovery_score}}/100
- Schlafschuld: {{sleep_debt_hours}}h
Gesamt-Score ({{goal_mode}}-optimiert): {{overall_score}}/100
Analyse den Fortschritt aus Sicht eines {{goal_mode}} Ziels...
"""
```
### Vorteile
- ✅ Größter strategischer Impact (System wird intelligent)
- ✅ Ziele werden tatsächlich genutzt (nicht nur Display)
- ✅ Basis für alle zukünftigen Features
- ✅ Automatische Trainingsphasen-Erkennung möglich
### Nachteile
- ⚠️ Hoher Aufwand (16-20h)
- ⚠️ Komplexe Logik (viel Testing nötig)
- ⚠️ Erfordert mehr Daten für sinnvolle Scores
---
## Option C: Issue #47 - Value Table Refinement 🔬
**Aufwand:** 4-6 Stunden
**Priorität:** Low (Polishing)
**Typ:** Enhancement
### Beschreibung
Wertetabelle übersichtlicher gestalten - Normal-Modus nur Einzelwerte, Experten-Modus mit Stage-Rohdaten.
### Vorteile
- ✅ Bessere UX für Value Table
- ✅ Weniger Überforderung im Normal-Modus
### Nachteile
- ⚠️ Kosmetisch, kein funktionaler Impact
- ⚠️ Besser warten bis Phase 0b (dann 120+ Platzhalter)
**Empfehlung:** Später (nach Phase 0b)
---
## Empfehlung 🎯
### Szenario 1: "Quick Wins first"
```
1. Issue #49 - Prompt Assignment (6-8h)
→ Bessere UX sofort
2. Phase 0b - Goal-Aware Placeholders (16-20h)
→ Neue Platzhalter profitieren von Page Assignment
→ Volle Power mit beiden Features
Total: 22-28h
```
### Szenario 2: "Strategic Depth first"
```
1. Phase 0b - Goal-Aware Placeholders (16-20h)
→ System wird intelligent
2. Issue #49 - Prompt Assignment (6-8h)
→ Intelligente Prompts dann auf allen Seiten
Total: 22-28h
```
### Persönliche Empfehlung: **Szenario 1**
**Begründung:**
- Issue #49 ist relativ einfach und bringt sofort UX-Nutzen
- Nutzt bestehendes Unified Prompt System optimal
- Phase 0b profitiert dann von besserer Navigation
- User kann neue Platzhalter (Phase 0b) direkt auf relevanten Seiten nutzen
- Psychologisch: Zwei Erfolgserlebnisse statt einem großen
---
## Nächste Session: Action Items
**Falls Issue #49 gewählt:**
1. [ ] Migration 023 erstellen (available_on JSONB)
2. [ ] Backend: `/api/prompts/for-page/{slug}` Endpoint
3. [ ] Backend: CRUD erweitern (available_on in PUT)
4. [ ] Frontend: PAGE_OPTIONS in UnifiedPromptModal
5. [ ] Frontend: PagePrompts Komponente (wiederverwendbar)
6. [ ] Integration: WeightPage, NutritionPage, ActivityPage
7. [ ] Testing: Multi-select, Modal-Inline-Analyse
**Falls Phase 0b gewählt:**
1. [ ] Placeholder-Funktionen kategorieweise implementieren (KÖRPER → ERNÄHRUNG → AKTIVITÄT → RECOVERY → KORRELATIONEN → META)
2. [ ] Score-Gewichtung pro goal_mode definieren
3. [ ] Backend: score_calculator.py erstellen
4. [ ] Baseline-Berechnungen implementieren
5. [ ] Integration in bestehende Prompts
6. [ ] Testing mit verschiedenen goal_modes
---
## Metriken & Timeline
**Geschätzte Timeline (bei 4h/Tag Entwicklung):**
| Szenario | Dauer | Fertig bis |
|----------|-------|------------|
| Issue #49 | 1.5-2 Tage | ~28.03.2026 |
| Phase 0b | 4-5 Tage | ~31.03.2026 |
| Szenario 1 (Quick Wins first) | 5.5-7 Tage | ~02.04.2026 |
| Szenario 2 (Strategic first) | 5.5-7 Tage | ~02.04.2026 |
**Bei 8h/Tag Entwicklung:** Timeline halbiert sich (~01.04.2026)
---
**Erstellt:** 26. März 2026
**Status:** Aktiv - Wartet auf Entscheidung
**Nächste Aktualisierung:** Nach Completion von gewähltem Path

View File

@ -0,0 +1,194 @@
# Status Report: 26. März 2026
## Audit & Synchronisation
Vollständige Überprüfung aller Dokumente und Gitea Issues durchgeführt.
---
## ✅ Abgeschlossene Arbeiten
### 1. Gitea Issue #28: AI-Prompts Flexibilisierung
**Status:** ✅ CLOSED (26.03.2026)
**Implementierte Features:**
- Unified Prompt System (4 Phasen)
- DB-Migration zu einheitlichem Schema (base + pipeline)
- Universeller Executor (prompt_executor.py)
- Frontend UI Consolidation (UnifiedPromptModal)
- Debug & Development Tools (Test-Button, Export/Import)
- 32 aktive Platzhalter mit Kategorisierung
- `{{placeholder|d}}` Modifier
**Commits:** 20+ commits (2e0838c bis ae6bd0d)
**Dokumentation:** CLAUDE.md "Feature: Unified Prompt System"
**Gitea Aktion:** Issue geschlossen mit Completion-Kommentar
---
### 2. Gitea Issue #44: BUG - Analysen löschen
**Status:** ✅ CLOSED (26.03.2026)
**Fix:**
- Delete-Button in InsightCard hinzugefügt
- `api.deleteInsight(id)` Funktion implementiert
- Auth-Token wird korrekt übergeben
- Liste aktualisiert sich nach Löschen
**Commit:** c56d2b2
**Dokumentation:** Gitea-Kommentar mit Code-Beispiel
**Gitea Aktion:** Issue geschlossen mit Fix-Details
---
### 3. Feature: Comprehensive Value Table
**Status:** ✅ Basis-Implementierung COMPLETE (26.03.2026)
**Implementierte Features:**
- Metadata Collection System (alle Platzhalter mit Werten)
- Expert Mode Toggle (🔬 Experten-Modus)
- Stage Output Extraction (Einzelwerte aus JSON)
- Category Grouping (PROFIL, KÖRPER, ERNÄHRUNG, etc.)
- Collapsible JSON für Stage-Rohdaten
- Best-of-Each circ_summary mit Altersangaben
**Commits:** 10+ commits (c0a50de bis 6e651b5, 159fcab)
**Dokumentation:** CLAUDE.md "Feature: Comprehensive Value Table"
**Gitea:** Basis abgeschlossen, Issue #47 für Refinement erstellt
---
### 4. Placeholder System Enhancements
**Status:** ✅ COMPLETE
**Fixes & Verbesserungen:**
- `circ_summary`: Alle 8 Umfangspunkte (statt nur 3)
- `circ_summary`: Best-of-Each mit Altersangaben ("heute", "vor 2 Wochen")
- `sleep_avg_quality`: Lowercase stage names fix
- `calculate_age`: PostgreSQL DATE object handling
- Stage outputs in debug info für Value Table
**Commits:** 7daa2e4, a43a9f1, 3ad1a19, d06d3d8, 159fcab, 6e651b5
---
## 🔲 Neue/Offene Issues
### Gitea Issue #47: Wertetabelle Optimierung
**Status:** 🔲 OPEN (neu erstellt 26.03.2026)
**Priority:** Medium
**Aufwand:** 4-6 Stunden
**Ziel:** Value Table übersichtlicher gestalten
**Kernpunkte:**
- Normal-Modus: Nur Einzelwerte (~24 statt 32)
- Experten-Modus: Zusätzlich Stage-Rohdaten
- Beschreibungen für alle 32 Platzhalter vervollständigen
- Schema-basierte Beschreibungen für extrahierte Werte
**Dokumentation:** `docs/issues/issue-50-value-table-refinement.md`
---
## 📊 Gitea Issue Übersicht
### Geschlossen (heute)
- ✅ #28: AI-Prompts Flexibilisierung
- ✅ #44: BUG - Analysen löschen
### Neu erstellt (heute)
- 🆕 #47: Wertetabelle Optimierung
### Weiterhin offen (Backlog)
- 🔲 #25: Ziele-System (Goals)
- 🔲 #26: Charts erweitern
- 🔲 #27: Korrelationen & Insights
- 🔲 #29: Abilities-Matrix UI
- 🔲 #30: Responsive UI
- 🔲 #42, #43: Enhanced Debug UI
- 🔲 #45: KI Prompt-Optimierer
- 🔲 #46: KI Prompt-Ersteller
### Bereits geschlossen (früher)
- ✅ #24: Quality-Filter für KI-Auswertungen
---
## 📝 Dokumentations-Updates
### CLAUDE.md
- ✅ "Letzte Updates (26.03.2026)" Sektion hinzugefügt
- ✅ Gitea Issue-Referenzen klargestellt (Prefix "Gitea #")
- ✅ Feature-Sections umbenannt (nicht "Issue #28/47")
- ✅ "Claude Code Verantwortlichkeiten" Sektion
- ✅ Issue-Management via Gitea API dokumentiert
### docs/issues/
- ✅ issue-50-value-table-refinement.md erstellt
- Weitere Files in .claude/issues/ (nicht versioniert)
### Gitea Kommentare
- ✅ Issue #28: Completion-Details mit Features & Commits
- ✅ Issue #44: Fix-Details mit Code-Beispiel
---
## 🔄 Nächste Schritte
### Empfohlen (Kurzfristig)
1. **Testing auf dev.mitai.jinkendo.de:**
- Value Table im Experten-Modus testen
- Stage-Outputs JSON Anzeige prüfen
- circ_summary mit Altersangaben verifizieren
2. **Production Deployment:**
- Develop → Main Merge (wenn Tests OK)
- Alle Features (Unified Prompts + Value Table) deployen
3. **Issue #47 Refinement:**
- Wertetabelle im Normal-Modus optimieren
- Beschreibungen vervollständigen
### Optional (Mittelfristig)
4. **Weitere offene Issues priorisieren:**
- #25: Ziele-System (Phase 1)
- #27: Korrelationen (Phase 2)
- #30: Responsive UI (Phase 0)
---
## 📈 Metriken
**Commits (heute):** 12
**Issues geschlossen:** 2 (#28, #44)
**Issues erstellt:** 1 (#47)
**Dokumentations-Updates:** 3 (CLAUDE.md, STATUS_REPORT, issue-50)
**Gitea Kommentare:** 2
**Entwicklungszeit (geschätzt):** ~6-8 Stunden
- circ_summary Enhancement: 1h
- Stage Outputs Fix: 1h
- Value Table Collapsible JSON: 1h
- Issue-Management System: 1h
- Dokumentation & Sync: 2-4h
---
## ✅ Verifizierung
- [x] Alle Gitea Issues überprüft (47 Issues total)
- [x] Abgeschlossene Arbeiten identifiziert (#28, #44)
- [x] Issues in Gitea geschlossen
- [x] Completion-Kommentare hinzugefügt
- [x] CLAUDE.md aktualisiert
- [x] Status Report erstellt
- [x] Entwicklungs-Dokumentation aktuell
**Audit durchgeführt von:** Claude Code
**Datum:** 26. März 2026, 14:55 Uhr
**Branch:** develop
**Letzter Commit:** 582f125

284
docs/TODO_GOAL_SYSTEM.md Normal file
View File

@ -0,0 +1,284 @@
# Goal System - TODO & Offene Punkte
**Erstellt:** 27. März 2026
**Status:** Aktiv
**Zweck:** Zentrale Tracking-Liste für Goal System Entwicklung
---
## ✅ Erledigt (27.03.2026)
### Phase 0a: Minimal Goal System (26.03.2026)
- ✅ Migration 022 (goal_mode, goals, training_phases, fitness_tests)
- ✅ Backend Router goals.py (490 Zeilen)
- ✅ Frontend GoalsPage (570 Zeilen)
- ✅ Navigation Integration (Dashboard + Analysis)
### Phase 1: Quick Fixes (27.03.2026)
- ✅ goal_utils.py Abstraction Layer
- ✅ Primary Goal Toggle Fix
- ✅ Lean Mass Berechnung
- ✅ VO2Max Spaltenname Fix
---
## 🔲 Nächste Schritte (Priorität)
### Phase 1.5: Flexibles Goal System - DB-Registry ✅ KOMPLETT (27.03.2026)
**Status:** ✅ ABGESCHLOSSEN
**Priorität:** CRITICAL (blockt Phase 0b)
**Aufwand:** 8h (geplant 8-12h)
**Entscheidung:** 27.03.2026 - Option B gewählt
**Problem:**
- Aktuelles System: Hardcoded goal types (nur 8 Typen möglich)
- Jedes neue Ziel braucht Code-Änderung + Deploy
- Zukünftige Ziele (Meditation, Rituale, Planabweichung) nicht möglich
**Lösung: DB-Registry**
- Goal Types in Datenbank definiert
- Admin UI: Neue Ziele ohne Code erstellen
- Universal Value Fetcher (konfigurierbar)
- User kann eigene Custom-Metriken definieren
**Tasks:**
- ✅ Migration 024: goal_type_definitions Tabelle
- ✅ Backend: Universal Value Fetcher (_fetch_latest, _fetch_avg, _fetch_count)
- ✅ Backend: CRUD API für Goal Type Definitions
- ✅ Frontend: Dynamisches Goal Types Dropdown
- ✅ Admin UI: Goal Type Management Page
- ✅ Seed Data: 8 existierende Typen migriert
- 🔲 Testing: Alle Goals + Custom Goal erstellen (NEXT)
**Warum JETZT (vor Phase 0b)?**
- Phase 0b Platzhalter nutzen Goals für Score-Berechnungen
- Flexible Goals → automatisch in Platzhaltern verfügbar
- Später umbauen = 120+ Platzhalter anpassen (Doppelarbeit)
**Dokumentation:** Siehe unten "Flexibles Goal System Details"
---
### Phase 0b: Goal-Aware Placeholders (NACH 1.5 - 16-20h)
**Status:** 🔲 BEREIT ZUM START (Phase 1.5 ✅)
**Priorität:** HIGH (strategisch kritisch)
**Aufwand:** 16-20h
**Blockt:** Intelligente KI-Analysen
**Tasks:**
- [ ] 18 KÖRPER Platzhalter (weight_7d_rolling_median, fm_28d_delta, lbm_28d_delta, recomposition_score, etc.)
- [ ] 15 ERNÄHRUNG Platzhalter (protein_g_per_kg, nutrition_adherence_score, energy_availability_status, etc.)
- [ ] 25 AKTIVITÄT Platzhalter (activity_quality_avg_28d, activity_strain_28d, ability_balance_score, etc.)
- [ ] 12 RECOVERY Platzhalter (recovery_score, sleep_regularity_index, sleep_debt_hours, etc.)
- [ ] 8 KORRELATIONEN Platzhalter (corr_energy_weight_lag, plateau_detected, etc.)
- [ ] 6 META Platzhalter (goal_mode, data_quality_score, profile_age_years, etc.)
- [ ] Score-Gewichtung pro goal_mode (SCORE_WEIGHTS Dictionary)
- [ ] Baseline-Berechnungen (7d/28d/90d Referenzwerte)
- [ ] Integration in bestehende Prompts
**Vorteile:**
- System wird "intelligent" (kein Datensammler mehr)
- Ziele werden tatsächlich genutzt
- Basis für automatische Trainingsphasen-Erkennung
**Dokumentation:** `docs/NEXT_STEPS_2026-03-26.md` (Zeile 116-300)
---
### v2.0 Redesign (SPÄTER - 8-10h)
**Status:** 📋 KONZEPTION
**Priorität:** MEDIUM (nach Phase 0b & User-Feedback)
**Aufwand:** 8-10h (dank Abstraction Layer)
**Probleme zu lösen:**
1. ❌ Primärziel zu simplistisch (nur 1 erlaubt)
2. ❌ Goal Mode zu simpel (nur 1 Modus wählbar)
3. ✅ Fehlende Current Values (ERLEDIGT in Phase 1)
4. ❌ Abstrakte Zieltypen (strength, flexibility)
5. ❌ Blutdruck braucht 2 Werte (systolisch/diastolisch)
6. ❌ Keine Guidance für User (Richtwerte fehlen)
**Lösung:**
- Migration 023: focus_areas Tabelle mit Gewichtungssystem
- UI: Slider für 6 Fokus-Bereiche (Summe = 100%)
- Backend: `get_focus_weights()` V2 Implementierung (eine Funktion!)
- Compound Goals für BP
- Konkrete Test-basierte Goals (Cooper, Plank, etc.)
- Richtwerte & Normen in UI
**Dokumentation:** `docs/GOAL_SYSTEM_REDESIGN_v2.md`
**Entscheidung:** ⏳ Wartet auf User-Feedback nach Phase 0b
---
## 🔗 Verwandte Issues
### Gitea (http://192.168.2.144:3000/Lars/mitai-jinkendo/issues)
- **#49:** Prompt-Zuordnung zu Verlaufsseiten (6-8h, Quick Win)
- **#47:** Wertetabelle Optimierung (4-6h, Polishing)
- **#50:** Phase 0a Goal System (✅ CLOSED)
### Interne Docs
- `docs/issues/issue-50-phase-0a-goal-system.md` (✅ Completed)
- `docs/issues/issue-51-prompt-page-assignment.md` (#49 Spec)
---
## 📊 Roadmap-Übersicht
| Phase | Was | Status | Aufwand |
|-------|-----|--------|---------|
| **Phase 0a** | Minimal Goal System | ✅ DONE | 3-4h |
| **Phase 1** | Quick Fixes + Abstraction | ✅ DONE | 4-6h |
| **Phase 1.5** | 🆕 **Flexibles Goal System (DB-Registry)** | ✅ **DONE** | 8h |
| **Phase 0b** | Goal-Aware Placeholders | 🔲 READY | 16-20h |
| **Issue #49** | Prompt Page Assignment | 🔲 OPEN | 6-8h |
| **v2.0** | Redesign (Focus Areas) | 📋 LATER | 8-10h |
**Total Roadmap:** ~45-60h bis vollständiges intelligentes Goal System
**KRITISCH:** Phase 1.5 MUSS vor Phase 0b abgeschlossen sein, sonst Doppelarbeit!
---
## 💡 Wichtige Notizen
### Abstraction Layer (Keine Doppelarbeit!)
**Datei:** `backend/goal_utils.py`
```python
get_focus_weights(conn, profile_id)
```
- **V1 (jetzt):** Mappt goal_mode → Gewichte
- **V2 (v2.0):** Liest focus_areas Tabelle
- **Vorteil:** 120+ Phase 0b Platzhalter müssen NICHT umgeschrieben werden
### Testing Checklist (nach jedem Deploy)
- [ ] Goal Mode ändern → Gewichtung korrekt?
- [ ] Primäres Ziel setzen → Andere auf false?
- [ ] Lean Mass Ziel → Current Value berechnet?
- [ ] VO2Max Ziel → Kein Server Error?
- [ ] Mehrere Ziele → Progress korrekt?
---
## 📅 Timeline
| Datum | Event |
|-------|-------|
| 26.03.2026 | Phase 0a Complete |
| 27.03.2026 | Phase 1 Complete (Quick Fixes) |
| 28.03.2026 | **Phase 0b Start (geplant)** |
| 02.04.2026 | Phase 0b Complete (geschätzt bei 4h/Tag) |
| 04.04.2026 | v2.0 Redesign (wenn validiert) |
---
## 🔧 Flexibles Goal System - Technische Details
### Architektur: DB-Registry Pattern
**Vorher (Phase 0a/1):**
```javascript
// Frontend: Hardcoded
const GOAL_TYPES = {
weight: { label: 'Gewicht', unit: 'kg', icon: '⚖️' }
}
// Backend: Hardcoded if/elif
if goal_type == 'weight':
cur.execute("SELECT weight FROM weight_log...")
elif goal_type == 'body_fat':
cur.execute("SELECT body_fat_pct FROM caliper_log...")
```
**Nachher (Phase 1.5):**
```sql
-- Datenbank: Konfigurierbare Goal Types
CREATE TABLE goal_type_definitions (
type_key VARCHAR(50) UNIQUE,
label_de VARCHAR(100),
unit VARCHAR(20),
icon VARCHAR(10),
category VARCHAR(50),
source_table VARCHAR(50),
source_column VARCHAR(50),
aggregation_method VARCHAR(20), -- latest, avg_7d, count_7d, etc.
calculation_formula TEXT, -- JSON für komplexe Berechnungen
is_system BOOLEAN -- System-Typen nicht löschbar
);
```
```python
# Backend: Universal Fetcher
def get_current_value_for_goal(conn, profile_id, goal_type):
"""Liest Config aus DB, führt Query aus"""
config = get_goal_type_config(conn, goal_type)
if config['calculation_formula']:
return execute_formula(conn, profile_id, config['calculation_formula'])
else:
return fetch_by_method(
conn, profile_id,
config['source_table'],
config['source_column'],
config['aggregation_method']
)
```
```javascript
// Frontend: Dynamisch
const goalTypes = await api.getGoalTypeDefinitions()
// Lädt aktuell verfügbare Typen von API
```
### Vorteile:
**Flexibilität:**
- ✅ Neue Ziele via Admin UI (KEIN Code-Deploy)
- ✅ User kann Custom-Metriken definieren
- ✅ Zukünftige Module automatisch integriert
**Beispiele neuer Ziele:**
- 🧘 Meditation (min/Tag) → `meditation_log.duration_minutes`, avg_7d
- 📅 Trainingshäufigkeit (x/Woche) → `activity_log.id`, count_7d
- 📊 Planabweichung (%) → `activity_log.planned_vs_actual`, avg_30d
- 🎯 Ritual-Adherence (%) → `rituals_log.completed`, avg_30d
- 💤 Schlafqualität (%) → `sleep_log.quality_score`, avg_7d
**Integration mit Phase 0b:**
- Platzhalter nutzen `get_current_value_for_goal()` → automatisch alle Typen verfügbar
- Neue Ziele → sofort in KI-Analysen nutzbar
- Keine Platzhalter-Anpassungen nötig
---
**Letzte Aktualisierung:** 27. März 2026 (Phase 1.5 ✅ ABGESCHLOSSEN)
**Nächste Aktualisierung:** Nach Phase 0b Completion
---
## 🎉 Phase 1.5 Completion Report (27.03.2026)
**Commits:**
- `65ee5f8` - Phase 1.5 Part 1/2 (Backend, Migration, Universal Fetcher)
- `640ef81` - Phase 1.5 Part 2/2 (Frontend Dynamic, Admin UI) - **COMPLETE**
**Implementiert:**
1. ✅ DB-Registry für Goal Types (8 System Types seeded)
2. ✅ Universal Value Fetcher (8 Aggregationsmethoden)
3. ✅ CRUD API (admin-only, System Types geschützt)
4. ✅ Dynamic Frontend (keine hardcoded Types mehr)
5. ✅ Admin UI (vollständiges CRUD Interface)
**System ist jetzt flexibel:**
- Neue Goal Types via UI ohne Code-Deploy
- Phase 0b Platzhalter nutzen automatisch alle Types
- Custom Metrics möglich (Meditation, Rituale, etc.)
**Ready für Phase 0b:** 120+ Goal-Aware Placeholders 🚀

View File

@ -0,0 +1,245 @@
# Phase 0a: Minimal Goal System (Strategic + Tactical)
**Status:** ✅ ABGESCHLOSSEN (26.03.2026)
**Labels:** feature, enhancement, goal-system
**Priority:** High (Foundation for Phase 0b)
**Aufwand:** 3-4h (geschätzt) / ~4h (tatsächlich)
---
## Beschreibung
Implementierung des minimalen Zielsystems als Basis für goal-aware KI-Analysen. Zwei-Ebenen-Architektur:
- **Strategic Layer:** Goal Modes (beeinflusst Score-Gewichtung)
- **Tactical Layer:** Konkrete Zielwerte mit Progress-Tracking
---
## Implementiert ✅
### Strategic Layer (Goal Modes)
- `goal_mode` in `profiles` table
- 5 Modi: `weight_loss`, `strength`, `endurance`, `recomposition`, `health`
- Bestimmt Score-Gewichtung für alle KI-Analysen
- **UI:** 5 Goal Mode Cards mit Beschreibungen und Icons
### Tactical Layer (Concrete Goals)
- `goals` table mit vollständigem Tracking:
- Target/Current/Start values
- Progress percentage (auto-calculated)
- Projection date & on-track status
- Primary/Secondary goal concept
- 8 Goal-Typen: weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr
- **UI:**
- Goal CRUD mit Fortschrittsbalken
- Mobile-friendly Design (full-width inputs, labels above fields)
- Inline editing vorbereitet
### Training Phases Framework
- `training_phases` table (Auto-Detection vorbereitet für Phase 2)
- 5 Phase-Typen: calorie_deficit, calorie_surplus, deload, maintenance, periodization
- Status-Flow: suggested → accepted → active → completed → rejected
- Confidence scoring für KI-basierte Erkennung
- JSONB detection_params für Flexibilität
### Fitness Tests
- `fitness_tests` table für standardisierte Tests
- 8 Test-Typen: cooper_12min, step_test, pushups_max, plank_max, flexibility_sit_reach, vo2max_est, strength_1rm_squat, strength_1rm_bench
- Norm-Kategorisierung vorbereitet (age/gender-spezifisch)
- Baseline-Tracking für Fortschrittsmessung
---
## Technische Umsetzung
### Backend
**Migration 022:** `backend/migrations/022_goal_system.sql`
```sql
-- Strategic Layer
ALTER TABLE profiles ADD COLUMN goal_mode VARCHAR(50) DEFAULT 'health';
-- Tactical Layer
CREATE TABLE goals (...);
CREATE TABLE training_phases (...);
CREATE TABLE fitness_tests (...);
```
**Router:** `backend/routers/goals.py` (490 Zeilen)
- Vollständiges CRUD für alle 3 Ebenen
- Progress calculation (auto-update current values)
- Linear projection für target_date
- Helper functions für goal-type spezifische Current-Values
**API Endpoints:** `/api/goals/*`
- `GET/PUT /mode` - Strategic goal mode
- `GET /list` - All goals with progress
- `POST /create` - Create goal
- `PUT /{id}` - Update goal
- `DELETE /{id}` - Delete goal
- `GET/POST /phases` - Training phases
- `PUT /phases/{id}/status` - Accept/reject auto-detected phases
- `GET/POST /tests` - Fitness tests
### Frontend
**GoalsPage:** `frontend/src/pages/GoalsPage.jsx` (570 Zeilen)
- **Goal Mode Selector:** 5 Karten mit Icons, Farben, Beschreibungen
- **Goal List:** Cards mit Progress-Balken, Projection-Display, Edit/Delete
- **Goal Form:** Mobile-optimiertes Modal
- Full-width inputs
- Labels above fields (not beside)
- Section headers with emoji (🎯 Zielwert)
- Unit display as styled badge
- Primary goal checkbox in highlighted section
- Text-align: left für Text-Felder, right für Zahlen
- **Empty State:** Placeholder mit CTA
**Navigation Integration:**
- **Dashboard:** Goals Preview Card mit "Verwalten →" Link
- **Analysis Page:** 🎯 Ziele Button neben Titel (direkter Zugang)
- **Route:** `/goals` in App.jsx registriert
**api.js:** 15+ neue API-Funktionen
```javascript
// Goal Modes
getGoalMode(), updateGoalMode(mode)
// Goals CRUD
listGoals(), createGoal(data), updateGoal(id, data), deleteGoal(id)
// Training Phases
listTrainingPhases(), createTrainingPhase(data), updatePhaseStatus(id, status)
// Fitness Tests
listFitnessTests(), createFitnessTest(data)
```
---
## Commits
| Commit | Beschreibung |
|--------|-------------|
| `337667f` | feat: Phase 0a - Minimal Goal System (Strategic + Tactical) |
| `906a3b7` | fix: Migration 022 - remove invalid schema_migrations tracking |
| `75f0a5d` | refactor: mobile-friendly goal form design |
| `5be52bc` | feat: goals navigation + UX improvements |
**Branch:** `develop`
**Deployed to:** `dev.mitai.jinkendo.de`
---
## Dokumentation
- ✅ `docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md` (538 Zeilen)
- Analyse beider Fachkonzepte (Konzept v2 + GOALS_VITALS.md)
- Zwei-Ebenen-Architektur erklärt
- 120+ Placeholder-Kategorisierung für Phase 0b
- ✅ Migration 022 mit vollständigen COMMENT ON statements
- ✅ API-Dokumentation in Router-Docstrings
- ✅ Dieses Issue-Dokument
---
## Basis für Phase 0b
Phase 0a bietet die Foundation für:
### Phase 0b: Goal-Aware Placeholders (16-20h)
- ✅ 120+ neue Platzhalter die `goal_mode` berücksichtigen
- ✅ Score-Berechnungen abhängig von Strategic Layer
- ✅ Baseline-Berechnungen (7d/28d/90d Trends)
- ✅ Lag-basierte Korrelationen
- ✅ Confidence Scoring
**Beispiel Goal-Mode Impact:**
```python
# Gleiche Daten, unterschiedliche Interpretation:
Δ: -5kg FM, -2kg LBM
goal_mode = "weight_loss"
→ body_progress_score = 78/100 (FM↓ gut, LBM↓ tolerierbar)
goal_mode = "strength"
→ body_progress_score = 32/100 (LBM↓ ist KATASTROPHE!)
goal_mode = "health"
→ body_progress_score = 50/100 (neutral, ohne Bias)
```
---
## Testing
✅ Migration erfolgreich auf dev.mitai.jinkendo.de
✅ Goal Mode wechselbar
✅ Goal CRUD funktioniert
✅ Progress calculation korrekt
✅ Mobile UI responsive
✅ Navigation von Dashboard + Analysis
**Manuelle Tests durchgeführt:**
- [x] Goal Mode ändern
- [x] Ziel erstellen (alle 8 Typen)
- [x] Ziel bearbeiten
- [x] Ziel löschen
- [x] Primary Goal setzen
- [x] Progress-Balken korrekt
- [x] Mobile UI full-width
- [x] Text-Align korrekt
---
## Akzeptanzkriterien
- [x] Migration 022 erfolgreich
- [x] Goal Mode in profiles funktioniert
- [x] Goals CRUD vollständig
- [x] Progress-Tracking funktioniert
- [x] Primary Goal Konzept implementiert
- [x] Mobile-friendly UI
- [x] Navigation von 2+ Stellen
- [x] API-Dokumentation vollständig
- [x] Frontend form validation
- [x] Error handling korrekt
---
## Nächste Schritte
**Empfohlen:**
1. **Option A: Issue #49 - Prompt Page Assignment (6-8h)**
- Prompts auf Verlaufsseiten zuordnen
- Quick Win für bessere UX
- Nutzt bestehendes Unified Prompt System
2. **Option B: Phase 0b - Goal-Aware Placeholders (16-20h)**
- 120+ neue Platzhalter
- Score-Berechnungen mit goal_mode
- Größter strategischer Impact
**Siehe:** `docs/NEXT_STEPS_2026-03-26.md` für detaillierte Planung
---
## Lessons Learned
### Was gut lief:
- ✅ Zwei-Ebenen-Architektur (Strategic + Tactical) macht Sinn
- ✅ Mobile-first Design von Anfang an
- ✅ Unified Analysis vor Implementierung (beide Fachkonzepte)
- ✅ Migration-System funktioniert einwandfrei
### Was zu beachten ist:
- ⚠️ Schema_migrations verwendet `filename`, nicht `version`
- ⚠️ Unnötige DO-Blocks in Migrationen vermeiden
- ⚠️ Text-align: right als Default in form-input (für Textfelder überschreiben)
---
**Erstellt:** 26. März 2026
**Status:** ✅ COMPLETE - Ready for Phase 0b
**Related Issues:** #49 (Prompt Assignment), #47 (Value Table Refinement)

View File

@ -0,0 +1,425 @@
# Feature: Prompt-Zuordnung zu Verlaufsseiten
**Labels:** feature, ux, enhancement
**Priority:** Medium (Phase 1-2)
**Related:** Issue #28 (Unified Prompt System - Complete)
## Beschreibung
KI-Prompts sollen flexibel auf verschiedenen Verlaufsseiten verfügbar gemacht werden können. Jeder Prompt kann auf mehreren Seiten gleichzeitig angeboten werden (Mehrfachauswahl).
## Problem (aktueller Stand)
**Aktuell:**
- Prompts sind nur über die zentrale Analyse-Seite (📊 Analyse) verfügbar
- Kein kontextbezogener Zugriff auf relevante Analysen
- User muss immer zur Analyse-Seite navigieren
**Beispiel-Szenario:**
```
User ist auf: Gewicht → Verlauf
Will: Gewichtstrend analysieren
Muss: Zur Analyse-Seite → Prompt auswählen → Zurück
```
**Wünschenswert:**
```
User ist auf: Gewicht → Verlauf
Sieht: "🤖 KI-Analyse" Button mit relevanten Prompts
Kann: Direkt "Gewichtstrend-Analyse" starten
```
## Gewünschtes Verhalten
### 1. Prompt-Konfiguration erweitern
**Admin → KI-Prompts → Prompt bearbeiten:**
```
┌─────────────────────────────────────────────┐
│ Prompt bearbeiten: Gewichtstrend-Analyse │
├─────────────────────────────────────────────┤
│ Name: Gewichtstrend-Analyse │
│ Slug: weight_trend │
│ Type: Pipeline │
│ │
│ 📍 Verfügbar auf Seiten: │
│ ┌─────────────────────────────────────────┐ │
│ │ ☑ Analyse (Hauptseite) │ │
│ │ ☑ Gewicht → Verlauf │ │
│ │ ☐ Umfänge → Verlauf │ │
│ │ ☐ Caliper → Verlauf │ │
│ │ ☐ Aktivität → Verlauf │ │
│ │ ☐ Ernährung → Verlauf │ │
│ │ ☐ Schlaf → Verlauf │ │
│ │ ☐ Vitalwerte → Verlauf │ │
│ │ ☐ Dashboard │ │
│ └─────────────────────────────────────────┘ │
│ │
│ [Speichern] [Abbrechen] │
└─────────────────────────────────────────────┘
```
**Mehrfachauswahl:**
- Ein Prompt kann auf mehreren Seiten gleichzeitig verfügbar sein
- Mindestens eine Seite muss ausgewählt sein
- Default: "Analyse (Hauptseite)" ist immer vorausgewählt
### 2. UI auf Verlaufsseiten
**Gewicht → Verlauf:**
```
┌─────────────────────────────────────────────┐
│ 📊 Gewicht - Verlauf │
│ [Filter: 7d] [30d] [90d] [Alle] │
├─────────────────────────────────────────────┤
│ │
│ [Chart: Gewichtsverlauf] │
│ │
├─────────────────────────────────────────────┤
│ 🤖 KI-Analysen │
│ ┌─────────────────────────────────────────┐ │
│ │ Gewichtstrend-Analyse [▶ Starten]│ │
│ │ Körperkomposition-Check [▶ Starten]│ │
│ └─────────────────────────────────────────┘ │
│ │
│ [Einträge-Tabelle...] │
└─────────────────────────────────────────────┘
```
**Features:**
- Kompaktes Widget unterhalb des Charts
- Nur relevante Prompts werden angezeigt
- Button startet Analyse inline (Modal oder expandierend)
- Ergebnis wird direkt auf der Seite angezeigt
### 3. Inline-Analyse Anzeige
**Option A: Modal (empfohlen für MVP):**
```
Click auf [▶ Starten]
┌─────────────────────────────────────────────┐
│ ✕ Gewichtstrend-Analyse │
├─────────────────────────────────────────────┤
│ [Spinner] Analysiere Gewichtsdaten... │
│ │
│ [Nach Abschluss:] │
│ Analyse-Text... │
│ │
│ 📊 Verwendete Werte (12) [🔬 Experten] │
│ [Value Table...] │
│ │
│ [Schließen] [In Verlauf speichern] │
└─────────────────────────────────────────────┘
```
**Option B: Expandierend (später):**
```
Click auf [▶ Starten]
Widget expandiert nach unten
Zeigt Analyse-Ergebnis inline
[△ Einklappen] Button
```
## Technische Umsetzung
### 1. Datenbankschema erweitern
**Tabelle: `ai_prompts`**
```sql
ALTER TABLE ai_prompts ADD COLUMN available_on JSONB DEFAULT '["analysis"]';
COMMENT ON COLUMN ai_prompts.available_on IS
'Array of page slugs where prompt is available.
Values: analysis, weight_history, circ_history, caliper_history,
activity_history, nutrition_history, sleep_history, vitals_history, dashboard';
-- Migration 022
```
**Beispiel-Werte:**
```json
{
"slug": "weight_trend",
"name": "Gewichtstrend-Analyse",
"available_on": ["analysis", "weight_history"]
}
{
"slug": "pipeline_master",
"name": "Vollständige Analyse",
"available_on": ["analysis", "dashboard"]
}
{
"slug": "nutrition_check",
"name": "Ernährungs-Check",
"available_on": ["analysis", "nutrition_history", "activity_history"]
}
```
### 2. Backend API erweitern
**Neuer Endpoint: GET /api/prompts/for-page/{page_slug}**
```python
@router.get("/for-page/{page_slug}")
def get_prompts_for_page(page_slug: str, session: dict = Depends(require_auth)):
"""Get all prompts available for a specific page.
Args:
page_slug: Page identifier (e.g., 'weight_history', 'analysis')
Returns:
List of prompts with available_on containing page_slug
"""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"""SELECT id, name, slug, type, description, available_on
FROM ai_prompts
WHERE available_on @> %s
ORDER BY name""",
(json.dumps([page_slug]),)
)
return [r2d(row) for row in cur.fetchall()]
```
**Beispiel-Aufruf:**
```javascript
// In WeightPage.jsx
const prompts = await api.getPromptsForPage('weight_history')
// Returns: [{slug: 'weight_trend', name: 'Gewichtstrend-Analyse', ...}]
```
**Prompt CRUD erweitern:**
```python
@router.put("/unified/{id}")
def update_unified_prompt(id: str, p: UnifiedPromptCreate, session=Depends(require_admin)):
# ... existing code ...
cur.execute(
"""UPDATE ai_prompts
SET name=%s, slug=%s, template=%s, ..., available_on=%s
WHERE id=%s""",
(..., json.dumps(p.available_on), id)
)
```
### 3. Frontend: Prompt-Editor erweitern
**UnifiedPromptModal.jsx:**
```javascript
const PAGE_OPTIONS = [
{ value: 'analysis', label: '📊 Analyse (Hauptseite)', default: true },
{ value: 'weight_history', label: '⚖️ Gewicht → Verlauf' },
{ value: 'circ_history', label: '📏 Umfänge → Verlauf' },
{ value: 'caliper_history', label: '📐 Caliper → Verlauf' },
{ value: 'activity_history', label: '🏃 Aktivität → Verlauf' },
{ value: 'nutrition_history', label: '🍎 Ernährung → Verlauf' },
{ value: 'sleep_history', label: '😴 Schlaf → Verlauf' },
{ value: 'vitals_history', label: '❤️ Vitalwerte → Verlauf' },
{ value: 'dashboard', label: '🏠 Dashboard' },
]
// In form:
<div className="form-row">
<label>Verfügbar auf Seiten</label>
<div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
{PAGE_OPTIONS.map(opt => (
<label key={opt.value} style={{display: 'flex', gap: 8, alignItems: 'center'}}>
<input
type="checkbox"
checked={availableOn.includes(opt.value)}
onChange={e => {
if (e.target.checked) {
setAvailableOn([...availableOn, opt.value])
} else {
// Don't allow unchecking all
if (availableOn.length > 1) {
setAvailableOn(availableOn.filter(v => v !== opt.value))
}
}
}}
/>
{opt.label}
</label>
))}
</div>
</div>
```
### 4. Frontend: Verlaufsseiten erweitern
**WeightPage.jsx (Beispiel):**
```javascript
function WeightPage() {
const [prompts, setPrompts] = useState([])
const [runningAnalysis, setRunningAnalysis] = useState(null)
const [analysisResult, setAnalysisResult] = useState(null)
useEffect(() => {
loadPrompts()
}, [])
const loadPrompts = async () => {
try {
const data = await api.getPromptsForPage('weight_history')
setPrompts(data)
} catch(e) {
console.error('Failed to load prompts:', e)
}
}
const runAnalysis = async (promptSlug) => {
setRunningAnalysis(promptSlug)
try {
const result = await api.executePrompt(promptSlug, {save: true})
setAnalysisResult(result)
} catch(e) {
setError(e.message)
} finally {
setRunningAnalysis(null)
}
}
return (
<div className="page">
<h1>Gewicht - Verlauf</h1>
{/* Chart */}
<WeightChart data={data} />
{/* AI Prompts Widget */}
{prompts.length > 0 && (
<div className="ai-prompts-widget">
<h3>🤖 KI-Analysen</h3>
{prompts.map(p => (
<button
key={p.slug}
onClick={() => runAnalysis(p.slug)}
disabled={runningAnalysis === p.slug}
>
{p.name} {runningAnalysis === p.slug ? '⏳' : '▶'}
</button>
))}
</div>
)}
{/* Analysis Result Modal */}
{analysisResult && (
<AnalysisResultModal
result={analysisResult}
onClose={() => setAnalysisResult(null)}
/>
)}
{/* Data Table */}
<DataTable entries={entries} />
</div>
)
}
```
**Wiederverwendbare Komponente:**
```javascript
// components/PagePrompts.jsx
export function PagePrompts({ pageSlug }) {
// ... logic ...
return (
<div className="page-prompts">
<h3>🤖 KI-Analysen</h3>
{prompts.map(p => (
<PromptButton key={p.slug} prompt={p} onRun={runAnalysis} />
))}
</div>
)
}
// Usage in any page:
<PagePrompts pageSlug="weight_history" />
```
## Akzeptanzkriterien
- [ ] DB-Migration: `available_on` JSONB column in ai_prompts
- [ ] Backend: `GET /api/prompts/for-page/{page_slug}` Endpoint
- [ ] Backend: CRUD operations unterstützen available_on
- [ ] Frontend: Prompt-Editor zeigt Page-Auswahl (Mehrfachauswahl)
- [ ] Frontend: Mindestens 1 Page muss ausgewählt sein
- [ ] Frontend: Wiederverwendbare PagePrompts Komponente
- [ ] Frontend: Integration in mind. 2 Verlaufsseiten (Weight, Nutrition)
- [ ] UI: Inline-Analyse via Modal mit Value Table
- [ ] UI: Loading-State während Analyse läuft
- [ ] Dokumentation: API-Dokumentation aktualisiert
## Abschätzung
**Aufwand:** 6-8 Stunden
- 1h: DB-Migration + Backend Endpoint
- 2h: Prompt-Editor erweitern (Page-Auswahl)
- 2h: PagePrompts Komponente + Modal
- 2h: Integration in Verlaufsseiten (2-3 Seiten)
- 1h: Testing + Feintuning
**Priorität:** Medium
- Verbessert UX erheblich (kontextbezogene Analysen)
- Nutzt bestehendes Prompt-System (Issue #28)
- Relativ einfach zu implementieren (kein neues Backend-System)
## Use Cases
### UC1: Gewichtstrend auf Gewicht-Seite
```
User: Navigiert zu "Gewicht → Verlauf"
System: Zeigt Gewichts-Chart + verfügbare Prompts
User: Click "Gewichtstrend-Analyse ▶"
System: Startet Analyse, zeigt Modal mit Ergebnis
User: Click "In Verlauf speichern"
System: Speichert in ai_insights, zeigt in Analyse-Verlauf
```
### UC2: Ernährungs-Check auf Ernährung-Seite
```
User: Navigiert zu "Ernährung → Verlauf"
System: Zeigt Ernährungs-Charts + verfügbare Prompts
User: Click "Ernährungs-Check ▶"
System: Analysiert Makros + Kalorien der letzten 7 Tage
User: Sieht Empfehlungen direkt auf Ernährungs-Seite
```
### UC3: Multi-Page Prompt (z.B. "Vollständige Analyse")
```
Admin: Konfiguriert "Vollständige Analyse"
- Verfügbar auf: [Analyse, Dashboard, Gewicht, Ernährung]
User: Sieht denselben Prompt auf 4 verschiedenen Seiten
User: Kann von überall die gleiche umfassende Analyse starten
```
## Notizen
- **Rückwärtskompatibilität:** Bestehende Prompts ohne `available_on` → Default `["analysis"]`
- **Migration:** Alle existierenden Prompts bekommen `["analysis"]` gesetzt
- **Permissions:** Prompts respektieren weiterhin Feature-Enforcement (ai_calls)
- **Caching:** Prompts könnten gecacht werden (selten geändert)
- **Mobile:** PagePrompts sollte auch auf Mobile gut aussehen (Stack-Layout)
- **Performance:** Lazy-Loading der Prompts (nur laden wenn Seite besucht)
## Erweiterungen (Future)
- **Conditional Display:** Prompts nur anzeigen wenn Daten vorhanden
- Beispiel: "Gewichtstrend" nur wenn min. 3 Gewichts-Einträge
- **Quick Actions:** Direkt-Buttons im Chart (ohne separates Widget)
- **Page-spezifische Variablen:** Automatisch aktuelle Filter übergeben
- Beispiel: Wenn "30d" Filter aktiv → `{{timeframe}}` = 30
- **Prompt-Templates pro Page:** Vordefinierte Vorlagen für jede Seite
- **Favoriten:** User kann Prompts auf Seiten favorisieren (User-spezifisch)
## Verwandte Issues
- #28: Unified Prompt System (Basis für dieses Feature)
- #45: KI Prompt-Optimierer (könnte Page-Kontext nutzen)
- #46: KI Prompt-Ersteller (sollte Page-Auswahl anbieten)

View File

@ -31,10 +31,13 @@ import AdminTrainingTypesPage from './pages/AdminTrainingTypesPage'
import AdminActivityMappingsPage from './pages/AdminActivityMappingsPage'
import 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>

View File

@ -0,0 +1,520 @@
import { useState, useEffect } from 'react'
import { Settings, Plus, Pencil, Trash2, Database } from 'lucide-react'
import { api } from '../utils/api'
export default function AdminGoalTypesPage() {
const [goalTypes, setGoalTypes] = useState([])
const [schemaInfo, setSchemaInfo] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showForm, setShowForm] = useState(false)
const [editingType, setEditingType] = useState(null)
const [toast, setToast] = useState(null)
const [formData, setFormData] = useState({
type_key: '',
label_de: '',
unit: '',
icon: '',
category: 'custom',
source_table: '',
source_column: '',
aggregation_method: 'latest',
filter_conditions: '',
description: ''
})
const CATEGORIES = ['body', 'mind', 'activity', 'nutrition', 'recovery', 'custom']
const AGGREGATION_METHODS = [
{ value: 'latest', label: 'Letzter Wert' },
{ value: 'avg_7d', label: 'Durchschnitt 7 Tage' },
{ value: 'avg_30d', label: 'Durchschnitt 30 Tage' },
{ value: 'sum_30d', label: 'Summe 30 Tage' },
{ value: 'count_7d', label: 'Anzahl 7 Tage' },
{ value: 'count_30d', label: 'Anzahl 30 Tage' },
{ value: 'min_30d', label: 'Minimum 30 Tage' },
{ value: 'max_30d', label: 'Maximum 30 Tage' }
]
useEffect(() => {
loadGoalTypes()
}, [])
const loadGoalTypes = async () => {
setLoading(true)
setError(null)
try {
const [typesData, schema] = await Promise.all([
api.listGoalTypeDefinitions(),
api.getSchemaInfo()
])
console.log('[DEBUG] Loaded goal types:', typesData)
console.log('[DEBUG] Loaded schema info:', schema)
setGoalTypes(typesData || [])
setSchemaInfo(schema || {})
} catch (err) {
console.error('[ERROR] Failed to load goal types:', err)
setError(`Fehler beim Laden der Goal Types: ${err.message || err.toString()}`)
} finally {
setLoading(false)
}
}
const showToast = (message) => {
setToast(message)
setTimeout(() => setToast(null), 2000)
}
const handleCreate = () => {
setEditingType(null)
setFormData({
type_key: '',
label_de: '',
unit: '',
icon: '',
category: 'custom',
source_table: '',
source_column: '',
aggregation_method: 'latest',
filter_conditions: '',
description: ''
})
setShowForm(true)
}
const handleEdit = (type) => {
setEditingType(type.id)
setFormData({
type_key: type.type_key,
label_de: type.label_de,
unit: type.unit,
icon: type.icon || '',
category: type.category || 'custom',
source_table: type.source_table || '',
source_column: type.source_column || '',
aggregation_method: type.aggregation_method || 'latest',
filter_conditions: type.filter_conditions ? JSON.stringify(type.filter_conditions, null, 2) : '',
description: type.description || ''
})
setShowForm(true)
}
const handleSave = async () => {
if (!formData.label_de || !formData.unit) {
setError('Bitte Label und Einheit ausfüllen')
return
}
// Parse filter_conditions from string to JSON
let payload = { ...formData }
if (formData.filter_conditions && formData.filter_conditions.trim()) {
try {
payload.filter_conditions = JSON.parse(formData.filter_conditions)
} catch (e) {
setError('Ungültiges JSON in Filter-Bedingungen')
return
}
} else {
payload.filter_conditions = null
}
try {
if (editingType) {
await api.updateGoalType(editingType, payload)
showToast('✓ Goal Type aktualisiert')
} else {
if (!formData.type_key) {
setError('Bitte eindeutigen Key angeben (z.B. meditation_minutes)')
return
}
await api.createGoalType(payload)
showToast('✓ Goal Type erstellt')
}
await loadGoalTypes()
setShowForm(false)
setError(null)
} catch (err) {
setError(err.message || 'Fehler beim Speichern')
}
}
const handleDelete = async (typeId, typeName, isSystem) => {
if (isSystem) {
if (!confirm(`System Goal Type "${typeName}" deaktivieren? (Nicht löschbar)`)) return
} else {
if (!confirm(`Goal Type "${typeName}" wirklich löschen?`)) return
}
try {
await api.deleteGoalType(typeId)
showToast('✓ Goal Type gelöscht/deaktiviert')
await loadGoalTypes()
} catch (err) {
setError(err.message || 'Fehler beim Löschen')
}
}
if (loading) {
return (
<div className="page">
<div style={{ textAlign: 'center', padding: 40 }}>
<div className="spinner"></div>
</div>
</div>
)
}
return (
<div className="page">
<div className="page-header">
<h1><Database size={24} /> Goal Type Verwaltung</h1>
</div>
{error && (
<div className="card" style={{ background: '#FEF2F2', border: '1px solid #FCA5A5', marginBottom: 16 }}>
<p style={{ color: '#DC2626', margin: 0 }}>{error}</p>
</div>
)}
{toast && (
<div style={{
position: 'fixed',
top: 16,
right: 16,
background: 'var(--accent)',
color: 'white',
padding: '12px 20px',
borderRadius: 8,
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
zIndex: 1000
}}>
{toast}
</div>
)}
<div className="card" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div>
<h2 style={{ margin: 0, marginBottom: 4 }}>Verfügbare Goal Types</h2>
<p style={{ fontSize: 13, color: 'var(--text2)', margin: 0 }}>
{goalTypes.length} Types registriert ({goalTypes.filter(t => t.is_system).length} System, {goalTypes.filter(t => !t.is_system).length} Custom)
</p>
</div>
<button className="btn-primary" onClick={handleCreate}>
<Plus size={16} /> Neuer Type
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{goalTypes.map(type => (
<div
key={type.id}
className="card"
style={{
background: 'var(--surface2)',
padding: 12,
border: type.is_system ? '2px solid var(--accent)' : '1px solid var(--border)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 20 }}>{type.icon || '📊'}</span>
<span style={{ fontWeight: 600 }}>{type.label_de}</span>
<span style={{
fontSize: 11,
padding: '2px 8px',
borderRadius: 4,
background: 'var(--surface)',
color: 'var(--text2)'
}}>
{type.unit}
</span>
{type.is_system && (
<span style={{
fontSize: 11,
padding: '2px 8px',
borderRadius: 4,
background: 'var(--accent)',
color: 'white'
}}>
SYSTEM
</span>
)}
{!type.is_active && (
<span style={{
fontSize: 11,
padding: '2px 8px',
borderRadius: 4,
background: '#F59E0B',
color: 'white'
}}>
INAKTIV
</span>
)}
</div>
<div style={{ fontSize: 12, color: 'var(--text2)' }}>
<strong>Key:</strong> {type.type_key}
{type.source_table && (
<>
{' | '}<strong>Quelle:</strong> {type.source_table}.{type.source_column}
{' | '}<strong>Methode:</strong> {type.aggregation_method}
</>
)}
{type.calculation_formula && (
<>
{' | '}<strong>Formel:</strong> Komplex (JSON)
</>
)}
</div>
{type.description && (
<div style={{ fontSize: 12, color: 'var(--text2)', marginTop: 4, fontStyle: 'italic' }}>
{type.description}
</div>
)}
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button
className="btn-secondary"
onClick={() => handleEdit(type)}
style={{ padding: '6px 12px' }}
>
<Pencil size={14} />
</button>
<button
className="btn-secondary"
onClick={() => handleDelete(type.id, type.label_de, type.is_system)}
style={{ padding: '6px 12px', color: type.is_system ? '#F59E0B' : '#DC2626' }}
>
<Trash2 size={14} />
</button>
</div>
</div>
</div>
))}
</div>
</div>
{/* Form Modal */}
{showForm && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
zIndex: 1000,
padding: 16,
paddingTop: 40,
overflowY: 'auto'
}}>
<div className="card" style={{ maxWidth: 600, width: '100%', marginBottom: 40 }}>
<div className="card-title">
{editingType ? 'Goal Type bearbeiten' : 'Neuer Goal Type'}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* Type Key (nur bei Create) */}
{!editingType && (
<div>
<label className="form-label">
Eindeutiger Key * (z.B. meditation_minutes)
</label>
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.type_key}
onChange={e => setFormData(f => ({ ...f, type_key: e.target.value }))}
placeholder="snake_case verwenden"
/>
</div>
)}
{/* Label */}
<div>
<label className="form-label">Label (Deutsch) *</label>
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.label_de}
onChange={e => setFormData(f => ({ ...f, label_de: e.target.value }))}
placeholder="z.B. Meditation"
/>
</div>
{/* Unit & Icon */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<label className="form-label">Einheit *</label>
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.unit}
onChange={e => setFormData(f => ({ ...f, unit: e.target.value }))}
placeholder="z.B. min/Tag"
/>
</div>
<div>
<label className="form-label">Icon (Emoji)</label>
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.icon}
onChange={e => setFormData(f => ({ ...f, icon: e.target.value }))}
placeholder="🧘"
/>
</div>
</div>
{/* Category */}
<div>
<label className="form-label">Kategorie</label>
<select
className="form-input"
style={{ width: '100%' }}
value={formData.category}
onChange={e => setFormData(f => ({ ...f, category: e.target.value }))}
>
{CATEGORIES.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
{/* Data Source */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div>
<label className="form-label">Tabelle</label>
{schemaInfo ? (
<select
className="form-input"
style={{ width: '100%' }}
value={formData.source_table}
onChange={e => setFormData(f => ({ ...f, source_table: e.target.value, source_column: '' }))}
>
<option value="">-- Optional --</option>
{Object.entries(schemaInfo).map(([table, info]) => (
<option key={table} value={table} title={info.description}>
{table} - {info.description}
</option>
))}
</select>
) : (
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.source_table}
onChange={e => setFormData(f => ({ ...f, source_table: e.target.value }))}
placeholder="Lade Schema..."
disabled
/>
)}
</div>
<div>
<label className="form-label">Spalte</label>
{schemaInfo && formData.source_table && schemaInfo[formData.source_table] ? (
<select
className="form-input"
style={{ width: '100%' }}
value={formData.source_column}
onChange={e => setFormData(f => ({ ...f, source_column: e.target.value }))}
>
<option value="">-- Wählen --</option>
{Object.entries(schemaInfo[formData.source_table].columns).map(([col, info]) => (
<option key={col} value={col} title={info.description}>
{col} - {info.description}
</option>
))}
</select>
) : (
<input
type="text"
className="form-input"
style={{ width: '100%' }}
value={formData.source_column}
onChange={e => setFormData(f => ({ ...f, source_column: e.target.value }))}
placeholder={formData.source_table ? "Spalte wählen..." : "Erst Tabelle wählen"}
disabled={!formData.source_table}
/>
)}
</div>
</div>
{/* Aggregation Method */}
<div>
<label className="form-label">Aggregationsmethode</label>
<select
className="form-input"
style={{ width: '100%' }}
value={formData.aggregation_method}
onChange={e => setFormData(f => ({ ...f, aggregation_method: e.target.value }))}
>
{AGGREGATION_METHODS.map(method => (
<option key={method.value} value={method.value}>{method.label}</option>
))}
</select>
</div>
{/* Filter Conditions */}
<div>
<label className="form-label">Filter (optional, JSON)</label>
<textarea
className="form-input"
style={{ width: '100%', minHeight: 80, fontFamily: 'monospace', fontSize: 13 }}
value={formData.filter_conditions}
onChange={e => setFormData(f => ({ ...f, filter_conditions: e.target.value }))}
placeholder={'Beispiel:\n{\n "training_type": "strength"\n}\n\nOder mehrere Werte:\n{\n "training_type": ["strength", "hiit"]\n}'}
/>
<div style={{ fontSize: 12, color: 'var(--text2)', marginTop: 4 }}>
💡 Filtert Einträge nach Spalten. Beispiel: <code>{`{"training_type": "strength"}`}</code> zählt nur Krafttraining
</div>
</div>
{/* Description */}
<div>
<label className="form-label">Beschreibung (optional)</label>
<textarea
className="form-input"
style={{ width: '100%', minHeight: 60 }}
value={formData.description}
onChange={e => setFormData(f => ({ ...f, description: e.target.value }))}
placeholder="Kurze Erklärung..."
/>
</div>
{/* Buttons */}
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
<button className="btn-primary" onClick={handleSave} style={{ flex: 1 }}>
Speichern
</button>
<button
className="btn-secondary"
onClick={() => {
setShowForm(false)
setError(null)
}}
style={{ flex: 1 }}
>
Abbrechen
</button>
</div>
</div>
</div>
</div>
)}
</div>
)
}

View File

@ -453,7 +453,7 @@ export default function AdminPanel() {
</div>
{/* 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>
)
}

View File

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

View File

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

View File

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

View File

@ -497,6 +497,21 @@ export default function Dashboard() {
</div>
)}
{/* 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}}>

File diff suppressed because it is too large Load Diff

View File

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