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,112 @@
|
||||
# Endpoint für die Zeitmessung (Lichtschranke)
|
||||
|
||||
> Der **Mess-Pi-Client selbst ist nicht Teil dieses Projekts** — dieses Dokument
|
||||
> beschreibt nur den **Endpoint**, den ein beliebiger Client (der zweite Raspberry
|
||||
> Pi an der Lichtschranke) bespielt. Fachlicher Hintergrund: `plan.md` §3.6 / §6.2.
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
Alle Geräte-Endpunkte erwarten einen **Geräte-Token** (nicht den Auswerter-Login):
|
||||
|
||||
```
|
||||
Authorization: Bearer <geraete-token>
|
||||
```
|
||||
|
||||
Token anlegen (interne Admin-Session):
|
||||
|
||||
```
|
||||
POST /api/v1/admin/geraete
|
||||
{ "bezeichnung": "lichtschranke-ziel-01", "typ": "lichtschranke" }
|
||||
→ 201 { "id": 1, "bezeichnung": "...", "token": "oamc_dev_…" } # Token nur hier einmalig
|
||||
```
|
||||
|
||||
Token neu ausgeben: `POST /api/v1/admin/geraete/{id}/token` ·
|
||||
Gerät sperren: `POST /api/v1/admin/geraete/{id}/sperren`
|
||||
|
||||
## Ablauf auf dem Client (empfohlen)
|
||||
|
||||
1. `POST /api/v1/geraete/heartbeat` alle ~15 s → Statusanzeige + `aktives_turnier_id`
|
||||
2. `GET /api/v1/geraete/konfiguration` → Entprellzeit, Messpunkte, Plausibilitätsgrenzen
|
||||
3. `GET /api/v1/turniere/aktiv/startliste` (ETag-fähig) → Startreihenfolge für die Anzeige
|
||||
4. Pro Durchfahrt eine Messung lokal puffern, dann als **Batch** senden:
|
||||
`POST /api/v1/zeitmessungen`
|
||||
5. `GET /api/v1/turniere/aktiv/anzeige` (SSE) → Live-Stream für die Anzeige am Parcours
|
||||
|
||||
## `POST /api/v1/zeitmessungen` — Batch, idempotent
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"geraet": "lichtschranke-ziel-01", // informativ
|
||||
"messungen": [
|
||||
{
|
||||
"id": "9f2c1e7a-4d38-4b6e-9a11-5c0b2d7e8f30", // UUID VOM GERÄT → Idempotenz
|
||||
"turnier_id": 42,
|
||||
"startnummer_gemeldet": 32, // Hinweis, kein Fremdschlüssel
|
||||
"messpunkt": "durchgang-1",
|
||||
"dauer_sekunden": 159.16, // vom Gerät berechnet (monotone Zeit)
|
||||
"gemessen_am": "2026-05-31T10:14:02.482+02:00",
|
||||
"monotonic_ns": 884213771004,
|
||||
"roh": { "trigger_start_ns": 884054611004, "trigger_ziel_ns": 884213771004 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Antwort — jede Messung wird **einzeln** quittiert
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"quittungen": [
|
||||
{ "id": "9f2c1e7a-…", "status": "angenommen", "start_id": 512, "hinweis": null }
|
||||
],
|
||||
"angenommen": 1, "duplikate": 0, "unzugeordnet": 0, "unplausibel": 0
|
||||
}
|
||||
```
|
||||
|
||||
| `status` | Bedeutung | Client-Reaktion |
|
||||
|----------------|-----------|-----------------|
|
||||
| `angenommen` | gespeichert und einem Start zugeordnet | aus lokaler Queue löschen |
|
||||
| `duplikat` | UUID war schon da (Nachlieferung nach Timeout) | aus lokaler Queue löschen |
|
||||
| `unzugeordnet` | gespeichert, aber `startnummer_gemeldet` nicht auflösbar → manuelle Zuordnung im Frontend | aus lokaler Queue löschen |
|
||||
| `unplausibel` | gespeichert, Dauer außerhalb der Grenzen → im Frontend markiert | aus lokaler Queue löschen |
|
||||
| `fehler` | z. B. `turnier_id` unbekannt | **in der Queue behalten**, später erneut |
|
||||
|
||||
**Grundregeln** (Server-seitig umgesetzt):
|
||||
|
||||
* **Idempotent über `id`** — ein doppelt gesendeter Batch erzeugt keine Dubletten.
|
||||
Ein abgebrochener Request darf gefahrlos wiederholt werden.
|
||||
* Die **Dauer kommt fertig vom Gerät** — der Server rechnet **keine** Differenz aus
|
||||
zwei Wanduhr-Zeitstempeln (NTP-Sprung auf einem Pi ohne RTC).
|
||||
* Eine Messung **ohne** zuordenbare Startnummer wird **nie verworfen**, sondern als
|
||||
`unzugeordnet` gespeichert und im internen Frontend zur Zuordnung angeboten.
|
||||
* Plausibilitätsverstöße werden **markiert**, nicht abgelehnt. Grenzen:
|
||||
`OAMC_MESS_MIN_SEKUNDEN` / `OAMC_MESS_MAX_SEKUNDEN` bzw. je Gerät in
|
||||
`geraet.konfiguration`.
|
||||
* `gemessen_am` weit in der Vergangenheit → die Messung wird als *nachgeliefert*
|
||||
markiert und trotzdem angenommen.
|
||||
|
||||
## Fehlmessung nachträglich verwerfen
|
||||
|
||||
```
|
||||
POST /api/v1/zeitmessungen/{id}/verwerfen
|
||||
{ "grund": "Zuschauer hat die Schranke ausgelöst" }
|
||||
```
|
||||
|
||||
Die Messung bleibt mit Grund gespeichert (Status `verworfen`), zählt aber nicht in
|
||||
die `summe_zeit`.
|
||||
|
||||
## Was der Server aus den Messungen macht
|
||||
|
||||
`ergebnis.summe_zeit` wird bei der Auswertung als **Summe aller gültigen,
|
||||
zugeordneten Einzelmessungen** eines Starts gebildet
|
||||
(`oamc.wertung.service.summe_zeit_fuer_start`). Aus Einzelmessungen lässt sich die
|
||||
Summe jederzeit bilden — der umgekehrte Weg existiert nicht, deshalb speichert der
|
||||
Server jede Einzelmessung.
|
||||
|
||||
## Offene Punkte (aus `plan.md` §10), die den Endpoint noch betreffen
|
||||
|
||||
* **O-13** Was misst die Lichtschranke genau (eine Gesamtdurchfahrt, mehrere
|
||||
Durchgänge, einzelne Abschnitte)? → bestimmt `messpunkt` und die Summenbildung.
|
||||
* **O-14** Woher kennt der Client die Startnummer? → bis geklärt: `startnummer_gemeldet`
|
||||
als Hinweis, Server löst gegen die Startliste auf.
|
||||
* **O-16** Geforderte Messgenauigkeit / eine oder zwei Lichtschranken.
|
||||
Reference in New Issue
Block a user