781 lines
20 KiB
Markdown
781 lines
20 KiB
Markdown
|
|
# Data Layer Extension Guide
|
|||
|
|
|
|||
|
|
**Version:** 1.0
|
|||
|
|
**Erstellt:** 28. März 2026
|
|||
|
|
**Zielgruppe:** Entwickler, Claude Code
|
|||
|
|
**Phase:** Post Phase 0c
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Überblick
|
|||
|
|
|
|||
|
|
Dieser Guide beschreibt, wie man das Data Layer System erweitert mit:
|
|||
|
|
- Neuen Modulen
|
|||
|
|
- Neuen Funktionen in bestehenden Modulen
|
|||
|
|
- Neuen Berechnungslogiken
|
|||
|
|
- Neuen Aggregationsmethoden
|
|||
|
|
|
|||
|
|
**Voraussetzung:** Phase 0c abgeschlossen (Multi-Layer Architecture implementiert)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Modul-Struktur
|
|||
|
|
|
|||
|
|
### Bestehende Module (Phase 0c)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
backend/data_layer/
|
|||
|
|
├── __init__.py # Exports all functions
|
|||
|
|
├── body_metrics.py # Gewicht, FM, LBM, Umfänge
|
|||
|
|
├── nutrition_metrics.py # Kalorien, Protein, Makros
|
|||
|
|
├── activity_metrics.py # Training, Volumen, Abilities
|
|||
|
|
├── recovery_metrics.py # Sleep, RHR, HRV, Recovery Score
|
|||
|
|
├── health_metrics.py # BP, VO2Max, Health Stability
|
|||
|
|
├── goals.py # Active goals, progress
|
|||
|
|
├── correlations.py # Lag-analysis, plateau detection
|
|||
|
|
└── utils.py # Shared: confidence, baseline, outliers
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Modul-Namenskonventionen
|
|||
|
|
|
|||
|
|
- **Singular:** `body_metrics.py` (nicht `bodies_metrics.py`)
|
|||
|
|
- **Domain-focused:** Ein Modul pro fachlichem Bereich
|
|||
|
|
- **Max ~500 Zeilen:** Bei >500 Zeilen → Split erwägen
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Neue Funktion hinzufügen
|
|||
|
|
|
|||
|
|
### Template
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/data_layer/<module>.py
|
|||
|
|
|
|||
|
|
def get_<metric>_data(
|
|||
|
|
profile_id: str,
|
|||
|
|
days: int = 28,
|
|||
|
|
**kwargs
|
|||
|
|
) -> dict:
|
|||
|
|
"""
|
|||
|
|
[Eine Zeile: Was liefert diese Funktion?]
|
|||
|
|
|
|||
|
|
[Optional: Ausführliche Beschreibung der Berechnung]
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
profile_id: User profile ID
|
|||
|
|
days: Analysis window (default 28)
|
|||
|
|
**kwargs: Additional parameters (z.B., goal_mode)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
{
|
|||
|
|
"<field>": <type>, # Main result
|
|||
|
|
"confidence": str, # REQUIRED: "high"/"medium"/"low"/"insufficient"
|
|||
|
|
"data_points": int, # REQUIRED: Number of data points used
|
|||
|
|
"<additional>": <type> # Any additional data
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Confidence Rules:
|
|||
|
|
- "high": >= X points
|
|||
|
|
- "medium": >= Y points
|
|||
|
|
- "low": >= Z points
|
|||
|
|
- "insufficient": < Z points
|
|||
|
|
|
|||
|
|
Example:
|
|||
|
|
>>> data = get_<metric>_data("profile_123", days=28)
|
|||
|
|
>>> print(data['<field>'])
|
|||
|
|
42.0
|
|||
|
|
"""
|
|||
|
|
with get_db() as conn:
|
|||
|
|
cur = get_cursor(conn)
|
|||
|
|
|
|||
|
|
# 1. DATA RETRIEVAL
|
|||
|
|
cur.execute("""
|
|||
|
|
SELECT ...
|
|||
|
|
FROM ...
|
|||
|
|
WHERE profile_id = %s
|
|||
|
|
AND date >= NOW() - INTERVAL '%s days'
|
|||
|
|
ORDER BY date
|
|||
|
|
""", (profile_id, days))
|
|||
|
|
rows = cur.fetchall()
|
|||
|
|
|
|||
|
|
# 2. CONFIDENCE CALCULATION
|
|||
|
|
from data_layer.utils import calculate_confidence
|
|||
|
|
confidence = calculate_confidence(
|
|||
|
|
data_points=len(rows),
|
|||
|
|
days_requested=days,
|
|||
|
|
metric_type="general" # or "correlation" or "trend"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 3. EARLY RETURN IF INSUFFICIENT
|
|||
|
|
if confidence == 'insufficient':
|
|||
|
|
return {
|
|||
|
|
"confidence": "insufficient",
|
|||
|
|
"data_points": len(rows),
|
|||
|
|
# Include all fields with safe defaults
|
|||
|
|
"<field>": 0.0,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 4. CALCULATION
|
|||
|
|
# ... your logic here ...
|
|||
|
|
|
|||
|
|
# 5. RETURN STRUCTURED DATA
|
|||
|
|
return {
|
|||
|
|
"<field>": result,
|
|||
|
|
"confidence": confidence,
|
|||
|
|
"data_points": len(rows),
|
|||
|
|
# Additional fields as needed
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Pflicht-Felder
|
|||
|
|
|
|||
|
|
**Jede Funktion MUSS zurückgeben:**
|
|||
|
|
```python
|
|||
|
|
{
|
|||
|
|
"confidence": str, # "high" | "medium" | "low" | "insufficient"
|
|||
|
|
"data_points": int, # Anzahl verwendeter Datenpunkte
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Warum?**
|
|||
|
|
- Confidence: UI kann User warnen bei niedriger Datenqualität
|
|||
|
|
- Data Points: Debugging + Monitoring
|
|||
|
|
|
|||
|
|
### Optionale Felder (Best Practices)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
{
|
|||
|
|
"first_date": date, # Ältester Datenpunkt
|
|||
|
|
"last_date": date, # Neuester Datenpunkt
|
|||
|
|
"avg": float, # Durchschnitt
|
|||
|
|
"std_dev": float, # Standardabweichung
|
|||
|
|
"min": float, # Minimum
|
|||
|
|
"max": float, # Maximum
|
|||
|
|
"outliers": list[int], # Indices von Ausreißern
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Neue Berechnungslogik hinzufügen
|
|||
|
|
|
|||
|
|
### 1. Statistik-Funktionen (utils.py)
|
|||
|
|
|
|||
|
|
**Wenn du eine neue statistische Berechnung brauchst:**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/data_layer/utils.py
|
|||
|
|
|
|||
|
|
def calculate_<statistic>(
|
|||
|
|
values: list[float],
|
|||
|
|
**kwargs
|
|||
|
|
) -> float:
|
|||
|
|
"""
|
|||
|
|
[Beschreibung der Statistik]
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
values: List of measurements
|
|||
|
|
**kwargs: Additional parameters
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Calculated statistic (float)
|
|||
|
|
|
|||
|
|
Example:
|
|||
|
|
>>> calculate_<statistic>([1.0, 2.0, 3.0])
|
|||
|
|
2.0
|
|||
|
|
"""
|
|||
|
|
# Implementation
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Beispiele:**
|
|||
|
|
```python
|
|||
|
|
def calculate_median_absolute_deviation(values: list[float]) -> float:
|
|||
|
|
"""
|
|||
|
|
MAD = median(|xi - median(x)|)
|
|||
|
|
|
|||
|
|
More robust than standard deviation for outlier detection.
|
|||
|
|
"""
|
|||
|
|
import statistics
|
|||
|
|
median = statistics.median(values)
|
|||
|
|
deviations = [abs(x - median) for x in values]
|
|||
|
|
return statistics.median(deviations)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def calculate_coefficient_of_variation(values: list[float]) -> float:
|
|||
|
|
"""
|
|||
|
|
CV = (std_dev / mean) * 100
|
|||
|
|
|
|||
|
|
Measures relative variability.
|
|||
|
|
"""
|
|||
|
|
import statistics
|
|||
|
|
mean = statistics.mean(values)
|
|||
|
|
std_dev = statistics.stdev(values)
|
|||
|
|
return (std_dev / mean) * 100 if mean != 0 else 0.0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def calculate_z_score(value: float, mean: float, std_dev: float) -> float:
|
|||
|
|
"""
|
|||
|
|
Z = (x - μ) / σ
|
|||
|
|
|
|||
|
|
Standardized score.
|
|||
|
|
"""
|
|||
|
|
return (value - mean) / std_dev if std_dev != 0 else 0.0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Aggregations-Funktionen (utils.py)
|
|||
|
|
|
|||
|
|
**Neue Aggregationsmethoden für Goal Types:**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/data_layer/utils.py
|
|||
|
|
|
|||
|
|
def aggregate_data(
|
|||
|
|
values: list[tuple], # [(date, value), ...]
|
|||
|
|
method: str,
|
|||
|
|
**kwargs
|
|||
|
|
) -> float:
|
|||
|
|
"""
|
|||
|
|
Aggregate data points using specified method.
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
values: List of (date, value) tuples
|
|||
|
|
method: Aggregation method (see below)
|
|||
|
|
**kwargs: Method-specific parameters
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
Aggregated value (float)
|
|||
|
|
|
|||
|
|
Supported Methods:
|
|||
|
|
- "latest": Most recent value
|
|||
|
|
- "avg_7d": Average last 7 days
|
|||
|
|
- "avg_30d": Average last 30 days
|
|||
|
|
- "avg_90d": Average last 90 days
|
|||
|
|
- "sum_7d": Sum last 7 days
|
|||
|
|
- "sum_30d": Sum last 30 days
|
|||
|
|
- "count_7d": Count last 7 days
|
|||
|
|
- "count_30d": Count last 30 days
|
|||
|
|
- "min_30d": Minimum last 30 days
|
|||
|
|
- "max_30d": Maximum last 30 days
|
|||
|
|
- "median_7d": Median last 7 days
|
|||
|
|
- "median_30d": Median last 30 days
|
|||
|
|
- "rolling_avg": Rolling average (window from kwargs)
|
|||
|
|
- "percentile": Nth percentile (n from kwargs)
|
|||
|
|
|
|||
|
|
Example:
|
|||
|
|
>>> values = [(date1, 85.0), (date2, 84.5), ...]
|
|||
|
|
>>> aggregate_data(values, "avg_7d")
|
|||
|
|
84.7
|
|||
|
|
"""
|
|||
|
|
from datetime import date, timedelta
|
|||
|
|
import statistics
|
|||
|
|
|
|||
|
|
if not values:
|
|||
|
|
return 0.0
|
|||
|
|
|
|||
|
|
# Sort by date (most recent first)
|
|||
|
|
sorted_values = sorted(values, key=lambda x: x[0], reverse=True)
|
|||
|
|
|
|||
|
|
if method == "latest":
|
|||
|
|
return float(sorted_values[0][1])
|
|||
|
|
|
|||
|
|
elif method.startswith("avg_"):
|
|||
|
|
days = int(method.split("_")[1].replace("d", ""))
|
|||
|
|
cutoff = date.today() - timedelta(days=days)
|
|||
|
|
recent = [v for d, v in sorted_values if d >= cutoff]
|
|||
|
|
return statistics.mean(recent) if recent else 0.0
|
|||
|
|
|
|||
|
|
elif method.startswith("sum_"):
|
|||
|
|
days = int(method.split("_")[1].replace("d", ""))
|
|||
|
|
cutoff = date.today() - timedelta(days=days)
|
|||
|
|
recent = [v for d, v in sorted_values if d >= cutoff]
|
|||
|
|
return sum(recent)
|
|||
|
|
|
|||
|
|
elif method.startswith("count_"):
|
|||
|
|
days = int(method.split("_")[1].replace("d", ""))
|
|||
|
|
cutoff = date.today() - timedelta(days=days)
|
|||
|
|
return len([v for d, v in sorted_values if d >= cutoff])
|
|||
|
|
|
|||
|
|
elif method.startswith("min_") or method.startswith("max_"):
|
|||
|
|
func_name, days_str = method.split("_")
|
|||
|
|
days = int(days_str.replace("d", ""))
|
|||
|
|
cutoff = date.today() - timedelta(days=days)
|
|||
|
|
recent = [v for d, v in sorted_values if d >= cutoff]
|
|||
|
|
if not recent:
|
|||
|
|
return 0.0
|
|||
|
|
return min(recent) if func_name == "min" else max(recent)
|
|||
|
|
|
|||
|
|
elif method.startswith("median_"):
|
|||
|
|
days = int(method.split("_")[1].replace("d", ""))
|
|||
|
|
cutoff = date.today() - timedelta(days=days)
|
|||
|
|
recent = [v for d, v in sorted_values if d >= cutoff]
|
|||
|
|
return statistics.median(recent) if recent else 0.0
|
|||
|
|
|
|||
|
|
elif method == "rolling_avg":
|
|||
|
|
window = kwargs.get("window", 7)
|
|||
|
|
if len(sorted_values) < window:
|
|||
|
|
return statistics.mean([v for _, v in sorted_values])
|
|||
|
|
recent = sorted_values[:window]
|
|||
|
|
return statistics.mean([v for _, v in recent])
|
|||
|
|
|
|||
|
|
elif method == "percentile":
|
|||
|
|
n = kwargs.get("n", 50) # Default: median
|
|||
|
|
values_only = [v for _, v in sorted_values]
|
|||
|
|
return statistics.quantiles(values_only, n=100)[n - 1] if len(values_only) > 1 else values_only[0]
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
raise ValueError(f"Unknown aggregation method: {method}")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. Korrelations-Funktionen (correlations.py)
|
|||
|
|
|
|||
|
|
**Neue Korrelations-Analysen:**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/data_layer/correlations.py
|
|||
|
|
|
|||
|
|
def get_<metric_a>_<metric_b>_correlation(
|
|||
|
|
profile_id: str,
|
|||
|
|
days: int = 90,
|
|||
|
|
max_lag: int = 7
|
|||
|
|
) -> dict:
|
|||
|
|
"""
|
|||
|
|
Correlation between <metric_a> and <metric_b> with lag analysis.
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
profile_id: User profile ID
|
|||
|
|
days: Analysis window
|
|||
|
|
max_lag: Maximum lag in days to test
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
{
|
|||
|
|
"correlation": float, # Pearson r at best lag
|
|||
|
|
"best_lag": int, # Days of lag
|
|||
|
|
"p_value": float, # Statistical significance
|
|||
|
|
"confidence": str,
|
|||
|
|
"paired_points": int,
|
|||
|
|
"interpretation": str # "strong"/"moderate"/"weak"/"none"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Interpretation:
|
|||
|
|
|r| > 0.7: "strong"
|
|||
|
|
|r| > 0.5: "moderate"
|
|||
|
|
|r| > 0.3: "weak"
|
|||
|
|
|r| <= 0.3: "none"
|
|||
|
|
"""
|
|||
|
|
# Implementation using scipy.stats or numpy
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Neues Modul erstellen
|
|||
|
|
|
|||
|
|
### Wann ein neues Modul?
|
|||
|
|
|
|||
|
|
**Erstelle ein neues Modul wenn:**
|
|||
|
|
- ✅ Neue fachliche Domäne (z.B., `stress_metrics.py`, `hormone_metrics.py`)
|
|||
|
|
- ✅ Bestehendes Modul >500 Zeilen
|
|||
|
|
- ✅ Klare thematische Trennung möglich
|
|||
|
|
|
|||
|
|
**KEIN neues Modul wenn:**
|
|||
|
|
- ❌ Nur 1-2 Funktionen (füge zu bestehendem Modul hinzu)
|
|||
|
|
- ❌ Starke Abhängigkeit zu bestehendem Modul (merge statt split)
|
|||
|
|
|
|||
|
|
### Modul-Template
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/data_layer/<new_module>.py
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
<Module Name> - <Brief description>
|
|||
|
|
|
|||
|
|
This module provides data functions for <domain>.
|
|||
|
|
|
|||
|
|
Functions:
|
|||
|
|
- get_<metric1>_data()
|
|||
|
|
- get_<metric2>_data()
|
|||
|
|
- ...
|
|||
|
|
|
|||
|
|
Usage:
|
|||
|
|
from data_layer.<new_module> import get_<metric>_data
|
|||
|
|
|
|||
|
|
data = get_<metric>_data(profile_id="123", days=28)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from typing import Optional, List, Dict, Tuple
|
|||
|
|
from datetime import date, timedelta
|
|||
|
|
from db import get_db, get_cursor
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── PUBLIC FUNCTIONS ─────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def get_<metric>_data(
|
|||
|
|
profile_id: str,
|
|||
|
|
days: int = 28,
|
|||
|
|
**kwargs
|
|||
|
|
) -> dict:
|
|||
|
|
"""
|
|||
|
|
[Docstring as per template above]
|
|||
|
|
"""
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── PRIVATE HELPERS ──────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _calculate_<internal_metric>(values: list[float]) -> float:
|
|||
|
|
"""
|
|||
|
|
Internal helper for <module>.
|
|||
|
|
|
|||
|
|
NOT exported from module.
|
|||
|
|
"""
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _validate_<data>(data: dict) -> bool:
|
|||
|
|
"""
|
|||
|
|
Internal validation helper.
|
|||
|
|
"""
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Exports in __init__.py
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/data_layer/__init__.py
|
|||
|
|
|
|||
|
|
# Existing modules
|
|||
|
|
from .body_metrics import *
|
|||
|
|
from .nutrition_metrics import *
|
|||
|
|
from .activity_metrics import *
|
|||
|
|
from .recovery_metrics import *
|
|||
|
|
from .health_metrics import *
|
|||
|
|
from .goals import *
|
|||
|
|
from .correlations import *
|
|||
|
|
from .utils import *
|
|||
|
|
|
|||
|
|
# NEW MODULE
|
|||
|
|
from .<new_module> import *
|
|||
|
|
|
|||
|
|
__all__ = [
|
|||
|
|
# Existing exports...
|
|||
|
|
|
|||
|
|
# NEW MODULE exports
|
|||
|
|
'get_<metric1>_data',
|
|||
|
|
'get_<metric2>_data',
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Integration mit Goal Types
|
|||
|
|
|
|||
|
|
### Goal Type mit neuer Aggregationsmethode
|
|||
|
|
|
|||
|
|
**Scenario:** Du hast eine neue Aggregationsmethode `avg_per_week_30d` implementiert.
|
|||
|
|
|
|||
|
|
#### 1. In utils.py implementieren
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/data_layer/utils.py
|
|||
|
|
|
|||
|
|
def aggregate_data(values, method, **kwargs):
|
|||
|
|
# ... existing methods ...
|
|||
|
|
|
|||
|
|
elif method == "avg_per_week_30d":
|
|||
|
|
# Group by week, calculate average per week
|
|||
|
|
from collections import defaultdict
|
|||
|
|
weeks = defaultdict(list)
|
|||
|
|
|
|||
|
|
for d, v in values:
|
|||
|
|
week_start = d - timedelta(days=d.weekday())
|
|||
|
|
weeks[week_start].append(v)
|
|||
|
|
|
|||
|
|
week_avgs = [sum(vals) / len(vals) for vals in weeks.values()]
|
|||
|
|
return sum(week_avgs) / len(week_avgs) if week_avgs else 0.0
|
|||
|
|
|
|||
|
|
# ...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. In goal_utils.py nutzen
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/goal_utils.py
|
|||
|
|
|
|||
|
|
def _fetch_by_aggregation_method(
|
|||
|
|
cur,
|
|||
|
|
profile_id: str,
|
|||
|
|
source_table: str,
|
|||
|
|
source_column: str,
|
|||
|
|
aggregation_method: str,
|
|||
|
|
date_column: str = 'date',
|
|||
|
|
filter_conditions: dict = None
|
|||
|
|
) -> Optional[float]:
|
|||
|
|
"""
|
|||
|
|
Fetch current value using aggregation method.
|
|||
|
|
|
|||
|
|
Now supports:
|
|||
|
|
- latest, avg_7d, avg_30d, sum_30d, count_7d, etc.
|
|||
|
|
- avg_per_week_30d (NEW)
|
|||
|
|
"""
|
|||
|
|
# Fetch data
|
|||
|
|
cur.execute(f"""
|
|||
|
|
SELECT {date_column}, {source_column}
|
|||
|
|
FROM {source_table}
|
|||
|
|
WHERE profile_id = %s
|
|||
|
|
ORDER BY {date_column} DESC
|
|||
|
|
LIMIT 100
|
|||
|
|
""", (profile_id,))
|
|||
|
|
rows = cur.fetchall()
|
|||
|
|
|
|||
|
|
if not rows:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# Use aggregate_data from utils
|
|||
|
|
from data_layer.utils import aggregate_data
|
|||
|
|
return aggregate_data(rows, aggregation_method)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. In Frontend verfügbar machen
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// frontend/src/pages/AdminGoalTypesPage.jsx
|
|||
|
|
|
|||
|
|
const AGGREGATION_METHODS = [
|
|||
|
|
{ value: 'latest', label: 'Aktuellster Wert' },
|
|||
|
|
{ value: 'avg_7d', label: 'Durchschnitt 7 Tage' },
|
|||
|
|
{ value: 'avg_30d', label: 'Durchschnitt 30 Tage' },
|
|||
|
|
{ value: 'sum_30d', label: 'Summe 30 Tage' },
|
|||
|
|
{ value: 'avg_per_week_30d', label: 'Durchschnitt pro Woche (30d)' }, // NEW
|
|||
|
|
// ...
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Testing-Strategie
|
|||
|
|
|
|||
|
|
### Unit Tests für neue Funktionen
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/tests/test_data_layer.py
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
from data_layer.<module> import get_<metric>_data
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def test_profile_with_data(db_connection):
|
|||
|
|
"""Create test profile with sample data"""
|
|||
|
|
# Setup
|
|||
|
|
profile_id = "test_profile_123"
|
|||
|
|
# Insert test data into relevant tables
|
|||
|
|
...
|
|||
|
|
yield profile_id
|
|||
|
|
# Teardown
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_get_metric_data_sufficient(test_profile_with_data):
|
|||
|
|
"""Test with sufficient data points"""
|
|||
|
|
data = get_<metric>_data(test_profile_with_data, days=28)
|
|||
|
|
|
|||
|
|
assert data['confidence'] in ['high', 'medium', 'low']
|
|||
|
|
assert data['data_points'] >= 18
|
|||
|
|
assert '<field>' in data
|
|||
|
|
assert isinstance(data['<field>'], float)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_get_metric_data_insufficient():
|
|||
|
|
"""Test with insufficient data"""
|
|||
|
|
data = get_<metric>_data("no_data_profile", days=28)
|
|||
|
|
|
|||
|
|
assert data['confidence'] == 'insufficient'
|
|||
|
|
assert data['data_points'] == 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_get_metric_data_edge_cases(test_profile_with_data):
|
|||
|
|
"""Test edge cases: outliers, missing values, etc."""
|
|||
|
|
# Test with extreme values
|
|||
|
|
# Test with gaps in data
|
|||
|
|
# Test with all same values
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_get_metric_data_parameters(test_profile_with_data):
|
|||
|
|
"""Test different parameter combinations"""
|
|||
|
|
# Test different days values
|
|||
|
|
for days in [7, 28, 90]:
|
|||
|
|
data = get_<metric>_data(test_profile_with_data, days=days)
|
|||
|
|
assert data is not None
|
|||
|
|
|
|||
|
|
# Test additional parameters
|
|||
|
|
data = get_<metric>_data(test_profile_with_data, days=28, goal_mode="strength")
|
|||
|
|
assert data is not None
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Integration Tests
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# backend/tests/test_charts_integration.py
|
|||
|
|
|
|||
|
|
def test_chart_uses_data_layer(client, auth_token):
|
|||
|
|
"""Test that chart endpoint uses data layer correctly"""
|
|||
|
|
response = client.get(
|
|||
|
|
"/api/charts/<metric>",
|
|||
|
|
headers={"X-Auth-Token": auth_token}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
assert response.status_code == 200
|
|||
|
|
data = response.json()
|
|||
|
|
|
|||
|
|
# Verify Chart.js structure
|
|||
|
|
assert 'chart_type' in data
|
|||
|
|
assert 'data' in data
|
|||
|
|
assert 'metadata' in data
|
|||
|
|
|
|||
|
|
# Verify metadata includes confidence
|
|||
|
|
assert 'confidence' in data['metadata']
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Performance Considerations
|
|||
|
|
|
|||
|
|
### 1. Query Optimization
|
|||
|
|
|
|||
|
|
**Problem:** N+1 Queries
|
|||
|
|
```python
|
|||
|
|
# ❌ BAD:
|
|||
|
|
for goal_id in goal_ids:
|
|||
|
|
cur.execute("SELECT * FROM goals WHERE id = %s", (goal_id,))
|
|||
|
|
# ... process each goal ...
|
|||
|
|
|
|||
|
|
# ✅ GOOD:
|
|||
|
|
cur.execute("SELECT * FROM goals WHERE id = ANY(%s)", (goal_ids,))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Problem:** Unindexed Columns
|
|||
|
|
```sql
|
|||
|
|
-- Add index if querying frequently by date range
|
|||
|
|
CREATE INDEX IF NOT EXISTS idx_weight_log_profile_date
|
|||
|
|
ON weight_log(profile_id, date DESC);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Caching
|
|||
|
|
|
|||
|
|
**For expensive calculations:**
|
|||
|
|
```python
|
|||
|
|
from functools import lru_cache
|
|||
|
|
|
|||
|
|
@lru_cache(maxsize=128)
|
|||
|
|
def get_expensive_calculation(profile_id: str, days: int) -> dict:
|
|||
|
|
"""Cache results for 128 most recent calls"""
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Note:** In-memory cache resets on restart. For persistent cache → Redis (later).
|
|||
|
|
|
|||
|
|
### 3. Pagination
|
|||
|
|
|
|||
|
|
**For large datasets:**
|
|||
|
|
```python
|
|||
|
|
def get_<metric>_data(
|
|||
|
|
profile_id: str,
|
|||
|
|
days: int = 28,
|
|||
|
|
limit: int = 1000,
|
|||
|
|
offset: int = 0
|
|||
|
|
) -> dict:
|
|||
|
|
"""
|
|||
|
|
Paginated data retrieval.
|
|||
|
|
"""
|
|||
|
|
cur.execute("""
|
|||
|
|
SELECT ...
|
|||
|
|
FROM ...
|
|||
|
|
WHERE profile_id = %s
|
|||
|
|
ORDER BY date DESC
|
|||
|
|
LIMIT %s OFFSET %s
|
|||
|
|
""", (profile_id, limit, offset))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Checkliste: Neue Funktion
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
[ ] Richtiges Modul gewählt (oder neues Modul erstellt)
|
|||
|
|
[ ] Funktion implementiert mit korrekter Signatur
|
|||
|
|
[ ] Docstring vollständig (Args, Returns, Example)
|
|||
|
|
[ ] Confidence calculation included
|
|||
|
|
[ ] Returns structured data (dict with primitives)
|
|||
|
|
[ ] NO formatting (no strings with units)
|
|||
|
|
[ ] Decimal → Float conversion wo nötig
|
|||
|
|
[ ] Safe dict access (.get() mit defaults)
|
|||
|
|
[ ] SQL parameter binding (keine String-Concatenation)
|
|||
|
|
[ ] Unit tests geschrieben (sufficient/insufficient/edge cases)
|
|||
|
|
[ ] Integration test geschrieben (wenn Chart/API endpoint)
|
|||
|
|
[ ] Performance geprüft (< 500ms)
|
|||
|
|
[ ] In __init__.py exportiert
|
|||
|
|
[ ] Dokumentation aktualisiert (CLAUDE.md)
|
|||
|
|
[ ] Commit mit aussagekräftiger Message
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Häufige Fehler
|
|||
|
|
|
|||
|
|
### 1. Vergessen Confidence zu berechnen
|
|||
|
|
```python
|
|||
|
|
# ❌ WRONG:
|
|||
|
|
return {"value": result}
|
|||
|
|
|
|||
|
|
# ✅ CORRECT:
|
|||
|
|
from data_layer.utils import calculate_confidence
|
|||
|
|
confidence = calculate_confidence(len(rows), days, "general")
|
|||
|
|
return {"value": result, "confidence": confidence, "data_points": len(rows)}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Formatierung im Data Layer
|
|||
|
|
```python
|
|||
|
|
# ❌ WRONG (Data Layer):
|
|||
|
|
return {"slope": f"{slope:.2f} kg/Woche"}
|
|||
|
|
|
|||
|
|
# ✅ CORRECT (Data Layer):
|
|||
|
|
return {"slope": 0.23} # Just the number
|
|||
|
|
|
|||
|
|
# ✅ FORMATTING (KI Layer):
|
|||
|
|
return f"{data['slope']:.2f} kg/Woche"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. Hardcoded Thresholds
|
|||
|
|
```python
|
|||
|
|
# ❌ WRONG:
|
|||
|
|
if len(rows) < 18: # Magic number
|
|||
|
|
return {"confidence": "insufficient"}
|
|||
|
|
|
|||
|
|
# ✅ CORRECT:
|
|||
|
|
confidence = calculate_confidence(len(rows), days, "general")
|
|||
|
|
if confidence == "insufficient":
|
|||
|
|
return {"confidence": "insufficient", ...}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Support & Hilfe
|
|||
|
|
|
|||
|
|
**Bei Fragen:**
|
|||
|
|
1. Lies PLACEHOLDER_DEVELOPMENT_GUIDE.md
|
|||
|
|
2. Prüfe bestehende Funktionen als Beispiel
|
|||
|
|
3. Frag im Team oder erstelle Gitea Issue
|
|||
|
|
|
|||
|
|
**Debugging:**
|
|||
|
|
1. Unit Test schreiben
|
|||
|
|
2. Print intermediate results
|
|||
|
|
3. Check SQL query mit `EXPLAIN ANALYZE`
|
|||
|
|
4. Profile mit `cProfile` wenn Performance-Problem
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Autor:** Claude Sonnet 4.5
|
|||
|
|
**Version:** 1.0
|
|||
|
|
**Letzte Aktualisierung:** 28. März 2026
|