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