From 4a8d6bc5d7bb62ee9413ba9ea2f3523680330511 Mon Sep 17 00:00:00 2001 From: Server Ubunto - HOME Date: Wed, 29 Oct 2025 20:38:39 +0000 Subject: [PATCH] Nice and stable before liveview --- app/api/drivers/route.ts | 214 -------------------- app/api/events/cars/route.ts | 22 ++ app/api/events/register/route.ts | 5 +- app/api/telemetry/route.ts | 89 ++++++++ app/dashboard/page.tsx | 4 +- app/events/[event_id]/page.tsx | 2 +- app/events/page.tsx | 3 +- app/globals.css | 4 +- app/live/page.tsx | 132 ++++++++++++ app/page.tsx | 1 + app/rankings/page.tsx | 3 +- components/events/EventRegistrationForm.tsx | 69 +++++-- components/live/LiveRefreshWrapper.tsx | 20 ++ components/live/LiveSessionClient.tsx | 130 ++++++++++++ components/live/LiveTiming.tsx | 125 ++++++++++++ components/live/LiveTrackMap.tsx | 145 +++++++++++++ components/ui/icons.tsx | 58 +++--- hooks/useLiveTelemetry.ts | 74 +++++++ lib/serverConfig.ts | 46 +++++ lib/telemetryBridge.ts | 202 ++++++++++++++++++ lib/trackUtils.ts | 57 ++++++ 21 files changed, 1135 insertions(+), 270 deletions(-) create mode 100644 app/api/events/cars/route.ts create mode 100644 app/api/telemetry/route.ts create mode 100644 app/live/page.tsx create mode 100644 components/live/LiveRefreshWrapper.tsx create mode 100644 components/live/LiveSessionClient.tsx create mode 100644 components/live/LiveTiming.tsx create mode 100644 components/live/LiveTrackMap.tsx create mode 100644 hooks/useLiveTelemetry.ts create mode 100644 lib/serverConfig.ts create mode 100644 lib/telemetryBridge.ts create mode 100644 lib/trackUtils.ts diff --git a/app/api/drivers/route.ts b/app/api/drivers/route.ts index 6507b61..ee9cbd4 100644 --- a/app/api/drivers/route.ts +++ b/app/api/drivers/route.ts @@ -86,220 +86,6 @@ export async function GET(request: Request) { } } -// POST endpoint for updating driver data (optional, for future use) -export async function POST(request: Request) { - try { - const body = await request.json(); - - // Add your update logic here - // Example: Update driver rank, stats, etc. - - return NextResponse.json({ - success: true, - message: 'Driver updated', - }); - } catch (error) { - console.error('Error updating driver:', error); - return NextResponse.json( - { success: false, error: 'Failed to update driver' }, - { status: 500 } - ); - } -}// app/api/drivers/route.ts -// API Route: GET /api/drivers -// Returns all connected drivers with their server info - -import { NextResponse } from 'next/server'; -import { query } from '@/lib/db'; -import { DriverWithServer } from '@/types/racing'; - -export async function GET(request: Request) { - try { - // Parse query params (e.g., ?connected=true) - const { searchParams } = new URL(request.url); - const connectedOnly = searchParams.get('connected') === 'true'; - - // SQL query - joins drivers with servers - const sql = ` - SELECT - u.driver_guid, - u.driver_name, - u.driver_team, - u.car_model, - u.car_skin, - u.cuts_alltime, - u.contacts_alltime, - u.laps_completed, - u.user_rank, - u.is_connect, - u.is_loading, - u.current_server, - u.created_at, - s.server_id, - s.server_name, - s.server_track, - s.session_type, - s.session_flag, - s.connected_players - FROM users u - LEFT JOIN servers s ON u.current_server = s.server_id - ${connectedOnly ? 'WHERE u.is_connect = true' : ''} - ORDER BY u.user_rank ASC - `; - - const rows = await query(sql); - - // Transform to proper structure - const drivers: DriverWithServer[] = rows.map((row: any) => ({ - driver_guid: row.driver_guid, - driver_name: row.driver_name, - driver_team: row.driver_team, - car_model: row.car_model, - car_skin: row.car_skin, - cuts_alltime: row.cuts_alltime, - contacts_alltime: row.contacts_alltime, - laps_completed: row.laps_completed, - user_rank: row.user_rank, - is_connect: row.is_connect, - is_loading: row.is_loading, - current_server: row.current_server, - created_at: row.created_at, - // Include server info if they're connected - server: row.server_id ? { - server_id: row.server_id, - server_name: row.server_name, - server_track: row.server_track, - session_type: row.session_type, - session_flag: row.session_flag, - connected_players: row.connected_players, - } : undefined, - })); - - return NextResponse.json({ - success: true, - data: drivers, - count: drivers.length, - }); - - } catch (error) { - console.error('Error fetching drivers:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to fetch drivers' - }, - { status: 500 } - ); - } -} - -// POST endpoint for updating driver data (optional, for future use) -export async function POST(request: Request) { - try { - const body = await request.json(); - - // Add your update logic here - // Example: Update driver rank, stats, etc. - - return NextResponse.json({ - success: true, - message: 'Driver updated', - }); - } catch (error) { - console.error('Error updating driver:', error); - return NextResponse.json( - { success: false, error: 'Failed to update driver' }, - { status: 500 } - ); - } -}// app/api/drivers/route.ts -// API Route: GET /api/drivers -// Returns all connected drivers with their server info - -import { NextResponse } from 'next/server'; -import { query } from '@/lib/db'; -import { DriverWithServer } from '@/types/racing'; - -export async function GET(request: Request) { - try { - // Parse query params (e.g., ?connected=true) - const { searchParams } = new URL(request.url); - const connectedOnly = searchParams.get('connected') === 'true'; - - // SQL query - joins drivers with servers - const sql = ` - SELECT - u.driver_guid, - u.driver_name, - u.driver_team, - u.car_model, - u.car_skin, - u.cuts_alltime, - u.contacts_alltime, - u.laps_completed, - u.user_rank, - u.is_connect, - u.is_loading, - u.current_server, - u.created_at, - s.server_id, - s.server_name, - s.server_track, - s.session_type, - s.session_flag, - s.connected_players - FROM users u - LEFT JOIN servers s ON u.current_server = s.server_id - ${connectedOnly ? 'WHERE u.is_connect = true' : ''} - ORDER BY u.user_rank ASC - `; - - const rows = await query(sql); - - // Transform to proper structure - const drivers: DriverWithServer[] = rows.map((row: any) => ({ - driver_guid: row.driver_guid, - driver_name: row.driver_name, - driver_team: row.driver_team, - car_model: row.car_model, - car_skin: row.car_skin, - cuts_alltime: row.cuts_alltime, - contacts_alltime: row.contacts_alltime, - laps_completed: row.laps_completed, - user_rank: row.user_rank, - is_connect: row.is_connect, - is_loading: row.is_loading, - current_server: row.current_server, - created_at: row.created_at, - // Include server info if they're connected - server: row.server_id ? { - server_id: row.server_id, - server_name: row.server_name, - server_track: row.server_track, - session_type: row.session_type, - session_flag: row.session_flag, - connected_players: row.connected_players, - } : undefined, - })); - - return NextResponse.json({ - success: true, - data: drivers, - count: drivers.length, - }); - - } catch (error) { - console.error('Error fetching drivers:', error); - return NextResponse.json( - { - success: false, - error: 'Failed to fetch drivers' - }, - { status: 500 } - ); - } -} - // POST endpoint for updating driver data (optional, for future use) export async function POST(request: Request) { try { diff --git a/app/api/events/cars/route.ts b/app/api/events/cars/route.ts new file mode 100644 index 0000000..2e93a10 --- /dev/null +++ b/app/api/events/cars/route.ts @@ -0,0 +1,22 @@ +// app/api/events/cars/route.ts +// API endpoint to get available cars from server config + +import { NextResponse } from 'next/server'; +import { getAvailableCars } from '@/lib/serverConfig'; + +export async function GET() { + try { + const cars = await getAvailableCars(); + + return NextResponse.json({ + success: true, + data: cars, + }); + } catch (error) { + console.error('Error fetching cars:', error); + return NextResponse.json( + { success: false, error: 'Failed to load available cars' }, + { status: 500 } + ); + } +} diff --git a/app/api/events/register/route.ts b/app/api/events/register/route.ts index d9f4c24..586b701 100644 --- a/app/api/events/register/route.ts +++ b/app/api/events/register/route.ts @@ -20,16 +20,15 @@ export async function POST(request: Request) { } // Validate steamId format to prevent SQL injection - const steamId = inputSteamId.trim(); // just in case + const driverGuid = steamId.trim(); // just in case - if (!/^[0-9]{15,20}$/.test(steamId)) { + if (!/^[0-9]{15,20}$/.test(driverGuid)) { return NextResponse.json( { success: false, error: "Invalid Steam ID format" }, { status: 400 } ); } - const driverGuid = steamId; console.log('Parsed driver GUID:', driverGuid); if (isNaN(driverGuid)) { return NextResponse.json( diff --git a/app/api/telemetry/route.ts b/app/api/telemetry/route.ts new file mode 100644 index 0000000..9b8a996 --- /dev/null +++ b/app/api/telemetry/route.ts @@ -0,0 +1,89 @@ +// app/api/live/telemetry/route.ts +// Server-Sent Events endpoint streaming from C++ Unix socket + +import { getTelemetryBridge } from '@/lib/telemetryBridge'; + +export const dynamic = 'force-dynamic'; +export const runtime = 'nodejs'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const serverId = searchParams.get('serverId'); + const filterServerId = serverId ? parseInt(serverId) : null; + + const encoder = new TextEncoder(); + const bridge = getTelemetryBridge(); + + const stream = new ReadableStream({ + start(controller) { + console.log('[SSE] Client connected', filterServerId ? `(server ${filterServerId})` : '(all servers)'); + + // Send initial connection message + controller.enqueue(encoder.encode(`data: ${JSON.stringify({ + type: 'connected', + bridge_status: bridge.isConnected() ? 'connected' : 'connecting' + })}\n\n`)); + + // Subscribe to telemetry updates + const unsubscribe = bridge.subscribe((packet) => { + // Filter by server if specified + if (filterServerId && packet.server_id !== filterServerId) { + return; + } + + // Transform to client format + const telemetry = { + type: 'update', + timestamp: Date.now(), + server_id: packet.server_id, + cars: packet.cars.map((car, index) => ({ + carID: car.carID, + driver_guid: car.driver_guid, + driver_name: car.driver_name, + car_model: car.car_model, + position: car.position || index + 1, + current_lap: car.current_lap, + normalizedSplinePos: car.normalizedSplinePos, + speed: Math.round(car.speed_kmh), + gear: car.gear, + rpm: car.rpm, + last_lap_time: car.last_lap_time || null, + best_lap_time: car.best_lap_time || null, + })), + }; + + try { + controller.enqueue(encoder.encode(`data: ${JSON.stringify(telemetry)}\n\n`)); + } catch (error) { + console.error('[SSE] Error sending data:', error); + } + }); + + // Heartbeat every 30 seconds + const heartbeat = setInterval(() => { + try { + controller.enqueue(encoder.encode(`: heartbeat\n\n`)); + } catch (error) { + clearInterval(heartbeat); + } + }, 30000); + + // Cleanup on connection close + request.signal.addEventListener('abort', () => { + console.log('[SSE] Client disconnected'); + unsubscribe(); + clearInterval(heartbeat); + controller.close(); + }); + }, + }); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no', // Disable nginx buffering + }, + }); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 8e101ed..186a124 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -4,6 +4,8 @@ import { query } from '@/lib/db'; import { DriverWithServer } from '@/types/racing'; import { UsersIcon, ServerIcon, ActivityIcon, MapPinIcon, FlagIcon, LiveDotIcon } from '@/components/ui/icons'; +import { cleanTrackName, cleanTrackConfig } from '@/lib/trackUtils'; +export const dynamic = "force-dynamic"; async function getConnectedDrivers(): Promise { const sql = ` @@ -130,7 +132,7 @@ export default async function DashboardPage() { ID: {server?.server_id} -

