chore: veraltete Scripts unter frontend/scripts entfernen

Alle vier Scripts setzten auf einem Schema auf, das es nicht mehr gibt:

- seed-test-data.ts schrieb in stages und results (heute: runs und times)
  und kannte users.role, riders.email/birthday sowie events.created_by/
  timekeepers -- alles nicht mehr vorhanden. Teams kannte es gar nicht,
  ohne Team-Zuordnung greifen sämtliche API-Rules und die Daten wären
  unsichtbar geblieben.
- migrate-schema.ts und migrate-teams.ts waren Einmal-Migrationen. Ihr
  Ergebnis steckt längst im Schema (events.status, riders.number, times.
  correction, die teams-Collection, die team-Felder). Ein erneuter Lauf
  gegen die produktive Instanz wäre bestenfalls wirkungslos.
- test-token.ts war ein Wegwerf-Prüfscript für den Superuser-Token.

Das Schema ist jetzt in backend/pb_migrations/ versioniert; das ist der
Weg für Änderungen. tsx entfällt als Abhängigkeit, es wurde nur von
diesen Scripts benutzt.

PB_SUPERUSER_TOKEN bleibt in .env -- generate-pocketbase-types braucht ihn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Michelberger 2026-08-06 13:42:24 +02:00
parent 1061a8b9ad
commit 5658ef7bcc
7 changed files with 7 additions and 1019 deletions

View file

@ -38,19 +38,12 @@ Die Variable wird von SvelteKit zur **Buildzeit** eingesetzt
Neustart des Dev-Servers bzw. einen neuen Build — ein blosser Neustart des
Containers genügt nicht.
`PB_TYPEGEN_URL` und `PB_SUPERUSER_TOKEN` gelten getrennt davon für die
Typgenerierung und die Scripts unter `scripts/`.
`PB_TYPEGEN_URL` und `PB_SUPERUSER_TOKEN` gelten getrennt davon für
`npm run generate-pocketbase-types`.
## Scripts
Die Dateien unter `scripts/` sind Einmal-Werkzeuge gegen eine PocketBase-Instanz
und laufen über `tsx`. Sie lesen ihre Zugangsdaten aus `.env`.
> **Achtung, veralteter Stand:** `seed-test-data.ts` (`npm run seed`),
> `migrate-schema.ts` und `migrate-teams.ts` stammen aus einer früheren
> Schema-Generation. `seed-test-data.ts` schreibt in die Collections `stages`
> und `results`, die es im aktuellen Schema **nicht mehr gibt** — der Aufruf
> schlägt fehl. Vor einer Verwendung müssen die Scripts auf das aktuelle Schema
> (`users`, `teams`, `events`, `runs`, `riders`, `times`) angepasst werden.
## Schema
Das maßgebliche Schema liegt als Migration in `../backend/pb_migrations/`.
Nach einer Schema-Änderung `npm run generate-pocketbase-types` ausführen — das
aktualisiert `src/lib/types.d.ts`, aus dem der PocketBase-Client seine Typen
bezieht.

View file

@ -31,7 +31,6 @@
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.13",
"tsx": "^4.19.2",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.2",
"vite": "^7.1.7"
@ -2379,19 +2378,6 @@
"node": ">= 0.4"
}
},
"node_modules/get-tsconfig": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz",
"integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@ -3914,16 +3900,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@ -4646,26 +4622,6 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.6",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz",
"integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

View file

