feat: game info display, session car/track metadata

- Add GameInfo type + getGameInfo API call.
- Telemetry store polls /api/game-info every ~2s.
- Dashboard shows car @ track in Sim Connection section when receiving.
- SessionDetailView shows car_name and track_name in session info.
- Add car_name/track_name to UseCaseSession type.
This commit is contained in:
Jan-Peter von Hunnius 2026-07-04 22:19:35 +02:00
parent 02d3ea0515
commit 63e926f26e
5 changed files with 39 additions and 2 deletions

View file

@ -28,6 +28,7 @@ import type {
TrackSummary, TrackSummary,
BuildTrackModelRequest, BuildTrackModelRequest,
CarProfile, CarProfile,
GameInfo,
} from '@/types' } from '@/types'
const api = axios.create({ const api = axios.create({
@ -45,6 +46,11 @@ export async function getLiveTelemetry(): Promise<LiveTelemetryResponse> {
return res.data return res.data
} }
export async function getGameInfo(): Promise<GameInfo> {
const res = await api.get<GameInfo>('/game-info')
return res.data
}
// ── Recordings ── // ── Recordings ──
export async function listRecordings(): Promise<RecordingManifest[]> { export async function listRecordings(): Promise<RecordingManifest[]> {

View file

@ -15,17 +15,19 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import type { LiveTelemetryResponse, LiveFeedSnapshot } from '@/types' import type { LiveTelemetryResponse, LiveFeedSnapshot, GameInfo } from '@/types'
import { getLiveTelemetry } from '@/api/client' import { getLiveTelemetry, getGameInfo } from '@/api/client'
export const useTelemetryStore = defineStore('telemetry', () => { export const useTelemetryStore = defineStore('telemetry', () => {
const feeds = ref<LiveFeedSnapshot[]>([]) const feeds = ref<LiveFeedSnapshot[]>([])
const activeRecording = ref<string | null>(null) const activeRecording = ref<string | null>(null)
const gameInfo = ref<GameInfo>({})
const connected = ref(false) const connected = ref(false)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const lastUpdate = ref<Date | null>(null) const lastUpdate = ref<Date | null>(null)
let pollTimer: ReturnType<typeof setInterval> | null = null let pollTimer: ReturnType<typeof setInterval> | null = null
let gameInfoCounter = 0
const anyFeedAlive = computed(() => feeds.value.some((f) => f.health === 'alive')) const anyFeedAlive = computed(() => feeds.value.some((f) => f.health === 'alive'))
const anyFeedStale = computed(() => feeds.value.some((f) => f.health === 'stale')) const anyFeedStale = computed(() => feeds.value.some((f) => f.health === 'stale'))
@ -65,6 +67,12 @@ export const useTelemetryStore = defineStore('telemetry', () => {
connected.value = true connected.value = true
error.value = null error.value = null
lastUpdate.value = new Date() lastUpdate.value = new Date()
gameInfoCounter++
if (gameInfoCounter % 8 === 0) {
try {
gameInfo.value = await getGameInfo()
} catch { /* non-critical */ }
}
} catch (e: unknown) { } catch (e: unknown) {
connected.value = false connected.value = false
error.value = e instanceof Error ? e.message : 'Connection failed' error.value = e instanceof Error ? e.message : 'Connection failed'
@ -87,6 +95,7 @@ export const useTelemetryStore = defineStore('telemetry', () => {
return { return {
feeds, feeds,
activeRecording, activeRecording,
gameInfo,
connected, connected,
error, error,
lastUpdate, lastUpdate,

View file

@ -118,6 +118,8 @@ export interface UseCaseSession {
analysis: AnalysisResult | null analysis: AnalysisResult | null
guidance: string | null guidance: string | null
recording_id: string | null recording_id: string | null
car_name?: string | null
track_name?: string | null
} }
export interface CreateSessionRequest { export interface CreateSessionRequest {
@ -139,6 +141,14 @@ export interface UseCaseInfo {
guidance: string guidance: string
} }
// ── Game Info (from KSUDP handshake) ──
export interface GameInfo {
car_name?: string | null
track_name?: string | null
driver_name?: string | null
}
// ── Analysis Results ── // ── Analysis Results ──
// Shift Point Analysis (use_case = "shift_points") // Shift Point Analysis (use_case = "shift_points")

View file

@ -114,6 +114,10 @@ const aliveCount = computed(() => telemetry.feeds.filter((f) => f.health === 'al
<span class="text-success">{{ aliveCount }} feed(s) receiving data</span> <span class="text-success">{{ aliveCount }} feed(s) receiving data</span>
<span class="text-muted">·</span> <span class="text-muted">·</span>
<span class="text-secondary">{{ telemetry.totalPackets.toLocaleString() }} packets total</span> <span class="text-secondary">{{ telemetry.totalPackets.toLocaleString() }} packets total</span>
<template v-if="telemetry.gameInfo.car_name || telemetry.gameInfo.track_name">
<span class="text-muted">·</span>
<span class="text-secondary">{{ [telemetry.gameInfo.car_name, telemetry.gameInfo.track_name].filter(Boolean).join(' @ ') }}</span>
</template>
</template> </template>
<template v-else-if="telemetry.anyFeedStale"> <template v-else-if="telemetry.anyFeedStale">
<span class="text-warning">Data stream stalled</span> <span class="text-warning">Data stream stalled</span>

View file

@ -240,6 +240,14 @@ async function handleBuildTrack() {
<span class="info-label">Use Case</span> <span class="info-label">Use Case</span>
<span>{{ session.use_case.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) }}</span> <span>{{ session.use_case.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) }}</span>
</div> </div>
<div v-if="session.car_name" class="info-item">
<span class="info-label">Car</span>
<span>{{ session.car_name }}</span>
</div>
<div v-if="session.track_name" class="info-item">
<span class="info-label">Track</span>
<span>{{ session.track_name }}</span>
</div>
<div class="info-item"> <div class="info-item">
<span class="info-label">Created</span> <span class="info-label">Created</span>
<span>{{ formatTime(session.created_at) }}</span> <span>{{ formatTime(session.created_at) }}</span>