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

89
src/warnings.rs Normal file
View file

@ -0,0 +1,89 @@
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Instant;
const MAX_WARNINGS: usize = 50;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WarningLevel {
Info,
Warn,
Error,
}
#[derive(Debug, Clone)]
pub struct WarningEntry {
pub message: String,
pub timestamp: Instant,
pub level: WarningLevel,
}
#[derive(Clone)]
pub struct Warnings {
entries: Arc<Mutex<VecDeque<WarningEntry>>>,
}
impl Warnings {
pub fn new() -> Self {
Self {
entries: Arc::new(Mutex::new(VecDeque::with_capacity(MAX_WARNINGS))),
}
}
fn push(&self, level: WarningLevel, msg: &str) {
match level {
WarningLevel::Error => eprintln!("[ERROR] {}", msg),
WarningLevel::Warn => eprintln!("[WARN] {}", msg),
WarningLevel::Info => {}
}
let mut guard = match self.entries.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
if guard.len() >= MAX_WARNINGS {
guard.pop_front();
}
guard.push_back(WarningEntry {
message: msg.to_string(),
timestamp: Instant::now(),
level,
});
}
pub fn warn(&self, msg: &str) {
self.push(WarningLevel::Warn, msg);
}
pub fn info(&self, msg: &str) {
self.push(WarningLevel::Info, msg);
}
pub fn error(&self, msg: &str) {
self.push(WarningLevel::Error, msg);
}
/// Returns warnings from the last `max_age_secs` seconds.
/// Currently unused but kept for potential future TUI scrolling.
#[allow(dead_code)]
pub fn recent(&self, max_age_secs: u64) -> Vec<WarningEntry> {
let guard = match self.entries.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
let cutoff = Instant::now() - std::time::Duration::from_secs(max_age_secs);
guard
.iter()
.filter(|e| e.timestamp >= cutoff)
.cloned()
.collect()
}
pub fn latest(&self) -> Option<WarningEntry> {
let guard = match self.entries.lock() {
Ok(g) => g,
Err(e) => e.into_inner(),
};
guard.back().cloned()
}
}