Erste Version Platzhalter EAV #86

Merged
Lars merged 21 commits from develop into main 2026-04-17 21:52:14 +02:00
3 changed files with 64 additions and 12 deletions
Showing only changes of commit 38797d687d - Show all commits

View File

@ -129,7 +129,7 @@ Phasen sind **sequentiell** wo „Abhängigkeit“ steht; Teile können parallel
**Abhängigkeit:** Phase A für „welche Spalten noch Fallback sind“.
**Audit-Stand (2026-04-16):**
**Audit-Stand (2026-04-16, ergänzt Export):**
| Consumer | Nutzt Layer-1-Merge (`enrich_sessions_with_metrics` / `get_activity_session_logical_unit`) | Anmerkung |
|----------|---------------------------------------------------------------------------------------------|-----------|
@ -138,6 +138,9 @@ Phasen sind **sequentiell** wo „Abhängigkeit“ steht; Teile können parallel
| `activity_metrics.get_activity_detail_data` | ✅ | Platzhalter `{{activity_detail}}` |
| `activity_metrics.get_training_sessions_recent_weeks_data` | ✅ | KI-Kontext |
| `placeholder_resolver` (Aktivität) | ✅ nur `activity_metrics` | kein paralleles SQL |
| `GET /api/export/json` (`activity`) | ✅ `enrich_sessions_with_metrics` + `serialize_dates` | `session_metrics` pro Zeile |
| `GET /api/export/csv` (Training-Zeilen) | ✅ `enrich_sessions_with_metrics` | gemergte EAV in Spalte „Details“ |
| `GET /api/export/zip` (`data/activity.csv`) | ✅ `enrich_sessions_with_metrics` | Zusatzspalte `session_metrics_json` (Import ignoriert sie) |
| `get_activity_summary_data` | n.a. | rein aggregiert (`SUM`/`COUNT`), keine Session-EAV |
| `routers/charts.py` (A1A8) | Spalten-Aggregate | bewusst: Dauer/RPE/HF aus **`activity_log`**-Kanon; kein EAV-Join nötig für definierte Charts |
| `activity_stats` (`GET /api/activity/stats`) | nur Spalten | Kacheln: `kcal`/`duration` aus Kernspalten |
@ -197,4 +200,4 @@ Phasen sind **sequentiell** wo „Abhängigkeit“ steht; Teile können parallel
---
**Version:** 1.2 · §2.1a Navigationsregel (Read-Merge vs. Orchestrator-Schreiben vs. `activity_metrics`-Berechnungen).
**Version:** 1.3 · Phase-B-Export an `enrich_sessions_with_metrics` angebunden (`exportdata.py`).

View File

@ -127,6 +127,7 @@ frontend/src/
- **Phase A:** Skalar-Kanon schriftlich fixiert — `.claude/docs/technical/ACTIVITY_SCALAR_KANON_TABLE.md`; `ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md` v1.1; Agent-Guide Checkliste Phase A erledigt.
- **Phase B:** `GET /api/activity` (Liste) reichert jede Zeile mit `session_metrics` über `enrich_sessions_with_metrics` an (gleiche Merge-Logik wie Detail); Consumer-Audit-Tabelle in Produktions-Architektur-Dok §4 Phase B.
- **Phase B (Export):** `routers/exportdata.py` — JSON-Export `activity` mit `session_metrics`; CSV-Gesamtexport Training-Details mit EAV-Zusammenfassung; ZIP `data/activity.csv` mit Zusatzspalte `session_metrics_json` (Standard-Import unverändert).
### Updates (11.04.2026 - Ernährung: TDEE, Bilanz, Kalorien-Score)

View File

