Erste Version Platzhalter EAV #86
|
|
@ -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` (A1–A8) | 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`).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user