From 3fa5a1441a9f117299485df48217dcda37520d5b Mon Sep 17 00:00:00 2001 From: "Jan-Peter v. Hunnius" Date: Sat, 6 Jun 2026 08:16:10 +0200 Subject: [PATCH 01/10] feat: add RPM vs. Force and Speed vs. RPM charts to shift point analysis --- src/views/ShiftPointsView.vue | 79 +++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/src/views/ShiftPointsView.vue b/src/views/ShiftPointsView.vue index 06f76ed..49f9d5b 100644 --- a/src/views/ShiftPointsView.vue +++ b/src/views/ShiftPointsView.vue @@ -43,10 +43,41 @@ onMounted(async () => { } }) +const colors = ['#4f8ff7', '#34d399', '#fbbf24', '#f87171', '#a78bfa', '#60a5fa', '#f472b6'] + +const rpmVsAccelChartData = computed(() => { + if (!analysis.value) return null + const curves = analysis.value.gear_curves.filter((g) => g.gear >= 2) + return { + datasets: curves.map((curve, i) => ({ + label: `${curve.gear_display} (${curve.run_label})`, + data: curve.data_points.map((p) => ({ x: p.rpm, y: p.acceleration_g })), + borderColor: colors[i % colors.length], + backgroundColor: colors[i % colors.length] + '20', + showLine: true, + fill: false, + tension: 0.3, + pointRadius: 0, + borderWidth: 2, + })), + } +}) + +const rpmVsAccelChartOptions = { + responsive: true, + plugins: { + legend: { labels: { color: '#8b8fa3' } }, + title: { display: true, text: 'RPM vs. Force', color: '#e2e4ea' }, + }, + scales: { + x: { ticks: { color: '#8b8fa3' }, grid: { color: '#2a2e3f' }, title: { display: true, text: 'RPM', color: '#8b8fa3' } }, + y: { ticks: { color: '#8b8fa3' }, grid: { color: '#2a2e3f' }, title: { display: true, text: 'Force (g)', color: '#8b8fa3' } }, + }, +} + const gearCurvesChartData = computed(() => { if (!analysis.value) return null const curves = analysis.value.gear_curves.filter((g) => g.gear >= 2) - const colors = ['#4f8ff7', '#34d399', '#fbbf24', '#f87171', '#a78bfa', '#60a5fa', '#f472b6'] return { datasets: curves.map((curve, i) => ({ label: `${curve.gear_display} (${curve.run_label})`, @@ -66,11 +97,41 @@ const gearCurvesOptions = { responsive: true, plugins: { legend: { labels: { color: '#8b8fa3' } }, - title: { display: true, text: 'Gear Acceleration Curves', color: '#e2e4ea' }, + title: { display: true, text: 'Speed vs. Force', color: '#e2e4ea' }, }, scales: { x: { ticks: { color: '#8b8fa3' }, grid: { color: '#2a2e3f' }, title: { display: true, text: 'Speed (km/h)', color: '#8b8fa3' } }, - y: { ticks: { color: '#8b8fa3' }, grid: { color: '#2a2e3f' }, title: { display: true, text: 'Acceleration (g)', color: '#8b8fa3' } }, + y: { ticks: { color: '#8b8fa3' }, grid: { color: '#2a2e3f' }, title: { display: true, text: 'Force (g)', color: '#8b8fa3' } }, + }, +} + +const speedVsRpmChartData = computed(() => { + if (!analysis.value) return null + const curves = analysis.value.gear_curves.filter((g) => g.gear >= 2) + return { + datasets: curves.map((curve, i) => ({ + label: `${curve.gear_display} (${curve.run_label})`, + data: curve.data_points.map((p) => ({ x: p.speed_kmh, y: p.rpm })), + borderColor: colors[i % colors.length], + backgroundColor: colors[i % colors.length] + '20', + showLine: true, + fill: false, + tension: 0.3, + pointRadius: 0, + borderWidth: 2, + })), + } +}) + +const speedVsRpmChartOptions = { + responsive: true, + plugins: { + legend: { labels: { color: '#8b8fa3' } }, + title: { display: true, text: 'Speed vs. RPM (Gear Ratios)', color: '#e2e4ea' }, + }, + scales: { + x: { ticks: { color: '#8b8fa3' }, grid: { color: '#2a2e3f' }, title: { display: true, text: 'Speed (km/h)', color: '#8b8fa3' } }, + y: { ticks: { color: '#8b8fa3' }, grid: { color: '#2a2e3f' }, title: { display: true, text: 'RPM', color: '#8b8fa3' } }, }, } @@ -157,11 +218,21 @@ const crossoverChartOptions = { - + +
+ +
+ +
+ +
+ +
+
From a6af6e4b985e09e7dbf7f4466370cfdb005aac3f Mon Sep 17 00:00:00 2001 From: "Jan-Peter v. Hunnius" Date: Sat, 6 Jun 2026 10:20:43 +0200 Subject: [PATCH 02/10] feat: add run deletion, session reopen, and feed merging UI --- src/api/client.ts | 9 +++++ src/stores/sessions.ts | 36 +++++++++++++++++++ src/types/index.ts | 1 + src/views/SessionDetailView.vue | 61 +++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/src/api/client.ts b/src/api/client.ts index c46563c..c5918eb 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -88,6 +88,15 @@ export async function stopRun(sessionId: string, runId: string): Promise { + await api.delete(`/sessions/${sessionId}/runs/${runId}`) +} + +export async function reopenSession(id: string): Promise { + const res = await api.post(`/sessions/${id}/reopen`) + return res.data +} + export async function completeSession(id: string): Promise { const res = await api.post(`/sessions/${id}/complete`) return res.data diff --git a/src/stores/sessions.ts b/src/stores/sessions.ts index 606a40f..fd1c62b 100644 --- a/src/stores/sessions.ts +++ b/src/stores/sessions.ts @@ -119,6 +119,40 @@ export const useSessionsStore = defineStore('sessions', () => { } } + async function deleteRun(sessionId: string, runId: string): Promise { + try { + await api.deleteRun(sessionId, runId) + if (currentSession.value?.id === sessionId) { + currentSession.value.runs = currentSession.value.runs.filter((r) => r.id !== runId) + const sIdx = sessions.value.findIndex((s) => s.id === sessionId) + if (sIdx !== -1) { + sessions.value[sIdx] = { ...sessions.value[sIdx], runs: [...currentSession.value.runs] } + } + } + return true + } catch (e: unknown) { + error.value = e instanceof Error ? e.message : 'Failed to delete run' + return false + } + } + + async function reopenSession(id: string): Promise { + try { + const session = await api.reopenSession(id) + const idx = sessions.value.findIndex((s) => s.id === id) + if (idx !== -1) { + sessions.value[idx] = session + } + if (currentSession.value?.id === id) { + currentSession.value = session + } + return session + } catch (e: unknown) { + error.value = e instanceof Error ? e.message : 'Failed to reopen session' + return null + } + } + return { sessions, currentSession, @@ -130,7 +164,9 @@ export const useSessionsStore = defineStore('sessions', () => { deleteSession, startRun, stopRun, + deleteRun, completeSession, + reopenSession, reAnalyze, } }) diff --git a/src/types/index.ts b/src/types/index.ts index e1d4b56..7edd1ec 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -112,6 +112,7 @@ export interface CreateSessionRequest { export interface StartRunRequest { feed_name: string label?: string | null + merge_feeds?: string[] } // ── Use Cases ── diff --git a/src/views/SessionDetailView.vue b/src/views/SessionDetailView.vue index c121443..a1e11f7 100644 --- a/src/views/SessionDetailView.vue +++ b/src/views/SessionDetailView.vue @@ -16,12 +16,18 @@ const store = useSessionsStore() const showStartRun = ref(false) const runFeedName = ref('ksudp') const runLabel = ref('') +const runMergeEnabled = ref(false) +const runMergeFeeds = ref([]) const sessionId = computed(() => props.id ?? (route.params.id as string)) const session = computed(() => store.currentSession) const feedOptions = ['ksudp', 'telemetry_tool', 'pcars'] +const mergeFeedOptions = computed(() => { + return feedOptions.filter((f) => f !== runFeedName.value) +}) + const hasActiveRun = computed(() => { return session.value?.runs.some((r) => r.status === 'recording') ?? false }) @@ -61,10 +67,13 @@ async function handleStartRun() { const run = await store.startRun(sessionId.value, { feed_name: runFeedName.value, label: runLabel.value || undefined, + merge_feeds: runMergeEnabled.value && runMergeFeeds.value.length > 0 ? runMergeFeeds.value : undefined, }) if (run) { showStartRun.value = false runLabel.value = '' + runMergeEnabled.value = false + runMergeFeeds.value = [] } } @@ -102,6 +111,16 @@ async function handleDelete() { router.push('/sessions') } } + +async function handleDeleteRun(runId: string, runLabel: string) { + if (confirm(`Delete run "${runLabel || 'Untitled Run'}"?`)) { + await store.deleteRun(sessionId.value, runId) + } +} + +async function handleReopen() { + await store.reopenSession(sessionId.value) +} 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 @@ + + + + + From 7bda8b4af1293c4409b5b80ca6cd5c289960b245 Mon Sep 17 00:00:00 2001 From: "Jan-Peter v. Hunnius" Date: Wed, 24 Jun 2026 09:39:59 +0200 Subject: [PATCH 04/10] docs: update README and AGENTS.md for characteristics, tracks, endpoint changes - Add Car Characteristics and Track Mapping feature sections - Update project structure (8 routes, tracks store, new views) - Update API contract (23 endpoints, add characteristics/tracks/build-track-model) - Add Features and State Management sections to AGENTS.md --- AGENTS.md | 19 ++++++++++++++++++- README.md | 25 +++++++++++++++++++------ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9d5b17b..da152fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,24 @@ Use the `joplin_get_note` tool with note ID `6c0dcb2a567348fd9796f50c790082e4` t ## API Communication -Racecraft communicates exclusively with the rusty-telemetry REST API. In development, Vite proxies `/api/*` to `http://127.0.0.1:8080`. See the rusty-telemetry README for full endpoint documentation. +Racecraft communicates exclusively with the rusty-telemetry REST API (23 endpoints). In development, Vite proxies `/api/*` to `http://127.0.0.1:8080`. See the rusty-telemetry README for full endpoint documentation. + +## Features + +- **Dashboard** — system status, live feed cards, recent sessions +- **Live Telemetry** — real-time gauges (speed, RPM, gear, inputs), per-feed status +- **Sessions** — create/complete/reopen sessions, start/stop/delete runs, view analysis +- **Shift Points** — gear RPM range charts, shift event timeline, recommended shift RPM +- **Car Characteristics** — aggregated shift-point profiles per car across all sessions +- **Track Mapping** — canvas-rendered track boundaries, persistent track models + +## State Management (Pinia Stores) + +| Store | Purpose | +|-------|---------| +| `stores/telemetry.ts` | Live telemetry polling + derived status | +| `stores/sessions.ts` | Session CRUD, run management, analysis retrieval | +| `stores/tracks.ts` | Track model list + detail | ## Commands diff --git a/README.md b/README.md index eaf1768..0ca89da 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Web-based sim racing telemetry analysis client. Connects to [rusty-telemetry](ht - Polling at 250ms for responsive updates ### Session Management -- Create analysis sessions with use case selection (Shift Points, Track Map, etc.) +- Create analysis sessions with use case selection (Shift Points, Track Mapping) - Start/stop runs within a session to capture telemetry data - Complete sessions to trigger automatic analysis - Session detail view with run history and status @@ -59,10 +59,16 @@ Web-based sim racing telemetry analysis client. Connects to [rusty-telemetry](ht - Recommended shift RPM from aggregated data - Detailed shift event table -### Track Map +### Car Characteristics +- Aggregated shift-point profiles per car across all sessions +- Expandable car cards showing per-gear RPM/speed ranges +- Recommended shift RPM per car + +### Track Mapping - Canvas-rendered track boundaries from coordinate data - Center line computation from left/right boundary runs - Boundary legend with point counts +- Persistent track models with track list and detail views - Responsive canvas that redraws on window resize ## Tech Stack @@ -137,19 +143,22 @@ racecraft/ ├── src/ │ ├── main.ts # App bootstrap (Vue + Pinia + Router) │ ├── App.vue # Root layout with navigation -│ ├── router/index.ts # 6 routes with lazy loading +│ ├── router/index.ts # 8 routes with lazy loading │ ├── types/index.ts # TypeScript interfaces matching API JSON shapes │ ├── api/client.ts # Typed Axios client (all API endpoints) │ ├── stores/ │ │ ├── telemetry.ts # Live telemetry with polling + derived status -│ │ └── sessions.ts # Session CRUD, run management, analysis +│ │ ├── sessions.ts # Session CRUD, run management, analysis +│ │ └── tracks.ts # Track model list + detail │ ├── views/ │ │ ├── DashboardView.vue # System status, feeds, quick actions │ │ ├── LiveView.vue # Real-time gauges and telemetry │ │ ├── SessionsView.vue # Session list with create/delete │ │ ├── SessionDetailView.vue # Run workflow, complete, analyze │ │ ├── ShiftPointsView.vue # RPM charts and shift event table -│ │ └── TrackMapView.vue # Canvas track map renderer +│ │ ├── CharacteristicsView.vue # Car characteristics aggregation +│ │ ├── TracksView.vue # Track model list +│ │ └── TrackDetailView.vue # Canvas track map renderer │ ├── components/ │ │ └── FeedStatus.vue # Reusable feed card with health + telemetry │ └── assets/ @@ -160,7 +169,7 @@ racecraft/ ## API Contract -Racecraft consumes the rusty-telemetry REST API (19 endpoints). See the [rusty-telemetry README](https://codeberg.org/jhunnius/rusty-telemetry) for full endpoint documentation. +Racecraft consumes the rusty-telemetry REST API (23 endpoints). See the [rusty-telemetry README](https://codeberg.org/jhunnius/rusty-telemetry) for full endpoint documentation. Key endpoints used: - `GET /api/live` — polled every 250ms–1000ms for live telemetry @@ -168,6 +177,10 @@ Key endpoints used: - `POST /api/sessions/{id}/runs` / `POST /api/sessions/{id}/runs/{run_id}/stop` — run capture - `POST /api/sessions/{id}/complete` — complete session and trigger analysis - `GET /api/sessions/{id}/analysis` — retrieve analysis results +- `GET /api/characteristics/cars` — aggregated car shift-point profiles +- `GET /api/characteristics/cars/{car}` — detailed per-gear data for a car +- `GET /api/tracks` / `GET /api/tracks/{track}` — track model list and detail +- `POST /api/sessions/{id}/build-track-model` — build track model from session ## License From c6bcab486cad09e643392ca0e01b791520835c57 Mon Sep 17 00:00:00 2001 From: "Jan-Peter v. Hunnius" Date: Wed, 24 Jun 2026 09:46:42 +0200 Subject: [PATCH 05/10] docs: fix endpoint count (28), correct track endpoint paths --- AGENTS.md | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index da152fd..19fb50d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,7 @@ Use the `joplin_get_note` tool with note ID `6c0dcb2a567348fd9796f50c790082e4` t ## API Communication -Racecraft communicates exclusively with the rusty-telemetry REST API (23 endpoints). In development, Vite proxies `/api/*` to `http://127.0.0.1:8080`. See the rusty-telemetry README for full endpoint documentation. +Racecraft communicates exclusively with the rusty-telemetry REST API (28 endpoints). In development, Vite proxies `/api/*` to `http://127.0.0.1:8080`. See the rusty-telemetry README for full endpoint documentation. ## Features diff --git a/README.md b/README.md index 0ca89da..7409a87 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ racecraft/ ## API Contract -Racecraft consumes the rusty-telemetry REST API (23 endpoints). See the [rusty-telemetry README](https://codeberg.org/jhunnius/rusty-telemetry) for full endpoint documentation. +Racecraft consumes the rusty-telemetry REST API (28 endpoints). See the [rusty-telemetry README](https://codeberg.org/jhunnius/rusty-telemetry) for full endpoint documentation. Key endpoints used: - `GET /api/live` — polled every 250ms–1000ms for live telemetry @@ -179,8 +179,8 @@ Key endpoints used: - `GET /api/sessions/{id}/analysis` — retrieve analysis results - `GET /api/characteristics/cars` — aggregated car shift-point profiles - `GET /api/characteristics/cars/{car}` — detailed per-gear data for a car -- `GET /api/tracks` / `GET /api/tracks/{track}` — track model list and detail -- `POST /api/sessions/{id}/build-track-model` — build track model from session +- `GET /api/tracks` / `GET /api/tracks/{game}/{track_name}` — track model list and detail +- `POST /api/tracks/build` — build track model from completed session ## License From 43ffedbf789e88d5fd063b86ccf0662b85288a74 Mon Sep 17 00:00:00 2001 From: "Jan-Peter v. Hunnius" Date: Wed, 24 Jun 2026 10:18:23 +0200 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20GripAnalysisView=20=E2=80=94=20ca?= =?UTF-8?q?r=20grip=20envelope=20visualization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New GripAnalysisView with 4 scatter charts: 1. Grip Envelope (peak G vs speed: lateral/accel/brake) 2. Grip Circle (longitudinal vs lateral G scatter) 3. Cornering Grip vs Speed 4. Steering Response (steer angle vs lateral G) - Summary stats: max lateral/accel/brake/combined G, peak lateral speed - GripAnalysis types + type guard - car_grip use case in SessionDetailView (quick stats + view button) - Route /analysis/grip/:id - Global .grid-4 CSS utility --- src/assets/main.css | 6 +- src/router/index.ts | 6 + src/types/index.ts | 40 ++++- src/utils/analysis.ts | 10 +- src/views/GripAnalysisView.vue | 300 ++++++++++++++++++++++++++++++++ src/views/SessionDetailView.vue | 36 +++- 6 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 src/views/GripAnalysisView.vue diff --git a/src/assets/main.css b/src/assets/main.css index 432fcec..0a5bff9 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -196,8 +196,12 @@ input, select, textarea { grid-template-columns: repeat(3, 1fr); } +.grid-4 { + grid-template-columns: repeat(4, 1fr); +} + @media (max-width: 768px) { - .grid-2, .grid-3 { + .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } } diff --git a/src/router/index.ts b/src/router/index.ts index 865a1e7..905216e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -30,6 +30,12 @@ const router = createRouter({ component: () => import('@/views/ShiftPointsView.vue'), props: true, }, + { + path: '/analysis/grip/:id', + name: 'grip-analysis', + component: () => import('@/views/GripAnalysisView.vue'), + props: true, + }, { path: '/tracks', name: 'tracks', diff --git a/src/types/index.ts b/src/types/index.ts index b0b9954..5d3fbc2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,6 +5,7 @@ export type UseCase = | 'shift_points' + | 'car_grip' | 'racing_line' | 'braking_analysis' | 'lap_times' @@ -161,6 +162,43 @@ export interface ShiftPointAnalysis { total_frames_analyzed: number } +// Car Grip Analysis (use_case = "car_grip") +export interface GripEnvelopePoint { + speed_kmh: number + max_lateral_g: number + max_accel_g: number + max_brake_g: number + sample_count: number +} + +export interface GripCirclePoint { + longitudinal_g: number + lateral_g: number +} + +export interface SteeringResponsePoint { + steer: number + lateral_g: number + speed_kmh: number +} + +export interface GripSummary { + max_lateral_g: number + max_accel_g: number + max_brake_g: number + max_combined_g: number + peak_lateral_speed_kmh: number +} + +export interface CarGripAnalysis { + summary: GripSummary + grip_envelope: GripEnvelopePoint[] + grip_circle: GripCirclePoint[] + steering_response: SteeringResponsePoint[] + runs_analyzed: number + total_frames_analyzed: number +} + // Track Map Analysis (use_case = "track_mapping") export interface TrackPoint { x: number @@ -237,7 +275,7 @@ export interface BuildTrackModelRequest { export type TrackMappingAnalysis = TrackMapAnalysis -export type AnalysisResult = ShiftPointAnalysis | TrackMapAnalysis | NotImplementedResponse +export type AnalysisResult = ShiftPointAnalysis | CarGripAnalysis | TrackMapAnalysis | NotImplementedResponse // ── Characteristics (aggregated across sessions) ── diff --git a/src/utils/analysis.ts b/src/utils/analysis.ts index 370f80f..9debb18 100644 --- a/src/utils/analysis.ts +++ b/src/utils/analysis.ts @@ -1,4 +1,4 @@ -import type { ShiftPointAnalysis, TrackMapAnalysis } from '@/types' +import type { ShiftPointAnalysis, CarGripAnalysis, TrackMapAnalysis } from '@/types' /** * Runtime type guard: checks if an unknown value is a ShiftPointAnalysis. @@ -8,6 +8,14 @@ export function isShiftPointAnalysis(data: unknown): data is ShiftPointAnalysis return typeof data === 'object' && data !== null && 'gear_curves' in data } +/** + * Runtime type guard: checks if an unknown value is a CarGripAnalysis. + * Uses the 'grip_envelope' discriminator field. + */ +export function isCarGripAnalysis(data: unknown): data is CarGripAnalysis { + return typeof data === 'object' && data !== null && 'grip_envelope' in data +} + /** * Runtime type guard: checks if an unknown value is a TrackMapAnalysis. * Uses the 'boundaries' discriminator field that the backend includes. diff --git a/src/views/GripAnalysisView.vue b/src/views/GripAnalysisView.vue new file mode 100644 index 0000000..6114d25 --- /dev/null +++ b/src/views/GripAnalysisView.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/src/views/SessionDetailView.vue b/src/views/SessionDetailView.vue index fcc27b5..0e8b2f2 100644 --- a/src/views/SessionDetailView.vue +++ b/src/views/SessionDetailView.vue @@ -2,8 +2,8 @@ import { ref, onMounted, computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useSessionsStore } from '@/stores/sessions' -import type { ShiftPointAnalysis, TrackMapAnalysis } from '@/types' -import { isShiftPointAnalysis, isTrackMapAnalysis } from '@/utils/analysis' +import type { ShiftPointAnalysis, CarGripAnalysis, TrackMapAnalysis } from '@/types' +import { isShiftPointAnalysis, isCarGripAnalysis, isTrackMapAnalysis } from '@/utils/analysis' import * as api from '@/api/client' const props = defineProps<{ @@ -56,6 +56,14 @@ const trackAnalysis = computed((): TrackMapAnalysis | null => { return null }) +const gripAnalysis = computed((): CarGripAnalysis | null => { + if (!analysisData.value) return null + if (isCarGripAnalysis(analysisData.value)) { + return analysisData.value + } + return null +}) + onMounted(async () => { await store.fetchSession(sessionId.value) }) @@ -101,6 +109,8 @@ 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 === 'car_grip') { + router.push({ name: 'grip-analysis', params: { id: session.value.id } }) } } @@ -339,8 +349,28 @@ async function handleBuildTrack() {
+ +
+
+ Max Lateral G + {{ gripAnalysis.summary.max_lateral_g.toFixed(2) }} +
+
+ Max Accel G + {{ gripAnalysis.summary.max_accel_g.toFixed(2) }} +
+
+ Max Brake G + {{ gripAnalysis.summary.max_brake_g.toFixed(2) }} +
+
+ Max Combined G + {{ gripAnalysis.summary.max_combined_g.toFixed(2) }} +
+
+
-
+
+
+ Laps + {{ racingLineAnalysis.total_laps }} +
+
+ Best Lap + + {{ racingLineAnalysis.laps.length > 0 + ? formatLapTimeShort(Math.min(...racingLineAnalysis.laps.filter(l => l.lap_time_ms > 0).map(l => l.lap_time_ms))) + : '--' }} + +
+
+ Max Speed + {{ Math.round(Math.max(...racingLineAnalysis.laps.map(l => l.max_speed_kmh), 0)) }} + km/h +
+
+ Frames + {{ racingLineAnalysis.total_frames_analyzed.toLocaleString() }} +
+
+
-

- Both boundary runs (left + right) are recorded. Build the persistent track model to enable corner analysis and sector timing. + Both boundary runs (left + right) are recorded. Name the track and build the persistent track model to enable corner analysis and sector timing.

- +
+ + + +
Track model built successfully. View in Tracks →
diff --git a/src/views/TrackDetailView.vue b/src/views/TrackDetailView.vue index 24042a9..6c985e5 100644 --- a/src/views/TrackDetailView.vue +++ b/src/views/TrackDetailView.vue @@ -17,6 +17,7 @@ import { ref, onMounted, onUnmounted, computed, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useTracksStore } from '@/stores/tracks' +import { useCanvasZoom } from '@/composables/useCanvasZoom' import type { Corner } from '@/types' let resizeRafId: number | null = null @@ -33,6 +34,8 @@ const showCurvature = ref(true) const showCorners = ref(true) const showSectors = ref(true) +const { zoom, onWheel, onMouseDown, onMouseMove, onMouseUp, reset: resetZoom, applyTransform } = useCanvasZoom(canvas, drawTrack) + onMounted(async () => { await store.fetchTrack(game.value, trackName.value) }) @@ -58,6 +61,9 @@ function drawTrack() { ctx.fillStyle = '#0f1117' ctx.fillRect(0, 0, width, height) + ctx.save() + applyTransform(ctx) + ctx.strokeStyle = '#1a1d27' ctx.lineWidth = 1 for (let i = 0; i <= 10; i++) { @@ -212,6 +218,8 @@ function drawTrack() { ctx.arc(startX, startY, 6, 0, Math.PI * 2) ctx.stroke() } + + ctx.restore() } function handleResize() { @@ -328,6 +336,9 @@ watch([showCurvature, showCorners, showSectors], () => { Sectors +
@@ -350,7 +361,15 @@ watch([showCurvature, showCorners, showSectors], () => {
- +
From 63e926f26eb151845a22f4be7671db752d1afd57 Mon Sep 17 00:00:00 2001 From: "Jan-Peter v. Hunnius" Date: Sat, 4 Jul 2026 22:19:35 +0200 Subject: [PATCH 10/10] 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. --- src/api/client.ts | 6 ++++++ src/stores/telemetry.ts | 13 +++++++++++-- src/types/index.ts | 10 ++++++++++ src/views/DashboardView.vue | 4 ++++ src/views/SessionDetailView.vue | 8 ++++++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index cb8551c..6be2756 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -28,6 +28,7 @@ import type { TrackSummary, BuildTrackModelRequest, CarProfile, + GameInfo, } from '@/types' const api = axios.create({ @@ -45,6 +46,11 @@ export async function getLiveTelemetry(): Promise { return res.data } +export async function getGameInfo(): Promise { + const res = await api.get('/game-info') + return res.data +} + // ── Recordings ── export async function listRecordings(): Promise { diff --git a/src/stores/telemetry.ts b/src/stores/telemetry.ts index 3897ad1..b7f1b05 100644 --- a/src/stores/telemetry.ts +++ b/src/stores/telemetry.ts @@ -15,17 +15,19 @@ // along with this program. If not, see . import { defineStore } from 'pinia' import { ref, computed } from 'vue' -import type { LiveTelemetryResponse, LiveFeedSnapshot } from '@/types' -import { getLiveTelemetry } from '@/api/client' +import type { LiveTelemetryResponse, LiveFeedSnapshot, GameInfo } from '@/types' +import { getLiveTelemetry, getGameInfo } from '@/api/client' export const useTelemetryStore = defineStore('telemetry', () => { const feeds = ref([]) const activeRecording = ref(null) + const gameInfo = ref({}) const connected = ref(false) const error = ref(null) const lastUpdate = ref(null) let pollTimer: ReturnType | null = null + let gameInfoCounter = 0 const anyFeedAlive = computed(() => feeds.value.some((f) => f.health === 'alive')) const anyFeedStale = computed(() => feeds.value.some((f) => f.health === 'stale')) @@ -65,6 +67,12 @@ export const useTelemetryStore = defineStore('telemetry', () => { connected.value = true error.value = null lastUpdate.value = new Date() + gameInfoCounter++ + if (gameInfoCounter % 8 === 0) { + try { + gameInfo.value = await getGameInfo() + } catch { /* non-critical */ } + } } catch (e: unknown) { connected.value = false error.value = e instanceof Error ? e.message : 'Connection failed' @@ -87,6 +95,7 @@ export const useTelemetryStore = defineStore('telemetry', () => { return { feeds, activeRecording, + gameInfo, connected, error, lastUpdate, diff --git a/src/types/index.ts b/src/types/index.ts index c8a4b13..e113895 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -118,6 +118,8 @@ export interface UseCaseSession { analysis: AnalysisResult | null guidance: string | null recording_id: string | null + car_name?: string | null + track_name?: string | null } export interface CreateSessionRequest { @@ -139,6 +141,14 @@ export interface UseCaseInfo { guidance: string } +// ── Game Info (from KSUDP handshake) ── + +export interface GameInfo { + car_name?: string | null + track_name?: string | null + driver_name?: string | null +} + // ── Analysis Results ── // Shift Point Analysis (use_case = "shift_points") diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index baed79f..a4e61de 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -114,6 +114,10 @@ const aliveCount = computed(() => telemetry.feeds.filter((f) => f.health === 'al {{ aliveCount }} feed(s) receiving data · {{ telemetry.totalPackets.toLocaleString() }} packets total +