From 87e5bcae5bc68c1bb1b5d2fd1b3aaef771a8b933 Mon Sep 17 00:00:00 2001 From: "Jan-Peter v. Hunnius" Date: Fri, 5 Jun 2026 21:28:27 +0200 Subject: [PATCH] feat: align frontend with rusty-telemetry v0.5.0 API changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update ShiftPointAnalysis types: gear_ranges → gear_curves (with acceleration data points), shift_events → crossover_points, add runs_analyzed field - Add UseCaseInfo type for GET /api/use-cases (now returns objects with id, name, guidance instead of plain strings) - Add guidance and recording_id fields to UseCaseSession - Rewrite ShiftPointsView: scatter chart for gear acceleration curves, dual y-axis bar chart for crossover RPM/speed, 4-column summary - Update SessionDetailView: show session guidance, crossover points and runs analyzed in analysis stats - Extract shared type guard functions into src/utils/analysis.ts - Type getAnalysis/reAnalyze as AnalysisResult instead of Record - Type getGames return as string[] to match backend --- src/api/client.ts | 20 ++-- src/stores/sessions.ts | 4 +- src/types/index.ts | 38 ++++++-- src/utils/analysis.ts | 24 +++++ src/views/SessionDetailView.vue | 26 ++++-- src/views/ShiftPointsView.vue | 158 +++++++++++++++----------------- 6 files changed, 156 insertions(+), 114 deletions(-) create mode 100644 src/utils/analysis.ts diff --git a/src/api/client.ts b/src/api/client.ts index 5509c86..c46563c 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -7,8 +7,8 @@ import type { CreateSessionRequest, StartRunRequest, SessionRun, - UseCase, - Game, + UseCaseInfo, + AnalysisResult, } from '@/types' const api = axios.create({ @@ -93,25 +93,25 @@ export async function completeSession(id: string): Promise { return res.data } -export async function getAnalysis(id: string): Promise> { - const res = await api.get>(`/sessions/${id}/analysis`) +export async function getAnalysis(id: string): Promise { + const res = await api.get(`/sessions/${id}/analysis`) return res.data } -export async function reAnalyze(id: string): Promise> { - const res = await api.post>(`/sessions/${id}/analyze`) +export async function reAnalyze(id: string): Promise { + const res = await api.post(`/sessions/${id}/analyze`) return res.data } // ── Metadata ── -export async function getUseCases(): Promise { - const res = await api.get('/use-cases') +export async function getUseCases(): Promise { + const res = await api.get('/use-cases') return res.data } -export async function getGames(): Promise { - const res = await api.get('/games') +export async function getGames(): Promise { + const res = await api.get('/games') return res.data } diff --git a/src/stores/sessions.ts b/src/stores/sessions.ts index 271ef77..606a40f 100644 --- a/src/stores/sessions.ts +++ b/src/stores/sessions.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia' import { ref } from 'vue' -import type { UseCaseSession, CreateSessionRequest, StartRunRequest, SessionRun } from '@/types' +import type { UseCaseSession, CreateSessionRequest, StartRunRequest, SessionRun, AnalysisResult } from '@/types' import * as api from '@/api/client' export const useSessionsStore = defineStore('sessions', () => { @@ -110,7 +110,7 @@ export const useSessionsStore = defineStore('sessions', () => { } } - async function reAnalyze(id: string): Promise | null> { + async function reAnalyze(id: string): Promise { try { return await api.reAnalyze(id) } catch (e: unknown) { diff --git a/src/types/index.ts b/src/types/index.ts index 1441e4c..e1d4b56 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -100,6 +100,8 @@ export interface UseCaseSession { completed_at: string | null runs: SessionRun[] analysis: AnalysisResult | null + guidance: string | null + recording_id: string | null } export interface CreateSessionRequest { @@ -112,31 +114,49 @@ export interface StartRunRequest { label?: string | null } +// ── Use Cases ── + +export interface UseCaseInfo { + id: string + name: string + guidance: string +} + // ── Analysis Results ── // Shift Point Analysis (use_case = "shift_points") -export interface GearRpmRange { +export interface GearDataPoint { + speed_kmh: number + rpm: number + acceleration_g: number +} + +export interface GearCurve { gear: number gear_display: string + run_label: string + data_points: GearDataPoint[] min_rpm: number max_rpm: number - avg_rpm: number + min_speed_kmh: number + max_speed_kmh: number sample_count: number } -export interface ShiftEvent { +export interface CrossoverPoint { from_gear: number to_gear: number - rpm_before_shift: number - rpm_after_shift: number - rpm_drop: number - speed_kmh: number + from_gear_display: string + to_gear_display: string + crossover_speed_kmh: number + crossover_rpm: number } export interface ShiftPointAnalysis { - gear_ranges: GearRpmRange[] - shift_events: ShiftEvent[] + gear_curves: GearCurve[] + crossover_points: CrossoverPoint[] recommended_shift_rpm: number | null + runs_analyzed: number total_frames_analyzed: number } diff --git a/src/utils/analysis.ts b/src/utils/analysis.ts new file mode 100644 index 0000000..370f80f --- /dev/null +++ b/src/utils/analysis.ts @@ -0,0 +1,24 @@ +import type { ShiftPointAnalysis, TrackMapAnalysis } from '@/types' + +/** + * Runtime type guard: checks if an unknown value is a ShiftPointAnalysis. + * Uses the 'gear_curves' discriminator field that the backend includes. + */ +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 TrackMapAnalysis. + * Uses the 'boundaries' discriminator field that the backend includes. + */ +export function isTrackMapAnalysis(data: unknown): data is TrackMapAnalysis { + return typeof data === 'object' && data !== null && 'boundaries' in data +} + +/** + * Runtime type guard: checks if an unknown value is a NotImplementedResponse. + */ +export function isNotImplemented(data: unknown): data is { status: 'not_implemented'; use_case: string } { + return typeof data === 'object' && data !== null && 'status' in data && (data as { status: string }).status === 'not_implemented' +} diff --git a/src/views/SessionDetailView.vue b/src/views/SessionDetailView.vue index abcba7b..c121443 100644 --- a/src/views/SessionDetailView.vue +++ b/src/views/SessionDetailView.vue @@ -3,6 +3,7 @@ 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' const props = defineProps<{ id: string @@ -34,19 +35,16 @@ const analysisData = computed(() => { const shiftAnalysis = computed((): ShiftPointAnalysis | null => { if (!analysisData.value) return null - // The analysis field is the full analysis object - const data = analysisData.value - if (data && typeof data === 'object' && 'gear_ranges' in data) { - return data as unknown as ShiftPointAnalysis + if (isShiftPointAnalysis(analysisData.value)) { + return analysisData.value } return null }) const trackAnalysis = computed((): TrackMapAnalysis | null => { if (!analysisData.value) return null - const data = analysisData.value - if (data && typeof data === 'object' && 'boundaries' in data) { - return data as unknown as TrackMapAnalysis + if (isTrackMapAnalysis(analysisData.value)) { + return analysisData.value } return null }) @@ -147,6 +145,10 @@ async function handleDelete() { Completed {{ formatTime(session.completed_at) }} +
+ Guidance + {{ session.guidance }} +
@@ -243,14 +245,18 @@ async function handleDelete() { -
+
Recommended Shift RPM {{ Math.round(shiftAnalysis.recommended_shift_rpm ?? 0) }}
- Shift Events Detected - {{ shiftAnalysis.shift_events.length }} + Crossover Points + {{ shiftAnalysis.crossover_points.length }} +
+
+ Runs Analyzed + {{ shiftAnalysis.runs_analyzed }}
diff --git a/src/views/ShiftPointsView.vue b/src/views/ShiftPointsView.vue index 4255ec9..06f76ed 100644 --- a/src/views/ShiftPointsView.vue +++ b/src/views/ShiftPointsView.vue @@ -1,7 +1,7 @@