fix: PR review changes

pull/4/head
lambda 5 days ago
parent 0888056268
commit 3c06100a23
No known key found for this signature in database
GPG Key ID: CF45A8EE158BBCB6

@ -99,6 +99,8 @@ struct RawStep {
#[derive(Deserialize)] #[derive(Deserialize)]
struct RawPower { struct RawPower {
#[serde(default)]
units: Option<String>,
#[serde(default)] #[serde(default)]
value: Option<f64>, value: Option<f64>,
#[serde(default)] #[serde(default)]
@ -160,6 +162,12 @@ fn convert_step(raw: RawStep) -> WorkoutStep {
/// линейный ramp от свободного диапазона. Предполагаем `units == "%ftp"` /// линейный ramp от свободного диапазона. Предполагаем `units == "%ftp"`
/// (все скачанные воркауты используют %ftp); абсолютные ватты не поддерживаются. /// (все скачанные воркауты используют %ftp); абсолютные ватты не поддерживаются.
fn convert_power(p: RawPower, ramp: bool) -> Option<PowerTarget> { fn convert_power(p: RawPower, ramp: bool) -> Option<PowerTarget> {
// Отвергаем воркауты в абсолютных ваттах: перевод watts → %FTP требует FTP,
// которого нет на этапе парсинга. Принимаем только %ftp (units == None
// трактуем как %ftp — все скачанные воркауты используют %ftp).
if matches!(p.units.as_deref(), Some(u) if u != "%ftp") {
return None;
}
if let Some(v) = p.value { if let Some(v) = p.value {
return Some(PowerTarget::Fixed { percent: v }); return Some(PowerTarget::Fixed { percent: v });
} }
@ -199,7 +207,11 @@ pub fn total_duration_secs(segments: &[Segment]) -> u64 {
} }
/// Целевая мощность (%FTP) в момент `elapsed_secs` внутри шага. /// Целевая мощность (%FTP) в момент `elapsed_secs` внутри шага.
/// Fixed — константа; Ramp — линейная интерполяция from→to; Range — середина. /// Fixed — константа; Ramp — линейная интерполяция from→to.
///
/// Range (свободный диапазон low..high) сводится к середине: в ERG-режиме
/// нужна конкретная цель, середина — осознанный дефолт. Альтернативы: не слать
/// ERG вовсе (свободный ход) или слать low как «пол».
pub fn target_percent(power: &PowerTarget, elapsed_secs: f64, duration_secs: u32) -> f64 { pub fn target_percent(power: &PowerTarget, elapsed_secs: f64, duration_secs: u32) -> f64 {
match power { match power {
PowerTarget::Fixed { percent } => *percent, PowerTarget::Fixed { percent } => *percent,
@ -330,6 +342,17 @@ mod tests {
assert_eq!(w.steps[0].cadence_rpm, Some(85)); assert_eq!(w.steps[0].cadence_rpm, Some(85));
} }
#[test]
fn rejects_non_ftp_power_units() {
// units:"watts" нельзя перевести в %FTP без FTP на этапе парсинга —
// шаг должен терять цель мощности, а не трактовать ватты как проценты.
let json = r#"{"name":"W","workout_doc":{"steps":[
{"text":"X","duration":60,"power":{"units":"watts","value":200}}
]}}"#;
let w = parse_workout(json).unwrap();
assert_eq!(w.steps[0].power, None);
}
#[test] #[test]
fn flatten_expands_repeats() { fn flatten_expands_repeats() {
let w = parse_workout(EVENT).unwrap(); let w = parse_workout(EVENT).unwrap();

@ -58,13 +58,11 @@
<span class="metric__label">мощность</span> <span class="metric__label">мощность</span>
<span class="metric__target" id="power-target">—</span> <span class="metric__target" id="power-target">—</span>
<span class="metric__actual" id="power-value">—</span> <span class="metric__actual" id="power-value">—</span>
<span class="metric__warn" id="power-warn"></span>
</div> </div>
<div class="metric metric--cadence" id="cadence-metric"> <div class="metric metric--cadence" id="cadence-metric">
<span class="metric__label">каденс</span> <span class="metric__label">каденс</span>
<span class="metric__target" id="cadence-target">—</span> <span class="metric__target" id="cadence-target">—</span>
<span class="metric__actual" id="cadence-value">—</span> <span class="metric__actual" id="cadence-value">—</span>
<span class="metric__warn" id="cadence-warn"></span>
</div> </div>
<div class="metric metric--hr"> <div class="metric metric--hr">
<span class="metric__label">чсс</span> <span class="metric__label">чсс</span>

@ -211,10 +211,8 @@ const workoutStopBtn = document.querySelector("#workout-stop");
// --- Отклонение от цели --- // --- Отклонение от цели ---
const powerMetric = document.querySelector("#power-metric"); const powerMetric = document.querySelector("#power-metric");
const powerTarget = document.querySelector("#power-target"); const powerTarget = document.querySelector("#power-target");
const powerWarn = document.querySelector("#power-warn");
const cadenceMetric = document.querySelector("#cadence-metric"); const cadenceMetric = document.querySelector("#cadence-metric");
const cadenceTarget = document.querySelector("#cadence-target"); const cadenceTarget = document.querySelector("#cadence-target");
const cadenceWarn = document.querySelector("#cadence-warn");
const POWER_TOL_PCT = 0.1; const POWER_TOL_PCT = 0.1;
const POWER_TOL_ABS = 15; // Вт const POWER_TOL_ABS = 15; // Вт
@ -229,11 +227,10 @@ function powerTolerance(target) {
return Math.max(POWER_TOL_ABS, Math.round(target * POWER_TOL_PCT)); return Math.max(POWER_TOL_ABS, Math.round(target * POWER_TOL_PCT));
} }
function updateDeviation(metricEl, targetEl, warnEl, actual, target, tolerance, unit) { function updateDeviation(metricEl, targetEl, actual, target, tolerance, unit) {
targetEl.textContent = target != null ? `${target} ${unit}` : "—"; targetEl.textContent = target != null ? `${target} ${unit}` : "—";
if (actual == null || target == null) { if (actual == null || target == null) {
metricEl.classList.remove("metric--off"); metricEl.classList.remove("metric--off");
warnEl.textContent = "";
return; return;
} }
const diff = actual - target; const diff = actual - target;
@ -245,11 +242,11 @@ function updateDeviation(metricEl, targetEl, warnEl, actual, target, tolerance,
} }
function updatePowerDeviation() { function updatePowerDeviation() {
updateDeviation(powerMetric, powerTarget, powerWarn, actualWatts, targetWatts, powerTolerance(targetWatts ?? 0), "W"); updateDeviation(powerMetric, powerTarget, actualWatts, targetWatts, powerTolerance(targetWatts ?? 0), "W");
} }
function updateCadenceDeviation() { function updateCadenceDeviation() {
updateDeviation(cadenceMetric, cadenceTarget, cadenceWarn, actualCadence, targetCadence, CADENCE_TOL_ABS, "rpm"); updateDeviation(cadenceMetric, cadenceTarget, actualCadence, targetCadence, CADENCE_TOL_ABS, "rpm");
} }
function clearTargets() { function clearTargets() {

@ -347,15 +347,6 @@ body {
font-weight: var(--fw-semibold); font-weight: var(--fw-semibold);
} }
/* предупреждение — резервируем строку, чтобы не прыгало */
.metric__warn {
min-height: var(--text-base);
font-size: var(--text-xs);
font-weight: var(--fw-medium);
color: var(--c-danger);
line-height: 1.2;
}
/* --- График мощности/пульса --- */ /* --- График мощности/пульса --- */
.graph canvas { .graph canvas {

Loading…
Cancel
Save