Remember the rider, not only the hardware
`RiderConfig` lived in `RideInputs` and nowhere else, and nothing in the UI ever called `set_rider_config`. So every ride was ridden as the struct's own default — a 105 kg rider on an 8 kg bike — with no way to say otherwise short of editing the source. Mass is not a preference: it sets the speed a given power produces, the ETA that follows from it, the calorie estimate, and how a 6% ramp feels. FR-7.4 is a Must, and a command the UI never calls does not satisfy it. - settings.rs persists rider config, safety limits and display preferences to app_data_dir()/settings.json, written atomically and read back before the first tick, so no snapshot is ever computed against the default. Advisory like known.rs: an unreadable file costs the rider their setup, never their ride. - A stored file is refused *whole* if it fails the same checks the commands apply. It may predate a tightened bound or have been edited by hand, and a zero mass reaching the engine divides by itself on the next tick. - The commands validate with instructions rather than codes — "CdA must be between 0.1 and 1.5 m² — a road position is about 0.32" — because this is now a form a rider fills in, not a struct only I ever touched. - Preferences (FTP, maximum heart rate, units) are Tauri-side, not in `RiderConfig`. None of it reaches the physics, and crates/core is the frozen contract the engine and the FIT writer share. Zero is a real answer for both references and means "no zones", not "unset and guessed at". - SettingsScreen commits on field-exit and reseats every input from what Rust returned, so a rejected value can never sit on screen looking accepted. Weight, FTP and units are on top; the eight settings with a defensible default are folded away. - Reachable on `,` from any screen, returning to whichever screen opened it. Setup swallows the ride controls while it is up — a stray arrow key while reading the form must not trim the gradient of a ride happening behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -441,10 +441,19 @@ pub fn set_rider_config(
|
||||
state: State<'_, AppState>,
|
||||
config: RiderConfig,
|
||||
) -> Cmd<RiderConfig> {
|
||||
if config.rider_kg <= 20.0 || config.bike_kg <= 0.0 {
|
||||
return Err("Rider and bike mass must be positive and realistic".into());
|
||||
// Every bound refused here is one that makes the engine produce nonsense
|
||||
// rather than something merely unusual, and each refusal says what a
|
||||
// workable value looks like — this is now a form a rider fills in, not a
|
||||
// struct only the developer ever touched.
|
||||
crate::settings::validate_rider(&config)?;
|
||||
{
|
||||
let mut inner = state.lock();
|
||||
inner.inputs.rider = config;
|
||||
// Written down immediately. A setup that lasts only as long as the
|
||||
// process is what left every rider on the 105 kg default (FR-7.4).
|
||||
let inner = &*inner;
|
||||
inner.settings.save(&inner.inputs.rider, &inner.inputs.limits);
|
||||
}
|
||||
state.lock().inputs.rider = config;
|
||||
emit_ride_state(&app);
|
||||
Ok(config)
|
||||
}
|
||||
@@ -460,14 +469,40 @@ pub fn set_safety_limits(
|
||||
state: State<'_, AppState>,
|
||||
limits: SafetyLimits,
|
||||
) -> Cmd<SafetyLimits> {
|
||||
if limits.min_gradient_pct >= limits.max_gradient_pct {
|
||||
return Err("Gradient limits are inverted".into());
|
||||
crate::settings::validate_limits(&limits)?;
|
||||
{
|
||||
let mut inner = state.lock();
|
||||
inner.inputs.limits = limits;
|
||||
let inner = &*inner;
|
||||
inner.settings.save(&inner.inputs.rider, &inner.inputs.limits);
|
||||
}
|
||||
state.lock().inputs.limits = limits;
|
||||
emit_ride_state(&app);
|
||||
Ok(limits)
|
||||
}
|
||||
|
||||
/// Display preferences: FTP, maximum heart rate, units (§4.3).
|
||||
///
|
||||
/// Separate from [`rider_config`] because nothing here reaches the physics —
|
||||
/// these decide how a number is drawn, not what it is. They are stored in the
|
||||
/// same file because that is where the rider expects to find them.
|
||||
#[tauri::command]
|
||||
pub fn preferences(state: State<'_, AppState>) -> crate::settings::Preferences {
|
||||
state.lock().settings.prefs
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_preferences(
|
||||
state: State<'_, AppState>,
|
||||
prefs: crate::settings::Preferences,
|
||||
) -> Cmd<crate::settings::Preferences> {
|
||||
crate::settings::validate_prefs(&prefs)?;
|
||||
let mut inner = state.lock();
|
||||
inner.settings.prefs = prefs;
|
||||
let inner = &*inner;
|
||||
inner.settings.save(&inner.inputs.rider, &inner.inputs.limits);
|
||||
Ok(prefs)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Profiles (§5.5, §5.6)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user