@ -10,10 +10,7 @@
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"generate-pocketbase-types": "set -a && . ./.env && set +a && pocketbase-typegen --url \"$PB_TYPEGEN_URL\" --token \"$PB_SUPERUSER_TOKEN\" --out ./src/lib/types.d.ts",
"seed": "tsx scripts/seed-test-data.ts",
"migrate": "tsx scripts/migrate-schema.ts",
"migrate-teams": "tsx scripts/migrate-teams.ts"
"generate-pocketbase-types": "set -a && . ./.env && set +a && pocketbase-typegen --url \"$PB_TYPEGEN_URL\" --token \"$PB_SUPERUSER_TOKEN\" --out ./src/lib/types.d.ts"
},
"devDependencies": {
"@internationalized/date": "^3.12.1",
@ -32,7 +29,6 @@
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.1.13",
"tsx": "^4.19.2",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.2",
"vite": "^7.1.7"

View file

@ -1,245 +0,0 @@
/**
* Schema-Migration für PocketBase
*
* Erweitert das vorhandene Schema und migriert bestehende Daten:
* - events: + name (Text), + status (Select draft/active/finished)
* Daten: description name kopieren, status = 'draft'
* - riders: + number (Text), + firstname (Text), + lastname (Text)
* Daten: bestehender name-Wert number kopieren
* - times: + correction (Number, Sekunden)
* status erweitern: + dnf, dns, dsq
*
* Aufruf: npx tsx scripts/migrate-schema.ts
*/
import { readFileSync } from 'node:fs'
// .env minimal selbst parsen
for (const line of readFileSync('.env', 'utf8').split('\n')) {
const m = line.match(/^([A-Z_]+)=(.*)$/)
if (m && !process.env[m[1]]) process.env[m[1]] = m[2]
}
const API = process.env.PB_TYPEGEN_URL?.replace(/\/$/, '') ?? 'https://api.stammtisch-hersbruck.de'
const TOKEN = process.env.PB_SUPERUSER_TOKEN
if (!TOKEN) {
console.error('PB_SUPERUSER_TOKEN fehlt in .env')
process.exit(1)
}
const headers = {
'Authorization': TOKEN,
'Content-Type': 'application/json',
}
async function req(method: string, path: string, body?: unknown) {
const res = await fetch(`${API}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
})
if (!res.ok) {
const text = await res.text()
throw new Error(`${method} ${path}${res.status}: ${text}`)
}
return res.status === 204 ? null : await res.json()
}
async function getCollection(name: string) {
return req('GET', `/api/collections/${name}`)
}
async function updateCollection(name: string, body: any) {
return req('PATCH', `/api/collections/${name}`, body)
}
async function listRecords(collection: string) {
const all: any[] = []
let page = 1
while (true) {
const res: any = await req('GET', `/api/collections/${collection}/records?page=${page}&perPage=200`)
all.push(...res.items)
if (page >= res.totalPages || res.totalPages === 0) break
page++
}
return all
}
async function updateRecord(collection: string, id: string, data: any) {
return req('PATCH', `/api/collections/${collection}/records/${id}`, data)
}
function hasField(coll: any, name: string) {
return coll.fields.some((f: any) => f.name === name)
}
// ---- Migration ----
async function migrateEvents() {
console.log('\n== events ==')
const coll = await getCollection('events')
const fields = [...coll.fields]
if (!hasField(coll, 'name')) {
fields.push({
name: 'name',
type: 'text',
required: false,
presentable: true,
max: 0,
min: 0,
pattern: '',
autogeneratePattern: '',
})
console.log(' + Feld "name" (text)')
} else {
console.log(' = Feld "name" existiert bereits')
}
if (!hasField(coll, 'status')) {
fields.push({
name: 'status',
type: 'select',
required: false,
presentable: false,
maxSelect: 1,
values: ['draft', 'active', 'finished'],
})
console.log(' + Feld "status" (select draft/active/finished)')
} else {
console.log(' = Feld "status" existiert bereits')
}
await updateCollection('events', { fields })
// Daten migrieren
const records = await listRecords('events')
for (const r of records) {
const patch: any = {}
if (!r.name && r.description) patch.name = r.description
if (!r.status) patch.status = 'draft'
if (Object.keys(patch).length) {
await updateRecord('events', r.id, patch)
console.log(`${r.id}: ${JSON.stringify(patch)}`)
}
}
}
async function migrateRiders() {
console.log('\n== riders ==')
const coll = await getCollection('riders')
const fields = [...coll.fields]
if (!hasField(coll, 'number')) {
fields.push({
name: 'number',
type: 'text',
required: false,
presentable: true,
max: 0,
min: 0,
pattern: '',
autogeneratePattern: '',
})
console.log(' + Feld "number" (text)')
} else {
console.log(' = Feld "number" existiert bereits')
}
if (!hasField(coll, 'firstname')) {
fields.push({
name: 'firstname',
type: 'text',
required: false,
presentable: true,
max: 0,
min: 0,
pattern: '',
autogeneratePattern: '',
})
console.log(' + Feld "firstname" (text)')
} else {
console.log(' = Feld "firstname" existiert bereits')
}
if (!hasField(coll, 'lastname')) {
fields.push({
name: 'lastname',
type: 'text',
required: false,
presentable: true,
max: 0,
min: 0,
pattern: '',
autogeneratePattern: '',
})
console.log(' + Feld "lastname" (text)')
} else {
console.log(' = Feld "lastname" existiert bereits')
}
await updateCollection('riders', { fields })
// Daten migrieren: name → number, falls number leer
const records = await listRecords('riders')
for (const r of records) {
if (!r.number && r.name) {
await updateRecord('riders', r.id, { number: r.name })
console.log(`${r.id}: number = "${r.name}"`)
}
}
}
async function migrateTimes() {
console.log('\n== times ==')
const coll = await getCollection('times')
const fields = [...coll.fields]
let changed = false
if (!hasField(coll, 'correction')) {
fields.push({
name: 'correction',
type: 'number',
required: false,
presentable: false,
min: null,
max: null,
onlyInt: false,
})
console.log(' + Feld "correction" (number)')
changed = true
} else {
console.log(' = Feld "correction" existiert bereits')
}
// status um dnf, dns, dsq erweitern
const statusField = fields.find((f: any) => f.name === 'status')
if (statusField) {
const current: string[] = statusField.values || []
const target = ['active', 'finished', 'dnf', 'dns', 'dsq']
const missing = target.filter((v) => !current.includes(v))
if (missing.length) {
statusField.values = [...current, ...missing]
console.log(` ~ Feld "status" erweitert um: ${missing.join(', ')}`)
changed = true
} else {
console.log(' = Feld "status" hat bereits alle Werte')
}
}
if (changed) await updateCollection('times', { fields })
}
async function main() {
console.log(`Migration gegen ${API}`)
await migrateEvents()
await migrateRiders()
await migrateTimes()
console.log('\n✓ Migration fertig')
}
main().catch((e) => {
console.error('\n✗ Fehler:', e.message)
process.exit(1)
})

View file

@ -1,329 +0,0 @@
/**
* Schema-Migration: Teams einführen
*
* - neue Collection 'teams' (name, owner, admins[], users[])
* - events, riders, runs, times bekommen ein 'team'-Feld
* - listRule/viewRule auf allen vier wird auf team.users.id ?= @request.auth.id umgestellt
* - events.users wird entfernt
* - Migration der Daten:
* Team "Stammtisch Hersbruck" anlegen
* Owner = aktuell eingeloggter User (über .env: PB_OWNER_EMAIL falls gesetzt, sonst erster events.users)
* users = Union aller events.users
* events/riders/runs/times werden alle diesem Team zugeordnet
*
* Aufruf: npm run migrate-teams
*/
import { readFileSync } from 'node:fs'
// .env parsen — Zeilen die mit . anfangen werden an die vorherige Variable angehängt
// (Workaround für zerbrochene JWT-Tokens über mehrere Zeilen)
const env = readFileSync('.env', 'utf8').split('\n')
let lastKey = ''
for (const line of env) {
const m = line.match(/^([A-Z_]+)=(.*)$/)
if (m) {
process.env[m[1]] = m[2]
lastKey = m[1]
} else if (lastKey && line.length > 0) {
process.env[lastKey] = (process.env[lastKey] ?? '') + line
}
}
const API = process.env.PB_TYPEGEN_URL?.replace(/\/$/, '') ?? 'https://api.stammtisch-hersbruck.de'
const TOKEN = process.env.PB_SUPERUSER_TOKEN
if (!TOKEN) {
console.error('PB_SUPERUSER_TOKEN fehlt')
process.exit(1)
}
const headers = { Authorization: TOKEN, 'Content-Type': 'application/json' }
async function req(method: string, path: string, body?: unknown) {
const res = await fetch(`${API}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
})
if (!res.ok) throw new Error(`${method} ${path}${res.status}: ${await res.text()}`)
return res.status === 204 ? null : await res.json()
}
async function getCollection(name: string) {
try {
return await req('GET', `/api/collections/${name}`)
} catch {
return null
}
}
async function createCollection(body: any) {
return req('POST', '/api/collections', body)
}
async function updateCollection(name: string, body: any) {
return req('PATCH', `/api/collections/${name}`, body)
}
async function listAll(collection: string) {
const all: any[] = []
let page = 1
while (true) {
const res: any = await req('GET', `/api/collections/${collection}/records?page=${page}&perPage=200`)
all.push(...res.items)
if (page >= res.totalPages || res.totalPages === 0) break
page++
}
return all
}
async function updateRecord(collection: string, id: string, data: any) {
return req('PATCH', `/api/collections/${collection}/records/${id}`, data)
}
async function createRecord(collection: string, data: any) {
return req('POST', `/api/collections/${collection}/records`, data)
}
function hasField(coll: any, name: string) {
return coll.fields.some((f: any) => f.name === name)
}
const USERS_AUTH = '_pb_users_auth_'
// ---- Schritt 1: teams Collection anlegen ----
async function ensureTeamsCollection() {
console.log('\n== teams Collection ==')
const existing = await getCollection('teams')
if (existing) {
console.log(' = teams existiert bereits')
return existing
}
const body = {
name: 'teams',
type: 'base',
listRule: 'users.id ?= @request.auth.id',
viewRule: 'users.id ?= @request.auth.id',
createRule: '@request.auth.id != ""',
updateRule: 'owner.id ?= @request.auth.id || admins.id ?= @request.auth.id',
deleteRule: 'owner.id ?= @request.auth.id',
fields: [
{
name: 'name',
type: 'text',
required: true,
presentable: true,
max: 100,
min: 1,
pattern: '',
autogeneratePattern: '',
},
{
name: 'owner',
type: 'relation',
required: true,
maxSelect: 1,
minSelect: 1,
collectionId: USERS_AUTH,
cascadeDelete: false,
},
{
name: 'admins',
type: 'relation',
required: false,
maxSelect: 999,
minSelect: 0,
collectionId: USERS_AUTH,
cascadeDelete: false,
},
{
name: 'users',
type: 'relation',
required: false,
maxSelect: 999,
minSelect: 0,
collectionId: USERS_AUTH,
cascadeDelete: false,
},
{
name: 'created',
type: 'autodate',
onCreate: true,
onUpdate: false,
},
{
name: 'updated',
type: 'autodate',
onCreate: true,
onUpdate: true,
},
],
}
const created = await createCollection(body)
console.log(' + Collection "teams" angelegt')
return created
}
// ---- Schritt 2: team-Feld zu events/riders/runs/times hinzufügen ----
async function addTeamField(collName: string) {
const coll = await getCollection(collName)
if (!coll) throw new Error(`Collection ${collName} fehlt`)
if (hasField(coll, 'team')) {
console.log(` = ${collName}.team existiert bereits`)
return
}
const fields = [
...coll.fields,
{
name: 'team',
type: 'relation',
required: false,
maxSelect: 1,
minSelect: 0,
collectionId: (await getCollection('teams'))!.id,
cascadeDelete: false,
},
]
await updateCollection(collName, { fields })
console.log(` + ${collName}.team hinzugefügt`)
}
// ---- Schritt 3: Default-Team anlegen + alle Records zuordnen ----
async function migrateData(teamsId: string) {
console.log('\n== Daten-Migration ==')
// Owner-User suchen
const ownerEmail = process.env.PB_OWNER_EMAIL || 'stammtisch-hersbruck@dne.name'
const usersRes: any = await req(
'GET',
`/api/collections/${USERS_AUTH}/records?filter=` + encodeURIComponent(`email = "${ownerEmail}"`),
)
if (!usersRes.items.length) {
throw new Error(`User mit E-Mail "${ownerEmail}" nicht gefunden. Setze PB_OWNER_EMAIL in .env.`)
}
const owner = usersRes.items[0]
console.log(` Owner: ${owner.email} (${owner.id})`)
// Existierendes Team suchen
const teamsList: any = await req('GET', `/api/collections/teams/records?filter=` + encodeURIComponent(`name = "Stammtisch Hersbruck"`))
let team: any = teamsList.items[0]
if (!team) {
// alle User aus events.users sammeln
const events = await listAll('events')
const userSet = new Set<string>([owner.id])
for (const e of events) {
if (Array.isArray(e.users)) for (const u of e.users) userSet.add(u)
}
team = await createRecord('teams', {
name: 'Stammtisch Hersbruck',
owner: owner.id,
admins: [],
users: [...userSet],
})
console.log(` + Team "Stammtisch Hersbruck" mit ${userSet.size} Mitgliedern angelegt`)
} else {
console.log(` = Team "Stammtisch Hersbruck" existiert (${team.id})`)
}
// Datensätze ohne team patchen
for (const collName of ['events', 'riders', 'runs', 'times']) {
const records = await listAll(collName)
let touched = 0
for (const r of records) {
if (!r.team) {
await updateRecord(collName, r.id, { team: team.id })
touched++
}
}
console.log(`${collName}: ${touched} Records zugeordnet (${records.length - touched} bereits gesetzt)`)
}
return team
}
// ---- Schritt 4: Rules auf team-basiert umstellen ----
async function updateRules() {
console.log('\n== Rules auf team-basiert umstellen ==')
const rules = {
events: {
listRule: 'team.users.id ?= @request.auth.id',
viewRule: 'team.users.id ?= @request.auth.id',
createRule: 'team.users.id ?= @request.auth.id',
updateRule: 'team.users.id ?= @request.auth.id',
deleteRule: 'team.users.id ?= @request.auth.id',
},
riders: {
listRule: 'team.users.id ?= @request.auth.id',
viewRule: 'team.users.id ?= @request.auth.id',
createRule: 'team.users.id ?= @request.auth.id',
updateRule: 'team.users.id ?= @request.auth.id',
deleteRule: 'team.users.id ?= @request.auth.id',
},
runs: {
listRule: 'team.users.id ?= @request.auth.id',
viewRule: 'team.users.id ?= @request.auth.id',
createRule: 'team.users.id ?= @request.auth.id',
updateRule: 'team.users.id ?= @request.auth.id',
deleteRule: 'team.users.id ?= @request.auth.id',
},
times: {
listRule: 'team.users.id ?= @request.auth.id',
viewRule: 'team.users.id ?= @request.auth.id',
createRule: 'team.users.id ?= @request.auth.id',
updateRule: 'team.users.id ?= @request.auth.id',
deleteRule: 'team.users.id ?= @request.auth.id',
},
}
for (const [coll, r] of Object.entries(rules)) {
await updateCollection(coll, r)
console.log(` ~ ${coll} Rules aktualisiert`)
}
}
// ---- Schritt 5: events.users entfernen ----
async function removeEventsUsers() {
console.log('\n== events.users entfernen ==')
const coll = await getCollection('events')
if (!coll) return
if (!hasField(coll, 'users')) {
console.log(' = events.users existiert nicht (mehr)')
return
}
const fields = coll.fields.filter((f: any) => f.name !== 'users')
await updateCollection('events', { fields })
console.log(' - events.users entfernt')
}
// ---- Main ----
async function main() {
console.log(`Migration gegen ${API}`)
const teams = await ensureTeamsCollection()
console.log('\n== team-Feld zu Collections hinzufügen ==')
await addTeamField('events')
await addTeamField('riders')
await addTeamField('runs')
await addTeamField('times')
await migrateData(teams.id)
await updateRules()
await removeEventsUsers()
console.log('\n✓ Teams-Migration fertig')
}
main().catch((e) => {
console.error('\n✗ Fehler:', e.message)
process.exit(1)
})

