RECHERCHE-EGYM.md — eGYM / Netpulse API (Iserlohner Fitness & Sportpark)
Reverse-Engineering der App „IFS Fitness" (
com.netpulse.iserlohnerfitnesspark, Build 112 / v1.9) per MITM (mitmproxy 12, iPhone 16). Quelle: Flow-Dumpegym(349 HTTP-Flows, 06.06.2026, eingeloggt als Dennis). Diese Datei ist die Daten-Grundlage für den geplanten „Aktivitäten"-Screen (eGYM-Strang). Garmin ist ein separater Strang (inoffizielle Library, kein MITM) — hier nicht behandelt.⚠️ Secrets: Alle Bearer-/Session-Tokens,
deviceUid, Push-Token und Passcodes sind in diesem Dokument redigiert (<REDIGIERT>). Die enthaltenen IDs (exerciserId,egymAccountId,gymLocationId…) sind die der Familie Fisch bzw. des Studios — behandeln wie.env-Inhalt, nicht öffentlich machen.
1. Die große Erkenntnis: ZWEI Backends, ZWEI Auth-Welten
Die App spricht zwei voneinander unabhängige Systeme an, die unterschiedlich authentifizieren. Das ist der wichtigste Punkt — wer das mischt, scheitert.
| Netpulse (Studio-App-Plattform) | eGYM MWA (Geräte/Waage) | |
|---|---|---|
| Hosts | iserlohnerfitnesspark.netpulse.com, mobile-api.int.api.egym.com |
mwa-api.int.api.egym.com |
| Auth | Session-Cookie JSESSIONID=<REDIGIERT> |
Authorization: Bearer <Firebase-JWT> |
| Identität | exerciserId (Netpulse-UUID) |
egymAccountId + membershipId (im JWT) |
| Inhalt | Profil, Mitgliedschaft, Check-ins (Besuche), Ziele, Punkte/Rewards, Ranking, Feed, Challenges | Körperwaage (FITHUB), Kraft je Gerät, Workouts, Bio-Alter, Muskel-Imbalancen, Cardio, Flexibilität |
mwa = |
— | „My eGym Web App" |
mobile-api.int.api.egym.com ist eGYM-gehostet, nutzt aber denselben Netpulse-Cookie
wie der Haupthost (Feed, Ranking, Machine-Challenges) — gehört auth-seitig zu Netpulse.
Für unsere App heißt das: zwei Connector-Module bzw. zwei Token-Quellen pro User.
Die „Ground-Truth"-Körperdaten (Anforderung #6) liegen ausschließlich im
eGYM-MWA-Strang (source: "FITHUB").
2. Authentifizierung
Komplette Auth-Kette (verifiziert per Login-Capture
egym-login, 06.06.2026):1. POST /np/exerciser/login (username + password, form-urlencoded) → Set-Cookie: JSESSIONID ← der DAUERHAFTE Credential 2. GET /np/micro-web-app/v1.0/exercisers/{exerciserId}/tokens/FLS (mit JSESSIONID) → { accessToken, accessTokenExpiresAt } ← IST der MWA-Bearer (Firebase, ~1 h) 3. Authorization: Bearer {accessToken} gegen mwa-api.int.api.egym.com 4. Token (1 h) abgelaufen? → Schritt 2 wiederholen. Session tot? → Schritt 1.Kein Google-/Auth0-Refresh-Token nötig (anders als HelloFresh ADR-22): der langlebige Credential ist die Netpulse-Session; der 1-h-MWA-Token wird bei Bedarf frisch austokens/FLSgezogen. Pro User je ein Login (Anforderung #4).
2.1 Netpulse-Session (Cookie)
- Header bei JEDEM Call (Pflicht, sonst 401):
Cookie: JSESSIONID=<REDIGIERT> X-NP-API-Version: 1.5 X-NP-User-Agent: clientType=MOBILE_DEVICE; devicePlatform=IOS; deviceUid=<REDIGIERT>; applicationName=IFS Fitness; applicationVersion=1.9; applicationVersionCode=112; X-NP-APP-Version: 1.9 User-Agent: IserlohnerFitnessSportpark/1.9 (com.netpulse.iserlohnerfitnesspark; build:112; iOS 26.5.0) Alamofire/5.9.1 Accept-Language: de-DE;q=1.0, en-DE;q=0.9 - ⚠️ Genau dieser
X-NP-User-Agent-Header hatte beim Capturen ein Trailing-Whitespace → mitmproxy HTTP/2 brach ab. Lösung war--set http2=false(siehe §9).
Login (Cookie-Mint), verifiziert:
- Optionaler Pre-Check: GET /np/egym/v1.0/users?email=<urlenc> →
{firstName, termsAccepted, passwordSet, gymLocationData} (prüft, ob E-Mail bekannt).
- POST /np/exerciser/login
- Content-Type: application/x-www-form-urlencoded
- Body: password=<PASSWORT>&username=<email-urlenc> (+ die X-NP-*-Header)
- Antwort: Set-Cookie: JSESSIONID=<…> (→ Session) und JSON mit
uuid (= exerciserId), egymAccountId, homeClubUuid, chainUuid, membershipType.
- ⚠️ Felder externalAuthToken / externalRefreshToken / externalIdToken im
Login-Body waren null — die eGYM-Tokens kommen NICHT aus dem Login, sondern
aus tokens/FLS (§2.2).
- ⚠️ username/password im Klartext POSTen → in unserer DB die eGYM-Credentials je
Profil verschlüsselt ablegen (oder nur die JSESSIONID + bei Ablauf neu einloggen).
Niemals automatisiert mit gespeichertem Passwort, falls vermeidbar — Muster wie Lidl
(Dennis/Jaqueline hinterlegen selbst). Entscheidung beim Datenmodell.
2.2 eGYM-MWA-Token (Firebase-JWT)
- Header:
Authorization: Bearer <Firebase-ID-Token> Content-Type: application/json Accept-Language: de-DE,de;q=0.5 User-Agent: IFS%20Fitness/112 CFNetwork/3860.600.12 Darwin/25.5.0 - Der Bearer ist ein Google-Firebase-Secure-Token (
iss: securetoken.google.com/prod-egym-id), Lebensdauer ~1 h (exp-iat= 3600 s). Entschlüsselte Claims:json { "brandId": "b43d8104-93b7-4068-b6eb-688e4b5a8c0b", "egymAccountId": "3481a20b-10aa-5fb7-8bb3-174dfa15cefb", "membershipId": "vcw7jlwbjd74", "iss": "https://securetoken.google.com/prod-egym-id", "aud": "prod-egym-id", "user_id": "iserlohner-fitness-und-sportpark-<...>:vcw7jlwbjd74", "iat": …, "exp": …, "firebase": { "sign_in_provider": "custom" } } - Token-Mint (AUFGELÖST, byte-identisch verifiziert): Der MWA-Bearer wird von
Netpulse geliefert via
GET /np/micro-web-app/v1.0/exercisers/{exerciserId}/tokens/FLS(🟦 Cookie-Auth) →{ "provider":"FLS", "partner":"FLS", "accessToken":"<Firebase-JWT>", "accessTokenExpiresAt":"…" }. DeraccessTokenist exakt (Byte-für-Byte) derAuthorization: Bearergegenmwa-api— kein separater Token-Austausch, kein Google-Refresh-Token. - Beleg, dass
FLSder richtige Slug ist: diedynamic-features-UI-Config definiert die eGYM-Bereiche als „Micro-Web-App"-Widgets mit"partner":"FLS","backendUrl":"https://mwa-api.int.api.egym.com","partnerChannelId":"egymproduction". Das Widgetid:"workoutsMWA"trägt"requestToken":true→ Auslöser des Token-Holens beim Öffnen des Trainings-/Geräte-Bereichs. (partnerAppIdvariiert je Widget:851e0894Workouts,068a3720Bioage — Token ist aber partner-, nicht app-spezifisch.) - „Refresh" = simpel: Bei 401/Ablauf einfach
tokens/FLSerneut mit der JSESSIONID aufrufen. Stirbt die Session, neu einloggen (§2.1). → Dauerbetrieb ohne ADR-22- Komplexität: ein langlebiger Credential (Netpulse-Session) statt rotierendem Refresh-Token. - ⚠️ Token-Cache mind. 5 min Buffer (Erkenntnis 06.06.26 Dennis-Sample): Netpulse
liefert oft den bereits gemintet liegenden Token zurück, auch wenn der nur noch
2–3 min lebt. Mit kleinerem Cache-Buffer würde der Client ihn akzeptieren und beim
ersten Call 401 sehen.
egym.pysetzt 300 s — bei kürzerer Restlaufzeit wird der Token verworfen und tokens/FLS erneut aufgerufen (Netpulse mintet ggf. einen neuen). - Verknüpfungs-Status prüfbar via Netpulse:
GET /np/egym/v1.0/users/{exerciserId}/linking-status→{"status":"linked","egymEmail":"…"}.
2.3 Identifier-Spickzettel (Familie Fisch / Studio)
| Name | Wert | Bedeutung |
|---|---|---|
exerciserId (Dennis) |
c995da83-bf36-4a3b-948a-75ad4601e2d6 |
Netpulse-User-UUID (in fast jedem Netpulse-Pfad) |
egymAccountId (Dennis) |
3481a20b-10aa-5fb7-8bb3-174dfa15cefb |
eGYM-Account (im Bearer-JWT) |
membershipId (Dennis) |
vcw7jlwbjd74 |
eGYM-Membership (im Bearer-JWT) |
gymLocationId / Club |
cec51c99-15cb-4659-b30c-6075c31b928d |
Studio (Query-Param bei MWA-Workouts/Feed/Features) |
gymChainId |
38a6c221-9911-44f0-84b2-de5263f92d38 |
Kette |
brandId |
b43d8104-93b7-4068-b6eb-688e4b5a8c0b |
Marke (im JWT) |
→ Jeder User hat eigene exerciserId/egymAccountId (Anforderung #4): Jaqueline
bekommt eigene IDs + eigenen Login/Token. In unserer DB pro Silverscale-Profil hinterlegen.
3. Endpoint-Katalog
Legende: 🟦 = Netpulse-Cookie-Auth · 🟩 = eGYM-MWA-Bearer-Auth.
Basis-URLs weggelassen (siehe §1). Alle Antworten application/json.
3.1 Auth, Profil & Stammdaten
| Endpoint | Auth | Liefert |
|---|---|---|
GET /np/egym/v1.0/users?email=… |
— | Pre-Login-Check (passwordSet, firstName, Club) |
POST /np/exerciser/login |
— | Login → Set-Cookie: JSESSIONID + uuid/egymAccountId (§2.1) |
GET /np/micro-web-app/v1.0/exercisers/{exerciserId}/tokens/FLS |
🟦 | MWA-Bearer-Mint (accessToken = Firebase-JWT, §2.2) |
GET /np/exerciser/{exerciserId}/profile |
🟦 | Name, E-Mail, Geburtstag, Geschlecht, height/weight (Selbstangabe!), Avatar-URL, Home-Club |
GET /np/exerciser/{exerciserId}/membership |
🟦 | Vertrag (membershipType, contractEndDate, …) |
GET /np/egym/v1.0/users/{exerciserId}/linking-status |
🟦 | eGYM-Verknüpfung + egymEmail |
GET /mwa/api/users/v1.0/profile |
🟩 | eGYM-Profil (birthDate, gender, name, email) |
GET /np/company/children?responseType=detail |
🟦 | Studio-Stammdaten (Adresse, Öffnungszeiten, Geo) |
profile.weight/height = Selbstangabe (79.9 kg / 172 cm), NICHT die Waage —
für „Ground Truth" die MWA-Körperdaten nehmen (§3.4).
3.2 Besuche / Check-ins 🟦
| Endpoint | Liefert |
|---|---|
GET /np/exercisers/{exerciserId}/latest-check-in |
letzter Check-in (Datum, TZ) |
GET /np/exercisers/{exerciserId}/check-ins/history?startDate=…&endDate=… |
Liste aller Studio-Besuche im Zeitraum |
check-ins/history-Item:
{ "gymLocationName":"Iserlohner Fitness und Sportpark",
"gymLocationAddress":"Auf der Aeumes 35",
"checkInDate":"2025-04-30T07:14:33", "timezone":"Europe/Berlin",
"checkinId":null, "expiresIn":null, "duration":null }
- Reine Tür-Check-ins (kein Geräte-Detail,
duration:null). Für „Besuch = Geräteliste" (Anforderung) müssen wir diese Check-ins mit den Kraft-Messungen (§3.3) nach Datum joinen — siehe Datenmodell §6. - Query-Muster (App fragt monats-/jahresweise):
startDate=2026-05-01T00:00:00&endDate=2026-05-31T23:59:59.
3.3 Kraft je Gerät (Workouts an EGYM-Maschinen) 🟩
Kern für „Detailansicht je Gerät" (Anforderung #5). Jede EGYM-Maschine loggt pro
Training eine Kraft-Messung. activityId = stabile Geräte-ID (Katalog §4).
| Endpoint | Query | Liefert |
|---|---|---|
GET /mwa/api/measurements/v1.0/strength/latest-measurements |
zoneId, locale |
letzte Messung pro Gerät (Übersicht) |
GET /mwa/api/measurements/v1.0/strength |
activityId, zoneId, startDate, endDate |
Roh-Messungen eines Geräts im Zeitraum |
GET /mwa/api/measurements/v1.0/strength-measurements-yearly-summary |
activityId, shift, zoneId |
Monats-Aggregat über ein Jahr |
GET /mwa/api/measurements/v1.0/strength-measurements-monthly-summary |
activityId, shift, zoneId |
Aggregat über einen Monat |
Messung (strengthMeasurements[]):
{ "id":308609429, "createdAt":"2026-06-01T09:13:06.879Z", "timezone":"Europe/Berlin",
"activity":{ "activityId":1011, "label":"EGYM Schulterpresse",
"icons":["…/prod/icons/1011_0.jpeg","…/prod/icons/1011_1.jpeg"],
"availableForManualTest":false },
"strength":{ "value":95, "progress":"up", "percentageDiff":6.13, "amountDiff":5,
"createdAt":"2026-06-01T09:13:02.838" },
"strengthSet":{ "reps":1, "weight":95.333 },
"source":"FITNESS_MACHINE", "bodyRegion":"UPPER", "sourceLabel":"Fitnessgerät" }
strength.value= Kraft-Score (eGYM-Index, das was die App als „Kraft" plottet);strengthSet.weight= tatsächl. Gewicht (kg) beireps.progress/percentageDiff/amountDiff= fertige Trend-Bausteine (für Pfeile ↑↓ direkt nutzbar!).bodyRegion ∈ {UPPER, LOWER, CORE}.- Verlaufs-Toggles (30 T / 6 M / 1 J / Alles): Client-seitig komponiert aus den
obigen Endpoints —
shiftzählt Perioden zurück (shift=0aktuelle,1vorige …);yearly-summaryfür Jahres-Charts,monthly-summaryfür Monats-Charts,strength(mitstartDate/endDate) für Roh-Punkte. „Alles" = übershiftiterieren bis leer.
Verwandt:
- GET /mwa/api/analysis/v1.0/muscle-imbalances?zoneId=… → Agonist/Antagonist-Paare
mit position in optimalRange (z. B. BICEPS↔TRICEPS, LATS↔SHOULDER) — perfekte
„Balance"-Visualisierung. Enthält agonistActivityId/antagonistActivityId (→ Geräte).
- GET /challenges/api/v1.0/exercisers/{exerciserId}/machine-challenges?locale=de-DE 🟦
→ machineType (M3, M5, M7, M8, M15…) ↔ machineLabel (dt.) ↔ exerciseCode.
Brücke von der physischen Studio-Gerätenummer (M-Code) auf activityId.
3.4 Körperdaten / Waage = „Ground Truth" 🟩
Quelle source:"FITHUB" (EGYM Fitness Hub, InBody-Segmentwaage). Das ist die
hochwertige Waage aus Anforderung #6 — priorisieren vor Garmin Index S2.
| Endpoint | Query | Liefert |
|---|---|---|
GET /mwa/api/measurements/v1.0/latest-body-metrics |
— | alle aktuellen Körperwerte (51 Typen, §5) |
GET /mwa/api/measurements/v1.0/body-metrics |
types, granularity, startDate, endDate, zoneId |
Verlauf je Typ (Punkt-Reihe) |
GET /mwa/api/measurements/v1.0/body-measurements-grouped |
types, zoneId, startDate, endDate |
je Mess-Session gruppiert (id, createdAt, metrics[]) |
body-metrics (Verlauf):
[ { "type":"WEIGHT_KG", "history":[ {"value":79.9,"date":"2026-06-01"},
{"value":80.4,"date":"2026-05-01"} ] } ]
body-measurements-grouped (eine Messung = eine Waage-Sitzung):
{ "id":285708882, "createdAt":"2026-06-01T08:18:49", "source":"FITHUB",
"metrics":[ {"type":"WEIGHT_KG","value":79.9,"valueInterpretation":"…"}, … ] }
granularity: gesehenONE_ITEM_PER_MONTH(für 6M/1J-Charts). Für 30-Tage feiner (vermutlichONE_ITEM_PER_DAY/ kein Filter → roh) — beim nächsten Capture Tages-Toggle klicken, um den genauen Wert zu bestätigen.valueInterpretation(z. B.INTERPRETATION_LABEL_EXCELLENT/NORMAL/ELEVATED) = fertige Ampel-Bewertung → direkt für Farb-Chips nutzbar.
3.5 Bio-Alter, Cardio, Flexibilität, Squat 🟩
Starke Motivations-Widgets („Feuerwerk", Anforderung):
| Endpoint | Liefert |
|---|---|
| GET /mwa/api/analysis/v1.0/bio-age?locale=de-DE | Gesamt-/Metabol-/Flex-/Cardio-Bio-Alter + Quotes |
| GET /mwa/api/analysis/v1.0/bio-age-yearly-summary?types=MUSCLE&types=METABOLIC&…&shift=0 | Bio-Alter-Verlauf (Jahr) je Typ |
| GET /mwa/api/analysis/v1.0/bio-age-monthly-summary?types=…&shift=0 | dito (Monat) |
| GET /mwa/api/measurements/v1.0/cardio?metricType=BLOOD_PRESSURE\|RESTING_HEART_RATE\|VO2MAX&zoneId&startDate&endDate | Blutdruck / Ruhepuls / VO2max |
| GET /mwa/api/measurements/v1.0/cardio-measurements-yearly-summary?shift=0&zoneId | Cardio-Jahres-Summary |
| GET /mwa/api/measurements/v1.0/flexibility-latest-metrics?zoneId&locale | Beweglichkeit je Gelenk (FITHUB) |
| GET /mwa/api/measurements/v1.0/squat-latest-metrics?zoneId&locale | Kniebeuge-Analyse (Tiefe/Rücken/Schultern) |
| GET /mwa/api/measurements/v1.0/squat-history?granularity=ONE_ITEM_PER_MONTH&startDate&endDate&locale&zoneId | Squat-Verlauf |
bio-age (Auszug): totalBioAge.value=35, metabolicAge=43 (mit bodyFat, bmi),
flexibilityAge=43, cardioAge=31 (mit restingHeartRate, systolic/diastolicPressure,
vo2Max, jeweils source: MANUAL/…). Jeder Wert trägt progress/percentageDiff/amountDiff.
3.6 Workouts (Trainingseinheiten, gemischt) 🟩
GET /mwa/api/workouts/v1.0/workouts?completedAfter=…&completedBefore=…&gymLocationId=…&measurementSystem=METRIC&locale=de-DE
→ Array von Einheiten; Item:
{ "completedAt":"2026-05-22T17:21:45", "timezone":"Europe/Berlin",
"workoutPlanName":null,
"exercises":[ {
"id":5488740128653312, "label":"Fahrrad Outdoor",
"activity":{ "activityId":429, "category":"CARDIO_OUTDOOR" },
"sets":[ {"setType":"TIME_BASED","numberOfReps":null,"weight":null,
"duration":2440,"distance":13575.26,"speed":5.56,"heartRate":null} ],
"completedAt":"2026-05-22T17:21:45", "source":"APPLE_HEALTH",
"sourceType":"CONNECTED_APP", "kiloCalories":225.0, "points":190,
"summary":{ "totalDuration":2440, "totalDistance":13575.26, "avgSpeed":5.56 },
"netpulseWorkoutId":"…" } ] }
- ⚠️ Mischt Apple-Health-Importe (Outdoor-Bike), Cardio-Geräte und EGYM-Maschinen.
source ∈ {APPLE_HEALTH, FITNESS_MACHINE, EGYM_MACHINE_STANDALONE, …},setType ∈ {TIME_BASED, REP_BASED}. - Für EGYM-Maschinen-Besuche ist
measurements/strength(§3.3) die sauberere Quelle (eine Messung pro Gerät, klareactivityId);workoutsergänzt Cardio/Kalorien. POST /mwa/api/workouts/v1.0/calories?measurementSystem=METRIC(Body:{exercises:[{activityId,sets:[…]}]}) →{calories, points}— Live-Kalorien-Schätzer (für manuelle Eingabe, brauchen wir kaum).GET /mwa/api/workouts/v1.1/available-plans?gymLocationId&measurementSystem&locale→ Trainingspläne inkl. voller Übungs-Bibliothek (Muskel-Mapping, Beschreibungen 12-sprachig, Icons/Videos unterstorage.googleapis.com/activity-library-prod-…).GET /mwa/api/workouts/v2.0/favourite-activities?gymLocationId→ Favoriten.
3.7 Gamification / Punkte / Ranking 🟦
| Endpoint | Liefert |
|---|---|
GET /analysis/api/v1.0/exercisers/{exerciserId}/activitylevels |
points, level (bronze/silver/gold), goal, daysLeft |
GET /analysis/api/v1.0/exercisers/{exerciserId}/rankingwithleaderboard?languageCode=de&size=20 |
eigener Rang + Leaderboard (mit fremden Avataren!) |
GET /np/np-rewards/v1/programAccount/{exerciserId} |
Punkte-Konto (currentPointsBalance) |
GET /np/exerciser/{exerciserId}/goal · …/completedGoals |
aktuelles/erledigte Ziele |
GET /np/exerciser/{exerciserId}/challenges?grouping=joinStatus · …/challenges/active |
Challenges |
GET /feed/api/v1.0/gym-locations/{gymLocationId}/feed?startDate&endDate&locale |
Studio-Activity-Feed (fremde Nutzer) |
⚠️ rankingwithleaderboard und feed enthalten Daten/Avatare anderer Mitglieder
(Klarnamen abgekürzt, Profilbilder). Für unsere App: entweder weglassen oder nur
den eigenen Rang/Punkte ziehen — fremde PII nicht persistieren (vgl. §7).
3.8 Feature-Flags & Sonstiges (für Doku-Vollständigkeit)
GET /mwa/api/feature-settings/v1.0/gym-locations/{gymLocationId}/features/{flag}/status→{enabled, preferences}(Flags:mwaBioageStrengthAge,…MetabolicAge,…FlexibilityAge,…CardioAge,mwaConnectCardioHealthData,mwaBioageRemovalMeasurements).GET /np/dynamic-features/v2.1/exercisers/{exerciserId}/application-layout→ UI-Layout-Config.GET /np/brand/description?appVersion=10900→ Marke, Passwort-Policy, Feature-Schalter.POST /analytics/api/v1.0/app-events🟩 — Telemetrie (67× im Dump, ignorieren).GET /workouts/api/connected-apps/appleHealth/exercise-mappings🟦 → Apple-Health-↔-eGYM-Aktivitäts-Mapping.
4. Geräte-Katalog (EGYM-Maschinen, activityId)
Stabile IDs aus measurements/strength. Icons liegen als Assets unter
app/egym-assets/machine-icons/{activityId}-{slug}.jpeg (öffentlich geladen, §8).
M-Codes (physische Studio-Nummer) aus machine-challenges, soweit gesehen.
Deutsche Labels wie sie der strength-Endpoint mit locale=de-DE ausliefert
(verifiziert beim Login-Capture 06.06.2026 Dennis-Account).
| activityId | Label (DE, aus API) | Region | M-Code | Asset |
|---|---|---|---|---|
| 994 | EGYM Beinstrecker | LOWER | 994-leg-extension.jpeg |
|
| 995 | EGYM Bauchtrainer | CORE | 995-abdominal-crunch.jpeg |
|
| 996 | EGYM Rückentrainer | CORE | M3 | 996-back-extension.jpeg |
| 997 | EGYM Beinbeuger | LOWER | 997-leg-curl.jpeg |
|
| 998 | EGYM Brustpresse | UPPER | M5 | 998-chest-press.jpeg |
| 999 | EGYM Ruderzug | UPPER | 999-seated-row.jpeg |
|
| 1000 | EGYM Latzug | UPPER | M7 | 1000-lat-pulldown.jpeg |
| 1001 | EGYM Glutaeus | CORE | M8 | 1001-glutes.jpeg |
| 1002 | EGYM Beinpresse | LOWER | 1002-leg-press.jpeg |
|
| 1003 | EGYM Abduktor | CORE | 1003-abductor.jpeg |
|
| 1004 | EGYM Adduktor | CORE | 1004-adductor.jpeg |
|
| 1005 | EGYM Rotator | CORE | 1005-rotary-torso.jpeg |
|
| 1007 | EGYM Butterfly Reverse | UPPER | 1007-reverse-flys.jpeg |
|
| 1008 | EGYM Butterfly | UPPER | 1008-butterfly.jpeg |
|
| 1009 | EGYM Bizeps | UPPER | M15 | 1009-bicep-curl.jpeg |
| 1010 | EGYM Wadentrainer | LOWER | 1010-calf-press.jpeg |
|
| 1011 | EGYM Schulterpresse | UPPER | 1011-shoulder-press.jpeg |
|
| 1012 | EGYM Trizeps | UPPER | 1012-triceps.jpeg |
1006 existiert im Studio nicht / wurde nicht trainiert. Restliche M-Codes beim nächsten Capture (Gameday-/Challenge-Screen vollständig öffnen). Englische Labels kommen mit
locale=en-DE(z.B. „EGYM Leg extension") – wir bleiben bei DE.
⚠️ UI-Falle "letzte Messung": strength/latest-measurements zeigt das letzte je
Gerät – auch wenn es Jahre her ist (Dennis-Sample: 6 Geräte vom 01.06.2026, 12 noch
aus Mai 2025!). Im Screen muss „zuletzt vor X" sichtbar werden, sonst wirkt der Score
veraltet/falsch.
5. Körperdaten-Vokabular (51 Metrik-Typen aus latest-body-metrics)
Alle source:"FITHUB". Basis-Typ = der Wert; _LOW/_TOP-Varianten = gesunde
Spannbreite (Band um den Wert, für „Sollbereich"-Visualisierung). Beispiel Dennis
01.06.2026:
Primär (für unsere App):
| Typ | Wert | Bedeutung |
|---|---|---|
| WEIGHT_KG | 79.9 | Gewicht (Ground Truth) |
| BMI | 27.01 | BMI |
| BODY_FAT_PERCENTS | 22.4 | Körperfett % |
| BODY_FAT_MASS_KG | 17.9 | Körperfett (kg) |
| SKELETAL_MUSCLE_MASS_KG | 35.3 | Skelettmuskelmasse |
| FAT_FREE_MASS_KG | 62.0 | fettfreie Masse |
| BODY_WATER_LITER | 45.3 | Körperwasser (Liter) |
| BASAL_METABOLIC_RATE_KJ | 7155 | Grundumsatz (kJ) |
| VISCERAL_FAT_AREA_CM2 | 75.7 | viszerales Fett (Fläche) |
| VISCERAL_FAT_LEVEL_CM2 | 7.0 | viszerales Fett (Level) |
| BODY_PHASE_ANGLE | 6.9 | Phasenwinkel (Zellgesundheit) |
| ECW_TBW_PERCENT | 36.8 | Wasserverteilung (Ödem-Indikator) |
| HEIGHT_CM | 172 | Größe |
⚠️ Knochenmasse (Anforderung #2) war in diesem InBody-Datensatz nicht als eigener Typ dabei (InBody liefert oft
BONE_MINERAL_CONTENTseparat). Garmin Index S2 liefert Knochenmasse → ggf. von dort ergänzen (Garmin als Sekundärquelle).
Segmental (je Gliedmaße, für Körper-Heatmap): SEGMENTAL_LEAN_{RIGHT,LEFT}_{ARM,LEG}_KG,
SEGMENTAL_LEAN_TRUNK_KG, SEGMENTAL_FAT_{…}_KG — plus _TOP/_LOW-Bänder.
Wasser-Detail: INTRA_CELLULAR_WATER_LITER, EXTRA_CELLULAR_WATER_LITER (+ Bänder).
valueInterpretation: …_EXCELLENT / _NORMAL / _ELEVATED / _UNSPECIFIED → Ampelfarbe.
6. Mapping auf unser Datenmodell (Skizze für den „Aktivitäten"-Screen)
Anforderung: „Besuche als Liste; ein Besuch = Liste von Geräten; Klick auf Gerät → Besuchs-Detail + Verlauf + Liste der Besuche, bei denen das Gerät genutzt wurde" — das ist exakt die Artikel↔Kassenbon-Beziehung aus der bestehenden App.
egym_account (pro Silverscale-Profil)
exerciser_id, egym_account_id, membership_id, gym_location_id, tokens…
egym_visit (= „Bon") ← Cluster aus check-in + Messungen eines Tages
id, profile_id, visited_at, source(checkin|derived), duration?, kcal?, points?
egym_machine (= „Artikel", Stammdaten) ← Katalog §4
activity_id (PK), label_de, body_region, m_code?, icon_path
egym_strength_set (= „Bon-Position") ← measurements/strength
id (eGYM id), visit_id, activity_id, created_at,
strength_value, weight_kg, reps, progress, percentage_diff, body_region
egym_body_measurement (= Waage-Sitzung) ← body-measurements-grouped
id (eGYM id), profile_id, measured_at, source='FITHUB'
egym_body_metric
measurement_id, type, value, interpretation
- Besuch-Rekonstruktion: Tür-Check-ins (§3.2) haben kein Geräte-Detail → einen
„Besuch" als Cluster nach Tag/Session bilden: alle
strength-Messungen + ggf. Waage-Messung + Check-in mit gleichem Datum = ein Besuch. (Maschinen-Timestamps liegen je Gerät wenige Minuten auseinander → Session-Fenster z. B. ±3 h.) - Geräte-Detailseite =
egym_strength_setgefiltert aufactivity_id, sortiert nach Zeit → Verlaufschart (Kraft-Score + Gewicht) + „Besuche, bei denen genutzt" = die zugehörigenegym_visit-Einträge. (= die „Kassenbon-Liste" je Artikel.) - Ground-Truth-Flag: Körperwerte mit
source='FITHUB'als „Ground Truth" kennzeichnen und vor Garmin-Werten anzeigen (Anforderung #6). Garmin-Gewicht/ Knochenmasse nur als Ergänzung/Lücken-Füller. - Idempotenz: eGYM liefert stabile numerische
idje Messung → wie HFhf_idals Upsert-Schlüssel nutzen.
7. Datenschutz (WICHTIG)
rankingwithleaderboard,feed,feed/briefundmachine-challengesenthalten personenbezogene Daten anderer Studio-Mitglieder (Klarnamen, Avatare, Trainingszeiten). Der Capture enthielt 4 fremde Avatar-Fotos — bewusst NICHT als Assets abgelegt. → In unserer App diese Endpoints nicht persistieren; höchstens den eigenen Rang/ Punktestand extrahieren.- Tokens (
JSESSIONID, Bearer, FLS-JWT, Push-Token,deviceUid,clientLoginPasscode) sind hier redigiert; der roheegym-Flow-Dump enthält sie im Klartext → nicht committen, nach Auswertung Session invalidieren.
8. Assets
app/egym-assets/machine-icons/ — 18 EGYM-Geräte-Icons (500×500 JPEG), direkt von
public-measurements-prod-egym-com.storage.googleapis.com/prod/icons/{activityId}_0.jpeg
(öffentlich, keine PII). Dateiname = {activityId}-{slug}.jpeg (Mapping §4).
_0 und _1 waren je identisch → nur eine Variante gespeichert.
Nicht abgelegt: fremde Mitglieder-Avatare (DSGVO, §7); eigener Avatar + Widget-PNGs
kamen im Capture als 304/0 Byte (gecacht). Bei Bedarf nachladbar:
- eigener Avatar: https://galaxy.cdn.egym.com/avatar-scaled/{exerciserId}.jpg
- Übungs-Bibliothek (Bilder/Videos): storage.googleapis.com/activity-library-prod-prod-co/images/activities/processed/egym/{exerciseCode}/…
9. Reproduktion des Captures
# Mac:
mitmweb --listen-port 8080 --set http2=false --allow-hosts '(netpulse\.com|egym\.com)'
# iPhone: WLAN-Proxy → Mac-IP:8080; mitm.it-Cert installieren UND unter
# Info → Zertifikatsvertrauen freischalten (sonst TLS-Fail für alles).
# --set http2=false : Netpulse-Header hat Trailing-Whitespace → HTTP/2 bricht sonst ab.
# --allow-hosts : nur eGYM/Netpulse abfangen → iCloud etc. ungestört, kein Log-Müll.
# Auswertung (Wegwerf-venv mit mitmproxy 12):
python3 -m venv /tmp/mitmtool && /tmp/mitmtool/bin/pip install mitmproxy
/tmp/mitmtool/bin/python -c 'from mitmproxy.io import FlowReader; …' # Flows lesen
Flow-Dump-Format ist kein HAR, sondern mitmproxys natives tnetstring-Format
(Bodies gzip) → mit mitmproxy.io.FlowReader lesen; flow.response.get_text() dekomprimiert.
10. Offene Punkte / nächster Capture
- ✅ Login + Token-Mint ERLEDIGT (Capture
egym-login, §2): Netpulse-Login (POST /np/exerciser/login→ JSESSIONID) und MWA-Bearer austokens/FLS(byte-identisch verifiziert). Kein Refresh-Token nötig. - Verbleibend: JSESSIONID-Lebensdauer unbekannt (Java-Session — kann kurz sein). Empirisch ermitteln bzw. Re-Login bei 401 einbauen.accessTokenExpiresAtder FLS-Antwort als Vorlauf für proaktiven Token-Refresh nutzen. - Verlaufs-Granularität des 30-Tage-Toggles bestätigen (vermutlich
ONE_ITEM_PER_DAY). - Restliche M-Codes (physische Gerätenummern) im Gameday/Challenge-Screen sammeln.
- Knochenmasse: prüfen, ob InBody das als eigenen Typ liefert (anderer Mess-Screen) oder ob wir es aus Garmin ziehen.
- Garmin-Strang separat (inoffizielle Library
garth/garminconnect, kein MITM) — eigenes Dokument, sobald wir dort sind.