feat: Update Gitea issues index and enhance data layer metrics
All checks were successful
Deploy Development / deploy (push) Successful in 1m3s
Build Test / pytest-backend (push) Successful in 9s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s

- Updated the Gitea issues index to reflect the latest state as of 2026-04-11, adding issue #76 to the list.
- Refined data handling in `activity_metrics.py`, `body_metrics.py`, `nutrition_metrics.py`, and `scores.py` to ensure consistent float conversions for calculations, improving accuracy in metric evaluations.
- Enhanced the calculation logic for various metrics to handle potential None values more robustly, ensuring smoother data processing and improved reliability across the application.

These changes improve the clarity of the Gitea issues documentation and enhance the overall accuracy and reliability of health and fitness metrics.
This commit is contained in:
Lars 2026-04-11 22:14:45 +02:00
parent 4868e44882
commit d7cefdd9e9
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.