Erstumsetzung: OAMC Turnierauswertung (AP 0–7, 9)
* Datenmodell + Alembic-Initialmigration (Plan §4), Seed Saison 2026 * Wertungs-Engine (Plan §3.3), regelbasiert je Kategorie/Saison - Abnahmetest gegen die Ergebnisliste 2026-05-31 * Meisterschaftsberechnung mit Streichresultaten + Tie-Break (PLATZHALTER-Punktetabelle, O-1 offen) * Ingest-API (idempotent, dry_run) inkl. Excel-Leser und Fahrer-Matching/Merge * Zeitmessungs-Endpoint (Batch, idempotent, unzugeordnet/unplausibel) + SSE-Anzeige - Mess-Pi-Client bewusst NICHT enthalten (docs/zeitmessung-endpoint.md) * Frontend: öffentliche Seiten + internes Backend (Jinja2/HTMX, kein Build) * Auslieferung: Docker (multi-arch) + docker-compose sowie Debian-Paket/APT-Repo * 57 Tests grün gegen echte PostgreSQL-Testdatenbank Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,848 @@
|
||||
# Plan: OAMC Turnierauswertung — Webanwendung
|
||||
|
||||
**Stand:** 2026-09-05 · **Status:** Entwurf, noch nicht freigegeben
|
||||
**Änderung 05.09.:** Zeitmessung per Lichtschranke aufgenommen (§3.6, §5.4, §6.2, AP 5)
|
||||
|
||||
---
|
||||
|
||||
## 1. Ausgangslage
|
||||
|
||||
### 1.1 Der Verein und die Sportart
|
||||
|
||||
Der **OAMC Reinheim e.V. im ADAC** (Odenwälder Automobil- und Motorsportclub) richtet
|
||||
**ADAC Motorrad-Turniere** aus. Diese laufen nach der **ADAC-Turnierordnung** im Bereich
|
||||
des **ADAC Hessen-Thüringen (HTH)**.
|
||||
|
||||
Motorrad-Turniersport ist kein Rennsport, sondern ein Geschicklichkeitswettbewerb: Die
|
||||
Fahrer durchfahren einen Parcours mit festen Aufgaben, dabei zählen **Fehlerpunkte** und
|
||||
**Fahrzeit**.
|
||||
|
||||
Aufgaben im Parcours (laut Sportbeschreibung auf oamc.de):
|
||||
|
||||
Fahrzeugabnahme · Schätzen (im Stand und in Fahrt) · Slalom · Acht-Figur · Spurgasse ·
|
||||
Kreisel · Tor · Schrägbrett · Wippe (mit und ohne Spurbrett) · Gummiringe ·
|
||||
Dose umsetzen · Fahrgasse · Stop Haltelinie · Langsamfahrstrecke
|
||||
|
||||
### 1.2 Aktueller Ist-Zustand der Website
|
||||
|
||||
Die bestehende Seite **oamc.de** (Zweitdomain: oamc-reinheim.de) ist eine statische
|
||||
**HTML-4.01-Frameset-Website**:
|
||||
|
||||
- Aufbau: `index` → `oben.htm` (Navigation) + `hauptfenster.htm`, jede Rubrik wieder
|
||||
ein eigenes Frameset (`m/frame-m.htm` → `m1.htm` Menü + `m2.htm` Inhalt)
|
||||
- Rubriken: Verein · VÜP ADAC-Platz · Kartgruppe · **Motorradgruppe** · Fahrrad JVS ·
|
||||
Anfahrt · Download · Weblinks · Flohmarkt · BikerSafetyDay
|
||||
- **Alle Ergebnisse liegen ausschließlich als PDF** unter `/datei/` und werden auf
|
||||
`d/d-mot-turnier.htm` verlinkt — Tagesergebnisse, Meisterschaftsstände (HTH 2002–2026),
|
||||
Bundesturnier-Ergebnisse, Protokolle
|
||||
- Die Seite **`m/m-fahrer.htm` ist eine „BAUSTELLE"** — es gibt aktuell **keine
|
||||
Fahrerdatenbank**. Genau das ist die Lücke, die dieses Projekt schließt.
|
||||
- `m-ergebnisse.htm` und `m-ausschreibung.htm` verweisen nur auf die PDF-Downloadseite
|
||||
|
||||
### 1.3 Wie heute ausgewertet wird
|
||||
|
||||
Die Rahmenausschreibung 2026 nennt als Anforderung an den Veranstalter:
|
||||
> „Auswertungscomputer mit MSOffice (**Excel**) und Drucker sollte zeitgemäß vorhanden sein."
|
||||
|
||||
Es existieren Vorlagen als `.xlsx` (z. B. `MT-NENNLISTE-5-2025.xlsx`). Die Auswertung
|
||||
läuft also heute **in Excel**, das Ergebnis wird als PDF exportiert und manuell auf die
|
||||
Website gelegt. Meisterschaftsstände werden separat fortgeschrieben.
|
||||
|
||||
**Schmerzpunkte:**
|
||||
|
||||
| Problem | Auswirkung |
|
||||
|---|---|
|
||||
| Ergebnisse nur als PDF | nicht durchsuchbar, nicht verlinkbar, nicht mobil lesbar |
|
||||
| Meisterschaftsstand manuell | fehleranfällig, spät verfügbar |
|
||||
| Keine Fahrerhistorie | „Wie oft war X schon dabei?" nicht beantwortbar |
|
||||
| Fahrerstammdaten pro Turnier neu erfasst | Tippfehler, Dubletten, Namensvarianten |
|
||||
| Frameset-Website | kein Deep-Link, schlechtes SEO, unbrauchbar auf dem Handy |
|
||||
|
||||
---
|
||||
|
||||
## 2. Ziel
|
||||
|
||||
Eine Webanwendung, die
|
||||
|
||||
1. **Turnierergebnisse auswertet** — Fehlerpunkte + Zeit → Platzierung je Wertungsklasse
|
||||
2. **Alle Daten in einer Datenbank hält** statt in Excel und PDF
|
||||
3. **Zeiten per API von der Lichtschranken-Zeitmessung entgegennimmt** (zweiter Raspberry
|
||||
Pi am Parcours, siehe §3.6), in die Datenbank schreibt und **live anzeigt**
|
||||
4. **Fahrer samt Stammdaten und Historie** bereitstellt (die heutige „Baustelle")
|
||||
5. Die **Meisterschafts-/Pokalwertung** automatisch fortschreibt
|
||||
|
||||
### Nicht-Ziele (bewusst außerhalb dieses Projekts)
|
||||
|
||||
- Ablösung der kompletten oamc.de (Kart, Fahrrad, Flohmarkt, VÜP bleiben, wo sie sind)
|
||||
- Online-Nennung/Anmeldung durch Teilnehmer → **Phase 2**, siehe §9
|
||||
- Elektronische Erfassung der **Fehlerpunkte** — die bleiben beim Wertungspersonal im
|
||||
Parcours und werden manuell eingegeben (siehe §3.6)
|
||||
- Zahlungsabwicklung des Nenngelds (10 € je Start, wird vor Ort kassiert)
|
||||
|
||||
---
|
||||
|
||||
## 3. Fachliche Grundlagen
|
||||
|
||||
> ⚠️ Die folgenden Regeln wurden aus der Ergebnisliste
|
||||
> `2026-05-31-MT-REIN-HAIN.PDF` und der `MT-Rahmenausschreibung-2026.pdf`
|
||||
> **abgeleitet**. Sie sind **vor der Implementierung gegen die ADAC-Turnierordnung 2026
|
||||
> (`MT-D2025.pdf`) und mit dem Bereichsleiter zu verifizieren** — siehe §10.
|
||||
|
||||
### 3.1 Veranstaltungsstruktur
|
||||
|
||||
- Ein Renntag kann ein **Doppelturnier** sein: zwei Turniere zweier Ortsclubs laufen
|
||||
parallel auf einem Platz, z. B. „ADAC-Motorrad-Doppelturnier des MSC Hainstadt und
|
||||
des OAMC Reinheim".
|
||||
- **Jeder Fahrer wird in beiden Turnieren getrennt gewertet** — die Ergebnisliste vom
|
||||
31.05.2026 enthält für dieselben Fahrer je einen Block „Reinheim" und einen Block
|
||||
„Hainstadt" mit **eigenen Zeiten, Fehlern und Platzierungen**.
|
||||
- ⇒ Datenmodell-Konsequenz: `veranstaltung` (Renntag) : `turnier` (Wertungslauf) = 1:n
|
||||
|
||||
### 3.2 Klassen
|
||||
|
||||
**Jugendklassen** (Einteilung strikt nach Jahrgang, Ausweispflicht bei der Nennung):
|
||||
|
||||
| Klasse | Jahrgang | ~Alter | Fahrzeug |
|
||||
|---|---|---|---|
|
||||
| J 1 | 2019 / 2018 / 2017 | 7–9 | Kindermotorrad max. 110 ccm / 5,5 kW |
|
||||
| J 2 | 2016 / 2015 | 10–11 | Kindermotorrad max. 110 ccm / 5,5 kW |
|
||||
| J 3 | 2014 / 2013 | 12–13 | Mofa, Motorrad + Roller max. 125 ccm / 11 kW |
|
||||
| J 4 | 2012 / 2011 | 14–15 | Mofa, Motorrad + Roller max. 125 ccm / 11 kW |
|
||||
| J 5 | 2010 / 2009 / 2008 (bis 18. Geburtstag) | 16–18 | Mofa, Motorrad + Roller max. 125 ccm / 11 kW |
|
||||
|
||||
Für Klasse 1 muss der Teilnehmer **am Veranstaltungstag** bereits 7 Jahre alt sein.
|
||||
⇒ Die Jahrgangstabelle ist **jahresabhängig** und muss versioniert in der DB liegen,
|
||||
nicht im Code (`klassen_jahrgang` mit `saison`).
|
||||
|
||||
**Erwachsenenklassen:** `S 1`–`S 9` (Stammfahrer) und `A 1`–`A 4` (Anfänger), unterteilt
|
||||
nach Fahrzeugtyp/Hubraum. Aus der Sportbeschreibung: Mokicks und Roller bis 80 ccm ·
|
||||
bis 250 ccm · bis 650 ccm · über 650 ccm · Enduro (mehrere Hubraumklassen) ·
|
||||
Seitenwagen · **Pedelec / E-Bike** · **E-Scooter** (neu).
|
||||
|
||||
> ❗ Die genaue Zuordnung `S 1`…`S 9` / `A 1`…`A 4` → Fahrzeugtyp ist aus den
|
||||
> öffentlichen Seiten **nicht** ersichtlich. Muss erfragt werden (§10).
|
||||
|
||||
**Wertungsklasse ≠ Klasse:** In der Ergebnisliste werden Klassen zu Wertungsklassen
|
||||
zusammengefasst, wenn zu wenige Starter da sind — z. B. eine gemeinsame Wertung
|
||||
„S 5, S 9, S 3, S 6, S 7" und „A 1, A 4". Das Zusammenlegen ist eine
|
||||
**Entscheidung pro Turnier** und gehört als Datum in die DB, nicht als Regel in den Code.
|
||||
|
||||
### 3.3 Wertungslogik ← Kernstück der Anwendung
|
||||
|
||||
Spalten der Ergebnisliste:
|
||||
|
||||
`Start-Nr.` · `Voller Name` · `Regionalclub` · `Klasse` · `Geschlecht` · `Kategorie` ·
|
||||
`Summe Zeit` · `Summe Fehler` · `Gesamt` · `Platzierung`
|
||||
|
||||
**Es gibt zwei verschiedene Berechnungen für `Gesamt`:**
|
||||
|
||||
**a) Erwachsene (Kategorie S, und A-Fahrer):**
|
||||
```
|
||||
Gesamt = Summe Zeit + Summe Fehler
|
||||
Platzierung = aufsteigend nach Gesamt
|
||||
```
|
||||
Belege aus der Liste vom 31.05.2026, Wertungsklasse „S 5, S 9, S 3, S 6, S 7":
|
||||
| Fahrer | Klasse | Zeit | Fehler | Gesamt | Platz |
|
||||
|---|---|---|---|---|---|
|
||||
| Bernius Meik | S 6 | 105,43 | 0 | **105,43** | 1 |
|
||||
| Schmunk Christian | S 9 | 103,26 | 10 | **113,26** | 2 |
|
||||
| Krack Robin | S 5 | 130,33 | 0 | **130,33** | 4 |
|
||||
| Steinbrech Nicolai | S 5 | 65,71 | 1210 | **1275,71** | 11 |
|
||||
|
||||
**b) Jugend (Kategorie J):**
|
||||
```
|
||||
Gesamt = Summe Fehler ← die Zeit geht NICHT in die Wertung ein
|
||||
Platzierung = aufsteigend nach Fehler, bei Gleichstand aufsteigend nach Zeit
|
||||
```
|
||||
Das deckt sich mit der Sportbeschreibung:
|
||||
> „Bei den Turnieren kommt es in der Jugendklasse vorrangig auf die fehlerfreie
|
||||
> Durchfahrt an, da die Zeit erst bei Punktgleichheit herangezogen wird."
|
||||
|
||||
Belege, Wertungsklasse J 3:
|
||||
| Fahrer | Zeit | Fehler | Gesamt | Platz | Anmerkung |
|
||||
|---|---|---|---|---|---|
|
||||
| Freund Philipp | 136,88 | 0 | **0** | 1 | drei Fahrer mit 0 Fehlern … |
|
||||
| D'Angelo Leandro | 140,40 | 0 | **0** | 2 | … Reihenfolge über die Zeit |
|
||||
| Heil Luca | 144,46 | 0 | **0** | 3 | |
|
||||
| Mattes Flynn | 160,05 | 10 | **10** | 4 | |
|
||||
| Ratter Mitja | 148,42 | 60 | **60** | 7 | Gleichstand 60 Fehler … |
|
||||
| Kemesies Finlay | 165,39 | 60 | **60** | 8 | … 148,42 < 165,39 |
|
||||
|
||||
⇒ Die Wertungsformel ist **pro Kategorie konfigurierbar** zu implementieren, nicht
|
||||
hart zu verdrahten (`wertungsregel` als Strategie/Enum je Kategorie und Saison).
|
||||
|
||||
**Fehlerpunkte** treten in Vielfachen von 10 auf (10/20/30/…/1210). Die Zuordnung
|
||||
„welcher Fehler an welcher Aufgabe kostet wie viele Punkte" steht in der
|
||||
ADAC-Turnierordnung und ist zu beschaffen (§10).
|
||||
|
||||
**Mehrfachstarts:** Ein Fahrer kann am selben Tag **mehrfach starten** (anderes Fahrzeug,
|
||||
andere Klasse). Beispiel 31.05.2026: *Schmunk Christian* startet in S 9 **und** S 3,
|
||||
*Bernius Meik* in S 6 **und** S 7 — jeweils mit eigener Startnummer und eigenem Ergebnis.
|
||||
⇒ Der Schlüssel eines Ergebnisses ist **nicht** (Turnier, Fahrer), sondern
|
||||
(Turnier, Start), und Nenngeld fällt „für jeden Start" an.
|
||||
|
||||
### 3.4 Meisterschafts-/Pokalwertung (HTH)
|
||||
|
||||
- **Erwachsene:** ein Gesamtklassement der A- und S-Fahrer/innen (klassenübergreifend)
|
||||
- **Jugend:** Pokalwertung **klassenweise** J 1 – J 5
|
||||
|
||||
**Streichresultate** — nicht alle Läufe zählen:
|
||||
|
||||
| Turniere in der Saison | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| **davon gewertet** | 8 | 8 | 8 | 7 | 6 | 5 | 4 | 3 | 2 |
|
||||
|
||||
Nicht jede Veranstaltung zählt zur Meisterschaft — der Terminkalender markiert Läufe
|
||||
„mit Meisterschaftswertung" bzw. „o. M. = ohne Meisterschaft".
|
||||
|
||||
**Endlaufqualifikation:** die besten **10 Erwachsenen** und **10 Jugendlichen** der
|
||||
HTH-Meisterschaft (Jugend ohne Trennung Damen/Herren, Verteilung prozentual nach
|
||||
Teilnehmerzahl der 5 Jugendklassen). **Bei Punktgleichheit zählt die Majorität der
|
||||
Siege.** Endgültige Festlegung durch den Bereichsleiter ⇒ die Anwendung *schlägt vor*,
|
||||
sie *entscheidet nicht*.
|
||||
|
||||
> ❗ Die **Punktetabelle** (Platz → Meisterschaftspunkte) ist noch nicht bekannt (§10).
|
||||
|
||||
### 3.5 Weitere Regeln aus der Rahmenausschreibung 2026
|
||||
|
||||
- Nenngeld einheitlich **10 €** je Start (S, A, Jugend, Mofa/Moped, E-Bike, Pedelec, E-Scooter)
|
||||
- **Nennschluss:** Klasse S + Jugendklassen 14:00 Uhr, A-Klasse + E-Bike/Pedelec/E-Scooter 15:00 Uhr
|
||||
- Pokalläufe bei Doppelveranstaltungen beginnen generell 09:00 Uhr
|
||||
- Gültiger Jugendausweis + unterschriebener Haftungsausschluss sind bei der Nennung vorzulegen
|
||||
- Eine **nummerierte Nennungsliste** ist zu führen, die Reihenfolge nach Möglichkeit einzuhalten
|
||||
- Für A-Klassen-Fahrer und Gelegenheitsfahrer wird z. B. jede 10. Startnummer freigehalten
|
||||
- Schlussbericht + Ergebnisliste müssen nach FFM gemeldet werden — **der Veranstalter-Zuschuss
|
||||
wird erst überwiesen, wenn beide Dokumente dort sind** ⇒ Export in genau diesen Formaten
|
||||
ist kein „nice to have", sondern zahlungsrelevant
|
||||
|
||||
### 3.6 Zeitmessung per Lichtschranke *(Vorgabe des Auftraggebers)*
|
||||
|
||||
Die API wird von einem **zweiten Raspberry Pi am Parcours** bespielt. Dieser ist an eine
|
||||
**Lichtschranke** angeschlossen, misst die Fahrzeiten, sendet sie an die Anwendung und
|
||||
**zeigt die Daten des aktuellen Fahrzeugs/Starters an**.
|
||||
|
||||
Damit gibt es **zwei Datenquellen pro Ergebnis**:
|
||||
|
||||
| Wert | Quelle | Weg |
|
||||
|---|---|---|
|
||||
| `Summe Zeit` | **Lichtschranke** (Messgerät) | automatisch per API |
|
||||
| `Summe Fehler` | **Wertungspersonal im Parcours** | manuell im internen Frontend |
|
||||
|
||||
Ein Ergebnis ist also erst vollständig, wenn **beide** Seiten geliefert haben. Das
|
||||
Datenmodell muss einen Start mit Zeit, aber ohne Fehlerpunkte (und umgekehrt) sauber
|
||||
abbilden können — ein „halbes" Ergebnis ist der Normalzustand während der Veranstaltung,
|
||||
kein Fehler.
|
||||
|
||||
#### Konsequenzen für den Entwurf
|
||||
|
||||
**a) Rohmessungen statt Summen speichern.**
|
||||
Die Spalte heißt in der Ergebnisliste `Summe Zeit` — die Zeit ist also vermutlich eine
|
||||
**Summe mehrerer Messungen** (mehrere Durchgänge oder mehrere gemessene Abschnitte,
|
||||
z. B. Langsamfahrstrecke und Gesamtdurchfahrt). Die Anwendung speichert deshalb **jede
|
||||
Einzelmessung** (`zeitmessung`) und bildet die Summe selbst. Rückweg gibt es nicht:
|
||||
aus einer gespeicherten Summe lassen sich Einzelmessungen nie wieder herstellen, aus
|
||||
Einzelmessungen die Summe jederzeit. → offene Frage **O-13**
|
||||
|
||||
**b) Das Messgerät ist ein eigenständiger API-Client, kein Teil der Anwendung.**
|
||||
Es bekommt einen **Geräte-Token** (nicht den Auswerter-Login), darf nur Messungen
|
||||
schreiben und Starter-Daten lesen, und wird in der DB als `geraet` geführt. Ein Gerät
|
||||
am offenen Platz muss man notfalls einzeln sperren können.
|
||||
|
||||
**c) Netzausfall ist der Normalfall, nicht die Ausnahme.**
|
||||
Am ADAC-Platz kann das Netz wegbrechen. Der Mess-Pi **muss lokal puffern** (SQLite oder
|
||||
Datei-Queue) und nach Wiederverbindung nachliefern. Deshalb gilt zwingend:
|
||||
- jede Messung bekommt eine **UUID vom Messgerät** → Server ist idempotent, doppelt
|
||||
gesendete Messungen erzeugen keine Dubletten
|
||||
- die Messung trägt **zwei Zeitstempel**: Wanduhr (`gemessen_am`) und **monotone
|
||||
Gerätezeit** (`monotonic_ns`). Die Wanduhr eines Pi ohne Netz und ohne RTC springt
|
||||
beim NTP-Sync — die Dauer zwischen zwei Lichtschranken-Ereignissen darf davon nicht
|
||||
abhängen. **Der Pi berechnet die Dauer selbst und sendet sie**, der Server rechnet
|
||||
keine Differenz aus zwei Wanduhr-Zeitstempeln.
|
||||
- der Server nimmt Messungen **rückwirkend** an (`gemessen_am` in der Vergangenheit)
|
||||
und markiert sie als nachgeliefert
|
||||
|
||||
**d) Zuordnung Messung → Starter ist der kritische Punkt.**
|
||||
Eine Lichtschranke misst eine Zeit, sie weiß nicht, **wer** durchgefahren ist. Die
|
||||
Zuordnung muss irgendwo herkommen — Eingabe der Startnummer am Mess-Pi, eine
|
||||
Warteschlange „nächster Starter" aus der Nennliste, oder ein Transponder/RFID.
|
||||
→ offene Frage **O-14**. Bis zur Klärung wird konservativ geplant:
|
||||
- der Mess-Pi holt sich per API die **Startreihenfolge** des laufenden Turniers
|
||||
- er sendet die Messung **mit** der Startnummer, die er gerade anzeigt
|
||||
- eine Messung **ohne** zuordenbare Startnummer wird nicht verworfen, sondern als
|
||||
`zeitmessung` ohne `start_id` gespeichert und im Frontend zur manuellen Zuordnung
|
||||
angeboten. Eine gemessene Zeit ist nicht reproduzierbar — sie darf nie verloren gehen,
|
||||
nur weil die Zuordnung fehlte.
|
||||
|
||||
**e) Fehlmessungen sind Betriebsrealität.**
|
||||
Zuschauer, Helfer oder ein zurücksetzendes Motorrad lösen die Lichtschranke aus.
|
||||
Nötig sind: **Entprellung/Mindestabstand** zwischen zwei Triggern auf dem Pi, ein
|
||||
**Verwerfen-Knopf** in der Erfassung, sowie Plausibilitätsprüfung serverseitig
|
||||
(eine Fahrzeit von 3 s oder 900 s ist zu markieren, nicht stillschweigend zu übernehmen).
|
||||
Jede verworfene Messung bleibt mit Grund gespeichert.
|
||||
|
||||
**f) Die Anzeige am Parcours ist ein eigener Anwendungsfall.**
|
||||
Der Mess-Pi zeigt Starter- und Fahrzeugdaten an, zieht sie also **lesend** aus der API.
|
||||
Das braucht einen schlanken, schnellen Endpunkt und eine Live-Aktualisierung (SSE),
|
||||
und er muss **auch bei Netzausfall die zuletzt geladene Startliste weiter anzeigen**.
|
||||
|
||||
> ❗ Was genau angezeigt werden soll (Startnummer, Name, Klasse, Fahrzeug, laufende Zeit,
|
||||
> letzte Zeit?) und auf welcher Hardware (HDMI-Monitor, kleines Display, LED-Tafel) ist
|
||||
> noch offen → **O-15**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Datenmodell
|
||||
|
||||
```
|
||||
verein ──┐
|
||||
├──< fahrer ──< start >── turnier >── veranstaltung
|
||||
saison ──┤ │ │ │
|
||||
├──< klasse ───┘ │ └──< wertungsklasse
|
||||
│ ├──< ergebnis (Zeit + Fehler, berechnet)
|
||||
│ └──< zeitmessung >── geraet (Lichtschranke)
|
||||
└──< meisterschaft ──< meisterschaft_stand
|
||||
```
|
||||
|
||||
### Tabellen
|
||||
|
||||
**`verein`** — Regionalclub / Ortsclub
|
||||
`id` · `name` („OAMC Reinheim", „MSC Schotten", „PMS Kassel", „Hainstadt") ·
|
||||
`kurzname` · `adac_gau` · `aktiv`
|
||||
|
||||
**`fahrer`** — Stammdaten, **einmalig**, nicht pro Turnier
|
||||
`id` · `nachname` · `vorname` · `geburtsjahr` · `geburtsdatum` (nullable, für J1-Prüfung
|
||||
„am Veranstaltungstag 7 Jahre") · `geschlecht` (M/W/D) · `verein_id` ·
|
||||
`adac_mitgliedsnr` (nullable) · `jugendausweis_gueltig_bis` · `aktiv` · `notiz` ·
|
||||
`erstellt_am` / `geaendert_am`
|
||||
→ **Dublettenerkennung** beim Import nötig, siehe §6.3
|
||||
|
||||
**`saison`** — Jahr, macht Regeln versionierbar
|
||||
`jahr` (PK) · `turnierordnung_version` · `streichresultate_tabelle` (JSONB) ·
|
||||
`nenngeld_cent` · `aktiv`
|
||||
|
||||
**`klasse`** — Klassendefinition **pro Saison**
|
||||
`id` · `saison_jahr` · `code` („J 1", „S 6", „A 4") · `kategorie` (J/S/A) ·
|
||||
`bezeichnung` · `jahrgaenge` (int[], nur Jugend) · `ccm_max` · `kw_max` ·
|
||||
`fahrzeugtyp` · `sortierung`
|
||||
|
||||
**`veranstaltung`** — der Renntag
|
||||
`id` · `datum` · `titel` · `ort` · `ausrichter_verein_id` · `ist_doppelturnier` ·
|
||||
`veranstaltungsleiter` · `turnierleiter` · `status` (geplant/laufend/vorlaeufig/final)
|
||||
|
||||
**`turnier`** — der einzelne Wertungslauf (bei Doppelturnier zwei pro Veranstaltung)
|
||||
`id` · `veranstaltung_id` · `name` („Reinheim" / „Hainstadt") · `veranstalter_verein_id` ·
|
||||
`zaehlt_meisterschaft` (bool) · `status`
|
||||
|
||||
**`wertungsklasse`** — pro Turnier zusammengefasste Klassen
|
||||
`id` · `turnier_id` · `bezeichnung` („S 5, S 9, S 3, S 6, S 7") ·
|
||||
`klasse_ids` (int[]) · `sortierung`
|
||||
|
||||
**`start`** — eine Teilnahme; ein Fahrer kann mehrere pro Turnier haben
|
||||
`id` · `turnier_id` · `fahrer_id` · `klasse_id` · `wertungsklasse_id` · `startnummer` ·
|
||||
`fahrzeug` (Text) · `nenngeld_bezahlt` · `status` (genannt/gestartet/DNF/DNS/DSQ)
|
||||
→ UNIQUE (`turnier_id`, `startnummer`)
|
||||
|
||||
**`ergebnis`** — 1:1 zum Start, getrennt für saubere Nachträge/Korrekturen
|
||||
`start_id` (PK) · `summe_zeit` (numeric(7,2), **aus `zeitmessung` summiert**) ·
|
||||
`summe_fehler` (int, **manuell erfasst**) · `gesamt` (numeric(9,2), **berechnet**) ·
|
||||
`platzierung` (int, **berechnet**) · `zeit_vollstaendig` (bool) ·
|
||||
`fehler_vollstaendig` (bool) · `erfasst_von` · `erfasst_am` · `korrigiert_am` ·
|
||||
`korrektur_grund`
|
||||
→ Die beiden `*_vollstaendig`-Flags machen sichtbar, welche der zwei Quellen (§3.6)
|
||||
noch fehlt. Gewertet wird erst, wenn beide gesetzt sind.
|
||||
|
||||
**`geraet`** — Messgerät / Anzeige am Parcours
|
||||
`id` · `bezeichnung` („Lichtschranke Ziel", „Anzeige Start") · `typ`
|
||||
(lichtschranke/anzeige) · `token_hash` · `aktiv` · `letzte_meldung_am` ·
|
||||
`firmware_version` · `notiz`
|
||||
→ `letzte_meldung_am` ist der Heartbeat: das Frontend zeigt, ob die Messung noch lebt
|
||||
|
||||
**`zeitmessung`** — Einzelmessung, **nicht** die Summe
|
||||
`id` (UUID, **vom Gerät vergeben** → Idempotenz) · `geraet_id` · `turnier_id` ·
|
||||
`start_id` (**nullable** — unzugeordnete Messungen gehen nicht verloren) ·
|
||||
`startnummer_gemeldet` (int, was das Gerät angezeigt hat) · `messpunkt`
|
||||
(z. B. „Durchgang 1", „Langsamfahrstrecke") · `dauer_sekunden` (numeric(7,2),
|
||||
**vom Gerät berechnet**) · `gemessen_am` (timestamptz, Wanduhr) · `monotonic_ns` (bigint) ·
|
||||
`empfangen_am` · `nachgeliefert` (bool) · `status` (gueltig/verworfen/unzugeordnet) ·
|
||||
`verworfen_grund` · `roh` (JSONB, Rohereignisse der Lichtschranke)
|
||||
→ UNIQUE (`id`); Index auf (`turnier_id`, `status`), (`start_id`)
|
||||
|
||||
**`aufgabe`** / **`ergebnis_aufgabe`** — *optional, Phase 2*: Einzelwertung je Parcours-Aufgabe
|
||||
(Slalom, Wippe, Kreisel …). Die heutigen PDFs enthalten nur Summen; die Struktur
|
||||
vorzusehen kostet jetzt nichts und erlaubt später Detailauswertungen. Sobald die
|
||||
Lichtschranke mehrere Messpunkte bedient, wächst `zeitmessung.messpunkt` natürlich in
|
||||
diese Struktur hinein.
|
||||
|
||||
**`meisterschaft`** — z. B. „HTH Erwachsene 2026", „HTH Jugend J 3 2026"
|
||||
`id` · `saison_jahr` · `bezeichnung` · `kategorie` · `klasse_id` (nullable) · `regelwerk` (JSONB)
|
||||
|
||||
**`meisterschaft_stand`** — materialisiert, nach jedem Turnier neu berechnet
|
||||
`meisterschaft_id` · `fahrer_id` · `punkte_gesamt` · `punkte_je_lauf` (JSONB) ·
|
||||
`gestrichene_laeufe` (JSONB) · `anzahl_siege` (Tie-Break!) · `platz` · `berechnet_am`
|
||||
|
||||
**`import_job`** — Audit-Trail jedes API-/Datei-Imports
|
||||
`id` · `quelle` · `dateiname` · `roh_payload` (JSONB) · `status` · `fehler` ·
|
||||
`angelegt_von` · `angelegt_am`
|
||||
|
||||
**`benutzer`** / **`rolle`** — Admin, Auswerter, Lesend
|
||||
|
||||
### Kernprinzipien
|
||||
|
||||
- `gesamt` und `platzierung` werden **von der Anwendung berechnet**, nicht importiert —
|
||||
importierte Werte werden gegen die Berechnung **geprüft** und Abweichungen gemeldet
|
||||
(fängt Excel-Fehler in Altdaten und Regelmissverständnisse auf beiden Seiten)
|
||||
- Alles **saisonversioniert**: Klassen, Jahrgänge, Streichresultate, Punktetabelle.
|
||||
Ergebnisse aus 2019 müssen 2030 noch korrekt reproduzierbar sein.
|
||||
- Ergebnisse sind **append-only mit Korrektur-Historie**, kein stilles UPDATE
|
||||
|
||||
---
|
||||
|
||||
## 5. Architektur & Technologie
|
||||
|
||||
### 5.1 Vorhandene Umgebung (geprüft auf diesem Gerät)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Hardware | Raspberry Pi 4 Model B Rev 1.5, 8 GB RAM, 117 GB Speicher (94 GB frei) |
|
||||
| OS | Debian 13 (trixie), Linux 6.18 aarch64 |
|
||||
| **PostgreSQL 17.10** | installiert **und aktiv** ✅ |
|
||||
| **nginx** | installiert **und aktiv** ✅ |
|
||||
| **Python 3.13.5** + pip 25.1.1 | ✅ |
|
||||
| Node.js / npm | ❌ nicht installiert |
|
||||
| Docker | ❌ nicht installiert |
|
||||
| git | ❌ **nicht installiert** — vor Projektstart nachinstallieren |
|
||||
|
||||
### 5.2 Empfohlener Stack
|
||||
|
||||
Der Stack folgt dem, was **schon läuft** — auf einem Pi 4 zählt jedes vermiedene
|
||||
Laufzeit-Ökosystem.
|
||||
|
||||
| Schicht | Wahl | Begründung |
|
||||
|---|---|---|
|
||||
| Datenbank | **PostgreSQL 17** | läuft bereits; JSONB für Regelwerke, saubere Constraints, echte Transaktionen |
|
||||
| Backend | **Python 3.13 + FastAPI** | Python ist da; FastAPI liefert die API-Doku (OpenAPI/Swagger) automatisch mit — wichtig, weil Dritte die API bespielen sollen |
|
||||
| ORM / Migration | **SQLAlchemy 2 + Alembic** | Schemaänderungen über Jahre nachvollziehbar |
|
||||
| Validierung | **Pydantic v2** | ein Schema für API-Validierung *und* Doku |
|
||||
| Templates | **Jinja2**, serverseitig gerendert | Ergebnislisten sind Dokumente, keine App. Kein Build-Schritt, kein Node, sofort suchmaschinenlesbar |
|
||||
| Interaktivität | **HTMX** (~14 kB, per CDN oder lokal) | Filter/Sortierung/Live-Nachladen ohne SPA-Apparat |
|
||||
| Live-Updates | **SSE** (Server-Sent Events), von FastAPI direkt unterstützt | Anzeige am Parcours und Live-Ergebnisse; einseitiger Datenfluss, kein WebSocket-Zustand nötig, reconnectet von selbst |
|
||||
| CSS | handgeschrieben oder **Pico.css** | keine Build-Pipeline |
|
||||
| Excel/CSV-Import | **openpyxl** + `csv` | die Nennlisten sind `.xlsx` |
|
||||
| PDF-Export | **WeasyPrint** | Schlussbericht + Ergebnisliste müssen nach FFM |
|
||||
| Server | **uvicorn** hinter dem vorhandenen **nginx** | nginx macht TLS und liefert Statisches |
|
||||
| Prozess | **systemd-Unit** | kein Docker nötig |
|
||||
| TLS | **Let's Encrypt / certbot** | |
|
||||
|
||||
**Bewusst nicht gewählt:** React/Vue-SPA (Build-Kette auf dem Pi, kein SEO, viel Aufwand
|
||||
für Tabellenanzeige) · Docker (Overhead ohne Nutzen bei einer App) · SQLite
|
||||
(Mehrbenutzerbetrieb am Turniertag, Postgres ist schon da)
|
||||
|
||||
### 5.3 Betrieb
|
||||
|
||||
```
|
||||
╔═══ Am Parcours ═══════════════╗
|
||||
║ Pi #2 „Mess-Pi" ║
|
||||
║ Lichtschranke → GPIO ║
|
||||
║ lokale Queue (SQLite) ║──┐ POST Messungen (idempotent, gepuffert)
|
||||
║ Anzeige Starter/Fahrzeug ║<─┼─ GET Startliste + SSE Live
|
||||
╚═══════════════════════════════╝ │
|
||||
│
|
||||
Internet / LAN ──> nginx (TLS, statisch) ┘
|
||||
└→ uvicorn/FastAPI (systemd) → PostgreSQL 17 (lokal)
|
||||
→ /var/oamc/uploads (PDFs, Bilder)
|
||||
```
|
||||
|
||||
- **Backup:** täglicher `pg_dump` + wöchentliche Vollsicherung auf externes Medium.
|
||||
Auf einer SD-Karte/SSD im Pi ist das nicht optional.
|
||||
- **Offline-Fähigkeit am Turniertag** ist durch die Zeitmessung **Pflicht geworden**, nicht
|
||||
mehr optional (§3.6c). Zwei Ebenen:
|
||||
1. der **Mess-Pi** puffert lokal und liefert nach — deckt kurze Aussetzer ab
|
||||
2. bei Veranstaltungen **ohne Internet** muss der Anwendungs-Pi **mit vor Ort** und
|
||||
beide Geräte hängen an einem lokalen Netz (Router/Hotspot ohne Uplink). Die
|
||||
Synchronisation zum öffentlichen Server erfolgt danach.
|
||||
⇒ Entscheidung dazu offen, siehe **O-7**.
|
||||
- **Zeitserver:** ohne Internet gibt es kein NTP. Der Anwendungs-Pi sollte im lokalen
|
||||
Netz selbst NTP anbieten, damit die Zeitstempel zusammenpassen. Für die *Messung*
|
||||
ist das unkritisch (der Mess-Pi rechnet monoton, §3.6c), für die Protokollierung nicht.
|
||||
|
||||
### 5.4 Software auf dem Mess-Pi
|
||||
|
||||
Bewusst klein und eigenständig — das Gerät muss auch dann sinnvoll weiterlaufen, wenn
|
||||
der Server nicht erreichbar ist.
|
||||
|
||||
| Teil | Wahl |
|
||||
|---|---|
|
||||
| GPIO / Lichtschranke | `gpiozero` mit `pigpio`- oder `lgpio`-Backend; Trigger als **Interrupt**, nicht Polling |
|
||||
| Zeitmessung | `time.monotonic_ns()`, Entprellung/Mindestabstand konfigurierbar |
|
||||
| Lokale Queue | SQLite, Einträge bleiben bis vom Server quittiert |
|
||||
| Versand | HTTP + Geräte-Token, Retry mit Backoff, Batch-Nachlieferung |
|
||||
| Anzeige | Browser im Kiosk-Modus auf die Anzeige-Seite der Anwendung (§7), Daten per SSE, letzte Startliste im `localStorage` als Offline-Rückfall |
|
||||
| Betrieb | systemd-Unit, `WatchdogSec`, Autostart, read-only Root-FS erwägen (Stromausfall am Platz) |
|
||||
|
||||
> Für saubere Zeiten unter Last ist ein Interrupt-basierter Trigger Pflicht. Ob die
|
||||
> Genauigkeit eines Linux-Userspace-Prozesses reicht, hängt von der geforderten
|
||||
> Auflösung ab → **O-16**. Die Ergebnislisten führen **1/100 s** (z. B. `159,16`);
|
||||
> das ist mit `pigpio` gut erreichbar, mit Polling in Python nicht zuverlässig.
|
||||
|
||||
---
|
||||
|
||||
## 6. API
|
||||
|
||||
Zwei Richtungen: **Ingest** (Daten kommen rein) und **Read** (Daten gehen raus).
|
||||
|
||||
### 6.1 Ingest — `POST /api/v1/…` (authentifiziert, API-Key)
|
||||
|
||||
| Endpunkt | Zweck |
|
||||
|---|---|
|
||||
| `POST /api/v1/veranstaltungen` | Renntag anlegen |
|
||||
| `POST /api/v1/turniere` | Wertungslauf zum Renntag |
|
||||
| `POST /api/v1/fahrer` · `PUT /api/v1/fahrer/{id}` | Stammdaten |
|
||||
| `POST /api/v1/turniere/{id}/starts` | Nennliste (Bulk) |
|
||||
| `POST /api/v1/turniere/{id}/ergebnisse` | **Ergebnis-Bulk-Upload** ← Kernfall |
|
||||
| `POST /api/v1/import/xlsx` | Excel-Nennliste/Ergebnisliste als Datei |
|
||||
| `POST /api/v1/turniere/{id}/finalisieren` | Wertung rechnen, Meisterschaft fortschreiben |
|
||||
|
||||
Beispiel-Payload Ergebnis-Upload:
|
||||
|
||||
```json
|
||||
{
|
||||
"quelle": "excel-auswertung-v3",
|
||||
"idempotenz_schluessel": "2026-05-31-reinheim-final",
|
||||
"ergebnisse": [
|
||||
{ "startnummer": 32, "fahrer": { "nachname": "Gál-Szász", "vorname": "Péter",
|
||||
"geburtsjahr": 2015, "geschlecht": "M", "verein": "OAMC Reinheim" },
|
||||
"klasse": "J 2", "summe_zeit": 159.16, "summe_fehler": 10,
|
||||
"gesamt_erwartet": 10, "platzierung_erwartet": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Regeln für den Ingest:**
|
||||
|
||||
- **Idempotent** über `idempotenz_schluessel` — ein doppelt gesendeter Upload legt keine
|
||||
Dubletten an. Am Turniertag mit wackligem Netz ist das Pflicht, nicht Komfort.
|
||||
- **Zweistufig:** `?dry_run=true` liefert eine Validierungsvorschau (welche Fahrer sind
|
||||
neu, welche werden gematcht, welche Werte weichen ab) — erst danach wird geschrieben.
|
||||
- **`gesamt_erwartet` / `platzierung_erwartet`** sind optional; wenn geliefert, rechnet
|
||||
der Server nach und meldet Abweichungen als Warnung, statt sie zu übernehmen.
|
||||
- Jeder Aufruf landet in `import_job` mit Roh-Payload — bei Streit über eine Platzierung
|
||||
ist rekonstruierbar, was wann ankam.
|
||||
|
||||
### 6.2 Geräte-API — Zeitmessung & Anzeige *(Mess-Pi, Token-Auth)*
|
||||
|
||||
Eigener Namensraum mit eigenem Rechteprofil: schreiben **nur** Messungen, lesen **nur**
|
||||
das, was die Anzeige braucht. Kompromittierung des Geräts am offenen Platz darf keine
|
||||
Ergebnisse manipulieren können.
|
||||
|
||||
| Endpunkt | Zweck |
|
||||
|---|---|
|
||||
| `POST /api/v1/geraete/heartbeat` | „ich lebe" + Firmware/Queue-Länge → Statusanzeige im Frontend |
|
||||
| `GET /api/v1/geraete/konfiguration` | Entprellzeit, Messpunkte, aktives Turnier — zentral gepflegt statt auf dem Pi |
|
||||
| `GET /api/v1/turniere/aktiv/startliste` | Startreihenfolge für die Anzeige, ETag-fähig, offlinefest zwischenspeicherbar |
|
||||
| `GET /api/v1/turniere/aktiv/anzeige` (SSE) | Live-Stream: aktueller Starter, letzte Zeit |
|
||||
| `POST /api/v1/zeitmessungen` | **Messungen (Batch, idempotent)** ← Kernfall |
|
||||
| `POST /api/v1/zeitmessungen/{id}/verwerfen` | Fehlmessung markieren |
|
||||
|
||||
Beispiel-Payload der Zeitmessung — bewusst als **Batch**, weil der Pi nach einem
|
||||
Netzausfall mehrere Messungen auf einmal nachliefert:
|
||||
|
||||
```json
|
||||
{
|
||||
"geraet": "lichtschranke-ziel-01",
|
||||
"messungen": [
|
||||
{
|
||||
"id": "9f2c1e7a-4d38-4b6e-9a11-5c0b2d7e8f30",
|
||||
"turnier_id": 42,
|
||||
"startnummer_gemeldet": 32,
|
||||
"messpunkt": "durchgang-1",
|
||||
"dauer_sekunden": 159.16,
|
||||
"gemessen_am": "2026-05-31T10:14:02.482+02:00",
|
||||
"monotonic_ns": 884213771004,
|
||||
"roh": { "trigger_start_ns": 884054611004, "trigger_ziel_ns": 884213771004 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Antwort quittiert **jede Messung einzeln** (`angenommen` / `duplikat` / `unzugeordnet` /
|
||||
`unplausibel`) — erst eine Quittung löscht den Eintrag aus der lokalen Queue des Pi.
|
||||
|
||||
**Festlegungen:**
|
||||
- Die **`id` vergibt das Gerät** (UUIDv4/v7). Nur so ist Nachliefern nach Timeout ohne
|
||||
Dubletten möglich — der Pi weiß nach einem abgebrochenen Request nicht, ob der Server
|
||||
ihn verarbeitet hat, und darf ihn deshalb gefahrlos wiederholen.
|
||||
- Die **Dauer kommt fertig vom Gerät**, der Server rechnet keine Differenz aus Wanduhr-
|
||||
Zeitstempeln (Begründung §3.6c).
|
||||
- `startnummer_gemeldet` ist ein **Hinweis, kein Fremdschlüssel**. Der Server löst sie
|
||||
gegen die Startliste auf; scheitert das, wird die Messung als `unzugeordnet`
|
||||
gespeichert und im Frontend zur Zuordnung angeboten — **nie verworfen**.
|
||||
- Plausibilitätsgrenzen (min/max Fahrzeit je Klasse) sind konfigurierbar; Verstöße
|
||||
werden **markiert**, nicht abgelehnt.
|
||||
- Der Geräte-Token ist **nicht** der Auswerter-Login und einzeln sperrbar.
|
||||
|
||||
### 6.3 Read — `GET /api/v1/…` (öffentlich, ohne Key)
|
||||
|
||||
```
|
||||
GET /api/v1/veranstaltungen?saison=2026
|
||||
GET /api/v1/turniere/{id}/ergebnisse?wertungsklasse=J3
|
||||
GET /api/v1/fahrer?q=bernius&verein=OAMC+Reinheim
|
||||
GET /api/v1/fahrer/{id}/historie
|
||||
GET /api/v1/meisterschaften/{id}/stand
|
||||
GET /api/v1/meisterschaften/{id}/endlauf-qualifikation
|
||||
```
|
||||
|
||||
Zusätzlich **iCal** (`/termine.ics`) für den Terminkalender und **CSV-Export** je Liste.
|
||||
|
||||
### 6.4 Fahrer-Matching beim Import
|
||||
|
||||
Der heikelste Teil des Imports. Aus den Altdaten sichtbar: Namen mit Sonderzeichen
|
||||
(*Gál-Szász*), Vereinsangaben mal als „OAMC Reinheim", mal als „Hainstadt" (Ortsname
|
||||
statt Clubname), Namensgleichheiten in Familien (*Gál-Szász Péter* / *Máté* / *János*;
|
||||
*Bernius Meik* / *Jörg*; *Krack Robin* / *Luca*; *Tinz Xenia* / *Enrique*).
|
||||
|
||||
Vorgehen:
|
||||
1. Exakter Match auf (Nachname, Vorname, Geburtsjahr) — normalisiert (Unicode NFKD,
|
||||
Kleinschreibung, Bindestriche)
|
||||
2. Unscharfer Match (Levenshtein / `pg_trgm`) → **Vorschlag**, nie automatisch
|
||||
3. Kein Match → neuer Fahrer wird angelegt, aber im Import-Report als „NEU" markiert
|
||||
4. **Merge-Funktion** im Admin, um nachträglich erkannte Dubletten zusammenzuführen
|
||||
|
||||
---
|
||||
|
||||
## 7. Seiten (Frontend)
|
||||
|
||||
**Öffentlich:**
|
||||
|
||||
| Seite | Inhalt |
|
||||
|---|---|
|
||||
| Startseite Motorrad-Turnier | nächster Termin, letzte Ergebnisse, aktueller Meisterschaftsstand |
|
||||
| **Termine** | Saisonkalender, mit/ohne Meisterschaftswertung, Ausrichter, iCal-Abo |
|
||||
| **Ergebnisse** | je Veranstaltung → Turnier → Wertungsklasse, sortierbar, mobil lesbar |
|
||||
| **Live** (Turniertag) | vorläufiger Stand, sobald Zeiten eintreffen — deutlich als *vorläufig* gekennzeichnet, solange Fehlerpunkte fehlen (§3.6) |
|
||||
| **Fahrer** | durchsuchbare Liste (löst die „BAUSTELLE" ab) |
|
||||
| **Fahrerprofil** | Stammdaten, alle Starts, Platzierungen, Bestzeiten, Saisonverlauf |
|
||||
| **Meisterschaft** | HTH-Stand Erwachsene + Jugend J 1–J 5, inkl. Streichresultat-Kennzeichnung |
|
||||
| **Endlauf-Qualifikation** | aktueller Stand der Top-10-Plätze |
|
||||
| Sportbeschreibung / Ausschreibung / Downloads | bestehende Inhalte, sauber übernommen |
|
||||
|
||||
**Intern (Login):**
|
||||
|
||||
| Seite | Inhalt |
|
||||
|---|---|
|
||||
| Nennungsannahme | Fahrer suchen/anlegen, Klasse automatisch aus Jahrgang vorschlagen, Startnummer vergeben, Nenngeld quittieren |
|
||||
| Ergebniserfassung | schnelle Eingabe Zeit/Fehler je Startnummer, tastaturoptimiert |
|
||||
| Auswertung | Wertungsklassen bilden, rechnen, vorläufig/final setzen |
|
||||
| Korrekturen | mit Begründung, mit Historie |
|
||||
| Exporte | Ergebnisliste (PDF, ADAC-Layout), Schlussbericht, Nennliste, CSV |
|
||||
| **Messungen zuordnen** | unzugeordnete/unplausible Lichtschranken-Messungen einem Start zuweisen oder mit Grund verwerfen |
|
||||
| **Geräte** | Mess-Pi-Status (Heartbeat, Queue-Länge), Token vergeben/sperren, Entprellzeit und Messpunkte konfigurieren |
|
||||
| Import-Monitor | Import-Jobs, Warnungen, Fahrer-Dubletten mergen |
|
||||
| Stammdaten | Saison, Klassen, Vereine, Punktetabelle, Benutzer |
|
||||
|
||||
**Anzeige am Parcours** (eigener, minimaler Modus für den Mess-Pi):
|
||||
|
||||
| Seite | Inhalt |
|
||||
|---|---|
|
||||
| `/anzeige` | aktueller Starter groß (Nr., Name, Klasse, Fahrzeug), laufende/letzte Zeit, nächste Starter; SSE-Update; Kiosk-tauglich; **behält bei Verbindungsverlust die letzte Startliste** und zeigt den Offline-Zustand deutlich an |
|
||||
|
||||
**Anforderungen quer:** responsiv (am Platz wird das Handy benutzt) · Deep-Links auf jede
|
||||
Ergebnisliste · druckbare Ansichten · Aushang-/Beamer-Modus (große Schrift,
|
||||
Autorefresh) · barrierearm (Kontrast, Tabellen-Semantik)
|
||||
|
||||
---
|
||||
|
||||
## 8. Arbeitspakete
|
||||
|
||||
### AP 0 — Klärung & Setup *(vor allem anderen)*
|
||||
- Offene Fragen §10 mit dem Verein / Bereichsleiter klären
|
||||
- **ADAC-Turnierordnung 2026 (`MT-D2025.pdf`) beschaffen und Wertungsregeln verifizieren**
|
||||
- Punktetabelle Meisterschaft beschaffen
|
||||
- `git` installieren, Repository anlegen, Python-venv, Projektgerüst
|
||||
- Zugriff auf die Excel-Vorlagen und ein paar echte Nennlisten
|
||||
|
||||
### AP 1 — Datenmodell & Migrationen
|
||||
- SQLAlchemy-Modelle + Alembic-Initialmigration
|
||||
- Stammdaten-Seed: Saison 2026, Klassen J 1–J 5, S/A-Klassen, Vereine
|
||||
- Unit-Tests auf Constraints (Mehrfachstart erlaubt, Startnummer eindeutig je Turnier)
|
||||
|
||||
### AP 2 — Wertungs-Engine *(der Kern — zuerst und mit Tests)*
|
||||
- Regelstrategien je Kategorie (Jugend: Fehler → Zeit; Erwachsene: Zeit + Fehler)
|
||||
- Platzierung inkl. Gleichstandsauflösung, DNF/DNS/DSQ-Behandlung
|
||||
- **Regressionstest gegen die reale Ergebnisliste vom 31.05.2026** — alle 13 Wertungs-
|
||||
blöcke müssen exakt die publizierten Platzierungen reproduzieren. Das ist der
|
||||
Abnahmetest für die Engine.
|
||||
|
||||
### AP 3 — Meisterschaftsberechnung
|
||||
- Punktevergabe, Streichresultate nach Tabelle §3.4
|
||||
- Tie-Break „Majorität der Siege"
|
||||
- Endlauf-Qualifikationsvorschlag Top 10 / Top 10 Jugend
|
||||
- Nachrechnen gegen einen veröffentlichten HTH-Jahresstand als Test
|
||||
|
||||
### AP 4 — API
|
||||
- Read-Endpunkte + OpenAPI-Doku
|
||||
- Ingest-Endpunkte, Idempotenz, `dry_run`, API-Key-Auth, Rate-Limit
|
||||
- Import-Job-Protokollierung
|
||||
- Fahrer-Matching inkl. Merge
|
||||
|
||||
### AP 5 — Zeitmessung: Geräte-API & Mess-Pi
|
||||
- Geräte-Registrierung, Token, Heartbeat, zentrale Konfiguration
|
||||
- `POST /zeitmessungen` mit Batch, Idempotenz, Einzelquittung, Plausibilitätsprüfung
|
||||
- Auflösung `startnummer_gemeldet` → Start; unzugeordnete Messungen im Frontend behandeln
|
||||
- **Client auf dem Mess-Pi:** GPIO-Trigger, Entprellung, monotone Zeitmessung, lokale
|
||||
SQLite-Queue, Retry mit Backoff, Kiosk-Anzeige mit Offline-Rückfall
|
||||
- SSE-Endpunkt und `/anzeige`-Seite
|
||||
- **Abnahmetest:** Netzstecker während der Messung ziehen — nach Wiederverbindung müssen
|
||||
alle Messungen genau einmal in der DB stehen. Ebenso: Batch doppelt senden → keine Dubletten.
|
||||
- **Kalibrierung:** gemessene Zeiten gegen eine Handstoppung/Referenz vergleichen, bevor
|
||||
produktiv damit gewertet wird
|
||||
|
||||
### AP 6 — Öffentliches Frontend
|
||||
- Layout, Navigation, responsive Tabellen
|
||||
- Termine, Ergebnisse, Fahrerliste, Fahrerprofil, Meisterschaft
|
||||
- iCal, CSV, Druckansichten
|
||||
|
||||
### AP 7 — Internes Frontend
|
||||
- Login/Rollen, Nennungsannahme, Ergebniserfassung, Auswertung, Korrekturen
|
||||
- Exporte (PDF-Ergebnisliste im ADAC-Layout, Schlussbericht)
|
||||
|
||||
### AP 8 — Datenübernahme Altbestand
|
||||
- Import der PDF-Ergebnisse ab einem zu definierenden Jahr
|
||||
(⚠️ die PDFs nutzen Subset-Fonts mit eigener Kodierung — reine Textextraktion schlägt
|
||||
bei den Meisterschafts-PDFs fehl; realistisch ist ein **halbautomatischer** Import mit
|
||||
manueller Nacharbeit, ggf. direkt aus den Excel-Originalen statt aus den PDFs)
|
||||
- Vereinheitlichung Vereinsnamen, Dublettenbereinigung Fahrer
|
||||
|
||||
### AP 9 — Deployment & Betrieb
|
||||
- systemd-Unit, nginx-vHost, TLS
|
||||
- Backup-Automatik + **getesteter Restore**
|
||||
- Monitoring, Logrotation
|
||||
- Einweisung der Auswerter, Kurzanleitung
|
||||
|
||||
### AP 10 — Pilotbetrieb
|
||||
- Eine Veranstaltung **parallel** zu Excel auswerten und Ergebnisse vergleichen
|
||||
- **Zeitmessung zunächst nur mitlaufen lassen**, nicht werten: Lichtschranken-Zeit gegen
|
||||
die manuell gestoppte Zeit stellen. Erst wenn beide über eine ganze Veranstaltung
|
||||
übereinstimmen, wird die Messung wertungsrelevant.
|
||||
- Erst nach fehlerfreiem Parallellauf umstellen
|
||||
|
||||
---
|
||||
|
||||
## 9. Ausbaustufen nach dem Kern
|
||||
|
||||
- **Online-Voranmeldung** — die Rahmenausschreibung wünscht ausdrücklich Voranmeldung
|
||||
größerer Teilnehmergruppen und eine Anmeldeliste; das ist die naheliegendste Erweiterung
|
||||
- **Aufgaben-Einzelwertung** (Tabellen aus §4 bereits vorgesehen) → „woran verliere ich Zeit?"
|
||||
- Bildarchiv mit Verknüpfung Foto ↔ Fahrer ↔ Veranstaltung
|
||||
- Automatische Meldung von Schlussbericht/Ergebnisliste an den ADAC
|
||||
- Mehrmandantenfähigkeit für andere Ortsclubs (MSC Schotten, MSC Hainstadt, PMS Kassel …)
|
||||
- Übernahme der übrigen oamc.de-Rubriken aus dem Frameset
|
||||
|
||||
---
|
||||
|
||||
## 10. Offene Fragen
|
||||
|
||||
| # | Frage | Warum blockierend |
|
||||
|---|---|---|
|
||||
| **O-1** | **Punktetabelle der Meisterschaft** (Platz → Punkte) | AP 3 nicht baubar |
|
||||
| **O-2** | Zuordnung `S 1`–`S 9` und `A 1`–`A 4` → Fahrzeugtyp/Hubraum/kW | Stammdaten-Seed |
|
||||
| **O-3** | Zählen **A-Fahrer** zur Meisterschaft? Die Rahmenausschreibung sagt „Gesamtklassement der A- und S-Fahrer/innen", die Spalte `Kategorie` ist bei A-Fahrern in der Ergebnisliste jedoch **leer** (bei S steht „S", bei Jugend „J") | AP 3 |
|
||||
| **O-4** | Gilt die Jugend-Wertung (nur Fehler) auch beim Bundesendlauf, oder dort Zeit+Fehler? | AP 2 |
|
||||
| **O-5** | Behandlung von DNF / DNS / Disqualifikation in Tages- und Jahreswertung | AP 2/3 |
|
||||
| ~~O-6~~ | ~~Wer soll die API bespielen?~~ → **beantwortet:** ein zweiter Raspberry Pi an der Lichtschranke, der Zeiten sendet und Starter-/Fahrzeugdaten anzeigt (§3.6) | erledigt |
|
||||
| **O-7** | **Ist am ADAC-Platz ein verlässliches Netz?** Wenn nein: muss der Anwendungs-Pi mit vor Ort? WLAN, LTE oder Kabel zwischen Mess-Pi und Server? | **hoch** — bestimmt die gesamte Betriebsarchitektur (§5.3) |
|
||||
| **O-8** | Ab welchem Jahr sollen Altdaten übernommen werden (PDFs reichen bis 2002 zurück)? | Aufwand AP 8 |
|
||||
| **O-9** | Wo wird gehostet — dieser Raspberry Pi, ein Server beim Verein, Webhosting? | AP 9, Erreichbarkeit |
|
||||
| **O-10** | Verhältnis zur bestehenden oamc.de: Unterseite, Subdomain (z. B. `mt.oamc.de`) oder Ablösung? | Navigation, DNS |
|
||||
| **O-11** | Wer bekommt Schreibrechte, wie viele Auswerter, gemeinsamer Account oder je Person? | AP 7 |
|
||||
| **O-12** | Liegen die Excel-Originale der Altveranstaltungen noch vor? | macht AP 8 um ein Vielfaches einfacher |
|
||||
|
||||
### Neue Fragen zur Zeitmessung
|
||||
|
||||
| # | Frage | Warum blockierend |
|
||||
|---|---|---|
|
||||
| **O-13** | Was misst die Lichtschranke genau? **Eine** Gesamtdurchfahrt, **mehrere Durchgänge** oder einzelne Abschnitte (Langsamfahrstrecke …)? Woraus setzt sich `Summe Zeit` zusammen? | bestimmt `zeitmessung.messpunkt` und die Summenbildung; AP 5 |
|
||||
| **O-14** | **Woher weiß der Mess-Pi, wer gerade fährt?** Startnummer manuell eintippen, Warteschlange „nächster Starter" aus der Nennliste, Transponder/RFID? | kritischster Punkt der Zuordnung; AP 5 |
|
||||
| **O-15** | Was genau soll die **Anzeige** zeigen und auf welcher Hardware (HDMI-Monitor, kleines Display, LED-Tafel)? Für wen — Fahrer, Publikum, Wertungspersonal? | Gestaltung `/anzeige`; AP 5 |
|
||||
| **O-16** | Geforderte **Messgenauigkeit**? Die Listen führen 1/100 s. Reicht Userspace-GPIO oder braucht es dedizierte Hardware? Ein oder zwei Lichtschranken (Start **und** Ziel)? | Hardware-Auswahl, AP 5 |
|
||||
| **O-17** | **Wie kommen die Fehlerpunkte in die Anwendung** — Zettel vom Wertungspersonal und zentrale Eingabe, oder Tablets im Parcours? | AP 7; Phase 2 könnte hier ansetzen |
|
||||
| **O-18** | Löst die Lichtschranke **automatisch** aus (Fahrzeug unterbricht Strahl) oder gibt es zusätzlich einen manuellen Start-/Stopp-Taster als Rückfall? | Ausfallkonzept, AP 5 |
|
||||
| **O-19** | Wer baut/betreut die **Hardware** (Lichtschranke, Verkabelung, Stromversorgung am Platz)? Existiert sie schon oder wird sie beschafft? | Terminplanung |
|
||||
|
||||
---
|
||||
|
||||
## 11. Datenschutz
|
||||
|
||||
**Das ist hier kein Formalpunkt.** Es werden **Namen, Geburtsjahre, Geschlecht und
|
||||
Vereinszugehörigkeit von Kindern ab 7 Jahren** verarbeitet und **öffentlich im Internet
|
||||
angezeigt**. Die heutige Praxis (Namen in öffentlichen PDFs) wird durch eine
|
||||
durchsuchbare Datenbank mit Personenprofilen **qualitativ verändert** — aus einer
|
||||
schwer auffindbaren PDF-Liste wird ein über Jahre verknüpftes Profil eines Minderjährigen.
|
||||
Das ist datenschutzrechtlich eine andere Kategorie und muss vor dem Livegang geklärt sein.
|
||||
|
||||
Zu klären und umzusetzen:
|
||||
|
||||
- **Rechtsgrundlage** je Verarbeitung; Einwilligung der Erziehungsberechtigten für die
|
||||
Veröffentlichung von Jugenddaten (idealerweise in Nennformular/Haftungsausschluss
|
||||
aufnehmen — die Formulare werden ohnehin unterschrieben)
|
||||
- **Datenminimierung öffentlich:** Geburts*jahr* statt Geburtsdatum, kein Wohnort,
|
||||
keine Kontaktdaten, keine ADAC-Mitgliedsnummer
|
||||
- **`robots.txt` / `noindex`** für Fahrerprofile — Ergebnislisten dürfen auffindbar sein,
|
||||
Personenprofile von Minderjährigen sollten es nicht sein
|
||||
- **Opt-out**: Anzeige als „Teilnehmer 12" oder Namenskürzel auf Wunsch
|
||||
- **Löschkonzept / Aufbewahrungsfristen**, Auskunfts- und Berichtigungsrecht
|
||||
- **Verzeichnis von Verarbeitungstätigkeiten**, TOMs (Backup-Verschlüsselung, TLS,
|
||||
Zugriffsrollen, Protokollierung)
|
||||
- Ggf. **Auftragsverarbeitung** mit dem Hoster
|
||||
|
||||
> Empfehlung: Vor AP 6 (öffentliches Frontend) eine Entscheidung des Vorstands
|
||||
> herbeiführen, wie Jugenddaten öffentlich dargestellt werden. Die technische Umsetzung
|
||||
> ist einfach — die Entscheidung ist es nicht, und sie nachträglich zu ändern bedeutet,
|
||||
> bereits indexierte Seiten wieder einzufangen.
|
||||
|
||||
---
|
||||
|
||||
## 12. Risiken
|
||||
|
||||
| Risiko | Auswirkung | Gegenmaßnahme |
|
||||
|---|---|---|
|
||||
| Wertungsregeln in §3.3 sind aus PDFs **abgeleitet**, nicht bestätigt | falsche Platzierungen, Vertrauensverlust | AP 0: Turnierordnung beschaffen; AP 2: Regressionstest gegen echte Listen; AP 10: Parallellauf |
|
||||
| Ausfall am Turniertag (Pi, Netz, Strom) | Veranstaltung blockiert | Excel-Fallback bereithalten, bis der Parallellauf sauber ist; Export vor Ort möglich halten |
|
||||
| **Lichtschranke fällt aus oder misst falsch** | Zeiten des laufenden Turniers fehlen — **nicht nachholbar**, die Fahrt ist vorbei | manuelle Zeitnahme als Rückfall **immer** parallel bereithalten; manuelle Nacherfassung im Frontend; Plausibilitätsprüfung serverseitig; AP 10 misst zunächst nur mit |
|
||||
| **Fehlauslösung** durch Zuschauer/Helfer/rangierendes Motorrad | falsche Zeit im Ergebnis | Entprellung auf dem Pi, Verwerfen-Funktion mit Grund, Plausibilitätsgrenzen, Vier-Augen-Prinzip vor „final" |
|
||||
| **Messung dem falschen Fahrer zugeordnet** | zwei Ergebnisse gleichzeitig falsch, schwer zu bemerken | Zuordnung (O-14) früh und sauber klären; Anzeige am Parcours macht die Zuordnung für alle sichtbar → Fehler fällt sofort auf |
|
||||
| Netzausfall zwischen Mess-Pi und Server | Messungen gehen verloren | lokale Queue + Idempotenz (§3.6c); Abnahmetest in AP 5 zieht bewusst den Netzstecker |
|
||||
| Altdaten-Import aus PDFs scheitert an Font-Kodierung | AP 8 wird teuer | Excel-Originale beschaffen (O-12); Import zeitlich begrenzen; notfalls Start ab 2026 |
|
||||
| Fahrer-Dubletten durch Namensvarianten | falsche Meisterschaftsstände | Matching + Merge-Werkzeug in AP 4, Import-Report mit „NEU"-Markierung |
|
||||
| Datenschutzthema Jugenddaten wird spät geklärt | Livegang verzögert oder Rückbau | §11 vor AP 6 entscheiden |
|
||||
| Projekt hängt an einer Person | keine Wartung | Doku, sauberes Repo, zweite eingewiesene Person |
|
||||
| Raspberry Pi: SD-Karte/Speicherausfall | Datenverlust | Backup extern + Restore testen, SSD statt SD-Karte erwägen; auf dem Mess-Pi read-only Root-FS erwägen |
|
||||
| Scope-Ausweitung (Online-Nennung, Bildarchiv …) | Kern wird nie fertig | §9 ist bewusst *nach* dem Kern; AP 10 ist der Zielpunkt |
|
||||
|
||||
---
|
||||
|
||||
## 13. Nächster Schritt
|
||||
|
||||
1. Diesen Plan im Verein durchgehen, insbesondere **§10 (offene Fragen)** und
|
||||
**§11 (Datenschutz)**
|
||||
2. **ADAC-Turnierordnung 2026 und Punktetabelle beschaffen** — ohne die stehen AP 2 und AP 3
|
||||
3. **Zeitmessung klären: O-13 (was wird gemessen), O-14 (Zuordnung zum Fahrer),
|
||||
O-16 (Genauigkeit/Hardware), O-19 (wer baut sie)** — davon hängt AP 5 komplett ab
|
||||
4. Entscheidung zu **O-7** (Netz am Platz) und **O-9/O-10** (Hosting, Domain)
|
||||
5. Danach AP 0 → AP 1 → AP 2
|
||||
|
||||
> **Reihenfolge-Hinweis:** Die Zeitmessung ist das sichtbarste, aber nicht das erste
|
||||
> Arbeitspaket. Sie liefert nur die Hälfte eines Ergebnisses (§3.6) und braucht Turnier,
|
||||
> Startliste und Wertungslogik als Unterbau. AP 1–3 zuerst, dann AP 5 — sonst hat der
|
||||
> Mess-Pi nichts, wogegen er Startnummern auflösen könnte.
|
||||
|
||||
---
|
||||
|
||||
### Quellen
|
||||
|
||||
- `www.oamc.de` — Startseite, Navigation, Rubrik Motorradgruppe (`m/m1.htm`, `m/m2.htm`,
|
||||
`m/m-sportbeschreibung.htm`), Downloadseite `d/d-mot-turnier.htm`
|
||||
- `www.oamc.de/datei/2026-05-31-MT-REIN-HAIN.PDF` — Tagesergebnis Doppelturnier
|
||||
MSC Hainstadt / OAMC Reinheim, 13 Wertungsblöcke; Grundlage für §3.3
|
||||
- `www.oamc.de/datei/MT-Rahmenausschreibung-2026.pdf` (Stand 1.2.2026) — Nenngeld,
|
||||
Nennschluss, Streichresultate, Jugend-Klasseneinteilung, Endlaufqualifikation
|
||||
- Systemprüfung auf dem Zielgerät (Raspberry Pi 4B) für §5.1
|
||||
Reference in New Issue
Block a user