diff --git a/backend/routers/exercises.py b/backend/routers/exercises.py
index fc4cef0..6ac2b89 100644
--- a/backend/routers/exercises.py
+++ b/backend/routers/exercises.py
@@ -26,6 +26,24 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["exercises"])
+
+def _coerce_json_str_list(val: Any) -> List[str]:
+ """JSON-Aggregat oder JSON-String aus PG in eine saubere str-Liste für die Listen-API."""
+ if val is None:
+ return []
+ if isinstance(val, list):
+ return [str(x) for x in val if x is not None and str(x).strip()]
+ if isinstance(val, str):
+ try:
+ parsed = json.loads(val)
+ if isinstance(parsed, list):
+ return [str(x) for x in parsed if x is not None and str(x).strip()]
+ except Exception:
+ return []
+ return []
+ return []
+
+
# Kanonische Fähigkeitsstufen 1–5 (Übung ↔ Skill-Zeile), siehe Migration 029
_CANONICAL_SKILL_LEVELS = frozenset(
{"basis", "grundlagen", "aufbau", "fortgeschritten", "optimierung"}
@@ -971,7 +989,34 @@ def list_exercises(
WHERE efa.exercise_id = e.id
ORDER BY efa.is_primary DESC NULLS LAST, fa.name ASC
LIMIT 1
- ) AS primary_focus_name
+ ) AS primary_focus_name,
+ (
+ SELECT COALESCE(
+ json_agg(fa.name ORDER BY efa.is_primary DESC NULLS LAST, fa.name ASC),
+ '[]'::json
+ )
+ FROM exercise_focus_areas efa
+ JOIN focus_areas fa ON fa.id = efa.focus_area_id
+ WHERE efa.exercise_id = e.id
+ ) AS focus_area_names,
+ (
+ SELECT COALESCE(
+ json_agg(sd.name ORDER BY esd.is_primary DESC NULLS LAST, sd.name ASC),
+ '[]'::json
+ )
+ FROM exercise_style_directions esd
+ JOIN style_directions sd ON sd.id = esd.style_direction_id
+ WHERE esd.exercise_id = e.id
+ ) AS style_direction_names,
+ (
+ SELECT COALESCE(
+ json_agg(tt.name ORDER BY ett.is_primary DESC NULLS LAST, tt.sort_order NULLS LAST, tt.name ASC),
+ '[]'::json
+ )
+ FROM exercise_training_types ett
+ JOIN training_types tt ON tt.id = ett.training_type_id
+ WHERE ett.exercise_id = e.id
+ ) AS training_type_names
{variants_sql}
FROM exercises e
LEFT JOIN profiles p ON e.created_by = p.id
@@ -990,6 +1035,9 @@ def list_exercises(
d = r2d(r)
pfn = d.get("primary_focus_name")
d["focus_area"] = pfn
+ d["focus_area_names"] = _coerce_json_str_list(d.get("focus_area_names"))
+ d["style_direction_names"] = _coerce_json_str_list(d.get("style_direction_names"))
+ d["training_type_names"] = _coerce_json_str_list(d.get("training_type_names"))
if include_variants:
v = d.get("variants")
if isinstance(v, str):
diff --git a/frontend/src/app.css b/frontend/src/app.css
index bea00ec..e3a636a 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -1442,12 +1442,6 @@ button.capture-shell__nav-item {
.skills-page__tabs-scroll::-webkit-scrollbar {
display: none;
}
-
- .exercises-page-mode-switch,
- .skills-page-mode-switch {
- width: max(100%, min(20rem, 100vw - 24px));
- max-width: none;
- }
}
/* Trainingsplanung: kompakte Segmente (Gruppe / Verein) */
@@ -2463,6 +2457,12 @@ button.capture-shell__nav-item {
flex: 1;
}
+.clubs-page__intro {
+ margin: 0 0 1.25rem;
+ max-width: 46rem;
+ line-height: 1.55;
+}
+
/* Übungsliste: Kopf, Modus-Segmente, Hinweise */
.exercises-page__header {
display: flex;
@@ -2478,11 +2478,6 @@ button.capture-shell__nav-item {
.exercises-page-toolbar-tabs {
margin-bottom: 14px;
}
-.exercises-page-mode-switch,
-.skills-page-mode-switch {
- width: 100%;
- max-width: min(100%, 28rem);
-}
.exercise-search-hint {
font-size: 12px;
color: var(--text3);
@@ -2515,14 +2510,24 @@ button.capture-shell__nav-item {
}
.exercises-list-grid {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
- gap: 12px;
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
+ gap: 14px;
+ align-items: stretch;
+}
+.exercises-list-grid > .exercise-card {
+ height: 100%;
+ min-height: 0;
}
.exercise-card-layout {
display: flex;
gap: 10px;
align-items: flex-start;
}
+.exercise-card-layout--grow {
+ flex: 1 1 auto;
+ min-height: 0;
+ width: 100%;
+}
.exercise-card-layout__check {
margin-top: 4px;
flex-shrink: 0;
@@ -2562,6 +2567,28 @@ button.capture-shell__nav-item {
line-height: 1.4;
margin: 0;
}
+.exercise-card-summary--rich {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 4;
+ overflow: hidden;
+ word-break: break-word;
+}
+.exercise-card-summary--rich b,
+.exercise-card-summary--rich strong {
+ font-weight: 700;
+ color: var(--text1);
+}
+.exercise-card-summary--rich i,
+.exercise-card-summary--rich em {
+ font-style: italic;
+}
+.exercise-card-summary--rich p {
+ margin: 0 0 0.35em;
+}
+.exercise-card-summary--rich p:last-child {
+ margin-bottom: 0;
+}
.exercises-meta-line {
font-size: 13px;
color: var(--text2);
@@ -3665,7 +3692,31 @@ button.capture-shell__nav-item {
.exercise-card {
display: flex;
flex-direction: column;
- min-height: 200px;
+ min-height: 0;
+ border-left: 4px solid var(--border2);
+ transition: border-color 0.15s, box-shadow 0.15s;
+}
+.exercise-card--scope-official {
+ border-left-color: var(--accent);
+ background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 7%, var(--surface)) 0%, var(--surface) 64%);
+}
+.exercise-card--scope-club {
+ border-left-color: var(--warn);
+ background: linear-gradient(180deg, color-mix(in srgb, var(--warn) 10%, var(--surface)) 0%, var(--surface) 64%);
+}
+.exercise-card--scope-private {
+ border-left-color: var(--text3);
+}
+.exercise-card--mine {
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 28%, var(--border));
+}
+@media (prefers-color-scheme: dark) {
+ .exercise-card--scope-official {
+ background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 12%, var(--surface)) 0%, var(--surface) 64%);
+ }
+ .exercise-card--scope-club {
+ background: linear-gradient(180deg, color-mix(in srgb, var(--warn) 12%, var(--surface)) 0%, var(--surface) 64%);
+ }
}
.exercise-card__body {
flex: 1 1 auto;
@@ -3714,10 +3765,51 @@ button.capture-shell__nav-item {
display: flex;
gap: 6px;
flex-wrap: wrap;
- margin-top: 12px;
+ margin-top: auto;
padding-top: 10px;
border-top: 1px solid var(--border);
}
+.exercise-card__actions--icons {
+ justify-content: flex-end;
+ gap: 8px;
+ flex-wrap: nowrap;
+}
+.exercise-card__icon-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ padding: 0;
+ border-radius: 8px;
+ border: 1px solid var(--border2);
+ background: var(--surface2);
+ color: var(--text1);
+ text-decoration: none;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
+ -webkit-tap-highlight-color: transparent;
+}
+.exercise-card__icon-btn:hover {
+ background: var(--surface);
+ border-color: var(--accent);
+ color: var(--accent-dark);
+}
+@media (prefers-color-scheme: dark) {
+ .exercise-card__icon-btn:hover {
+ color: var(--accent);
+ }
+}
+.exercise-card__icon-btn--danger {
+ color: var(--danger);
+ border-color: color-mix(in srgb, var(--danger) 35%, var(--border2));
+}
+.exercise-card__icon-btn--danger:hover {
+ background: color-mix(in srgb, var(--danger) 10%, var(--surface2));
+ border-color: var(--danger);
+ color: var(--danger);
+}
.exercise-card__actions .btn,
.exercise-card__actions a.btn {
flex: 1 1 auto;
@@ -3747,6 +3839,28 @@ button.capture-shell__nav-item {
color: var(--accent-dark);
border-color: transparent;
}
+.exercise-tag--style {
+ background: color-mix(in srgb, var(--accent) 12%, var(--surface2));
+ color: var(--accent-dark);
+ border-color: color-mix(in srgb, var(--accent) 22%, var(--border));
+}
+.exercise-tag--training {
+ background: var(--surface2);
+ color: var(--text1);
+ border-color: var(--border2);
+}
+.exercise-tag--scope {
+ font-weight: 700;
+ background: var(--surface);
+ color: var(--text2);
+}
+.exercise-tag--meta {
+ font-weight: 600;
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+ color: var(--text3);
+}
.exercise-detail-shell {
max-width: none;
diff --git a/frontend/src/pages/AdminMaturityModelsPage.jsx b/frontend/src/pages/AdminMaturityModelsPage.jsx
index a1087cf..109a29f 100644
--- a/frontend/src/pages/AdminMaturityModelsPage.jsx
+++ b/frontend/src/pages/AdminMaturityModelsPage.jsx
@@ -27,12 +27,14 @@ export default function AdminMaturityModelsPage() {
-
+