stammtisch-hersbruck/frontend/scripts/seed-test-data.ts
Daniel Michelberger 1061a8b9ad chore: Repo-Struktur mit frontend/ und backend/ aufsetzen
Die SvelteKit-App liegt unter frontend/, das Backend unter backend/ als
eigenständige PocketBase-Instanz mit Dockerfile, docker-compose und
versioniertem Schema.

- PocketBase-URL über PUBLIC_PB_URL konfigurierbar, Default bleibt die
  produktive Instanz https://api.stammtisch-hersbruck.de
- Schema als Snapshot-Migration der sechs fachlichen Collections
  (users, teams, events, runs, riders, times), abgezogen von der
  produktiven Instanz. Verifiziert: ein Erststart gegen leere pb_data
  legt alle sechs an, Felder und API-Rules stimmen überein.
- pb_data und pb_migrations als Bind-Mounts, damit Daten persistieren und
  im Admin-UI erzeugte Migrationen im Repo landen
- Veraltete Dokumentation entfernt: pocketbase_schema.json nannte
  Collections (stages, results, organizers), die es nicht gibt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 13:16:43 +02:00

329 lines
No EOL
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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)
})