diff --git a/backend/data_layer/activity_session_metrics.py b/backend/data_layer/activity_session_metrics.py index de54f40..6e3de0e 100644 --- a/backend/data_layer/activity_session_metrics.py +++ b/backend/data_layer/activity_session_metrics.py @@ -11,10 +11,21 @@ from typing import Any, Dict, List, Mapping, Optional, Sequence logger = logging.getLogger(__name__) -# activity_log-Spalten (ohne Kernfelder aus CSV-Minimal-Insert), die über source_field beschrieben werden können. +# activity_log-Spalten, die per training_parameters.source_field aus CSV (Parameter-Key) befüllt werden dürfen. +# Muss mit sync_column_backed_session_metrics übereinstimmen (inkl. Kernmetriken wie hr_avg). ACTIVITY_LOG_PATCHABLE_COLUMNS = frozenset( { + "start_time", + "end_time", + "activity_type", + "duration_min", + "kcal_active", + "kcal_resting", + "hr_avg", + "hr_max", "hr_min", + "distance_km", + "rpe", "pace_min_per_km", "cadence", "avg_power", @@ -23,11 +34,24 @@ ACTIVITY_LOG_PATCHABLE_COLUMNS = frozenset( "humidity_percent", "avg_hr_percent", "kcal_per_km", - "rpe", "notes", } ) +# Diese Spalten nicht aus CSV-Parameter-Zuordnung überschreiben (kommen aus Typ-Mapping / System). +ACTIVITY_LOG_PATCH_FORBIDDEN = frozenset( + { + "id", + "profile_id", + "date", + "created", + "training_type_id", + "training_category", + "training_subcategory", + "source", + } +) + class ActivitySessionMetricsError(Exception): """Raised by Layer 1; routers map to HTTP (404/400).""" @@ -218,8 +242,14 @@ def _coerce_raw_value_for_parameter(data_type: str, raw: Any) -> Any: if data_type == "integer": if isinstance(raw, bool): raise TypeError("boolean nicht als integer erlaubt") + if isinstance(raw, str): + s = raw.strip().replace(",", ".") + return int(round(float(s))) return int(round(float(raw))) if data_type == "float": + if isinstance(raw, str): + s = raw.strip().replace(",", ".") + return float(s) return float(raw) if data_type == "string": return str(raw) if raw is not None else "" @@ -248,7 +278,9 @@ def resolve_activity_log_column_patch_from_csv( patch: Dict[str, Any] = {} for spec in schema: src_col = (spec.get("source_field") or "").strip() - if not src_col or src_col not in ACTIVITY_LOG_PATCHABLE_COLUMNS: + if not src_col or src_col in ACTIVITY_LOG_PATCH_FORBIDDEN: + continue + if src_col not in ACTIVITY_LOG_PATCHABLE_COLUMNS: continue pkey = spec["key"] if pkey not in mapped: diff --git a/frontend/src/pages/ActivityPage.jsx b/frontend/src/pages/ActivityPage.jsx index a53a58f..c38283e 100644 --- a/frontend/src/pages/ActivityPage.jsx +++ b/frontend/src/pages/ActivityPage.jsx @@ -118,13 +118,18 @@ function buildMetricsPayload(schema, draft) { return out } -function SessionMetricsFields({ schema, values, setValues }) { - if (!schema || schema.length === 0) return null +function SessionMetricsFields({ schema, values, setValues, metrics }) { + const schemaList = Array.isArray(schema) ? schema : [] + const metricRows = Array.isArray(metrics) ? metrics : [] + const schemaKeys = new Set(schemaList.map((s) => s.key)) + const orphanMetrics = metricRows.filter((row) => row && row.key && !schemaKeys.has(row.key)) + + if (schemaList.length === 0 && orphanMetrics.length === 0) return null const set = (k, v) => setValues((prev) => ({ ...prev, [k]: v })) return (
Weitere Kennwerte (Profil)
- {schema.map((s) => ( + {schemaList.map((s) => (
))} + {orphanMetrics.length > 0 && ( +
+
+ Werte aus Import/älteren Daten, die zum aktuellen Trainingsprofil dieser Session (Kategorie/Typ + in activity_log) nicht ins Schema passen — nur Anzeige. Sichtbar nach erneutem Laden, wenn die Daten in der + Datenbank stehen. +
+ {orphanMetrics.map((row) => { + const disp = + values[row.key] === null || values[row.key] === undefined || values[row.key] === '' + ? '—' + : String(values[row.key]) + return ( +
+ + {row.data_type === 'boolean' ? ( + + ) : ( +
+ {disp} +
+ )} + +
+ ) + })} +
+ )}
) } @@ -699,6 +742,7 @@ export default function ActivityPage() { )}