From 9e0b46596e67eaace86fcacf98dffa6aead1ede1 Mon Sep 17 00:00:00 2001 From: "Jan-Peter v. Hunnius" Date: Wed, 24 Jun 2026 09:32:39 +0200 Subject: [PATCH] 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) --- .gitignore | 3 + src/App.vue | 2 + src/api/client.ts | 32 ++ src/router/index.ts | 16 +- src/stores/tracks.ts | 62 ++++ src/types/index.ts | 82 ++++- src/views/CharacteristicsView.vue | 174 ++++++++++ src/views/DashboardView.vue | 7 +- src/views/SessionDetailView.vue | 72 +++- src/views/SessionsView.vue | 2 +- src/views/TrackDetailView.vue | 545 ++++++++++++++++++++++++++++++ src/views/TrackMapView.vue | 330 ------------------ src/views/TracksView.vue | 116 +++++++ 13 files changed, 1098 insertions(+), 345 deletions(-) create mode 100644 src/stores/tracks.ts create mode 100644 src/views/CharacteristicsView.vue create mode 100644 src/views/TrackDetailView.vue delete mode 100644 src/views/TrackMapView.vue create mode 100644 src/views/TracksView.vue diff --git a/.gitignore b/.gitignore index 67a0cde..33bbb36 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ dist/ # Environment files with secrets .env.local .env.*.local + +# Agent notes +AGENTS.md diff --git a/src/App.vue b/src/App.vue index f3240a5..bd94102 100644 --- a/src/App.vue +++ b/src/App.vue @@ -9,6 +9,8 @@ const navItems = [ { path: '/', label: 'Dashboard', icon: '๐Ÿ ' }, { path: '/live', label: 'Live', icon: '๐Ÿ“ก' }, { path: '/sessions', label: 'Sessions', icon: '๐Ÿ“Š' }, + { path: '/tracks', label: 'Tracks', icon: '๐Ÿ—บ' }, + { path: '/characteristics', label: 'Characteristics', icon: '๐Ÿญ' }, ] diff --git a/src/api/client.ts b/src/api/client.ts index c5918eb..483284a 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -9,6 +9,10 @@ import type { SessionRun, UseCaseInfo, AnalysisResult, + TrackModel, + TrackSummary, + BuildTrackModelRequest, + CarProfile, } from '@/types' const api = axios.create({ @@ -124,4 +128,32 @@ export async function getGames(): Promise { return res.data } +// โ”€โ”€ Tracks โ”€โ”€ + +export async function listTracks(): Promise { + const res = await api.get('/tracks') + return res.data +} + +export async function getTrack(game: string, trackName: string): Promise { + const res = await api.get(`/tracks/${encodeURIComponent(game)}/${encodeURIComponent(trackName)}`) + return res.data +} + +export async function deleteTrack(game: string, trackName: string): Promise { + await api.delete(`/tracks/${encodeURIComponent(game)}/${encodeURIComponent(trackName)}`) +} + +export async function buildTrackModel(req: BuildTrackModelRequest): Promise { + const res = await api.post('/tracks/build', req) + return res.data +} + +// โ”€โ”€ Characteristics โ”€โ”€ + +export async function listCarProfiles(): Promise { + const res = await api.get('/characteristics/cars') + return res.data +} + export default api diff --git a/src/router/index.ts b/src/router/index.ts index cfe0fee..865a1e7 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -31,11 +31,21 @@ const router = createRouter({ props: true, }, { - path: '/analysis/track-map/:id', - name: 'track-map', - component: () => import('@/views/TrackMapView.vue'), + path: '/tracks', + name: 'tracks', + component: () => import('@/views/TracksView.vue'), + }, + { + path: '/tracks/:game/:trackName', + name: 'track-detail', + component: () => import('@/views/TrackDetailView.vue'), props: true, }, + { + path: '/characteristics', + name: 'characteristics', + component: () => import('@/views/CharacteristicsView.vue'), + }, ], }) diff --git a/src/stores/tracks.ts b/src/stores/tracks.ts new file mode 100644 index 0000000..9d34c7f --- /dev/null +++ b/src/stores/tracks.ts @@ -0,0 +1,62 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import type { TrackModel, TrackSummary } from '@/types' +import * as api from '@/api/client' + +export const useTracksStore = defineStore('tracks', () => { + const tracks = ref([]) + const currentTrack = ref(null) + const loading = ref(false) + const error = ref(null) + + async function fetchTracks() { + loading.value = true + error.value = null + try { + tracks.value = await api.listTracks() + } catch (e: unknown) { + error.value = e instanceof Error ? e.message : 'Failed to load tracks' + } finally { + loading.value = false + } + } + + async function fetchTrack(game: string, trackName: string) { + loading.value = true + error.value = null + try { + currentTrack.value = await api.getTrack(game, trackName) + } catch (e: unknown) { + error.value = e instanceof Error ? e.message : 'Failed to load track' + } finally { + loading.value = false + } + } + + async function deleteTrack(game: string, trackName: string) { + try { + await api.deleteTrack(game, trackName) + tracks.value = tracks.value.filter( + (t) => !(t.game === game && t.track_name === trackName), + ) + if ( + currentTrack.value?.game === game && + currentTrack.value?.track_name === trackName + ) { + currentTrack.value = null + } + } catch (e: unknown) { + error.value = e instanceof Error ? e.message : 'Failed to delete track' + } + } + + return { + tracks, + currentTrack, + loading, + error, + fetchTracks, + fetchTrack, + deleteTrack, + } +}) diff --git a/src/types/index.ts b/src/types/index.ts index 7edd1ec..b0b9954 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -9,14 +9,14 @@ export type UseCase = | 'braking_analysis' | 'lap_times' | 'sector_analysis' - | 'track_map' + | 'track_mapping' | 'general' export type Game = | 'assetto_corsa' | 'assetto_corsa_competizione' - | 'project_cars1' - | 'project_cars2' + | 'project_cars_1' + | 'project_cars_2' export type RecordingStatus = 'recording' | 'stopped' export type SessionStatus = 'active' | 'completed' @@ -161,7 +161,7 @@ export interface ShiftPointAnalysis { total_frames_analyzed: number } -// Track Map Analysis (use_case = "track_map") +// Track Map Analysis (use_case = "track_mapping") export interface TrackPoint { x: number y: number @@ -187,5 +187,77 @@ export interface NotImplementedResponse { use_case: string } -// Union of all analysis result types โ€” matches backend discriminant responses +// โ”€โ”€ Track Models (persistent, from /api/tracks) โ”€โ”€ + +export interface Corner { + index: number + number: number + entry_s: number + apex_s: number + exit_s: number + curvature_peak: number +} + +export interface Sector { + index: number + start_s: number + end_s: number +} + +export type TrackModelSource = 'boundary_recording' + +export interface TrackModel { + game: string + track_name: string + center_line: number[][] + left_boundary: number[][] + right_boundary: number[][] + track_width: number[] + heading: number[] + curvature: number[] + corners: Corner[] + sectors: Sector[] + source: TrackModelSource + created_at: string +} + +export interface TrackSummary { + game: string + track_name: string + source: TrackModelSource + created_at: string + corner_count: number +} + +export interface BuildTrackModelRequest { + session_id: string + game?: string | null + track_name?: string | null +} + +export type TrackMappingAnalysis = TrackMapAnalysis + export type AnalysisResult = ShiftPointAnalysis | TrackMapAnalysis | NotImplementedResponse + +// โ”€โ”€ Characteristics (aggregated across sessions) โ”€โ”€ + +export interface CarGearProfile { + gear: number + gear_display: string + min_rpm: number + max_rpm: number + min_speed_kmh: number + max_speed_kmh: number + session_count: number +} + +export interface CarProfile { + car_name: string + session_count: number + recommended_shift_rpm_avg: number | null + recommended_shift_rpm_min: number | null + recommended_shift_rpm_max: number | null + gear_profiles: CarGearProfile[] + last_session_id: string + last_session_date: string +} diff --git a/src/views/CharacteristicsView.vue b/src/views/CharacteristicsView.vue new file mode 100644 index 0000000..51c570e --- /dev/null +++ b/src/views/CharacteristicsView.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 6449276..19bee24 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -29,7 +29,7 @@ function useCaseLabel(uc: string): string { braking_analysis: 'Braking Analysis', lap_times: 'Lap Times', sector_analysis: 'Sector Analysis', - track_map: 'Track Map', + track_mapping: 'Track Mapping', general: 'General', } return labels[uc] ?? uc @@ -164,8 +164,9 @@ const aliveCount = computed(() => telemetry.feeds.filter((f) => f.health === 'al

