Kein fertiger Stand, sondern ein Zwischenstand: Hier wurde im September 2026 angefangen, die App auf „Kurbeler" umzubenennen — Wortmarke, Icons, brand.ts, Benachrichtigungen, das Benutzerverzeichnis. Weitergegangen ist es dann im eigenen Repo `~/Dokumente/Projekte/kurbeler`, das die vollständige Historie dieses Repos mitgenommen hat. Committet wird das hier nicht, weil es gebraucht würde, sondern damit es nicht als Haufen unversionierter Dateien im Arbeitsverzeichnis verrottet. Wer in zwei Jahren nachsieht, findet so eine Geschichte statt eines Rätsels — und das Stammtisch-Wappen, das nur hier je existiert hat. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTw6xVYgjMy9AQtfs1Gdyu
198 lines
No EOL
9.5 KiB
Markdown
198 lines
No EOL
9.5 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Overview
|
|
|
|
**Kurbeler** (bis 2026-09-08 „stammtisch-hersbruck.de"; Domain und
|
|
Repository-Verzeichnis heißen weiterhin so) is a SvelteKit 5 application using Svelte 5's new runes syntax ($state, $props, etc.) with PocketBase as the backend. The app manages events, stages, riders, times and teams for a motorsport/cycling event organization.
|
|
|
|
The repository is a monorepo: the SvelteKit app lives in `frontend/`, the PocketBase instance in `backend/`.
|
|
|
|
## Development Commands
|
|
|
|
Alle Frontend-Befehle laufen aus `frontend/`:
|
|
|
|
```bash
|
|
cd frontend
|
|
|
|
# Start development server (runs on http://stammtisch-hersbruck.de.localhost:31337)
|
|
npm run dev
|
|
|
|
# Build for production
|
|
npm run build
|
|
|
|
# Preview production build
|
|
npm run preview
|
|
|
|
# Type-check Svelte files
|
|
npm run check
|
|
|
|
# Type-check with watch mode
|
|
npm run check:watch
|
|
|
|
# Generate TypeScript types from the schema migration in backend/pb_migrations
|
|
# (no running instance, no token, no network access needed)
|
|
npm run generate-pocketbase-types
|
|
```
|
|
|
|
Backend (PocketBase) aus `backend/`:
|
|
|
|
```bash
|
|
cd backend
|
|
|
|
# PocketBase lokal starten (Admin-UI auf http://127.0.0.1:8090/_/)
|
|
docker compose up -d --build
|
|
|
|
# Stoppen
|
|
docker compose down
|
|
```
|
|
|
|
## Architecture
|
|
|
|
### Backend Integration (PocketBase)
|
|
|
|
- **API Base URL**: konfigurierbar über `PUBLIC_PB_URL` in `frontend/.env`.
|
|
Default ist die produktive Instanz `https://api.stammtisch-hersbruck.de`;
|
|
für das lokale Backend aus `backend/` auf `http://127.0.0.1:8090` umstellen.
|
|
- **Schema**: versioniert als Migration in `backend/pb_migrations/`, kommt per
|
|
`COPY` ins Image (kein Bind-Mount — der würde das Image-Verzeichnis
|
|
überdecken). Im Admin-UI erzeugte Migrationen liegen deshalb zunächst nur im
|
|
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, team_invites, events, stages, riders, times,
|
|
trails, trail_versions, trail_flags, trail_markers, trail_comments,
|
|
event_participants, event_series, notification_prefs, notifications
|
|
- **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 — die öffentlichen Routen der Einladungslinks,
|
|
den Uhrenabgleich, `GET /api/directory/users` (Benutzerverzeichnis des
|
|
Superadmins), `POST /api/team-invites/send` (Einladungsmail) und die
|
|
Benachrichtigungen. Jeder Route- und Event-Handler läuft in einer eigenen
|
|
JS-Laufzeit; **was neben `routerAdd`/`onRecord…` im Dateiscope steht, sieht
|
|
der Handler nicht.** Gemeinsamer Code gehört in ein Modul und wird per
|
|
``require(`${__hooks}/…`)`` im Handler geholt. Details in
|
|
`backend/README.md`.
|
|
- **Mails**: SMTP im Admin-UI; dazu `APP_URL` (Adresse des **Frontends**, für
|
|
die Links) und optional `APP_NAME`. Vorlagen liegen in
|
|
`backend/pb_hooks/views/` und werden mit `$template.loadFiles(…).render(…)`
|
|
gefüllt.
|
|
- **Benachrichtigungen**: Kein Auslöser verschickt eine Mail — alle legen
|
|
einen Eintrag in `notifications`, ein Cron-Job macht daraus **eine
|
|
Zusammenfassung pro Konto und Tag** (6 Uhr). Voreinstellungen stehen doppelt,
|
|
in `notifications_lib.js` und `notificationPrefs.svelte.ts`; beide Listen
|
|
müssen zusammenpassen.
|
|
- **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
|
|
|
|
### State Management Pattern
|
|
|
|
The app uses Svelte 5 runes for state management with a custom store pattern:
|
|
|
|
1. **Global stores** in `src/lib/stores/`:
|
|
- `pocketbase.svelte.ts`: PocketBase client, auth, and collection operations
|
|
- `app.svelte.ts`: Global app state (theme, navigation, confirm dialogs, hotkeys)
|
|
- `teams.svelte.ts`: Example of collection-specific store pattern
|
|
|
|
2. **Collection store pattern**:
|
|
- Each collection has a context-based store (see `teams.svelte.ts` as template)
|
|
- Use `setTeamContext()` in parent and `getTeamContext()` in children
|
|
- Stores provide: `records`, `refresh()`, `create()`, `edit()`, `remove()`
|
|
- The `collections` helper in `pocketbase.svelte.ts` provides reusable CRUD operations
|
|
|
|
3. **Auth flow**:
|
|
- Auth state lives in `auth` store from `pocketbase.svelte.ts`
|
|
- Cookie-based persistence available via `auth.cookie` flag
|
|
- Auth store syncs with PocketBase `authStore.onChange()`
|
|
|
|
### UI Components
|
|
|
|
- **UI Library**: Using shadcn-svelte components (bits-ui based)
|
|
- **Component path alias**: `@/components/ui/*` maps to `$lib/components/ui/*`
|
|
- **Styling**: Tailwind CSS 4.x with custom configuration
|
|
- **Dark mode**: Handled by `mode-watcher` package, toggle with `toggleMode()`
|
|
- **Tooltips**: Global `tooltip` action available from `app.svelte.ts` using tippy.js
|
|
|
|
### Important Patterns
|
|
|
|
**Svelte 5 Runes**: This project uses Svelte 5 syntax exclusively:
|
|
- `$state()` for reactive state (not `let` with `$:`)
|
|
- `$props()` for component props
|
|
- `$derived()` for computed values
|
|
- `{@render children?.()}` for slot content
|
|
|
|
**Type Generation**: After modifying the schema migration in `backend/pb_migrations/`, run `npm run generate-pocketbase-types` to update `frontend/src/lib/types.d.ts`. The script reads the collections array straight out of the migration (`frontend/scripts/schema-to-json.mjs`) — schema and types come from the same versioned source and cannot drift apart. Commit the regenerated file.
|
|
|
|
**Async Data Loading**: Root layout (`src/routes/+layout.svelte`) shows pattern:
|
|
```svelte
|
|
{#await load()}
|
|
<!-- loading state -->
|
|
{:then _}
|
|
<!-- main content -->
|
|
{/await}
|
|
```
|
|
|
|
**Confirm Dialogs**: Use `app.confirm.request()` for user confirmations (see `events.svelte.ts` remove pattern).
|
|
|
|
**Hotkeys**: Use `app.hotkey(event, condition, callback)` which auto-ignores input fields and contenteditable elements.
|
|
|
|
**Tooltips**: Never use the `title` attribute for tooltips — appwide. `<Button>`
|
|
takes a `tooltip="…"` prop (it also becomes the `aria-label`, so icon-only
|
|
buttons keep an accessible name); every other element uses the action:
|
|
`use:tooltip={{ content: '…' }}` from `app.svelte.ts`. Both render through
|
|
tippy.js, which the action initializes, updates and destroys.
|
|
|
|
### Project Configuration
|
|
|
|
- **Repo-Struktur**: Monorepo mit `frontend/` (SvelteKit) und `backend/`
|
|
(PocketBase). Ein einziges Git-Repo im Root.
|
|
- **Dev server port**: 31337 (strict mode, custom domain: `stammtisch-hersbruck.de.localhost`)
|
|
- **Path alias**: `@/*` resolves to `./src/lib/*` (configured in frontend/svelte.config.js)
|
|
- **Adapter**: `@sveltejs/adapter-node` — Deployment läuft über Coolify mit
|
|
`node build`. `adapter-auto` erkennt Coolify nicht und erzeugt kein `build/`.
|
|
- **Node**: `engines` verlangt `^20.19 || ^22.12 || >=24`; mit
|
|
`engine-strict=true` (`.npmrc`) bricht `npm ci` sonst mit `EBADENGINE` ab.
|
|
- **Deployment-Env**: `PUBLIC_PB_URL` muss dort gesetzt sein — sie wird zur
|
|
Buildzeit eingesetzt, und die lokale `.env` ist gitignored.
|
|
|
|
### File Structure Notes
|
|
|
|
- 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, Benachrichtigungen, Flag-Typen) — dazu für
|
|
Superadmins der Reiter **Benutzer**, das appweite Kontoverzeichnis.
|
|
- **Konten löscht die Teamleitung nicht.** Im Kader gibt es genau einen
|
|
Eingriff an einer fremden Person: „Zugang zum Team entziehen"
|
|
(`teams.removeMember`). Fahrer, Teilnahmen und Zeiten bleiben dabei stehen —
|
|
sie sind die Geschichte des Teams, nicht die des Logins. Ein Konto löscht
|
|
allein sein Inhaber (Einstellungen → Profil) oder der Superadmin
|
|
(Einstellungen → Benutzer); `users.deleteRule` erzwingt das. Ein
|
|
„Fahrer löschen" gibt es bewusst nicht mehr.
|
|
- **Ein Fahrer, ein Name, eine Richtung**: Ein Konto entsteht immer am Fahrer
|
|
— die Teamleitung lädt per Mail ein (`POST /api/team-invites/send`), das
|
|
Passwort setzt der Eingeladene selbst. Ein Passwort im Kader zu vergeben gibt
|
|
es nicht mehr. Verknüpft wird der Fahrer beim Einlösen der Einladung; 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/`
|
|
- `components.json` configures shadcn-svelte CLI
|
|
|
|
## When Working with This Codebase
|
|
|
|
- Always use Svelte 5 runes syntax, never legacy Svelte syntax
|
|
- Use the official Svelte MCP server to validate Svelte code
|
|
- Collection stores should follow the pattern in `teams.svelte.ts`
|
|
- PocketBase operations go through the `collections` helper for consistency
|
|
- UI components should use the shadcn-svelte imports from `@/components/ui/` |