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>
329 lines
No EOL
11 KiB
TypeScript
329 lines
No EOL
11 KiB
TypeScript
/**
|
||
* 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)
|
||
}) |