Quick Actions

- ๐Ÿ“ก Live Telemetry - ๐Ÿ“Š Sessions + Live Telemetry + Sessions + Tracks
diff --git a/src/views/SessionDetailView.vue b/src/views/SessionDetailView.vue index a1e11f7..fcc27b5 100644 --- a/src/views/SessionDetailView.vue +++ b/src/views/SessionDetailView.vue @@ -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(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 + } +} diff --git a/src/views/SessionsView.vue b/src/views/SessionsView.vue index 1eb05c7..d2ce782 100644 --- a/src/views/SessionsView.vue +++ b/src/views/SessionsView.vue @@ -12,7 +12,7 @@ const newLabel = ref('') const useCaseOptions: { value: UseCase; label: string }[] = [ { value: 'shift_points', label: 'Shift Points' }, - { value: 'track_map', label: 'Track Map' }, + { value: 'track_mapping', label: 'Track Mapping' }, { value: 'racing_line', label: 'Racing Line' }, { value: 'braking_analysis', label: 'Braking Analysis' }, { value: 'lap_times', label: 'Lap Times' }, diff --git a/src/views/TrackDetailView.vue b/src/views/TrackDetailView.vue new file mode 100644 index 0000000..6cd58c0 --- /dev/null +++ b/src/views/TrackDetailView.vue @@ -0,0 +1,545 @@ + + + + + diff --git a/src/views/TrackMapView.vue b/src/views/TrackMapView.vue deleted file mode 100644 index b78f7b9..0000000 --- a/src/views/TrackMapView.vue +++ /dev/null @@ -1,330 +0,0 @@ - - - - - diff --git a/src/views/TracksView.vue b/src/views/TracksView.vue new file mode 100644 index 0000000..20d049c --- /dev/null +++ b/src/views/TracksView.vue @@ -0,0 +1,116 @@ + + + + +