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
9.5 KiB
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/:
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/:
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_URLinfrontend/.env. Default ist die produktive Instanzhttps://api.stammtisch-hersbruck.de; für das lokale Backend ausbackend/aufhttp://127.0.0.1:8090umstellen. - Schema: versioniert als Migration in
backend/pb_migrations/, kommt perCOPYins 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 mitdocker compose cpins Repo geholt werden; siehebackend/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.iderwähnt, beginnt mit@request.auth.id != "" && (…). Ohne diesen Vorspann ist eine leere Relation gleich dem leeren@request.auth.ideiner anonymen Anfrage — ein Team ohne Admins stünde damit offen im Netz. Siehebackend/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 nebenrouterAdd/onRecord…im Dateiscope steht, sieht der Handler nicht. Gemeinsamer Code gehört in ein Modul und wird perrequire(`${__hooks}/…`)im Handler geholt. Details inbackend/README.md. - Mails: SMTP im Admin-UI; dazu
APP_URL(Adresse des Frontends, für die Links) und optionalAPP_NAME. Vorlagen liegen inbackend/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, innotifications_lib.jsundnotificationPrefs.svelte.ts; beide Listen müssen zusammenpassen. - Authentication: Handled through
src/lib/stores/pocketbase.svelte.tswith theAuthStoreclass - 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:
-
Global stores in
src/lib/stores/:pocketbase.svelte.ts: PocketBase client, auth, and collection operationsapp.svelte.ts: Global app state (theme, navigation, confirm dialogs, hotkeys)teams.svelte.ts: Example of collection-specific store pattern
-
Collection store pattern:
- Each collection has a context-based store (see
teams.svelte.tsas template) - Use
setTeamContext()in parent andgetTeamContext()in children - Stores provide:
records,refresh(),create(),edit(),remove() - The
collectionshelper inpocketbase.svelte.tsprovides reusable CRUD operations
- Each collection has a context-based store (see
-
Auth flow:
- Auth state lives in
authstore frompocketbase.svelte.ts - Cookie-based persistence available via
auth.cookieflag - Auth store syncs with PocketBase
authStore.onChange()
- Auth state lives in
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-watcherpackage, toggle withtoggleMode() - Tooltips: Global
tooltipaction available fromapp.svelte.tsusing tippy.js
Important Patterns
Svelte 5 Runes: This project uses Svelte 5 syntax exclusively:
$state()for reactive state (notletwith$:)$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:
{#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) undbackend/(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 mitnode build.adapter-autoerkennt Coolify nicht und erzeugt keinbuild/. - Node:
enginesverlangt^20.19 || ^22.12 || >=24; mitengine-strict=true(.npmrc) brichtnpm cisonst mitEBADENGINEab. - Deployment-Env:
PUBLIC_PB_URLmuss dort gesetzt sein — sie wird zur Buildzeit eingesetzt, und die lokale.envist 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.deleteRuleerzwingt 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, weilusers.updateRulenur den Kontoinhaber selbst ändern lässt. - Reusable components in
src/lib/components/ - Stores use
.svelte.tsextension for Svelte 5 runes - Static assets in
static/ components.jsonconfigures 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
collectionshelper for consistency - UI components should use the shadcn-svelte imports from
@/components/ui/