@ -22,6 +22,8 @@ from auth import require_auth, check_feature_access, increment_feature_usage
from routers.profiles import get_pid
from feature_logger import log_feature_usage
from caliper_composition import enrich_caliper_row_for_response, load_weight_rows
from data_layer.activity_session_metrics import enrich_sessions_with_metrics
from data_layer.utils import serialize_dates
router = APIRouter(prefix="/api/export", tags=["export"])
logger = logging.getLogger(__name__)
@ -90,10 +92,23 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D
for r in cur.fetchall():
writer.writerow(["Ernährung", r['date'], f"{float(r['kcal'])}kcal", f"Protein:{float(r['protein_g'])}g"])
# Activity
cur.execute("SELECT date, activity_type, duration_min, kcal_active FROM activity_log WHERE profile_id=%s ORDER BY date", (pid,))
for r in cur.fetchall():
writer.writerow(["Training", r['date'], r['activity_type'], f"{float(r['duration_min'])}min {float(r['kcal_active'])}kcal"])
# Activity (Layer-1: gemergte session_metrics in Details)
cur.execute(
"SELECT id, date, activity_type, duration_min, kcal_active FROM activity_log WHERE profile_id=%s ORDER BY date",
(pid,),
)
act_rows = [r2d(r) for r in cur.fetchall()]
enrich_sessions_with_metrics(cur, act_rows)
for r in act_rows:
base = f"{float(r['duration_min'])}min {float(r['kcal_active'])}kcal"
eav_parts = []
for m in r.get("session_metrics") or []:
k, v = m.get("key"), m.get("value")
if k is None or v is None:
continue
eav_parts.append(f"{k}={v}")
details = base + (" | " + "; ".join(eav_parts) if eav_parts else "")
writer.writerow(["Training", r["date"], r["activity_type"], details])
output.seek(0)
@ -148,7 +163,9 @@ def export_json(x_profile_id: Optional[str]=Header(default=None), session: dict=
data['nutrition'] = [r2d(r) for r in cur.fetchall()]
cur.execute("SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date", (pid,))
data['activity'] = [r2d(r) for r in cur.fetchall()]
data["activity"] = [r2d(r) for r in cur.fetchall()]
enrich_sessions_with_metrics(cur, data["activity"])
data["activity"] = serialize_dates(data["activity"])
cur.execute("SELECT * FROM ai_insights WHERE profile_id=%s ORDER BY created DESC", (pid,))
data['insights'] = [r2d(r) for r in cur.fetchall()]
@ -243,6 +260,9 @@ Dieser Export kann in Mitai Jinkendo unter
Einstellungen Import "Mitai Backup importieren"
wieder eingespielt werden.
activity.csv (optional): Spalte session_metrics_json (JSON-Array, Layer-1-merge)
wird beim Standard-Import ignoriert; für Vollständigkeit/externe Tools.
Format-Version 2 (ab v9b):
Alle CSV-Dateien sind UTF-8 mit BOM kodiert.
Trennzeichen: Semikolon (;)
@ -318,13 +338,41 @@ Datumsformat: YYYY-MM-DD
r['fiber'] = None; r['note'] = ''
write_csv(zf, "nutrition.csv", rows, ['id','date','meal_name','kcal','protein','fat','carbs','fiber','note','source','created'])
cur.execute("SELECT id, date, activity_type, duration_min, kcal_active, hr_avg, hr_max, distance_km, notes, source, created FROM activity_log WHERE profile_id=%s ORDER BY date", (pid,))
cur.execute(
"SELECT id, date, activity_type, duration_min, kcal_active, hr_avg, hr_max, distance_km, notes, source, created FROM activity_log WHERE profile_id=%s ORDER BY date",
(pid,),
)
rows = [r2d(r) for r in cur.fetchall()]
enrich_sessions_with_metrics(cur, rows)
for r in rows:
r['name'] = r['activity_type']; r['type'] = r.pop('activity_type', None)
r['kcal'] = r.pop('kcal_active', None); r['heart_rate_avg'] = r.pop('hr_avg', None)
r['heart_rate_max'] = r.pop('hr_max', None); r['note'] = r.pop('notes', None)
write_csv(zf, "activity.csv", rows, ['id','date','name','type','duration_min','kcal','heart_rate_avg','heart_rate_max','distance_km','note','source','created'])
sm = r.pop("session_metrics", None) or []
r["session_metrics_json"] = json.dumps(sm, ensure_ascii=False, default=str)
r["name"] = r["activity_type"]
r["type"] = r.pop("activity_type", None)
r["kcal"] = r.pop("kcal_active", None)
r["heart_rate_avg"] = r.pop("hr_avg", None)
r["heart_rate_max"] = r.pop("hr_max", None)
r["note"] = r.pop("notes", None)
write_csv(
zf,
"activity.csv",
rows,
[
"id",
"date",
"name",
"type",
"duration_min",
"kcal",
"heart_rate_avg",
"heart_rate_max",
"distance_km",
"note",
"source",
"created",
"session_metrics_json",
],
)
# 8. insights/ai_insights.json
cur.execute("SELECT id, scope, content, created FROM ai_insights WHERE profile_id=%s ORDER BY created DESC", (pid,))