diff --git a/backend/data_layer/activity_session_metrics.py b/backend/data_layer/activity_session_metrics.py index e00fc8b..ed6f335 100644 --- a/backend/data_layer/activity_session_metrics.py +++ b/backend/data_layer/activity_session_metrics.py @@ -391,8 +391,8 @@ def replace_activity_session_metrics( (activity_log_id, spec["training_parameter_id"], vn, vi, vt, vb), ) - # Übergang: Spalten in activity_log sind maßgeblich — EAV für alle source_field-Parameter angleichen - sync_column_backed_session_metrics(cur, profile_id, activity_log_id) + # Kein sync_column_backed nach PUT /metrics: der Request ist maßgeblich für EAV. Ein Spalten-Sync würde + # Werte aus nicht mitgeschriebenen activity_log-Spalten wieder verwerfen. return fetch_activity_session_metrics(cur, activity_log_id) @@ -408,10 +408,40 @@ def get_activity_session_logical_unit(cur, profile_id: str, activity_log_id: str cur, header.get("training_category"), header.get("training_type_id") ) metrics = fetch_activity_session_metrics(cur, activity_log_id) + by_key = {m["key"]: m for m in metrics} + merged_metrics: List[Dict[str, Any]] = list(metrics) + for s in schema: + k = s["key"] + if k in by_key: + continue + sf = s.get("source_field") + if not sf or (isinstance(sf, str) and not str(sf).strip()): + continue + col = str(sf).strip() + if col not in header: + continue + raw = header.get(col) + if raw is None: + continue + dt = s["data_type"] + try: + val = _coerce_raw_value_for_parameter(dt, raw) + except (TypeError, ValueError): + continue + merged_metrics.append( + { + "training_parameter_id": s["training_parameter_id"], + "key": k, + "data_type": dt, + "unit": s.get("unit"), + "value": val, + } + ) + merged_metrics.sort(key=lambda x: x["key"]) return { "header": header, "schema": schema, - "metrics": metrics, + "metrics": merged_metrics, } diff --git a/backend/models.py b/backend/models.py index 8be2d09..b0462ad 100644 --- a/backend/models.py +++ b/backend/models.py @@ -83,8 +83,17 @@ class ActivityEntry(BaseModel): kcal_resting: Optional[float] = None hr_avg: Optional[float] = None hr_max: Optional[float] = None + hr_min: Optional[int] = None # DB-Spalte hr_min (Parameter min_hr) distance_km: Optional[float] = None rpe: Optional[int] = None + pace_min_per_km: Optional[float] = None + cadence: Optional[int] = None + avg_power: Optional[int] = None + elevation_gain: Optional[int] = None + temperature_celsius: Optional[float] = None + humidity_percent: Optional[int] = None + avg_hr_percent: Optional[float] = None + kcal_per_km: Optional[float] = None source: Optional[str] = 'manual' notes: Optional[str] = None training_type_id: Optional[int] = None # v9d: Training type categorization diff --git a/backend/routers/activity.py b/backend/routers/activity.py index e8f7609..7d8eaea 100644 --- a/backend/routers/activity.py +++ b/backend/routers/activity.py @@ -102,9 +102,10 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default cur.execute( """INSERT INTO activity_log (id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting, - hr_avg,hr_max,distance_km,rpe,source,notes, + hr_avg,hr_max,hr_min,distance_km,pace_min_per_km,cadence,avg_power,elevation_gain, + temperature_celsius,humidity_percent,avg_hr_percent,kcal_per_km,rpe,source,notes, training_type_id,training_category,training_subcategory,created) - VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)""", + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)""", ( eid, pid, @@ -117,7 +118,16 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default d["kcal_resting"], d["hr_avg"], d["hr_max"], + d.get("hr_min"), d["distance_km"], + d.get("pace_min_per_km"), + d.get("cadence"), + d.get("avg_power"), + d.get("elevation_gain"), + d.get("temperature_celsius"), + d.get("humidity_percent"), + d.get("avg_hr_percent"), + d.get("kcal_per_km"), d["rpe"], d["source"], d["notes"], diff --git a/frontend/src/pages/ActivityPage.jsx b/frontend/src/pages/ActivityPage.jsx index 2ebfb7d..9b4b845 100644 --- a/frontend/src/pages/ActivityPage.jsx +++ b/frontend/src/pages/ActivityPage.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef, startTransition } from 'react' import { Upload, Pencil, Trash2, Check, X, CheckCircle } from 'lucide-react' import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts' import { api } from '../utils/api' @@ -26,7 +26,16 @@ const ACTIVITY_LOG_PAYLOAD_KEYS = new Set([ 'kcal_resting', 'hr_avg', 'hr_max', + 'hr_min', 'distance_km', + 'pace_min_per_km', + 'cadence', + 'avg_power', + 'elevation_gain', + 'temperature_celsius', + 'humidity_percent', + 'avg_hr_percent', + 'kcal_per_km', 'rpe', 'source', 'notes', @@ -412,7 +421,9 @@ export default function ActivityPage() { } setEditing(null) setSessionDetail(null) - await load() + startTransition(() => { + void load() + }) } catch (err) { setError(err.message || 'Speichern fehlgeschlagen') setTimeout(() => setError(null), 6000)