diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx
index 71164ff..b0eb75a 100644
--- a/frontend/src/pages/History.jsx
+++ b/frontend/src/pages/History.jsx
@@ -4,7 +4,8 @@ import { useProfile } from '../context/ProfileContext'
import {
LineChart, Line, BarChart, Bar,
XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
- ReferenceLine, PieChart, Pie, Cell, ComposedChart
+ ReferenceLine, PieChart, Pie, Cell, ComposedChart,
+ ScatterChart, Scatter,
} from 'recharts'
import { ChevronRight, Brain, ChevronDown, ChevronUp, Trash2 } from 'lucide-react'
import { api } from '../utils/api'
@@ -990,6 +991,9 @@ function NutritionSection({ profile, insights, onRequest, loadingSlug, filterAct
Kennzahlen und Charts nutzen dieselbe Berechnung wie die KI-Platzhalter (Ernährungs-Data-Layer).{' '}
Kalorien vs. Gewicht und TDEE-Referenz entsprechen Mifflin–St Jeor × PAL 1,55 bzw. kg-Fallback (32,5 kcal/kg).
+ {' '}
+ Kalorienbilanz , Protein vs. Magermasse und den Block{' '}
+ «Kurz-Einordnung» finden Sie hier — früher im eigenen Reiter «Korrelation» (jetzt Data-Layer-Bundle).
@@ -1242,36 +1246,163 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA
)
}
-function LagCorrelationMetaCard({ title, block }) {
- if (!block) return null
- const ok = block.available
+function overviewSectionTone(sec) {
+ const kpis = sec.kpi_short || []
+ if (kpis.some((k) => k.status === 'bad')) return 'bad'
+ if (kpis.some((k) => k.status === 'warn')) return 'warn'
+ const interp = sec.interpretation_short || []
+ if (interp.some((x) => x.status === 'bad')) return 'bad'
+ if (interp.some((x) => x.status === 'warn')) return 'warn'
+ const heur = sec.heuristic_short || []
+ if (heur.some((h) => h.status === 'warn')) return 'warn'
+ return 'good'
+}
+
+function overviewConfidenceUi(conf) {
+ if (conf === 'high') return { label: 'Datenlage: gut', tone: 'good', hint: 'Ausreichend Messpunkte für sinnvolle Kurzinfos.' }
+ if (conf === 'medium') return { label: 'Datenlage: mittel', tone: 'warn', hint: 'Einzelne Bereiche sind noch dünn besetzt.' }
+ return { label: 'Datenlage: dünn', tone: 'bad', hint: 'Mehr Einträge verbessern die Aussagekraft.' }
+}
+
+function chartJsScatterPoints(payload) {
+ const raw = payload?.data?.datasets?.[0]?.data || []
+ if (!Array.isArray(raw)) return []
+ return raw.map((p) => ({ x: Number(p.x), y: Number(p.y) }))
+}
+
+function driverBarFromStatus(st) {
+ const s = String(st || '').toLowerCase()
+ if (s.includes('hinder')) return { v: -1, fill: 'var(--danger)' }
+ if (s.includes('förder') || s.includes('foerder')) return { v: 1, fill: 'var(--accent)' }
+ return { v: 0.15, fill: '#6B7280' }
+}
+
+function chartJsBarRows(payload, fallbackDrivers) {
+ const labels = payload?.data?.labels || []
+ const values = payload?.data?.datasets?.[0]?.data || []
+ const colors = payload?.data?.datasets?.[0]?.backgroundColor
+ if (labels.length && values.length) {
+ return labels.map((name, i) => ({
+ name: name.length > 42 ? `${name.slice(0, 40)}…` : name,
+ value: Number(values[i]),
+ fill: Array.isArray(colors) ? colors[i] : Number(values[i]) < 0 ? '#EF4444' : '#1D9E75',
+ }))
+ }
+ if (fallbackDrivers?.length) {
+ return fallbackDrivers.map((d) => {
+ const { v, fill } = driverBarFromStatus(d.status)
+ return {
+ name: String(d.factor || '—').length > 40 ? `${String(d.factor).slice(0, 38)}…` : String(d.factor || '—'),
+ value: v,
+ fill,
+ subtitle: d.reason,
+ }
+ })
+ }
+ return []
+}
+
+function CorrelationScatterTile({ title, accent, payload }) {
+ const meta = payload?.metadata || {}
+ const pts = chartJsScatterPoints(payload)
+ const hasChart = pts.length > 0 && meta.correlation != null
+ const r = Number(meta.correlation)
+ const strength =
+ !Number.isFinite(r) ? 'bad' : Math.abs(r) >= 0.35 ? 'good' : Math.abs(r) >= 0.15 ? 'warn' : 'bad'
+
return (
-
-
{title}
- {!ok ? (
-
Nicht genug Daten für diese Auswertung.
+
+
{title}
+
+ r = {meta.correlation != null ? Number(meta.correlation).toFixed(3) : '—'}
+ {meta.best_lag_days != null ? ` · Lag ${meta.best_lag_days} T` : ''}
+ {meta.metric ? ` · ${meta.metric}` : ''}
+ {meta.confidence ? ` · ${meta.confidence}` : ''}
+
+ {!hasChart ? (
+
{meta.message || 'Keine Daten für diese Korrelation.'}
) : (
- <>
-
- r ≈ {block.correlation != null ? Number(block.correlation).toFixed(3) : '—'}
- {block.best_lag_days != null ? ` · Lag ${block.best_lag_days} Tage` : ''}
- {block.metric ? ` · ${block.metric}` : ''}
- {block.confidence ? ` · ${block.confidence}` : ''}
-
- {block.interpretation ? (
-
{block.interpretation}
- ) : null}
- >
+
+
+
+
+
+
+
+
+
+
)}
+ {meta.interpretation ? (
+
{meta.interpretation}
+ ) : null}
)
}
-// ── Gesamtansicht (Layer 2b: GET /charts/history-overview-viz) ─────────────────
+function DriversImpactTile({ payload, driversFallback }) {
+ const meta = payload?.metadata || {}
+ const rows = chartJsBarRows(payload, driversFallback)
+ if (!rows.length) {
+ return (
+
+
C4 Einflussfaktoren
+
{meta.message || 'Keine Treiber-Daten.'}
+
+ )
+ }
+ const h = Math.min(220, Math.max(96, rows.length * 34))
+ return (
+
+
C4 Einflussfaktoren
+
+
+
+
+ {
+ if (!active || !pp?.length) return null
+ const p = pp[0].payload
+ return (
+
+
{p.name}
+ {p.subtitle ?
{p.subtitle}
: null}
+
+ )
+ }}
+ />
+
+ {rows.map((e, i) => (
+ |
+ ))}
+
+
+
+
+ )
+}
+
+// ── Gesamtansicht (Layer 2b: overview + Chart-Endpunkte C1–C4) ──────────────────
function HistoryOverviewSection({ insights, onRequest, loadingSlug, filterActiveSlugs }) {
const navigate = useNavigate()
const [period, setPeriod] = useState(30)
- const [data, setData] = useState(null)
+ const [bundle, setBundle] = useState(null)
const [err, setErr] = useState(null)
const [loading, setLoading] = useState(true)
@@ -1279,10 +1410,16 @@ function HistoryOverviewSection({ insights, onRequest, loadingSlug, filterActive
let cancelled = false
const daysReq = period === 9999 ? 3650 : period
setLoading(true)
- api.getHistoryOverviewViz(daysReq)
- .then((d) => {
+ Promise.all([
+ api.getHistoryOverviewViz(daysReq),
+ api.getWeightEnergyCorrelationChart(14),
+ api.getLbmProteinCorrelationChart(14),
+ api.getLoadVitalsCorrelationChart(14),
+ api.getRecoveryPerformanceChart(),
+ ])
+ .then(([overview, chartC1, chartC2, chartC3, chartC4]) => {
if (!cancelled) {
- setData(d)
+ setBundle({ overview, chartC1, chartC2, chartC3, chartC4 })
setErr(null)
}
})
@@ -1315,86 +1452,153 @@ function HistoryOverviewSection({ insights, onRequest, loadingSlug, filterActive
)
}
+ const data = bundle?.overview
+ const chartC1 = bundle?.chartC1
+ const chartC2 = bundle?.chartC2
+ const chartC3 = bundle?.chartC3
+ const chartC4 = bundle?.chartC4
+
const lag = data?.lag_correlations || {}
- const c4 = lag.recovery_performance
+ const c4drivers = lag.recovery_performance?.drivers || []
const sections = data?.sections || []
+ const confUi = overviewConfidenceUi(data?.confidence)
return (
-
- Kurzüberblick aus denselben Data-Layer-Bundles wie die Reiter Körper bis Erholung (Issue 53). Lag-Korrelationen C1–C4
- stammen aus correlations.py / Chart-Endpunkte.
+
+
+
{confUi.tone === 'good' ? '●' : confUi.tone === 'warn' ? '◐' : '○'}
+
+
{confUi.label}
+
{confUi.hint}
+
+
+
+
+ KPIs und Texte kommen aus den Layer-2b-Bundles (Körper, Ernährung, Fitness, Erholung).{' '}
+ Ehem. «Korrelation»-Charts (Bilanz, Protein/Mager, Kurz-Einordnung) liegen unter{' '}
+ navigate('/history', { state: { tab: 'nutrition' } })}>
+ Ernährung
+
+ . Die Kacheln C1–C4 unten nutzen dieselben Chart-Endpunkte wie die API (/api/charts/*).
- {sections.map((sec) => (
-
-
-
{sec.title}
-
navigate('/history', { state: { tab: sec.tab_id } })}
+
+ {sections.map((sec) => {
+ const tone = overviewSectionTone(sec)
+ const stripe = getStatusColor(tone)
+ const badgeBg = getStatusBg(tone)
+ return (
+
- Zum Reiter
-
-
-
{sec.summary_line}
- {(sec.interpretation_short || []).map((it, i) => (
-
-
{it.title}
-
{it.detail}
-
- ))}
- {(sec.kpi_short || []).map((k, i) => (
-
- {k.icon} {k.category}
- {' · '}
- {k.value}
- {k.sublabel ? — {k.sublabel} : null}
-
- ))}
- {(sec.heuristic_short || []).map((h, i) => (
-
-
{h.title}
-
{h.detail}
-
- ))}
- {(sec.insights_short || []).map((ins, i) => (
-
-
{ins.title}
-
{ins.body}
-
- ))}
-
- ))}
+
+
+
+ {tone === 'good' ? '✓' : tone === 'warn' ? '!' : '!!'}
+
+
{sec.title}
+
+
navigate('/history', { state: { tab: sec.tab_id } })}
+ >
+ Öffnen
+
+
+ {sec.summary_line}
-
- Lag-Korrelationen (C1–C3)
-
-
-
-
+ {(sec.kpi_short || []).length > 0 && (
+
+ {(sec.kpi_short || []).map((k, i) => (
+
+
{k.category}
+
{k.value}
+ {k.sublabel ?
{k.sublabel}
: null}
+
+ ))}
+
+ )}
-
- Einflussfaktoren (C4)
-
-
- {c4?.drivers?.length ? (
- c4.drivers.map((d, i) => (
-
-
{d.factor}
-
({d.status})
-
{d.reason}
+ {(sec.interpretation_short || []).map((it, i) => (
+
+
{it.title}
+
{it.detail}
+
+ ))}
+ {(sec.heuristic_short || []).map((h, i) => (
+
+
{h.title}
+
{h.detail}
+
+ ))}
+ {(sec.insights_short || []).map((ins, i) => (
+
+
{ins.title}
+
{ins.body}
+
+ ))}
- ))
- ) : (
-
Keine Treiber-Daten.
- )}
+ )
+ })}
+ Lag-Korrelationen (C1–C3)
+
+
+
+
+
+
+ Einflussfaktoren (C4)
+
+
)