Colour effort by zone, and read distance in the rider's units

A watt is a fact; a zone is what it costs you. The biggest number on the
ride screen was the same shade of white at 90 W and at 400 W, which is a
thing no training app has done in fifteen years.

Zones, with two rules:

- **No reference, no zone.** An unset FTP draws the plain number. A zone
  measured against a guessed threshold would paint every ride with a
  confident lie.
- **Colour never carries it alone.** "Z4" renders beside the swatch, so
  the meaning survives a colour-blind rider, a phone in direct sun and a
  black-and-white screenshot.

Read off the rolling average, not the instantaneous watts: at 4 Hz the
raw figure crosses two boundaries every pedal stroke, and a colour that
strobes is worse than no colour. Not pedalling is not zone 1.

Units are a display preference applied at the last step before the
glass. Everything computed, stored and recorded stays SI, so a FIT file
never depends on what the screen was set to. `format.ts` takes the unit
system as an argument rather than reading a module-level setting — pure
functions are what let every readout redraw the moment it changes. The
`km` helper is gone rather than left beside `dist`, so there is no
second way to format a distance that ignores the preference.

Rust's block labels lose their baked-in kilometres. The block already
carries start_x and end_x and the frontend renders that span in the
rider's units; a kilometre in the text sat inside a sentence saying
miles everywhere else.

Also on the ride screen:

- The gradient gets a wedge beside the number. A signed decimal has to
  be read; a slope is seen. Exaggerated and clamped, because a true-scale
  6% is indistinguishable from 3% at 40 px wide.
- What is coming, from the profile's own block list — "2.1 km at 12% in
  460 m". The chart says where the rider is; what is about to happen is
  what decides whether to shift now. The data was already computed
  Rust-side and thrown away here. Close in, the small unit reads better
  than a fraction of the big one.
- Mode and target merge into one chip. They are a single fact, and
  splitting them spent a chip of header width repeating the word
  "target".
- The pod chip no longer reports a missing `+` pod while the `−` pod is
  connected. The `−` pod relays its twin, so that is the intended
  configuration — the ride screen was calling it a fault, contradicting
  the device screen two keystrokes away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 20:15:11 +02:00
co-authored by Claude Opus 5
parent cdff678167
commit 0c757a4a15
9 changed files with 477 additions and 58 deletions
+10 -6
View File
@@ -333,13 +333,17 @@ fn block_label(block: &Block) -> String {
amplitude,
repeats
),
// No distance in the label. The block already carries `start_x` and
// `end_x`, and the frontend renders that span in the rider's own units
// (FR-7.5a) — a kilometre baked into the text here would sit inside a
// sentence that says miles everywhere else.
Block::Segments { segments } => {
let d: f64 = segments.iter().map(|s| s.distance_m).sum();
format!("{} segments · {:.1} km", segments.len(), d / 1000.0)
}
Block::Terrain { points } => {
let d = points.last().map(|p| p.distance_m).unwrap_or(0.0);
format!("terrain · {:.1} km", d / 1000.0)
format!(
"{} segment{}",
segments.len(),
if segments.len() == 1 { "" } else { "s" }
)
}
Block::Terrain { .. } => "terrain".to_string(),
}
}