View file

@ -1,329 +0,0 @@
/**
* Test Data Seeding Script for PocketBase
*
* This script creates initial test data for the timing app:
* - Admin user
* - Timekeeper users
* - Test riders
* - Sample event with stages
*
* Usage:
* npx tsx scripts/seed-test-data.ts
*
* Environment Variables (from .env):
* PB_TYPEGEN_URL - PocketBase API URL
* PB_TYPEGEN_TOKEN - Authentication token for impersonation
*/
import PocketBase from 'pocketbase'
import type { TypedPocketBase } from '../src/lib/types.js'
// Validate required environment variables
const PB_URL = process.env.PB_TYPEGEN_URL
const PB_TOKEN = process.env.PB_TYPEGEN_TOKEN
if (!PB_URL) {
console.error('❌ Error: PB_TYPEGEN_URL is not set in .env file')
console.error(' Please set PB_TYPEGEN_URL in your .env file')
process.exit(1)
}
if (!PB_TOKEN) {
console.error('❌ Error: PB_TYPEGEN_TOKEN is not set in .env file')
console.error(' Please set PB_TYPEGEN_TOKEN in your .env file')
process.exit(1)
}
// Initialize PocketBase with URL from .env
const pb = new PocketBase(PB_URL) as TypedPocketBase
// Authenticate using token from .env (impersonation)
pb.authStore.save(PB_TOKEN)
console.log(`🔗 Connecting to PocketBase at: ${PB_URL}`)
console.log(`🔑 Using authentication token from .env\n`)
// Configuration
const ADMIN_EMAIL = 'admin@stammtisch-hersbruck.de'
const ADMIN_PASSWORD = 'Admin123!secure'
async function seedData() {
console.log('🌱 Starting test data seeding...\n')
try {
// 1. Create Admin User
console.log('👤 Creating admin user...')
let adminUser
try {
adminUser = await pb.collection('users').create({
email: ADMIN_EMAIL,
password: ADMIN_PASSWORD,
passwordConfirm: ADMIN_PASSWORD,
name: 'Administrator',
role: 'admin',
verified: true
})
console.log(`✅ Admin created: ${adminUser.email}`)
} catch (e: any) {
if (e.message.includes('already exists')) {
console.log('⚠️ Admin user already exists, logging in...')
await pb.collection('users').authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD)
adminUser = pb.authStore.record
} else {
throw e
}
}
// 2. Create Timekeeper Users
console.log('\n⏱ Creating timekeeper users...')
const timekeepers = []
const timekeeperData = [
{ email: 'timer1@stammtisch-hersbruck.de', name: 'Max Zeitnehmer', password: 'Timer123!' },
{ email: 'timer2@stammtisch-hersbruck.de', name: 'Anna Stoppuhr', password: 'Timer123!' },
{ email: 'timer3@stammtisch-hersbruck.de', name: 'Tom Chronograph', password: 'Timer123!' }
]
for (const data of timekeeperData) {
try {
const user = await pb.collection('users').create({
email: data.email,
password: data.password,
passwordConfirm: data.password,
name: data.name,
role: 'timekeeper',
verified: true
})
timekeepers.push(user)
console.log(`✅ Timekeeper created: ${user.name} (${user.email})`)
} catch (e: any) {
if (e.message.includes('already exists')) {
console.log(`⚠️ Timekeeper ${data.email} already exists, skipping...`)
// Fetch existing user
const existing = await pb.collection('users').getFirstListItem(`email="${data.email}"`)
timekeepers.push(existing)
} else {
console.error(`❌ Error creating ${data.email}:`, e.message)
}
}
}
// 3. Create Test Riders
console.log('\n🚴 Creating test riders...')
const riders = []
const riderData = [
{ number: '1', name: 'Max Müller', email: 'max.mueller@example.com', birthday: '1995-03-15' },
{ number: '2', name: 'Anna Schmidt', email: 'anna.schmidt@example.com', birthday: '1992-07-22' },
{ number: '3', name: 'Tom Weber', email: 'tom.weber@example.com', birthday: '1998-11-08' },
{ number: '17', name: 'Lisa Hoffmann', email: 'lisa.hoffmann@example.com', birthday: '2000-01-30' },
{ number: '23', name: 'Felix Wagner', email: 'felix.wagner@example.com', birthday: '1996-05-12' },
{ number: '42', name: 'Sarah Becker', email: 'sarah.becker@example.com', birthday: '1994-09-25' },
{ number: '99', name: 'Lukas Fischer', email: 'lukas.fischer@example.com', birthday: '1999-12-03' },
{ number: '101', name: 'Julia Schulz', email: 'julia.schulz@example.com', birthday: '1997-04-18' }
]
for (const data of riderData) {
try {
const rider = await pb.collection('riders').create({
number: data.number,
name: data.name,
email: data.email,
birthday: data.birthday
})
riders.push(rider)
console.log(`✅ Rider created: #${rider.number} ${rider.name}`)
} catch (e: any) {
if (e.message.includes('already exists')) {
console.log(`⚠️ Rider #${data.number} already exists, skipping...`)
const existing = await pb.collection('riders').getFirstListItem(`number="${data.number}"`)
riders.push(existing)
} else {
console.error(`❌ Error creating rider #${data.number}:`, e.message)
}
}
}
// 4. Create Test Event
console.log('\n🏁 Creating test event...')
let event
try {
// Event date: 2 weeks from now
const eventDate = new Date()
eventDate.setDate(eventDate.getDate() + 14)
const startTime = new Date(eventDate)
startTime.setHours(9, 0, 0, 0)
const endTime = new Date(eventDate)
endTime.setHours(17, 0, 0, 0)
event = await pb.collection('events').create({
name: 'MTB Hersbruck Cup 2025',
location: 'Hersbruck, Deutschland',
start: startTime.toISOString(),
end: endTime.toISOString(),
status: 'draft',
created_by: adminUser?.id,
timekeepers: timekeepers.map(t => t.id)
})
console.log(`✅ Event created: ${event.name}`)
console.log(` Date: ${new Date(event.start).toLocaleDateString('de-DE')}`)
console.log(` Timekeepers: ${timekeepers.length} assigned`)
} catch (e: any) {
if (e.message.includes('already exists')) {
console.log('⚠️ Event already exists, fetching...')
event = await pb.collection('events').getFirstListItem('name~"MTB Hersbruck"')
} else {
throw e
}
}
// 5. Create Stages
console.log('\n🏔 Creating stages...')
const stages = []
const stageData = [
{
name: 'Downhill',
location: 'Steinberg Trail',
order: 1,
start_number: 1,
end_number: 50,
status: 'pending'
},
{
name: 'Cross-Country',
location: 'Wald-Rundkurs',
order: 2,
start_number: 51,
end_number: 100,
status: 'pending'
},
{
name: 'Enduro Stage 1',
location: 'Berg-Passage',
order: 3,
start_number: 101,
end_number: 150,
status: 'pending'
}
]
for (const data of stageData) {
try {
// Stage times based on event date
const stageStart = new Date(event.start)
stageStart.setHours(stageStart.getHours() + (data.order - 1) * 2)
const stageEnd = new Date(stageStart)
stageEnd.setHours(stageEnd.getHours() + 2)
const stage = await pb.collection('stages').create({
event: event.id,
name: data.name,
location: data.location,
start: stageStart.toISOString(),
end: stageEnd.toISOString(),
order: data.order,
start_number: data.start_number,
end_number: data.end_number,
status: data.status
})
stages.push(stage)
console.log(`✅ Stage created: ${stage.name} (Startnummern: ${data.start_number}-${data.end_number})`)
} catch (e: any) {
console.error(`❌ Error creating stage ${data.name}:`, e.message)
}
}
// 6. Create Sample Results (optional - for demo)
console.log('\n⏱ Creating sample results...')
// Create 2 finished results for demonstration
if (stages.length > 0 && riders.length >= 3 && timekeepers.length > 0) {
const firstStage = stages[0]
const sampleResults = [
{
rider: riders[0],
duration: 185.5, // 3:05.5
status: 'finished'
},
{
rider: riders[1],
duration: 192.3, // 3:12.3
status: 'finished'
},
{
rider: riders[2],
duration: 0, // currently running
status: 'running'
}
]
for (const data of sampleResults) {
try {
const now = new Date()
const startTime = new Date(now.getTime() - (data.duration * 1000) - 60000) // Started some time ago
const endTime = data.status === 'finished' ? new Date(startTime.getTime() + data.duration * 1000) : undefined
const result = await pb.collection('results').create({
stage: firstStage.id,
rider: data.rider.id,
start: startTime.toISOString(),
end: endTime?.toISOString() || null,
status: data.status,
started_by: timekeepers[0].id,
stopped_by: data.status === 'finished' ? timekeepers[0].id : null,
correction: 0
})
const timeStr = data.status === 'finished'
? `${Math.floor(data.duration / 60)}:${(data.duration % 60).toFixed(1).padStart(4, '0')}`
: 'RUNNING'
console.log(`✅ Result created: #${data.rider.number} ${data.rider.name} - ${timeStr}`)
} catch (e: any) {
console.error(`❌ Error creating result:`, e.message)
}
}
}
// Summary
console.log('\n' + '='.repeat(50))
console.log('✅ Test data seeding completed!')
console.log('='.repeat(50))
console.log('\n📊 Summary:')
console.log(` Users: ${1 + timekeepers.length} (1 admin, ${timekeepers.length} timekeepers)`)
console.log(` Riders: ${riders.length}`)
console.log(` Events: 1`)
console.log(` Stages: ${stages.length}`)
console.log(` Results: 3 (2 finished, 1 running)`)
console.log('\n🔑 Login Credentials:')
console.log(` Admin: ${ADMIN_EMAIL} / ${ADMIN_PASSWORD}`)
console.log(` Timekeeper: timer1@stammtisch-hersbruck.de / Timer123!`)
console.log(` Timekeeper: timer2@stammtisch-hersbruck.de / Timer123!`)
console.log(` Timekeeper: timer3@stammtisch-hersbruck.de / Timer123!`)
console.log('\n🚀 Next steps:')
console.log(' 1. Run: npm run generate-pocketbase-types')
console.log(' 2. Start development: npm run dev')
console.log(' 3. Login with one of the credentials above')
console.log('')
} catch (error: any) {
console.error('\n❌ Error during seeding:', error)
if (error.response) {
console.error('Response:', error.response)
}
process.exit(1)
}
}
// Run the seeding
seedData()
.then(() => {
console.log('✅ Seeding completed successfully!')
process.exit(0)
})
.catch((error) => {
console.error('❌ Seeding failed:', error)
process.exit(1)
})

