Laufsport – Jogging mit IT verbinden

Laufsport – Jogging mit IT verbinden

Man kann sein Trainingskontingent auch mit der IT verbinden. Die meisten werden jetzt an ihre Laufuhren denken, die sie meist am linken Arm tragen.

Ich denke, da untypischer oder typisch IT’ler, nämlich an Datenbanken. Vor längerer Zeit natürlich auch mithilfe von KI habe ich mir ein Frontend als HTML-Seite gebastelt, welches ich mithilfe von Node.JS, und Express.JS verbinde und einer SQLite3 Datenbank.

Datenbankanbindung

Die SQLite3 Datenbank ist eine kleine serverlose Datenbank, die auf den meisten Linux-Distributionen schon vorinstalliert ist, aber in Windows und vielleicht auch im Mac noch nachinstalliert werden muss.

SQLite3 ist eine schnörkellose Datenbank, mit ihren eigenen Befehlen, befolgt aber dennoch die meisten SQL-Befehle. Sie speichert die Daten in eine Datei. Das heißt, man kann sie ohne Probleme von A nach B transportieren.

Die Ausgabe von meinen Läufen in den letzten Wochen

sqlite> SELECT * FrOM speicherung;
laufenid|Datum|Zeit|Strecke
9|2025-12-21|00:16|1.4
10|2025-12-22|00:17|1.5
11|2025-12-24|00:18|1.9
12|2025-12-28|00:13|1.27
13|2025-12-29|00:07|0.55
14|2025-12-30|00:15|1.4
15|2026-01-01|00:18|1.5
16|2026-01-08|00:09|0.9
18|2026-01-11|00:14|1.58
19|2026-01-12|00:14|1.45
20|2026-01-13|00:24|2.374
21|2026-01-14|00:25|2.2

Die Überschriften über den Spalten bekommt man auch nur, wenn man bevor SELECT * FROM speicherung, noch den internen Befehl .header on geschrieben hat.

Wie man sieht, bin ich erst in den letzten Tagen über 2 km gekommen. Die kalte Luft setzt mir doch ziemlich zu. Die geringen Läufe von 550 Metern bis 1,5 km waren auf das sehr kalte Winterwetter zurückzuführen (für mich jedenfalls). Normalerweise mache ich im Januar oder Februar an Freiluftsport gar nichts. Klar, dass die meisten Leute auch jetzt ganz normal joggen / laufen gehen und sich jetzt bei diesen Zeilen verwundert die Augen reiben.

Rad- und Fußweg zur Wallmannaue in Essen Stoppenberg. Der andere Weg kann in der Auftauphase auch noch so aussehen.
Weg mit 40 Meter Höhenunterschied

Aber es sei gesagt, es gibt auch Menschen, die gar keinen Sport machen. Ich fahre ja auch noch alles, weil ich kein Auto habe, mit dem Fahrrad. Essen-Stoppenberg ist nicht flach. Höhenunterschiede von bis zu 50 Metern sind hier normal, die ich fürs Laufen und für das Fahrradfahren ständig hier überwinden muss. Wenn in den Fahrradtaschen 3 Flaschen von 1,25 Litern oder 1,5 Litern drin sind von Einkäufen plus noch andere Sachen, die müssen nach oben zur Wohnung gebracht werden. Nach dem Parken des Fahrrads im eigenen Keller, muss ich zu Fuß, dann vier Etagen (Keller + 3 Etagen) ganz nach oben zur eigenen Wohnung. Also die Flaschen, die hinten gemütlich in den Fahrradtaschen drin lagen, müssen noch nach oben gebracht werden.

Anscheinend bin ich einer von den ganz wenigen, die regelmäßig hier solche Höhenunterschiede meistern muss fürs Laufen und Fahrradfahren (oder zu Fuß gehen).

Zurück zur IT

Hier mal der Code, der ich starte, damit das Frontend starten kann, mit dem Befehl node server.js.

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');

const app = express();
// Verbindung zur umbenannten Datenbankdatei
const db = new sqlite3.Database('./laufstrecke.db');

app.use(express.json());
app.use(express.static('public'));