+

{server?.server_name}

diff --git a/app/events/[event_id]/page.tsx b/app/events/[event_id]/page.tsx index cc322fc..f4dd4ca 100644 --- a/app/events/[event_id]/page.tsx +++ b/app/events/[event_id]/page.tsx @@ -49,7 +49,7 @@ function formatDate(date: Date): string { export default async function EventDetailPage({ params, }: { - params: Promise<{ id: string }>; + params: Promise<{ event_id: string }>; }) { const { event_id } = await params; diff --git a/app/events/page.tsx b/app/events/page.tsx index 102b8ce..eb00b98 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -5,6 +5,7 @@ import { query } from '@/lib/db'; import { Event } from '@/types/racing'; import Link from 'next/link'; import { TrophyIcon, UsersIcon, MapPinIcon, ClockIcon, CalendarIcon } from '@/components/ui/icons'; +export const dynamic = "force-dynamic"; async function getEvents(): Promise { const sql = ` @@ -75,7 +76,7 @@ export default async function EventsPage() {
{event.event_name} diff --git a/app/globals.css b/app/globals.css index 92a734e..8a073c4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -10,10 +10,10 @@ body { h1, h2, h3, h4, h5, h6 { font-family: 'BBH Sans Bartle', sans-serif; - letter-spacing: -0.02em; + letter-spacing: +0.07em; } -span p a{ +span { letter-spacing: +0.08em; } diff --git a/app/live/page.tsx b/app/live/page.tsx new file mode 100644 index 0000000..bab7859 --- /dev/null +++ b/app/live/page.tsx @@ -0,0 +1,132 @@ +// app/live/page.tsx +// Live race view with track map and timing + +import { query } from '@/lib/db'; +import { ActivityIcon } from '@/components/ui/icons'; +import LiveSessionClient from '@/components/live/LiveSessionClient'; +export const dynamic = "force-dynamic"; + +interface LiveData { + server_id: number; + server_name: string; + server_track: string; + server_config: string; + connected_players: number; + cars: any[]; +} + +async function getLiveData(): Promise { + const sql = ` + SELECT + s.server_id, + s.server_name, + s.server_track, + s.server_config, + s.connected_players + FROM servers s + WHERE s.connected_players > 0 + ORDER BY s.connected_players DESC + `; + + const servers = await query(sql); + + // For each server, get connected cars with their positions + const liveData = await Promise.all( + servers.map(async (server: any) => { + const carsSql = ` + SELECT + u.driver_guid, + u.driver_name, + u.car_model, + u.laps_completed + FROM users u + WHERE u.current_server = $1 AND u.is_connect = true + ORDER BY u.user_rank ASC + `; + + const cars = await query(carsSql, [server.server_id]); + + // Add mock data for positions (real data will come from telemetry stream) + const carsWithPositions = cars.map((car: any, index: number) => ({ + ...car, + carID: index, + position: index + 1, + current_lap: car.laps_completed || 0, + normalizedSplinePos: Math.random(), + speed: 0, + gear: 0, + rpm: 0, + last_lap_time: null, + best_lap_time: null, + })); + + return { + ...server, + cars: carsWithPositions, + }; + }) + ); + + return liveData; +} + +export default async function LivePage() { + const liveData = await getLiveData(); + + return ( + <> + {/* Hero */} +
+
+
+
+ + LIVE TIMING +
+

+ LIVE VIEW +

+

+ Real-time race positions and telemetry from active servers +

+
+
+
+ + {/* Live Sessions */} +
+ {liveData.length === 0 ? ( +
+ +

NO ACTIVE SESSIONS

+

Join a server to see live timing

+
+ ) : ( + liveData.map((session) => ( + + )) + )} +
+ + {/* Info Box */} +
+
+

ABOUT LIVE VIEW

+
+

• Data updates in real-time from UDP telemetry stream

+

• Track positions calculated from normalized spline position (0.0 - 1.0)

+

• Lap times and gaps updated every sector

+
+
+
+ + ); +} diff --git a/app/page.tsx b/app/page.tsx index 70fe3e1..b1ebca8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import { query } from '@/lib/db'; import { TrophyIcon, ChartIcon, CircleIcon, UsersIcon, ServerIcon, ActivityIcon } from '@/components/ui/icons'; +export const dynamic = "force-dynamic"; async function getStats() { const driversOnline = await query(` diff --git a/app/rankings/page.tsx b/app/rankings/page.tsx index 163e0e4..47377a0 100644 --- a/app/rankings/page.tsx +++ b/app/rankings/page.tsx @@ -4,6 +4,7 @@ import { query } from '@/lib/db'; import { TrophyIcon } from '@/components/ui/icons'; import Link from 'next/link'; +export const dynamic = "force-dynamic"; interface RankingDriver { driver_guid: string; @@ -178,7 +179,7 @@ export default async function RankingsPage({ )} - + {driver.driver_name} diff --git a/components/events/EventRegistrationForm.tsx b/components/events/EventRegistrationForm.tsx index c15a9dc..f29ca7b 100644 --- a/components/events/EventRegistrationForm.tsx +++ b/components/events/EventRegistrationForm.tsx @@ -1,7 +1,7 @@ // components/events/EventRegistrationForm.tsx 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; export default function EventRegistrationForm({ eventId }: { eventId: number }) { @@ -12,10 +12,35 @@ export default function EventRegistrationForm({ eventId }: { eventId: number }) carSkin: '', teamName: '', }); + const [availableCars, setAvailableCars] = useState([]); const [loading, setLoading] = useState(false); + const [loadingCars, setLoadingCars] = useState(true); const [error, setError] = useState(''); const [success, setSuccess] = useState(false); + // Fetch available cars on mount + useEffect(() => { + const fetchCars = async () => { + try { + const response = await fetch('/api/events/cars'); + const data = await response.json(); + + if (data.success) { + setAvailableCars(data.data); + } else { + setError('Failed to load available cars'); + } + } catch (err) { + console.error('Error fetching cars:', err); + setError('Failed to load available cars'); + } finally { + setLoadingCars(false); + } + }; + + fetchCars(); + }, []); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); @@ -49,6 +74,13 @@ export default function EventRegistrationForm({ eventId }: { eventId: number }) } }; + // Format car name for display + const formatCarName = (carId: string) => { + return carId + .replace(/_/g, ' ') + .replace(/\b\w/g, char => char.toUpperCase()); + }; + return (
{/* Steam ID */} @@ -68,21 +100,32 @@ export default function EventRegistrationForm({ eventId }: { eventId: number })

Your Steam ID from the database

- {/* Car Model */} + {/* Car Model Dropdown */}
- setFormData({ ...formData, carModel: e.target.value })} - className="w-full px-4 py-3 bg-black border border-white/20 text-white focus:border-white focus:outline-none transition-colors font-mono" - placeholder="ks_ferrari_488_gt3" - /> -

Assetto Corsa car folder name

+ {loadingCars ? ( +
+ Loading available cars... +
+ ) : ( + + )} +

Choose from available cars for this event

{/* Car Skin */} @@ -132,7 +175,7 @@ export default function EventRegistrationForm({ eventId }: { eventId: number }) {/* Submit Button */}