From e97ffe17011eadd7ec0b8817be51785099f4c859 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 1 Sep 2026 14:57:28 +0200 Subject: [PATCH] Update --- .../boehmitools/training/domain/JsonDomain.kt | 16 + .../de/drazz/boehmitools/training/ui/App.kt | 159 +- .../training/ui/setup/SettingsScreen.kt | 124 +- .../boehmitools/training/ui/theme/Theme.kt | 81 +- .../training/ui/tracker/TrackerScreen.kt | 1405 +- .../training/domain/ResultDataEditingTest.kt | 91 + android/gradlew | 0 plugins/trainingsplan/TOOL-README.md | 161 +- plugins/trainingsplan/app.py | 738 +- plugins/trainingsplan/backend.py | 20 +- plugins/trainingsplan/plan_builder.py | 669 - plugins/trainingsplan/plugin.json | 28 +- .../trainingsplan/presets/couchsnacks.json | 1607 -- .../trainingsplan/presets/fruehstueck.json | 1240 -- plugins/trainingsplan/presets/granola.json | 530 - .../trainingsplan/presets/nachmittags.json | 1450 -- plugins/trainingsplan/recipe_builder.py | 399 - plugins/trainingsplan/schema_contract.py | 568 +- plugins/trainingsplan/static/index.html | 1118 - plugins/trainingsplan/tests/test_contract.py | 157 +- testdata/plans/phase-1.json | 16995 ---------------- 21 files changed, 2028 insertions(+), 25528 deletions(-) create mode 100644 android/app/src/test/java/de/drazz/boehmitools/training/domain/ResultDataEditingTest.kt mode change 100644 => 100755 android/gradlew delete mode 100644 plugins/trainingsplan/plan_builder.py delete mode 100644 plugins/trainingsplan/presets/couchsnacks.json delete mode 100644 plugins/trainingsplan/presets/fruehstueck.json delete mode 100644 plugins/trainingsplan/presets/granola.json delete mode 100644 plugins/trainingsplan/presets/nachmittags.json delete mode 100644 plugins/trainingsplan/recipe_builder.py delete mode 100644 plugins/trainingsplan/static/index.html delete mode 100644 testdata/plans/phase-1.json diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/domain/JsonDomain.kt b/android/app/src/main/java/de/drazz/boehmitools/training/domain/JsonDomain.kt index 35e4440..0a78a30 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/domain/JsonDomain.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/domain/JsonDomain.kt @@ -511,3 +511,19 @@ fun resultDataWithSets(data: JsonObject, sets: Int): JsonObject { } return result } + +fun resultDataWithoutSet(data: JsonObject, index: Int): JsonObject { + val currentCount = data.int("sets", 1).coerceIn(1, 20) + if (currentCount <= 1 || index !in 0 until currentCount) return data + + val values = data.toMutableMap() + val newCount = currentCount - 1 + values["sets"] = JsonPrimitive(newCount) + for (field in listOf("values", "left_values", "right_values")) { + val list = data.array(field).toMutableList() + if (index in list.indices) list.removeAt(index) + while (list.size < newCount) list.add(JsonNull) + values[field] = JsonArray(list.take(newCount)) + } + return JsonObject(values) +} diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt index b6714b9..f225bdf 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/App.kt @@ -1,29 +1,35 @@ package de.drazz.boehmitools.training.ui +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Analytics import androidx.compose.material.icons.filled.FitnessCenter +import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState @@ -45,64 +51,106 @@ fun AppRoot( ) { val navController = rememberNavController() val destinations = listOf( - AppDestination("tracker", "Tracking") { Icon(Icons.Default.FitnessCenter, null) }, - AppDestination("settings", "Mehr") { Icon(Icons.Default.Settings, null) }, + AppDestination("plan", "Plan") { Icon(Icons.Default.Home, null) }, + AppDestination("tracking", "Tracking") { Icon(Icons.Default.FitnessCenter, null) }, + AppDestination("analysis", "Analyse") { Icon(Icons.Default.Analytics, null) }, + AppDestination("more", "Mehr") { Icon(Icons.Default.Settings, null) }, ) val backStack by navController.currentBackStackEntryAsState() + val currentRoute = backStack?.destination?.route ?: "tracking" - Scaffold( - containerColor = MaterialTheme.colorScheme.background, - bottomBar = { - Box(Modifier.padding(horizontal = 18.dp, vertical = 12.dp)) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) { + Scaffold( + containerColor = Color.Transparent, + bottomBar = { + CompactBottomBar(destinations, currentRoute) { route -> + navController.navigate(route) { + popUpTo(navController.graph.startDestinationId) { saveState = true } + launchSingleTop = true + restoreState = true + } + } + }, + ) { padding -> + NavHost(navController = navController, startDestination = "tracking") { + composable("plan") { TrackerScreen(contentPadding = paddedContent(padding), initialTab = "plan") } + composable("tracking") { TrackerScreen(contentPadding = paddedContent(padding), initialTab = "session") } + composable("analysis") { TrackerScreen(contentPadding = paddedContent(padding), initialTab = "progress") } + composable("more") { + SettingsScreen( + contentPadding = paddedContent(padding), + current = settings, + onTheme = onTheme, + onShowWhatsNew = onShowWhatsNew, + ) + } + } + } + + if (showWhatsNew) { + WhatsNewDialog(onDismiss = onDismissWhatsNew) + } + } +} + +private fun paddedContent(original: PaddingValues): PaddingValues = PaddingValues( + top = original.calculateTopPadding(), + bottom = original.calculateBottomPadding(), +) + +@Composable +private fun CompactBottomBar( + destinations: List, + currentRoute: String, + onNavigate: (String) -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 1.dp, + shadowElevation = 6.dp, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 6.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + destinations.forEach { destination -> + val selected = currentRoute == destination.route Surface( - shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surface, - shadowElevation = 14.dp, - tonalElevation = 4.dp, + onClick = { onNavigate(destination.route) }, + modifier = Modifier.weight(1f), + color = Color.Transparent, ) { - NavigationBar( - containerColor = Color.Transparent, - tonalElevation = 0.dp, + Column( + modifier = Modifier.padding(vertical = 6.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - destinations.forEach { destination -> - val selected = backStack?.destination?.hierarchy?.any { it.route == destination.route } == true - NavigationBarItem( - selected = selected, - onClick = { - navController.navigate(destination.route) { - popUpTo("tracker") { saveState = true } - launchSingleTop = true - restoreState = true - } - }, - icon = destination.icon, - label = { Text(destination.label) }, - colors = NavigationBarItemDefaults.colors( - indicatorColor = MaterialTheme.colorScheme.primaryContainer, - selectedIconColor = MaterialTheme.colorScheme.primary, - selectedTextColor = MaterialTheme.colorScheme.primary, - ), - ) + Box( + modifier = Modifier + .size(34.dp) + .clip(CircleShape) + .background(if (selected) MaterialTheme.colorScheme.primary.copy(alpha = 0.16f) else Color.Transparent), + contentAlignment = Alignment.Center, + ) { + destination.icon() } + Text( + destination.label, + style = MaterialTheme.typography.labelMedium, + color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, + ) } } } - }, - ) { padding -> - NavHost(navController = navController, startDestination = "tracker", modifier = Modifier.fillMaxSize()) { - composable("tracker") { TrackerScreen(contentPadding = padding) } - composable("settings") { - SettingsScreen( - contentPadding = padding, - current = settings, - onTheme = onTheme, - onShowWhatsNew = onShowWhatsNew, - ) - } } } - - if (showWhatsNew) WhatsNewDialog(onDismiss = onDismissWhatsNew) } @Composable @@ -110,7 +158,14 @@ private fun WhatsNewDialog(onDismiss: () -> Unit) { AlertDialog( onDismissRequest = onDismiss, title = { Text("Neu in RipMe ${BuildConfig.VERSION_NAME}") }, - text = { Text("Modern Card UI, sicherer Offline-Tracker und vollständige Analyseansichten.") }, + text = { + Text( + "• RipMe konzentriert sich vollständig auf den Trainingstracker.\n" + + "• Analyseergebnisse werden jetzt vollständig in der App angezeigt.\n" + + "• Wochen und Trainingstage zeigen ihren aktuellen Abschlussstatus.\n" + + "• Offline erfasste Sessionänderungen werden weiterhin sicher synchronisiert.", + ) + }, confirmButton = { TextButton(onClick = onDismiss) { Text("Verstanden") } }, ) } diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/setup/SettingsScreen.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/setup/SettingsScreen.kt index f68c859..1e21cfd 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/setup/SettingsScreen.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/setup/SettingsScreen.kt @@ -2,6 +2,7 @@ package de.drazz.boehmitools.training.ui.setup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -9,24 +10,27 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CloudDone import androidx.compose.material.icons.filled.Info import androidx.compose.material3.Button -import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -49,90 +53,60 @@ fun SettingsScreen( .padding(contentPadding) .verticalScroll(rememberScrollState()) .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), ) { - Text("RipMe Einstellungen", style = MaterialTheme.typography.headlineMedium) + Text("Mehr", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text("Einstellungen & App", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("Server & Anmeldung", style = MaterialTheme.typography.titleLarge) - Text( - "Basic-Auth-Zugangsdaten werden mit einem Android-Keystore-Schlüssel verschlüsselt. Release-Builds akzeptieren ausschließlich HTTPS.", - style = MaterialTheme.typography.bodyMedium, - ) - OutlinedTextField( - value = state.serverUrl, - onValueChange = viewModel::updateServer, - modifier = Modifier.fillMaxWidth(), - label = { Text("Server-URL") }, - singleLine = true, - ) - OutlinedTextField( - value = state.username, - onValueChange = viewModel::updateUsername, - modifier = Modifier.fillMaxWidth(), - label = { Text("Benutzername") }, - singleLine = true, - ) - OutlinedTextField( - value = state.password, - onValueChange = viewModel::updatePassword, - modifier = Modifier.fillMaxWidth(), - label = { Text("Passwort") }, - visualTransformation = PasswordVisualTransformation(), - singleLine = true, - ) - Button( - onClick = viewModel::saveAndTest, - enabled = !state.loading, - modifier = Modifier.fillMaxWidth(), - ) { - if (state.loading) { - CircularProgressIndicator(modifier = Modifier.height(18.dp), strokeWidth = 2.dp) - } else { - Icon(Icons.Default.CloudDone, null) - } - Spacer(Modifier.width(8.dp)) - Text("Speichern & Verbindung testen") + SettingsCard(title = "Server & Anmeldung", subtitle = "Zugangsdaten werden lokal verschlüsselt gespeichert.") { + OutlinedTextField(value = state.serverUrl, onValueChange = viewModel::updateServer, modifier = Modifier.fillMaxWidth(), label = { Text("Server-URL") }, singleLine = true) + OutlinedTextField(value = state.username, onValueChange = viewModel::updateUsername, modifier = Modifier.fillMaxWidth(), label = { Text("Benutzername") }, singleLine = true) + OutlinedTextField(value = state.password, onValueChange = viewModel::updatePassword, modifier = Modifier.fillMaxWidth(), label = { Text("Passwort") }, visualTransformation = PasswordVisualTransformation(), singleLine = true) + Button(onClick = viewModel::saveAndTest, enabled = !state.loading, modifier = Modifier.fillMaxWidth()) { + if (state.loading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Default.CloudDone, null) } - if (state.message.isNotBlank()) Text(state.message, color = MaterialTheme.colorScheme.primary) - if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error) + Spacer(Modifier.size(8.dp)) + Text("Speichern & Verbindung testen") } + if (state.message.isNotBlank()) Text(state.message, color = MaterialTheme.colorScheme.primary) + if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error) } - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("Darstellung", style = MaterialTheme.typography.titleLarge) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(6.dp), - ) { - listOf("system" to "System", "light" to "Hell", "dark" to "Dunkel").forEach { (value, label) -> - FilterChip( - selected = current.theme == value, - onClick = { onTheme(value) }, - label = { Text(label, maxLines = 1) }, - modifier = Modifier.weight(1f), - ) - } + SettingsCard(title = "Darstellung", subtitle = "Modern Card Style in Hell, Dunkel oder System.") { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { + listOf("system" to "System", "light" to "Hell", "dark" to "Dunkel").forEach { (value, label) -> + FilterChip(selected = current.theme == value, onClick = { onTheme(value) }, label = { Text(label) }, modifier = Modifier.weight(1f)) } } } - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Über RipMe", style = MaterialTheme.typography.titleLarge) - Text("Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") - Text( - "Native Android-App für deinen Trainingstracker.", - style = MaterialTheme.typography.bodyMedium, - ) - OutlinedButton(onClick = onShowWhatsNew, modifier = Modifier.fillMaxWidth()) { - Icon(Icons.Default.Info, null) - Spacer(Modifier.width(8.dp)) - Text("Was ist neu?") - } + SettingsCard(title = "Über RipMe", subtitle = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") { + Text("Native Android-App für deinen Trainingstracker im Modern-Card-Stil.") + OutlinedButton(onClick = onShowWhatsNew, modifier = Modifier.fillMaxWidth()) { + Icon(Icons.Default.Info, null) + Spacer(Modifier.size(8.dp)) + Text("Was ist neu?") } } } } + +@Composable +private fun SettingsCard(title: String, subtitle: String, content: @Composable ColumnScope.() -> Unit) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surface, + tonalElevation = 1.dp, + shadowElevation = 8.dp, + ) { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp), content = { + Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + content() + }) + } +} diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt index 8661e35..f3c6d9f 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/theme/Theme.kt @@ -1,46 +1,66 @@ package de.drazz.boehmitools.training.ui.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.Typography import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp private val Light = lightColorScheme( - primary = Color(0xFF7C3AED), + primary = Color(0xFF8E56FF), onPrimary = Color.White, - primaryContainer = Color(0xFFEDE9FE), - onPrimaryContainer = Color(0xFF2E1065), - secondary = Color(0xFF4F46E5), + primaryContainer = Color(0xFFEDE3FF), + onPrimaryContainer = Color(0xFF2B1459), + secondary = Color(0xFF6A7086), onSecondary = Color.White, - secondaryContainer = Color(0xFFE0E7FF), - tertiary = Color(0xFF06B6D4), - background = Color(0xFFF4F5FB), - onBackground = Color(0xFF171725), + secondaryContainer = Color(0xFFF0F2F8), + onSecondaryContainer = Color(0xFF1C2132), + tertiary = Color(0xFFAC8BFF), + background = Color(0xFFF5F6FB), + onBackground = Color(0xFF171923), surface = Color(0xFFFFFFFF), - onSurface = Color(0xFF171725), - surfaceVariant = Color(0xFFF0F1F7), - onSurfaceVariant = Color(0xFF69697A), - outline = Color(0xFFD9DAE5), + onSurface = Color(0xFF171923), + surfaceVariant = Color(0xFFF1F2F8), + onSurfaceVariant = Color(0xFF666B7B), + outline = Color(0xFFDEE2EE), + outlineVariant = Color(0xFFE9ECF6), + error = Color(0xFFC54646), + onError = Color.White, ) private val Dark = darkColorScheme( - primary = Color(0xFFA78BFA), - onPrimary = Color(0xFF24005B), - primaryContainer = Color(0xFF4C1D95), - onPrimaryContainer = Color(0xFFEDE9FE), - secondary = Color(0xFF818CF8), - onSecondary = Color(0xFF10144A), - secondaryContainer = Color(0xFF312E81), - tertiary = Color(0xFF22D3EE), - background = Color(0xFF0B0D17), - onBackground = Color(0xFFF5F5FA), - surface = Color(0xFF151824), - onSurface = Color(0xFFF5F5FA), - surfaceVariant = Color(0xFF202432), - onSurfaceVariant = Color(0xFFB8BBC8), - outline = Color(0xFF343849), + primary = Color(0xFFA66BFF), + onPrimary = Color(0xFF1D0C45), + primaryContainer = Color(0xFF2A175B), + onPrimaryContainer = Color(0xFFEFE7FF), + secondary = Color(0xFFAAB1C8), + onSecondary = Color(0xFF1B2236), + secondaryContainer = Color(0xFF1C2235), + onSecondaryContainer = Color(0xFFDEE3F7), + tertiary = Color(0xFFCC9DFF), + background = Color(0xFF0B1020), + onBackground = Color(0xFFF3F5FB), + surface = Color(0xFF161C2D), + onSurface = Color(0xFFF3F5FB), + surfaceVariant = Color(0xFF21283C), + onSurfaceVariant = Color(0xFFB1B8CD), + outline = Color(0xFF363D55), + outlineVariant = Color(0xFF272E45), + error = Color(0xFFFF8A8A), + onError = Color(0xFF320F16), +) + +private val AppShapes = Shapes( + extraSmall = RoundedCornerShape(12.dp), + small = RoundedCornerShape(18.dp), + medium = RoundedCornerShape(24.dp), + large = RoundedCornerShape(28.dp), + extraLarge = RoundedCornerShape(32.dp), ) @Composable @@ -50,5 +70,10 @@ fun TrainingTheme(theme: String, content: @Composable () -> Unit) { "light" -> false else -> isSystemInDarkTheme() } - MaterialTheme(colorScheme = if (dark) Dark else Light, content = content) + MaterialTheme( + colorScheme = if (dark) Dark else Light, + typography = Typography(), + shapes = AppShapes, + content = content, + ) } diff --git a/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt b/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt index c6aeb9e..6ddee9f 100644 --- a/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt +++ b/android/app/src/main/java/de/drazz/boehmitools/training/ui/tracker/TrackerScreen.kt @@ -1,10 +1,13 @@ package de.drazz.boehmitools.training.ui.tracker +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -12,34 +15,37 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.CloudOff +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.RemoveCircleOutline import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -47,12 +53,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight -import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -64,15 +71,15 @@ import de.drazz.boehmitools.training.domain.PlanUi import de.drazz.boehmitools.training.domain.ResultSchemaUi import de.drazz.boehmitools.training.domain.array import de.drazz.boehmitools.training.domain.bool -import de.drazz.boehmitools.training.domain.emptyResultData -import de.drazz.boehmitools.training.domain.formatResultData import de.drazz.boehmitools.training.domain.int import de.drazz.boehmitools.training.domain.itemFor import de.drazz.boehmitools.training.domain.normalizedResultData import de.drazz.boehmitools.training.domain.obj import de.drazz.boehmitools.training.domain.resolveResultSchema import de.drazz.boehmitools.training.domain.resultDataWithSets +import de.drazz.boehmitools.training.domain.resultDataWithoutSet import de.drazz.boehmitools.training.domain.resultDataWithValue +import de.drazz.boehmitools.training.domain.sessionKey import de.drazz.boehmitools.training.domain.string import de.drazz.boehmitools.training.domain.trackerSession import de.drazz.boehmitools.training.domain.updated @@ -81,101 +88,85 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.doubleOrNull import kotlinx.serialization.json.jsonPrimitive +import java.util.Locale -@OptIn(ExperimentalMaterial3Api::class) @Composable fun TrackerScreen( contentPadding: PaddingValues, + initialTab: String = "session", viewModel: TrackerViewModel = hiltViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() val pending by viewModel.pending.collectAsStateWithLifecycle() + val plan = state.plan + val day = plan?.days?.firstOrNull { it.num == state.day } val currentPending = pending.filter { it.planId == state.selectedPlanId } var planMenu by remember { mutableStateOf(false) } - val tabs = listOf("session" to "Session", "progress" to "Fortschritt", "plan" to "Plan", "faq" to "FAQ") + + val screenMode = when (initialTab) { + "progress" -> "progress" + "plan" -> "plan" + else -> "session" + } Column( - Modifier + modifier = Modifier .fillMaxSize() .padding(contentPadding) - .background(MaterialTheme.colorScheme.background), + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), ) { - Row( - Modifier.fillMaxWidth().padding(start = 20.dp, end = 12.dp, top = 16.dp, bottom = 10.dp), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(Modifier.weight(1f)) { - Text("TRAININGSPLAN", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.primary) - Text( - state.plan?.title ?: "RipMe", - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.Bold, - maxLines = 2, - ) - if (state.saveState.isNotBlank()) { - Text(state.saveState, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - } - } - Box { - IconButton(onClick = { planMenu = true }) { Icon(Icons.Default.MoreVert, contentDescription = "Plan wechseln") } - DropdownMenu(expanded = planMenu, onDismissRequest = { planMenu = false }) { - state.plans.forEach { plan -> - DropdownMenuItem(text = { Text(plan.title) }, onClick = { planMenu = false; viewModel.selectPlan(plan.id) }) - } - } - } - } - - Row( - Modifier - .padding(horizontal = 16.dp, vertical = 4.dp) - .fillMaxWidth() - .clip(RoundedCornerShape(18.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) - .padding(4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - tabs.forEach { (id, label) -> - val selected = state.tab == id - Surface( - modifier = Modifier.weight(1f), - shape = RoundedCornerShape(14.dp), - color = if (selected) MaterialTheme.colorScheme.surface else Color.Transparent, - shadowElevation = if (selected) 3.dp else 0.dp, - onClick = { viewModel.setTab(id) }, - ) { - Text( - label, - modifier = Modifier.padding(vertical = 10.dp), - textAlign = androidx.compose.ui.text.style.TextAlign.Center, - style = MaterialTheme.typography.labelMedium, - fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium, - color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + CompactHeader( + title = when (screenMode) { + "plan" -> plan?.title ?: "Plan" + "progress" -> plan?.title ?: "Tracking" + else -> day?.focus ?: plan?.title ?: "Tracking" + }, + subtitle = "Woche ${state.week}", + onMenu = { planMenu = true }, + ) + Box { + DropdownMenu(expanded = planMenu, onDismissRequest = { planMenu = false }) { + state.plans.forEach { item -> + DropdownMenuItem( + text = { Text(item.title) }, + onClick = { + planMenu = false + viewModel.selectPlan(item.id) + }, ) } } } - Box(Modifier.fillMaxSize()) { - when { - state.loading && state.plan == null -> Box(Modifier.fillMaxSize().padding(32.dp)) { CircularProgressIndicator() } - state.error.isNotBlank() && state.plan == null -> ErrorPane(state.error) { viewModel.loadPlans(force = true) } - state.plan == null -> ErrorPane("Kein Trainingsplan verfügbar.") { viewModel.loadPlans(force = true) } - else -> when (state.tab) { - "progress" -> ProgressPane(state, currentPending, viewModel) - "plan" -> PlanPane(state.plan!!) - "faq" -> FaqPane(state) - else -> SessionPane(state, currentPending, viewModel) - } + if (state.loading && plan == null) { + LoadingCard() + } else if (state.error.isNotBlank() && plan == null) { + ErrorPane(state.error) { viewModel.loadPlans(force = true) } + } else if (plan == null || day == null) { + ErrorPane("Kein Trainingsplan verfügbar.") { viewModel.loadPlans(force = true) } + } else { + TopWeekStrip(plan, state.week, state.day, state.tracker, viewModel) + when (screenMode) { + "progress" -> ProgressPane(state, currentPending, viewModel) + "plan" -> PlanPane(plan, state) + else -> SessionPane(state, day, currentPending, viewModel) } } + + Spacer(Modifier.height(18.dp)) } state.conflict?.let { conflict -> AlertDialog( onDismissRequest = viewModel::dismissConflict, title = { Text("Revisionskonflikt") }, - text = { Text("Der Server steht bereits auf Revision ${conflict.currentRevision}. Deine lokale Änderung wird nicht automatisch überschrieben.") }, + text = { + Text( + "Der Server steht bereits auf Revision ${conflict.currentRevision}. Deine lokale Änderung wird nicht automatisch überschrieben. Lade den Serverstand neu und wende die Änderung bewusst erneut an.", + ) + }, confirmButton = { TextButton(onClick = viewModel::reloadAfterConflict) { Text("Serverstand neu laden") } }, dismissButton = { TextButton(onClick = viewModel::dismissConflict) { Text("Lokale Ansicht behalten") } }, ) @@ -183,152 +174,246 @@ fun TrackerScreen( } @Composable -private fun ErrorPane(message: String, retry: () -> Unit) { - Column(Modifier.fillMaxSize().padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text(message, color = MaterialTheme.colorScheme.error) - Button(onClick = retry) { Icon(Icons.Default.Refresh, null); Spacer(Modifier.width(8.dp)); Text("Neu laden") } +private fun CompactHeader(title: String, subtitle: String, onMenu: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + Surface(shape = CircleShape, color = MaterialTheme.colorScheme.surfaceVariant) { + IconButton(onClick = onMenu) { Icon(Icons.Default.MoreVert, contentDescription = "Plan wechseln") } + } } } @Composable -private fun SessionPane(state: TrackerUiState, pending: List, vm: TrackerViewModel) { - val plan = state.plan ?: return - val day = plan.days.firstOrNull { it.num == state.day } ?: return - val session = trackerSession(state.tracker, de.drazz.boehmitools.training.domain.sessionKey(state.week, state.day), plan) - Column( - Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 16.dp, vertical = 14.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), - ) { - if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error) - if (pending.isNotEmpty()) PendingQueue(pending, vm) - SessionHeader(plan, day, session, state.week, vm) - WeekDayPicker(plan, state.tracker, state.week, state.day, vm) +private fun LoadingCard() { + ModernCard(title = "Laden") { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { + CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp) + Text("Trainingsdaten werden geladen …") + } + } +} - SequenceCard("Aufwärmen", "warmup", day.warmup, session, vm) - day.rotations.forEach { rotation -> - Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text(rotation.label.uppercase(), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) - rotation.exercises.forEach { exercise -> ExerciseCard(exercise, session, plan, vm) } +@Composable +private fun ErrorPane(message: String, retry: () -> Unit) { + ModernCard(title = "Fehler") { + Text(message, color = MaterialTheme.colorScheme.error) + Button(onClick = retry) { + Icon(Icons.Default.Refresh, null) + Spacer(Modifier.size(8.dp)) + Text("Neu laden") + } + } +} + +@Composable +private fun TopWeekStrip(plan: PlanUi, week: Int, day: Int, tracker: JsonObject, vm: TrackerViewModel) { + val dayLabels = listOf("Mo", "Di", "Mi", "Do", "Fr", "Sa", "So") + ModernCard(noPadding = true) { + Column(Modifier.padding(horizontal = 14.dp, vertical = 14.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(18.dp), + ) { + plan.days.forEachIndexed { index, item -> + val selected = item.num == day + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text(dayLabels.getOrElse(index) { "T${item.num}" }, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(if (selected) MaterialTheme.colorScheme.primary else Color.Transparent) + .clickable { vm.setDay(item.num) }, + contentAlignment = Alignment.Center, + ) { + Text( + item.num.toString(), + color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) + } + } + } + } + Row( + modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + (1..plan.weeks).forEach { w -> + val sessions = plan.days.mapNotNull { d -> tracker.obj("sessions")[sessionKey(w, d.num)] as? JsonObject } + val done = sessions.count { it.string("status") == "completed" } + SmallPill( + text = if (done > 0) "W$w · $done" else "W$w", + selected = w == week, + ) { vm.setWeek(w) } + } } } - SequenceCard("Cooldown", "cooldown", day.cooldown, session, vm) - SequenceCard("Stretch", "stretch", day.stretch, session, vm) - Spacer(Modifier.height(20.dp)) + } +} + +@Composable +private fun SessionPane(state: TrackerUiState, day: DayUi, pending: List, vm: TrackerViewModel) { + val plan = state.plan ?: return + val session = trackerSession(state.tracker, sessionKey(state.week, state.day), plan) + val status = session.string("status", "planned") + val completedDays = plan.days.count { d -> + (state.tracker.obj("sessions")[sessionKey(state.week, d.num)] as? JsonObject)?.string("status") == "completed" + } + + WeeklyHeroCard( + title = "Wochenerfolg", + countLabel = "$completedDays / ${plan.days.size}", + subLabel = "Einheiten", + progress = completedDays.toFloat() / plan.days.size.coerceAtLeast(1), + ) + + if (pending.isNotEmpty()) PendingQueue(pending, vm) + + SessionStatusCard(plan, day, session, state.week, status, vm) + + day.rotations.forEach { rotation -> + rotation.exercises.forEach { exercise -> + ExerciseCard(exercise, session, plan, vm) + } + } + + if (day.warmup.isNotEmpty()) SequenceCard("Aufwärmen", "warmup", day.warmup, session, vm) + if (day.cooldown.isNotEmpty()) SequenceCard("Cooldown", "cooldown", day.cooldown, session, vm) + if (day.stretch.isNotEmpty()) SequenceCard("Stretch", "stretch", day.stretch, session, vm) +} + +@Composable +private fun WeeklyHeroCard(title: String, countLabel: String, subLabel: String, progress: Float) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + shadowElevation = 10.dp, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + Brush.linearGradient( + listOf( + MaterialTheme.colorScheme.primary.copy(alpha = 0.96f), + MaterialTheme.colorScheme.tertiary.copy(alpha = 0.85f), + ) + ) + ) + .padding(18.dp), + ) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text(title, color = MaterialTheme.colorScheme.onPrimary, style = MaterialTheme.typography.labelLarge) + Text(countLabel, color = MaterialTheme.colorScheme.onPrimary, style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold) + Text(subLabel, color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.9f), style = MaterialTheme.typography.bodyMedium) + Box( + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .clip(CircleShape) + .background(Color.White.copy(alpha = 0.25f)), + ) { + Box( + modifier = Modifier + .fillMaxWidth(progress.coerceIn(0f, 1f)) + .height(6.dp) + .clip(CircleShape) + .background(Color.White), + ) + } + } + } + } +} + +@Composable +private fun SessionStatusCard(plan: PlanUi, day: DayUi, session: JsonObject, week: Int, status: String, vm: TrackerViewModel) { + val statusLabel = when (status) { + "in_progress" -> "Läuft" + "stopped" -> "Gestoppt" + "completed" -> "Abgeschlossen" + else -> "Geplant" + } + ModernCard(title = day.focus, subtitle = "Woche $week · Tag ${day.num} · Status: $statusLabel") { + Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)}", color = MaterialTheme.colorScheme.onSurfaceVariant) + when (status) { + "in_progress" -> Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { + Button(onClick = { vm.setSessionStatus("completed") }, modifier = Modifier.weight(1f)) { + Icon(Icons.Default.CheckCircle, null) + Spacer(Modifier.size(6.dp)) + Text("Abschließen") + } + OutlinedButton(onClick = { vm.setSessionStatus("stopped") }, modifier = Modifier.weight(1f)) { + Icon(Icons.Default.Stop, null) + Spacer(Modifier.size(6.dp)) + Text("Stoppen") + } + } + "stopped" -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { + Icon(Icons.Default.PlayArrow, null) + Spacer(Modifier.size(6.dp)) + Text("Session fortsetzen") + } + "completed" -> OutlinedButton(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { + Text("Session wieder öffnen") + } + else -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { + Icon(Icons.Default.PlayArrow, null) + Spacer(Modifier.size(6.dp)) + Text("Session starten") + } + } + TextButton(onClick = vm::resetSession, modifier = Modifier.fillMaxWidth()) { Text("Session zurücksetzen") } } } @Composable private fun PendingQueue(pending: List, vm: TrackerViewModel) { - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Icon(Icons.Default.CloudOff, null) - Text("${pending.size} ausstehende Änderung(en)", fontWeight = FontWeight.Bold) - } - pending.take(3).forEach { item -> - Text("${item.sessionKey} · Revision ${item.expectedRevision} · ${item.state}", style = MaterialTheme.typography.bodySmall) - if (item.state == PendingMutation.STATE_CONFLICT) { - Text(item.lastError, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall) - TextButton(onClick = { vm.discardQueuedConflict(item.id) }) { Text("Konflikt-Patch verwerfen") } + ModernCard(title = "Ausstehende Änderungen", subtitle = "Offline erfasste Patches warten auf Synchronisation.") { + pending.take(3).forEach { item -> + Surface(shape = RoundedCornerShape(18.dp), color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier.fillMaxWidth()) { + Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.CloudOff, null) + Text(item.sessionKey, fontWeight = FontWeight.SemiBold) + } + Text("Revision ${item.expectedRevision} · ${item.state}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + if (item.state == PendingMutation.STATE_CONFLICT) { + Text(item.lastError, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error) + TextButton(onClick = { vm.discardQueuedConflict(item.id) }) { Text("Konflikt-Patch verwerfen") } + } } } } } } -@Composable -private fun WeekDayPicker(plan: PlanUi, tracker: JsonObject, week: Int, day: Int, vm: TrackerViewModel) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - WeekPicker(plan, tracker, week, vm) - Row( - Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - plan.days.forEach { value -> - val session = tracker.obj("sessions")[de.drazz.boehmitools.training.domain.sessionKey(week, value.num)] as? JsonObject - val marker = when (session?.string("status")) { - "completed" -> " ✓" - "stopped" -> " ■" - "in_progress" -> " ●" - else -> "" - } - FilterChip( - selected = value.num == day, - onClick = { vm.setDay(value.num) }, - label = { Text("Tag ${value.num}$marker") }, - ) - } - } - } -} - -@Composable -private fun WeekPicker(plan: PlanUi, tracker: JsonObject, week: Int, vm: TrackerViewModel) { - Row( - Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - (1..plan.weeks).forEach { value -> - val sessions = plan.days.mapNotNull { day -> - tracker.obj("sessions")[de.drazz.boehmitools.training.domain.sessionKey(value, day.num)] as? JsonObject - } - val closed = tracker.obj("week_statuses").obj(value.toString()).string("status") == "closed" - val allCompleted = sessions.size == plan.days.size && sessions.all { it.string("status") == "completed" } - val hasCompleted = sessions.any { it.string("status") == "completed" } - val marker = when { - closed || allCompleted -> " ✓" - hasCompleted -> " •" - else -> "" - } - FilterChip( - selected = value == week, - onClick = { vm.setWeek(value) }, - label = { Text("W$value$marker") }, - ) - } - } -} - -@Composable -private fun SessionHeader(plan: PlanUi, day: DayUi, session: JsonObject, week: Int, vm: TrackerViewModel) { - val status = session.string("status", "planned") - val statusLabel = when (status) { "in_progress" -> "Läuft"; "stopped" -> "Gestoppt"; "completed" -> "Abgeschlossen"; else -> "Geplant" } - val gradient = Brush.linearGradient(listOf(Color(0xFF4F46E5), Color(0xFF9333EA))) - Column( - Modifier.fillMaxWidth().clip(RoundedCornerShape(26.dp)).background(gradient).padding(20.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text("WOCHE $week · TAG ${day.num}", color = Color.White.copy(alpha = .78f), style = MaterialTheme.typography.labelMedium) - Text(statusLabel, color = Color.White, style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold) - } - Text(day.focus, style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = Color.White) - Text("Planrevision R${session.int("plan_revision", plan.publishedRevision)}", color = Color.White.copy(alpha = .72f), style = MaterialTheme.typography.bodySmall) - when (status) { - "in_progress" -> Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Button(onClick = { vm.setSessionStatus("completed") }, modifier = Modifier.weight(1f)) { Text("Abschließen") } - OutlinedButton(onClick = { vm.setSessionStatus("stopped") }, modifier = Modifier.weight(1f)) { Text("Stoppen", color = Color.White) } - } - "stopped" -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session fortsetzen") } - "completed" -> OutlinedButton(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session wieder öffnen", color = Color.White) } - else -> Button(onClick = { vm.setSessionStatus("in_progress") }, modifier = Modifier.fillMaxWidth()) { Text("Session starten") } - } - } -} - @Composable private fun SequenceCard(title: String, prefix: String, items: List, session: JsonObject, vm: TrackerViewModel) { - if (items.isEmpty()) return - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - items.forEachIndexed { index, label -> - val id = "$prefix-$index" - val item = itemFor(session, id) - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Checkbox(checked = item.string("completion_status") == "completed" || item.bool("done"), onCheckedChange = { vm.setSequenceStatus(id, it) }) - Text(label, modifier = Modifier.padding(top = 12.dp)) - } + ModernCard(title = title) { + items.forEachIndexed { index, label -> + val id = "$prefix-$index" + val item = itemFor(session, id) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + FilterChip( + selected = item.string("completion_status") == "completed" || item.bool("done"), + onClick = { vm.setSequenceStatus(id, !(item.string("completion_status") == "completed" || item.bool("done"))) }, + label = { Text(if (item.string("completion_status") == "completed" || item.bool("done")) "Erledigt" else "Offen") }, + ) + Text(label) } } } @@ -344,60 +429,93 @@ private fun ExerciseCard(exercise: ExerciseUi, session: JsonObject, plan: PlanUi val data = normalizedResultData(item, schema) var progressionMenu by remember(exercise.id, progression) { mutableStateOf(false) } - Card( - Modifier.fillMaxWidth(), - shape = RoundedCornerShape(22.dp), - colors = androidx.compose.material3.CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), - elevation = androidx.compose.material3.CardDefaults.cardElevation(defaultElevation = 6.dp), - ) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Column(Modifier.weight(1f)) { - Text(exercise.name, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) - if (exercise.cue.isNotBlank()) Text(exercise.cue, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - } - Surface( - shape = RoundedCornerShape(50), - color = if (status == "completed") Color(0xFF10B981) else MaterialTheme.colorScheme.surfaceVariant, - onClick = { vm.setItemStatus(exercise.id, if (status == "completed") "planned" else "completed") }, + ModernCard(title = exercise.name, subtitle = exercise.cue.ifBlank { "Übung" }) { + val statusOptions = listOf( + "planned" to "Offen", + "completed" to "Erledigt", + "partial" to "Teilweise", + "skipped" to "Übersprungen", + ) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + statusOptions.chunked(2).forEach { rowItems -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Icon(Icons.Default.CheckCircle, null, modifier = Modifier.padding(8.dp), tint = if (status == "completed") Color.White else MaterialTheme.colorScheme.onSurfaceVariant) + rowItems.forEach { (value, label) -> + SmallPill( + text = label, + selected = status == value, + modifier = Modifier.weight(1f), + ) { vm.setItemStatus(exercise.id, value) } + } + if (rowItems.size == 1) Spacer(Modifier.weight(1f)) } } + } - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp)) { - listOf("planned" to "Offen", "completed" to "Erledigt", "partial" to "Teilweise", "skipped" to "Skip").forEach { (value, label) -> - FilterChip(selected = status == value, onClick = { vm.setItemStatus(exercise.id, value) }, label = { Text(label, maxLines = 1) }, modifier = Modifier.weight(1f)) + if (status == "skipped") { + OutlinedTextField( + value = item.string("skip_reason"), + onValueChange = { vm.setItemText(exercise.id, "skip_reason", it) }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Grund") }, + minLines = 2, + ) + } else { + if (progressionModel?.steps?.isNotEmpty() == true) { + Box { + OutlinedButton(onClick = { progressionMenu = true }, modifier = Modifier.fillMaxWidth()) { + Text(if (progression.isBlank()) "Stufe auswählen" else progression) + } + DropdownMenu(expanded = progressionMenu, onDismissRequest = { progressionMenu = false }) { + progressionModel.steps.forEach { step -> + DropdownMenuItem( + text = { Text(step.name) }, + onClick = { + progressionMenu = false + vm.setItemProgression(exercise.id, step.name, step.id) + }, + ) + } + } } - } - - if (status == "skipped") { - OutlinedTextField(value = item.string("skip_reason"), onValueChange = { vm.setItemText(exercise.id, "skip_reason", it) }, modifier = Modifier.fillMaxWidth(), label = { Text("Grund") }, shape = RoundedCornerShape(16.dp)) } else { OutlinedTextField( value = progression, onValueChange = { vm.setItemProgression(exercise.id, it) }, modifier = Modifier.fillMaxWidth(), label = { Text("Progressionsstufe") }, - shape = RoundedCornerShape(16.dp), ) - if (progressionModel?.steps?.isNotEmpty() == true) { - Box { - OutlinedButton(onClick = { progressionMenu = true }, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp)) { Text("Stufe auswählen") } - DropdownMenu(expanded = progressionMenu, onDismissRequest = { progressionMenu = false }) { - progressionModel.steps.forEach { step -> DropdownMenuItem(text = { Text(step.name) }, onClick = { progressionMenu = false; vm.setItemProgression(exercise.id, step.name, step.id) }) } - } - } - } - ManualResultFormatEditor(inherited = schema, manualOverride = item["result_format"] as? JsonObject, onChange = { vm.setItemResultFormat(exercise.id, it) }) - ResultEditor(schema, data) { vm.setItemResultData(exercise.id, it) } - OutlinedTextField(value = item.string("note"), onValueChange = { vm.setItemText(exercise.id, "note", it) }, modifier = Modifier.fillMaxWidth(), label = { Text("Sessionnotiz") }, minLines = 2, shape = RoundedCornerShape(16.dp)) + } + + ManualResultFormatEditor( + inherited = schema, + manualOverride = item["result_format"] as? JsonObject, + onChange = { vm.setItemResultFormat(exercise.id, it) }, + ) + + Text("Notizen", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + OutlinedTextField( + value = item.string("note"), + onValueChange = { vm.setItemText(exercise.id, "note", it) }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Wie hat sich die Übung angefühlt?") }, + minLines = 3, + ) + + ResultEditor(schema, data) { vm.setItemResultData(exercise.id, it) } + + Button( + onClick = { vm.setItemStatus(exercise.id, "completed") }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Speichern & Abschließen") } } } } - @Composable private fun ManualResultFormatEditor( inherited: ResultSchemaUi, @@ -408,11 +526,7 @@ private fun ManualResultFormatEditor( val mode = manualOverride?.string("mode", inherited.mode) ?: inherited.mode val weightMode = manualOverride?.string("weight_mode", inherited.weightMode) ?: inherited.weightMode val laterality = manualOverride?.string("laterality", inherited.laterality) ?: inherited.laterality - val sidesMode = if (laterality == "unilateral") { - manualOverride?.string("sides_mode", inherited.sidesMode) ?: inherited.sidesMode - } else { - "same" - } + val sidesMode = if (laterality == "unilateral") manualOverride?.string("sides_mode", inherited.sidesMode) ?: inherited.sidesMode else "same" fun changed(field: String, value: String): JsonObject { val values = linkedMapOf( @@ -422,21 +536,16 @@ private fun ManualResultFormatEditor( "sides_mode" to JsonPrimitive(sidesMode), ) values[field] = JsonPrimitive(value) - if ((values["laterality"] as JsonPrimitive).content == "bilateral") { - values["sides_mode"] = JsonPrimitive("same") - } + if ((values["laterality"] as JsonPrimitive).content == "bilateral") values["sides_mode"] = JsonPrimitive("same") return JsonObject(values) } - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Column(Modifier.weight(1f)) { - Text("Manuelle Session-Anpassung", style = MaterialTheme.typography.labelLarge) - Text( - if (manualOverride == null) "Vererbt: ${inherited.source}" else "Überschreibt Progressionsstufe und Übung", - style = MaterialTheme.typography.bodySmall, - ) + OutlinedCard(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) { + Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + Text("Manuelle Session-Anpassung", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text(if (manualOverride == null) "Vererbt: ${inherited.source}" else "Überschreibt Progressionsstufe und Übung", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } TextButton(onClick = { expanded = !expanded }) { Text(if (expanded) "Schließen" else "Anpassen") } } @@ -448,9 +557,7 @@ private fun ManualResultFormatEditor( ChoiceChips("Seitenwerte", listOf("same", "separate"), sidesMode) { onChange(changed("sides_mode", it)) } } if (manualOverride != null) { - OutlinedButton(onClick = { onChange(null) }, modifier = Modifier.fillMaxWidth()) { - Text("Manuelle Anpassung zurücksetzen") - } + OutlinedButton(onClick = { onChange(null) }, modifier = Modifier.fillMaxWidth()) { Text("Manuelle Anpassung zurücksetzen") } } } } @@ -459,109 +566,324 @@ private fun ManualResultFormatEditor( @Composable private fun ChoiceChips(label: String, choices: List, selected: String, onSelect: (String) -> Unit) { - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text(label, style = MaterialTheme.typography.labelMedium) - choices.forEach { value -> - FilterChip( - selected = selected == value, - onClick = { onSelect(value) }, - label = { Text(value) }, + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text(label, style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold) + choices.chunked(2).forEach { rowItems -> + Row( modifier = Modifier.fillMaxWidth(), - ) + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + rowItems.forEach { value -> + SmallPill( + text = value, + selected = selected == value, + modifier = Modifier.weight(1f), + ) { onSelect(value) } + } + if (rowItems.size == 1) Spacer(Modifier.weight(1f)) + } } } } @Composable private fun ResultEditor(schema: ResultSchemaUi, data: JsonObject, onChange: (JsonObject) -> Unit) { - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("Ergebnis · ${schema.source}", style = MaterialTheme.typography.labelLarge) - Text("${schema.mode}, ${schema.laterality}, Gewicht ${schema.weightMode}", style = MaterialTheme.typography.bodySmall) + OutlinedCard(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) { + Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Sätze", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + if (schema.mode == "none") { Text("Für diese Stufe ist kein numerischer Messwert vorgesehen.") return@Column } - if (schema.weightMode != "none") { - val weight = data["weight_kg"]?.jsonPrimitive?.doubleOrNull?.toString().orEmpty() - OutlinedTextField( - value = weight, - onValueChange = { raw -> onChange(data.updated("weight_kg", raw.replace(',', '.').toDoubleOrNull()?.let(::JsonPrimitive) ?: JsonNull)) }, - modifier = Modifier.fillMaxWidth(), - label = { Text(if (schema.weightMode == "required") "Gewicht kg (Pflicht)" else "Gewicht kg (optional)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - singleLine = true, - ) - } - if (!schema.lockedSets) { - OutlinedTextField( - value = data.int("sets", schema.defaultSets).toString(), - onValueChange = { raw -> raw.toIntOrNull()?.let { onChange(resultDataWithSets(data, it)) } }, - modifier = Modifier.fillMaxWidth(), - label = { Text("Sätze") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - singleLine = true, - ) - } + val count = data.int("sets", schema.defaultSets).coerceIn(1, 20) + val separateSides = data.string("laterality", schema.laterality) == "unilateral" && + data.string("sides_mode", schema.sidesMode) == "separate" + val showWeight = schema.weightMode != "none" + val valueLabel = when (schema.mode) { + "seconds" -> "Sek." + "minutes" -> "Min." + else -> "Wdh." + } + + SetTableHeader(showWeight = showWeight, separateSides = separateSides, valueLabel = valueLabel) + repeat(count) { index -> - if (data.string("laterality", schema.laterality) == "unilateral" && data.string("sides_mode", schema.sidesMode) == "separate") { - OutlinedTextField( - value = data.array("left_values").getOrNull(index)?.jsonPrimitive?.doubleOrNull?.toString().orEmpty(), - onValueChange = { raw -> onChange(resultDataWithValue(data, "left_values", index, raw.replace(',', '.').toDoubleOrNull())) }, - modifier = Modifier.fillMaxWidth(), - label = { Text("Satz ${index + 1} links") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - singleLine = true, - ) - OutlinedTextField( - value = data.array("right_values").getOrNull(index)?.jsonPrimitive?.doubleOrNull?.toString().orEmpty(), - onValueChange = { raw -> onChange(resultDataWithValue(data, "right_values", index, raw.replace(',', '.').toDoubleOrNull())) }, - modifier = Modifier.fillMaxWidth(), - label = { Text("Satz ${index + 1} rechts") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - singleLine = true, - ) - } else { - OutlinedTextField( - value = data.array("values").getOrNull(index)?.jsonPrimitive?.doubleOrNull?.toString().orEmpty(), - onValueChange = { raw -> onChange(resultDataWithValue(data, "values", index, raw.replace(',', '.').toDoubleOrNull())) }, - modifier = Modifier.fillMaxWidth(), - label = { Text("Satz ${index + 1}${if (schema.laterality == "unilateral") " je Seite" else ""}") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - singleLine = true, - ) + SetInputRow( + setNumber = index + 1, + showWeight = showWeight, + sharedWeight = editableNumber(data["weight_kg"]?.jsonPrimitive?.doubleOrNull), + separateSides = separateSides, + value = editableNumber(data.array("values").getOrNull(index)?.jsonPrimitive?.doubleOrNull), + leftValue = editableNumber(data.array("left_values").getOrNull(index)?.jsonPrimitive?.doubleOrNull), + rightValue = editableNumber(data.array("right_values").getOrNull(index)?.jsonPrimitive?.doubleOrNull), + canRemove = !schema.lockedSets && count > 1, + onWeightChange = { raw -> + onChange( + data.updated( + "weight_kg", + raw.toStoredNumber()?.let(::JsonPrimitive) ?: JsonNull, + ) + ) + }, + onValueChange = { raw -> + onChange(resultDataWithValue(data, "values", index, raw.toStoredNumber())) + }, + onLeftChange = { raw -> + onChange(resultDataWithValue(data, "left_values", index, raw.toStoredNumber())) + }, + onRightChange = { raw -> + onChange(resultDataWithValue(data, "right_values", index, raw.toStoredNumber())) + }, + onRemove = { onChange(resultDataWithoutSet(data, index)) }, + ) + } + + if (!schema.lockedSets && count < 20) { + OutlinedButton( + onClick = { onChange(resultDataWithSets(data, count + 1)) }, + modifier = Modifier.fillMaxWidth(), + ) { + Icon(Icons.Default.Add, contentDescription = null) + Spacer(Modifier.size(8.dp)) + Text("Satz hinzufügen") } } - Text("Gespeichert als: ${formatResultData(data)}", style = MaterialTheme.typography.bodySmall) + + Text("Zusammenfassung", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + SetSummary(schema = schema, data = data, separateSides = separateSides) } } } +@Composable +private fun SetTableHeader(showWeight: Boolean, separateSides: Boolean, valueLabel: String) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("Satz", modifier = Modifier.size(width = 42.dp, height = 20.dp), style = MaterialTheme.typography.labelSmall) + if (showWeight) { + Text("Gewicht (kg)", modifier = Modifier.weight(1.2f), style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center) + } + if (separateSides) { + Text("Links", modifier = Modifier.weight(1f), style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center) + Text("Rechts", modifier = Modifier.weight(1f), style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center) + } else { + Text(valueLabel, modifier = Modifier.weight(1f), style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center) + } + Spacer(Modifier.size(width = 34.dp, height = 20.dp)) + } +} + +@Composable +private fun SetInputRow( + setNumber: Int, + showWeight: Boolean, + sharedWeight: String, + separateSides: Boolean, + value: String, + leftValue: String, + rightValue: String, + canRemove: Boolean, + onWeightChange: (String) -> Unit, + onValueChange: (String) -> Unit, + onLeftChange: (String) -> Unit, + onRightChange: (String) -> Unit, + onRemove: () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Surface( + modifier = Modifier.size(width = 42.dp, height = 48.dp), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Box(contentAlignment = Alignment.Center) { + Text(setNumber.toString(), fontWeight = FontWeight.SemiBold) + } + } + + if (showWeight) { + CompactNumberField( + value = sharedWeight, + onValueChange = onWeightChange, + modifier = Modifier.weight(1.2f), + ) + } + + if (separateSides) { + CompactNumberField(value = leftValue, onValueChange = onLeftChange, modifier = Modifier.weight(1f)) + CompactNumberField(value = rightValue, onValueChange = onRightChange, modifier = Modifier.weight(1f)) + } else { + CompactNumberField(value = value, onValueChange = onValueChange, modifier = Modifier.weight(1f)) + } + + IconButton( + onClick = onRemove, + enabled = canRemove, + modifier = Modifier.size(34.dp), + ) { + Icon( + Icons.Default.RemoveCircleOutline, + contentDescription = "Satz entfernen", + tint = if (canRemove) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.outlineVariant, + ) + } + } +} + +@Composable +private fun CompactNumberField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + var text by remember(value) { mutableStateOf(value) } + + Surface( + modifier = modifier.height(48.dp), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + BasicTextField( + value = text, + onValueChange = { raw -> + val filtered = raw.filter { it.isDigit() || it == ',' || it == '.' || it == '-' } + text = filtered + onValueChange(filtered) + }, + modifier = Modifier.fillMaxSize(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + textStyle = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + ), + decorationBox = { innerTextField -> + Box( + modifier = Modifier.fillMaxSize().padding(horizontal = 6.dp), + contentAlignment = Alignment.Center, + ) { + if (text.isBlank()) { + Text("--", color = MaterialTheme.colorScheme.onSurfaceVariant) + } + innerTextField() + } + }, + ) + } +} + +@Composable +private fun SetSummary(schema: ResultSchemaUi, data: JsonObject, separateSides: Boolean) { + val weight = data["weight_kg"]?.jsonPrimitive?.doubleOrNull + val values = if (separateSides) { + data.array("left_values").mapNotNull { it.jsonPrimitive.doubleOrNull } + + data.array("right_values").mapNotNull { it.jsonPrimitive.doubleOrNull } + } else { + data.array("values").mapNotNull { it.jsonPrimitive.doubleOrNull } + } + val workSets = if (separateSides) { + val count = data.int("sets", schema.defaultSets).coerceIn(1, 20) + (0 until count).count { index -> + data.array("left_values").getOrNull(index)?.jsonPrimitive?.doubleOrNull != null || + data.array("right_values").getOrNull(index)?.jsonPrimitive?.doubleOrNull != null + } + } else { + data.array("values").count { it.jsonPrimitive.doubleOrNull != null } + } + val total = values.sum() + val average = values.takeIf { it.isNotEmpty() }?.average() + val volume = if (schema.mode == "reps" && weight != null && values.isNotEmpty()) total * weight else null + val unit = when (schema.mode) { + "seconds" -> " s" + "minutes" -> " min" + else -> "" + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + SummaryMetric( + label = if (volume != null) "Volumen" else "Gesamt", + value = if (volume != null) "${displayNumber(volume)} kg" else if (values.isEmpty()) "–" else "${displayNumber(total)}$unit", + modifier = Modifier.weight(1f), + ) + SummaryMetric( + label = "Arbeitssätze", + value = workSets.toString(), + modifier = Modifier.weight(1f), + ) + SummaryMetric( + label = when (schema.mode) { + "seconds" -> "Ø Sekunden" + "minutes" -> "Ø Minuten" + else -> "Ø Wdh." + }, + value = average?.let { "${displayNumber(it)}$unit" } ?: "–", + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun SummaryMetric(label: String, value: String, modifier: Modifier = Modifier) { + Surface( + modifier = modifier, + shape = RoundedCornerShape(14.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), + ) { + Column( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text(label, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + Text(value, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + } + } +} + +private fun editableNumber(value: Double?): String = when { + value == null -> "" + value % 1.0 == 0.0 -> value.toInt().toString() + else -> value.toString().replace('.', ',') +} + +private fun String.toStoredNumber(): Double? = trim().replace(',', '.').toDoubleOrNull() + +private fun displayNumber(value: Double): String { + return if (value % 1.0 == 0.0) { + String.format(Locale.GERMANY, "%,.0f", value) + } else { + String.format(Locale.GERMANY, "%,.1f", value).trimEnd('0').trimEnd(',') + } +} + @Composable private fun ProgressPane(state: TrackerUiState, pending: List, vm: TrackerViewModel) { val plan = state.plan ?: return val sessions = state.tracker.obj("sessions").values.mapNotNull { it as? JsonObject } val completed = sessions.count { it.string("status") == "completed" } - val stopped = sessions.count { it.string("status") == "stopped" } - val analysisState = state.response.obj("analysis_state").takeIf { it.isNotEmpty() } - ?: state.tracker.obj("analysis_state") + val analysisState = state.response.obj("analysis_state").takeIf { it.isNotEmpty() } ?: state.tracker.obj("analysis_state") val running = analysisState.string("status") == "running" - val cache = state.response.obj("analysis_cache").takeIf { it.isNotEmpty() } - ?: state.tracker.obj("analysis_cache") + val cache = state.response.obj("analysis_cache").takeIf { it.isNotEmpty() } ?: state.tracker.obj("analysis_cache") val weekRecord = cache.obj("weeks")[state.week.toString()] as? JsonObject val overallRecord = cache["overall"] as? JsonObject val selectedRecord = if (state.analysisScope == "overall") overallRecord else weekRecord val selectedTitle = if (state.analysisScope == "overall") "Gesamtanalyse" else "Analyse Woche ${state.week}" val catalog = state.response.obj("analysis_catalog") - val catalogEntry = if (state.analysisScope == "overall") { - catalog.obj("overall") - } else { - catalog.array("weeks") - .mapNotNull { it as? JsonObject } - .firstOrNull { it.int("week") == state.week } - ?: JsonObject(emptyMap()) - } + val catalogEntry = if (state.analysisScope == "overall") catalog.obj("overall") else catalog.array("weeks").mapNotNull { it as? JsonObject }.firstOrNull { it.int("week") == state.week } ?: JsonObject(emptyMap()) val catalogStatus = when (catalogEntry.string("status")) { "current" -> "aktuell" "stale" -> "veraltet" @@ -570,307 +892,204 @@ private fun ProgressPane(state: TrackerUiState, pending: List, else -> catalogEntry.string("status", "unbekannt") } - Column( - Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - Text("Fortschritt", style = MaterialTheme.typography.headlineSmall) + WeeklyHeroCard("Wochenerfolg", "$completed / ${plan.days.size}", "Einheiten", completed.toFloat() / plan.days.size.coerceAtLeast(1)) - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { - Text("Lokale Übersicht", style = MaterialTheme.typography.titleMedium) - Text("$completed abgeschlossene Sessions · $stopped gestoppte Sessions · ${sessions.size} angelegte Sessions") - Text("Trackerrevision ${state.tracker.int("revision", 1)} · Planrevision ${plan.publishedRevision}") - } + if (pending.isNotEmpty()) PendingQueue(pending, vm) + + ModernCard(title = "Auswertung auswählen") { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { + SmallPill("Woche", state.analysisScope == "week", modifier = Modifier.weight(1f)) { vm.setAnalysisScope("week") } + SmallPill("Gesamt", state.analysisScope == "overall", modifier = Modifier.weight(1f)) { vm.setAnalysisScope("overall") } } - - if (pending.isNotEmpty()) PendingQueue(pending, vm) - - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Auswertung auswählen", style = MaterialTheme.typography.titleMedium) + if (state.analysisScope == "week") { + (1..plan.weeks).toList().chunked(4).forEach { weekRow -> Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - FilterChip( - selected = state.analysisScope == "week", - onClick = { vm.setAnalysisScope("week") }, - label = { Text("Woche", maxLines = 1) }, - modifier = Modifier.weight(1f), - ) - FilterChip( - selected = state.analysisScope == "overall", - onClick = { vm.setAnalysisScope("overall") }, - label = { Text("Gesamt", maxLines = 1) }, - modifier = Modifier.weight(1f), - ) - } - if (state.analysisScope == "week") { - WeekPicker(plan, state.tracker, state.week, vm) - } - Text("Status: $catalogStatus", style = MaterialTheme.typography.bodySmall) - } - } - - if (state.analysisScope == "week") { - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Wochenstatus", style = MaterialTheme.typography.titleMedium) - val closed = state.tracker.obj("week_statuses").obj(state.week.toString()).string("status") == "closed" - FilterChip( - selected = closed, - onClick = { vm.setWeekClosed(!closed) }, - label = { Text(if (closed) "Woche ${state.week} abgeschlossen" else "Woche ${state.week} läuft") }, - modifier = Modifier.fillMaxWidth(), - ) - Text( - "Eine laufende Woche erzeugt eine Zwischenanalyse, eine geschlossene Woche eine Abschlussanalyse.", - style = MaterialTheme.typography.bodySmall, - ) + weekRow.forEach { w -> + SmallPill( + text = "W$w", + selected = state.week == w, + modifier = Modifier.weight(1f), + ) { vm.setWeek(w) } + } + repeat(4 - weekRow.size) { Spacer(Modifier.weight(1f)) } } } } - - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text("Manuelle Analyse", style = MaterialTheme.typography.titleMedium) - Button( - onClick = { - if (state.analysisScope == "overall") vm.startAnalysis("overall") - else vm.startAnalysis("week", state.week) - }, - enabled = !running, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - if (running) "Analyse läuft …" - else if (state.analysisScope == "overall") "Gesamtanalyse starten" - else "Woche ${state.week} analysieren", - ) - } - if (running) { - CircularProgressIndicator() - Text(analysisState.string("message", "Analysejob läuft …")) - } else if (state.analysisMessage.isNotBlank()) { - Text(state.analysisMessage) - } - if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error) - } - } - - AnalysisCard(selectedTitle, selectedRecord) - Spacer(Modifier.height(24.dp)) + Text("Status: $catalogStatus", color = MaterialTheme.colorScheme.onSurfaceVariant) } + + if (state.analysisScope == "week") { + val closed = state.tracker.obj("week_statuses").obj(state.week.toString()).string("status") == "closed" + ModernCard(title = "Wochenstatus") { + SmallPill(if (closed) "Woche ${state.week} abgeschlossen" else "Woche ${state.week} läuft", true, modifier = Modifier.fillMaxWidth()) { vm.setWeekClosed(!closed) } + Text("Eine laufende Woche erzeugt eine Zwischenanalyse, eine geschlossene Woche eine Abschlussanalyse.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + + ModernCard(title = "Manuelle Analyse") { + Button( + onClick = { if (state.analysisScope == "overall") vm.startAnalysis("overall") else vm.startAnalysis("week", state.week) }, + enabled = !running, + modifier = Modifier.fillMaxWidth(), + ) { + Text(if (running) "Analyse läuft …" else if (state.analysisScope == "overall") "Gesamtanalyse starten" else "Woche ${state.week} analysieren") + } + if (running) { + CircularProgressIndicator(modifier = Modifier.size(22.dp), strokeWidth = 2.dp) + Text(analysisState.string("message", "Analysejob läuft …")) + } else if (state.analysisMessage.isNotBlank()) { + Text(state.analysisMessage) + } + if (state.error.isNotBlank()) Text(state.error, color = MaterialTheme.colorScheme.error) + } + + AnalysisCard(selectedTitle, selectedRecord) } @Composable private fun AnalysisCard(title: String, record: JsonObject?) { - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text(title, style = MaterialTheme.typography.titleMedium) - if (record == null) { - Text("Für diese Auswahl liegt noch keine Analyse vor.") - return@Column - } - - val result = record.obj("result").takeIf { it.isNotEmpty() } - ?: record.obj("response") + ModernCard(title = title) { + if (record == null) { + Text("Für diese Auswahl liegt noch keine Analyse vor.") + } else { + val result = record.obj("result").takeIf { it.isNotEmpty() } ?: record.obj("response") if (result.isEmpty()) { Text("Der Server hat einen Analysedatensatz ohne darstellbares Ergebnis geliefert.") - return@Column - } + } else { + val scope = if (record.string("type") == "week") "Woche ${record.int("week")}" else "Gesamt" + val metadata = buildList { + add(scope) + if (record.string("created_at").isNotBlank()) add("erstellt ${record.string("created_at")}") + if (record.int("sessions_considered") > 0) add("${record.int("sessions_considered")} Sessions") + if (record.string("model").isNotBlank()) add(record.string("model")) + }.joinToString(" · ") + if (metadata.isNotBlank()) Text(metadata, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) - val scope = if (record.string("type") == "week") "Woche ${record.int("week")}" else "Gesamt" - val metadata = buildList { - add(scope) - if (record.string("created_at").isNotBlank()) add("erstellt ${record.string("created_at")}") - if (record.int("sessions_considered") > 0) add("${record.int("sessions_considered")} Sessions") - if (record.string("model").isNotBlank()) add(record.string("model")) - }.joinToString(" · ") - if (metadata.isNotBlank()) Text(metadata, style = MaterialTheme.typography.labelSmall) + Text(result.string("headline", "Progressionsanalyse"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + val summary = result.string("summary", result.obj("overview").string("summary")) + if (summary.isNotBlank()) Text(summary) - Text(result.string("headline", "Progressionsanalyse"), style = MaterialTheme.typography.headlineSmall) - val summary = result.string("summary", result.obj("overview").string("summary")) - if (summary.isNotBlank()) Text(summary) - - val dataQuality = result.string("data_quality", result.obj("overview").string("data_quality")) - if (dataQuality.isNotBlank()) { - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text("Datenbasis", fontWeight = FontWeight.Bold) - Text(dataQuality) - } - } - } - - val metrics = result.array("metrics").mapNotNull { it as? JsonObject } - if (metrics.isNotEmpty()) { - Text("Kennzahlen", style = MaterialTheme.typography.titleMedium) + val metrics = result.array("metrics").mapNotNull { it as? JsonObject } metrics.forEach { metric -> - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(3.dp)) { - Text(metric.string("label"), style = MaterialTheme.typography.labelMedium) - Text(metric.string("value"), style = MaterialTheme.typography.titleMedium) + OutlinedCard(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) { + Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(metric.string("label"), style = MaterialTheme.typography.labelLarge) + Text(metric.string("value"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) if (metric.string("detail").isNotBlank()) Text(metric.string("detail"), style = MaterialTheme.typography.bodySmall) } } } - } - AnalysisVisuals(record.obj("visuals")) - - val exercises = result.array("exercise_updates") - .ifEmpty { result.array("exercises") } - .mapNotNull { it as? JsonObject } - if (exercises.isNotEmpty()) { - Text("Übungsstand", style = MaterialTheme.typography.titleMedium) - exercises.forEach { update -> - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) { - Text(update.string("name", "Übung"), fontWeight = FontWeight.Bold) - val trend = when (update.string("trend")) { - "up" -> "↗ aufwärts" - "stable" -> "→ stabil" - "down" -> "↘ rückläufig" - else -> "? unklar" + val exercises = result.array("exercise_updates").ifEmpty { result.array("exercises") }.mapNotNull { it as? JsonObject } + if (exercises.isNotEmpty()) { + Text("Übungsstand", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + exercises.forEach { update -> + OutlinedCard(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) { + Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(update.string("name", "Übung"), fontWeight = FontWeight.Bold) + if (update.string("current_level").isNotBlank()) Text("Stand: ${update.string("current_level")}") + if (update.string("evidence").isNotBlank()) Text(update.string("evidence"), style = MaterialTheme.typography.bodySmall) + val next = update.string("next_action", update.string("next_step")) + if (next.isNotBlank()) Text("Nächster Schritt: $next") } - Text(trend, style = MaterialTheme.typography.labelMedium) - if (update.string("current_level").isNotBlank()) Text("Stand: ${update.string("current_level")}") - if (update.string("evidence").isNotBlank()) Text(update.string("evidence"), style = MaterialTheme.typography.bodySmall) - val next = update.string("next_action", update.string("next_step")) - if (next.isNotBlank()) Text("Nächster Schritt: $next") - if (update.string("criterion").isNotBlank()) Text("Kriterium: ${update.string("criterion")}", style = MaterialTheme.typography.bodySmall) } } } - } - val adjustments = result.array("plan_adjustments").mapNotNull { it as? JsonObject } - if (adjustments.isNotEmpty()) { - Text("Optionale Planvorschläge", style = MaterialTheme.typography.titleMedium) - Text( - "Diese Vorschläge werden niemals automatisch übernommen.", - style = MaterialTheme.typography.bodySmall, - ) - adjustments.forEach { item -> - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) { - Text(item.string("target", "Vorschlag"), fontWeight = FontWeight.Bold) - if (item.string("suggested_change").isNotBlank()) Text(item.string("suggested_change")) - if (item.string("reason").isNotBlank()) Text("Warum: ${item.string("reason")}", style = MaterialTheme.typography.bodySmall) - if (item.string("manual_step").isNotBlank()) Text("Manueller Schritt: ${item.string("manual_step")}", style = MaterialTheme.typography.bodySmall) - if (item.string("condition").isNotBlank()) Text("Bedingung: ${item.string("condition")}", style = MaterialTheme.typography.bodySmall) + val warnings = result.array("warnings") + if (warnings.isNotEmpty()) { + OutlinedCard(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) { + Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text("Hinweise", fontWeight = FontWeight.Bold) + warnings.forEach { warning -> Text("• ${warning.jsonPrimitive.content}") } } } } } - - val warnings = result.array("warnings") - if (warnings.isNotEmpty()) { - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) { - Text("Hinweise", fontWeight = FontWeight.Bold) - warnings.forEach { warning -> - Text("• ${warning.jsonPrimitive.content}") - } - } - } - } - - val conclusion = result.string("conclusion", result.string("closing")) - if (conclusion.isNotBlank()) { - Text(conclusion, style = MaterialTheme.typography.titleSmall) - } - } - } -} - -@Composable -private fun AnalysisVisuals(visuals: JsonObject) { - val charts = visuals.array("charts").mapNotNull { it as? JsonObject } - val flowcharts = visuals.array("flowcharts").mapNotNull { it as? JsonObject } - if (charts.isEmpty() && flowcharts.isEmpty()) return - - Text("Visualisierte Erkenntnisse", style = MaterialTheme.typography.titleMedium) - charts.forEach { chart -> - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text(chart.string("title", "Verlauf"), fontWeight = FontWeight.Bold) - val series = chart.array("series").mapNotNull { it as? JsonObject } - series.forEach { row -> - val values = row.array("values").joinToString(" · ") { it.jsonPrimitive.content } - Text("${row.string("name", "Werte")}: $values", style = MaterialTheme.typography.bodySmall) - } - if (chart.string("insight").isNotBlank()) Text(chart.string("insight"), style = MaterialTheme.typography.bodySmall) - } - } - } - flowcharts.forEach { flow -> - OutlinedCard(Modifier.fillMaxWidth()) { - Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { - Text(flow.string("title", "Progressionspfad"), fontWeight = FontWeight.Bold) - val nodes = flow.array("nodes").mapNotNull { it as? JsonObject } - nodes.forEachIndexed { index, node -> - Text("${if (index == nodes.lastIndex) "└" else "├"} ${node.string("label", node.string("id"))}") - } - if (flow.string("insight").isNotBlank()) Text(flow.string("insight"), style = MaterialTheme.typography.bodySmall) - } - } - } -} - -@Composable -private fun PlanPane(plan: PlanUi) { - Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text(plan.title, style = MaterialTheme.typography.headlineSmall) - Text(plan.subtitle) - Text("${plan.weeks} Wochen · veröffentlichte Revision ${plan.publishedRevision}") - plan.days.forEach { day -> - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("Tag ${day.num}: ${day.focus}", style = MaterialTheme.typography.titleMedium) - day.rotations.forEach { rotation -> - Text(rotation.label, fontWeight = FontWeight.Bold) - rotation.exercises.forEach { exercise -> Text("• ${exercise.name}") } - } - } - } } } } @Composable -private fun FaqPane(state: TrackerUiState) { - val plan = state.plan ?: return - val guide = state.response.obj("equivalence_guide") - Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text("FAQ & Variantencluster", style = MaterialTheme.typography.headlineSmall) - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { - Text("Trainingsformat", style = MaterialTheme.typography.titleMedium) - Text("Modus: ${plan.trainingFormat.string("mode", "sets_reps")}") - if (plan.trainingFormat.bool("fixed_interval")) { - Text("Festes Intervall: ${plan.trainingFormat.int("work_seconds")} s Arbeit / ${plan.trainingFormat.int("rest_seconds")} s Pause · ${plan.trainingFormat.int("rounds")} Intervalle pro Block") - Text("Die App implementiert bewusst keinen Timer und verändert dieses Format nicht.") +private fun PlanPane(plan: PlanUi, state: TrackerUiState) { + var showFaq by remember { mutableStateOf(false) } + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { + SmallPill("Plan", !showFaq, modifier = Modifier.weight(1f)) { showFaq = false } + SmallPill("FAQ", showFaq, modifier = Modifier.weight(1f)) { showFaq = true } + } + + if (!showFaq) { + WeeklyHeroCard("Wochenplan", "${plan.weeks}", "Wochen", 1f) + ModernCard(title = plan.title, subtitle = plan.subtitle) { + plan.days.forEach { day -> + Surface(shape = RoundedCornerShape(20.dp), color = MaterialTheme.colorScheme.surfaceVariant, modifier = Modifier.fillMaxWidth()) { + Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text("Tag ${day.num}: ${day.focus}", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + day.rotations.forEach { rotation -> + Text(rotation.label, fontWeight = FontWeight.SemiBold) + rotation.exercises.forEach { exercise -> Text("• ${exercise.name}") } + } + } } } } - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text("Planbezogene Übungsbibliothek", style = MaterialTheme.typography.titleMedium) - plan.exerciseCatalog.values.mapNotNull { it as? JsonObject }.take(100).forEach { entry -> - Text("${entry.string("movement_label", entry.string("name"))} · ${entry.array("variants").size} Variante(n)") - } - } - } - if (guide.isNotEmpty()) { - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { - Text("Fallbackregeln des Servers", style = MaterialTheme.typography.titleMedium) - Text(guide.toString(), style = MaterialTheme.typography.bodySmall) - } + } else { + val guide = state.response.obj("equivalence_guide") + ModernCard(title = "FAQ & Variantencluster") { + Text("Trainingsformat", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text("Modus: ${plan.trainingFormat.string("mode", "sets_reps")}") + if (plan.trainingFormat.bool("fixed_interval")) { + Text("Festes Intervall: ${plan.trainingFormat.int("work_seconds")} s Arbeit / ${plan.trainingFormat.int("rest_seconds")} s Pause · ${plan.trainingFormat.int("rounds")} Intervalle pro Block") } + if (guide.isNotEmpty()) Text(guide.toString(), style = MaterialTheme.typography.bodySmall) + } + } +} + +@Composable +private fun SmallPill(text: String, selected: Boolean, modifier: Modifier = Modifier, onClick: () -> Unit) { + Surface( + onClick = onClick, + modifier = modifier, + shape = CircleShape, + color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, + ) { + Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), contentAlignment = Alignment.Center) { + Text( + text, + color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold, + ) + } + } +} + +@Composable +private fun ModernCard( + title: String? = null, + subtitle: String? = null, + noPadding: Boolean = false, + content: @Composable ColumnScope.() -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surface, + tonalElevation = 1.dp, + shadowElevation = 8.dp, + ) { + Column( + modifier = if (noPadding) Modifier else Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + if (title != null) Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + if (!subtitle.isNullOrBlank()) Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + content() } } } diff --git a/android/app/src/test/java/de/drazz/boehmitools/training/domain/ResultDataEditingTest.kt b/android/app/src/test/java/de/drazz/boehmitools/training/domain/ResultDataEditingTest.kt new file mode 100644 index 0000000..5429717 --- /dev/null +++ b/android/app/src/test/java/de/drazz/boehmitools/training/domain/ResultDataEditingTest.kt @@ -0,0 +1,91 @@ +package de.drazz.boehmitools.training.domain + +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.jsonPrimitive +import org.junit.Assert.assertEquals +import org.junit.Test + +class ResultDataEditingTest { + @Test + fun addingSetPreservesAllExistingValuesAndWeight() { + val original = buildJsonObject { + put("version", 2) + put("mode", "reps") + put("laterality", "bilateral") + put("sides_mode", "same") + put("sets", 3) + put("weight_kg", 80.0) + put("values", buildJsonArray { + add(8.0) + add(7.0) + add(6.0) + }) + put("left_values", buildJsonArray { repeat(3) { add(JsonNull) } }) + put("right_values", buildJsonArray { repeat(3) { add(JsonNull) } }) + } + + val expanded = resultDataWithSets(original, 4) + + assertEquals(4, expanded.int("sets")) + assertEquals(80.0, expanded["weight_kg"]!!.jsonPrimitive.doubleOrNull!!, 0.0) + assertEquals(listOf(8.0, 7.0, 6.0), expanded.array("values").take(3).map { it.jsonPrimitive.doubleOrNull }) + assertEquals(JsonNull, expanded.array("values")[3]) + } + + @Test + fun removingMiddleSetKeepsRemainingValuesInOrder() { + val original = buildJsonObject { + put("version", 2) + put("mode", "reps") + put("laterality", "bilateral") + put("sides_mode", "same") + put("sets", 3) + put("weight_kg", JsonPrimitive(85.0)) + put("values", buildJsonArray { + add(10.0) + add(8.0) + add(6.0) + }) + put("left_values", buildJsonArray { repeat(3) { add(JsonNull) } }) + put("right_values", buildJsonArray { repeat(3) { add(JsonNull) } }) + } + + val reduced = resultDataWithoutSet(original, 1) + + assertEquals(2, reduced.int("sets")) + assertEquals(listOf(10.0, 6.0), reduced.array("values").map { it.jsonPrimitive.doubleOrNull }) + assertEquals(85.0, reduced["weight_kg"]!!.jsonPrimitive.doubleOrNull!!, 0.0) + } + + @Test + fun removingUnilateralSetPreservesBothSides() { + val original = buildJsonObject { + put("version", 2) + put("mode", "seconds") + put("laterality", "unilateral") + put("sides_mode", "separate") + put("sets", 3) + put("weight_kg", JsonNull) + put("values", buildJsonArray { repeat(3) { add(JsonNull) } }) + put("left_values", buildJsonArray { + add(20.0) + add(18.0) + add(16.0) + }) + put("right_values", buildJsonArray { + add(19.0) + add(17.0) + add(15.0) + }) + } + + val reduced = resultDataWithoutSet(original, 0) + + assertEquals(listOf(18.0, 16.0), reduced.array("left_values").map { it.jsonPrimitive.doubleOrNull }) + assertEquals(listOf(17.0, 15.0), reduced.array("right_values").map { it.jsonPrimitive.doubleOrNull }) + } +} diff --git a/android/gradlew b/android/gradlew old mode 100644 new mode 100755 diff --git a/plugins/trainingsplan/TOOL-README.md b/plugins/trainingsplan/TOOL-README.md index 94f504a..14aa7b7 100644 --- a/plugins/trainingsplan/TOOL-README.md +++ b/plugins/trainingsplan/TOOL-README.md @@ -1,107 +1,96 @@ -# Trainingsplan- & Rezept-Editor 2.0 +# Trainingsplan-Backend -Der Editor verwaltet Trainingspläne und Rezeptsammlungen innerhalb von boehmitools. Trainingspläne sind zugleich die verbindliche Datenquelle für den `trainingstracker`. +Dieses Plugin ist nur noch das Eintragebackend fuer Trainingsplaene. -## Planlebenszyklus +Es verwaltet: -Jede Plandatei enthält eine veröffentlichte Fassung und einen bearbeitbaren Entwurf: +- Plaene +- Uebungen in einer zentralen Uebungsbibliothek +- Trainingstage +- Warm-up und Cool-down pro Trainingstag +- Uebungen, die an Trainingstagen hinterlegt sind -```json -{ - "plan_id": "phase-1", - "revision": 4, - "published_revision": 2, - "config": {}, - "draft": {} -} -``` +Nicht mehr enthalten sind PDF-Erzeugung, Rezeptsammlungen, Progressionen, +Planvorschlaege, Analyse-/ChatGPT-Anbindungen und Farbdefinitionen fuer eine +Ausgabeoberflaeche. -- **Entwurf speichern** erhöht die Bearbeitungsrevision, verändert aber nicht den vom Tracker gelesenen Plan. -- **Plan prüfen** validiert IDs, Progressionsbezüge, Ergebnisschemata und feste Intervalle. -- **Entwurf veröffentlichen** erzeugt eine neue veröffentlichte Planrevision. -- Bereits dokumentierte Sessions speichern die Planrevision, nach der sie ausgeführt wurden. -- Vor dem Ersetzen einer Plandatei wird atomisch geschrieben und ein Backup unter `data/trainingsplan/backups/` angelegt. -- Gleichzeitige Bearbeitungen werden über eine Revisionsprüfung erkannt und nicht still überschrieben. +## Datenvertrag -## Stabiler Datenvertrag - -Trainingspläne verwenden: +Trainingsplaene verwenden: ```text -schema_version: 3 -contract_version: 2 +schema_version: 4 +contract_version: 3 ``` -Phasen, Tage, Rotationen, Übungsplatzierungen, Bewegungen, Progressionen und Progressionsstufen besitzen dauerhafte IDs. Beim Verschieben im Editor bleiben diese IDs erhalten. - -Progressionsstufen sind Objekte und keine parallelen Text-/Schema-Arrays mehr: +Eine Plan-Config ist bewusst flach: ```json { - "id": "step-squat-hold-...", - "name": "Deep Squat Hold", - "phase_id": "phase-start-...", - "movement_cluster": "knee_dominant", - "factor": 1.0, - "result_schema": { - "mode": "seconds", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } + "schema_version": 4, + "contract_version": 3, + "plan_id": "basis-plan", + "meta": { + "title": "Basis-Plan" + }, + "exercises": [ + { + "id": "exercise-squat", + "name": "Squat", + "cue": "Sauber und kontrolliert" + } + ], + "days": [ + { + "id": "day-1", + "num": 1, + "name": "Tag 1", + "warmup": { "items": ["Gelenke mobilisieren"] }, + "cooldown": { "items": ["Locker auslaufen"] }, + "exercises": [ + { + "id": "day-exercise-1", + "exercise_id": "exercise-squat", + "sets": 3, + "reps": "8-12" + } + ] + } + ] } ``` -Dadurch reisen Stufenname, Eingabeformat und Analysefaktor immer gemeinsam. +## API -## Ergebniserfassung +- `GET /api/plans` +- `POST /api/plans` +- `GET /api/plans/` +- `POST /api/plans/` +- `POST /api/plans//publish` +- `POST /api/plans//rename` +- `DELETE /api/plans/` +- `GET /api/plans//exercises` +- `POST /api/plans//exercises` +- `PATCH /api/plans//exercises/` +- `DELETE /api/plans//exercises/` +- `GET /api/plans//days` +- `POST /api/plans//days` +- `PATCH /api/plans//days/` +- `DELETE /api/plans//days/` +- `POST /api/plans//days//exercises` +- `PATCH /api/plans//days//exercises/` +- `DELETE /api/plans//days//exercises/` -Eine Übung besitzt ein Standardformat. Jede Progressionsstufe kann es überschreiben. +Der alte aktive Config-Zugriff bleibt fuer einfache Clients erhalten: -Priorität im Tracker: +- `GET /api/config` +- `POST /api/config` +- `GET /api/default` +- `POST /api/reset` -```text -manuelle Anpassung der aktuellen Session -→ Schema der gewählten Progressionsstufe -→ Standard der Übung -→ automatische Erkennung als Fallback -``` +## Migration -Unterstützt werden Wiederholungen, Sekunden, Minuten, keine Zahl, optionales oder verpflichtendes Gewicht sowie gemeinsame oder getrennte Seitenwerte. - -## Tabata und feste Intervalle - -Das Trainingsformat wird ausdrücklich gespeichert: - -```json -{ - "mode": "tabata", - "fixed_interval": true, - "work_seconds": 20, - "rest_seconds": 10, - "rounds": 8, - "rounds_scope": "block" -} -``` - -`rounds` bezeichnet Intervalle pro Block. Bei acht Intervallen und vier Übungen entstehen zwei Durchgänge pro Übung. Eine zusätzlich im Rotationstitel angegebene Satzanzahl wird damit multipliziert. Ist die Intervallzahl nicht gleichmäßig durch die Übungszahl teilbar, meldet die Validierung einen Hinweis und verlangt eine feste Wertezahl im Ergebnisschema. - -## Übungsbibliothek und Analysefaktoren - -Aus den Übungen und Progressionen wird eine planbezogene `exercise_catalog` erzeugt. Sie enthält stabile Bewegungs-IDs, Cluster, Varianten, Ergebnisschemata und Faktoren. Diese Angaben haben im Tracker Vorrang vor Namens- und Regex-Erkennung. - -## Prüfung und Tracker-Vorschau - -Der Tab **Prüfung & Vorschläge** zeigt: - -- Validierungsfehler und Hinweise, -- das vom Tracker erwartete Ergebnisformat pro Übung, -- stufenabhängige Überschreibungen, -- die berechneten Durchgänge fester Intervallblöcke, -- strukturierte KI-Vorschläge aus dem Tracker mit stabilen Ziel-IDs. - -Vorschläge werden niemals automatisch in den Plan geschrieben. Über **Ziel im Entwurf öffnen** springt der Editor anhand der stabilen IDs direkt zur betroffenen Übung oder Progression. Nach der manuellen Änderung kann der Vorschlag als übernommen oder abgelehnt markiert werden. - -## Kompatibilität - -Ältere Pläne werden beim Laden in das neue Schema überführt. Fehlende Ergebnisschemata erhalten dabei einmalig explizite, im Editor korrigierbare Migrationswerte. Die veröffentlichten Beispielpläne liegen bereits in Version 3 vor. Rezeptsammlungen bleiben vom Trainingsvertrag unberührt. +Aeltere Trainingsplaene werden beim Laden auf den flachen Vertrag reduziert. +Uebungen aus alten Rotationen werden in die Uebungsbibliothek uebernommen und +als Tagesuebungen referenziert. Warm-up und Cool-down werden aus den bisherigen +Tages- oder `prepost`-Feldern uebernommen. diff --git a/plugins/trainingsplan/app.py b/plugins/trainingsplan/app.py index f5d7afc..5f1b1a3 100644 --- a/plugins/trainingsplan/app.py +++ b/plugins/trainingsplan/app.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Trainingsplan- und Rezepteditor mit Entwurf/Veröffentlichung und Konfliktschutz.""" +"""Backend fuer einen schlanken Trainingsplan-Editor.""" from __future__ import annotations import io @@ -8,42 +8,29 @@ import os import re from copy import deepcopy from pathlib import Path -from typing import Any +from typing import Any, Callable -from flask import Flask, Response, jsonify, request, send_file +from flask import Flask, jsonify, request, send_file -import plan_builder -import recipe_builder -from schema_contract import migrate_training_config, validate_training_config, stable_id +from schema_contract import default_config, normalize_training_config, stable_id, validate_training_config from storage import atomic_json_write, utc_now BASE_DIR = Path(__file__).resolve().parent DATA_DIR = Path(os.environ.get("DATA_DIR", BASE_DIR / "data")).resolve() PLANS_DIR = DATA_DIR / "plans" -PRESETS_DIR = BASE_DIR / "presets" ACTIVE_FILE = DATA_DIR / "active.txt" LEGACY_CONFIG = DATA_DIR / "config.json" -STATIC_DIR = BASE_DIR / "static" BACKUPS_DIR = DATA_DIR / "backups" -TRACKER_PROPOSALS_DIR = DATA_DIR.parent / "trainingstracker" / "proposals" -TRACKER_SESSIONS_DIR = DATA_DIR.parent / "trainingstracker" / "sessions" PLANS_DIR.mkdir(parents=True, exist_ok=True) BACKUPS_DIR.mkdir(parents=True, exist_ok=True) + app = Flask(__name__, static_folder=None) -def builder_for(cfg: dict[str, Any]): - return recipe_builder if (cfg or {}).get("type") == "recipe" else plan_builder - - -def default_for(kind: str): - cfg = recipe_builder.default_config() if kind == "recipe" else plan_builder.default_config() - return cfg if kind == "recipe" else migrate_training_config(cfg) - - def slugify(name: Any) -> str: - s = str(name or "").strip().lower().replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss") - return re.sub(r"[^a-z0-9]+", "-", s).strip("-") or "phase" + text = str(name or "").strip().lower() + text = text.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss") + return re.sub(r"[^a-z0-9]+", "-", text).strip("-") or "trainingsplan" def plan_path(pid: str) -> Path: @@ -53,26 +40,29 @@ def plan_path(pid: str) -> Path: def unique_id(base: str) -> str: stem, index = base, 2 while plan_path(stem).exists(): - stem = f"{base}-{index}"; index += 1 + stem = f"{base}-{index}" + index += 1 return stem +def _payload() -> dict[str, Any]: + body = request.get_json(silent=True) + return body if isinstance(body, dict) else {} + + def _normalize_wrapper(pid: str, raw: Any) -> dict[str, Any]: if not isinstance(raw, dict): raw = {} name = str(raw.get("name") or pid) - published = raw.get("config") if isinstance(raw.get("config"), dict) else {} - is_recipe = published.get("type") == "recipe" - if not is_recipe: - published = migrate_training_config(published, plan_id=str(raw.get("plan_id") or pid)) - draft = raw.get("draft") if isinstance(raw.get("draft"), dict) else deepcopy(published) - if not is_recipe: - draft = migrate_training_config(draft, plan_id=published.get("plan_id") or pid) + published_source = raw.get("config") if isinstance(raw.get("config"), dict) else raw + published = normalize_training_config(published_source, plan_id=str(raw.get("plan_id") or pid)) + draft_source = raw.get("draft") if isinstance(raw.get("draft"), dict) else published + draft = normalize_training_config(draft_source, plan_id=published.get("plan_id") or pid) revision = max(1, int(raw.get("revision") or 1)) published_revision = max(1, int(raw.get("published_revision") or 1)) return { "id": pid, - "plan_id": str(raw.get("plan_id") or published.get("plan_id") or stable_id("plan", pid, name)), + "plan_id": str(raw.get("plan_id") or published.get("plan_id") or pid), "name": name, "revision": revision, "published_revision": published_revision, @@ -96,25 +86,41 @@ def write_plan(pid: str, wrapper: dict[str, Any], *, backup: bool = True) -> dic return normalized -def create_wrapper(pid: str, name: str, cfg: dict[str, Any]) -> dict[str, Any]: - if cfg.get("type") != "recipe": - cfg = migrate_training_config(cfg, plan_id=pid) +def create_wrapper(pid: str, name: str, cfg: dict[str, Any] | None = None) -> dict[str, Any]: + plan = normalize_training_config(cfg or default_config(), plan_id=pid) + plan["plan_id"] = pid + plan["meta"]["title"] = str(name or plan["meta"].get("title") or "Neuer Trainingsplan") now = utc_now() return { - "id": pid, "plan_id": cfg.get("plan_id") or pid, "name": name, - "revision": 1, "published_revision": 1, - "created_at": now, "updated_at": now, "published_at": now, - "config": deepcopy(cfg), "draft": deepcopy(cfg), + "id": pid, + "plan_id": pid, + "name": plan["meta"]["title"], + "revision": 1, + "published_revision": 1, + "created_at": now, + "updated_at": now, + "published_at": now, + "config": deepcopy(plan), + "draft": deepcopy(plan), } def list_plan_ids() -> list[str]: - return sorted(path.stem for path in PLANS_DIR.glob("*.json")) + ids = [] + for path in PLANS_DIR.glob("*.json"): + try: + raw = json.loads(path.read_text(encoding="utf-8")) + except Exception: + raw = {} + source = raw.get("config") if isinstance(raw, dict) and isinstance(raw.get("config"), dict) else raw + if isinstance(source, dict) and source.get("type") == "recipe": + continue + ids.append(path.stem) + return sorted(ids) -def plan_name(pid: str) -> str: - try: return read_plan(pid)["name"] - except Exception: return pid +def editable_plan_exists(pid: str) -> bool: + return pid in list_plan_ids() def get_active() -> str | None: @@ -128,323 +134,531 @@ def set_active(pid: str) -> None: ACTIVE_FILE.write_text(pid, encoding="utf-8") -def install_presets() -> None: - if not PRESETS_DIR.is_dir(): return - for source in sorted(PRESETS_DIR.glob("*.json")): - if plan_path(source.stem).exists(): continue - try: - raw = json.loads(source.read_text(encoding="utf-8")) - cfg = raw.get("config", {}) if isinstance(raw, dict) else {} - wrapper = create_wrapper(source.stem, str(raw.get("name") or source.stem), cfg) - write_plan(source.stem, wrapper, backup=False) - except Exception: pass - - def ensure_seed() -> None: if not list_plan_ids(): - try: cfg = json.loads(LEGACY_CONFIG.read_text(encoding="utf-8")) if LEGACY_CONFIG.exists() else default_for("training") - except Exception: cfg = default_for("training") - write_plan("phase-1", create_wrapper("phase-1", "Phase 1", cfg), backup=False) - install_presets() - if get_active() is None and list_plan_ids(): set_active(list_plan_ids()[0]) - - -def _tracked_session_count(pid: str) -> int: - path = TRACKER_SESSIONS_DIR / f"{pid}.json" - if not path.exists(): - return 0 - try: - raw = json.loads(path.read_text(encoding="utf-8")) - except Exception: - return 0 - sessions = raw.get("sessions") if isinstance(raw, dict) and isinstance(raw.get("sessions"), dict) else {} - count = 0 - for session in sessions.values(): - if not isinstance(session, dict): - continue - if str(session.get("status") or "planned") != "planned" or any( - isinstance(item, dict) and ( - item.get("done") or item.get("completion_status") in {"completed", "partial", "skipped"} - or any(str(item.get(field) or "").strip() for field in ("result", "note", "progression", "skip_reason")) - ) for item in (session.get("items") or {}).values() - ): - count += 1 - return count + try: + raw = json.loads(LEGACY_CONFIG.read_text(encoding="utf-8")) if LEGACY_CONFIG.exists() else default_config() + except Exception: + raw = default_config() + write_plan("trainingsplan", create_wrapper("trainingsplan", "Trainingsplan", raw), backup=False) + if get_active() is None and list_plan_ids(): + set_active(list_plan_ids()[0]) def public_wrapper(wrapper: dict[str, Any], *, draft: bool = True) -> dict[str, Any]: return { - "id": wrapper["id"], "plan_id": wrapper["plan_id"], "name": wrapper["name"], - "revision": wrapper["revision"], "published_revision": wrapper["published_revision"], - "updated_at": wrapper["updated_at"], "published_at": wrapper["published_at"], + "id": wrapper["id"], + "plan_id": wrapper["plan_id"], + "name": wrapper["name"], + "revision": wrapper["revision"], + "published_revision": wrapper["published_revision"], + "updated_at": wrapper["updated_at"], + "published_at": wrapper["published_at"], "has_unpublished_changes": wrapper["draft"] != wrapper["config"], - "tracked_sessions": _tracked_session_count(wrapper["id"]), "config": deepcopy(wrapper["draft"] if draft else wrapper["config"]), } -def _validation(config: dict[str, Any]) -> dict[str, Any]: - if config.get("type") == "recipe": return {"errors": [], "warnings": [], "config": config} - return validate_training_config(config) +def _save_draft(pid: str, wrapper: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]: + wrapper["draft"] = normalize_training_config(config, plan_id=wrapper["plan_id"]) + wrapper["draft"]["meta"]["title"] = str(wrapper["name"]) + wrapper["revision"] += 1 + return write_plan(pid, wrapper) -def _diff_summary(old: Any, new: Any) -> dict[str, Any]: - """Erzeugt einen stabil-ID-basierten Änderungsbericht für die Veröffentlichung.""" - old_cfg = old if isinstance(old, dict) else {} - new_cfg = new if isinstance(new, dict) else {} - - def collect(cfg: dict[str, Any]) -> dict[str, dict[str, dict[str, Any]]]: - result: dict[str, dict[str, dict[str, Any]]] = { - "days": {}, "rotations": {}, "exercises": {}, "progression_steps": {}, - } - for di, day in enumerate(cfg.get("days", []) if isinstance(cfg.get("days"), list) else []): - if not isinstance(day, dict): - continue - day_id = str(day.get("id") or f"day-index-{di}") - result["days"][day_id] = { - "label": str(day.get("focus") or f"Tag {di + 1}"), - "value": {"num": day.get("num"), "focus": day.get("focus"), "light": bool(day.get("light"))}, - } - for ri, rotation in enumerate(day.get("rotations", []) if isinstance(day.get("rotations"), list) else []): - if not isinstance(rotation, dict): - continue - rotation_id = str(rotation.get("id") or f"{day_id}:rotation:{ri}") - result["rotations"][rotation_id] = { - "label": f"{day.get('focus') or 'Tag'} · {rotation.get('label') or f'Block {ri + 1}'}", - "value": {"day_id": day_id, "label": rotation.get("label")}, - } - for ei, exercise in enumerate(rotation.get("exercises", []) if isinstance(rotation.get("exercises"), list) else []): - if not isinstance(exercise, dict): - continue - exercise_id = str(exercise.get("id") or f"{rotation_id}:exercise:{ei}") - result["exercises"][exercise_id] = { - "label": str(exercise.get("name") or f"Übung {ei + 1}"), - "value": { - "rotation_id": rotation_id, - "name": exercise.get("name"), "cue": exercise.get("cue"), - "progression_id": exercise.get("progression_id") or exercise.get("key"), - "exercise_id": exercise.get("exercise_id"), - "movement_cluster": exercise.get("movement_cluster"), - "result_schema": exercise.get("result_schema"), - }, - } - stages = cfg.get("stages") if isinstance(cfg.get("stages"), dict) else {} - for key, stage in stages.items(): - if not isinstance(stage, dict): - continue - for si, step in enumerate(stage.get("steps", []) if isinstance(stage.get("steps"), list) else []): - if not isinstance(step, dict): - continue - step_id = str(step.get("id") or f"{key}:step:{si}") - result["progression_steps"][step_id] = { - "label": f"{stage.get('name') or key} · {step.get('name') or f'Stufe {si + 1}'}", - "value": { - "progression_id": key, "name": step.get("name"), - "phase_id": step.get("phase_id"), "factor": step.get("factor"), - "movement_cluster": step.get("movement_cluster"), - "result_schema": step.get("result_schema"), - }, - } - return result - - before, after = collect(old_cfg), collect(new_cfg) - summary: dict[str, Any] = {} - changes: list[dict[str, Any]] = [] - for section in ("days", "rotations", "exercises", "progression_steps"): - left, right = before[section], after[section] - added = sorted(set(right) - set(left)) - removed = sorted(set(left) - set(right)) - changed = sorted(key for key in set(left) & set(right) if left[key]["value"] != right[key]["value"]) - summary[section] = { - "before": len(left), "after": len(right), "delta": len(right) - len(left), - "added": len(added), "removed": len(removed), "changed": len(changed), - } - for key in added: - changes.append({"section": section, "kind": "added", "id": key, "label": right[key]["label"]}) - for key in removed: - changes.append({"section": section, "kind": "removed", "id": key, "label": left[key]["label"]}) - for key in changed: - changes.append({"section": section, "kind": "changed", "id": key, "label": right[key]["label"]}) - summary["changes"] = changes[:50] - summary["total_changes"] = len(changes) - summary["training_format_changed"] = old_cfg.get("training_format") != new_cfg.get("training_format") - summary["plan_meta_changed"] = old_cfg.get("meta") != new_cfg.get("meta") - return summary +def _mutate_draft(pid: str, mutator: Callable[[dict[str, Any]], Any]) -> tuple[dict[str, Any], Any]: + if not editable_plan_exists(pid): + raise FileNotFoundError(pid) + wrapper = read_plan(pid) + draft = deepcopy(wrapper["draft"]) + result = mutator(draft) + if isinstance(result, str) and result in {"day", "exercise", "item", "used"}: + return wrapper, result + saved = _save_draft(pid, wrapper, draft) + return saved, result -def _proposal_file(pid: str) -> Path: - return TRACKER_PROPOSALS_DIR / f"{pid}.json" +def _find_by_id(items: list[dict[str, Any]], item_id: str) -> dict[str, Any] | None: + return next((item for item in items if str(item.get("id")) == item_id), None) + + +def _replace_list_item(items: list[dict[str, Any]], item_id: str, replacement: dict[str, Any]) -> bool: + for index, item in enumerate(items): + if str(item.get("id")) == item_id: + items[index] = replacement + return True + return False + + +def _delete_list_item(items: list[dict[str, Any]], item_id: str) -> bool: + before = len(items) + items[:] = [item for item in items if str(item.get("id")) != item_id] + return len(items) != before ensure_seed() + @app.route("/") def index(): - return Response((STATIC_DIR / "index.html").read_text(encoding="utf-8"), mimetype="text/html") + return jsonify({"name": "Trainingsplan-Backend", "status": "ok"}) + @app.route("/api/plans", methods=["GET"]) def api_plans(): - active = get_active(); plans = [] + active = get_active() + plans = [] for pid in list_plan_ids(): try: - p = read_plan(pid) - plans.append({"id": pid, "name": p["name"], "active": pid == active, "revision": p["revision"], "published_revision": p["published_revision"], "has_unpublished_changes": p["draft"] != p["config"]}) - except Exception: continue + plan = read_plan(pid) + plans.append({ + "id": pid, + "name": plan["name"], + "active": pid == active, + "revision": plan["revision"], + "published_revision": plan["published_revision"], + "has_unpublished_changes": plan["draft"] != plan["config"], + }) + except Exception: + continue return jsonify({"plans": plans, "active": active}) + @app.route("/api/plans", methods=["POST"]) def api_plan_create(): - body = request.get_json(force=True) or {}; name = str(body.get("name") or "Neue Phase").strip() - src = body.get("from"); kind = body.get("kind") or body.get("type") or "training" - cfg = deepcopy(read_plan(src)["draft"]) if src and plan_path(str(src)).exists() else default_for(str(kind)) - pid = unique_id(slugify(name)); wrapper = create_wrapper(pid, name, cfg); write_plan(pid, wrapper, backup=False); set_active(pid) - return jsonify(public_wrapper(wrapper)) + body = _payload() + name = str(body.get("name") or "Neuer Trainingsplan").strip() + source = body.get("from") + cfg = deepcopy(read_plan(str(source))["draft"]) if source and editable_plan_exists(str(source)) else default_config() + pid = unique_id(slugify(name)) + wrapper = create_wrapper(pid, name, cfg) + write_plan(pid, wrapper, backup=False) + set_active(pid) + return jsonify(public_wrapper(wrapper)), 201 + @app.route("/api/plans/", methods=["GET"]) def api_plan_get(pid: str): - if not plan_path(pid).exists(): return jsonify({"error": "not found"}), 404 + if not editable_plan_exists(pid): + return jsonify({"error": "not found"}), 404 return jsonify(public_wrapper(read_plan(pid))) + @app.route("/api/plans//select", methods=["POST"]) def api_plan_select(pid: str): - if not plan_path(pid).exists(): return jsonify({"error": "not found"}), 404 - set_active(pid); return jsonify(public_wrapper(read_plan(pid))) + if not editable_plan_exists(pid): + return jsonify({"error": "not found"}), 404 + set_active(pid) + return jsonify(public_wrapper(read_plan(pid))) + @app.route("/api/plans/", methods=["POST"]) def api_plan_save(pid: str): - if not plan_path(pid).exists(): return jsonify({"error": "not found"}), 404 - body = request.get_json(force=True) or {} + if not editable_plan_exists(pid): + return jsonify({"error": "not found"}), 404 + body = _payload() config = body.get("config") if isinstance(body.get("config"), dict) else body expected = body.get("expected_revision") if isinstance(body, dict) else None wrapper = read_plan(pid) if expected is not None and int(expected) != wrapper["revision"]: return jsonify({"error": "Der Plan wurde zwischenzeitlich geändert.", "code": "revision_conflict", "current_revision": wrapper["revision"]}), 409 - validation = _validation(config) - wrapper["draft"] = validation["config"] - wrapper["revision"] += 1 - saved = write_plan(pid, wrapper) + saved = _save_draft(pid, wrapper, config) + validation = validate_training_config(saved["draft"]) return jsonify({"ok": True, **public_wrapper(saved), "validation": {"errors": validation["errors"], "warnings": validation["warnings"]}}) -@app.route("/api/plans//validate", methods=["POST"]) -def api_plan_validate(pid: str): - if not plan_path(pid).exists(): return jsonify({"error": "not found"}), 404 - body = request.get_json(force=True) or {} - cfg = body.get("config") if isinstance(body.get("config"), dict) else body - result = _validation(cfg) - wrapper = read_plan(pid) - diff = _diff_summary(wrapper["config"], result["config"]) - diff["tracked_sessions"] = _tracked_session_count(pid) - return jsonify({"errors": result["errors"], "warnings": result["warnings"], "config": result["config"], "diff": diff}) @app.route("/api/plans//publish", methods=["POST"]) def api_plan_publish(pid: str): - if not plan_path(pid).exists(): return jsonify({"error": "not found"}), 404 - body = request.get_json(silent=True) or {}; wrapper = read_plan(pid) + if not editable_plan_exists(pid): + return jsonify({"error": "not found"}), 404 + body = _payload() + wrapper = read_plan(pid) expected = body.get("expected_revision") if expected is not None and int(expected) != wrapper["revision"]: return jsonify({"error": "Der Entwurf wurde zwischenzeitlich geändert.", "code": "revision_conflict", "current_revision": wrapper["revision"]}), 409 - validation = _validation(wrapper["draft"]) + validation = validate_training_config(wrapper["draft"]) if validation["errors"]: return jsonify({"error": "Der Plan enthält Validierungsfehler.", "validation": {"errors": validation["errors"], "warnings": validation["warnings"]}}), 400 - diff = _diff_summary(wrapper["config"], validation["config"]) - diff["tracked_sessions"] = _tracked_session_count(pid) wrapper["config"] = deepcopy(validation["config"]) wrapper["draft"] = deepcopy(validation["config"]) wrapper["published_revision"] += 1 wrapper["revision"] += 1 wrapper["published_at"] = utc_now() saved = write_plan(pid, wrapper) - return jsonify({"ok": True, **public_wrapper(saved), "diff": diff, "validation": {"errors": [], "warnings": validation["warnings"]}}) + return jsonify({"ok": True, **public_wrapper(saved, draft=False), "validation": {"errors": [], "warnings": validation["warnings"]}}) + @app.route("/api/plans//rename", methods=["POST"]) def api_plan_rename(pid: str): - if not plan_path(pid).exists(): return jsonify({"error": "not found"}), 404 - name = str((request.get_json(force=True) or {}).get("name") or "").strip() - if not name: return jsonify({"error": "name required"}), 400 - wrapper = read_plan(pid); wrapper["name"] = name; wrapper["revision"] += 1; saved = write_plan(pid, wrapper) + if not editable_plan_exists(pid): + return jsonify({"error": "not found"}), 404 + name = str(_payload().get("name") or "").strip() + if not name: + return jsonify({"error": "name required"}), 400 + wrapper = read_plan(pid) + wrapper["name"] = name + wrapper["draft"]["meta"]["title"] = name + wrapper["config"]["meta"]["title"] = name + wrapper["revision"] += 1 + saved = write_plan(pid, wrapper) return jsonify({"id": pid, "name": name, "revision": saved["revision"]}) + @app.route("/api/plans/", methods=["DELETE"]) def api_plan_delete(pid: str): ids = list_plan_ids() - if pid not in ids: return jsonify({"error": "not found"}), 404 - if len(ids) <= 1: return jsonify({"error": "letzte Phase kann nicht gelöscht werden"}), 400 - atomic_json_write(BACKUPS_DIR / f"deleted-{pid}-{utc_now().replace(':','')}.json", {k:v for k,v in read_plan(pid).items() if k != 'id'}) + if pid not in ids: + return jsonify({"error": "not found"}), 404 + if len(ids) <= 1: + return jsonify({"error": "letzter Plan kann nicht gelöscht werden"}), 400 + atomic_json_write(BACKUPS_DIR / f"deleted-{pid}-{utc_now().replace(':', '')}.json", {k: v for k, v in read_plan(pid).items() if k != "id"}) plan_path(pid).unlink() - if get_active() is None: set_active(list_plan_ids()[0]) + if get_active() is None: + set_active(list_plan_ids()[0]) return jsonify({"ok": True, "active": get_active()}) + @app.route("/api/plans//export") def api_plan_export(pid: str): - if not plan_path(pid).exists(): return jsonify({"error": "not found"}), 404 + if not editable_plan_exists(pid): + return jsonify({"error": "not found"}), 404 return send_file(io.BytesIO(plan_path(pid).read_bytes()), mimetype="application/json", as_attachment=True, download_name=f"{pid}.json") + @app.route("/api/plans/import", methods=["POST"]) def api_plan_import(): raw = request.files["file"].read() if "file" in request.files else request.data - if not raw: return jsonify({"error": "keine Datei"}), 400 - try: obj = json.loads(raw.decode("utf-8")) - except Exception as exc: return jsonify({"error": f"ungültiges JSON: {exc}"}), 400 - if isinstance(obj, dict) and "config" in obj: - cfg = obj.get("config", {}); name = str(obj.get("name") or "Importierte Phase") - else: - cfg = obj if isinstance(obj, dict) else {}; name = str((cfg.get("meta") or {}).get("title") or "Importierte Phase") - pid = unique_id(slugify(name)); wrapper = create_wrapper(pid, name, cfg); write_plan(pid, wrapper, backup=False); set_active(pid) - return jsonify(public_wrapper(wrapper)) + if not raw: + return jsonify({"error": "keine Datei"}), 400 + try: + obj = json.loads(raw.decode("utf-8")) + except Exception as exc: + return jsonify({"error": f"ungültiges JSON: {exc}"}), 400 + cfg = obj.get("config", {}) if isinstance(obj, dict) and isinstance(obj.get("config"), dict) else (obj if isinstance(obj, dict) else {}) + name = str(obj.get("name") or (cfg.get("meta") or {}).get("title") or "Importierter Trainingsplan") + pid = unique_id(slugify(name)) + wrapper = create_wrapper(pid, name, cfg) + write_plan(pid, wrapper, backup=False) + set_active(pid) + return jsonify(public_wrapper(wrapper)), 201 -@app.route("/api/plans//proposals") -def api_plan_proposals(pid: str): - raw = {} - path = _proposal_file(pid) - if path.exists(): - try: raw = json.loads(path.read_text(encoding="utf-8")) - except Exception: raw = {} - return jsonify(raw if isinstance(raw, dict) else {}) -@app.route("/api/plans//proposals/", methods=["POST"]) -def api_plan_proposal_status(pid: str, proposal_id: str): - path = _proposal_file(pid); body = request.get_json(silent=True) or {}; status = str(body.get("status") or "reviewed") - if status not in {"open", "accepted", "rejected", "reviewed"}: - return jsonify({"error": "ungültiger Vorschlagsstatus"}), 400 - raw = {} - if path.exists(): - try: raw = json.loads(path.read_text(encoding="utf-8")) - except Exception: raw = {} - proposals = raw.get("proposals") if isinstance(raw.get("proposals"), list) else [] - for proposal in proposals: - if str(proposal.get("id")) == proposal_id: - proposal["status"] = status; proposal["reviewed_at"] = utc_now() - raw["proposals"] = proposals; raw["updated_at"] = utc_now(); atomic_json_write(path, raw) - return jsonify({"ok": True, "proposals": proposals}) +@app.route("/api/plans//exercises", methods=["GET"]) +def api_exercises_list(pid: str): + if not editable_plan_exists(pid): + return jsonify({"error": "not found"}), 404 + return jsonify({"exercises": read_plan(pid)["draft"].get("exercises", [])}) + + +@app.route("/api/plans//exercises", methods=["POST"]) +def api_exercise_create(pid: str): + body = _payload() + name = str(body.get("name") or "").strip() + if not name: + return jsonify({"error": "name required"}), 400 + + def mutate(cfg: dict[str, Any]) -> dict[str, Any]: + exercise = { + "id": stable_id("exercise", cfg.get("plan_id"), len(cfg.get("exercises", [])), name), + "name": name, + } + for key in ("cue", "description", "equipment", "notes"): + value = str(body.get(key) or "").strip() + if value: + exercise[key] = value + tags = body.get("tags") + if isinstance(tags, list): + exercise["tags"] = [str(tag).strip() for tag in tags if str(tag).strip()] + cfg.setdefault("exercises", []).append(exercise) + return exercise + + try: + saved, exercise = _mutate_draft(pid, mutate) + except FileNotFoundError: + return jsonify({"error": "not found"}), 404 + return jsonify({"ok": True, "exercise": exercise, "revision": saved["revision"]}), 201 + + +@app.route("/api/plans//exercises/", methods=["PATCH"]) +def api_exercise_update(pid: str, exercise_id: str): + body = _payload() + + def mutate(cfg: dict[str, Any]) -> dict[str, Any] | None: + exercises = cfg.setdefault("exercises", []) + exercise = _find_by_id(exercises, exercise_id) + if not exercise: + return None + updated = deepcopy(exercise) + for key in ("name", "cue", "description", "equipment", "notes"): + if key in body: + value = str(body.get(key) or "").strip() + if value: + updated[key] = value + else: + updated.pop(key, None) + if isinstance(body.get("tags"), list): + updated["tags"] = [str(tag).strip() for tag in body["tags"] if str(tag).strip()] + _replace_list_item(exercises, exercise_id, updated) + return updated + + try: + saved, exercise = _mutate_draft(pid, mutate) + except FileNotFoundError: + return jsonify({"error": "not found"}), 404 + if exercise is None: + return jsonify({"error": "exercise not found"}), 404 + return jsonify({"ok": True, "exercise": exercise, "revision": saved["revision"]}) + + +@app.route("/api/plans//exercises/", methods=["DELETE"]) +def api_exercise_delete(pid: str, exercise_id: str): + force = request.args.get("force") in {"1", "true", "yes"} + + def mutate(cfg: dict[str, Any]) -> bool | str: + used = any(item.get("exercise_id") == exercise_id for day in cfg.get("days", []) for item in day.get("exercises", [])) + if used and not force: + return "used" + removed = _delete_list_item(cfg.setdefault("exercises", []), exercise_id) + if force: + for day in cfg.get("days", []): + day["exercises"] = [item for item in day.get("exercises", []) if item.get("exercise_id") != exercise_id] + return removed + + try: + saved, result = _mutate_draft(pid, mutate) + except FileNotFoundError: + return jsonify({"error": "not found"}), 404 + if result == "used": + return jsonify({"error": "exercise is used by training days", "code": "exercise_in_use"}), 409 + if not result: + return jsonify({"error": "exercise not found"}), 404 + return jsonify({"ok": True, "revision": saved["revision"]}) + + +@app.route("/api/plans//days", methods=["GET"]) +def api_days_list(pid: str): + if not editable_plan_exists(pid): + return jsonify({"error": "not found"}), 404 + return jsonify({"days": read_plan(pid)["draft"].get("days", [])}) + + +@app.route("/api/plans//days", methods=["POST"]) +def api_day_create(pid: str): + body = _payload() + + def mutate(cfg: dict[str, Any]) -> dict[str, Any]: + days = cfg.setdefault("days", []) + next_num = max([int(day.get("num") or 0) for day in days], default=0) + 1 + num = int(body.get("num") or next_num) + name = str(body.get("name") or body.get("focus") or f"Tag {num}").strip() + day = { + "id": stable_id("day", cfg.get("plan_id"), num, name), + "num": num, + "name": name, + "warmup": {"items": [str(item).strip() for item in body.get("warmup", []) if str(item).strip()]} if isinstance(body.get("warmup"), list) else {"items": []}, + "cooldown": {"items": [str(item).strip() for item in body.get("cooldown", []) if str(item).strip()]} if isinstance(body.get("cooldown"), list) else {"items": []}, + "exercises": [], + } + days.append(day) + return day + + try: + saved, day = _mutate_draft(pid, mutate) + except FileNotFoundError: + return jsonify({"error": "not found"}), 404 + return jsonify({"ok": True, "day": day, "revision": saved["revision"]}), 201 + + +@app.route("/api/plans//days/", methods=["PATCH"]) +def api_day_update(pid: str, day_id: str): + body = _payload() + + def mutate(cfg: dict[str, Any]) -> dict[str, Any] | None: + days = cfg.setdefault("days", []) + day = _find_by_id(days, day_id) + if not day: + return None + updated = deepcopy(day) + for key in ("name", "focus", "notes"): + if key in body: + value = str(body.get(key) or "").strip() + if value: + updated[key] = value + else: + updated.pop(key, None) + if "num" in body: + updated["num"] = int(body.get("num") or updated.get("num") or 1) + for key in ("warmup", "cooldown"): + if isinstance(body.get(key), list): + updated[key] = {"items": [str(item).strip() for item in body[key] if str(item).strip()]} + elif isinstance(body.get(key), dict): + updated[key] = body[key] + _replace_list_item(days, day_id, updated) + return updated + + try: + saved, day = _mutate_draft(pid, mutate) + except FileNotFoundError: + return jsonify({"error": "not found"}), 404 + if day is None: + return jsonify({"error": "day not found"}), 404 + return jsonify({"ok": True, "day": day, "revision": saved["revision"]}) + + +@app.route("/api/plans//days/", methods=["DELETE"]) +def api_day_delete(pid: str, day_id: str): + def mutate(cfg: dict[str, Any]) -> bool: + return _delete_list_item(cfg.setdefault("days", []), day_id) + + try: + saved, removed = _mutate_draft(pid, mutate) + except FileNotFoundError: + return jsonify({"error": "not found"}), 404 + if not removed: + return jsonify({"error": "day not found"}), 404 + return jsonify({"ok": True, "revision": saved["revision"]}) + + +@app.route("/api/plans//days//exercises", methods=["POST"]) +def api_day_exercise_add(pid: str, day_id: str): + body = _payload() + + def mutate(cfg: dict[str, Any]) -> dict[str, Any] | str: + day = _find_by_id(cfg.setdefault("days", []), day_id) + if not day: + return "day" + exercise_id = str(body.get("exercise_id") or "").strip() + if not exercise_id and str(body.get("name") or "").strip(): + exercise = { + "id": stable_id("exercise", cfg.get("plan_id"), len(cfg.get("exercises", [])), body.get("name")), + "name": str(body.get("name")).strip(), + } + cfg.setdefault("exercises", []).append(exercise) + exercise_id = exercise["id"] + if not _find_by_id(cfg.get("exercises", []), exercise_id): + return "exercise" + item = { + "id": stable_id("day-exercise", day_id, exercise_id, len(day.get("exercises", []))), + "exercise_id": exercise_id, + } + for key in ("sets", "duration_seconds"): + if body.get(key): + item[key] = int(body[key]) + for key in ("reps", "tempo", "rest", "notes"): + value = str(body.get(key) or "").strip() + if value: + item[key] = value + day.setdefault("exercises", []).append(item) + return item + + try: + saved, item = _mutate_draft(pid, mutate) + except FileNotFoundError: + return jsonify({"error": "not found"}), 404 + if item == "day": + return jsonify({"error": "day not found"}), 404 + if item == "exercise": + return jsonify({"error": "exercise not found"}), 404 + return jsonify({"ok": True, "day_exercise": item, "revision": saved["revision"]}), 201 + + +@app.route("/api/plans//days//exercises/", methods=["PATCH"]) +def api_day_exercise_update(pid: str, day_id: str, item_id: str): + body = _payload() + + def mutate(cfg: dict[str, Any]) -> dict[str, Any] | str: + day = _find_by_id(cfg.setdefault("days", []), day_id) + if not day: + return "day" + item = _find_by_id(day.setdefault("exercises", []), item_id) + if not item: + return "item" + updated = deepcopy(item) + if "exercise_id" in body: + exercise_id = str(body.get("exercise_id") or "").strip() + if not _find_by_id(cfg.get("exercises", []), exercise_id): + return "exercise" + updated["exercise_id"] = exercise_id + for key in ("sets", "duration_seconds"): + if key in body: + value = int(body.get(key) or 0) + if value > 0: + updated[key] = value + else: + updated.pop(key, None) + for key in ("reps", "tempo", "rest", "notes"): + if key in body: + value = str(body.get(key) or "").strip() + if value: + updated[key] = value + else: + updated.pop(key, None) + _replace_list_item(day["exercises"], item_id, updated) + return updated + + try: + saved, item = _mutate_draft(pid, mutate) + except FileNotFoundError: + return jsonify({"error": "not found"}), 404 + if item == "day": + return jsonify({"error": "day not found"}), 404 + if item == "item": + return jsonify({"error": "day exercise not found"}), 404 + if item == "exercise": + return jsonify({"error": "exercise not found"}), 404 + return jsonify({"ok": True, "day_exercise": item, "revision": saved["revision"]}) + + +@app.route("/api/plans//days//exercises/", methods=["DELETE"]) +def api_day_exercise_delete(pid: str, day_id: str, item_id: str): + def mutate(cfg: dict[str, Any]) -> bool | str: + day = _find_by_id(cfg.setdefault("days", []), day_id) + if not day: + return "day" + return _delete_list_item(day.setdefault("exercises", []), item_id) + + try: + saved, result = _mutate_draft(pid, mutate) + except FileNotFoundError: + return jsonify({"error": "not found"}), 404 + if result == "day": + return jsonify({"error": "day not found"}), 404 + if not result: + return jsonify({"error": "day exercise not found"}), 404 + return jsonify({"ok": True, "revision": saved["revision"]}) + -# Backward-compatible active plan endpoints @app.route("/api/config", methods=["GET"]) def api_get_config(): - pid = get_active(); return jsonify(public_wrapper(read_plan(pid)) if pid else {"config": default_for("training")}) + pid = get_active() + return jsonify(public_wrapper(read_plan(pid)) if pid else {"config": default_config()}) + @app.route("/api/config", methods=["POST"]) def api_save_config(): pid = get_active() - if not pid: return jsonify({"error": "kein aktiver Plan"}), 400 + if not pid: + return jsonify({"error": "kein aktiver Plan"}), 400 return api_plan_save(pid) + @app.route("/api/default") -def api_default(): return jsonify(default_for(request.args.get("kind", "training"))) +def api_default(): + return jsonify(default_config()) + @app.route("/api/reset", methods=["POST"]) def api_reset(): - pid = get_active(); wrapper = read_plan(pid); kind = "recipe" if wrapper["draft"].get("type") == "recipe" else "training" - wrapper["draft"] = default_for(kind); wrapper["revision"] += 1; saved = write_plan(pid, wrapper) + pid = get_active() + if not pid: + return jsonify({"error": "kein aktiver Plan"}), 400 + wrapper = read_plan(pid) + wrapper["draft"] = normalize_training_config(default_config(), plan_id=wrapper["plan_id"]) + wrapper["draft"]["meta"]["title"] = wrapper["name"] + wrapper["revision"] += 1 + saved = write_plan(pid, wrapper) return jsonify(public_wrapper(saved)) -@app.route("/api/pdf", methods=["POST"]) -def api_pdf(): - body = request.get_json(force=True) or {}; cfg = body.get("config") if isinstance(body.get("config"), dict) else body - if cfg.get("type") != "recipe": cfg = migrate_training_config(cfg) - try: pdf_bytes = builder_for(cfg).build_pdf(cfg) - except Exception as exc: return jsonify({"error": str(exc)}), 400 - name = cfg.get("meta", {}).get("output_name", "Trainingsplan.pdf") - return send_file(io.BytesIO(pdf_bytes), mimetype="application/pdf", as_attachment=True, download_name=name) if __name__ == "__main__": app.run(host=os.environ.get("HOST", "0.0.0.0"), port=int(os.environ.get("PORT", "8080")), debug=False) diff --git a/plugins/trainingsplan/backend.py b/plugins/trainingsplan/backend.py index 6ad78c3..ab97b2c 100644 --- a/plugins/trainingsplan/backend.py +++ b/plugins/trainingsplan/backend.py @@ -1,16 +1,5 @@ # -*- coding: utf-8 -*- -""" -Plugin-Adapter für den Trainingsplan- und Rezept-Editor. - -Das Original ist eine Flask-Anwendung (app.py) und bleibt vollständig -unverändert. Hier wird sie lediglich - - * auf ihren eigenen Datenordner data/trainingsplan/ gezeigt und - * als WSGI-App in die ASGI-Suite eingehängt. - -Das Tool läuft dadurch weiterhin auch allein: - cd plugins/trainingsplan && python app.py -""" +"""Plugin-Adapter fuer das Trainingsplan-Backend.""" from __future__ import annotations import os @@ -21,7 +10,7 @@ from core.loader import load_module def create_app(ctx): - # app.py liest DATA_DIR beim Import und legt dort die Phasen an. + # app.py liest DATA_DIR beim Import und legt dort den ersten Plan an. previous = os.environ.get("DATA_DIR") os.environ["DATA_DIR"] = str(ctx.data_dir) try: @@ -32,7 +21,6 @@ def create_app(ctx): else: os.environ["DATA_DIR"] = previous - # Flask erzeugt seine URLs anhand von SCRIPT_NAME; a2wsgi übernimmt dafür - # den root_path des Mounts. Damit funktionieren alle Routen unter - # /plugins/trainingsplan genauso wie vorher unter /. + # Flask erzeugt seine URLs anhand von SCRIPT_NAME; a2wsgi uebernimmt dafuer + # den root_path des Mounts. return WSGIMiddleware(module.app) diff --git a/plugins/trainingsplan/plan_builder.py b/plugins/trainingsplan/plan_builder.py deleted file mode 100644 index fe90b44..0000000 --- a/plugins/trainingsplan/plan_builder.py +++ /dev/null @@ -1,669 +0,0 @@ -# -*- coding: utf-8 -*- -""" -plan_builder.py -Rendert den 8-Wochen-Calisthenics-Plan als A4-PDF. -Layout 1:1 wie im Original-Skript, aber alle Inhalte kommen aus einem Config-Dict. -build_pdf(config) -> bytes -default_config() -> dict (Standard-Inhalte) -""" -import copy -from schema_contract import migrate_training_config -from io import BytesIO - -from reportlab.lib.pagesizes import A4 -from reportlab.lib.units import cm -from reportlab.lib import colors -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle -from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, - PageBreak, HRFlowable, KeepTogether) -from reportlab.lib.enums import TA_RIGHT - -# ========================================================================= -# STANDARD-KONFIGURATION (entspricht exakt dem Original-Skript) -# ========================================================================= -DEFAULTS = { - "meta": { - "title": "Calisthenics-Basis – 8-Wochen Komplettplan", - "subtitle": "Blutiger Anfänger · 1,90 m · 95 kg · Rucksack + Couch + Kettlebell · tägliche ~30-Min Tabata-Session", - "output_name": "Calisthenics-8-Wochen-Komplett.pdf", - "footer_left": "Calisthenics 8-Wochen Komplettplan \u00b7 mit Kettlebell", - "doc_title": "Calisthenics 8-Wochen Komplettplan (mit Kettlebell)", - "weeks": 8, - }, - "colors": { - "INK": "#1A1A1A", "ACCENT": "#C2410C", "ACC2": "#0F766E", "KBCOL": "#6D28D9", - "LINK": "#1565C0", "LIGHT": "#F4F1EC", "GRID": "#CFC9BF", "MUTE": "#6B6256", - "DARK": "#23201C", "WARMBG": "#EAF1F0", "GOALBG": "#FBF1E9", "KBBG": "#F1ECFB", - }, - "front": { - "session_how_head": "So funktioniert eine Session", - "session_how_body": ( - "Tabata-Block = 4 Min: 10 Sek einzählen, dann 8× (20 Sek Arbeit / 10 Sek Pause). " - "Pro Block rotieren 4 Übungen, je 2 Durchgänge = 8 Intervalle = 1 Block = 1 Satz. " - "Eine Rotation läuft 3 Sätze (in Woche 1 ruhig mit 2 starten). Warm-up steht über, Cool-down unter den Übungen." - ), - "flow_rows": [ - ["Teil", "Inhalt", "Zeit"], - ["Warm-up", "Gelenk-Prep (dynamisch)", "~3 Min"], - ["Rotation A", "4 Übungen, 3 Sätze (kurze Pausen)", "~14 Min"], - ["Rotation B", "4 Übungen, 3 Sätze (kurze Pausen)", "~14 Min"], - ["Cool-down", "Ausklingen + Dehnen", "~6 Min"], - ["", "Gesamt", "~35 Min"], - ], - "timer_note": "Timer: „Tabata / Interval Timer“ auf 10s Vorlauf, 8 Runden, 20/10.", - "kb_head": "Warum Kettlebell – und warum es zu deinem Ziel passt", - "kb_box": ( - "Du trainierst für Calisthenics, nicht für Maximalkraft. Genau da liegen die belegten Stärken der Kettlebell – " - "ohne dass dich ihr dokumentierter Schwachpunkt (Maximalkraft/Hypertrophie wegen fixer Gewichtssprünge) trifft:
" - "•  Griffkraft (Studie BELL: +6–7 kg) – dein größter Mehrwert auf dem Weg zu Klimmzug & Dip.
" - "•  Power / ballistische Hüftstreckung (Swing) – explosive Basis für spätere Skills.
" - "•  Kondition & Körperfett – wirksam belegt; du startest mit wenig Ausdauer.
" - "•  Ganzkörper, ein Gerät, zeiteffizient – passt ideal ins 30-Min-Tabata-Format; niedriges Einstiegs-Verletzungsrisiko bei sauberer Technik.
" - "Ehrlich: Reines KB ersetzt kein Maximalkraft-/Muskelaufbau-Programm. Für dein Ziel (Basis-Fundament) liegt das ohnehin unter der relevanten Schwelle." - ), - "goals_head": "Realistische Ziele nach 8 Wochen", - "goals_box": ( - "Du startest bei 2–3 Liegestützen und 0 Dips. Realistisch und ehrlich nach 8 Wochen sauberer Arbeit:
" - "•  Liegestütze: von 2–3 auf erste saubere volle Reps (Rest in Knie-/Negativ-Form).
" - "•  Bank-Dips: saubere Reps mit Füßen etwas weiter weg.   Pike Push-up: am Boden mit kleinem ROM.
" - "•  Plank ~45–60 s, Wall Sit ~60 s, viele saubere Goblet-Kniebeugen, sauberer Swing + spürbar bessere Griffkraft.
" - "•  Noch NICHT dran: Diamond Push-ups, L-Sit, Handstand, schwerer Turkish Get-Up, Snatch – Stoff für Woche 12+." - ), - "goals_note": ( - "Wichtig: Die Wochen-Stufen sind ein Rahmen, kein Muss. Bleib auf einer Stufe, solange sie fordert. " - "Aufstiegs-Regel: erst hoch (oder schwerere Glocke), wenn du eine Übung 2 Sessions in Folge in allen Sätzen sauber über die vollen 20s schaffst. " - "KB = Kettlebell bevorzugt; Rucksack bleibt als Alternative." - ), - "lib_head": "Übungs-Bibliothek (mit Anleitungen)", - "lib_intro": ( - "Für Übungen, die du noch nicht kennst, ist ein Link zu einer bebilderten Anleitung dabei. " - "Lila Gruppe = Kettlebell-Übungen." - ), - "baseline_head": "Standortbestimmung & Tracking", - "baseline_intro": "Mach die Tests vor Tag 1 und in Woche 8 identisch nochmal – dein Vorher/Nachher.", - "baseline_rows": [ - ["Test", "Tag 0", "Woche 8"], - ["Liegestütze (max, saubere Reps)", "", ""], - ["Bank-Dips an der Couch (max)", "", ""], - ["Kniebeugen in 60 Sekunden", "", ""], - ["Ausfallschritte (max gesamt)", "", ""], - ["Plank (max Sekunden)", "", ""], - ["Wall Sit (max Sekunden)", "", ""], - ["KB Farmer-Hold (max Sekunden, Griff)", "", ""], - ["Körpergewicht (kg)", "95", ""], - ], - "reminder_head": "Kurz-Reminder", - "reminders": [ - "Form vor Reps. Wähle die Stufe / Glocke, bei der du die 20s kontrolliert durcharbeitest – mit 1–2 Reps Reserve.", - "Swing-Technik first. Der Swing ist ein Hüft-Schnapp, KEIN Kniebeuge und KEIN Armheben. Erst Dead-Stop-Swings, bis die Hüftstreckung sitzt.", - "Einseitige Übungen (Tag 2): Durchgang 1 = links, Durchgang 2 = rechts – so ist jede Seite automatisch ausbalanciert.", - "Täglich ist viel. Tag 5 leicht, Tag 7 frei. Bei starkem Muskelkater einen Extra-Ruhetag – gehört zum Aufbau.", - "Schlaf & Eiweiß. 7–9 h Schlaf, ~1,6 g Eiweiß pro kg. Stechender Schmerz = stoppen.", - ], - "sheets_note": ( - "Ab der nächsten Seite: 1 Druckblatt pro Session (Woche 1–8). Tag 6 = reine Kettlebell-Session. " - "Warm-up steht über, Cool-down + Dehnen unter den Übungen. Bei Vorerkrankungen/Verletzungen vorher ärztlich abklären." - ), - }, - "library": [ - {"group": "DRÜCKEN (Brust / Schultern / Trizeps)", "color": "ACC2", "items": [ - {"name": "Liegestütze", "desc": "Stütz, Körper gerade, Brust kontrolliert senken & drücken. Stufen: Inkline (Couch), Knie, Negativ, Voll.", "url": "https://blog.calimove.com/2023/01/25/push-up-variations-beginner-expert/"}, - {"name": "Bank-Dips (Couch)", "desc": "Rücken zur Couch, Hände auf der Kante, Po davor absenken (Ellbogen nach hinten), hochdrücken. Knie beugen = leichter.", "url": "https://www.healthline.com/health/fitness-exercise/bench-dips"}, - {"name": "Pike Push-up / Hold", "desc": "Umgekehrtes V (Hintern hoch), Kopf Richtung Boden senken, hochdrücken. Schulter-Fokus. Hände erhöht = leichter.", "url": "https://www.hingehealth.com/resources/articles/pike-push-up/"}, - {"name": "Seitheben (KB/Rucksack)", "desc": "Arme seitlich bis Schulterhöhe heben, Ellbogen leicht gebeugt, kontrolliert senken. Seitlicher Deltamuskel.", "url": ""}, - ]}, - {"group": "ZIEHEN (Rücken / Bizeps – KB oder Rucksack + Boden)", "color": "ACC2", "items": [ - {"name": "Einarm-Rudern ENG", "desc": "Hüfte beugen, Rücken GERADE, abgestützt. Ellbogen an den Rippen, Zug zur Hüfte – Lat.", "url": "https://www.puregym.com/exercises/back/rows/dumbbell-bent-over-row/"}, - {"name": "Einarm-Rudern BREIT", "desc": "Gleiche Position, aber Ellbogen ~45° geöffnet, Zug zum Brustkorb – oberer Rücken / hinterer Delta.", "url": "https://www.puregym.com/exercises/back/rows/dumbbell-bent-over-row/"}, - {"name": "Bizeps-Curls (KB/Rucksack)", "desc": "Last unten greifen, Oberarme fix am Körper, kontrolliert zu den Schultern curlen.", "url": ""}, - {"name": "Superman / Reverse Snow Angels / Y-T-W", "desc": "Bäuchlings Arme+Beine heben (Superman). Snow Angels = Arme am Boden auf/ab. Rücken ohne Stange.", "url": "https://www.calixpert.com/exercises/hollow-body-to-superman-hold"}, - ]}, - {"group": "BEINE", "color": "ACC2", "items": [ - {"name": "Kniebeuge / Goblet Squat (KB/Rucksack)", "desc": "Hüfte zurück, Oberschenkel parallel, Fersen am Boden. Goblet = Glocke/Rucksack vor der Brust umarmen.", "url": ""}, - {"name": "Ausfallschritte", "desc": "Großer Schritt (rückwärts = knieschonend), hinteres Knie Richtung Boden. Anfangs ggf. festhalten.", "url": ""}, - {"name": "RDL / Hip Hinge (KB/Rucksack)", "desc": "Knie leicht gebeugt, Hüfte nach hinten, Rücken gerade, Last entlang der Beine senken bis Dehnung hinten (Hamstrings).", "url": ""}, - {"name": "Glute Bridge / Wadenheben / Wall Sit", "desc": "Bridge: Hüfte hochdrücken (1-beinig steigern). Waden: Zehenspitzen hoch/runter. Wall Sit: Rücken an die Wand, halten.", "url": ""}, - ]}, - {"group": "KETTLEBELL (Power / Griff / Ganzkörper)", "color": "KBCOL", "items": [ - {"name": "Swing (zweiarmig, Hardstyle)", "desc": "Wichtigste KB-Übung. Hüfthinge, Glocke aus der Hüfte explosiv auf Brusthöhe schnappen (kein Armheben), oben Po+Bauch fest. Dead-Stop zum Lernen.", "url": "https://www.onepeloton.com/blog/kettlebell-swing/"}, - {"name": "Goblet Squat (KB)", "desc": "Glocke an den Hörnern vor der Brust umarmen, tief und aufrecht hocken. Beste Einsteiger-KB-Kniebeuge.", "url": "https://www.onepeloton.com/blog/goblet-squat/"}, - {"name": "KB-Deadlift (beidbeinig)", "desc": "Glocke zwischen den Füßen, Hüfte hinge, Rücken gerade, vom Boden heben und Hüfte durchstrecken. Stabile Swing-Vorbereitung.", "url": "https://www.onepeloton.com/blog/kettlebell-deadlift/"}, - {"name": "KB Floor Press", "desc": "Rücklings am Boden, Glocke vom Boden weg nach oben drücken. Schulterschonend, gut für Anfänger.", "url": "https://www.masterclass.com/articles/kettlebell-floor-press-guide"}, - {"name": "Halo", "desc": "Glocke (oder Rucksack) vor der Brust, langsam eng um den Kopf kreisen. Schulter-Mobility + Core.", "url": "https://www.tomsguide.com/how-to/kettlebell-halo-exercise-how-to-do-it-and-the-benefits-for-your-core-and-shoulder-muscles"}, - {"name": "Bottoms-up KB-Hold / Carry", "desc": "Glocke am Griff, Boden nach oben, Unterarm senkrecht, still halten. Schwierigkeit kommt aus der Balance – fordert Griff + Handgelenk auch mit leichter Glocke.", "url": "https://www.onepeloton.com/blog/farmers-carry/"}, - {"name": "Around-the-World + Uppercut", "desc": "Glocke kreist um den Körper (Around-the-World); danach diagonal nach oben zur gegenüberliegenden Schulter/Brust führen, die andere Hand fängt ab (wie ein Uppercut von unten). Rotatorischer Core + Koordination. Erst nur das Umkreisen lernen, Uppercut später.", "url": ""}, - ]}, - {"group": "CORE", "color": "ACC2", "items": [ - {"name": "Plank / Side Plank", "desc": "Unterarmstütz, Körper gerade. Anfangs auf den Knien. Side Plank seitlich auf einem Unterarm.", "url": ""}, - {"name": "Hollow Hold", "desc": "Auf dem Rücken, unterer Rücken am Boden, Arme+Beine leicht angehoben. Anfänger: angewinkelt (Tuck).", "url": "https://www.calixpert.com/exercises/hollow-body-to-superman-hold"}, - {"name": "Dead Bug", "desc": "Auf dem Rücken, Arme+Knie 90°, gegengleich Arm+Bein langsam ausstrecken, unterer Rücken bleibt am Boden.", "url": "https://dieringe.com/exercises/dead-bug"}, - {"name": "Bird Dog", "desc": "Vierfüßler, gegengleich Arm+Bein ausstrecken, Rumpf stabil, kurz halten.", "url": "https://www.hingehealth.com/resources/articles/bird-dog/"}, - {"name": "Mountain Climbers / Bicycle / Russian Twist / Beinheben", "desc": "Climbers: Knie zügig zur Brust. Bicycle: Ellbogen zum Gegenknie. Twist: sitzend drehen (KB). Beinheben: Knie gebeugt = leichter.", "url": ""}, - ]}, - {"group": "WARM-UP & MOBILITY", "color": "ACC2", "items": [ - {"name": "Inchworm", "desc": "Aus dem Stand vorbeugen, Hände nach vorne in den Stütz wandern, dann zurück.", "url": "https://www.onepeloton.com/blog/inchworm-exercise"}, - {"name": "Jacks / High Knees / Kreisen / Cat-Cow", "desc": "Klassische dynamische Aufwärm-Bewegungen – locker, zum Warmwerden.", "url": ""}, - ]}, - ], - "phases": { - "names": ["Phase 1 · Fundament", "Phase 2 · Aufbau", "Phase 3 · Steigerung", "Phase 4 · Festigen"], - "params": [ - "Fokus: Technik (v.a. Swing als Dead-Stop). 2–3 Reps Reserve. Pausen bis ~60s. Leichte Glocke. Woche 1 mit 2 Sätzen pro Rotation.", - "Sauber bleiben, letzter Satz fordernd. Pausen ~45–60s. Etwas schwerere Glocke / mehr Reps. 2–3 Sätze.", - "Fordernd, aber 1 Rep Reserve. Pausen ~45s. Schwerere Glocke / einarmig wo angegeben. 3 Sätze.", - "Fordernd & sauber (NICHT bis zum Muskelversagen). Pausen ~45s. Ende Woche 8 leichter (Deload). 3 Sätze.", - ], - "param_light": "Bewusst locker – aktive Erholung. Saubere, leichte Wiederholungen, keine Belastung bis zum Limit.", - }, - "stages": { - 'ls': ['Inkline (hoch)', 'Inkline tiefer / Knie', 'Knie / Negativ', 'Negativ + erste Volle'], - 'dips': ['Knie stark gebeugt, klein', 'Knie gebeugt, voll', 'Füße etwas weiter', 'Füße weiter weg'], - 'pike': ['Pike Hold', 'Hold + kleine Senkung', 'Push-up Hände erhöht', 'Push-up Boden, klein'], - 'lseng': ['Inkline (hoch)', 'Inkline mittel', 'Inkline tief', 'Knie-Liegestütze'], - 'load': ['leicht', 'etwas mehr', 'mehr', 'fordernd'], - 'squat': ['Körpergewicht', 'Körpergewicht, mehr Reps', 'Goblet leicht', 'Goblet mittel'], - 'tsquat': ['3s runter', '3s + Pause unten', 'Goblet 3s', 'Goblet 3s + Pause'], - 'lunge': ['Körpergewicht (ggf. festhalten)', 'Körpergewicht', 'mehr Reps', '+ Last leicht'], - 'bridge': ['beidbeinig', 'beidbeinig, 2s halten', 'einbeinig', 'einbeinig, 2s halten'], - 'wall': ['15–25 s', '25–40 s', '40–55 s', '55–70 s'], - 'calf': ['Körpergewicht', 'langsam', 'langsam, mehr Reps', '1-beinig'], - 'sup': ['Superman Hold', 'Superman Hold länger', 'Reverse Snow Angels', 'Y-T-W Raises'], - 'plank': ['auf Knien 15–20 s', 'voll 20–30 s', 'voll + Schulter-Taps', 'voll, länger'], - 'mtn': ['moderat', 'moderat', 'zügig', 'zügig'], - 'db': ['Arme ODER Beine einzeln', 'gegengleich', 'gegengleich + Pause', 'Beine weiter gestreckt'], - 'lr': ['Knie gebeugt', 'Knie gebeugt, langsam', 'halb gestreckt', 'gestreckt'], - 'holl': ['Tuck (Knie an)', 'Tuck, länger', 'halb gestreckt', 'halb/voll'], - 'sp': ['auf Knien', 'auf Knien, länger', 'voll', 'voll, länger'], - 'bic': ['langsam', 'moderat', 'moderat', 'zügig'], - 'tw': ['ohne Gewicht', 'ohne Gewicht', 'KB leicht', 'KB'], - 'swing': ['Dead-Stop, leicht (Technik)', 'Dead-Stop, mehr Reps', 'durchgehend, mittel', 'durchgehend, schwerer'], - 'gobletkb': ['leicht, Tiefe lernen', 'leicht, mehr Reps', 'mittel', 'mittel, 3s runter'], - 'press': ['leicht (Floor Press)', 'Floor Press mehr', 'Overhead leicht', 'Overhead mittel'], - 'halo': ['leicht, langsam', 'leicht', 'mittel', 'mittel, langsam'], - 'carry': ['im Stand 10–15s', '15–20s, ruhig', 'bis Schulterhöhe 15s', 'länger / ein paar Schritte'], - 'kbdl': ['leicht, Technik', 'leicht, mehr Reps', 'mittel', 'schwerer, kontrolliert'], - 'rowe': ['leicht', 'etwas mehr', 'mehr', 'fordernd'], - 'row1': ['leicht', 'etwas mehr', 'einarmig leicht', 'einarmig mittel'], - 'row2': ['leicht (Technik)', 'leicht, mehr Reps', 'mittel', 'mittel, Pause oben'], - 'lraise': ['leicht (Technik)', 'leicht, mehr Reps', 'mittel', 'mittel, langsam'], - 'atw': ['nur Around-the-World, leicht', 'Around-the-World, mehr Reps', '+ Uppercut, leicht', '+ Uppercut, flüssig'], - }, - "prepost": { - "1": {"warmup": "Handflächen-Wippen · Armkreisen/Wand-Slides · Hüftkreisen + Beinschwünge · Knie-über-Zeh an der Wand", - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Türrahmen (Brust/Schulter, je 30–40s) · Trizeps (je 30s) · Seitheben/Lat-Seitdehnung (je 30s) · Quad im Stand (je 30s) · Knieender Hüftbeuger (je 30–40s) · Wade an der Wand (je 30s)"}, - "2": {"warmup": "Handflächen-Wippen · Handtuch-Dislocates (8–10×) · Cat-Cow · Hüftkreisen + Beinschwünge", - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Lat-/Seitdehnung (je 30s) · Türrahmen (je 30–40s) · BWS über die Couch (30–40s) · Cobra/Sphinx (Bauch, 30s) · Unterarm (je 20s) · Knieender Hüftbeuger (je 30s)"}, - "3": {"warmup": "Hüftkreisen + Beinschwünge · Knie-über-Zeh an der Wand · Cat-Cow · Handflächen-Wippen", - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Hamstring-Dehnung (Couch, je 30–40s) · Quad im Stand (je 30s) · Knieender Hüftbeuger (je 30–40s) · Figure-4/Taube (je 30–40s) · Cobra + liegende Wirbelsäulen-Rotation (je 30s) · Wade an der Wand (je 30s)"}, - "4": {"warmup": "Handflächen-Wippen · Handtuch-Dislocates (8–10×) · Armkreisen/Wand-Slides · Cat-Cow", - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Türrahmen (je 30–40s) · Trizeps (je 30s) · Lat-/Seitdehnung (je 30s) · BWS über die Couch (30–40s) · Unterarm (je 20s)"}, - "5": {"warmup": "Cat-Cow · Hüftkreisen + Beinschwünge · Armkreisen/Wand-Slides · Knie-über-Zeh an der Wand", - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Türrahmen (je 30s) · Knieender Hüftbeuger (je 30s) · BWS über die Couch (30–40s) · Figure-4/Taube (je 30s) · Wade an der Wand (je 30s)"}, - "6": {"warmup": "Halo (leicht) · Hüftkreisen + Beinschwünge · Cat-Cow · Handflächen-Wippen", - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Hamstring-Dehnung (Couch, je 30–40s) · Knieender Hüftbeuger (je 30–40s) · Figure-4/Taube (je 30–40s) · Unterarm (je 20s) · BWS über die Couch (30–40s) · Lat-/Seitdehnung (je 30s)"}, - }, - "days": [ - {"num": 1, "focus": "Drücken + Beine", - "rotA": [{"name": "Liegestütze", "key": "ls", "cue": "Brust kontrolliert senken & drücken"}, - {"name": "Bank-Dips (Couch)", "key": "dips", "cue": "Po vor der Couch senken, hochdrücken"}, - {"name": "Pike", "key": "pike", "cue": "umgek. V, Kopf senken (Schultern)"}, - {"name": "Seitheben (KB/Rucksack)", "key": "lraise", "cue": "Arme seitlich bis Schulterhöhe, kontrolliert"}], - "rotB": [{"name": "Goblet Squat (KB/Rucksack)", "key": "gobletkb", "cue": "Last umarmen, tief"}, - {"name": "Ausfallschritt rückw.", "key": "lunge", "cue": "großer Schritt, Knie Richtung Boden"}, - {"name": "Glute Bridge", "key": "bridge", "cue": "Hüfte hochdrücken, Po fest"}, - {"name": "Wadenheben", "key": "calf", "cue": "auf Zehenspitzen hoch/runter"}]}, - {"num": 2, "focus": "Ziehen + Core", - "rotA": [{"name": "Einarm-Rudern ENG (KB)", "key": "rowe", "cue": "1 Arm, Ellbogen an den Rippen, Zug zur Hüfte · Lat · DG1 links / DG2 rechts"}, - {"name": "Bizeps-Curls (KB/Rucksack)", "key": "load", "cue": "Oberarme fix, zu den Schultern"}, - {"name": "Einarm-Rudern BREIT (KB)", "key": "row2", "cue": "1 Arm, Ellbogen ~45° auf, Zug zum Brustkorb · oberer Rücken · DG1 links / DG2 rechts"}, - {"name": "Superman / Y-T-W", "key": "sup", "cue": "bäuchlings Arme+Beine heben"}], - "rotB": [{"name": "Plank", "key": "plank", "cue": "gerade Linie halten"}, - {"name": "Mountain Climbers", "key": "mtn", "cue": "Knie zügig zur Brust"}, - {"name": "Dead Bug", "key": "db", "cue": "gegengleich, Rücken am Boden"}, - {"name": "Beinheben", "key": "lr", "cue": "Beine heben/senken"}]}, - {"num": 3, "focus": "Beine + Core", - "rotA": [{"name": "Goblet Squat (KB/Rucksack)", "key": "gobletkb", "cue": "Last umarmen, tief"}, - {"name": "RDL / Hip Hinge (KB/Rucksack)", "key": "load", "cue": "Hüfte zurück, Rücken gerade, Hamstrings"}, - {"name": "Glute Bridge 1-beinig", "key": "bridge", "cue": "ein Bein gestreckt"}, - {"name": "Wall Sit", "key": "wall", "cue": "Rücken an die Wand, halten"}], - "rotB": [{"name": "Hollow Hold", "key": "holl", "cue": "unterer Rücken am Boden"}, - {"name": "Side Plank (Wechsel)", "key": "sp", "cue": "seitlich auf einem Unterarm"}, - {"name": "Bicycle Crunch", "key": "bic", "cue": "Ellbogen zum Gegenknie"}, - {"name": "Russian Twist (KB)", "key": "tw", "cue": "sitzend Rumpf drehen"}]}, - {"num": 4, "focus": "Oberkörper: Drücken + Ziehen", - "rotA": [{"name": "Liegestütze", "key": "ls", "cue": "Brust kontrolliert senken & drücken"}, - {"name": "Bank-Dips (Couch)", "key": "dips", "cue": "Po vor der Couch senken"}, - {"name": "Pike", "key": "pike", "cue": "umgek. V, Kopf senken"}, - {"name": "KB Floor Press", "key": "press", "cue": "Glocke vom Boden weg drücken"}], - "rotB": [{"name": "Einarm-Rudern ENG (KB)", "key": "rowe", "cue": "1 Arm, Ellbogen an den Rippen, Zug zur Hüfte · Lat · DG1 links / DG2 rechts"}, - {"name": "Bizeps-Curls (KB/Rucksack)", "key": "load", "cue": "Oberarme fix, zu den Schultern"}, - {"name": "Einarm-Rudern BREIT (KB)", "key": "row2", "cue": "1 Arm, Ellbogen ~45° auf, Zug zum Brustkorb · oberer Rücken · DG1 links / DG2 rechts"}, - {"name": "Y-T-W / Superman", "key": "sup", "cue": "bäuchlings, Rücken"}]}, - {"num": 5, "focus": "Ganzkörper LEICHT + Mobility", - "rotA": [{"name": "Kniebeuge (leicht)", "key": "", "cue": "locker, sauber"}, - {"name": "Liegestütze (1 Stufe leichter)", "key": "", "cue": "leichtere Variante"}, - {"name": "Rudern (leicht)", "key": "", "cue": "wenig Gewicht"}, - {"name": "Mountain Climbers (locker)", "key": "", "cue": "ruhiges Tempo"}], - "rotB": [{"name": "Inchworm", "key": "", "cue": "in den Stütz wandern, zurück"}, - {"name": "Cat-Cow", "key": "", "cue": "Rücken ründen/strecken"}, - {"name": "Bird Dog", "key": "", "cue": "gegengleich Arm+Bein, kurz halten"}, - {"name": "KB-Halo (leicht)", "key": "", "cue": "langsam um den Kopf kreisen"}]}, - {"num": 6, "focus": "KETTLEBELL Ganzkörper", - "rotA": [{"name": "KB Swing (zweiarmig)", "key": "swing", "cue": "Hüft-Schnapp, kein Armheben"}, - {"name": "Goblet Squat (KB)", "key": "gobletkb", "cue": "Glocke vor der Brust, tief"}, - {"name": "KB-Deadlift (beidbeinig)", "key": "kbdl", "cue": "Glocke zwischen den Füßen, Hüfte hinge"}, - {"name": "Halo", "key": "halo", "cue": "eng um den Kopf kreisen"}], - "rotB": [{"name": "KB Floor Press", "key": "press", "cue": "Glocke vom Boden weg drücken"}, - {"name": "Einarmiges Rudern (KB)", "key": "row1", "cue": "Rücken gerade, zum Bauch"}, - {"name": "Bottoms-up KB-Hold (Griff)", "key": "carry", "cue": "Glocke Boden nach oben, Unterarm senkrecht, still halten"}, - {"name": "Around-the-World + Uppercut (KB)", "key": "atw", "cue": "Glocke um den Körper, dann diagonal zur Gegen-Schulter, andere Hand fängt"}]}, - ], -} - - -KB_NOTE = ("Reine Kettlebell-Session. Swing = Hüft-Schnapp, kein Armheben. " - "Glocke so wählen, dass alle Reps sauber sind.") - - -def _lighten(hexstr, w=0.90): - try: - c = colors.HexColor(hexstr) - except Exception: - c = colors.HexColor("#0F766E") - return colors.Color(c.red * (1 - w) + w, c.green * (1 - w) + w, c.blue * (1 - w) + w) - - -def migrate_config(cfg): - """Führt alte Trainings-Configs in das neue, flexible Format über (idempotent). - Neu: phases.items[{name,params,weeks}], stages{key:{name,steps[]}}, - days[{...,rotations:[{label,exercises[]}]}] statt rotA/rotB, - Tag-Farbe/Notiz/Badge statt fest verdrahtetem KB-Tag.""" - cfg = copy.deepcopy(cfg or {}) - meta = cfg.setdefault("meta", {}) - days = cfg.get("days", []) or [] - - # phases: {names,params} -> {items:[{name,params,weeks}], param_light} - ph = cfg.get("phases", {}) or {} - if "items" not in ph: - names = ph.get("names", []) or [] - params = ph.get("params", []) or [] - n = max(len(names), len(params)) - items = [{"name": names[i] if i < len(names) else ("Phase %d" % (i + 1)), - "params": params[i] if i < len(params) else "", - "weeks": 2} for i in range(n)] - cfg["phases"] = {"items": items, "param_light": ph.get("param_light", "")} - - # Übungsnamen je Key sammeln (für Stufen-Namen) - name_by_key = {} - for d in days: - groups = [d.get("rotA", []) or [], d.get("rotB", []) or []] - for rot in d.get("rotations", []) or []: - groups.append(rot.get("exercises", []) or []) - for grp in groups: - for it in grp: - k = it.get("key") - if k and it.get("name") and k not in name_by_key: - name_by_key[k] = it["name"] - - # stages: [..] -> {name, steps:[..], result_schemas:[..]} - # ``result_schemas`` liegt parallel zu ``steps``. Ein Eintrag kann ``None`` - # sein und bedeutet dann: Übungsstandard bzw. Tracker-Fallback verwenden. - newstages = {} - for key, val in (cfg.get("stages", {}) or {}).items(): - if isinstance(val, dict) and "steps" in val: - stage = copy.deepcopy(val) - steps = stage.get("steps") if isinstance(stage.get("steps"), list) else [] - schemas = stage.get("result_schemas") if isinstance(stage.get("result_schemas"), list) else [] - schemas = list(schemas[:len(steps)]) - while len(schemas) < len(steps): - schemas.append(None) - stage["steps"] = steps - stage["result_schemas"] = schemas - newstages[key] = stage - else: - steps = val if isinstance(val, list) else [] - nm = name_by_key.get(key) or next((s for s in steps if s), "") or key - newstages[key] = {"name": nm, "steps": steps, "result_schemas": [None] * len(steps)} - cfg["stages"] = newstages - - # Explizites Trainingsformat. Alte Pläne bleiben auf ``auto`` und werden - # vom Tracker weiterhin aus den Plantexten erkannt. Neue Pläne können - # Tabata/Intervalle damit ohne Text-Heuristik eindeutig beschreiben. - fmt = cfg.get("training_format") - if not isinstance(fmt, dict): - fmt = {} - mode = str(fmt.get("mode") or "auto") - if mode not in {"auto", "sets_reps", "tabata", "fixed_interval"}: - mode = "auto" - normalized_fmt = {"mode": mode} - for field in ("work_seconds", "rest_seconds", "rounds"): - try: - value = int(fmt.get(field)) - except (TypeError, ValueError): - value = 0 - normalized_fmt[field] = value if value > 0 else None - normalized_fmt["fixed_interval"] = bool( - fmt.get("fixed_interval") or mode in {"tabata", "fixed_interval"} - ) - normalized_fmt["rounds_scope"] = "block" - cfg["training_format"] = normalized_fmt - - # days: rotA/rotB -> rotations (+ KB-Styling & light-Tag nur bei Alt-Konvertierung) - for d in days: - if "rotations" not in d: - rots = [] - if d.get("rotA"): - rots.append({"label": "ROTATION A — 3 Sätze", "exercises": d.get("rotA")}) - if d.get("rotB"): - rots.append({"label": "ROTATION B — 3 Sätze", "exercises": d.get("rotB")}) - d["rotations"] = rots - d.pop("rotA", None) - d.pop("rotB", None) - if d.get("num") == 6: - d.setdefault("color", "#6D28D9") - d.setdefault("note", KB_NOTE) - d.setdefault("note_bg", "#F1ECFB") - if d.get("num") == 5: - d.setdefault("light", True) - cfg["days"] = days - meta.setdefault("days", len(days)) - return migrate_training_config(cfg) - - -def default_config(): - return migrate_config(copy.deepcopy(DEFAULTS)) - - -# ========================================================================= -# RENDERER -# ========================================================================= -def build_pdf(cfg): - """cfg: Trainings-Config (altes oder neues Format). Gibt PDF-Bytes zurück.""" - _D = DEFAULTS - cfg = dict(cfg or {}) - meta = dict(_D["meta"]); meta.update(cfg.get("meta") or {}) - cols = dict(_D["colors"]); cols.update(cfg.get("colors") or {}) - cfg["meta"], cfg["colors"] = meta, cols - for k in ("front", "library", "phases", "stages", "prepost", "days"): - if not cfg.get(k): - cfg[k] = copy.deepcopy(_D[k]) - cfg = migrate_config(cfg) # auf neues Modell normalisieren - - C = {k: colors.HexColor(v) for k, v in cfg["colors"].items()} - INK, ACCENT, ACC2, KBCOL = C["INK"], C["ACCENT"], C["ACC2"], C["KBCOL"] - LINK, LIGHT, GRID, MUTE = C["LINK"], C["LIGHT"], C["GRID"], C["MUTE"] - DARK, WARMBG, GOALBG, KBBG = C["DARK"], C["WARMBG"], C["GOALBG"], C["KBBG"] - link_hex = cfg["colors"]["LINK"] - - styles = getSampleStyleSheet() - - def Sty(name, **kw): - base = kw.pop("parent", styles["Normal"]) - return ParagraphStyle(name, parent=base, **kw) - - title_s = Sty("title", fontName="Helvetica-Bold", fontSize=23, textColor=INK, leading=26, spaceAfter=2) - sub_s = Sty("sub", fontName="Helvetica", fontSize=10.5, textColor=MUTE, leading=14, spaceAfter=2) - h2_s = Sty("h2", fontName="Helvetica-Bold", fontSize=14, textColor=ACCENT, leading=17, spaceBefore=13, spaceAfter=5) - body_s = Sty("body", fontName="Helvetica", fontSize=9.6, textColor=INK, leading=13.5, spaceAfter=4) - smallm_s = Sty("smallm", fontName="Helvetica", fontSize=8.2, textColor=MUTE, leading=10.5) - cell_s = Sty("cell", fontName="Helvetica", fontSize=8.3, textColor=INK, leading=10.6) - cellb_s = Sty("cellb", fontName="Helvetica-Bold", fontSize=8.3, textColor=INK, leading=10.6) - link_s = Sty("link", fontName="Helvetica", fontSize=8.2, textColor=LINK, leading=10.6) - th_s = Sty("th", fontName="Helvetica-Bold", fontSize=8.6, textColor=colors.white, leading=11) - tag_s = Sty("tag", fontName="Helvetica-Bold", fontSize=8.4, textColor=colors.white, leading=11) - note_s = Sty("note", fontName="Helvetica", fontSize=9, textColor=INK, leading=13) - goal_s = Sty("goal", fontName="Helvetica", fontSize=8.9, textColor=INK, leading=12.5) - kb_s = Sty("kb", fontName="Helvetica", fontSize=8.9, textColor=INK, leading=12.5) - bandL = Sty("bandL", fontName="Helvetica-Bold", fontSize=17, textColor=colors.white, leading=19) - bandR = Sty("bandR", fontName="Helvetica-Bold", fontSize=11.5, textColor=colors.white, leading=14, alignment=TA_RIGHT) - howto = Sty("howto", fontName="Helvetica", fontSize=8.6, textColor=MUTE, leading=11.5, spaceBefore=4, spaceAfter=2) - warm = Sty("warm", fontName="Helvetica", fontSize=9, textColor=INK, leading=12.5) - roth = Sty("roth", fontName="Helvetica-Bold", fontSize=11, textColor=colors.white, leading=13) - exn = Sty("exn", fontName="Helvetica-Bold", fontSize=9.6, textColor=INK, leading=11.5) - stage_s = Sty("stage", fontName="Helvetica-Bold", fontSize=9, textColor=ACCENT, leading=11) - param_s = Sty("param", fontName="Helvetica", fontSize=8.8, textColor=INK, leading=12) - logst = Sty("logst", fontName="Helvetica", fontSize=9.5, textColor=INK, leading=18) - - USABLE = A4[0] - 2 * 1.3 * cm - - def link(url, label="Anleitung (Bilder/Video)"): - return Paragraph('%s' % (url, link_hex, label), link_s) - - front = cfg["front"] - story = [] - - # ===================== FRONT MATTER ===================== - story.append(Paragraph(cfg["meta"]["title"], title_s)) - story.append(Paragraph(cfg["meta"]["subtitle"], sub_s)) - story.append(HRFlowable(width="100%", thickness=2, color=ACCENT, spaceBefore=6, spaceAfter=6)) - - story.append(Paragraph(front["session_how_head"], h2_s)) - story.append(Paragraph(front["session_how_body"], body_s)) - - flow = front["flow_rows"] - fr = [[Paragraph(c, th_s if i == 0 else (cellb_s if (j == 1 and r[0] == '') else cell_s)) for j, c in enumerate(r)] for i, r in enumerate(flow)] - t = Table(fr, colWidths=[3.2 * cm, 9.6 * cm, 2.7 * cm]) - t.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, 0), DARK), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, LIGHT]), - ("GRID", (0, 0), (-1, -1), 0.5, GRID), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4), ("LEFTPADDING", (0, 0), (-1, -1), 6)])) - story.append(t) - story.append(Spacer(1, 4)) - story.append(Paragraph(front["timer_note"], smallm_s)) - - # KB evidence box - story.append(Paragraph(front["kb_head"], h2_s)) - kb_rows = [[Paragraph(front["kb_box"], kb_s)]] - gt = Table(kb_rows, colWidths=[USABLE]) - gt.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), KBBG), ("BOX", (0, 0), (-1, -1), 0.8, KBCOL), - ("TOPPADDING", (0, 0), (-1, -1), 8), ("BOTTOMPADDING", (0, 0), (-1, -1), 8), - ("LEFTPADDING", (0, 0), (-1, -1), 10), ("RIGHTPADDING", (0, 0), (-1, -1), 10)])) - story.append(gt) - - story.append(Paragraph(front["goals_head"], h2_s)) - goal_rows = [[Paragraph(front["goals_box"], goal_s)]] - gt2 = Table(goal_rows, colWidths=[USABLE]) - gt2.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), GOALBG), ("BOX", (0, 0), (-1, -1), 0.8, ACCENT), - ("TOPPADDING", (0, 0), (-1, -1), 8), ("BOTTOMPADDING", (0, 0), (-1, -1), 8), - ("LEFTPADDING", (0, 0), (-1, -1), 10), ("RIGHTPADDING", (0, 0), (-1, -1), 10)])) - story.append(gt2) - story.append(Spacer(1, 5)) - story.append(Paragraph(front["goals_note"], note_s)) - story.append(PageBreak()) - - # ---- exercise library ---- - story.append(Paragraph(front["lib_head"], h2_s)) - story.append(Paragraph(front["lib_intro"], smallm_s)) - - for grp in cfg["library"]: - gcol = C.get(grp["color"], ACC2) - rows = [[Paragraph(grp["group"], tag_s), "", ""], - [Paragraph("Übung", th_s), Paragraph("Ausführung (Kurz)", th_s), Paragraph("Anleitung", th_s)]] - for it in grp["items"]: - rows.append([Paragraph(it["name"], cellb_s), Paragraph(it["desc"], cell_s), - link(it["url"]) if it.get("url") else Paragraph("Standard-Bewegung", smallm_s)]) - tstyle = [("SPAN", (0, 0), (-1, 0)), ("BACKGROUND", (0, 0), (-1, 0), gcol), ("BACKGROUND", (0, 1), (-1, 1), DARK), - ("ROWBACKGROUNDS", (0, 2), (-1, -1), [colors.white, LIGHT]), ("GRID", (0, 1), (-1, -1), 0.5, GRID), - ("VALIGN", (0, 0), (-1, -1), "TOP"), ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4), - ("LEFTPADDING", (0, 0), (-1, -1), 6), ("RIGHTPADDING", (0, 0), (-1, -1), 6)] - story.append(KeepTogether(Table(rows, colWidths=[4.0 * cm, 8.9 * cm, 3.5 * cm], style=TableStyle(tstyle)))) - story.append(Spacer(1, 6)) - story.append(PageBreak()) - - # ---- baseline + reminders ---- - story.append(Paragraph(front["baseline_head"], h2_s)) - story.append(Paragraph(front["baseline_intro"], body_s)) - track = front["baseline_rows"] - tr = [[Paragraph(c, th_s if i == 0 else cell_s) for c in r] for i, r in enumerate(track)] - tt = Table(tr, colWidths=[9.5 * cm, 3.0 * cm, 3.0 * cm]) - tt.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, 0), DARK), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, LIGHT]), - ("GRID", (0, 0), (-1, -1), 0.5, GRID), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5), ("LEFTPADDING", (0, 0), (-1, -1), 6)])) - story.append(tt) - story.append(Spacer(1, 8)) - story.append(Paragraph(front["reminder_head"], h2_s)) - for r in front["reminders"]: - story.append(Paragraph("•  " + r, body_s)) - story.append(Spacer(1, 6)) - story.append(HRFlowable(width="100%", thickness=1, color=GRID)) - story.append(Paragraph(front["sheets_note"], smallm_s)) - story.append(PageBreak()) - - # ===================== SESSION SHEETS ===================== - stages = cfg["stages"] - phase_items = cfg["phases"]["items"] - PARAM_LIGHT = cfg["phases"].get("param_light", "") - prepost = cfg["prepost"] - - def phase_index(week): - acc = 0 - for i, ph in enumerate(phase_items): - try: - acc += int(ph.get("weeks", 2) or 0) - except Exception: - acc += 2 - if week <= acc: - return i - return max(0, len(phase_items) - 1) - - def phase_name(pi): - return phase_items[pi]["name"] if 0 <= pi < len(phase_items) else "" - - def phase_params(pi): - return phase_items[pi].get("params", "") if 0 <= pi < len(phase_items) else "" - - def stage_text(key, pi): - st = stages.get(key) if key else None - if st: - steps = st.get("steps", []) if isinstance(st, dict) else st - if 0 <= pi < len(steps) and steps[pi]: - step = steps[pi] - return step.get("name", "") if isinstance(step, dict) else step - return "leicht" - - def rotation_table(rot, pi): - rows = [[Paragraph("Übung", th_s), Paragraph("Stufe heute", th_s), - Paragraph("Satz 1", th_s), Paragraph("Satz 2", th_s), Paragraph("Satz 3", th_s)]] - for item in rot: - name, key, cue = item.get("name", ""), item.get("key") or None, item.get("cue", "") - ex = Paragraph("%s
%s" % (name, cue), exn) - sc = Paragraph(stage_text(key, pi), stage_s) - rows.append([ex, sc, "", "", ""]) - tb = Table(rows, colWidths=[7.2 * cm, 3.5 * cm, 1.95 * cm, 1.95 * cm, 1.95 * cm]) - tb.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, 0), DARK), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, LIGHT]), - ("GRID", (0, 0), (-1, -1), 0.6, GRID), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("TOPPADDING", (0, 1), (-1, -1), 8), ("BOTTOMPADDING", (0, 1), (-1, -1), 8), - ("TOPPADDING", (0, 0), (-1, 0), 4), ("BOTTOMPADDING", (0, 0), (-1, 0), 4), - ("LEFTPADDING", (0, 0), (-1, -1), 6), ("RIGHTPADDING", (0, 0), (-1, -1), 6), ("ALIGN", (2, 0), (-1, -1), "CENTER")])) - return tb - - def day_has_exercises(day): - return any((rot.get("exercises") or []) for rot in (day.get("rotations") or [])) - - def session_page(week, day): - num, focus = day.get("num"), day.get("focus", "") - pi = phase_index(week) - color = day.get("color") - band_col = colors.HexColor(color) if color else ACCENT - rot_col = colors.HexColor(color) if color else ACC2 - pp = prepost.get(str(num), {"warmup": "", "cooldown": "", "stretch": ""}) - pre_txt = pp.get("warmup", ""); cd_txt = pp.get("cooldown", ""); stretch_txt = pp.get("stretch", "") - flow = [] - badge = day.get("badge") or {} - btext = badge.get("text", "").strip() - left_html = "WOCHE %d · TAG %d" % (week, num) - if btext: - left_html += " %s " % (badge.get("color", "#6D28D9"), btext) - band = Table([[Paragraph(left_html, bandL), - Paragraph("%s
%s" % (focus, phase_name(pi)), bandR)]], - colWidths=[USABLE * 0.5, USABLE * 0.5]) - band.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), band_col), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("TOPPADDING", (0, 0), (-1, -1), 9), ("BOTTOMPADDING", (0, 0), (-1, -1), 9), - ("LEFTPADDING", (0, 0), (0, 0), 12), ("RIGHTPADDING", (-1, 0), (-1, 0), 12)])) - flow.append(band) - flow.append(Paragraph("Tabata: 20s an / 10s aus · pro Block 4 Übungen × 2 Durchgänge = 1 Satz · 10s Vorlauf", howto)) - wb = Table([[Paragraph("WARM-UP · Gelenk-Prep (dynamisch, ~3 Min, kein Halten): " + pre_txt, warm)]], colWidths=[USABLE]) - wb.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), WARMBG), ("BOX", (0, 0), (-1, -1), 0.6, ACC2), - ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9)])) - flow.append(Spacer(1, 5)) - flow.append(wb) - note = (day.get("note") or "").strip() - if note: - nb = colors.HexColor(day["note_bg"]) if day.get("note_bg") else _lighten(color or "#0F766E") - nborder = colors.HexColor(color) if color else ACC2 - flow.append(Spacer(1, 4)) - nt = Table([[Paragraph(note, warm)]], colWidths=[USABLE]) - nt.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), nb), ("BOX", (0, 0), (-1, -1), 0.6, nborder), - ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9)])) - flow.append(nt) - flow.append(Spacer(1, 8)) - for rot in (day.get("rotations") or []): - exs = rot.get("exercises") or [] - if not exs: - continue - rh = Table([[Paragraph(rot.get("label", ""), roth)]], colWidths=[USABLE]) - rh.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), rot_col), ("TOPPADDING", (0, 0), (-1, -1), 4), - ("BOTTOMPADDING", (0, 0), (-1, -1), 4), ("LEFTPADDING", (0, 0), (-1, -1), 8)])) - flow.append(rh) - flow.append(Spacer(1, 2)) - flow.append(rotation_table(exs, pi)) - flow.append(Spacer(1, 8)) - pbp = day.get("params_by_phase") or [] - if 0 <= pi < len(pbp) and (pbp[pi] or "").strip(): - ptxt = pbp[pi] - else: - ptxt = PARAM_LIGHT if day.get("light") else phase_params(pi) - pb = Table([[Paragraph(ptxt, param_s)]], colWidths=[USABLE]) - pb.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), LIGHT), ("BOX", (0, 0), (-1, -1), 0.6, GRID), - ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), ("LEFTPADDING", (0, 0), (-1, -1), 9)])) - flow.append(pb) - flow.append(Spacer(1, 7)) - cd = Table([[Paragraph("COOL-DOWN (~1–2 Min): " + cd_txt + - "
DEHNEN (warm & statisch, bis mildes Ziehen, ~4–6 Min): " + stretch_txt, warm)]], colWidths=[USABLE]) - cd.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), WARMBG), ("BOX", (0, 0), (-1, -1), 0.6, ACC2), - ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9)])) - flow.append(cd) - flow.append(Spacer(1, 7)) - flow.append(Paragraph("Gefühl heute (1–10): ______ Glocke (kg): ______ Notizen: ________________________________", logst)) - return flow - - weeks = int(cfg["meta"].get("weeks", 8) or 0) - for week in range(1, weeks + 1): - for day in cfg["days"]: - if not day_has_exercises(day): - continue - story += session_page(week, day) - story.append(PageBreak()) - if story and isinstance(story[-1], PageBreak): - story.pop() - - footer_left = cfg["meta"]["footer_left"] - - def footer(canvas, doc): - canvas.saveState() - canvas.setFont("Helvetica", 7.5) - canvas.setFillColor(MUTE) - canvas.drawString(1.3 * cm, 1.0 * cm, footer_left) - canvas.drawRightString(A4[0] - 1.3 * cm, 1.0 * cm, "Seite %d" % doc.page) - canvas.restoreState() - - buf = BytesIO() - doc = SimpleDocTemplate(buf, pagesize=A4, leftMargin=1.3 * cm, rightMargin=1.3 * cm, - topMargin=1.2 * cm, bottomMargin=1.4 * cm, title=cfg["meta"]["doc_title"]) - doc.build(story, onFirstPage=footer, onLaterPages=footer) - return buf.getvalue() - - -if __name__ == "__main__": - data = build_pdf(default_config()) - with open("/tmp/test_plan.pdf", "wb") as f: - f.write(data) - print("OK", len(data), "bytes") diff --git a/plugins/trainingsplan/plugin.json b/plugins/trainingsplan/plugin.json index df7252b..b23f38d 100644 --- a/plugins/trainingsplan/plugin.json +++ b/plugins/trainingsplan/plugin.json @@ -1,30 +1,24 @@ { "id": "trainingsplan", - "name": "Trainingsplan- & Rezept-Editor", - "summary": "Versionierte Trainingspläne mit stabilen IDs, Validierung und Tracker-Vorschau bearbeiten", - "description": "Editor mit Entwurf und Veröffentlichung, stabilem Planvertrag, progressionsabhängigen Ergebnisschemata, planbezogener Übungsbibliothek, Konfliktschutz und strukturierten Vorschlägen aus dem Session-Tracker.", + "name": "Trainingsplan-Backend", + "summary": "Schlankes Backend zum Anlegen von Trainingsplänen, Trainingstagen und Übungen", + "description": "Eintragebackend für Trainingspläne mit zentraler Übungsbibliothek, Trainingstagen, Warm-up, Cool-down und Tagesübungen.", "icon": "🏋", "category": "Planung", - "version": "2.0.0", + "version": "3.0.0", "entrypoint": "backend:create_app", "order": 10, "requires": [], "contract": { - "plan_schema": 3, - "training_contract": 2, - "result_schema": 2 + "plan_schema": 4, + "training_contract": 3 }, "features": [ - "Entwurf und veröffentlichte Planrevision sind getrennt", - "Stabile IDs für Phasen, Tage, Rotationen, Übungen und Progressionsstufen", - "Progressionsstufen als Objekte mit Ergebnisschema, Cluster und Faktor", - "Planbezogene Übungsbibliothek für den Tracker", - "Validierung von IDs, Schemata und festen Intervallen", - "Tracker-Vorschau inklusive Tabata-Durchgängen", - "Atomisches Speichern mit Backups und Revisionskonfliktschutz", - "Strukturiertes Prüfpostfach für KI-Planvorschläge", - "Import, Export und PDF-Erzeugung", - "Rezeptsammlungen bleiben unterstützt" + "Pläne anlegen, umbenennen, auswählen und löschen", + "Übungen in einer zentralen Bibliothek verwalten", + "Trainingstage mit Warm-up und Cool-down verwalten", + "Übungen an Trainingstagen hinterlegen", + "Atomisches Speichern mit Backups und Revisionskonfliktschutz" ], "docs": "TOOL-README.md" } diff --git a/plugins/trainingsplan/presets/couchsnacks.json b/plugins/trainingsplan/presets/couchsnacks.json deleted file mode 100644 index e6eeb48..0000000 --- a/plugins/trainingsplan/presets/couchsnacks.json +++ /dev/null @@ -1,1607 +0,0 @@ -{ - "name": "Couch-Snacks (Abend)", - "config": { - "type": "recipe", - "meta": { - "title": "Couch-Snacks für den Abend", - "header_left": "COUCH-SNACKS · Abend-Edition", - "header_right": "Begleitend zum 8-Wochen-Plan", - "footer_left": "Teller = Portionsgrenze · Tüten in den Schrank · Trend über die Woche zählt", - "output_name": "Couch-Snacks_Abend-Edition.pdf", - "doc_title": "Couch-Snacks – Abend-Edition (100 Rezepte)", - "brand_color": "#6D28D9", - "tip_label": "Abend-Tipp", - "steps_label": "SO GEHT'S" - }, - "intro": { - "headline": "Couch-Snacks für den Abend", - "subtitle": "100 Rezepte für vor dem Fernseher & vorm Schlafengehen – inkl. Airfryer-Rezepten und Orientierungs-Nährwerten, viele mit Würz- und Geschmacks-Variationen. Nicht verzichten, sondern clever ersetzen.", - "paragraphs": [ - "Diese Snacks lösen dein Couch-Problem: Sie befriedigen die Lust auf Süßes oder Knabbern, machen aber satt statt nur den Mund zu beschäftigen. Der Hebel ist nicht „gesund oder ungesund“, sondern die Portion und der Teller. Eiweiß (Quark, Skyr, Hüttenkäse, Eier, Edamame) hält bis zum Schlaf satt. Wo ein Rezept einen VARIATIONEN-Block hat, ist das dieselbe Basis in mehreren Geschmacksrichtungen. Rezepte mit AIRFRYER-Badge funktionieren im Heißluftfritteuse. Der NÄHRWERTE-Streifen liefert Orientierungswerte (ca.-Werte für die angegebene Menge) – keine Zähl-Pflicht." - ], - "rule_head": "Die eine Grundregel", - "rule_text": "Teller fertig machen → Tüten und Beutel zu → ab in den Schrank. Der Teller ist deine Portionsgrenze – nicht dein Sättigungsgefühl.", - "categories_head": "13 Kategorien" - }, - "categories": [ - { - "name": "Cremig & süß · Löffelsnacks", - "color": "#7c3aed" - }, - { - "name": "Aus dem Tiefkühler · Eis- & Süßigkeiten-Ersatz", - "color": "#0ea5e9" - }, - { - "name": "Knusprig & herzhaft · Chip-Ersatz", - "color": "#ea580c" - }, - { - "name": "Frisch & zum Knabbern", - "color": "#16a34a" - }, - { - "name": "Herzhaft & sättigend · Mini-Mahlzeiten", - "color": "#dc2626" - }, - { - "name": "Süße Häppchen & Energy Bites", - "color": "#db2777" - }, - { - "name": "Warm & gemütlich · vorm Schlafen", - "color": "#b45309" - }, - { - "name": "Airfryer · Knusprig & herzhaft", - "color": "#4338ca" - }, - { - "name": "Airfryer · Süß & Gebäck", - "color": "#2563eb" - }, - { - "name": "Mehr herzhafte Mini-Mahlzeiten", - "color": "#475569" - }, - { - "name": "Mehr süße Löffel- & Obst-Snacks", - "color": "#a21caf" - }, - { - "name": "Mehr Bowls & Frisches", - "color": "#059669" - }, - { - "name": "Mehr warme Snacks & Getränke", - "color": "#92400e" - } - ], - "recipes": [ - { - "num": "01", - "category": "Cremig & süß · Löffelsnacks", - "title": "Schoko-Quark-Mousse", - "tagline": "Schoko-Pudding-Gefühl, das wirklich satt macht.", - "nutrition": "ca. ≈ 190 kcal · Eiweiß 25 g · Fett 4 g · KH 15 g · Zucker 13 g · Ballaststoffe 2 g", - "ingredients": "200 g Magerquark · 1 TL Backkakao · etwas Süße · optional Schuss Milch", - "steps": "Quark mit Kakao und Süße glattrühren, bei Bedarf Milch unter, bis es cremig-dunkel ist.", - "methods": "", - "variations": [ - "Erdnuss-Schoko: + 1 TL Erdnussmus", - "Vanille-Beere: Vanille statt Kakao + TK-Beeren", - "Mokka: + Schuss kalter Espresso", - "Zimt: Zimt statt Kakao" - ], - "tip": "Abend-Tipp Eiweiß-reich, Schoko-Kick ohne ganze Tafel.", - "badge": "", - "yield": "" - }, - { - "num": "02", - "category": "Cremig & süß · Löffelsnacks", - "title": "Skyr-Beeren-Bowl mit Crunch", - "tagline": "Dessert-Optik, Knusper inklusive.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 20 g · Fett 1 g · KH 17 g · Zucker 11 g · Ballaststoffe 2 g", - "ingredients": "150–200 g Skyr · Handvoll (TK-)Beeren · 1 zerbröselte Mais-/Linsenwaffel · Prise Zimt", - "steps": "Skyr in die Schale, Beeren druntermischen, Waffelbrösel und Zimt obendrauf.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Die Waffel liefert den Crunch ohne Müsli-Zuckerladung.", - "badge": "", - "yield": "" - }, - { - "num": "03", - "category": "Cremig & süß · Löffelsnacks", - "title": "Protein-Tiramisu im Glas", - "tagline": "Schmeckt nach Dessert, ist Eiweiß-Quark.", - "nutrition": "ca. ≈ 205 kcal · Eiweiß 26 g · Fett 3 g · KH 19 g · Zucker 14 g · Ballaststoffe 1 g", - "ingredients": "200 g Magerquark/Skyr · Schuss Milch · etwas Süße · kalter Espresso · 1 Vollkornkeks · Kakao", - "steps": "Quark cremig rühren. Keks in Espresso tunken, ins Glas, Quark drauf, mit Kakao bestäuben.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Tiramisu-Gefühl ohne Mascarpone-Sahne-Bombe.", - "badge": "", - "yield": "" - }, - { - "num": "04", - "category": "Cremig & süß · Löffelsnacks", - "title": "Chia-Pudding", - "tagline": "Abends vorbereiten, am Couch-Abend fertig.", - "nutrition": "ca. ≈ 255 kcal · Eiweiß 14 g · Fett 13 g · KH 16 g · Zucker 10 g · Ballaststoffe 10 g", - "ingredients": "3 EL Chiasamen · 200 ml Milch/Pflanzendrink · etwas Süße", - "steps": "Verrühren, 10 Min stehen, nochmal rühren, min. 1 Std (oder über Nacht) kühlen.", - "methods": "", - "variations": [ - "Schoko: + 1 TL Kakao", - "Beere: TK-Beeren untermischen", - "Vanille-Kokos: Vanille + Schuss Kokosmilch", - "Erdnuss: + 1 TL Erdnussmus" - ], - "tip": "Abend-Tipp Quillt im Magen und macht erstaunlich satt.", - "badge": "", - "yield": "" - }, - { - "num": "05", - "category": "Cremig & süß · Löffelsnacks", - "title": "Apfel-Zimt-Quark", - "tagline": "„Apfelkuchen-Gefühl“ ohne Teig.", - "nutrition": "ca. ≈ 240 kcal · Eiweiß 26 g · Fett 7 g · KH 31 g · Zucker 26 g · Ballaststoffe 5 g", - "ingredients": "200 g Magerquark · 1 geraspelter Apfel · Zimt · optional 1 TL Honig, paar Walnüsse", - "steps": "Apfel raspeln, mit Quark, Zimt und Süße verrühren, Nüsse drauf.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Süß und sättigend – der Apfel bringt Volumen statt Zucker.", - "badge": "", - "yield": "" - }, - { - "num": "06", - "category": "Cremig & süß · Löffelsnacks", - "title": "Griech. Joghurt mit Honig & Walnuss", - "tagline": "Cremig + Crunch + süß in 30 Sekunden.", - "nutrition": "ca. ≈ 285 kcal · Eiweiß 10 g · Fett 22 g · KH 12 g · Zucker 11 g · Ballaststoffe 1 g", - "ingredients": "150 g griech. Joghurt oder Skyr · 1 TL Honig · paar Walnüsse · Zimt", - "steps": "Joghurt in die Schale, Honig, Walnüsse und Zimt darüber.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Honig sparsam – ein Teelöffel reicht für den süßen Ton.", - "badge": "", - "yield": "" - }, - { - "num": "07", - "category": "Cremig & süß · Löffelsnacks", - "title": "Mango-Quark-Bowl", - "tagline": "Tropisch-cremig, ohne Zuckerzusatz.", - "nutrition": "ca. ≈ 170 kcal · Eiweiß 24 g · Fett 1 g · KH 16 g · Zucker 15 g · Ballaststoffe 1 g", - "ingredients": "200 g Magerquark/Skyr · paar (TK-)Mangostücke · Spritzer Limette", - "steps": "Mango antauen, pürieren oder klein, unter den Quark, Limette drüber.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Die Limette bremst die Süße-Gier und frischt auf.", - "badge": "", - "yield": "" - }, - { - "num": "08", - "category": "Cremig & süß · Löffelsnacks", - "title": "Beeren-„Eton-Mess“ light", - "tagline": "Sieht aufwändig aus, ist in 2 Min fertig.", - "nutrition": "ca. ≈ 145 kcal · Eiweiß 18 g · Fett 1 g · KH 16 g · Zucker 10 g · Ballaststoffe 1 g", - "ingredients": "150 g Skyr · Handvoll Beeren · 1 zerbröselte Reiswaffel (oder Baiser-Splitter)", - "steps": "Beeren leicht zerdrücken, locker (nicht ganz) unter den Skyr, Knusper drüber.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Knusper + Beere + Eiweiß als schnelles Dessert.", - "badge": "", - "yield": "" - }, - { - "num": "09", - "category": "Aus dem Tiefkühler · Eis- & Süßigkeiten-Ersatz", - "title": "Frozen Yogurt Bark", - "tagline": "Selbstgemachte Joghurt-Schokolade zum Brechen.", - "nutrition": "ca. ≈ 235 kcal · Eiweiß 28 g · Fett 4 g · KH 19 g · Zucker 15 g · Ballaststoffe 3 g · ganzer Ansatz", - "ingredients": "250 g Skyr/Joghurt · etwas Süße · Beeren · optional Kakao-Nibs/Nüsse", - "steps": "Joghurt auf Backpapier dünn streichen, Toppings reindrücken, 2–3 Std einfrieren, in Stücke brechen.", - "methods": "", - "variations": [ - "Beere pur", - "Schoko-Nuss: Kakao-Nibs + gehackte Nüsse", - "PB&J: Klecks Erdnussmus + Beeren marmoriert", - "Mango-Kokos: Mangostücke + Kokosraspel" - ], - "tip": "Abend-Tipp Einmal machen, ganze Woche naschen – der Süßigkeiten-Ersatz schlechthin.", - "badge": "", - "yield": "" - }, - { - "num": "10", - "category": "Aus dem Tiefkühler · Eis- & Süßigkeiten-Ersatz", - "title": "Gefrorene Trauben & Beeren", - "tagline": "Dein Gummibärchen-Ersatz.", - "nutrition": "ca. ≈ 70 kcal · Eiweiß 1 g · Fett 0 g · KH 17 g · Zucker 16 g · Ballaststoffe 1 g", - "ingredients": "Weintrauben und/oder Beeren", - "steps": "Gewaschen ins Gefrierfach, ein paar Stunden. Direkt aus dem Tiefkühler naschen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Kalt, süß, leicht zäh – beschäftigt den Mund wie Gummibären, ist aber nur Obst.", - "badge": "", - "yield": "" - }, - { - "num": "11", - "category": "Aus dem Tiefkühler · Eis- & Süßigkeiten-Ersatz", - "title": "Joghurt-Beeren-Eis am Stiel", - "tagline": "Selbstgemachtes Eis ohne Zuckerbombe.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 28 g · Fett 1 g · KH 22 g · Zucker 20 g · Ballaststoffe 2 g · ganzer Ansatz", - "ingredients": "250 g Skyr/Joghurt · Beeren · etwas Süße · Eisförmchen", - "steps": "Joghurt mit Beeren und Süße mischen, in Förmchen füllen, einfrieren.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Immer im TK parat, wenn der Eis-Reflex kommt.", - "badge": "", - "yield": "" - }, - { - "num": "12", - "category": "Aus dem Tiefkühler · Eis- & Süßigkeiten-Ersatz", - "title": "Zartbitter-Bananen-Bites", - "tagline": "Knack-Schokoschicht + kaltes Obst.", - "nutrition": "ca. ≈ 285 kcal · Eiweiß 4 g · Fett 14 g · KH 36 g · Zucker 18 g · Ballaststoffe 6 g", - "ingredients": "1 Banane in Scheiben · 30 g Zartbitter (geschmolzen) · optional gehackte Nüsse", - "steps": "Bananenscheiben kurz in geschmolzene Schoko tunken, auf Backpapier, Nüsse drauf, einfrieren.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Ein, zwei Stück reichen – wenig Schoko, viel Effekt.", - "badge": "", - "yield": "" - }, - { - "num": "13", - "category": "Aus dem Tiefkühler · Eis- & Süßigkeiten-Ersatz", - "title": "Frozen Joghurt-Drops", - "tagline": "Mini-Häppchen statt großer Portion.", - "nutrition": "ca. ≈ 110 kcal · Eiweiß 17 g · Fett 1 g · KH 10 g · Zucker 9 g · Ballaststoffe 0 g · ganzer Ansatz", - "ingredients": "Skyr/Joghurt · etwas Süße", - "steps": "Kleine Joghurt-Kleckse aufs Backpapier setzen, einfrieren, aus dem TK naschen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Befriedigt das „immer-wieder-greifen“, ohne reinzuhauen.", - "badge": "", - "yield": "" - }, - { - "num": "14", - "category": "Aus dem Tiefkühler · Eis- & Süßigkeiten-Ersatz", - "title": "Gefrorene Mango-/Melonen-Sticks", - "tagline": "Fast wie Sorbet am Stiel.", - "nutrition": "ca. ≈ 60 kcal · Eiweiß 1 g · Fett 0 g · KH 15 g · Zucker 14 g · Ballaststoffe 2 g", - "ingredients": "Mango oder Melone in Sticks", - "steps": "In Sticks schneiden, einfrieren, direkt naschen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Erfrischend süß, null Zusatz.", - "badge": "", - "yield": "" - }, - { - "num": "15", - "category": "Knusprig & herzhaft · Chip-Ersatz", - "title": "Würzige geröstete Kichererbsen", - "tagline": "Knuspern wie Chips, ohne Tüte.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 15 g · Fett 9 g · KH 41 g · Zucker 1 g · Ballaststoffe 12 g · ganzer Ansatz", - "ingredients": "1 Dose Kichererbsen · 1 TL Öl · Gewürz nach Wahl (siehe Variationen)", - "steps": "Abtropfen, gut trocken tupfen, mit Öl und Gewürz mischen.", - "methods": "Airfryer: 190 °C, 12–15 Min, 2× schütteln · Ofen: 200 °C, 25–30 Min", - "variations": [ - "Paprika-rauchig: geräuch. Paprika + Salz", - "Knoblauch-Kräuter: Knoblauchgranulat + Oregano/Thymian", - "Curry: Currypulver + Prise Salz", - "Chili-Limette: Chili + Limettenabrieb", - "„Käsig“: Hefeflocken + Salz", - "Süß: Zimt + Prise Süße (nach dem Rösten)" - ], - "tip": "Abend-Tipp Eiweiß + Ballaststoffe statt leerem Fett. Vorrat hält ein paar Tage.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "16", - "category": "Knusprig & herzhaft · Chip-Ersatz", - "title": "Edamame mit Meersalz & Chili", - "tagline": "Pulen beschäftigt – das bremst von allein.", - "nutrition": "ca. ≈ 95 kcal · Eiweiß 9 g · Fett 4 g · KH 8 g · Zucker 2 g · Ballaststoffe 5 g", - "ingredients": "TK-Edamame in der Schote · Meersalz · Chiliflocken", - "steps": "Nach Packung dämpfen/Mikrowelle, würzen, aus der Schote pulen.", - "methods": "", - "variations": [ - "Knoblauch-Soja: Sojasauce + Knoblauch", - "Sesam: geröstete Sesamkörner + Salz", - "Limette-Chili: Limettensaft + Chiliflocken" - ], - "tip": "Abend-Tipp Proteinreich, und das Pulen verlangsamt das Essen.", - "badge": "", - "yield": "" - }, - { - "num": "17", - "category": "Knusprig & herzhaft · Chip-Ersatz", - "title": "Herzhaftes Popcorn (luftgepoppt)", - "tagline": "Kino-Gefühl ohne Butter-Bombe.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 4 g · Fett 5 g · KH 22 g · Zucker 0 g · Ballaststoffe 4 g", - "ingredients": "30 g Mais-Kerner · optional 1 TL Öl · Gewürz statt Zucker/viel Salz", - "steps": "Im Topf mit Deckel oder Heißluft-Popper poppen, Gewürz oder Hefeflocken drüber.", - "methods": "", - "variations": [ - "Paprika", - "„Käse“: Hefeflocken", - "Knoblauch-Kräuter", - "Curry", - "Zimt-süß (sparsam)", - "Chili-Schoko: Chili + Kakao-Nibs" - ], - "tip": "Abend-Tipp Riesen-Volumen für wenig – füllt die Schüssel optisch.", - "badge": "", - "yield": "" - }, - { - "num": "18", - "category": "Knusprig & herzhaft · Chip-Ersatz", - "title": "Knäcke-Brett", - "tagline": "Salzig-frisch zum Draufbeißen.", - "nutrition": "ca. ≈ 155 kcal · Eiweiß 10 g · Fett 3 g · KH 23 g · Zucker 2 g · Ballaststoffe 5 g", - "ingredients": "2–3 Knäckebrote/Maiswaffeln · Belag nach Wahl", - "steps": "Knäcke bestreichen, belegen, würzen.", - "methods": "", - "variations": [ - "Hüttenkäse + Gurke", - "Tomate-Mozzarella + Basilikum", - "Avocado + hartes Ei + Pfeffer", - "Frischkäse + Räucherlachs", - "Hummus + Paprikastreifen" - ], - "tip": "Abend-Tipp Knusper + Eiweiß + Frische, sättigt mehr als die Chipstüte.", - "badge": "", - "yield": "" - }, - { - "num": "19", - "category": "Knusprig & herzhaft · Chip-Ersatz", - "title": "Geröstete Nüsse, würzig", - "tagline": "Sättigend – bewusst eine Handvoll.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 4 g · Fett 14 g · KH 4 g · Zucker 1 g · Ballaststoffe 2 g", - "ingredients": "1 Handvoll gemischte Nüsse · Prise Öl · Gewürz", - "steps": "Nüsse (optional mit einem Hauch Öl) mit Gewürz mischen. Alternativ in der Pfanne ohne Öl rösten.", - "methods": "Airfryer: 160 °C, 5–7 Min, schütteln (verbrennen schnell – im Blick behalten) · Ofen: 160 °C, 8–10 Min", - "variations": [ - "Rosmarin-Salz", - "Chili-Honig (Spur Honig)", - "Curry", - "Zimt-süß" - ], - "tip": "Abend-Tipp Kaloriendicht – eine Handvoll als klare Portion.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "20", - "category": "Knusprig & herzhaft · Chip-Ersatz", - "title": "Gemüsechips", - "tagline": "Echter Chips-Ersatz aus Gemüse.", - "nutrition": "ca. ≈ 90 kcal · Eiweiß 2 g · Fett 5 g · KH 9 g · Zucker 5 g · Ballaststoffe 3 g", - "ingredients": "Zucchini, Karotte oder Rote Bete dünn gehobelt · wenig Öl · Salz", - "steps": "Hauchdünn hobeln, trocken tupfen, mit wenig Öl und Salz mischen.", - "methods": "Airfryer: 160 °C, 12–18 Min, zwischendurch schütteln · Ofen: 150 °C, 30–40 Min", - "variations": [ - "Paprika", - "Rosmarin", - "Knoblauch", - "Meersalz pur" - ], - "tip": "Abend-Tipp Dünn = knusprig. Geht auch als „Mix-Teller“.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "21", - "category": "Knusprig & herzhaft · Chip-Ersatz", - "title": "Parmesan-Crisps", - "tagline": "Knusprig, herzhaft, sehr sättigend.", - "nutrition": "ca. ≈ 120 kcal · Eiweiß 11 g · Fett 8 g · KH 1 g · Zucker 0 g · Ballaststoffe 0 g", - "ingredients": "geriebener Parmesan/Hartkäse", - "steps": "Kleine Häufchen auf Backpapier setzen (im Korb luftdurchlässig auslegen).", - "methods": "Airfryer: 180 °C, 4–6 Min bis golden · Ofen: 200 °C, 5–7 Min (oder Mikrowelle ~1 Min)", - "variations": [], - "tip": "Abend-Tipp Wenige Stück genügen – intensiv und proteinreich.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "22", - "category": "Knusprig & herzhaft · Chip-Ersatz", - "title": "Reiswaffel-„Nachos“", - "tagline": "Nacho-Gefühl ohne Chip-Berg.", - "nutrition": "ca. ≈ 140 kcal · Eiweiß 6 g · Fett 6 g · KH 14 g · Zucker 2 g · Ballaststoffe 1 g", - "ingredients": "2 Reiswaffeln · 1 EL Salsa/Tomate · etwas Käse · optional Jalapeño", - "steps": "Belegen, 1 Min in der Mikrowelle bis der Käse schmilzt.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Schnell, herzhaft, klar portioniert.", - "badge": "", - "yield": "" - }, - { - "num": "23", - "category": "Knusprig & herzhaft · Chip-Ersatz", - "title": "Herzhafter Knabber-Mix", - "tagline": "Der salzige Studentenfutter-Ersatz.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 11 g · Fett 14 g · KH 21 g · Zucker 2 g · Ballaststoffe 6 g", - "ingredients": "geröstete Kichererbsen + paar Nüsse + geröstete Sojabohnen/Edamame · optional Salzbrezel", - "steps": "Alles mischen, eine kleine Schale füllen (nicht die ganze Schüssel).", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Portion abfüllen, Rest in den Schrank – sonst läuft's wie die Chipstüte.", - "badge": "", - "yield": "" - }, - { - "num": "24", - "category": "Frisch & zum Knabbern", - "title": "Gemüsesticks mit Dip", - "tagline": "Knabbern, das du nicht bereuen kannst.", - "nutrition": "ca. ≈ 115 kcal · Eiweiß 13 g · Fett 1 g · KH 12 g · Zucker 6 g · Ballaststoffe 4 g", - "ingredients": "Paprika, Gurke, Karotte, Stangensellerie · Dip nach Wahl", - "steps": "Gemüse in Sticks schneiden, Dip anrühren, dippen.", - "methods": "", - "variations": [ - "Kräuterquark", - "Hummus", - "Avocado-Limette (Guacamole-light)", - "Curry-Joghurt", - "Tomate-Feta", - "Erdnuss-Soja" - ], - "tip": "Abend-Tipp Die freieste Option – beim Gemüse-Volumen darfst du großzügig sein.", - "badge": "", - "yield": "" - }, - { - "num": "25", - "category": "Frisch & zum Knabbern", - "title": "Spieße zum Knabbern", - "tagline": "Süß-herzhafter Mini-Apéro.", - "nutrition": "ca. ≈ 145 kcal · Eiweiß 8 g · Fett 8 g · KH 10 g · Zucker 8 g · Ballaststoffe 1 g", - "ingredients": "Zutaten nach Variation · Zahnstocher/Spieße", - "steps": "Abwechselnd aufspießen, auf einem Teller anrichten.", - "methods": "", - "variations": [ - "Käse-Trauben", - "Caprese: Tomate-Mozzarella-Basilikum", - "Melone-Feta", - "Gurke-Oliven-Feta", - "Apfel-Käse" - ], - "tip": "Abend-Tipp Die Spieße zwingen zur kleinen Menge – genau der Trick.", - "badge": "", - "yield": "" - }, - { - "num": "26", - "category": "Frisch & zum Knabbern", - "title": "Apfelscheiben mit Erdnussmus", - "tagline": "Süß-sättigend, schnell gemacht.", - "nutrition": "ca. ≈ 135 kcal · Eiweiß 3 g · Fett 4 g · KH 24 g · Zucker 18 g · Ballaststoffe 5 g", - "ingredients": "1 Apfel in Scheiben · 1 TL Erdnussmus", - "steps": "Apfel in Scheiben, dünn Erdnussmus drauf.", - "methods": "", - "variations": [ - "+ Zartbitter-Raspel", - "+ Zimt", - "+ gehackte Walnuss", - "+ paar Rosinen" - ], - "tip": "Abend-Tipp Erdnussmus sparsam – ein Teelöffel reicht.", - "badge": "", - "yield": "" - }, - { - "num": "27", - "category": "Frisch & zum Knabbern", - "title": "„Ants on a log“ – gefüllter Sellerie", - "tagline": "Knackig, kindlich-gut, kaum Kalorien.", - "nutrition": "ca. ≈ 90 kcal · Eiweiß 2 g · Fett 5 g · KH 9 g · Zucker 6 g · Ballaststoffe 2 g", - "ingredients": "Stangensellerie · Frischkäse oder Erdnussmus · ein paar Rosinen", - "steps": "Sellerie mit Frischkäse/Erdnussmus füllen, Rosinen drauf.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Mit Frischkäse-Variante besonders leicht.", - "badge": "", - "yield": "" - }, - { - "num": "28", - "category": "Frisch & zum Knabbern", - "title": "Gurken-Boote", - "tagline": "Proteinreich, frisch, sättigend für wenig.", - "nutrition": "ca. ≈ 145 kcal · Eiweiß 25 g · Fett 2 g · KH 8 g · Zucker 4 g · Ballaststoffe 2 g", - "ingredients": "1 Gurke längs halbiert & ausgehöhlt · Thunfisch + Quark (oder Frischkäse) · Pfeffer", - "steps": "Gurke aushöhlen, mit der Masse füllen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Viel Volumen, viel Eiweiß, wenig drumherum.", - "badge": "", - "yield": "" - }, - { - "num": "29", - "category": "Frisch & zum Knabbern", - "title": "Caprese-Häppchen", - "tagline": "Fühlt sich nach Apéro an, ist leicht.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 11 g · Fett 12 g · KH 8 g · Zucker 5 g · Ballaststoffe 1 g", - "ingredients": "Cherrytomaten · Mini-Mozzarella · Basilikum · Spritzer Balsamico", - "steps": "Aufspießen oder mischen, Balsamico drüber.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Frisch und herzhaft statt salzig-fettig.", - "badge": "", - "yield": "" - }, - { - "num": "30", - "category": "Frisch & zum Knabbern", - "title": "Paprika-Schiffchen mit Hüttenkäse", - "tagline": "Knackiges Löffel-zum-Mund-Snacking.", - "nutrition": "ca. ≈ 135 kcal · Eiweiß 14 g · Fett 5 g · KH 11 g · Zucker 8 g · Ballaststoffe 3 g", - "ingredients": "Paprika in Schiffchen · Hüttenkäse · Schnittlauch, Pfeffer", - "steps": "Paprika füllen oder mit gewürztem Hüttenkäse dippen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Süße Paprika + Eiweiß, sehr sättigend.", - "badge": "", - "yield": "" - }, - { - "num": "31", - "category": "Herzhaft & sättigend · Mini-Mahlzeiten", - "title": "Gekochte Eier, gewürzt", - "tagline": "Der unterschätzte Sattmacher.", - "nutrition": "ca. ≈ 155 kcal · Eiweiß 13 g · Fett 11 g · KH 1 g · Zucker 0 g · Ballaststoffe 0 g", - "ingredients": "1–2 hartgekochte Eier · Salz, Pfeffer, Paprika", - "steps": "Eier pellen, würzen.", - "methods": "", - "variations": [ - "+ Senf", - "+ Sriracha", - "+ Everything-Bagel-Gewürz", - "halbiert mit Avocado" - ], - "tip": "Abend-Tipp Eiweiß pur, kaum Aufwand – ideal bei echtem Hunger.", - "badge": "", - "yield": "" - }, - { - "num": "32", - "category": "Herzhaft & sättigend · Mini-Mahlzeiten", - "title": "Omelett-Muffins (vorbereitet)", - "tagline": "Sonntag machen, ganze Woche snacken.", - "nutrition": "ca. ≈ 325 kcal · Eiweiß 26 g · Fett 23 g · KH 5 g · Zucker 3 g · Ballaststoffe 1 g · ganzer Ansatz", - "ingredients": "3 Eier · Gemüse-Würfel · etwas Käse · Salz/Pfeffer · Muffinform", - "steps": "Eier verquirlen, Füllung in die Muffinform geben. Kalt oder warm genießen.", - "methods": "Airfryer: 160 °C, 10–12 Min · Ofen: 180 °C, 18 Min", - "variations": [ - "Spinat-Feta", - "Paprika-Zwiebel", - "Schinken-Käse", - "Pilz-Kräuter" - ], - "tip": "Abend-Tipp Eiweiß-Snack zum Vorbereiten – immer griffbereit.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "33", - "category": "Herzhaft & sättigend · Mini-Mahlzeiten", - "title": "Vollkorn-Toast belegt", - "tagline": "Eine Scheibe sättigt mehr als halbe Chipstüte.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 12 g · Fett 4 g · KH 18 g · Zucker 3 g · Ballaststoffe 4 g", - "ingredients": "1 Scheibe Vollkorntoast · Belag nach Wahl", - "steps": "Toasten, belegen.", - "methods": "", - "variations": [ - "Hüttenkäse + Tomate", - "Avocado + Ei", - "Frischkäse + Gurke", - "Magerquark + Schnittlauch", - "Erdnussmus + Banane (süß)" - ], - "tip": "Abend-Tipp Bewusst eine Scheibe – das ist die Portion.", - "badge": "", - "yield": "" - }, - { - "num": "34", - "category": "Herzhaft & sättigend · Mini-Mahlzeiten", - "title": "Herzhafter Quark-Dip mit Gemüse", - "tagline": "Großes Volumen, viel Eiweiß.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 25 g · Fett 1 g · KH 16 g · Zucker 9 g · Ballaststoffe 4 g", - "ingredients": "200 g Magerquark · Schnittlauch, Salz, Pfeffer, Spritzer Wasser · Gemüsesticks", - "steps": "Quark cremig würzen, mit Gemüse dippen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Sättigt stark bei wenig drumherum.", - "badge": "", - "yield": "" - }, - { - "num": "35", - "category": "Herzhaft & sättigend · Mini-Mahlzeiten", - "title": "Thunfisch-Quark-Aufstrich auf Knäcke", - "tagline": "Sehr sättigend, eiweißstark.", - "nutrition": "ca. ≈ 210 kcal · Eiweiß 33 g · Fett 2 g · KH 15 g · Zucker 2 g · Ballaststoffe 3 g", - "ingredients": "1 Dose Thunfisch (im Wasser) · 2 EL Magerquark · Zitrone, Pfeffer · Knäckebrot", - "steps": "Thunfisch mit Quark und Zitrone verrühren, auf Knäcke streichen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Gute Wahl, wenn der Hunger echt ist.", - "badge": "", - "yield": "" - }, - { - "num": "36", - "category": "Herzhaft & sättigend · Mini-Mahlzeiten", - "title": "Mini-Wrap-Röllchen", - "tagline": "Fingerfood-Optik, ordentlich Eiweiß.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 15 g · Fett 9 g · KH 25 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "1 Vollkorn-Tortilla · Frischkäse · Putenbrust/Schinken · Gurke/Paprika", - "steps": "Bestreichen, belegen, fest rollen, in Scheiben schneiden.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Leicht zu portionieren – Röllchen abzählen.", - "badge": "", - "yield": "" - }, - { - "num": "37", - "category": "Herzhaft & sättigend · Mini-Mahlzeiten", - "title": "Hüttenkäse-Bowl", - "tagline": "Frisch und proteinreich, fast Mini-Mahlzeit.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 20 g · Fett 8 g · KH 8 g · Zucker 5 g · Ballaststoffe 2 g", - "ingredients": "150 g Hüttenkäse · Cherrytomaten · Gurke · Pfeffer · optional Leinöl", - "steps": "Alles in eine Schale, würzen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Sättigt lange und ist schnell zusammengeworfen.", - "badge": "", - "yield": "" - }, - { - "num": "38", - "category": "Herzhaft & sättigend · Mini-Mahlzeiten", - "title": "Edamame-„Hummus“ mit Gemüse", - "tagline": "Eiweißreicher Dip-Twist.", - "nutrition": "ca. ≈ 270 kcal · Eiweiß 19 g · Fett 14 g · KH 20 g · Zucker 4 g · Ballaststoffe 9 g", - "ingredients": "150 g Edamame · 1 TL Tahin/Öl · Zitrone, Knoblauch, Salz · Gemüsesticks", - "steps": "Edamame mit den Zutaten pürieren, mit Gemüse dippen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Sättigt besser als Chips & Salsa.", - "badge": "", - "yield": "" - }, - { - "num": "39", - "category": "Süße Häppchen & Energy Bites", - "title": "Energy Balls", - "tagline": "1–2 Stück als idealer Süß-Stopper.", - "nutrition": "ca. ≈ 645 kcal · Eiweiß 28 g · Fett 22 g · KH 90 g · Zucker 20 g · Ballaststoffe 13 g · ganzer Ansatz", - "ingredients": "100 g Haferflocken · 2 EL Erdnussmus · 4 entkernte Datteln (o. 1 EL Honig) · Schuss Milch · optional 1 EL Proteinpulver", - "steps": "Alles zu einer Masse mixen/kneten, Kugeln formen, kalt stellen.", - "methods": "", - "variations": [ - "Schoko: + Kakao/Schoko-Drops", - "Kokos: in Kokosraspel wälzen", - "Cranberry-Nuss", - "Zimt-Apfel: + getrocknete Apfelstücke" - ], - "tip": "Abend-Tipp Vorrat im Kühlschrank – greifbar, wenn die Lust kommt.", - "badge": "", - "yield": "" - }, - { - "num": "40", - "category": "Süße Häppchen & Energy Bites", - "title": "„Snickers“-Datteln", - "tagline": "Schmeckt nach Riegel, ist Dattel + Nuss.", - "nutrition": "ca. ≈ 290 kcal · Eiweiß 6 g · Fett 17 g · KH 28 g · Zucker 20 g · Ballaststoffe 4 g · ganzer Ansatz", - "ingredients": "entkernte Datteln · Erdnussmus · 20 g Zartbitter (geschmolzen) · optional Meersalz", - "steps": "Datteln aufklappen, mit Erdnussmus füllen, in Schoko tunken, kühlen/gefrieren, Salz drauf.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Riegel-Gefühl mit nur ein bisschen Zartbitter.", - "badge": "", - "yield": "" - }, - { - "num": "41", - "category": "Süße Häppchen & Energy Bites", - "title": "Schoko-Obst (Erdbeeren/Banane)", - "tagline": "Edler Nachtisch-Effekt mit wenig Schoko.", - "nutrition": "ca. ≈ 210 kcal · Eiweiß 4 g · Fett 14 g · KH 20 g · Zucker 12 g · Ballaststoffe 4 g", - "ingredients": "Erdbeeren oder Bananenstücke · 30 g Zartbitterschokolade", - "steps": "Schoko schmelzen, Obst tunken, auf Backpapier fest werden lassen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Obst trägt die Süße, die Schoko ist nur Hülle.", - "badge": "", - "yield": "" - }, - { - "num": "42", - "category": "Süße Häppchen & Energy Bites", - "title": "Mini-Bananen-Hafer-Kekse", - "tagline": "2 Zutaten, kein Zuckerzusatz nötig.", - "nutrition": "ca. ≈ 290 kcal · Eiweiß 8 g · Fett 4 g · KH 60 g · Zucker 15 g · Ballaststoffe 8 g · ganzer Ansatz", - "ingredients": "1 reife Banane · 5 EL Haferflocken · optional Extras", - "steps": "Banane zerdrücken, mit Haferflocken (+ Extras) mischen, kleine Häufchen formen.", - "methods": "Airfryer: 160 °C, 8–10 Min · Ofen: 180 °C, 12 Min", - "variations": [ - "+ Zartbitter-Drops", - "+ Rosinen", - "+ Zimt", - "+ gehackte Nuss" - ], - "tip": "Abend-Tipp Frisch gebacken am besten – warme Mini-Kekse.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "43", - "category": "Süße Häppchen & Energy Bites", - "title": "Kokoskugeln (Quark-Kokos)", - "tagline": "„Raffaello-Gefühl“ mit Eiweiß-Basis.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 19 g · Fett 13 g · KH 12 g · Zucker 8 g · Ballaststoffe 3 g · ganzer Ansatz", - "ingredients": "150 g Magerquark · Kokosraspel · etwas Süße · Vanille", - "steps": "Quark mit Kokos und Süße zu fester Masse, Kugeln formen, in Kokos wälzen, kühlen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Cremig-kokosig, ohne Sahne-Füllung.", - "badge": "", - "yield": "" - }, - { - "num": "44", - "category": "Süße Häppchen & Energy Bites", - "title": "Dattel-Walnuss-Bissen", - "tagline": "Eine Zutat-Kombi, natürlich süß.", - "nutrition": "ca. ≈ 120 kcal · Eiweiß 2 g · Fett 6 g · KH 17 g · Zucker 14 g · Ballaststoffe 2 g", - "ingredients": "entkernte Datteln · je 1 Walnusshälfte", - "steps": "Datteln aufklappen, Walnuss reinsetzen, zuklappen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Knackig + süß, schnell satt – Datteln sind zuckerintensiv, also wenige.", - "badge": "", - "yield": "" - }, - { - "num": "45", - "category": "Warm & gemütlich · vorm Schlafen", - "title": "Warmer Protein-Kakao", - "tagline": "Beruhigt und sättigt zugleich.", - "nutrition": "ca. ≈ 110 kcal · Eiweiß 9 g · Fett 2 g · KH 15 g · Zucker 13 g · Ballaststoffe 1 g", - "ingredients": "250 ml Magermilch (o. ungesüßte Pflanzenmilch) · 1 TL Backkakao · Prise Zimt · etwas Süße", - "steps": "Milch erhitzen, Kakao und Zimt einrühren, leicht süßen, langsam trinken.", - "methods": "", - "variations": [ - "Goldene Milch: Kurkuma + Prise Pfeffer statt Kakao", - "Chai: Zimt-Kardamom-Ingwer", - "Mandel-Zimt", - "„White“: ohne Kakao, mit Vanille" - ], - "tip": "Abend-Tipp Warmes Getränk + Eiweiß statt Cola vor dem Schlaf.", - "badge": "", - "yield": "" - }, - { - "num": "46", - "category": "Warm & gemütlich · vorm Schlafen", - "title": "Haferflocken-Mug-Cake", - "tagline": "„Kuchen“ in der Tasse, in 2 Minuten.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 12 g · Fett 8 g · KH 26 g · Zucker 12 g · Ballaststoffe 4 g", - "ingredients": "3 EL Haferflocken · 1 Ei (o. 30 g Quark) · Prise Backpulver · 1 TL Kakao · etwas Süße · Schuss Milch", - "steps": "Alles in einer Tasse verrühren, 1,5–2 Min in der Mikrowelle, kurz abkühlen.", - "methods": "", - "variations": [ - "Schoko", - "Apfel-Zimt", - "Beere", - "Erdnuss-Banane", - "Carrot-Cake: geraspelte Karotte + Zimt" - ], - "tip": "Abend-Tipp Hafer + Eiweiß statt Mehl + Zucker. Warm am besten.", - "badge": "", - "yield": "" - }, - { - "num": "47", - "category": "Warm & gemütlich · vorm Schlafen", - "title": "Warmes Beeren-Kompott mit Quark", - "tagline": "Warm-kalt-Kontrast wie Dessert.", - "nutrition": "ca. ≈ 155 kcal · Eiweiß 19 g · Fett 1 g · KH 16 g · Zucker 12 g · Ballaststoffe 3 g", - "ingredients": "Handvoll TK-Beeren · Spur Süße · 150 g Magerquark", - "steps": "Beeren in Mikrowelle/Topf warm machen, über kalten Quark geben.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Beeren = wenig Zucker, viel Geschmack.", - "badge": "", - "yield": "" - }, - { - "num": "48", - "category": "Warm & gemütlich · vorm Schlafen", - "title": "Porridge / Overnight Oats klein", - "tagline": "Hält ruhig satt bis zum Schlaf.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 10 g · Fett 6 g · KH 32 g · Zucker 12 g · Ballaststoffe 4 g", - "ingredients": "4 EL Haferflocken · 150 ml Milch · etwas Süße", - "steps": "Warm: 2 Min köcheln/Mikrowelle. Kalt: über Nacht quellen lassen.", - "methods": "", - "variations": [ - "Schoko", - "Apfel-Zimt", - "Beere", - "Banane-Erdnuss" - ], - "tip": "Abend-Tipp Kleine Portion abends ist okay – Hafer + Milch beruhigen.", - "badge": "", - "yield": "" - }, - { - "num": "49", - "category": "Warm & gemütlich · vorm Schlafen", - "title": "Warme Milch mit Honig & Zimt", - "tagline": "Ruhiger Absacker statt Cola.", - "nutrition": "ca. ≈ 140 kcal · Eiweiß 9 g · Fett 4 g · KH 18 g · Zucker 17 g · Ballaststoffe 0 g", - "ingredients": "250 ml Milch · 1 TL Honig · Zimt", - "steps": "Milch erwärmen, Honig und Zimt einrühren.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Honig sparsam – ein Teelöffel reicht.", - "badge": "", - "yield": "" - }, - { - "num": "50", - "category": "Warm & gemütlich · vorm Schlafen", - "title": "Warmer Milchreis / Grießbrei (klein)", - "tagline": "Cremiger Kindheits-Comfort.", - "nutrition": "ca. ≈ 215 kcal · Eiweiß 11 g · Fett 4 g · KH 34 g · Zucker 15 g · Ballaststoffe 1 g", - "ingredients": "30 g Milchreis oder Grieß · 200 ml Milch · etwas Süße · Zimt", - "steps": "In Milch nach Packung weich köcheln, mit Zimt/Süße abschmecken.", - "methods": "", - "variations": [ - "Apfelmus", - "Beeren", - "Kakao", - "Zimt-Zucker (sparsam)" - ], - "tip": "Abend-Tipp Kleine Portion, warm, sättigend – ohne Riesenschüssel.", - "badge": "", - "yield": "" - }, - { - "num": "51", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Tofu-Würfel", - "tagline": "Knuspriger Eiweiß-Snack zum Knabbern.", - "nutrition": "ca. ≈ 355 kcal · Eiweiß 32 g · Fett 20 g · KH 15 g · Zucker 1 g · Ballaststoffe 2 g", - "ingredients": "200 g fester Tofu (gewürfelt) · 1 TL Öl · 1 TL Speisestärke · Sojasauce · Gewürz", - "steps": "Tofu trocken pressen, mit Sojasauce, Stärke, Öl und Gewürz mischen.", - "methods": "Airfryer: 200 °C, 12–15 Min, 1× schütteln · Ofen: 220 °C, 25 Min, einmal wenden", - "variations": [ - "BBQ", - "Sweet-Chili", - "Curry", - "Sesam-Soja" - ], - "tip": "Abend-Tipp Außen knusprig, innen weich – sättigt wie ein Mini-Gericht.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "52", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Edamame, knusprig", - "tagline": "Aus weich wird knusprig.", - "nutrition": "ca. ≈ 115 kcal · Eiweiß 9 g · Fett 6 g · KH 8 g · Zucker 2 g · Ballaststoffe 4 g", - "ingredients": "TK-Edamame (geschält) · Spritzer Öl · Salz, Chili", - "steps": "Edamame auftauen, gut trocken tupfen, mit Öl und Gewürz mischen.", - "methods": "Airfryer: 190 °C, 10–12 Min, schütteln · Ofen: 200 °C, 18–20 Min", - "variations": [], - "tip": "Abend-Tipp Proteinreicher Chip-Ersatz – knuspert wie geröstete Nüsse.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "53", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Mini-Pizza-Toasts", - "tagline": "Pizza-Gefühl als Mini-Happen.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 9 g · Fett 7 g · KH 18 g · Zucker 3 g · Ballaststoffe 2 g", - "ingredients": "Vollkorn-Toast/Brot · 1 EL Tomatensauce · etwas Mozzarella/Käse · Oregano · optional Gemüse", - "steps": "Brot mit Sauce, Käse und Oregano belegen.", - "methods": "Airfryer: 180 °C, 5–7 Min · Ofen: 200 °C, 8–10 Min", - "variations": [], - "tip": "Abend-Tipp Herzhaft-warm; ein, zwei Stück statt halber Tiefkühlpizza.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "54", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Brokkoli / Rosenkohl", - "tagline": "Geröstetes Gemüse mit Röstaromen.", - "nutrition": "ca. ≈ 120 kcal · Eiweiß 7 g · Fett 8 g · KH 8 g · Zucker 3 g · Ballaststoffe 4 g", - "ingredients": "Brokkoli-Röschen oder halbierter Rosenkohl · 1 TL Öl · Salz, Knoblauch, optional Parmesan", - "steps": "Gemüse mit Öl und Gewürz mischen.", - "methods": "Airfryer: 190 °C, 10–12 Min, schütteln · Ofen: 210 °C, 20–25 Min", - "variations": [], - "tip": "Abend-Tipp Knusprige Röschen – herzhaft genug, um Chips zu ersetzen.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "55", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Blumenkohl-„Wings“", - "tagline": "„Wings“-Gefühl aus Gemüse.", - "nutrition": "ca. ≈ 90 kcal · Eiweiß 4 g · Fett 5 g · KH 9 g · Zucker 4 g · Ballaststoffe 4 g", - "ingredients": "Blumenkohl-Röschen · 1 TL Öl · Paprika, Knoblauch, Salz · optional scharfe Sauce", - "steps": "Röschen mit Öl und Gewürz mischen; nach dem Garen optional Sauce.", - "methods": "Airfryer: 190 °C, 12–15 Min, schütteln · Ofen: 220 °C, 25 Min", - "variations": [], - "tip": "Abend-Tipp Herzhaft-scharf, sättigend, leicht.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "56", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Süßkartoffel-Pommes", - "tagline": "Knusprige Pommes ohne Fritteuse.", - "nutrition": "ca. ≈ 190 kcal · Eiweiß 3 g · Fett 5 g · KH 35 g · Zucker 6 g · Ballaststoffe 5 g", - "ingredients": "Süßkartoffel (Stifte) · 1 TL Öl · 1 TL Stärke · Salz, Paprika", - "steps": "Stifte mit Öl, Stärke und Gewürz mischen (Korb nicht überladen).", - "methods": "Airfryer: 190 °C, 15–18 Min, schütteln · Ofen: 220 °C, 25–30 Min", - "variations": [], - "tip": "Abend-Tipp Süß-salzig und knusprig; Portion bewusst abmessen.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "57", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Apfelchips", - "tagline": "Süß-knusprig ganz ohne Zucker.", - "nutrition": "ca. ≈ 85 kcal · Eiweiß 1 g · Fett 0 g · KH 22 g · Zucker 18 g · Ballaststoffe 4 g", - "ingredients": "1 Apfel (hauchdünn) · Zimt", - "steps": "Apfel sehr dünn hobeln, trocken tupfen, mit Zimt bestreuen.", - "methods": "Airfryer: 150 °C, 12–16 Min, zwischendurch wenden · Ofen: 120 °C, 1,5–2 Std", - "variations": [], - "tip": "Abend-Tipp Dein Chips-Ersatz für die süße Schiene.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "58", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Tortilla-Chips", - "tagline": "Selbstgemachte Nacho-Basis.", - "nutrition": "ca. ≈ 170 kcal · Eiweiß 5 g · Fett 6 g · KH 24 g · Zucker 2 g · Ballaststoffe 3 g", - "ingredients": "Vollkorn-Tortilla (in Dreiecke) · Spritzer Öl · Salz/Paprika", - "steps": "Dreiecke leicht ölen und würzen.", - "methods": "Airfryer: 180 °C, 5–7 Min · Ofen: 200 °C, 8–10 Min", - "variations": [], - "tip": "Abend-Tipp Mit Salsa oder Skyr-Dip ein herzhafter Teller.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "59", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Knusper-Erbsen", - "tagline": "Grüne Erbsen als Knabber-Kugeln.", - "nutrition": "ca. ≈ 100 kcal · Eiweiß 5 g · Fett 3 g · KH 14 g · Zucker 5 g · Ballaststoffe 5 g", - "ingredients": "TK-Erbsen · Spritzer Öl · Salz, Paprika", - "steps": "Erbsen auftauen, gut trocken tupfen, mit Öl und Gewürz mischen.", - "methods": "Airfryer: 190 °C, 10–14 Min, schütteln · Ofen: 200 °C, 20 Min", - "variations": [], - "tip": "Abend-Tipp Klein, knusprig, proteinreich – überraschend gut.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "60", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Croutons", - "tagline": "Herzhaft-knusprig zum Knabbern oder Dippen.", - "nutrition": "ca. ≈ 130 kcal · Eiweiß 4 g · Fett 5 g · KH 16 g · Zucker 1 g · Ballaststoffe 3 g", - "ingredients": "Altbackenes Vollkornbrot (Würfel) · 1 TL Öl · Knoblauch, Kräuter, Salz", - "steps": "Würfel mit Öl und Gewürz mischen.", - "methods": "Airfryer: 180 °C, 5–7 Min, schütteln · Ofen: 200 °C, 8–10 Min", - "variations": [], - "tip": "Abend-Tipp Pur knabbern oder über den Quark-Dip streuen.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "61", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Hähnchen-„Popcorn“", - "tagline": "Der Eiweiß-Snack bei echtem Hunger.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 28 g · Fett 6 g · KH 8 g · Zucker 0 g · Ballaststoffe 1 g", - "ingredients": "Hähnchenbrust (Würfel) · 1 TL Öl · Paprika, Salz · optional Vollkorn-Paniermehl", - "steps": "Würfel würzen, optional in Paniermehl wälzen.", - "methods": "Airfryer: 200 °C, 10–12 Min, schütteln · Ofen: 220 °C, 18–20 Min", - "variations": [], - "tip": "Abend-Tipp Sehr sättigend; mit Joghurt-Dip ein kleines Gericht.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "62", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-Falafel", - "tagline": "Herzhaft-sättigender Kichererbsen-Happen.", - "nutrition": "ca. ≈ 260 kcal · Eiweiß 8 g · Fett 14 g · KH 25 g · Zucker 2 g · Ballaststoffe 5 g", - "ingredients": "TK-Falafel (oder selbst: Kichererbsen + Kräuter + Knoblauch zu Bällchen)", - "steps": "Fertige Falafel in den Korb; selbstgemacht zu kleinen Bällchen formen.", - "methods": "Airfryer: 190 °C, 10–12 Min, wenden · Ofen: 200 °C, 18–20 Min", - "variations": [], - "tip": "Abend-Tipp Mit Joghurt-Dip und Gemüse ein vollwertiger Teller.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "63", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-gefüllte Champignons", - "tagline": "Warmer, feiner Couch-Happen.", - "nutrition": "ca. ≈ 145 kcal · Eiweiß 6 g · Fett 11 g · KH 5 g · Zucker 3 g · Ballaststoffe 2 g", - "ingredients": "Champignons · Frischkäse/Hüttenkäse · Kräuter · optional etwas Parmesan", - "steps": "Stiele entfernen, Köpfe mit der Käse-Kräuter-Masse füllen.", - "methods": "Airfryer: 180 °C, 8–10 Min · Ofen: 200 °C, 15 Min", - "variations": [], - "tip": "Abend-Tipp Herzhaft und leicht – kaum Kalorien, viel Geschmack.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "64", - "category": "Airfryer · Knusprig & herzhaft", - "title": "Airfryer-paniertes Gemüse", - "tagline": "Knuspriges Gemüse statt Chips.", - "nutrition": "ca. ≈ 225 kcal · Eiweiß 15 g · Fett 11 g · KH 16 g · Zucker 4 g · Ballaststoffe 3 g", - "ingredients": "Zucchini-/Paprika-Sticks · 1 Ei · Vollkorn-Paniermehl + Parmesan", - "steps": "Sticks durch Ei ziehen, in der Panade wälzen.", - "methods": "Airfryer: 200 °C, 8–10 Min · Ofen: 220 °C, 15–18 Min", - "variations": [], - "tip": "Abend-Tipp Knusprig-paniert, herzhaft, mit Eiweiß vom Ei.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "65", - "category": "Airfryer · Süß & Gebäck", - "title": "Airfryer-Bratapfel", - "tagline": "Gemütlich, aber schneller als im Ofen.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 2 g · Fett 7 g · KH 30 g · Zucker 25 g · Ballaststoffe 5 g", - "ingredients": "1 Apfel (entkernt) · Zimt · gehackte Nüsse · 1 TL Rosinen", - "steps": "Apfel entkernen, mit Nuss-Rosinen-Zimt-Füllung füllen.", - "methods": "Airfryer: 170 °C, 12–15 Min · Ofen: 180 °C, 20–25 Min", - "variations": [], - "tip": "Abend-Tipp Süß ohne Kuchenteig; mit Quark dazu noch sättigender.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "66", - "category": "Airfryer · Süß & Gebäck", - "title": "Airfryer-Bananenbrot-Muffin", - "tagline": "Saftiger Mini-Kuchen auf Hafer-Banane-Basis.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 12 g · Fett 9 g · KH 51 g · Zucker 15 g · Ballaststoffe 6 g · ganzer Ansatz", - "ingredients": "1 Banane · 1 Ei · 4 EL Haferflocken · Prise Backpulver · optional Extras · kleine Förmchen", - "steps": "Banane zerdrücken, alles verrühren, in Förmchen füllen.", - "methods": "Airfryer: 160 °C, 12–15 Min · Ofen: 180 °C, 18–20 Min", - "variations": [ - "Schoko", - "Walnuss", - "Heidelbeere", - "Erdnuss" - ], - "tip": "Abend-Tipp Mini-Kuchengefühl ohne Mehl und Zucker.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "67", - "category": "Airfryer · Süß & Gebäck", - "title": "Airfryer-Beeren-Crumble (Single)", - "tagline": "Warmer Crumble in der Single-Portion.", - "nutrition": "ca. ≈ 210 kcal · Eiweiß 5 g · Fett 7 g · KH 32 g · Zucker 14 g · Ballaststoffe 5 g", - "ingredients": "TK-Beeren · 3 EL Haferflocken + 1 TL Öl/Butter + Prise Süße (Streusel)", - "steps": "Beeren in eine Förmchen, Streusel-Mischung darüber.", - "methods": "Airfryer: 170 °C, 10–12 Min · Ofen: 190 °C, 18–20 Min", - "variations": [], - "tip": "Abend-Tipp Mit einem Klecks Quark ein vollwertiges Dessert.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "68", - "category": "Airfryer · Süß & Gebäck", - "title": "Airfryer-Quark-Bällchen", - "tagline": "„Quarkbällchen“ ohne Frittierfett.", - "nutrition": "ca. ≈ 305 kcal · Eiweiß 29 g · Fett 8 g · KH 27 g · Zucker 10 g · Ballaststoffe 3 g · ganzer Ansatz", - "ingredients": "Magerquark · 1 Ei · Haferflocken/Mehl · Prise Backpulver · etwas Süße", - "steps": "Zu festem Teig verrühren, kleine Bällchen formen.", - "methods": "Airfryer: 180 °C, 8–10 Min · Ofen: 190 °C, 15 Min", - "variations": [], - "tip": "Abend-Tipp Eiweißreicher Süß-Happen, warm am besten.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "69", - "category": "Airfryer · Süß & Gebäck", - "title": "Airfryer-French-Toast-Sticks", - "tagline": "Süßes Frühstücks-Feeling als Abend-Happen.", - "nutrition": "ca. ≈ 185 kcal · Eiweiß 12 g · Fett 7 g · KH 18 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "Vollkornbrot (in Sticks) · 1 Ei + Schuss Milch + Zimt", - "steps": "Sticks in der Eimasse wenden.", - "methods": "Airfryer: 180 °C, 6–8 Min, wenden · Ofen: 200 °C, 12 Min", - "variations": [], - "tip": "Abend-Tipp Eiweiß vom Ei macht's sättigend; ohne Sirup-Flut.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "70", - "category": "Airfryer · Süß & Gebäck", - "title": "Airfryer-Zimt-Apfel-Spalten", - "tagline": "Warme Zimtäpfel – wie Bratapfel, schneller.", - "nutrition": "ca. ≈ 85 kcal · Eiweiß 1 g · Fett 0 g · KH 22 g · Zucker 18 g · Ballaststoffe 4 g", - "ingredients": "Apfel (Spalten) · Zimt · Spritzer Zitrone", - "steps": "Spalten mit Zimt und Zitrone mischen.", - "methods": "Airfryer: 180 °C, 8–10 Min · Ofen: 200 °C, 15 Min", - "variations": [], - "tip": "Abend-Tipp Warm und süß ganz ohne Zuckerzusatz.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "71", - "category": "Airfryer · Süß & Gebäck", - "title": "Airfryer-Schoko-Bananen-Päckchen", - "tagline": "Warm-schokoladig, fast wie Dessert.", - "nutrition": "ca. ≈ 165 kcal · Eiweiß 2 g · Fett 5 g · KH 30 g · Zucker 15 g · Ballaststoffe 4 g", - "ingredients": "Banane (längs) · 10 g Zartbitter-Raspel · Backpapier", - "steps": "Banane einschneiden, Schoko-Raspel reinstecken, locker in Backpapier wickeln.", - "methods": "Airfryer: 180 °C, 6–8 Min · Ofen: 200 °C, 10–12 Min", - "variations": [], - "tip": "Abend-Tipp Geschmolzene Schoko in warmer Banane – wenig reicht.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "72", - "category": "Airfryer · Süß & Gebäck", - "title": "Airfryer-gebackene Birne", - "tagline": "Saftig-süß mit Röstnote.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 2 g · Fett 7 g · KH 30 g · Zucker 20 g · Ballaststoffe 4 g", - "ingredients": "1 Birne (halbiert, entkernt) · Zimt · paar Walnüsse · optional Spur Honig", - "steps": "Birne halbieren, mit Zimt und Nüssen belegen.", - "methods": "Airfryer: 170 °C, 10–12 Min · Ofen: 190 °C, 18–20 Min", - "variations": [], - "tip": "Abend-Tipp Warme Birne als feiner, leichter Nachtisch.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "73", - "category": "Mehr herzhafte Mini-Mahlzeiten", - "title": "Reiswaffel mit Avocado & Ei", - "tagline": "Cremig + Eiweiß, sättigt für wenig.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 9 g · Fett 16 g · KH 14 g · Zucker 1 g · Ballaststoffe 5 g", - "ingredients": "Reiswaffel · ½ Avocado · 1 hartes Ei · Chili, Salz", - "steps": "Avocado zerdrücken, auf die Waffel, Ei in Scheiben darauf.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Herzhafter Mini-Snack mit gutem Sattmacher-Mix.", - "badge": "", - "yield": "" - }, - { - "num": "74", - "category": "Mehr herzhafte Mini-Mahlzeiten", - "title": "Skyr-Tzatziki mit Gemüse", - "tagline": "Frisch, proteinreich, großes Volumen.", - "nutrition": "ca. ≈ 145 kcal · Eiweiß 18 g · Fett 1 g · KH 13 g · Zucker 6 g · Ballaststoffe 3 g", - "ingredients": "150 g Skyr · ½ Gurke (geraspelt, ausgedrückt) · Knoblauch, Salz, Dill · Gemüsesticks", - "steps": "Skyr mit Gurke, Knoblauch und Dill verrühren, mit Gemüse dippen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Viel Dip-Volumen bei wenig drumherum.", - "badge": "", - "yield": "" - }, - { - "num": "75", - "category": "Mehr herzhafte Mini-Mahlzeiten", - "title": "Linsen-Bohnen-Salat (Mini)", - "tagline": "Proteinreich, hält lange satt.", - "nutrition": "ca. ≈ 240 kcal · Eiweiß 13 g · Fett 6 g · KH 30 g · Zucker 5 g · Ballaststoffe 9 g", - "ingredients": "Gekochte Linsen/Bohnen (Dose) · Tomate, Gurke, Zwiebel · Zitrone, Öl, Salz", - "steps": "Alles klein schneiden und mischen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Sättigt deutlich – gut bei echtem Abendhunger.", - "badge": "", - "yield": "" - }, - { - "num": "76", - "category": "Mehr herzhafte Mini-Mahlzeiten", - "title": "Hüttenkäse pikant auf Gurke", - "tagline": "Mini-Häppchen, viel Eiweiß.", - "nutrition": "ca. ≈ 115 kcal · Eiweiß 14 g · Fett 5 g · KH 6 g · Zucker 4 g · Ballaststoffe 1 g", - "ingredients": "Hüttenkäse · Gurkenscheiben · Paprikapulver, Schnittlauch, Pfeffer", - "steps": "Hüttenkäse würzen, auf Gurkenscheiben setzen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Knackig-frisch und sehr leicht.", - "badge": "", - "yield": "" - }, - { - "num": "77", - "category": "Mehr herzhafte Mini-Mahlzeiten", - "title": "Airfryer-Mozzarella-Sticks (light)", - "tagline": "„Frittiertes“ Gefühl mit wenig Fett.", - "nutrition": "ca. ≈ 255 kcal · Eiweiß 20 g · Fett 12 g · KH 16 g · Zucker 2 g · Ballaststoffe 1 g", - "ingredients": "Light-Mozzarella/Käse-Sticks · 1 Ei · Vollkorn-Paniermehl", - "steps": "Sticks panieren, kurz anfrieren (hält die Form).", - "methods": "Airfryer: 200 °C, 6–7 Min · Ofen: 220 °C, 8–10 Min", - "variations": [], - "tip": "Abend-Tipp Klein halten – als Highlight, nicht als Hauptmenge.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "78", - "category": "Mehr herzhafte Mini-Mahlzeiten", - "title": "Pute-Frischkäse-Röllchen", - "tagline": "Low-Carb Eiweiß-Röllchen.", - "nutrition": "ca. ≈ 145 kcal · Eiweiß 15 g · Fett 8 g · KH 3 g · Zucker 2 g · Ballaststoffe 1 g", - "ingredients": "Putenbrust-/Schinkenscheiben · Frischkäse · Gurkenstreifen", - "steps": "Scheiben bestreichen, Gurke einrollen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Schnell, herzhaft, klar portionierbar.", - "badge": "", - "yield": "" - }, - { - "num": "79", - "category": "Mehr herzhafte Mini-Mahlzeiten", - "title": "Airfryer-Frittata-Cup", - "tagline": "Warmer Eiweiß-Snack aus dem Korb.", - "nutrition": "ca. ≈ 245 kcal · Eiweiß 18 g · Fett 17 g · KH 4 g · Zucker 3 g · Ballaststoffe 1 g", - "ingredients": "2 Eier · Gemüse-Würfel · etwas Käse · Salz/Pfeffer · ofenfeste Tasse", - "steps": "Eier verquirlen, mit Füllung in die Tasse geben.", - "methods": "Airfryer: 160 °C, 10–12 Min · Ofen: 180 °C, 18 Min", - "variations": [], - "tip": "Abend-Tipp Frisch und warm – ideal nach dem Training.", - "badge": "AIRFRYER", - "yield": "" - }, - { - "num": "80", - "category": "Mehr herzhafte Mini-Mahlzeiten", - "title": "Avocado-Ei-Boot", - "tagline": "Cremig, herzhaft, sättigend.", - "nutrition": "ca. ≈ 190 kcal · Eiweiß 8 g · Fett 16 g · KH 6 g · Zucker 1 g · Ballaststoffe 5 g", - "ingredients": "½ Avocado · 1 hartes Ei · Salz, Pfeffer, Chili", - "steps": "Avocado-Mulde nutzen, Ei klein schneiden, hineingeben, würzen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Gute Fette + Eiweiß; bewusst eine halbe Avocado.", - "badge": "", - "yield": "" - }, - { - "num": "81", - "category": "Mehr süße Löffel- & Obst-Snacks", - "title": "Skyr-Schoko-Crunch", - "tagline": "Schoko-Stückchen im cremigen Skyr.", - "nutrition": "ca. ≈ 145 kcal · Eiweiß 18 g · Fett 3 g · KH 11 g · Zucker 9 g · Ballaststoffe 1 g", - "ingredients": "150 g Skyr · 1 TL Kakao oder Süße · 5 g Zartbitter (gehackt)", - "steps": "Skyr (optional mit Kakao) cremig rühren, Schoko-Stücke unterheben.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Cremig + knackig – Schoko als Akzent, nicht als Tafel.", - "badge": "", - "yield": "" - }, - { - "num": "82", - "category": "Mehr süße Löffel- & Obst-Snacks", - "title": "Erdnuss-Schoko-Joghurt", - "tagline": "Wie ein Dessert, mit Eiweiß-Basis.", - "nutrition": "ca. ≈ 170 kcal · Eiweiß 19 g · Fett 5 g · KH 12 g · Zucker 9 g · Ballaststoffe 2 g", - "ingredients": "150 g Joghurt/Skyr · 1 TL Erdnussmus · 1 TL Kakao · etwas Süße", - "steps": "Alles verrühren, bis cremig.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp „Reese’s“-Gefühl ohne Riegel.", - "badge": "", - "yield": "" - }, - { - "num": "83", - "category": "Mehr süße Löffel- & Obst-Snacks", - "title": "Beeren-Chia-„Marmelade“ auf Quark", - "tagline": "Schnelle Marmelade ohne Zucker.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 19 g · Fett 2 g · KH 12 g · Zucker 9 g · Ballaststoffe 3 g", - "ingredients": "Handvoll TK-Beeren · 1 TL Chiasamen · 150 g Magerquark", - "steps": "Beeren warm zerdrücken, Chia unter, 5–10 Min andicken, über den Quark.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Fruchtig-süß auf Eiweiß-Basis, wenig Zucker.", - "badge": "", - "yield": "" - }, - { - "num": "84", - "category": "Mehr süße Löffel- & Obst-Snacks", - "title": "Magerquark mit Zimt & Röst-Haferflocken", - "tagline": "Cremig + warmer Crunch.", - "nutrition": "ca. ≈ 225 kcal · Eiweiß 27 g · Fett 2 g · KH 24 g · Zucker 12 g · Ballaststoffe 3 g", - "ingredients": "200 g Magerquark · Zimt · 2 EL kurz angeröstete Haferflocken · etwas Süße", - "steps": "Haferflocken trocken anrösten, über den gewürzten Quark geben.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Sättigt cremig, der Röst-Crunch macht's besonders.", - "badge": "", - "yield": "" - }, - { - "num": "85", - "category": "Mehr süße Löffel- & Obst-Snacks", - "title": "Bananensplit light", - "tagline": "Eisdielen-Optik, leicht gemacht.", - "nutrition": "ca. ≈ 245 kcal · Eiweiß 19 g · Fett 3 g · KH 40 g · Zucker 22 g · Ballaststoffe 4 g", - "ingredients": "1 Banane (längs) · 150 g Skyr · Beeren · 5 g Zartbitter-Raspel", - "steps": "Banane längs in die Schale, Skyr drauf, Beeren und Raspel verteilen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Bananensplit-Gefühl ohne Eis und Sahne.", - "badge": "", - "yield": "" - }, - { - "num": "86", - "category": "Mehr süße Löffel- & Obst-Snacks", - "title": "Quark-„Käsekuchen“ im Glas", - "tagline": "Käsekuchen-Geschmack, kein Backen.", - "nutrition": "ca. ≈ 185 kcal · Eiweiß 25 g · Fett 2 g · KH 18 g · Zucker 13 g · Ballaststoffe 1 g", - "ingredients": "200 g Magerquark · Vanille · etwas Süße · 1 zerbröselter Vollkornkeks", - "steps": "Quark mit Vanille/Süße cremig rühren, ins Glas, Keksbrösel als Boden/Topping.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Dessert-Feeling mit Eiweiß statt Sahne-Boden.", - "badge": "", - "yield": "" - }, - { - "num": "87", - "category": "Mehr süße Löffel- & Obst-Snacks", - "title": "Dattel-Kakao-Creme mit Obst", - "tagline": "Natürlich süße Schoko-Creme zum Dippen.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 3 g · Fett 3 g · KH 34 g · Zucker 27 g · Ballaststoffe 5 g", - "ingredients": "3–4 entkernte Datteln · 1 TL Kakao · Schuss Milch · Obst zum Dippen", - "steps": "Datteln mit Kakao und Milch pürieren, Obst hineintunken.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Schoko-Dip ohne Zuckerzusatz; Datteln sind süß – wenig reicht.", - "badge": "", - "yield": "" - }, - { - "num": "88", - "category": "Mehr süße Löffel- & Obst-Snacks", - "title": "Skyr-„Eiskaffee“", - "tagline": "Löffelbarer Eiskaffee als Süß-Stopper.", - "nutrition": "ca. ≈ 115 kcal · Eiweiß 18 g · Fett 1 g · KH 10 g · Zucker 8 g · Ballaststoffe 1 g", - "ingredients": "150 g Skyr · 1 Schuss kalter Espresso · etwas Süße · Prise Kakao", - "steps": "Skyr mit Espresso und Süße verrühren, Kakao darüber.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Koffein-Kick + Eiweiß; abends ggf. entkoffeiniert.", - "badge": "", - "yield": "" - }, - { - "num": "89", - "category": "Mehr Bowls & Frisches", - "title": "Hüttenkäse-Obst-Bowl", - "tagline": "Herzhaft trifft süß.", - "nutrition": "ca. ≈ 190 kcal · Eiweiß 21 g · Fett 7 g · KH 15 g · Zucker 11 g · Ballaststoffe 2 g", - "ingredients": "150 g Hüttenkäse · Beeren oder Ananasstücke · Zimt", - "steps": "Hüttenkäse in die Schale, Obst und Zimt darüber.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Cremig-körnig + fruchtig, viel Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "90", - "category": "Mehr Bowls & Frisches", - "title": "Skyr-Müsli-Bowl", - "tagline": "Knusper-Bowl ohne Zuckermüsli-Falle.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 25 g · Fett 2 g · KH 27 g · Zucker 15 g · Ballaststoffe 3 g", - "ingredients": "200 g Skyr · 2 EL Haferflocken/Müsli · Obst der Wahl", - "steps": "Skyr in die Schale, Haferflocken und Obst darauf.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Bewusst 2 EL Müsli – als Topping, nicht als Berg.", - "badge": "", - "yield": "" - }, - { - "num": "91", - "category": "Mehr Bowls & Frisches", - "title": "Quark-Bowl „PB&J“", - "tagline": "Erdnuss-Beere – die Wohlfühl-Kombi.", - "nutrition": "ca. ≈ 205 kcal · Eiweiß 26 g · Fett 5 g · KH 15 g · Zucker 9 g · Ballaststoffe 3 g", - "ingredients": "200 g Magerquark · 1 TL Erdnussmus · Handvoll Beeren", - "steps": "Quark in die Schale, Erdnussmus und Beeren einswirbeln.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Sättigend und süß; Erdnussmus sparsam.", - "badge": "", - "yield": "" - }, - { - "num": "92", - "category": "Mehr Bowls & Frisches", - "title": "Beeren-Smoothie-Bowl (dick)", - "tagline": "Dick und löffelbar statt zum Trinken.", - "nutrition": "ca. ≈ 175 kcal · Eiweiß 19 g · Fett 3 g · KH 20 g · Zucker 13 g · Ballaststoffe 3 g", - "ingredients": "150 g Skyr · Handvoll TK-Beeren · Schuss Milch · Topping", - "steps": "Skyr mit gefrorenen Beeren dick pürieren, in die Schale, Topping drauf.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Eis-Bowl-Gefühl, proteinreich, ohne Zuckerzusatz.", - "badge": "", - "yield": "" - }, - { - "num": "93", - "category": "Mehr Bowls & Frisches", - "title": "Melonen-Feta-Minz-Schale", - "tagline": "Süß-salzig-frisch.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 6 g · Fett 8 g · KH 13 g · Zucker 12 g · Ballaststoffe 1 g", - "ingredients": "Melonenwürfel · etwas Feta · frische Minze", - "steps": "Melone in die Schale, Feta zerbröseln, Minze darüber.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Erfrischend, leicht, überraschend befriedigend.", - "badge": "", - "yield": "" - }, - { - "num": "94", - "category": "Mehr Bowls & Frisches", - "title": "Caprese-Bowl", - "tagline": "Herzhaft-frische Schale.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 14 g · Fett 18 g · KH 8 g · Zucker 5 g · Ballaststoffe 1 g", - "ingredients": "Cherrytomaten · Mini-Mozzarella · Basilikum · Spritzer Balsamico/Olivenöl", - "steps": "Alles in eine Schale, Basilikum und Balsamico darüber.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Apéro-Gefühl als leichte Schale.", - "badge": "", - "yield": "" - }, - { - "num": "95", - "category": "Mehr warme Snacks & Getränke", - "title": "Heißer Apfel-Zimt-Drink", - "tagline": "Warmer, gemütlicher Absacker.", - "nutrition": "ca. ≈ 115 kcal · Eiweiß 1 g · Fett 0 g · KH 28 g · Zucker 26 g · Ballaststoffe 0 g", - "ingredients": "250 ml naturtrüber Apfelsaft (oder Apfelstücke + Wasser) · Zimt · 1 Nelke", - "steps": "Saft mit Zimt und Nelke kurz erwärmen (nicht kochen), Nelke entfernen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Warm und süß-würzig statt Cola am Abend.", - "badge": "", - "yield": "" - }, - { - "num": "96", - "category": "Mehr warme Snacks & Getränke", - "title": "Matcha-Latte (warm)", - "tagline": "Sanfter, grüner Wachmacher.", - "nutrition": "ca. ≈ 140 kcal · Eiweiß 9 g · Fett 4 g · KH 16 g · Zucker 15 g · Ballaststoffe 1 g", - "ingredients": "1 TL Matcha · 250 ml heiße Milch/Pflanzendrink · etwas Süße", - "steps": "Matcha mit wenig heißem Wasser anrühren, mit warmer Milch aufgießen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Cremig-warm; abends mild dosieren (enthält Koffein).", - "badge": "", - "yield": "" - }, - { - "num": "97", - "category": "Mehr warme Snacks & Getränke", - "title": "Warmer Beeren-Punsch", - "tagline": "Fruchtig-warm, alkoholfrei.", - "nutrition": "ca. ≈ 50 kcal · Eiweiß 1 g · Fett 0 g · KH 12 g · Zucker 11 g · Ballaststoffe 2 g", - "ingredients": "Handvoll TK-Beeren · 250 ml Wasser · Zimt, Sternanis · etwas Süße", - "steps": "Beeren mit Wasser und Gewürzen kurz erwärmen, leicht süßen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Wärmt und schmeckt nach Winter, fast ohne Zucker.", - "badge": "", - "yield": "" - }, - { - "num": "98", - "category": "Mehr warme Snacks & Getränke", - "title": "Heiße Zitrone-Ingwer-Honig", - "tagline": "Klassiker zum Runterkommen.", - "nutrition": "ca. ≈ 30 kcal · Eiweiß 0 g · Fett 0 g · KH 8 g · Zucker 6 g · Ballaststoffe 0 g", - "ingredients": "Saft ½ Zitrone · paar Scheiben Ingwer · heißes Wasser · 1 TL Honig", - "steps": "Zitrone und Ingwer mit heißem Wasser aufgießen, Honig einrühren.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Beruhigend und wärmend; Honig sparsam.", - "badge": "", - "yield": "" - }, - { - "num": "99", - "category": "Mehr warme Snacks & Getränke", - "title": "Warmer Schoko-Bananen-„Pudding“", - "tagline": "Warm, schokoladig, sättigend.", - "nutrition": "ca. ≈ 210 kcal · Eiweiß 6 g · Fett 3 g · KH 40 g · Zucker 16 g · Ballaststoffe 6 g", - "ingredients": "1 reife Banane · 1 TL Kakao · 2 EL Haferflocken · Schuss Milch", - "steps": "Banane zerdrücken, mit Kakao, Haferflocken und Milch verrühren, 1–2 Min in der Mikrowelle.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Warmer Schoko-Pudding ganz ohne Zuckerzusatz.", - "badge": "", - "yield": "" - }, - { - "num": "100", - "category": "Mehr warme Snacks & Getränke", - "title": "Kräutertee + 1 Stück Zartbitter", - "tagline": "Das bewusste Absacker-Ritual.", - "nutrition": "ca. ≈ 45 kcal · Eiweiß 1 g · Fett 3 g · KH 3 g · Zucker 1 g · Ballaststoffe 1 g", - "ingredients": "1 Kräutertee · 1 Stück Zartbitterschokolade (85 %+)", - "steps": "Tee aufgießen, EIN Stück Schokolade ganz bewusst dazu genießen.", - "methods": "", - "variations": [], - "tip": "Abend-Tipp Das Ritual ersetzt das Tüten-Grasen: ein Stück, achtsam – Schluss.", - "badge": "", - "yield": "" - } - ] - } -} \ No newline at end of file diff --git a/plugins/trainingsplan/presets/fruehstueck.json b/plugins/trainingsplan/presets/fruehstueck.json deleted file mode 100644 index a58d72e..0000000 --- a/plugins/trainingsplan/presets/fruehstueck.json +++ /dev/null @@ -1,1240 +0,0 @@ -{ - "name": "Frühstück (Morgen)", - "config": { - "type": "recipe", - "meta": { - "title": "Frühstück für den Morgen", - "header_left": "FRÜHSTÜCK · Morgen-Edition · Begleitend zum 8-Wochen-Plan", - "footer_left": "Eiweiß zuerst · satt bis Mittag · Trend über die Woche zählt", - "output_name": "Fruehstueck_Morgen.pdf", - "doc_title": "Frühstück für den Morgen", - "brand_color": "#6D28D9", - "steps_label": "SO GEHT'S" - }, - "intro": { - "headline": "Frühstück für den Morgen", - "subtitle": "83 Rezepte plus zahlreiche Variationen – schnell, abends vorbereitbar oder warm.", - "paragraphs": [ - "Nährwerte pro Portion als Richtwert. Nicht auslassen, sondern satt starten.", - "Diese Rezepte lösen dein Morgen-Problem: Ein Frühstück mit Eiweiß macht satt bis zum Mittag und dämpft den", - "Vormittags-Zucker im Büro – die heimlichen Snacks fallen leichter weg, wenn du nicht schon um zehn unterzuckert", - "vor der Schublade stehst. Der Hebel ist nicht „gesund oder ungesund“, sondern genug Eiweiß und eine echte", - "Portion. Quark, Skyr, Hüttenkäse, Eier, Milch und Haferflocken liefern Baustoff für die Regeneration nach dem", - "Training und halten lange satt. Wo ein Rezept einen VARIATIONEN-Block hat, ist das dieselbe Basis in mehreren", - "Geschmacksrichtungen – einmal lernen, nie langweilig. Viele Rezepte lassen sich abends vorbereiten, damit", - "morgens keine Ausrede bleibt. Rezepte mit AIRFRYER-Badge nennen im Block ZUBEREITUNG Zeit und", - "Temperatur für Airfryer und Ofen (Werte je Gerät leicht unterschiedlich – beim ersten Mal kurz kontrollieren). Jeder", - "Eintrag nennt unter NÄHRWERTE ca. grobe Richtwerte pro Portion – Kalorien plus alle Makros (Eiweiß, Fett, KH,", - "Zucker, Ballaststoffe). Das sind Schätzwerte zur Orientierung, kein Zwang zum Zählen – Eiweiß zuerst und eine" - ], - "rule_head": "echte Portion bleiben die Regel.", - "rule_text": "Die eine Grundregel: Eiweiß zuerst Bau das Frühstück um eine Eiweißquelle (Quark, Skyr, Ei, Hüttenkäse, Milch) und füll mit Hafer, Obst oder Gemüse auf. Ein satter Vormittag ist der beste Schutz gegen den Griff in die Süßigkeiten-Schublade um zehn – nicht Disziplin, sondern schlicht kein Hunger.", - "categories_head": "12 Kategorien" - }, - "categories": [ - { - "name": "Quark & Skyr · Löffel-Frühstück", - "color": "#7C3AED" - }, - { - "name": "Overnight Oats · abends vorbereiten", - "color": "#0EA5E9" - }, - { - "name": "Warmer Porridge & Getreidebrei", - "color": "#EA580C" - }, - { - "name": "Eier · herzhaft & proteinreich", - "color": "#16A34A" - }, - { - "name": "Herzhaftes Brot & Belag", - "color": "#DC2626" - }, - { - "name": "Pfannkuchen, Waffeln & Protein-Backwerk", - "color": "#DB2777" - }, - { - "name": "Smoothies & Trink-Frühstück", - "color": "#B45309" - }, - { - "name": "Frisch & schnell · unter 5 Minuten", - "color": "#4338CA" - }, - { - "name": "Airfryer · warm & knusprig", - "color": "#2563EB" - }, - { - "name": "Mehr Löffel- & Joghurt-Frühstück", - "color": "#475569" - }, - { - "name": "Mehr herzhafte Frühstücke", - "color": "#A21CAF" - }, - { - "name": "Mehr warme & schnelle Frühstücke", - "color": "#059669" - } - ], - "recipes": [ - { - "num": "01", - "category": "Quark & Skyr · Löffel-Frühstück", - "title": "Quark- & Skyr-Bowl · Basis", - "tagline": "Kalt gerührt, in 3 Minuten fertig.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 30 g · Fett 6 g · KH 18 g · Zucker 10 g · Ballaststoffe 3 g", - "ingredients": "200 g Magerquark oder Skyr · Schuss Milch · 1 EL Haferflocken oder Nüsse · Topping nach Wahl", - "steps": "Quark/Skyr mit Milch cremig rühren, Topping unterheben oder drüberstreuen.", - "variations": [ - "Beeren-Crunch Beeren + 1 EL Hafer + 1 EL Nüsse", - "Schoko-Banane 1 TL Backkakao + 1/2 Banane", - "Erdnuss-Banane 1 EL Erdnussmus + 1/2 Banane", - "Kirsch-Mandel TK-Kirschen + 1 EL Mandeln", - "Kaffee-Proffee Schuss kalter Espresso + 1 TL Kakao", - "Als Parfait im Glas mit Beeren und Hafer schichten" - ], - "tip": "Morgen-Tipp Skyr oder Magerquark bringen 20–25 g Eiweiß pro Becher – der beste Sattmacher am Morgen.", - "badge": "", - "yield": "" - }, - { - "num": "02", - "category": "Quark & Skyr · Löffel-Frühstück", - "title": "Hüttenkäse-Becher süß", - "tagline": "Der unterschätzte Eiweiß-Löffel.", - "nutrition": "ca. ≈ 320 kcal · Eiweiß 28 g · Fett 13 g · KH 18 g · Zucker 14 g · Ballaststoffe 3 g", - "ingredients": "200 g Hüttenkäse · Obst nach Wahl · 1 EL Nüsse · optional 1 TL Honig", - "steps": "Hüttenkäse in die Schale, Obst und Nüsse drüber, dünn süßen.", - "variations": [ - "Beeren-Honig Beeren + 1 TL Honig + Walnüsse", - "Zimt-Apfel gewürfelter Apfel + Zimt + Rosinen", - "Ananas Ananasstücke – klassisch herzhaft-süß" - ], - "tip": "Morgen-Tipp Ohne Kochen in unter 3 Minuten fertig – ideal, wenn morgens keine Zeit ist.", - "badge": "", - "yield": "" - }, - { - "num": "03", - "category": "Quark & Skyr · Löffel-Frühstück", - "title": "Vanille-Skyr mit warmem Apfel", - "tagline": "Warmes Obst trifft kühlen Skyr.", - "nutrition": "ca. ≈ 280 kcal · Eiweiß 20 g · Fett 5 g · KH 38 g · Zucker 22 g · Ballaststoffe 4 g", - "ingredients": "150–200 g Skyr · 1 Apfel gewürfelt · Zimt · 1 EL Haferflocken · optional Nüsse", - "steps": "Apfel mit Zimt in der Pfanne weich dünsten, auf den kalten Skyr geben, Hafer drüber.", - "variations": [ - "Birne Birne statt Apfel", - "Warme Beeren TK-Beeren kurz erwärmen statt Apfel", - "Bratbanane Banane in der Pfanne karamellisieren" - ], - "tip": "Morgen-Tipp Gedünstetes Obst süßt von allein – der warm-kalt-Kontrast macht satt und zufrieden.", - "badge": "", - "yield": "" - }, - { - "num": "04", - "category": "Overnight Oats · abends vorbereiten", - "title": "Overnight Oats · Basis", - "tagline": "Abends 2 Minuten, morgens null Ausrede.", - "nutrition": "ca. ≈ 340 kcal · Eiweiß 24 g · Fett 8 g · KH 43 g · Zucker 12 g · Ballaststoffe 7 g", - "ingredients": "50 g Haferflocken · 150 ml Milch/Pflanzendrink · 100 g Skyr oder Quark · 1 TL Chiasamen", - "steps": "Alles im Glas verrühren, über Nacht kühlen; morgens Topping drauf.", - "variations": [ - "Schoko-Banane 1 TL Kakao + 1/2 zerdrückte Banane", - "Apfel-Zimt 1 geriebener Apfel + Zimt", - "Beeren-Vanille Handvoll TK-Beeren + Vanille", - "Erdnuss 1 EL Erdnussmus", - "Kaffee-Proffee Schuss kalter Espresso + Kakao", - "Kokos-Mango Mangostücke + 1 EL Kokosraspel", - "Karottenkuchen 1 geriebene Karotte + Zimt + Walnüsse + Rosinen" - ], - "tip": "Morgen-Tipp Skyr oder Quark mit rein hebt das Eiweiß deutlich – so hält das Glas bis Mittag.", - "badge": "", - "yield": "" - }, - { - "num": "05", - "category": "Overnight Oats · abends vorbereiten", - "title": "Bircher-Müsli", - "tagline": "Der Schweizer Ur-Klassiker mit frischem Apfel.", - "nutrition": "ca. ≈ 360 kcal · Eiweiß 12 g · Fett 10 g · KH 50 g · Zucker 22 g · Ballaststoffe 7 g", - "ingredients": "50 g Haferflocken · 150 ml Milch · 1 geriebener Apfel · 1 EL Zitronensaft · 1 EL Nüsse · optional Quark", - "steps": "Apfel reiben, sofort mit Zitrone mischen, mit Hafer und Milch über Nacht ziehen lassen.", - "variations": [ - "Beeren-Bircher zusätzlich Handvoll Beeren", - "Protein-Bircher 100 g Quark/Skyr unterrühren" - ], - "tip": "Morgen-Tipp Zitrone hält den Apfel hell und bringt Frische. Für mehr Eiweiß Quark dazu.", - "badge": "", - "yield": "" - }, - { - "num": "06", - "category": "Warmer Porridge & Getreidebrei", - "title": "Haferporridge · Basis", - "tagline": "Warm, günstig, endlos variierbar.", - "nutrition": "ca. ≈ 300 kcal · Eiweiß 15 g · Fett 8 g · KH 42 g · Zucker 12 g · Ballaststoffe 5 g", - "ingredients": "50 g Haferflocken · 250 ml Milch/Wasser · Prise Salz", - "steps": "Hafer mit Flüssigkeit 3–5 Min cremig köcheln; nach Wunsch toppen.", - "variations": [ - "Schoko-Protein 1 TL Kakao mitkochen, nach dem Kochen 1 Portion Skyr/Quark einrühren", - "Apfel-Zimt geriebenen Apfel + Zimt mitkochen", - "Beere TK-Beeren die letzten 2 Min mitkochen", - "Erdnuss-Banane 1 EL Erdnussmus + Bananenscheiben" - ], - "tip": "Morgen-Tipp Eine Prise Salz macht Haferbrei erst rund. Quark erst nach dem Kochen einrühren, sonst gerinnt er.", - "badge": "", - "yield": "" - }, - { - "num": "07", - "category": "Warmer Porridge & Getreidebrei", - "title": "Herzhafter Porridge mit Ei", - "tagline": "Wer's morgens nicht süß mag.", - "nutrition": "ca. ≈ 375 kcal · Eiweiß 21 g · Fett 18 g · KH 31 g · Zucker 2 g · Ballaststoffe 5 g", - "ingredients": "50 g Haferflocken · 250 ml Wasser/Brühe · 1 Ei · geriebener Käse · Pfeffer", - "steps": "Hafer in Brühe köcheln, ein Ei unterrühren, Käse drauf schmelzen lassen.", - "variations": [ - "Mit Lachs geräucherten Lachs + Schnittlauch", - "Mit Spinat eine Handvoll Blattspinat unterrühren" - ], - "tip": "Morgen-Tipp Herzhafte Oats sind unterschätzt und mega sättigend.", - "badge": "", - "yield": "" - }, - { - "num": "08", - "category": "Warmer Porridge & Getreidebrei", - "title": "Protein-Grießbrei", - "tagline": "Kindheit trifft Eiweiß.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 18 g · Fett 5 g · KH 41 g · Zucker 16 g · Ballaststoffe 3 g", - "ingredients": "3 EL Weichweizengrieß · 250 ml Milch · etwas Süße · optional 1 Portion Quark", - "steps": "Grieß in heiße Milch einrühren, kurz quellen lassen; Obst dazu.", - "variations": [ - "Beeren-Grieß TK-Beeren unterrühren", - "Schoko-Grieß 1 TL Kakao einrühren" - ], - "tip": "Morgen-Tipp Für mehr Eiweiß nach dem Quellen etwas Quark unterheben.", - "badge": "", - "yield": "" - }, - { - "num": "09", - "category": "Warmer Porridge & Getreidebrei", - "title": "Quinoa-Frühstücksbrei", - "tagline": "Glutenfreie Abwechslung.", - "nutrition": "ca. ≈ 350 kcal · Eiweiß 14 g · Fett 11 g · KH 46 g · Zucker 16 g · Ballaststoffe 6 g", - "ingredients": "50 g Quinoa · 200 ml Milch · Zimt · Beeren · 1 EL Nüsse", - "steps": "Quinoa gründlich spülen, in Milch 15 Min weich köcheln; toppen.", - "variations": [ - "Schoko-Quinoa 1 TL Kakao einrühren", - "Apfel-Zimt geriebenen Apfel + Zimt" - ], - "tip": "Morgen-Tipp Gleich die doppelte Menge kochen – hält 3 Tage im Kühlschrank.", - "badge": "", - "yield": "" - }, - { - "num": "10", - "category": "Warmer Porridge & Getreidebrei", - "title": "Hirse-Brei mit Beeren", - "tagline": "Mineralstoff-Booster.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 13 g · Fett 6 g · KH 52 g · Zucker 18 g · Ballaststoffe 4 g", - "ingredients": "50 g Hirse · 250 ml Milch · Zimt · Beeren · etwas Süße", - "steps": "Hirse spülen, in Milch ca. 20 Min weich köcheln, Beeren unterrühren.", - "variations": [ - "Mit Apfel gewürfelten Apfel mitkochen", - "Nuss-Honig 1 EL Nüsse + 1 TL Honig" - ], - "tip": "Morgen-Tipp Hirse liefert Eisen und Magnesium – gute Ergänzung zum Hafer.", - "badge": "", - "yield": "" - }, - { - "num": "11", - "category": "Warmer Porridge & Getreidebrei", - "title": "Couscous-Frühstück süß", - "tagline": "In 5 Minuten fertig, ohne Kochen.", - "nutrition": "ca. ≈ 360 kcal · Eiweiß 12 g · Fett 8 g · KH 58 g · Zucker 19 g · Ballaststoffe 5 g", - "ingredients": "50 g Couscous · 150 ml heiße Milch · Zimt · Apfel/Beeren · 1 EL Nüsse", - "steps": "Couscous mit heißer Milch übergießen, 5 Min ziehen lassen, auflockern, Obst drauf.", - "variations": [ - "Herzhaft mit Hüttenkäse + Tomate statt Obst", - "Schoko 1 TL Kakao + etwas Süße" - ], - "tip": "Morgen-Tipp Schnellste warme Getreide-Basis überhaupt – nur Quellen statt Kochen.", - "badge": "", - "yield": "" - }, - { - "num": "12", - "category": "Eier · herzhaft & proteinreich", - "title": "Rührei · Basis", - "tagline": "Der Eiweiß-Anker am Morgen.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 19 g · Fett 19 g · KH 2 g · Zucker 2 g · Ballaststoffe 0 g", - "ingredients": "2–3 Eier · Schuss Milch · Salz, Pfeffer · 1 TL Öl/Butter", - "steps": "Eier verquirlen, in der Pfanne bei mittlerer Hitze langsam stocken lassen, cremig ziehen.", - "variations": [ - "Käse gegen Ende geriebenen Käse einrühren", - "Kräuter Schnittlauch/Petersilie unter", - "Gemüse Paprika/Spinat mitbraten", - "Lachs Räucherlachs am Ende dazu" - ], - "tip": "Morgen-Tipp Niedrige Hitze und Geduld machen Rührei cremig statt trocken.", - "badge": "", - "yield": "" - }, - { - "num": "13", - "category": "Eier · herzhaft & proteinreich", - "title": "Spiegelei auf Vollkorntoast", - "tagline": "Klassiker mit Biss.", - "nutrition": "ca. ≈ 260 kcal · Eiweiß 16 g · Fett 13 g · KH 18 g · Zucker 2 g · Ballaststoffe 3 g", - "ingredients": "1–2 Eier · 1 Scheibe Vollkornbrot · Salz, Pfeffer · optional Tomate", - "steps": "Ei brutzeln, bis das Weiße fest und das Gelbe noch flüssig ist, auf Toast legen.", - "variations": [], - "tip": "Morgen-Tipp Vollkorn statt Weißbrot hält den Vormittag über satt.", - "badge": "", - "yield": "" - }, - { - "num": "14", - "category": "Eier · herzhaft & proteinreich", - "title": "Gemüse-Omelett", - "tagline": "Sattes Frühstück, das nach Mittag schmeckt.", - "nutrition": "ca. ≈ 360 kcal · Eiweiß 26 g · Fett 26 g · KH 6 g · Zucker 4 g · Ballaststoffe 2 g", - "ingredients": "3 Eier · Handvoll Gemüse (Paprika, Zwiebel, Spinat) · Käse · Öl", - "steps": "Gemüse anbraten, verquirlte Eier drüber, stocken lassen, zusammenklappen.", - "variations": [], - "tip": "Morgen-Tipp Reste-Gemüse vom Vortag landet perfekt im Omelett.", - "badge": "", - "yield": "" - }, - { - "num": "15", - "category": "Eier · herzhaft & proteinreich", - "title": "Shakshuka", - "tagline": "Eier in würziger Tomatensauce.", - "nutrition": "ca. ≈ 280 kcal · Eiweiß 18 g · Fett 15 g · KH 18 g · Zucker 10 g · Ballaststoffe 4 g", - "ingredients": "2 Eier · 1 Dose Tomaten · 1/2 Zwiebel · Paprika · Kreuzkümmel · Knoblauch", - "steps": "Zwiebel, Paprika, Tomaten einköcheln, würzen, Eier reinschlagen, zugedeckt stocken.", - "variations": [], - "tip": "Morgen-Tipp Am Wochenende, wenn Zeit ist – herzhaft und ohne viel Fett.", - "badge": "", - "yield": "" - }, - { - "num": "16", - "category": "Eier · herzhaft & proteinreich", - "title": "Ei auf Avocado-Toast", - "tagline": "Modern-Klassiker, richtig sättigend.", - "nutrition": "ca. ≈ 340 kcal · Eiweiß 13 g · Fett 23 g · KH 20 g · Zucker 3 g · Ballaststoffe 7 g", - "ingredients": "1 Scheibe Vollkornbrot · 1/2 Avocado · 1 Ei (pochiert/gekocht) · Chili, Salz", - "steps": "Avocado auf Toast zerdrücken, Ei drauf, würzen.", - "variations": [], - "tip": "Morgen-Tipp Gute Fette plus Eiweiß – hält lange, aber Avocado in Maßen.", - "badge": "", - "yield": "" - }, - { - "num": "17", - "category": "Eier · herzhaft & proteinreich", - "title": "Frühstücks-Wrap mit Ei", - "tagline": "Grab-and-go für unterwegs.", - "nutrition": "ca. ≈ 400 kcal · Eiweiß 24 g · Fett 22 g · KH 30 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "1 Vollkorn-Wrap · 2 Eier (Rührei) · Käse · Handvoll Gemüse/Spinat", - "steps": "Rührei zubereiten, mit Käse und Gemüse in den Wrap, einrollen.", - "variations": [], - "tip": "Morgen-Tipp Kalt genauso gut – abends machen, morgens mitnehmen.", - "badge": "", - "yield": "" - }, - { - "num": "18", - "category": "Eier · herzhaft & proteinreich", - "title": "Eiersalat-Brot", - "tagline": "Meal-Prep-Held der Woche.", - "nutrition": "ca. ≈ 300 kcal · Eiweiß 20 g · Fett 15 g · KH 20 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "2 gekochte Eier · 1 EL Skyr/Joghurt · Senf · Schnittlauch · Vollkornbrot", - "steps": "Eier zerdrücken, mit Skyr und Senf mischen, auf Brot streichen.", - "variations": [], - "tip": "Morgen-Tipp Skyr statt Mayo spart Fett und bringt Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "19", - "category": "Eier · herzhaft & proteinreich", - "title": "Gekochte Eier + Brot", - "tagline": "Simpler geht Eiweiß nicht.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 18 g · Fett 14 g · KH 34 g · Zucker 3 g · Ballaststoffe 5 g", - "ingredients": "2 gekochte Eier · 1–2 Scheiben Vollkornbrot · Butter dünn · Salz", - "steps": "Eier am Wochenanfang kochen, morgens pellen, mit Brot essen.", - "variations": [], - "tip": "Morgen-Tipp 6 Eier auf Vorrat kochen – 3 Frühstücke in 10 Minuten erledigt.", - "badge": "", - "yield": "" - }, - { - "num": "20", - "category": "Eier · herzhaft & proteinreich", - "title": "Frittata-Muffins", - "tagline": "Vorbereiten, einfrieren, aufwärmen.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 15 g · Fett 14 g · KH 4 g · Zucker 3 g · Ballaststoffe 1 g", - "ingredients": "6 Eier · Handvoll Gemüse gewürfelt · Käse · Salz, Pfeffer · Muffinform", - "steps": "Eier verquirlen, mit Gemüse in die Form, 18–20 Min bei 180 °C backen.", - "variations": [], - "tip": "Morgen-Tipp Batch kochen: 6 Muffins = 3 Frühstücke, halten 4 Tage.", - "badge": "", - "yield": "" - }, - { - "num": "21", - "category": "Eier · herzhaft & proteinreich", - "title": "Herzhafte Ei-Haferflocken-Pfannkuchen", - "tagline": "Oats mal salzig.", - "nutrition": "ca. ≈ 370 kcal · Eiweiß 24 g · Fett 20 g · KH 26 g · Zucker 2 g · Ballaststoffe 4 g", - "ingredients": "40 g Haferflocken · 2 Eier · Käse · Kräuter · Salz", - "steps": "Alles verrühren, wie kleine Pfannkuchen in wenig Öl ausbacken.", - "variations": [], - "tip": "Morgen-Tipp Sättigt wie ein Mini-Gericht – ideal nach früher Session.", - "badge": "", - "yield": "" - }, - { - "num": "22", - "category": "Herzhaftes Brot & Belag", - "title": "Hüttenkäse-Brot", - "tagline": "Cremiger Eiweiß-Belag statt Wurst.", - "nutrition": "ca. ≈ 260 kcal · Eiweiß 18 g · Fett 6 g · KH 34 g · Zucker 4 g · Ballaststoffe 5 g", - "ingredients": "1–2 Scheiben Vollkornbrot · 100 g Hüttenkäse · Radieschen/Gurke · Pfeffer, Schnittlauch", - "steps": "Hüttenkäse aufs Brot, Gemüse drauf, würzen.", - "variations": [ - "Tomate-Basilikum Tomatenscheiben + Basilikum", - "Kresse Kresse + etwas Salz", - "Paprika Paprikastreifen + Paprikapulver" - ], - "tip": "Morgen-Tipp Hüttenkäse schlägt jeden Frischkäse beim Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "23", - "category": "Herzhaftes Brot & Belag", - "title": "Avocado-Ei-Brot", - "tagline": "Sattmacher-Kombi.", - "nutrition": "ca. ≈ 340 kcal · Eiweiß 13 g · Fett 23 g · KH 20 g · Zucker 2 g · Ballaststoffe 7 g", - "ingredients": "1 Scheibe Vollkornbrot · 1/2 Avocado · 1 gekochtes Ei · Chili, Salz", - "steps": "Avocado zerdrücken, aufs Brot, Ei in Scheiben drauf, würzen.", - "variations": [], - "tip": "Morgen-Tipp Halbe Avocado reicht – ganz wird's schnell fettig.", - "badge": "", - "yield": "" - }, - { - "num": "24", - "category": "Herzhaftes Brot & Belag", - "title": "Lachs-Frischkäse-Brot", - "tagline": "Sonntags-Feeling, werktags machbar.", - "nutrition": "ca. ≈ 270 kcal · Eiweiß 16 g · Fett 14 g · KH 19 g · Zucker 2 g · Ballaststoffe 3 g", - "ingredients": "1 Scheibe Vollkornbrot · 1 EL Frischkäse · Räucherlachs · Zitrone, Dill", - "steps": "Frischkäse streichen, Lachs auflegen, Zitrone drüber.", - "variations": [], - "tip": "Morgen-Tipp Lachs liefert Eiweiß und Omega-3 – gute Abwechslung zum Ei.", - "badge": "", - "yield": "" - }, - { - "num": "25", - "category": "Herzhaftes Brot & Belag", - "title": "Kräuter-Frischkäse mit Tomate", - "tagline": "Frisch und schnell.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 12 g · Fett 5 g · KH 34 g · Zucker 5 g · Ballaststoffe 4 g", - "ingredients": "1–2 Scheiben Vollkornbrot · Kräuterquark/Frischkäse · Tomate · Pfeffer", - "steps": "Aufstrich streichen, Tomatenscheiben drauf, salzen und pfeffern.", - "variations": [], - "tip": "Morgen-Tipp Kräuterquark statt Frischkäse = mehr Eiweiß, weniger Fett.", - "badge": "", - "yield": "" - }, - { - "num": "26", - "category": "Herzhaftes Brot & Belag", - "title": "Harzer-Käse-Brot", - "tagline": "Der Eiweiß-König unter den Käsen.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 34 g · Fett 3 g · KH 20 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "1 Scheibe Vollkornbrot · 1 Harzer Käse · Zwiebel · Kümmel · Senf", - "steps": "Käse aufs Brot, Zwiebelringe und Kümmel drauf.", - "variations": [], - "tip": "Morgen-Tipp Harzer hat fast kein Fett und extrem viel Eiweiß – Geschmackssache, aber top.", - "badge": "", - "yield": "" - }, - { - "num": "27", - "category": "Herzhaftes Brot & Belag", - "title": "Thunfisch-Brot", - "tagline": "Herzhaft, günstig, proteinreich.", - "nutrition": "ca. ≈ 280 kcal · Eiweiß 34 g · Fett 4 g · KH 20 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "1 Dose Thunfisch (Wasser) · 1 EL Skyr/Joghurt · Zwiebel · Vollkornbrot", - "steps": "Thunfisch abtropfen, mit Skyr und Zwiebel mischen, aufs Brot.", - "variations": [], - "tip": "Morgen-Tipp Thunfisch im eigenen Saft, nicht in Öl – spart unnötiges Fett.", - "badge": "", - "yield": "" - }, - { - "num": "28", - "category": "Herzhaftes Brot & Belag", - "title": "Skyr-Dip mit Gemüsesticks", - "tagline": "Frühstück zum Dippen.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 18 g · Fett 3 g · KH 26 g · Zucker 10 g · Ballaststoffe 5 g", - "ingredients": "150 g Skyr · Kräuter · Knoblauch · Salz · Gurke, Paprika, Karotte", - "steps": "Skyr würzen, Gemüse in Sticks schneiden und dippen; dazu 1 Scheibe Brot.", - "variations": [], - "tip": "Morgen-Tipp Wer morgens nicht auf Brot steht: rohes Gemüse plus Eiweiß-Dip.", - "badge": "", - "yield": "" - }, - { - "num": "29", - "category": "Herzhaftes Brot & Belag", - "title": "Erdnussbutter-Bananen-Brot", - "tagline": "Süß-herzhafte Brücke.", - "nutrition": "ca. ≈ 290 kcal · Eiweiß 9 g · Fett 12 g · KH 34 g · Zucker 10 g · Ballaststoffe 5 g", - "ingredients": "1 Scheibe Vollkornbrot · 1 EL Erdnussmus · 1/2 Banane · Zimt", - "steps": "Erdnussmus streichen, Bananenscheiben drauf, Zimt darüber.", - "variations": [], - "tip": "Morgen-Tipp Nimm 100 % Erdnussmus ohne Zucker und Palmöl.", - "badge": "", - "yield": "" - }, - { - "num": "30", - "category": "Herzhaftes Brot & Belag", - "title": "Bohnen- oder Hummus-Brot", - "tagline": "Pflanzliches Eiweiß am Morgen.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 8 g · Fett 9 g · KH 30 g · Zucker 3 g · Ballaststoffe 7 g", - "ingredients": "1 Scheibe Vollkornbrot · 2 EL Hummus/Bohnenpüree · Tomate · Paprikapulver", - "steps": "Hummus streichen, Tomate drauf, würzen.", - "variations": [], - "tip": "Morgen-Tipp Kichererbsen und Bohnen liefern Eiweiß plus jede Menge Ballaststoffe.", - "badge": "", - "yield": "" - }, - { - "num": "31", - "category": "Pfannkuchen, Waffeln & Protein-Backwerk", - "title": "Protein-Pancakes · Basis", - "tagline": "Fluffig, ohne Backmischung.", - "nutrition": "ca. ≈ 360 kcal · Eiweiß 20 g · Fett 14 g · KH 40 g · Zucker 14 g · Ballaststoffe 5 g", - "ingredients": "1 Banane · 2 Eier · 40 g Haferflocken · Prise Backpulver · etwas Zimt", - "steps": "Alles pürieren, kleine Pfannkuchen bei mittlerer Hitze ausbacken.", - "variations": [ - "Schoko + 1 TL Kakao in den Teig", - "Beere TK-Beeren in den Teig", - "Protein 1 Löffel Proteinpulver, dafür weniger Hafer", - "Apfel-Zimt geriebener Apfel + mehr Zimt" - ], - "tip": "Morgen-Tipp Reife Banane süßt genug – kein extra Zucker nötig.", - "badge": "", - "yield": "" - }, - { - "num": "32", - "category": "Pfannkuchen, Waffeln & Protein-Backwerk", - "title": "Quark-Pfannkuchen", - "tagline": "Saftig und eiweißreich.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 26 g · Fett 14 g · KH 24 g · Zucker 6 g · Ballaststoffe 3 g", - "ingredients": "100 g Quark · 2 Eier · 30 g Haferflocken/Mehl · etwas Süße · Backpulver", - "steps": "Teig verrühren, in wenig Öl goldbraun ausbacken.", - "variations": [], - "tip": "Morgen-Tipp Quark macht die Pfannkuchen saftig, nicht trocken.", - "badge": "", - "yield": "" - }, - { - "num": "33", - "category": "Pfannkuchen, Waffeln & Protein-Backwerk", - "title": "Haferflocken-Waffeln", - "tagline": "Wenn ein Waffeleisen da ist.", - "nutrition": "ca. ≈ 400 kcal · Eiweiß 28 g · Fett 16 g · KH 34 g · Zucker 6 g · Ballaststoffe 5 g", - "ingredients": "50 g Haferflocken (fein) · 2 Eier · 100 g Quark · Backpulver · Vanille", - "steps": "Teig verrühren, portionsweise im Waffeleisen backen.", - "variations": [], - "tip": "Morgen-Tipp Doppelte Menge backen und einfrieren – toasten reicht dann morgens.", - "badge": "", - "yield": "" - }, - { - "num": "34", - "category": "Pfannkuchen, Waffeln & Protein-Backwerk", - "title": "Bananenbrot-Muffins", - "tagline": "Vorrat für die ganze Woche.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 9 g · Fett 8 g · KH 38 g · Zucker 12 g · Ballaststoffe 4 g", - "ingredients": "2 reife Bananen · 2 Eier · 150 g Haferflocken · Backpulver · Zimt · Nüsse", - "steps": "Bananen zerdrücken, alles mischen, in Muffinform 20 Min bei 180 °C backen.", - "variations": [], - "tip": "Morgen-Tipp Ohne Zucker – die Bananen tragen die Süße.", - "badge": "", - "yield": "" - }, - { - "num": "35", - "category": "Pfannkuchen, Waffeln & Protein-Backwerk", - "title": "Skyr-Pancakes", - "tagline": "Extra Eiweiß, extra fluffig.", - "nutrition": "ca. ≈ 370 kcal · Eiweiß 26 g · Fett 13 g · KH 34 g · Zucker 7 g · Ballaststoffe 3 g", - "ingredients": "150 g Skyr · 2 Eier · 40 g Mehl/Hafer · Backpulver · Süße", - "steps": "Verrühren, kleine Pfannkuchen backen; mit Beeren servieren.", - "variations": [], - "tip": "Morgen-Tipp Mit Skyr statt Milch wird der Teig cremig und proteinreich.", - "badge": "", - "yield": "" - }, - { - "num": "36", - "category": "Pfannkuchen, Waffeln & Protein-Backwerk", - "title": "Beeren-Waffeln", - "tagline": "Sonntagsfrühstück, das reinpasst.", - "nutrition": "ca. ≈ 410 kcal · Eiweiß 28 g · Fett 16 g · KH 38 g · Zucker 9 g · Ballaststoffe 5 g", - "ingredients": "50 g Hafer · 2 Eier · 100 g Quark · TK-Beeren · Backpulver", - "steps": "Beeren vorsichtig unterheben, im Waffeleisen backen.", - "variations": [], - "tip": "Morgen-Tipp Beeren erst zum Schluss unter den Teig, sonst zerlaufen sie.", - "badge": "", - "yield": "" - }, - { - "num": "37", - "category": "Pfannkuchen, Waffeln & Protein-Backwerk", - "title": "Protein-Kaiserschmarrn", - "tagline": "Österreich, aber alltagstauglich.", - "nutrition": "ca. ≈ 350 kcal · Eiweiß 26 g · Fett 14 g · KH 30 g · Zucker 8 g · Ballaststoffe 4 g", - "ingredients": "100 g Quark · 2 Eier · 30 g Hafer · Süße · Backpulver · TK-Beeren", - "steps": "Teig in die Pfanne, stocken lassen, zerreißen und kurz weiterbraten.", - "variations": [], - "tip": "Morgen-Tipp Ein Löffel Puderzucker als Deko reicht – nicht die ganze Dose.", - "badge": "", - "yield": "" - }, - { - "num": "38", - "category": "Pfannkuchen, Waffeln & Protein-Backwerk", - "title": "3-Zutaten-Bananen-Ei-Pfannkuchen", - "tagline": "Minimalismus, der funktioniert.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 14 g · Fett 11 g · KH 26 g · Zucker 14 g · Ballaststoffe 3 g", - "ingredients": "1 Banane · 2 Eier · Prise Zimt", - "steps": "Banane zerdrücken, mit Ei verrühren, kleine Pfannkuchen vorsichtig backen.", - "variations": [], - "tip": "Morgen-Tipp Wenden erst, wenn die Unterseite fest ist – sonst reißt es.", - "badge": "", - "yield": "" - }, - { - "num": "39", - "category": "Smoothies & Trink-Frühstück", - "title": "Protein-Beeren-Smoothie", - "tagline": "Frühstück zum Trinken, wenn's schnell muss.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 24 g · Fett 6 g · KH 45 g · Zucker 24 g · Ballaststoffe 5 g", - "ingredients": "150 g Skyr/Quark · Handvoll TK-Beeren · 1/2 Banane · 150 ml Milch · Haferflocken", - "steps": "Alles in den Mixer, cremig mixen.", - "variations": [ - "Grün Handvoll Spinat dazu", - "Schoko + 1 TL Kakao", - "Mango Mango statt Beeren", - "Erdnuss + 1 EL Erdnussmus" - ], - "tip": "Morgen-Tipp Trinken ersetzt kein Kauen dauerhaft – aber besser als Frühstück auslassen.", - "badge": "", - "yield": "" - }, - { - "num": "40", - "category": "Smoothies & Trink-Frühstück", - "title": "Grüner Smoothie", - "tagline": "Gemüse, das man nicht schmeckt.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 18 g · Fett 2 g · KH 34 g · Zucker 22 g · Ballaststoffe 4 g", - "ingredients": "Handvoll Spinat · 1/2 Banane · 150 g Skyr · 150 ml Wasser/Milch · 1/2 Apfel", - "steps": "Alles mixen, bis glatt.", - "variations": [], - "tip": "Morgen-Tipp Banane und Apfel übertönen den Spinat komplett.", - "badge": "", - "yield": "" - }, - { - "num": "41", - "category": "Smoothies & Trink-Frühstück", - "title": "Schoko-Erdnuss-Shake", - "tagline": "Schmeckt wie Milchshake.", - "nutrition": "ca. ≈ 400 kcal · Eiweiß 16 g · Fett 18 g · KH 44 g · Zucker 28 g · Ballaststoffe 4 g", - "ingredients": "250 ml Milch · 1 Banane · 1 EL Erdnussmus · 1 TL Kakao · optional Proteinpulver", - "steps": "Alles mixen.", - "variations": [], - "tip": "Morgen-Tipp Nach früher Session ideal: flüssig, aber sättigend.", - "badge": "", - "yield": "" - }, - { - "num": "42", - "category": "Smoothies & Trink-Frühstück", - "title": "Proffee (Kaffee-Protein-Shake)", - "tagline": "Kaffee und Frühstück in einem Glas.", - "nutrition": "ca. ≈ 220 kcal · Eiweiß 24 g · Fett 4 g · KH 16 g · Zucker 12 g · Ballaststoffe 0 g", - "ingredients": "1 Espresso/starker Kaffee (kalt) · 200 ml Milch · 1 Portion Quark/Proteinpulver · Eis", - "steps": "Alles mit Eis mixen.", - "variations": [], - "tip": "Morgen-Tipp Der wache Zwilling zum warmen Kaffee – plus rund 20 g Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "43", - "category": "Smoothies & Trink-Frühstück", - "title": "Mango-Kokos-Smoothie", - "tagline": "Urlaub im Glas.", - "nutrition": "ca. ≈ 270 kcal · Eiweiß 18 g · Fett 4 g · KH 40 g · Zucker 26 g · Ballaststoffe 3 g", - "ingredients": "Mangostücke · 150 g Skyr · 100 ml Kokoswasser/Milch · Limette", - "steps": "Alles mixen; Limette bringt Frische.", - "variations": [], - "tip": "Morgen-Tipp TK-Mango macht den Smoothie schön cremig-kalt.", - "badge": "", - "yield": "" - }, - { - "num": "44", - "category": "Smoothies & Trink-Frühstück", - "title": "Bananen-Hafer-Smoothie", - "tagline": "Der sättigende unter den Smoothies.", - "nutrition": "ca. ≈ 380 kcal · Eiweiß 14 g · Fett 8 g · KH 60 g · Zucker 28 g · Ballaststoffe 6 g", - "ingredients": "1 Banane · 3 EL Haferflocken · 250 ml Milch · Zimt · optional Erdnussmus", - "steps": "Alles mixen; kurz stehen lassen, damit der Hafer quillt.", - "variations": [], - "tip": "Morgen-Tipp Hafer macht aus dem Smoothie ein echtes Frühstück.", - "badge": "", - "yield": "" - }, - { - "num": "45", - "category": "Smoothies & Trink-Frühstück", - "title": "Beeren-Kefir-Drink", - "tagline": "Leicht, spritzig, für den Darm.", - "nutrition": "ca. ≈ 240 kcal · Eiweiß 12 g · Fett 8 g · KH 30 g · Zucker 20 g · Ballaststoffe 5 g", - "ingredients": "250 ml Kefir · Handvoll Beeren · etwas Süße · 1 TL Chiasamen", - "steps": "Alles mixen, 5 Min quellen lassen.", - "variations": [], - "tip": "Morgen-Tipp Kefir bringt Eiweiß und probiotische Kulturen.", - "badge": "", - "yield": "" - }, - { - "num": "46", - "category": "Smoothies & Trink-Frühstück", - "title": "Smoothie-Prep aus dem Tiefkühler", - "tagline": "Morgens nur noch mixen.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 22 g · Fett 6 g · KH 48 g · Zucker 26 g · Ballaststoffe 6 g", - "ingredients": "Beutel mit TK-Obst + Haferflocken vorportioniert · morgens: Milch + Skyr", - "steps": "Portionsbeutel vorbereiten, morgens Inhalt mit Milch und Skyr mixen.", - "variations": [], - "tip": "Morgen-Tipp 5 Beutel am Sonntag füllen – die ganze Woche ohne Denken.", - "badge": "", - "yield": "" - }, - { - "num": "47", - "category": "Frisch & schnell · unter 5 Minuten", - "title": "Joghurt-Obst-Nuss-Schale", - "tagline": "Das schnellste vollwertige Frühstück.", - "nutrition": "ca. ≈ 290 kcal · Eiweiß 20 g · Fett 9 g · KH 28 g · Zucker 20 g · Ballaststoffe 4 g", - "ingredients": "200 g Naturjoghurt/Skyr · Obst der Saison · 1 EL Nüsse", - "steps": "Joghurt in die Schale, Obst schneiden, Nüsse drüber.", - "variations": [], - "tip": "Morgen-Tipp Naturjoghurt statt Fruchtjoghurt spart einen Haufen versteckten Zucker.", - "badge": "", - "yield": "" - }, - { - "num": "48", - "category": "Frisch & schnell · unter 5 Minuten", - "title": "Banane + Erdnussmus", - "tagline": "Zwei Zutaten, sofort satt.", - "nutrition": "ca. ≈ 190 kcal · Eiweiß 5 g · Fett 9 g · KH 26 g · Zucker 14 g · Ballaststoffe 4 g", - "ingredients": "1 Banane · 1 EL Erdnussmus", - "steps": "Banane längs aufschneiden, Erdnussmus drauf.", - "variations": [], - "tip": "Morgen-Tipp Notfrühstück, das schlägt jeden Riegel aus dem Automaten.", - "badge": "", - "yield": "" - }, - { - "num": "49", - "category": "Frisch & schnell · unter 5 Minuten", - "title": "Käse-Apfel-Brot to go", - "tagline": "In der Hand aus dem Haus.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 12 g · Fett 11 g · KH 30 g · Zucker 12 g · Ballaststoffe 4 g", - "ingredients": "1 Scheibe Vollkornbrot · 1 Scheibe Käse · 1/2 Apfel", - "steps": "Brot mit Käse belegen, Apfel dazu essen oder drauflegen.", - "variations": [], - "tip": "Morgen-Tipp Käse plus Apfel: Eiweiß, Fett und Frische in Sekunden.", - "badge": "", - "yield": "" - }, - { - "num": "50", - "category": "Frisch & schnell · unter 5 Minuten", - "title": "Skyr + Müsli schnell", - "tagline": "Fertig, bevor der Kaffee durch ist.", - "nutrition": "ca. ≈ 280 kcal · Eiweiß 18 g · Fett 6 g · KH 38 g · Zucker 14 g · Ballaststoffe 5 g", - "ingredients": "150 g Skyr · 3 EL zuckerarmes Müsli/Haferflocken · Beeren", - "steps": "Skyr in die Schale, Müsli und Beeren drüber.", - "variations": [], - "tip": "Morgen-Tipp Blick aufs Etikett: viele Müslis sind halb Zucker – lieber pure Flocken.", - "badge": "", - "yield": "" - }, - { - "num": "51", - "category": "Frisch & schnell · unter 5 Minuten", - "title": "Obstsalat mit Quark-Klecks", - "tagline": "Frisch und leicht.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 14 g · Fett 1 g · KH 30 g · Zucker 22 g · Ballaststoffe 4 g", - "ingredients": "Obst der Saison gewürfelt · 100 g Quark · Zimt · optional Nüsse", - "steps": "Obst würfeln, Quark-Klecks drauf, Zimt darüber.", - "variations": [], - "tip": "Morgen-Tipp Der Quark-Klecks macht aus Obst ein sättigendes Frühstück.", - "badge": "", - "yield": "" - }, - { - "num": "52", - "category": "Frisch & schnell · unter 5 Minuten", - "title": "Hüttenkäse pur mit Obst", - "tagline": "Ohne Schnickschnack, viel Eiweiß.", - "nutrition": "ca. ≈ 240 kcal · Eiweiß 26 g · Fett 8 g · KH 16 g · Zucker 12 g · Ballaststoffe 2 g", - "ingredients": "200 g Hüttenkäse · Handvoll Beeren oder 1/2 Banane", - "steps": "Hüttenkäse in die Schale, Obst dazu.", - "variations": [], - "tip": "Morgen-Tipp Direkt aus dem Becher – ehrlich schneller geht's kaum.", - "badge": "", - "yield": "" - }, - { - "num": "53", - "category": "Frisch & schnell · unter 5 Minuten", - "title": "Vollkorn-Cracker mit Käse & Tomate", - "tagline": "Knabber-Frühstück.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 10 g · Fett 10 g · KH 26 g · Zucker 4 g · Ballaststoffe 4 g", - "ingredients": "3–4 Vollkorn-Cracker · Frischkäse/Käse · Cherrytomaten", - "steps": "Cracker mit Käse bestreichen, Tomate drauf.", - "variations": [], - "tip": "Morgen-Tipp Für die, die morgens nicht viel runterbekommen – klein, aber mit Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "54", - "category": "Frisch & schnell · unter 5 Minuten", - "title": "Edamame-Schale herzhaft", - "tagline": "Grünes Protein für Salzige.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 16 g · Fett 8 g · KH 12 g · Zucker 3 g · Ballaststoffe 5 g", - "ingredients": "150 g Edamame (TK, geschält) · Salz · Chili · Sesam", - "steps": "Edamame kurz in kochendem Wasser ziehen lassen, würzen.", - "variations": [], - "tip": "Morgen-Tipp Ungewöhnlich zum Frühstück, aber sattmachend und proteinreich.", - "badge": "", - "yield": "" - }, - { - "num": "55", - "category": "Airfryer · warm & knusprig", - "title": "Airfryer-Egg-Muffins AIRFRYER", - "tagline": "Vorkochen, aufwärmen, fertig.", - "nutrition": "ca. ≈ 220 kcal · Eiweiß 16 g · Fett 15 g · KH 4 g · Zucker 3 g · Ballaststoffe 1 g", - "ingredients": "4 Eier · Gemüse gewürfelt · Käse · Salz, Pfeffer · kleine Silikonförmchen", - "steps": "Eier verquirlen, mit Gemüse in die Förmchen, in den Airfryer. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 175 °C, 10–12 Min Ofen: 180 °C, 15–18 Min", - "variations": [ - "Schinken gewürfelten Schinken/Pute dazu", - "Spinat-Feta Spinat + Feta", - "Paprika Paprikawürfel + Paprikapulver" - ], - "tip": "Morgen-Tipp Batch machen: halten 4 Tage, morgens 2 Min aufwärmen.", - "badge": "", - "yield": "" - }, - { - "num": "56", - "category": "Airfryer · warm & knusprig", - "title": "Airfryer-Baked-Oats AIRFRYER", - "tagline": "Warmer Haferkuchen zum Löffeln.", - "nutrition": "ca. ≈ 360 kcal · Eiweiß 15 g · Fett 10 g · KH 48 g · Zucker 18 g · Ballaststoffe 6 g", - "ingredients": "50 g Haferflocken · 1 Ei · 100 ml Milch · 1/2 Banane · Backpulver · Beeren", - "steps": "Alles verrühren, in eine kleine Form, in den Airfryer, bis fest. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 175 °C, 12–15 Min Ofen: 180 °C, 20–25 Min", - "variations": [], - "tip": "Morgen-Tipp Schmeckt wie Kuchen, ist aber im Kern nur Haferbrei.", - "badge": "", - "yield": "" - }, - { - "num": "57", - "category": "Airfryer · warm & knusprig", - "title": "Airfryer-French-Toast-Sticks AIRFRYER", - "tagline": "Arme Ritter, entfettet.", - "nutrition": "ca. ≈ 290 kcal · Eiweiß 14 g · Fett 9 g · KH 38 g · Zucker 6 g · Ballaststoffe 4 g", - "ingredients": "2 Scheiben Vollkornbrot in Streifen · 1 Ei · Schuss Milch · Zimt", - "steps": "Brotstreifen in Ei-Milch wenden, in den Airfryer, bis goldbraun. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 190 °C, 6–8 Min, 1× wenden Ofen: 200 °C, 10–12 Min", - "variations": [], - "tip": "Morgen-Tipp Ohne Butterpfanne – knusprig mit einem Bruchteil des Fetts.", - "badge": "", - "yield": "" - }, - { - "num": "58", - "category": "Airfryer · warm & knusprig", - "title": "Airfryer-Frühstücks-Hash AIRFRYER", - "tagline": "Herzhaft, knusprig, mit Ei.", - "nutrition": "ca. ≈ 320 kcal · Eiweiß 10 g · Fett 13 g · KH 40 g · Zucker 6 g · Ballaststoffe 6 g", - "ingredients": "1 kleine Süßkartoffel/Kartoffel gewürfelt · Paprika · 1 TL Öl · Gewürz · 1 Ei", - "steps": "Gemüse würfeln, ölen, würzen, im Airfryer knusprig garen; dazu ein Ei braten. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 200 °C, 15–18 Min, 1× schütteln Ofen: 210 °C, 25–30 Min", - "variations": [], - "tip": "Morgen-Tipp Kartoffel vorher klein würfeln, dann wird sie gleichmäßig knusprig.", - "badge": "", - "yield": "" - }, - { - "num": "59", - "category": "Airfryer · warm & knusprig", - "title": "Airfryer-Hafer-Bananen-Riegel AIRFRYER", - "tagline": "Selbstgemacht statt gekauft.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 6 g · Fett 6 g · KH 40 g · Zucker 12 g · Ballaststoffe 5 g", - "ingredients": "150 g Haferflocken · 2 zerdrückte Bananen · 1 EL Nüsse · Zimt", - "steps": "Alles mischen, flach in eine Form drücken, im Airfryer backen, in Riegel schneiden. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 170 °C, 12–15 Min Ofen: 180 °C, 18–22 Min", - "variations": [], - "tip": "Morgen-Tipp Ohne Sirup und Schokoglasur – nur Hafer, Banane, Nuss.", - "badge": "", - "yield": "" - }, - { - "num": "60", - "category": "Airfryer · warm & knusprig", - "title": "Airfryer-Käse-Toast AIRFRYER", - "tagline": "Croque-Gefühl in Minuten.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 20 g · Fett 13 g · KH 34 g · Zucker 4 g · Ballaststoffe 4 g", - "ingredients": "2 Scheiben Vollkornbrot · Käse · optional Pute/Schinken · Senf", - "steps": "Brot belegen, zusammenklappen, im Airfryer knusprig überbacken. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 180 °C, 6–8 Min Ofen: 200 °C, 8–10 Min", - "variations": [], - "tip": "Morgen-Tipp Vollkorn und magerer Belag machen aus dem Toast ein sattes Frühstück.", - "badge": "", - "yield": "" - }, - { - "num": "61", - "category": "Airfryer · warm & knusprig", - "title": "Airfryer-Frühstücks-Burrito AIRFRYER", - "tagline": "Warm, knusprig, zum Mitnehmen.", - "nutrition": "ca. ≈ 450 kcal · Eiweiß 24 g · Fett 20 g · KH 42 g · Zucker 5 g · Ballaststoffe 6 g", - "ingredients": "1 Vollkorn-Wrap · Rührei · Bohnen · Käse · Salsa", - "steps": "Wrap füllen, fest einrollen, im Airfryer knusprig anrösten. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 190 °C, 6–8 Min Ofen: 200 °C, 10–12 Min", - "variations": [], - "tip": "Morgen-Tipp Vorab rollen, einwickeln – morgens nur in den Airfryer.", - "badge": "", - "yield": "" - }, - { - "num": "62", - "category": "Airfryer · warm & knusprig", - "title": "Airfryer-Apfel-Zimt-Chips AIRFRYER", - "tagline": "Süßer Crunch ohne Zucker.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 14 g · Fett 1 g · KH 32 g · Zucker 20 g · Ballaststoffe 4 g", - "ingredients": "1 Apfel dünn gehobelt · Zimt · dazu 150 g Quark", - "steps": "Apfelscheiben dünn hobeln, mit Zimt bestreuen, im Airfryer knusprig trocknen; Quark dazu. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 150 °C, 15–18 Min Ofen: 140 °C, 30–40 Min", - "variations": [], - "tip": "Morgen-Tipp Süß und knusprig aus einer einzigen Frucht – Quark liefert das Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "63", - "category": "Mehr Löffel- & Joghurt-Frühstück", - "title": "Griechischer Joghurt, Honig & Walnuss", - "tagline": "Simpel, aber gut.", - "nutrition": "ca. ≈ 300 kcal · Eiweiß 18 g · Fett 16 g · KH 22 g · Zucker 18 g · Ballaststoffe 1 g", - "ingredients": "200 g griechischer Joghurt · 1 TL Honig · 1 EL Walnüsse", - "steps": "Joghurt in die Schale, Walnüsse drüber, dünn Honig darüber.", - "variations": [], - "tip": "Morgen-Tipp Griechischer Joghurt bringt mehr Eiweiß als der normale.", - "badge": "", - "yield": "" - }, - { - "num": "64", - "category": "Mehr Löffel- & Joghurt-Frühstück", - "title": "Chia-Pudding · Basis", - "tagline": "Abends rühren, morgens fertig.", - "nutrition": "ca. ≈ 170 kcal · Eiweiß 10 g · Fett 9 g · KH 20 g · Zucker 10 g · Ballaststoffe 8 g", - "ingredients": "3 EL Chiasamen · 200 ml Milch · etwas Süße · Beeren zum Toppen", - "steps": "Verrühren, 10 Min stehen, nochmal rühren, über Nacht kühlen.", - "variations": [ - "Schoko + 1 TL Kakao", - "Vanille-Kokos Vanille + Schuss Kokosmilch", - "Beere TK-Beeren untermischen", - "Erdnuss + 1 EL Erdnussmus" - ], - "tip": "Morgen-Tipp Nochmal rühren nach 10 Min verhindert Klümpchen.", - "badge": "", - "yield": "" - }, - { - "num": "65", - "category": "Mehr Löffel- & Joghurt-Frühstück", - "title": "Skyr-Schoko-Mousse", - "tagline": "Nachtisch, den man morgens darf.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 22 g · Fett 2 g · KH 12 g · Zucker 9 g · Ballaststoffe 2 g", - "ingredients": "200 g Skyr · 1 TL Kakao · etwas Süße · Schuss Milch", - "steps": "Alles glattrühren, bis cremig-dunkel.", - "variations": [], - "tip": "Morgen-Tipp Schoko-Kick am Morgen, ohne dass eine Tafel wandert.", - "badge": "", - "yield": "" - }, - { - "num": "66", - "category": "Mehr Löffel- & Joghurt-Frühstück", - "title": "Kefir-Beeren-Bowl", - "tagline": "Frisch und leicht verdaulich.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 12 g · Fett 8 g · KH 32 g · Zucker 16 g · Ballaststoffe 5 g", - "ingredients": "200 g Kefir · Beeren · 2 EL Haferflocken · 1 TL Leinsamen", - "steps": "Kefir in die Schale, Beeren, Hafer und Leinsamen drüber.", - "variations": [], - "tip": "Morgen-Tipp Leinsamen frisch schroten, sonst gehen sie unverdaut durch.", - "badge": "", - "yield": "" - }, - { - "num": "67", - "category": "Mehr Löffel- & Joghurt-Frühstück", - "title": "Quark-Müsli-Schichtglas", - "tagline": "Fürs Büro vorbereiten.", - "nutrition": "ca. ≈ 300 kcal · Eiweiß 22 g · Fett 9 g · KH 34 g · Zucker 12 g · Ballaststoffe 4 g", - "ingredients": "150 g Quark · 3 EL Müsli/Hafer · Beeren · Nüsse", - "steps": "Abwechselnd Quark, Müsli und Beeren ins Glas schichten.", - "variations": [], - "tip": "Morgen-Tipp Getrennt schichten hält das Müsli bis mittags knusprig.", - "badge": "", - "yield": "" - }, - { - "num": "68", - "category": "Mehr Löffel- & Joghurt-Frühstück", - "title": "Joghurt mit gebratener Banane", - "tagline": "Warm trifft kühl.", - "nutrition": "ca. ≈ 280 kcal · Eiweiß 18 g · Fett 8 g · KH 32 g · Zucker 22 g · Ballaststoffe 3 g", - "ingredients": "200 g Joghurt/Skyr · 1 Banane · Zimt · 1 TL Öl", - "steps": "Banane in Scheiben kurz anbraten, mit Zimt auf den Joghurt geben.", - "variations": [], - "tip": "Morgen-Tipp Gebratene Banane karamellisiert von selbst – kein Zucker nötig.", - "badge": "", - "yield": "" - }, - { - "num": "69", - "category": "Mehr Löffel- & Joghurt-Frühstück", - "title": "Protein-Milchreis (kalt)", - "tagline": "Reispudding, aber eiweißreich.", - "nutrition": "ca. ≈ 290 kcal · Eiweiß 15 g · Fett 4 g · KH 45 g · Zucker 16 g · Ballaststoffe 2 g", - "ingredients": "150 g gekochter Milchreis · 100 g Skyr · Zimt · Beeren", - "steps": "Kalten Milchreis mit Skyr verrühren, Beeren und Zimt drauf.", - "variations": [], - "tip": "Morgen-Tipp Reste-Milchreis vom Vortag plus Skyr = fertiges Frühstück.", - "badge": "", - "yield": "" - }, - { - "num": "70", - "category": "Mehr herzhafte Frühstücke", - "title": "Herzhafte Frühstücks-Bowl", - "tagline": "Ein Teller, alles drin.", - "nutrition": "ca. ≈ 340 kcal · Eiweiß 15 g · Fett 22 g · KH 22 g · Zucker 5 g · Ballaststoffe 8 g", - "ingredients": "1 gekochtes/pochiertes Ei · 1/2 Avocado · 3 EL Bohnen · Cherrytomaten · Gewürz", - "steps": "Alles in eine Schale, würzen, warm oder kalt essen.", - "variations": [], - "tip": "Morgen-Tipp Reste vom Abendessen lassen sich prima zur Frühstücks-Bowl umbauen.", - "badge": "", - "yield": "" - }, - { - "num": "71", - "category": "Mehr herzhafte Frühstücke", - "title": "Tofu-Rührei (Scramble)", - "tagline": "Pflanzliche Rührei-Alternative.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 18 g · Fett 15 g · KH 6 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "150 g fester Tofu · Kurkuma · Kala Namak/Salz · Gemüse · Öl", - "steps": "Tofu zerbröseln, mit Gewürzen und Gemüse anbraten.", - "variations": [], - "tip": "Morgen-Tipp Kala Namak (Schwarzsalz) gibt den typischen Ei-Geschmack.", - "badge": "", - "yield": "" - }, - { - "num": "72", - "category": "Mehr herzhafte Frühstücke", - "title": "Käse-Omelett-Wrap", - "tagline": "Herzhaft und tragbar.", - "nutrition": "ca. ≈ 400 kcal · Eiweiß 24 g · Fett 22 g · KH 28 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "2 Eier · Käse · 1 Vollkorn-Wrap · Spinat", - "steps": "Dünnes Omelett braten, mit Käse und Spinat in den Wrap rollen.", - "variations": [], - "tip": "Morgen-Tipp Dünn ausgebacken lässt sich das Omelett besser einrollen.", - "badge": "", - "yield": "" - }, - { - "num": "73", - "category": "Mehr herzhafte Frühstücke", - "title": "Pute-Ei-Sandwich", - "tagline": "Deftig und proteinstark.", - "nutrition": "ca. ≈ 360 kcal · Eiweiß 30 g · Fett 12 g · KH 34 g · Zucker 4 g · Ballaststoffe 5 g", - "ingredients": "2 Scheiben Vollkornbrot · Putenbrust · 1 gekochtes Ei · Senf · Salat", - "steps": "Brot belegen, Senf drauf, zusammenklappen.", - "variations": [], - "tip": "Morgen-Tipp Magere Pute plus Ei bringt richtig viel Eiweiß pro Bissen.", - "badge": "", - "yield": "" - }, - { - "num": "74", - "category": "Mehr herzhafte Frühstücke", - "title": "Linsen-Frühstückssalat", - "tagline": "Ungewöhnlich, aber stark.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 18 g · Fett 14 g · KH 34 g · Zucker 6 g · Ballaststoffe 9 g", - "ingredients": "150 g gekochte Linsen · Tomate · Gurke · Feta · Zitrone, Öl", - "steps": "Alles mischen, mit Zitrone und wenig Öl abschmecken.", - "variations": [], - "tip": "Morgen-Tipp Vorkochen und 3 Tage im Kühlschrank – herzhaftes Meal-Prep.", - "badge": "", - "yield": "" - }, - { - "num": "75", - "category": "Mehr herzhafte Frühstücke", - "title": "Räuchertofu-/Schinken-Brot", - "tagline": "Schneller herzhafter Belag.", - "nutrition": "ca. ≈ 240 kcal · Eiweiß 16 g · Fett 10 g · KH 20 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "1 Scheibe Vollkornbrot · Räuchertofu oder magerer Schinken · Gurke · Senf", - "steps": "Brot belegen, Gurke drauf.", - "variations": [], - "tip": "Morgen-Tipp Räuchertofu ist die pflanzliche, proteinreiche Schinken-Alternative.", - "badge": "", - "yield": "" - }, - { - "num": "76", - "category": "Mehr herzhafte Frühstücke", - "title": "Champignon-Rührei", - "tagline": "Herzhaft mit Umami.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 18 g · Fett 18 g · KH 4 g · Zucker 2 g · Ballaststoffe 2 g", - "ingredients": "2–3 Eier · Handvoll Champignons · Zwiebel · Petersilie · Öl", - "steps": "Pilze und Zwiebel anbraten, verquirlte Eier drüber, stocken lassen.", - "variations": [], - "tip": "Morgen-Tipp Pilze erst gut anbraten, bis das Wasser weg ist – dann werden sie aromatisch.", - "badge": "", - "yield": "" - }, - { - "num": "77", - "category": "Mehr warme & schnelle Frühstücke", - "title": "Blitz-Porridge aus der Mikrowelle", - "tagline": "Warm in 2 Minuten.", - "nutrition": "ca. ≈ 260 kcal · Eiweiß 11 g · Fett 6 g · KH 38 g · Zucker 12 g · Ballaststoffe 5 g", - "ingredients": "40 g Haferflocken · 150 ml Milch · Prise Salz · Beeren", - "steps": "Hafer mit Milch 2 Min in der Mikrowelle, umrühren, Beeren drauf.", - "variations": [], - "tip": "Morgen-Tipp Tiefer Teller nehmen – Haferbrei kocht in der Mikrowelle gern über.", - "badge": "", - "yield": "" - }, - { - "num": "78", - "category": "Mehr warme & schnelle Frühstücke", - "title": "Schoko-Bananen-Mug-Cake", - "tagline": "Kuchen in der Tasse, protein-style.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 12 g · Fett 9 g · KH 32 g · Zucker 12 g · Ballaststoffe 4 g", - "ingredients": "1 Ei · 1/2 Banane · 3 EL Haferflocken · 1 TL Kakao · Backpulver", - "steps": "Alles in einer Tasse verrühren, 1,5–2 Min in der Mikrowelle.", - "variations": [], - "tip": "Morgen-Tipp Fertig, bevor der Ofen überhaupt vorgeheizt wäre.", - "badge": "", - "yield": "" - }, - { - "num": "79", - "category": "Mehr warme & schnelle Frühstücke", - "title": "Grießbrei aus der Mikrowelle", - "tagline": "Kindheit in 3 Minuten.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 12 g · Fett 5 g · KH 40 g · Zucker 16 g · Ballaststoffe 2 g", - "ingredients": "3 EL Grieß · 200 ml Milch · Süße · Zimt", - "steps": "Grieß und Milch verrühren, 2–3 Min in der Mikrowelle, zwischendurch rühren.", - "variations": [], - "tip": "Morgen-Tipp Zwischendurch rühren, sonst klumpt der Grieß.", - "badge": "", - "yield": "" - }, - { - "num": "80", - "category": "Mehr warme & schnelle Frühstücke", - "title": "Warmer Zimt-Apfel mit Quark", - "tagline": "Bratapfel-Feeling ohne Ofen.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 18 g · Fett 1 g · KH 26 g · Zucker 20 g · Ballaststoffe 4 g", - "ingredients": "1 Apfel gewürfelt · Zimt · Schuss Wasser · 150 g Quark", - "steps": "Apfel mit Zimt und etwas Wasser 2 Min in der Mikrowelle weich garen, Quark dazu.", - "variations": [], - "tip": "Morgen-Tipp Der warme Apfel auf kaltem Quark ist ein kleines Highlight.", - "badge": "", - "yield": "" - }, - { - "num": "81", - "category": "Mehr warme & schnelle Frühstücke", - "title": "Protein-Kakao mit Vollkornbrot", - "tagline": "Trinken und kauen kombiniert.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 18 g · Fett 14 g · KH 32 g · Zucker 14 g · Ballaststoffe 3 g", - "ingredients": "250 ml warme Milch · 1 TL Kakao · optional Proteinpulver · 1 Scheibe Vollkornbrot mit Käse", - "steps": "Kakao anrühren, dazu ein belegtes Brot.", - "variations": [], - "tip": "Morgen-Tipp Warme Milch plus Käsebrot: einfach, warm und sättigend.", - "badge": "", - "yield": "" - }, - { - "num": "82", - "category": "Mehr warme & schnelle Frühstücke", - "title": "Mikrowellen-Ei-Muffin", - "tagline": "Rührei ohne Pfanne.", - "nutrition": "ca. ≈ 240 kcal · Eiweiß 18 g · Fett 18 g · KH 3 g · Zucker 2 g · Ballaststoffe 0 g", - "ingredients": "2 Eier · Schuss Milch · Käse · Salz · Tasse/Becher", - "steps": "Eier in der Tasse verquirlen, 1–1,5 Min in der Mikrowelle, zwischendurch rühren.", - "variations": [], - "tip": "Morgen-Tipp In 30-Sekunden-Schritten garen und prüfen, sonst wird's gummiartig.", - "badge": "", - "yield": "" - }, - { - "num": "83", - "category": "Mehr warme & schnelle Frühstücke", - "title": "Zimt-Porridge-Ritual", - "tagline": "Der bewusste warme Start.", - "nutrition": "ca. ≈ 300 kcal · Eiweiß 12 g · Fett 10 g · KH 40 g · Zucker 12 g · Ballaststoffe 5 g", - "ingredients": "40 g Haferflocken · 200 ml Milch · Zimt · 1 TL Nüsse", - "steps": "Hafer mit Milch warm köcheln, Zimt und Nüsse drüber, in Ruhe löffeln.", - "variations": [], - "tip": "Morgen-Tipp Fünf Minuten am Tisch statt im Stehen – der Tag beginnt anders, wenn man wirklich frühstückt.", - "badge": "", - "yield": "" - } - ] - } -} \ No newline at end of file diff --git a/plugins/trainingsplan/presets/granola.json b/plugins/trainingsplan/presets/granola.json deleted file mode 100644 index 3952713..0000000 --- a/plugins/trainingsplan/presets/granola.json +++ /dev/null @@ -1,530 +0,0 @@ -{ - "name": "Granola (Vorrat)", - "config": { - "type": "recipe", - "meta": { - "title": "Die Granola-Bibel", - "header_left": "GRANOLA · Vorrats-Edition · Begleitend zum 8-Wochen-Plan", - "footer_left": "Topping statt Schüssel · Portion abwiegen · Zartbitter statt Zucker", - "output_name": "Granola_Vorrat.pdf", - "doc_title": "Die Granola-Bibel", - "brand_color": "#6D28D9", - "steps_label": "SO GEHT'S" - }, - "intro": { - "headline": "Die Granola-Bibel", - "subtitle": "28 Rezepte mit exakten Arbeitsschritten – deutlich weniger Zucker als jedes Fertig-Granola, Nährwerte", - "paragraphs": [ - "pro Portion als Richtwert.", - "Gekauftes Granola ist meist eine als Müsli getarnte Süßigkeit – 20–30 % Zucker sind normal. Diese Rezepte", - "drehen das um: Hafer, Nüsse, Kerne und Buchweizen tragen, gesüßt wird sparsam und bewusst. Das ist genau", - "dein Substitutions-Hebel: Granola ist hier ein Topping für Skyr oder Quark, kein Schüssel-Füller. Eine", - "Eiweiß-Basis plus Crunch schlägt abends auf der Couch die Schoko-Chips-Cola-Kombi – nicht durch Verzicht,", - "sondern durch einen besseren Reiz. Ein Wort zur Ehrlichkeit: Granola bleibt kaloriendicht. Der Hebel ist die", - "Portion, nicht die Zutat. Wieg dir 40–50 g ab und stell das Glas weg – aus dem Glas gelöffelt wird jedes Granola", - "zur Falle. Die Nährwerte unter NÄHRWERTE ca. sind Richtwerte pro Portion (≈50 g), keine Zielvorgabe.", - "Die Grund-Technik – gilt für fast jedes Rezept", - "1 Trocken mischen: Hafer, grob gehackte Nüsse, Kerne, Gewürze, ggf. Kakao/Proteinpulver in einer großen", - "Schüssel verrühren.", - "2 Bindung erwärmen: Öl bzw. Nussmus mit Süße und Aroma kurz schmelzen, bis es dünnflüssig ist – so", - "verteilt es sich gleichmäßig.", - "3 Benetzen: über das Trockene gießen und gründlich mischen, bis alles glänzt. Trockene Nester = weiche", - "Stellen später.", - "4 Cluster-Trick: für feste Brocken 1 Eiweiß schaumig schlagen und unterheben, Masse fest aufs Blech", - "drücken – und beim Backen nicht rühren.", - "5 Backen: meist 150–160 °C Umluft. Niedrig und länger röstet gleichmäßig; ohne Cluster einmal wenden, mit", - "Cluster in Ruhe lassen.", - "6 Auskühlen = knusprig: Granola ist frisch aus dem Ofen noch weich. Erst vollständig auf dem Blech", - "auskühlen lassen, dann brechen.", - "7 Empfindliches später: Schokolade, Trockenfrüchte und Kokoschips erst nach dem Abkühlen zugeben, sonst", - "verbrennen sie.", - "8 Lagern: luftdicht, kühl und dunkel 4–6 Wochen. Nicht warm einfüllen, sonst schwitzt es und wird zäh." - ], - "rule_head": "", - "rule_text": "", - "categories_head": "7 Kategorien" - }, - "categories": [ - { - "name": "Schoko & Kakao", - "color": "#7C3AED" - }, - { - "name": "Nuss, Kern & Klassik", - "color": "#0EA5E9" - }, - { - "name": "Frucht & Beere", - "color": "#EA580C" - }, - { - "name": "Gewürz & Saison", - "color": "#16A34A" - }, - { - "name": "Protein-Boost & Post-Workout", - "color": "#DC2626" - }, - { - "name": "Grain-free & Low-Carb", - "color": "#DB2777" - }, - { - "name": "Ohne Ofen & Overnight", - "color": "#B45309" - } - ], - "recipes": [ - { - "num": "01", - "category": "Schoko & Kakao", - "title": "Malz-Schoko-Buchweizen-Granola", - "tagline": "Die Referenz nach Anna Jones – zuckerreduziert adaptiert.", - "nutrition": "ca. ≈ 238 kcal · Eiweiß 7 g · Fett 13 g · KH 24 g · Zucker 6 g · Ballaststoffe 5 g", - "ingredients": "250 g Haferflocken (kernig) · 100 g roher Buchweizen · 80 g Haselnüsse grob · 40 g Kakaonibs · 30 g Backkakao · 1 EL Gerstenmalz-Extrakt · 2 EL Ahornsirup · 3 EL Kokosöl · 1 Prise Meersalz · 60 g Zartbitter 85 % gehackt (nach dem Backen)", - "steps": "1 Ofen auf 150 °C Umluft vorheizen, Blech mit Backpapier.\n2 Hafer, Buchweizen, Haselnüsse, Kakaonibs, Kakao und Salz in einer großen Schüssel gut vermischen.\n3 Kokosöl, Malz-Extrakt und Ahornsirup in einem Töpfchen bei kleiner Hitze schmelzen und glatt rühren.\n4 Flüssige Mischung über das Trockene gießen und gründlich wenden, bis alles dunkel glänzt.\n5 Gleichmäßig aufs Blech verteilen und leicht andrücken.\n6 25–30 Min backen, nach der Hälfte einmal wenden – Kakao verbrennt schnell, lieber kürzer.\n7 Vollständig auf dem Blech auskühlen lassen, dann die gehackte Zartbitter untermischen.", - "variations": [ - "Nuss-Tausch Mandeln oder Pekannüsse statt Haselnüssen", - "Ohne Malz Malz weglassen, dafür 1 TL Instant-Kaffee für Tiefe" - ], - "tip": "Coach-Tipp Kakaonibs bringen Schoko-Bitterkeit fast ohne Zucker – der Trick, um Vollmilch-Schoko-Gelüste auf der Couch abzufangen.", - "badge": "", - "yield": "ergibt ca. 800 g · ~16 Portionen à 50 g" - }, - { - "num": "02", - "category": "Schoko & Kakao", - "title": "Tiramisu-Granola", - "tagline": "Nach The Doctor's Kitchen – Kaffee, Kakao, feste Cluster.", - "nutrition": "ca. ≈ 245 kcal · Eiweiß 8 g · Fett 15 g · KH 20 g · Zucker 6 g · Ballaststoffe 5 g", - "ingredients": "250 g Haferflocken · 100 g gemischte Nüsse grob · 40 g Kürbiskerne · 40 g Sonnenblumenkerne · 2 EL Leinsamen · 20 g Backkakao · 1 TL Zimt · 2 EL Mandelmus · 2 EL Honig · 3 EL Kokosöl · 60 ml starker Espresso (abgekühlt) · 1 TL Vanille · 1 Eiweiß · Kakao zum Bestäuben", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Nüsse, Kürbis- und Sonnenblumenkerne, Leinsamen, Kakao und Zimt vermischen.\n3 Mandelmus, Honig, Kokosöl und Espresso leicht erwärmen und glatt rühren, Vanille einrühren.\n4 Über das Trockene gießen und gründlich mischen.\n5 Eiweiß schaumig schlagen und unterheben – das gibt die Tiramisu-typischen Cluster.\n6 Masse fest aufs Blech drücken (ca. 1,5 cm dick), nicht verteilen.\n7 30–35 Min backen, dabei NICHT rühren, damit die Brocken halten.\n8 Auskühlen lassen, in Brocken brechen und mit etwas Kakao bestäuben.", - "variations": [ - "Entkoffeiniert Espresso durch Lupinen- oder Malzkaffee ersetzen", - "Nüsse pur Kakao weglassen für reine Cappuccino-Note" - ], - "tip": "Coach-Tipp Kaffee + Kakao schmecken „Dessert“ ohne Süße-Bombe. Über Skyr wird das dein Abend-Dessert-Ersatz.", - "badge": "", - "yield": "ergibt ca. 700 g · ~14 Portionen à 50 g" - }, - { - "num": "03", - "category": "Schoko & Kakao", - "title": "Doppel-Kakao-Haselnuss", - "tagline": "Die „Nutella“-Idee als knuspriges Topping.", - "nutrition": "ca. ≈ 252 kcal · Eiweiß 8 g · Fett 16 g · KH 20 g · Zucker 6 g · Ballaststoffe 5 g", - "ingredients": "250 g Haferflocken · 120 g Haselnüsse grob · 30 g Backkakao · 2 EL Haselnussmus · 2 EL Kokosöl · 3 EL Ahornsirup · 1 Prise Salz · 40 g Kakaonibs (nach dem Backen)", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Haselnüsse, Kakao und Salz mischen.\n3 Haselnussmus, Kokosöl und Ahornsirup schmelzen und glatt rühren.\n4 Über das Trockene gießen und mischen, bis alles dunkel und gleichmäßig ist.\n5 Aufs Blech, leicht andrücken.\n6 25 Min backen, nach der Hälfte wenden – wegen Kakao auf die Farbe achten.\n7 Auskühlen lassen, Kakaonibs untermischen.", - "variations": [ - "Extra Crunch 1 EL Buchweizen mitrösten", - "Milder Kakao halbieren, 1 TL Vanille zugeben" - ], - "tip": "Coach-Tipp Haselnuss + Kakao trifft exakt den Nutella-Reiz – aber mit Ballaststoffen statt Palmöl und Zucker.", - "badge": "", - "yield": "ergibt ca. 750 g · ~15 Portionen à 50 g" - }, - { - "num": "04", - "category": "Schoko & Kakao", - "title": "Schoko-Meersalz-Cluster", - "tagline": "Feste Brocken, salzig-bitter, sehr gering gesüßt.", - "nutrition": "ca. ≈ 248 kcal · Eiweiß 8 g · Fett 15 g · KH 21 g · Zucker 5 g · Ballaststoffe 6 g", - "ingredients": "250 g Haferflocken · 80 g Mandeln grob · 40 g Sonnenblumenkerne · 25 g Backkakao · 2 EL Mandelmus · 2 EL Kokosöl · 2 EL Ahornsirup · 1 Eiweiß · grobes Meersalz · 60 g Zartbitter 85 % (nach dem Backen)", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Mandeln, Kerne und Kakao mischen.\n3 Mandelmus, Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Eiweiß schaumig schlagen, mit unterheben – gibt die Cluster-Bindung.\n5 Fest aufs Blech drücken, mit etwas grobem Meersalz bestreuen.\n6 30 Min backen, NICHT rühren.\n7 Vollständig auskühlen lassen, in Brocken brechen, gehackte Zartbitter untermischen.", - "variations": [ - "Orange Abrieb 1/2 Bio-Orange in die Bindung", - "Chili 1 Prise Chili für Schoko-Chili-Kick" - ], - "tip": "Coach-Tipp Salz + 85 %-Schoko: der intensivste Schoko-Hit pro Gramm. Kleine Menge sättigt den Gusto komplett.", - "badge": "", - "yield": "ergibt ca. 700 g · ~14 Portionen à 50 g" - }, - { - "num": "05", - "category": "Schoko & Kakao", - "title": "Mokka-Walnuss", - "tagline": "Espresso trifft Röstaroma, kaum Süße.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 8 g · Fett 16 g · KH 19 g · Zucker 5 g · Ballaststoffe 5 g", - "ingredients": "250 g Haferflocken · 100 g Walnüsse grob · 40 g Leinsamen · 15 g Backkakao · 1 TL Instant-Espresso · 3 EL Kokosöl · 3 EL Ahornsirup · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Walnüsse, Leinsamen, Kakao, Espressopulver und Salz mischen.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Über das Trockene gießen, gut mischen.\n5 Aufs Blech, leicht andrücken.\n6 25–30 Min backen, einmal wenden.\n7 Auskühlen lassen – wird erst kalt knusprig.", - "variations": [ - "Pekan Pekannüsse statt Walnüssen", - "Stärker 2 TL Espressopulver für echten Mokka-Punch" - ], - "tip": "Coach-Tipp Walnüsse liefern Omega-3 – passt gut in die Regenerationswochen mit viel Volumen.", - "badge": "", - "yield": "ergibt ca. 720 g · ~14 Portionen à 50 g" - }, - { - "num": "06", - "category": "Nuss, Kern & Klassik", - "title": "Klassik pur · Nuss-Kern-Granola", - "tagline": "Die Basis. Minimaler Zucker, maximal variierbar.", - "nutrition": "ca. ≈ 236 kcal · Eiweiß 7 g · Fett 13 g · KH 22 g · Zucker 4 g · Ballaststoffe 5 g", - "ingredients": "300 g Haferflocken · 100 g gemischte Nüsse grob · 40 g Kürbiskerne · 40 g Sonnenblumenkerne · 2 EL Leinsamen · 3 EL Kokosöl · 2 EL Ahornsirup · 1 TL Zimt · 1 Prise Salz", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Alle trockenen Zutaten mit Zimt und Salz mischen.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren, bis alles glänzt.\n4 Aufs Blech, gleichmäßig dünn verteilen.\n5 25–30 Min backen, nach der Hälfte wenden, bis goldbraun.\n6 Auf dem Blech auskühlen lassen.", - "variations": [ - "Frucht nach dem Backen 40 g Trockenfrüchte untermischen", - "Nüsse pur Kerne durch mehr Nüsse ersetzen für weniger KH", - "Vanille 1 TL Vanille in die Bindung" - ], - "tip": "Coach-Tipp Lern dieses Rezept auswendig – alle anderen sind nur Varianten davon. Sparsame Süße ist Absicht.", - "badge": "", - "yield": "ergibt ca. 750 g · ~15 Portionen à 50 g" - }, - { - "num": "07", - "category": "Nuss, Kern & Klassik", - "title": "Erdnuss-Protein-Cluster", - "tagline": "Post-Workout-tauglich, feste Brocken, extra Eiweiß.", - "nutrition": "ca. ≈ 258 kcal · Eiweiß 12 g · Fett 14 g · KH 21 g · Zucker 5 g · Ballaststoffe 5 g", - "ingredients": "250 g Haferflocken · 60 g Erdnüsse grob · 40 g Sonnenblumenkerne · 30 g Whey oder Erbsenprotein (neutral/vanille) · 3 EL Erdnussmus · 2 EL Kokosöl · 2 EL Ahornsirup · 1 Eiweiß · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Erdnüsse, Kerne und Proteinpulver mischen (Pulver gut unterheben, keine Klumpen).\n3 Erdnussmus, Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Eiweiß schaumig schlagen und unterheben.\n5 Fest aufs Blech drücken.\n6 28–32 Min bei niedriger Hitze backen, NICHT rühren – Protein bräunt schneller, Farbe im Auge behalten.\n7 Vollständig auskühlen lassen, in Brocken brechen.", - "variations": [ - "Schoko 20 g Kakao ins Trockene", - "Casein-Abend Casein statt Whey für langsame Abend-Variante" - ], - "tip": "Coach-Tipp Proteinpulver bräunt früher – lieber 150 °C und Sichtkontrolle. Über Skyr kommst du locker auf 25–30 g Eiweiß.", - "badge": "", - "yield": "ergibt ca. 700 g · ~14 Portionen à 50 g" - }, - { - "num": "08", - "category": "Nuss, Kern & Klassik", - "title": "Mandel-Vanille", - "tagline": "Schlicht, hell, alltagstauglich.", - "nutrition": "ca. ≈ 240 kcal · Eiweiß 7 g · Fett 14 g · KH 21 g · Zucker 5 g · Ballaststoffe 5 g", - "ingredients": "300 g Haferflocken · 120 g Mandeln grob · 40 g Leinsamen · 3 EL Kokosöl · 2 EL Ahornsirup · 2 TL Vanille · 1 Prise Salz", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Mandeln, Leinsamen und Salz mischen.\n3 Kokosöl und Ahornsirup schmelzen, Vanille einrühren, über das Trockene gießen.\n4 Gut mischen, aufs Blech, andrücken.\n5 25–30 Min backen, einmal wenden.\n6 Auskühlen lassen.", - "variations": [ - "Mandel-Zimt 1 TL Zimt zugeben", - "Kirsch nach dem Backen getrocknete Sauerkirschen unter)" - ], - "tip": "Coach-Tipp Die neutralste Basis – perfekt, wenn der Rest der Schüssel (Beeren, Quark) den Geschmack trägt.", - "badge": "", - "yield": "ergibt ca. 750 g · ~15 Portionen à 50 g" - }, - { - "num": "09", - "category": "Nuss, Kern & Klassik", - "title": "Kokos-Cashew", - "tagline": "Cremig-tropisch, leicht süß von allein.", - "nutrition": "ca. ≈ 255 kcal · Eiweiß 7 g · Fett 16 g · KH 21 g · Zucker 5 g · Ballaststoffe 5 g", - "ingredients": "260 g Haferflocken · 100 g Cashews grob · 50 g Kokoschips · 40 g Sonnenblumenkerne · 3 EL Kokosöl · 2 EL Ahornsirup · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Cashews, Kerne und Salz mischen – Kokoschips noch NICHT zugeben.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 20 Min backen, dann wenden und die Kokoschips untermischen.\n6 Weitere 5–8 Min backen, bis die Kokoschips goldgelb sind – sie verbrennen schnell.\n7 Auskühlen lassen.", - "variations": [ - "Schoko-Kokos 40 g Zartbitter nach dem Backen", - "Limette Abrieb 1 Limette in die Bindung" - ], - "tip": "Coach-Tipp Kokoschips zum Schluss zugeben ist Pflicht – sonst sind sie schwarz, bevor der Rest fertig ist.", - "badge": "", - "yield": "ergibt ca. 730 g · ~14 Portionen à 50 g" - }, - { - "num": "10", - "category": "Nuss, Kern & Klassik", - "title": "Tahin-Sesam-Halva", - "tagline": "Nährig, orientalisch, unerwartet gut.", - "nutrition": "ca. ≈ 252 kcal · Eiweiß 9 g · Fett 16 g · KH 19 g · Zucker 5 g · Ballaststoffe 5 g", - "ingredients": "250 g Haferflocken · 80 g Mandeln grob · 40 g Sesam · 40 g Sonnenblumenkerne · 3 EL Tahin · 2 EL Kokosöl · 2 EL Honig · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Mandeln, Sesam, Kerne und Salz mischen.\n3 Tahin, Kokosöl und Honig leicht erwärmen und glatt rühren.\n4 Über das Trockene gießen, gut mischen.\n5 Aufs Blech, andrücken.\n6 22–26 Min backen, einmal wenden – Sesam bräunt schnell.\n7 Auskühlen lassen.", - "variations": [ - "Schoko 30 g Kakaonibs nach dem Backen", - "Kardamom 1/2 TL gemahlener Kardamom in die Bindung" - ], - "tip": "Coach-Tipp Tahin bringt Kalzium und einen leicht bitteren Halva-Ton – sättigt richtig, kleine Portion reicht.", - "badge": "", - "yield": "ergibt ca. 720 g · ~14 Portionen à 50 g" - }, - { - "num": "11", - "category": "Nuss, Kern & Klassik", - "title": "Ahorn-Pekan", - "tagline": "Der Klassiker – hier bewusst sparsam gesüßt.", - "nutrition": "ca. ≈ 246 kcal · Eiweiß 6 g · Fett 15 g · KH 22 g · Zucker 7 g · Ballaststoffe 4 g", - "ingredients": "290 g Haferflocken · 110 g Pekannüsse grob · 40 g Sonnenblumenkerne · 3 EL Kokosöl · 3 EL Ahornsirup · 1 TL Zimt · 1 Prise Salz", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Pekannüsse, Kerne, Zimt und Salz mischen.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 25–30 Min backen, einmal wenden.\n6 Auskühlen lassen – die Karamell-Note kommt erst kalt.", - "variations": [ - "Ahorn-Apfel nach dem Backen getrocknete Apfelstücke unter", - "Weniger süß nur 2 EL Ahornsirup" - ], - "tip": "Coach-Tipp Das klassische Frühstücks-Granola – aber mit 7 g Zucker statt der üblichen 15–20 g im Handel.", - "badge": "", - "yield": "ergibt ca. 750 g · ~15 Portionen à 50 g" - }, - { - "num": "12", - "category": "Frucht & Beere", - "title": "Beeren-Chia", - "tagline": "Säuerlich-fruchtig, Beeren als frisches Topping.", - "nutrition": "ca. ≈ 232 kcal · Eiweiß 7 g · Fett 12 g · KH 24 g · Zucker 7 g · Ballaststoffe 6 g", - "ingredients": "280 g Haferflocken · 80 g Mandeln grob · 30 g Chiasamen · 30 g Sonnenblumenkerne · 3 EL Kokosöl · 2 EL Ahornsirup · 1 Prise Salz · 40 g gefriergetrocknete Beeren (nach dem Backen)", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Mandeln, Chia, Kerne und Salz mischen.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 25 Min backen, einmal wenden.\n6 Auskühlen lassen, dann die gefriergetrockneten Beeren untermischen.\n7 Frische oder TK-Beeren erst in der Schüssel dazugeben, nicht ins Vorratsglas.", - "variations": [ - "Himbeer-Kakao 20 g Kakao + gefriergetrocknete Himbeeren", - "Frisch in der Schüssel mit TK-Heidelbeeren toppen" - ], - "tip": "Coach-Tipp Gefrorene Beeren statt Gummibärchen – hier direkt eingebaut. Säure bremst die Süß-Gier.", - "badge": "", - "yield": "ergibt ca. 740 g · ~14 Portionen à 50 g" - }, - { - "num": "13", - "category": "Frucht & Beere", - "title": "Apfel-Zimt-Bratapfel", - "tagline": "Apfelkuchen-Gefühl ohne Teig.", - "nutrition": "ca. ≈ 234 kcal · Eiweiß 6 g · Fett 12 g · KH 25 g · Zucker 8 g · Ballaststoffe 5 g", - "ingredients": "290 g Haferflocken · 90 g Walnüsse grob · 40 g Sonnenblumenkerne · 2 TL Zimt · 3 EL Kokosöl · 2 EL Ahornsirup · 1 Prise Salz · 60 g getrocknete Apfelstücke (nach dem Backen)", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Walnüsse, Kerne, Zimt und Salz mischen.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 25–30 Min backen, einmal wenden.\n6 Auskühlen lassen, dann getrocknete Apfelstücke untermischen (sonst werden sie zäh/hart).", - "variations": [ - "Rosine zusätzlich eine kleine Handvoll Rosinen", - "Warm in der Schüssel mit warmem, gewürfeltem Apfel toppen" - ], - "tip": "Coach-Tipp Der Kuchen-Ersatz. Über Skyr mit warmem Apfel schmeckt es wie Apfelstrudel – ohne Zuckerschock.", - "badge": "", - "yield": "ergibt ca. 760 g · ~15 Portionen à 50 g" - }, - { - "num": "14", - "category": "Frucht & Beere", - "title": "Bananenbrot-Granola", - "tagline": "Reife Banane bringt Süße fast ohne Sirup.", - "nutrition": "ca. ≈ 228 kcal · Eiweiß 7 g · Fett 12 g · KH 24 g · Zucker 8 g · Ballaststoffe 5 g", - "ingredients": "270 g Haferflocken · 90 g Walnüsse grob · 40 g Leinsamen · 1 sehr reife Banane · 2 EL Kokosöl · 1 EL Ahornsirup · 1 TL Zimt · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Banane mit einer Gabel fein zerdrücken.\n3 Hafer, Walnüsse, Leinsamen, Zimt und Salz mischen.\n4 Bananenpüree, geschmolzenes Kokosöl und Ahornsirup verrühren, über das Trockene geben.\n5 Gut mischen, aufs Blech, andrücken.\n6 30–35 Min backen – Banane braucht länger zum Trocknen; wenden nach 20 Min.\n7 Vollständig auskühlen lassen (bleibt sonst weich).", - "variations": [ - "Schoko-Chunk 40 g Zartbitter nach dem Backen", - "Erdnuss 2 EL Erdnussmus in die Bindung" - ], - "tip": "Coach-Tipp Braune Bananen sinnvoll verwerten statt wegwerfen – sie ersetzen einen Gutteil des Sirups.", - "badge": "", - "yield": "ergibt ca. 700 g · ~14 Portionen à 50 g" - }, - { - "num": "15", - "category": "Frucht & Beere", - "title": "Orange-Cranberry", - "tagline": "Weihnachtlich-fruchtig, säuerlicher Kontrast.", - "nutrition": "ca. ≈ 238 kcal · Eiweiß 6 g · Fett 12 g · KH 26 g · Zucker 9 g · Ballaststoffe 5 g", - "ingredients": "280 g Haferflocken · 90 g Mandeln grob · 40 g Sonnenblumenkerne · Abrieb 1 Bio-Orange · 3 EL Kokosöl · 2 EL Ahornsirup · 1 Prise Salz · 50 g getrocknete Cranberries (nach dem Backen)", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Mandeln, Kerne, Orangenabrieb und Salz mischen.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 25 Min backen, einmal wenden.\n6 Auskühlen lassen, dann Cranberries untermischen.", - "variations": [ - "Pistazie statt Mandeln für Farbe und Weihnachtslook", - "Zitrone Zitronenabrieb statt Orange" - ], - "tip": "Coach-Tipp Cranberries sind von Natur aus sauer und oft gezuckert – lieber sparsam, der Orangenabrieb trägt den Geschmack.", - "badge": "", - "yield": "ergibt ca. 740 g · ~14 Portionen à 50 g" - }, - { - "num": "16", - "category": "Frucht & Beere", - "title": "Trail-Mix-Granola", - "tagline": "Studentenfutter-Idee, ideal als Wander-/Büro-Snack.", - "nutrition": "ca. ≈ 256 kcal · Eiweiß 8 g · Fett 15 g · KH 23 g · Zucker 9 g · Ballaststoffe 5 g", - "ingredients": "250 g Haferflocken · 120 g gemischte Nüsse grob · 40 g Kürbiskerne · 3 EL Kokosöl · 2 EL Ahornsirup · 1 Prise Salz · 50 g Rosinen + 30 g Zartbitter-Tropfen (nach dem Backen)", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Nüsse, Kürbiskerne und Salz mischen.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 25–30 Min backen, einmal wenden.\n6 Vollständig auskühlen lassen, dann Rosinen und Zartbitter untermischen (Schoko schmilzt sonst).", - "variations": [ - "Salzig 1 Prise mehr Salz + geröstete Erdnüsse", - "Ohne Schoko stattdessen mehr Nüsse" - ], - "tip": "Coach-Tipp Portionsbeutel abwiegen und in die Tasche – das ist dein Büro-Swap gegen Chips und Gummibärchen.", - "badge": "", - "yield": "ergibt ca. 780 g · ~15 Portionen à 50 g" - }, - { - "num": "17", - "category": "Gewürz & Saison", - "title": "Kürbisgewürz (Pumpkin Spice)", - "tagline": "Herbst im Glas, wärmende Gewürze.", - "nutrition": "ca. ≈ 240 kcal · Eiweiß 8 g · Fett 14 g · KH 21 g · Zucker 6 g · Ballaststoffe 5 g", - "ingredients": "270 g Haferflocken · 90 g Pekannüsse grob · 60 g Kürbiskerne · 2 TL Zimt · 1/2 TL Ingwer · 1/2 TL Muskat · 1 Prise Nelke · 3 EL Kokosöl · 2 EL Ahornsirup · 1 Prise Salz", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Pekannüsse, Kürbiskerne, alle Gewürze und Salz gründlich mischen.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 25–30 Min backen, einmal wenden.\n6 Auskühlen lassen.", - "variations": [ - "Mit Kürbis 2 EL Kürbispüree in die Bindung, 5 Min länger backen", - "Ahorn-Pekan-Plus zusätzlich Vanille" - ], - "tip": "Coach-Tipp Das Gewürz macht die Süße subjektiv größer – typischer Trick, um mit weniger Zucker auszukommen.", - "badge": "", - "yield": "ergibt ca. 740 g · ~14 Portionen à 50 g" - }, - { - "num": "18", - "category": "Gewürz & Saison", - "title": "Ingwer-Kurkuma „Golden“", - "tagline": "Wärmend, leicht scharf, entzündungsfreundlich.", - "nutrition": "ca. ≈ 242 kcal · Eiweiß 7 g · Fett 14 g · KH 22 g · Zucker 6 g · Ballaststoffe 5 g", - "ingredients": "270 g Haferflocken · 90 g Cashews grob · 40 g Sonnenblumenkerne · 1 TL Kurkuma · 1 TL Ingwer · 1 Prise Pfeffer · 3 EL Kokosöl · 2 EL Honig · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Cashews, Kerne, Kurkuma, Ingwer, Pfeffer und Salz mischen (Pfeffer macht Kurkuma verfügbar).\n3 Kokosöl und Honig schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 25 Min backen, einmal wenden.\n6 Auskühlen lassen.", - "variations": [ - "Kokos 40 g Kokoschips in den letzten 6 Min", - "Schärfer frischen geriebenen Ingwer in die Bindung" - ], - "tip": "Coach-Tipp Kurkuma färbt alles gelb (auch das Blech-Papier) – harmlos. Eine Prise Pfeffer gehört dazu.", - "badge": "", - "yield": "ergibt ca. 730 g · ~14 Portionen à 50 g" - }, - { - "num": "19", - "category": "Gewürz & Saison", - "title": "Spekulatius-Lebkuchen", - "tagline": "Winter-Edition, ohne die übliche Zuckerlast.", - "nutrition": "ca. ≈ 238 kcal · Eiweiß 7 g · Fett 13 g · KH 23 g · Zucker 7 g · Ballaststoffe 5 g", - "ingredients": "280 g Haferflocken · 90 g Mandeln grob · 40 g Haselnüsse grob · 2 TL Lebkuchengewürz · 1 TL Zimt · 3 EL Kokosöl · 3 EL Ahornsirup · 1 Prise Salz", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Mandeln, Haselnüsse, Lebkuchengewürz, Zimt und Salz mischen.\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 25–30 Min backen, einmal wenden.\n6 Auskühlen lassen.", - "variations": [ - "Orange Orangenabrieb dazu", - "Schoko 40 g Zartbitter nach dem Backen" - ], - "tip": "Coach-Tipp Weihnachtsgebäck-Ersatz für den Advent – stillt den Spekulatius-Reflex, ohne die Kekstüte.", - "badge": "", - "yield": "ergibt ca. 750 g · ~15 Portionen à 50 g" - }, - { - "num": "20", - "category": "Gewürz & Saison", - "title": "Chai-Gewürz", - "tagline": "Schwarzer Tee trifft wärmende Gewürze.", - "nutrition": "ca. ≈ 239 kcal · Eiweiß 7 g · Fett 13 g · KH 23 g · Zucker 6 g · Ballaststoffe 5 g", - "ingredients": "280 g Haferflocken · 90 g Mandeln grob · 40 g Sonnenblumenkerne · 1 TL Zimt · 1/2 TL Kardamom · 1/2 TL Ingwer · 1 Prise Nelke · 1 Prise Pfeffer · 3 EL Kokosöl · 2 EL Honig · 1 Prise Salz", - "steps": "1 Ofen auf 155 °C Umluft, Blech mit Backpapier.\n2 Hafer, Mandeln, Kerne und alle Gewürze mischen.\n3 Kokosöl und Honig schmelzen, unterrühren.\n4 Aufs Blech, andrücken.\n5 25 Min backen, einmal wenden.\n6 Auskühlen lassen.", - "variations": [ - "Vanille-Chai 1 TL Vanille zugeben", - "Milchkaffee über warme Milch als Chai-Porridge" - ], - "tip": "Coach-Tipp Kardamom und Nelke sind intensiv – sparsam dosieren, sonst überdeckt es alles.", - "badge": "", - "yield": "ergibt ca. 730 g · ~14 Portionen à 50 g" - }, - { - "num": "21", - "category": "Gewürz & Saison", - "title": "Karottenkuchen-Granola", - "tagline": "Carrot Cake als Frühstücks-Topping.", - "nutrition": "ca. ≈ 236 kcal · Eiweiß 7 g · Fett 13 g · KH 23 g · Zucker 8 g · Ballaststoffe 5 g", - "ingredients": "260 g Haferflocken · 80 g Walnüsse grob · 40 g Sonnenblumenkerne · 80 g fein geraspelte Möhre · 2 TL Zimt · 1/2 TL Muskat · 3 EL Kokosöl · 2 EL Ahornsirup · 1 Prise Salz · 30 g Rosinen (nach dem Backen)", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Möhre fein raspeln und mit Küchenpapier gut ausdrücken – sonst wird das Granola nicht knusprig.\n3 Hafer, Walnüsse, Kerne, Gewürze, Salz und die geraspelte Möhre mischen.\n4 Kokosöl und Ahornsirup schmelzen, unterrühren.\n5 Aufs Blech, dünn verteilen, andrücken.\n6 30–35 Min backen – Möhre bringt Feuchtigkeit, länger und niedriger trocknen; nach 20 Min wenden.\n7 Vollständig auskühlen lassen, dann Rosinen untermischen.", - "variations": [ - "Frischkäse-Note über Skyr = Carrot-Cake-Frosting-Effekt", - "Kokos 2 EL Kokosraspel dazu" - ], - "tip": "Coach-Tipp Möhre gut ausdrücken ist der ganze Trick – sonst bleibt es zäh statt knusprig.", - "badge": "", - "yield": "ergibt ca. 740 g · ~14 Portionen à 50 g" - }, - { - "num": "22", - "category": "Protein-Boost & Post-Workout", - "title": "Vanille-Whey-Cluster", - "tagline": "Post-Workout: viel Eiweiß, feste Brocken.", - "nutrition": "ca. ≈ 260 kcal · Eiweiß 13 g · Fett 13 g · KH 22 g · Zucker 5 g · Ballaststoffe 5 g", - "ingredients": "240 g Haferflocken · 70 g Mandeln grob · 40 g Sonnenblumenkerne · 40 g Whey Vanille · 3 EL Mandelmus · 2 EL Kokosöl · 2 EL Ahornsirup · 1 Eiweiß · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Mandeln, Kerne und Whey mischen – Pulver klumpenfrei unterheben.\n3 Mandelmus, Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Eiweiß schaumig schlagen, unterheben.\n5 Fest aufs Blech drücken.\n6 25–30 Min bei 150 °C backen, NICHT rühren – Whey bräunt früh, Farbe kontrollieren.\n7 Vollständig auskühlen lassen, in Brocken brechen.", - "variations": [ - "Schoko-Whey Schoko-Whey + 15 g Kakao", - "Erdnuss Erdnussmus statt Mandelmus" - ], - "tip": "Coach-Tipp Über 150 g Skyr sind das ~30 g Eiweiß – idealer Post-Session-Snack rund ums Training.", - "badge": "", - "yield": "ergibt ca. 700 g · ~14 Portionen à 50 g" - }, - { - "num": "23", - "category": "Protein-Boost & Post-Workout", - "title": "Schoko-Casein-Abend", - "tagline": "Langsames Eiweiß für den Couch-Abend.", - "nutrition": "ca. ≈ 256 kcal · Eiweiß 13 g · Fett 13 g · KH 21 g · Zucker 5 g · Ballaststoffe 6 g", - "ingredients": "240 g Haferflocken · 70 g Haselnüsse grob · 40 g Leinsamen · 40 g Casein Schoko · 20 g Kakao · 3 EL Mandelmus · 2 EL Kokosöl · 2 EL Ahornsirup · 1 Eiweiß · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Haselnüsse, Leinsamen, Casein und Kakao mischen.\n3 Mandelmus, Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Eiweiß schaumig schlagen, unterheben.\n5 Fest aufs Blech drücken.\n6 25–28 Min backen, NICHT rühren – Kakao + Casein bräunen schnell.\n7 Vollständig auskühlen lassen, in Brocken brechen.", - "variations": [ - "Meersalz grobes Salz vor dem Backen aufstreuen", - "Nibs 30 g Kakaonibs nach dem Backen" - ], - "tip": "Coach-Tipp Casein macht langsam satt – als Abend-Dessert über Magerquark dein bester Schoko-Couch-Ersatz.", - "badge": "", - "yield": "ergibt ca. 700 g · ~14 Portionen à 50 g" - }, - { - "num": "24", - "category": "Protein-Boost & Post-Workout", - "title": "Skyr-Topper · grob & pur", - "tagline": "Extra grobe Brocken, kaum Süße – nur zum Toppen.", - "nutrition": "ca. ≈ 244 kcal · Eiweiß 9 g · Fett 15 g · KH 19 g · Zucker 3 g · Ballaststoffe 6 g", - "ingredients": "240 g Haferflocken kernig · 100 g gemischte Nüsse grob · 40 g Kürbiskerne · 30 g Leinsamen · 3 EL Mandelmus · 2 EL Kokosöl · 1 EL Ahornsirup · 1 Eiweiß · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Hafer, Nüsse, Kerne und Leinsamen mischen.\n3 Mandelmus, Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Eiweiß schaumig schlagen, unterheben – für maximal grobe Cluster sehr fest andrücken.\n5 28–32 Min backen, NICHT rühren.\n6 Vollständig auskühlen lassen, in große Brocken brechen.", - "variations": [ - "Herzhaft Salz erhöhen, Süße weglassen", - "Zimt 1 TL Zimt für milde Süße ohne Zucke" - ], - "tip": "Coach-Tipp Bewusst wenig Zucker (3 g) – die Süße kommt vom Obst in der Schüssel. Das gröbste Rezept, perfekt als Crunch auf Quark.", - "badge": "", - "yield": "ergibt ca. 720 g · ~14 Portionen à 50 g" - }, - { - "num": "25", - "category": "Grain-free & Low-Carb", - "title": "Grain-free Nuss-Granola", - "tagline": "Ohne Getreide, sehr wenig KH, viel Crunch.", - "nutrition": "ca. ≈ 272 kcal · Eiweiß 9 g · Fett 23 g · KH 8 g · Zucker 4 g · Ballaststoffe 6 g", - "ingredients": "120 g Mandeln · 120 g Pekannüsse · 80 g Kürbiskerne · 60 g Sonnenblumenkerne · 80 g Kokoschips · 30 g Leinsamen · 3 EL Kokosöl · 2 EL Ahornsirup · 1 Eiweiß · 1 Prise Salz", - "steps": "1 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n2 Nüsse grob hacken; mit Kernen und Leinsamen mischen (Kokoschips zurückhalten).\n3 Kokosöl und Ahornsirup schmelzen, unterrühren.\n4 Eiweiß schaumig schlagen, unterheben – bindet die Cluster ohne Hafer.\n5 Fest aufs Blech drücken.\n6 18–22 Min backen; in den letzten 6 Min Kokoschips untermischen – Nüsse und Kokos verbrennen schnell.\n7 Vollständig auskühlen lassen, in Brocken brechen.", - "variations": [ - "Schoko 20 g Kakao ins Trockene", - "Zimt-Kokos 1 TL Zimt zugeben" - ], - "tip": "Coach-Tipp Für Tage mit wenig Bewegung: kaum KH, sättigt lange über Fett und Ballaststoffe. Portion trotzdem abwiegen – sehr kaloriendicht.", - "badge": "", - "yield": "ergibt ca. 650 g · ~13 Portionen à 50 g" - }, - { - "num": "26", - "category": "Grain-free & Low-Carb", - "title": "Buchweizen-Buckini-Crunch", - "tagline": "Aktivierter Buchweizen – extrem knusprig, glutenfrei.", - "nutrition": "ca. ≈ 234 kcal · Eiweiß 8 g · Fett 11 g · KH 27 g · Zucker 5 g · Ballaststoffe 5 g", - "ingredients": "200 g roher Buchweizen · 100 g Haferflocken (oder mehr Buchweizen für glutenfrei) · 80 g Mandeln grob · 40 g Sonnenblumenkerne · 3 EL Kokosöl · 2 EL Ahornsirup · 1 TL Zimt · 1 Prise Salz", - "steps": "1 Buchweizen 4–6 h in Wasser einweichen, dann gründlich abspülen und gut abtropfen lassen (macht ihn\nbekömmlicher und knuspriger).\n2 Ofen auf 150 °C Umluft, Blech mit Backpapier.\n3 Buchweizen, Hafer, Mandeln, Kerne, Zimt und Salz mischen.\n4 Kokosöl und Ahornsirup schmelzen, unterrühren.\n5 Dünn aufs Blech, andrücken.\n6 30–35 Min backen, alle 10 Min wenden – eingeweichter Buchweizen braucht länger zum Trocknen.\n7 Vollständig auskühlen lassen – wird dann glasig-knusprig.", - "variations": [ - "Kakao 20 g Kakao für Schoko-Buckini", - "Schnell Buchweizen ohne Einweichen – etwas härter, geht abe" - ], - "tip": "Coach-Tipp Der knusprigste Crunch überhaupt. Wenn Hafer dir schwer im Magen liegt, ist das die leichtere Basis.", - "badge": "", - "yield": "ergibt ca. 700 g · ~14 Portionen à 50 g" - }, - { - "num": "27", - "category": "Ohne Ofen & Overnight", - "title": "No-Bake Erdnuss-Cluster", - "tagline": "Ohne Ofen, aus dem Kühlschrank fest.", - "nutrition": "ca. ≈ 248 kcal · Eiweiß 10 g · Fett 14 g · KH 22 g · Zucker 9 g · Ballaststoffe 5 g", - "ingredients": "200 g Haferflocken · 60 g Erdnüsse grob · 30 g Leinsamen · 6 entsteinte Datteln · 4 EL Erdnussmus · 2 EL Kokosöl · 1 Prise Salz · optional 20 g Zartbitter", - "steps": "1 Datteln 10 Min in warmem Wasser einweichen, abgießen und zu einer Paste zerdrücken.\n2 Erdnussmus und Kokosöl leicht erwärmen, mit der Dattelpaste glatt rühren.\n3 Hafer, Erdnüsse, Leinsamen und Salz untermischen, bis alles klebt.\n4 Masse fest in eine mit Papier ausgelegte Form drücken (ca. 1,5 cm).\n5 Mindestens 1 h kalt stellen, bis sie fest ist.\n6 In Riegel oder Brocken schneiden; optional mit geschmolzener Zartbitter bestreichen.\n7 Kühl gelagert 1 Woche haltbar (kein Backen = kürzer haltbar als gebäcken).", - "variations": [ - "Schoko 20 g Kakao in die Paste", - "Riegel als Snack-Riegel schneiden für unterwegs" - ], - "tip": "Coach-Tipp Kein Ofen nötig – aber Datteln bringen Zucker (9 g). Portion klein halten, dann ein solider Riegel-Ersatz.", - "badge": "", - "yield": "ergibt ca. 600 g · ~12 Portionen à 50 g" - }, - { - "num": "28", - "category": "Ohne Ofen & Overnight", - "title": "Overnight-Bircher-Mix", - "tagline": "Trockenmischung zum Einweichen – quasi Granola rückwärts.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 8 g · Fett 10 g · KH 27 g · Zucker 6 g · Ballaststoffe 6 g", - "ingredients": "350 g Haferflocken · 60 g Mandeln grob · 40 g Leinsamen · 30 g Chiasamen · 1 TL Zimt · 40 g Rosinen – pro Portion: 150 g Skyr/Milch + geraspelter Apfel Trockenmix für ~14 Portionen à 50 g", - "steps": "1 Alle trockenen Zutaten mischen und in ein großes Vorratsglas füllen (kein Backen).\n2 Am Vorabend 50 g Mix mit 150 g Skyr oder Milch übergießen und verrühren.\n3 Optional geraspelten Apfel oder Beeren unterheben.\n4 Abgedeckt über Nacht in den Kühlschrank.\n5 Am Morgen kurz umrühren; bei Bedarf einen Schuss Milch für die Konsistenz.\n6 Nach Wunsch mit frischem Obst oder einem Teelöffel gebackenem Granola toppen.", - "variations": [ - "Schoko 1 TL Kakao einrühren", - "Protein 1/2 Portion Whey unterrühren", - "Beere TK-Beeren mit einweichen" - ], - "tip": "Coach-Tipp Das Löst die Ausrede „keine Zeit“ – abends 30 Sekunden Aufwand, morgens Eiweiß-Basis ohne Nachdenken.", - "badge": "", - "yield": "" - } - ] - } -} \ No newline at end of file diff --git a/plugins/trainingsplan/presets/nachmittags.json b/plugins/trainingsplan/presets/nachmittags.json deleted file mode 100644 index 109faf7..0000000 --- a/plugins/trainingsplan/presets/nachmittags.json +++ /dev/null @@ -1,1450 +0,0 @@ -{ - "name": "Kaffee-Snacks (Nachmittag)", - "config": { - "type": "recipe", - "meta": { - "title": "Kaffee-Snacks für den Nachmittag", - "header_left": "KAFFEE-SNACKS · Nachmittags-Edition · Begleitend zum 8-Wochen-Plan", - "footer_left": "Portion abfüllen · Rest aus dem Sichtfeld · bewusst zum Kaffee", - "output_name": "KaffeeSnacks_Nachmittag.pdf", - "doc_title": "Kaffee-Snacks für den Nachmittag", - "brand_color": "#6D28D9", - "steps_label": "SO GEHT'S" - }, - "intro": { - "headline": "Kaffee-Snacks für den Nachmittag", - "subtitle": "100 einfache Snacks für die Lücke zwischen Mittag und Abendessen – Grab & Eat und Vorrat, inkl.", - "paragraphs": [ - "Airfryer-Rezepten und Orientierungs-Nährwerten.", - "Nicht verzichten, sondern clever ersetzen: ein Eiweiß- oder Ballaststoff-Snack macht satt, statt nur den Mund zu", - "beschäftigen – und nimmt dem späteren Süßigkeiten-Schub die Wucht. Der Schwerpunkt liegt auf Grab & Eat (null bis", - "zwei Minuten) und auf Vorrat, den du einmal machst und die Woche über greifbar hast. Als Orientierung reichen", - "nachmittags grob 150–250 kcal – kein Ziel, sondern eine Richtung: der echte Hebel ist der Tausch, nicht das Zählen.", - "Jede Karte nennt unter NÄHRWERTE ca. Richtwerte pro Portion (kcal + Eiweiß, Fett, KH, Zucker, Ballaststoffe); bei", - "Vorrat gilt der Wert für den ganzen Ansatz. Rezepte mit AIRFRYER-Badge nennen Zeit und Temperatur für Airfryer und", - "Ofen. Für exakte Zahlen dein Produkt in YAZIO scannen.", - "Die eine Grundregel: vorher portionieren", - "Teller oder Tütchen fertig machen → Rest aus dem Sichtfeld → bewusst zum Kaffee genießen. Aus der offenen Packung", - "am Schreibtisch zu grasen ist dasselbe Loch wie früher die Chipstüte. Die abgefüllte Portion ist deine Grenze – nicht dein", - "Sättigungsgefühl." - ], - "rule_head": "", - "rule_text": "", - "categories_head": "13 Kategorien" - }, - "categories": [ - { - "name": "Cremig & süß · Löffel zum Kaffee", - "color": "#7C3AED" - }, - { - "name": "Schnell & süß · Kuchengefühl in Minuten", - "color": "#0EA5E9" - }, - { - "name": "Frisch & fruchtig · schnell gegriffen", - "color": "#EA580C" - }, - { - "name": "Knabbern & herzhaft · Chip-Ersatz", - "color": "#16A34A" - }, - { - "name": "Herzhaft & sättigend · kleine Happen", - "color": "#DC2626" - }, - { - "name": "Grab & Eat · null Aufwand", - "color": "#DB2777" - }, - { - "name": "Büro-Schublade · haltbar & vorportioniert", - "color": "#B45309" - }, - { - "name": "Energy Bites & Kugeln · Vorrat", - "color": "#4338CA" - }, - { - "name": "Riegel & Gebäck · Vorrat", - "color": "#2563EB" - }, - { - "name": "Airfryer · herzhaft & knusprig", - "color": "#475569" - }, - { - "name": "Airfryer · süß & Gebäck", - "color": "#A21CAF" - }, - { - "name": "Warm & gemütlich zum Kaffee", - "color": "#059669" - }, - { - "name": "Kaffee-Drinks & Protein-Shakes", - "color": "#92400E" - } - ], - "recipes": [ - { - "num": "1", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Skyr mit Beeren & Zimt", - "tagline": "Der schnelle Kuchen-Ersatz mit Eiweiß.", - "nutrition": "ca. ≈ 130 kcal · Eiweiß 18 g · Fett 0 g · KH 12 g · Zucker 9 g · Ballaststoffe 3 g", - "ingredients": "150 g Skyr · 1 Handvoll TK-Beeren · Zimt", - "steps": "Beeren untermischen, Zimt drüber – tauen im Skyr auf.", - "variations": [ - "+ 1 TL Honig", - "+ Kakaonibs", - "+ 1 EL Haferflocken", - "+ Vanille" - ], - "tip": "Nachmittags-Tipp Beeren färben den Skyr lila und bringen Süße ohne Zucker.", - "badge": "", - "yield": "" - }, - { - "num": "2", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Cappuccino-Quark", - "tagline": "Schmeckt wie Kaffeecreme, hält lange satt.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 28 g · Fett 1 g · KH 8 g · Zucker 5 g · Ballaststoffe 1 g", - "ingredients": "200 g Magerquark · 1 TL lösl. Espresso · Prise Kakao · etwas Süße", - "steps": "Alles glatt rühren, evtl. Schuss Milch.", - "variations": [ - "+ Zimt", - "+ Kakaonibs", - "Vanille-Version", - "+ 1 TL Erdnussmus" - ], - "tip": "Nachmittags-Tipp Der Kaffee gibt Bitternote – wirkt süßer als er ist.", - "badge": "", - "yield": "" - }, - { - "num": "3", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Skyr-„Tiramisu“ im Glas", - "tagline": "Löffelbiskuit-Gefühl ohne Zuckerbombe.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 19 g · Fett 3 g · KH 22 g · Zucker 8 g · Ballaststoffe 3 g", - "ingredients": "150 g Skyr · 2 EL Haferflocken · Schuss Espresso · Kakao", - "steps": "Haferflocken in Espresso tränken, mit Skyr schichten, Kakao drüber.", - "variations": [], - "tip": "Nachmittags-Tipp Espresso-Hafer statt Keksboden – selbe Idee, mehr Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "4", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Hüttenkäse mit Honig & Walnuss", - "tagline": "Süß-salzig, überraschend gut zum Kaffee.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 24 g · Fett 12 g · KH 8 g · Zucker 7 g · Ballaststoffe 1 g", - "ingredients": "200 g Hüttenkäse · 1 TL Honig · 15 g Walnüsse", - "steps": "Hüttenkäse in die Schale, Honig und Nuss drüber.", - "variations": [], - "tip": "Nachmittags-Tipp Sehr eiweißreich – trägt bis zum Abendessen.", - "badge": "", - "yield": "" - }, - { - "num": "5", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Schoko-Protein-Pudding", - "tagline": "Dunkle Schokonote ohne Riegel.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 24 g · Fett 2 g · KH 9 g · Zucker 5 g · Ballaststoffe 2 g", - "ingredients": "150 g Magerquark · 1 EL Kakao · etwas Süße · Schuss Milch", - "steps": "Kakao und Süße einrühren, mit Milch cremig ziehen.", - "variations": [ - "+ Erdnussmus", - "+ 1 TL Espresso", - "+ 1/2 Banane", - "+ Kakaonibs" - ], - "tip": "Nachmittags-Tipp Deckt die Schokolust mit fast nur Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "6", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Vanille-Quark mit Nuss-Krokant", - "tagline": "Krokant gibt Biss zum weichen Quark.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 20 g · Fett 8 g · KH 8 g · Zucker 6 g · Ballaststoffe 1 g", - "ingredients": "150 g Quark · Vanille · 1 TL geh. Nüsse (in der Pfanne geröstet)", - "steps": "Nüsse ohne Öl rösten, über den Vanille-Quark streuen.", - "variations": [], - "tip": "Nachmittags-Tipp Warme Nuss auf kaltem Quark – kleiner Kontrast, große Wirkung.", - "badge": "", - "yield": "" - }, - { - "num": "7", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Skyr mit Apfel & Zimt", - "tagline": "Apfel-Crunch im cremigen Skyr.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 17 g · Fett 0 g · KH 18 g · Zucker 14 g · Ballaststoffe 2 g", - "ingredients": "150 g Skyr · 1/2 Apfel gewürfelt · Zimt", - "steps": "Apfel würfeln, unterheben, Zimt drüber.", - "variations": [], - "tip": "Nachmittags-Tipp Frischer Biss statt Kuchengabel.", - "badge": "", - "yield": "" - }, - { - "num": "8", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Griechischer Joghurt mit Beeren-Swirl", - "tagline": "Vollfett macht satt – kleine Portion reicht.", - "nutrition": "ca. ≈ 170 kcal · Eiweiß 7 g · Fett 14 g · KH 9 g · Zucker 8 g · Ballaststoffe 2 g", - "ingredients": "150 g griech. Joghurt 10% · 2 EL zerdrückte Beeren", - "steps": "Beeren zerdrücken, in den Joghurt strudeln.", - "variations": [], - "tip": "Nachmittags-Tipp Das Fett sättigt – bewusst eine kleine Schale.", - "badge": "", - "yield": "" - }, - { - "num": "9", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Kaffee-Bananen-Creme", - "tagline": "Süße kommt aus der Banane, kein Zucker nötig.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 20 g · Fett 1 g · KH 24 g · Zucker 16 g · Ballaststoffe 2 g", - "ingredients": "1/2 Banane · 150 g Quark · 1 TL lösl. Espresso", - "steps": "Banane zerdrücken, mit Quark und Kaffee glatt rühren.", - "variations": [], - "tip": "Nachmittags-Tipp Reife Banane süßt von allein.", - "badge": "", - "yield": "" - }, - { - "num": "10", - "category": "Cremig & süß · Löffel zum Kaffee", - "title": "Zimt-Quark mit Kaffeekrokant", - "tagline": "Bratapfel-Aroma zum Espresso.", - "nutrition": "ca. ≈ 170 kcal · Eiweiß 20 g · Fett 5 g · KH 12 g · Zucker 8 g · Ballaststoffe 2 g", - "ingredients": "150 g Quark · Zimt · 1 TL geröstete Haferflocken + Nuss", - "steps": "Hafer und Nuss trocken rösten, über den Zimt-Quark.", - "variations": [], - "tip": "Nachmittags-Tipp Knusper-Topping macht aus Quark ein Dessert.", - "badge": "", - "yield": "" - }, - { - "num": "11", - "category": "Schnell & süß · Kuchengefühl in Minuten", - "title": "Protein-Mugcake (Schoko)", - "tagline": "Warmer Kuchen in der Tasse, frisch am besten.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 15 g · Fett 8 g · KH 30 g · Zucker 6 g · Ballaststoffe 5 g", - "ingredients": "3 EL Haferflocken · 1 Ei · 1 EL Kakao · Prise Backpulver · Süße", - "steps": "Alles in der Tasse verrühren, 80–90 s in die Mikrowelle.", - "variations": [ - "+ Zartbitter-Drops", - "+ Beeren", - "+ 1 TL Erdnussmus", - "Vanille-Version" - ], - "tip": "Nachmittags-Tipp Der Klassiker gegen den Kaffee-und-Kuchen-Reflex.", - "badge": "", - "yield": "" - }, - { - "num": "12", - "category": "Schnell & süß · Kuchengefühl in Minuten", - "title": "Blitz-Käsekuchen im Glas", - "tagline": "No-Bake, in 2 Minuten geschichtet.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 20 g · Fett 5 g · KH 20 g · Zucker 10 g · Ballaststoffe 1 g", - "ingredients": "150 g Magerquark · 1 Vollkornkeks zerbröselt · Süße · Vanille", - "steps": "Keks als Boden, Quark mit Süße drüber schichten.", - "variations": [], - "tip": "Nachmittags-Tipp Käsekuchen-Gefühl ohne Ofen und ohne halbe Torte.", - "badge": "", - "yield": "" - }, - { - "num": "13", - "category": "Schnell & süß · Kuchengefühl in Minuten", - "title": "Bananen-Ei-Pfännchen", - "tagline": "2 Zutaten, warmes Kuchengefühl.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 9 g · Fett 6 g · KH 28 g · Zucker 15 g · Ballaststoffe 2 g", - "ingredients": "1 Banane · 1 Ei", - "steps": "Zerdrücken, verquirlen, als kleine Pancakes in die Pfanne.", - "variations": [ - "+ Zimt", - "+ Beeren", - "+ 1 EL Haferflocken" - ], - "tip": "Nachmittags-Tipp Nichts süßen – die Banane reicht.", - "badge": "", - "yield": "" - }, - { - "num": "14", - "category": "Schnell & süß · Kuchengefühl in Minuten", - "title": "Apfel-Zimt-Tasse", - "tagline": "Wie warmer Bratapfel, ohne Ofen.", - "nutrition": "ca. ≈ 130 kcal · Eiweiß 5 g · Fett 0 g · KH 25 g · Zucker 20 g · Ballaststoffe 4 g", - "ingredients": "1 Apfel gewürfelt · Zimt · danach Klecks Quark", - "steps": "Apfel mit Zimt 2 min in die Mikrowelle, Quark drauf.", - "variations": [], - "tip": "Nachmittags-Tipp Schnellster Bratapfel der Welt.", - "badge": "", - "yield": "" - }, - { - "num": "15", - "category": "Schnell & süß · Kuchengefühl in Minuten", - "title": "Haferflocken-Mikro-Kuchen", - "tagline": "Löffelbarer Mini-Auflauf.", - "nutrition": "ca. ≈ 230 kcal · Eiweiß 8 g · Fett 4 g · KH 42 g · Zucker 14 g · Ballaststoffe 6 g", - "ingredients": "4 EL Haferflocken · Schuss Milch · 1/2 zerdrückte Banane", - "steps": "Verrühren, 2 min in die Mikrowelle.", - "variations": [], - "tip": "Nachmittags-Tipp Wird fest wie ein warmer Kuchen – mit dem Löffel essen.", - "badge": "", - "yield": "" - }, - { - "num": "16", - "category": "Schnell & süß · Kuchengefühl in Minuten", - "title": "Protein-Waffel", - "tagline": "Wenn das Eisen da ist: knusprig, herzhaft-süß.", - "nutrition": "ca. ≈ 220 kcal · Eiweiß 18 g · Fett 7 g · KH 22 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "3 EL gem. Haferflocken · 1 Ei · 50 g Quark", - "steps": "Zu Teig rühren, im Waffeleisen backen.", - "variations": [], - "tip": "Nachmittags-Tipp Warm mit Beeren oder Quark ein kleines Dessert.", - "badge": "", - "yield": "" - }, - { - "num": "17", - "category": "Schnell & süß · Kuchengefühl in Minuten", - "title": "Schoko-Tassenkuchen ohne Ei", - "tagline": "Vegan-tauglich, saftig.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 7 g · Fett 4 g · KH 35 g · Zucker 8 g · Ballaststoffe 5 g", - "ingredients": "3 EL gem. Haferflocken · 1 EL Kakao · Schuss Milch · Backpulver", - "steps": "Verrühren, 80 s in die Mikrowelle.", - "variations": [], - "tip": "Nachmittags-Tipp Ohne Ei, trotzdem fluffig.", - "badge": "", - "yield": "" - }, - { - "num": "18", - "category": "Schnell & süß · Kuchengefühl in Minuten", - "title": "Beeren-Quark-Auflauf (Mikro)", - "tagline": "Fluffig-warm, sehr proteinreich.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 24 g · Fett 6 g · KH 10 g · Zucker 7 g · Ballaststoffe 2 g", - "ingredients": "150 g Quark · 1 Ei · Handvoll Beeren", - "steps": "Quark und Ei verrühren, Beeren rein, 2–3 min Mikrowelle.", - "variations": [], - "tip": "Nachmittags-Tipp Warmer Eiweiß-Snack, der wie Nachtisch schmeckt.", - "badge": "", - "yield": "" - }, - { - "num": "19", - "category": "Frisch & fruchtig · schnell gegriffen", - "title": "Apfelspalten mit Erdnussmus", - "tagline": "Süß-nussig, deckt die Naschlust.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 6 g · Fett 10 g · KH 24 g · Zucker 18 g · Ballaststoffe 4 g", - "ingredients": "1 Apfel · 1 EL Erdnussmus", - "steps": "Apfel in Spalten, Mus als Dip.", - "variations": [], - "tip": "Nachmittags-Tipp Frucht + gutes Fett – hält länger als Gummibärchen.", - "badge": "", - "yield": "" - }, - { - "num": "20", - "category": "Frisch & fruchtig · schnell gegriffen", - "title": "Beeren-Becher pur", - "tagline": "Wenig kcal, viel Volumen.", - "nutrition": "ca. ≈ 70 kcal · Eiweiß 1 g · Fett 0 g · KH 14 g · Zucker 10 g · Ballaststoffe 4 g", - "ingredients": "150 g gemischte Beeren", - "steps": "In die Schale, fertig.", - "variations": [], - "tip": "Nachmittags-Tipp Füllt das Auge und die Hand für fast nichts.", - "badge": "", - "yield": "" - }, - { - "num": "21", - "category": "Frisch & fruchtig · schnell gegriffen", - "title": "Weintrauben gefroren", - "tagline": "Dein Gummibärchen-Ersatz, kalt und langsam.", - "nutrition": "ca. ≈ 60 kcal · Eiweiß 0 g · Fett 0 g · KH 15 g · Zucker 15 g · Ballaststoffe 1 g", - "ingredients": "1 Handvoll Trauben (TK)", - "steps": "Trauben ins Gefrierfach, gefroren snacken.", - "variations": [], - "tip": "Nachmittags-Tipp Gefroren isst man sie langsam – genau der Trick.", - "badge": "", - "yield": "" - }, - { - "num": "22", - "category": "Frisch & fruchtig · schnell gegriffen", - "title": "Banane mit Zartbitter-Dip", - "tagline": "Schoko als Hülle, Frucht trägt die Süße.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 3 g · Fett 8 g · KH 28 g · Zucker 18 g · Ballaststoffe 3 g", - "ingredients": "1 Banane · 15 g 85%-Schokolade", - "steps": "Schoko schmelzen, Bananenstücke tunken, kurz fest werden lassen.", - "variations": [], - "tip": "Nachmittags-Tipp Wenig Schoko, viel Effekt.", - "badge": "", - "yield": "" - }, - { - "num": "23", - "category": "Frisch & fruchtig · schnell gegriffen", - "title": "Orangen-Filets mit Zimt", - "tagline": "Erfrischend, passt zum Espresso.", - "nutrition": "ca. ≈ 80 kcal · Eiweiß 1 g · Fett 0 g · KH 18 g · Zucker 14 g · Ballaststoffe 3 g", - "ingredients": "1 Orange · Zimt", - "steps": "Filetieren, Zimt drüber.", - "variations": [], - "tip": "Nachmittags-Tipp Säure weckt – gute Kaffee-Begleitung.", - "badge": "", - "yield": "" - }, - { - "num": "24", - "category": "Frisch & fruchtig · schnell gegriffen", - "title": "Melone-Feta-Spieße", - "tagline": "Süß-salzig, sommerlich.", - "nutrition": "ca. ≈ 140 kcal · Eiweiß 6 g · Fett 8 g · KH 12 g · Zucker 10 g · Ballaststoffe 1 g", - "ingredients": "150 g Melone · 30 g Feta", - "steps": "Würfeln, abwechselnd aufspießen.", - "variations": [], - "tip": "Nachmittags-Tipp Der Salz-Kontrast macht die Frucht spannend.", - "badge": "", - "yield": "" - }, - { - "num": "25", - "category": "Frisch & fruchtig · schnell gegriffen", - "title": "Birne mit Walnuss", - "tagline": "Klassische Kaffee-Kombi.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 3 g · Fett 10 g · KH 24 g · Zucker 16 g · Ballaststoffe 4 g", - "ingredients": "1 Birne · 15 g Walnüsse", - "steps": "Birne in Spalten, Nuss dazu.", - "variations": [], - "tip": "Nachmittags-Tipp Zwei Zutaten, fertig ist der Nachmittagsteller.", - "badge": "", - "yield": "" - }, - { - "num": "26", - "category": "Frisch & fruchtig · schnell gegriffen", - "title": "Physalis-Handvoll", - "tagline": "Klein, süß-säuerlich, beschäftigt die Hände.", - "nutrition": "ca. ≈ 55 kcal · Eiweiß 1 g · Fett 0 g · KH 11 g · Zucker 8 g · Ballaststoffe 3 g", - "ingredients": "100 g Physalis", - "steps": "Aus den Blättern pulen, essen.", - "variations": [], - "tip": "Nachmittags-Tipp Das Pulen bremst das Tempo.", - "badge": "", - "yield": "" - }, - { - "num": "27", - "category": "Knabbern & herzhaft · Chip-Ersatz", - "title": "Edamame mit Meersalz", - "tagline": "Pulen bremst das Tempo von allein.", - "nutrition": "ca. ≈ 120 kcal · Eiweiß 12 g · Fett 5 g · KH 9 g · Zucker 2 g · Ballaststoffe 6 g", - "ingredients": "150 g TK-Edamame (Schote) · Meersalz", - "steps": "Nach Packung erwärmen, salzen, aus der Schote pulen.", - "variations": [ - "Chili-Flocken", - "Sesam + Salz", - "Knoblauch-Soja", - "Limette-Chili" - ], - "tip": "Nachmittags-Tipp Proteinreich – und das Pulen verlangsamt das Essen.", - "badge": "", - "yield": "" - }, - { - "num": "28", - "category": "Knabbern & herzhaft · Chip-Ersatz", - "title": "Salzige Mandeln", - "tagline": "Eine Handvoll = eine Portion, dann Tüte zu.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 6 g · Fett 13 g · KH 3 g · Zucker 1 g · Ballaststoffe 3 g", - "ingredients": "25 g Mandeln · Prise Salz", - "steps": "Abzählen oder abwiegen, salzen.", - "variations": [], - "tip": "Nachmittags-Tipp Kaloriendicht – die abgewogene Menge ist die Grenze.", - "badge": "", - "yield": "" - }, - { - "num": "29", - "category": "Knabbern & herzhaft · Chip-Ersatz", - "title": "Käsewürfel", - "tagline": "Herzhaft, sättigt, null Aufwand.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 10 g · Fett 12 g · KH 0 g · Zucker 0 g · Ballaststoffe 0 g", - "ingredients": "40 g Gouda/Emmentaler", - "steps": "In Würfel schneiden.", - "variations": [], - "tip": "Nachmittags-Tipp Reines Eiweiß-Fett-Paket gegen den Chips-Reflex.", - "badge": "", - "yield": "" - }, - { - "num": "30", - "category": "Knabbern & herzhaft · Chip-Ersatz", - "title": "Oliven & Cornichons", - "tagline": "Salzig ohne Chips-Fett-Bombe.", - "nutrition": "ca. ≈ 90 kcal · Eiweiß 1 g · Fett 8 g · KH 2 g · Zucker 1 g · Ballaststoffe 1 g", - "ingredients": "8 Oliven · 3 Gewürzgurken", - "steps": "In ein Schälchen, fertig.", - "variations": [], - "tip": "Nachmittags-Tipp Deckt die Salz-Gier mit fast keinen Kalorien.", - "badge": "", - "yield": "" - }, - { - "num": "31", - "category": "Knabbern & herzhaft · Chip-Ersatz", - "title": "Knäckebrot mit Frischkäse", - "tagline": "Knusper + herzhaft.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 6 g · Fett 7 g · KH 16 g · Zucker 2 g · Ballaststoffe 4 g", - "ingredients": "2 Knäckebrote · 30 g Frischkäse · Schnittlauch", - "steps": "Bestreichen, Schnittlauch drüber.", - "variations": [], - "tip": "Nachmittags-Tipp Der Crunch ersetzt die Chips-Textur.", - "badge": "", - "yield": "" - }, - { - "num": "32", - "category": "Knabbern & herzhaft · Chip-Ersatz", - "title": "Reiswaffeln mit Hüttenkäse", - "tagline": "Leicht, proteinreich.", - "nutrition": "ca. ≈ 140 kcal · Eiweiß 9 g · Fett 4 g · KH 16 g · Zucker 2 g · Ballaststoffe 1 g", - "ingredients": "2 Reiswaffeln · 60 g Hüttenkäse · Pfeffer", - "steps": "Belegen, pfeffern.", - "variations": [], - "tip": "Nachmittags-Tipp Viel Volumen, wenig Kalorien.", - "badge": "", - "yield": "" - }, - { - "num": "33", - "category": "Knabbern & herzhaft · Chip-Ersatz", - "title": "Wasabi-Erbsen", - "tagline": "Scharf-knusprig, kleine Menge reicht.", - "nutrition": "ca. ≈ 120 kcal · Eiweiß 5 g · Fett 3 g · KH 18 g · Zucker 2 g · Ballaststoffe 3 g", - "ingredients": "30 g Wasabi-Erbsen (gekauft)", - "steps": "Portion abfüllen.", - "variations": [], - "tip": "Nachmittags-Tipp Die Schärfe bremst – man isst automatisch langsamer.", - "badge": "", - "yield": "" - }, - { - "num": "34", - "category": "Knabbern & herzhaft · Chip-Ersatz", - "title": "Salzstangen mit Senf-Dip", - "tagline": "Portion abzählen, nicht die Tüte.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 5 g · Fett 2 g · KH 28 g · Zucker 2 g · Ballaststoffe 1 g", - "ingredients": "30 g Salzstangen · 1 EL Quark-Senf-Dip", - "steps": "Dip anrühren (Quark + Senf), Stangen dazu.", - "variations": [], - "tip": "Nachmittags-Tipp Klar portioniert schlägt die offene Tüte.", - "badge": "", - "yield": "" - }, - { - "num": "35", - "category": "Knabbern & herzhaft · Chip-Ersatz", - "title": "Trockenfleisch (Jerky)", - "tagline": "Herzhaft-zäh, hoher Eiweißanteil, lange haltbar.", - "nutrition": "ca. ≈ 110 kcal · Eiweiß 18 g · Fett 3 g · KH 4 g · Zucker 3 g · Ballaststoffe 0 g", - "ingredients": "30 g Beef Jerky", - "steps": "Aus der Packung, fertig.", - "variations": [], - "tip": "Nachmittags-Tipp Perfekt für die Büro-Schublade.", - "badge": "", - "yield": "" - }, - { - "num": "36", - "category": "Herzhaft & sättigend · kleine Happen", - "title": "Gekochtes Ei, gewürzt", - "tagline": "Der unterschätzte Sattmacher.", - "nutrition": "ca. ≈ 155 kcal · Eiweiß 13 g · Fett 11 g · KH 1 g · Zucker 0 g · Ballaststoffe 0 g", - "ingredients": "2 Eier · Salz, Pfeffer, Paprika", - "steps": "Pellen, würzen.", - "variations": [ - "+ Senf", - "+ Sriracha", - "+ Everything-Gewürz", - "halbiert mit Avocado" - ], - "tip": "Nachmittags-Tipp Sonntag vorkochen – immer griffbereit.", - "badge": "", - "yield": "" - }, - { - "num": "37", - "category": "Herzhaft & sättigend · kleine Happen", - "title": "Vollkorn-Toast mit Hüttenkäse", - "tagline": "Eine Scheibe = die Portion.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 12 g · Fett 4 g · KH 18 g · Zucker 3 g · Ballaststoffe 4 g", - "ingredients": "1 Scheibe Vollkorntoast · 60 g Hüttenkäse · Tomate", - "steps": "Toasten, belegen.", - "variations": [], - "tip": "Nachmittags-Tipp Bewusst eine Scheibe statt halber Brottüte.", - "badge": "", - "yield": "" - }, - { - "num": "38", - "category": "Herzhaft & sättigend · kleine Happen", - "title": "Mini-Wrap mit Putenbrust", - "tagline": "Rollen, halbieren, essen.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 14 g · Fett 5 g · KH 15 g · Zucker 2 g · Ballaststoffe 2 g", - "ingredients": "1/2 Vollkorn-Tortilla · 40 g Putenbrust · Frischkäse", - "steps": "Bestreichen, belegen, rollen.", - "variations": [], - "tip": "Nachmittags-Tipp Klein und transportabel – auch fürs Büro.", - "badge": "", - "yield": "" - }, - { - "num": "39", - "category": "Herzhaft & sättigend · kleine Happen", - "title": "Gurken-Boote mit Thunfisch", - "tagline": "Frisch, sehr proteinreich.", - "nutrition": "ca. ≈ 110 kcal · Eiweiß 15 g · Fett 2 g · KH 3 g · Zucker 2 g · Ballaststoffe 1 g", - "ingredients": "1/2 Gurke · 50 g Thunfisch (Wasser) · 1 EL Joghurt", - "steps": "Gurke halbieren, aushöhlen, Thunfisch-Joghurt reinfüllen.", - "variations": [], - "tip": "Nachmittags-Tipp Kaum Kalorien, viel Sättigung.", - "badge": "", - "yield": "" - }, - { - "num": "40", - "category": "Herzhaft & sättigend · kleine Happen", - "title": "Käse-Schinken-Röllchen", - "tagline": "Low Carb, herzhaft.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 16 g · Fett 13 g · KH 1 g · Zucker 1 g · Ballaststoffe 0 g", - "ingredients": "30 g Käse · 30 g Schinken", - "steps": "Aufeinanderlegen, rollen.", - "variations": [], - "tip": "Nachmittags-Tipp Kein Brot nötig – der Belag ist der Snack.", - "badge": "", - "yield": "" - }, - { - "num": "41", - "category": "Herzhaft & sättigend · kleine Happen", - "title": "Tomate-Mozzarella (Mini)", - "tagline": "Frisch und mediterran.", - "nutrition": "ca. ≈ 120 kcal · Eiweiß 9 g · Fett 8 g · KH 4 g · Zucker 4 g · Ballaststoffe 1 g", - "ingredients": "5 Kirschtomaten · 40 g Mini-Mozzarella · Basilikum", - "steps": "Halbieren, mischen, Basilikum drüber.", - "variations": [], - "tip": "Nachmittags-Tipp Leicht, sättigend, sieht nach mehr aus.", - "badge": "", - "yield": "" - }, - { - "num": "42", - "category": "Herzhaft & sättigend · kleine Happen", - "title": "Hummus mit Gemüsesticks", - "tagline": "Cremig-herzhaft, Ballaststoffe satt.", - "nutrition": "ca. ≈ 140 kcal · Eiweiß 5 g · Fett 8 g · KH 12 g · Zucker 4 g · Ballaststoffe 4 g", - "ingredients": "40 g Hummus · Paprika- und Möhrensticks", - "steps": "Gemüse schneiden, dippen.", - "variations": [], - "tip": "Nachmittags-Tipp Der Dip macht rohes Gemüse zum Genuss.", - "badge": "", - "yield": "" - }, - { - "num": "43", - "category": "Herzhaft & sättigend · kleine Happen", - "title": "Vollkorn-Cracker mit Käse", - "tagline": "Klar portioniert, herzhaft.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 9 g · Fett 11 g · KH 12 g · Zucker 1 g · Ballaststoffe 2 g", - "ingredients": "4 Vollkorn-Cracker · 30 g Käse", - "steps": "Belegen.", - "variations": [], - "tip": "Nachmittags-Tipp Abgezählte Stück statt Griff in die Packung.", - "badge": "", - "yield": "" - }, - { - "num": "44", - "category": "Grab & Eat · null Aufwand", - "title": "Fertiger Protein-Pudding", - "tagline": "Für den Impuls-Griff im Kühlschrank.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 20 g · Fett 3 g · KH 12 g · Zucker 8 g · Ballaststoffe 0 g", - "ingredients": "1 Becher (200 g)", - "steps": "Deckel auf, löffeln.", - "variations": [], - "tip": "Nachmittags-Tipp Wenn es schnell gehen muss – besser als der Riegel-Automat.", - "badge": "", - "yield": "" - }, - { - "num": "45", - "category": "Grab & Eat · null Aufwand", - "title": "Skyr-Becher pur", - "tagline": "Nichts drin, was reinmuss – einfach löffeln.", - "nutrition": "ca. ≈ 90 kcal · Eiweiß 17 g · Fett 0 g · KH 6 g · Zucker 4 g · Ballaststoffe 0 g", - "ingredients": "1 Becher Skyr (150 g)", - "steps": "Öffnen, essen.", - "variations": [], - "tip": "Nachmittags-Tipp Purer Eiweiß-Sattmacher, null Vorbereitung.", - "badge": "", - "yield": "" - }, - { - "num": "46", - "category": "Grab & Eat · null Aufwand", - "title": "Harzer Käse", - "tagline": "Eiweiß-Bombe, fast fettfrei.", - "nutrition": "ca. ≈ 65 kcal · Eiweiß 15 g · Fett 0 g · KH 0 g · Zucker 0 g · Ballaststoffe 0 g", - "ingredients": "1 Rolle (~50 g)", - "steps": "Aus der Packung, evtl. pfeffern.", - "variations": [], - "tip": "Nachmittags-Tipp Kaum Kalorien, extrem viel Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "47", - "category": "Grab & Eat · null Aufwand", - "title": "Babybel + Apfel", - "tagline": "Klassisch, transportabel.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 7 g · Fett 6 g · KH 18 g · Zucker 16 g · Ballaststoffe 3 g", - "ingredients": "1 Babybel · 1 Apfel", - "steps": "Auspacken, essen.", - "variations": [], - "tip": "Nachmittags-Tipp Passt in jede Tasche.", - "badge": "", - "yield": "" - }, - { - "num": "48", - "category": "Grab & Eat · null Aufwand", - "title": "Handvoll Studentenfutter", - "tagline": "Portion abfüllen, Rest weg.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 4 g · Fett 10 g · KH 14 g · Zucker 10 g · Ballaststoffe 2 g", - "ingredients": "30 g (gekauft)", - "steps": "Abwiegen, Rest wegräumen.", - "variations": [], - "tip": "Nachmittags-Tipp Kaloriendicht – die Handvoll ist die Grenze.", - "badge": "", - "yield": "" - }, - { - "num": "49", - "category": "Grab & Eat · null Aufwand", - "title": "Proteinriegel (gekauft)", - "tagline": "Notfall-Griff – Zutatenliste checken.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 20 g · Fett 7 g · KH 18 g · Zucker 3 g · Ballaststoffe 2 g", - "ingredients": "1 Riegel (~50 g)", - "steps": "Auspacken.", - "variations": [], - "tip": "Nachmittags-Tipp Für unterwegs okay – aber kein Dauer-Ersatz für echtes Essen.", - "badge": "", - "yield": "" - }, - { - "num": "50", - "category": "Grab & Eat · null Aufwand", - "title": "Trinkjoghurt / Kefir", - "tagline": "Flüssig, schnell – Zucker im Blick.", - "nutrition": "ca. ≈ 130 kcal · Eiweiß 9 g · Fett 5 g · KH 14 g · Zucker 14 g · Ballaststoffe 0 g", - "ingredients": "250 ml", - "steps": "Trinken.", - "variations": [], - "tip": "Nachmittags-Tipp Praktisch, aber gezuckerte Sorten meiden.", - "badge": "", - "yield": "" - }, - { - "num": "51", - "category": "Grab & Eat · null Aufwand", - "title": "Zwei Stück Zartbitter 85%", - "tagline": "", - "nutrition": "ca. ≈ 90 kcal · Eiweiß 1 g · Fett 7 g · KH 5 g · Zucker 3 g · Ballaststoffe 2 g", - "ingredients": "Intensiv – 2 Stück genügen wirklich. 2 Stück (~15 g)", - "steps": "Langsam auf der Zunge schmelzen lassen.", - "variations": [], - "tip": "Nachmittags-Tipp Dunkle Schoko sättigt den Impuls schneller als Vollmilch.", - "badge": "", - "yield": "" - }, - { - "num": "52", - "category": "Büro-Schublade · haltbar & vorportioniert", - "title": "Nuss-Portionstütchen", - "tagline": "Sonntag abfüllen, Woche mitnehmen.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 5 g · Fett 14 g · KH 4 g · Zucker 1 g · Ballaststoffe 2 g", - "ingredients": "25 g gemischte Nüsse pro Tütchen", - "steps": "In kleine Beutel/Dosen portionieren.", - "variations": [], - "tip": "Nachmittags-Tipp Ein Tütchen = eine Portion, nicht die Tüte.", - "badge": "", - "yield": "" - }, - { - "num": "53", - "category": "Büro-Schublade · haltbar & vorportioniert", - "title": "Zartbitter + Mandeln", - "tagline": "Deckt Schoko- UND Knabber-Lust.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 4 g · Fett 14 g · KH 8 g · Zucker 3 g · Ballaststoffe 3 g", - "ingredients": "2 Stück 85% · 10 Mandeln", - "steps": "Zusammen ins Tütchen.", - "variations": [], - "tip": "Nachmittags-Tipp Zwei Impulse mit einer vorportionierten Ration bedient.", - "badge": "", - "yield": "" - }, - { - "num": "54", - "category": "Büro-Schublade · haltbar & vorportioniert", - "title": "Geröstete Kichererbsen (Vorrat)", - "tagline": "Knusprig, unbekühlt haltbar.", - "nutrition": "ca. ≈ 130 kcal · Eiweiß 6 g · Fett 4 g · KH 16 g · Zucker 1 g · Ballaststoffe 5 g", - "ingredients": "30 g (selbst geröstet o. gekauft)", - "steps": "In verschließbare Dose.", - "variations": [], - "tip": "Nachmittags-Tipp Hält ein paar Tage – Chip-Ersatz für den Schreibtisch.", - "badge": "", - "yield": "" - }, - { - "num": "55", - "category": "Büro-Schublade · haltbar & vorportioniert", - "title": "Reiswaffeln (einzeln)", - "tagline": "Leer knabbern oder mit Portionsmus.", - "nutrition": "ca. ≈ 60 kcal · Eiweiß 1 g · Fett 1 g · KH 13 g · Zucker 0 g · Ballaststoffe 0 g", - "ingredients": "2 Stück", - "steps": "Einzeln in der Schublade lagern.", - "variations": [], - "tip": "Nachmittags-Tipp Trocken-Vorrat, kombinierbar mit allem aus dem Bürokühlschrank.", - "badge": "", - "yield": "" - }, - { - "num": "56", - "category": "Büro-Schublade · haltbar & vorportioniert", - "title": "Erdnussmus-Portionsbeutel", - "tagline": "Kein Glas am Platz – einzeln dosiert.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 7 g · Fett 13 g · KH 8 g · Zucker 2 g · Ballaststoffe 2 g", - "ingredients": "1 Portionsbeutel (20 g) · dazu Reiswaffel", - "steps": "Beutel aufreißen, auf die Waffel.", - "variations": [], - "tip": "Nachmittags-Tipp Ein Beutel begrenzt – das Glas nicht.", - "badge": "", - "yield": "" - }, - { - "num": "57", - "category": "Büro-Schublade · haltbar & vorportioniert", - "title": "Trockenobst-Nuss-Mix", - "tagline": "Trockenobst bewusst wenig – zuckerintensiv.", - "nutrition": "ca. ≈ 150 kcal · Eiweiß 4 g · Fett 9 g · KH 14 g · Zucker 11 g · Ballaststoffe 2 g", - "ingredients": "15 g Trockenobst · 15 g Nüsse", - "steps": "Mischen, portionieren.", - "variations": [], - "tip": "Nachmittags-Tipp Nüsse tragen, Trockenobst ist nur Deko.", - "badge": "", - "yield": "" - }, - { - "num": "58", - "category": "Büro-Schublade · haltbar & vorportioniert", - "title": "Knäckebrot einzeln", - "tagline": "Trocken-Vorrat, dazu was aus dem Kühlschrank.", - "nutrition": "ca. ≈ 80 kcal · Eiweiß 2 g · Fett 1 g · KH 14 g · Zucker 1 g · Ballaststoffe 3 g", - "ingredients": "2 Scheiben", - "steps": "In der Schublade lagern.", - "variations": [], - "tip": "Nachmittags-Tipp Basis für einen schnellen herzhaften Happen.", - "badge": "", - "yield": "" - }, - { - "num": "59", - "category": "Büro-Schublade · haltbar & vorportioniert", - "title": "Geröstete Sojabohnen", - "tagline": "Knusprig, sehr proteinreich, lange haltbar.", - "nutrition": "ca. ≈ 120 kcal · Eiweiß 11 g · Fett 6 g · KH 7 g · Zucker 2 g · Ballaststoffe 3 g", - "ingredients": "25 g", - "steps": "Portion abfüllen.", - "variations": [], - "tip": "Nachmittags-Tipp Der proteinreichste Knabber-Vorrat fürs Büro.", - "badge": "", - "yield": "" - }, - { - "num": "60", - "category": "Energy Bites & Kugeln · Vorrat", - "title": "Kaffee-Energy-Balls", - "tagline": "1–2 Kugeln reichen – Kaffee macht sie besonders.", - "nutrition": "ca. ≈ 700 kcal · Eiweiß 22 g · Fett 26 g · KH 92 g · Zucker 35 g · Ballaststoffe 12 g · ganzer Ansatz", - "ingredients": "100 g Haferflocken · 2 EL Erdnussmus · 4 Datteln · 1 TL lösl. Espresso", - "steps": "Alles zu einer Masse mixen, Kugeln formen, kalt stellen.", - "variations": [ - "+ Kakao", - "in Kokos wälzen", - "+ Cranberrys" - ], - "tip": "Nachmittags-Tipp Kühlschrank-Vorrat, mitnehmbar – der Kaffee-Kick zum Nachmittag.", - "badge": "", - "yield": "" - }, - { - "num": "61", - "category": "Energy Bites & Kugeln · Vorrat", - "title": "Kakao-Kokos-Kugeln", - "tagline": "Raffaello-Gefühl mit Hafer-Basis.", - "nutrition": "ca. ≈ 640 kcal · Eiweiß 14 g · Fett 20 g · KH 100 g · Zucker 34 g · Ballaststoffe 14 g · ganzer Ansatz", - "ingredients": "100 g Haferflocken · 3 Datteln · 1 EL Kakao · Kokosraspel", - "steps": "Mixen, Kugeln formen, in Kokos wälzen, kalt stellen.", - "variations": [], - "tip": "Nachmittags-Tipp Cremig-kokosig, ohne Sahnefüllung.", - "badge": "", - "yield": "" - }, - { - "num": "62", - "category": "Energy Bites & Kugeln · Vorrat", - "title": "Protein-Bliss-Balls", - "tagline": "Mehr Eiweiß pro Kugel.", - "nutrition": "ca. ≈ 640 kcal · Eiweiß 45 g · Fett 26 g · KH 55 g · Zucker 10 g · Ballaststoffe 8 g · ganzer Ansatz", - "ingredients": "60 g Haferflocken · 30 g Proteinpulver · 2 EL Erdnussmus · Schuss Milch", - "steps": "Zu fester Masse kneten, Kugeln formen.", - "variations": [], - "tip": "Nachmittags-Tipp Für die Tage, an denen der Eiweiß-Bedarf hoch ist.", - "badge": "", - "yield": "" - }, - { - "num": "63", - "category": "Energy Bites & Kugeln · Vorrat", - "title": "Cranberry-Nuss-Kugeln", - "tagline": "Fruchtig-herb.", - "nutrition": "ca. ≈ 620 kcal · Eiweiß 12 g · Fett 24 g · KH 88 g · Zucker 38 g · Ballaststoffe 10 g · ganzer Ansatz", - "ingredients": "80 g Haferflocken · 30 g Cranberrys · 30 g Nüsse · 2 Datteln", - "steps": "Mixen, Kugeln formen, kalt stellen.", - "variations": [], - "tip": "Nachmittags-Tipp Die Säure der Cranberrys bricht die Süße.", - "badge": "", - "yield": "" - }, - { - "num": "64", - "category": "Energy Bites & Kugeln · Vorrat", - "title": "Dattel-Mandel-Kugeln", - "tagline": "Nur 2 Zutaten, sehr süß – klein portionieren.", - "nutrition": "ca. ≈ 700 kcal · Eiweiß 16 g · Fett 38 g · KH 72 g · Zucker 58 g · Ballaststoffe 14 g · ganzer Ansatz", - "ingredients": "8 Datteln · 60 g Mandeln", - "steps": "Zusammen mixen, Kugeln formen.", - "variations": [], - "tip": "Nachmittags-Tipp Datteln sind zuckerintensiv – kleine Kugeln, wenige davon.", - "badge": "", - "yield": "" - }, - { - "num": "65", - "category": "Energy Bites & Kugeln · Vorrat", - "title": "Zimt-Apfel-Kugeln", - "tagline": "Schmeckt nach Apfelkuchen.", - "nutrition": "ca. ≈ 560 kcal · Eiweiß 12 g · Fett 8 g · KH 110 g · Zucker 40 g · Ballaststoffe 14 g · ganzer Ansatz", - "ingredients": "100 g Haferflocken · 3 Datteln · getr. Apfelstücke · Zimt", - "steps": "Mixen, Kugeln formen, kalt stellen.", - "variations": [], - "tip": "Nachmittags-Tipp Apfelkuchen-Aroma in einer Kugel.", - "badge": "", - "yield": "" - }, - { - "num": "66", - "category": "Energy Bites & Kugeln · Vorrat", - "title": "Schoko-Erdnuss-Kugeln", - "tagline": "Wie ein weicher Schoko-Riegel.", - "nutrition": "ca. ≈ 720 kcal · Eiweiß 22 g · Fett 38 g · KH 72 g · Zucker 26 g · Ballaststoffe 12 g · ganzer Ansatz", - "ingredients": "80 g Haferflocken · 3 EL Erdnussmus · 1 EL Kakao · 2 Datteln", - "steps": "Kneten, Kugeln formen, kalt stellen.", - "variations": [], - "tip": "Nachmittags-Tipp Riegel-Gefühl ohne die Zuckerfüllung.", - "badge": "", - "yield": "" - }, - { - "num": "67", - "category": "Energy Bites & Kugeln · Vorrat", - "title": "Marzipan-Kugeln (Mandel)", - "tagline": "Weihnachtsgefühl das ganze Jahr.", - "nutrition": "ca. ≈ 640 kcal · Eiweiß 18 g · Fett 44 g · KH 40 g · Zucker 34 g · Ballaststoffe 12 g · ganzer Ansatz", - "ingredients": "80 g gemahlene Mandeln · 3 Datteln · Tropfen Mandelaroma", - "steps": "Mixen, kleine Kugeln formen.", - "variations": [], - "tip": "Nachmittags-Tipp Marzipan-Nähe ohne den vielen Zucker.", - "badge": "", - "yield": "" - }, - { - "num": "68", - "category": "Riegel & Gebäck · Vorrat", - "title": "Protein-Haferriegel AIRFRYER", - "tagline": "", - "nutrition": "ca. ≈ 1400 kcal · Eiweiß 55 g · Fett 45 g · KH 170 g · Zucker 40 g · Ballaststoffe 20 g · ganzer Ansatz", - "ingredients": "8 Riegel, hält 5 Tage, transportabel. 150 g Haferflocken · 3 EL Erdnussmus · 2 Bananen · 30 g Proteinpulver", - "steps": "Verkneten, aufs Blech drücken, backen, in Riegel schneiden. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 180 °C, 12–15 Min Ofen: 180 °C, 20 Min", - "variations": [], - "tip": "Nachmittags-Tipp Der Büro-Klassiker: einzeln einpacken, mitnehmen.", - "badge": "", - "yield": "" - }, - { - "num": "69", - "category": "Riegel & Gebäck · Vorrat", - "title": "Baked Oats (Blech) AIRFRYER", - "tagline": "In Stücke schneiden, Kuchen-Textur.", - "nutrition": "ca. ≈ 1000 kcal · Eiweiß 38 g · Fett 22 g · KH 150 g · Zucker 40 g · Ballaststoffe 18 g · ganzer Ansatz", - "ingredients": "150 g Haferflocken · 1 Ei · 250 ml Milch · 1 Banane · Backpulver", - "steps": "Verrühren, in die Form, backen, abkühlen, schneiden. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 170 °C, 18–20 Min Ofen: 180 °C, 25–30 Min", - "variations": [], - "tip": "Nachmittags-Tipp Schmeckt wie Kuchen, trägt aber echte Portion Hafer.", - "badge": "", - "yield": "" - }, - { - "num": "70", - "category": "Riegel & Gebäck · Vorrat", - "title": "Bananenbrot-Muffins AIRFRYER", - "tagline": "6 Muffins, einzeln einpacken.", - "nutrition": "ca. ≈ 1100 kcal · Eiweiß 55 g · Fett 25 g · KH 150 g · Zucker 50 g · Ballaststoffe 16 g · ganzer Ansatz", - "ingredients": "2 Bananen · 120 g Haferflocken · 2 Eier · 30 g Proteinpulver", - "steps": "Verrühren, in die Muffinform, backen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 160 °C, 12–15 Min Ofen: 180 °C, 20 Min", - "variations": [], - "tip": "Nachmittags-Tipp Kuchen-Gefühl portioniert – perfekt für die Woche.", - "badge": "", - "yield": "" - }, - { - "num": "71", - "category": "Riegel & Gebäck · Vorrat", - "title": "Haferflocken-Cookies AIRFRYER", - "tagline": "3 Zutaten, Blech, 15 Min Ofen.", - "nutrition": "ca. ≈ 700 kcal · Eiweiß 14 g · Fett 18 g · KH 120 g · Zucker 40 g · Ballaststoffe 12 g · ganzer Ansatz", - "ingredients": "100 g Haferflocken · 1 Banane · 30 g Zartbitter-Drops", - "steps": "Mischen, kleine Häufchen, backen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 160 °C, 8–10 Min Ofen: 180 °C, 12–15 Min", - "variations": [], - "tip": "Nachmittags-Tipp Kein Zuckerzusatz nötig – die Banane bindet und süßt.", - "badge": "", - "yield": "" - }, - { - "num": "72", - "category": "Riegel & Gebäck · Vorrat", - "title": "Käsekuchen-Gläser (No-Bake)", - "tagline": "Hält 3 Tage im Kühlschrank.", - "nutrition": "ca. ≈ 800 kcal · Eiweiß 60 g · Fett 35 g · KH 55 g · Zucker 30 g · Ballaststoffe 4 g · ganzer Ansatz", - "ingredients": "300 g Magerquark · 100 g Frischkäse · Haferboden · Süße · Vanille", - "steps": "Boden in 4 Gläser, Creme drüber, kalt stellen.", - "variations": [], - "tip": "Nachmittags-Tipp Vier fertige Kaffee-Desserts auf einen Schlag.", - "badge": "", - "yield": "" - }, - { - "num": "73", - "category": "Riegel & Gebäck · Vorrat", - "title": "Datteln-Hafer-Riegel (ohne Backen)", - "tagline": "Kalt pressen, schneiden, sehr süß.", - "nutrition": "ca. ≈ 1000 kcal · Eiweiß 20 g · Fett 35 g · KH 150 g · Zucker 70 g · Ballaststoffe 18 g · ganzer Ansatz", - "ingredients": "10 Datteln · 100 g Haferflocken · 50 g Nüsse", - "steps": "Mixen, in Form pressen, kalt stellen, schneiden.", - "variations": [], - "tip": "Nachmittags-Tipp Ohne Ofen – aber zuckerintensiv, kleine Riegel schneiden.", - "badge": "", - "yield": "" - }, - { - "num": "74", - "category": "Riegel & Gebäck · Vorrat", - "title": "Protein-Brownies AIRFRYER", - "tagline": "Saftig, in Würfel schneiden.", - "nutrition": "ca. ≈ 900 kcal · Eiweiß 60 g · Fett 25 g · KH 100 g · Zucker 25 g · Ballaststoffe 18 g · ganzer Ansatz", - "ingredients": "100 g Haferflocken · 40 g Kakao · 2 Eier · 200 g Quark · Süße", - "steps": "Verrühren, backen, abkühlen, würfeln. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 170 °C, 15 Min Ofen: 180 °C, 22–25 Min", - "variations": [], - "tip": "Nachmittags-Tipp Schokoladig und trotzdem proteinreich.", - "badge": "", - "yield": "" - }, - { - "num": "75", - "category": "Riegel & Gebäck · Vorrat", - "title": "Apfel-Zimt-Schnitten AIRFRYER", - "tagline": "Bratapfel als Blechkuchen.", - "nutrition": "ca. ≈ 800 kcal · Eiweiß 28 g · Fett 15 g · KH 130 g · Zucker 35 g · Ballaststoffe 16 g · ganzer Ansatz", - "ingredients": "120 g Haferflocken · 1 Apfel geraspelt · 2 Eier · Zimt", - "steps": "Verrühren, aufs Blech, backen, schneiden. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 170 °C, 15 Min Ofen: 180 °C, 22 Min", - "variations": [], - "tip": "Nachmittags-Tipp Warmer Apfelkuchen-Effekt, portioniert für die Woche.", - "badge": "", - "yield": "" - }, - { - "num": "76", - "category": "Airfryer · herzhaft & knusprig", - "title": "Airfryer-Kichererbsen AIRFRYER", - "tagline": "Knuspern wie Chips, ohne Tüte.", - "nutrition": "ca. ≈ 330 kcal · Eiweiß 15 g · Fett 9 g · KH 41 g · Zucker 1 g · Ballaststoffe 12 g · ganzer Ansatz", - "ingredients": "1 Dose Kichererbsen · 1 TL Öl · Gewürz nach Wahl", - "steps": "Abtropfen, trocken tupfen, mit Öl und Gewürz mischen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 190 °C, 12–15 Min, 2× schütteln Ofen: 200 °C, 25–30 Min", - "variations": [ - "Paprika-rauchig", - "Curry", - "Chili-Limette", - "Knoblauch-Kräuter" - ], - "tip": "Nachmittags-Tipp Eiweiß + Ballaststoffe statt leerem Fett. Vorrat hält Tage.", - "badge": "", - "yield": "" - }, - { - "num": "77", - "category": "Airfryer · herzhaft & knusprig", - "title": "Parmesan-Crisps AIRFRYER", - "tagline": "Wenige Stück genügen, sehr intensiv.", - "nutrition": "ca. ≈ 120 kcal · Eiweiß 11 g · Fett 8 g · KH 1 g · Zucker 0 g · Ballaststoffe 0 g", - "ingredients": "geriebener Parmesan/Hartkäse", - "steps": "Kleine Häufchen auf luftdurchlässiges Papier setzen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 180 °C, 4–6 Min Ofen: 200 °C, 5–7 Min", - "variations": [], - "tip": "Nachmittags-Tipp Herzhaft, proteinreich, fast keine Kohlenhydrate.", - "badge": "", - "yield": "" - }, - { - "num": "78", - "category": "Airfryer · herzhaft & knusprig", - "title": "Airfryer-Gemüsechips AIRFRYER", - "tagline": "Hauchdünn = knusprig.", - "nutrition": "ca. ≈ 90 kcal · Eiweiß 2 g · Fett 5 g · KH 9 g · Zucker 5 g · Ballaststoffe 3 g", - "ingredients": "Zucchini/Möhre dünn gehobelt · wenig Öl · Salz", - "steps": "Hauchdünn hobeln, trocken tupfen, mit Öl und Salz mischen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 160 °C, 12–18 Min Ofen: 150 °C, 30–40 Min", - "variations": [ - "Paprika", - "Rosmarin", - "Knoblauch", - "Meersalz pur" - ], - "tip": "Nachmittags-Tipp Echter Chip-Ersatz aus Gemüse.", - "badge": "", - "yield": "" - }, - { - "num": "79", - "category": "Airfryer · herzhaft & knusprig", - "title": "Käse-Cracker (Airfryer) AIRFRYER", - "tagline": "Herzhaft, Low Carb.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 12 g · Fett 12 g · KH 2 g · Zucker 0 g · Ballaststoffe 1 g", - "ingredients": "40 g Käse-Häufchen · Kürbiskerne", - "steps": "Käse in Häufchen, Kerne drüber. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 180 °C, 5–6 Min Ofen: 200 °C, 6–8 Min", - "variations": [], - "tip": "Nachmittags-Tipp Knusprig ohne Mehl.", - "badge": "", - "yield": "" - }, - { - "num": "80", - "category": "Airfryer · herzhaft & knusprig", - "title": "Airfryer-Nüsse würzig AIRFRYER", - "tagline": "Verbrennen schnell – im Blick behalten.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 5 g · Fett 16 g · KH 5 g · Zucker 1 g · Ballaststoffe 2 g", - "ingredients": "30 g Nüsse · Prise Öl · Gewürz", - "steps": "Mit Gewürz mischen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 160 °C, 5–7 Min, schütteln Ofen: 160 °C, 8–10 Min", - "variations": [], - "tip": "Nachmittags-Tipp Warm geröstet schmeckt intensiver – kleine Menge.", - "badge": "", - "yield": "" - }, - { - "num": "81", - "category": "Airfryer · herzhaft & knusprig", - "title": "Airfryer-Edamame knusprig AIRFRYER", - "tagline": "Aus der Schote gepult, dann geröstet.", - "nutrition": "ca. ≈ 130 kcal · Eiweiß 11 g · Fett 6 g · KH 9 g · Zucker 2 g · Ballaststoffe 5 g", - "ingredients": "100 g Edamame (gepult) · Öl · Salz", - "steps": "Trocken tupfen, mit Öl und Salz mischen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 190 °C, 10–12 Min Ofen: 200 °C, 18–20 Min", - "variations": [], - "tip": "Nachmittags-Tipp Überraschend knusprig, sehr proteinreich.", - "badge": "", - "yield": "" - }, - { - "num": "82", - "category": "Airfryer · herzhaft & knusprig", - "title": "Vollkorn-Pita-Chips AIRFRYER", - "tagline": "Dazu Hummus oder Quark-Dip.", - "nutrition": "ca. ≈ 160 kcal · Eiweiß 5 g · Fett 5 g · KH 24 g · Zucker 1 g · Ballaststoffe 3 g", - "ingredients": "1 Vollkorn-Pita in Dreiecke · Öl · Salz", - "steps": "Dreiecke leicht ölen und würzen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 180 °C, 5–7 Min Ofen: 200 °C, 8–10 Min", - "variations": [], - "tip": "Nachmittags-Tipp Nacho-Gefühl aus einem Fladen.", - "badge": "", - "yield": "" - }, - { - "num": "83", - "category": "Airfryer · süß & Gebäck", - "title": "Airfryer-Apfelchips AIRFRYER", - "tagline": "Natürlich süß, knusprig, Vorrat.", - "nutrition": "ca. ≈ 90 kcal · Eiweiß 0 g · Fett 0 g · KH 24 g · Zucker 18 g · Ballaststoffe 4 g", - "ingredients": "1 Apfel hauchdünn · Zimt", - "steps": "Dünn hobeln, mit Zimt bestreuen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 150 °C, 15–20 Min Ofen: 120 °C, 40–60 Min", - "variations": [], - "tip": "Nachmittags-Tipp Ohne Zuckerzusatz – Gummibärchen-Ersatz zum Knabbern.", - "badge": "", - "yield": "" - }, - { - "num": "84", - "category": "Airfryer · süß & Gebäck", - "title": "Zimt-Bananen-Scheiben AIRFRYER", - "tagline": "Karamellig ohne Zuckerzusatz.", - "nutrition": "ca. ≈ 110 kcal · Eiweiß 1 g · Fett 0 g · KH 28 g · Zucker 16 g · Ballaststoffe 3 g", - "ingredients": "1 Banane in Scheiben · Zimt", - "steps": "Scheiben mit Zimt bestreuen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 180 °C, 8–10 Min Ofen: 180 °C, 12–15 Min", - "variations": [], - "tip": "Nachmittags-Tipp Die Banane karamellisiert von allein.", - "badge": "", - "yield": "" - }, - { - "num": "85", - "category": "Airfryer · süß & Gebäck", - "title": "Mini-Bananen-Hafer-Kekse AIRFRYER", - "tagline": "Frisch am besten, warm.", - "nutrition": "ca. ≈ 290 kcal · Eiweiß 8 g · Fett 4 g · KH 60 g · Zucker 15 g · Ballaststoffe 8 g · ganzer Ansatz", - "ingredients": "1 reife Banane · 5 EL Haferflocken", - "steps": "Banane zerdrücken, mit Haferflocken mischen, Häufchen formen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 160 °C, 8–10 Min Ofen: 180 °C, 12 Min", - "variations": [ - "+ Zartbitter-Drops", - "+ Rosinen", - "+ Zimt", - "+ Nuss" - ], - "tip": "Nachmittags-Tipp 2 Zutaten, kein Zuckerzusatz nötig.", - "badge": "", - "yield": "" - }, - { - "num": "86", - "category": "Airfryer · süß & Gebäck", - "title": "Airfryer-Pfirsich mit Quark AIRFRYER", - "tagline": "Warme Frucht, kühler Quark.", - "nutrition": "ca. ≈ 120 kcal · Eiweiß 10 g · Fett 1 g · KH 15 g · Zucker 13 g · Ballaststoffe 2 g", - "ingredients": "1/2 Pfirsich · danach Klecks Quark", - "steps": "Pfirsich kurz garen, mit Quark servieren. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 180 °C, 6–8 Min Ofen: 190 °C, 12 Min", - "variations": [], - "tip": "Nachmittags-Tipp Warm-kalt-Kontrast wie ein Mini-Dessert.", - "badge": "", - "yield": "" - }, - { - "num": "87", - "category": "Airfryer · süß & Gebäck", - "title": "Haferflocken-Cookie-Bites AIRFRYER", - "tagline": "Kleine Häppchen zum Espresso.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 7 g · Fett 4 g · KH 48 g · Zucker 12 g · Ballaststoffe 7 g · ganzer Ansatz", - "ingredients": "60 g Haferflocken · 1/2 Banane · Zimt", - "steps": "Mischen, kleine Bites formen. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 160 °C, 8–10 Min Ofen: 180 °C, 12 Min", - "variations": [], - "tip": "Nachmittags-Tipp Espresso-Begleiter statt Keksschale.", - "badge": "", - "yield": "" - }, - { - "num": "88", - "category": "Airfryer · süß & Gebäck", - "title": "Airfryer-Birne mit Walnuss AIRFRYER", - "tagline": "Warmer Nachtisch-Effekt.", - "nutrition": "ca. ≈ 140 kcal · Eiweiß 2 g · Fett 8 g · KH 18 g · Zucker 12 g · Ballaststoffe 3 g", - "ingredients": "1/2 Birne · 10 g Walnüsse", - "steps": "Birne halbieren, Nuss in die Mulde. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 180 °C, 8 Min Ofen: 190 °C, 14 Min", - "variations": [], - "tip": "Nachmittags-Tipp Fühlt sich edler an als es Aufwand ist.", - "badge": "", - "yield": "" - }, - { - "num": "89", - "category": "Airfryer · süß & Gebäck", - "title": "Zimt-Kichererbsen (süß) AIRFRYER", - "tagline": "Süße Variante des Chip-Ersatzes.", - "nutrition": "ca. ≈ 340 kcal · Eiweiß 15 g · Fett 9 g · KH 42 g · Zucker 4 g · Ballaststoffe 12 g · ganzer Ansatz", - "ingredients": "1 Dose Kichererbsen · Zimt · Prise Süße nach dem Rösten", - "steps": "Abtropfen, trocken tupfen, mit Zimt mischen, rösten. ZUBEREITUNG · AIRFRYER / OFEN Airfryer: 190 °C, 12–15 Min Ofen: 200 °C, 25–30 Min", - "variations": [], - "tip": "Nachmittags-Tipp Süß und knusprig – Ballaststoffe inklusive.", - "badge": "", - "yield": "" - }, - { - "num": "90", - "category": "Warm & gemütlich zum Kaffee", - "title": "Warme Milch mit Zimt", - "tagline": "Beruhigend, dazu ein Stück Zartbitter.", - "nutrition": "ca. ≈ 130 kcal · Eiweiß 9 g · Fett 5 g · KH 13 g · Zucker 13 g · Ballaststoffe 0 g", - "ingredients": "250 ml Milch · Zimt · Prise Süße", - "steps": "Milch erwärmen, Zimt einrühren.", - "variations": [], - "tip": "Nachmittags-Tipp Warmes Getränk beschäftigt – oft reicht das schon.", - "badge": "", - "yield": "" - }, - { - "num": "91", - "category": "Warm & gemütlich zum Kaffee", - "title": "Porridge-Tasse", - "tagline": "Warm sättigt lange.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 12 g · Fett 7 g · KH 38 g · Zucker 12 g · Ballaststoffe 5 g", - "ingredients": "40 g Haferflocken · 200 ml Milch", - "steps": "3 Min köcheln, Toppings nach Wahl.", - "variations": [ - "+ Beeren", - "+ Apfel-Zimt", - "+ Kakao", - "+ 1 TL Erdnussmus" - ], - "tip": "Nachmittags-Tipp Kleiner Nachmittags-Porridge trägt bis zum Abendessen.", - "badge": "", - "yield": "" - }, - { - "num": "92", - "category": "Warm & gemütlich zum Kaffee", - "title": "Ofen-Apfel (Mikro)", - "tagline": "Bratapfel-Gefühl in Minuten.", - "nutrition": "ca. ≈ 140 kcal · Eiweiß 2 g · Fett 4 g · KH 25 g · Zucker 20 g · Ballaststoffe 4 g", - "ingredients": "1 Apfel entkernt · Zimt · 5 g Walnuss", - "steps": "Füllen, 3 Min in die Mikrowelle.", - "variations": [], - "tip": "Nachmittags-Tipp Warmer Apfel schlägt kalten Kuchen.", - "badge": "", - "yield": "" - }, - { - "num": "93", - "category": "Warm & gemütlich zum Kaffee", - "title": "Heiße Schokolade (mager)", - "tagline": "Echter Kakao, kein Fertigpulver-Zucker.", - "nutrition": "ca. ≈ 140 kcal · Eiweiß 9 g · Fett 5 g · KH 16 g · Zucker 13 g · Ballaststoffe 2 g", - "ingredients": "200 ml Milch · 1 EL Kakao · Süße", - "steps": "Kakao mit etwas Milch anrühren, Rest heiß dazu.", - "variations": [], - "tip": "Nachmittags-Tipp Selbst gemacht steckt weniger Zucker drin als im Beutel.", - "badge": "", - "yield": "" - }, - { - "num": "94", - "category": "Warm & gemütlich zum Kaffee", - "title": "Grießbrei klein", - "tagline": "Kindheits-Gefühl, kleine Portion.", - "nutrition": "ca. ≈ 200 kcal · Eiweiß 10 g · Fett 6 g · KH 28 g · Zucker 14 g · Ballaststoffe 1 g", - "ingredients": "20 g Grieß · 200 ml Milch · Zimt", - "steps": "Grieß in die Milch einrühren, köcheln bis cremig.", - "variations": [], - "tip": "Nachmittags-Tipp Warm und tröstlich – bewusst eine kleine Schüssel.", - "badge": "", - "yield": "" - }, - { - "num": "95", - "category": "Warm & gemütlich zum Kaffee", - "title": "Warmer Beeren-Quark", - "tagline": "Warm-kalt-Kontrast, sehr proteinreich.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 20 g · Fett 1 g · KH 20 g · Zucker 14 g · Ballaststoffe 4 g", - "ingredients": "100 g TK-Beeren · 150 g Quark", - "steps": "Beeren erwärmen, über den kalten Quark geben.", - "variations": [], - "tip": "Nachmittags-Tipp Wie warme Grütze auf Quark – Dessert-Gefühl mit Eiweiß.", - "badge": "", - "yield": "" - }, - { - "num": "96", - "category": "Kaffee-Drinks & Protein-Shakes", - "title": "Protein-Kaffee (Iced)", - "tagline": "Wach + satt, ersetzt den süßen Coffee-to-go.", - "nutrition": "ca. ≈ 180 kcal · Eiweiß 24 g · Fett 5 g · KH 8 g · Zucker 6 g · Ballaststoffe 1 g", - "ingredients": "200 ml kalter Kaffee · 150 ml Milch · 20 g Proteinpulver · Eis", - "steps": "Alles shaken, über Eis gießen.", - "variations": [], - "tip": "Nachmittags-Tipp Der Griff zum Zucker-Coffee-Shop fällt weg.", - "badge": "", - "yield": "" - }, - { - "num": "97", - "category": "Kaffee-Drinks & Protein-Shakes", - "title": "Cappuccino mit Magermilch", - "tagline": "Cremig ohne Sirup-Zucker.", - "nutrition": "ca. ≈ 80 kcal · Eiweiß 9 g · Fett 1 g · KH 10 g · Zucker 10 g · Ballaststoffe 0 g", - "ingredients": "200 ml Magermilch · Espresso", - "steps": "Milch aufschäumen, Espresso dazu.", - "variations": [], - "tip": "Nachmittags-Tipp Der Schaum gibt Genuss – ohne Sirup.", - "badge": "", - "yield": "" - }, - { - "num": "98", - "category": "Kaffee-Drinks & Protein-Shakes", - "title": "Bananen-Kaffee-Shake", - "tagline": "Süße aus der Banane, kein Zuckerzusatz.", - "nutrition": "ca. ≈ 220 kcal · Eiweiß 9 g · Fett 5 g · KH 36 g · Zucker 24 g · Ballaststoffe 2 g", - "ingredients": "1 Banane · 200 ml Milch · 1 TL lösl. Espresso", - "steps": "Alles mixen.", - "variations": [], - "tip": "Nachmittags-Tipp Milchshake-Gefühl, aber natürlich gesüßt.", - "badge": "", - "yield": "" - }, - { - "num": "99", - "category": "Kaffee-Drinks & Protein-Shakes", - "title": "Schoko-Protein-Shake", - "tagline": "Wie ein Milchshake, sättigt echt.", - "nutrition": "ca. ≈ 250 kcal · Eiweiß 32 g · Fett 8 g · KH 15 g · Zucker 13 g · Ballaststoffe 1 g", - "ingredients": "300 ml Milch · 30 g Schoko-Proteinpulver · Eis", - "steps": "Shaken oder mixen.", - "variations": [], - "tip": "Nachmittags-Tipp Deckt Schokolust und Eiweiß in einem Glas.", - "badge": "", - "yield": "" - }, - { - "num": "100", - "category": "Kaffee-Drinks & Protein-Shakes", - "title": "Iced Latte mit Zimt", - "tagline": "", - "nutrition": "ca. ≈ 145 kcal · Eiweiß 25 g · Fett 1 g · KH 10 g · Zucker 7 g · Ballaststoffe 0 g · Basis, pro Portion", - "ingredients": "Erfrischend, dazu 2 Stück Zartbitter. 200 ml Milch · Espresso · Zimt · Eiswürfel 200 g Magerquark oder Skyr · Schuss Milch/Wasser · Süße nach Bedarf (Süßstoff/Erythrit)", - "steps": "Über Eis gießen, Zimt drüber.\nQuark mit dem Schuss Milch glatt rühren, Zutat\nder Variante untermischen. ~30 Sekunden.\n20 VARIATIONEN\nTrocken gesüßt · bleibt bei ~25 g Eiweiß · ~7 g Zucker (nur Milchzucker)\nSchoko: 1 TL Kakao + Süßstoff\nVanille: Vanillemark/-extrakt + Süßstoff\nCappuccino: 1 TL lösl. Espresso + Süßstoff\nZimt: Zimt + Süßstoff\nSchoko-Minze: Kakao + Tropfen Minzaroma\nKokos: 1 TL Kokosraspel + Süßstoff\nKaramell: Karamell-Aroma + Süßstoff\nMandel-Marzipan: Tropfen Mandelaroma + Süßstoff\nMit Frucht · + natürlicher Zucker (~10–14 g), + Ballaststoffe\nBeere: 60 g TK-Beeren untermischen\nApfel-Zimt: ½ Apfel gewürfelt + Zimt\nBanane: ½ zerdrückte Banane\nMango: 60 g Mangostücke\nKirsche: 60 g TK-Kirschen\nHimbeer-Kakao: Himbeeren + 1 TL Kakao\nPfirsich: ½ Pfirsich gewürfelt\nTopping & Booster · mehr Biss oder mehr Eiweiß\nErdnuss-Schoko: 1 TL Erdnussmus + Kakao\nNuss-Krokant: 1 TL geröstete geh. Nüsse\nGranola: 2 EL Granola drüber (Crunch)\nKakaonibs: 1 EL Kakaonibs\nProtein-Booster: 15 g Schoko-Proteinpulver → ~37 g EW", - "variations": [], - "tip": "Nachmittags-Tipp Morgens zuhause anrühren, in eine Dose. Obst erst am Platz reinschneiden – sonst wird's wässrig.", - "badge": "", - "yield": "" - } - ] - } -} \ No newline at end of file diff --git a/plugins/trainingsplan/recipe_builder.py b/plugins/trainingsplan/recipe_builder.py deleted file mode 100644 index 49fe6d7..0000000 --- a/plugins/trainingsplan/recipe_builder.py +++ /dev/null @@ -1,399 +0,0 @@ -# -*- coding: utf-8 -*- -""" -recipe_builder.py -Rendert eine Rezeptsammlung als A4-PDF im Design der Original-Editionen: -farbiges Kategorie-Band, farbiges Rezept-Titelband (weiße Schrift), getönter -Untertitel, NÄHRWERTE-Strip, zweispaltig ZUTATEN | SO GEHT'S, getönte -VARIATIONEN- und Tipp-Box, dünn umrandete Karte. - -Regeln (wie im Original-Generator): - * jedes Rezept ist KeepTogether -> wird nie durch einen Seitenumbruch geteilt - * vor jeder Kategorie ein PageBreak -> jede Kategorie beginnt auf neuer Seite - -build_pdf(cfg) -> bytes ; default_config() -> dict -""" -import re -import copy -from io import BytesIO - -from reportlab.lib.pagesizes import A4 -from reportlab.lib.units import cm -from reportlab.lib import colors -from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle -from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, - KeepTogether, PageBreak) - -DEFAULTS = { - "type": "recipe", - "meta": { - "title": "Neue Rezeptsammlung", - "header_left": "REZEPTE · Edition", - "header_right": "Begleitend zum 8-Wochen-Plan", - "footer_left": "Teller = Portionsgrenze · Trend über die Woche zählt", - "output_name": "Rezepte.pdf", - "doc_title": "Rezeptsammlung", - "brand_color": "#6D28D9", - "tip_label": "Tipp", - "steps_label": "SO GEHT'S", - }, - "intro": { - "headline": "Neue Rezeptsammlung", - "subtitle": "Kurzbeschreibung der Edition.", - "paragraphs": ["Einleitender Absatz."], - "rule_head": "Die eine Grundregel", - "rule_text": "Teller fertig machen, Rest aus dem Sichtfeld.", - "categories_head": "1 Kategorie", - }, - "categories": [{"name": "Cremig & süß", "color": "#7C3AED"}], - "recipes": [ - {"num": "01", "category": "Cremig & süß", "title": "Beispiel-Rezept", - "tagline": "Kurzer Untertitel.", "nutrition": "", "badge": "", "yield": "", - "ingredients": "200 g Magerquark · etwas Süße", - "steps": "Alles verrühren, fertig.", - "variations": ["Schoko: + 1 TL Kakao"], "tip": "Tipp Eiweiß-reich."}, - ], -} - - -def default_config(): - return copy.deepcopy(DEFAULTS) - - -# ---------------------------------------------------------------- helpers -_ENT = re.compile(r"&(?!(?:amp|lt|gt|quot|apos|nbsp|#\d+|#x[0-9a-fA-F]+);)") - - -def esc(s): - if s is None: - return "" - s = str(s).replace("<", "<").replace(">", ">") - return _ENT.sub("&", s) - - -def _hx(c): - return "#" + c.hexval()[2:] - - -def lighten(hexstr, w=0.92): - """Mischt eine Farbe mit Weiß (w = Weiß-Anteil) -> heller Ton.""" - try: - c = colors.HexColor(hexstr) - except Exception: - c = colors.HexColor("#334155") - r = c.red * 255 * (1 - w) + 255 * w - g = c.green * 255 * (1 - w) + 255 * w - b = c.blue * 255 * (1 - w) + 255 * w - return colors.Color(r / 255, g / 255, b / 255) - - -TIP_LABEL_RE = re.compile(r"^\s*([A-Za-zÄÖÜäöü]+-Tipp|Tipp)\b[:\s]*(.*)$", re.S) - - -def split_tip(text, default_label): - """Zerlegt 'Morgen-Tipp Text' -> ('Morgen-Tipp', 'Text').""" - if not text: - return default_label, "" - m = TIP_LABEL_RE.match(text) - if m: - return m.group(1), m.group(2).strip() - return default_label, text.strip() - - -def bold_lead(v): - """Erste Bezeichnung einer Variation fett (bis Doppelpunkt bzw. erstes Wort).""" - v = v.strip() - m = re.match(r"^([^:]{2,40}):\s*(.*)$", v, re.S) - if m: - return "%s: %s" % (esc(m.group(1)), esc(m.group(2))) - m = re.match(r"^(\S+)\s+(.*)$", v, re.S) - if m: - return "%s %s" % (esc(m.group(1)), esc(m.group(2))) - return esc(v) - - -NOSPLIT = {"bis", "damit", "sodass", "bevor", "nachdem", "während", "wenn", "falls", "sobald", - "weil", "sowie", "oder", "aber", "bei", "je", "ohne", "mit", "in", "im", "am", "zum", - "zur", "auf", "für", "über", "unter", "dabei", "dazu", "nach", "vor", "als"} - - -def split_steps(text): - t = (text or "").strip().replace("z. B.", "z.B.") - raw = re.split(r"(?<=[.!?])\s+|,\s+", t) - steps = [] - for frag in raw: - frag = frag.strip() - if not frag: - continue - first = frag.split()[0].lower().strip("(„\"'.:;»«") - if steps and first in NOSPLIT: - steps[-1] = steps[-1].rstrip(".") + ", " + frag - else: - steps.append(frag) - steps = [s if s.endswith((".", "!", "?")) else s + "." for s in steps] - return [s[:1].upper() + s[1:] for s in steps] - - -def numbered_steps(steps_txt): - """Bereits nummerierte (mehrzeilige) Schritte -> Zeilen; sonst Prosa splitten.""" - if steps_txt and "\n" in steps_txt: - out = [] - for ln in steps_txt.split("\n"): - ln = re.sub(r"^\s*\d+[\.\)]?\s+", "", ln.strip()) - if ln: - out.append(ln) - return out - return split_steps(steps_txt) - - -# ---------------------------------------------------------------- renderer -def build_pdf(cfg): - _D = DEFAULTS - meta = dict(_D["meta"]); meta.update(cfg.get("meta") or {}) - intro = dict(_D["intro"]); intro.update(cfg.get("intro") or {}) - cats = cfg.get("categories") or _D["categories"] - recipes = cfg.get("recipes") or [] - - INK = colors.HexColor("#23272F") - SUB = colors.HexColor("#5B6270") - HAIR = colors.HexColor("#E3E1EA") - NUTBG = colors.HexColor("#F7F7F9") - BRAND = colors.HexColor(meta.get("brand_color", "#6D28D9")) - TIPBG = lighten(_hx(BRAND), 0.93) - - def catcolor(name): - for c in cats: - if c.get("name") == name: - try: - return colors.HexColor(c.get("color", "#334155")) - except Exception: - return colors.HexColor("#334155") - return colors.HexColor("#334155") - - styles = getSampleStyleSheet() - - def S(name, **kw): - base = kw.pop("parent", styles["Normal"]) - return ParagraphStyle(name, parent=base, **kw) - - st_title = S("t", fontName="Helvetica-Bold", fontSize=22, leading=25, textColor=INK) - st_sub = S("s", fontName="Helvetica", fontSize=10.5, leading=14, textColor=SUB) - st_intro = S("i", fontName="Helvetica", fontSize=10, leading=15, textColor=INK) - st_callh = S("ch", fontName="Helvetica-Bold", fontSize=10.5, leading=13, textColor=INK) - st_legend= S("lg", fontName="Helvetica", fontSize=8.6, leading=12, textColor=INK) - st_cat = S("c", fontName="Helvetica-Bold", fontSize=12.5, leading=15, textColor=colors.white) - st_rname = S("rn", fontName="Helvetica-Bold", fontSize=11.5, leading=14, textColor=colors.white) - st_badge = S("bd", fontName="Helvetica-Bold", fontSize=7.5, leading=9) - st_tag = S("tg", fontName="Helvetica-Oblique", fontSize=9, leading=12, textColor=SUB) - st_lbl = S("lb", fontName="Helvetica-Bold", fontSize=7.5, leading=10, spaceAfter=1) - st_body = S("bd2", fontName="Helvetica", fontSize=8.8, leading=12, textColor=INK) - st_var = S("vr", fontName="Helvetica", fontSize=8.4, leading=11.6, textColor=INK) - st_tip = S("tp", fontName="Helvetica", fontSize=8.5, leading=12, textColor=INK) - st_yield = S("yl", fontName="Helvetica-Oblique", fontSize=8, leading=11, textColor=SUB) - - PAGE_W, PAGE_H = A4 - LM = RM = 1.5 * cm - CW = PAGE_W - LM - RM - - def lbl(text, acc): - return Paragraph(text, S("lx", parent=st_lbl, textColor=acc)) - - # ---- category band ---- - def category_band(name): - acc = catcolor(name) - t = Table([[Paragraph(esc(name), st_cat)]], colWidths=[CW]) - t.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), acc), - ("LEFTPADDING", (0, 0), (-1, -1), 10), ("RIGHTPADDING", (0, 0), (-1, -1), 10), - ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6)])) - return t - - # ---- one recipe card ---- - def recipe_card(r): - acc = catcolor(r.get("category", "")) - accx = _hx(acc) - tint = lighten(accx, 0.92) - - # Titelband (+ optional AIRFRYER-Badge) - name_p = Paragraph("%s  %s" % (esc(r.get("num", "")), esc(r.get("title", ""))), st_rname) - if r.get("badge"): - badge = Table([[Paragraph(esc(r["badge"]), S("bx", parent=st_badge, textColor=acc))]]) - badge.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), colors.white), - ("LEFTPADDING", (0, 0), (-1, -1), 5), ("RIGHTPADDING", (0, 0), (-1, -1), 5), - ("TOPPADDING", (0, 0), (-1, -1), 2), ("BOTTOMPADDING", (0, 0), (-1, -1), 2)])) - badge.hAlign = "RIGHT" - head = Table([[name_p, badge]], colWidths=[CW - 82, 82]) - head.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), acc), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("LEFTPADDING", (0, 0), (0, 0), 9), ("RIGHTPADDING", (0, 0), (0, 0), 4), - ("LEFTPADDING", (1, 0), (1, 0), 0), ("RIGHTPADDING", (1, 0), (1, 0), 9), - ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5)])) - else: - head = Table([[name_p]], colWidths=[CW]) - head.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), acc), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9), - ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5)])) - - rows = [[head]] - - # Untertitel (getönt) - if r.get("tagline"): - tg = Table([[Paragraph(esc(r["tagline"]), st_tag)]], colWidths=[CW]) - tg.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), tint), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9), - ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4)])) - rows.append([tg]) - - # NÄHRWERTE-Strip - if r.get("nutrition"): - ntext = 'NÄHRWERTE  %s' % (accx, esc(r["nutrition"])) - n = Table([[Paragraph(ntext, st_var)]], colWidths=[CW]) - n.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), NUTBG), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9), - ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4), - ("LINEBELOW", (0, 0), (-1, 0), 0.6, HAIR)])) - rows.append([n]) - - # ZUTATEN – volle Breite - zut_cell = [lbl("ZUTATEN", acc), Paragraph(esc(r.get("ingredients", "")), st_body)] - if r.get("yield"): - zut_cell.append(Spacer(1, 2)) - zut_cell.append(Paragraph("%s" % esc(r["yield"]), st_yield)) - zut = Table([[zut_cell]], colWidths=[CW]) - zut.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "TOP"), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9), - ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 5), - ("BACKGROUND", (0, 0), (-1, -1), colors.white)])) - rows.append([zut]) - - # Schritte – nummeriert, volle Breite - steps = numbered_steps(r.get("steps", "")) - srows = [[Paragraph('%d' % (accx, i), st_body), - Paragraph(esc(s), st_body)] for i, s in enumerate(steps, 1)] - if srows: - steps_inner = Table(srows, colWidths=[15, CW - 18 - 15]) - steps_inner.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "TOP"), - ("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (0, 0), 4), ("RIGHTPADDING", (1, 0), (1, 0), 0), - ("TOPPADDING", (0, 0), (-1, 0), 0), ("TOPPADDING", (0, 1), (-1, -1), 3), ("BOTTOMPADDING", (0, 0), (-1, -1), 0)])) - arb_cell = [lbl(meta.get("steps_label", "SO GEHT'S"), acc), Spacer(1, 2), steps_inner] - else: - arb_cell = [lbl(meta.get("steps_label", "SO GEHT'S"), acc)] - arb = Table([[arb_cell]], colWidths=[CW]) - arb.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "TOP"), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9), - ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 7), - ("LINEABOVE", (0, 0), (-1, 0), 0.6, HAIR), ("BACKGROUND", (0, 0), (-1, -1), colors.white)])) - rows.append([arb]) - - # ZUBEREITUNG (Airfryer/Ofen), optional - if r.get("methods"): - m_cell = [lbl("ZUBEREITUNG · AIRFRYER / OFEN", acc), Paragraph(esc(r["methods"]), st_var)] - mt = Table([[m_cell]], colWidths=[CW]) - mt.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#F3F4F6")), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9), - ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), - ("LINEABOVE", (0, 0), (-1, 0), 0.6, HAIR)])) - rows.append([mt]) - - # VARIATIONEN (getönt) - if r.get("variations"): - vhtml = "
".join(bold_lead(v) for v in r["variations"] if v) - v = Table([[[lbl("VARIATIONEN", acc), Paragraph(vhtml, st_var)]]], colWidths=[CW]) - v.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), tint), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9), - ("TOPPADDING", (0, 0), (-1, -1), 6), ("BOTTOMPADDING", (0, 0), (-1, -1), 6), - ("LINEABOVE", (0, 0), (-1, 0), 0.6, HAIR)])) - rows.append([v]) - - # Tipp (Marken-Tint) - if r.get("tip"): - lab, txt = split_tip(r["tip"], meta.get("tip_label", "Tipp")) - tip_html = '%s  %s' % (accx, esc(lab), esc(txt)) - tp = Table([[Paragraph(tip_html, st_tip)]], colWidths=[CW]) - tp.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), TIPBG), - ("LEFTPADDING", (0, 0), (-1, -1), 9), ("RIGHTPADDING", (0, 0), (-1, -1), 9), - ("TOPPADDING", (0, 0), (-1, -1), 5), ("BOTTOMPADDING", (0, 0), (-1, -1), 5)])) - rows.append([tp]) - - card = Table(rows, colWidths=[CW]) - card.setStyle(TableStyle([("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (-1, -1), 0), - ("TOPPADDING", (0, 0), (-1, -1), 0), ("BOTTOMPADDING", (0, 0), (-1, -1), 0), - ("BOX", (0, 0), (-1, -1), 0.8, HAIR)])) - return KeepTogether([card, Spacer(1, 7)]) - - # ---- story ---- - story = [] - story.append(Spacer(1, 0.3 * cm)) - story.append(Paragraph(esc(intro.get("headline", meta["title"])), st_title)) - story.append(Spacer(1, 3)) - if intro.get("subtitle"): - story.append(Paragraph(esc(intro["subtitle"]), st_sub)) - story.append(Spacer(1, 10)) - for p in intro.get("paragraphs", []): - story.append(Paragraph(esc(p), st_intro)) - story.append(Spacer(1, 6)) - if intro.get("rule_head") or intro.get("rule_text"): - gr = Table([[[Paragraph(esc(intro.get("rule_head", "")), st_callh), Spacer(1, 3), - Paragraph(esc(intro.get("rule_text", "")), st_intro)]]], colWidths=[CW]) - gr.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), TIPBG), - ("LEFTPADDING", (0, 0), (-1, -1), 12), ("RIGHTPADDING", (0, 0), (-1, -1), 12), - ("TOPPADDING", (0, 0), (-1, -1), 9), ("BOTTOMPADDING", (0, 0), (-1, -1), 9), - ("LINEBEFORE", (0, 0), (0, 0), 3, BRAND)])) - story.append(Spacer(1, 4)) - story.append(gr) - story.append(Spacer(1, 12)) - story.append(Paragraph(esc(intro.get("categories_head", "%d Kategorien" % len(cats))), - S("lh", parent=st_callh, fontSize=10))) - story.append(Spacer(1, 4)) - leg_rows = [] - for c in cats: - sw = Table([[""]], colWidths=[10], rowHeights=[10]) - sw.setStyle(TableStyle([("BACKGROUND", (0, 0), (-1, -1), catcolor(c["name"])), - ("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (-1, -1), 0), - ("TOPPADDING", (0, 0), (-1, -1), 0), ("BOTTOMPADDING", (0, 0), (-1, -1), 0)])) - leg_rows.append([sw, Paragraph(esc(c["name"]), st_legend)]) - if leg_rows: - leg = Table(leg_rows, colWidths=[16, CW - 16]) - leg.setStyle(TableStyle([("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("LEFTPADDING", (0, 0), (-1, -1), 0), ("RIGHTPADDING", (0, 0), (0, 0), 6), - ("TOPPADDING", (0, 0), (-1, -1), 2), ("BOTTOMPADDING", (0, 0), (-1, -1), 2)])) - story.append(leg) - - # Rezepte, gruppiert je Kategorie -> neue Seite pro Kategorie, Rezept nie geteilt - cur = None - for r in recipes: - c = r.get("category", "") - if c != cur: - story.append(PageBreak()) - story.append(category_band(c)) - story.append(Spacer(1, 7)) - cur = c - story.append(recipe_card(r)) - - # ---- Kopf-/Fußzeile ---- - header_left = meta.get("header_left", "") - header_right = meta.get("header_right", "Begleitend zum 8-Wochen-Plan") - footer_left = meta.get("footer_left", "") - - def deco(canvas, doc): - canvas.saveState() - canvas.setFillColor(BRAND); canvas.setFont("Helvetica-Bold", 8) - canvas.drawString(LM, PAGE_H - 1.05 * cm, header_left) - canvas.setFont("Helvetica", 8); canvas.setFillColor(SUB) - canvas.drawRightString(PAGE_W - RM, PAGE_H - 1.05 * cm, header_right) - canvas.setStrokeColor(HAIR); canvas.setLineWidth(0.7) - canvas.line(LM, PAGE_H - 1.2 * cm, PAGE_W - RM, PAGE_H - 1.2 * cm) - canvas.line(LM, 1.15 * cm, PAGE_W - RM, 1.15 * cm) - canvas.setFont("Helvetica", 7.5); canvas.setFillColor(SUB) - canvas.drawString(LM, 0.8 * cm, footer_left) - canvas.drawRightString(PAGE_W - RM, 0.8 * cm, "Seite %d" % doc.page) - canvas.restoreState() - - buf = BytesIO() - doc = SimpleDocTemplate(buf, pagesize=A4, leftMargin=LM, rightMargin=RM, - topMargin=1.6 * cm, bottomMargin=1.5 * cm, - title=meta.get("doc_title", meta.get("title", "Rezepte"))) - doc.build(story, onFirstPage=deco, onLaterPages=deco) - return buf.getvalue() - - -if __name__ == "__main__": - open("/tmp/recipe_test.pdf", "wb").write(build_pdf(default_config())) - print("OK") diff --git a/plugins/trainingsplan/schema_contract.py b/plugins/trainingsplan/schema_contract.py index 0a2462d..635a9a8 100644 --- a/plugins/trainingsplan/schema_contract.py +++ b/plugins/trainingsplan/schema_contract.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Versionierter Datenvertrag zwischen Trainingsplan-Editor und Session-Tracker.""" +"""Schlanker Datenvertrag fuer den Trainingsplan-Editor.""" from __future__ import annotations import copy @@ -7,15 +7,8 @@ import hashlib import re from typing import Any -PLAN_SCHEMA_VERSION = 3 -CONTRACT_VERSION = 2 -RESULT_SCHEMA_VERSION = 2 - -VALID_RESULT_MODES = {"auto", "reps", "seconds", "minutes", "none"} -VALID_WEIGHT_MODES = {"none", "optional", "required"} -VALID_LATERALITY = {"bilateral", "unilateral"} -VALID_SIDES_MODES = {"same", "separate"} -VALID_TRAINING_MODES = {"auto", "sets_reps", "tabata", "fixed_interval"} +PLAN_SCHEMA_VERSION = 4 +CONTRACT_VERSION = 3 def slug(value: Any) -> str: @@ -30,289 +23,220 @@ def stable_id(prefix: str, *parts: Any) -> str: return f"{prefix}-{slug(parts[-1] if parts else prefix)[:30]}-{digest}" -def normalize_result_schema(value: Any, partial: bool = False) -> dict[str, Any] | None: - if value is None: - return None - if not isinstance(value, dict): - value = {} - mode = str(value.get("mode") or ("auto" if not partial else "")) - weight_mode = str(value.get("weight_mode") or ("none" if not partial else "")) - laterality = str(value.get("laterality") or ("bilateral" if not partial else "")) - sides_mode = str(value.get("sides_mode") or ("same" if not partial else "")) - result: dict[str, Any] = {} - if mode in VALID_RESULT_MODES: - result["mode"] = mode - elif not partial: - result["mode"] = "auto" - if weight_mode in VALID_WEIGHT_MODES: - result["weight_mode"] = weight_mode - elif not partial: - result["weight_mode"] = "none" - if laterality in VALID_LATERALITY: - result["laterality"] = laterality - elif not partial: - result["laterality"] = "bilateral" - if sides_mode in VALID_SIDES_MODES: - result["sides_mode"] = sides_mode - elif not partial: - result["sides_mode"] = "same" +def default_config() -> dict[str, Any]: + return { + "schema_version": PLAN_SCHEMA_VERSION, + "contract_version": CONTRACT_VERSION, + "plan_id": "neuer-trainingsplan", + "meta": {"title": "Neuer Trainingsplan"}, + "exercises": [], + "days": [], + } + + +def _string(value: Any) -> str: + return str(value or "").strip() + + +def _int(value: Any, fallback: int = 0) -> int: try: - sets = int(value.get("sets")) + return int(value) except (TypeError, ValueError): - sets = 0 - if 1 <= sets <= 40: - result["sets"] = sets - if "locked_sets" in value: - result["locked_sets"] = bool(value.get("locked_sets")) - if result.get("laterality") == "bilateral": - result["sides_mode"] = "same" - return result or None + return fallback - -def infer_result_schema(name: Any, cue: Any = "", *, stage: bool = False) -> dict[str, Any]: - """Leitet für Legacy- und neue Standardpläne ein explizites Basisschema ab. - - Die Ableitung ist nur eine Migrationshilfe. Nach dem Speichern liegt das - Ergebnis fest im Plan und kann im Editor jederzeit korrigiert werden. - """ - text = f"{name or ''} {cue or ''}".casefold() - no_measure = re.search( - r"(?:regeneration|schlaf|schmerzcheck|pausieren|protokollieren|prüfen|check|" - r"mobility|mobilität|dehnen|stretch|cool.?down|warm.?up|spaziergang locker)$", - text, - ) - if no_measure: - mode = "none" - elif re.search(r"(?:spaziergang|gehen|walk).*(?:min|minute)|(?:min|minute).*(?:spaziergang|gehen|walk)", text): - mode = "minutes" - elif re.search(r"(?:hold|plank|hang|wall\s*sit|wandsitz|isometr|halte|stützposition|deep\s+squat)", text): - mode = "seconds" - elif re.search(r"(?:sek(?:unde)?|\d+\s*s)", text) and not re.search(r"(?:reps?|wiederhol|negativ|push|pull|squat|row|rudern|dip)", text): - mode = "seconds" +def _text_items(value: Any, fallback: Any = None) -> dict[str, list[str]]: + source = value if value not in (None, "") else fallback + if isinstance(source, dict): + source = source.get("items") + if isinstance(source, list): + items = [_string(item) for item in source if _string(item)] else: - mode = "reps" + text = _string(source) + parts = re.split(r"|\n|·", text, flags=re.I) + items = [_string(re.sub(r"<[^>]+>", "", item)) for item in parts] + items = [item for item in items if item] + return {"items": items} - has_load = bool(re.search(r"(?:kb|kettlebell|kurzhantel|dumbbell|rucksack|gewicht|last)", text)) - bodyweight_mix = bool(re.search(r"(?:optional|wahlweise|ggf\.?|bodyweight|bw|ungewichtet|körpergewicht)", text)) - weight_mode = "optional" if has_load and bodyweight_mix else ("required" if has_load else "none") - # Bei generischen Stufen mit „gewichtet“ bleibt Gewicht optional, wenn eine - # ungewichtete Variante im selben Text genannt wird. - if "gewichtet" in text and ("ungewichtet" in text or "bw" in text): - weight_mode = "optional" - unilateral = bool(re.search(r"(?:einarm|einbein|je\s+seite|pro\s+seite|links.?/?rechts|seitenwechsel|side\s+plank|wechsel)", text)) - schema = { - "mode": mode, - "weight_mode": weight_mode, - "laterality": "unilateral" if unilateral else "bilateral", - "sides_mode": "separate" if unilateral else "same", - } - # Nicht messbare Checklistenpunkte benötigen nie Gewicht oder Seitenfelder. - if mode == "none": - schema.update({"weight_mode": "none", "laterality": "bilateral", "sides_mode": "same"}) - return normalize_result_schema(schema, partial=False) or { - "mode": "auto", "weight_mode": "none", "laterality": "bilateral", "sides_mode": "same" +def _normalize_exercise(raw: Any, plan_id: str, index: int) -> dict[str, Any] | None: + if not isinstance(raw, dict): + return None + name = _string(raw.get("name") or raw.get("title")) + exercise_id = _string(raw.get("id") or raw.get("exercise_id")) + if not exercise_id: + exercise_id = stable_id("exercise", plan_id, index, name or "uebung") + exercise = {"id": exercise_id, "name": name} + for key in ("cue", "description", "equipment", "notes"): + value = _string(raw.get(key)) + if value: + exercise[key] = value + tags = raw.get("tags") + if isinstance(tags, list): + clean_tags = [_string(tag) for tag in tags if _string(tag)] + if clean_tags: + exercise["tags"] = clean_tags + return exercise + + +def _catalog_sources(cfg: dict[str, Any]) -> list[Any]: + sources: list[Any] = [] + raw_exercises = cfg.get("exercises") + if isinstance(raw_exercises, list): + sources.extend(raw_exercises) + raw_catalog = cfg.get("exercise_catalog") + if isinstance(raw_catalog, dict): + sources.extend(raw_catalog.values()) + elif isinstance(raw_catalog, list): + sources.extend(raw_catalog) + return sources + + +def _legacy_day_exercises(day: dict[str, Any]) -> list[dict[str, Any]]: + result: list[dict[str, Any]] = [] + direct = day.get("exercises") + if isinstance(direct, list): + result.extend(item for item in direct if isinstance(item, dict)) + rotations = day.get("rotations") + if not isinstance(rotations, list): + rotations = [] + if isinstance(day.get("rotA"), list): + rotations.append({"label": "Rotation A", "exercises": day["rotA"]}) + if isinstance(day.get("rotB"), list): + rotations.append({"label": "Rotation B", "exercises": day["rotB"]}) + for rotation in rotations: + if not isinstance(rotation, dict): + continue + label = _string(rotation.get("label")) + exercises = rotation.get("exercises") if isinstance(rotation.get("exercises"), list) else [] + for exercise in exercises: + if not isinstance(exercise, dict): + continue + item = copy.deepcopy(exercise) + item["__legacy_catalog"] = True + if label and not _string(item.get("notes")): + item["notes"] = label + result.append(item) + return result + + +def _ensure_catalog_entry( + catalog: dict[str, dict[str, Any]], + raw: dict[str, Any], + plan_id: str, + index: int, +) -> str: + exercise_id = _string(raw.get("exercise_id") or raw.get("id")) + name = _string(raw.get("name") or raw.get("title")) + if not exercise_id: + exercise_id = stable_id("exercise", plan_id, index, name or "uebung") + if exercise_id not in catalog: + source = { + "id": exercise_id, + "name": name, + "cue": raw.get("cue"), + "description": raw.get("description"), + "equipment": raw.get("equipment"), + "tags": raw.get("tags"), + } + catalog[exercise_id] = _normalize_exercise(source, plan_id, index) or { + "id": exercise_id, + "name": name, + } + return exercise_id + + +def _normalize_day_exercise(raw: dict[str, Any], day_id: str, exercise_id: str, index: int) -> dict[str, Any]: + item_id = _string(raw.get("id")) + if not item_id or item_id == exercise_id: + item_id = stable_id("day-exercise", day_id, exercise_id, index) + item: dict[str, Any] = {"id": item_id, "exercise_id": exercise_id} + for key in ("sets", "duration_seconds"): + value = _int(raw.get(key), 0) + if value > 0: + item[key] = value + for key in ("reps", "tempo", "rest", "notes", "legacy_id"): + value = _string(raw.get(key)) + if value: + item[key] = value + return item + + +def normalize_training_config(config: Any, plan_id: str | None = None) -> dict[str, Any]: + cfg = copy.deepcopy(config) if isinstance(config, dict) else {} + meta = cfg.get("meta") if isinstance(cfg.get("meta"), dict) else {} + resolved_plan_id = _string(cfg.get("plan_id") or plan_id or stable_id("plan", meta.get("title") or "training")) + title = _string(meta.get("title") or cfg.get("title") or "Neuer Trainingsplan") + normalized: dict[str, Any] = { + "schema_version": PLAN_SCHEMA_VERSION, + "contract_version": CONTRACT_VERSION, + "plan_id": resolved_plan_id, + "meta": {"title": title}, + "exercises": [], + "days": [], } + subtitle = _string(meta.get("subtitle") or cfg.get("subtitle")) + if subtitle: + normalized["meta"]["subtitle"] = subtitle -def infer_cluster(name: str, key: str = "") -> tuple[str, str]: - text = f"{key} {name}".lower() - rules = [ - (r"liegest|push.?up|\bls\b", "horizontal_push", "Push-up"), - (r"dip", "vertical_push", "Dip"), - (r"pike|overhead|floor press", "vertical_push", "Schulterdrücken"), - (r"pull.?up|klimm|hang", "vertical_pull", "Pull-up / Hang"), - (r"rudern|row", "horizontal_pull", "Rudern"), - (r"squat|kniebeuge|goblet|lunge|ausfallschritt|wall sit", "knee_dominant", "Kniebeuge / Ausfallschritt"), - (r"deadlift|rdl|hinge|swing|glute bridge", "hip_hinge", "Hip Hinge / Hüftstreckung"), - (r"plank|hollow|dead bug|side plank|core", "core", "Core"), - (r"curl", "elbow_flexion", "Bizeps"), - (r"waden|calf", "calf", "Waden"), - (r"halo|around|carry|bottoms", "loaded_mobility", "Kettlebell-Kontrolle"), - ] - for pattern, cid, label in rules: - if re.search(pattern, text): - return cid, label - return "general", "Allgemein" + catalog: dict[str, dict[str, Any]] = {} + for index, raw in enumerate(_catalog_sources(cfg)): + exercise = _normalize_exercise(raw, resolved_plan_id, index) + if exercise: + catalog[exercise["id"]] = exercise + legacy_prepost = cfg.get("prepost") if isinstance(cfg.get("prepost"), dict) else {} + days = cfg.get("days") if isinstance(cfg.get("days"), list) else [] + exercise_counter = len(catalog) + for day_index, raw_day in enumerate(days): + if not isinstance(raw_day, dict): + continue + day_num = _int(raw_day.get("num"), day_index + 1) or day_index + 1 + name = _string(raw_day.get("name") or raw_day.get("focus") or f"Tag {day_num}") + day_id = _string(raw_day.get("id")) or stable_id("day", resolved_plan_id, day_num, name) + prepost = legacy_prepost.get(str(day_num)) if isinstance(legacy_prepost.get(str(day_num)), dict) else {} + day: dict[str, Any] = { + "id": day_id, + "num": day_num, + "name": name, + "warmup": _text_items(raw_day.get("warmup"), prepost.get("warmup")), + "cooldown": _text_items(raw_day.get("cooldown"), prepost.get("cooldown")), + "exercises": [], + } + focus = _string(raw_day.get("focus")) + if focus and focus != name: + day["focus"] = focus + notes = _string(raw_day.get("notes") or raw_day.get("note")) + if notes: + day["notes"] = notes -def infer_factor(name: str, cluster_id: str) -> float: - text = str(name or "").lower() - if cluster_id == "horizontal_push": - if "wand" in text: return 0.05 - if "hoch" in text and ("incline" in text or "inkline" in text): return 0.10 - if "mittel" in text and ("incline" in text or "inkline" in text): return 0.20 - if "tief" in text and ("incline" in text or "inkline" in text): return 0.35 - if "negativ" in text and ("voll" in text or "erste" in text): return 0.85 - if "negativ" in text: return 0.70 - if "assist" in text: return 0.55 - if "knie" in text: return 0.45 - if "teil" in text or "partial" in text: return 0.35 - if "voll" in text or "standard" in text or "normal" in text: return 1.00 - if cluster_id == "vertical_pull": - if "active hang" in text or "hang" in text: return 0.20 - if "scapular" in text: return 0.25 - if "negativ" in text: return 0.70 - if "leicht assist" in text: return 0.80 - if "assist" in text: return 0.55 - if "voll" in text or "klimm" in text or "pull-up" in text: return 1.00 - if cluster_id == "horizontal_pull": - if "aufrecht" in text or "stark assist" in text: return 0.25 - if "bent" in text or "gebeugt" in text: return 0.50 - if "straight" in text or "gestreckt" in text: return 1.00 - if "füße erhöht" in text: return 1.15 - if cluster_id == "knee_dominant": - if "hold" in text: return 1.00 - if "chair" in text or "box" in text or "stuhl" in text: return 0.35 - if "negativ" in text: return 0.75 - if "assist" in text or "festhalten" in text: return 0.55 - if "voll" in text or "goblet" in text or "körpergewicht" in text: return 1.00 - if cluster_id == "vertical_push": - if "hold" in text: return 0.30 - if "assist" in text: return 0.60 - if "negativ" in text: return 0.80 - if "voll" in text: return 1.00 - if cluster_id == "core": - if "knie" in text or "tuck" in text: return 0.65 - if "halb" in text: return 0.82 - if "voll" in text: return 1.00 - return 1.00 + for item_index, raw_item in enumerate(_legacy_day_exercises(raw_day)): + should_create = bool(raw_item.pop("__legacy_catalog", False) or _string(raw_item.get("name"))) + if should_create: + exercise_id = _ensure_catalog_entry(catalog, raw_item, resolved_plan_id, exercise_counter) + exercise_counter += 1 + else: + exercise_id = _string(raw_item.get("exercise_id")) + day["exercises"].append(_normalize_day_exercise(raw_item, day_id, exercise_id, item_index)) + normalized["days"].append(day) + + normalized["exercises"] = sorted(catalog.values(), key=lambda item: (slug(item.get("name")), item.get("id"))) + normalized["days"] = sorted(normalized["days"], key=lambda item: item.get("num", 0)) + return normalized def migrate_training_config(config: Any, plan_id: str | None = None) -> dict[str, Any]: - cfg = copy.deepcopy(config or {}) - if cfg.get("type") == "recipe": - return cfg - cfg["schema_version"] = PLAN_SCHEMA_VERSION - cfg["contract_version"] = CONTRACT_VERSION - meta = cfg.setdefault("meta", {}) - resolved_plan_id = str(cfg.get("plan_id") or plan_id or stable_id("plan", meta.get("title") or "training")) - cfg["plan_id"] = resolved_plan_id - - phases = cfg.setdefault("phases", {}).setdefault("items", []) - for index, phase in enumerate(phases): - if not isinstance(phase, dict): - phases[index] = phase = {"name": str(phase or "")} - phase.setdefault("id", stable_id("phase", resolved_plan_id, index, phase.get("name"))) - phase_ids = [phase.get("id") for phase in phases] - - fmt = cfg.get("training_format") if isinstance(cfg.get("training_format"), dict) else {} - mode = str(fmt.get("mode") or "auto") - if mode not in VALID_TRAINING_MODES: - mode = "auto" - fmt["mode"] = mode - fmt["fixed_interval"] = bool(fmt.get("fixed_interval") or mode in {"tabata", "fixed_interval"}) - fmt["rounds_scope"] = str(fmt.get("rounds_scope") or "block") - fmt["schema_version"] = 1 - cfg["training_format"] = fmt - - days = cfg.get("days") if isinstance(cfg.get("days"), list) else [] - for di, day in enumerate(days): - if not isinstance(day, dict): - continue - day.setdefault("id", stable_id("day", resolved_plan_id, day.get("num", di + 1), day.get("focus"))) - rotations = day.get("rotations") if isinstance(day.get("rotations"), list) else [] - for ri, rotation in enumerate(rotations): - if not isinstance(rotation, dict): - continue - rotation.setdefault("id", stable_id("rotation", day["id"], ri, rotation.get("label"))) - exercises = rotation.get("exercises") if isinstance(rotation.get("exercises"), list) else [] - day_num = day.get("num", di + 1) - for ei, exercise in enumerate(exercises): - if not isinstance(exercise, dict): - continue - # Persist the original positional key once so legacy session data can - # still be migrated safely even after the published plan is reordered. - exercise.setdefault("legacy_id", f"d{day_num}-r{ri}-e{ei}") - explicit_progression = str(exercise.get("progression_id") or exercise.get("key") or "").strip() - progression_id = explicit_progression or f"name:{slug(exercise.get('name'))}" - exercise["progression_id"] = progression_id - exercise["key"] = str(exercise.get("key") or progression_id) - exercise.setdefault("id", stable_id("exercise", rotation["id"], progression_id, exercise.get("name"), ei)) - exercise.setdefault("exercise_id", stable_id("movement", progression_id, exercise.get("name"))) - cid, label = infer_cluster(str(exercise.get("name") or ""), progression_id) - exercise.setdefault("movement_cluster", cid) - exercise.setdefault("movement_label", label) - if exercise.get("result_schema") is not None: - exercise["result_schema"] = normalize_result_schema(exercise.get("result_schema"), partial=False) - else: - exercise["result_schema"] = infer_result_schema(exercise.get("name"), exercise.get("cue")) - cfg["days"] = days - - old_stages = cfg.get("stages") if isinstance(cfg.get("stages"), dict) else {} - new_stages: dict[str, Any] = {} - for key, raw_stage in old_stages.items(): - stage = copy.deepcopy(raw_stage) if isinstance(raw_stage, dict) else {"steps": raw_stage if isinstance(raw_stage, list) else []} - stage_id = str(stage.get("id") or stable_id("progression", resolved_plan_id, key, stage.get("name"))) - stage["id"] = stage_id - stage["key"] = str(stage.get("key") or key) - raw_steps = stage.get("steps") if isinstance(stage.get("steps"), list) else [] - schemas = stage.get("result_schemas") if isinstance(stage.get("result_schemas"), list) else [] - steps: list[dict[str, Any]] = [] - for index, raw_step in enumerate(raw_steps): - if isinstance(raw_step, dict): - step = copy.deepcopy(raw_step) - step_name = str(step.get("name") or step.get("label") or "") - else: - step_name = str(raw_step or "") - step = {"name": step_name} - step.setdefault("id", stable_id("step", stage_id, index, step_name)) - step.setdefault("phase_id", phase_ids[index] if index < len(phase_ids) else "") - schema = step.get("result_schema") - if schema is None and index < len(schemas): - schema = schemas[index] - if schema is not None: - step["result_schema"] = normalize_result_schema(schema, partial=False) - else: - step["result_schema"] = infer_result_schema(step_name, stage.get("name"), stage=True) - cid, _ = infer_cluster(str(stage.get("name") or step_name), key) - step.setdefault("movement_cluster", cid) - step.setdefault("factor", infer_factor(step_name, cid)) - steps.append(step) - stage["steps"] = steps - stage.pop("result_schemas", None) - new_stages[str(key)] = stage - cfg["stages"] = new_stages - - # Zentrale, planbezogene Übungsbibliothek. Der Tracker nutzt diese vor Regex-Fallbacks. - catalog: dict[str, Any] = {} - for day in days: - for rotation in day.get("rotations", []): - for exercise in rotation.get("exercises", []): - eid = str(exercise.get("exercise_id")) - pid = str(exercise.get("progression_id") or "") - entry = catalog.setdefault(eid, { - "id": eid, - "name": str(exercise.get("name") or ""), - "progression_id": pid, - "movement_cluster": exercise.get("movement_cluster") or "general", - "movement_label": exercise.get("movement_label") or "Allgemein", - "result_schema": copy.deepcopy(exercise.get("result_schema")), - "variants": [], - }) - stage = new_stages.get(pid) - if stage: - entry["variants"] = [ - { - "id": step.get("id"), "name": step.get("name", ""), - "factor": step.get("factor", 1.0), - "movement_cluster": step.get("movement_cluster") or entry["movement_cluster"], - "result_schema": copy.deepcopy(step.get("result_schema")), - } - for step in stage.get("steps", []) - ] - cfg["exercise_catalog"] = catalog - return cfg + return normalize_training_config(config, plan_id=plan_id) -def validate_training_config(config: Any) -> dict[str, list[dict[str, Any]]]: - cfg = migrate_training_config(config) +def validate_training_config(config: Any) -> dict[str, Any]: + cfg = normalize_training_config(config) errors: list[dict[str, Any]] = [] warnings: list[dict[str, Any]] = [] ids: dict[str, str] = {} def register(value: Any, location: str) -> None: - sid = str(value or "") + sid = _string(value) if not sid: errors.append({"code": "missing_id", "location": location, "message": "Stabile ID fehlt."}) elif sid in ids: @@ -320,72 +244,38 @@ def validate_training_config(config: Any) -> dict[str, list[dict[str, Any]]]: else: ids[sid] = location - def validate_result_schema(schema: Any, location: str, *, required: bool) -> None: - if not isinstance(schema, dict): - target = errors if required else warnings - target.append({"code": "missing_result_schema", "location": location, "message": "Ergebniserfassung ist nicht eindeutig festgelegt."}) - return - mode = str(schema.get("mode") or "auto") - weight = str(schema.get("weight_mode") or "none") - laterality = str(schema.get("laterality") or "bilateral") - sides = str(schema.get("sides_mode") or "same") - if mode == "none" and weight != "none": - errors.append({"code": "invalid_result_schema", "location": location, "message": "Ohne Messwert darf kein Gewicht verlangt werden."}) - if laterality == "bilateral" and sides == "separate": - errors.append({"code": "invalid_sides", "location": location, "message": "Getrennte Seitenwerte sind nur bei einseitigen Übungen möglich."}) - if laterality == "unilateral" and sides not in {"same", "separate"}: - errors.append({"code": "invalid_sides", "location": location, "message": "Für einseitige Übungen muss die Seitenlogik festgelegt sein."}) - try: - sets = int(schema.get("sets") or 0) - except (TypeError, ValueError): - sets = 0 - if "sets" in schema and not 1 <= sets <= 40: - errors.append({"code": "invalid_sets", "location": location, "message": "Die feste Anzahl muss zwischen 1 und 40 liegen."}) + if not _string(cfg.get("meta", {}).get("title")): + errors.append({"code": "empty_title", "location": "Plan", "message": "Plantitel fehlt."}) + + exercise_ids: set[str] = set() + for index, exercise in enumerate(cfg.get("exercises", [])): + location = f"Übung {index + 1}" + register(exercise.get("id"), location) + exercise_ids.add(_string(exercise.get("id"))) + if not _string(exercise.get("name")): + errors.append({"code": "empty_exercise", "location": location, "message": "Übungsname fehlt."}) + + if not cfg.get("days"): + warnings.append({"code": "no_days", "location": "Plan", "message": "Noch keine Trainingstage angelegt."}) + + for day_index, day in enumerate(cfg.get("days", [])): + location = f"Tag {day_index + 1}" + register(day.get("id"), location) + if not _string(day.get("name")): + errors.append({"code": "empty_day", "location": location, "message": "Tagesname fehlt."}) + if not day.get("exercises"): + warnings.append({"code": "empty_day", "location": location, "message": "Tag enthält keine Übungen."}) + for item_index, item in enumerate(day.get("exercises", [])): + item_location = f"{location}, Übung {item_index + 1}" + register(item.get("id"), item_location) + exercise_id = _string(item.get("exercise_id")) + if exercise_id not in exercise_ids: + errors.append({"code": "unknown_exercise", "location": item_location, "message": "Hinterlegte Übung existiert nicht in der Übungsbibliothek."}) + sets = _int(item.get("sets"), 0) + if "sets" in item and not 1 <= sets <= 40: + errors.append({"code": "invalid_sets", "location": item_location, "message": "Sätze müssen zwischen 1 und 40 liegen."}) + duration = _int(item.get("duration_seconds"), 0) + if "duration_seconds" in item and duration <= 0: + errors.append({"code": "invalid_duration", "location": item_location, "message": "Dauer muss größer als 0 sein."}) - for pi, phase in enumerate(cfg.get("phases", {}).get("items", [])): - register(phase.get("id"), f"Phase {pi + 1}") - if not str(phase.get("name") or "").strip(): - errors.append({"code": "empty_phase", "location": f"Phase {pi + 1}", "message": "Phasenname fehlt."}) - for di, day in enumerate(cfg.get("days", [])): - register(day.get("id"), f"Tag {di + 1}") - for ri, rotation in enumerate(day.get("rotations", [])): - register(rotation.get("id"), f"Tag {di + 1}, Rotation {ri + 1}") - exercises = rotation.get("exercises", []) - if not exercises: - warnings.append({"code": "empty_rotation", "location": f"Tag {di + 1}, Rotation {ri + 1}", "message": "Rotation enthält keine Übungen."}) - for ei, exercise in enumerate(exercises): - location = f"Tag {di + 1}, Rotation {ri + 1}, Übung {ei + 1}" - register(exercise.get("id"), location) - if not str(exercise.get("name") or "").strip(): - errors.append({"code": "empty_exercise", "location": location, "message": "Übungsname fehlt."}) - pid = str(exercise.get("progression_id") or "") - if pid and not pid.startswith("name:") and pid not in cfg.get("stages", {}): - warnings.append({"code": "unknown_progression", "location": location, "message": f"Progression „{pid}“ ist nicht definiert."}) - validate_result_schema(exercise.get("result_schema"), f"{location}, Ergebniserfassung", required=True) - for key, stage in cfg.get("stages", {}).items(): - register(stage.get("id"), f"Progression {key}") - for si, step in enumerate(stage.get("steps", [])): - register(step.get("id"), f"Progression {key}, Stufe {si + 1}") - step_location = f"Progression {key}, Stufe {si + 1}" - if not str(step.get("name") or "").strip(): - warnings.append({"code": "empty_step", "location": step_location, "message": "Stufenname ist leer."}) - validate_result_schema(step.get("result_schema"), f"{step_location}, Ergebniserfassung", required=False) - try: - factor = float(step.get("factor") or 0) - except (TypeError, ValueError): - factor = 0 - if factor <= 0: - errors.append({"code": "invalid_factor", "location": step_location, "message": "Der Analysefaktor muss größer als 0 sein."}) - fmt = cfg.get("training_format", {}) - if fmt.get("mode") in {"tabata", "fixed_interval"}: - if not fmt.get("work_seconds"): - errors.append({"code": "missing_work_seconds", "location": "Trainingsformat", "message": "Arbeitszeit fehlt."}) - if not fmt.get("rounds"): - errors.append({"code": "missing_rounds", "location": "Trainingsformat", "message": "Intervallzahl fehlt."}) - rounds = int(fmt.get("rounds") or 0) - for di, day in enumerate(cfg.get("days", [])): - for ri, rotation in enumerate(day.get("rotations", [])): - count = len(rotation.get("exercises", [])) - if rounds and count and rounds % count: - warnings.append({"code": "uneven_intervals", "location": f"Tag {di + 1}, Rotation {ri + 1}", "message": f"{rounds} Intervalle sind nicht durch {count} Übungen teilbar. Feste Satzanzahl im Ergebnisschema setzen."}) return {"errors": errors, "warnings": warnings, "config": cfg} diff --git a/plugins/trainingsplan/static/index.html b/plugins/trainingsplan/static/index.html deleted file mode 100644 index a46a907..0000000 --- a/plugins/trainingsplan/static/index.html +++ /dev/null @@ -1,1118 +0,0 @@ - - - - - -Trainingsplan-Editor - - - - - -
-

Trainingsplan-Editor

- - - Revision - - - - - -
- -
- Phase: - - - - - - - - - -
- -
- -
-
- Umlaute und Sonderzeichen (ü ä ö ß – · • × °) einfach direkt eintippen — sie landen so im PDF. - Für Formatierung: <b>…</b> = fett, <br/> = Zeilenumbruch, - <font color='#6D28D9'>…</font> = Farbe. Ein kaufmännisches Und tippst du normal als &. -
- -
-
- -
- - - - diff --git a/plugins/trainingsplan/tests/test_contract.py b/plugins/trainingsplan/tests/test_contract.py index e05478b..65c2b16 100644 --- a/plugins/trainingsplan/tests/test_contract.py +++ b/plugins/trainingsplan/tests/test_contract.py @@ -1,70 +1,123 @@ from __future__ import annotations + import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parents[1])) + from schema_contract import migrate_training_config, validate_training_config -def sample_config(): +def legacy_config(): return { - "meta": {"title": "Test", "weeks": 2}, - "training_format": {"mode": "tabata", "fixed_interval": True, "work_seconds": 20, "rest_seconds": 10, "rounds": 8}, - "phases": {"items": [{"name": "Start", "weeks": 1}, {"name": "Aufbau", "weeks": 1}]}, - "days": [{"num": 1, "focus": "A", "rotations": [{"label": "Block", "exercises": [{ - "name": "Squat-Progression", "key": "squat", "result_schema": {"mode": "reps", "weight_mode": "none", "laterality": "bilateral"} - }]}]}], - "stages": {"squat": {"name": "Squat-Progression", "steps": [ - "Deep Squat Hold", "Volle Squats" - ], "result_schemas": [ - {"mode": "seconds", "weight_mode": "optional", "laterality": "bilateral"}, - {"mode": "reps", "weight_mode": "optional", "laterality": "bilateral"}, - ]}}, + "meta": { + "title": "Alter Plan", + "subtitle": "wird reduziert", + "output_name": "plan.pdf", + }, + "colors": {"ACCENT": "#ff00ff"}, + "training_format": {"mode": "tabata", "rounds": 8}, + "phases": {"items": [{"name": "Phase 1"}]}, + "stages": {"squat": {"name": "Squat", "steps": ["leicht", "schwer"]}}, + "prepost": { + "1": { + "warmup": "Handgelenke · Kniebeugen", + "cooldown": "Atmen
Locker gehen", + "stretch": "alte Dehnung", + } + }, + "days": [ + { + "num": 1, + "focus": "Beine", + "color": "#123456", + "rotations": [ + { + "label": "Block A", + "exercises": [ + { + "id": "old-placement", + "exercise_id": "exercise-squat", + "name": "Squat", + "key": "squat", + "cue": "tief", + "result_schema": {"mode": "reps"}, + } + ], + } + ], + } + ], } -def test_migration_creates_stable_ids_object_steps_and_catalog(): - first = migrate_training_config(sample_config(), plan_id="test") +def test_migration_flattens_legacy_plan_to_days_and_exercises(): + migrated = migrate_training_config(legacy_config(), plan_id="test") + + assert migrated["schema_version"] == 4 + assert migrated["contract_version"] == 3 + assert migrated["plan_id"] == "test" + assert "colors" not in migrated + assert "training_format" not in migrated + assert "phases" not in migrated + assert "stages" not in migrated + assert "exercise_catalog" not in migrated + + assert migrated["meta"] == {"title": "Alter Plan", "subtitle": "wird reduziert"} + assert migrated["exercises"] == [{"id": "exercise-squat", "name": "Squat", "cue": "tief"}] + + day = migrated["days"][0] + assert day["name"] == "Beine" + assert day["warmup"]["items"] == ["Handgelenke", "Kniebeugen"] + assert day["cooldown"]["items"] == ["Atmen", "Locker gehen"] + assert "stretch" not in day + assert "rotations" not in day + + item = day["exercises"][0] + assert item == { + "id": "old-placement", + "exercise_id": "exercise-squat", + "notes": "Block A", + } + + +def test_migration_is_stable_across_repeated_runs(): + first = migrate_training_config(legacy_config(), plan_id="test") second = migrate_training_config(first, plan_id="test") - assert first["schema_version"] == 3 - assert first["contract_version"] == 2 - assert first["days"][0]["id"] == second["days"][0]["id"] - exercise = first["days"][0]["rotations"][0]["exercises"][0] - assert exercise["id"] == second["days"][0]["rotations"][0]["exercises"][0]["id"] - steps = first["stages"]["squat"]["steps"] - assert all(isinstance(step, dict) and step.get("id") for step in steps) - assert steps[0]["result_schema"]["mode"] == "seconds" - assert steps[1]["result_schema"]["mode"] == "reps" - assert first["exercise_catalog"] + assert first == second -def test_validation_accepts_complete_plan(): - result = validate_training_config(sample_config()) - assert result["errors"] == [] +def test_validation_accepts_complete_minimal_plan(): + cfg = { + "meta": {"title": "Minimal"}, + "exercises": [{"id": "exercise-pushup", "name": "Push-up"}], + "days": [ + { + "id": "day-1", + "num": 1, + "name": "Tag 1", + "warmup": {"items": ["Mobilisieren"]}, + "cooldown": {"items": ["Atmen"]}, + "exercises": [{"id": "item-1", "exercise_id": "exercise-pushup", "sets": 3, "reps": "8-12"}], + } + ], + } + assert validate_training_config(cfg)["errors"] == [] -def test_missing_result_schemas_are_migrated_to_explicit_defaults(): - cfg = sample_config() - exercise = cfg["days"][0]["rotations"][0]["exercises"][0] - exercise.pop("result_schema", None) - cfg["stages"]["squat"].pop("result_schemas", None) - migrated = migrate_training_config(cfg, plan_id="test") - ex_schema = migrated["days"][0]["rotations"][0]["exercises"][0]["result_schema"] - assert ex_schema["mode"] == "reps" - steps = migrated["stages"]["squat"]["steps"] - assert steps[0]["result_schema"]["mode"] == "seconds" - assert steps[1]["result_schema"]["mode"] == "reps" - assert validate_training_config(migrated)["errors"] == [] +def test_validation_rejects_unknown_day_exercise_reference(): + cfg = { + "meta": {"title": "Kaputt"}, + "exercises": [{"id": "exercise-pushup", "name": "Push-up"}], + "days": [ + { + "id": "day-1", + "num": 1, + "name": "Tag 1", + "exercises": [{"id": "item-1", "exercise_id": "missing"}], + } + ], + } - -def test_legacy_position_id_is_persisted_across_reordering(): - migrated = migrate_training_config(sample_config(), plan_id="test") - exercise = migrated["days"][0]["rotations"][0]["exercises"][0] - legacy_id = exercise["legacy_id"] - exercise_id = exercise["id"] - migrated["days"][0]["rotations"][0]["exercises"].insert(0, { - "name": "Neue Übung", "key": "new", "result_schema": {"mode": "reps"} - }) - remigrated = migrate_training_config(migrated, plan_id="test") - original = next(item for item in remigrated["days"][0]["rotations"][0]["exercises"] if item["id"] == exercise_id) - assert original["legacy_id"] == legacy_id + errors = validate_training_config(cfg)["errors"] + assert [error["code"] for error in errors] == ["unknown_exercise"] diff --git a/testdata/plans/phase-1.json b/testdata/plans/phase-1.json deleted file mode 100644 index c06dd15..0000000 --- a/testdata/plans/phase-1.json +++ /dev/null @@ -1,16995 +0,0 @@ -{ - "plan_id": "phase-1", - "name": "Phase 1", - "revision": 1, - "published_revision": 1, - "created_at": "2026-07-24T09:43:48+00:00", - "updated_at": "2026-07-24T09:43:48+00:00", - "published_at": "2026-07-24T09:43:48+00:00", - "config": { - "colors": { - "ACC2": "#0F766E", - "ACCENT": "#C2410C", - "DARK": "#23201C", - "GOALBG": "#FBF1E9", - "GRID": "#CFC9BF", - "INK": "#1A1A1A", - "KBBG": "#F1ECFB", - "KBCOL": "#6D28D9", - "LIGHT": "#F4F1EC", - "LINK": "#1565C0", - "MUTE": "#6B6256", - "WARMBG": "#EAF1F0" - }, - "days": [ - { - "focus": "Drücken + Beine", - "num": 1, - "rotations": [ - { - "exercises": [ - { - "cue": "Brust kontrolliert senken & drücken", - "key": "ls", - "name": "Liegestütze", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "ls", - "id": "exercise-item-b5c3e6155f", - "exercise_id": "movement-liegestuetze-57eacf74ee", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "legacy_id": "d1-r0-e0" - }, - { - "cue": "Po vor der Couch senken, hochdrücken", - "key": "dips", - "name": "Bank-Dips (Couch)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "dips", - "id": "exercise-1-ada8cf48d3", - "exercise_id": "movement-bank-dips-couch-ad6c829e1b", - "movement_cluster": "vertical_push", - "movement_label": "Dip", - "legacy_id": "d1-r0-e1" - }, - { - "cue": "umgek. V, Kopf senken (Schultern)", - "key": "pike", - "name": "Pike", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "pike", - "id": "exercise-2-1cb8b9bbd7", - "exercise_id": "movement-pike-17f7716e00", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "legacy_id": "d1-r0-e2" - }, - { - "cue": "Arme seitlich bis Schulterhöhe, kontrolliert", - "key": "lraise", - "name": "Seitheben (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "lraise", - "id": "exercise-3-2908dafdf9", - "exercise_id": "movement-seitheben-kb-rucksack-91de5dc2f5", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d1-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-7bd8374b98" - }, - { - "exercises": [ - { - "cue": "Last umarmen, tief", - "key": "gobletkb", - "name": "Goblet Squat (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "gobletkb", - "id": "exercise-item-7816b10b95", - "exercise_id": "movement-goblet-squat-kb-rucksack-6ff9f90901", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d1-r1-e0" - }, - { - "cue": "großer Schritt, Knie Richtung Boden", - "key": "lunge", - "name": "Ausfallschritt rückw.", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "lunge", - "id": "exercise-1-232e3c9065", - "exercise_id": "movement-ausfallschritt-rueckw-2a4d06b799", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d1-r1-e1" - }, - { - "cue": "Hüfte hochdrücken, Po fest", - "key": "bridge", - "name": "Glute Bridge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "bridge", - "id": "exercise-2-0a8da65f1b", - "exercise_id": "movement-glute-bridge-1c0a7bf699", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d1-r1-e2" - }, - { - "cue": "auf Zehenspitzen hoch/runter", - "key": "calf", - "name": "Wadenheben", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "calf", - "id": "exercise-3-bfc4e8f9c3", - "exercise_id": "movement-wadenheben-0c7839551b", - "movement_cluster": "calf", - "movement_label": "Waden", - "legacy_id": "d1-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-b9e0a052af" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-druecken-beine-9f04ffe00b" - }, - { - "focus": "Ziehen + Core", - "num": 2, - "rotations": [ - { - "exercises": [ - { - "cue": "1 Arm, Ellbogen an den Rippen, Zug zur Hüfte · Lat · DG1 links / DG2 rechts", - "key": "rowe", - "name": "Einarm-Rudern ENG (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "rowe", - "id": "exercise-item-8b823b51cb", - "exercise_id": "movement-einarm-rudern-eng-kb-4907fbb5c7", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d2-r0-e0" - }, - { - "cue": "Oberarme fix, zu den Schultern", - "key": "load", - "name": "Bizeps-Curls (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "load", - "id": "exercise-1-1a66ac153c", - "exercise_id": "movement-bizeps-curls-kb-rucksack-67944f00d5", - "movement_cluster": "elbow_flexion", - "movement_label": "Bizeps", - "legacy_id": "d2-r0-e1" - }, - { - "cue": "1 Arm, Ellbogen ~45° auf, Zug zum Brustkorb · oberer Rücken · DG1 links / DG2 rechts", - "key": "row2", - "name": "Einarm-Rudern BREIT (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "row2", - "id": "exercise-2-ab58589731", - "exercise_id": "movement-einarm-rudern-breit-kb-0146abcfbb", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d2-r0-e2" - }, - { - "cue": "bäuchlings Arme+Beine heben", - "key": "sup", - "name": "Superman / Y-T-W", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "sup", - "id": "exercise-3-8974cdd4a3", - "exercise_id": "movement-superman-y-t-w-279f03a544", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d2-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-024cf0041d" - }, - { - "exercises": [ - { - "cue": "gerade Linie halten", - "key": "plank", - "name": "Plank", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "plank", - "id": "exercise-item-40e37731b0", - "exercise_id": "movement-plank-c8449f2937", - "movement_cluster": "core", - "movement_label": "Core", - "legacy_id": "d2-r1-e0" - }, - { - "cue": "Knie zügig zur Brust", - "key": "mtn", - "name": "Mountain Climbers", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "mtn", - "id": "exercise-1-69ed72c9c3", - "exercise_id": "movement-mountain-climbers-79efdb62dd", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d2-r1-e1" - }, - { - "cue": "gegengleich, Rücken am Boden", - "key": "db", - "name": "Dead Bug", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "progression_id": "db", - "id": "exercise-2-0fc9fa4d38", - "exercise_id": "movement-dead-bug-21d4b207d5", - "movement_cluster": "core", - "movement_label": "Core", - "legacy_id": "d2-r1-e2" - }, - { - "cue": "Beine heben/senken", - "key": "lr", - "name": "Beinheben", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "lr", - "id": "exercise-3-1373648dc9", - "exercise_id": "movement-beinheben-faf19a629b", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d2-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-f767e75f57" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-ziehen-core-26dd9ab93d" - }, - { - "focus": "Beine + Core", - "num": 3, - "rotations": [ - { - "exercises": [ - { - "cue": "Last umarmen, tief", - "key": "gobletkb", - "name": "Goblet Squat (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "gobletkb", - "id": "exercise-item-0264c1e44a", - "exercise_id": "movement-goblet-squat-kb-rucksack-6ff9f90901", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d3-r0-e0" - }, - { - "cue": "Hüfte zurück, Rücken gerade, Hamstrings", - "key": "load", - "name": "RDL / Hip Hinge (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "load", - "id": "exercise-1-9d276268ca", - "exercise_id": "movement-rdl-hip-hinge-kb-rucksack-9ada5a67c3", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d3-r0-e1" - }, - { - "cue": "ein Bein gestreckt", - "key": "bridge", - "name": "Glute Bridge 1-beinig", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "bridge", - "id": "exercise-2-4b6e8a5bc6", - "exercise_id": "movement-glute-bridge-1-beinig-ccc8c9fb53", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d3-r0-e2" - }, - { - "cue": "Rücken an die Wand, halten", - "key": "wall", - "name": "Wall Sit", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "wall", - "id": "exercise-3-add8004d7e", - "exercise_id": "movement-wall-sit-6b5f695f6a", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d3-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-de121c91a0" - }, - { - "exercises": [ - { - "cue": "unterer Rücken am Boden", - "key": "holl", - "name": "Hollow Hold", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "holl", - "id": "exercise-item-072e8d32be", - "exercise_id": "movement-hollow-hold-54c452f77b", - "movement_cluster": "core", - "movement_label": "Core", - "legacy_id": "d3-r1-e0" - }, - { - "cue": "seitlich auf einem Unterarm", - "key": "sp", - "name": "Side Plank (Wechsel)", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "sp", - "id": "exercise-1-cedec1156e", - "exercise_id": "movement-side-plank-wechsel-309b0bc82c", - "movement_cluster": "core", - "movement_label": "Core", - "legacy_id": "d3-r1-e1" - }, - { - "cue": "Ellbogen zum Gegenknie", - "key": "bic", - "name": "Bicycle Crunch", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "progression_id": "bic", - "id": "exercise-2-fe1a6ad3ca", - "exercise_id": "movement-bicycle-crunch-15c87e28c5", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d3-r1-e2" - }, - { - "cue": "sitzend Rumpf drehen", - "key": "tw", - "name": "Russian Twist (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "same" - }, - "progression_id": "tw", - "id": "exercise-3-bd5b54584a", - "exercise_id": "movement-russian-twist-kb-d6e382fb3c", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d3-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-017be520c7" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-beine-core-13963d0eea" - }, - { - "focus": "Oberkörper: Drücken + Ziehen", - "num": 4, - "rotations": [ - { - "exercises": [ - { - "cue": "Brust kontrolliert senken & drücken", - "key": "ls", - "name": "Liegestütze", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "ls", - "id": "exercise-item-3cc877a2d9", - "exercise_id": "movement-liegestuetze-57eacf74ee", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "legacy_id": "d4-r0-e0" - }, - { - "cue": "Po vor der Couch senken", - "key": "dips", - "name": "Bank-Dips (Couch)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "dips", - "id": "exercise-1-57f3db08a5", - "exercise_id": "movement-bank-dips-couch-ad6c829e1b", - "movement_cluster": "vertical_push", - "movement_label": "Dip", - "legacy_id": "d4-r0-e1" - }, - { - "cue": "umgek. V, Kopf senken", - "key": "pike", - "name": "Pike", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "pike", - "id": "exercise-2-f8e46033f7", - "exercise_id": "movement-pike-17f7716e00", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "legacy_id": "d4-r0-e2" - }, - { - "cue": "Glocke vom Boden weg drücken", - "key": "press", - "name": "KB Floor Press", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "press", - "id": "exercise-3-1bfa657543", - "exercise_id": "movement-kb-floor-press-060586ebd6", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "legacy_id": "d4-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-761392c073" - }, - { - "exercises": [ - { - "cue": "1 Arm, Ellbogen an den Rippen, Zug zur Hüfte · Lat · DG1 links / DG2 rechts", - "key": "rowe", - "name": "Einarm-Rudern ENG (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "rowe", - "id": "exercise-item-3eecbcd916", - "exercise_id": "movement-einarm-rudern-eng-kb-4907fbb5c7", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d4-r1-e0" - }, - { - "cue": "Oberarme fix, zu den Schultern", - "key": "load", - "name": "Bizeps-Curls (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "load", - "id": "exercise-1-5503931f9a", - "exercise_id": "movement-bizeps-curls-kb-rucksack-67944f00d5", - "movement_cluster": "elbow_flexion", - "movement_label": "Bizeps", - "legacy_id": "d4-r1-e1" - }, - { - "cue": "1 Arm, Ellbogen ~45° auf, Zug zum Brustkorb · oberer Rücken · DG1 links / DG2 rechts", - "key": "row2", - "name": "Einarm-Rudern BREIT (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "row2", - "id": "exercise-2-7d45b03b10", - "exercise_id": "movement-einarm-rudern-breit-kb-0146abcfbb", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d4-r1-e2" - }, - { - "cue": "bäuchlings, Rücken", - "key": "sup", - "name": "Y-T-W / Superman", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "sup", - "id": "exercise-3-ffc825e874", - "exercise_id": "movement-y-t-w-superman-1d331ef219", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d4-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-62363a4e87" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-oberkoerper-druecken-ziehen-15d0e7f677" - }, - { - "focus": "Ganzkörper LEICHT + Mobility", - "light": true, - "num": 5, - "rotations": [ - { - "exercises": [ - { - "cue": "locker, sauber", - "key": "name:kniebeuge-leicht", - "name": "Kniebeuge (leicht)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:kniebeuge-leicht", - "id": "exercise-item-4b0e2395e9", - "exercise_id": "movement-kniebeuge-leicht-4ae3fb9fc0", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d5-r0-e0" - }, - { - "cue": "leichtere Variante", - "key": "name:liegestuetze-1-stufe-leichter", - "name": "Liegestütze (1 Stufe leichter)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:liegestuetze-1-stufe-leichter", - "id": "exercise-1-9303fa85bc", - "exercise_id": "movement-liegestuetze-1-stufe-leichter-6033b9cb92", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "legacy_id": "d5-r0-e1" - }, - { - "cue": "wenig Gewicht", - "key": "name:rudern-leicht", - "name": "Rudern (leicht)", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:rudern-leicht", - "id": "exercise-2-5224ba2082", - "exercise_id": "movement-rudern-leicht-566f8de8f2", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d5-r0-e2" - }, - { - "cue": "ruhiges Tempo", - "key": "name:mountain-climbers-locker", - "name": "Mountain Climbers (locker)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:mountain-climbers-locker", - "id": "exercise-3-3f1a039ad3", - "exercise_id": "movement-mountain-climbers-locker-e4a7854ab9", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d5-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-1750a46419" - }, - { - "exercises": [ - { - "cue": "in den Stütz wandern, zurück", - "key": "name:inchworm", - "name": "Inchworm", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:inchworm", - "id": "exercise-item-03be6e8387", - "exercise_id": "movement-inchworm-50016ff2a4", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d5-r1-e0" - }, - { - "cue": "Rücken ründen/strecken", - "key": "name:cat-cow", - "name": "Cat-Cow", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:cat-cow", - "id": "exercise-1-92862f7d6a", - "exercise_id": "movement-cat-cow-d8e060c65f", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d5-r1-e1" - }, - { - "cue": "gegengleich Arm+Bein, kurz halten", - "key": "name:bird-dog", - "name": "Bird Dog", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "progression_id": "name:bird-dog", - "id": "exercise-2-46d9abeab7", - "exercise_id": "movement-bird-dog-f2c9d49c80", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d5-r1-e2" - }, - { - "cue": "langsam um den Kopf kreisen", - "key": "name:kb-halo-leicht", - "name": "KB-Halo (leicht)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "name:kb-halo-leicht", - "id": "exercise-3-e6cefad215", - "exercise_id": "movement-kb-halo-leicht-e3c156d79e", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "legacy_id": "d5-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-dde0c1adc1" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-ganzkoerper-leicht-mobility-ff8134fa48" - }, - { - "color": "#6D28D9", - "focus": "KETTLEBELL Ganzkörper", - "note": "Reine Kettlebell-Session. Swing = Hüft-Schnapp, kein Armheben. Glocke so wählen, dass alle Reps sauber sind.", - "note_bg": "#F1ECFB", - "num": 6, - "rotations": [ - { - "exercises": [ - { - "cue": "Hüft-Schnapp, kein Armheben", - "key": "swing", - "name": "KB Swing (zweiarmig)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "swing", - "id": "exercise-item-2fbf3bf70d", - "exercise_id": "movement-kb-swing-zweiarmig-327d7b5ad6", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d6-r0-e0" - }, - { - "cue": "Glocke vor der Brust, tief", - "key": "gobletkb", - "name": "Goblet Squat (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "gobletkb", - "id": "exercise-1-8542eb2e51", - "exercise_id": "movement-goblet-squat-kb-f065dfd5b6", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d6-r0-e1" - }, - { - "cue": "Glocke zwischen den Füßen, Hüfte hinge", - "key": "kbdl", - "name": "KB-Deadlift (beidbeinig)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "kbdl", - "id": "exercise-2-f072cf1841", - "exercise_id": "movement-kb-deadlift-beidbeinig-6f7c8bca76", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d6-r0-e2" - }, - { - "cue": "eng um den Kopf kreisen", - "key": "halo", - "name": "Halo", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "halo", - "id": "exercise-3-031200e160", - "exercise_id": "movement-halo-faca9a5cc9", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "legacy_id": "d6-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-1165003f5a" - }, - { - "exercises": [ - { - "cue": "Glocke vom Boden weg drücken", - "key": "press", - "name": "KB Floor Press", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "press", - "id": "exercise-item-04a3fceec5", - "exercise_id": "movement-kb-floor-press-060586ebd6", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "legacy_id": "d6-r1-e0" - }, - { - "cue": "Rücken gerade, zum Bauch", - "key": "row1", - "name": "Einarmiges Rudern (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "row1", - "id": "exercise-1-01087258d2", - "exercise_id": "movement-einarmiges-rudern-kb-0ec1c11b35", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d6-r1-e1" - }, - { - "cue": "Glocke Boden nach oben, Unterarm senkrecht, still halten", - "key": "carry", - "name": "Bottoms-up KB-Hold (Griff)", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "carry", - "id": "exercise-2-0a835e4b76", - "exercise_id": "movement-bottoms-up-kb-hold-griff-4a5f657f41", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "legacy_id": "d6-r1-e2" - }, - { - "cue": "Glocke um den Körper, dann diagonal zur Gegen-Schulter, andere Hand fängt", - "key": "atw", - "name": "Around-the-World + Uppercut (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "atw", - "id": "exercise-3-6386738afd", - "exercise_id": "movement-around-the-world-uppercut-kb-7ea726bbde", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "legacy_id": "d6-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-c851db6efb" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-kettlebell-ganzkoerper-e53128a814" - } - ], - "front": { - "baseline_head": "Standortbestimmung & Tracking", - "baseline_intro": "Mach die Tests vor Tag 1 und in Woche 8 identisch nochmal – dein Vorher/Nachher.", - "baseline_rows": [ - [ - "Test", - "Tag 0", - "Woche 8" - ], - [ - "Liegestütze (max, saubere Reps)", - "", - "" - ], - [ - "Bank-Dips an der Couch (max)", - "", - "" - ], - [ - "Kniebeugen in 60 Sekunden", - "", - "" - ], - [ - "Ausfallschritte (max gesamt)", - "", - "" - ], - [ - "Plank (max Sekunden)", - "", - "" - ], - [ - "Wall Sit (max Sekunden)", - "", - "" - ], - [ - "KB Farmer-Hold (max Sekunden, Griff)", - "", - "" - ], - [ - "Körpergewicht (kg)", - "95", - "" - ] - ], - "flow_rows": [ - [ - "Teil", - "Inhalt", - "Zeit" - ], - [ - "Warm-up", - "Gelenk-Prep (dynamisch)", - "~3 Min" - ], - [ - "Rotation A", - "4 Übungen, 3 Sätze (kurze Pausen)", - "~14 Min" - ], - [ - "Rotation B", - "4 Übungen, 3 Sätze (kurze Pausen)", - "~14 Min" - ], - [ - "Cool-down", - "Ausklingen + Dehnen", - "~6 Min" - ], - [ - "", - "Gesamt", - "~35 Min" - ] - ], - "goals_box": "Du startest bei 2–3 Liegestützen und 0 Dips. Realistisch und ehrlich nach 8 Wochen sauberer Arbeit:
•  Liegestütze: von 2–3 auf erste saubere volle Reps (Rest in Knie-/Negativ-Form).
•  Bank-Dips: saubere Reps mit Füßen etwas weiter weg.   Pike Push-up: am Boden mit kleinem ROM.
•  Plank ~45–60 s, Wall Sit ~60 s, viele saubere Goblet-Kniebeugen, sauberer Swing + spürbar bessere Griffkraft.
•  Noch NICHT dran: Diamond Push-ups, L-Sit, Handstand, schwerer Turkish Get-Up, Snatch – Stoff für Woche 12+.", - "goals_head": "Realistische Ziele nach 8 Wochen", - "goals_note": "Wichtig: Die Wochen-Stufen sind ein Rahmen, kein Muss. Bleib auf einer Stufe, solange sie fordert. Aufstiegs-Regel: erst hoch (oder schwerere Glocke), wenn du eine Übung 2 Sessions in Folge in allen Sätzen sauber über die vollen 20s schaffst. KB = Kettlebell bevorzugt; Rucksack bleibt als Alternative.", - "kb_box": "Du trainierst für Calisthenics, nicht für Maximalkraft. Genau da liegen die belegten Stärken der Kettlebell – ohne dass dich ihr dokumentierter Schwachpunkt (Maximalkraft/Hypertrophie wegen fixer Gewichtssprünge) trifft:
•  Griffkraft (Studie BELL: +6–7 kg) – dein größter Mehrwert auf dem Weg zu Klimmzug & Dip.
•  Power / ballistische Hüftstreckung (Swing) – explosive Basis für spätere Skills.
•  Kondition & Körperfett – wirksam belegt; du startest mit wenig Ausdauer.
•  Ganzkörper, ein Gerät, zeiteffizient – passt ideal ins 30-Min-Tabata-Format; niedriges Einstiegs-Verletzungsrisiko bei sauberer Technik.
Ehrlich: Reines KB ersetzt kein Maximalkraft-/Muskelaufbau-Programm. Für dein Ziel (Basis-Fundament) liegt das ohnehin unter der relevanten Schwelle.", - "kb_head": "Warum Kettlebell – und warum es zu deinem Ziel passt", - "lib_head": "Übungs-Bibliothek (mit Anleitungen)", - "lib_intro": "Für Übungen, die du noch nicht kennst, ist ein Link zu einer bebilderten Anleitung dabei. Lila Gruppe = Kettlebell-Übungen.", - "reminder_head": "Kurz-Reminder", - "reminders": [ - "Form vor Reps. Wähle die Stufe / Glocke, bei der du die 20s kontrolliert durcharbeitest – mit 1–2 Reps Reserve.", - "Swing-Technik first. Der Swing ist ein Hüft-Schnapp, KEIN Kniebeuge und KEIN Armheben. Erst Dead-Stop-Swings, bis die Hüftstreckung sitzt.", - "Einseitige Übungen (Tag 2): Durchgang 1 = links, Durchgang 2 = rechts – so ist jede Seite automatisch ausbalanciert.", - "Täglich ist viel. Tag 5 leicht, Tag 7 frei. Bei starkem Muskelkater einen Extra-Ruhetag – gehört zum Aufbau.", - "Schlaf & Eiweiß. 7–9 h Schlaf, ~1,6 g Eiweiß pro kg. Stechender Schmerz = stoppen." - ], - "session_how_body": "Tabata-Block = 4 Min: 10 Sek einzählen, dann 8× (20 Sek Arbeit / 10 Sek Pause). Pro Block rotieren 4 Übungen, je 2 Durchgänge = 8 Intervalle = 1 Block = 1 Satz. Eine Rotation läuft 3 Sätze (in Woche 1 ruhig mit 2 starten). Warm-up steht über, Cool-down unter den Übungen.", - "session_how_head": "So funktioniert eine Session", - "sheets_note": "Ab der nächsten Seite: 1 Druckblatt pro Session (Woche 1–8). Tag 6 = reine Kettlebell-Session. Warm-up steht über, Cool-down + Dehnen unter den Übungen. Bei Vorerkrankungen/Verletzungen vorher ärztlich abklären.", - "timer_note": "Timer: „Tabata / Interval Timer“ auf 10s Vorlauf, 8 Runden, 20/10." - }, - "library": [ - { - "color": "ACC2", - "group": "DRÜCKEN (Brust / Schultern / Trizeps)", - "items": [ - { - "desc": "Stütz, Körper gerade, Brust kontrolliert senken & drücken. Stufen: Inkline (Couch), Knie, Negativ, Voll.", - "name": "Liegestütze", - "url": "https://blog.calimove.com/2023/01/25/push-up-variations-beginner-expert/" - }, - { - "desc": "Rücken zur Couch, Hände auf der Kante, Po davor absenken (Ellbogen nach hinten), hochdrücken. Knie beugen = leichter.", - "name": "Bank-Dips (Couch)", - "url": "https://www.healthline.com/health/fitness-exercise/bench-dips" - }, - { - "desc": "Umgekehrtes V (Hintern hoch), Kopf Richtung Boden senken, hochdrücken. Schulter-Fokus. Hände erhöht = leichter.", - "name": "Pike Push-up / Hold", - "url": "https://www.hingehealth.com/resources/articles/pike-push-up/" - }, - { - "desc": "Arme seitlich bis Schulterhöhe heben, Ellbogen leicht gebeugt, kontrolliert senken. Seitlicher Deltamuskel.", - "name": "Seitheben (KB/Rucksack)", - "url": "" - } - ] - }, - { - "color": "ACC2", - "group": "ZIEHEN (Rücken / Bizeps – KB oder Rucksack + Boden)", - "items": [ - { - "desc": "Hüfte beugen, Rücken GERADE, abgestützt. Ellbogen an den Rippen, Zug zur Hüfte – Lat.", - "name": "Einarm-Rudern ENG", - "url": "https://www.puregym.com/exercises/back/rows/dumbbell-bent-over-row/" - }, - { - "desc": "Gleiche Position, aber Ellbogen ~45° geöffnet, Zug zum Brustkorb – oberer Rücken / hinterer Delta.", - "name": "Einarm-Rudern BREIT", - "url": "https://www.puregym.com/exercises/back/rows/dumbbell-bent-over-row/" - }, - { - "desc": "Last unten greifen, Oberarme fix am Körper, kontrolliert zu den Schultern curlen.", - "name": "Bizeps-Curls (KB/Rucksack)", - "url": "" - }, - { - "desc": "Bäuchlings Arme+Beine heben (Superman). Snow Angels = Arme am Boden auf/ab. Rücken ohne Stange.", - "name": "Superman / Reverse Snow Angels / Y-T-W", - "url": "https://www.calixpert.com/exercises/hollow-body-to-superman-hold" - } - ] - }, - { - "color": "ACC2", - "group": "BEINE", - "items": [ - { - "desc": "Hüfte zurück, Oberschenkel parallel, Fersen am Boden. Goblet = Glocke/Rucksack vor der Brust umarmen.", - "name": "Kniebeuge / Goblet Squat (KB/Rucksack)", - "url": "" - }, - { - "desc": "Großer Schritt (rückwärts = knieschonend), hinteres Knie Richtung Boden. Anfangs ggf. festhalten.", - "name": "Ausfallschritte", - "url": "" - }, - { - "desc": "Knie leicht gebeugt, Hüfte nach hinten, Rücken gerade, Last entlang der Beine senken bis Dehnung hinten (Hamstrings).", - "name": "RDL / Hip Hinge (KB/Rucksack)", - "url": "" - }, - { - "desc": "Bridge: Hüfte hochdrücken (1-beinig steigern). Waden: Zehenspitzen hoch/runter. Wall Sit: Rücken an die Wand, halten.", - "name": "Glute Bridge / Wadenheben / Wall Sit", - "url": "" - } - ] - }, - { - "color": "KBCOL", - "group": "KETTLEBELL (Power / Griff / Ganzkörper)", - "items": [ - { - "desc": "Wichtigste KB-Übung. Hüfthinge, Glocke aus der Hüfte explosiv auf Brusthöhe schnappen (kein Armheben), oben Po+Bauch fest. Dead-Stop zum Lernen.", - "name": "Swing (zweiarmig, Hardstyle)", - "url": "https://www.onepeloton.com/blog/kettlebell-swing/" - }, - { - "desc": "Glocke an den Hörnern vor der Brust umarmen, tief und aufrecht hocken. Beste Einsteiger-KB-Kniebeuge.", - "name": "Goblet Squat (KB)", - "url": "https://www.onepeloton.com/blog/goblet-squat/" - }, - { - "desc": "Glocke zwischen den Füßen, Hüfte hinge, Rücken gerade, vom Boden heben und Hüfte durchstrecken. Stabile Swing-Vorbereitung.", - "name": "KB-Deadlift (beidbeinig)", - "url": "https://www.onepeloton.com/blog/kettlebell-deadlift/" - }, - { - "desc": "Rücklings am Boden, Glocke vom Boden weg nach oben drücken. Schulterschonend, gut für Anfänger.", - "name": "KB Floor Press", - "url": "https://www.masterclass.com/articles/kettlebell-floor-press-guide" - }, - { - "desc": "Glocke (oder Rucksack) vor der Brust, langsam eng um den Kopf kreisen. Schulter-Mobility + Core.", - "name": "Halo", - "url": "https://www.tomsguide.com/how-to/kettlebell-halo-exercise-how-to-do-it-and-the-benefits-for-your-core-and-shoulder-muscles" - }, - { - "desc": "Glocke am Griff, Boden nach oben, Unterarm senkrecht, still halten. Schwierigkeit kommt aus der Balance – fordert Griff + Handgelenk auch mit leichter Glocke.", - "name": "Bottoms-up KB-Hold / Carry", - "url": "https://www.onepeloton.com/blog/farmers-carry/" - }, - { - "desc": "Glocke kreist um den Körper (Around-the-World); danach diagonal nach oben zur gegenüberliegenden Schulter/Brust führen, die andere Hand fängt ab (wie ein Uppercut von unten). Rotatorischer Core + Koordination. Erst nur das Umkreisen lernen, Uppercut später.", - "name": "Around-the-World + Uppercut", - "url": "" - } - ] - }, - { - "color": "ACC2", - "group": "CORE", - "items": [ - { - "desc": "Unterarmstütz, Körper gerade. Anfangs auf den Knien. Side Plank seitlich auf einem Unterarm.", - "name": "Plank / Side Plank", - "url": "" - }, - { - "desc": "Auf dem Rücken, unterer Rücken am Boden, Arme+Beine leicht angehoben. Anfänger: angewinkelt (Tuck).", - "name": "Hollow Hold", - "url": "https://www.calixpert.com/exercises/hollow-body-to-superman-hold" - }, - { - "desc": "Auf dem Rücken, Arme+Knie 90°, gegengleich Arm+Bein langsam ausstrecken, unterer Rücken bleibt am Boden.", - "name": "Dead Bug", - "url": "https://dieringe.com/exercises/dead-bug" - }, - { - "desc": "Vierfüßler, gegengleich Arm+Bein ausstrecken, Rumpf stabil, kurz halten.", - "name": "Bird Dog", - "url": "https://www.hingehealth.com/resources/articles/bird-dog/" - }, - { - "desc": "Climbers: Knie zügig zur Brust. Bicycle: Ellbogen zum Gegenknie. Twist: sitzend drehen (KB). Beinheben: Knie gebeugt = leichter.", - "name": "Mountain Climbers / Bicycle / Russian Twist / Beinheben", - "url": "" - } - ] - }, - { - "color": "ACC2", - "group": "WARM-UP & MOBILITY", - "items": [ - { - "desc": "Aus dem Stand vorbeugen, Hände nach vorne in den Stütz wandern, dann zurück.", - "name": "Inchworm", - "url": "https://www.onepeloton.com/blog/inchworm-exercise" - }, - { - "desc": "Klassische dynamische Aufwärm-Bewegungen – locker, zum Warmwerden.", - "name": "Jacks / High Knees / Kreisen / Cat-Cow", - "url": "" - } - ] - } - ], - "meta": { - "days": 6, - "doc_title": "Calisthenics 8-Wochen Komplettplan (mit Kettlebell)", - "footer_left": "Calisthenics 8-Wochen Komplettplan · mit Kettlebell", - "output_name": "Calisthenics-8-Wochen-Komplett.pdf", - "subtitle": "Blutiger Anfänger · 1,90 m · 95 kg · Rucksack + Couch + Kettlebell · tägliche ~30-Min Tabata-Session", - "title": "Calisthenics-Basis – 8-Wochen Komplettplan", - "weeks": 8 - }, - "phases": { - "items": [ - { - "name": "Phase 1 · Fundament", - "params": "Fokus: Technik (v.a. Swing als Dead-Stop). 2–3 Reps Reserve. Pausen bis ~60s. Leichte Glocke. Woche 1 mit 2 Sätzen pro Rotation.", - "weeks": 1, - "id": "phase-phase-1-fundament-9d3418c792" - }, - { - "name": "Phase 2 · Fundament", - "params": "Fokus: Technik (v.a. Swing als Dead-Stop). 2–3 Reps Reserve. Pausen bis ~60s. Leichte Glocke. Woche 1 mit 2 Sätzen pro Rotation.", - "weeks": 1, - "id": "phase-phase-2-fundament-f815c541ec" - }, - { - "name": "Phase 3 · Aufbau", - "params": "Sauber bleiben, letzter Satz fordernd. Pausen ~45–60s. Etwas schwerere Glocke / mehr Reps. 2–3 Sätze.", - "weeks": 1, - "id": "phase-phase-3-aufbau-fa21a3469e" - }, - { - "name": "Phase 4 · Aufbau", - "params": "Sauber bleiben, letzter Satz fordernd. Pausen ~45–60s. Etwas schwerere Glocke / mehr Reps. 2–3 Sätze.", - "weeks": 1, - "id": "phase-phase-4-aufbau-98dc807ce2" - }, - { - "name": "Phase 5 · Steigerung", - "params": "Fordernd, aber 1 Rep Reserve. Pausen ~45s. Schwerere Glocke / einarmig wo angegeben. 3 Sätze.", - "weeks": 1, - "id": "phase-phase-5-steigerung-f023eda6a2" - }, - { - "name": "Phase 6 · Steigerung", - "params": "Fordernd, aber 1 Rep Reserve. Pausen ~45s. Schwerere Glocke / einarmig wo angegeben. 3 Sätze.", - "weeks": 1, - "id": "phase-phase-6-steigerung-3dc7eb5c32" - }, - { - "name": "Phase 7 · Festigen", - "params": "Fordernd & sauber (NICHT bis zum Muskelversagen). Pausen ~45s. Ende Woche 8 leichter (Deload). 3 Sätze.", - "weeks": 1, - "id": "phase-phase-7-festigen-d8284c58fc" - }, - { - "name": "Phase 8 · Festigen", - "params": "Fordernd & sauber (NICHT bis zum Muskelversagen). Pausen ~45s. Ende Woche 8 leichter (Deload). 3 Sätze.", - "weeks": 1, - "id": "phase-phase-8-festigen-bfaebd57b4" - } - ], - "param_light": "Bewusst locker – aktive Erholung. Saubere, leichte Wiederholungen, keine Belastung bis zum Limit." - }, - "prepost": { - "1": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Türrahmen (Brust/Schulter, je 30–40s) · Trizeps (je 30s) · Seitheben/Lat-Seitdehnung (je 30s) · Quad im Stand (je 30s) · Knieender Hüftbeuger (je 30–40s) · Wade an der Wand (je 30s)", - "warmup": "Handflächen-Wippen · Armkreisen/Wand-Slides · Hüftkreisen + Beinschwünge · Knie-über-Zeh an der Wand" - }, - "2": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Lat-/Seitdehnung (je 30s) · Türrahmen (je 30–40s) · BWS über die Couch (30–40s) · Cobra/Sphinx (Bauch, 30s) · Unterarm (je 20s) · Knieender Hüftbeuger (je 30s)", - "warmup": "Handflächen-Wippen · Handtuch-Dislocates (8–10×) · Cat-Cow · Hüftkreisen + Beinschwünge" - }, - "3": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Hamstring-Dehnung (Couch, je 30–40s) · Quad im Stand (je 30s) · Knieender Hüftbeuger (je 30–40s) · Figure-4/Taube (je 30–40s) · Cobra + liegende Wirbelsäulen-Rotation (je 30s) · Wade an der Wand (je 30s)", - "warmup": "Hüftkreisen + Beinschwünge · Knie-über-Zeh an der Wand · Cat-Cow · Handflächen-Wippen" - }, - "4": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Türrahmen (je 30–40s) · Trizeps (je 30s) · Lat-/Seitdehnung (je 30s) · BWS über die Couch (30–40s) · Unterarm (je 20s)", - "warmup": "Handflächen-Wippen · Handtuch-Dislocates (8–10×) · Armkreisen/Wand-Slides · Cat-Cow" - }, - "5": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Türrahmen (je 30s) · Knieender Hüftbeuger (je 30s) · BWS über die Couch (30–40s) · Figure-4/Taube (je 30s) · Wade an der Wand (je 30s)", - "warmup": "Cat-Cow · Hüftkreisen + Beinschwünge · Armkreisen/Wand-Slides · Knie-über-Zeh an der Wand" - }, - "6": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Hamstring-Dehnung (Couch, je 30–40s) · Knieender Hüftbeuger (je 30–40s) · Figure-4/Taube (je 30–40s) · Unterarm (je 20s) · BWS über die Couch (30–40s) · Lat-/Seitdehnung (je 30s)", - "warmup": "Halo (leicht) · Hüftkreisen + Beinschwünge · Cat-Cow · Handflächen-Wippen" - } - }, - "stages": { - "atw": { - "name": "Around-the-World + Uppercut (KB)", - "steps": [ - { - "name": "nur Around-the-World, leicht", - "id": "step-nur-around-the-world-leicht-97c90b17c5", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "nur Around-the-World, leicht", - "id": "step-nur-around-the-world-leicht-fd2fab13cb", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "Around-the-World, mehr Reps", - "id": "step-around-the-world-mehr-reps-7af159bd81", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "Around-the-World, mehr Reps", - "id": "step-around-the-world-mehr-reps-43f1762f06", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "+ Uppercut, leicht", - "id": "step-uppercut-leicht-cb2ce4fe29", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "+ Uppercut, leicht", - "id": "step-uppercut-leicht-db86827e21", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "+ Uppercut, flüssig", - "id": "step-uppercut-fluessig-4b3453d5f9", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "+ Uppercut, flüssig", - "id": "step-uppercut-fluessig-2463ac3c0e", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - } - ], - "id": "progression-around-the-world-uppercut-kb-3cca10157e", - "key": "atw" - }, - "bic": { - "name": "Bicycle Crunch", - "steps": [ - { - "name": "langsam", - "id": "step-langsam-b16ce156f7", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "langsam", - "id": "step-langsam-3bc865247d", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-9b37c0536d", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-6a0ed59bbd", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-1a29fed611", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-60b7d1ac93", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-85e9262405", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-2b4933822c", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-bicycle-crunch-026823904f", - "key": "bic" - }, - "bridge": { - "name": "Glute Bridge", - "steps": [ - { - "name": "beidbeinig", - "id": "step-beidbeinig-36eb2634d6", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "beidbeinig", - "id": "step-beidbeinig-8a52d360c3", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "beidbeinig, 2s halten", - "id": "step-beidbeinig-2s-halten-1ad3dfd97b", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "beidbeinig, 2s halten", - "id": "step-beidbeinig-2s-halten-6087f8f6d1", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "einbeinig", - "id": "step-einbeinig-3a0c0abdfa", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "einbeinig", - "id": "step-einbeinig-43807e1138", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "einbeinig, 2s halten", - "id": "step-einbeinig-2s-halten-4e42109f20", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "einbeinig, 2s halten", - "id": "step-einbeinig-2s-halten-6335420028", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - } - ], - "id": "progression-glute-bridge-830b37f200", - "key": "bridge" - }, - "calf": { - "name": "Wadenheben", - "steps": [ - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-e8311d7913", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-95b36efc85", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "langsam", - "id": "step-langsam-9e41650da8", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "langsam", - "id": "step-langsam-74db877975", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "langsam, mehr Reps", - "id": "step-langsam-mehr-reps-16ced31e33", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "langsam, mehr Reps", - "id": "step-langsam-mehr-reps-bd0e8e15c4", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "1-beinig", - "id": "step-1-beinig-0c2e9de2f7", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "1-beinig", - "id": "step-1-beinig-f823fd2149", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "calf", - "factor": 1.0 - } - ], - "id": "progression-wadenheben-3833cd235f", - "key": "calf" - }, - "carry": { - "name": "Bottoms-up KB-Hold (Griff)", - "steps": [ - { - "name": "im Stand 10–15s", - "id": "step-im-stand-10-15s-e90dd57ff1", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "im Stand 10–15s", - "id": "step-im-stand-10-15s-db8eec9df0", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "15–20s, ruhig", - "id": "step-15-20s-ruhig-321e582ab2", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "15–20s, ruhig", - "id": "step-15-20s-ruhig-7db6eb1a01", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "bis Schulterhöhe 15s", - "id": "step-bis-schulterhoehe-15s-f1763ea9e5", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "bis Schulterhöhe 15s", - "id": "step-bis-schulterhoehe-15s-21c8fd4d01", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "länger / ein paar Schritte", - "id": "step-laenger-ein-paar-schritte-fa5838da65", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "länger / ein paar Schritte", - "id": "step-laenger-ein-paar-schritte-ec93f04473", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - } - ], - "id": "progression-bottoms-up-kb-hold-griff-7c46cda772", - "key": "carry" - }, - "db": { - "name": "Dead Bug", - "steps": [ - { - "name": "Arme ODER Beine einzeln", - "id": "step-arme-oder-beine-einzeln-7a6a9d581b", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "Arme ODER Beine einzeln", - "id": "step-arme-oder-beine-einzeln-94f8e1785d", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "gegengleich", - "id": "step-gegengleich-41e02caee7", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "gegengleich", - "id": "step-gegengleich-31e878a2f9", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "gegengleich + Pause", - "id": "step-gegengleich-pause-9e84e14ecc", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "gegengleich + Pause", - "id": "step-gegengleich-pause-bf8ebc0b7e", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "Beine weiter gestreckt", - "id": "step-beine-weiter-gestreckt-453d185cf7", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "Beine weiter gestreckt", - "id": "step-beine-weiter-gestreckt-1c5f566001", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - } - ], - "id": "progression-dead-bug-e390771cf1", - "key": "db" - }, - "dips": { - "name": "Bank-Dips (Couch)", - "steps": [ - { - "name": "Knie stark gebeugt, klein", - "id": "step-knie-stark-gebeugt-klein-72a7bffc76", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Knie stark gebeugt, klein", - "id": "step-knie-stark-gebeugt-klein-56482dde46", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Knie gebeugt, voll", - "id": "step-knie-gebeugt-voll-07cc34cd51", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Knie gebeugt, voll", - "id": "step-knie-gebeugt-voll-80070f8ecf", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Füße etwas weiter", - "id": "step-fuesse-etwas-weiter-42e0983b7f", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Füße etwas weiter", - "id": "step-fuesse-etwas-weiter-7b887f1140", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Füße weiter weg", - "id": "step-fuesse-weiter-weg-60c057769a", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Füße weiter weg", - "id": "step-fuesse-weiter-weg-2959fb77fb", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - } - ], - "id": "progression-bank-dips-couch-64352512e0", - "key": "dips" - }, - "gobletkb": { - "name": "Goblet Squat (KB/Rucksack)", - "steps": [ - { - "name": "leicht, Tiefe lernen", - "id": "step-leicht-tiefe-lernen-77201f2b6b", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "leicht, Tiefe lernen", - "id": "step-leicht-tiefe-lernen-70ea0f2e9b", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-57415830b2", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-abf827246a", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-2d542b9c09", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-eda93ad7ea", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mittel, 3s runter", - "id": "step-mittel-3s-runter-9dd7fec573", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mittel, 3s runter", - "id": "step-mittel-3s-runter-3f76b2391b", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-goblet-squat-kb-rucksack-0e2a5969bc", - "key": "gobletkb" - }, - "halo": { - "name": "Halo", - "steps": [ - { - "name": "leicht, langsam", - "id": "step-leicht-langsam-5d15ba8134", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "leicht, langsam", - "id": "step-leicht-langsam-c2a5c59e62", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-81a736180a", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-fe775a4a23", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-c5c0bf8989", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-3898a7cea7", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "mittel, langsam", - "id": "step-mittel-langsam-c9db464d00", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "mittel, langsam", - "id": "step-mittel-langsam-e1a2e46fd3", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - } - ], - "id": "progression-halo-8470d68816", - "key": "halo" - }, - "holl": { - "name": "Hollow Hold", - "steps": [ - { - "name": "Tuck (Knie an)", - "id": "step-tuck-knie-an-be15e1c436", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "Tuck (Knie an)", - "id": "step-tuck-knie-an-5f62fcb360", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "Tuck, länger", - "id": "step-tuck-laenger-b05dd43bd8", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "Tuck, länger", - "id": "step-tuck-laenger-c70e4ba401", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "halb gestreckt", - "id": "step-halb-gestreckt-39bccf6e6b", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.82 - }, - { - "name": "halb gestreckt", - "id": "step-halb-gestreckt-9147433141", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.82 - }, - { - "name": "halb/voll", - "id": "step-halb-voll-13f8ebb8c0", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.82 - }, - { - "name": "halb/voll", - "id": "step-halb-voll-cde344cba9", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.82 - } - ], - "id": "progression-hollow-hold-7e44b4d78e", - "key": "holl" - }, - "kbdl": { - "name": "KB-Deadlift (beidbeinig)", - "steps": [ - { - "name": "leicht, Technik", - "id": "step-leicht-technik-54ca22fe3d", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "leicht, Technik", - "id": "step-leicht-technik-fcd2dc3979", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-db29d97bd7", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-fd7e87b98b", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-7ed7284a10", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-e9887f19f9", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "schwerer, kontrolliert", - "id": "step-schwerer-kontrolliert-dd0f4f4538", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "schwerer, kontrolliert", - "id": "step-schwerer-kontrolliert-ab6af6caea", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - } - ], - "id": "progression-kb-deadlift-beidbeinig-0e27bd039a", - "key": "kbdl" - }, - "load": { - "name": "Bizeps-Curls (KB/Rucksack)", - "steps": [ - { - "name": "leicht", - "id": "step-leicht-273b789e0f", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-93f0046a45", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-bd37c72d4b", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-31ffb19393", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "mehr", - "id": "step-mehr-ca55180c8e", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "mehr", - "id": "step-mehr-d15ecf67a9", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "fordernd", - "id": "step-fordernd-b1f6c26c72", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "fordernd", - "id": "step-fordernd-b575d29101", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - } - ], - "id": "progression-bizeps-curls-kb-rucksack-b6ad39301a", - "key": "load" - }, - "lr": { - "name": "Beinheben", - "steps": [ - { - "name": "Knie gebeugt", - "id": "step-knie-gebeugt-1855b4de0f", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie gebeugt", - "id": "step-knie-gebeugt-863780bc32", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie gebeugt, langsam", - "id": "step-knie-gebeugt-langsam-425f61fdb6", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie gebeugt, langsam", - "id": "step-knie-gebeugt-langsam-aad675fda3", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "halb gestreckt", - "id": "step-halb-gestreckt-c84cc4988c", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "halb gestreckt", - "id": "step-halb-gestreckt-298f78b046", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "gestreckt", - "id": "step-gestreckt-a4f6fc11b8", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "gestreckt", - "id": "step-gestreckt-d43210faa3", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-beinheben-9e54736d11", - "key": "lr" - }, - "lraise": { - "name": "Seitheben (KB/Rucksack)", - "steps": [ - { - "name": "leicht (Technik)", - "id": "step-leicht-technik-a0504bbbb1", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "leicht (Technik)", - "id": "step-leicht-technik-a691c813e1", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-82a36d8e86", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-c736c1e695", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-3c1675e579", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-82cacf1f60", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "mittel, langsam", - "id": "step-mittel-langsam-6b417be46f", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "mittel, langsam", - "id": "step-mittel-langsam-b3e2812063", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-seitheben-kb-rucksack-98cf022c83", - "key": "lraise" - }, - "ls": { - "name": "Liegestütze", - "steps": [ - { - "name": "Inkline (hoch)", - "id": "step-inkline-hoch-7495b7e9cc", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.1 - }, - { - "name": "Inkline (hoch)", - "id": "step-inkline-hoch-231f998085", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.1 - }, - { - "name": "Inkline tiefer / Knie", - "id": "step-inkline-tiefer-knie-631d725178", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.35 - }, - { - "name": "Inkline tiefer / Knie", - "id": "step-inkline-tiefer-knie-2f351c5335", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.35 - }, - { - "name": "Knie / Negativ", - "id": "step-knie-negativ-0cf75923cb", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.7 - }, - { - "name": "Knie / Negativ", - "id": "step-knie-negativ-d225638aef", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.7 - }, - { - "name": "Negativ + erste Volle", - "id": "step-negativ-erste-volle-d68abebd8f", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.85 - }, - { - "name": "Negativ + erste Volle", - "id": "step-negativ-erste-volle-d93d10d94d", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.85 - } - ], - "id": "progression-liegestuetze-8c8a5e07de", - "key": "ls" - }, - "lseng": { - "name": "Inkline (hoch)", - "steps": [ - { - "name": "Inkline (hoch)", - "id": "step-inkline-hoch-7d971a7291", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline (hoch)", - "id": "step-inkline-hoch-59da475437", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline mittel", - "id": "step-inkline-mittel-821fadec71", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline mittel", - "id": "step-inkline-mittel-199e2aaf70", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline tief", - "id": "step-inkline-tief-6173a85a04", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline tief", - "id": "step-inkline-tief-54d7a4a0ff", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie-Liegestütze", - "id": "step-knie-liegestuetze-7de6b6dcfb", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie-Liegestütze", - "id": "step-knie-liegestuetze-2d4d23db17", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-inkline-hoch-e770797129", - "key": "lseng" - }, - "lunge": { - "name": "Ausfallschritt rückw.", - "steps": [ - { - "name": "Körpergewicht (ggf. festhalten)", - "id": "step-koerpergewicht-ggf-festhalten-3d6301d1bd", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 0.55 - }, - { - "name": "Körpergewicht (ggf. festhalten)", - "id": "step-koerpergewicht-ggf-festhalten-0d19269674", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 0.55 - }, - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-180853b5dd", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-0f7d4a63bd", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mehr Reps", - "id": "step-mehr-reps-d19d6c0eab", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mehr Reps", - "id": "step-mehr-reps-9235507680", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "+ Last leicht", - "id": "step-last-leicht-10d2acd532", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "+ Last leicht", - "id": "step-last-leicht-6ecdd4c919", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-ausfallschritt-rueckw-52ab461616", - "key": "lunge" - }, - "mtn": { - "name": "Mountain Climbers", - "steps": [ - { - "name": "moderat", - "id": "step-moderat-f76617e319", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-a73e3b0549", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-99c93498d6", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-a3b745efcb", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-c304accace", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-23a81c4905", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-25e355b703", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-4d2a2fd39c", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-mountain-climbers-97f6359644", - "key": "mtn" - }, - "pike": { - "name": "Pike", - "steps": [ - { - "name": "Pike Hold", - "id": "step-pike-hold-d46aa2e93f", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 0.3 - }, - { - "name": "Pike Hold", - "id": "step-pike-hold-cc334c7fc6", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 0.3 - }, - { - "name": "Hold + kleine Senkung", - "id": "step-hold-kleine-senkung-4f3bfac4b2", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 0.3 - }, - { - "name": "Hold + kleine Senkung", - "id": "step-hold-kleine-senkung-b93d503cb1", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 0.3 - }, - { - "name": "Push-up Hände erhöht", - "id": "step-push-up-haende-erhoeht-7c94ab03f7", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Push-up Hände erhöht", - "id": "step-push-up-haende-erhoeht-5af7a607db", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Push-up Boden, klein", - "id": "step-push-up-boden-klein-0a27dd0a33", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Push-up Boden, klein", - "id": "step-push-up-boden-klein-44741d722a", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - } - ], - "id": "progression-pike-9726e02fef", - "key": "pike" - }, - "plank": { - "name": "Plank", - "steps": [ - { - "name": "auf Knien 15–20 s", - "id": "step-auf-knien-15-20-s-66aa989041", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "auf Knien 15–20 s", - "id": "step-auf-knien-15-20-s-28c28cffdc", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "voll 20–30 s", - "id": "step-voll-20-30-s-ebbb758203", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll 20–30 s", - "id": "step-voll-20-30-s-465691f146", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll + Schulter-Taps", - "id": "step-voll-schulter-taps-9fbac9d915", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll + Schulter-Taps", - "id": "step-voll-schulter-taps-738cd33982", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll, länger", - "id": "step-voll-laenger-3da43cf026", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll, länger", - "id": "step-voll-laenger-827f28be3a", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - } - ], - "id": "progression-plank-88a9d6bafb", - "key": "plank" - }, - "press": { - "name": "KB Floor Press", - "steps": [ - { - "name": "leicht (Floor Press)", - "id": "step-leicht-floor-press-de744d6e05", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "leicht (Floor Press)", - "id": "step-leicht-floor-press-5987ddc1d2", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Floor Press mehr", - "id": "step-floor-press-mehr-3b454ea446", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Floor Press mehr", - "id": "step-floor-press-mehr-b34710868b", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Overhead leicht", - "id": "step-overhead-leicht-7f9274fd29", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Overhead leicht", - "id": "step-overhead-leicht-3413a54a3a", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Overhead mittel", - "id": "step-overhead-mittel-4b236720a7", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Overhead mittel", - "id": "step-overhead-mittel-ea724f9a42", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - } - ], - "id": "progression-kb-floor-press-0f387c469f", - "key": "press" - }, - "row1": { - "name": "Einarmiges Rudern (KB)", - "steps": [ - { - "name": "leicht", - "id": "step-leicht-db8c75b358", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-d1e4feaafa", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-19ded398d6", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-0ce270eb41", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "einarmig leicht", - "id": "step-einarmig-leicht-a6f388b349", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "einarmig leicht", - "id": "step-einarmig-leicht-a9539fbadb", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "einarmig mittel", - "id": "step-einarmig-mittel-74f0fe3e1e", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "einarmig mittel", - "id": "step-einarmig-mittel-3f961280f6", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - } - ], - "id": "progression-einarmiges-rudern-kb-c142975b5b", - "key": "row1" - }, - "row2": { - "name": "Einarm-Rudern BREIT (KB)", - "steps": [ - { - "name": "leicht (Technik)", - "id": "step-leicht-technik-32d83f79d0", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht (Technik)", - "id": "step-leicht-technik-76c26b9c7f", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-3bbb6ef617", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-1fea9ba00a", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-5f7bc5489c", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-d4c65b81c6", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mittel, Pause oben", - "id": "step-mittel-pause-oben-d5cdfdfe94", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mittel, Pause oben", - "id": "step-mittel-pause-oben-8f7a255ad4", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - } - ], - "id": "progression-einarm-rudern-breit-kb-a06708cc5d", - "key": "row2" - }, - "rowe": { - "name": "Einarm-Rudern ENG (KB)", - "steps": [ - { - "name": "leicht", - "id": "step-leicht-7638fa3210", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-d1490519d3", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-4f2775725c", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-35098ff9fa", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mehr", - "id": "step-mehr-cd74b90448", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mehr", - "id": "step-mehr-558d6deb5b", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "fordernd", - "id": "step-fordernd-fec615da4c", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "fordernd", - "id": "step-fordernd-60a8a2aedd", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - } - ], - "id": "progression-einarm-rudern-eng-kb-b622a3fbfc", - "key": "rowe" - }, - "sp": { - "name": "Side Plank (Wechsel)", - "steps": [ - { - "name": "auf Knien", - "id": "step-auf-knien-3ed08acc27", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "auf Knien", - "id": "step-auf-knien-000b6c864b", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "auf Knien, länger", - "id": "step-auf-knien-laenger-d1c3028780", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "auf Knien, länger", - "id": "step-auf-knien-laenger-227ec9b5f7", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "voll", - "id": "step-voll-6702e0693f", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll", - "id": "step-voll-727fcc161a", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll, länger", - "id": "step-voll-laenger-8c287bb42a", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll, länger", - "id": "step-voll-laenger-ef549f755a", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 1.0 - } - ], - "id": "progression-side-plank-wechsel-335152ff85", - "key": "sp" - }, - "squat": { - "name": "Körpergewicht", - "steps": [ - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-e4bfd724af", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-cf13a813b5", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Körpergewicht, mehr Reps", - "id": "step-koerpergewicht-mehr-reps-0156c8a851", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Körpergewicht, mehr Reps", - "id": "step-koerpergewicht-mehr-reps-9fccf7b6ed", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet leicht", - "id": "step-goblet-leicht-e52d2635ab", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet leicht", - "id": "step-goblet-leicht-758736b15a", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet mittel", - "id": "step-goblet-mittel-50a2e4ec3e", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet mittel", - "id": "step-goblet-mittel-0c01361ea0", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-koerpergewicht-6b7808a004", - "key": "squat" - }, - "sup": { - "name": "Superman / Y-T-W", - "steps": [ - { - "name": "Superman Hold", - "id": "step-superman-hold-06efa2683a", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Superman Hold", - "id": "step-superman-hold-34af38f543", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Superman Hold länger", - "id": "step-superman-hold-laenger-b383f0e540", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Superman Hold länger", - "id": "step-superman-hold-laenger-6b326b0a11", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Reverse Snow Angels", - "id": "step-reverse-snow-angels-871916bcee", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Reverse Snow Angels", - "id": "step-reverse-snow-angels-78c24f8835", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Y-T-W Raises", - "id": "step-y-t-w-raises-b7c40c3579", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Y-T-W Raises", - "id": "step-y-t-w-raises-a6d76cb15c", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-superman-y-t-w-06938b356b", - "key": "sup" - }, - "swing": { - "name": "KB Swing (zweiarmig)", - "steps": [ - { - "name": "Dead-Stop, leicht (Technik)", - "id": "step-dead-stop-leicht-technik-02548f1a05", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "Dead-Stop, leicht (Technik)", - "id": "step-dead-stop-leicht-technik-3b9715b104", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "Dead-Stop, mehr Reps", - "id": "step-dead-stop-mehr-reps-7be430a756", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "Dead-Stop, mehr Reps", - "id": "step-dead-stop-mehr-reps-5c68075818", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "durchgehend, mittel", - "id": "step-durchgehend-mittel-183cc7dfa6", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "durchgehend, mittel", - "id": "step-durchgehend-mittel-7762c8f240", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "durchgehend, schwerer", - "id": "step-durchgehend-schwerer-6da670bed8", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "durchgehend, schwerer", - "id": "step-durchgehend-schwerer-a1609b5bad", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - } - ], - "id": "progression-kb-swing-zweiarmig-b9b86d8de1", - "key": "swing" - }, - "tsquat": { - "name": "3s runter", - "steps": [ - { - "name": "3s runter", - "id": "step-3s-runter-1cafcb49f1", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "3s runter", - "id": "step-3s-runter-c29275cc81", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "3s + Pause unten", - "id": "step-3s-pause-unten-8b4f16c542", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "3s + Pause unten", - "id": "step-3s-pause-unten-d51468b370", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet 3s", - "id": "step-goblet-3s-ebd1293f2a", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet 3s", - "id": "step-goblet-3s-06003d3bdd", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet 3s + Pause", - "id": "step-goblet-3s-pause-a70c738b23", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet 3s + Pause", - "id": "step-goblet-3s-pause-40369412a4", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-3s-runter-b85ef7f990", - "key": "tsquat" - }, - "tw": { - "name": "Russian Twist (KB)", - "steps": [ - { - "name": "ohne Gewicht", - "id": "step-ohne-gewicht-3bd115d2ec", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "ohne Gewicht", - "id": "step-ohne-gewicht-478be9efcd", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "ohne Gewicht", - "id": "step-ohne-gewicht-32fc20afc1", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "ohne Gewicht", - "id": "step-ohne-gewicht-fbe33d8a04", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "KB leicht", - "id": "step-kb-leicht-cf37c395cd", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "KB leicht", - "id": "step-kb-leicht-5cca613a86", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "KB", - "id": "step-kb-f50c52e3e3", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "KB", - "id": "step-kb-eb15afb22c", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-russian-twist-kb-4ae08fd89a", - "key": "tw" - }, - "wall": { - "name": "Wall Sit", - "steps": [ - { - "name": "15–25 s", - "id": "step-15-25-s-c7aec770d4", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "15–25 s", - "id": "step-15-25-s-dbdcddfcac", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "25–40 s", - "id": "step-25-40-s-7a71cfd7e1", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "25–40 s", - "id": "step-25-40-s-851e536e20", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "40–55 s", - "id": "step-40-55-s-96e1d7939b", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "40–55 s", - "id": "step-40-55-s-ebe34a6bbd", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "55–70 s", - "id": "step-55-70-s-3ddd9ace4a", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "55–70 s", - "id": "step-55-70-s-19c7e1d6d2", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-wall-sit-e5b793fbdf", - "key": "wall" - } - }, - "training_format": { - "mode": "tabata", - "fixed_interval": true, - "work_seconds": 20, - "rest_seconds": 10, - "rounds": 8, - "rounds_scope": "block", - "schema_version": 1 - }, - "schema_version": 3, - "contract_version": 2, - "plan_id": "phase-1", - "exercise_catalog": { - "movement-liegestuetze-57eacf74ee": { - "id": "movement-liegestuetze-57eacf74ee", - "name": "Liegestütze", - "progression_id": "ls", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-inkline-hoch-7495b7e9cc", - "name": "Inkline (hoch)", - "factor": 0.1, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-inkline-hoch-231f998085", - "name": "Inkline (hoch)", - "factor": 0.1, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-inkline-tiefer-knie-631d725178", - "name": "Inkline tiefer / Knie", - "factor": 0.35, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-inkline-tiefer-knie-2f351c5335", - "name": "Inkline tiefer / Knie", - "factor": 0.35, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-negativ-0cf75923cb", - "name": "Knie / Negativ", - "factor": 0.7, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-negativ-d225638aef", - "name": "Knie / Negativ", - "factor": 0.7, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-negativ-erste-volle-d68abebd8f", - "name": "Negativ + erste Volle", - "factor": 0.85, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-negativ-erste-volle-d93d10d94d", - "name": "Negativ + erste Volle", - "factor": 0.85, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-bank-dips-couch-ad6c829e1b": { - "id": "movement-bank-dips-couch-ad6c829e1b", - "name": "Bank-Dips (Couch)", - "progression_id": "dips", - "movement_cluster": "vertical_push", - "movement_label": "Dip", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-knie-stark-gebeugt-klein-72a7bffc76", - "name": "Knie stark gebeugt, klein", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-stark-gebeugt-klein-56482dde46", - "name": "Knie stark gebeugt, klein", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-voll-07cc34cd51", - "name": "Knie gebeugt, voll", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-voll-80070f8ecf", - "name": "Knie gebeugt, voll", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fuesse-etwas-weiter-42e0983b7f", - "name": "Füße etwas weiter", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fuesse-etwas-weiter-7b887f1140", - "name": "Füße etwas weiter", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fuesse-weiter-weg-60c057769a", - "name": "Füße weiter weg", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fuesse-weiter-weg-2959fb77fb", - "name": "Füße weiter weg", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-pike-17f7716e00": { - "id": "movement-pike-17f7716e00", - "name": "Pike", - "progression_id": "pike", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-pike-hold-d46aa2e93f", - "name": "Pike Hold", - "factor": 0.3, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-pike-hold-cc334c7fc6", - "name": "Pike Hold", - "factor": 0.3, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-hold-kleine-senkung-4f3bfac4b2", - "name": "Hold + kleine Senkung", - "factor": 0.3, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-hold-kleine-senkung-b93d503cb1", - "name": "Hold + kleine Senkung", - "factor": 0.3, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-push-up-haende-erhoeht-7c94ab03f7", - "name": "Push-up Hände erhöht", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-push-up-haende-erhoeht-5af7a607db", - "name": "Push-up Hände erhöht", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-push-up-boden-klein-0a27dd0a33", - "name": "Push-up Boden, klein", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-push-up-boden-klein-44741d722a", - "name": "Push-up Boden, klein", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-seitheben-kb-rucksack-91de5dc2f5": { - "id": "movement-seitheben-kb-rucksack-91de5dc2f5", - "name": "Seitheben (KB/Rucksack)", - "progression_id": "lraise", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-technik-a0504bbbb1", - "name": "leicht (Technik)", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-technik-a691c813e1", - "name": "leicht (Technik)", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-mehr-reps-82a36d8e86", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-mehr-reps-c736c1e695", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-3c1675e579", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-82cacf1f60", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-langsam-6b417be46f", - "name": "mittel, langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-langsam-b3e2812063", - "name": "mittel, langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-goblet-squat-kb-rucksack-6ff9f90901": { - "id": "movement-goblet-squat-kb-rucksack-6ff9f90901", - "name": "Goblet Squat (KB/Rucksack)", - "progression_id": "gobletkb", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-tiefe-lernen-77201f2b6b", - "name": "leicht, Tiefe lernen", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-tiefe-lernen-70ea0f2e9b", - "name": "leicht, Tiefe lernen", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-57415830b2", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-abf827246a", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-2d542b9c09", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-eda93ad7ea", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-3s-runter-9dd7fec573", - "name": "mittel, 3s runter", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-3s-runter-3f76b2391b", - "name": "mittel, 3s runter", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-ausfallschritt-rueckw-2a4d06b799": { - "id": "movement-ausfallschritt-rueckw-2a4d06b799", - "name": "Ausfallschritt rückw.", - "progression_id": "lunge", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-koerpergewicht-ggf-festhalten-3d6301d1bd", - "name": "Körpergewicht (ggf. festhalten)", - "factor": 0.55, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-koerpergewicht-ggf-festhalten-0d19269674", - "name": "Körpergewicht (ggf. festhalten)", - "factor": 0.55, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-koerpergewicht-180853b5dd", - "name": "Körpergewicht", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-koerpergewicht-0f7d4a63bd", - "name": "Körpergewicht", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mehr-reps-d19d6c0eab", - "name": "mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mehr-reps-9235507680", - "name": "mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-last-leicht-10d2acd532", - "name": "+ Last leicht", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-last-leicht-6ecdd4c919", - "name": "+ Last leicht", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-glute-bridge-1c0a7bf699": { - "id": "movement-glute-bridge-1c0a7bf699", - "name": "Glute Bridge", - "progression_id": "bridge", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-beidbeinig-36eb2634d6", - "name": "beidbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-8a52d360c3", - "name": "beidbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-2s-halten-1ad3dfd97b", - "name": "beidbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-2s-halten-6087f8f6d1", - "name": "beidbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-einbeinig-3a0c0abdfa", - "name": "einbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-43807e1138", - "name": "einbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-2s-halten-4e42109f20", - "name": "einbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-2s-halten-6335420028", - "name": "einbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - } - ] - }, - "movement-wadenheben-0c7839551b": { - "id": "movement-wadenheben-0c7839551b", - "name": "Wadenheben", - "progression_id": "calf", - "movement_cluster": "calf", - "movement_label": "Waden", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-koerpergewicht-e8311d7913", - "name": "Körpergewicht", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-koerpergewicht-95b36efc85", - "name": "Körpergewicht", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-9e41650da8", - "name": "langsam", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-74db877975", - "name": "langsam", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-mehr-reps-16ced31e33", - "name": "langsam, mehr Reps", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-mehr-reps-bd0e8e15c4", - "name": "langsam, mehr Reps", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-1-beinig-0c2e9de2f7", - "name": "1-beinig", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-1-beinig-f823fd2149", - "name": "1-beinig", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - } - ] - }, - "movement-einarm-rudern-eng-kb-4907fbb5c7": { - "id": "movement-einarm-rudern-eng-kb-4907fbb5c7", - "name": "Einarm-Rudern ENG (KB)", - "progression_id": "rowe", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-7638fa3210", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-d1490519d3", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-etwas-mehr-4f2775725c", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-etwas-mehr-35098ff9fa", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mehr-cd74b90448", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mehr-558d6deb5b", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-fordernd-fec615da4c", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-fordernd-60a8a2aedd", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-bizeps-curls-kb-rucksack-67944f00d5": { - "id": "movement-bizeps-curls-kb-rucksack-67944f00d5", - "name": "Bizeps-Curls (KB/Rucksack)", - "progression_id": "load", - "movement_cluster": "elbow_flexion", - "movement_label": "Bizeps", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-273b789e0f", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-93f0046a45", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-etwas-mehr-bd37c72d4b", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-etwas-mehr-31ffb19393", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mehr-ca55180c8e", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mehr-d15ecf67a9", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fordernd-b1f6c26c72", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fordernd-b575d29101", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-einarm-rudern-breit-kb-0146abcfbb": { - "id": "movement-einarm-rudern-breit-kb-0146abcfbb", - "name": "Einarm-Rudern BREIT (KB)", - "progression_id": "row2", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-technik-32d83f79d0", - "name": "leicht (Technik)", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-technik-76c26b9c7f", - "name": "leicht (Technik)", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-mehr-reps-3bbb6ef617", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-mehr-reps-1fea9ba00a", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-5f7bc5489c", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-d4c65b81c6", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-pause-oben-d5cdfdfe94", - "name": "mittel, Pause oben", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-pause-oben-8f7a255ad4", - "name": "mittel, Pause oben", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-superman-y-t-w-279f03a544": { - "id": "movement-superman-y-t-w-279f03a544", - "name": "Superman / Y-T-W", - "progression_id": "sup", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-superman-hold-06efa2683a", - "name": "Superman Hold", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-34af38f543", - "name": "Superman Hold", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-laenger-b383f0e540", - "name": "Superman Hold länger", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-laenger-6b326b0a11", - "name": "Superman Hold länger", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-reverse-snow-angels-871916bcee", - "name": "Reverse Snow Angels", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-reverse-snow-angels-78c24f8835", - "name": "Reverse Snow Angels", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-y-t-w-raises-b7c40c3579", - "name": "Y-T-W Raises", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-y-t-w-raises-a6d76cb15c", - "name": "Y-T-W Raises", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-plank-c8449f2937": { - "id": "movement-plank-c8449f2937", - "name": "Plank", - "progression_id": "plank", - "movement_cluster": "core", - "movement_label": "Core", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-auf-knien-15-20-s-66aa989041", - "name": "auf Knien 15–20 s", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-auf-knien-15-20-s-28c28cffdc", - "name": "auf Knien 15–20 s", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-20-30-s-ebbb758203", - "name": "voll 20–30 s", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-20-30-s-465691f146", - "name": "voll 20–30 s", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-schulter-taps-9fbac9d915", - "name": "voll + Schulter-Taps", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-schulter-taps-738cd33982", - "name": "voll + Schulter-Taps", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-laenger-3da43cf026", - "name": "voll, länger", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-laenger-827f28be3a", - "name": "voll, länger", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-mountain-climbers-79efdb62dd": { - "id": "movement-mountain-climbers-79efdb62dd", - "name": "Mountain Climbers", - "progression_id": "mtn", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-moderat-f76617e319", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-a73e3b0549", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-99c93498d6", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-a3b745efcb", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-c304accace", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-23a81c4905", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-25e355b703", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-4d2a2fd39c", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-dead-bug-21d4b207d5": { - "id": "movement-dead-bug-21d4b207d5", - "name": "Dead Bug", - "progression_id": "db", - "movement_cluster": "core", - "movement_label": "Core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-arme-oder-beine-einzeln-7a6a9d581b", - "name": "Arme ODER Beine einzeln", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-arme-oder-beine-einzeln-94f8e1785d", - "name": "Arme ODER Beine einzeln", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gegengleich-41e02caee7", - "name": "gegengleich", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gegengleich-31e878a2f9", - "name": "gegengleich", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gegengleich-pause-9e84e14ecc", - "name": "gegengleich + Pause", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gegengleich-pause-bf8ebc0b7e", - "name": "gegengleich + Pause", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beine-weiter-gestreckt-453d185cf7", - "name": "Beine weiter gestreckt", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beine-weiter-gestreckt-1c5f566001", - "name": "Beine weiter gestreckt", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-beinheben-faf19a629b": { - "id": "movement-beinheben-faf19a629b", - "name": "Beinheben", - "progression_id": "lr", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-knie-gebeugt-1855b4de0f", - "name": "Knie gebeugt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-863780bc32", - "name": "Knie gebeugt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-langsam-425f61fdb6", - "name": "Knie gebeugt, langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-langsam-aad675fda3", - "name": "Knie gebeugt, langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-gestreckt-c84cc4988c", - "name": "halb gestreckt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-gestreckt-298f78b046", - "name": "halb gestreckt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gestreckt-a4f6fc11b8", - "name": "gestreckt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gestreckt-d43210faa3", - "name": "gestreckt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-rdl-hip-hinge-kb-rucksack-9ada5a67c3": { - "id": "movement-rdl-hip-hinge-kb-rucksack-9ada5a67c3", - "name": "RDL / Hip Hinge (KB/Rucksack)", - "progression_id": "load", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-273b789e0f", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-93f0046a45", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-etwas-mehr-bd37c72d4b", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-etwas-mehr-31ffb19393", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mehr-ca55180c8e", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mehr-d15ecf67a9", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fordernd-b1f6c26c72", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fordernd-b575d29101", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-glute-bridge-1-beinig-ccc8c9fb53": { - "id": "movement-glute-bridge-1-beinig-ccc8c9fb53", - "name": "Glute Bridge 1-beinig", - "progression_id": "bridge", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-beidbeinig-36eb2634d6", - "name": "beidbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-8a52d360c3", - "name": "beidbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-2s-halten-1ad3dfd97b", - "name": "beidbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-2s-halten-6087f8f6d1", - "name": "beidbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-einbeinig-3a0c0abdfa", - "name": "einbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-43807e1138", - "name": "einbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-2s-halten-4e42109f20", - "name": "einbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-2s-halten-6335420028", - "name": "einbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - } - ] - }, - "movement-wall-sit-6b5f695f6a": { - "id": "movement-wall-sit-6b5f695f6a", - "name": "Wall Sit", - "progression_id": "wall", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-15-25-s-c7aec770d4", - "name": "15–25 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-15-25-s-dbdcddfcac", - "name": "15–25 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-25-40-s-7a71cfd7e1", - "name": "25–40 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-25-40-s-851e536e20", - "name": "25–40 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-40-55-s-96e1d7939b", - "name": "40–55 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-40-55-s-ebe34a6bbd", - "name": "40–55 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-55-70-s-3ddd9ace4a", - "name": "55–70 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-55-70-s-19c7e1d6d2", - "name": "55–70 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-hollow-hold-54c452f77b": { - "id": "movement-hollow-hold-54c452f77b", - "name": "Hollow Hold", - "progression_id": "holl", - "movement_cluster": "core", - "movement_label": "Core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-tuck-knie-an-be15e1c436", - "name": "Tuck (Knie an)", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-tuck-knie-an-5f62fcb360", - "name": "Tuck (Knie an)", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-tuck-laenger-b05dd43bd8", - "name": "Tuck, länger", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-tuck-laenger-c70e4ba401", - "name": "Tuck, länger", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-gestreckt-39bccf6e6b", - "name": "halb gestreckt", - "factor": 0.82, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-gestreckt-9147433141", - "name": "halb gestreckt", - "factor": 0.82, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-voll-13f8ebb8c0", - "name": "halb/voll", - "factor": 0.82, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-voll-cde344cba9", - "name": "halb/voll", - "factor": 0.82, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-side-plank-wechsel-309b0bc82c": { - "id": "movement-side-plank-wechsel-309b0bc82c", - "name": "Side Plank (Wechsel)", - "progression_id": "sp", - "movement_cluster": "core", - "movement_label": "Core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-auf-knien-3ed08acc27", - "name": "auf Knien", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-auf-knien-000b6c864b", - "name": "auf Knien", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-auf-knien-laenger-d1c3028780", - "name": "auf Knien, länger", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-auf-knien-laenger-227ec9b5f7", - "name": "auf Knien, länger", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-voll-6702e0693f", - "name": "voll", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-voll-727fcc161a", - "name": "voll", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-voll-laenger-8c287bb42a", - "name": "voll, länger", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-voll-laenger-ef549f755a", - "name": "voll, länger", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-bicycle-crunch-15c87e28c5": { - "id": "movement-bicycle-crunch-15c87e28c5", - "name": "Bicycle Crunch", - "progression_id": "bic", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-langsam-b16ce156f7", - "name": "langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-3bc865247d", - "name": "langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-9b37c0536d", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-6a0ed59bbd", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-1a29fed611", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-60b7d1ac93", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-85e9262405", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-2b4933822c", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-russian-twist-kb-d6e382fb3c": { - "id": "movement-russian-twist-kb-d6e382fb3c", - "name": "Russian Twist (KB)", - "progression_id": "tw", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-ohne-gewicht-3bd115d2ec", - "name": "ohne Gewicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-ohne-gewicht-478be9efcd", - "name": "ohne Gewicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-ohne-gewicht-32fc20afc1", - "name": "ohne Gewicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-ohne-gewicht-fbe33d8a04", - "name": "ohne Gewicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-kb-leicht-cf37c395cd", - "name": "KB leicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-kb-leicht-5cca613a86", - "name": "KB leicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-kb-f50c52e3e3", - "name": "KB", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-kb-eb15afb22c", - "name": "KB", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-kb-floor-press-060586ebd6": { - "id": "movement-kb-floor-press-060586ebd6", - "name": "KB Floor Press", - "progression_id": "press", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-floor-press-de744d6e05", - "name": "leicht (Floor Press)", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-floor-press-5987ddc1d2", - "name": "leicht (Floor Press)", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-floor-press-mehr-3b454ea446", - "name": "Floor Press mehr", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-floor-press-mehr-b34710868b", - "name": "Floor Press mehr", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-overhead-leicht-7f9274fd29", - "name": "Overhead leicht", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-overhead-leicht-3413a54a3a", - "name": "Overhead leicht", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-overhead-mittel-4b236720a7", - "name": "Overhead mittel", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-overhead-mittel-ea724f9a42", - "name": "Overhead mittel", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-y-t-w-superman-1d331ef219": { - "id": "movement-y-t-w-superman-1d331ef219", - "name": "Y-T-W / Superman", - "progression_id": "sup", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-superman-hold-06efa2683a", - "name": "Superman Hold", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-34af38f543", - "name": "Superman Hold", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-laenger-b383f0e540", - "name": "Superman Hold länger", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-laenger-6b326b0a11", - "name": "Superman Hold länger", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-reverse-snow-angels-871916bcee", - "name": "Reverse Snow Angels", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-reverse-snow-angels-78c24f8835", - "name": "Reverse Snow Angels", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-y-t-w-raises-b7c40c3579", - "name": "Y-T-W Raises", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-y-t-w-raises-a6d76cb15c", - "name": "Y-T-W Raises", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-kniebeuge-leicht-4ae3fb9fc0": { - "id": "movement-kniebeuge-leicht-4ae3fb9fc0", - "name": "Kniebeuge (leicht)", - "progression_id": "name:kniebeuge-leicht", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-liegestuetze-1-stufe-leichter-6033b9cb92": { - "id": "movement-liegestuetze-1-stufe-leichter-6033b9cb92", - "name": "Liegestütze (1 Stufe leichter)", - "progression_id": "name:liegestuetze-1-stufe-leichter", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-rudern-leicht-566f8de8f2": { - "id": "movement-rudern-leicht-566f8de8f2", - "name": "Rudern (leicht)", - "progression_id": "name:rudern-leicht", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-mountain-climbers-locker-e4a7854ab9": { - "id": "movement-mountain-climbers-locker-e4a7854ab9", - "name": "Mountain Climbers (locker)", - "progression_id": "name:mountain-climbers-locker", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-inchworm-50016ff2a4": { - "id": "movement-inchworm-50016ff2a4", - "name": "Inchworm", - "progression_id": "name:inchworm", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-cat-cow-d8e060c65f": { - "id": "movement-cat-cow-d8e060c65f", - "name": "Cat-Cow", - "progression_id": "name:cat-cow", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-bird-dog-f2c9d49c80": { - "id": "movement-bird-dog-f2c9d49c80", - "name": "Bird Dog", - "progression_id": "name:bird-dog", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-kb-halo-leicht-e3c156d79e": { - "id": "movement-kb-halo-leicht-e3c156d79e", - "name": "KB-Halo (leicht)", - "progression_id": "name:kb-halo-leicht", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [] - }, - "movement-kb-swing-zweiarmig-327d7b5ad6": { - "id": "movement-kb-swing-zweiarmig-327d7b5ad6", - "name": "KB Swing (zweiarmig)", - "progression_id": "swing", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-dead-stop-leicht-technik-02548f1a05", - "name": "Dead-Stop, leicht (Technik)", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-dead-stop-leicht-technik-3b9715b104", - "name": "Dead-Stop, leicht (Technik)", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-dead-stop-mehr-reps-7be430a756", - "name": "Dead-Stop, mehr Reps", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-dead-stop-mehr-reps-5c68075818", - "name": "Dead-Stop, mehr Reps", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-durchgehend-mittel-183cc7dfa6", - "name": "durchgehend, mittel", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-durchgehend-mittel-7762c8f240", - "name": "durchgehend, mittel", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-durchgehend-schwerer-6da670bed8", - "name": "durchgehend, schwerer", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-durchgehend-schwerer-a1609b5bad", - "name": "durchgehend, schwerer", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-goblet-squat-kb-f065dfd5b6": { - "id": "movement-goblet-squat-kb-f065dfd5b6", - "name": "Goblet Squat (KB)", - "progression_id": "gobletkb", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-tiefe-lernen-77201f2b6b", - "name": "leicht, Tiefe lernen", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-tiefe-lernen-70ea0f2e9b", - "name": "leicht, Tiefe lernen", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-57415830b2", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-abf827246a", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-2d542b9c09", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-eda93ad7ea", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-3s-runter-9dd7fec573", - "name": "mittel, 3s runter", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-3s-runter-3f76b2391b", - "name": "mittel, 3s runter", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-kb-deadlift-beidbeinig-6f7c8bca76": { - "id": "movement-kb-deadlift-beidbeinig-6f7c8bca76", - "name": "KB-Deadlift (beidbeinig)", - "progression_id": "kbdl", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-technik-54ca22fe3d", - "name": "leicht, Technik", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-technik-fcd2dc3979", - "name": "leicht, Technik", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-db29d97bd7", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-fd7e87b98b", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-7ed7284a10", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-e9887f19f9", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-schwerer-kontrolliert-dd0f4f4538", - "name": "schwerer, kontrolliert", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-schwerer-kontrolliert-ab6af6caea", - "name": "schwerer, kontrolliert", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-halo-faca9a5cc9": { - "id": "movement-halo-faca9a5cc9", - "name": "Halo", - "progression_id": "halo", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-langsam-5d15ba8134", - "name": "leicht, langsam", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-langsam-c2a5c59e62", - "name": "leicht, langsam", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-81a736180a", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-fe775a4a23", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-c5c0bf8989", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-3898a7cea7", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-langsam-c9db464d00", - "name": "mittel, langsam", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-langsam-e1a2e46fd3", - "name": "mittel, langsam", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-einarmiges-rudern-kb-0ec1c11b35": { - "id": "movement-einarmiges-rudern-kb-0ec1c11b35", - "name": "Einarmiges Rudern (KB)", - "progression_id": "row1", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-db8c75b358", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-d1e4feaafa", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-etwas-mehr-19ded398d6", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-etwas-mehr-0ce270eb41", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-einarmig-leicht-a6f388b349", - "name": "einarmig leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-einarmig-leicht-a9539fbadb", - "name": "einarmig leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-einarmig-mittel-74f0fe3e1e", - "name": "einarmig mittel", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-einarmig-mittel-3f961280f6", - "name": "einarmig mittel", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-bottoms-up-kb-hold-griff-4a5f657f41": { - "id": "movement-bottoms-up-kb-hold-griff-4a5f657f41", - "name": "Bottoms-up KB-Hold (Griff)", - "progression_id": "carry", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-im-stand-10-15s-e90dd57ff1", - "name": "im Stand 10–15s", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-im-stand-10-15s-db8eec9df0", - "name": "im Stand 10–15s", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-15-20s-ruhig-321e582ab2", - "name": "15–20s, ruhig", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-15-20s-ruhig-7db6eb1a01", - "name": "15–20s, ruhig", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-bis-schulterhoehe-15s-f1763ea9e5", - "name": "bis Schulterhöhe 15s", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-bis-schulterhoehe-15s-21c8fd4d01", - "name": "bis Schulterhöhe 15s", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-laenger-ein-paar-schritte-fa5838da65", - "name": "länger / ein paar Schritte", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-laenger-ein-paar-schritte-ec93f04473", - "name": "länger / ein paar Schritte", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-around-the-world-uppercut-kb-7ea726bbde": { - "id": "movement-around-the-world-uppercut-kb-7ea726bbde", - "name": "Around-the-World + Uppercut (KB)", - "progression_id": "atw", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-nur-around-the-world-leicht-97c90b17c5", - "name": "nur Around-the-World, leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-nur-around-the-world-leicht-fd2fab13cb", - "name": "nur Around-the-World, leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-around-the-world-mehr-reps-7af159bd81", - "name": "Around-the-World, mehr Reps", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-around-the-world-mehr-reps-43f1762f06", - "name": "Around-the-World, mehr Reps", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-uppercut-leicht-cb2ce4fe29", - "name": "+ Uppercut, leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-uppercut-leicht-db86827e21", - "name": "+ Uppercut, leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-uppercut-fluessig-4b3453d5f9", - "name": "+ Uppercut, flüssig", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-uppercut-fluessig-2463ac3c0e", - "name": "+ Uppercut, flüssig", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - } - } - }, - "draft": { - "colors": { - "ACC2": "#0F766E", - "ACCENT": "#C2410C", - "DARK": "#23201C", - "GOALBG": "#FBF1E9", - "GRID": "#CFC9BF", - "INK": "#1A1A1A", - "KBBG": "#F1ECFB", - "KBCOL": "#6D28D9", - "LIGHT": "#F4F1EC", - "LINK": "#1565C0", - "MUTE": "#6B6256", - "WARMBG": "#EAF1F0" - }, - "days": [ - { - "focus": "Drücken + Beine", - "num": 1, - "rotations": [ - { - "exercises": [ - { - "cue": "Brust kontrolliert senken & drücken", - "key": "ls", - "name": "Liegestütze", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "ls", - "id": "exercise-item-b5c3e6155f", - "exercise_id": "movement-liegestuetze-57eacf74ee", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "legacy_id": "d1-r0-e0" - }, - { - "cue": "Po vor der Couch senken, hochdrücken", - "key": "dips", - "name": "Bank-Dips (Couch)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "dips", - "id": "exercise-1-ada8cf48d3", - "exercise_id": "movement-bank-dips-couch-ad6c829e1b", - "movement_cluster": "vertical_push", - "movement_label": "Dip", - "legacy_id": "d1-r0-e1" - }, - { - "cue": "umgek. V, Kopf senken (Schultern)", - "key": "pike", - "name": "Pike", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "pike", - "id": "exercise-2-1cb8b9bbd7", - "exercise_id": "movement-pike-17f7716e00", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "legacy_id": "d1-r0-e2" - }, - { - "cue": "Arme seitlich bis Schulterhöhe, kontrolliert", - "key": "lraise", - "name": "Seitheben (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "lraise", - "id": "exercise-3-2908dafdf9", - "exercise_id": "movement-seitheben-kb-rucksack-91de5dc2f5", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d1-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-7bd8374b98" - }, - { - "exercises": [ - { - "cue": "Last umarmen, tief", - "key": "gobletkb", - "name": "Goblet Squat (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "gobletkb", - "id": "exercise-item-7816b10b95", - "exercise_id": "movement-goblet-squat-kb-rucksack-6ff9f90901", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d1-r1-e0" - }, - { - "cue": "großer Schritt, Knie Richtung Boden", - "key": "lunge", - "name": "Ausfallschritt rückw.", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "lunge", - "id": "exercise-1-232e3c9065", - "exercise_id": "movement-ausfallschritt-rueckw-2a4d06b799", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d1-r1-e1" - }, - { - "cue": "Hüfte hochdrücken, Po fest", - "key": "bridge", - "name": "Glute Bridge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "bridge", - "id": "exercise-2-0a8da65f1b", - "exercise_id": "movement-glute-bridge-1c0a7bf699", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d1-r1-e2" - }, - { - "cue": "auf Zehenspitzen hoch/runter", - "key": "calf", - "name": "Wadenheben", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "calf", - "id": "exercise-3-bfc4e8f9c3", - "exercise_id": "movement-wadenheben-0c7839551b", - "movement_cluster": "calf", - "movement_label": "Waden", - "legacy_id": "d1-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-b9e0a052af" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-druecken-beine-9f04ffe00b" - }, - { - "focus": "Ziehen + Core", - "num": 2, - "rotations": [ - { - "exercises": [ - { - "cue": "1 Arm, Ellbogen an den Rippen, Zug zur Hüfte · Lat · DG1 links / DG2 rechts", - "key": "rowe", - "name": "Einarm-Rudern ENG (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "rowe", - "id": "exercise-item-8b823b51cb", - "exercise_id": "movement-einarm-rudern-eng-kb-4907fbb5c7", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d2-r0-e0" - }, - { - "cue": "Oberarme fix, zu den Schultern", - "key": "load", - "name": "Bizeps-Curls (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "load", - "id": "exercise-1-1a66ac153c", - "exercise_id": "movement-bizeps-curls-kb-rucksack-67944f00d5", - "movement_cluster": "elbow_flexion", - "movement_label": "Bizeps", - "legacy_id": "d2-r0-e1" - }, - { - "cue": "1 Arm, Ellbogen ~45° auf, Zug zum Brustkorb · oberer Rücken · DG1 links / DG2 rechts", - "key": "row2", - "name": "Einarm-Rudern BREIT (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "row2", - "id": "exercise-2-ab58589731", - "exercise_id": "movement-einarm-rudern-breit-kb-0146abcfbb", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d2-r0-e2" - }, - { - "cue": "bäuchlings Arme+Beine heben", - "key": "sup", - "name": "Superman / Y-T-W", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "sup", - "id": "exercise-3-8974cdd4a3", - "exercise_id": "movement-superman-y-t-w-279f03a544", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d2-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-024cf0041d" - }, - { - "exercises": [ - { - "cue": "gerade Linie halten", - "key": "plank", - "name": "Plank", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "plank", - "id": "exercise-item-40e37731b0", - "exercise_id": "movement-plank-c8449f2937", - "movement_cluster": "core", - "movement_label": "Core", - "legacy_id": "d2-r1-e0" - }, - { - "cue": "Knie zügig zur Brust", - "key": "mtn", - "name": "Mountain Climbers", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "mtn", - "id": "exercise-1-69ed72c9c3", - "exercise_id": "movement-mountain-climbers-79efdb62dd", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d2-r1-e1" - }, - { - "cue": "gegengleich, Rücken am Boden", - "key": "db", - "name": "Dead Bug", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "progression_id": "db", - "id": "exercise-2-0fc9fa4d38", - "exercise_id": "movement-dead-bug-21d4b207d5", - "movement_cluster": "core", - "movement_label": "Core", - "legacy_id": "d2-r1-e2" - }, - { - "cue": "Beine heben/senken", - "key": "lr", - "name": "Beinheben", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "lr", - "id": "exercise-3-1373648dc9", - "exercise_id": "movement-beinheben-faf19a629b", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d2-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-f767e75f57" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-ziehen-core-26dd9ab93d" - }, - { - "focus": "Beine + Core", - "num": 3, - "rotations": [ - { - "exercises": [ - { - "cue": "Last umarmen, tief", - "key": "gobletkb", - "name": "Goblet Squat (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "gobletkb", - "id": "exercise-item-0264c1e44a", - "exercise_id": "movement-goblet-squat-kb-rucksack-6ff9f90901", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d3-r0-e0" - }, - { - "cue": "Hüfte zurück, Rücken gerade, Hamstrings", - "key": "load", - "name": "RDL / Hip Hinge (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "load", - "id": "exercise-1-9d276268ca", - "exercise_id": "movement-rdl-hip-hinge-kb-rucksack-9ada5a67c3", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d3-r0-e1" - }, - { - "cue": "ein Bein gestreckt", - "key": "bridge", - "name": "Glute Bridge 1-beinig", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "bridge", - "id": "exercise-2-4b6e8a5bc6", - "exercise_id": "movement-glute-bridge-1-beinig-ccc8c9fb53", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d3-r0-e2" - }, - { - "cue": "Rücken an die Wand, halten", - "key": "wall", - "name": "Wall Sit", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "wall", - "id": "exercise-3-add8004d7e", - "exercise_id": "movement-wall-sit-6b5f695f6a", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d3-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-de121c91a0" - }, - { - "exercises": [ - { - "cue": "unterer Rücken am Boden", - "key": "holl", - "name": "Hollow Hold", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "holl", - "id": "exercise-item-072e8d32be", - "exercise_id": "movement-hollow-hold-54c452f77b", - "movement_cluster": "core", - "movement_label": "Core", - "legacy_id": "d3-r1-e0" - }, - { - "cue": "seitlich auf einem Unterarm", - "key": "sp", - "name": "Side Plank (Wechsel)", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "sp", - "id": "exercise-1-cedec1156e", - "exercise_id": "movement-side-plank-wechsel-309b0bc82c", - "movement_cluster": "core", - "movement_label": "Core", - "legacy_id": "d3-r1-e1" - }, - { - "cue": "Ellbogen zum Gegenknie", - "key": "bic", - "name": "Bicycle Crunch", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "progression_id": "bic", - "id": "exercise-2-fe1a6ad3ca", - "exercise_id": "movement-bicycle-crunch-15c87e28c5", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d3-r1-e2" - }, - { - "cue": "sitzend Rumpf drehen", - "key": "tw", - "name": "Russian Twist (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "same" - }, - "progression_id": "tw", - "id": "exercise-3-bd5b54584a", - "exercise_id": "movement-russian-twist-kb-d6e382fb3c", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d3-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-017be520c7" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-beine-core-13963d0eea" - }, - { - "focus": "Oberkörper: Drücken + Ziehen", - "num": 4, - "rotations": [ - { - "exercises": [ - { - "cue": "Brust kontrolliert senken & drücken", - "key": "ls", - "name": "Liegestütze", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "ls", - "id": "exercise-item-3cc877a2d9", - "exercise_id": "movement-liegestuetze-57eacf74ee", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "legacy_id": "d4-r0-e0" - }, - { - "cue": "Po vor der Couch senken", - "key": "dips", - "name": "Bank-Dips (Couch)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "dips", - "id": "exercise-1-57f3db08a5", - "exercise_id": "movement-bank-dips-couch-ad6c829e1b", - "movement_cluster": "vertical_push", - "movement_label": "Dip", - "legacy_id": "d4-r0-e1" - }, - { - "cue": "umgek. V, Kopf senken", - "key": "pike", - "name": "Pike", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "pike", - "id": "exercise-2-f8e46033f7", - "exercise_id": "movement-pike-17f7716e00", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "legacy_id": "d4-r0-e2" - }, - { - "cue": "Glocke vom Boden weg drücken", - "key": "press", - "name": "KB Floor Press", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "press", - "id": "exercise-3-1bfa657543", - "exercise_id": "movement-kb-floor-press-060586ebd6", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "legacy_id": "d4-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-761392c073" - }, - { - "exercises": [ - { - "cue": "1 Arm, Ellbogen an den Rippen, Zug zur Hüfte · Lat · DG1 links / DG2 rechts", - "key": "rowe", - "name": "Einarm-Rudern ENG (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "rowe", - "id": "exercise-item-3eecbcd916", - "exercise_id": "movement-einarm-rudern-eng-kb-4907fbb5c7", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d4-r1-e0" - }, - { - "cue": "Oberarme fix, zu den Schultern", - "key": "load", - "name": "Bizeps-Curls (KB/Rucksack)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "load", - "id": "exercise-1-5503931f9a", - "exercise_id": "movement-bizeps-curls-kb-rucksack-67944f00d5", - "movement_cluster": "elbow_flexion", - "movement_label": "Bizeps", - "legacy_id": "d4-r1-e1" - }, - { - "cue": "1 Arm, Ellbogen ~45° auf, Zug zum Brustkorb · oberer Rücken · DG1 links / DG2 rechts", - "key": "row2", - "name": "Einarm-Rudern BREIT (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "row2", - "id": "exercise-2-7d45b03b10", - "exercise_id": "movement-einarm-rudern-breit-kb-0146abcfbb", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d4-r1-e2" - }, - { - "cue": "bäuchlings, Rücken", - "key": "sup", - "name": "Y-T-W / Superman", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "sup", - "id": "exercise-3-ffc825e874", - "exercise_id": "movement-y-t-w-superman-1d331ef219", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d4-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-62363a4e87" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-oberkoerper-druecken-ziehen-15d0e7f677" - }, - { - "focus": "Ganzkörper LEICHT + Mobility", - "light": true, - "num": 5, - "rotations": [ - { - "exercises": [ - { - "cue": "locker, sauber", - "key": "name:kniebeuge-leicht", - "name": "Kniebeuge (leicht)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:kniebeuge-leicht", - "id": "exercise-item-4b0e2395e9", - "exercise_id": "movement-kniebeuge-leicht-4ae3fb9fc0", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d5-r0-e0" - }, - { - "cue": "leichtere Variante", - "key": "name:liegestuetze-1-stufe-leichter", - "name": "Liegestütze (1 Stufe leichter)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:liegestuetze-1-stufe-leichter", - "id": "exercise-1-9303fa85bc", - "exercise_id": "movement-liegestuetze-1-stufe-leichter-6033b9cb92", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "legacy_id": "d5-r0-e1" - }, - { - "cue": "wenig Gewicht", - "key": "name:rudern-leicht", - "name": "Rudern (leicht)", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:rudern-leicht", - "id": "exercise-2-5224ba2082", - "exercise_id": "movement-rudern-leicht-566f8de8f2", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d5-r0-e2" - }, - { - "cue": "ruhiges Tempo", - "key": "name:mountain-climbers-locker", - "name": "Mountain Climbers (locker)", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:mountain-climbers-locker", - "id": "exercise-3-3f1a039ad3", - "exercise_id": "movement-mountain-climbers-locker-e4a7854ab9", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d5-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-1750a46419" - }, - { - "exercises": [ - { - "cue": "in den Stütz wandern, zurück", - "key": "name:inchworm", - "name": "Inchworm", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:inchworm", - "id": "exercise-item-03be6e8387", - "exercise_id": "movement-inchworm-50016ff2a4", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d5-r1-e0" - }, - { - "cue": "Rücken ründen/strecken", - "key": "name:cat-cow", - "name": "Cat-Cow", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "name:cat-cow", - "id": "exercise-1-92862f7d6a", - "exercise_id": "movement-cat-cow-d8e060c65f", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d5-r1-e1" - }, - { - "cue": "gegengleich Arm+Bein, kurz halten", - "key": "name:bird-dog", - "name": "Bird Dog", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "progression_id": "name:bird-dog", - "id": "exercise-2-46d9abeab7", - "exercise_id": "movement-bird-dog-f2c9d49c80", - "movement_cluster": "general", - "movement_label": "Allgemein", - "legacy_id": "d5-r1-e2" - }, - { - "cue": "langsam um den Kopf kreisen", - "key": "name:kb-halo-leicht", - "name": "KB-Halo (leicht)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "name:kb-halo-leicht", - "id": "exercise-3-e6cefad215", - "exercise_id": "movement-kb-halo-leicht-e3c156d79e", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "legacy_id": "d5-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-dde0c1adc1" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-ganzkoerper-leicht-mobility-ff8134fa48" - }, - { - "color": "#6D28D9", - "focus": "KETTLEBELL Ganzkörper", - "note": "Reine Kettlebell-Session. Swing = Hüft-Schnapp, kein Armheben. Glocke so wählen, dass alle Reps sauber sind.", - "note_bg": "#F1ECFB", - "num": 6, - "rotations": [ - { - "exercises": [ - { - "cue": "Hüft-Schnapp, kein Armheben", - "key": "swing", - "name": "KB Swing (zweiarmig)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "swing", - "id": "exercise-item-2fbf3bf70d", - "exercise_id": "movement-kb-swing-zweiarmig-327d7b5ad6", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d6-r0-e0" - }, - { - "cue": "Glocke vor der Brust, tief", - "key": "gobletkb", - "name": "Goblet Squat (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "gobletkb", - "id": "exercise-1-8542eb2e51", - "exercise_id": "movement-goblet-squat-kb-f065dfd5b6", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "legacy_id": "d6-r0-e1" - }, - { - "cue": "Glocke zwischen den Füßen, Hüfte hinge", - "key": "kbdl", - "name": "KB-Deadlift (beidbeinig)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "kbdl", - "id": "exercise-2-f072cf1841", - "exercise_id": "movement-kb-deadlift-beidbeinig-6f7c8bca76", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "legacy_id": "d6-r0-e2" - }, - { - "cue": "eng um den Kopf kreisen", - "key": "halo", - "name": "Halo", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "halo", - "id": "exercise-3-031200e160", - "exercise_id": "movement-halo-faca9a5cc9", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "legacy_id": "d6-r0-e3" - } - ], - "label": "ROTATION A — 3 Sätze", - "id": "rotation-rotation-a-3-saetze-1165003f5a" - }, - { - "exercises": [ - { - "cue": "Glocke vom Boden weg drücken", - "key": "press", - "name": "KB Floor Press", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "progression_id": "press", - "id": "exercise-item-04a3fceec5", - "exercise_id": "movement-kb-floor-press-060586ebd6", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "legacy_id": "d6-r1-e0" - }, - { - "cue": "Rücken gerade, zum Bauch", - "key": "row1", - "name": "Einarmiges Rudern (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "row1", - "id": "exercise-1-01087258d2", - "exercise_id": "movement-einarmiges-rudern-kb-0ec1c11b35", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "legacy_id": "d6-r1-e1" - }, - { - "cue": "Glocke Boden nach oben, Unterarm senkrecht, still halten", - "key": "carry", - "name": "Bottoms-up KB-Hold (Griff)", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "carry", - "id": "exercise-2-0a835e4b76", - "exercise_id": "movement-bottoms-up-kb-hold-griff-4a5f657f41", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "legacy_id": "d6-r1-e2" - }, - { - "cue": "Glocke um den Körper, dann diagonal zur Gegen-Schulter, andere Hand fängt", - "key": "atw", - "name": "Around-the-World + Uppercut (KB)", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "progression_id": "atw", - "id": "exercise-3-6386738afd", - "exercise_id": "movement-around-the-world-uppercut-kb-7ea726bbde", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "legacy_id": "d6-r1-e3" - } - ], - "label": "ROTATION B — 3 Sätze", - "id": "rotation-rotation-b-3-saetze-c851db6efb" - } - ], - "badge": { - "text": "", - "color": "#6D28D9" - }, - "id": "day-kettlebell-ganzkoerper-e53128a814" - } - ], - "front": { - "baseline_head": "Standortbestimmung & Tracking", - "baseline_intro": "Mach die Tests vor Tag 1 und in Woche 8 identisch nochmal – dein Vorher/Nachher.", - "baseline_rows": [ - [ - "Test", - "Tag 0", - "Woche 8" - ], - [ - "Liegestütze (max, saubere Reps)", - "", - "" - ], - [ - "Bank-Dips an der Couch (max)", - "", - "" - ], - [ - "Kniebeugen in 60 Sekunden", - "", - "" - ], - [ - "Ausfallschritte (max gesamt)", - "", - "" - ], - [ - "Plank (max Sekunden)", - "", - "" - ], - [ - "Wall Sit (max Sekunden)", - "", - "" - ], - [ - "KB Farmer-Hold (max Sekunden, Griff)", - "", - "" - ], - [ - "Körpergewicht (kg)", - "95", - "" - ] - ], - "flow_rows": [ - [ - "Teil", - "Inhalt", - "Zeit" - ], - [ - "Warm-up", - "Gelenk-Prep (dynamisch)", - "~3 Min" - ], - [ - "Rotation A", - "4 Übungen, 3 Sätze (kurze Pausen)", - "~14 Min" - ], - [ - "Rotation B", - "4 Übungen, 3 Sätze (kurze Pausen)", - "~14 Min" - ], - [ - "Cool-down", - "Ausklingen + Dehnen", - "~6 Min" - ], - [ - "", - "Gesamt", - "~35 Min" - ] - ], - "goals_box": "Du startest bei 2–3 Liegestützen und 0 Dips. Realistisch und ehrlich nach 8 Wochen sauberer Arbeit:
•  Liegestütze: von 2–3 auf erste saubere volle Reps (Rest in Knie-/Negativ-Form).
•  Bank-Dips: saubere Reps mit Füßen etwas weiter weg.   Pike Push-up: am Boden mit kleinem ROM.
•  Plank ~45–60 s, Wall Sit ~60 s, viele saubere Goblet-Kniebeugen, sauberer Swing + spürbar bessere Griffkraft.
•  Noch NICHT dran: Diamond Push-ups, L-Sit, Handstand, schwerer Turkish Get-Up, Snatch – Stoff für Woche 12+.", - "goals_head": "Realistische Ziele nach 8 Wochen", - "goals_note": "Wichtig: Die Wochen-Stufen sind ein Rahmen, kein Muss. Bleib auf einer Stufe, solange sie fordert. Aufstiegs-Regel: erst hoch (oder schwerere Glocke), wenn du eine Übung 2 Sessions in Folge in allen Sätzen sauber über die vollen 20s schaffst. KB = Kettlebell bevorzugt; Rucksack bleibt als Alternative.", - "kb_box": "Du trainierst für Calisthenics, nicht für Maximalkraft. Genau da liegen die belegten Stärken der Kettlebell – ohne dass dich ihr dokumentierter Schwachpunkt (Maximalkraft/Hypertrophie wegen fixer Gewichtssprünge) trifft:
•  Griffkraft (Studie BELL: +6–7 kg) – dein größter Mehrwert auf dem Weg zu Klimmzug & Dip.
•  Power / ballistische Hüftstreckung (Swing) – explosive Basis für spätere Skills.
•  Kondition & Körperfett – wirksam belegt; du startest mit wenig Ausdauer.
•  Ganzkörper, ein Gerät, zeiteffizient – passt ideal ins 30-Min-Tabata-Format; niedriges Einstiegs-Verletzungsrisiko bei sauberer Technik.
Ehrlich: Reines KB ersetzt kein Maximalkraft-/Muskelaufbau-Programm. Für dein Ziel (Basis-Fundament) liegt das ohnehin unter der relevanten Schwelle.", - "kb_head": "Warum Kettlebell – und warum es zu deinem Ziel passt", - "lib_head": "Übungs-Bibliothek (mit Anleitungen)", - "lib_intro": "Für Übungen, die du noch nicht kennst, ist ein Link zu einer bebilderten Anleitung dabei. Lila Gruppe = Kettlebell-Übungen.", - "reminder_head": "Kurz-Reminder", - "reminders": [ - "Form vor Reps. Wähle die Stufe / Glocke, bei der du die 20s kontrolliert durcharbeitest – mit 1–2 Reps Reserve.", - "Swing-Technik first. Der Swing ist ein Hüft-Schnapp, KEIN Kniebeuge und KEIN Armheben. Erst Dead-Stop-Swings, bis die Hüftstreckung sitzt.", - "Einseitige Übungen (Tag 2): Durchgang 1 = links, Durchgang 2 = rechts – so ist jede Seite automatisch ausbalanciert.", - "Täglich ist viel. Tag 5 leicht, Tag 7 frei. Bei starkem Muskelkater einen Extra-Ruhetag – gehört zum Aufbau.", - "Schlaf & Eiweiß. 7–9 h Schlaf, ~1,6 g Eiweiß pro kg. Stechender Schmerz = stoppen." - ], - "session_how_body": "Tabata-Block = 4 Min: 10 Sek einzählen, dann 8× (20 Sek Arbeit / 10 Sek Pause). Pro Block rotieren 4 Übungen, je 2 Durchgänge = 8 Intervalle = 1 Block = 1 Satz. Eine Rotation läuft 3 Sätze (in Woche 1 ruhig mit 2 starten). Warm-up steht über, Cool-down unter den Übungen.", - "session_how_head": "So funktioniert eine Session", - "sheets_note": "Ab der nächsten Seite: 1 Druckblatt pro Session (Woche 1–8). Tag 6 = reine Kettlebell-Session. Warm-up steht über, Cool-down + Dehnen unter den Übungen. Bei Vorerkrankungen/Verletzungen vorher ärztlich abklären.", - "timer_note": "Timer: „Tabata / Interval Timer“ auf 10s Vorlauf, 8 Runden, 20/10." - }, - "library": [ - { - "color": "ACC2", - "group": "DRÜCKEN (Brust / Schultern / Trizeps)", - "items": [ - { - "desc": "Stütz, Körper gerade, Brust kontrolliert senken & drücken. Stufen: Inkline (Couch), Knie, Negativ, Voll.", - "name": "Liegestütze", - "url": "https://blog.calimove.com/2023/01/25/push-up-variations-beginner-expert/" - }, - { - "desc": "Rücken zur Couch, Hände auf der Kante, Po davor absenken (Ellbogen nach hinten), hochdrücken. Knie beugen = leichter.", - "name": "Bank-Dips (Couch)", - "url": "https://www.healthline.com/health/fitness-exercise/bench-dips" - }, - { - "desc": "Umgekehrtes V (Hintern hoch), Kopf Richtung Boden senken, hochdrücken. Schulter-Fokus. Hände erhöht = leichter.", - "name": "Pike Push-up / Hold", - "url": "https://www.hingehealth.com/resources/articles/pike-push-up/" - }, - { - "desc": "Arme seitlich bis Schulterhöhe heben, Ellbogen leicht gebeugt, kontrolliert senken. Seitlicher Deltamuskel.", - "name": "Seitheben (KB/Rucksack)", - "url": "" - } - ] - }, - { - "color": "ACC2", - "group": "ZIEHEN (Rücken / Bizeps – KB oder Rucksack + Boden)", - "items": [ - { - "desc": "Hüfte beugen, Rücken GERADE, abgestützt. Ellbogen an den Rippen, Zug zur Hüfte – Lat.", - "name": "Einarm-Rudern ENG", - "url": "https://www.puregym.com/exercises/back/rows/dumbbell-bent-over-row/" - }, - { - "desc": "Gleiche Position, aber Ellbogen ~45° geöffnet, Zug zum Brustkorb – oberer Rücken / hinterer Delta.", - "name": "Einarm-Rudern BREIT", - "url": "https://www.puregym.com/exercises/back/rows/dumbbell-bent-over-row/" - }, - { - "desc": "Last unten greifen, Oberarme fix am Körper, kontrolliert zu den Schultern curlen.", - "name": "Bizeps-Curls (KB/Rucksack)", - "url": "" - }, - { - "desc": "Bäuchlings Arme+Beine heben (Superman). Snow Angels = Arme am Boden auf/ab. Rücken ohne Stange.", - "name": "Superman / Reverse Snow Angels / Y-T-W", - "url": "https://www.calixpert.com/exercises/hollow-body-to-superman-hold" - } - ] - }, - { - "color": "ACC2", - "group": "BEINE", - "items": [ - { - "desc": "Hüfte zurück, Oberschenkel parallel, Fersen am Boden. Goblet = Glocke/Rucksack vor der Brust umarmen.", - "name": "Kniebeuge / Goblet Squat (KB/Rucksack)", - "url": "" - }, - { - "desc": "Großer Schritt (rückwärts = knieschonend), hinteres Knie Richtung Boden. Anfangs ggf. festhalten.", - "name": "Ausfallschritte", - "url": "" - }, - { - "desc": "Knie leicht gebeugt, Hüfte nach hinten, Rücken gerade, Last entlang der Beine senken bis Dehnung hinten (Hamstrings).", - "name": "RDL / Hip Hinge (KB/Rucksack)", - "url": "" - }, - { - "desc": "Bridge: Hüfte hochdrücken (1-beinig steigern). Waden: Zehenspitzen hoch/runter. Wall Sit: Rücken an die Wand, halten.", - "name": "Glute Bridge / Wadenheben / Wall Sit", - "url": "" - } - ] - }, - { - "color": "KBCOL", - "group": "KETTLEBELL (Power / Griff / Ganzkörper)", - "items": [ - { - "desc": "Wichtigste KB-Übung. Hüfthinge, Glocke aus der Hüfte explosiv auf Brusthöhe schnappen (kein Armheben), oben Po+Bauch fest. Dead-Stop zum Lernen.", - "name": "Swing (zweiarmig, Hardstyle)", - "url": "https://www.onepeloton.com/blog/kettlebell-swing/" - }, - { - "desc": "Glocke an den Hörnern vor der Brust umarmen, tief und aufrecht hocken. Beste Einsteiger-KB-Kniebeuge.", - "name": "Goblet Squat (KB)", - "url": "https://www.onepeloton.com/blog/goblet-squat/" - }, - { - "desc": "Glocke zwischen den Füßen, Hüfte hinge, Rücken gerade, vom Boden heben und Hüfte durchstrecken. Stabile Swing-Vorbereitung.", - "name": "KB-Deadlift (beidbeinig)", - "url": "https://www.onepeloton.com/blog/kettlebell-deadlift/" - }, - { - "desc": "Rücklings am Boden, Glocke vom Boden weg nach oben drücken. Schulterschonend, gut für Anfänger.", - "name": "KB Floor Press", - "url": "https://www.masterclass.com/articles/kettlebell-floor-press-guide" - }, - { - "desc": "Glocke (oder Rucksack) vor der Brust, langsam eng um den Kopf kreisen. Schulter-Mobility + Core.", - "name": "Halo", - "url": "https://www.tomsguide.com/how-to/kettlebell-halo-exercise-how-to-do-it-and-the-benefits-for-your-core-and-shoulder-muscles" - }, - { - "desc": "Glocke am Griff, Boden nach oben, Unterarm senkrecht, still halten. Schwierigkeit kommt aus der Balance – fordert Griff + Handgelenk auch mit leichter Glocke.", - "name": "Bottoms-up KB-Hold / Carry", - "url": "https://www.onepeloton.com/blog/farmers-carry/" - }, - { - "desc": "Glocke kreist um den Körper (Around-the-World); danach diagonal nach oben zur gegenüberliegenden Schulter/Brust führen, die andere Hand fängt ab (wie ein Uppercut von unten). Rotatorischer Core + Koordination. Erst nur das Umkreisen lernen, Uppercut später.", - "name": "Around-the-World + Uppercut", - "url": "" - } - ] - }, - { - "color": "ACC2", - "group": "CORE", - "items": [ - { - "desc": "Unterarmstütz, Körper gerade. Anfangs auf den Knien. Side Plank seitlich auf einem Unterarm.", - "name": "Plank / Side Plank", - "url": "" - }, - { - "desc": "Auf dem Rücken, unterer Rücken am Boden, Arme+Beine leicht angehoben. Anfänger: angewinkelt (Tuck).", - "name": "Hollow Hold", - "url": "https://www.calixpert.com/exercises/hollow-body-to-superman-hold" - }, - { - "desc": "Auf dem Rücken, Arme+Knie 90°, gegengleich Arm+Bein langsam ausstrecken, unterer Rücken bleibt am Boden.", - "name": "Dead Bug", - "url": "https://dieringe.com/exercises/dead-bug" - }, - { - "desc": "Vierfüßler, gegengleich Arm+Bein ausstrecken, Rumpf stabil, kurz halten.", - "name": "Bird Dog", - "url": "https://www.hingehealth.com/resources/articles/bird-dog/" - }, - { - "desc": "Climbers: Knie zügig zur Brust. Bicycle: Ellbogen zum Gegenknie. Twist: sitzend drehen (KB). Beinheben: Knie gebeugt = leichter.", - "name": "Mountain Climbers / Bicycle / Russian Twist / Beinheben", - "url": "" - } - ] - }, - { - "color": "ACC2", - "group": "WARM-UP & MOBILITY", - "items": [ - { - "desc": "Aus dem Stand vorbeugen, Hände nach vorne in den Stütz wandern, dann zurück.", - "name": "Inchworm", - "url": "https://www.onepeloton.com/blog/inchworm-exercise" - }, - { - "desc": "Klassische dynamische Aufwärm-Bewegungen – locker, zum Warmwerden.", - "name": "Jacks / High Knees / Kreisen / Cat-Cow", - "url": "" - } - ] - } - ], - "meta": { - "days": 6, - "doc_title": "Calisthenics 8-Wochen Komplettplan (mit Kettlebell)", - "footer_left": "Calisthenics 8-Wochen Komplettplan · mit Kettlebell", - "output_name": "Calisthenics-8-Wochen-Komplett.pdf", - "subtitle": "Blutiger Anfänger · 1,90 m · 95 kg · Rucksack + Couch + Kettlebell · tägliche ~30-Min Tabata-Session", - "title": "Calisthenics-Basis – 8-Wochen Komplettplan", - "weeks": 8 - }, - "phases": { - "items": [ - { - "name": "Phase 1 · Fundament", - "params": "Fokus: Technik (v.a. Swing als Dead-Stop). 2–3 Reps Reserve. Pausen bis ~60s. Leichte Glocke. Woche 1 mit 2 Sätzen pro Rotation.", - "weeks": 1, - "id": "phase-phase-1-fundament-9d3418c792" - }, - { - "name": "Phase 2 · Fundament", - "params": "Fokus: Technik (v.a. Swing als Dead-Stop). 2–3 Reps Reserve. Pausen bis ~60s. Leichte Glocke. Woche 1 mit 2 Sätzen pro Rotation.", - "weeks": 1, - "id": "phase-phase-2-fundament-f815c541ec" - }, - { - "name": "Phase 3 · Aufbau", - "params": "Sauber bleiben, letzter Satz fordernd. Pausen ~45–60s. Etwas schwerere Glocke / mehr Reps. 2–3 Sätze.", - "weeks": 1, - "id": "phase-phase-3-aufbau-fa21a3469e" - }, - { - "name": "Phase 4 · Aufbau", - "params": "Sauber bleiben, letzter Satz fordernd. Pausen ~45–60s. Etwas schwerere Glocke / mehr Reps. 2–3 Sätze.", - "weeks": 1, - "id": "phase-phase-4-aufbau-98dc807ce2" - }, - { - "name": "Phase 5 · Steigerung", - "params": "Fordernd, aber 1 Rep Reserve. Pausen ~45s. Schwerere Glocke / einarmig wo angegeben. 3 Sätze.", - "weeks": 1, - "id": "phase-phase-5-steigerung-f023eda6a2" - }, - { - "name": "Phase 6 · Steigerung", - "params": "Fordernd, aber 1 Rep Reserve. Pausen ~45s. Schwerere Glocke / einarmig wo angegeben. 3 Sätze.", - "weeks": 1, - "id": "phase-phase-6-steigerung-3dc7eb5c32" - }, - { - "name": "Phase 7 · Festigen", - "params": "Fordernd & sauber (NICHT bis zum Muskelversagen). Pausen ~45s. Ende Woche 8 leichter (Deload). 3 Sätze.", - "weeks": 1, - "id": "phase-phase-7-festigen-d8284c58fc" - }, - { - "name": "Phase 8 · Festigen", - "params": "Fordernd & sauber (NICHT bis zum Muskelversagen). Pausen ~45s. Ende Woche 8 leichter (Deload). 3 Sätze.", - "weeks": 1, - "id": "phase-phase-8-festigen-bfaebd57b4" - } - ], - "param_light": "Bewusst locker – aktive Erholung. Saubere, leichte Wiederholungen, keine Belastung bis zum Limit." - }, - "prepost": { - "1": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Türrahmen (Brust/Schulter, je 30–40s) · Trizeps (je 30s) · Seitheben/Lat-Seitdehnung (je 30s) · Quad im Stand (je 30s) · Knieender Hüftbeuger (je 30–40s) · Wade an der Wand (je 30s)", - "warmup": "Handflächen-Wippen · Armkreisen/Wand-Slides · Hüftkreisen + Beinschwünge · Knie-über-Zeh an der Wand" - }, - "2": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Lat-/Seitdehnung (je 30s) · Türrahmen (je 30–40s) · BWS über die Couch (30–40s) · Cobra/Sphinx (Bauch, 30s) · Unterarm (je 20s) · Knieender Hüftbeuger (je 30s)", - "warmup": "Handflächen-Wippen · Handtuch-Dislocates (8–10×) · Cat-Cow · Hüftkreisen + Beinschwünge" - }, - "3": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Hamstring-Dehnung (Couch, je 30–40s) · Quad im Stand (je 30s) · Knieender Hüftbeuger (je 30–40s) · Figure-4/Taube (je 30–40s) · Cobra + liegende Wirbelsäulen-Rotation (je 30s) · Wade an der Wand (je 30s)", - "warmup": "Hüftkreisen + Beinschwünge · Knie-über-Zeh an der Wand · Cat-Cow · Handflächen-Wippen" - }, - "4": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Türrahmen (je 30–40s) · Trizeps (je 30s) · Lat-/Seitdehnung (je 30s) · BWS über die Couch (30–40s) · Unterarm (je 20s)", - "warmup": "Handflächen-Wippen · Handtuch-Dislocates (8–10×) · Armkreisen/Wand-Slides · Cat-Cow" - }, - "5": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Türrahmen (je 30s) · Knieender Hüftbeuger (je 30s) · BWS über die Couch (30–40s) · Figure-4/Taube (je 30s) · Wade an der Wand (je 30s)", - "warmup": "Cat-Cow · Hüftkreisen + Beinschwünge · Armkreisen/Wand-Slides · Knie-über-Zeh an der Wand" - }, - "6": { - "cooldown": "Ausschütteln + Gehen (60–90s) · Box-Atmung (4–5 Runden)", - "stretch": "Hamstring-Dehnung (Couch, je 30–40s) · Knieender Hüftbeuger (je 30–40s) · Figure-4/Taube (je 30–40s) · Unterarm (je 20s) · BWS über die Couch (30–40s) · Lat-/Seitdehnung (je 30s)", - "warmup": "Halo (leicht) · Hüftkreisen + Beinschwünge · Cat-Cow · Handflächen-Wippen" - } - }, - "stages": { - "atw": { - "name": "Around-the-World + Uppercut (KB)", - "steps": [ - { - "name": "nur Around-the-World, leicht", - "id": "step-nur-around-the-world-leicht-97c90b17c5", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "nur Around-the-World, leicht", - "id": "step-nur-around-the-world-leicht-fd2fab13cb", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "Around-the-World, mehr Reps", - "id": "step-around-the-world-mehr-reps-7af159bd81", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "Around-the-World, mehr Reps", - "id": "step-around-the-world-mehr-reps-43f1762f06", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "+ Uppercut, leicht", - "id": "step-uppercut-leicht-cb2ce4fe29", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "+ Uppercut, leicht", - "id": "step-uppercut-leicht-db86827e21", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "+ Uppercut, flüssig", - "id": "step-uppercut-fluessig-4b3453d5f9", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "+ Uppercut, flüssig", - "id": "step-uppercut-fluessig-2463ac3c0e", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - } - ], - "id": "progression-around-the-world-uppercut-kb-3cca10157e", - "key": "atw" - }, - "bic": { - "name": "Bicycle Crunch", - "steps": [ - { - "name": "langsam", - "id": "step-langsam-b16ce156f7", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "langsam", - "id": "step-langsam-3bc865247d", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-9b37c0536d", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-6a0ed59bbd", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-1a29fed611", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-60b7d1ac93", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-85e9262405", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-2b4933822c", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-bicycle-crunch-026823904f", - "key": "bic" - }, - "bridge": { - "name": "Glute Bridge", - "steps": [ - { - "name": "beidbeinig", - "id": "step-beidbeinig-36eb2634d6", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "beidbeinig", - "id": "step-beidbeinig-8a52d360c3", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "beidbeinig, 2s halten", - "id": "step-beidbeinig-2s-halten-1ad3dfd97b", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "beidbeinig, 2s halten", - "id": "step-beidbeinig-2s-halten-6087f8f6d1", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "einbeinig", - "id": "step-einbeinig-3a0c0abdfa", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "einbeinig", - "id": "step-einbeinig-43807e1138", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "einbeinig, 2s halten", - "id": "step-einbeinig-2s-halten-4e42109f20", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "einbeinig, 2s halten", - "id": "step-einbeinig-2s-halten-6335420028", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - } - ], - "id": "progression-glute-bridge-830b37f200", - "key": "bridge" - }, - "calf": { - "name": "Wadenheben", - "steps": [ - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-e8311d7913", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-95b36efc85", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "langsam", - "id": "step-langsam-9e41650da8", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "langsam", - "id": "step-langsam-74db877975", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "langsam, mehr Reps", - "id": "step-langsam-mehr-reps-16ced31e33", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "langsam, mehr Reps", - "id": "step-langsam-mehr-reps-bd0e8e15c4", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "1-beinig", - "id": "step-1-beinig-0c2e9de2f7", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "calf", - "factor": 1.0 - }, - { - "name": "1-beinig", - "id": "step-1-beinig-f823fd2149", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - }, - "movement_cluster": "calf", - "factor": 1.0 - } - ], - "id": "progression-wadenheben-3833cd235f", - "key": "calf" - }, - "carry": { - "name": "Bottoms-up KB-Hold (Griff)", - "steps": [ - { - "name": "im Stand 10–15s", - "id": "step-im-stand-10-15s-e90dd57ff1", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "im Stand 10–15s", - "id": "step-im-stand-10-15s-db8eec9df0", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "15–20s, ruhig", - "id": "step-15-20s-ruhig-321e582ab2", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "15–20s, ruhig", - "id": "step-15-20s-ruhig-7db6eb1a01", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "bis Schulterhöhe 15s", - "id": "step-bis-schulterhoehe-15s-f1763ea9e5", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "bis Schulterhöhe 15s", - "id": "step-bis-schulterhoehe-15s-21c8fd4d01", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "länger / ein paar Schritte", - "id": "step-laenger-ein-paar-schritte-fa5838da65", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "länger / ein paar Schritte", - "id": "step-laenger-ein-paar-schritte-ec93f04473", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - } - ], - "id": "progression-bottoms-up-kb-hold-griff-7c46cda772", - "key": "carry" - }, - "db": { - "name": "Dead Bug", - "steps": [ - { - "name": "Arme ODER Beine einzeln", - "id": "step-arme-oder-beine-einzeln-7a6a9d581b", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "Arme ODER Beine einzeln", - "id": "step-arme-oder-beine-einzeln-94f8e1785d", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "gegengleich", - "id": "step-gegengleich-41e02caee7", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "gegengleich", - "id": "step-gegengleich-31e878a2f9", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "gegengleich + Pause", - "id": "step-gegengleich-pause-9e84e14ecc", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "gegengleich + Pause", - "id": "step-gegengleich-pause-bf8ebc0b7e", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "Beine weiter gestreckt", - "id": "step-beine-weiter-gestreckt-453d185cf7", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "Beine weiter gestreckt", - "id": "step-beine-weiter-gestreckt-1c5f566001", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - } - ], - "id": "progression-dead-bug-e390771cf1", - "key": "db" - }, - "dips": { - "name": "Bank-Dips (Couch)", - "steps": [ - { - "name": "Knie stark gebeugt, klein", - "id": "step-knie-stark-gebeugt-klein-72a7bffc76", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Knie stark gebeugt, klein", - "id": "step-knie-stark-gebeugt-klein-56482dde46", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Knie gebeugt, voll", - "id": "step-knie-gebeugt-voll-07cc34cd51", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Knie gebeugt, voll", - "id": "step-knie-gebeugt-voll-80070f8ecf", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Füße etwas weiter", - "id": "step-fuesse-etwas-weiter-42e0983b7f", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Füße etwas weiter", - "id": "step-fuesse-etwas-weiter-7b887f1140", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Füße weiter weg", - "id": "step-fuesse-weiter-weg-60c057769a", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Füße weiter weg", - "id": "step-fuesse-weiter-weg-2959fb77fb", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - } - ], - "id": "progression-bank-dips-couch-64352512e0", - "key": "dips" - }, - "gobletkb": { - "name": "Goblet Squat (KB/Rucksack)", - "steps": [ - { - "name": "leicht, Tiefe lernen", - "id": "step-leicht-tiefe-lernen-77201f2b6b", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "leicht, Tiefe lernen", - "id": "step-leicht-tiefe-lernen-70ea0f2e9b", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-57415830b2", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-abf827246a", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-2d542b9c09", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-eda93ad7ea", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mittel, 3s runter", - "id": "step-mittel-3s-runter-9dd7fec573", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mittel, 3s runter", - "id": "step-mittel-3s-runter-3f76b2391b", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-goblet-squat-kb-rucksack-0e2a5969bc", - "key": "gobletkb" - }, - "halo": { - "name": "Halo", - "steps": [ - { - "name": "leicht, langsam", - "id": "step-leicht-langsam-5d15ba8134", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "leicht, langsam", - "id": "step-leicht-langsam-c2a5c59e62", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-81a736180a", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-fe775a4a23", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-c5c0bf8989", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-3898a7cea7", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "mittel, langsam", - "id": "step-mittel-langsam-c9db464d00", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - }, - { - "name": "mittel, langsam", - "id": "step-mittel-langsam-e1a2e46fd3", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "loaded_mobility", - "factor": 1.0 - } - ], - "id": "progression-halo-8470d68816", - "key": "halo" - }, - "holl": { - "name": "Hollow Hold", - "steps": [ - { - "name": "Tuck (Knie an)", - "id": "step-tuck-knie-an-be15e1c436", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "Tuck (Knie an)", - "id": "step-tuck-knie-an-5f62fcb360", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "Tuck, länger", - "id": "step-tuck-laenger-b05dd43bd8", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "Tuck, länger", - "id": "step-tuck-laenger-c70e4ba401", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "halb gestreckt", - "id": "step-halb-gestreckt-39bccf6e6b", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.82 - }, - { - "name": "halb gestreckt", - "id": "step-halb-gestreckt-9147433141", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.82 - }, - { - "name": "halb/voll", - "id": "step-halb-voll-13f8ebb8c0", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.82 - }, - { - "name": "halb/voll", - "id": "step-halb-voll-cde344cba9", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.82 - } - ], - "id": "progression-hollow-hold-7e44b4d78e", - "key": "holl" - }, - "kbdl": { - "name": "KB-Deadlift (beidbeinig)", - "steps": [ - { - "name": "leicht, Technik", - "id": "step-leicht-technik-54ca22fe3d", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "leicht, Technik", - "id": "step-leicht-technik-fcd2dc3979", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-db29d97bd7", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-fd7e87b98b", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-7ed7284a10", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-e9887f19f9", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "schwerer, kontrolliert", - "id": "step-schwerer-kontrolliert-dd0f4f4538", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "schwerer, kontrolliert", - "id": "step-schwerer-kontrolliert-ab6af6caea", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - } - ], - "id": "progression-kb-deadlift-beidbeinig-0e27bd039a", - "key": "kbdl" - }, - "load": { - "name": "Bizeps-Curls (KB/Rucksack)", - "steps": [ - { - "name": "leicht", - "id": "step-leicht-273b789e0f", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-93f0046a45", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-bd37c72d4b", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-31ffb19393", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "mehr", - "id": "step-mehr-ca55180c8e", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "mehr", - "id": "step-mehr-d15ecf67a9", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "fordernd", - "id": "step-fordernd-b1f6c26c72", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - }, - { - "name": "fordernd", - "id": "step-fordernd-b575d29101", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "elbow_flexion", - "factor": 1.0 - } - ], - "id": "progression-bizeps-curls-kb-rucksack-b6ad39301a", - "key": "load" - }, - "lr": { - "name": "Beinheben", - "steps": [ - { - "name": "Knie gebeugt", - "id": "step-knie-gebeugt-1855b4de0f", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie gebeugt", - "id": "step-knie-gebeugt-863780bc32", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie gebeugt, langsam", - "id": "step-knie-gebeugt-langsam-425f61fdb6", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie gebeugt, langsam", - "id": "step-knie-gebeugt-langsam-aad675fda3", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "halb gestreckt", - "id": "step-halb-gestreckt-c84cc4988c", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "halb gestreckt", - "id": "step-halb-gestreckt-298f78b046", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "gestreckt", - "id": "step-gestreckt-a4f6fc11b8", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "gestreckt", - "id": "step-gestreckt-d43210faa3", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-beinheben-9e54736d11", - "key": "lr" - }, - "lraise": { - "name": "Seitheben (KB/Rucksack)", - "steps": [ - { - "name": "leicht (Technik)", - "id": "step-leicht-technik-a0504bbbb1", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "leicht (Technik)", - "id": "step-leicht-technik-a691c813e1", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-82a36d8e86", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-c736c1e695", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-3c1675e579", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-82cacf1f60", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "mittel, langsam", - "id": "step-mittel-langsam-6b417be46f", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "mittel, langsam", - "id": "step-mittel-langsam-b3e2812063", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-seitheben-kb-rucksack-98cf022c83", - "key": "lraise" - }, - "ls": { - "name": "Liegestütze", - "steps": [ - { - "name": "Inkline (hoch)", - "id": "step-inkline-hoch-7495b7e9cc", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.1 - }, - { - "name": "Inkline (hoch)", - "id": "step-inkline-hoch-231f998085", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.1 - }, - { - "name": "Inkline tiefer / Knie", - "id": "step-inkline-tiefer-knie-631d725178", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.35 - }, - { - "name": "Inkline tiefer / Knie", - "id": "step-inkline-tiefer-knie-2f351c5335", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.35 - }, - { - "name": "Knie / Negativ", - "id": "step-knie-negativ-0cf75923cb", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.7 - }, - { - "name": "Knie / Negativ", - "id": "step-knie-negativ-d225638aef", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.7 - }, - { - "name": "Negativ + erste Volle", - "id": "step-negativ-erste-volle-d68abebd8f", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.85 - }, - { - "name": "Negativ + erste Volle", - "id": "step-negativ-erste-volle-d93d10d94d", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "horizontal_push", - "factor": 0.85 - } - ], - "id": "progression-liegestuetze-8c8a5e07de", - "key": "ls" - }, - "lseng": { - "name": "Inkline (hoch)", - "steps": [ - { - "name": "Inkline (hoch)", - "id": "step-inkline-hoch-7d971a7291", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline (hoch)", - "id": "step-inkline-hoch-59da475437", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline mittel", - "id": "step-inkline-mittel-821fadec71", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline mittel", - "id": "step-inkline-mittel-199e2aaf70", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline tief", - "id": "step-inkline-tief-6173a85a04", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Inkline tief", - "id": "step-inkline-tief-54d7a4a0ff", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie-Liegestütze", - "id": "step-knie-liegestuetze-7de6b6dcfb", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Knie-Liegestütze", - "id": "step-knie-liegestuetze-2d4d23db17", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-inkline-hoch-e770797129", - "key": "lseng" - }, - "lunge": { - "name": "Ausfallschritt rückw.", - "steps": [ - { - "name": "Körpergewicht (ggf. festhalten)", - "id": "step-koerpergewicht-ggf-festhalten-3d6301d1bd", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 0.55 - }, - { - "name": "Körpergewicht (ggf. festhalten)", - "id": "step-koerpergewicht-ggf-festhalten-0d19269674", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 0.55 - }, - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-180853b5dd", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-0f7d4a63bd", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mehr Reps", - "id": "step-mehr-reps-d19d6c0eab", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "mehr Reps", - "id": "step-mehr-reps-9235507680", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "+ Last leicht", - "id": "step-last-leicht-10d2acd532", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "+ Last leicht", - "id": "step-last-leicht-6ecdd4c919", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-ausfallschritt-rueckw-52ab461616", - "key": "lunge" - }, - "mtn": { - "name": "Mountain Climbers", - "steps": [ - { - "name": "moderat", - "id": "step-moderat-f76617e319", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-a73e3b0549", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-99c93498d6", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "moderat", - "id": "step-moderat-a3b745efcb", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-c304accace", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-23a81c4905", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-25e355b703", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "zügig", - "id": "step-zuegig-4d2a2fd39c", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-mountain-climbers-97f6359644", - "key": "mtn" - }, - "pike": { - "name": "Pike", - "steps": [ - { - "name": "Pike Hold", - "id": "step-pike-hold-d46aa2e93f", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 0.3 - }, - { - "name": "Pike Hold", - "id": "step-pike-hold-cc334c7fc6", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 0.3 - }, - { - "name": "Hold + kleine Senkung", - "id": "step-hold-kleine-senkung-4f3bfac4b2", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 0.3 - }, - { - "name": "Hold + kleine Senkung", - "id": "step-hold-kleine-senkung-b93d503cb1", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 0.3 - }, - { - "name": "Push-up Hände erhöht", - "id": "step-push-up-haende-erhoeht-7c94ab03f7", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Push-up Hände erhöht", - "id": "step-push-up-haende-erhoeht-5af7a607db", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Push-up Boden, klein", - "id": "step-push-up-boden-klein-0a27dd0a33", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Push-up Boden, klein", - "id": "step-push-up-boden-klein-44741d722a", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - } - ], - "id": "progression-pike-9726e02fef", - "key": "pike" - }, - "plank": { - "name": "Plank", - "steps": [ - { - "name": "auf Knien 15–20 s", - "id": "step-auf-knien-15-20-s-66aa989041", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "auf Knien 15–20 s", - "id": "step-auf-knien-15-20-s-28c28cffdc", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "voll 20–30 s", - "id": "step-voll-20-30-s-ebbb758203", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll 20–30 s", - "id": "step-voll-20-30-s-465691f146", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll + Schulter-Taps", - "id": "step-voll-schulter-taps-9fbac9d915", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll + Schulter-Taps", - "id": "step-voll-schulter-taps-738cd33982", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll, länger", - "id": "step-voll-laenger-3da43cf026", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll, länger", - "id": "step-voll-laenger-827f28be3a", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "core", - "factor": 1.0 - } - ], - "id": "progression-plank-88a9d6bafb", - "key": "plank" - }, - "press": { - "name": "KB Floor Press", - "steps": [ - { - "name": "leicht (Floor Press)", - "id": "step-leicht-floor-press-de744d6e05", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "leicht (Floor Press)", - "id": "step-leicht-floor-press-5987ddc1d2", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Floor Press mehr", - "id": "step-floor-press-mehr-3b454ea446", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Floor Press mehr", - "id": "step-floor-press-mehr-b34710868b", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Overhead leicht", - "id": "step-overhead-leicht-7f9274fd29", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Overhead leicht", - "id": "step-overhead-leicht-3413a54a3a", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Overhead mittel", - "id": "step-overhead-mittel-4b236720a7", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - }, - { - "name": "Overhead mittel", - "id": "step-overhead-mittel-ea724f9a42", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "vertical_push", - "factor": 1.0 - } - ], - "id": "progression-kb-floor-press-0f387c469f", - "key": "press" - }, - "row1": { - "name": "Einarmiges Rudern (KB)", - "steps": [ - { - "name": "leicht", - "id": "step-leicht-db8c75b358", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-d1e4feaafa", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-19ded398d6", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-0ce270eb41", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "einarmig leicht", - "id": "step-einarmig-leicht-a6f388b349", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "einarmig leicht", - "id": "step-einarmig-leicht-a9539fbadb", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "einarmig mittel", - "id": "step-einarmig-mittel-74f0fe3e1e", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "einarmig mittel", - "id": "step-einarmig-mittel-3f961280f6", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - } - ], - "id": "progression-einarmiges-rudern-kb-c142975b5b", - "key": "row1" - }, - "row2": { - "name": "Einarm-Rudern BREIT (KB)", - "steps": [ - { - "name": "leicht (Technik)", - "id": "step-leicht-technik-32d83f79d0", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht (Technik)", - "id": "step-leicht-technik-76c26b9c7f", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-3bbb6ef617", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht, mehr Reps", - "id": "step-leicht-mehr-reps-1fea9ba00a", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-5f7bc5489c", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mittel", - "id": "step-mittel-d4c65b81c6", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mittel, Pause oben", - "id": "step-mittel-pause-oben-d5cdfdfe94", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mittel, Pause oben", - "id": "step-mittel-pause-oben-8f7a255ad4", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - } - ], - "id": "progression-einarm-rudern-breit-kb-a06708cc5d", - "key": "row2" - }, - "rowe": { - "name": "Einarm-Rudern ENG (KB)", - "steps": [ - { - "name": "leicht", - "id": "step-leicht-7638fa3210", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "leicht", - "id": "step-leicht-d1490519d3", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-4f2775725c", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "etwas mehr", - "id": "step-etwas-mehr-35098ff9fa", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mehr", - "id": "step-mehr-cd74b90448", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "mehr", - "id": "step-mehr-558d6deb5b", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "fordernd", - "id": "step-fordernd-fec615da4c", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - }, - { - "name": "fordernd", - "id": "step-fordernd-60a8a2aedd", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "horizontal_pull", - "factor": 1.0 - } - ], - "id": "progression-einarm-rudern-eng-kb-b622a3fbfc", - "key": "rowe" - }, - "sp": { - "name": "Side Plank (Wechsel)", - "steps": [ - { - "name": "auf Knien", - "id": "step-auf-knien-3ed08acc27", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "auf Knien", - "id": "step-auf-knien-000b6c864b", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "auf Knien, länger", - "id": "step-auf-knien-laenger-d1c3028780", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "auf Knien, länger", - "id": "step-auf-knien-laenger-227ec9b5f7", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 0.65 - }, - { - "name": "voll", - "id": "step-voll-6702e0693f", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll", - "id": "step-voll-727fcc161a", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll, länger", - "id": "step-voll-laenger-8c287bb42a", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 1.0 - }, - { - "name": "voll, länger", - "id": "step-voll-laenger-ef549f755a", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "movement_cluster": "core", - "factor": 1.0 - } - ], - "id": "progression-side-plank-wechsel-335152ff85", - "key": "sp" - }, - "squat": { - "name": "Körpergewicht", - "steps": [ - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-e4bfd724af", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Körpergewicht", - "id": "step-koerpergewicht-cf13a813b5", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Körpergewicht, mehr Reps", - "id": "step-koerpergewicht-mehr-reps-0156c8a851", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Körpergewicht, mehr Reps", - "id": "step-koerpergewicht-mehr-reps-9fccf7b6ed", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet leicht", - "id": "step-goblet-leicht-e52d2635ab", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet leicht", - "id": "step-goblet-leicht-758736b15a", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet mittel", - "id": "step-goblet-mittel-50a2e4ec3e", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet mittel", - "id": "step-goblet-mittel-0c01361ea0", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-koerpergewicht-6b7808a004", - "key": "squat" - }, - "sup": { - "name": "Superman / Y-T-W", - "steps": [ - { - "name": "Superman Hold", - "id": "step-superman-hold-06efa2683a", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Superman Hold", - "id": "step-superman-hold-34af38f543", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Superman Hold länger", - "id": "step-superman-hold-laenger-b383f0e540", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Superman Hold länger", - "id": "step-superman-hold-laenger-6b326b0a11", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Reverse Snow Angels", - "id": "step-reverse-snow-angels-871916bcee", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Reverse Snow Angels", - "id": "step-reverse-snow-angels-78c24f8835", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Y-T-W Raises", - "id": "step-y-t-w-raises-b7c40c3579", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "Y-T-W Raises", - "id": "step-y-t-w-raises-a6d76cb15c", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-superman-y-t-w-06938b356b", - "key": "sup" - }, - "swing": { - "name": "KB Swing (zweiarmig)", - "steps": [ - { - "name": "Dead-Stop, leicht (Technik)", - "id": "step-dead-stop-leicht-technik-02548f1a05", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "Dead-Stop, leicht (Technik)", - "id": "step-dead-stop-leicht-technik-3b9715b104", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "Dead-Stop, mehr Reps", - "id": "step-dead-stop-mehr-reps-7be430a756", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "Dead-Stop, mehr Reps", - "id": "step-dead-stop-mehr-reps-5c68075818", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "durchgehend, mittel", - "id": "step-durchgehend-mittel-183cc7dfa6", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "durchgehend, mittel", - "id": "step-durchgehend-mittel-7762c8f240", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "durchgehend, schwerer", - "id": "step-durchgehend-schwerer-6da670bed8", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - }, - { - "name": "durchgehend, schwerer", - "id": "step-durchgehend-schwerer-a1609b5bad", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "hip_hinge", - "factor": 1.0 - } - ], - "id": "progression-kb-swing-zweiarmig-b9b86d8de1", - "key": "swing" - }, - "tsquat": { - "name": "3s runter", - "steps": [ - { - "name": "3s runter", - "id": "step-3s-runter-1cafcb49f1", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "3s runter", - "id": "step-3s-runter-c29275cc81", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "3s + Pause unten", - "id": "step-3s-pause-unten-8b4f16c542", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "3s + Pause unten", - "id": "step-3s-pause-unten-d51468b370", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet 3s", - "id": "step-goblet-3s-ebd1293f2a", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet 3s", - "id": "step-goblet-3s-06003d3bdd", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet 3s + Pause", - "id": "step-goblet-3s-pause-a70c738b23", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "Goblet 3s + Pause", - "id": "step-goblet-3s-pause-40369412a4", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-3s-runter-b85ef7f990", - "key": "tsquat" - }, - "tw": { - "name": "Russian Twist (KB)", - "steps": [ - { - "name": "ohne Gewicht", - "id": "step-ohne-gewicht-3bd115d2ec", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "ohne Gewicht", - "id": "step-ohne-gewicht-478be9efcd", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "ohne Gewicht", - "id": "step-ohne-gewicht-32fc20afc1", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "ohne Gewicht", - "id": "step-ohne-gewicht-fbe33d8a04", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "KB leicht", - "id": "step-kb-leicht-cf37c395cd", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "KB leicht", - "id": "step-kb-leicht-5cca613a86", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "KB", - "id": "step-kb-f50c52e3e3", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - }, - { - "name": "KB", - "id": "step-kb-eb15afb22c", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - }, - "movement_cluster": "general", - "factor": 1.0 - } - ], - "id": "progression-russian-twist-kb-4ae08fd89a", - "key": "tw" - }, - "wall": { - "name": "Wall Sit", - "steps": [ - { - "name": "15–25 s", - "id": "step-15-25-s-c7aec770d4", - "phase_id": "phase-phase-1-fundament-9d3418c792", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "15–25 s", - "id": "step-15-25-s-dbdcddfcac", - "phase_id": "phase-phase-2-fundament-f815c541ec", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "25–40 s", - "id": "step-25-40-s-7a71cfd7e1", - "phase_id": "phase-phase-3-aufbau-fa21a3469e", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "25–40 s", - "id": "step-25-40-s-851e536e20", - "phase_id": "phase-phase-4-aufbau-98dc807ce2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "40–55 s", - "id": "step-40-55-s-96e1d7939b", - "phase_id": "phase-phase-5-steigerung-f023eda6a2", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "40–55 s", - "id": "step-40-55-s-ebe34a6bbd", - "phase_id": "phase-phase-6-steigerung-3dc7eb5c32", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "55–70 s", - "id": "step-55-70-s-3ddd9ace4a", - "phase_id": "phase-phase-7-festigen-d8284c58fc", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - }, - { - "name": "55–70 s", - "id": "step-55-70-s-19c7e1d6d2", - "phase_id": "phase-phase-8-festigen-bfaebd57b4", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "movement_cluster": "knee_dominant", - "factor": 1.0 - } - ], - "id": "progression-wall-sit-e5b793fbdf", - "key": "wall" - } - }, - "training_format": { - "mode": "tabata", - "fixed_interval": true, - "work_seconds": 20, - "rest_seconds": 10, - "rounds": 8, - "rounds_scope": "block", - "schema_version": 1 - }, - "schema_version": 3, - "contract_version": 2, - "plan_id": "phase-1", - "exercise_catalog": { - "movement-liegestuetze-57eacf74ee": { - "id": "movement-liegestuetze-57eacf74ee", - "name": "Liegestütze", - "progression_id": "ls", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-inkline-hoch-7495b7e9cc", - "name": "Inkline (hoch)", - "factor": 0.1, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-inkline-hoch-231f998085", - "name": "Inkline (hoch)", - "factor": 0.1, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-inkline-tiefer-knie-631d725178", - "name": "Inkline tiefer / Knie", - "factor": 0.35, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-inkline-tiefer-knie-2f351c5335", - "name": "Inkline tiefer / Knie", - "factor": 0.35, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-negativ-0cf75923cb", - "name": "Knie / Negativ", - "factor": 0.7, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-negativ-d225638aef", - "name": "Knie / Negativ", - "factor": 0.7, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-negativ-erste-volle-d68abebd8f", - "name": "Negativ + erste Volle", - "factor": 0.85, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-negativ-erste-volle-d93d10d94d", - "name": "Negativ + erste Volle", - "factor": 0.85, - "movement_cluster": "horizontal_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-bank-dips-couch-ad6c829e1b": { - "id": "movement-bank-dips-couch-ad6c829e1b", - "name": "Bank-Dips (Couch)", - "progression_id": "dips", - "movement_cluster": "vertical_push", - "movement_label": "Dip", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-knie-stark-gebeugt-klein-72a7bffc76", - "name": "Knie stark gebeugt, klein", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-stark-gebeugt-klein-56482dde46", - "name": "Knie stark gebeugt, klein", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-voll-07cc34cd51", - "name": "Knie gebeugt, voll", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-voll-80070f8ecf", - "name": "Knie gebeugt, voll", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fuesse-etwas-weiter-42e0983b7f", - "name": "Füße etwas weiter", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fuesse-etwas-weiter-7b887f1140", - "name": "Füße etwas weiter", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fuesse-weiter-weg-60c057769a", - "name": "Füße weiter weg", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fuesse-weiter-weg-2959fb77fb", - "name": "Füße weiter weg", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-pike-17f7716e00": { - "id": "movement-pike-17f7716e00", - "name": "Pike", - "progression_id": "pike", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-pike-hold-d46aa2e93f", - "name": "Pike Hold", - "factor": 0.3, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-pike-hold-cc334c7fc6", - "name": "Pike Hold", - "factor": 0.3, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-hold-kleine-senkung-4f3bfac4b2", - "name": "Hold + kleine Senkung", - "factor": 0.3, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-hold-kleine-senkung-b93d503cb1", - "name": "Hold + kleine Senkung", - "factor": 0.3, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-push-up-haende-erhoeht-7c94ab03f7", - "name": "Push-up Hände erhöht", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-push-up-haende-erhoeht-5af7a607db", - "name": "Push-up Hände erhöht", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-push-up-boden-klein-0a27dd0a33", - "name": "Push-up Boden, klein", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-push-up-boden-klein-44741d722a", - "name": "Push-up Boden, klein", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-seitheben-kb-rucksack-91de5dc2f5": { - "id": "movement-seitheben-kb-rucksack-91de5dc2f5", - "name": "Seitheben (KB/Rucksack)", - "progression_id": "lraise", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-technik-a0504bbbb1", - "name": "leicht (Technik)", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-technik-a691c813e1", - "name": "leicht (Technik)", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-mehr-reps-82a36d8e86", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-mehr-reps-c736c1e695", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-3c1675e579", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-82cacf1f60", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-langsam-6b417be46f", - "name": "mittel, langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-langsam-b3e2812063", - "name": "mittel, langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-goblet-squat-kb-rucksack-6ff9f90901": { - "id": "movement-goblet-squat-kb-rucksack-6ff9f90901", - "name": "Goblet Squat (KB/Rucksack)", - "progression_id": "gobletkb", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-tiefe-lernen-77201f2b6b", - "name": "leicht, Tiefe lernen", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-tiefe-lernen-70ea0f2e9b", - "name": "leicht, Tiefe lernen", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-57415830b2", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-abf827246a", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-2d542b9c09", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-eda93ad7ea", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-3s-runter-9dd7fec573", - "name": "mittel, 3s runter", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-3s-runter-3f76b2391b", - "name": "mittel, 3s runter", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-ausfallschritt-rueckw-2a4d06b799": { - "id": "movement-ausfallschritt-rueckw-2a4d06b799", - "name": "Ausfallschritt rückw.", - "progression_id": "lunge", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-koerpergewicht-ggf-festhalten-3d6301d1bd", - "name": "Körpergewicht (ggf. festhalten)", - "factor": 0.55, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-koerpergewicht-ggf-festhalten-0d19269674", - "name": "Körpergewicht (ggf. festhalten)", - "factor": 0.55, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-koerpergewicht-180853b5dd", - "name": "Körpergewicht", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-koerpergewicht-0f7d4a63bd", - "name": "Körpergewicht", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mehr-reps-d19d6c0eab", - "name": "mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mehr-reps-9235507680", - "name": "mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-last-leicht-10d2acd532", - "name": "+ Last leicht", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-last-leicht-6ecdd4c919", - "name": "+ Last leicht", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-glute-bridge-1c0a7bf699": { - "id": "movement-glute-bridge-1c0a7bf699", - "name": "Glute Bridge", - "progression_id": "bridge", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-beidbeinig-36eb2634d6", - "name": "beidbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-8a52d360c3", - "name": "beidbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-2s-halten-1ad3dfd97b", - "name": "beidbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-2s-halten-6087f8f6d1", - "name": "beidbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-einbeinig-3a0c0abdfa", - "name": "einbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-43807e1138", - "name": "einbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-2s-halten-4e42109f20", - "name": "einbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-2s-halten-6335420028", - "name": "einbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - } - ] - }, - "movement-wadenheben-0c7839551b": { - "id": "movement-wadenheben-0c7839551b", - "name": "Wadenheben", - "progression_id": "calf", - "movement_cluster": "calf", - "movement_label": "Waden", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-koerpergewicht-e8311d7913", - "name": "Körpergewicht", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-koerpergewicht-95b36efc85", - "name": "Körpergewicht", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-9e41650da8", - "name": "langsam", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-74db877975", - "name": "langsam", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-mehr-reps-16ced31e33", - "name": "langsam, mehr Reps", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-mehr-reps-bd0e8e15c4", - "name": "langsam, mehr Reps", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-1-beinig-0c2e9de2f7", - "name": "1-beinig", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-1-beinig-f823fd2149", - "name": "1-beinig", - "factor": 1.0, - "movement_cluster": "calf", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - } - ] - }, - "movement-einarm-rudern-eng-kb-4907fbb5c7": { - "id": "movement-einarm-rudern-eng-kb-4907fbb5c7", - "name": "Einarm-Rudern ENG (KB)", - "progression_id": "rowe", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-7638fa3210", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-d1490519d3", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-etwas-mehr-4f2775725c", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-etwas-mehr-35098ff9fa", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mehr-cd74b90448", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mehr-558d6deb5b", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-fordernd-fec615da4c", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-fordernd-60a8a2aedd", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-bizeps-curls-kb-rucksack-67944f00d5": { - "id": "movement-bizeps-curls-kb-rucksack-67944f00d5", - "name": "Bizeps-Curls (KB/Rucksack)", - "progression_id": "load", - "movement_cluster": "elbow_flexion", - "movement_label": "Bizeps", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-273b789e0f", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-93f0046a45", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-etwas-mehr-bd37c72d4b", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-etwas-mehr-31ffb19393", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mehr-ca55180c8e", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mehr-d15ecf67a9", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fordernd-b1f6c26c72", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fordernd-b575d29101", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-einarm-rudern-breit-kb-0146abcfbb": { - "id": "movement-einarm-rudern-breit-kb-0146abcfbb", - "name": "Einarm-Rudern BREIT (KB)", - "progression_id": "row2", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-technik-32d83f79d0", - "name": "leicht (Technik)", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-technik-76c26b9c7f", - "name": "leicht (Technik)", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-mehr-reps-3bbb6ef617", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-mehr-reps-1fea9ba00a", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-5f7bc5489c", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-d4c65b81c6", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-pause-oben-d5cdfdfe94", - "name": "mittel, Pause oben", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-pause-oben-8f7a255ad4", - "name": "mittel, Pause oben", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-superman-y-t-w-279f03a544": { - "id": "movement-superman-y-t-w-279f03a544", - "name": "Superman / Y-T-W", - "progression_id": "sup", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-superman-hold-06efa2683a", - "name": "Superman Hold", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-34af38f543", - "name": "Superman Hold", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-laenger-b383f0e540", - "name": "Superman Hold länger", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-laenger-6b326b0a11", - "name": "Superman Hold länger", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-reverse-snow-angels-871916bcee", - "name": "Reverse Snow Angels", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-reverse-snow-angels-78c24f8835", - "name": "Reverse Snow Angels", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-y-t-w-raises-b7c40c3579", - "name": "Y-T-W Raises", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-y-t-w-raises-a6d76cb15c", - "name": "Y-T-W Raises", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-plank-c8449f2937": { - "id": "movement-plank-c8449f2937", - "name": "Plank", - "progression_id": "plank", - "movement_cluster": "core", - "movement_label": "Core", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-auf-knien-15-20-s-66aa989041", - "name": "auf Knien 15–20 s", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-auf-knien-15-20-s-28c28cffdc", - "name": "auf Knien 15–20 s", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-20-30-s-ebbb758203", - "name": "voll 20–30 s", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-20-30-s-465691f146", - "name": "voll 20–30 s", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-schulter-taps-9fbac9d915", - "name": "voll + Schulter-Taps", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-schulter-taps-738cd33982", - "name": "voll + Schulter-Taps", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-laenger-3da43cf026", - "name": "voll, länger", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-voll-laenger-827f28be3a", - "name": "voll, länger", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-mountain-climbers-79efdb62dd": { - "id": "movement-mountain-climbers-79efdb62dd", - "name": "Mountain Climbers", - "progression_id": "mtn", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-moderat-f76617e319", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-a73e3b0549", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-99c93498d6", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-a3b745efcb", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-c304accace", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-23a81c4905", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-25e355b703", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-4d2a2fd39c", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-dead-bug-21d4b207d5": { - "id": "movement-dead-bug-21d4b207d5", - "name": "Dead Bug", - "progression_id": "db", - "movement_cluster": "core", - "movement_label": "Core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-arme-oder-beine-einzeln-7a6a9d581b", - "name": "Arme ODER Beine einzeln", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-arme-oder-beine-einzeln-94f8e1785d", - "name": "Arme ODER Beine einzeln", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gegengleich-41e02caee7", - "name": "gegengleich", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gegengleich-31e878a2f9", - "name": "gegengleich", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gegengleich-pause-9e84e14ecc", - "name": "gegengleich + Pause", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gegengleich-pause-bf8ebc0b7e", - "name": "gegengleich + Pause", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beine-weiter-gestreckt-453d185cf7", - "name": "Beine weiter gestreckt", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beine-weiter-gestreckt-1c5f566001", - "name": "Beine weiter gestreckt", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-beinheben-faf19a629b": { - "id": "movement-beinheben-faf19a629b", - "name": "Beinheben", - "progression_id": "lr", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-knie-gebeugt-1855b4de0f", - "name": "Knie gebeugt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-863780bc32", - "name": "Knie gebeugt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-langsam-425f61fdb6", - "name": "Knie gebeugt, langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-knie-gebeugt-langsam-aad675fda3", - "name": "Knie gebeugt, langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-gestreckt-c84cc4988c", - "name": "halb gestreckt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-gestreckt-298f78b046", - "name": "halb gestreckt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gestreckt-a4f6fc11b8", - "name": "gestreckt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-gestreckt-d43210faa3", - "name": "gestreckt", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-rdl-hip-hinge-kb-rucksack-9ada5a67c3": { - "id": "movement-rdl-hip-hinge-kb-rucksack-9ada5a67c3", - "name": "RDL / Hip Hinge (KB/Rucksack)", - "progression_id": "load", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-273b789e0f", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-93f0046a45", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-etwas-mehr-bd37c72d4b", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-etwas-mehr-31ffb19393", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mehr-ca55180c8e", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mehr-d15ecf67a9", - "name": "mehr", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fordernd-b1f6c26c72", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-fordernd-b575d29101", - "name": "fordernd", - "factor": 1.0, - "movement_cluster": "elbow_flexion", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-glute-bridge-1-beinig-ccc8c9fb53": { - "id": "movement-glute-bridge-1-beinig-ccc8c9fb53", - "name": "Glute Bridge 1-beinig", - "progression_id": "bridge", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-beidbeinig-36eb2634d6", - "name": "beidbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-8a52d360c3", - "name": "beidbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-2s-halten-1ad3dfd97b", - "name": "beidbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-beidbeinig-2s-halten-6087f8f6d1", - "name": "beidbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-einbeinig-3a0c0abdfa", - "name": "einbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-43807e1138", - "name": "einbeinig", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-2s-halten-4e42109f20", - "name": "einbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - }, - { - "id": "step-einbeinig-2s-halten-6335420028", - "name": "einbeinig, 2s halten", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate", - "sets": 3, - "locked_sets": true - } - } - ] - }, - "movement-wall-sit-6b5f695f6a": { - "id": "movement-wall-sit-6b5f695f6a", - "name": "Wall Sit", - "progression_id": "wall", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-15-25-s-c7aec770d4", - "name": "15–25 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-15-25-s-dbdcddfcac", - "name": "15–25 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-25-40-s-7a71cfd7e1", - "name": "25–40 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-25-40-s-851e536e20", - "name": "25–40 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-40-55-s-96e1d7939b", - "name": "40–55 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-40-55-s-ebe34a6bbd", - "name": "40–55 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-55-70-s-3ddd9ace4a", - "name": "55–70 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-55-70-s-19c7e1d6d2", - "name": "55–70 s", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-hollow-hold-54c452f77b": { - "id": "movement-hollow-hold-54c452f77b", - "name": "Hollow Hold", - "progression_id": "holl", - "movement_cluster": "core", - "movement_label": "Core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-tuck-knie-an-be15e1c436", - "name": "Tuck (Knie an)", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-tuck-knie-an-5f62fcb360", - "name": "Tuck (Knie an)", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-tuck-laenger-b05dd43bd8", - "name": "Tuck, länger", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-tuck-laenger-c70e4ba401", - "name": "Tuck, länger", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-gestreckt-39bccf6e6b", - "name": "halb gestreckt", - "factor": 0.82, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-gestreckt-9147433141", - "name": "halb gestreckt", - "factor": 0.82, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-voll-13f8ebb8c0", - "name": "halb/voll", - "factor": 0.82, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-halb-voll-cde344cba9", - "name": "halb/voll", - "factor": 0.82, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-side-plank-wechsel-309b0bc82c": { - "id": "movement-side-plank-wechsel-309b0bc82c", - "name": "Side Plank (Wechsel)", - "progression_id": "sp", - "movement_cluster": "core", - "movement_label": "Core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-auf-knien-3ed08acc27", - "name": "auf Knien", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-auf-knien-000b6c864b", - "name": "auf Knien", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-auf-knien-laenger-d1c3028780", - "name": "auf Knien, länger", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-auf-knien-laenger-227ec9b5f7", - "name": "auf Knien, länger", - "factor": 0.65, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-voll-6702e0693f", - "name": "voll", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-voll-727fcc161a", - "name": "voll", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-voll-laenger-8c287bb42a", - "name": "voll, länger", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-voll-laenger-ef549f755a", - "name": "voll, länger", - "factor": 1.0, - "movement_cluster": "core", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-bicycle-crunch-15c87e28c5": { - "id": "movement-bicycle-crunch-15c87e28c5", - "name": "Bicycle Crunch", - "progression_id": "bic", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-langsam-b16ce156f7", - "name": "langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-langsam-3bc865247d", - "name": "langsam", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-9b37c0536d", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-6a0ed59bbd", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-1a29fed611", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-moderat-60b7d1ac93", - "name": "moderat", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-85e9262405", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-zuegig-2b4933822c", - "name": "zügig", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-russian-twist-kb-d6e382fb3c": { - "id": "movement-russian-twist-kb-d6e382fb3c", - "name": "Russian Twist (KB)", - "progression_id": "tw", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "unilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-ohne-gewicht-3bd115d2ec", - "name": "ohne Gewicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-ohne-gewicht-478be9efcd", - "name": "ohne Gewicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-ohne-gewicht-32fc20afc1", - "name": "ohne Gewicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-ohne-gewicht-fbe33d8a04", - "name": "ohne Gewicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-kb-leicht-cf37c395cd", - "name": "KB leicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-kb-leicht-5cca613a86", - "name": "KB leicht", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-kb-f50c52e3e3", - "name": "KB", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - } - }, - { - "id": "step-kb-eb15afb22c", - "name": "KB", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-kb-floor-press-060586ebd6": { - "id": "movement-kb-floor-press-060586ebd6", - "name": "KB Floor Press", - "progression_id": "press", - "movement_cluster": "vertical_push", - "movement_label": "Schulterdrücken", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-floor-press-de744d6e05", - "name": "leicht (Floor Press)", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-floor-press-5987ddc1d2", - "name": "leicht (Floor Press)", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-floor-press-mehr-3b454ea446", - "name": "Floor Press mehr", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-floor-press-mehr-b34710868b", - "name": "Floor Press mehr", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-overhead-leicht-7f9274fd29", - "name": "Overhead leicht", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-overhead-leicht-3413a54a3a", - "name": "Overhead leicht", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-overhead-mittel-4b236720a7", - "name": "Overhead mittel", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-overhead-mittel-ea724f9a42", - "name": "Overhead mittel", - "factor": 1.0, - "movement_cluster": "vertical_push", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-y-t-w-superman-1d331ef219": { - "id": "movement-y-t-w-superman-1d331ef219", - "name": "Y-T-W / Superman", - "progression_id": "sup", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "auto", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-superman-hold-06efa2683a", - "name": "Superman Hold", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-34af38f543", - "name": "Superman Hold", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-laenger-b383f0e540", - "name": "Superman Hold länger", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-superman-hold-laenger-6b326b0a11", - "name": "Superman Hold länger", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "seconds", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-reverse-snow-angels-871916bcee", - "name": "Reverse Snow Angels", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-reverse-snow-angels-78c24f8835", - "name": "Reverse Snow Angels", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-y-t-w-raises-b7c40c3579", - "name": "Y-T-W Raises", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-y-t-w-raises-a6d76cb15c", - "name": "Y-T-W Raises", - "factor": 1.0, - "movement_cluster": "general", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-kniebeuge-leicht-4ae3fb9fc0": { - "id": "movement-kniebeuge-leicht-4ae3fb9fc0", - "name": "Kniebeuge (leicht)", - "progression_id": "name:kniebeuge-leicht", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-liegestuetze-1-stufe-leichter-6033b9cb92": { - "id": "movement-liegestuetze-1-stufe-leichter-6033b9cb92", - "name": "Liegestütze (1 Stufe leichter)", - "progression_id": "name:liegestuetze-1-stufe-leichter", - "movement_cluster": "horizontal_push", - "movement_label": "Push-up", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-rudern-leicht-566f8de8f2": { - "id": "movement-rudern-leicht-566f8de8f2", - "name": "Rudern (leicht)", - "progression_id": "name:rudern-leicht", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "result_schema": { - "mode": "reps", - "weight_mode": "optional", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-mountain-climbers-locker-e4a7854ab9": { - "id": "movement-mountain-climbers-locker-e4a7854ab9", - "name": "Mountain Climbers (locker)", - "progression_id": "name:mountain-climbers-locker", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-inchworm-50016ff2a4": { - "id": "movement-inchworm-50016ff2a4", - "name": "Inchworm", - "progression_id": "name:inchworm", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-cat-cow-d8e060c65f": { - "id": "movement-cat-cow-d8e060c65f", - "name": "Cat-Cow", - "progression_id": "name:cat-cow", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-bird-dog-f2c9d49c80": { - "id": "movement-bird-dog-f2c9d49c80", - "name": "Bird Dog", - "progression_id": "name:bird-dog", - "movement_cluster": "general", - "movement_label": "Allgemein", - "result_schema": { - "mode": "reps", - "weight_mode": "none", - "laterality": "unilateral", - "sides_mode": "same" - }, - "variants": [] - }, - "movement-kb-halo-leicht-e3c156d79e": { - "id": "movement-kb-halo-leicht-e3c156d79e", - "name": "KB-Halo (leicht)", - "progression_id": "name:kb-halo-leicht", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [] - }, - "movement-kb-swing-zweiarmig-327d7b5ad6": { - "id": "movement-kb-swing-zweiarmig-327d7b5ad6", - "name": "KB Swing (zweiarmig)", - "progression_id": "swing", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-dead-stop-leicht-technik-02548f1a05", - "name": "Dead-Stop, leicht (Technik)", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-dead-stop-leicht-technik-3b9715b104", - "name": "Dead-Stop, leicht (Technik)", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-dead-stop-mehr-reps-7be430a756", - "name": "Dead-Stop, mehr Reps", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-dead-stop-mehr-reps-5c68075818", - "name": "Dead-Stop, mehr Reps", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-durchgehend-mittel-183cc7dfa6", - "name": "durchgehend, mittel", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-durchgehend-mittel-7762c8f240", - "name": "durchgehend, mittel", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-durchgehend-schwerer-6da670bed8", - "name": "durchgehend, schwerer", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-durchgehend-schwerer-a1609b5bad", - "name": "durchgehend, schwerer", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-goblet-squat-kb-f065dfd5b6": { - "id": "movement-goblet-squat-kb-f065dfd5b6", - "name": "Goblet Squat (KB)", - "progression_id": "gobletkb", - "movement_cluster": "knee_dominant", - "movement_label": "Kniebeuge / Ausfallschritt", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-tiefe-lernen-77201f2b6b", - "name": "leicht, Tiefe lernen", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-tiefe-lernen-70ea0f2e9b", - "name": "leicht, Tiefe lernen", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-57415830b2", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-abf827246a", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-2d542b9c09", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-eda93ad7ea", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-3s-runter-9dd7fec573", - "name": "mittel, 3s runter", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-3s-runter-3f76b2391b", - "name": "mittel, 3s runter", - "factor": 1.0, - "movement_cluster": "knee_dominant", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-kb-deadlift-beidbeinig-6f7c8bca76": { - "id": "movement-kb-deadlift-beidbeinig-6f7c8bca76", - "name": "KB-Deadlift (beidbeinig)", - "progression_id": "kbdl", - "movement_cluster": "hip_hinge", - "movement_label": "Hip Hinge / Hüftstreckung", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - }, - "variants": [ - { - "id": "step-leicht-technik-54ca22fe3d", - "name": "leicht, Technik", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-technik-fcd2dc3979", - "name": "leicht, Technik", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-db29d97bd7", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-leicht-mehr-reps-fd7e87b98b", - "name": "leicht, mehr Reps", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-7ed7284a10", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-mittel-e9887f19f9", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-schwerer-kontrolliert-dd0f4f4538", - "name": "schwerer, kontrolliert", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - }, - { - "id": "step-schwerer-kontrolliert-ab6af6caea", - "name": "schwerer, kontrolliert", - "factor": 1.0, - "movement_cluster": "hip_hinge", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "bilateral", - "sides_mode": "same" - } - } - ] - }, - "movement-halo-faca9a5cc9": { - "id": "movement-halo-faca9a5cc9", - "name": "Halo", - "progression_id": "halo", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-langsam-5d15ba8134", - "name": "leicht, langsam", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-langsam-c2a5c59e62", - "name": "leicht, langsam", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-81a736180a", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-fe775a4a23", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-c5c0bf8989", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-3898a7cea7", - "name": "mittel", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-langsam-c9db464d00", - "name": "mittel, langsam", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-mittel-langsam-e1a2e46fd3", - "name": "mittel, langsam", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-einarmiges-rudern-kb-0ec1c11b35": { - "id": "movement-einarmiges-rudern-kb-0ec1c11b35", - "name": "Einarmiges Rudern (KB)", - "progression_id": "row1", - "movement_cluster": "horizontal_pull", - "movement_label": "Rudern", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-leicht-db8c75b358", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-leicht-d1e4feaafa", - "name": "leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-etwas-mehr-19ded398d6", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-etwas-mehr-0ce270eb41", - "name": "etwas mehr", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-einarmig-leicht-a6f388b349", - "name": "einarmig leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-einarmig-leicht-a9539fbadb", - "name": "einarmig leicht", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-einarmig-mittel-74f0fe3e1e", - "name": "einarmig mittel", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-einarmig-mittel-3f961280f6", - "name": "einarmig mittel", - "factor": 1.0, - "movement_cluster": "horizontal_pull", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-bottoms-up-kb-hold-griff-4a5f657f41": { - "id": "movement-bottoms-up-kb-hold-griff-4a5f657f41", - "name": "Bottoms-up KB-Hold (Griff)", - "progression_id": "carry", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-im-stand-10-15s-e90dd57ff1", - "name": "im Stand 10–15s", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-im-stand-10-15s-db8eec9df0", - "name": "im Stand 10–15s", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-15-20s-ruhig-321e582ab2", - "name": "15–20s, ruhig", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-15-20s-ruhig-7db6eb1a01", - "name": "15–20s, ruhig", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-bis-schulterhoehe-15s-f1763ea9e5", - "name": "bis Schulterhöhe 15s", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-bis-schulterhoehe-15s-21c8fd4d01", - "name": "bis Schulterhöhe 15s", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-laenger-ein-paar-schritte-fa5838da65", - "name": "länger / ein paar Schritte", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-laenger-ein-paar-schritte-ec93f04473", - "name": "länger / ein paar Schritte", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "seconds", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - }, - "movement-around-the-world-uppercut-kb-7ea726bbde": { - "id": "movement-around-the-world-uppercut-kb-7ea726bbde", - "name": "Around-the-World + Uppercut (KB)", - "progression_id": "atw", - "movement_cluster": "loaded_mobility", - "movement_label": "Kettlebell-Kontrolle", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - }, - "variants": [ - { - "id": "step-nur-around-the-world-leicht-97c90b17c5", - "name": "nur Around-the-World, leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-nur-around-the-world-leicht-fd2fab13cb", - "name": "nur Around-the-World, leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-around-the-world-mehr-reps-7af159bd81", - "name": "Around-the-World, mehr Reps", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-around-the-world-mehr-reps-43f1762f06", - "name": "Around-the-World, mehr Reps", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-uppercut-leicht-cb2ce4fe29", - "name": "+ Uppercut, leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-uppercut-leicht-db86827e21", - "name": "+ Uppercut, leicht", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-uppercut-fluessig-4b3453d5f9", - "name": "+ Uppercut, flüssig", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - }, - { - "id": "step-uppercut-fluessig-2463ac3c0e", - "name": "+ Uppercut, flüssig", - "factor": 1.0, - "movement_cluster": "loaded_mobility", - "result_schema": { - "mode": "reps", - "weight_mode": "required", - "laterality": "unilateral", - "sides_mode": "separate" - } - } - ] - } - } - } -}