feat: KSUDP parser fix, warnings system, shift point rewrite, code quality (v0.5.1)

Bug Fixes:
- Fixed KSUDP binary parser offsets from packed (323B) to natural C
  alignment (328B) — 3 bytes padding after char identifier, 2 bytes
  after 6 bools
- Fixed setup_terminal() leaving terminal in raw mode on early exit

New Features:
- Thread-safe warning buffer (warnings.rs) with TUI bar + stderr fallback
- Rewrote shift point analysis per AC forum methodology: per-gear
  acceleration curves, crossover detection, same-gear merging
- UseCase::guidance() — step-by-step recording instructions
- Recording metadata enrichment in analysis responses
- recording_id field on sessions

Code Quality:
- Extracted finalize_run() helper (session.rs)
- Extracted execute_and_store_analysis() helper (api.rs)
- Extracted lerp_search<T>() generic helper (analysis.rs)

Breaking API Changes:
- GET /api/use-cases: string[] -> [{id, name, guidance}[]]
- Shift point analysis: gear_ranges/shift_events -> gear_curves/crossover_points

Updated README.md with new API response examples and TUI warning bar.
This commit is contained in:
Jan-Peter von Hunnius 2026-06-05 21:12:57 +02:00
parent 090ab7e559
commit e3aa511ac7
9 changed files with 696 additions and 312 deletions

View file

@ -17,6 +17,7 @@ use crate::analysis;
use crate::feed_state::FeedState;
use crate::recording::{Game, RecordingManager, RecordingStatus, StartRecordingRequest, UseCase};
use crate::session::SessionManager;
use crate::telemetry_frame::TelemetryFrame;
use crate::ui::FeedDisplayInfo;
pub struct AppState {
@ -90,8 +91,9 @@ async fn create_session(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateSessionRequest>,
) -> Response {
let recording_id = state.recording_manager.active_recording_id().await;
let mut mgr = lock_sessions(&state);
match mgr.create_session(req.use_case, req.label) {
match mgr.create_session(req.use_case, req.label, recording_id) {
Ok(session) => (StatusCode::CREATED, Json(session)).into_response(),
Err(msg) => err(StatusCode::BAD_REQUEST, &msg),
}
@ -147,28 +149,62 @@ async fn stop_run(
}
}
async fn enrich_with_recording_meta(
state: &Arc<AppState>,
result: &mut serde_json::Value,
recording_id: Option<&str>,
) {
let rec_id = match recording_id {
Some(id) => id,
None => return,
};
if let Ok(manifest) = state.recording_manager.get_recording(rec_id).await {
if let Some(obj) = result.as_object_mut() {
obj.insert("car".to_string(), serde_json::json!(manifest.car));
obj.insert("game".to_string(), serde_json::json!(manifest.game));
obj.insert("track".to_string(), serde_json::json!(manifest.track));
}
}
}
async fn execute_and_store_analysis(
state: &Arc<AppState>,
session_id: &str,
use_case: UseCase,
frames_by_run: Vec<Vec<TelemetryFrame>>,
run_labels: Vec<String>,
recording_id: Option<String>,
) -> serde_json::Value {
let mut result = analysis::analyze(&use_case, &frames_by_run, &run_labels);
enrich_with_recording_meta(state, &mut result, recording_id.as_deref()).await;
let mut mgr = lock_sessions(state);
let _ = mgr.set_analysis(session_id, result.clone());
result
}
async fn complete_session(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Response {
let (use_case, frames_by_run, run_labels) = {
let (use_case, frames_by_run, run_labels, recording_id) = {
let mut mgr = lock_sessions(&state);
match mgr.complete_session(&id) {
Ok(_session) => {}
Err(msg) => return err(StatusCode::BAD_REQUEST, &msg),
}
match mgr.collect_run_frames(&id) {
Some((uc, frames, labels)) => (uc, frames, labels),
Some((uc, frames, labels)) => {
let rec_id = mgr.get_session(&id).and_then(|s| s.recording_id.clone());
(uc, frames, labels, rec_id)
}
None => return err(StatusCode::NOT_FOUND, "Session not found after completion"),
}
};
let result = analysis::analyze(&use_case, &frames_by_run, &run_labels);
{
let mut mgr = lock_sessions(&state);
let _ = mgr.set_analysis(&id, result);
}
execute_and_store_analysis(&state, &id, use_case, frames_by_run, run_labels, recording_id).await;
let mgr = lock_sessions(&state);
match mgr.get_session(&id) {
@ -197,7 +233,7 @@ async fn run_analysis(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> Response {
let (use_case, frames_by_run, run_labels) = {
let (use_case, frames_by_run, run_labels, recording_id) = {
let mgr = lock_sessions(&state);
match mgr.get_session(&id) {
Some(session) => {
@ -207,19 +243,14 @@ async fn run_analysis(
}
None => return err(StatusCode::NOT_FOUND, "Session not found"),
}
let rec_id = mgr.get_session(&id).unwrap().recording_id.clone();
match mgr.collect_run_frames(&id) {
Some(data) => data,
Some((uc, frames, labels)) => (uc, frames, labels, rec_id),
None => return err(StatusCode::NOT_FOUND, "Session not found"),
}
};
let result = analysis::analyze(&use_case, &frames_by_run, &run_labels);
{
let mut mgr = lock_sessions(&state);
let _ = mgr.set_analysis(&id, result.clone());
}
let result = execute_and_store_analysis(&state, &id, use_case, frames_by_run, run_labels, recording_id).await;
ok_json(result)
}
@ -458,8 +489,26 @@ async fn get_live(State(state): State<Arc<AppState>>) -> Response {
})
}
#[derive(Serialize)]
struct UseCaseInfo {
id: String,
name: String,
guidance: String,
}
async fn list_use_cases() -> Response {
ok_json(UseCase::all())
let use_cases: Vec<UseCaseInfo> = UseCase::all()
.into_iter()
.map(|uc| UseCaseInfo {
id: serde_json::to_value(&uc)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_default(),
name: format!("{:?}", uc),
guidance: uc.guidance().to_string(),
})
.collect();
ok_json(use_cases)
}
async fn list_games() -> Response {