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:
parent
02d3ea0515
commit
63e926f26e
5 changed files with 39 additions and 2 deletions
|
|
@ -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[]> {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue