Website-Dokumentation

mag.biking · Technische Referenz · Automatisch aus Live-Daten
Lädt…
← Admin
🗺 Architektur-Überblick

Die Website nutzt eine Hybrid-Architektur: Statische Seiten (GitHub Pages + Cloudflare CDN) + Pi-API Backend (Express.js + SQLite auf Raspberry Pi). Content-Daten werden im Browser gespeichert (localStorage) und als statische JavaScript-Datei exportiert. Nutzerdaten, Auth und Kontaktformular laufen über die Pi-API.

🧱 Technologie-Stack
Vanilla HTML · CSS · JavaScript
Kein Framework, kein Build-Tool
Läuft direkt im Browser
💾 Datenpersistenz
localStorage (Admin-Bearbeitungen)
content-data.js (exportiert, deployed)
GitHub → Pi-Server (optional)
📱 PWA-Support
manifest.json vorhanden
app.html als Standalone-App
Offline-fähig nach erstem Laden
🔐 Authentifizierung
Passwort-geschütztes Admin-Panel
JWT-basiertes Auth-System auf Pi-API (bcrypt + jsonwebtoken)
Session via localStorage + Refresh-Tokens

Seitenstruktur

DateiZweckZugriff
index.htmlHauptseite (Landing Page)Öffentlich
app.htmlPWA-App: Geplante RideoutsÖffentlich
blog.htmlBlog-ÜbersichtÖffentlich
blog-detail.htmlBlog-Einzelseite (?post=id)Öffentlich
events.htmlEvents-Übersicht (.edc-Karten)Öffentlich
event-detail.htmlEvent-Einzelseite (?event=slug)Öffentlich
loadout.htmlAusrüstungs-ShowcaseÖffentlich
packlists.htmlPacklisten-AnsichtÖffentlich
stats.htmlClub-Statistik (Strava km, Jahresvergleich)Öffentlich
map.htmlInteraktive Karte (Startpunkte & Locations)Öffentlich
login.htmlLogin / Registrierung / Magic LinkÖffentlich
profil.htmlEigenes Profil bearbeitenEingeloggt (JWT)
mitglieder.htmlCommunity-ÜbersichtEingeloggt (JWT)
impressum.htmlImpressum / DatenschutzÖffentlich
admin.htmlContent Management SystemPasswort-geschützt
admin-docs.htmlDiese DokumentationAdmin-intern
🔄 Datenfluss

So fließen Inhalte vom Admin-Formular bis zur öffentlichen Seite:

Publish-Flow (vollständig)

Admin-Formular
localStorage
mb_content
„Speichern & Deployen"
content-data.js
generiert
GitHub Push
Pi-Server
git pull / 2min
Öffentliche Website

Content-Lade-Reihenfolge (Seitenladen)

content-data.js
MagBikingStatic
+
localStorage
mb_content
deepMerge()
content.js
DOM-Injection
content-inject.js
⚠️ Merge-Priorität
Basis: window.MagBikingStatic (aus content-data.js) — ist das deployed, wird dieses als Basis genommen.
Override: localStorage.mb_content — Admin-Änderungen überschreiben die Basis.
Fallback: Wenn beide leer, werden die DEFAULTS aus content.js genutzt.

Wichtig: Arrays werden komplett ersetzt (nicht gemergt). Leere Strings aus localStorage überschreiben keine vorhandenen Werte in der Basis.

Script-Ladereihenfolge (index.html)

// Alle Scripts haben defer — Ausführung in Dokumentreihenfolge:
1. content-data.js      // window.MagBikingStatic = {...}
2. js/content.js        // window.MagBiking.Content API
3. js/content-inject.js // DOM mit Inhalten befüllen
4. js/nav.js            // Navigation, Burger-Menü
5. js/animations.js    // Scroll-Reveal, Parallax
6. js/counter.js       // Hero-Stats animieren (läuft nach inject!)
7. js/supabase-client.js // API-Client (Pi-API REST + JWT), muss vor club-stats!
8. js/club-stats.js    // Community-Statistiken (Pi-API /api/stats)
9. js/contact.js      // Formular-Handler (Self-Hosted Pi-API)
11. js/gpx-parser.js   // GPX-Parsing & Canvas-Charts
12. js/rideouts.js     // window.MagBiking.getRidesForDisplay()
13. js/index-rideouts.js // Rideout-Widget rendern
14. js/lightbox.js     // Galerie-Lightbox
📊 Live-Status LIVE aus localStorage

Diese Werte werden direkt aus dem aktuellen Datenstand gelesen.

Blog-Beiträge
Geplante Rides
Katalog-Items
Events
Galerie-Bilder
localStorage-Größe
💾 Aktueller Datenstand
Lädt…
📄 HTML-Seiten
📋 index.html — Hauptseite

Die Landing Page enthält alle 10 Sektionen als statisches HTML-Gerüst. Inhalte werden von content-inject.js überschrieben.

Sektionen (in Reihenfolge)

ID / KlasseAbschnittWird befüllt von
#heroHero mit Hintergrundbild & Statscontent-inject.js
#aboutCommunity-Beschreibungcontent-inject.js
#eventsFeatured Event (großer Karte)content-inject.js
#rideoutsGeplante Rides (Featured + Grid)index-rideouts.js
#ridesEvents-Grid (8 Karten)content-inject.js
#equipmentEquipment-Bereichcontent-inject.js
#blogBlog-Vorschau (neuester Beitrag)content-inject.js
#galleryMasonry-Galeriecontent-inject.js
#sponsorsPartner & Sponsorencontent-inject.js
#contactKontaktformular & Social-Linkscontent-inject.js + contact.js
⚙️ admin.html — CMS

Das vollständige Content-Management-System. Login-Status in localStorage (bleibt über Reloads, Tab-Schließen und Browser-Neustarts gespeichert). Abmelden explizit über „← Abmelden". Achtung: Passwort ist im HTML-Quellcode sichtbar.

Admin-Sektionen (Sidebar)

Hero · About · Featured Event · Rideouts · Events-Grid · Equipment · Katalog · Loadout · Kontakt · Galerie · Sponsoren · Blog · Packlisten · Footer

Admin-Modals

Versionsverlauf: Letzte gespeicherte Stände wiederherstellen (in mb_backups)
Speicher-Übersicht: localStorage-Nutzung visualisieren
GitHub-Einstellungen: Repo, Branch, Token konfigurieren

📑 Unter-Seiten (blog, events, loadout, packlists, app)

Alle Unterseiten folgen dem gleichen Muster:

// Script-Ladereihenfolge auf jeder Unterseite:
1. content-data.js    // MagBikingStatic laden
2. js/content.js     // Content-API bereitstellen
3. js/gpx-parser.js  // (wenn GPX benötigt)
4. js/[page].js      // Seiten-spezifischer Renderer
SeiteRendererDatenquelle
blog.htmljs/blog.jsdata.blog.posts[]
events.htmljs/events.jsdata.eventsGrid.cards[]
loadout.htmljs/loadout.jsdata.catalog.items[] + data.loadout.selectedIds[]
packlists.htmljs/packlists.jsdata.packlists.lists[] + data.catalog.items[]
app.htmljs/app.jsMagBiking.getRidesForDisplay(8)
JavaScript-Module
Core
Seiten-Renderer
Admin
Utilities
DateiModulBeschreibung
js/content.jsContent-StoreZentrale Daten-API. DEFAULTS definiert, deepMerge(), Content.get/set/getSection. MUSS als erstes geladen werden.
js/content-inject.jsDOM-InjectorÜberschreibt alle statischen HTML-Elemente mit Inhalten aus dem Content-Store. Läuft einmalig beim Seitenladen.
js/rideouts.jsRideout-EngineRide-Generierung, Template-Rotation, getRidesForDisplay(). Lädt Rides aus localStorage → MagBikingStatic → Content.getSection.
content-data.jsStatischer Exportwindow.MagBikingStatic = {...} — generiert vom Admin. Enthält alle deployt-ten Inhalte inkl. Thumbnails.
DateiSeiteBeschreibung
js/index-rideouts.jsindex.htmlRendert Featured-Ride-Karte + 3er-Grid. Zeigt GPX-Höhenprofil wenn vorhanden.
js/blog.jsblog.htmlRendert alle sichtbaren Blog-Beiträge. Markdown-Parsing (**fett**, _kursiv_). Sektionstypen: text, image-text, gallery, gpx.
js/events.jsevents.htmlRendert Events-Detail-Karten mit Komoot/Strava-Links, GPX-Download, Streckenbeschreibung.
js/loadout.jsloadout.htmlLiest catalog.items gefiltert durch loadout.selectedIds. Gruppiert nach Kategorie. Zeigt imageSrcThumb || imageSrc.
js/packlists.jspacklists.htmlLiest packlists.lists mit item-IDs. Löst IDs gegen catalog.items auf. Zeigt Gesamtgewicht.
js/app.jsapp.htmlPWA-Listenansicht aller geplanten Rides. Zeigt GPX-Höhenprofil, Komoot/Strava-Links, Download.
DateiFunktionBeschreibung
js/admin.jsHaupt-Editor~3000 Zeilen. Alle Formular-Renderer, Image-Upload, Version-History, collectAll(), loadAll(). Größte Datei im Projekt.
js/admin-github.jsGitHub-IntegrationToken-Management, Push content-data.js, Privacy-Scanner, Verbindungstest, Branch-Auswahl.
js/admin-storage.jsSpeicher-ModalStorage-Übersicht, GitHub-Dateibrowser, localStorage-Quota-Anzeige.
js/admin-backup.jsBackup/RestoreJSON-Export und -Import aller Inhalte. Strippt Base64-Bilder (imageSrc + src) vor Export.
DateiFunktionBeschreibung
js/gpx-parser.jsGPX-ParserparseGPX(xmlStr) → {points, stats}. Haversine-Distanz. drawChart(canvas, points, stats). Downsampling auf 200 Punkte.
js/nav.jsNavigationScroll-Schatten, Burger-Menü Toggle, Active-Link via IntersectionObserver.
js/animations.jsAnimationenScroll-Reveal (.reveal), Card-Stagger (80ms), Parallax Hero (0.25× Scroll).
js/counter.jsStat-CounterAnimiert .stat-number[data-target]. Ease-out-Kurve, 2s Dauer. >1000 → "Xk". Trigger via IntersectionObserver.
js/contact.jsKontaktformularSelf-Hosted Pi-API via /api/contact. Honeypot-Spam-Schutz, DSGVO-Checkbox, Fehlermeldung bei Fehler.
js/lightbox.jsGalerie-LightboxBildvergrößerung bei Klick. Tastatur-Navigation. Touch-Support.
js/supabase-client.jsAPI-ClientAPI-Client (Pi-API REST + JWT Token-Handling). Verwaltet Access-/Refresh-Tokens, automatisches Token-Refresh, Auth-Header-Injection. Muss vor club-stats.js geladen werden.
js/club-stats.jsCommunity-StatsLiest gefahrene km aus Pi-API (/api/stats). Zeigt Jahresvergleich der Strava-Club-Statistiken.
🎨 CSS-System

Design-Tokens (css/base.css)

/* Farben */
--accent: #FF6B00;          /* Orange – Primärfarbe */
--accent-light: #FF8C3A;    /* Hover-Orange */
--accent-glow: rgba(255,107,0,0.08);

/* Hintergründe */
--bg: #FFFFFF;  --bg-2: #F7F7F9;  --bg-3: #EFEFF2;

/* Text */
--text: #1A1A2E;  --text-muted: #6B6B80;  --text-faint: #ABABBA;

/* Schriften */
--font-body: 'Inter', sans-serif;   /* Fließtext */
--font-display: 'Bebas Neue';       /* Headlines, Zahlen */

/* Layout */
--nav-h: 72px;  --radius: 14px;  --radius-lg: 22px;

Responsive Breakpoints

🖥 Desktop (>1024px)
Volle Layouts, alle Elemente sichtbar, mehrspaltige Grids.
💻 Tablet (≤1024px)
Einspaltiger Layout, einige Elemente (ab 5) ausgeblendet.
📱 Mobil (≤768px)
Burger-Menü, kleinere Schrift, keine seitlichen Abstände.
📱 Mobil-S (≤480px)
Minimales Padding, vollständig gestapelte Layouts.

CSS-Dateien

DateiAbschnittGeladen auf
css/base.cssReset, Variablen, Buttons, Utility-KlassenAlle
css/nav.cssNavigationsleisteindex.html
css/hero.cssHero-Sektionindex.html
css/about.cssCommunity-Sektionindex.html
css/featured-event.cssFeatured-Event-Karteindex.html
css/events-grid.cssEvents-Karten-Gridindex.html
css/rideouts.cssRideout-Widget auf Startseiteindex.html
css/app.cssPWA-App-Stileapp.html
css/blog.cssBlog-Vorschau & Blog-Seiteindex + blog.html
css/equipment.cssEquipment-Sektionindex.html
css/gallery.cssMasonry-Galerieindex.html
css/sponsors.cssSponsoren-Bereichindex.html
css/contact.cssKontaktformularindex.html
css/footer.cssFooterindex.html
css/responsive.cssAlle Media Queriesindex.html
css/lightbox.cssBild-Lightboxindex.html
css/loadout.cssLoadout-Seiteloadout.html
css/packlists.cssPacklisten-Seitepacklists.html
css/events.cssEvents-Detail-Seiteevents.html
css/admin.css + 4 weitereAdmin-Panel-Stileadmin.html
🗄 Content-Store (Daten-API)

Alle Seiteninhalte werden über window.MagBiking.Content gelesen. Diese API ist in js/content.js definiert.

API-Methoden

// Alle Inhalte (merged: MagBikingStatic + localStorage):
Content.get()              → { hero, about, featuredEvent, ... }

// Einzelne Sektion:
Content.getSection('blog') → { posts: [...] }

// Speichern (gibt { ok, bytes } oder { ok: false, quota } zurück):
Content.set(data)          → { ok: true, bytes: 12345 }

// Sektion auf Default zurücksetzen:
Content.resetSection('blog')
📐 Datenmodell (DEFAULTS)
📝 blog.posts[] — Blog-Beiträge
{
  id:          'post_1234567890',
  title:       'Beitragstitel',
  date:        '2026-03-15',     // ISO-Format YYYY-MM-DD
  category:   'Rideout',
  teaser:      'Kurzbeschreibung…',
  hidden:      false,            // true = nicht öffentlich
  sections: [
    {
      type: 'text',             // text | image-text | gallery | gpx
      content: 'Fließtext…',
      images: [{ src, srcThumb, caption, imagePosition }],
      imageAlign: 'left',       // nur bei image-text
      gpxFile: { name, points, stats, downloadUrl },  // nur bei gpx
      gpxTitle: 'Tag 1: Route…'  // nur bei gpx
    }
  ],
  importedFrom: 'ride_2026-03-15'  // optional: Quelle beim Import
}
🚴 rideouts.rides[] — Geplante Rides
{
  date:         '2026-04-06',   // YYYY-MM-DD — Pflichtfeld!
  title:        'Sunday Ride',
  subtitle:     'Bodensee Loop',
  time:         '09:00',
  distance:     80,              // km
  elevation:    650,             // Höhenmeter
  difficulty:  'Mittel',        // Leicht | Mittel | Anspruchsvoll
  meetupPlace: 'Marktplatz, Bad Waldsee',
  tag:          'Rennrad',
  tagColor:    '#FF6B00',
  komootLink:  'https://…',
  stravaLink:  'https://…',
  gpxFiles: [{ name, points, stats, downloadUrl }],
  hidden:      false
}
📦 catalog.items[] — Zentrale Ausrüstungsdatenbank
{
  id:             'cat_1234567890',  // eindeutige ID (genId())
  name:           'Specialized Tarmac SL7',
  category:      'Bikes & Rahmen',
  weight:         6800,            // Gramm (oder leer)
  note:           'Di2, 50/34, 11-30',
  imageSrc:      'data:image/jpeg;base64,…',  // Full-Bild
  imageSrcThumb: 'data:image/jpeg;base64,…',  // 200px Thumb für Ladezeit
  imagePosition: { x: 50, y: 50 }  // object-position %
}

// loadout.selectedIds[] = [ 'cat_1234567890', … ]  (geordnet)
// packlists.lists[].items[] = [ 'cat_1234567890', … ]  (item-IDs)
📅 eventsGrid.cards[] — Events
{
  category:     'Epic Ride',
  title:        'Nizza – Barcelona',
  description: 'Kurzbeschreibung',
  imageSrc:    '',             // leer → defaultSrc
  defaultSrc:  'Grafiken/Nizza-Barcelona.webp',
  imageSrcThumb, imagePosition,
  date:        '2026-07-15',
  distance:   '820',  elevation: '9200',
  startLocation, route, detailText,
  komootLink, stravaLink, stravaStats,
  gpxData: { name, points, stats, downloadUrl },
  hidden: false
}
🖼 Bilder-System

Bilder werden als Base64 direkt in localStorage gespeichert. Dabei werden automatisch zwei Versionen angelegt:

📷 imageSrc
Original-Bild in voller Größe. Base64 kodiert. Wird beim Deploy nach content-data.js exportiert aber mit stripBase64() → leer gesetzt (um Dateigröße zu reduzieren). Beim nächsten Admin-Besuch aus localStorage wiederhergestellt.
🔍 imageSrcThumb
Thumbnail (max. ~200px). Base64 kodiert. Wird beim Export in content-data.js behalten. Dient als LQIP (Low Quality Image Placeholder) für schnelles Laden. Auf der öffentlichen Seite wird zuerst der Thumb gezeigt, dann das Vollbild nachgeladen.

Progressive Image Loading

progressiveImg(imgEl, thumb, full, position):
  1. Setzt sofort img.src = thumb  (schnell, sofort sichtbar)
  2. Lädt full als neues Image-Objekt im Hintergrund
  3. Wenn geladen: img.src = full  (sanfter Übergang)
  // Base64-Bilder werden direkt gesetzt (kein nachladen nötig)
⚠️ Wichtig: Deploy vs. Admin-Ansicht
Auf der öffentlichen Website (nach Deploy): Nur imageSrcThumb ist in content-data.js gespeichert. Das Vollbild (imageSrc) ist leer → das Thumbnail wird als Vollbild verwendet.

Im Admin: Vollbild ist aus localStorage verfügbar und wird angezeigt.

Für echte Vollbilder auf der öffentlichen Seite → GitHub-Upload aktivieren. Das Bild wird zu GitHub hochgeladen, eine URL wird gespeichert und exportiert.
📍 GPX & Routen-System

GPX-Dateien werden im Browser geparst und als JavaScript-Objekte gespeichert. Kein Server-seitiges Parsing.

GPX-Parser API

// Parsen:
var result = MagBiking.GPX.parseGPX(xmlString);
// result:
{
  name:   'route-name.gpx',
  points: [[0.0, 450.2], [1.3, 480.1], …],  // [distKm, elevM]
  stats: {
    distance: '82.4', elevGain: '1240',
    elevLoss: '1180', maxElev: '1120', minElev: '390'
  },
  downloadUrl: null  // wird nach GitHub-Upload befüllt
}

// Höhenprofil zeichnen:
MagBiking.GPX.drawChart(canvasEl, points, stats);

GPX-Nutzung je Bereich

BereichDateifeldDarstellung
Rideoutsride.gpxFiles[]Höhenprofil in Featured-Karte (index.html) + app.html
Events-Gridcard.gpxDataHöhenprofil in events.html Detail-Ansicht
Blogsection.gpxFileHöhenprofil + Stats als eigene Sektion im Beitrag
🚴 Rideout-System

Datenlage-Priorität

1. localStorage
mb_content.rideouts.rides
→ sonst →
2. MagBikingStatic
rideouts.rides
→ sonst →
3. leeres Array
kein Auto-Generate mehr
💡 Rides anlegen
Rides werden ausschließlich manuell im Admin unter „Geplante Rideouts" angelegt. Die 4 Templates (Sunday Ride, Long Ride, Easy Ride, Gravel Sunday) dienen nur noch als Basis beim schnellen Anlegen über „Aus Template anlegen".

Vergangenheitsdaten werden automatisch herausgefiltert (date < today).
📝 Blog-System

Beitrag-Struktur

Jeder Blog-Beitrag besteht aus beliebig vielen Abschnitten (Sections):

🔤 Text
Fließtext mit optionalem Bild oben. Markdown-Subset: **fett**, _kursiv_, • Aufzählung (Zeilenstart mit •), Leerzeile = Absatz.
🖼 Bild + Text
Bild links oder rechts neben dem Text. Ausrichtung im Admin wählbar.
📸 Galerie
Mehrere Bilder in einem Grid mit optionalen Bildunterschriften. Lightbox-fähig.
📍 GPX-Route
Höhenprofil-Canvas + Streckenstatistiken + optionaler Download-Link. Titel-Feld für Routenbeschreibung.

Import-Funktion

Blog-Beiträge können aus Events-Grid-Einträgen oder Rideouts importiert werden. Der Import erstellt automatisch Text-, Bild- und GPX-Sektionen aus dem vorhandenen Inhalt.

📦 Zentral-Katalog (Ausrüstung)
catalog.items[]
Zentrale Datenbank
loadout.selectedIds[]
Auswahl für Loadout-Seite
+
packlists.lists[].items[]
Auswahl für Packlisten
💡 Workflow: Neues Equipment
1. Im Admin → Katalog: Neuen Gegenstand mit Name, Kategorie, Gewicht, Notiz und Bild anlegen.
2. Im Admin → Loadout: Gegenstand via Checkbox auswählen → erscheint auf loadout.html.
3. Im Admin → Packlisten: Gegenstand einer Liste zuweisen → erscheint auf packlists.html.
4. Speichern & Deployen.
🛠 Admin-Guide (Kurzreferenz)
💾 Speichern & Deployen

„Speichern & Deployen" (Haupt-Button oben rechts) führt folgendes aus:

1. collectAll() — alle Formularfelder werden gelesen
2. Content.set(data) — in localStorage gespeichert
3. Snapshot für Versionsverlauf wird angelegt (mb_backups)
4. content-data.js wird generiert (window.MagBikingStatic = ...)
5. Optional: GitHub-Push wenn Token konfiguriert
6. Pi-Server: git pull alle 2 Minuten → Änderungen live

🖼 Bilder hochladen

Klick auf Bild-Widget: Dateiauswahl öffnet sich.
Drag & Drop: Bild auf das Widget ziehen.
Position: Im Widget kann die Bildausrichtung (object-position) angepasst werden.
Entfernen: × Button im Bild-Widget.

Bilder werden als Base64 gespeichert. Ein kleines Thumbnail (~200px) wird automatisch erstellt und in content-data.js exportiert. Das Vollbild bleibt nur in localStorage.

📋 Versionsverlauf

Jedes Speichern legt automatisch einen Snapshot an. Zugriff über „Verlauf" Modal (Uhr-Icon oben).

Snapshots sind in localStorage.mb_backups gespeichert. Bei vollem localStorage werden älteste Snapshots nicht automatisch gelöscht — manuell bereinigen über das Speicher-Modal.

🐙 GitHub-Sync einrichten

Einstellungen: ⚙️ Icon oben rechts → GitHub-Tab
Felder: Repository (user/repo), Branch (main), Pfad (content-data.js), Token (GitHub PAT)

Token braucht: repo Scope (Lesen + Schreiben)
Token wird in localStorage.mb_gh_settings gespeichert.

🚀 Deployment-Setup
📡 Pi-Server Setup
Die Website läuft auf einem Raspberry Pi (oder ähnlichem Server). Setup-Skript: pi-setup.sh.

Cron-Job (alle 2 Min):
*/2 * * * * cd /var/www/magbiking && git pull origin main

Pi holt Änderungen automatisch aus dem GitHub-Repo und stellt sie sofort bereit.
🧰 Pi-Stack & Patchstände
Übersicht der auf dem Pi installierten Software. Stand: 2026-04-15 – Hostname OpenClaw.
KomponenteVersion / PatchstandPrüfbefehl
HardwareRaspberry Pi 5 Model B Rev 1.0 (aarch64)cat /proc/device-tree/model
OSDebian GNU/Linux 13 (trixie)lsb_release -a
Kernel6.12.62+rpt-rpi-2712uname -a
Firmware / EEPROMup to date (2025-12-08)sudo rpi-eeprom-update
Webservernginx 1.26.3nginx -v
Git2.47.3git --version
Cron3.0pl1-197dpkg -l cron | tail -1
Node.js (API)22.22.0node -v
npm10.9.4npm -v
Express (API)noch ermittelnnpm ls express
SQLitenicht installiert (CLI)sqlite3 --version
cloudflared (Tunnel)2026.3.0 (built 2026-03-09)cloudflared --version
API-Servicenoch ermittelnsystemctl status magbiking-api
Pi Uptimeuptime -p
Letztes apt-Updatels -l /var/log/apt/history.log
Auto-Update: pi-sysinfo.sh läuft täglich per Cron auf dem Pi und schreibt /var/lib/magbiking/sysinfo.json. Diese Seite holt die Daten live über api.mag-biking.com/api/sysinfo. Falls der Endpoint nicht erreichbar ist, werden die zuletzt manuell eingetragenen Werte angezeigt (Fallback).
🔄 Publish-Workflow Schritt für Schritt
1. admin.html aufrufen
2. Inhalte bearbeiten
3. „Speichern & Deployen" klicken
4. Browser: content-data.js wird heruntergeladen
5. Datei manuell committen ODER GitHub-Push ist aktiviert → automatisch
6. Pi: git pull (max. 2 Minuten) → Änderungen live
💾 Speicher-Verwaltung

localStorage-Schlüssel

SchlüsselInhaltGröße
Wird geladen…
⚠️ localStorage Limit
Browser erlauben typischerweise 5–10 MB pro Domain. Base64-Bilder verbrauchen viel Platz (~1.3× der Originalgröße). Bei Quota-Fehler: Bilder im Speicher-Modal löschen oder Anzahl Backups reduzieren.
🐛 Bekannte Issues

Offen (2)

Bug BUG-02 · deepMerge überschreibt keine leeren Felder
Datei: js/content.js Zeile 137. Leere Strings aus localStorage überschreiben keine nicht-leeren Default-Werte. Admin kann Felder nicht wirklich leeren wenn ein Default existiert.
Warum offen: Die Logik ist ein absichtlicher Schutz gegen teils leere localStorage-Einträge aus alten Versionen. Eine Änderung erfordert einen expliziten "Feld leeren"-Mechanismus (z.B. Sentinel-Wert), ohne bestehende Inhalte zu brechen. Kein Datenverlust-Risiko – nur UX-Einschränkung.
⚠ Teilweise Legal BUG-05 · Impressum/Datenschutz Links zeigen auf #
Footer-Links für Impressum, Datenschutz und Newsletter zeigten auf #. Fehlende DSGVO-Seiten sind rechtlich problematisch.

Impressum (2026-03-31 behoben): impressum.html erstellt. Admin-Editor unter "⚖️ Impressum" zum Befüllen der Pflichtangaben (§5 TMG: Name, Adresse, E-Mail). Footer-Link in index.html zeigt jetzt auf impressum.html.

Noch offen: datenschutz.html (DSGVO Art. 13) fehlt noch – Link zeigt weiterhin auf #. Newsletter-Link zeigt auf # (Newsletter noch nicht implementiert).

Nächster Schritt: Impressum im Admin befüllen + veröffentlichen. Dann datenschutz.html anlegen (Empfehlung: Datenschutz-Generator von eRecht24.de).
Behoben (9) – BUG-01, 03, 04, 06–11
✓ Behoben Bug BUG-01 · Footer-Rideout-Link funktioniert nie
Datei: js/content-inject.js. Prüfte window.MagBiking.M — diese Eigenschaft existiert nicht.
Fix (2026-03-31): Guard-Bedingung auf window.MagBiking.getRidesForDisplay geändert.
✓ Behoben Architektur BUG-03 · Packlisten-Mismatch auf index.html
content-inject.js las nur equipment.packlists (alte Struktur).
Fix (2026-03-31): Prüft jetzt zuerst d.packlists.lists (neue Struktur), Fallback auf alte Daten.
✓ Behoben Typo BUG-04 · "Marllorca" in Dateinamen
Fix (2026-03-31): Datei umbenannt zu Grafiken/Mallorca Trainingslager.png. Alle Referenzen aktualisiert.
✓ Behoben Hoch BUG-06 · Kontaktformular zeigt Erfolg auch bei Fehler
Behebung (2026-04-21): EmailJS komplett durch Self-Hosted Pi-API ersetzt. catch-Zweig zeigt jetzt Fehlermeldung.
✓ Behoben Hoch BUG-07 · Unhandled Promise-Rejections in events.js / event-detail.js
Behebung (2026-04-21): Pro hydrateGpx()-Promise eigenes .catch(() => null). Zusätzlich .catch(console.warn) am äußeren Promise.all.
✓ Behoben Mittel BUG-08 · Resize-Listener-Leak in Höhenprofil-Rendern
Behebung (2026-04-21): removeEventListener vor jedem addEventListener. Betrifft app.js, events.js, event-detail.js, blog-detail.js.
✓ Behoben Mittel BUG-09 · Service-Worker-Admin-Bypass ohne respondWith
Behebung (2026-04-21): Expliziter e.respondWith(fetch(e.request, { cache: 'no-store' })) für Admin-Pfade in sw.js.
✓ Behoben Mittel BUG-10 · MutationObserver-Spam in lightbox.js
Behebung (2026-04-21): Callback mit 100 ms Debounce versehen.
✓ Behoben Niedrig BUG-11 · Null-Deref in event-detail.js
Behebung (2026-04-21): Guard if (miniEl && miniEl.parentElement) vor dem Zugriff.
🔒 Security Backlog

Bekannte Sicherheitsrisiken, priorisiert nach Schweregrad. Letzter Audit: 2026-04-21 (Full-Stack inkl. Pi-API). 15 von 20 Findings behoben.

📊 Status-Übersicht
IDSchweregradTitelStatus
SEC-01HochGitHub Token in localStorage✓ Behoben
SEC-02HochAdmin-Passwort clientseitig✓ Behoben
SEC-03HochKein Brute-Force-Schutz✓ Behoben
SEC-04Mittelcontent-data.js öffentlich✓ Behoben
SEC-05MittelKein CSP-Header✓ Behoben
SEC-06MittelinnerHTML XSS-Vektoren✓ Behoben
SEC-07Mitteladmin.html ohne Server-Schutz✓ Behoben CF Access
SEC-08Niedrigbtoa kein Sicherheitsmechanismus✓ Behoben
SEC-09NiedrigLegacy Supabase Anon-Key (migriert zu Pi-API)✓ Migriert
SEC-10HochXSS via onerror Fallback-URL✓ Behoben
SEC-11HochTiming-Attack API-Key✓ Behoben
SEC-12HochKontaktformular kein Rate-Limit✓ Behoben
SEC-13MittelCORS-Header Leak✓ Behoben
SEC-14MittelFehlender HSTS-Header✓ Behoben
SEC-15MittelSysinfo Pfad-Leak✓ Behoben
SEC-16NiedrigKein Body-Size-Limit✓ Behoben
SEC-17MittelCSP unsafe-inline✓ Behoben
SEC-18MittelFehlende SRI✓ Behoben
SEC-19NiedrigPW-Hash im ClientEntschärft (CF Access)
SEC-20NiedrigGH-Token im FrontendEntschärft (CF Access)

Architekturabhängig (3) – Langfristig

✓ Migriert SEC-09 · Supabase Anon-Key – nicht mehr relevant
Supabase wurde durch die self-hosted Pi-API (Express + SQLite + JWT) ersetzt. Kein Anon-Key mehr im Frontend.

Status: Erledigt durch Migration zu Pi-API. Auth läuft jetzt über JWT-Tokens (Access + Refresh), keine externen Cloud-Dienste mehr.
Niedrig SEC-19 · Admin-Passwort-Hash im Client-Code
_adminPWHash in content-data.js sichtbar. Durch Cloudflare Access entschärft.

Architekturabhängig: Erfordert serverseitige Auth (Pi-API mit bcrypt/Argon2). Aktuell durch CF Access geschützt.
Niedrig SEC-20 · GitHub Personal Access Token im Frontend
Token in sessionStorage (nicht persistent), aber in DevTools sichtbar. Durch Cloudflare Access geschützt.

Architekturabhängig: Erfordert Backend-Proxy (Pi-API als GitHub-Vermittler). Aktuell durch CF Access geschützt.
Behoben (18) – SEC-01–08, 10–18

Hoch (2026-03-31)

✓ Behoben Hoch SEC-01 · GitHub Token im localStorage
Behebung (2026-03-31): Token nur noch in sessionStorage. Auto-Migration entfernt alten Eintrag.
✓ Behoben Hoch SEC-02 · Admin-Passwort clientseitig
Behebung (2026-03-31): SHA-256 + Salt via Web Crypto API. Session-Token mit Ablauf (7 Tage).
✓ Behoben Hoch SEC-03 · Kein Brute-Force-Schutz
Behebung (2026-03-31): 5 Fehlversuche → 15 Min Sperre. Exponentielles Backoff.
✓ Behoben Hoch SEC-10 · XSS via onerror Fallback-URL
Behebung (2026-03-31): esc(fallback).replace(/'/g,"\\'") in events.js und event-detail.js.

Mittel (2026-03-31)

✓ Behoben Mittel SEC-05 · Kein CSP-Header
Behebung (2026-03-31): CSP-Meta-Tags in alle 9 HTML-Dateien eingefügt.
✓ Behoben Mittel SEC-06 · innerHTML XSS-Vektoren
Behebung (2026-03-31): Code-Review aller 39 innerHTML-Stellen. 4 fehlende esc()-Aufrufe ergänzt.
✓ Behoben Mittel SEC-07 · admin.html ohne Server-Schutz
Behebung (2026-04-21): Cloudflare Access (Zero Trust) mit E-Mail-OTP. Defense in Depth: Admin-Passwort bleibt als zweite Schicht.
✓ Behoben Niedrig SEC-08 · btoa ist Encoding, keine Verschlüsselung
Behebung (2026-03-31): Klärender Kommentar in admin-github.js ergänzt.

Pi-API Audit (2026-04-21)

✓ Behoben Hoch SEC-11 · Timing-Attack auf API-Key
Behebung: crypto.timingSafeEqual() für konstante Vergleichszeit.
✓ Behoben Hoch SEC-12 · Kontaktformular ohne eigenes Rate-Limiting
Behebung: Separater Rate-Limiter: max. 5 Anfragen/Minute pro IP.
✓ Behoben Mittel SEC-13 · CORS-Header bei nicht-erlaubter Origin
Behebung: CORS-Header nur bei erlaubter Origin gesetzt.
✓ Behoben Mittel SEC-14 · Fehlender HSTS-Header
Behebung: Strict-Transport-Security: max-age=31536000; includeSubDomains; preload.
✓ Behoben Mittel SEC-15 · Sysinfo-Route leakt Dateisystem-Pfade
Behebung: Generische Fehlermeldung ohne Pfad-Informationen.
✓ Behoben Niedrig SEC-16 · Kein Body-Size-Limit
Behebung: express.json({ limit: '3mb' }) (erhöht für Avatar-Upload).

Frontend-Hardening (2026-04-21)

✓ Behoben Mittel SEC-04 · content-data.js öffentlich lesbar auf GitHub
Behebung (2026-04-21): Repository auf privat gesetzt.
✓ Behoben Mittel SEC-17 · CSP 'unsafe-inline' für Scripts
Behebung (2026-04-21): Alle Inline-Scripts in externe .js-Dateien extrahiert (13 neue Module). 'unsafe-inline' aus script-src in allen 13 HTML-Dateien entfernt.
✓ Behoben Mittel SEC-18 · Fehlende Subresource Integrity (SRI)
Behebung (2026-04-21): Google Fonts self-hosted (woff2 in fonts/, css/fonts.css). Externe Font-Abhängigkeit vollständig eliminiert.
🔍 Funktions-Audit

Geprüft: 2026-04-21 · Vollständige Prüfung aller JS-Module, Script-Ladeorder und Admin↔Öffentlich-Datenkonsistenz. Inkl. Pi-API Security Audit.

Ergebnis: Alle Module

Modul Geladen auf Status
content.jsAlle Seiten✓ OK
content-data.jsAlle Seiten✓ OK
content-inject.jsindex.html✓ OK
nav.jsindex.html✓ OK
animations.jsindex.html✓ OK
counter.jsindex.html✓ OK
supabase-client.jsindex.html✓ Behoben fehlte – jetzt vor club-stats.js
club-stats.jsindex.html✓ OK Exit via DEMO_MODE-Guard
contact.js (Self-Hosted API)index.html✓ OK
gpx-parser.jsindex, events, event-detail, app✓ OK
rideouts.jsindex, app, event-detail✓ OK
index-rideouts.jsindex.html✓ OK
lightbox.jsindex, events, event-detail, blog✓ OK
events.jsevents.html✓ OK
event-detail.jsevent-detail.html✓ OK
blog.jsblog.html✓ OK
packlists.jspacklists.html✓ OK
loadout.jsloadout.html✓ OK
app.jsapp.html✓ OK
admin.jsadmin.html✓ OK
admin-github.jsadmin.html✓ OK
admin-backup.jsadmin.html✓ OK
admin-storage.jsadmin.html✓ OK

Admin ↔ Öffentlich: Datenkonsistenz

Datenbereich Admin verwaltet Öffentlich angezeigt durch Status
herocollectHero()content-inject.js
aboutcollectAbout()content-inject.js
featuredEventcollectFeaturedEvent()content-inject.js (Slider-Fallback)
eventsGridcollectEventsGrid()content-inject.js, events.js, Slider
rideoutscollectRideouts()index-rideouts.js, app.js
equipmentcollectEquipment()content-inject.js
contactcollectContact()content-inject.js
gallerycollectGallery()content-inject.js
catalog / loadoutcollectCatalogData(), collectLoadout()packlists.js, loadout.js
sponsorscollectSponsors()content-inject.js
blogcollectBlog()blog.js, Vorschau in content-inject.js
footercollectFooter()content-inject.js (teilw.)
Behobene Fehler (2026-03-31)
✓ Behoben supabase-client.js fehlte in index.html
Fix: supabase-client.js vor club-stats.js in index.html eingefügt.
✓ Behoben app.html: Service Worker nicht registriert
Fix: navigator.serviceWorker.register('/sw.js') am Ende von app.html ergänzt.

Hinweise (kein Handlungsbedarf)

supabase-client.js: Jetzt Pi-API-Client (JWT Token-Handling)
supabase-client.js wurde zur Pi-API migriert und fungiert als REST-Client mit JWT-Token-Management. Kommuniziert mit api.mag-biking.com statt Supabase Cloud. Kein DEMO_MODE mehr nötig.
admin.html: Kein Service Worker (gewollt)
Der Admin-Bereich registriert bewusst keinen Service Worker. Admin-Requests sollen nicht gecacht werden – jeder Reload muss die aktuellen Daten laden.
🛡 API-Sicherheit & DSGVO-Vorbereitung
Architekturübersicht
[Browser] → HTTPS → [Cloudflare Edge / DDoS-Schutz]
    ↓ Tunnel (kein offener Port)
[Raspberry Pi 5 / nginx 1.26.3]
    ↓ Reverse-Proxy
[Express.js / Node 22.22.0]
    ↓
Middleware-Chain: Rate-Limit → CORS → Security-Headers → API-Key
    ↓
Routes: /api/stats, /health (public) | /api/contact, /api/sysinfo (API-Key) | /api/auth/*, /api/members/*, /api/events/* (JWT)
    ↓
[SQLite] (Strava-Aktivitäten, zukünftig: Nutzerprofile, Tokens)
Security-Middleware (pi-api/security.js)

Zentrale Middleware-Datei für alle Express-Routes. Einbindung: require('./security').init(app) vor allen Route-Definitionen.

Middleware Schutz Konfiguration
Rate-LimiterMax. Requests pro IP pro Zeitfenster (Default: 60/min). In-Memory, kein npm-Paket.RATE_LIMIT_MAX, RATE_LIMIT_WINDOW_MS
API-KeyPrüft X-API-Key Header, ?key= Query oder Bearer Token gegen API_KEY Env-Var.API_KEY=<random-hex>
Security-HeadersX-Content-Type-Options: nosniff, X-Frame-Options: DENY, Permissions-Policy, kein X-Powered-By
CORSNur erlaubte Origins, GET, POST, PUT, DELETE, OPTIONS, X-API-Key Header erlaubtCORS_ORIGIN=https://mag-biking.com
Input-SanitizerEntfernt HTML-Tags aus POST-Body-Strings, Längenbegrenzung (10.000 Zeichen)
Request-LoggerLoggt Method, Path, Status, Dauer. IP-Adresse wird maskiert (letztes Oktett → .xxx) für DSGVO-Konformität.
API-Key generieren & einrichten
1. Key erzeugen:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

2. In .env eintragen (im API-Verzeichnis auf dem Pi):
API_KEY=<generierter-hex-key>
CORS_ORIGIN=https://mag-biking.com

3. API neustarten: sudo systemctl restart magbiking-api

4. In Admin-Doku testen: Oben in der Pi-Stack-Tabelle den Key eingeben und „Laden“ klicken.
SQLite-Backup (pi-api/backup.sh)
Tägliches Backup der SQLite-Datenbank mit 14-Tage-Rotation.

Setup:
chmod +x /var/www/html/pi-api/backup.sh
echo '30 3 * * * root /var/www/html/pi-api/backup.sh' | sudo tee /etc/cron.d/magbiking-backup

Features:
  • sqlite3 .backup für konsistenten Snapshot (Fallback: cp + WAL)
  • Automatische gzip-Komprimierung
  • Löscht Backups älter als 14 Tage
  • Integritätsprüfung via PRAGMA integrity_check
  • Log in /var/log/magbiking-backup.log
Pfade anpassen: DB_PATH und BACKUP_DIR im Script oder via Env-Vars MAGBIKING_DB_PATH / MAGBIKING_BACKUP_DIR.
⚖ DSGVO-Vorbereitung für personenbezogene Strava-Daten

Sobald das Ranking-/Punkte-System personenbezogene Daten verarbeitet (Strava OAuth, Aktivitäten mit GPS, Nutzerprofile), gelten strenge DSGVO-Anforderungen. Diese müssen vor dem Go-Live umgesetzt sein:

Pflicht Beschreibung Status
Rechtsgrundlage Art. 6 Abs. 1 lit. a DSGVO: Einwilligung. Nutzer verbindet Strava freiwillig. Einwilligung muss explizit, informiert und widerrufbar sein.
Datenschutzerklärung impressum.html erweitern: Welche Daten (Aktivitäten, Distanz, Hm, Zeitstempel, Strava-User-ID), Zweck (Ranking, Stempelbuch), Speicherdauer, Empfänger (nur mag.biking, kein Dritter), Rechte (Auskunft, Löschung, Export, Widerruf).
Datenminimierung Nur speichern was nötig: Distanz, Höhenmeter, Datum, Dauer. Keine GPS-Tracks (Standortdaten = besonders schützenswert). Keine Herzfrequenz, keine privaten Notizen.
Token-Sicherheit Strava OAuth Access- & Refresh-Tokens AES-256-GCM verschlüsselt in SQLite gespeichert. Key aus Env-Var TOKEN_ENCRYPTION_KEY. Modul: pi-api/crypto.js + pi-api/strava-tokens.js.
Recht auf Löschung API-Endpoint DELETE /api/user/data: Löscht alle Aktivitäten, Tokens, Badges, Profildaten eines Nutzers. Muss innerhalb 30 Tagen umsetzbar sein (Art. 17 DSGVO).
Datenexport API-Endpoint GET /api/user/export: Gibt alle gespeicherten Daten als JSON/CSV zurück (Art. 20 DSGVO, Datenübertragbarkeit).
Speicherbegrenzung Aktivitäten max. 2 Jahre aufbewahren (oder nach Saison-Ende aggregieren und Rohdaten löschen). Gelöschte Accounts: Daten sofort entfernen, nicht nur deaktivieren.
Widerruf (Opt-out) Nutzer kann jederzeit Strava-Verbindung trennen und alle Daten löschen lassen. Button im Profil-Bereich: „Strava trennen & Daten löschen“.
Cloudflare DPA Cloudflare-DPA (Data Processing Addendum) im Dashboard akzeptieren. In Datenschutzerklärung als Auftragsverarbeiter nennen – Daten laufen technisch über Cloudflare Edge.
Supabase RLS Migriert zu Pi-API JWT Middleware. Zugriffskontrolle erfolgt über authRoute.jwt / authRoute.jwtOptional Middleware pro Route. Kein Anon-Key mehr im Frontend.
Verschlüsselung at Rest SQLite-Datenbank auf dem Pi: Filesystem-Verschlüsselung (LUKS) oder SQLCipher. Backups ebenfalls verschlüsselt speichern (gpg --symmetric).
Strava-spezifische Anforderungen
  • Strava API Agreement: Vor OAuth-Nutzung akzeptieren – verbietet u.a. Daten-Weitergabe und dauerhafte GPS-Speicherung
  • „Powered by Strava“: Logo-Badge Pflicht auf allen Seiten die Strava-Daten anzeigen
  • Rate-Limits: 100 Requests / 15 Min, 1000 / Tag (pro App). Webhook bevorzugen statt Polling
  • Deauthorization-Webhook: Strava kann POST /api/strava/deauth senden wenn Nutzer Zugriff widerruft → alle Daten löschen
  • Refresh-Token-Flow: Access-Token läuft nach 6h ab, Refresh-Token erneuern und sicher speichern
📑 Verarbeitungsverzeichnis (Art. 30 DSGVO)

Pflichtdokumentation für alle Verarbeitungstätigkeiten mit personenbezogenen Daten. Dieses Verzeichnis muss aktuell gehalten und auf Anfrage der Aufsichtsbehörde vorgelegt werden können.

Feld Inhalt
Verantwortlicher mag.biking Community – Lucas M. (Einzelperson, kein Unternehmen)
Kontakt: siehe impressum.html
Verarbeitungstätigkeit 1 Strava Club Statistik (aktuell aktiv)
Zweck: Gesamtkilometer und Fahrtenanzahl des Strava-Clubs anzeigen
Kategorien betroffener Personen: Keine natürlichen Personen identifizierbar
Datenkategorien: Tages-Aggregatwerte (km-Summe, Anzahl Fahrten) – keine Einzelaktivitäten, keine Nutzernamen, keine Strava-IDs
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)
Empfänger: Keine Weitergabe an Dritte
Speicherdauer: Unbegrenzt (nicht personenbezogen)
Technische Maßnahmen: SQLite auf Raspberry Pi, Zugriff nur über Cloudflare Tunnel, kein offener Port
Verarbeitungstätigkeit 2 Kontaktformular (aktuell aktiv)
Zweck: Kontaktanfragen von Website-Besuchern entgegennehmen und per E-Mail weiterleiten
Kategorien betroffener Personen: Website-Besucher die das Formular ausfüllen
Datenkategorien: Name, E-Mail-Adresse, Interessengebiet, Freitext-Nachricht, maskierte IP-Adresse
Rechtsgrundlage: Art. 6 Abs. 1 lit. a DSGVO (Einwilligung via Checkbox)
Empfänger: E-Mail an Betreiber via Gmail SMTP. Cloudflare als technischer Durchleiter
Speicherdauer: In SQLite auf Pi gespeichert. Löschung auf Anfrage
Technische Maßnahmen: DSGVO-Checkbox, Honeypot-Spam-Schutz, Rate-Limiting, IP-Maskierung, CORS, Input-Validierung
Verarbeitungstätigkeit 4 Strava OAuth & Ranking (geplant, noch nicht aktiv)
Zweck: Personalisiertes Ranking, Punkte-System, Stempelbuch für Community-Mitglieder
Kategorien betroffener Personen: Strava-Nutzer die sich freiwillig verbinden
Datenkategorien: Strava-User-ID, Anzeigename, Aktivitätsdaten (Distanz, Höhenmeter, Datum, Dauer), OAuth-Tokens
Rechtsgrundlage: Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)
Empfänger: Keine Weitergabe an Dritte. Cloudflare als technischer Auftragsverarbeiter (DPA abschließen!)
Speicherdauer: Aktivitäten max. 2 Kalenderjahre, danach Aggregation. Bei Widerruf/Löschung: sofortige Löschung aller Daten
Technische Maßnahmen:
• OAuth-Tokens AES-256-GCM verschlüsselt (crypto.js)
• Encryption-Key in Env-Variable, nicht im Code
• IP-Adressen in Logs maskiert (security.js)
• Rate-Limiting (60 req/min)
• API-Key für sensible Endpoints
• Tägliche Backups mit 14-Tage-Rotation
• Keine GPS-Tracks gespeichert
Verarbeitungstätigkeit 5 Blog-Likes (aktuell aktiv)
Zweck: Anonyme Like-Zählung für Blog-Beiträge
Datenkategorien: Zählerstand pro Post, anonyme Browser-ID (localStorage). Im DEMO_MODE: rein lokal, kein Server
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)
Speicherdauer: Unbegrenzt (anonym)
Auftragsverarbeiter Cloudflare Inc. – Tunnel/CDN (DPA abschließen vor Strava-OAuth-Launch)
Supabase Inc. – Migriert zu self-hosted Pi-API (SQLite). Kein externer Datenbank-Dienst mehr.
Strava Inc. – API-Datenquelle (API Agreement beachten)
Letzte Aktualisierung 21. April 2026
Dateien im Repository (pi-api/)
Datei Zweck
server.jsExpress-App mit Security-Middleware, Stats-, Health-, Sysinfo-, Contact-Endpoints
contact-route.jsPOST /api/contact: Validierung, Honeypot, SQLite-Speicherung, Nodemailer SMTP-Versand
security.jsRate-Limit, API-Key, CORS, Security-Headers, Input-Sanitizer, Logger (zero dependencies)
sysinfo-route.js/api/sysinfo Route mit Stale-Check (>48h Warnung)
crypto.jsAES-256-GCM Verschlüsselung für OAuth-Tokens (Node.js crypto, zero dependencies)
strava-tokens.jsToken-Store: CRUD für verschlüsselte Strava-Tokens in SQLite, DSGVO-Löschung
backup.shSQLite-Backup mit 14-Tage-Rotation, gzip, Integritätscheck. Cron aktiv: täglich 03:30
example-integration.jsZeigt wie alle Module in die bestehende Express-App eingebunden werden
🔌 API-Endpoint-Referenz

Vollständige Liste aller Endpoints der Pi-API (api.mag-biking.com). Auth-Spalte: = öffentlich, Rate = Rate-Limited, API-Key = X-API-Key Header, JWT = Bearer Token, JWT? = optional.

MethodPathAuthBeschreibung
GET/healthServer-Status + System-Metriken (CPU, RAM, Temperatur, Uptime)
GET/api/statsStrava Club Jahresstatistiken (Baseline + seen_rides)
GET/api/sysinfoAPI-KeyPi-Systeminfo (detailliert)
POST/api/contactRateKontaktformular (Validierung + Honeypot + SMTP-Versand)
GET/api/contact/messagesAPI-KeyAlle Kontakt-Nachrichten auflisten
DELETE/api/contact/messages/:idAPI-KeyEinzelne Nachricht löschen
DELETE/api/contact/cleanupAPI-KeyAlte Nachrichten aufräumen
POST/api/auth/registerRateBenutzer-Registrierung (E-Mail + Passwort + Name)
POST/api/auth/loginRateLogin (E-Mail + Passwort → JWT + Refresh-Token)
POST/api/auth/logoutLogout (Refresh-Token invalidieren)
POST/api/auth/magic-linkRateMagic Link per E-Mail senden
GET/api/auth/magic-link/verifyMagic Link prüfen & einloggen
POST/api/auth/refreshJWT Access-Token erneuern (Refresh-Token)
GET/api/auth/meJWTEigene Benutzerdaten abrufen
GET/api/auth/stravaStrava OAuth Redirect starten
GET/api/auth/strava/callbackStrava OAuth Callback verarbeiten
GET/api/membersJWTMitglieder-Liste (öffentliche Profile)
GET/api/members/profileJWTEigenes Profil abrufen
PUT/api/members/profileJWTProfil aktualisieren
POST/api/members/avatarJWTAvatar-Bild hochladen (Base64, max 3 MB)
GET/api/members/my-eventsJWTEigene Event-Anmeldungen auflisten
GET/api/events/:slug/registrationsJWT?Teilnehmerliste eines Events
POST/api/events/:slug/registerJWTFür Event anmelden
DELETE/api/events/:slug/registerJWTEvent-Anmeldung zurückziehen
🗃 Datenbank-Schema (SQLite)

Alle Tabellen werden beim Server-Start automatisch erstellt (CREATE TABLE IF NOT EXISTS). Datenbank-Datei: pi-api/strava-stats.db.

users
Benutzer-Accounts (Auth)
SpalteTypBeschreibung
idTEXT PKUUID
emailTEXT UNIQUEE-Mail-Adresse
password_hashTEXTbcrypt-Hash
display_nameTEXTAnzeigename
email_confirmedINTEGER0/1 – E-Mail bestätigt
strava_idTEXTVerknüpfte Strava-ID (nullable)
created_atINTEGERUnix-Timestamp
updated_atINTEGERUnix-Timestamp
profiles
Öffentliche Profildetails (1:1 zu users)
SpalteTypBeschreibung
user_idTEXT PK FKReferenz auf users.id (CASCADE)
display_nameTEXTAnzeigename
bikeTEXTFahrrad-Beschreibung
avatar_urlTEXTAvatar-Bild-URL
favorite_routeTEXTLieblingsstrecke
strava_urlTEXTStrava-Profil-Link
strava_idTEXTStrava-ID
bioTEXTKurzbiografie
is_publicINTEGER1 = öffentlich sichtbar
created_atINTEGERUnix-Timestamp
updated_atINTEGERUnix-Timestamp
event_registrations
Event-Anmeldungen (User × Event)
SpalteTypBeschreibung
idTEXT PKUUID
event_slugTEXTEvent-Slug (aus Content)
user_idTEXT FKReferenz auf users.id (CASCADE)
commentTEXTOptionaler Kommentar
created_atINTEGERUnix-Timestamp

UNIQUE-Constraint auf (event_slug, user_id) – verhindert doppelte Anmeldung.

refresh_tokens
Refresh-Tokens für JWT-Erneuerung (30 Tage gültig)
SpalteTypBeschreibung
tokenTEXT PKZufälliger 32-Byte Hex-String
user_idTEXTBenutzer-ID
expires_atINTEGERAblauf-Timestamp
magic_links
Einmal-Login-Links (15 Min gültig)
SpalteTypBeschreibung
tokenTEXT PKZufälliger 32-Byte Hex-String
emailTEXTEmpfänger E-Mail
expires_atINTEGERAblauf-Timestamp
oauth_states
CSRF-Schutz für Strava OAuth Flow (kurzlebig)
SpalteTypBeschreibung
stateTEXT PKZufälliger State-Parameter
created_atINTEGERErstellungs-Timestamp
oauth_codes
Temporäre OAuth-Codes für Strava Callback-zu-Frontend-Übergabe
SpalteTypBeschreibung
codeTEXT PKEinmal-Code
user_idTEXTBenutzer-ID
access_tokenTEXTStrava Access-Token
refresh_tokenTEXTStrava Refresh-Token
created_atINTEGERErstellungs-Timestamp
expires_atINTEGERAblauf-Timestamp
contact_messages
Kontaktformular-Nachrichten (SQLite-Backup bei SMTP-Ausfall)
SpalteTypBeschreibung
idINTEGER PK AIAuto-Increment ID
nameTEXTAbsender-Name
emailTEXTAbsender E-Mail
interestTEXTInteresse (events, training, etc.)
messageTEXTNachricht
ip_maskedTEXTIP-Adresse (letztes Oktett maskiert)
sentINTEGER0/1 – SMTP-Versand erfolgreich
created_atINTEGERUnix-Timestamp
baseline
Strava-Statistik Basiswerte pro Jahr
SpalteTypBeschreibung
yearINTEGER PKKalenderjahr
total_kmREALBasis-Kilometer
ride_countINTEGERBasis-Fahrtenanzahl
seen_rides
Einzelne erfasste Strava-Aktivitäten (inkrementell)
SpalteTypBeschreibung
hashTEXT PKAktivitäts-Hash (Deduplizierung)
yearINTEGERKalenderjahr (Index)
kmREALDistanz in km
seen_atINTEGERErfassungs-Timestamp
🚀 Roadmap – Geplante Erweiterungen

Geplante Features, Architektur-Entscheidungen und offene Fragen. Jedes Vorhaben hat einen Status:

Geplant In Arbeit Fertig Idee
📧 Kontaktformular – Self-Hosting auf Raspberry Pi Fertig
Abgeschlossen – Details anzeigen
Ziel

Das aktuelle Kontaktformular nutzt EmailJS (US-Drittanbieter). Ziel: Formular-Daten direkt an einen eigenen Raspberry Pi senden, der die E-Mail-Benachrichtigung übernimmt. Dadurch entfällt der Auftragsverarbeitungsvertrag mit EmailJS und die Daten bleiben auf eigener Infrastruktur.

Architektur
[Website-Formular] → HTTPS → [Cloudflare Edge] → Tunnel → [Raspberry Pi API] → SMTP → [E-Mail]
  • Kein offener Port am Heimnetz – Cloudflare Tunnel baut ausgehende Verbindung auf
  • Kein DynDNS nötig – Cloudflare routet über den Tunnel
  • HTTPS automatisch durch Cloudflare (Domain mag-biking.com ist bereits bei Cloudflare)
  • Subdomain: z.B. api.mag-biking.com → Pi via Tunnel
Tech-Stack auf dem Pi
RuntimeNode.js + Express (oder Python Flask)
E-Mail-Versandnodemailer (SMTP via Gmail App-Passwort / GMX / eigene Domain-Mail)
Tunnelcloudflared Daemon als systemd-Service
Spam-SchutzHoneypot-Feld + Rate-Limiting (express-rate-limit)
CORSNur mag-biking.com erlauben
DSGVO-Anforderungen
  • ✅ Einwilligungs-Checkbox am Formular (DSGVO-Pflicht) – umgesetzt 2026-04-21
  • ☐ Datenschutztext erweitern: erhobene Daten, Zweck, Speicherdauer, Rechte
  • ☐ Cloudflare DPA im Dashboard akzeptieren (Cloudflare als Durchleiter nennen)
  • ✅ Kein AVV mit E-Mail-Dienst nötig – eigener SMTP via Gmail
  • ✅ Formular-Daten nicht im Browser gespeichert – nur serverseitig in SQLite
Umsetzungs-Schritte
  1. cloudflared auf dem Pi installieren + Tunnel erstellen
  2. ✅ DNS-Record: api.mag-biking.com → Tunnel-ID
  3. ✅ Node.js API-Server: /api/contact Endpoint (POST) – pi-api/contact-route.js
  4. ☐ SMTP-Konfiguration: Gmail App-Passwort + Cloudflare Email Routing ([email protected])
  5. ✅ Rate-Limiting + Honeypot + CORS eingerichtet (security.js + Formular-Honeypot)
  6. ✅ Kontaktformular umgebaut: js/contact.js nutzt fetch POST statt EmailJS
  7. ✅ DSGVO-Checkbox + Link zur Datenschutzerklärung im Formular
  8. ✅ EmailJS-Script + CSP-Eintrag entfernt
Gelöste Fragen
  • OS: Debian 13 (trixie) auf Raspberry Pi 5
  • Node.js: v22.22.0 + Express 5.2.1
  • E-Mail: Gmail SMTP + Cloudflare Email Routing ([email protected])
  • Andere Dienste: nginx 1.26.3 (Reverse Proxy), cloudflared Tunnel, Strava-Stats-API
  • DB-Speicherung: Ja, contact_messages Tabelle in SQLite (Backup bei SMTP-Ausfall)
Risiken & Bedenken
  • Verfügbarkeit: Pi offline = Formular geht ins Leere → Fallback-Meldung einbauen
  • Sicherheit: Rate-Limiting + Input-Validierung + CORS sind Pflicht
  • Cloudflare DSGVO: Daten laufen technisch über Cloudflare Edge → DPA akzeptieren + in Datenschutzerklärung nennen
🚴 Strava Club km – Automatische Jahresstatistik Fertig
Abgeschlossen – Details anzeigen
Ziel

Die Hero-Statistik "km gefahren" (Stat 3 auf der Startseite) wird automatisch aus den tatsächlich gefahrenen Kilometern des Strava-Clubs berechnet. Die Statistik-Unterseite (stats.html) zeigt Jahreswerte archiviert mit Balkendiagramm an.

Aktueller Stand

Umgesetzt am 10.04.2026. Express-API auf Raspberry Pi (api.mag-biking.com) liefert live Daten aus SQLite. Stündlicher Cron sammelt neue Aktivitäten via Strava API. Hero-Stat wird automatisch aktualisiert, stats.html zeigt Jahresvergleich. Initialer Seed: 7.729 km (Summe aller 8 Mitglieder).

Architektur
[Strava API] ← Cron stündlich ← [Raspberry Pi] → SQLite → [/api/stats + /health] → Cloudflare Tunnel → [Website]
  • Pi als Collector: Cron-Job pollt Strava Club-Activities stündlich
  • SQLite-Datenbank: Speichert nur aggregierte km pro Tag (keine personenbezogenen Daten)
  • API-Endpoints: GET /api/stats (Jahres-km), GET /health (System-Metriken: CPU, RAM, Temp, Uptime)
  • Cloudflare Tunnel: api.mag-biking.comlocalhost:3000
  • Website: club-stats.js holt live km + Fahrten → Hero-Stat 2 + 3 (je nach Admin-Toggle)
  • Statistik-Seite: stats.html zeigt Jahresvergleich mit Balkendiagramm
  • Admin-Pi-Tab: Speicherübersicht → Pi Server zeigt live CPU/RAM/Temperatur/Uptime
Strava API – Details & Einschränkungen
EndpointGET /api/v3/clubs/{id}/activities
AuthOAuth2 – Access Token + Refresh Token (automatische Erneuerung)
VoraussetzungAuthentifizierter Nutzer muss Mitglied des Clubs sein
Paginationpage + per_page (max. 200 pro Seite, Default 30)
Response-Felderdistance (Meter), firstname, type (Ride/Run/etc.)
Rate Limit100 Requests / 15 Min, 1.000 / Tag
PrivacyEnhanced Privacy Mode wird respektiert – diese Activities fehlen im Response
EinschränkungNur neueste Activities – kein historischer Abruf. Darum: regelmäßig pollen + lokal kumulieren
Datenmodell (SQLite auf dem Pi)
-- Nur aggregierte Daten, keine Namen/User-IDs
CREATE TABLE daily_km (
  date       TEXT PRIMARY KEY,   -- '2026-04-09'
  total_km   REAL DEFAULT 0,     -- Summe aller Club-Rides an dem Tag
  ride_count INTEGER DEFAULT 0   -- Anzahl Fahrten
);

-- Jahres-Cache für schnelle Abfrage
CREATE TABLE yearly_stats (
  year       INTEGER PRIMARY KEY,
  total_km   REAL DEFAULT 0,
  ride_count INTEGER DEFAULT 0
);

Wichtig: Es werden keine Personennamen, Strava-IDs oder Einzelfahrten gespeichert – nur die Tages-Summe der Kilometer. Das ist der Schlüssel zur DSGVO-Konformität.

Tech-Stack auf dem Pi
Strava AppRegistrieren unter strava.com/settings/api → Client ID + Secret
OAuth2-TokenEinmalig manuell autorisieren → Refresh Token wird lokal gespeichert + automatisch erneuert
Collector-ScriptNode.js Cron (stündlich): Strava API → Activities filtern (type: Ride + VirtualRide) → distance summieren → in SQLite schreiben
API-ServerExpress: GET /api/stats (Jahres-km), GET /health (System-Metriken)
DatenbankSQLite (eine Datei, kein separater DB-Server nötig)
TunnelGleicher Cloudflare Tunnel wie Kontaktformular
API-Responses
GET https://api.mag-biking.com/api/stats

{
  "current_year": 2026,
  "current_km": 7729,
  "current_rides": 151,
  "years": [
    { "year": 2026, "km": 7729, "rides": 151 }
  ],
  "last_updated": "2026-04-10T10:00:04.029Z"
}
GET https://api.mag-biking.com/health

{
  "status": "ok",
  "ts": 1712764800,
  "cpu_pct": 13,
  "ram_used_mb": 843,
  "ram_total_mb": 8063,
  "temp_c": "51.3",
  "uptime": "15d 2h 23m"
}
Website-Integration
  • Hero-Stat 2 + 3: Admin-Toggle (Checkbox) schaltet zwischen API und manuellem Wert. club-stats.js prüft data-source="api" + data-api-stat="km|rides"
  • Fallback: Wenn API nicht erreichbar → manueller Admin-Wert bleibt stehen
  • Statistik-Seite (stats.html): Jahresvergleich-Balkendiagramm, Hero-Cards (km, Fahrten, Gesamt). Merge von API-Jahren + manuellen historischen Jahren aus Admin
  • Footer: "Club Statistik" Link (ein-/ausblendbar im Admin, Linktext konfigurierbar)
  • Admin-Einstellungen: Footer → Club Statistik: Titel, Untertitel, Club-URL, API-URL, historische Jahreswerte
  • Pi-Metriken: Speicherübersicht → Tab "Pi Server" zeigt live CPU/RAM/Temp via /health
  • Impressum: DSGVO-Datenschutzhinweis für Strava-Daten (Art. 6 Abs. 1 lit. f)
  • "Powered by Strava": Attribution auf stats.html (API Agreement)
DSGVO-Bewertung
✅ Keine PersonendatenNur aggregierte km pro Tag gespeichert – kein Name, keine Strava-ID, keine Einzelfahrt
✅ Lokale SpeicherungSQLite auf eigenem Pi – kein Cloud-Dienst als Datenspeicher
✅ Kein AVV nötigDa keine personenbezogenen Daten verarbeitet werden
⚠ Strava API AgreementNutzung nur im Rahmen des Strava API Agreement – Logo-Attribution ("Powered by Strava") erforderlich
✅ DatenschutztextIn impressum.html eingebaut: DSGVO Art. 6 Abs. 1 lit. f, keine personenbezogenen Daten, Link zur Strava Privacy Policy
✅ Strava Attribution"Powered by Strava" auf stats.html eingebaut
⚠ Cloudflare TunnelDaten durchlaufen Cloudflare Edge → DPA im Dashboard akzeptieren
Umsetzungs-Schritte (alle erledigt ✅)
  1. ✅ Strava API App registriert (Client ID: 223038)
  2. ✅ OAuth2-Flow durchlaufen → Refresh Token in .env auf Pi
  3. ✅ SQLite-Datenbank + Tabellen angelegt
  4. ✅ Collector-Script: Cron stündlich → Club Activities (Ride + VirtualRide) → daily_km
  5. ✅ API-Server: GET /api/stats + GET /health (System-Metriken)
  6. ✅ Systemd-Service (mag-biking-api.service) für Auto-Start
  7. ✅ Cloudflare Tunnel: api.mag-biking.comlocalhost:3000
  8. club-stats.js: API-Fetch mit Admin-Toggle (Stat 2: Fahrten, Stat 3: km)
  9. stats.html: Jahresvergleich + Merge historischer Jahre aus Admin
  10. ✅ Admin: Checkbox API/Manuell, Club-Statistik-Einstellungen, historische Jahreswerte
  11. ✅ Admin: Pi-Metriken Tab (Speicherübersicht → Pi Server)
  12. ✅ Footer-Link + "Powered by Strava" Attribution
  13. ✅ DSGVO-Datenschutzhinweis in impressum.html
  14. ✅ Initialer Seed: 7.729 km (Summe 8 Mitglieder) als Baseline in daily_km
Konfiguration
Strava ClubID 2031165 · strava.com/clubs/2031165
AktivitätstypenNur Ride + VirtualRide
Cron-IntervallStündlich (0 * * * *)
Pi-Dateien~/mag-biking-api/server.js, collector.js, .env, strava-stats.db
Systemdmag-biking-api.service (User: lucas, Restart: always)
Statistik-SeiteÖffentlich im Footer (ein-/ausblendbar im Admin)
Bekannte Einschränkungen
  • API-Limit: 1.000 Requests/Tag – bei 24 Abrufen/Tag (stündlich, ~5 Seiten) kein Problem
  • Kein historischer Abruf: Strava liefert nur neueste Activities → Jahre vor API-Start manuell im Admin eintragen
  • Initialer Seed: 2026 wurde mit 4.012 km Differenz als Baseline am 01.01.2026 in daily_km eingefügt (echte Sammlung ab 10.04.2026)
  • Token-Ablauf: Refresh Token wird automatisch erneuert. Bei Widerruf durch Strava: collector.log prüfen
  • Enhanced Privacy: Mitglieder mit Privacy-Modus fehlen → km ist eine Untergrenze
  • Pi offline: Fehlende Stunden werden nicht nachgeholt – Strava bietet keinen Zeitraum-Filter
Synergie mit Kontaktformular-Pi

Dieses Feature nutzt die gleiche Infrastruktur wie das Kontaktformular Self-Hosting: gleicher Pi, gleicher Express-Server, gleicher Cloudflare Tunnel. Der Aufwand für das zweite Feature ist daher deutlich geringer, wenn das erste bereits steht.

💬 Testimonials – Mitglieder-Zitate & Social Proof Idee
Warum?

Erfolgreiche Communities (Rapha RCC, etc.) nutzen echte Zitate von Mitgliedern als Social Proof. Besucher vertrauen Erfahrungsberichten von echten Menschen mehr als Marketing-Texten. Aktuell fehlt dieser Vertrauensfaktor auf der mag.biking Startseite komplett.

Umsetzung

1. Neue Section auf index.html zwischen Galerie und Kontakt
2. 2–4 Zitat-Karten mit Foto, Name, Rolle (z.B. „Mitglied seit 2025") und Zitat
3. Admin-Editor: Testimonials verwalten (Bild, Name, Text, sichtbar/unsichtbar)
4. Schema.org: Review Markup für jedes Testimonial

Aufwand

Klein – HTML-Section, CSS-Karten, Admin-Editor nach bestehendem Pattern (wie Sponsors). Kein Backend nötig.

📨 Newsletter – E-Mail-Capture & Ride-Updates In Arbeit · Phase 1 fertig
Warum?

Fast jede erfolgreiche Community hat einen Newsletter. E-Mail ist der direkteste Kanal zu den Mitgliedern – unabhängig von Social-Media-Algorithmen. Aktuell gibt es keinen Weg, Besucher regelmäßig zu erreichen außer über Instagram und Strava.

Umsetzung

1. Einfaches E-Mail-Eingabefeld im Footer oder eigene Section
2. Optionen: Eigenhosting auf Raspberry Pi (DSGVO, Synergien mit Kontaktformular) oder Drittanbieter (Buttondown, Mailchimp Free)
3. Double-Opt-In Pflicht (DSGVO)
4. Inhalte: Wöchentlicher Ride-Reminder, Event-Ankündigungen, Blog-Highlights

DSGVO-Hinweis

Einwilligung nötig, Abmeldelink in jeder Mail, Verarbeitungsverzeichnis führen. Bei Self-Hosting auf Pi: Daten bleiben in eigener Hand.

Phase 1 – Admin-Composer (verfügbar)

Im Admin gibt es unter 📨 Newsletter bereits einen Composer zur Zusammenstellung. Ablauf:

1. Seit dem letzten Versand neu hinzugekommene Events, Blog-Posts und Rideouts werden automatisch vorgeschlagen und können per Checkbox ausgewählt werden.
2. Featured Events lassen sich als Opt-In erneut einbinden, selbst wenn sie nicht neu sind.
3. Betreff, Intro-Text, CTA (Label + URL) und Outro-Text werden im Block „Zusammenstellung & Versand“ eingegeben.
4. Über Vorschau öffnet sich ein Modal mit der gerenderten HTML-Mail in einem Iframe. Mit HTML kopieren landet der komplette Quelltext in der Zwischenablage und kann in den Mail-Client eingefügt werden.
5. Als gesendet markieren schreibt Zeitstempel + Betreff + Item-Anzahl in den Verlauf (letzte 10 werden angezeigt) und setzt die Auswahl zurück – Betreff / Intro / Outro bleiben für schnelle Wiederverwendung erhalten.

Noch nicht enthalten: automatischer Versand, Abonnenten-Opt-In auf der öffentlichen Website, Double-Opt-In, Unsubscribe-Flow. Phase 1 ist bewusst export-only.

🎬 Hero-Upgrade – Video / Slider / Event-Countdown Idee
Warum?

Premium-Communities (Rapha, Canyon) nutzen cinematic Video-Backgrounds oder dynamische Bild-Slider im Hero. Der erste Eindruck entscheidet – aktuell ist der Hero statisch mit einem einzelnen Bild. Ein Countdown zum nächsten Event erzeugt Dringlichkeit.

Optionen

A) Video-Background: Kurzes Loop-Video (10–15s) von Gruppenfahrten, Autoplay muted, Fallback auf Bild für Mobile/langsame Verbindung.
B) Bild-Slider: 3–5 Bilder mit Crossfade, verschiedene Event-Eindrücke.
C) Event-Countdown: Nächstes Event mit Titel, Datum und Countdown-Timer prominent im Hero.
Empfehlung: C zuerst (einfach, dynamisch aus Admin-Daten), dann B oder A.

Aufwand

Countdown: Klein – JS liest nächstes Event-Datum aus content-data.js, Anzeige im Hero.
Slider: Mittel – Bilder im Admin verwalten, CSS-Transition, Touch-Support.
Video: Mittel – Video-Hosting (GitHub LFS oder extern), Performance-Optimierung.

📝 Blog & Stories ausbauen – Ride-Reports & Mitglieder-Spotlights In Arbeit
Warum?

Der Blog existiert, aber die Startseite zeigt nur einen Beitrag als Vorschau. Erfolgreiche Communities leben von regelmäßigen Ride-Reports („Wir waren dort") und Mitglieder-Spotlights („Wer fährt bei uns mit?"). Das erzeugt Zugehörigkeit und motiviert Neue mitzumachen.

Status

Umgesetzt: Blog-Detail-Seite mit Hero-Bild, Bild-Text-Abschnitten, GPX-Höhenprofilen pro Sektion, BlogPosting-Schema, Likes-System (js/likes.js + Pi-API). Reihenfolge im Admin bestimmt öffentliche Reihenfolge.

Noch offen: Blog-Preview auf Startseite erweitern (aktuell nur 1 Karte → 2–3), Kategorie-Filter auf blog.html, expliziter Kartentyp „Ride-Report“ / „Mitglieder-Spotlight“ als Admin-Dropdown. Blog-Detail-Karten-Funktion (mapShow-Flag pro GPX) ist skizziert aber noch nicht implementiert.

🗺 Interaktive Karte – Rideout-Startpunkte & Event-Locations Fertig
Warum?

Eine Karte zeigt sofort wo die Community aktiv ist – Startpunkte der Rideouts, Event-Locations, Streckenverläufe. Besonders für Neue hilft das bei der Orientierung. Komoot und Strava zeigen Routen, aber keine eigene Community-Übersicht.

Umgesetzt (April 2026)
  • Leaflet.js 1.9.4 vendored unter vendor/leaflet/, CartoDB Voyager Tiles (passt zum Light-Theme)
  • map.html – eigene Subseite, eingebunden per CTA hinter der Galerie auf index.html
  • Rideout-Treffpunkt-Karte auf app.html (PWA): Marker für Wutöschingen + Jestetten mit Hover-Popup „Nächster Ride“
  • Event-Marker + GPX-Tracks auf map.html – Start-Koordinaten werden via MagBiking.Map.hydrateGpx() aus den GPX-Dateien extrahiert, Track-Polyline optional
  • CSP erweitert auf https://*.basemaps.cartocdn.com in allen betroffenen Seiten
  • Zentrales Modul: js/mini-map.js (MagBiking.Map.renderFull(), hydrateGpx()) – von map.html, app.html, events.html, event-detail.html, blog-detail.html wiederverwendet
Noch offen

Komoot/Strava-Embed für einzelne Routen (optional).
✅ Treffpunkt-Koordinaten (2026-04-15): Admin-UI existiert unter „Karte → Rideout-Treffpunkte“ (map.rideoutLocations). app.js liest Standorte jetzt dynamisch aus content-data.js statt hardcodiert.

👤 Mitglieder-Bereich – Event-Anmeldung & Profil In Arbeit
Umsetzung: Self-Hosted Pi-API (JWT + SQLite)

Entscheidung: Self-hosted Lösung gewählt (Pi-API mit JWT Auth + SQLite statt Supabase Cloud).

Features:
• Login: E-Mail + Passwort, Magic Link, Strava OAuth
• Profil: Name, Bike, Foto, Lieblingsstrecke, Strava-Link, Bio
• Mitglieder-Übersicht: Steckbrief-Karten aller öffentlichen Profile
• Event-Anmeldung: Direkt auf Event-Detail-Seite mit Teilnehmerliste
• Nav-Integration: Login-Button / Avatar-Dropdown auf allen Seiten

Neue Dateien

login.html – Login/Registrier-Seite
profil.html – Eigenes Profil bearbeiten
mitglieder.html – Community-Übersicht (nur eingeloggt)
js/supabase-client.js – API-Client (Pi-API REST + JWT Token-Handling)
js/supabase-auth.js – Auth-State, Login/Logout, Nav-UI
js/login-page.js – Login-Seite UI-Logik
js/profile.js – Profil laden/speichern, Avatar-Upload
js/members.js – Mitglieder-Grid
js/event-registration.js – Anmeldung + Teilnehmerliste
css/auth.css – Alle Auth/Profil/Member-Styles
pi-api/auth-route.js – JWT Auth (Register, Login, Magic Link, Strava OAuth)
pi-api/members-route.js – Profil-CRUD, Mitglieder-Liste, Avatar-Upload, Event-Registrierung

Setup-Schritte (Pi-API)

1. Auf dem Pi: npm install bcryptjs jsonwebtoken im pi-api/-Ordner
2. In pi-api/.env folgende Variablen setzen:
JWT_SECRET=<zufälliger-string-32-zeichen>
SITE_URL=https://mag-biking.com
API_URL=https://api.mag-biking.com
3. Optional: Strava Developer App erstellen, Callback: https://api.mag-biking.com/api/auth/strava/callback
STRAVA_CLIENT_ID=<id>
STRAVA_CLIENT_SECRET=<secret>
4. Pi-API neustarten: Tabellen werden automatisch erstellt
5. SMTP-Variablen (bereits für Kontaktformular vorhanden) werden für Magic Links und Bestätigungs-Mails mitgenutzt

Ist-Stand (2026-04-21)

Komplett self-hosted: Alle Seiten, JS-Module, CSS, Datenschutz-Texte und Backend-API (Pi-API) sind implementiert.
Backend: JWT-Auth mit bcryptjs/jsonwebtoken, SQLite-Datenbank, Strava OAuth, Magic Link via SMTP.
Kein Cloud-Dienst: Supabase wurde durch lokale Pi-API ersetzt. Daten bleiben auf dem Raspberry Pi.

🏆 Ranking & Belohnungen – Km/Hm-Tracking, Stempelbuch, Sticker Idee
Warum?

Gamification ist ein wirksamer Motivator – Strava macht es vor. Ein eigenes Ranking im Mitglieder-Bereich erzeugt Bindung zur mag.biking-Plattform und macht den Unterschied zwischen „einsam fahren“ und „Teil der Community sein“ sichtbar. Stempelbuch/Sticker-Mechanik aus der analogen Welt (Wandernadel, Brevet-Karten) überträgt sich 1:1 auf digitale Badges.

Kernidee – zwei Wertungen nebeneinander
KategorieWas zählt?
Solo-KilometerAlle privat gefahrenen Aktivitäten. Datenquelle: Strava OAuth (ideal) oder manuelle Eingabe. Zählt für persönliche Jahres-Bilanz, Höhenmeter-Meilensteine.
Community-KilometerNur Fahrten in Verbindung mit einem Rideout oder Event („ich war dort“). Teilnahme wird im Admin bestätigt ODER automatisch über Strava-Segment/GPX-Match erkannt. Eigene Wertung, eigenes Stempelbuch.

Die Trennung ist wichtig: Wer nur solo fährt, sieht seine Zahlen – wird aber im Community-Ranking nicht belohnt. Das schafft Anreiz, bei Rideouts dabei zu sein.

Punkte-Schlüssel – Konzept

Damit Km und Hm in einer gemeinsamen Wertung vergleichbar werden, braucht es einen festen Umrechnungs-Schlüssel. Zusätzlich sollen offiziell (=im Community-Kontext) gefahrene Kilometer mehr wert sein als solo gefahrene. Vorschlag in drei Bausteinen:

1. Grundformel – Basispunkte

Basispunkte = km + (hm ÷ 10)

Warum hm ÷ 10? 100 Höhenmeter entsprechen im Schnitt dem Aufwand von ca. 10 zusätzlichen Flachland-Kilometern (etablierter Faktor aus Brevet- und Radmarathon-Szene: 1 hm ≈ 0,1 km Zuschlag, also 10 hm = 1 km). Das passt zu realer Fahrleistung: Ein 80 km-Ride mit 1500 hm ergäbe 80 + 150 = 230 Basispunkte – die Kletterei dominiert, wie es sich auch anfühlt.

2. Kategorie-Multiplikator – offiziell zählt mehr

Die Basispunkte werden mit einem Kategorie-Faktor multipliziert. Solo als 1,0× ist der neutrale Referenzwert, alles mit mag.biking-Bezug bekommt einen Aufschlag:

Kategorie Faktor Kriterium
Solo 1,0× Private Ausfahrt ohne Bezug zu einem Club-Termin. Zählt nur für Solo-Wertung.
Rideout 1,5× Teilnahme an einem offiziellen wöchentlichen Rideout (Treffpunkt & Zeit bestätigt).
Event (Tagesveranstaltung) 2,0× Radmarathon, gemeinsamer Tagesausflug, Schwarzwald-Cross etc. – einzelnes, aber besonderes Datum.
Mehrtägiges Event 2,5× Trainingslager, Bikepacking-Tour, Alpencross, Germany-Austria III – höhere Hürde, seltener, wertvoller.
Club-Challenge 3,0× Optional: Saison-weite Challenges („Everesting“, „1000 km-Monat“) als seltene Highlight-Wertung.

Kategorie-Erkennung: Phase 1 manuell per Admin-Bestätigung (Teilnehmerliste zu einem Event/Rideout). Phase 4 automatisch via GPX-Start-Match (Treffpunkt-Koordinaten ±500 m, Startzeit ±60 min).

3. Gesamt-Formel

Punkte = (km + hm ÷ 10) × Kategorie-Faktor

4. Beispielrechnungen

Fahrt Km Hm Kat. Rechnung Punkte
Solo Feierabendrunde, flach 40 200 1,0× (40 + 20) × 1,0 60
Rideout Sonntag, hügelig 80 1000 1,5× (80 + 100) × 1,5 270
Schwarzwald-Cross (Event) 150 2500 2,0× (150 + 250) × 2,0 800
Alpencross Tag 1 (mehrtägig) 95 2200 2,5× (95 + 220) × 2,5 787,5
Solo Gravel, anspruchsvoll 65 900 1,0× (65 + 90) × 1,0 155

Beobachtung: Ein Rideout „schlägt“ die vierfache Distanz solo, ein Event die zehnfache. Das Punktesystem belohnt die Teilnahme spürbar, ohne Solo-Fahrten zu entwerten.

5. Tuning-Parameter (konfigurierbar im Admin)

Alle Stellschrauben sollten in einer ranking_config-Tabelle in der Pi-API SQLite-DB liegen, damit sie ohne Code-Deploy angepasst werden können:

  • hm_per_km (Default 10) – „Wie viele Hm entsprechen 1 km?“ Niedriger = Berge zählen mehr, höher = flach-freundlicher.
  • factor_solo (Default 1,0), factor_rideout (1,5), factor_event (2,0), factor_multiday (2,5), factor_challenge (3,0)
  • min_distance_km (z.B. 5) – Fahrten unter Schwelle werden ignoriert (keine Strava-„Commute“-Spam)
  • max_points_per_day (optional, z.B. 1500) – Cap gegen Ausreißer-Langstrecken
  • season_start / season_end – Jahres-Ranking-Zeitraum (z.B. 01.03. – 31.10. für Roadbike-Saison)

6. Validierung & Fairness

  • Doppelt-zähl-Schutz: Eine Fahrt zählt entweder als Solo oder Community, nie beides. Community-Markierung überschreibt Solo.
  • Nachweis: Für Community-Faktor muss entweder Admin-Bestätigung oder GPX-Match vorliegen – sonst Fallback auf Solo (1,0×).
  • Pro-Rata bei Ausstieg: Wer ein Rideout früh abbricht, bekommt nur den tatsächlich gefahrenen Anteil mit Community-Faktor. Rest solo.
  • Einheitliche Datenquelle: Alle km/hm kommen aus derselben Quelle (Strava-Aktivität). Keine manuelle Veränderung nach Import.
Datenquellen – Optionen

A) Strava OAuth (empfohlen): Nutzer verbindet einmal sein Strava-Konto, api.mag-biking.com holt Aktivitäten per Webhook / Pull. Vorteil: Keine manuelle Pflege, Distanz/Hm/Zeit automatisch. Nachteil: Strava-Account Pflicht, API-Rate-Limits, Token-Refresh-Logik.
B) Manuelle Eingabe: Nutzer trägt nach jeder Fahrt Distanz/Hm selbst ein. Vorteil: Einfach, keine externen Abhängigkeiten. Nachteil: UX-Hürde, Vertrauensproblem.
C) Hybrid: Strava-OAuth als Default, manuelle Eingabe als Fallback für Nutzer ohne Strava.
Community-Attribution: Admin markiert beim Rideout/Event eine Teilnehmerliste, oder das System matcht Strava-Aktivität (Start innerhalb 500 m + ±1 h um Rideout-Zeit) automatisch.

Stempelbuch & Sticker – Belohnungs-Mechanik

Belohnungen sind nicht-kompetitiv (jeder kann sie erreichen), im Gegensatz zum Ranking (Top 10). Ideen:

  • Meilenstein-Sticker: 100 / 500 / 1000 / 2500 / 5000 km pro Jahr (Solo & Community getrennt)
  • Höhenmeter-Badges: 1000 / 5000 / 10 000 hm, „Alpencross-Äquivalent“ ab 15 000 hm
  • Rideout-Stempel: Ein Stempel pro besuchtem Rideout. Bei 10/25/50 Rideouts: eigener Badge („Regular“, „Veteran“, „Legend“)
  • Event-Stempel: Pro mehrtägigem Event (Bikepacking, Trainingslager) ein eigener Stempel mit Event-Logo – seltener, wertvoller
  • Saison-Challenges: „Winter-Warrior“ (3 Rideouts im Januar), „Gravel-Sommer“ (5 Gravel-Touren zwischen Juni–August), editierbar im Admin
  • Team-Badges: „Bergziege“ (hohes Hm/Km-Verhältnis), „Frühaufsteher“ (10 Starts vor 8:00 Uhr), „Kilometerfresser“ (>200 km an einem Tag)

Darstellung: Sticker-Sammelbogen im Profil (wie im realen Stempelheft), gesperrte Felder grau, freigeschaltete mit Datum & Titel.

Ranking-Ansichten
  • Jahres-Ranking: Top 10 Community-Km / Community-Hm des laufenden Jahres
  • Monats-Ranking: Wer war im aktuellen Monat am aktivsten?
  • Rideout-Frequenz: Wer war bei den meisten Rideouts dabei? (nicht Km-basiert)
  • Team-Leistung: Aggregierte Community-Km aller Mitglieder – kollektives Ziel („Gemeinsam um die Welt: 40 075 km“)
  • Eigene Historie: Persönlicher Verlauf mit Grafik, nicht-öffentlich

Opt-out: Wer nicht im öffentlichen Ranking erscheinen will, kann sich mit einem Flag im Profil ausblenden – Solo-Km und Stempelbuch bleiben privat sichtbar.

Architektur-Skizze
[Strava OAuth] → api.mag-biking.com/strava/callback
    ↓ Token speichern (Pi-API SQLite: strava_tokens)
[Webhook /strava/webhook] ← neue Aktivität
    ↓ Insert in activities (user_id, distance, elev, date, source)
    ↓ Match gegen rideout_dates → ggf. community_flag=true
    ↓ Badge-Trigger: Aggregate je user_id prüfen, neue Sticker vergeben
[Frontend Userbereich] → Pi-API REST Read → Ranking + Stempelbuch anzeigen

Neue Pi-API SQLite-Tabellen: profiles (aus Mitglieder-Bereich, bereits vorhanden), activities (eine Zeile pro Fahrt), rideout_attendance (User × Rideout), badges (Katalog aller Sticker + Regeln), user_badges (freigeschaltete Sticker pro User). Zugriffskontrolle über JWT Middleware.

Abhängigkeiten & offene Fragen
  • Mitglieder-Bereich (#roadmap-members) muss zuerst stehen – ohne Login kein Ranking
  • Strava API Access: Existiert bereits für Club-Km (api.mag-biking.com/api/stats), müsste um OAuth-Flow erweitert werden
  • DSGVO: Aktivitätsdaten sind personenbezogen, sensibel (Standort!). Einwilligung explizit, Datenschutzerklärung erweitern, Exportfunktion + Löschung auf Anfrage
  • Offene Frage: Soll Community-Attribution automatisch (GPX-Match) oder manuell (Admin bestätigt Teilnehmerliste) laufen? Erste Phase wohl manuell – einfacher, fehlertoleranter
  • Offene Frage: Sollen Solo-Km öffentlich sichtbar sein oder nur im eigenen Profil? Empfehlung: Nur Community-Km öffentlich (passt zum Community-Gedanken)
  • Offene Frage: Physische Sticker (drucken, an aktivste Mitglieder verschicken) als Jahresabschluss-Aktion?
Aufwand & Phasen-Vorschlag

Phase 1 – Manuelles Tracking (klein): Nutzer tragen nach Login Km/Hm selbst ein, Community-Flag vom Admin bestätigt. Einfaches Ranking, noch ohne Sticker. Setzt nur Mitglieder-Bereich voraus.
Phase 2 – Stempelbuch (mittel): Badge-Katalog im Admin pflegen, Trigger-Regeln (Km-Summen, Anzahl Rideouts), Sticker-Ansicht im Profil.
Phase 3 – Strava OAuth (groß): OAuth-Flow auf api.mag-biking.com, Webhook für neue Aktivitäten, automatische Aggregation. Ersetzt manuelle Eingabe für Strava-Nutzer.
Phase 4 – Auto-Community-Match (optional): GPX-/Segment-Matching gegen Rideout-Startpunkte + Zeiten.

📍 Live-Tracking via Garmin & Co. – Interaktive Karte im Mitglieder-Bereich Idee
Warum?

Während längerer Touren (Bikepacking, Alpencross, Trainingslager) ist es für die Community spannend mitzuverfolgen, wo ein Mitglied gerade unterwegs ist. Garmin „LiveTrack“ und ähnliche Dienste bieten das technisch an – aber als öffentliche Links, die beliebig weitergegeben werden können. Ziel: Dieses Tracking innerhalb des Mitglieder-Bereichs verfügbar machen, als Opt-in pro Tour, mit feingranularer Freigabe (alle Mitglieder / bestimmte User / privater Link).

Kernidee
  • Opt-in pro Tour: Tracking wird nicht automatisch aktiviert, sondern pro Ausfahrt vom Mitglied freigegeben (Datenschutz).
  • Sichtbarkeits-Stufen: (a) nur ich selbst, (b) ausgewählte Mitglieder (z.B. „meine Tour-Crew“), (c) alle eingeloggten Mitglieder, (d) privater Share-Link für Außenstehende (signierter Token mit Ablaufdatum).
  • Interaktive Karte: Leaflet + CartoDB Voyager (konsistent mit map.html), Position-Marker, Tour-Track als Polyline, Auto-Refresh alle 30–60 s.
  • Leistungsdaten nebenbei: Geschwindigkeit, Höhe, Distanz, ggf. Herzfrequenz/Power – je nachdem was die jeweilige API liefert.
Datenquellen zu prüfen
Anbieter Schnittstelle Leistungsdaten (potenziell)
Garmin LiveTrack (öffentliche Session-URL, kein offizielles API-Programm für kleine Projekte). Garmin Connect IQ / Health API nur mit Partnervertrag. Alternative: Eigene Datenfeld-App auf dem Gerät, die per HTTP an unsere API postet. Position (GPS), Speed, Herzfrequenz, Power (Wattmesser), Trittfrequenz, Höhe, Temperatur, Batteriestand.
Strava Beacon (Premium-Feature, Sharing per Link nur an Kontakte). Keine offene API für Live-Position. Nur Position + elapsed time (kein Power/HR via Beacon-Link).
Wahoo Wahoo Live Track ähnlich zu Garmin LiveTrack, Session-URL. Position, Speed, HR, Power, Cadence.
Smartphone Eigene PWA-Funktion in app.html: Geolocation-API + Background-Sync, Position an Pi-API posten. Nur Position + Speed/Accuracy. Keine HR/Power ohne Geräte-Pairing. Akku-Verbrauch hoch.
Komoot Kein offizielles Live-Tracking-API (Stand 2026). Touren werden erst nach Abschluss geteilt.
Technische Architektur (Skizze)
  • Pi-API: Neue Tabelle live_sessions (id, user_id, started_at, ended_at, visibility, share_token, device_source). Tabelle live_points (session_id, ts, lat, lng, speed, altitude, hr, power, cadence). Endpoints: POST /api/live/session (start), POST /api/live/point (push), GET /api/live/session/:id (read, mit Auth-Prüfung gegen Sichtbarkeit).
  • Ingest-Adapter pro Quelle: Garmin LiveTrack-Link per Scrape/Proxy polling (falls offiziell erlaubt), Wahoo ähnlich, Smartphone direkt per JWT-auth.
  • Mitglieder-Bereich: neue Seite tracking.html mit Leaflet-Karte, Teilnehmer-Liste (aktuell live), Tour-Info-Box (km, ø Speed, HR/Power falls vorhanden).
  • Share-Link: /live/<token> – signierter JWT mit session_id + exp, keine Login-Pflicht. Nur Position + Speed sichtbar, HR/Power per Default aus.
Offene Fragen zu prüfen
  • Garmin LiveTrack-Scraping: Ist das Polling des öffentlichen Session-Links AGB-konform? Gibt es ein offenes API-Programm? Connect IQ Data-Field-App als legitimer Workaround prüfen.
  • Wahoo API: Developer-Portal checken, ob es ein Live-Tracking-Endpoint gibt.
  • Leistungsdaten: Welche Werte werden tatsächlich mitübertragen? Herzfrequenz/Power als sensible Gesundheitsdaten (Art. 9 DSGVO) – dürfen nur mit expliziter Einwilligung pro Session verarbeitet werden.
  • Akku-Impact: Continuous Position-Push vom Garmin-Gerät reduziert Laufzeit. Relevanz besonders bei Mehrtages-Touren.
  • Retention: Live-Punkte nach Tour-Ende (z.B. 48 h) löschen oder als Track archivieren? DSGVO-konform: Opt-in pro Vorgang.
  • Missbrauchs-Szenarien: Stalking-Risiko bei Share-Links – Token mit kurzer Gültigkeit, Revoke-Funktion im Profil, Rate-Limit beim Polling.
Aufwand & Phasen

Phase 1 (Recherche): API-Landschaft Garmin/Wahoo prüfen, rechtliche Fragen (Gesundheitsdaten), 1–2 Tage.
Phase 2 (MVP „Smartphone“): PWA-Tracking aus app.html heraus + Karte im Mitglieder-Bereich. Nur Position, keine HR/Power. 3–5 Tage.
Phase 3 (Garmin-Integration): Je nach Ergebnis Phase 1 – LiveTrack-Proxy oder Connect-IQ-App. 5–10 Tage.
Phase 4 (Freigabe-Logik): Sichtbarkeitsstufen, Share-Tokens, Revoke-UI im Profil. 2–3 Tage.

🔁 Blog Cross-Posting – Website + Instagram + Strava Idee
Warum?

Aktuell muss ein Beitrag (Ride-Report, Event-Ankündigung, Community-News) dreimal geschrieben werden: einmal im Admin für die Website, einmal als Post im Strava-Club, einmal als Instagram-Caption. Ziel: Ein einziger Schreibvorgang, automatisches Fan-out auf alle drei Kanäle.

TL;DR – Machbarkeit (Stand 2026-04)
  • Website – voll automatisiert möglich (bestehender GitHub-Sync + Pi-API).
  • Instagram – automatisierbar via Instagram Graph API (Meta for Developers). Voraussetzung: IG-Account als Business/Creator mit verknüpfter Facebook-Page + Meta-App.
  • Strava Club-Postsnicht automatisierbar. Der club announcements-Endpoint wurde deprecated, nur noch DELETE verfügbar. Lösung: „Assist-Modus“ – Caption + Link in die Zwischenablage, Deep-Link zum Strava-Club, manueller Post-Klick.
Empfohlener Flow

Admin-first Authoring im vorhandenen Blog-Editor – dort sind alle Felder schon da (Titel, Teaser, Beschreibung, Bilder, Sections). Ein neuer Button „Cross-publish“ triggert den Pi-API-Job:

admin.html Blog-Editor -> [Cross-publish ▾]
  [x] Website   [x] Instagram   [ ] Strava

Pi-API  POST /api/publish/blog
   +--> GitHub push content-data.js     ==> Website live
   +--> Graph API: create media container
        Graph API: publish              ==> Instagram-Feed
   +--> Strava-Assist: Caption + Hero-URL
        in Zwischenablage, Deep-Link    ==> manuell
Instagram – Voraussetzungen & Limits
  • Account: IG Business oder Creator, verknüpft mit einer Facebook-Page.
  • Meta-App: In Meta for Developers anlegen, Scope instagram_content_publish aktivieren. Entwicklungs-Modus reicht für eigenen Account – sonst App-Review nötig (2–4 Wochen).
  • Token: Long-lived Page Access Token (60 Tage). Muss per Cron alle ~50 Tage refresht werden.
  • Bild-Regeln: JPEG, öffentliche HTTPS-URL (Meta pullt das Bild), Aspect 4:5 … 1.91:1, Caption ≤ 2200 Zeichen, ≤ 30 Hashtags.
  • Rate-Limit: 100 API-Posts / 24 h – praxisfern als Engpass.
  • Webhook „neuer Post“: existiert nicht. IG-first-Autoring-Richtung scheidet damit aus.
Strava – warum nicht automatisch

Der offizielle Strava-API-Changelog dokumentiert die Deprecation: Club-Announcements können nur noch gelöscht, aber nicht mehr erstellt werden. createActivity legt eine persönliche Aktivität an, keinen Club-Post – passt inhaltlich nicht zu einem Blog-Eintrag. Zapier, Make, Buffer und Publer bieten ebenfalls keine Strava-Club-Output-Aktion.

Assist-Modus statt Automatisierung: Beim Klick auf „Cross-publish“ generiert die Admin-UI eine vorbereitete Caption (Titel + Teaser + Link zum Website-Post) und kopiert sie in die Zwischenablage. Parallel öffnet sich die Strava-Club-Seite in einem neuen Tab – ein Klick zum Einfügen, fertig.

Alternativen geprüft
  • Zapier / Make – IG-Publish ja, Strava-Output nein. Würde uns mit einem Drittanbieter-Abo koppeln.
  • Buffer / Publer / Metricool / Hootsuite – alle unterstützen IG, keiner Strava Clubs.
  • Mixpost / Postiz (self-hosted) – fähig auf ARM/Pi, lösen aber Strava auch nicht und sind für einen einzelnen Account Overkill. Eigene ~100-Zeilen-Pi-API-Integration ist schlanker.
Aufwand & Risiken
Schritt Aufwand
Meta-App + FB-Page-Verknüpfung + IG Business-Switch + Token0.5 d (+ ggf. 2–4 Wo. Review)
Pi-API-Endpoint /api/publish/instagram (Container + Publish)1–1.5 d
Token-Refresh-Cron + Secret-Storage0.5 d
Admin-UI „Cross-publish“-Button + Status-Toast1 d
Strava-Assist (Caption-Generator + Clipboard + Deep-Link)0.5 d
Bild-Pipeline: HTTPS-URL + Aspect-Validierung + JPEG-Reencode1 d
Tests + Error-Pfade + Logging0.5–1 d

Summe: ~4–6 Entwicklertage + Meta-Review-Wartezeit.

Risiken: Token-Rotation (abgelaufener Token bricht Publish still ab), Bild-Aspect-Ratio (Blog-Bilder haben heute keine Einschränkung), Meta-App-Review falls IG-Account nicht im Dev-Mode läuft, Strava-Erwartungsmanagement (Assist-only statt voll-automatisch).

Offene Fragen vor Umsetzung
  1. IG-Account-Status: Ist @mag.biking (oder der Community-Account) bereits Business/Creator mit verknüpfter FB-Page? Falls nein – Umstellung ist Voraussetzung.
  2. Strava-Akzeptanz: Ist Assist-Modus (Caption kopieren + manuell einfügen) OK, oder ist Full-Auto ein Muss-Kriterium? Letzteres streicht Strava aus dem Scope.
  3. Carousel vs. Single Image: Blog-Posts haben oft mehrere Bilder. IG-Carousel (bis 10 Bilder) möglich, aber aufwändiger – starten mit Single-Hero-Image?
  4. Caption-Quelle: Automatisch aus Teaser generieren, oder eigenes Feld instagramCaption im Blog-Editor (Ton/Hashtags für IG anders als für Website)?
  5. Fehler-Policy: Wenn IG-Publish fehlschlägt – trotzdem Website-Publish (Teil-Erfolg) oder atomar alles-oder-nichts?
  6. Stories-Publishing: Zusätzlich zu Feed-Posts auch IG-Stories? Gleiche API, separate Entscheidung.
👥 User-Management & rollenbasierter Zugriff In Arbeit · Frontend fertig
Status (Stand 2026-04-22)
  • Backend committed (3fe5ca3): 3-Rollen RBAC (admin/editor/member), Role-aware JWT mit token_version-Revocation, requireRole()-Middleware, Admin-Endpoints (list/patch/delete/reset-password/export/audit-log), Last-Admin-Schutz, DSGVO-Anonymisierung, TOTP 2FA (RFC 6238, native HMAC-SHA1 ohne npm-Deps), Audit-Log (append-only).
  • Admin-UI committed (dc43dba): Section „Benutzer“ in admin.html mit Suche, Rollen-Dropdown, Sperren/Entsperren, Passwort-Reset, DSGVO-Export (JSON-Download), Löschung mit „LÖSCHEN“-Bestätigung und Audit-Log-Ansicht.
  • 2FA-Frontend committed (64c0a10): Profil-Seite mit Setup/Enable/Disable-Flow, Secret-Anzeige + Kopier-Button, 2-Step-Login (login.html erkennt totp_required → TOTP-Feld).
  • Deployment noch ausstehend: git pull auf Pi, pm2 restart mag-api; Migration läuft automatisch beim Start. Danach ADMIN_EMAIL in pi-api/.env setzen und neu starten → erster Admin wird gebootstrapt.
API-Endpoints (Pi-API)

Alle Admin-Endpoints erfordern JWT mit role=admin. Alle 2FA-Endpoints (außer verify) erfordern ein gültiges JWT (eingeloggt).

Methode Pfad Zweck
GET/api/admin/usersListe aller User. Query: search, include_deleted.
GET/api/admin/users/:idEinzelner User inkl. Sessions und Event-Anmeldungen.
PATCH/api/admin/users/:idRolle oder Status ändern. Body: { role } oder { status }. Inkrementiert token_version → Sessions sofort ungültig.
POST/api/admin/users/:id/reset-passwordLöst Magic-Link-Mail aus und beendet alle Sessions.
GET/api/admin/users/:id/exportDSGVO Art. 15 Auskunft: JSON-Download aller User-Daten.
DELETE/api/admin/users/:idDSGVO Art. 17 Löschung. Event-Teilnahmen werden als „Mitglied“ anonymisiert.
GET/api/admin/audit-logLetzte 200 Admin-Aktionen (Query limit).
POST/api/auth/2fa/setupGeneriert neues TOTP-Secret (Base32) + otpauth-URI. Noch NICHT aktiviert.
POST/api/auth/2fa/enableAktiviert 2FA nach erfolgreicher Code-Verifikation. Body: { code }.
POST/api/auth/2fa/disableDeaktiviert 2FA. Body: { password_or_code }.
POST/api/auth/2fa/verifyZweiter Schritt des Logins. Body: { totp_token, code }. Liefert access_token + refresh_token.

Die Login-Antwort enthält { totp_required: true, totp_token } wenn für den User 2FA aktiv ist – dann erscheint im Frontend das Code-Eingabefeld.

Warum?

Aktuell gibt es nur einen einzelnen Admin-Login; Mitglieder registrieren sich über den Mitglieder-Bereich. Es fehlt eine Übersicht aller registrierten User im Admin, sowie die Möglichkeit, Personen gezielt Berechtigungen zu geben (z.B. Blog-Editor ohne vollen Admin-Zugriff, Rideout-Leiter der Events pflegen darf, Moderator für spätere Community-Funktionen).

RBAC – sinnvoll für eine Community dieser Größe?

Ja, aber schlank. Full-RBAC mit feingranularen Permissions pro Ressource wäre Overkill. Eine 3-Rollen-Struktur reicht in der Praxis und ist später erweiterbar:

Rolle Darf Typisches Profil
Admin Alles: Content, Settings, User-Management, Deploy, GitHub-Sync, Impressum. Betreiber (du). 1–2 Personen max.
Editor Blog, Events, Rideouts, Galerie bearbeiten. Keine Settings, kein User-Management, kein GitHub-Push. Ride-Leiter, Content-Schreiber, Foto-Sammler.
Member Nur Mitglieder-Bereich: eigenes Profil, Event-Anmeldung, Mitglieder-Übersicht. Reguläres Community-Mitglied.

Warum jetzt entscheiden: Ein einzelnes role-Feld in der DB einziehen kostet jetzt ~30 Minuten. Nachträglich Permissions über eine binäre is_admin-Flag aufzuspalten ist ein Refactoring durch alle Auth-Middleware, Admin-UI-Guards und Tests – deutlich teurer.

Feature-Scope
  • Admin-Section „Benutzer“: Tabelle mit E-Mail, Anzeigename, Rolle, Registriert-am, Letzter-Login, Strava-verknüpft (ja/nein), Status (aktiv/gesperrt).
  • Suche & Filter: Freitext auf Name/Mail, Filter nach Rolle, Filter nach Anmeldequelle (Passwort vs. Magic-Link vs. Strava-OAuth).
  • Aktionen pro User: Rolle ändern (Admin/Editor/Member), Sperren/Entsperren, Passwort-Reset auslösen, Löschen (DSGVO Art. 17).
  • DSGVO-Export: User-Daten als JSON-Download für Auskunftsersuchen (Art. 15).
  • Audit-Log: Wer hat wann welche Rolle änderung vorgenommen (simple Tabelle admin_audit).
  • Anzahl-KPI im Admin-Dashboard: Gesamt-User, aktive letzte 30 Tage, Verteilung nach Rolle.
Technische Umsetzung (Skizze)
  • DB-Migration: Spalte users.role TEXT NOT NULL DEFAULT 'member' CHECK IN ('admin','editor','member'). Migration: bestehende is_admin=1'admin', sonst 'member'. is_admin kann deprecated bleiben, später entfernen.
  • JWT-Payload: role-Claim aufnehmen – kein extra Roundtrip zur DB pro Request.
  • Backend-Middleware: requireRole('admin'), requireRole('admin','editor') als Express-Middleware in pi-api/security.js.
  • Frontend-Guards: window.MagBiking.Auth.hasRole(r)-Helper, damit Admin-Sidebar-Buttons je nach Rolle ausgeblendet werden.
  • Sperren-Mechanik: users.status ('active','suspended'). Login prüft Status, bestehende JWTs werden bei Status-Änderung nicht sofort ungültig – daher Token-Version-Spalte (token_version), JWT speichert Version, bei Mismatch Logout.
  • Admin-API-Endpoints: GET /api/admin/users, PATCH /api/admin/users/:id, DELETE /api/admin/users/:id, POST /api/admin/users/:id/reset-password.
Aufwand
Schritt Aufwand
DB-Migration + JWT-Payload + Backend-Middleware0.5 d
Admin-UI: User-Liste + Tabelle + Suche/Filter1 d
Admin-Aktionen: Rolle ändern, Sperren, Löschen, Passwort-Reset1 d
DSGVO-Export (JSON-Download je User)0.5 d
Audit-Log (Tabelle + Append bei jeder Aktion)0.5 d
Frontend-Role-Guards (Sidebar je Rolle)0.5 d
Tests & Doku im Admin-Docs0.5 d

Summe: ~4–5 Entwicklertage für den vollen Umfang. MVP (User-Liste lesen, Rolle ändern, Sperren, DSGVO-Löschen): ~2 Tage.

Sicherheit & Risiken
  • Privilege-Escalation: Ein Editor darf niemals die eigene Rolle auf Admin setzen können. Backend muss explizit prüfen: req.user.role==='admin' AND targetUserId !== req.user.id für Admin-Aktionen.
  • Letzter Admin schutz: Beim Löschen/Downgrade des letzten verbleibenden Admins Fehler werfen, sonst Lock-out.
  • JWT-Revocation: Ohne Token-Version-Feld bleibt ein gesperrter User bis zum JWT-Ablauf eingeloggt. Lösung: token_version-Spalte, bei jedem Sperren/Rollen-Downgrade inkrementieren, JWT-Validation prüft Match.
  • Audit-Log unveränderlich: admin_audit darf nur Appends erhalten, keine UPDATEs/DELETEs – wichtig für spätere Nachvollziehbarkeit.
  • Passwort-Reset-Endpoint: Muss Rate-Limit bekommen (wie Login-Endpoint), sonst Missbrauchs-Vektor.
Offene Fragen vor Umsetzung
  1. Editor-Rolle: gebraucht oder Overkill? Wenn du der einzige bist, der Content pflegt, reicht Admin+Member. Editor-Rolle lohnt sich, sobald 2+ Leute Rideouts/Events pflegen wollen.
  2. Editor-Scope feinjustieren: Sollen Editor auch Admin-Docs oder Impressum bearbeiten dürfen? Vorschlag: nein – nur User-facing Content (Blog, Events, Rideouts, Galerie, Loadout, Packlisten).
  3. Editor-Vergabe: Nur Admin kann eine Rolle heben, oder gibt es einen Einladungs-Flow (Admin generiert One-Time-Link)?
  4. Sperren vs. Löschen: Auf Bitte eines Users reicht Sperren? Oder immer harte Löschung für DSGVO-Konformität? Vorschlag: beides anbieten, Default = Sperren, Löschen mit Bestätigung.
  5. Datenaufbewahrung nach Löschung: Event-Anmeldungen historisch behalten (anonymisiert, z.B. „Mitglied“) oder ebenfalls löschen? Beeinflusst Event-Teilnehmerstatistiken.
  6. 2FA für Admin/Editor: Separates Thema, aber denkbar als optionaler Schritt – TOTP-basierte 2FA für alle Rollen ≠ Member.
📷 Bildsprache professionalisieren Idee
Warum?

Die aktuelle Bildsprache nutzt hauptsächlich Event-Grafiken und Landschaftsbilder. Erfolgreiche Communities zeigen echte Gruppen-Fotos von Rides, Action-Shots und Rider-Portraits. Das vermittelt Gemeinschaft und Emotion – „Hier fahren echte Menschen zusammen".

Empfehlungen

1. Bei jedem Rideout und Event: 5–10 Fotos machen (Start, Gruppe, Landschaft, Pause, Ziel)
2. Konsistenter Bildstil: gleicher Filter/Farbton, dunkle Töne passend zum Dark-Theme
3. Galerie regelmäßig mit echten Ride-Fotos befüllen
4. Hero-Bild: Gruppenfahrt statt Solo-Landschaft
5. Blog-Beiträge: Rider-Portraits als Titelbild statt generische Grafiken

Aufwand

Kein Code-Aufwand – rein Content. Braucht einen „Foto-Beauftragten" oder regelmäßiges Sammeln von Bildern aus der Community (z.B. Strava-Fotos, Instagram-Reposts mit Erlaubnis).

🔎 SEO / AEO / GEO – Suchmaschinen & KI-Optimierung Aktiv
Was bedeutet SEO / AEO / GEO?
SEOSearch Engine Optimization – Klassische Suchmaschinen-Optimierung (Google, Bing). Ziel: Höheres Ranking in Suchergebnissen.
AEOAnswer Engine Optimization – Optimierung für KI-basierte Antwortsysteme (ChatGPT, Perplexity, Google AI Overview). Ziel: mag.biking wird als Antwort zitiert.
GEOGenerative Engine Optimization – Optimierung für generative KI-Suchen. Ziel: Strukturierte Daten die KI-Modelle direkt verwerten können.
Umgesetzte Maßnahmen
MaßnahmeDateienTyp
FAQ-Section + FAQPage Schemaindex.html (Section + JSON-LD)AEO
SportsOrganization JSON-LDAlle Seiten (<head>)GEO
SportsEvent JSON-LD (dynamisch)js/event-detail.jsGEO
Meta-DescriptionsAlle 10 öffentlichen SeitenSEO
Open Graph Tags (og:*)Alle Seiten + dynamisch in JSSEO
Canonical LinksAlle Seiten (<link rel="canonical">)SEO
BreadcrumbList Schemaevents, blog, stats, event-detailSEO
robots.txt für KI-Crawlerrobots.txt (GPTBot, ClaudeBot, PerplexityBot etc.)AEO
Sitemapsitemap.xml (bei Google Search Console eingereicht)SEO
About-Text als zitierbare Definitionindex.html (About-Section)AEO
<noscript>-Fallbackindex.html (Kerninhalte für JS-lose Crawler)AEO
Dynamische OG-Imagesevent-detail.js, blog-detail.jsSEO
Schema.org Structured Data

Folgende JSON-LD Blöcke sind eingebettet:

  • SportsOrganization – Auf allen Seiten. Beschreibt mag.biking als Organisation (Name, Region, Sport, Logo)
  • FAQPage – Nur auf index.html. 6 Fragen mit Antworten, Microdata + JSON-LD doppelt
  • SportsEvent – Dynamisch auf event-detail.html. Wird per JS aus den Event-Daten generiert (Titel, Datum, Ort, Bild)
  • BreadcrumbList – Auf Unterseiten (events, blog, stats, event-detail). Zeigt Navigationspfad
Regionale Keywords

Alle SEO-Texte verwenden konsistent diese Formulierung:

„Radsport-Community zwischen Bodensee und Schwarzwald an der Grenze zur Schweiz"

Nicht verwenden: „Bad Waldsee", „Oberschwaben" als Standort-Fokus. Diese Begriffe nur für konkrete Event-Namen (z.B. „Bad Waldsee Radmarathon").

Fokus-Themen: Wöchentliche Rideouts, Rennrad- & Gravel-Events, Bikepacking-Touren, Trainingslager Road & Gravel.

SEO-Check im Admin

Im Admin-Editor gibt es den Button „SEO-Check" in der Topbar. Dieser prüft alle öffentlichen Seiten auf:

  • Meta-Description vorhanden und richtige Länge (50–160 Zeichen)
  • Open Graph Tags vollständig (og:title, og:description, og:image, og:url)
  • Canonical Link vorhanden
  • Schema.org JSON-LD vorhanden (SportsOrganization)
  • FAQ-Section auf index.html vorhanden
  • robots.txt erreichbar
  • sitemap.xml erreichbar
  • <noscript>-Fallback auf index.html

Ergebnis wird als Modal mit Checkliste angezeigt (grün = OK, rot = fehlt).

Externe Maßnahmen (nicht im Code)
  • Google Search Console – Sitemap eingereicht (10.04.2026), Property verifiziert
  • Google Business Profil – Empfohlen: kostenloses lokales Listing anlegen
  • Strava Club Beschreibung – Keywords optimieren (Bodensee, Schwarzwald, Gravel, Bikepacking)
  • Komoot Club/Profil – Routen veröffentlichen für zusätzliche Sichtbarkeit
  • Lokalpresse – Artikel über Events platzieren (Backlinks + Autorität)
🔍 Offene SEO-Potenziale (Audit 2026-04-13)

Vollständiger Audit durch Code-Review aller 11 öffentlichen HTML-Seiten. Fundament ist solide (OG, Canonicals, JSON-LD, robots.txt, Sitemap, Alt-Texte, einheitliche lang="de"). Folgende Punkte bieten den höchsten Hebel:

Finding Auswirkung & Fix
SportsEvent-Schema nur auf Detail-Seite
event-detail.html
events.html (Übersicht) hat aktuell kein ItemList/Event-Schema. Rich-Result-Chance auf Event-Listen-Darstellung geht verloren. Fix: Dynamisches JSON-LD in js/events.js mit @type: ItemList + itemListElement: [{@type:'SportsEvent', ...}].
Canonical auf Detail-Seiten statisch
event-detail.html, blog-detail.html
Das <link rel="canonical"> zeigt initial auf /event-detail.html ohne Query-String. JS aktualisiert es zwar zu ?event=slug, Crawler können aber beide Varianten sehen ⇒ Duplicate-Content-Risiko. Fix: Canonical bereits beim ersten Paint über ein <script> in <head> setzen oder die statische Canonical entfernen und nur dynamisch injizieren.
BlogPosting-Schema nur clientseitig
blog-detail.js
Googlebot rendert zwar JS, Crawl-Budget sinkt aber. Fix optional: Minimal-Schema schon statisch im <head> (Org + Website), vollständiges BlogPosting per JS anreichern.
Sitemap unvollständig
sitemap.xml
map.html fehlt aktuell in der Sitemap. Detail-Seiten (event-detail.html?event=..., blog-detail.html?post=...) können nicht pro Item indexiert werden, solange Query-String-Routing genutzt wird. Fix Phase 1: map.html ergänzen. Phase 2: Sitemap bei Admin-Deploy per Script aus content-data.js generieren.
Twitter Cards fehlen komplett Nur Open Graph gesetzt. Twitter/X fällt dadurch auf OG zurück, aber twitter:card = summary_large_image liefert konsistentere Previews. Fix: In den Head-Blöcken je Seite 4 Meta-Tags ergänzen (twitter:card/title/description/image).
Impressum ohne Meta/OG
impressum.html
Korrekt noindex, aber keine Meta-Description, kein Canonical, kein OG. Kleiner Fix: Minimale Description + Canonical ergänzen, OG optional.
JS-abhängiger First Paint
events.html, blog.html, app.html
Kerninhalte erscheinen erst nach content-data.js-Injection. Für Googlebot unproblematisch (renders JS), aber First-Contentful-Paint-Score leidet. Fix optional: Statisches Fallback-Listing der 3 nächsten Events im HTML-Quelltext.
Top-5 Priorität 1) SportsEvent-Schema auf events.html  ·  2) Canonical-Fix Detail-Seiten  ·  3) Sitemap map.html + auto-Generation  ·  4) Twitter Cards  ·  5) Statisches Fallback-Listing
📋 Changelog (Architektur)
2026-03-27 · Zentral-Katalog & Blog-GPX
• Zentraler Katalog (catalog.items[]) eingeführt
• loadout.selectedIds[] und packlists.lists[].items[] referenzieren Katalog-IDs
• Blog unterstützt GPX-Route als eigenen Abschnittstyp
• Rideout-App zeigt Höhenprofile (arc-gpx-canvas)
• Migration: Alte equipment.items / packlists.catalog werden automatisch migriert
2026-03 · Rideout-System & PWA
• app.html als PWA-Standalone-App
• manifest.json für PWA
• GPX-Parser (Haversine, Höhenprofil-Canvas)
• Rideout-Featured-Karte mit GPX-Integration auf index.html
Ursprüngliche Architektur
• JAMstack ohne Backend
• content.js als zentraler Content-Store
• content-inject.js für DOM-Manipulation
• Admin mit GitHub-Push und Pi-Server-Sync

Changelog der letzten Admin-Speicherungen: Im Versionsverlauf-Modal einsehbar.