Merge pull request 'feat: Update Gitea issues index and enhance data layer metrics' (#78) from develop into main
Reviewed-on: #78
This commit is contained in:
commit
4d81ea2cf3
|
|
@ -1,6 +1,6 @@
|
||||||
# Gitea Issues – Landkarte (Auswertung)
|
# 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
|
**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.
|
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 |
|
| # | Titel |
|
||||||
|---|--------|
|
|---|--------|
|
||||||
| 15 | [FEAT-002] Quality-Filter für KI-Auswertungen & Charts integrieren |
|
| 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 |
|
| 36 | BUG-009: Trainingstyp-Erstellung führt zu Internal Server Error |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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
|
from data_layer.scores import get_user_focus_weights
|
||||||
focus_weights = get_user_focus_weights(profile_id)
|
focus_weights = get_user_focus_weights(profile_id)
|
||||||
|
|
||||||
# Activity-related focus areas (English keys from DB)
|
# Activity-related focus areas (English keys from DB); Gewichte float (kein Decimal×float)
|
||||||
# Strength training
|
strength = float(focus_weights.get('strength', 0) or 0)
|
||||||
strength = focus_weights.get('strength', 0)
|
strength_endurance = float(focus_weights.get('strength_endurance', 0) or 0)
|
||||||
strength_endurance = focus_weights.get('strength_endurance', 0)
|
power = float(focus_weights.get('power', 0) or 0)
|
||||||
power = focus_weights.get('power', 0)
|
|
||||||
total_strength = strength + strength_endurance + power
|
total_strength = strength + strength_endurance + power
|
||||||
|
|
||||||
# Endurance training
|
aerobic = float(focus_weights.get('aerobic_endurance', 0) or 0)
|
||||||
aerobic = focus_weights.get('aerobic_endurance', 0)
|
anaerobic = float(focus_weights.get('anaerobic_endurance', 0) or 0)
|
||||||
anaerobic = focus_weights.get('anaerobic_endurance', 0)
|
cardiovascular = float(focus_weights.get('cardiovascular_health', 0) or 0)
|
||||||
cardiovascular = focus_weights.get('cardiovascular_health', 0)
|
|
||||||
total_cardio = aerobic + anaerobic + cardiovascular
|
total_cardio = aerobic + anaerobic + cardiovascular
|
||||||
|
|
||||||
# Mobility/Coordination
|
flexibility = float(focus_weights.get('flexibility', 0) or 0)
|
||||||
flexibility = focus_weights.get('flexibility', 0)
|
mobility = float(focus_weights.get('mobility', 0) or 0)
|
||||||
mobility = focus_weights.get('mobility', 0)
|
balance = float(focus_weights.get('balance', 0) or 0)
|
||||||
balance = focus_weights.get('balance', 0)
|
reaction = float(focus_weights.get('reaction', 0) or 0)
|
||||||
reaction = focus_weights.get('reaction', 0)
|
rhythm = float(focus_weights.get('rhythm', 0) or 0)
|
||||||
rhythm = focus_weights.get('rhythm', 0)
|
coordination = float(focus_weights.get('coordination', 0) or 0)
|
||||||
coordination = focus_weights.get('coordination', 0)
|
|
||||||
total_ability = flexibility + mobility + balance + reaction + rhythm + coordination
|
total_ability = flexibility + mobility + balance + reaction + rhythm + coordination
|
||||||
|
|
||||||
total_activity_weight = total_strength + total_cardio + total_ability
|
total_activity_weight = total_strength + total_cardio + total_ability
|
||||||
|
|
|
||||||
|
|
@ -445,12 +445,16 @@ def calculate_weight_7d_median(profile_id: str) -> Optional[float]:
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
""", (profile_id,))
|
""", (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
|
if len(weights) < 4: # Need at least 4 measurements
|
||||||
return None
|
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]:
|
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
|
ORDER BY date
|
||||||
""", (profile_id, days))
|
""", (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
|
# Need minimum data points based on period
|
||||||
min_points = max(18, int(days * 0.6)) # 60% coverage
|
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
|
# Convert dates to days since start
|
||||||
start_date = data[0][0]
|
start_date = data[0][0]
|
||||||
x_values = [(date - start_date).days for date, _ in data]
|
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)
|
n = len(data)
|
||||||
x_mean = sum(x_values) / n
|
x_mean = float(sum(x_values)) / n
|
||||||
y_mean = sum(y_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))
|
numerator = sum(float(x - x_mean) * float(y - y_mean) for x, y in zip(x_values, y_values))
|
||||||
denominator = sum((x - x_mean) ** 2 for x in x_values)
|
denominator = float(sum((x - x_mean) ** 2 for x in x_values))
|
||||||
|
|
||||||
if denominator == 0:
|
if denominator == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
slope = numerator / denominator
|
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]:
|
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]
|
recent = data[0]
|
||||||
oldest = data[-1]
|
oldest = data[-1]
|
||||||
|
|
||||||
# Calculate FM and LBM
|
# Calculate FM and LBM (DB numeric → Decimal; für Regression/Scores nur float)
|
||||||
recent_fm = recent['weight'] * (recent['bf_pct'] / 100)
|
rw = float(safe_float(recent['weight']) or 0)
|
||||||
recent_lbm = recent['weight'] - recent_fm
|
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)
|
recent_fm = rw * (ob / 100)
|
||||||
oldest_lbm = oldest['weight'] - oldest_fm
|
recent_lbm = rw - recent_fm
|
||||||
|
|
||||||
|
oldest_fm = ow * (obf / 100)
|
||||||
|
oldest_lbm = ow - oldest_fm
|
||||||
|
|
||||||
if metric == 'fm':
|
if metric == 'fm':
|
||||||
change = recent_fm - oldest_fm
|
change = recent_fm - oldest_fm
|
||||||
else:
|
else:
|
||||||
change = recent_lbm - oldest_lbm
|
change = recent_lbm - oldest_lbm
|
||||||
|
|
||||||
return round(change, 2)
|
return round(float(change), 2)
|
||||||
|
|
||||||
|
|
||||||
# ── Circumference Calculations ──────────────────────────────────────────────
|
# ── 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
|
from data_layer.scores import get_user_focus_weights
|
||||||
focus_weights = get_user_focus_weights(profile_id)
|
focus_weights = get_user_focus_weights(profile_id)
|
||||||
|
|
||||||
weight_loss = focus_weights.get('weight_loss', 0)
|
weight_loss = float(focus_weights.get('weight_loss', 0) or 0)
|
||||||
muscle_gain = focus_weights.get('muscle_gain', 0)
|
muscle_gain = float(focus_weights.get('muscle_gain', 0) or 0)
|
||||||
body_recomp = focus_weights.get('body_recomposition', 0)
|
body_recomp = float(focus_weights.get('body_recomposition', 0) or 0)
|
||||||
|
|
||||||
total_body_weight = weight_loss + muscle_gain + body_recomp
|
total_body_weight = weight_loss + muscle_gain + body_recomp
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
from data_layer.scores import get_user_focus_weights
|
||||||
focus_weights = get_user_focus_weights(profile_id)
|
focus_weights = get_user_focus_weights(profile_id)
|
||||||
|
|
||||||
# Nutrition-related focus areas (English keys from DB)
|
# Nutrition-related focus areas (English keys from DB; Gewichte immer float)
|
||||||
protein_intake = focus_weights.get('protein_intake', 0)
|
protein_intake = float(focus_weights.get('protein_intake', 0) or 0)
|
||||||
calorie_balance = focus_weights.get('calorie_balance', 0)
|
calorie_balance = float(focus_weights.get('calorie_balance', 0) or 0)
|
||||||
macro_consistency = focus_weights.get('macro_consistency', 0)
|
macro_consistency = float(focus_weights.get('macro_consistency', 0) or 0)
|
||||||
meal_timing = focus_weights.get('meal_timing', 0)
|
meal_timing = float(focus_weights.get('meal_timing', 0) or 0)
|
||||||
hydration = focus_weights.get('hydration', 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:
|
if total_nutrition_weight == 0:
|
||||||
return None # No nutrition goals
|
return None # No nutrition goals
|
||||||
|
|
|
||||||
|
|
@ -224,8 +224,8 @@ def calculate_goal_progress_score(profile_id: str) -> Optional[int]:
|
||||||
if total_weight == 0:
|
if total_weight == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Normalize to 0-100
|
# Normalize to 0-100 (Explizit float: Zwischensummen können Decimal aus DB sein)
|
||||||
final_score = total_score / total_weight
|
final_score = float(total_score) / float(total_weight)
|
||||||
|
|
||||||
return int(final_score)
|
return int(final_score)
|
||||||
|
|
||||||
|
|
@ -533,11 +533,19 @@ def calculate_focus_area_progress(profile_id: str, focus_area_id: str) -> Option
|
||||||
if not goals:
|
if not goals:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Weighted average by contribution_weight (Numeric → float)
|
# Weighted average; progress_pct darf NULL sein (Ziele ohne quantitative Berechnung)
|
||||||
total_progress = sum(
|
parts: List[tuple] = []
|
||||||
float(g['progress_pct']) * float(g['contribution_weight']) for g in goals
|
for g in goals:
|
||||||
)
|
pct = g['progress_pct']
|
||||||
total_weight = sum(float(g['contribution_weight']) for g in goals)
|
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
|
return int(total_progress / total_weight) if total_weight > 0 else None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-53-phase-0c-multi-layer-architecture.md` |
|
||||||
| `issue-54-dynamic-placeholder-system.md` |
|
| `issue-54-dynamic-placeholder-system.md` |
|
||||||
| `issue-55-dynamic-aggregation-methods.md` |
|
| `issue-55-dynamic-aggregation-methods.md` |
|
||||||
|
| `issue-76-training-quality-goal-list-filter.md` |
|
||||||
| `PHASE_PLAN_RESPONSIVE_UI.md` |
|
| `PHASE_PLAN_RESPONSIVE_UI.md` |
|
||||||
| `REVIEW_OPEN_ISSUES_2026-04-04.md` |
|
| `REVIEW_OPEN_ISSUES_2026-04-04.md` |
|
||||||
| `UMSETZUNGSPLAN_TRAININGSPROFILE_SPORTSPEZIFISCH_2026-04-05.md` |
|
| `UMSETZUNGSPLAN_TRAININGSPROFILE_SPORTSPEZIFISCH_2026-04-05.md` |
|
||||||
|
|
|
||||||
49
docs/issues/issue-76-training-quality-goal-list-filter.md
Normal file
49
docs/issues/issue-76-training-quality-goal-list-filter.md
Normal 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.
|
||||||
Loading…
Reference in New Issue
Block a user