app.post('/api/speichern', (req, res) => {
    // Daten aus dem Request-Body holen
    const { datum, zeit, strecke } = req.body;
    
    // Debug-Anzeige im Terminal
    console.log("Daten empfangen:", req.body);

    // SQL-Befehl mit den exakten Spaltennamen aus deinem .schema
    const sql = `INSERT INTO speicherung (Datum, Zeit, Strecke) VALUES (?, ?, ?)`;

    db.run(sql, [datum, zeit, strecke], function(err) {
        if (err) {
            console.error("DB Fehler:", err.message);
            return res.status(500).json({ error: err.message });
        }
        console.log(`Erfolgreich gespeichert! ID: ${this.lastID}`);
        res.json({ message: 'Erfolgreich gespeichert', id: this.lastID });
    });
});

// Route zum Abrufen aller gespeicherten Läufe
app.get('/api/laufe', (req, res) => {
    const sql = "SELECT * FROM speicherung ORDER BY Datum DESC";
    db.all(sql, [], (err, rows) => {
        if (err) {
            res.status(500).json({ error: err.message });
            return;
        }
        res.json(rows);
    });
});

// Route zum Löschen eines Laufs anhand der ID
app.delete('/api/delete/:id', (req, res) => {
    const id = req.params.id;
    console.log("Lösch-Anfrage für ID:", id); // Dies zeigt dir im Terminal, ob die ID ankommt
    const sql = "DELETE FROM speicherung WHERE laufenid = ?";

    db.run(sql, id, function(err) {
        if (err) {
            console.error("Fehler beim Löschen:", err.message);
            res.status(500).json({ error: err.message });
            return;
        }
        console.log("Erfolgreich gelöscht, Zeilen verändert:", this.changes);
        res.json({ message: "Gelöscht", changes: this.changes });
    });
});

app.listen(3000, () => {
    console.log('Server läuft auf http://localhost:3000');
});

Der HTML-Code, also das Frontend, wo ich die Daten einfüge:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Laufstrecke</title>
    <link rel="stylesheet" href="CSS/laufstrecke.css">
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>

<h1>Laufstrecke</h1>

<div id="status-meldung" class="status-meldung"></div>

<div id="statistik-bereich" class="charts">
        <h2 id="gesamt-km" class="gesamtkilometer">Gesamtdistanz: 0.0 km</h2>
        <canvas id="laufChart" width="400" height="200"></canvas>
    </div>

    <div id="tabellen-container" class="table-container">
        </div>

<form class="grid-formular">
    <label for="datum">Geben Sie das Datum ein:</label>
    <input type="date" id="datum" name="datum" autofocus>
 
    <label for="zeit1">Geben Sie die gelaufene Zeit ein:</label>
    <input type="time" id="zeit_input" name="zeit" placeholder="in Std:Min">

    <label for="strecke">Geben Sie die Strecke ein:</label>
   <input type="number" id="strecke_input" min="0.1" step="0.1" placeholder="in km"></form>


   <form class="grid-formular">
    <label></label> <div>
        <button type="button" onclick="speichernUndLeeren()">Ergebnis speichern & leeren</button>
        <button type="button" onclick="felderlöschen()" style="background-color: #ccc;">Abbrechen</button>
        <button type="button" onclick="toggleTabelle()" id="btn-historie">Historie anzeigen</button>
    </div>
</form>

<div id="historie"></div>

 <form class="grid-formular">
    <label>Die Laufgeschwindigkeit beträgt:</label>
    <output id="ergebnis_geschwindigkeit">0 km/h</output>

    <label>Die Pace beträgt:</label>
    <output id="ergebnis_pace">0:00 min/km</output>
</form>



<div id="tabellen-container" class="table-container">
<h3>Deine Lauf-Historie</h3>
<table id="historie-tabelle">
    <thead>
        <tr>
            <th>Datum</th>
            <th>Zeit</th>
            <th>Strecke (km)</th>
        </tr>
    </thead>
    <tbody id="tabellen-body">
        </tbody>
</table>
</div>

<script src="javascript/laufstrecke.js"></script>

</body>
</html>

