Das Schema traegt es bereits: times.comment gibt es seit dem ersten
Snapshot und wurde von der Anwendung nie benutzt. Der Grund steht jetzt
dort — optional, aber nach dem Event ist eine Strafe ohne Grund schwer
zu verteidigen. Er haengt im Tooltip an der Korrektur und steht als
eigene Spalte im CSV.
"Strafzeit fuer alle" ist raus. Eine Strafe trifft einen Fahrer; was
fuer alle gleich gilt, ist keine Strafe, sondern eine andere Stage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Beim Test aufgefallen: Wer im Funkloch stoppte, sah die Zeit weiter als
laufend — der Ausgang hatte den Stopp, der Datensatz nicht. Ein zweiter
Druck legte einen zweiten Stopp daneben, und beim Uebertragen gewann der
letzte. Welche Zeit am Ende zaehlte, war nicht mehr zu sagen.
Drei Dinge dagegen. Ein Stopp im Ausgang traegt sich sofort auch am
Datensatz ein, die Zeit ist damit sichtbar beendet. Ein zweiter Stopp
fuer dieselbe Zeit wird abgewiesen, mit Ansage statt still. Und ist der
Ausgang leer, laedt der Store einmal neu und tauscht die vorlaeufigen
Zeiten gegen die echten samt ihren IDs.
Dabei fiel die groessere Luecke auf: Ein offline gestarteter Lauf stand
nirgends und liess sich deshalb gar nicht stoppen — die Oberflaeche
kennt nur, was im Store steht. Er bekommt jetzt einen vorlaeufigen
Datensatz mit einer local:-ID, laeuft sichtbar mit und laesst sich
stoppen, bevor der Server ueberhaupt von ihm weiss.
Dazu der Verbindungswaechter: /api/health im Takt, Symbol in der
Kopfleiste und eine Meldung, die stehen bleibt, bis der Server wieder
antwortet. navigator.onLine allein taugt dafuer nicht — im WLAN ohne
Uplink und hinter einem Portal steht es unbeirrt auf true. Kehrt die
Verbindung zurueck, geht der Ausgang von selbst raus.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Eine Stage laesst sich nicht wiederholen. Zwei Dinge durften deshalb
nicht so bleiben, wie sie waren.
Erstens die Uhr. Zeiten entstehen auf den Geraeten an der Strecke — nur
so laesst sich im Funkloch ueberhaupt stoppen. Damit steckte aber der
Versatz zweier Telefonuhren in jeder Dauer, bei der einer am Start und
ein anderer im Ziel drueckt. Jedes Geraet gleicht sich jetzt gegen den
Server ab (Cristian: t0 merken, Serverzeit holen, t1 merken, Versatz =
T + RTT/2 - t1), sieben Proben, Median ueber die schnellste Haelfte. Der
Versatz liegt im localStorage und wird auf jeden Zeitstempel gerechnet.
Dafuer eine eigene Route /api/clock in den Hooks: Der Date-Header jeder
gewoehnlichen Antwort hat nach RFC 9110 Sekundenaufloesung, allein
daraus folgte ein Fehler von bis zu einer halben Sekunde — auf einer
Stage der Unterschied zwischen Platz eins und Platz drei.
Zweitens das Netz. Scheitert die Uebertragung eines Starts oder Stopps
an einem Funkloch, wandert er in einen Ausgang im localStorage statt in
einen Fehler und geht raus, sobald wieder Empfang da ist. Die
massgebliche Zeit ist die des Druckens, nicht die der Uebertragung. Ein
Stopp kann dabei auf einen Start warten, der selbst noch aussteht.
Unterschieden wird streng zwischen Funkloch und Absage: Eine abgelehnte
Anfrage — fehlende Rechte, ungueltige Daten — wird gemeldet und nicht
aufgefangen, sonst sammelte der Ausgang stumm Eintraege, die auch beim
naechsten Versuch scheitern.
Die Leiste der laufenden Zeiten zeigt nicht uebertragene Zeiten ganz
oben und erscheint dafuer auch dann, wenn gerade nichts laeuft.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
"Run" hiess in dieser App immer schon der abgesteckte Abschnitt eines
Events, auf dem gefahren und gestoppt wird — also das, was im Rennsport
Stage heisst. "Run" ist daneben der einzelne Durchgang eines Fahrers,
und genau der steht hier als `times`. Zwei Bedeutungen fuer ein Wort, an
einer Stelle, an der beide vorkommen.
Die Migration benennt um statt neu anzulegen: Collection und Feld
behalten ihre IDs, PocketBase benennt Tabelle und Spalte um, die Daten
bleiben stehen. Gesucht wird ueber die Collection-ID und nicht ueber den
Namen, damit sie auch auf einer frischen Datenbank durchlaeuft, deren
Snapshot die Collection bereits `stages` nennt.
Im Frontend wandert `times.run` zu `times.stage`, der Store heisst
stages.svelte.ts, und aus /events/[id]/runs/[runId] wird
/events/[id]/stages/[stageId].
Nicht umbenannt: `running`, `allRunning` und RunningTimesToast. Das sind
laufende Zeiten und keine Stages — dieselben Buchstaben, andere Sache.
Und die Artikel: Der Run war maskulin, die Stage ist feminin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Die eigene Karte "Laufende Zeiten" entfaellt: Laufende stehen jetzt oben in
derselben Tabelle wie die gewerteten, mit "laeuft"-Badge statt Platzziffer.
Die Zeile ist Klickziel fuer den Stopp-Dialog, der Knopf daneben macht sie
auch per Tastatur bedienbar.
Neu ist eine dauerhaft sichtbare Leiste ueber allen Dashboard-Seiten,
solange irgendwo eine Zeit laeuft - bei mehreren eingeklappt mit Anzahl und
laengster Zeit, aufklappbar. Damit bleibt die Uhr im Blick, waehrend man
zwischen zwei Startern woanders unterwegs ist.
Der Stopp-Dialog wandert dafuer in eine eigene Komponente, die sich
Run-Seite und Leiste teilen, statt die Logik zweimal zu haben.
Monospace faellt appweit weg; einzige Ausnahme bleibt die grosse mitlaufende
Uhr im Stopp-Dialog. tabular-nums bleibt stehen: gleich breite Ziffern
derselben Schrift, ohne die eine laufende Uhr bei jedem Zehntel springt.
Tabellenkoepfe hatten den Hover-Hintergrund der Body-Zeilen geerbt; die
Regel sitzt jetzt im thead und gilt fuer alle Tabellen der App.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Die Routen /dashboard/timing und /dashboard/results entfallen samt ihrer
Menueeintraege. Beides passiert jetzt dort, wo es hingehoert: am Run
innerhalb seines Events.
Die Run-Seite zeigt laufende Zeiten in Echtzeit, jede Zeile ist Klickziel
fuer den Stopp-Dialog (Stoppen oder DNF). Gestartet wird ueber einen Dialog
mit Fahrersuche. Dazu Rangliste mit CSV-Export und ein Abschnitt fuer
Zeiten ohne Wertung.
Die Fahrer-Seite zeigt alle Runs des Events aus Sicht eines Fahrers, mit
Zeit, Status und Platz - auch Runs ohne Zeit, damit sichtbar bleibt, was
fehlt.
enrich() im times-Store nimmt now als Parameter: Ohne das waere Date.now()
fuer Svelte keine Abhaengigkeit und laufende Zeiten stuenden still. Der
neue ticker() liefert diesen Wert, wodurch die {#key tick}-Bloecke der
alten Zeitnahmeseite entfallen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Die SvelteKit-App liegt unter frontend/, das Backend unter backend/ als
eigenständige PocketBase-Instanz mit Dockerfile, docker-compose und
versioniertem Schema.
- PocketBase-URL über PUBLIC_PB_URL konfigurierbar, Default bleibt die
produktive Instanz https://api.stammtisch-hersbruck.de
- Schema als Snapshot-Migration der sechs fachlichen Collections
(users, teams, events, runs, riders, times), abgezogen von der
produktiven Instanz. Verifiziert: ein Erststart gegen leere pb_data
legt alle sechs an, Felder und API-Rules stimmen überein.
- pb_data und pb_migrations als Bind-Mounts, damit Daten persistieren und
im Admin-UI erzeugte Migrationen im Repo landen
- Veraltete Dokumentation entfernt: pocketbase_schema.json nannte
Collections (stages, results, organizers), die es nicht gibt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>