diff --git a/CLAUDE.md b/CLAUDE.md
index 6139626..4f7a888 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -60,7 +60,20 @@ docker compose down
Container und müssen mit `docker compose cp` ins Repo geholt werden; siehe
`backend/README.md`.
- **Type-safe client**: The PocketBase client is typed using auto-generated types in `frontend/src/lib/types.d.ts`
-- **Collections**: users, teams, events, runs, riders, times
+- **Collections**: users, teams, team_invites, events, runs, riders, times,
+ trails, trail_versions, trail_flags, trail_markers, trail_comments,
+ event_participants, event_series
+- **Regeln**: Jede Zugriffsregel, die `@request.auth.id` erwähnt, beginnt mit
+ `@request.auth.id != "" && (…)`. Ohne diesen Vorspann ist eine *leere*
+ Relation gleich dem leeren `@request.auth.id` einer anonymen Anfrage — ein
+ Team ohne Admins stünde damit offen im Netz. Siehe
+ `backend/pb_migrations/1754501700_rules_require_login.js`.
+- **Hooks**: `backend/pb_hooks/` enthält serverseitiges JS für das, was keine
+ Collection-Regel abbilden kann — derzeit die öffentlichen Routen der
+ Einladungslinks. Jeder Route-Handler läuft in einer eigenen JS-Laufzeit;
+ gemeinsamer Code gehört in ein Modul und wird per
+ ``require(`${__hooks}/…`)`` im Handler geholt. Details in
+ `backend/README.md`.
- **Authentication**: Handled through `src/lib/stores/pocketbase.svelte.ts` with the `AuthStore` class
- **File handling**: Use `getFileURL(record, file, options)` helper for PocketBase file URLs
@@ -136,7 +149,18 @@ tippy.js, which the action initializes, updates and destroys.
### File Structure Notes
-- Routes are in `src/routes/` following SvelteKit conventions
+- Routes are in `src/routes/` following SvelteKit conventions. **Routennamen
+ immer auf Englisch** (`/invite/[token]`, nicht `/einladung/[token]`) —
+ Oberflächentexte sind deutsch, Pfade nicht.
+- Der Menüpunkt **Team** (`/dashboard/team`) führt Team und Kader an einer
+ Stelle: Kennzahlen, Teamdaten, Einladungslinks und die Fahrerliste samt der
+ Verwaltungsknöpfe der Teamleitung. Unter **Einstellungen** steht nur noch,
+ was einen selbst betrifft (Profil, Flag-Typen).
+- **Ein Fahrer, ein Name, eine Richtung**: Ein Konto entsteht immer am Fahrer
+ (`riders.createLogin`), es gibt kein Verknüpfen bestehender Konten und kein
+ Lösen. Angezeigt wird ausschließlich der Fahrername — der Kontoname wäre
+ eine zweite Wahrheit, die niemand geradeziehen könnte, weil
+ `users.updateRule` nur den Kontoinhaber selbst ändern lässt.
- Reusable components in `src/lib/components/`
- Stores use `.svelte.ts` extension for Svelte 5 runes
- Static assets in `static/`
diff --git a/NAMING.md b/NAMING.md
new file mode 100644
index 0000000..faf414f
--- /dev/null
+++ b/NAMING.md
@@ -0,0 +1,31 @@
+Die "Wanderer"-Schiene
+
+ Shredder: Ziemlich actiongeladen, 100% MTB-Jargon und hat genau wie "Wanderer" dieses aktive, ausführende Element.
+
+ Kurbler: Die direktere, etwas rotzigere deutsche Alternative. Hat einen leichten Retro-Charme.
+
+ Dropper: Angelehnt an die Dropper-Post (absenkbare Sattelstütze) oder das "Reindroppen" in den Trail.
+
+Fokus auf Trail & Community
+
+ TrailTribe: Betont den Vereins- und Gruppenaspekt hervorragend.
+
+ DirtBase: Perfekt für die Traildatenbank – kurz, prägnant und technisch sauber.
+
+ FlowHub: Verbindet das ultimative MTB-Gefühl ("Flow") mit dem modernen Plattform-Gedanken für Eventveranstalter.
+
+Fokus auf Zeitnahme & Action
+
+ SendSync: "Send it!" gepaart mit der schnellen Datensynchronisation einer Webapp.
+
+ PacePeak: Klingt sportlich, kompetitiv und extrem passend für Rennveranstalter und Bestzeitenjäger.
+
+ TrailTicker: Etwas verspielter, bringt aber das Thema Zeitmessung und Live-Updates sofort auf den Punkt.
+
+Kurz & Knackig (Startup-Vibe)
+
+ Berm: Englisch für die Anliegerkurve. Kurz, international und unter Mountainbikern ein fester Begriff.
+
+ Roost: Der aufgeworfene Dreck in der Kurve. Hat Energie und ist sehr spezifisch für den Offroad-Sport.
+
+ Stollen: Ein sehr deutsches, geerdetes Wort (Stollenreifen), das MTB-Flair mit technischer Präzision verbindet.
diff --git a/TODO.md b/TODO.md
index 467d0c5..e69de29 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,118 +0,0 @@
-# Offene Punkte
-
-Stand: 2026-09-02, abgeglichen mit der produktiven Instanz. Die fachlichen
-Entscheidungen sind gefallen und gebaut; was hier steht, passiert außerhalb des
-Repos oder wartet auf den nächsten Deploy.
-
-## Vor dem nächsten Deploy
-
-### `PUBLIC_CARTO_API_KEY` in Coolify setzen
-
-Die Kartenkacheln kommen von CARTO. Der Schlüssel muss den Browser erreichen,
-deshalb das `PUBLIC_`-Präfix — ohne das reicht SvelteKit ihn nicht weiter.
-
-Gelesen wird er über `$env/dynamic/public`, das heißt: Die Variable muss in der
-**Laufzeitumgebung** des `node build`-Prozesses stehen, nicht nur zur Buildzeit.
-In Coolify als normale Umgebungsvariable eintragen genügt. Fehlt sie, laufen die
-Kacheln ohne Schlüssel weiter — die Karte bleibt also sichtbar.
-
-### Migrationen: sechs gelaufen, fünf offen
-
-Abgeglichen mit `api.stammtisch-hersbruck.de` (nur gelesen): Der Backend-Deploy
-hat alles bis `1754500900` mitgenommen, ab `1754501000` steht es noch aus.
-
-| Datei | Was sie tut | Stand |
-|---|---|---|
-| `1754500500_riders_user_and_admin_rules` | `riders.user`, Fahrerpflege nur Teamleitung, `users.viewRule`/`createRule` | gelaufen |
-| `1754500600_events_dates_instead_of_status` | `events.status` raus, `starts`/`ends` rein | gelaufen |
-| `1754500700_trail_status_meta` | `trails.status_changed`, `status_by` | gelaufen |
-| `1754500800_trail_markers_custom` | `trail_markers.label`/`icon`, `flag` optional | gelaufen |
-| `1754500900_users_superadmin` | `users.superadmin` samt Schutz vor Selbstbeförderung | gelaufen |
-| `1754501000_teams_logo` | `teams.logo` | **offen** |
-| `1754501100_trails_status_report` | Mitglieder dürfen den Trail-Zustand melden | **offen** |
-| `1754501200_users_list_for_invites` | `users.listRule` gelockert (Kontosuche) | **offen** |
-| `1754501300_event_participants` | neue Collection, **verschiebt Daten**, entfernt `riders.event`/`number` | **offen** |
-| `1754501400_events_timing_participation` | `events.timing`/`participation` | **offen** |
-| `1754501500_event_series` | `event_series`, `events.series` | **offen** |
-
-Bis zum nächsten Deploy laufen deshalb ins Leere: Team-Logo, Zustandsmeldung
-durch Mitglieder, Kontosuche per E-Mail, Teilnehmerlisten, die Event-Schalter
-und die Serien. Die Oberfläche dafür ist da und wird beim Speichern eine
-Fehlermeldung von PocketBase zeigen.
-
-**Zwei Dinge sind beim Nachziehen nicht zurückzunehmen:**
-
-1. `1754501300` legt für jeden Fahrer mit Event einen Teilnehmer an und entfernt
- danach `riders.event` und `riders.number`. Eine Startnummer an einem Fahrer
- **ohne** Event hat danach keinen Platz mehr und geht verloren — sie war
- ohnehin nirgends sichtbar, weil alle Listen nach Event filterten.
-2. Bestehende Events werden auf `timing = true` und `participation =
- geschlossen` gesetzt. Genau so wurden sie bisher benutzt; wer einen
- Stammtisch daraus machen will, schaltet die Zeitnahme im Dialog ab.
-
-Ebenfalls bewusst so: Nach `1754501200` kann jeder Eingeloggte Konten
-auflisten, damit sich bestehende Konten per E-Mail ins Team holen lassen.
-E-Mail-Adressen bleiben über `emailVisibility` verborgen.
-
-### Typen einmal neu erzeugen
-
-`frontend/src/lib/types.d.ts` wurde bei allen Schema-Änderungen von Hand
-gepflegt. Grund: `pocketbase-typegen` braucht native sqlite3-Bindings, die in
-der Arbeitsumgebung nicht gebaut werden durften.
-
-```bash
-cd frontend && npm run generate-pocketbase-types
-```
-
-Die Datei sollte danach unverändert bleiben. Tut sie es nicht, gilt das Ergebnis
-des Generators.
-
-### Regeln einmal durchspielen
-
-Die Zugriffsregeln der neuen Collections konnten nirgends laufen — weder Docker
-noch eine lokale PocketBase-Instanz standen zur Verfügung. Sie sind nach dem
-Muster der vorhandenen Collections geschrieben, aber ungetestet. Nach dem Deploy
-einmal ausprobieren:
-
-- Ein einfaches Teammitglied trägt sich bei einem **offenen** Event selbst ein.
-- Dasselbe bei einem Event **mit Bestätigung** — es muss als „Angefragt"
- landen, nicht als „Dabei".
-- Bei einem **geschlossenen** Event darf es gar nicht gehen.
-- Ein Mitglied meldet einen Trail-Zustand (muss gehen) und versucht, den Namen
- des Trails zu ändern (darf nicht gehen).
-
-Der Selbsteintrag setzt außerdem voraus, dass das Konto der Person mit einem
-Fahrer verknüpft ist (Einstellungen → Fahrer & Konten). Ohne Verknüpfung
-erscheint der Knopf gar nicht erst.
-
-### Logo des Stammtischs zuordnen
-
-`teams.logo` gibt es ab `1754501000`. Solange nichts hinterlegt ist, zeigt die
-App weiterhin das Wappen aus `static/` — für den Stammtisch Hersbruck sieht es
-also unverändert aus. Wer es als Datei am Team haben will, lädt es nach dem
-Deploy unter Einstellungen → Team → Logo hoch.
-
-## Kleinigkeiten
-
-- Wer Regeln direkt im PocketBase-Admin ändert, hinterlässt keine Spur im Repo.
- Die Migrationen bleiben die Quelle: Sie setzen dieselben Werte noch einmal und
- überschreiben eine abweichende Handänderung stillschweigend.
-- Die Rolle **Superadmin** ist gesetzt, gibt in der App aber noch keine
- besonderen Rechte. Sie ist bewusst nur in PocketBase vergebbar: Die
- `updateRule` der users-Collection schließt `superadmin` vom Selbstsetzen aus.
- Sobald klar ist, was sie dürfen soll, kommen die Regeln dazu.
-- Flag-Typen sind **teamspezifisch**. Ein neues Team bekommt beim Anlegen den
- hartkodierten Standardsatz aus `trailFlags.svelte.ts` und kann ihn frei
- ändern.
-- „Jedes Teammitglied ist ein Fahrer" hält die Oberfläche ein, das Backend
- erzwingt es nicht: Ein Konto kann im Team sein, ohne an einem Fahrer zu
- hängen. Solche Fälle stehen unter „Fahrer & Konten" mit dem Vermerk „kein
- Fahrer" und lassen sich per Knopf übernehmen.
-- „Als Fahrer übernehmen" rät den Namen aus dem Kontonamen und setzt ihn als
- Vornamen. „Max Mustermann" auf zwei Felder aufzuteilen wäre geraten und läge
- bei Doppelnamen daneben; der Name lässt sich danach korrigieren. Wenn das zu
- grob ist, müsste der Knopf stattdessen nach Vor- und Nachname fragen.
-- Serien erzeugen bis zu zwölf Termine je Klick und überspringen, was schon
- angelegt ist. Ein Termin, der ausfällt, wird als Event gelöscht oder
- verschoben — die Serie bleibt davon unberührt.
-- `NAMING.md` und `frontend/static/pedaler.svg` liegen bewusst nur lokal.
diff --git a/frontend/src/lib/stores/pocketbase.svelte.ts b/frontend/src/lib/stores/pocketbase.svelte.ts
index 413423b..af8f23f 100644
--- a/frontend/src/lib/stores/pocketbase.svelte.ts
+++ b/frontend/src/lib/stores/pocketbase.svelte.ts
@@ -27,19 +27,19 @@ class AuthStore {
api.authStore.loadFromCookie(cookie)
}
+ /**
+ * Anmelden. Ohne Argumente gelten die Felder des Stores, die das
+ * Login-Formular füllt; mit Argumenten die übergebenen — die
+ * Einladungsseite meldet damit das eben angelegte Konto an, ohne den
+ * Umweg über die Formularfelder.
+ */
login = async (user = auth.username, password = auth.password) => {
this.loading = true
this.error = ''
-
- if (this.error) {
- this.loading = false
- return
- }
-
try {
- await api.collection('users').authWithPassword(auth.username, auth.password, this.params)
+ await api.collection('users').authWithPassword(user, password, this.params)
this.user = api.authStore.record as AuthRecord | null
diff --git a/frontend/src/lib/stores/teams.svelte.ts b/frontend/src/lib/stores/teams.svelte.ts
index 498748e..4807784 100644
--- a/frontend/src/lib/stores/teams.svelte.ts
+++ b/frontend/src/lib/stores/teams.svelte.ts
@@ -246,19 +246,6 @@ export class TeamStore {
return user
}
- /** User per E-Mail suchen (für Einladungs-UI) */
- async findUserByEmail(email: string): Promise
Kein Team aktiv. Team verwalten.
+Kein Team aktiv. Team verwalten.
{/if} diff --git a/frontend/src/routes/dashboard/events/[id]/+page.svelte b/frontend/src/routes/dashboard/events/[id]/+page.svelte index 973492f..8b14b66 100644 --- a/frontend/src/routes/dashboard/events/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/events/[id]/+page.svelte @@ -3,7 +3,7 @@ import { goto } from '$app/navigation' import { getEventContext } from '$lib/stores/events.svelte' import { getRunContext } from '$lib/stores/runs.svelte' - import { fullName, getRiderContext } from '$lib/stores/riders.svelte' + import { riderName, getRiderContext } from '$lib/stores/riders.svelte' import { getTimeContext } from '$lib/stores/times.svelte' import { getTeamContext } from '$lib/stores/teams.svelte' import { @@ -283,7 +283,7 @@ {#if canManage && available.length === 0 && riders.scoped.length === 0}Das Team hat noch keine Fahrer. - Jetzt anlegen + Jetzt anlegen
{/if} @@ -322,8 +322,8 @@- {riders.scoped.length} im Team {teams.active?.name ?? ''} -
-Noch keine Fahrer
-- {#if riders.canManage} - Leg sie in den Einstellungen an. - {:else} - Die Teamleitung legt Fahrer in den Einstellungen an. - {/if} -
-- Jedes Teammitglied ist ein Fahrer — ein Konto braucht es dafür nicht. - Wer sich anmelden können soll, bekommt eins. -
- -- Das Konto kommt zugleich ins Team. Gib das Passwort weiter; es lässt - sich unter „Profil" ändern. -
-{error}
- {/if} -{found.name || found.email}
-- Wird ins Team aufgenommen und verknüpft. -
-{accountError}
- {/if} -{stat.label}
-{stat.value}
- {#if stat.hint} -{stat.hint}
- {/if} -- Teams, in denen du bist. Das aktive Team bestimmt, welche Events, Fahrer und - Trails du siehst. -
- -Noch in keinem Team
-Erstelle dein erstes Team.
- -{createError}
- {/if} -{renameError}
- {/if} -PNG, SVG oder WebP, bis 5 MB.
-{logoError}
- {/if} -+ {(team?.users ?? []).length} Mitglied{(team?.users ?? []).length === 1 ? '' : 'er'} + · {riders.scoped.length} im Kader +
+Noch in keinem Team
+Erstelle dein erstes Team.
+ +{stat.label}
+{stat.value}
+ {#if stat.hint} +{stat.hint}
+ {/if} +Noch niemand im Team
++ {#if canManage} + Leg die erste Person an oder verschick einen Einladungslink. + {:else} + Die Teamleitung legt den Kader an. + {/if} +
++ Admins verwalten Kader, Einladungslinks und Teamdaten. Löschen und + Rollen bleiben beim Owner. +
++ Owner des Teams — diese Rolle lässt sich hier nicht ändern. +
+ {/if} + + {#if !isEdit} + + + {#if form.withAccount} ++ Das Konto kommt zugleich ins Team. Gib das Passwort weiter; es lässt + sich unter „Einstellungen" ändern. +
+{error}
+ {/if} +{accountError}
+ {/if} +0 = läuft nicht ab.
+0 = beliebig oft.
+{inviteError}
+ {/if} +{renameError}
+ {/if} +PNG, SVG oder WebP, bis 5 MB.
+{logoError}
+ {/if} +