feat: Enhance activity session metrics handling and frontend display
- Updated the `ACTIVITY_LOG_PATCHABLE_COLUMNS` and `ACTIVITY_LOG_PATCH_FORBIDDEN` sets to improve validation of CSV imports, ensuring only allowed fields are patched. - Refactored the `_coerce_raw_value_for_parameter` function to handle string inputs for integer and float types, enhancing data coercion accuracy. - Modified the `SessionMetricsFields` component to display orphan metrics that do not match the current schema, improving user visibility of imported data discrepancies. - Enhanced the frontend to handle and display additional metrics, ensuring a more comprehensive representation of session data.
This commit is contained in:
parent
574af61349
commit
c570e67a09
|
|
@ -11,10 +11,21 @@ from typing import Any, Dict, List, Mapping, Optional, Sequence
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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(
|
ACTIVITY_LOG_PATCHABLE_COLUMNS = frozenset(
|
||||||
{
|
{
|
||||||
|
"start_time",
|
||||||
|
"end_time",
|
||||||
|
"activity_type",
|
||||||
|
"duration_min",
|
||||||
|
"kcal_active",
|
||||||
|
"kcal_resting",
|
||||||
|
"hr_avg",
|
||||||
|
"hr_max",
|
||||||
"hr_min",
|
"hr_min",
|
||||||
|
"distance_km",
|
||||||
|
"rpe",
|
||||||
"pace_min_per_km",
|
"pace_min_per_km",
|
||||||
"cadence",
|
"cadence",
|
||||||
"avg_power",
|
"avg_power",
|
||||||
|
|
@ -23,11 +34,24 @@ ACTIVITY_LOG_PATCHABLE_COLUMNS = frozenset(
|
||||||
"humidity_percent",
|
"humidity_percent",
|
||||||
"avg_hr_percent",
|
"avg_hr_percent",
|
||||||
"kcal_per_km",
|
"kcal_per_km",
|
||||||
"rpe",
|
|
||||||
"notes",
|
"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):
|
class ActivitySessionMetricsError(Exception):
|
||||||
"""Raised by Layer 1; routers map to HTTP (404/400)."""
|
"""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 data_type == "integer":
|
||||||
if isinstance(raw, bool):
|
if isinstance(raw, bool):
|
||||||
raise TypeError("boolean nicht als integer erlaubt")
|
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)))
|
return int(round(float(raw)))
|
||||||
if data_type == "float":
|
if data_type == "float":
|
||||||
|
if isinstance(raw, str):
|
||||||
|
s = raw.strip().replace(",", ".")
|
||||||
|
return float(s)
|
||||||
return float(raw)
|
return float(raw)
|
||||||
if data_type == "string":
|
if data_type == "string":
|
||||||
return str(raw) if raw is not None else ""
|
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] = {}
|
patch: Dict[str, Any] = {}
|
||||||
for spec in schema:
|
for spec in schema:
|
||||||
src_col = (spec.get("source_field") or "").strip()
|
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
|
continue
|
||||||
pkey = spec["key"]
|
pkey = spec["key"]
|
||||||
if pkey not in mapped:
|
if pkey not in mapped:
|
||||||
|
|
|
||||||
|
|
@ -118,13 +118,18 @@ function buildMetricsPayload(schema, draft) {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
function SessionMetricsFields({ schema, values, setValues }) {
|
function SessionMetricsFields({ schema, values, setValues, metrics }) {
|
||||||
if (!schema || schema.length === 0) return null
|
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 }))
|
const set = (k, v) => setValues((prev) => ({ ...prev, [k]: v }))
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--border)' }}>
|
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--border)' }}>
|
||||||
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8 }}>Weitere Kennwerte (Profil)</div>
|
<div style={{ fontSize: 13, fontWeight: 600, marginBottom: 8 }}>Weitere Kennwerte (Profil)</div>
|
||||||
{schema.map((s) => (
|
{schemaList.map((s) => (
|
||||||
<div key={s.key} className="form-row">
|
<div key={s.key} className="form-row">
|
||||||
<label className="form-label">
|
<label className="form-label">
|
||||||
{s.name_de}
|
{s.name_de}
|
||||||
|
|
@ -157,6 +162,44 @@ function SessionMetricsFields({ schema, values, setValues }) {
|
||||||
<span className="form-unit" />
|
<span className="form-unit" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{orphanMetrics.length > 0 && (
|
||||||
|
<div style={{ marginTop: 14 }}>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text3)', marginBottom: 8, lineHeight: 1.45 }}>
|
||||||
|
Werte aus Import/älteren Daten, die zum <strong>aktuellen</strong> 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.
|
||||||
|
</div>
|
||||||
|
{orphanMetrics.map((row) => {
|
||||||
|
const disp =
|
||||||
|
values[row.key] === null || values[row.key] === undefined || values[row.key] === ''
|
||||||
|
? '—'
|
||||||
|
: String(values[row.key])
|
||||||
|
return (
|
||||||
|
<div key={row.key} className="form-row">
|
||||||
|
<label className="form-label">
|
||||||
|
{row.key}
|
||||||
|
{row.unit ? ` (${row.unit})` : ''}
|
||||||
|
</label>
|
||||||
|
{row.data_type === 'boolean' ? (
|
||||||
|
<input type="checkbox" style={{ width: 'auto', marginRight: 'auto' }} checked={!!values[row.key]} readOnly disabled />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="form-input"
|
||||||
|
style={{
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
cursor: 'default',
|
||||||
|
color: 'var(--text1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{disp}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="form-unit" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -699,6 +742,7 @@ export default function ActivityPage() {
|
||||||
)}
|
)}
|
||||||
<SessionMetricsFields
|
<SessionMetricsFields
|
||||||
schema={sessionDetail?.schema}
|
schema={sessionDetail?.schema}
|
||||||
|
metrics={sessionDetail?.metrics}
|
||||||
values={metricDraft}
|
values={metricDraft}
|
||||||
setValues={setMetricDraft}
|
setValues={setMetricDraft}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user