diff --git a/TODO.md b/TODO.md
index cb9931e..0bd550e 100644
--- a/TODO.md
+++ b/TODO.md
@@ -36,6 +36,39 @@ Ein **bestehendes** fremdes Konto in ein Team zu holen braucht eine offenere
`listRule` (z. B. `@request.auth.id != ""`). Das erlaubt allerdings, alle
Konten der Instanz aufzulisten — deshalb bewusst nicht ohne Rücksprache.
+### Ausfahrten: geplant, noch nicht gebaut
+
+Ausfahrten werden **nicht** zu einem zweiten Konzept neben Events, sondern
+Events bekommen Schalter. Ein Bikepark-Ausflug mit gestoppten Läufen ist beides
+zugleich; eine Trennung wäre willkürlich und würde Datum, Rechte und Oberfläche
+doppelt führen.
+
+Geplant, in dieser Reihenfolge:
+
+1. **`events.timing`** (bool). Runs und Zeitnahme erscheinen nur, wenn gesetzt.
+ Der wöchentliche Stammtisch braucht sie nicht.
+2. **Teilnahme** als eigene Collection `event_participants` (Event, Person,
+ Status, Zeitpunkt) — nicht als Liste am Event, sonst hängen Status und
+ Zeitpunkt nirgends, und Gäste ohne Konto passen nicht hinein.
+
+ `events.participation` steuert, wie man hineinkommt:
+
+ | Wert | Bedeutung |
+ |---|---|
+ | `offen` | Jedes Teammitglied trägt sich selbst ein |
+ | `bestaetigung` | Anmeldung möglich, Ersteller oder Team-Admin bestätigt |
+ | `geschlossen` | Nur Ersteller oder Team-Admin trägt Teilnehmer ein |
+
+3. **Serien** (`event_series`): Regel, Zeitraum und Vorlage; daraus entstehen
+ echte Einzeltermine. Bewusst nicht ein Datensatz mit Wiederholungsregel, aus
+ dem Termine nur gerechnet werden — sobald „wer kommt am 12.?" oder „diese
+ Woche fällt aus" beantwortet werden soll, braucht jeder Termin einen eigenen
+ Datensatz.
+
+Offen: Sind Teilnehmer **Nutzerkonten** oder **Fahrer**? Für den Stammtisch
+wären es Konten, für eine gemeinsame Ausfahrt eher Fahrer (die es auch ohne
+Konto gibt). Beides zugleich geht, macht die Collection aber breiter.
+
## Vor dem nächsten Deploy
### `PUBLIC_CARTO_API_KEY` in Coolify setzen
@@ -50,13 +83,14 @@ Kacheln ohne Schlüssel weiter — die Karte bleibt also sichtbar.
### Migrationen greifen beim nächsten Start
-Fünf neue Migrationen liegen in `backend/pb_migrations/`:
+Sechs neue Migrationen liegen in `backend/pb_migrations/`:
- `1754500500_riders_user_and_admin_rules.js`
- `1754500600_events_dates_instead_of_status.js`
- `1754500700_trail_status_meta.js`
- `1754500800_trail_markers_custom.js`
- `1754500900_users_superadmin.js`
+- `1754501000_teams_logo.js`
Solange sie nicht gelaufen sind, scheitert alles, was auf den neuen Regeln
aufbaut. Am deutlichsten beim Anlegen eines Mitglieds oder eines Fahrer-Logins:
@@ -85,6 +119,13 @@ cd frontend && npm run generate-pocketbase-types
Die Datei sollte danach unverändert bleiben. Tut sie es nicht, gilt das Ergebnis
des Generators.
+### 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
- Der Melder eines Trail-Markers („wer") zeigt bis zum Deploy „Unbekannt". Die
diff --git a/backend/pb_migrations/1754400000_init_schema.js b/backend/pb_migrations/1754400000_init_schema.js
index 6848b17..7681761 100644
--- a/backend/pb_migrations/1754400000_init_schema.js
+++ b/backend/pb_migrations/1754400000_init_schema.js
@@ -334,6 +334,27 @@ migrate((app) => {
"system": false,
"type": "relation"
},
+ {
+ "help": "",
+ "hidden": false,
+ "id": "file_team_logo",
+ "maxSelect": 1,
+ "maxSize": 5242880,
+ "mimeTypes": [
+ "image/jpeg",
+ "image/png",
+ "image/svg+xml",
+ "image/gif",
+ "image/webp"
+ ],
+ "name": "logo",
+ "presentable": false,
+ "protected": false,
+ "required": false,
+ "system": false,
+ "thumbs": null,
+ "type": "file"
+ },
{
"hidden": false,
"id": "autodate2990389176",
diff --git a/backend/pb_migrations/1754501000_teams_logo.js b/backend/pb_migrations/1754501000_teams_logo.js
new file mode 100644
index 0000000..465eb19
--- /dev/null
+++ b/backend/pb_migrations/1754501000_teams_logo.js
@@ -0,0 +1,38 @@
+///
+
+// Jedes Team bringt sein eigenes Logo mit.
+//
+// Bisher steckte das Wappen fest in der Anwendung (static/schildl.svg und
+// static/radl.svg). Für den Stammtisch Hersbruck stimmt das, für jedes
+// weitere Team wäre es fremdes Vereinswappen. Ohne hinterlegte Datei bleibt
+// die bisherige Darstellung als Rückfalloption bestehen.
+//
+// 5 MB, weil die vorhandenen Wappen als Inkscape-SVG je rund 2,5 MB wiegen.
+
+migrate((app) => {
+ const teams = app.findCollectionByNameOrId('teams')
+
+ if (!teams.fields.getByName('logo')) {
+ teams.fields.add(new Field({
+ id: 'file_team_logo',
+ name: 'logo',
+ type: 'file',
+ maxSelect: 1,
+ maxSize: 5242880,
+ mimeTypes: [
+ 'image/jpeg',
+ 'image/png',
+ 'image/svg+xml',
+ 'image/gif',
+ 'image/webp',
+ ],
+ }))
+ }
+
+ app.save(teams)
+}, (app) => {
+ const teams = app.findCollectionByNameOrId('teams')
+
+ teams.fields.removeByName('logo')
+ app.save(teams)
+})
diff --git a/frontend/src/lib/components/Logo.svelte b/frontend/src/lib/components/Logo.svelte
index bf740d7..b58ef14 100644
--- a/frontend/src/lib/components/Logo.svelte
+++ b/frontend/src/lib/components/Logo.svelte
@@ -1,10 +1,28 @@
-
-
+ {#if url}
+
+ {:else}
+
+
+ {/if}
diff --git a/frontend/src/lib/stores/teams.svelte.ts b/frontend/src/lib/stores/teams.svelte.ts
index 4bb9790..498748e 100644
--- a/frontend/src/lib/stores/teams.svelte.ts
+++ b/frontend/src/lib/stores/teams.svelte.ts
@@ -112,6 +112,20 @@ export class TeamStore {
return await api.collection('teams').update(id, { name })
}
+ /**
+ * Logo setzen oder entfernen. Mit Datei als FormData — ein leeres Feld im
+ * FormData würde ein vorhandenes Bild löschen, deshalb der zweite Weg.
+ */
+ async setLogo(id: string, file: File | null) {
+ if (!file) {
+ return await api.collection('teams').update(id, { logo: null })
+ }
+
+ const data = new FormData()
+ data.append('logo', file)
+ return await api.collection('teams').update(id, data)
+ }
+
/** Mitglied hinzufügen (Owner/Admin only) */
async addMember(teamId: string, userId: string) {
const team = this.records.find((t) => t.id === teamId)
diff --git a/frontend/src/lib/types.d.ts b/frontend/src/lib/types.d.ts
index f5f5bd9..6e488be 100644
--- a/frontend/src/lib/types.d.ts
+++ b/frontend/src/lib/types.d.ts
@@ -86,6 +86,7 @@ export type TeamsRecord = {
admins?: RecordIdString[]
created?: IsoDateString
id: string
+ logo?: string
name: string
owner: RecordIdString
updated?: IsoDateString
diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte
index 0e3adb4..2662b5a 100644
--- a/frontend/src/routes/dashboard/+layout.svelte
+++ b/frontend/src/routes/dashboard/+layout.svelte
@@ -101,15 +101,28 @@
{#if auth.user}
-