feat: persistent sessions, remove track_map use case, car characteristics DB

- Remove obsolete TrackMap use case variant (superseded by TrackMapping)
- Add session/run/analysis persistence: metadata to data/sessions/<id>.json,
  frames to <id>.frames.json (split for lazy loading)
- Atomic writes (temp + rename), compact JSON, off-thread frame serialization
- load_all loads metadata only at startup; ensure_frames_loaded lazy-loads
  frames before analysis
- Normalize Recording runs to Stopped on reload (stale ring-buffer markers)
- Cache car_name on UseCaseSession during run finalization
- Add characteristics.rs: aggregate shift-point data per car across sessions
- New API: GET /api/characteristics/cars, GET /api/characteristics/cars/{car}
- Add Deserialize to TelemetryFrame and FeedSource for session round-trip
This commit is contained in:
Jan-Peter von Hunnius 2026-06-24 09:32:16 +02:00
parent 937090681f
commit ed7dbcf1c9
7 changed files with 477 additions and 17 deletions

View file

@ -14,6 +14,7 @@ use std::sync::{Arc, Mutex, MutexGuard};
use tower_http::cors::CorsLayer;
use crate::analysis;
use crate::characteristics;
use crate::feed_state::FeedState;
use crate::recording::{Game, RecordingManager, RecordingStatus, StartRecordingRequest, UseCase};
use crate::session::SessionManager;
@ -267,7 +268,7 @@ async fn run_analysis(
Path(id): Path<String>,
) -> Response {
let (use_case, frames_by_run, run_labels, recording_id) = {
let mgr = lock_sessions(&state);
let mut mgr = lock_sessions(&state);
match mgr.get_session(&id) {
Some(session) => {
if session.status != crate::session::SessionStatus::Completed {
@ -681,6 +682,24 @@ async fn list_games() -> Response {
ok_json(Game::valid_choices())
}
async fn list_car_profiles(
State(state): State<Arc<AppState>>,
) -> Response {
let mgr = lock_sessions(&state);
ok_json(characteristics::car_profiles(&mgr))
}
async fn get_car_profile(
State(state): State<Arc<AppState>>,
Path(car): Path<String>,
) -> Response {
let mgr = lock_sessions(&state);
match characteristics::car_profile(&mgr, &car) {
Some(profile) => ok_json(profile),
None => err(StatusCode::NOT_FOUND, &format!("No data for car '{}'", car)),
}
}
pub fn build_router(state: Arc<AppState>) -> Router {
Router::new()
.route("/api/sessions", get(list_sessions).post(create_session))
@ -714,6 +733,8 @@ pub fn build_router(state: Arc<AppState>) -> Router {
.route("/api/tracks/build", post(build_track))
.route("/api/use-cases", get(list_use_cases))
.route("/api/games", get(list_games))
.route("/api/characteristics/cars", get(list_car_profiles))
.route("/api/characteristics/cars/{car}", get(get_car_profile))
.layer(CorsLayer::permissive())
.with_state(state)
}