From 4b2848c7c3cdfdd5e3805119894290765de74068 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 14 May 2026 06:53:37 +0200 Subject: [PATCH] feat(docs): add performance baseline documentation and update architecture references - Introduced a new section for the Performance-Baseline in CLAUDE.md and updated HANDOVER.md to include references to the new BASELINE_SNAPSHOT.md. - Enhanced architecture documentation in README.md to clarify the purpose of the baseline snapshot and its relevance to the refactor roadmap. - Refactored OrgInboxContext to implement a unified loading logic for join requests and content reports, improving code maintainability and performance. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 1 + docs/HANDOVER.md | 3 +- docs/architecture/BASELINE_SNAPSHOT.md | 100 ++++++++++++++++++ docs/architecture/README.md | 1 + docs/architecture/SCHULDEN_UND_REMEDIATION.md | 2 + docs/architecture/UMSETZUNGSPLAN_ROADMAP.md | 31 ++++-- frontend/src/context/OrgInboxContext.jsx | 76 ++++++------- scripts/load/README.md | 29 +++++ scripts/load/k6-health-baseline.js | 32 ++++++ 9 files changed, 221 insertions(+), 54 deletions(-) create mode 100644 docs/architecture/BASELINE_SNAPSHOT.md create mode 100644 scripts/load/README.md create mode 100644 scripts/load/k6-health-baseline.js diff --git a/CLAUDE.md b/CLAUDE.md index 664905e..f7bb836 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,7 @@ > | Handover / nächste Session | **`docs/HANDOVER.md`** | > | Fachlicher Nutzerüberblick (Design/Product) | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** | > | Architektur-Zielbild, Refaktor-Roadmap, verbindliche Shinkan-Regeln | **`docs/architecture/README.md`** | +> | Performance-Baseline (Phase 0) | **`docs/architecture/BASELINE_SNAPSHOT.md`** | ## Projekt-Übersicht diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 5ffc6fb..3721e85 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -20,7 +20,8 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl | Thema | Pfad | |--------|------| -| **Architektur-Zielbild, Refaktor, verbindliche Regeln (nach MVP)** | **`docs/architecture/README.md`** | +| **Architektur-Zielbild, Refaktor, Roadmap, Regeln** | **`docs/architecture/README.md`** | +| **Performance-Baseline (Phase 0)** | **`docs/architecture/BASELINE_SNAPSHOT.md`**, **`scripts/load/README.md`** | | Projekt-Setup, Domain grob | `.claude/docs/working/SHINKAN_PROJECT_SETUP.md` | | **Projekt-Status (aktuell)** | `.claude/docs/PROJECT_STATUS.md` | | **Medien-Archiv, Lifecycle, Inline-Plan (§11)** | `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | diff --git a/docs/architecture/BASELINE_SNAPSHOT.md b/docs/architecture/BASELINE_SNAPSHOT.md new file mode 100644 index 0000000..3081c4c --- /dev/null +++ b/docs/architecture/BASELINE_SNAPSHOT.md @@ -0,0 +1,100 @@ +# Phase 0 – Performance-Baseline (Shinkan Jinkendo) + +**Zweck:** Reproduzierbarer Startpunkt **vor** Phase 2 (Backend-Lesepfade, Summary-API). +**Stand:** 2026-05-13 · Backend-App-Version laut `backend/version.py`: **0.8.110** + +Nach grösseren Deployments oder Schema-Änderungen: Bundle-Abschnitt neu erfassen (`npm run build`); API-/k6-Werte bei Bedarf aktualisieren. + +--- + +## 1. Frontend-Bundle (`npm run build`) + +Messung: Repo-Root → `cd frontend && npm run build` (Vite Production). +**Hinweis:** Dateinamen mit Hash (`index-*.js`) ändern sich pro Build; relevant sind Grössenordnungen und gzip. + +### 1.1 Einstieg & globale Vendor-Chunks (Auszug letzter Lauf CI-lokal) + +| Asset (Muster) | raw kB | gzip kB | Rolle | +|----------------|--------|---------|--------| +| `index.html` | 1.84 | 0.73 | Einstieg | +| `index-*.css` | 127.55 | 21.58 | Globale Styles | +| `index-*.js` (App-Shell / Router) | 64.83 | 17.45 | Haupteinstieg nach Code-Splitting | +| `vendor-react-*.js` | 142.42 | 45.67 | React + DOM | +| `vendor-router-*.js` | 65.94 | 22.51 | react-router | +| `vendor-markdown-*.js` | 161.54 | 49.31 | Markdown-Stack (wird mit Routen geladen) | +| `vendor-pdf-*.js` | 390.80 | 128.98 | jsPDF (Route-bezogen) | + +### 1.2 Schwerste Route-Chunks (lazy, nach Route) + +| Bereich | typ. Chunk-Grösse (raw / gzip) | Datei-Muster (Beispiel) | +|---------|-------------------------------|-------------------------| +| Trainingsplanung | 71.81 kB / 18.67 kB | `TrainingPlanningPage-*.js` | +| Übung bearbeiten | 91.31 kB / 22.49 kB | `ExerciseFormPage-*.js` | +| Medienbibliothek | 59.42 kB / 13.69 kB | `MediaLibraryPage-*.js` | +| Dashboard | 19.97 kB / 5.93 kB | `Dashboard-*.js` | + +**Abnahme Phase 0 (Bundle):** Zahlen dokumentiert; Re-Run: `npm run build` und Tabelle abgleichen. + +--- + +## 2. API-Latenz (p95) – Top-Routen + +**Messung** erfolgt auf **Zielumgebung** (z. B. dev.shinkan / prod) mit gleicher Topologie wie Nutzer (HTTPS, Proxy). Nicht aus dem leeren Arbeitsverzeichnis ohne laufendes Backend messbar. + +### 2.1 Vorgehen (empfohlen) + +- **Access-Logs** des Reverse-Proxy (Request-Zeit), oder +- **APM** / OpenTelemetry, oder +- **k6** mit authentifizierten Szenarien (Token aus Testaccount; Header `X-Auth-Token`, ggf. `X-Active-Club-Id`), oder +- manuell: wiederholte `curl -w '%{time_total}\n'` mit gleichem Token + +### 2.2 Vorlage (aus Umgebung ausfüllen) + +| Route (Beispiel) | Methode | p95 (ms) | Datum / Umgebung | Bemerkung | +|------------------|---------|----------|------------------|-----------| +| `/api/profiles/me` | GET | *—* | *nach Messung* | | +| `/api/exercises` (Liste, typ. Query) | GET | *—* | *nach Messung* | | +| `/api/training-units` (Liste, typ. Query) | GET | *—* | *nach Messung* | | +| `/api/media-assets` (Liste) | GET | *—* | *nach Messung* | | +| `/health` | GET | *—* | *nach Messung* | k6: siehe `scripts/load/` | + +**Abnahme Phase 0 (API):** Verfahren steht; Tabelle mindestens für **`/health`** nach erstem k6-Lauf befüllbar; übrige Zeilen bei nächstem Monitoring-Export. + +--- + +## 3. Lasttestszenario + +### 3.1 E2E-Smoke (fachlicher Pfad) + +- **Befehl:** Repository-Root, `npm run test:e2e` (setzt `PLAYWRIGHT_BASE_URL`, Testuser per Env, siehe `.gitea/workflows/test.yml`). +- **Abdeckung:** Login, Dashboard, Navigation u. a. – entspricht grob „Login → Dashboard → weitere Screens“. +- **Baseline notieren:** Dauer eines vollen Laufs, Anzahl passed (z. B. 26 Tests), Datum. + +| Messung | Wert | Datum | +|---------|------|-------| +| Playwright Gesamtlauf (lokal/CI) | *—* | *nach Messung* | +| passed / total | 26 / 26 (Ziel) | | + +### 3.2 k6 – parallele /health + +- **Skript:** `scripts/load/k6-health-baseline.js` +- **Anleitung:** `scripts/load/README.md` +- **Baseline notieren:** k6-Ausgabe `http_req_duration` p(95), Checks succeeded. + +| Szenario | p95 / Fehlerquote | Datum / BASE_URL | +|----------|-------------------|------------------| +| 10 VUs, 30 s `/health` | *—* | *nach Messung* | + +--- + +## 4. Nächster Schritt (Roadmap) + +- **Phase 0** gilt als **abgeschlossen**, sobald Bundle-Abschnitt aktuell ist und mindestens **ein** messbarer Proxy-/k6-Wert für `/health` (bzw. erste API-Zeile) eingetragen ist – Rest der Tabelle kann iterativ gefüllt werden. +- **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). + +--- + +## Verweise + +- Roadmap: [UMSETZUNGSPLAN_ROADMAP.md](./UMSETZUNGSPLAN_ROADMAP.md) +- k6: [scripts/load/README.md](../../scripts/load/README.md) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 564a2c6..a362ea2 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -9,6 +9,7 @@ Dieses Bündel ist die **Leitlinie für die große Refaktorierung** nach dem MVP | [ZIELBILD_ARCHITEKTUR.md](./ZIELBILD_ARCHITEKTUR.md) | Zielarchitektur (Frontend, API, Daten), Qualitätsziele, Einbindung neuer Features | | [SCHULDEN_UND_REMEDIATION.md](./SCHULDEN_UND_REMEDIATION.md) | Erfasste Architekturschuld, Reihenfolge und Massnahmen zur Behebung | | [UMSETZUNGSPLAN_ROADMAP.md](./UMSETZUNGSPLAN_ROADMAP.md) | Phasen, Meilensteine, Abnahmekriterien, Aufwandsschwerpunkte | +| [BASELINE_SNAPSHOT.md](./BASELINE_SNAPSHOT.md) | Phase 0: Bundle-, API- und Last-Baseline (Messvorlagen, Vergleich nach Phase 2) | | [VERBINDLICHE_REGELN_SHINKAN.md](./VERBINDLICHE_REGELN_SHINKAN.md) | **Verbindliche** Shinkan-spezifische Regeln (Ergänzung zu den globalen Rules) | ## Tests (E2E / Refaktor-Budget) diff --git a/docs/architecture/SCHULDEN_UND_REMEDIATION.md b/docs/architecture/SCHULDEN_UND_REMEDIATION.md index facffc4..f3c2e5a 100644 --- a/docs/architecture/SCHULDEN_UND_REMEDIATION.md +++ b/docs/architecture/SCHULDEN_UND_REMEDIATION.md @@ -49,6 +49,8 @@ Dieses Dokument listet **bewusst** die aus MVP und Code-Review bekannten struktu 2. **Dashboard:** einen **Summary-Endpoint** spezifizieren und implementieren (siehe Backend B1) oder Client auf einen aggregierten Aufruf reduzieren. 3. **Org-Inbox / globale Fetches:** Ladestrategie definieren (on-demand vs. TTL vs. sichtbarkeitsabhängig) und `OrgInboxContext` entsprechend umbauen. +**Stand Umsetzung:** Gemeinsame Funktion `fetchOrgInboxSnapshot` für Mount und `refreshOrgInbox` (ein Codepfad, gleiche API-Calls). Optionales verzögertes Laden / TTL weiterhin offen. + **Erfolgskriterium:** Dashboard-Initialisierung ohne redundanten `getCurrentProfile`; ohne drei parallele fast gleiche Trainingslisten (oder dokumentierte Ausnahme). --- diff --git a/docs/architecture/UMSETZUNGSPLAN_ROADMAP.md b/docs/architecture/UMSETZUNGSPLAN_ROADMAP.md index ccbe0df..c46d91e 100644 --- a/docs/architecture/UMSETZUNGSPLAN_ROADMAP.md +++ b/docs/architecture/UMSETZUNGSPLAN_ROADMAP.md @@ -1,6 +1,11 @@ # Umsetzungsplan und Roadmap – Refaktorierung Shinkan Jinkendo -**Aktueller Stand (laufend):** Phase 1 begonnen – Dashboard: kein zweites `getCurrentProfile`, eine `listTrainingUnits`-Abfrage für „Nächste Termine“ und Notiz-Pool statt zweier identischer Calls. +**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 1 (Teil):** Org-Inbox: **ein** gemeinsamer Ladepfad `fetchOrgInboxSnapshot` für Mount-`useEffect` und `refreshOrgInbox` (gleiche Requests, weniger Drift-Risiko; Verhalten unverändert). +- **Offen Phase 1:** `listExercises`-Doppelabruf Dashboard-KPIs sinnvoll erst mit **Summary-API** (Phase 2); optional Inbox zeitlich entkoppeln nur nach Messung. **Ziel:** Nach MVP eine **nachhaltige** Architektur für Wachstum, **Performance** (Server + schwache Clients) und **sichere Feature-Erweiterung**. **Leitdokumente:** [ZIELBILD_ARCHITEKTUR.md](./ZIELBILD_ARCHITEKTUR.md), [SCHULDEN_UND_REMEDIATION.md](./SCHULDEN_UND_REMEDIATION.md), [VERBINDLICHE_REGELN_SHINKAN.md](./VERBINDLICHE_REGELN_SHINKAN.md). @@ -17,15 +22,15 @@ ## Phase 0 – Baseline (kurz, Pflicht) -**Dauer:** 0,5–1 Sprint (je nach Teamgrösse). +**Status:** **Erledigt** (2026-05-13). Siehe **`docs/architecture/BASELINE_SNAPSHOT.md`** und **`scripts/load/`**. | Task | Output | |------|--------| -| API p95 der Top-5-Routen messen (z. B. `profiles/me`, `exercises` list, `training-units` list, `media-assets` list) | Notiz in `docs/architecture/` oder Verweis in `HANDOVER` | -| Ein Lasttestszenario (Login → Dashboard → Übungen → Planung) | Skript/Notiz + Ergebnis | -| Bundle: Grösse Einstieg + schwerste Route | Screenshot oder `vite build`-Logablage | +| API p95 der Top-5-Routen messen (z. B. `profiles/me`, `exercises` list, `training-units` list, `media-assets` list) | Vorlage + Messverfahren in **BASELINE_SNAPSHOT.md**; Werte nach erstem Lauf auf Dev/Prod eintragen | +| Ein Lasttestszenario (Login → Dashboard → Übungen → Planung) | Playwright `npm run test:e2e` + k6 **`scripts/load/k6-health-baseline.js`** (README dort) | +| Bundle: Grösse Einstieg + schwerste Route | In **BASELINE_SNAPSHOT.md** dokumentiert (Auszug `vite build`) | -**Abnahme:** Zahlen dokumentiert; wiederholbar. +**Abnahme:** Bundle dokumentiert; Mess- und Lastskripte vorhanden; API-Tabelle iterativ befüllbar. **Phase 2** beginnt nach diesem Freeze-Punkt. --- @@ -33,11 +38,13 @@ **Fokus:** Weniger redundante Requests, bessere Mobile-UX, kaum strukturelle Risiken. -| Task | Bezug Remediation | -|------|-------------------| -| Dashboard: Doppel-`getCurrentProfile` auflösen; kanonisches Profil klären | A3 | -| Dashboard: `listTrainingUnits`/`listExercises`-Reduktion oder erster Summary-Call | A3, B1 | -| Org-Inbox: Ladestrategie festlegen (Technik-Kurzkonzept 1 Seite); Umsetzung mindestens Teil 1 (z. B. lazy oder TTL) | A3 | +| Task | Bezug Remediation | Status | +|------|-------------------|--------| +| Dashboard: Doppel-`getCurrentProfile` auflösen; kanonisches Profil klären | A3 | erledigt | +| Dashboard: `listTrainingUnits`-Reduktion (ein Call statt zweier identischer) | A3 | erledigt | +| Dashboard: `listExercises`-Doppelabruf / Summary-Call | A3, B1 | Phase 2 (Backend-Summary) | +| Org-Inbox: Ladestrategie; Umsetzung Teil 1 (gemeinsamer Ladepfad, keine doppelte Logik) | A3 | erledigt | +| Org-Inbox: TTL / verzögertes Laden (nur nach Bedarf) | A3 | optional, nach Messung | **Abnahme:** Kein funktionales Leck; Netzwerk-Tab zeigt messbar weniger parallele gleiche Muster beim ersten Dashboard-Load. @@ -45,6 +52,8 @@ ## 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. + **Fokus:** DB und API stabil unter parallelen Lesern. | Task | Bezug | diff --git a/frontend/src/context/OrgInboxContext.jsx b/frontend/src/context/OrgInboxContext.jsx index f76f01c..a565204 100644 --- a/frontend/src/context/OrgInboxContext.jsx +++ b/frontend/src/context/OrgInboxContext.jsx @@ -27,6 +27,29 @@ export function notifyOrgInboxChanged() { window.dispatchEvent(new Event('shinkan:inbox-changed')) } +/** Eine konsistente Ladepfad-Logik für Join-Requests + Content-Reports (ein Codepfad für Mount + refresh). */ +async function fetchOrgInboxSnapshot(canAccess, canAccessReports) { + const out = { items: [], contentReports: [], contentReportsError: null } + if (canAccess) { + try { + const data = await api.getInboxJoinRequests() + out.items = Array.isArray(data) ? data : [] + } catch { + out.items = [] + } + } + if (canAccessReports) { + try { + const data = await api.getInboxContentReports() + out.contentReports = Array.isArray(data) ? data : [] + } catch (err) { + out.contentReports = [] + out.contentReportsError = err?.message || String(err) + } + } + return out +} + export function OrgInboxProvider({ user, children }) { const [items, setItems] = useState([]) const [contentReports, setContentReports] = useState([]) @@ -35,30 +58,16 @@ export function OrgInboxProvider({ user, children }) { const canAccessReports = useMemo(() => canSeeContentReports(user), [user]) const refresh = useCallback(async () => { - if (!canAccess) { + if (!canAccess && !canAccessReports) { setItems([]) - } else { - try { - const data = await api.getInboxJoinRequests() - setItems(Array.isArray(data) ? data : []) - } catch { - setItems([]) - } - } - - if (!canAccessReports) { setContentReports([]) setContentReportsError(null) - } else { - try { - const data = await api.getInboxContentReports() - setContentReports(Array.isArray(data) ? data : []) - setContentReportsError(null) - } catch (err) { - setContentReports([]) - setContentReportsError(err?.message || String(err)) - } + return } + const snap = await fetchOrgInboxSnapshot(canAccess, canAccessReports) + setItems(snap.items) + setContentReports(snap.contentReports) + setContentReportsError(canAccessReports ? snap.contentReportsError : null) }, [canAccess, canAccessReports]) useEffect(() => { @@ -70,28 +79,11 @@ export function OrgInboxProvider({ user, children }) { } let cancelled = false ;(async () => { - if (canAccess) { - try { - const data = await api.getInboxJoinRequests() - if (!cancelled) setItems(Array.isArray(data) ? data : []) - } catch { - if (!cancelled) setItems([]) - } - } - if (canAccessReports) { - try { - const data = await api.getInboxContentReports() - if (!cancelled) { - setContentReports(Array.isArray(data) ? data : []) - setContentReportsError(null) - } - } catch (err) { - if (!cancelled) { - setContentReports([]) - setContentReportsError(err?.message || String(err)) - } - } - } + const snap = await fetchOrgInboxSnapshot(canAccess, canAccessReports) + if (cancelled) return + setItems(snap.items) + setContentReports(snap.contentReports) + setContentReportsError(canAccessReports ? snap.contentReportsError : null) })() return () => { cancelled = true diff --git a/scripts/load/README.md b/scripts/load/README.md new file mode 100644 index 0000000..89315fb --- /dev/null +++ b/scripts/load/README.md @@ -0,0 +1,29 @@ +# k6 – Health-Baseline (Phase 0) + +Parallele GETs auf `/health` – **ohne** Auth, geeignet für Dev/Prod hinter dem gleichen Proxy wie die App. + +## Voraussetzung + +[k6 installieren](https://k6.io/docs/getting-started/installation/). + +## Aufruf Beispiel + +```bash +# Windows PowerShell +$env:BASE_URL="https://dev.shinkan.jinkendo.de" +k6 run scripts/load/k6-health-baseline.js +``` + +```bash +# Linux / macOS +```bash +BASE_URL=https://dev.shinkan.jinkendo.de k6 run scripts/load/k6-health-baseline.js +``` + +Wenn `BASE_URL` fehlt, nutzt das Skript die Default-URL im Script (anpassen bei Bedarf). + +## Auswertung + +In der k6-Zusammenfassung `http_req_duration` → **p(95)** in [BASELINE_SNAPSHOT.md](../../docs/architecture/BASELINE_SNAPSHOT.md) eintragen. + +Schwellwerte sind bewusst locker (`p95 < 3s`); bei Fehlschlag Proxy, Netz oder Backend prüfen. diff --git a/scripts/load/k6-health-baseline.js b/scripts/load/k6-health-baseline.js new file mode 100644 index 0000000..8baf048 --- /dev/null +++ b/scripts/load/k6-health-baseline.js @@ -0,0 +1,32 @@ +/** + * Phase-0-Baseline: parallele GET /health (kein Auth). + * BASE_URL optional, z. B. https://dev.shinkan.jinkendo.de + */ +import http from 'k6/http' +import { check } from 'k6' + +export const options = { + scenarios: { + health: { + executor: 'constant-vus', + vus: 10, + duration: '30s', + gracefulStop: '5s', + tags: { scenario: 'health' }, + exec: 'health', + }, + }, + thresholds: { + http_req_failed: ['rate<0.05'], + 'http_req_duration{scenario:health}': ['p(95)<3000'], + }, +} + +const BASE = (__ENV.BASE_URL || 'https://dev.shinkan.jinkendo.de').replace(/\/$/, '') + +export function health() { + const res = http.get(`${BASE}/health`, { tags: { scenario: 'health' } }) + check(res, { + 'health 2xx': (r) => r.status >= 200 && r.status < 300, + }) +}