Archive the three implemented + merged changes (activity-sport-type, activity-stats, journal-elevation-profile): their deltas are promoted into openspec/specs/, the changes move to changes/archive/2026-06-12-*, and the three new capabilities are added to the CAPABILITIES index. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 KiB
Context
Activities store distance (m), duration (s, elapsed), elevationGain/Loss
(m), startedAt, and gpx. Routes store distance + elevation. The three
surfaces that render these (feed, detail, profile) each hand-roll their own
markup and pick a different subset. There is no shared formatter and no derived
pace/speed.
Goals / Non-Goals
Goals
- One presentation component and one set of formatting/derivation helpers.
- Add average pace/speed and moving-vs-elapsed time as first-class headline metrics.
Non-Goals
- No imperial/metric toggle in this change — render metric (km, m, km/h, min/km) as the baseline; a units preference is a separate future change.
- No per-point analytics charts (that's
journal-elevation-profile). - No schema or API changes; everything is derived from existing fields.
Decisions
D1: One StatRow component
A presentational StatRow takes an ordered list of { value, label, unit }
items and renders the shared layout (value prominent, small caption beneath,
even spacing). Feed/detail/profile pass the items they have; the component never
fetches. A formatStat helper centralizes number/unit formatting and goes
through i18n.
D2: Canonical headline set
The headline set, in order, is: distance · time · avg pace/speed · ascent · descent. Surfaces may render a subset (e.g. the compact feed card may show distance · time · pace), but never reorder or relabel — the subset is a prefix or documented selection of the canonical set.
D3: Average pace vs. speed (sport-aware)
avgSpeed = distance / movingTime (fallback elapsed). Presentation chooses:
- pace (min/km) for foot sports (
hike,walk,run), - speed (km/h) for wheeled/ski sports (
ride,gravel,mtb,ski), - default to speed when sport type is unset.
This depends on
activity-sport-type; if that field is absent the rule still resolves (speed).
D4: Moving vs. elapsed time
- Elapsed = stored
duration. - Moving = derived from GPX trackpoint timestamps by summing inter-point
intervals and excluding intervals where the athlete is effectively stationary
(below a small speed threshold, e.g. < 0.5 m/s, with a max-gap guard). The
derivation lives in
@trails-cool/gpxas a pure, unit-tested helper. - When the GPX has no trackpoint timestamps (planned routes, some imports), moving time is unavailable and only elapsed is shown.
D5: Formatting
formatStat formats distance (km, 1 decimal < 100 km), elevation (m, integer),
time (h:mm or m:ss), speed (km/h, 1 decimal), pace (m:ss /km). Labels and unit
suffixes come from journal.stats.* i18n keys (en + de).
Risks / Trade-offs
- Moving-time heuristic: stop detection is approximate; the threshold is a single documented constant in the gpx helper, tunable later. When in doubt we fall back to elapsed, so we never show a moving time longer than elapsed.
- Subset discipline: allowing surfaces to render a subset risks drift; the canonical order + a shared component keeps it in check, and the spec forbids reordering/relabeling.