feat: Introduce activity schema headline binding for improved metrics handling
- Added a new function `activitySchemaHeadlineBinding` to streamline the binding of profile parameters to their corresponding headline columns, enhancing clarity in the metrics display logic. - Refactored the `SessionMetricsFields` component to utilize the new binding function, simplifying the filtering of schema entries and improving maintainability. - Updated the logic in `ActivityPage` to leverage the binding function for determining the appropriate column for metrics, ensuring consistent data handling across the application.
This commit is contained in:
parent
c9d71c0179
commit
2a6c437a08
179
backend/scripts/inspect_activity_eav.py
Normal file
179
backend/scripts/inspect_activity_eav.py
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
"""
|
||||||
|
Diagnose: Was liegt in activity_session_metrics (EAV) vs. activity_log?
|
||||||
|
|
||||||
|
Ausführung (mit gesetzten DB_*-Variablen wie die App, z. B. aus .env):
|
||||||
|
|
||||||
|
cd backend
|
||||||
|
python scripts/inspect_activity_eav.py
|
||||||
|
|
||||||
|
Lokal ohne Docker-Hostname: z. B. ``set DB_HOST=127.0.0.1`` (Windows) / ``export DB_HOST=127.0.0.1``,
|
||||||
|
Port/User/Pass wie in der laufenden Postgres-Instanz.
|
||||||
|
|
||||||
|
Im Backend-Container (Compose-Service meist ``backend``, Arbeitsverzeichnis ``/app``):
|
||||||
|
|
||||||
|
docker compose exec backend python /app/scripts/inspect_activity_eav.py
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
python scripts/inspect_activity_eav.py --limit 30
|
||||||
|
python scripts/inspect_activity_eav.py --profile <uuid>
|
||||||
|
python scripts/inspect_activity_eav.py --activity <activity_log uuid>
|
||||||
|
|
||||||
|
Keine Schreibzugriffe — nur SELECT.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# backend/ als Import-Root
|
||||||
|
_BACKEND_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
if _BACKEND_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, _BACKEND_ROOT)
|
||||||
|
|
||||||
|
|
||||||
|
def _val_row(r: dict) -> str | None:
|
||||||
|
dt = r.get("data_type")
|
||||||
|
if dt == "integer":
|
||||||
|
v = r.get("value_int")
|
||||||
|
return str(v) if v is not None else None
|
||||||
|
if dt == "float":
|
||||||
|
v = r.get("value_num")
|
||||||
|
return str(v) if v is not None else None
|
||||||
|
if dt == "string":
|
||||||
|
v = r.get("value_text")
|
||||||
|
return repr(v) if v is not None else None
|
||||||
|
if dt == "boolean":
|
||||||
|
v = r.get("value_bool")
|
||||||
|
return str(v) if v is not None else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="EAV activity_session_metrics inspizieren")
|
||||||
|
parser.add_argument("--limit", type=int, default=40, help="Zeilen Report A/B")
|
||||||
|
parser.add_argument("--profile", type=str, default=None, help="profile_id filtern")
|
||||||
|
parser.add_argument("--activity", type=str, default=None, help="activity_log.id (einzelne Session)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
from db import get_db, get_cursor
|
||||||
|
|
||||||
|
if args.activity:
|
||||||
|
with get_db() as conn:
|
||||||
|
with get_cursor(conn) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT al.id, al.profile_id, al.date, al.start_time, al.source,
|
||||||
|
al.training_category, al.training_type_id, al.activity_type,
|
||||||
|
al.duration_min, al.kcal_active, al.hr_avg, al.hr_max, al.distance_km
|
||||||
|
FROM activity_log al
|
||||||
|
WHERE al.id = %s::uuid
|
||||||
|
""",
|
||||||
|
(args.activity,),
|
||||||
|
)
|
||||||
|
h = cur.fetchone()
|
||||||
|
if not h:
|
||||||
|
print("activity_log: keine Zeile für diese id")
|
||||||
|
return
|
||||||
|
print("=== activity_log (Kopfzeile) ===")
|
||||||
|
for k, v in dict(h).items():
|
||||||
|
print(f" {k}: {v}")
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT m.id AS metric_id, tp.key, tp.data_type, tp.source_field,
|
||||||
|
m.value_num, m.value_int, m.value_text, m.value_bool, m.updated_at
|
||||||
|
FROM activity_session_metrics m
|
||||||
|
JOIN training_parameters tp ON tp.id = m.training_parameter_id
|
||||||
|
WHERE m.activity_log_id = %s::uuid
|
||||||
|
ORDER BY tp.key
|
||||||
|
""",
|
||||||
|
(args.activity,),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print(f"\n=== activity_session_metrics ({len(rows)} Zeilen) ===")
|
||||||
|
for r in rows:
|
||||||
|
d = dict(r)
|
||||||
|
print(
|
||||||
|
f" {d['key']} ({d['data_type']}) "
|
||||||
|
f"value={_val_row(d)!r} source_field={d.get('source_field')!r} "
|
||||||
|
f"updated_at={d.get('updated_at')}"
|
||||||
|
)
|
||||||
|
if not rows:
|
||||||
|
print(" (keine EAV-Zeilen)")
|
||||||
|
return
|
||||||
|
|
||||||
|
prof_filter = ""
|
||||||
|
if args.profile:
|
||||||
|
prof_filter = " AND al.profile_id = %s::uuid "
|
||||||
|
params_a: tuple = (args.profile, args.limit) if args.profile else (args.limit,)
|
||||||
|
params_b: tuple = (args.profile, args.limit) if args.profile else (args.limit,)
|
||||||
|
|
||||||
|
q_recent_eav = f"""
|
||||||
|
SELECT
|
||||||
|
al.id AS activity_id,
|
||||||
|
al.profile_id,
|
||||||
|
al.date,
|
||||||
|
al.start_time,
|
||||||
|
al.source,
|
||||||
|
al.training_type_id,
|
||||||
|
al.training_category,
|
||||||
|
tp.key AS parameter_key,
|
||||||
|
tp.data_type,
|
||||||
|
tp.source_field AS tp_source_field,
|
||||||
|
m.value_num,
|
||||||
|
m.value_int,
|
||||||
|
m.value_text,
|
||||||
|
m.value_bool,
|
||||||
|
m.updated_at
|
||||||
|
FROM activity_session_metrics m
|
||||||
|
JOIN activity_log al ON al.id = m.activity_log_id
|
||||||
|
JOIN training_parameters tp ON tp.id = m.training_parameter_id
|
||||||
|
WHERE 1=1 {prof_filter}
|
||||||
|
ORDER BY m.updated_at DESC NULLS LAST, al.date DESC, al.start_time DESC
|
||||||
|
LIMIT %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
q_csv_no_eav = f"""
|
||||||
|
SELECT
|
||||||
|
al.id AS activity_id,
|
||||||
|
al.profile_id,
|
||||||
|
al.date,
|
||||||
|
al.start_time,
|
||||||
|
al.source,
|
||||||
|
al.training_type_id,
|
||||||
|
al.training_category,
|
||||||
|
(SELECT COUNT(*) FROM activity_session_metrics m WHERE m.activity_log_id = al.id) AS eav_count
|
||||||
|
FROM activity_log al
|
||||||
|
WHERE al.source = 'csv' {prof_filter}
|
||||||
|
ORDER BY al.date DESC, al.start_time DESC
|
||||||
|
LIMIT %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
with get_cursor(conn) as cur:
|
||||||
|
print("=== A) Neueste EAV-Zeilen (join activity_log + training_parameters) ===\n")
|
||||||
|
cur.execute(q_recent_eav, params_a)
|
||||||
|
for r in cur.fetchall():
|
||||||
|
d = dict(r)
|
||||||
|
v = _val_row(d)
|
||||||
|
print(
|
||||||
|
f"{d['date']} {d['start_time']} | {d['activity_id']} | src={d['source']!r} | "
|
||||||
|
f"type={d['training_type_id']} cat={d['training_category']!r} | "
|
||||||
|
f"{d['parameter_key']}={v!r} (tp.source_field={d.get('tp_source_field')!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n=== B) Neueste CSV-importierte Sessions: EAV-Anzahl pro Zeile ===\n")
|
||||||
|
cur.execute(q_csv_no_eav, params_b)
|
||||||
|
for r in cur.fetchall():
|
||||||
|
d = dict(r)
|
||||||
|
print(
|
||||||
|
f"{d['date']} {d['start_time']} | {d['activity_id']} | "
|
||||||
|
f"type={d['training_type_id']} | eav_count={d['eav_count']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nFertig. Für eine Session im Detail: --activity <uuid>")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -106,6 +106,23 @@ const ENTRY_FORM_ACTIVITY_LOG_COLUMNS = new Set([
|
||||||
'notes',
|
'notes',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bindung Profilparameter ↔ Kopfzeile: Entweder source_field zeigt auf eine Kopfspalte,
|
||||||
|
* oder der Parameter-key ist selbst eine Kopfspalte (häufig nach Migration / ohne source_field).
|
||||||
|
* @returns {{ headlineCol: string, parameterKey: string } | null}
|
||||||
|
*/
|
||||||
|
function activitySchemaHeadlineBinding(s) {
|
||||||
|
if (!s || !s.key) return null
|
||||||
|
const sf = s.source_field != null ? String(s.source_field).trim() : ''
|
||||||
|
if (sf && ENTRY_FORM_ACTIVITY_LOG_COLUMNS.has(sf)) {
|
||||||
|
return { headlineCol: sf, parameterKey: s.key }
|
||||||
|
}
|
||||||
|
if (ENTRY_FORM_ACTIVITY_LOG_COLUMNS.has(s.key)) {
|
||||||
|
return { headlineCol: s.key, parameterKey: s.key }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
function empty() {
|
function empty() {
|
||||||
return {
|
return {
|
||||||
date: dayjs().format('YYYY-MM-DD'),
|
date: dayjs().format('YYYY-MM-DD'),
|
||||||
|
|
@ -157,18 +174,9 @@ function buildMetricsPayload(schema, draft) {
|
||||||
function SessionMetricsFields({ schema, values, setValues, metrics }) {
|
function SessionMetricsFields({ schema, values, setValues, metrics }) {
|
||||||
const schemaList = Array.isArray(schema) ? schema : []
|
const schemaList = Array.isArray(schema) ? schema : []
|
||||||
const headlineDuplicateKeys = new Set(
|
const headlineDuplicateKeys = new Set(
|
||||||
schemaList
|
schemaList.filter((s) => activitySchemaHeadlineBinding(s) != null).map((s) => s.key),
|
||||||
.filter((s) => {
|
|
||||||
const sf = s.source_field != null ? String(s.source_field).trim() : ''
|
|
||||||
return sf && ENTRY_FORM_ACTIVITY_LOG_COLUMNS.has(sf)
|
|
||||||
})
|
|
||||||
.map((s) => s.key),
|
|
||||||
)
|
)
|
||||||
const schemaForDisplay = schemaList.filter((s) => {
|
const schemaForDisplay = schemaList.filter((s) => activitySchemaHeadlineBinding(s) == null)
|
||||||
const sf = s.source_field != null ? String(s.source_field).trim() : ''
|
|
||||||
if (!sf) return true
|
|
||||||
return !ENTRY_FORM_ACTIVITY_LOG_COLUMNS.has(sf)
|
|
||||||
})
|
|
||||||
const metricRows = Array.isArray(metrics) ? metrics : []
|
const metricRows = Array.isArray(metrics) ? metrics : []
|
||||||
const schemaKeys = new Set(schemaForDisplay.map((s) => s.key))
|
const schemaKeys = new Set(schemaForDisplay.map((s) => s.key))
|
||||||
const orphanMetrics = metricRows.filter(
|
const orphanMetrics = metricRows.filter(
|
||||||
|
|
@ -655,10 +663,10 @@ export default function ActivityPage() {
|
||||||
if (sessionDetail?.schema?.length > 0) {
|
if (sessionDetail?.schema?.length > 0) {
|
||||||
const draftForMetrics = { ...metricDraft }
|
const draftForMetrics = { ...metricDraft }
|
||||||
for (const s of sessionDetail.schema) {
|
for (const s of sessionDetail.schema) {
|
||||||
const sf = s.source_field != null ? String(s.source_field).trim() : ''
|
const bind = activitySchemaHeadlineBinding(s)
|
||||||
if (!sf || !ENTRY_FORM_ACTIVITY_LOG_COLUMNS.has(sf)) continue
|
if (!bind || !(s.key in draftForMetrics)) continue
|
||||||
if (!(s.key in draftForMetrics)) continue
|
const rawCol =
|
||||||
const rawCol = payload[sf] !== undefined ? payload[sf] : editing?.[sf]
|
payload[bind.headlineCol] !== undefined ? payload[bind.headlineCol] : editing?.[bind.headlineCol]
|
||||||
if (rawCol === undefined) continue
|
if (rawCol === undefined) continue
|
||||||
if (s.data_type === 'boolean') {
|
if (s.data_type === 'boolean') {
|
||||||
draftForMetrics[s.key] = !!rawCol
|
draftForMetrics[s.key] = !!rawCol
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user