Erste Version Platzhalter EAV #86

Merged
Lars merged 21 commits from develop into main 2026-04-17 21:52:14 +02:00
4 changed files with 109 additions and 48 deletions
Showing only changes of commit 7d6fdab812 - Show all commits

View File

@ -899,6 +899,12 @@ def _import_activity(
start_time=workout_start_t,
end_time=end_str or None,
activity_type=wtype,
duration_min=registry_updates.get("duration_min"),
kcal_active=registry_updates.get("kcal_active"),
kcal_resting=registry_updates.get("kcal_resting"),
hr_avg=registry_updates.get("hr_avg"),
hr_max=registry_updates.get("hr_max"),
distance_km=registry_updates.get("distance_km"),
training_type_id=training_type_id,
training_category=training_category,
training_subcategory=training_subcategory,
@ -915,7 +921,14 @@ def _import_activity(
cur,
profile_id,
str(aid),
workout_date=iso,
training_type_id=training_type_id,
duration_min=registry_updates.get("duration_min"),
hr_avg=registry_updates.get("hr_avg"),
hr_max=registry_updates.get("hr_max"),
distance_km=registry_updates.get("distance_km"),
kcal_active=registry_updates.get("kcal_active"),
kcal_resting=registry_updates.get("kcal_resting"),
)
upsert_session_metrics_from_csv_mapped(
cur,

View File

@ -216,17 +216,18 @@ def insert_activity_csv_minimal(
start_time: Any,
end_time: Any,
activity_type: str,
duration_min: Any,
kcal_active: Any,
kcal_resting: Any,
hr_avg: Any,
hr_max: Any,
distance_km: Any,
training_type_id: Any,
training_category: Any,
training_subcategory: Any,
source: str,
) -> None:
"""
INSERT Kopfzeile für Universal-CSV / Legacy-Import.
Metriken aus ``activity_csv_registry_updates_from_mapped`` (oder manuelles Dict)
ausschließlich via ``update_activity_columns``; keine fest verdrahteten hr_avg-Parameter.
"""
"""INSERT activity_log-Zeile (Universal-CSV): Kernspalten im INSERT; optional zusätzliches PATCH."""
cur.execute(
"""
INSERT INTO activity_log (
@ -234,7 +235,7 @@ def insert_activity_csv_minimal(
kcal_active, kcal_resting, hr_avg, hr_max, distance_km,
source, training_type_id, training_category, training_subcategory, created
)
VALUES (%s,%s,%s,%s,%s,%s,NULL,NULL,NULL,NULL,NULL,NULL,%s,%s,%s,%s,CURRENT_TIMESTAMP)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)
""",
(
eid,
@ -243,6 +244,12 @@ def insert_activity_csv_minimal(
start_time,
end_time,
activity_type,
duration_min,
kcal_active,
kcal_resting,
hr_avg,
hr_max,
distance_km,
source,
training_type_id,
training_category,
@ -280,32 +287,37 @@ def run_activity_post_write_hooks_import(
profile_id: str,
eid: str,
*,
training_type_id: Optional[int] = None,
workout_date: str,
training_type_id: Optional[int],
duration_min: Any,
hr_avg: Any,
hr_max: Any,
distance_km: Any,
kcal_active: Any,
kcal_resting: Any,
) -> None:
"""Auto-Eval nach Import — liest die Session aus der DB (gleiche Felder wie REST-Hook)."""
if not _EVALUATION_AVAILABLE or not _evaluate_and_save_activity:
return
cur.execute(
"""
SELECT id, profile_id, date, training_type_id, duration_min,
hr_avg, hr_max, distance_km, kcal_active, kcal_resting,
rpe, pace_min_per_km, cadence, elevation_gain
FROM activity_log
WHERE id = %s AND profile_id = %s
""",
(eid, profile_id),
)
row = cur.fetchone()
if not row:
return
activity_dict = dict(row)
tid = training_type_id if training_type_id is not None else activity_dict.get("training_type_id")
if not tid:
return
try:
_evaluate_and_save_activity(cur, eid, activity_dict, int(tid), profile_id)
except Exception as eval_err:
logger.warning("[activity import] Auto-Eval fehlgeschlagen: %s", eval_err)
"""Auto-Eval nach Import (gleiche Transaktion wie Schreibpfad — keine Abhängigkeit vom DB-Read-Timing)."""
if _EVALUATION_AVAILABLE and training_type_id and _evaluate_and_save_activity:
try:
activity_dict = {
"id": eid,
"profile_id": profile_id,
"date": workout_date,
"training_type_id": training_type_id,
"duration_min": duration_min,
"hr_avg": hr_avg,
"hr_max": hr_max,
"distance_km": distance_km,
"kcal_active": kcal_active,
"kcal_resting": kcal_resting,
"rpe": None,
"pace_min_per_km": None,
"cadence": None,
"elevation_gain": None,
}
_evaluate_and_save_activity(cur, eid, activity_dict, training_type_id, profile_id)
except Exception as eval_err:
logger.warning("[activity import] Auto-Eval fehlgeschlagen: %s", eval_err)
def merge_activity_csv_module_fields(

View File

@ -641,7 +641,14 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional
cur,
pid,
str(existing_id),
workout_date=workout_date,
training_type_id=training_type_id,
duration_min=duration_min,
hr_avg=hr_av,
hr_max=hr_mx,
distance_km=dist_km,
kcal_active=kcal_a,
kcal_resting=kcal_r,
)
else:
new_id = new_activity_id()
@ -653,31 +660,30 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional
start_time=workout_start_t,
end_time=row.get("End", "") or None,
activity_type=wtype,
duration_min=duration_min,
kcal_active=kcal_a,
kcal_resting=kcal_r,
hr_avg=hr_av,
hr_max=hr_mx,
distance_km=dist_km,
training_type_id=training_type_id,
training_category=training_category,
training_subcategory=training_subcategory,
source="apple_health",
)
apple_metrics = {
k: v
for k, v in {
"duration_min": duration_min,
"kcal_active": kcal_a,
"kcal_resting": kcal_r,
"hr_avg": hr_av,
"hr_max": hr_mx,
"distance_km": dist_km,
}.items()
if v is not None
}
if apple_metrics:
update_activity_columns(cur, pid, new_id, apple_metrics)
inserted += 1
run_activity_post_write_hooks_import(
cur,
pid,
new_id,
workout_date=workout_date,
training_type_id=training_type_id,
duration_min=duration_min,
hr_avg=hr_av,
hr_max=hr_mx,
distance_km=dist_km,
kcal_active=kcal_a,
kcal_resting=kcal_r,
)
except Exception as e:
logger.warning(f"Import row failed: {e}")

View File

@ -96,7 +96,7 @@ const ACTIVITY_LOG_PAYLOAD_KEYS = new Set([
'training_subcategory',
])
/** activity_log-Spalten, die im EntryForm editiert werden (nicht aus metricDraft überschreiben). */
/** activity_log-Spalten, die im EntryForm editiert werden (Kurzform). */
const ACTIVITY_ENTRY_FORM_COLUMNS = new Set([
'duration_min',
'kcal_active',
@ -568,6 +568,27 @@ export default function ActivityPage() {
setMetricDraft(m)
}, [sessionDetail])
/** Nach GET /activity/:id: Kopf-Spalten ins EntryForm, wenn die Listenzeile leer war (Parameter-Keys vs. Spalten). */
useEffect(() => {
const h = sessionDetail?.header
if (!h?.id || !editing?.id) return
if (String(h.id) !== String(editing.id)) return
setEditing((prev) => {
if (String(h.id) !== String(prev.id)) return prev
let changed = false
const next = { ...prev }
for (const col of ACTIVITY_ENTRY_FORM_COLUMNS) {
const cur = next[col]
const empty = cur === null || cur === undefined || cur === ''
if (empty && h[col] != null && h[col] !== '') {
next[col] = h[col]
changed = true
}
}
return changed ? next : prev
})
}, [sessionDetail, editing?.id])
const handleSave = async () => {
setSaving(true)
setError(null)
@ -612,7 +633,16 @@ export default function ActivityPage() {
if (!col || !ACTIVITY_LOG_PAYLOAD_KEYS.has(col)) continue
const useFormColumn = ACTIVITY_ENTRY_FORM_COLUMNS.has(col)
if (!useFormColumn && !(s.key in metricDraft)) continue
const raw = useFormColumn ? editing[col] : metricDraft[s.key]
let raw
if (useFormColumn) {
const fromForm = editing[col]
const fromDraft = metricDraft[s.key]
const formEmpty = fromForm === null || fromForm === undefined || fromForm === ''
const draftEmpty = fromDraft === null || fromDraft === undefined || fromDraft === ''
raw = !formEmpty ? fromForm : !draftEmpty ? fromDraft : fromForm
} else {
raw = metricDraft[s.key]
}
const rawStr = raw === null || raw === undefined ? '' : String(raw).trim()
if (rawStr === '') {
payload[col] = null