Garmin kann alles – gibt aber nichts raus.
Garmin brachte vor ein paar Monaten mit Connect+ ein Performance-Dashboard raus. Ganz nett und macht Spaß, aber kostet halt Geld. Da nun auch mein Sohn mit einer Garmi Forerunner 165 unterwegs ist, wollte ich ihm dein Dashboard zur Verfügung stellen. Einfach um die Daten auszuwerten.
Und da mein Verstand der Programmierung nicht ganz so zugeneigt ist, habe ich einmal ein Seöbstversuch mit der KI/AI Claude gewagt (da Sie ja aktuell in aller Munde ist).
Was dann passierte, war eine mehrstundige Entwicklungsreise mit Claude – inklusive echter Fehler, echter Debugging-Sessions und einem Ergebnis das ich so nicht erwartet hatte: ein vollstaendiges, mobil-optimiertes Performance-Dashboard mit PMC-Chart, Rennprognosen, PWA-Support und automatischer Device-Erkennung. Und einer Erkenntnis uber Garmin, die ich schon ahnte, aber so direkt noch nie formuliert hatte.
Garmin: Die beste Hardware im Spiel – und eine API-Blackbox
Fangen wir mit dem Elefanten im Raum an. Garmin macht schlicht die beste Fitness-Hardware die es gibt (nein ich werde nicht gesponsort oder ähnliches, aber ich habe halt schon Jahrelang Apple und auch Wahoo getestet und genutzt). Die Uhr trackt alles – Herzfrequenz, VO₂max, Training Readiness, Schaltqualität, Body Battery, Stresslevel. Die Sensoren sind präzise, die Akkulaufzeit ist unschlagbar, das Ökosystem auf Garmin Connect ist ausgereift. Wer ernsthaft trainiert, läuft oder radelt, kommt an Garmin kaum vorbei.
Das Problem: Garmin hält seine API-Türen konsequent geschlossen (UND JA DAS NERVT ECHT DERBE
).
Die Garmin Health API und die Connect API existieren – aber sie sind ausschließlich für zertifizierte Unternehmenspartner und Gesundheitsplattformen zugänglich. Kein offener Developer-Zugang, kein simples OAuth wie bei anderen Plattformen. Wer da rein will, braucht eine formale Partnerschaft mit Garmin. Das ist nicht mal bürokratisch umständlich – es ist schlicht nicht vorgesehen.
⚠️ Kurz zusammengefasst: Garmin ist das Apple-Ökosystem unter den Fitness-Plattformen. Alles drin, nichts raus. Hervorragende Hardware, aber die Daten gehören Garmin – und die teilen sie nicht leichtfertig.
Strava: Die Datenkrake, die uns rettet 
Garmin synchronisiert automatisch alle Aktivitäten zu Strava. Lauf beendet, Uhr abgenommen, zehn Sekunden später ist die Aktivität auf Strava. Das ist der Standardworkflow für die meisten Garmin-Nutzer – und genau dieser Umweg wird zur Lösung.
Strava ist im Grunde die Datendrehscheibe für alles was sich auf zwei Beinen oder Rädern bewegt. Jede App, jedes Gerät, jede Plattform synct zu Strava. Und im Gegensatz zu Garmin hat Strava eine vollständig offene, gut dokumentierte API mit echtem OAuth-Flow und Rate Limits die man als Entwickler versteht.
💡 Die Erkenntnis: Strava ist nicht die Datenanwendung – Strava ist die Datenkrake, die all unsere Aktivitäten von allen Geräten saugt und als strukturierte API wieder rausgibt. Garmin liefert die Qualitätsdaten, Strava liefert den Zugang.
Der Umweg ist also eigentlich gar keiner. Er ist der einzig gangbare Weg – und wie sich zeigen wird, auch der bessere.
Iteration 1: Der erste Prototyp – und das CORS-Problem
Der erste Ansatz war ein React-Artifact direkt im Chat. Strava API abfragen, Daten anzeigen, fertig. Klingt einfach. Das Problem kommt beim Token-Austausch. Stravas OAuth-Endpoint blockiert Browser-Requests aus Sicherheitsgruenden – CORS macht das zunichte. Der Access Token kann nicht direkt im Browser geholt werden. Ein serverseitiger Schritt ist zwingend.
Die Lösung in Version 1: ein curl-Befehl den man manuell im Terminal ausführet, den Token copy-pastet. Funktioniert – als User kein Problem. Als dauerhafte Lösung aber nicht tragbar.
Das Ergebnis war ein dunkles Dashboard mit Strava-Orange, Athletenprofil, den letzten 30 Aktivitäten und YTD-Statistiken. Nicht schlecht für einen ersten Wurf. Aber der curl-Workflow war nichts, was ich meinem Sohn zumuten wollte. 😄 Sah halt auch sch***e aus
.
Iteration 2: PHP auf den Webspace – der richtige Ansatz
PHP löst das CORS-Problem elegant: Der Token-Austausch passiert serverseitig per cURL, der Browser bekommt den fertigen Token nie zu Gesicht. Dazu: automatisches Token-Refresh, Session-basierte Authentifizierung und paginierter Datenabruf für bis zu 12 Monate Aktivitätshistorie.
// Token läuft ab? Kein Problem – läuft automatisch im Hintergrund
if (time() > $_SESSION[‚expires_at‘]) {
// Neuen Token per refresh_token holen
// User merkt nichts davon
}
Die Strava API gibt maximal 200 Aktivitäten pro Request zurück. Ein Jahr Training kann locker 400-500 Einträge bedeuten – also paginiert man. Seite für Seite bis alle Daten da sind, dann alles lokal filtern.
Was in Version 2 dazukam:
- PHP-OAuth ohne manuelles curl – Strava-Login direkt im Browser, kein Gebastel
- 12 Monate Aktivitaetshistorie, vollständig paginiert
- Chart.js: Wochenvolumen gestapelt (Laufen/Radfahren), Pace-Trend, Sport-Verteilung
- GitHub-style Trainingskalender (Heatmap der letzten 26 Wochen)
- Sortierbare Aktivitätstabelle mit Suche und Filtern
Das Sicherheitsproblem: Wer darf rein?
Drei Optionen wurden diskutiert: htaccess-Passwort, PHP-Passwortabfrage, oder die eleganteste Lösung: die Strava Athlete-ID als Whitelist.
Nach dem ersten Login sieht man die eigene numerische Strava-ID im Header. Diese wird einmalig in der Konfigurationsdatei eingetragen. Danach kommt jeder andere Account nach dem OAuth-Callback auf eine Fehlermeldung – ohne jemals Daten zu sehen.
💡 Schöner Nebeneffekt: PHP-Sessions sind per Browser vollständig isoliert. Jeder Nutzer sieht automatisch nur seine eigenen Daten – ohne aufwändige Benutzerverwaltung.
Das Athleten-Limit: Wenn Strava querläuft (Runde 1)
Dann wollte auch mein Sohn das Dashboard nutzen. Und Strava lieferte prompt:
Fehler 403: Limit der verbundenen Sportler überschritten
Neue, nicht-verifizierte Strava API-Apps dürfen nur eine begrenzte Anzahl an Athleten verbinden. Nicht weil irgendetwas falsch konfiguriert ist – sondern weil Strava im Sandbox-Modus ein hartes Limit setzt.
Die Lösung: Jeder bekommt seine eigene Strava API-App. Kostenlos, dauert zwei Minuten.
$APPS = [
‚ICH‘ => [
‚client_id‘ => ‚12345‘, // Meine API-App
‚client_secret‘ => ‚abc…‘,
],
’sohn‘ => [
‚client_id‘ => ‚67890‘, // Seine API-App
‚client_secret‘ => ‚xyz…‘,
],
];
Ruft jemand strava.php ohne Parameter auf, erscheint eine Profilauswahl. Mit ?app=ICH oder ?app=sohn geht es direkt ins persoenliche Dashboard. Sessions vollständig isoliert – kein Athleten-Limit mehr.
Iteration 3: Das grosse Feature-Update – wie Garmin Connect, aber meins
Performance Management Chart (PMC)
CTL (Fitness, 42-Tage-EMA), ATL (Fatigue, 7-Tage-EMA) und TSB (Form = CTL minus ATL) als überlagerte Linien. Basis ist die Suffer Score aus der Strava API. Der Form-Indikator zeigt: Peak Form, Frisch, Neutral, Müde oder Übertraining. Das kenn ich von Garmin – und ja, es trifft meistens.
Rennprognosen per Riegel-Formel
Aus den schnellsten fünf Läufen werden automatisch Zielzeiten für 5K, 10K, Halbmarathon und Marathon berechnet: T2 = T1 x (D2/D1)^1.06 – bekannt aus der Laufwissenschaft, erstaunlich zuverlässig.
Herzfrequenz-Zonen und dynamische Filter
Z1 bis Z5 automatisch aus der maximalen Herzfrequenz berechnet. Zeitraum frei wählbar: 1M, 3M, 6M, 12M, alles. Sport-Filter, Freitextsuche, sortierbare Aktivitätstabelle. Alles live, alles ohne Seiten-Reload.
| Feature | Technik | Details |
| Performance Management Chart | CTL, ATL, TSB als Linien | Wie Garmin Connect – selbst gehostet |
| Rennprognosen | Riegel-Formel automatisch | 5K / 10K / HM / Marathon |
| Herzfrequenz-Zonen | Z1–Z5 automatisch | Basis: Max-HF aus eigenen Daten |
| Dynamische Filter | Zeitraum + Sport + Suche | Live ohne Seiten-Reload |
| Trainingskalender | GitHub-style Heatmap | 26 Wochen Ueberblick |
| Bestleistungen | Pro Sportart | Laengste, schnellste, hoechste |
| Aktivitaetstabelle | Alle Spalten sortierbar | Mit Paginierung |
Iteration 4: Mobile First – weil 90% Handy
Das bisherige Layout war Desktop-first – suboptimal wenn das Dashboard zu 90% vom Smartphone aus genutzt wird. Also: kompletter Neuaufbau, mobile-first.
- Bottom Navigation Bar – vier Tabs, immer unten fixiert, daumenfreundlich erreichbar
- KPI-Karten horizontal swipebar – Snap-to-Card, kein Tabellen-Cramming auf 375px
- Aktivitäten als Cards statt Tabelle – Tabellen auf Mobile sind einfach keine gute Idee
- PWA-Support – startet ohne Safari-UI vom iPhone-Homescreen als Vollbild-App
- Safe Area Support – Notch, Home Indicator, Dynamic Island: alles berücksichtigt
💡 Tipp: URL im Safari oeffnen, ‚Zum Home-Bildschirm‘ auswählen. Das Dashboard startet danach als Vollbild-App ohne Browser-Chrome. Fühlt sich nativ an.
Praxistest: Drei Fehler, drei Lernmomente
Bis hierher klingt alles glatt. War es nicht. Der Praxistest hat drei echte Probleme geliefert – die ich nicht verschweigen will, weil sie den interessantesten Teil der Geschichte ausmachen.
Fehler 1: Authorization Error beim Sohn
Nach dem Autorisieren bei Strava kam direkt beim Redirect zurück: Authorization Error. Ursache: In der Strava API-App war als Callback-Domain lajdych.com eingetragen, die Seite lief aber unter www.lajdych.com. Strava ist da gnadenlos exakt – ein fehlendes www reicht für einen 400er.
⚠️ Merke: Die Callback-Domain muss exakt mit der Domain übereinstimmen die im Browser aufgerufen wird. Kein https://, kein Pfad, nur die Domain – und genau so wie sie in der URL steht.
Fehler 2: Logout und Profilwechsel funktionierten nicht
Der Logout hat die Session gelöscht – aber dann zurück zu ?app=ICH geleitet. Damit wurde app_key sofort wieder in die Session geschrieben und man landete wieder auf der Login-Seite desselben Profils statt auf der Profilauswahl.
Der Fix: Logout leitet jetzt zu $baseUrl ohne jeden Parameter. Zusätzlich gibt es ?switch=1 als eigenen Endpunkt der nur den app_key löscht und zur Profilauswahl zurückschickt.
// Logout: KEIN ?app= in der Redirect-URL!
header(‚Location: ‚ . $baseUrl); exit;
// Profil wechseln
if (isset($_GET[’switch‘])) {
unset($_SESSION[‚app_key‘]);
header(‚Location: ‚ . $baseUrl); exit;
}
Fehler 3: HTTP 500 nach dem Update
Nach dem Einbauen der Device-Detection-Logik kam ein blanker HTTP 500. Kein Fehlertext im Browser, nur weisser Bildschirm. Ursache: beim Einfügen des neuen Code-Blocks hatte ich das schliessende <?php endif; ?> des Haupt-Dashboard-Blocks gelöscht. PHP bekam ein syntaktisch unvollständiges if/else und hat die Datei komplett verweigert.
Das ist die Art von Fehler, die halt manchmal trotzdem passiert er & proggen ist nicht meine passion
. Weil man auf den Inhalt schaut und nicht auf die Klammern. Display Errors einschalten war richtig, hat hier aber nichts angezeigt, weil der Parse Error vor dem Output passiert. Der Blick in die Server-Error-Logs hätte es sofort gezeigt. 😄
Iteration 5: Automatische Device-Erkennung
Das Dashboard erkennt jetzt automatisch ob es auf einem Handy oder am Desktop aufgerufen wird – und passt die Ansicht entsprechend an. Kein manueller Wechsel, keine separaten URLs.
$isMobile = (bool) preg_match(
‚/(android|iphone|ipad|ipod|mobile|blackberry|opera mini|windows phone)/i‘,
$_SERVER[‚HTTP_USER_AGENT‘] ?? “
);
iPhone, iPad, Android: Mobile-Ansicht mit Bottom-Nav und swipebaren KPI-Karten. Alles andere: Desktop-Ansicht mit Top-Tab-Navigation, 5-Spalten-KPI-Grid und breiteren Charts. Zusätzlich gibt es einen manuellen Override-Button im Header – wird in der Session gespeichert.
Das Athleten-Limit: Runde 2
Und dann kam es nochmal. Nachdem alles lief und ich mich mit meinem Account einloggen wollte:
Fehler 403: Limit der verbundenen Sportler überschritten
Diesmal nicht wegen meines Sohns – sondern weil durch die vielen Verbindungsversuche, Fehler und Neustarts während der Entwicklung das Kontingent der App aufgebraucht war. Strava zählt nicht gleichzeitig aktive Athleten, sondern alle die jemals verbunden waren – inklusive fehlgeschlagener Versuche.
Die Lösung: App löschen und neu anlegen. Frische App, frisches Kontingent. Für meinen Sohn dasselbe.
⚠️ Wichtig für alle die das nachbauen: Beim Testen häufen sich schnell verbrauchte Kontingente an. Sobald alles stabil läuft, eine frische API-App anlegen und die finalen Credentials eintragen. Verhindert genau diesen Frust.
Das Ergebnis: Eine Datei, ein Upload, fertig
Das Endergebnis ist eine einzige strava.php. Kein Node.js, kein npm, kein Build-Prozess, keine externen Abhängigkeiten außer PHP mit cURL. Einfach hochladen, API-Keys eintragen, fertig.
| Was | Technik | Details |
| Multi-User | Je eigene Strava API-App | Vollständig isolierte Sessions |
| OAuth | Automatisch + Token-Refresh | Kein manuelles Token-Handling |
| Datenabruf | 12 Monate, paginiert | Bis zu mehrere Hundert Aktivitäten |
| PMC Chart | CTL, ATL, TSB | Mit Form-Indikator + Trainingstipp |
| Rennprognosen | Riegel-Formel | 5K / 10K / HM / Marathon |
| HF-Zonen | Z1–Z5 | Verteilung nach Trainingszeit |
| Heatmap | 26 Wochen | GitHub-style Trainingskalender |
| Filter | Zeitraum + Sport + Suche | Dynamisch, live, ohne Reload |
| Device-Erkennung | PHP User-Agent + Session | Auto Mobile/Desktop + manueller Override |
| Mobile | Bottom Nav + PWA | Safe Area, swipeable KPIs |
| Logout/Switch | Session-basiert | Sauberer Wechsel zur Profilauswahl |
Und hier ein paar kleine Screenshots:





Alles in allem, eine tolle Erfahrung & eine wunderbare Kooperation zwischen Claude & mir