diff --git a/frontend/src/components/NutritionCharts.jsx b/frontend/src/components/NutritionCharts.jsx
new file mode 100644
index 0000000..e60c65b
--- /dev/null
+++ b/frontend/src/components/NutritionCharts.jsx
@@ -0,0 +1,284 @@
+import { useState, useEffect } from 'react'
+import {
+ LineChart, Line, BarChart, Bar, PieChart, Pie, Cell,
+ XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
+ ReferenceLine
+} from 'recharts'
+import { api } from '../utils/api'
+import dayjs from 'dayjs'
+
+const fmtDate = d => dayjs(d).format('DD.MM')
+
+function ChartCard({ title, loading, error, children }) {
+ return (
+
+
+ {title}
+
+ {loading && (
+
+
+
+ )}
+ {error && (
+
+ {error}
+
+ )}
+ {!loading && !error && children}
+
+ )
+}
+
+/**
+ * Nutrition Charts Component (E1-E5)
+ *
+ * Displays 4 nutrition chart endpoints:
+ * - Energy Balance Timeline (E1)
+ * - Macro Distribution (E2)
+ * - Protein Adequacy (E3)
+ * - Nutrition Consistency (E5)
+ */
+export default function NutritionCharts({ days = 28 }) {
+ const [energyData, setEnergyData] = useState(null)
+ const [macroData, setMacroData] = useState(null)
+ const [proteinData, setProteinData] = useState(null)
+ const [consistencyData, setConsistencyData] = useState(null)
+
+ const [loading, setLoading] = useState({})
+ const [errors, setErrors] = useState({})
+
+ useEffect(() => {
+ loadCharts()
+ }, [days])
+
+ const loadCharts = async () => {
+ // Load all 4 charts in parallel
+ await Promise.all([
+ loadEnergyBalance(),
+ loadMacroDistribution(),
+ loadProteinAdequacy(),
+ loadConsistency()
+ ])
+ }
+
+ const loadEnergyBalance = async () => {
+ setLoading(l => ({...l, energy: true}))
+ setErrors(e => ({...e, energy: null}))
+ try {
+ const data = await api.getEnergyBalanceChart(days)
+ setEnergyData(data)
+ } catch (err) {
+ setErrors(e => ({...e, energy: err.message}))
+ } finally {
+ setLoading(l => ({...l, energy: false}))
+ }
+ }
+
+ const loadMacroDistribution = async () => {
+ setLoading(l => ({...l, macro: true}))
+ setErrors(e => ({...e, macro: null}))
+ try {
+ const data = await api.getMacroDistributionChart(days)
+ setMacroData(data)
+ } catch (err) {
+ setErrors(e => ({...e, macro: err.message}))
+ } finally {
+ setLoading(l => ({...l, macro: false}))
+ }
+ }
+
+ const loadProteinAdequacy = async () => {
+ setLoading(l => ({...l, protein: true}))
+ setErrors(e => ({...e, protein: null}))
+ try {
+ const data = await api.getProteinAdequacyChart(days)
+ setProteinData(data)
+ } catch (err) {
+ setErrors(e => ({...e, protein: err.message}))
+ } finally {
+ setLoading(l => ({...l, protein: false}))
+ }
+ }
+
+ const loadConsistency = async () => {
+ setLoading(l => ({...l, consistency: true}))
+ setErrors(e => ({...e, consistency: null}))
+ try {
+ const data = await api.getNutritionConsistencyChart(days)
+ setConsistencyData(data)
+ } catch (err) {
+ setErrors(e => ({...e, consistency: err.message}))
+ } finally {
+ setLoading(l => ({...l, consistency: false}))
+ }
+ }
+
+ // E1: Energy Balance Timeline
+ const renderEnergyBalance = () => {
+ if (!energyData || energyData.metadata?.confidence === 'insufficient') {
+ return
+ Nicht genug Ernährungsdaten
+
+ }
+
+ const chartData = energyData.data.labels.map((label, i) => ({
+ date: fmtDate(label),
+ kcal: energyData.data.datasets[0]?.data[i],
+ tdee: energyData.data.datasets[1]?.data[i]
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ Ø {energyData.metadata.avg_kcal} kcal/Tag · {energyData.metadata.data_points} Einträge
+
+ >
+ )
+ }
+
+ // E2: Macro Distribution (Pie)
+ const renderMacroDistribution = () => {
+ if (!macroData || macroData.metadata?.confidence === 'insufficient') {
+ return
+ Nicht genug Makronährstoff-Daten
+
+ }
+
+ const chartData = macroData.data.labels.map((label, i) => ({
+ name: label,
+ value: macroData.data.datasets[0]?.data[i],
+ color: macroData.data.datasets[0]?.backgroundColor[i]
+ }))
+
+ return (
+ <>
+
+
+ `${name}: ${value}%`}
+ outerRadius={70}
+ dataKey="value"
+ >
+ {chartData.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+ P: {macroData.metadata.protein_g}g · C: {macroData.metadata.carbs_g}g · F: {macroData.metadata.fat_g}g
+
+ >
+ )
+ }
+
+ // E3: Protein Adequacy Timeline
+ const renderProteinAdequacy = () => {
+ if (!proteinData || proteinData.metadata?.confidence === 'insufficient') {
+ return
+ Nicht genug Protein-Daten
+
+ }
+
+ const chartData = proteinData.data.labels.map((label, i) => ({
+ date: fmtDate(label),
+ protein: proteinData.data.datasets[0]?.data[i],
+ targetLow: proteinData.data.datasets[1]?.data[i],
+ targetHigh: proteinData.data.datasets[2]?.data[i]
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {proteinData.metadata.days_in_target}/{proteinData.metadata.data_points} Tage im Zielbereich ({proteinData.metadata.target_compliance_pct}%)
+
+ >
+ )
+ }
+
+ // E5: Nutrition Consistency (Bar)
+ const renderConsistency = () => {
+ if (!consistencyData || consistencyData.metadata?.confidence === 'insufficient') {
+ return
+ Nicht genug Daten für Konsistenz-Analyse
+
+ }
+
+ const chartData = consistencyData.data.labels.map((label, i) => ({
+ name: label,
+ score: consistencyData.data.datasets[0]?.data[i],
+ color: consistencyData.data.datasets[0]?.backgroundColor[i]
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {chartData.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+ Gesamt-Score: {consistencyData.metadata.consistency_score}/100
+
+ >
+ )
+ }
+
+ return (
+
+
+ {renderEnergyBalance()}
+
+
+
+ {renderMacroDistribution()}
+
+
+
+ {renderProteinAdequacy()}
+
+
+
+ {renderConsistency()}
+
+
+ )
+}
diff --git a/frontend/src/components/RecoveryCharts.jsx b/frontend/src/components/RecoveryCharts.jsx
new file mode 100644
index 0000000..a07cdda
--- /dev/null
+++ b/frontend/src/components/RecoveryCharts.jsx
@@ -0,0 +1,320 @@
+import { useState, useEffect } from 'react'
+import {
+ LineChart, Line, BarChart, Bar,
+ XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid
+} from 'recharts'
+import { api } from '../utils/api'
+import dayjs from 'dayjs'
+
+const fmtDate = d => dayjs(d).format('DD.MM')
+
+function ChartCard({ title, loading, error, children }) {
+ return (
+
+
+ {title}
+
+ {loading && (
+
+
+
+ )}
+ {error && (
+
+ {error}
+
+ )}
+ {!loading && !error && children}
+
+ )
+}
+
+/**
+ * Recovery Charts Component (R1-R5)
+ *
+ * Displays 5 recovery chart endpoints:
+ * - Recovery Score Timeline (R1)
+ * - HRV/RHR vs Baseline (R2)
+ * - Sleep Duration + Quality (R3)
+ * - Sleep Debt (R4)
+ * - Vital Signs Matrix (R5)
+ */
+export default function RecoveryCharts({ days = 28 }) {
+ const [recoveryData, setRecoveryData] = useState(null)
+ const [hrvRhrData, setHrvRhrData] = useState(null)
+ const [sleepData, setSleepData] = useState(null)
+ const [debtData, setDebtData] = useState(null)
+ const [vitalsData, setVitalsData] = useState(null)
+
+ const [loading, setLoading] = useState({})
+ const [errors, setErrors] = useState({})
+
+ useEffect(() => {
+ loadCharts()
+ }, [days])
+
+ const loadCharts = async () => {
+ // Load all 5 charts in parallel
+ await Promise.all([
+ loadRecoveryScore(),
+ loadHrvRhr(),
+ loadSleepQuality(),
+ loadSleepDebt(),
+ loadVitalSigns()
+ ])
+ }
+
+ const loadRecoveryScore = async () => {
+ setLoading(l => ({...l, recovery: true}))
+ setErrors(e => ({...e, recovery: null}))
+ try {
+ const data = await api.getRecoveryScoreChart(days)
+ setRecoveryData(data)
+ } catch (err) {
+ setErrors(e => ({...e, recovery: err.message}))
+ } finally {
+ setLoading(l => ({...l, recovery: false}))
+ }
+ }
+
+ const loadHrvRhr = async () => {
+ setLoading(l => ({...l, hrvRhr: true}))
+ setErrors(e => ({...e, hrvRhr: null}))
+ try {
+ const data = await api.getHrvRhrBaselineChart(days)
+ setHrvRhrData(data)
+ } catch (err) {
+ setErrors(e => ({...e, hrvRhr: err.message}))
+ } finally {
+ setLoading(l => ({...l, hrvRhr: false}))
+ }
+ }
+
+ const loadSleepQuality = async () => {
+ setLoading(l => ({...l, sleep: true}))
+ setErrors(e => ({...e, sleep: null}))
+ try {
+ const data = await api.getSleepDurationQualityChart(days)
+ setSleepData(data)
+ } catch (err) {
+ setErrors(e => ({...e, sleep: err.message}))
+ } finally {
+ setLoading(l => ({...l, sleep: false}))
+ }
+ }
+
+ const loadSleepDebt = async () => {
+ setLoading(l => ({...l, debt: true}))
+ setErrors(e => ({...e, debt: null}))
+ try {
+ const data = await api.getSleepDebtChart(days)
+ setDebtData(data)
+ } catch (err) {
+ setErrors(e => ({...e, debt: err.message}))
+ } finally {
+ setLoading(l => ({...l, debt: false}))
+ }
+ }
+
+ const loadVitalSigns = async () => {
+ setLoading(l => ({...l, vitals: true}))
+ setErrors(e => ({...e, vitals: null}))
+ try {
+ const data = await api.getVitalSignsMatrixChart(7) // Last 7 days
+ setVitalsData(data)
+ } catch (err) {
+ setErrors(e => ({...e, vitals: err.message}))
+ } finally {
+ setLoading(l => ({...l, vitals: false}))
+ }
+ }
+
+ // R1: Recovery Score Timeline
+ const renderRecoveryScore = () => {
+ if (!recoveryData || recoveryData.metadata?.confidence === 'insufficient') {
+ return
+ Keine Recovery-Daten vorhanden
+
+ }
+
+ const chartData = recoveryData.data.labels.map((label, i) => ({
+ date: fmtDate(label),
+ score: recoveryData.data.datasets[0]?.data[i]
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ Aktuell: {recoveryData.metadata.current_score}/100 · {recoveryData.metadata.data_points} Einträge
+
+ >
+ )
+ }
+
+ // R2: HRV/RHR vs Baseline
+ const renderHrvRhr = () => {
+ if (!hrvRhrData || hrvRhrData.metadata?.confidence === 'insufficient') {
+ return
+ Keine Vitalwerte vorhanden
+
+ }
+
+ const chartData = hrvRhrData.data.labels.map((label, i) => ({
+ date: fmtDate(label),
+ hrv: hrvRhrData.data.datasets[0]?.data[i],
+ rhr: hrvRhrData.data.datasets[1]?.data[i]
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ HRV Ø {hrvRhrData.metadata.avg_hrv}ms · RHR Ø {hrvRhrData.metadata.avg_rhr}bpm
+
+ >
+ )
+ }
+
+ // R3: Sleep Duration + Quality
+ const renderSleepQuality = () => {
+ if (!sleepData || sleepData.metadata?.confidence === 'insufficient') {
+ return
+ Keine Schlafdaten vorhanden
+
+ }
+
+ const chartData = sleepData.data.labels.map((label, i) => ({
+ date: fmtDate(label),
+ duration: sleepData.data.datasets[0]?.data[i],
+ quality: sleepData.data.datasets[1]?.data[i]
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ Ø {sleepData.metadata.avg_duration_hours}h Schlaf
+
+ >
+ )
+ }
+
+ // R4: Sleep Debt
+ const renderSleepDebt = () => {
+ if (!debtData || debtData.metadata?.confidence === 'insufficient') {
+ return
+ Keine Schlafdaten für Schulden-Berechnung
+
+ }
+
+ const chartData = debtData.data.labels.map((label, i) => ({
+ date: fmtDate(label),
+ debt: debtData.data.datasets[0]?.data[i]
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ Aktuelle Schuld: {debtData.metadata.current_debt_hours.toFixed(1)}h
+
+ >
+ )
+ }
+
+ // R5: Vital Signs Matrix (Bar)
+ const renderVitalSigns = () => {
+ if (!vitalsData || vitalsData.metadata?.confidence === 'insufficient') {
+ return
+ Keine aktuellen Vitalwerte
+
+ }
+
+ const chartData = vitalsData.data.labels.map((label, i) => ({
+ name: label,
+ value: vitalsData.data.datasets[0]?.data[i]
+ }))
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ Letzte {vitalsData.metadata.data_points} Messwerte (7 Tage)
+
+ >
+ )
+ }
+
+ return (
+
+
+ {renderRecoveryScore()}
+
+
+
+ {renderHrvRhr()}
+
+
+
+ {renderSleepQuality()}
+
+
+
+ {renderSleepDebt()}
+
+
+
+ {renderVitalSigns()}
+
+
+ )
+}
diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx
index 9e96686..7a7e61e 100644
--- a/frontend/src/pages/History.jsx
+++ b/frontend/src/pages/History.jsx
@@ -12,6 +12,8 @@ import { getBfCategory } from '../utils/calc'
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
import Markdown from '../utils/Markdown'
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
+import NutritionCharts from '../components/NutritionCharts'
+import RecoveryCharts from '../components/RecoveryCharts'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
@@ -581,6 +583,13 @@ function NutritionSection({ nutrition, weights, profile, insights, onRequest, lo
BEWERTUNG
{macroRules.map((item,i)=>
)}
+
+ {/* New Nutrition Charts (Phase 0c) */}
+
+
📊 DETAILLIERTE CHARTS
+
+
+
)
@@ -915,10 +924,32 @@ function PhotoGrid() {
}
// ── Main ──────────────────────────────────────────────────────────────────────
+// ── Recovery Section ──────────────────────────────────────────────────────────
+function RecoverySection({ insights, onRequest, loadingSlug, filterActiveSlugs }) {
+ const [period, setPeriod] = useState(28)
+
+ return (
+