feat: Update Gitea issues index and enhance data layer metrics #78

Merged
Lars merged 1 commits from develop into main 2026-04-11 22:17:18 +02:00
7 changed files with 121 additions and 50 deletions

View File

@ -1,6 +1,6 @@
# Gitea Issues Landkarte (Auswertung)
**Quelle:** Gitea `Lars/mitai-jinkendo`, Stand **2026-04-09** (Abfrage `state=all`, ergänzt: #71).
**Quelle:** Gitea `Lars/mitai-jinkendo`, Stand **2026-04-11** (Abfrage `state=all`, ergänzt: #71, #76).
**URL:** http://192.168.2.144:3000/Lars/mitai-jinkendo/issues
Dieses Dokument ist ein **Orientierungs-Index** für Agenten und Entwickler. Verbindliches Tracking bleibt **in Gitea**; hier: Kategorien, Dubletten-Hinweise, grobe Prioritätseinschätzung.
@ -88,6 +88,7 @@ Dieses Dokument ist ein **Orientierungs-Index** für Agenten und Entwickler. Ver
| # | Titel |
|---|--------|
| 15 | [FEAT-002] Quality-Filter für KI-Auswertungen & Charts integrieren |
| 76 | Trainings-Qualität: zielbezogene Logik + Listen-Filter statt globalem „Hochwertig“-Hide |
| 36 | BUG-009: Trainingstyp-Erstellung führt zu Internal Server Error |
---

View File

@ -604,26 +604,23 @@ def calculate_activity_score(profile_id: str, focus_weights: Optional[Dict] = No
from data_layer.scores import get_user_focus_weights
focus_weights = get_user_focus_weights(profile_id)
# Activity-related focus areas (English keys from DB)
# Strength training
strength = focus_weights.get('strength', 0)
strength_endurance = focus_weights.get('strength_endurance', 0)
power = focus_weights.get('power', 0)
# Activity-related focus areas (English keys from DB); Gewichte float (kein Decimal×float)
strength = float(focus_weights.get('strength', 0) or 0)
strength_endurance = float(focus_weights.get('strength_endurance', 0) or 0)
power = float(focus_weights.get('power', 0) or 0)
total_strength = strength + strength_endurance + power
# Endurance training
aerobic = focus_weights.get('aerobic_endurance', 0)
anaerobic = focus_weights.get('anaerobic_endurance', 0)
cardiovascular = focus_weights.get('cardiovascular_health', 0)
aerobic = float(focus_weights.get('aerobic_endurance', 0) or 0)
anaerobic = float(focus_weights.get('anaerobic_endurance', 0) or 0)
cardiovascular = float(focus_weights.get('cardiovascular_health', 0) or 0)
total_cardio = aerobic + anaerobic + cardiovascular
# Mobility/Coordination
flexibility = focus_weights.get('flexibility', 0)
mobility = focus_weights.get('mobility', 0)
balance = focus_weights.get('balance', 0)
reaction = focus_weights.get('reaction', 0)
rhythm = focus_weights.get('rhythm', 0)
coordination = focus_weights.get('coordination', 0)
flexibility = float(focus_weights.get('flexibility', 0) or 0)
mobility = float(focus_weights.get('mobility', 0) or 0)
balance = float(focus_weights.get('balance', 0) or 0)
reaction = float(focus_weights.get('reaction', 0) or 0)
rhythm = float(focus_weights.get('rhythm', 0) or 0)
coordination = float(focus_weights.get('coordination', 0) or 0)
total_ability = flexibility + mobility + balance + reaction + rhythm + coordination
total_activity_weight = total_strength + total_cardio + total_ability

View File

@ -445,12 +445,16 @@ def calculate_weight_7d_median(profile_id: str) -> Optional[float]:
ORDER BY date DESC
""", (profile_id,))
weights = [row['weight'] for row in cur.fetchall()]
weights = [
safe_float(row['weight'])
for row in cur.fetchall()
if row['weight'] is not None
]
if len(weights) < 4: # Need at least 4 measurements
return None
return round(statistics.median(weights), 1)
return round(float(statistics.median(weights)), 1)
def calculate_weight_28d_slope(profile_id: str) -> Optional[float]:
@ -478,7 +482,11 @@ def _calculate_weight_slope(profile_id: str, days: int) -> Optional[float]:
ORDER BY date
""", (profile_id, days))
data = [(row['date'], row['weight']) for row in cur.fetchall()]
data = [
(row['date'], safe_float(row['weight']))
for row in cur.fetchall()
if row['weight'] is not None
]
# Need minimum data points based on period
min_points = max(18, int(days * 0.6)) # 60% coverage
@ -488,21 +496,21 @@ def _calculate_weight_slope(profile_id: str, days: int) -> Optional[float]:
# Convert dates to days since start
start_date = data[0][0]
x_values = [(date - start_date).days for date, _ in data]
y_values = [weight for _, weight in data]
y_values = [w for _, w in data]
# Linear regression
# Linear regression (alles float: PostgreSQL numeric → Decimal in Python)
n = len(data)
x_mean = sum(x_values) / n
y_mean = sum(y_values) / n
x_mean = float(sum(x_values)) / n
y_mean = float(sum(y_values)) / n
numerator = sum((x - x_mean) * (y - y_mean) for x, y in zip(x_values, y_values))
denominator = sum((x - x_mean) ** 2 for x in x_values)
numerator = sum(float(x - x_mean) * float(y - y_mean) for x, y in zip(x_values, y_values))
denominator = float(sum((x - x_mean) ** 2 for x in x_values))
if denominator == 0:
return None
slope = numerator / denominator
return round(slope, 4) # kg/day
return round(float(slope), 4) # kg/day
def calculate_goal_projection_date(profile_id: str, goal_id: str) -> Optional[str]:
@ -594,19 +602,24 @@ def _calculate_body_composition_change(profile_id: str, metric: str, days: int)
recent = data[0]
oldest = data[-1]
# Calculate FM and LBM
recent_fm = recent['weight'] * (recent['bf_pct'] / 100)
recent_lbm = recent['weight'] - recent_fm
# Calculate FM and LBM (DB numeric → Decimal; für Regression/Scores nur float)
rw = float(safe_float(recent['weight']) or 0)
ob = float(safe_float(recent['bf_pct']) or 0)
ow = float(safe_float(oldest['weight']) or 0)
obf = float(safe_float(oldest['bf_pct']) or 0)
oldest_fm = oldest['weight'] * (oldest['bf_pct'] / 100)
oldest_lbm = oldest['weight'] - oldest_fm
recent_fm = rw * (ob / 100)
recent_lbm = rw - recent_fm
oldest_fm = ow * (obf / 100)
oldest_lbm = ow - oldest_fm
if metric == 'fm':
change = recent_fm - oldest_fm
else:
change = recent_lbm - oldest_lbm
return round(change, 2)
return round(float(change), 2)
# ── Circumference Calculations ──────────────────────────────────────────────
@ -731,9 +744,9 @@ def calculate_body_progress_score(profile_id: str, focus_weights: Optional[Dict]
from data_layer.scores import get_user_focus_weights
focus_weights = get_user_focus_weights(profile_id)
weight_loss = focus_weights.get('weight_loss', 0)
muscle_gain = focus_weights.get('muscle_gain', 0)
body_recomp = focus_weights.get('body_recomposition', 0)
weight_loss = float(focus_weights.get('weight_loss', 0) or 0)
muscle_gain = float(focus_weights.get('muscle_gain', 0) or 0)
body_recomp = float(focus_weights.get('body_recomposition', 0) or 0)
total_body_weight = weight_loss + muscle_gain + body_recomp

View File

@ -844,14 +844,16 @@ def calculate_nutrition_score(profile_id: str, focus_weights: Optional[Dict] = N
from data_layer.scores import get_user_focus_weights
focus_weights = get_user_focus_weights(profile_id)
# Nutrition-related focus areas (English keys from DB)
protein_intake = focus_weights.get('protein_intake', 0)
calorie_balance = focus_weights.get('calorie_balance', 0)
macro_consistency = focus_weights.get('macro_consistency', 0)
meal_timing = focus_weights.get('meal_timing', 0)
hydration = focus_weights.get('hydration', 0)
# Nutrition-related focus areas (English keys from DB; Gewichte immer float)
protein_intake = float(focus_weights.get('protein_intake', 0) or 0)
calorie_balance = float(focus_weights.get('calorie_balance', 0) or 0)
macro_consistency = float(focus_weights.get('macro_consistency', 0) or 0)
meal_timing = float(focus_weights.get('meal_timing', 0) or 0)
hydration = float(focus_weights.get('hydration', 0) or 0)
total_nutrition_weight = protein_intake + calorie_balance + macro_consistency + meal_timing + hydration
total_nutrition_weight = (
protein_intake + calorie_balance + macro_consistency + meal_timing + hydration
)
if total_nutrition_weight == 0:
return None # No nutrition goals

View File

@ -224,8 +224,8 @@ def calculate_goal_progress_score(profile_id: str) -> Optional[int]:
if total_weight == 0:
return None
# Normalize to 0-100
final_score = total_score / total_weight
# Normalize to 0-100 (Explizit float: Zwischensummen können Decimal aus DB sein)
final_score = float(total_score) / float(total_weight)
return int(final_score)
@ -533,11 +533,19 @@ def calculate_focus_area_progress(profile_id: str, focus_area_id: str) -> Option
if not goals:
return None
# Weighted average by contribution_weight (Numeric → float)
total_progress = sum(
float(g['progress_pct']) * float(g['contribution_weight']) for g in goals
)
total_weight = sum(float(g['contribution_weight']) for g in goals)
# Weighted average; progress_pct darf NULL sein (Ziele ohne quantitative Berechnung)
parts: List[tuple] = []
for g in goals:
pct = g['progress_pct']
if pct is None:
continue
parts.append((float(pct), float(g['contribution_weight'])))
if not parts:
return None
total_progress = sum(p * w for p, w in parts)
total_weight = sum(w for _, w in parts)
return int(total_progress / total_weight) if total_weight > 0 else None

View File

@ -29,6 +29,7 @@ Dieser Ordner ist **immer mit Git versioniert**. Er ergänzt **`.claude/docs/`**
| `issue-53-phase-0c-multi-layer-architecture.md` |
| `issue-54-dynamic-placeholder-system.md` |
| `issue-55-dynamic-aggregation-methods.md` |
| `issue-76-training-quality-goal-list-filter.md` |
| `PHASE_PLAN_RESPONSIVE_UI.md` |
| `REVIEW_OPEN_ISSUES_2026-04-04.md` |
| `UMSETZUNGSPLAN_TRAININGSPROFILE_SPORTSPEZIFISCH_2026-04-05.md` |

View File

@ -0,0 +1,49 @@
# Issue #76: Trainings-Qualität Zielbezug + Listen-Filter
**Gitea:** http://192.168.2.144:3000/Lars/mitai-jinkendo/issues/76
**Status:** OFFEN
**Erstellt:** 11.04.2026
**Typ:** Enhancement (Backend, Frontend, Produkt)
**Verwandt:** #31 (globaler Quality-Filter), #15 (Quality-Filter KI/Charts historisch)
---
## Kurzfassung
Die heutige **`quality_label` / `quality_filter_level`**-Logik soll überdacht werden: nicht nur **Akzeptanz** (formales Label), sondern langfristig **Nutzen für die Zielerreichung**. Parallel: der **globale Profil-Filter** in der **Aktivitätsliste** versteckt u. a. **fehlende Einstufungen** (`NULL`) das erschwert Datenpflege und Transparenz.
---
## Ist-Zustand (technisch)
| Bereich | Filter nach `quality_filter_level`? |
|---------|--------------------------------------|
| `GET` Aktivitätsliste (`routers/activity.py`) | Ja (`get_quality_filter_sql`) |
| KI-Rohkontext (`routers/insights.py`, `_get_profile_data`) | Ja (letzte 90 Zeilen `activity_log`) |
| Data-Layer / Platzhalter / Chart-Aggregate (`data_layer/activity_metrics.py`) | Nein alle Einträge im Zeitfenster |
Bei Stufen ≠ `all` schließt SQL-`IN` typischerweise **`NULL`** und nicht erlaubte Labels aus.
---
## Ziele
1. **Konzept „Qualität“:** Richtung Ziel-/Focus-/Phasenbezug; Umsetzung kann in Folge-Issues zerlegt werden.
2. **Listendarstellung:** Standard soll **alle** relevanten Einträge zeigen; **optionale lokale** Filter (inkl. „ohne Einstufung“), kein unbemerktes globales Ausblenden.
3. **KI/Insights:** Klarstellen, ob gefilterte vs. vollständige Aktivitätsmenge; ggf. konfigurierbar oder im Prompt kenntlich.
4. **Dokumentation:** Kurz festhalten, wo `get_quality_filter_sql` greift und wie das zu den Aggregationen passt.
---
## Code-Referenzen
- `backend/quality_filter.py`
- `backend/routers/activity.py` (`list_activity`)
- `backend/routers/insights.py` (`_get_profile_data`)
- `backend/data_layer/activity_metrics.py`
---
## Akzeptanz (aus Gitea, Checkliste)
Siehe Issue-Body in Gitea #76; bei Umsetzung dort Häkchen setzen und diese Datei ggf. um **Umsetzungsnotizen / Abnahme** ergänzen.