View file

@ -1,54 +0,0 @@
/**
* Test-Script: prüft was der PB_SUPERUSER_TOKEN tatsächlich darf.
* Aufruf: npx tsx scripts/test-token.ts
*/
import { readFileSync } from 'node:fs'
const env = readFileSync('.env', 'utf8').split('\n')
let lastKey = ''
for (const line of env) {
const m = line.match(/^([A-Z_]+)=(.*)$/)
if (m) {
process.env[m[1]] = m[2]
lastKey = m[1]
} else if (lastKey && line.length > 0) {
process.env[lastKey] = (process.env[lastKey] ?? '') + line
}
}
const TOKEN = process.env.PB_SUPERUSER_TOKEN ?? ''
const API = 'https://api.stammtisch-hersbruck.de'
// JWT-Payload dekodieren
const [, payloadB64] = TOKEN.split('.')
if (payloadB64) {
const payload = JSON.parse(Buffer.from(payloadB64, 'base64').toString('utf8'))
console.log('Token-Payload:', payload)
console.log('Token-Länge:', TOKEN.length)
}
async function tryEndpoint(label: string, path: string, init?: RequestInit) {
const res = await fetch(`${API}${path}`, {
...init,
headers: { Authorization: TOKEN, 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
})
const text = await res.text()
console.log(`\n${label}: ${res.status}`)
try {
console.log(JSON.stringify(JSON.parse(text), null, 2).slice(0, 200))
} catch {
console.log(text.slice(0, 200))
}
}
await tryEndpoint('GET /api/collections', '/api/collections?perPage=1')
await tryEndpoint('GET /api/collections/_superusers', '/api/collections/_superusers')
await tryEndpoint('GET /api/collections/events', '/api/collections/events')
await tryEndpoint('POST /api/collections (Probe)', '/api/collections', {
method: 'POST',
body: JSON.stringify({
name: '__probe_temp__',
type: 'base',
fields: [{ name: 'foo', type: 'text' }],
}),
})