Dazu gibt es noch eine CSS-Datei:

/* Grundlegendes Styling */

body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
    line-height: 1.6;
    background-color: #f4f4f9;
    padding: 20px;
}

h1 {
    text-align: center;
    margin-bottom: 30px;
    color: #333;
}

h3 {
    color: #444;
}

/* Container & Grid */
.grid-formular {
    display: grid;
    grid-template-columns: 1fr 2fr; 
    gap: 15px;
    max-width: 600px; 
    margin: 0 auto 20px auto;
    align-items: center;
}

/* Formular Elemente */
label {
    font-weight: bold;
}

input, output {
    width: 100%;
    box-sizing: border-box; 
    padding: 10px;
    font-size: 1rem;
    border: 1px solid #ccc;
    border-radius: 4px;
}

output {
    background-color: #eee;
    display: block;
    font-weight: bold;
}

/* Buttons */
button {
    padding: 10px 15px;
    border-radius: 4px;
    border: none;
    cursor: pointer;
    font-size: 0.9rem;
    margin-right: 5px;
    transition: background 0.3s;
}

button[onclick="speichernUndLeeren()"] {
    background-color: #4CAF50;
    color: white;
}

button[onclick="speichernUndLeeren()"]:hover {
    background-color: #45a049;
}

button#btn-historie {
    background-color: #2196F3;
    color: white;
}

button#btn-historie:hover {
    background-color: #0b7dda;
}

/* Lösch-Button in der Tabelle */
.delete-btn-table {
    background-color: #f44336;
    color: white;
    padding: 5px 10px;
    font-size: 0.8rem;
}

.delete-btn-table:hover {
    background-color: #d32f2f;
}

