423 lines
14 KiB
Markdown
423 lines
14 KiB
Markdown
|
|
# Activity Quality Gates – Fachliches Konzept
|
|||
|
|
|
|||
|
|
**Issue:** #15
|
|||
|
|
**Status:** Design Phase
|
|||
|
|
**Erstellt:** 2026-03-23
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Problem-Statement
|
|||
|
|
|
|||
|
|
### Aktuelles Problem:
|
|||
|
|
Apple Health und andere Tracker importieren **alle** Workouts, unabhängig von ihrer Qualität:
|
|||
|
|
- 5-Minuten-Spaziergang → "Outdoor Run"
|
|||
|
|
- 10-Minuten-Krafttraining ohne echte Belastung
|
|||
|
|
- Versehentlich gestartete Workouts
|
|||
|
|
- Aufwärm-Sessions ohne echtes Training
|
|||
|
|
|
|||
|
|
**Folgen:**
|
|||
|
|
- KI-Analysen werden verfälscht ("Du trainierst täglich!" - aber nur 5min)
|
|||
|
|
- Statistiken zeigen unrealistische Trainingsfrequenz
|
|||
|
|
- Korrelationen (Training ↔ Erholung) werden unbrauchbar
|
|||
|
|
- User verliert Vertrauen in die Auswertungen
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fachliche Anforderungen
|
|||
|
|
|
|||
|
|
### Must-Have:
|
|||
|
|
1. **Pro Trainingstyp** unterschiedliche Qualitätskriterien
|
|||
|
|
2. **Automatische Bewertung** beim Import und bei manueller Eingabe
|
|||
|
|
3. **Nicht-destruktiv** - keine Daten löschen, nur markieren
|
|||
|
|
4. **Transparenz** - User sieht warum eine Aktivität als minderwertig gilt
|
|||
|
|
5. **Opt-Out möglich** - User kann Quality Gates pro Aktivität überschreiben
|
|||
|
|
|
|||
|
|
### Nice-to-Have:
|
|||
|
|
6. **Admin-Presets** für gängige Trainingstypen (Laufen, Krafttraining, etc.)
|
|||
|
|
7. **User-spezifische Anpassung** (z.B. für Reha-Patienten andere Schwellwerte)
|
|||
|
|
8. **Historische Nachbearbeitung** - bestehende Aktivitäten neu bewerten
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Lösungsansätze (Evaluation)
|
|||
|
|
|
|||
|
|
### Ansatz A: Quality Flag (Boolean)
|
|||
|
|
```sql
|
|||
|
|
ALTER TABLE activity_log ADD COLUMN is_valid BOOLEAN DEFAULT true;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Pro:**
|
|||
|
|
- Einfach zu implementieren
|
|||
|
|
- Schnelle Queries (`WHERE is_valid = true`)
|
|||
|
|
- Binäre Entscheidung: gültig oder nicht
|
|||
|
|
|
|||
|
|
**Contra:**
|
|||
|
|
- ❌ Keine Abstufungen (was ist mit "grenzwertig"?)
|
|||
|
|
- ❌ Kein Grund dokumentiert (warum ungültig?)
|
|||
|
|
- ❌ Schwer erweiterbar
|
|||
|
|
|
|||
|
|
**Bewertung:** ⭐⭐☆☆☆ (zu simpel)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Ansatz B: Quality Score (0-100)
|
|||
|
|
```sql
|
|||
|
|
ALTER TABLE activity_log ADD COLUMN quality_score INTEGER DEFAULT 100;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Pro:**
|
|||
|
|
- Abstufungen möglich (100 = perfekt, 0 = wertlos)
|
|||
|
|
- Filterbar: `WHERE quality_score >= 70`
|
|||
|
|
- Flexibel für zukünftige Erweiterungen
|
|||
|
|
|
|||
|
|
**Contra:**
|
|||
|
|
- ❌ Score-Berechnung komplex (wie gewichten?)
|
|||
|
|
- ❌ Kein Grund dokumentiert
|
|||
|
|
- ❌ Schwellwert (70? 80?) willkürlich
|
|||
|
|
|
|||
|
|
**Bewertung:** ⭐⭐⭐☆☆ (besser, aber intransparent)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Ansatz C: Validation Result (JSONB) ⭐ **EMPFEHLUNG**
|
|||
|
|
```sql
|
|||
|
|
ALTER TABLE activity_log ADD COLUMN quality_check JSONB DEFAULT NULL;
|
|||
|
|
|
|||
|
|
-- Beispiel-Daten:
|
|||
|
|
{
|
|||
|
|
"evaluated_at": "2026-03-23T10:30:00Z",
|
|||
|
|
"passed": false,
|
|||
|
|
"score": 45,
|
|||
|
|
"reasons": [
|
|||
|
|
{"rule": "duration_min", "expected": 15, "actual": 8, "passed": false},
|
|||
|
|
{"rule": "avg_hr_min", "expected": 100, "actual": 95, "passed": false},
|
|||
|
|
{"rule": "max_hr_min", "expected": 120, "actual": 125, "passed": true}
|
|||
|
|
],
|
|||
|
|
"override": null // User kann auf "valid" oder "invalid" setzen
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Pro:**
|
|||
|
|
- ✅ Transparent: Jede Regel einzeln nachvollziehbar
|
|||
|
|
- ✅ Erweiterbar: Neue Regeln einfach hinzufügbar
|
|||
|
|
- ✅ Override-Mechanismus eingebaut
|
|||
|
|
- ✅ Historisch: Wann wurde evaluiert?
|
|||
|
|
- ✅ Flexibel: Score + Boolean + Details
|
|||
|
|
|
|||
|
|
**Contra:**
|
|||
|
|
- Mehr Speicherplatz (aber marginal bei JSONB)
|
|||
|
|
- Komplexere Queries (aber PostgreSQL JSONB ist schnell)
|
|||
|
|
|
|||
|
|
**Bewertung:** ⭐⭐⭐⭐⭐ **BESTE LÖSUNG**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Empfohlene Lösung: Ansatz C (Validation Result)
|
|||
|
|
|
|||
|
|
### Datenmodell
|
|||
|
|
|
|||
|
|
#### 1. Training Types (Regel-Definition)
|
|||
|
|
```sql
|
|||
|
|
ALTER TABLE training_types ADD COLUMN quality_rules JSONB DEFAULT NULL;
|
|||
|
|
|
|||
|
|
-- Beispiel: Laufen
|
|||
|
|
UPDATE training_types SET quality_rules = '{
|
|||
|
|
"enabled": true,
|
|||
|
|
"rules": {
|
|||
|
|
"duration_min": {"min": 15, "weight": 3},
|
|||
|
|
"avg_hr_min": {"min": 100, "weight": 2},
|
|||
|
|
"max_hr_min": {"min": 120, "weight": 1},
|
|||
|
|
"distance_km": {"min": 1.0, "weight": 1}
|
|||
|
|
},
|
|||
|
|
"pass_threshold": 0.6,
|
|||
|
|
"description": "Mindestens 15min, Durchschnittspuls > 100"
|
|||
|
|
}'::jsonb WHERE name_de = 'Laufen';
|
|||
|
|
|
|||
|
|
-- Beispiel: Krafttraining
|
|||
|
|
UPDATE training_types SET quality_rules = '{
|
|||
|
|
"enabled": true,
|
|||
|
|
"rules": {
|
|||
|
|
"duration_min": {"min": 20, "weight": 5},
|
|||
|
|
"avg_hr_min": {"min": 90, "weight": 1}
|
|||
|
|
},
|
|||
|
|
"pass_threshold": 0.8,
|
|||
|
|
"description": "Mindestens 20 Minuten"
|
|||
|
|
}'::jsonb WHERE name_de = 'Krafttraining';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Erklärung:**
|
|||
|
|
- `enabled`: Quality Gates aktiv für diesen Typ?
|
|||
|
|
- `rules`: Dictionary der Regeln mit Schwellwerten + Gewichtung
|
|||
|
|
- `weight`: Wichtigkeit der Regel (1-5)
|
|||
|
|
- `pass_threshold`: Mindest-Score (0.0-1.0) für "bestanden"
|
|||
|
|
- `description`: User-freundliche Erklärung
|
|||
|
|
|
|||
|
|
#### 2. Activity Log (Validierungs-Ergebnis)
|
|||
|
|
```sql
|
|||
|
|
ALTER TABLE activity_log ADD COLUMN quality_check JSONB DEFAULT NULL;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Struktur siehe Ansatz C oben.**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Validierungs-Logik
|
|||
|
|
|
|||
|
|
### Backend-Funktion: `validate_activity_quality()`
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def validate_activity_quality(activity: dict, training_type: dict) -> dict:
|
|||
|
|
"""
|
|||
|
|
Evaluiert eine Aktivität gegen die Quality Rules des Trainingstyps.
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
{
|
|||
|
|
"evaluated_at": ISO timestamp,
|
|||
|
|
"passed": bool,
|
|||
|
|
"score": float (0.0-1.0),
|
|||
|
|
"reasons": [
|
|||
|
|
{"rule": str, "expected": value, "actual": value, "passed": bool}
|
|||
|
|
],
|
|||
|
|
"override": null | "valid" | "invalid"
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
rules = training_type.get('quality_rules', {})
|
|||
|
|
|
|||
|
|
if not rules or not rules.get('enabled'):
|
|||
|
|
return None # Keine Quality Gates aktiv
|
|||
|
|
|
|||
|
|
results = []
|
|||
|
|
total_weight = 0
|
|||
|
|
passed_weight = 0
|
|||
|
|
|
|||
|
|
for rule_name, rule_config in rules['rules'].items():
|
|||
|
|
weight = rule_config.get('weight', 1)
|
|||
|
|
total_weight += weight
|
|||
|
|
|
|||
|
|
actual_value = activity.get(rule_name)
|
|||
|
|
expected_min = rule_config.get('min')
|
|||
|
|
|
|||
|
|
passed = actual_value is not None and actual_value >= expected_min
|
|||
|
|
|
|||
|
|
if passed:
|
|||
|
|
passed_weight += weight
|
|||
|
|
|
|||
|
|
results.append({
|
|||
|
|
"rule": rule_name,
|
|||
|
|
"expected": expected_min,
|
|||
|
|
"actual": actual_value,
|
|||
|
|
"passed": passed
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
score = passed_weight / total_weight if total_weight > 0 else 1.0
|
|||
|
|
passed = score >= rules.get('pass_threshold', 0.6)
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"evaluated_at": datetime.now().isoformat(),
|
|||
|
|
"passed": passed,
|
|||
|
|
"score": round(score, 2),
|
|||
|
|
"reasons": results,
|
|||
|
|
"override": None
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Wann wird validiert?
|
|||
|
|
|
|||
|
|
1. **Beim Import** (CSV, API)
|
|||
|
|
- Automatisch nach INSERT
|
|||
|
|
- Spalte `quality_check` wird befüllt
|
|||
|
|
|
|||
|
|
2. **Beim manuellen Anlegen**
|
|||
|
|
- Automatisch nach INSERT
|
|||
|
|
- Spalte `quality_check` wird befüllt
|
|||
|
|
|
|||
|
|
3. **Beim Ändern der Quality Rules** (Admin)
|
|||
|
|
- Optionaler Batch-Job: Alle Aktivitäten neu evaluieren
|
|||
|
|
|
|||
|
|
4. **Beim User-Override**
|
|||
|
|
- User setzt `quality_check.override = "valid"` oder `"invalid"`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## User Experience
|
|||
|
|
|
|||
|
|
### 1. Activity-Liste (User-Ansicht)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────┐
|
|||
|
|
│ Aktivitäten (März 2026) [Filter ▼] │
|
|||
|
|
├─────────────────────────────────────────────────────────┤
|
|||
|
|
│ │
|
|||
|
|
│ 🏃 Laufen - 23. März 2026 │
|
|||
|
|
│ 45 Minuten · Ø 142 bpm · 5.2 km │
|
|||
|
|
│ ✅ Hochwertige Aktivität │
|
|||
|
|
│ │
|
|||
|
|
│ 🏃 Laufen - 22. März 2026 ⚠️ │
|
|||
|
|
│ 8 Minuten · Ø 95 bpm · 0.8 km │
|
|||
|
|
│ ⚠️ Niedrige Qualität - wird nicht gezählt │
|
|||
|
|
│ [Details anzeigen ▼] │
|
|||
|
|
│ │
|
|||
|
|
│ → Dauer zu kurz (8 min < 15 min erforderlich) ❌ │
|
|||
|
|
│ → Durchschnittspuls zu niedrig (95 < 100) ❌ │
|
|||
|
|
│ → Maximalpuls OK (125 ≥ 120) ✅ │
|
|||
|
|
│ │
|
|||
|
|
│ [Als gültig markieren] [Als ungültig markieren] │
|
|||
|
|
│ │
|
|||
|
|
└─────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Filter-Optionen:**
|
|||
|
|
- ☑ Hochwertige Aktivitäten
|
|||
|
|
- ☑ Minderwertige Aktivitäten
|
|||
|
|
- ☐ Nur in Statistiken gezählt
|
|||
|
|
- ☐ Nur manuell überschrieben
|
|||
|
|
|
|||
|
|
### 2. Admin-UI (Quality Rules konfigurieren)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────┐
|
|||
|
|
│ Trainingstyp bearbeiten: Laufen 🏃 │
|
|||
|
|
├─────────────────────────────────────────────────────────┤
|
|||
|
|
│ │
|
|||
|
|
│ Kategorie: Cardio │
|
|||
|
|
│ Name (DE): Laufen │
|
|||
|
|
│ Name (EN): Running │
|
|||
|
|
│ │
|
|||
|
|
│ ━━━ Quality Gates ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
|
|||
|
|
│ │
|
|||
|
|
│ ☑ Quality Gates aktiviert │
|
|||
|
|
│ │
|
|||
|
|
│ Mindest-Schwellwerte: │
|
|||
|
|
│ │
|
|||
|
|
│ • Dauer (Minuten) [15] min Gewicht: ●●●○○ │
|
|||
|
|
│ • Ø Herzfrequenz [100] bpm Gewicht: ●●○○○ │
|
|||
|
|
│ • Max. Herzfrequenz [120] bpm Gewicht: ●○○○○ │
|
|||
|
|
│ • Distanz [1.0] km Gewicht: ●○○○○ │
|
|||
|
|
│ │
|
|||
|
|
│ Erfolgs-Schwelle: [60]% (gewichteter Score) │
|
|||
|
|
│ │
|
|||
|
|
│ Beschreibung (User-sichtbar): │
|
|||
|
|
│ "Mindestens 15 Minuten, Durchschnittspuls > 100" │
|
|||
|
|
│ │
|
|||
|
|
│ [Speichern] [Abbrechen] [Alle Aktivitäten neu prüfen]│
|
|||
|
|
│ │
|
|||
|
|
└─────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. Dashboard / Statistiken
|
|||
|
|
|
|||
|
|
**Transparenz:**
|
|||
|
|
```
|
|||
|
|
📊 Training (März 2026)
|
|||
|
|
15 Aktivitäten erfasst
|
|||
|
|
12 hochwertig ✅
|
|||
|
|
3 minderwertig ⚠️ (werden nicht gezählt)
|
|||
|
|
|
|||
|
|
[Details zu minderwertigen Aktivitäten anzeigen]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Betroffene Bereiche (Impact Analysis)
|
|||
|
|
|
|||
|
|
### 1. Aktivitäten-Listen
|
|||
|
|
- **Query-Änderung:**
|
|||
|
|
```sql
|
|||
|
|
-- Alt:
|
|||
|
|
SELECT * FROM activity_log WHERE profile_id = %s
|
|||
|
|
|
|||
|
|
-- Neu (nur hochwertige):
|
|||
|
|
SELECT * FROM activity_log
|
|||
|
|
WHERE profile_id = %s
|
|||
|
|
AND (quality_check IS NULL
|
|||
|
|
OR quality_check->>'passed' = 'true'
|
|||
|
|
OR quality_check->>'override' = 'valid')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. KI-Pipeline
|
|||
|
|
- **insights.py:** Filter bei Daten-Aggregation
|
|||
|
|
- Nur hochwertige Aktivitäten für KI-Prompts
|
|||
|
|
|
|||
|
|
### 3. Charts / Statistiken
|
|||
|
|
- **Training Type Distribution:** Nur hochwertige zählen
|
|||
|
|
- **Korrelations-Charts:** Separate Kurven für "alle" vs "hochwertig"?
|
|||
|
|
|
|||
|
|
### 4. Activity Import (CSV)
|
|||
|
|
- Nach INSERT: `validate_activity_quality()` aufrufen
|
|||
|
|
- Quality Check speichern
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Offene Fragen (für Entscheidung)
|
|||
|
|
|
|||
|
|
### Frage 1: Rückwirkende Evaluierung?
|
|||
|
|
**Option A:** Nur neue Aktivitäten evaluieren
|
|||
|
|
**Option B:** Alle bestehenden Aktivitäten nachträglich evaluieren
|
|||
|
|
|
|||
|
|
**Empfehlung:** Option B mit Batch-Job (einmalig beim Rollout)
|
|||
|
|
|
|||
|
|
### Frage 2: Standard-Verhalten bei fehlenden Quality Rules?
|
|||
|
|
**Option A:** Alle Aktivitäten gelten als hochwertig (NULL = valid)
|
|||
|
|
**Option B:** Alle Aktivitäten gelten als minderwertig (NULL = invalid)
|
|||
|
|
|
|||
|
|
**Empfehlung:** Option A (konservativ, keine Daten-Verlust-Angst)
|
|||
|
|
|
|||
|
|
### Frage 3: User-Override-Berechtigung?
|
|||
|
|
**Option A:** Jeder User kann eigene Aktivitäten überschreiben
|
|||
|
|
**Option B:** Nur Admin kann überschreiben
|
|||
|
|
|
|||
|
|
**Empfehlung:** Option A (User kennt seinen Kontext am besten)
|
|||
|
|
|
|||
|
|
### Frage 4: Wie viele Default-Rules?
|
|||
|
|
**Option A:** Nur für Top 5 Trainingstypen (Laufen, Krafttraining, Radfahren, Schwimmen, HIIT)
|
|||
|
|
**Option B:** Für alle 29 Trainingstypen
|
|||
|
|
|
|||
|
|
**Empfehlung:** Option A (Rest kann Admin/User nachpflegen)
|
|||
|
|
|
|||
|
|
### Frage 5: Quality Check in Liste anzeigen?
|
|||
|
|
**Option A:** Immer sichtbar (Badge/Icon)
|
|||
|
|
**Option B:** Nur bei minderwertigen Aktivitäten
|
|||
|
|
|
|||
|
|
**Empfehlung:** Option B (weniger visuelles Rauschen)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Implementierungs-Reihenfolge (Vorschlag)
|
|||
|
|
|
|||
|
|
### Phase 1: Foundation (MVP)
|
|||
|
|
1. DB-Migration: `quality_rules` + `quality_check` Spalten
|
|||
|
|
2. Backend: `validate_activity_quality()` Funktion
|
|||
|
|
3. Backend: Validierung beim INSERT (activity.py)
|
|||
|
|
4. Admin-UI: Quality Rules konfigurieren (basic)
|
|||
|
|
|
|||
|
|
### Phase 2: User Experience
|
|||
|
|
5. Frontend: Badge/Warning in Activity-Liste
|
|||
|
|
6. Frontend: Details-Ansicht (welche Regeln failed?)
|
|||
|
|
7. Frontend: User-Override (Als gültig/ungültig markieren)
|
|||
|
|
|
|||
|
|
### Phase 3: Integration
|
|||
|
|
8. KI-Pipeline: Filter für hochwertige Aktivitäten
|
|||
|
|
9. Charts: Nur hochwertige zählen
|
|||
|
|
10. Batch-Job: Bestehende Aktivitäten evaluieren
|
|||
|
|
|
|||
|
|
### Phase 4: Polish
|
|||
|
|
11. Admin-UI: Presets für gängige Trainingstypen
|
|||
|
|
12. Filter-Optionen in Activity-Liste
|
|||
|
|
13. Dashboard: Statistik hochwertig vs. minderwertig
|
|||
|
|
|
|||
|
|
**Geschätzter Aufwand:** 4-6 Stunden (mit Tests)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Nächste Schritte
|
|||
|
|
|
|||
|
|
1. **Entscheidung:** Offene Fragen klären
|
|||
|
|
2. **Technisches Design:** DB-Schema + API-Endpoints spezifizieren
|
|||
|
|
3. **Implementation:** Phase 1 starten
|
|||
|
|
4. **Testing:** Mit echten Apple Health Daten testen
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Erstellt:** 2026-03-23
|
|||
|
|
**Review:** Pending User-Feedback
|