Ohne Service Worker war "offline" nur so lange wahr, wie der Tab offen
blieb: Ein Neuladen an der Stage endete in einer weissen Seite — samt
den Zeiten, die noch im Ausgang lagen.
Drei Sorten Anfragen, drei Strategien. Was zum Build gehoert, liegt
unter seiner Versions-URL und aendert sich nie: Cache zuerst.
Seitenaufrufe und lesende Anfragen an PocketBase: Netz zuerst, Antwort
merken, ohne Netz die zuletzt gesehene Fassung — nur so steht nach einem
Neuladen im Funkloch ueberhaupt ein Kader da, gegen den sich stoppen
laesst.
Schreibende Anfragen werden nie aus dem Cache beantwortet: Ein Start,
der aus dem Cache "gelingt", waere eine Zeit, die es nicht gibt. Dafuer
ist der Ausgang zustaendig. Und /api/clock ist ausgenommen — eine
gemerkte Uhrzeit ist das Gegenteil von dem, wozu die Route da ist.
Beim Abmelden raeumt der Worker seinen API-Vorrat weg. Sonst stuenden
auf einem geteilten Geraet die Daten des vorigen Nutzers offline weiter
da.
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
Die Leiste der laufenden Zeiten steht jetzt offen: Wer an der Strecke
steht, will sie sehen und nicht erst danach fragen. Und die ganze Zeile
ist der Knopf, nicht ein Icon an ihrem Rand — ein 16-px-Ziel neben einer
fingerbreiten Zeile ist eine Einladung zum Danebentippen. Der Name der
Stage verlinkt dafuer nicht mehr auf ihre Seite: ein Link im Knopf waere
weder gueltiges HTML noch eindeutig zu treffen.
Im Zeit-Dialog steht der Fahrer als Kachel statt in einer Auswahlliste —
die Startnummer gross wie am Trikot (font-startnumber: Impact, mit
Haettenschweiler und Arial Narrow Bold als Ersatz), der Name darunter.
Eine Liste verlangt aufklappen, zielen und treffen; drei Schritte fuer
etwas, das ein Zeitnehmer zwanzigmal am Tag macht.
Starten und Stoppen sind Flaechen von 96 px Hoehe, Symbol ueber Wort,
statt einer Zeile im Fussbereich. DNF steht daneben, gleich hoch, damit
es sich treffen laesst, aber schmaler, damit es nicht versehentlich
getroffen wird.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Icon-Auswahl: Das Raster ist jetzt die Auswahl. Daneben stand ein
Freitextfeld fuer jeden Namen von lucide.dev samt Anleitung. Das
verlangte, einen Katalog von tausend Icons zu kennen und den Namen
fehlerfrei abzutippen, und belohnte den Tippfehler mit einem leeren
Kaestchen. Fehlt ein Icon, gehoert es in FLAG_ICON_SUGGESTIONS.
Hoehenprofil in Weiss zu 90 % statt in Blau.
Profil: kein Speichern-Knopf mehr. Es sind zwei Felder, beide gehoeren
nur dem Kontoinhaber — der Name wird uebernommen, sobald das Feld den
Fokus verliert oder Enter kommt, das Bild, sobald eines gewaehlt ist.
Der Satz zur E-Mail-Adresse faellt weg: Das Feld ist abgeschaltet, das
sagt schon alles, was er erklaeren wollte.
Der Passwortwechsel steckt hinter einem Knopf in einem Dialog. Er ist
die Ausnahme; drei Felder dafuer standen dauerhaft offen auf der Seite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
"Höchstens einlösbar" war kein Satz und kein Begriff, sondern beides
halb. Aus den Feldern wird "Gültigkeit in Tagen" und "Anzahl der
Einlösungen", aus den Hinweisen darunter ganze Saetze statt
Gleichungen ("0 = beliebig oft").
Auch der Hinweis an jedem Link liest sich jetzt wie Sprache: "unbegrenzt
gueltig" statt "ohne Ablauf", "noch nicht eingeloest" statt "0x
genutzt".
Und der Knopf "Logo" sagt jetzt, was er tut.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Der Link lag in einem einzeiligen Feld. So etwas bricht nicht um, es
scrollt — im Dialog sah man ein Stueck aus der Mitte und musste raten,
ob der richtige Link kopiert wird.
Jetzt steht er in voller Laenge auf einer eigenen Zeile, darunter der
Hinweis und die beiden Knoepfe. break-all statt break-words, weil eine
URL keine Leerzeichen hat, an denen sich trennen liesse.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
<header> und <main> waren beide `relative z-10`. Bei gleichem z-index
gewinnt, was spaeter im DOM steht — also <main>. Das aufgeklappte
Team-Panel haengt unter die Kopfleiste hinein in diese Flaeche und lag
dadurch dahinter: Beim Hinunterfahren traf der Zeiger <main> statt das
Panel, die Komponente sah ein Verlassen und klappte nach delayDuration
zu, bevor ein Eintrag zu treffen war.
Ein z-50 am Panel half nicht — die Kopfleiste ist mit ihrem eigenen
z-index ein Stapelkontext, aus dem kein Kind herausragt. Also die
Kopfleiste selbst hoeher.
Nachgestellt und A/B geprueft: mit z-10 schliesst das Menue beim
Hinunterfahren, mit z-20 bleibt es offen und der Eintrag laesst sich
anklicken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Zwei Fehler, einer je Anlauf.
Der Inhalt gehoert ohne Viewport gerendert: Dann haengt er am
NavigationMenu.Item, das ohnehin `relative` ist, und steht damit unter
"Team". Mit Viewport ist er eine eigene Flaeche unter der ganzen Leiste
und beginnt an deren linkem Rand — unter "Dashboard".
Und der Standardabstand mt-1.5 war ein toter Streifen zwischen
Menuepunkt und Panel. Wer den Zeiger hinunterbewegte, verliess beides
zugleich; nach delayDuration klappte das Menue zu, bevor ein Eintrag zu
treffen war. Ein Hover-Menue darf keine Luecke haben.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Mit viewport={false} haengt der Inhalt per top-full/left-0 am naechsten
positionierten Vorfahren, und das ist die Leiste selbst und nicht der
Menuepunkt. Das Panel stand dadurch am linken Rand statt unter "Team":
Der Zeiger lief auf dem Weg dorthin ins Leere, und nach delayDuration
klappte das Menue zu, bevor jemand einen Eintrag treffen konnte.
Jetzt mit Viewport, so wie die Komponente gedacht ist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Das Team-Submenue stand auf einem DropdownMenu, und alles, was ein
Menuepunkt braucht und ein Dropdown nicht kann, stand hier als
Handarbeit daneben: ein Zeitgeber fuers Aufklappen, ueberschriebene
Zeigerhandler gegen das Flackern beim Klick, unterdrueckter Fokus gegen
den Ring nach dem Zeigerbesuch.
NavigationMenu kann das von sich aus. Es hat openOnHover samt
delayDuration, es haelt seinen eigenen onclick zurueck, wenn per Zeiger
aufgeklappt wurde ("if opened via pointer move, we prevent the click
event"), es faengt keinen Fokus und sperrt kein Scrollen. Der Trigger
bleibt ueber das child-Snippet ein Link auf die Teamseite.
viewport={false}: Das Panel haengt unter seinem Menuepunkt, statt als
animierte Flaeche ueber die ganze Leiste zu fahren. Bei einem einzigen
aufklappbaren Punkt waere das grosses Kino ohne Stueck.
Das mobile Menue bekommt die Teams stattdessen eingerueckt und
aufgeklappt untergestellt. Auf dem Touchscreen gibt es kein
Ueberfahren — mit dem Menue allein waere seit dem Wegfall der Liste
"Meine Teams" gar kein Wechsel mehr moeglich gewesen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
bits-ui gibt den Fokus beim Schliessen an den Trigger zurueck. Bei einem
Menue, das per Klick aufgeht, ist das richtig — hier schliesst der
Zeiger, und zurueck blieb ein Fokusring auf einem Menuepunkt, ueber den
jemand nur gefahren ist.
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
Bis zuletzt loeschte die Anwendung einen Fahrer, ohne sein Konto aus
teams.users zu nehmen. Zurueck blieb ein Konto ohne Fahrer — im Kader
eine Zeile, aus der kein Weg zurueckfuehrte: Ein bestehendes Konto
nachtraeglich an einen Fahrer zu haengen gibt es bewusst nicht, weil
dieselbe Person sonst zweimal in der Liste stehen koennte.
riders.remove nimmt die Mitgliedschaft inzwischen mit; diese Migration
raeumt auf, was vorher liegengeblieben ist, und stellt die Regel her,
auf die sich die Oberflaeche verlassen darf: Wer im Team ist, steht im
Kader.
Kein Rueckweg: Die hier angelegten Fahrer sind hinterher nicht mehr von
denen zu unterscheiden, die die Teamleitung selbst angelegt hat.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Das Bild steht links ueber Name und Adresse und ist zugleich der Knopf:
Das Dateifeld liegt unsichtbar darunter, angeklickt wird das Bild. Ein
Label "Bild" ueber einem Dateifeld sagte nichts, was das Bild nicht
schon zeigt. Dazu eine Vorschau der Wahl — ohne sie bliebe sie
unsichtbar, weil es keinen Dateinamen mehr zu lesen gibt.
Loeschen des eigenen Kontos erlaubt users.deleteRule laengst
(id = @request.auth.id); es fehlte nur der Weg dorthin. Die Fahrer
bleiben stehen: riders.user haengt ohne Kaskade daran, also behaelt das
Team Namen, Zeiten und Teilnahmen und die Person verliert nur ihr Login.
Wer ein Team besitzt, kommt nicht durch. teams.owner ist ein Pflichtfeld
ohne Kaskade; PocketBase muesste es beim Loeschen leeren und braeche
dabei den Datensatz. Der Versuch endete in einem Fehler, den niemand
deuten kann — deshalb steht die Bedingung vorne und im Klartext.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Die Karte bringt py-4 mit; Inhalt und Knopfzeile brachten es ein zweites
Mal. Unter dem letzten Knopf standen dadurch 32 statt 16 px.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Das Teamdropdown stand als eigenes Bedienelement neben dem Namen — ein
zweiter Schalter fuer eine Sache, die schon einen Menuepunkt hat. Jetzt
klappt der Menuepunkt Team beim Ueberfahren auf: die eigenen Teams, ein
Klick wechselt und oeffnet die Teamseite, darunter "Team erstellen".
Die Zeiger- und Tastenhandler von bits-ui werden am Trigger bewusst
ueberschrieben. Deren onclick ruft preventDefault und macht den Link
tot, ihr pointerdown klappt zu, was der Hover gerade aufgeklappt hat —
zwei Herren ueber dasselbe open, das Ergebnis war Flackern statt
Navigation. Das Menue verzichtet zudem auf Fokusfang und Scrollsperre;
beides gehoert zu einem Dialog, nicht zu etwas, ueber das der Zeiger nur
streift.
Der Anlegedialog liegt jetzt als CreateTeamDialog vor: Es gibt zwei
Einstiege, und ein neues Team muss an beiden dasselbe bedeuten — Team
anlegen, Standardsatz Flag-Typen dazu, ab dann ist es das aktive.
Das Logo ragt mit -my-3 leicht ueber die Kopfleiste hinaus, verlinkt
aufs Dashboard und teilt sich den Hover mit dessen Menuepunkt. Der
aktive Menuepunkt traegt das Primary-Design.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Frueher lag beides in den Einstellungen und ein blasses Verzeichnis
zusaetzlich unter "Fahrer". Wer einen Fahrer anlegen wollte, landete
erst im Verzeichnis, dann in den Einstellungen. Jetzt gibt es einen Ort,
und die Verwaltungsknoepfe stehen an den Zeilen, zu denen sie gehoeren.
Der Einladungslink sitzt im Kopf der Seite statt in einer eigenen
Section; die ausgegebenen Links stehen mit im Dialog, weil sie sonst mit
der Section auch das Kopieren und das Zuruecknehmen verloren haetten.
Die Adminrolle wandert aus der Tabellenzeile in den Personendialog. Ein
Knopf, der ohne Rueckfrage Rechte vergibt, gehoert nicht neben die Icons
fuers Entfernen und Loeschen. Der Bearbeiten-Knopf steht dafuer an jeder
Zeile, auch an Konten ohne Fahrer — sonst waere deren Rolle nirgends
mehr erreichbar.
Die Liste "Meine Teams" entfaellt: Wechseln und Anlegen stehen im
Submenue am Menuepunkt Team. Verlassen gilt nur dem aktiven Team und
steht deshalb jetzt im Kopf.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Vorname und Nachname getrennt zu fuehren versprach eine Ordnung, die es
hier nie gab: sortiert wird ueber den ganzen Namen, angezeigt wird der
ganze Name, gesucht wird ueber beides zugleich. Dafuer musste jede Maske
zwei Felder anbieten und jede Anzeige sie wieder zusammensetzen — und
wer schlicht "Schorsch" heisst, stand vor der Frage, welches der beiden
Felder das ist.
Das Feld name gibt es an riders laengst; es lag nur brach, weil die
Anwendung firstname und lastname benutzte. Es uebernimmt deren Inhalt —
kein neues Feld, keine zweite Spalte, nichts, was auf bestehenden
Instanzen erst entstehen muesste.
Zurueck geht es nur ungenau: Die Umkehrung trennt am ersten Leerzeichen
und raet damit bei jedem Doppelvornamen falsch. Das ist der Preis dafuer,
dass die Trennung ueberhaupt verschwindet.
fullName heisst jetzt riderName und ist nur noch ein Feldzugriff — die
Funktion bleibt als die eine Stelle, an der ein namenloser Fahrer zu
einem leeren String wird statt als undefined durch die Oberflaeche zu
geistern.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Damit eine frische Instanz die Collection direkt aus dem Snapshot
bekommt und die Typen aus derselben versionierten Quelle entstehen wie
das Schema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Sie standen in einer eigenen, abgetrennten Zeile unter Laenge und
Hoehenmetern, obwohl sie dasselbe sind: eine Zahl zum Trail. Jetzt eine
Zeile, ganz rechts die offenen Meldungen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Ein Datum muss man erst gegen den heutigen Tag rechnen, um zu wissen, ob
eine Sperrung von gestern stammt oder vom letzten Sommer — der Abstand
sagt es sofort. Das genaue Datum steht daneben im Tooltip und geht
dadurch nicht verloren.
date-fns lag bereits im Projekt, es brauchte keine neue Library.
parseDate nimmt PocketBases "2026-09-12 08:30:00.000Z" mit Leerzeichen
statt T; die meisten Browser schlucken das, garantiert ist es nicht.
formatDate auf der Trail-Seite war eine Kopie von formatDateTime und
kommt jetzt aus $lib/time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Tailwind 4 nimmt Knoepfen den Zeigefinger und ueberlaesst sie dem
Browser-Standard — was am Ende ein Pfeil ist, der nichts verspricht.
Eine Basisregel statt einer Klasse an jedem einzelnen Element.
Abgeschaltete Elemente behalten den Pfeil.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
CARTO liefert die Kacheln inzwischen mit einem eingebrannten
"API KEY REQUIRED" quer ueber der Karte — und zwar mit HTTP 200,
weshalb es wie ein Darstellungsfehler aussah und nicht wie eine Sperre.
Der Schluessel aus PUBLIC_CARTO_API_KEY aenderte daran nichts: Die
Kachel kam byteidentisch zurueck wie ohne ihn, als key=, api_key= und
apikey=.
OpenFreeMap braucht keinen Schluessel und kennt kein Limit; die Stile
heissen dort positron und dark und sind genau die, die CARTO als
Positron und Dark Matter gepraegt hat.
Weil das Vektorkacheln sind, tauscht der Themenwechsel jetzt den ganzen
Stil statt nur der Kachel-URLs. Ein Stilwechsel wirft Quellen und Layer
weg, deshalb legt style.load die Trail-Linie neu an — die Marker nicht,
die haengen als DOM-Elemente an der Karte und nicht am Stil.
PUBLIC_CARTO_API_KEY wird nicht mehr gelesen und kann aus .env und der
Deployment-Konfiguration verschwinden.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Stollenreifen, Felge, gekreuzte Speichen, Nabe — gezeichnet in
currentColor und groessengesteuert wie ein Lucide-Icon, damit es den
LoaderCircle ueberall ersetzen kann.
Das Ventil sitzt bewusst nur an einer Stelle: Ohne diese Unwucht sieht
ein achtfach symmetrisches Rad im Stillstand aus, egal wie schnell es
sich dreht.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Bisher legte die Teamleitung jedem Fahrer von Hand ein Konto an und gab
das Passwort weiter. Bei zwanzig Leuten in einer WhatsApp-Gruppe ist ein
Link die kuerzere Strecke.
Die oeffentliche Seite der Einladung laeuft ueber eigene Routen in
pb_hooks, nicht ueber die Collection: Ohne Login ist dort nichts
sichtbar, auch nicht mit Token. Der Token kommt vom Server, ein
mitgeschickter wird abgewiesen.
Dazu der Login-Vorspann vor jeder Regel, die @request.auth.id erwaehnt.
Das ist ein Loch und keine Kosmetik: Eine leere Relation ist in
PocketBase gleich dem leeren @request.auth.id einer anonymen Anfrage —
ein Team ohne Admins stuende sonst offen im Netz.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
Die Sidebar fuehrt ohnehin ueberall hin, der Schnellzugriff war eine
zweite Navigation ohne Mehrwert.
Die Kacheln unter "Heute und demnaechst" sind jetzt vollflaechig
verlinkt statt ueber einen Button darin -- dasselbe Muster wie in der
Event- und der Trailliste.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Ah9PvkL2xB7YkuadnfHRz
Der Pfad hiess noch "Einstellungen -> Fahrer -> Konto"; der Reiter heisst
seit e3addda "Fahrer & Konten". Dazu zwei Punkte, die beim Zusammenlegen
entstanden sind: dass die Regel "jedes Teammitglied ist ein Fahrer" nur in
der Oberflaeche gilt, und dass "Als Fahrer uebernehmen" den Namen raet.
Migrationsstand erneut gegen die produktive Instanz geprueft - unveraendert
fuenf offen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Dieselben Menschen wurden an zwei Stellen gepflegt: Mitglieder unter Team,
Fahrer unter Fahrer - und die Zuordnung dazwischen von Hand. Jedes
Teammitglied ist aber ein Fahrer; nur hat nicht jeder Fahrer ein Konto.
"Fahrer & Konten" fuehrt beides in einer Liste: Kader, daneben wer sich
anmelden kann und mit welcher Rolle. Angelegt wird eine Person als Fahrer,
das Konto ist eine Checkbox im selben Dialog - mit E-Mail, sobald sie
gesetzt ist. Rollen, Entfernen aus dem Team und die Kontosuche wandern
ebenfalls hierher.
Konten im Team ohne Fahrer bekommen eine eigene Zeile samt "Als Fahrer
uebernehmen". Sie verschwaenden sonst aus der Ansicht, obwohl sie Mitglied
sind.
"Team" bleibt als Uebersicht: Kennzahlen des aktiven Teams, Name, Logo,
Wechseln, Verlassen, Loeschen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Sechs der elf Migrationen sind laengst gelaufen - der Backend-Deploy hat
alles bis 1754500900 mitgenommen. Die Datei behauptete weiter, es stehe
alles aus, samt eines Symptoms ("Only superusers can perform this action"),
das es so nicht mehr gibt.
Die Tabelle sagt jetzt je Migration, was gelaufen ist und was nicht, und
benennt, welche Funktionen bis zum naechsten Deploy ins Leere laufen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Damit sind alle offenen Punkte aus TODO.md gebaut.
Fahrer gehoeren ab jetzt dem Team, nicht mehr einem Event. Wer bei einem
Termin dabei ist, steht in event_participants - dort haengt auch die
Startnummer, denn die gilt je Event: Derselbe Fahrer kann in zwei Rennen
zwei Nummern haben und beim Stammtisch gar keine. Die vorhandenen Daten
wandern in der Migration mit.
Events tragen zwei Schalter. timing blendet Runs und Zeitnahme ein - der
woechentliche Stammtisch braucht sie nicht. participation regelt, wie man
dazukommt: offen (jeder traegt sich selbst ein), bestaetigung (Anmeldung,
Ersteller oder Team-Admin bestaetigt) oder geschlossen. Durchgesetzt wird
das serverseitig, nicht nur im Formular.
Serien erzeugen echte Einzeltermine statt einer Wiederholungsregel, aus der
Termine nur gerechnet werden. Nur so laesst sich beantworten, wer am 12.
kommt, und nur so faellt eine einzelne Woche aus. Gerechnet wird auf
Kalendertagen: ueber die Zeitumstellung hinweg kippte ein Termin sonst auf
den Vortag - dafuer gibt es einen Test.
Ausserdem: Teammitglieder duerfen den Zustand eines Trails melden. Die Regel
gibt genau die drei Statusfelder frei und schliesst jedes andere aus; ohne
das haette man Mitgliedern das ganze Objekt oeffnen muessen. Und bestehende
Konten lassen sich wieder per E-Mail ins Team holen, wofuer users.listRule
sich oeffnet.
Die neuen Regeln sind ungetestet - weder Docker noch eine lokale
PocketBase-Instanz standen zur Verfuegung. TODO.md sagt, was nach dem Deploy
durchzuspielen ist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
title war fuer Hinweise die falsche Wahl: Es erscheint erst nach einer
Sekunde, sieht auf jedem System anders aus und laesst sich auf Touch-Geraeten
gar nicht aufrufen. Tippy war laengst als Abhaengigkeit da, aber kaum
benutzt.
<Button> nimmt jetzt eine tooltip-Prop, die zugleich als aria-label dient -
ohne das haetten die vielen Icon-Knoepfe beim Wegfall von title ihren Namen
fuer Screenreader verloren. Alle anderen Elemente nutzen die Action direkt.
Die Action selbst konnte bisher nur erzeugen. Sie beherrscht jetzt
Aktualisieren und Aufraeumen: Ein wechselnder Text ("Erledigt" / "Wieder
oeffnen") muss auch im Tooltip wechseln, und eine Instanz, die ihr Element
ueberlebt, haenge als leere Blase im Dokument.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
teams.logo nimmt eine Datei auf; hochgeladen wird sie in den
Team-Einstellungen. Ohne eigenes Logo bleibt das Wappen aus static/ als
Rueckfalloption - fuer den Stammtisch Hersbruck aendert sich damit nichts,
fuer jedes weitere Team waere es fremdes Vereinswappen gewesen.
Dasselbe Logo liegt gedreht und sehr blass in der rechten unteren Ecke der
Seite. Es haengt fest im Bildschirm, faengt keine Klicks ab und ist fuer
Screenreader nicht vorhanden; im Dunkelmodus etwas kraeftiger, sonst
verschwaende es im dunklen Grund.
In TODO.md ausserdem der Plan fuer Ausfahrten: Sie werden kein zweites
Konzept neben Events, sondern Schalter daran - timing, participation
(offen/bestaetigung/geschlossen) und Serien, die echte Einzeltermine
erzeugen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Laenge und Hoehenmeter tragen dieselben Icons wie der Kopf der Trail-Seite.
Dazu kommen Anzahl der Marker und Kommentare - und ein Punkt daneben, wenn
davon etwas aus den letzten 14 Tagen stammt. Die blosse Zahl sagt nichts
darueber, ob sich gerade etwas tut.
Kommentare bekommen dafuer einen eigenen Store. Sie lagen bisher direkt in
der Trail-Seite, was richtig war, solange es sie nur dort gab; fuer die
Liste waere ein zweiter Ladeweg auf dieselben Daten einer zu viel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Die Rolle superadmin kommt als Feld an users, der erste ist
stammtisch@dne.name. Vergeben wird sie nur in PocketBase: Die updateRule
schliesst das Feld ueber @request.body.superadmin:isset = false aus, sonst
koennte sich jeder selbst befoerdern - jeder darf schliesslich seinen
eigenen Datensatz aendern. Besondere Rechte in der App haengen noch nicht
daran.
Flag-Typen bleiben ausdruecklich Sache der Teams. Ein neues Team bekommt
beim Anlegen den hartkodierten Standardsatz aus der App und kann ihn danach
frei aendern; eine appweite Verwaltung entfaellt damit. Ein Team ohne
Flag-Typen koennte zwar Marker aufnehmen, aber nichts einordnen.
Im Kopf stehen Avatar und Name links, Design-Umschalter und Logout rechts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
"Only superusers can perform this action" beim Anlegen eines Mitglieds ist
kein Fehler im Frontend, sondern die unveraenderte users.createRule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Ohne hinterlegtes Bild stehen Initialen auf einem Farbton, der sich aus der
User-ID errechnet: Derselbe Mensch bekommt ueberall dasselbe Bild, ohne dass
es irgendwo gespeichert werden muss. Bewusst kein externer Avatar-Dienst -
der wuerde bei jedem Seitenaufruf verraten, wer hier unterwegs ist.
Kommentare zeigen jetzt ihren Verfasser, aber nur als Bild; der Name steht
im Tooltip. In einer schmalen Spalte neben der Karte ist fuer mehr kein
Platz.
Verwendet wird der Avatar ausserdem in der Kopfzeile, im Profil und in der
Mitgliederliste.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Auf dem Trail passt nicht immer ein vorgefertigter Typ - "Wespennest am
Anlieger" legt niemand vorher an. Der Marker-Dialog fuehrt deshalb ein
Dropdown, dessen erste Zeile "Eigener Typ" ist und ein Textfeld oeffnet.
Das Icon steht ab jetzt am Marker selbst. Ein gewaehlter Typ schlaegt seines
vor, ueberschreiben laesst es sich jederzeit: Dieselbe Art Hindernis sieht
nicht immer gleich aus. Der Picker ist deshalb immer sichtbar und wandert
als eigene Komponente auch in die Flag-Verwaltung.
Schema: trail_markers.flag ist nicht mehr Pflicht, dafuer gibt es label und
icon. Bestehende Marker bleiben gueltig - ohne eigene Angaben faellt die
Anzeige auf den Typ zurueck.
Damit braucht ein Trail auch keine Flag-Typen mehr, um ueberhaupt Meldungen
aufnehmen zu koennen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Vier Entscheidungen stehen aus (Rollenmodell mit Superadmin, appweite
Flag-Typen, Melderecht fuer den Trail-Status, Kader statt Event-Fahrer) und
drei Dinge passieren ausserhalb des Repos: PUBLIC_CARTO_API_KEY in Coolify,
die drei neuen Migrationen samt Datenverlust bei events.status, und einmal
Typen neu erzeugen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Der Kopf traegt jetzt Name, Zustand und Kennzahlen: Der Status sitzt als
farbiges Dropdown direkt hinter dem Namen und nennt darunter, wann und von
wem er gemeldet wurde. Laenge und Hoehenmeter stehen mit Icons daneben.
Karte und Hoehenprofil links, Marker, Kommentare und Versionen rechts
daneben in einer Spalte. Die Reiter entfallen - alles ist gleichzeitig zu
sehen. Versionen sind Archiv und deshalb eingeklappt; der GPX-Upload sitzt
dort statt im Kopf.
Marker lassen sich jetzt auch loeschen und auch ueber das Hoehenprofil
setzen: Die angeklickte Stelle wird auf dieselbe Linie zurueckgerechnet,
die ein Klick auf der Karte trifft.
Die Flag-Icons wurden bisher gespeichert, aber nirgends gezeichnet. Sie
erscheinen jetzt in der Karte, im Popup, in der Markerliste und in der
Verwaltung. Erlaubt ist jeder Name von lucide.dev - moeglich macht das ein
Glob ueber die Icon-Dateien, der jedes Icon zu einem eigenen, erst bei
Bedarf geladenen Chunk macht. Die Liste im Dialog sind nur Vorschlaege.
Die Karte kommt von CARTO statt von OSM direkt: zurueckhaltend gezeichnet,
sodass die Trail-Linie darueber steht, und mit einer echten dunklen
Fassung. Das vorherige Abdunkeln der OSM-Kacheln ergab nur ein dunkleres
Bild, keine dunkle Karte. In der Kachelvorschau der Liste sind Zoom,
Massstab und Quellenangabe abgeschaltet - dort ist die Karte Bild, nicht
Werkzeug; die grosse Karte traegt die Angabe weiterhin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Die Einstellungen bekommen Reiter: Profil, Team, Fahrer (nur Teamleitung)
und Flag-Typen. /dashboard/teams wandert dorthin.
Neu ist die Hauptroute Fahrer - das Verzeichnis des Teams, fuer alle
sichtbar und fuer niemanden dort aenderbar. Angelegt, geaendert und mit
einem Konto verknuepft wird in den Einstellungen; serverseitig setzen das
die neuen Regeln der Collection durch.
Ein Fahrer bekommt sein Login entweder aus den vorhandenen Teammitgliedern
oder direkt neu angelegt. Die Einladung per E-Mail-Suche entfaellt: Sie
konnte nie funktionieren, weil users.listRule nur den eigenen Datensatz
freigibt - daher auch die abgeschnittenen IDs in der alten Mitgliederliste.
Events zeigen statt eines Status ihr Datum, mit Checkbox fuer mehrtaegig.
Gespeichert wird ein Enddatum nur, wenn es sich vom Beginn unterscheidet -
es ist damit zugleich das Kennzeichen fuer mehrtaegig statt einer zweiten
Wahrheit ueber dieselbe Sache. Listen gruppieren nach laufend, anstehend,
ohne Termin und vorbei.
Im Kopf teilen sich Name, Design-Umschalter und Logout eine Zeile, das
Team-Dropdown sitzt darunter; das Logo passt jetzt in die Leiste, statt
beschnitten zu wirken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Drei Migrationen, dazu der Snapshot in 1754400000_init_schema.js, aus dem
die TypeScript-Typen erzeugt werden - beide muessen zusammenpassen, sonst
laufen Schema und Typen auseinander.
riders.user verknuepft einen Fahrer mit einem Login, ohne cascadeDelete:
Ein geloeschtes Konto darf keine Zeiten mitreissen. Anlegen, Aendern und
Loeschen von Fahrern wird zur Sache der Teamleitung; bisher durfte das
jedes Teammitglied.
users.viewRule von "nur ich selbst" auf "eingeloggt" - ohne das laesst sich
zu einer bekannten ID kein Name anzeigen, was Mitgliederliste,
Fahrerkonten und den Melder eines Trail-Markers betrifft. Die listRule
bleibt eng, E-Mails bleiben ueber emailVisibility verborgen. users.createRule
erlaubt eingeloggten Nutzern das Anlegen von Konten, nicht der ganzen Welt.
events verliert status und bekommt starts/ends. Der Status war Handarbeit:
Wer vergass, ein Event abzuschliessen, hatte eine falsche Uebersicht. Ein
Datum weiss von selbst, was ansteht, laeuft und vorbei ist. ACHTUNG: Die
Migration entfernt das Feld samt seiner Werte.
trails bekommt status_changed und status_by. Einer Sperrung soll man
ansehen, ob sie von heute Morgen oder vom letzten Herbst ist.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Start und Stopp laufen ausschliesslich ueber TimeDialog. Der fuehrt immer
beide Angaben - Fahrer und Run -, auch wenn eine davon aus dem Kontext
vorbelegt ist: Beim Stoppen an der Strecke soll kein Zweifel bleiben, wen
man gerade erwischt.
DNF fragt nach, bevor es schreibt. Eine Zeit ohne Wertung laesst sich nicht
zurueckholen, ein Fehlgriff waere also endgueltig.
Tabelle und Toast-Leiste zeigen damit nur noch an; ihre Knoepfe oeffnen den
Dialog mit passender Vorbelegung. Der alte Stopp-Dialog und der
Fahrerlisten-Start entfallen - beides waren Ein-Tipp-Aktionen und damit
genau das, was hier nicht mehr sein soll.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
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 Trail-Linie ist jetzt selbst die Bedienflaeche: Eine unsichtbare breite
Kopie der Linie faengt Hover und Klick ab, deshalb kann daneben gar nichts
mehr passieren. Geklickte Punkte werden auf die Linie projiziert, der
Setzmodus samt Button entfaellt.
Beim Ueberfahren wandert der Punkt in Karte und Hoehenprofil synchron mit,
inklusive Anzeige von Hoehe und Streckenkilometer. Die Kopplung laeuft in
beide Richtungen ueber die Distanz und interpoliert zwischen den
Stuetzpunkten, statt zum naechsten Vertex zu springen.
Ein Klick auf einen vorhandenen Marker oeffnet kein Setzen-Fenster mehr,
sondern ein Popup mit Typ, Notiz, Zeitpunkt und Melder. Fremde User-Namen
bleiben vorerst verborgen, weil users.listRule nur den eigenen Datensatz
freigibt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
Der vorige Fix wirkte nur im Dev-Modus. Live blieb die Trail-Linie aus,
obwohl das Worker-Asset ausgeliefert wurde (HTTP 200).
Ursache: Das per ?url eingebundene Asset ist nur eine Rohkopie der
Paketdatei. Deren erste Zeile ist ein relativer Import auf die
Nachbardatei:
import{...}from"./maplibre-gl-shared.mjs"
Diese Datei kopiert der Build nicht mit — unter
_app/immutable/assets/maplibre-gl-shared.mjs antwortet die Produktion mit
404. Der Worker startet dadurch, sein Import scheitert, und er antwortet auf
keine Nachricht mehr. GeoJSON-Quellen bleiben im Ladezustand, es wird keine
Linie gezeichnet. Im Dev-Modus fiel das nicht auf, weil Vite dort aus
node_modules aufloest, wo die shared-Datei danebenliegt.
?worker&url statt ?url laesst Vite den Worker samt Abhaengigkeiten zu einer
eigenstaendigen Datei buendeln.
Verifiziert am Build-Artefakt: Der Worker liegt nun unter
_app/immutable/workers/ und ist 469695 statt 19122 Bytes gross, enthaelt also
den shared-Code; eine Referenz auf maplibre-gl-shared existiert nicht mehr,
womit der 404 zur Laufzeit entfaellt. Ueber "node build" ausgeliefert
antwortet er mit HTTP 200 in voller Groesse. svelte-check ohne Fehler, alle
19 Tests bestehen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Karte zeigte Kacheln und stand am richtigen Ort, zeichnete aber weder in
der Trail-Uebersicht noch auf der Detailseite eine Linie. Das Hoehenprofil
aus denselben Versionsdaten wurde dagegen gerendert.
Ursache: MapLibre 6 liegt als drei Dateien vor (Haupteinstiegspunkt, shared,
worker) und laedt den Worker zur Laufzeit ueber import.meta.url aus dem
eigenen Verzeichnis nach. Vites Dependency-Optimierung buendelt jedoch nur
den Haupteinstiegspunkt nach node_modules/.vite/deps — daneben liegt keine
maplibre-gl-worker.mjs. Der Worker startet dadurch zwar und ist verbunden,
sein Import laeuft aber ins Leere: Er antwortet auf keine Nachricht mehr.
GeoJSON-Quellen bleiben deshalb dauerhaft im Ladezustand
(isSourceLoaded false, keine Source-Features, _updatePromise loest nie auf),
und es gibt nichts zu zeichnen. Rasterkacheln brauchen keinen Worker, deshalb
erschien die Karte trotzdem und der Fehler sah nach fehlenden Daten aus.
setWorkerUrl mit einem ?url-Import zeigt auf die echte Datei und umgeht das,
ohne in Vites Optimierung einzugreifen. Ein optimizeDeps.exclude waere der
naheliegendere Weg gewesen, bricht hier aber die Modulaufloesung von
SvelteKit.
Verifiziert: Linie erscheint in Uebersicht und Detailseite; die Daten waren
durchgehend intakt (LineString, 225 Koordinaten). svelte-check ohne Fehler,
alle 19 Tests bestehen, npm run build erfolgreich, und die Worker-Datei liegt
als Asset im Produktionsbuild und wird vom Client-Bundle referenziert.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
npm ci brach beim Deployment mit EBADENGINE ab: jsdom 30 verlangt
^22.22.2 || ^24.15.0 || >=26.0.0, Nixpacks liefert aber Node 24.10.0.
24.10 ist aelter als 24.15 und faellt damit durch; zusammen mit
engine-strict=true in .npmrc bricht die Installation hart ab.
Anders als bei frueheren Node-Problemen liegt es nicht an unserer eigenen
engines-Angabe (^20.19 || ^22.12 || >=24) — die ist erfuellt. Die
Anforderung kommt allein aus jsdom, und zwar erst seit Version 30; jsdom 29
verlangt nur >=24.0.0. Ein NIXPACKS_NODE_VERSION haette nicht geholfen, weil
Nixpacks nur Major-Versionen aufloest und damit ohnehin bei 24.10 landet.
jsdom ist eine devDependency und wird ausschliesslich als Testumgebung fuer
Vitest genutzt (vite.config.ts). Fuer den Produktionsbuild ist sie
irrelevant, wird aber mitinstalliert, weil Nixpacks NPM_CONFIG_PRODUCTION
auf false setzt.
Verifiziert: npm ci laeuft in einem node:24.10-slim-Container durch, also
auf exakt der Version, an der das Deployment gescheitert ist. Alle 19 Tests
in src/lib/gpx.test.ts bestehen unter jsdom 29.1.1, npm run build erzeugt
das build-Verzeichnis.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Pin stand auf 0.39.6, aktuelles Release ist 0.39.11. Reine Patch-Updates
innerhalb 0.39, keine Schema- oder API-Aenderungen, aber sicherheitsrelevant:
0.39.7 ersetzt ozzo-validation durch einen eigenen Fork, nachdem die
Originalbibliothek den Besitzer gewechselt hat; 0.39.8 und 0.39.11 ziehen
Sicherheitsfixes der golang.org/x-Abhaengigkeiten nach.
Version an beiden aktiven Stellen gesetzt (Dockerfile-ARG und der Default in
docker-compose.yaml). Die Planungsdokumente unter docs/ bleiben unangetastet,
sie halten den Stand ihrer Entstehung fest.
.env.example um den Deployment-Fall ergaenzt: Beim Deployment gibt es keine
.env-Datei, dieselben Variablen werden in Coolify als Environment Variables
gesetzt. Ausserdem der Hinweis, dass SUPERUSER_* nur greift, solange noch kein
Superuser existiert — nachtraegliche Aenderungen wirken nicht mehr.
Verifiziert: Image gebaut, Binary meldet 0.39.11, alle Migrationen laufen
gegen eine frische Datenbank durch, alle elf Collections vorhanden,
Team-Anlage als normaler User erfolgreich.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>