trails/openspec/changes/activity-stats/tasks.md
Ullrich Schäfer 0325d01bca
activity-stats: shared StatRow + canonical metrics across surfaces
Implements the activity-stats change (specs/activity-stats):

- gpx: `movingTime(tracks)` — moving seconds from trackpoint timestamps,
  excluding stationary spans + long gaps; null when no timestamps.
- stats.ts: pure formatters (distance/elevation/duration/speed/pace),
  sport-aware `deriveRate` (pace for foot sports, speed otherwise, speed
  default), and `activityStatItems` encoding the canonical order
  (distance · time · [moving] · pace/speed · ascent · descent; compact subset).
- StatRow: one shared presentational component (size sm/lg), adopted on the
  activity detail (full set; loader derives moving time), route detail
  (distance/ascent/descent), feed card + profile list (compact).
- i18n: journal.stats.* in en + de.

Tests: movingTime (stationary/gap exclusion, moving ≤ elapsed), deriveRate,
formatters, activityStatItems ordering + compact, StatRow render (jsdom).
typecheck + lint + unit (gpx 63, journal 311) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:48:36 +02:00

1.8 KiB

1. Helpers

  • 1.1 Add a pure movingTime(tracks) helper to @trails-cool/gpx (sum inter-point intervals, exclude sub-threshold/stationary segments, max-gap guard); unit-tested with and without trackpoint times (moving-time.ts + test).
  • 1.2 Add a deriveRate(distance, time, sportType) helper returning { kind: "pace" | "speed", value } with the sport→kind rule and speed default (stats.ts).
  • 1.3 Add pure format* helpers (distance/elevation/duration/speed/pace) in stats.ts; labels route through i18n at the call site (activityStatItems).

2. Component

  • 2.1 Build the shared StatRow (ordered { value, label } items; size sm/lg; no data fetching).
  • 2.2 Canonical order encoded once in activityStatItems (distance · time · [moving] · pace/speed · ascent · descent); surfaces draw from it.

3. Adopt across surfaces

  • 3.1 Activity detail: replaced the card grid with StatRow size="lg" (full set incl. pace/speed + moving/elapsed); loader computes moving time from the GPX.
  • 3.2 Route detail: adopt StatRow size="lg" (distance, ascent, descent; no time/pace).
  • 3.3 Feed card: compact StatRow (distance · time · pace/speed); feed loader already exposes duration + sportType.
  • 3.4 Profile activity list: compact StatRow.

4. i18n

  • 4.1 Add journal.stats.* keys (distance, duration, movingTime, avgSpeed, avgPace, ascent, descent) to en + de.

5. Tests & checks

  • 5.1 Unit: movingTime (with/without timestamps; moving ≤ elapsed; stationary/gap exclusion), deriveRate (pace/speed/default/null), formatters.
  • 5.2 Component: StatRow renders ordered items (jsdom); activityStatItems ordering + compact-subset order asserted.
  • 5.3 typecheck + lint + unit (gpx 63, journal 311) green. Full pnpm test:e2e runs in CI.