/* Tabelle */
.table-container {
    max-width: 800px;
    margin: 20px auto;
    overflow-x: auto;
    display: none; /* Initial versteckt */
    background: white;
    padding: 15px;
    border-radius: 8px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

table {
    width: 100%;
    border-collapse: collapse;
}

th, td {
    border-bottom: 1px solid #ddd;
    padding: 12px 8px;
    text-align: left;
}

th {
    background-color: #f8f8f8;
    font-weight: bold;
}

/* Responsive Anpassung */
@media (max-width: 600px) {
    .grid-formular {
        grid-template-columns: 1fr;
    }
    
    label {
        margin-bottom: -10px;
    }
}

.status-meldung {
    display: none; 
    text-align: center; 
    padding: 10px; 
    margin-bottom: 20px; 
    border-radius: 4px;
}

.charts {
    max-width: 800px; 
    margin: 30px auto; 
    text-align: center;
} 

.gesamtkilometer {
    color: #2c3e50; 
    font-family: sans-serif;
}

Hier der JavaScript-Code, der das ganze berechnet, die Geschwindigkeit, die Pace.

let meinChart = null;

document.addEventListener('DOMContentLoaded', () => {
    const datumInput = document.getElementById("datum");
    const zeitInput = document.getElementById("zeit_input");
    const streckeInput = document.getElementById("strecke_input");
    const geschwindigkeitOutput = document.getElementById("ergebnis_geschwindigkeit");
    const paceOutput = document.getElementById("ergebnis_pace");
    const meldungDiv = document.getElementById("status-meldung");

    function zeigeMeldung(text, istErfolg) {
        meldungDiv.textContent = text;
        meldungDiv.style.display = "block";
        meldungDiv.style.backgroundColor = istErfolg ? "#d4edda" : "#f8d7da";
        meldungDiv.style.color = istErfolg ? "#155724" : "#721c24";
        meldungDiv.style.border = `1px solid ${istErfolg ? "#c3e6cb" : "#f5c6cb"}`;
        setTimeout(() => { meldungDiv.style.display = "none"; }, 3000);
    }

    async function aktualisiereTabelle() {
        try {
            const response = await fetch('/api/laufe');
            const laufe = await response.json();
            const tbody = document.getElementById("tabellen-body");
            if (!tbody) return;
            
            tbody.innerHTML = ""; 
            laufe.forEach(lauf => {
                const row = tbody.insertRow();
                row.insertCell(0).textContent = lauf.laufenid;
                row.insertCell(1).textContent = lauf.Datum;
                row.insertCell(2).textContent = lauf.Zeit;
                row.insertCell(3).textContent = typeof lauf.Strecke === 'number' ? lauf.Strecke.toFixed(1) : lauf.Strecke;
                
                const aktionsZelle = row.insertCell(4);
                const deleteBtn = document.createElement("button");
                deleteBtn.textContent = "Löschen";
                deleteBtn.className = "delete-btn-table";
                // Wichtig: Hier rufen wir die globale window-Funktion auf
                deleteBtn.onclick = () => window.loescheLauf(lauf.laufenid);
                aktionsZelle.appendChild(deleteBtn);
            });
        } catch (error) {
            console.error("Fehler beim Laden:", error);
        }
    }

    function berechneWerte() {
        const zeitWert = zeitInput.value;
        const strecke = parseFloat(streckeInput.value);
        if (zeitWert && strecke > 0) {
            const [stunden, minuten] = zeitWert.split(':').map(Number);
            const gesamtStunden = stunden + (minuten / 60);
            const gesamtMinuten = (stunden * 60) + minuten;
            const geschwindigkeit = strecke / gesamtStunden;
            geschwindigkeitOutput.textContent = geschwindigkeit.toFixed(2) + " km/h";
            const paceDezimal = gesamtMinuten / strecke;
            const paceMin = Math.floor(paceDezimal);
            const paceSek = Math.round((paceDezimal - paceMin) * 60);
            paceOutput.textContent = `${paceMin}:${paceSek.toString().padStart(2, '0')} min/km`;
        } else {
            resetOutputs();
        }
    }

    async function aktualisiereTabelle() {
    try {
        const response = await fetch('/api/laufe');
        const laufe = await response.json();
        const tbody = document.getElementById("tabellen-body");
        if (!tbody) return;
        
        // --- 1. GESAMTKILOMETER BERECHNEN ---
        const gesamtKm = laufe.reduce((sum, lauf) => sum + (parseFloat(lauf.Strecke) || 0), 0);
        document.getElementById("gesamt-km").textContent = `Gesamtdistanz: ${gesamtKm.toFixed(1)} km`;

        // --- 2. TABELLE FÜLLEN ---
        tbody.innerHTML = ""; 
        laufe.forEach(lauf => {
            const row = tbody.insertRow();
            row.insertCell(0).textContent = lauf.laufenid;
            row.insertCell(1).textContent = lauf.Datum;
            row.insertCell(2).textContent = lauf.Zeit;
            row.insertCell(3).textContent = typeof lauf.Strecke === 'number' ? lauf.Strecke.toFixed(1) : lauf.Strecke;
            
            const aktionsZelle = row.insertCell(4);
            const deleteBtn = document.createElement("button");
            deleteBtn.textContent = "Löschen";
            deleteBtn.className = "delete-btn-table";
            deleteBtn.onclick = () => window.loescheLauf(lauf.laufenid);
            aktionsZelle.appendChild(deleteBtn);
        });

        // --- 3. CHART ZEICHNEN ---
        zeichneLaufChart(laufe);

    } catch (error) {
        console.error("Fehler beim Laden:", error);
    }
}

function zeichneLaufChart(laufe) {
    const ctx = document.getElementById('laufChart').getContext('2d');
    
    // Daten für das Chart: Chronologisch sortieren (älteste zuerst)
    const sortierteDaten = [...laufe].sort((a, b) => new Date(a.Datum) - new Date(b.Datum));
    
    const labels = sortierteDaten.map(l => l.Datum);
    const werte = sortierteDaten.map(l => l.Strecke);

    // Falls schon ein Chart existiert, löschen (sonst flackert es beim Update)
    if (meinChart) {
        meinChart.destroy();
    }

    meinChart = new Chart(ctx, {
        type: 'line',
        data: {
            labels: labels,
            datasets: [{
                label: 'Distanz (km)',
                data: werte,
                borderColor: '#4CAF50',
                backgroundColor: 'rgba(76, 175, 80, 0.1)',
                borderWidth: 2,
                tension: 0.3,
                fill: true
            }]
        },
        options: {
            scales: {
                y: { beginAtZero: true, title: { display: true, text: 'Kilometer' } },
                x: { title: { display: true, text: 'Datum' } }
            }
        }
    });
}

    function resetOutputs() {
        geschwindigkeitOutput.textContent = "0.00 km/h";
        paceOutput.textContent = "0:00 min/km";
    }

    // --- EXPORTIERTE FUNKTIONEN AN WINDOW ---
    window.speichernUndLeeren = async function() {
        const daten = { datum: datumInput.value, zeit: zeitInput.value, strecke: parseFloat(streckeInput.value) };
        if (!daten.datum || !daten.zeit || !daten.strecke) {
            zeigeMeldung("Bitte alle Felder ausfüllen!", false);
            return;
        }
        try {
            const response = await fetch('/api/speichern', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(daten)
            });
            if (response.ok) {
                zeigeMeldung("Erfolgreich gespeichert!", true);
                datumInput.value = ''; zeitInput.value = ''; streckeInput.value = '';
                resetOutputs();
                if (document.getElementById("tabellen-container").style.display === "block") aktualisiereTabelle();
            }
        } catch (error) { zeigeMeldung("Netzwerkfehler!", false); }
    };

    window.loescheLauf = async function(id) {
        if (!id || !confirm(`Lauf ID ${id} wirklich löschen?`)) return;
        try {
            const response = await fetch(`/api/delete/${id}`, { method: 'DELETE' });
            if (response.ok) {
                zeigeMeldung(`Eintrag ${id} gelöscht.`, true);
                aktualisiereTabelle();
            } else {
                zeigeMeldung("Löschen im Server fehlgeschlagen.", false);
            }
        } catch (error) { zeigeMeldung("Verbindungsfehler.", false); }
    };

    window.toggleTabelle = function() {
        const container = document.getElementById("tabellen-container");
        const btn = document.getElementById("btn-historie");
        const istSichtbar = container.style.display === "block";
        container.style.display = istSichtbar ? "none" : "block";
        btn.textContent = istSichtbar ? "Historie anzeigen" : "Historie ausblenden";
        if (!istSichtbar) aktualisiereTabelle();
    };

    window.felderlöschen = function() {
        datumInput.value = ''; zeitInput.value = ''; streckeInput.value = '';
        resetOutputs();
    };

    zeitInput.addEventListener('input', berechneWerte);
    streckeInput.addEventListener('input', berechneWerte);
});

