feat: persistent sessions support, remove track_map, add characteristics + tracks views

- Remove obsolete track_map use case from types, views, and router
- Delete TrackMapView.vue (superseded by persistent TrackDetailView)
- Auto-analyze in handleBuildTrack before building track model
- Add CharacteristicsView: expandable car cards with aggregated shift-point
  profiles (gear RPM/speed ranges, recommended shift RPM)
- Add TracksView + TrackDetailView + tracks Pinia store (Canvas 2D geometry)
- Add characteristics API client functions (listCarProfiles)
- Add tracks and characteristics nav items and routes
- Update Game type values to match backend snake_case (project_cars_1/2)
- Update use_case types and labels (remove track_map, keep track_mapping)
This commit is contained in:
Jan-Peter von Hunnius 2026-06-24 09:32:39 +02:00
parent a6af6e4b98
commit 9e0b46596e
13 changed files with 1098 additions and 345 deletions

View file

@ -4,6 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useSessionsStore } from '@/stores/sessions'
import type { ShiftPointAnalysis, TrackMapAnalysis } from '@/types'
import { isShiftPointAnalysis, isTrackMapAnalysis } from '@/utils/analysis'
import * as api from '@/api/client'
const props = defineProps<{
id: string
@ -100,8 +101,6 @@ function viewAnalysis() {
if (!session.value) return
if (session.value.use_case === 'shift_points') {
router.push({ name: 'shift-points', params: { id: session.value.id } })
} else if (session.value.use_case === 'track_map') {
router.push({ name: 'track-map', params: { id: session.value.id } })
}
}
@ -121,6 +120,37 @@ async function handleDeleteRun(runId: string, runLabel: string) {
async function handleReopen() {
await store.reopenSession(sessionId.value)
}
const buildingTrack = ref(false)
const buildTrackError = ref<string | null>(null)
const buildTrackSuccess = ref(false)
const isTrackMapping = computed(() => session.value?.use_case === 'track_mapping')
const hasLeftRun = computed(() =>
session.value?.runs.some((r) => r.label?.toLowerCase() === 'left' && r.status === 'stopped') ?? false,
)
const hasRightRun = computed(() =>
session.value?.runs.some((r) => r.label?.toLowerCase() === 'right' && r.status === 'stopped') ?? false,
)
const canBuildTrack = computed(() => isCompleted.value && hasLeftRun.value && hasRightRun.value)
async function handleBuildTrack() {
buildingTrack.value = true
buildTrackError.value = null
buildTrackSuccess.value = false
try {
await store.reAnalyze(sessionId.value)
await api.buildTrackModel({ session_id: sessionId.value })
buildTrackSuccess.value = true
} catch (e: unknown) {
buildTrackError.value = e instanceof Error ? e.message : 'Failed to build track model'
} finally {
buildingTrack.value = false
}
}
</script>
<template>
@ -310,7 +340,7 @@ async function handleReopen() {
</div>
<div class="flex gap-2">
<button class="btn btn-primary" @click="viewAnalysis">
<button v-if="session.use_case === 'shift_points'" class="btn btn-primary" @click="viewAnalysis">
📊 View Full Analysis
</button>
<button class="btn btn-secondary" @click="handleReAnalyze">
@ -329,6 +359,42 @@ async function handleReopen() {
</button>
</div>
</section>
<!-- Track Mapping: Build Track Model -->
<section v-if="isTrackMapping && isCompleted" class="card mb-3">
<div class="card-header">
<h2>Build Track Model</h2>
</div>
<div v-if="canBuildTrack">
<p class="text-secondary mb-2" style="font-size: 0.85rem">
Both boundary runs (left + right) are recorded. Build the persistent track model to enable corner analysis and sector timing.
</p>
<button
class="btn btn-primary"
:disabled="buildingTrack"
@click="handleBuildTrack"
>
{{ buildingTrack ? 'Building...' : 'Build Track Model' }}
</button>
<div v-if="buildTrackSuccess" class="text-success mt-2" style="font-size: 0.85rem">
Track model built successfully. <RouterLink to="/tracks">View in Tracks →</RouterLink>
</div>
<div v-if="buildTrackError" class="text-error mt-2" style="font-size: 0.85rem">
{{ buildTrackError }}
</div>
</div>
<div v-else class="text-secondary" style="font-size: 0.85rem">
<p>Requires 2 completed runs labeled "left" and "right".</p>
<div class="flex gap-2 mt-1">
<span :class="hasLeftRun ? 'text-success' : 'text-muted'">
{{ hasLeftRun ? '✓' : '○' }} Left boundary
</span>
<span :class="hasRightRun ? 'text-success' : 'text-muted'">
{{ hasRightRun ? '✓' : '○' }} Right boundary
</span>
</div>
</div>
</section>
</template>
</div>
</template>