Fix pace sentinel, route map thumbnails, tiled segments, health/dashboard layout
- Pace: FIT 0xFFFF sentinel (65.535 m/s) was stored as avg_speed_ms on every activity and lap; add _sanitize_speed() to parser falling back to dist/dur, plus a startup SQL migration that fixed 120 activities and 688 laps in-place - Records: remove swimming from Distance PRs; Route Records rows are clickable (navigate to activity), View button removed, small SVG route map per row; Segment Records uses same tiled route-card layout as Segments page - Segments: replace route dropdown with responsive tile grid showing SVG map thumbnails; selecting a tile reveals the segment management panel below - RouteMiniMap: new pure-SVG component (no Leaflet) for route thumbnails, decodes polyline and normalises coords into a fixed viewBox - Health: rename "Avg Heart Rate (day)" → "Heart Rate"; weight chart now filters to non-null rows and enables connectNulls + dots for sparse data - Dashboard: 4-col layout at lg breakpoint so Body Battery sits between weekly chart and Health Today; Body Battery card gains a 24-hr sparkline from the values[] already present in the health summary response Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,16 @@ def _safe_float(val) -> Optional[float]:
|
||||
return None
|
||||
|
||||
|
||||
def _sanitize_speed(val, dist_m=None, dur_s=None) -> Optional[float]:
|
||||
"""Reject the FIT invalid sentinel (0xFFFF/1000 = 65.535 m/s) and fall back to dist/dur."""
|
||||
fv = _safe_float(val)
|
||||
if fv is None or fv >= 65.0:
|
||||
if dist_m and dur_s and float(dur_s) > 0:
|
||||
return float(dist_m) / float(dur_s)
|
||||
return None
|
||||
return fv
|
||||
|
||||
|
||||
def _bounding_box(coords):
|
||||
if not coords:
|
||||
return None
|
||||
@@ -180,15 +190,19 @@ def parse_fit_file(filepath: str) -> dict:
|
||||
normalized_laps = []
|
||||
for i, lap in enumerate(laps):
|
||||
ls = _to_dt(get(lap, "startTime", "start_time"))
|
||||
lap_dist = _safe_float(get(lap, "totalDistance", "total_distance"))
|
||||
lap_dur = _safe_float(get(lap, "totalElapsedTime", "total_elapsed_time"))
|
||||
normalized_laps.append({
|
||||
"lap_number": i + 1,
|
||||
"start_time": ls.isoformat() if ls else None,
|
||||
"duration_s": _safe_float(get(lap, "totalElapsedTime", "total_elapsed_time")),
|
||||
"distance_m": _safe_float(get(lap, "totalDistance", "total_distance")),
|
||||
"duration_s": lap_dur,
|
||||
"distance_m": lap_dist,
|
||||
"avg_heart_rate": _safe_float(get(lap, "avgHeartRate", "avg_heart_rate")),
|
||||
"avg_cadence": _safe_float(get(lap, "avgCadence", "avg_cadence")),
|
||||
"avg_speed_ms": _safe_float(get(lap, "avgSpeed", "avg_speed",
|
||||
"enhancedAvgSpeed", "enhanced_avg_speed")),
|
||||
"avg_speed_ms": _sanitize_speed(
|
||||
get(lap, "avgSpeed", "avg_speed", "enhancedAvgSpeed", "enhanced_avg_speed"),
|
||||
dist_m=lap_dist, dur_s=lap_dur,
|
||||
),
|
||||
"avg_power": _safe_float(get(lap, "avgPower", "avg_power")),
|
||||
})
|
||||
|
||||
@@ -209,8 +223,11 @@ def parse_fit_file(filepath: str) -> dict:
|
||||
"avg_cadence": _safe_float(get(session_data, "avgCadence", "avg_cadence")),
|
||||
"avg_power": _safe_float(get(session_data, "avgPower", "avg_power")),
|
||||
"normalized_power": _safe_float(get(session_data, "normalizedPower", "normalized_power")),
|
||||
"avg_speed_ms": _safe_float(get(session_data, "avgSpeed", "avg_speed",
|
||||
"enhancedAvgSpeed", "enhanced_avg_speed")),
|
||||
"avg_speed_ms": _sanitize_speed(
|
||||
get(session_data, "avgSpeed", "avg_speed", "enhancedAvgSpeed", "enhanced_avg_speed"),
|
||||
dist_m=_safe_float(get(session_data, "totalDistance", "total_distance")),
|
||||
dur_s=_safe_float(get(session_data, "totalElapsedTime", "total_elapsed_time")),
|
||||
),
|
||||
"max_speed_ms": _safe_float(get(session_data, "maxSpeed", "max_speed",
|
||||
"enhancedMaxSpeed", "enhanced_max_speed")),
|
||||
"avg_temperature_c": _safe_float(get(session_data, "avgTemperature", "avg_temperature")),
|
||||
|
||||
Reference in New Issue
Block a user