Das Frontend für die Datenerfassung:

So sieht das Frontend aus. Klar, die Pace könnte vielleicht etwas besser sein, aber wie gesagt, besser als gar nicht gelaufen. Es war auch mein vierter Tag in Folge, im aktuellen Winter. Ich weiß, dass Läufer immer nur auf die Pace achten. Für mich ist sie bedeutungslos, aber ich habe mit hineingenommen, weil das ist halt so üblich. Ich laufe „Just for fun“, um mich fit zu halten und um keinen Marathon zu laufen oder gar einen Ultramarathon.

Ich habe andere, nachhaltigere Ziele im Leben, so etwas hier auch mal von ganz alleine zu programmieren ohne KI. HTML / CSS ist seit einem VHS-Kurs (ich weiß, die Augen werden jetzt rollen und sagen: „das ist doch kein Lernen“) von 2005. Ja, in 2005 gab es noch nicht so viele Möglichkeiten wie jetzt in 2026.

Das Frontend mit einem schönen Liniendiagramm:

So sieht die Grafik aus seit der Beginn der Erfassung der Läufe seit dem 21.12.2025 (ganz links auf der x-Achse. Es werden immer die Strecke der einzelnen Läufe. Wie gesagt, mir ist das ziemlich egal, wie viel ich schaffe. Für den Winter finde ich es ziemlich ehrgeizig, wo der Körper doch eigentlich eher auf Ruhepause getrimmt ist. Also zumindest bei mir. Andere Körper scheinen wohl anders drauf zu sein, die auch im Januar voll aufdrehen und fröhlicher sind als im warmen Frühling.

Die Kommentare sind geschlossen.