diff --git a/frontend/src/app.css b/frontend/src/app.css
index b9ce273..d7ca460 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -371,12 +371,20 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
margin-bottom: 16px;
}
-/* KPI: immer gleich breite Spalten — Mobile 2×2, Desktop 1×4 (kein „einzelne volle Zeile“) */
-.dashboard-stat-grid {
+/*
+ * Dashboard-Raster (KPI, Nebeneinander-Kacheln): 2 / 4 Spalten.
+ * StatCard, DashboardTile: span via --tile-sm / --tile-lg (JS clamp).
+ */
+.dashboard-stat-grid,
+.dashboard-tile-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
- margin-bottom: 16px;
+}
+
+.dashboard-stat-grid--mobile-4col,
+.dashboard-tile-grid--mobile-4col {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
}
.dashboard-stat-card {
@@ -385,34 +393,94 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we
padding: 12px 10px;
border: 1px solid var(--border);
transition: border-color 0.15s;
+}
+
+.dashboard-stat-card,
+.dashboard-tile {
min-width: 0;
box-sizing: border-box;
+ grid-column: span var(--tile-sm, 1);
}
-.dashboard-summary-row {
+@media (min-width: 1024px) {
+ .dashboard-stat-card,
+ .dashboard-tile {
+ grid-column: span var(--tile-lg, 1);
+ }
+}
+
+/* ── Dashboard-Abschnitte (Überschrift + Trennlinie) ─ */
+.dashboard-section {
+ margin-bottom: 22px;
+}
+
+.dashboard-section:last-child {
+ margin-bottom: 0;
+}
+
+.dashboard-section__header {
display: flex;
- gap: 8px;
- margin-bottom: 16px;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+ padding-bottom: 10px;
+ margin-bottom: 12px;
+ border-bottom: 1px solid var(--border);
}
-.dashboard-summary-row > .card {
+.dashboard-section__headline {
flex: 1;
min-width: 0;
}
+.dashboard-section__title {
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text3);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin: 0;
+}
+
+.dashboard-section__description {
+ font-size: 12px;
+ color: var(--text3);
+ margin: 4px 0 0 0;
+ line-height: 1.35;
+ max-width: 640px;
+}
+
+.dashboard-section__body {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.dashboard-section__actions {
+ flex-shrink: 0;
+}
+
+.dashboard-pill-row {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+/* Ernährung/Aktivität: Raster wie KPI; Kacheln per DashboardTile steuerbar */
+.dashboard-summary-row.dashboard-tile-grid {
+ margin-bottom: 0;
+}
+
+.dashboard-erholung-grid .dashboard-tile > .card,
+.dashboard-summary-row .dashboard-tile > .card {
+ height: 100%;
+}
+
@media (min-width: 1024px) {
- .dashboard-stat-grid {
+ .dashboard-stat-grid,
+ .dashboard-tile-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
-
- .dashboard-summary-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 12px;
- }
-
- .dashboard-summary-row > .card {
- flex: unset;
- }
}
diff --git a/frontend/src/components/DashboardSection.jsx b/frontend/src/components/DashboardSection.jsx
new file mode 100644
index 0000000..0609301
--- /dev/null
+++ b/frontend/src/components/DashboardSection.jsx
@@ -0,0 +1,31 @@
+/**
+ * Abschnitt mit optionalem Kopf (Titel, Beschreibung, Aktionen) und unterem Rand zur optischen Trennung.
+ */
+export default function DashboardSection({
+ title,
+ description,
+ headerRight,
+ children,
+ className = '',
+ bodyClassName = ''
+}) {
+ const showHeader = title || description || headerRight
+ return (
+
+ {showHeader && (
+
+ )}
+ {children}
+
+ )
+}
diff --git a/frontend/src/components/DashboardTile.jsx b/frontend/src/components/DashboardTile.jsx
new file mode 100644
index 0000000..4e86ef2
--- /dev/null
+++ b/frontend/src/components/DashboardTile.jsx
@@ -0,0 +1,29 @@
+import {
+ clampTileSpan,
+ DASHBOARD_TILE_GRID_COLS
+} from '../utils/dashboardLayout'
+
+/**
+ * Kachel im Raster `.dashboard-tile-grid` / `.dashboard-stat-grid`.
+ * Standard: volle Zeile (Mobile 2/2, Desktop 4/4). Anpassbar via spanMobile / spanDesktop.
+ */
+export default function DashboardTile({
+ children,
+ spanMobile = DASHBOARD_TILE_GRID_COLS.mobile,
+ spanDesktop = DASHBOARD_TILE_GRID_COLS.desktop,
+ className = ''
+}) {
+ const sm = clampTileSpan(spanMobile, DASHBOARD_TILE_GRID_COLS.mobile)
+ const lg = clampTileSpan(spanDesktop, DASHBOARD_TILE_GRID_COLS.desktop)
+ return (
+
+ {children}
+
+ )
+}
diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx
index fd0e4fa..eb2ae66 100644
--- a/frontend/src/pages/Dashboard.jsx
+++ b/frontend/src/pages/Dashboard.jsx
@@ -1,6 +1,6 @@
-import { useState, useEffect, useRef } from 'react'
+import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
-import { Check, ChevronRight, Brain } from 'lucide-react'
+import { Check, Brain } from 'lucide-react'
import {
LineChart, Line, XAxis, YAxis, Tooltip,
ResponsiveContainer, CartesianGrid
@@ -13,10 +13,17 @@ import EmailVerificationBanner from '../components/EmailVerificationBanner'
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
import SleepWidget from '../components/SleepWidget'
import RestDaysWidget from '../components/RestDaysWidget'
-import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
import Markdown from '../utils/Markdown'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
+import DashboardSection from '../components/DashboardSection'
+import DashboardTile from '../components/DashboardTile'
+import {
+ clampTileSpan,
+ DASHBOARD_TILE_GRID_COLS,
+ dashboardStatGridClassName,
+ dashboardTileGridClassName
+} from '../utils/dashboardLayout'
dayjs.locale('de')
// ── Helpers ───────────────────────────────────────────────────────────────────
@@ -144,14 +151,37 @@ function Pill({ label, value, status, sub }) {
}
// ── Stat Card ─────────────────────────────────────────────────────────────────
-function StatCard({ icon, label, value, unit, delta, deltaGoodWhenNeg=false, sub, onClick, color }) {
+/**
+ * KPI-Kachel im Dashboard-Raster (`dashboard-stat-grid` / `dashboard-tile-grid`).
+ * @param {number} [spanMobile=1] Spaltenbreite unter 1024px (max. = Raster-Spalten mobile)
+ * @param {number} [spanDesktop=1] Spaltenbreite ≥1024px (max. 4)
+ */
+function StatCard({
+ icon,
+ label,
+ value,
+ unit,
+ delta,
+ deltaGoodWhenNeg = false,
+ sub,
+ onClick,
+ color,
+ spanMobile = 1,
+ spanDesktop = 1
+}) {
const deltaColor = delta==null ? null
: (deltaGoodWhenNeg ? delta<0 : delta>0) ? 'var(--accent)' : 'var(--warn)'
+ const sm = clampTileSpan(spanMobile, DASHBOARD_TILE_GRID_COLS.mobile)
+ const lg = clampTileSpan(spanDesktop, DASHBOARD_TILE_GRID_COLS.desktop)
return (
onClick&&(e.currentTarget.style.borderColor='var(--accent)')}
onMouseLeave={e=>onClick&&(e.currentTarget.style.borderColor='var(--border)')}>
{icon}
@@ -260,7 +290,6 @@ export default function Dashboard() {
const runPipeline = async () => {
setPipelineLoading(true); setPipelineError(null)
try {
- const pid = localStorage.getItem('mitai-jinkendo_active_profile')||''
await api.insightPipeline()
await load()
} catch(e) {
@@ -268,12 +297,7 @@ export default function Dashboard() {
} finally { setPipelineLoading(false) }
}
- useEffect(()=>{
- console.log('[Dashboard] Component mounted, loading data...')
- load()
- },[])
-
- console.log('[Dashboard] Rendering, loading=', loading, 'activeProfile=', activeProfile?.name)
+ useEffect(()=>{ load() },[])
if (loading) return
@@ -317,7 +341,11 @@ export default function Dashboard() {
const hasAnyData = latestW||latestCal||nutrition.length>0
- console.log('[Dashboard] hasAnyData=', hasAnyData, 'latestW=', !!latestW, 'latestCal=', !!latestCal, 'nutrition.length=', nutrition.length)
+ const showNutrSummary = !!(avgKcal || avgProtein)
+ const showActSummary = actKcal != null
+ const summaryBoth = showNutrSummary && showActSummary
+ const summarySpanM = summaryBoth ? 1 : 2
+ const summarySpanD = summaryBoth ? 2 : 4
return (
@@ -349,46 +377,54 @@ export default function Dashboard() {
)}
{hasAnyData && <>
- {/* Quick weight entry */}
-
-
-
⚖️ Gewicht heute
-
+ }
+ >
+
+
-
-
+
- {/* Key metrics — Mobile: flex-wrap; Desktop: 4-spaltig (RESPONSIVE_UI P3) */}
-
- nav('/history')} color="#378ADD"/>
- {latestCal?.body_fat_pct && nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>}
- {latestCal?.lean_mass && nav('/history',{state:{tab:'body'}})}/>}
- {avgKcal && nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>}
-
-
- {/* Status pills */}
- {pills.length > 0 && (
-
- {pills.map((p,i)=>
)}
+
+
+ nav('/history')} color="#378ADD"/>
+ {latestCal?.body_fat_pct && nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>}
+ {latestCal?.lean_mass && nav('/history',{state:{tab:'body'}})}/>}
+ {avgKcal && nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>}
- )}
+ {pills.length > 0 && (
+
+ )}
+
- {/* Goals progress */}
{(activeProfile?.goal_weight||activeProfile?.goal_bf_pct) && latestW && (
-
-
🎯 Ziele
+
+
{activeProfile?.goal_weight && latestW && (()=>{
const start = Math.max(...weights.map(w=>w.weight))
const curr = latestW.weight
@@ -429,134 +465,167 @@ export default function Dashboard() {
)
})()}
+
)}
- {/* Combined chart */}
{(weights.length>2||nutrition.length>2) && (
-
-
-
📊 Kalorien + Gewicht (30 Tage)
-
-
-
-
- Ø Kalorien
- Gewicht
-
-
+ }
+ >
+
+
+
+
+ Ø Kalorien
+ Gewicht
+
+
+
+
)}
- {/* Activity + Nutrition summary row */}
-
- {(avgKcal||avgProtein) && (
-
nav('/history',{state:{tab:'nutrition'}})}>
-
🍽️ ERNÄHRUNG (Ø 7T)
- {avgKcal &&
{avgKcal} kcal
}
- {avgProtein &&
- {avgProtein}g Protein {proteinOk?'✓':'⚠️'}
-
}
-
→ Verlauf Ernährung
+ {(showNutrSummary || showActSummary) && (
+
+
+ {showNutrSummary && (
+
+ nav('/history',{state:{tab:'nutrition'}})}>
+
🍽️ ERNÄHRUNG (Ø 7T)
+ {avgKcal &&
{avgKcal} kcal
}
+ {avgProtein &&
+ {avgProtein}g Protein {proteinOk?'✓':'⚠️'}
+
}
+
→ Verlauf Ernährung
+
+
+ )}
+ {showActSummary && (
+
+ nav('/history',{state:{tab:'activity'}})}>
+
🏋️ AKTIVITÄT (7T)
+
{actKcal} kcal
+
{recentAct.length} Trainings
+
→ Verlauf Aktivität
+
+
+ )}
- )}
- {actKcal!=null && (
- nav('/history',{state:{tab:'activity'}})}>
-
🏋️ AKTIVITÄT (7T)
-
{actKcal} kcal
-
{recentAct.length} Trainings
-
→ Verlauf Aktivität
-
- )}
-
+
+ )}
- {/* Sleep Widget */}
-
-
-
+
+
+
+
+
+
+
+
+
+
- {/* Rest Days Widget */}
-
-
-
-
- {/* Training Type Distribution */}
{activities.length > 0 && (
-
-
-
🏋️ Trainingstyp-Verteilung
-
-
-
-
+ }
+ >
+
+
+
+
+
+
)}
- {/* Goals Preview */}
-
nav('/goals')}>
-
-
🎯 Ziele
-
-
-
- Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen
-
-
+ }
+ >
+
+ nav('/goals')}>
+
+ Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen
+
+
+
+
- {/* Latest AI insight */}
-
-
-
🤖 KI-Auswertung
-
-
- {/* Pipeline trigger */}
-