openspec: propose journal route/activity detail catch-up changes
Three OpenSpec proposals to bring the journal's route/activity detail and feed closer to best-in-class outdoor apps, in the order we'll implement them: - activity-sport-type: nullable sport enum on activities (hike/walk/run/ ride/gravel/mtb/ski/other), set on create or normalized on import, shown as a badge + feed verb, federated as a Note PropertyValue. - activity-stats: one reusable StatRow + canonical metric set, sport-aware avg pace/speed, moving-vs-elapsed time. Derive-only, no schema change. - journal-elevation-profile: elevation profile chart + chart<->map "active distance" hover/click sync on the read-only detail pages. All three validate with `openspec validate --strict`. Specs only; no code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e3e01a34ae
commit
26b59b9c95
15 changed files with 602 additions and 0 deletions
2
openspec/changes/activity-stats/.openspec.yaml
Normal file
2
openspec/changes/activity-stats/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-06-12
|
||||
66
openspec/changes/activity-stats/design.md
Normal file
66
openspec/changes/activity-stats/design.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
## 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/gpx` as 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.
|
||||
49
openspec/changes/activity-stats/proposal.md
Normal file
49
openspec/changes/activity-stats/proposal.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
## Why
|
||||
|
||||
Distance, duration, and elevation are rendered ad-hoc and inconsistently across
|
||||
the feed card, the activity/route detail page, and the profile list — different
|
||||
spacing, different label casing, and a different subset of metrics on each
|
||||
surface. Komoot and Strava both lead with one crisp, repeated metric row. We
|
||||
also discard derivable headline numbers: **average pace/speed** (we have
|
||||
distance + duration) and **moving vs. elapsed time**. A single reusable stat
|
||||
component plus those derived metrics makes every surface scannable and is a
|
||||
natural piece of the launch-blocking visual redesign.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Introduce one reusable `StatRow` presentation component (value + localized
|
||||
label + unit), used identically on the feed card, the activity & route detail
|
||||
pages, and the profile activity list.
|
||||
- Define the canonical headline metric set and render it consistently:
|
||||
distance, time, average pace **or** speed, ascent, descent.
|
||||
- Derive **average pace or speed** and choose the right one per sport (pace for
|
||||
foot sports, speed for wheeled/ski), degrading to speed when sport is unset.
|
||||
- Show **moving time** alongside **elapsed time** when moving time can be
|
||||
derived from GPX trackpoint timestamps; otherwise show elapsed only.
|
||||
- Localized number/unit formatting via i18n.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `activity-stats`: the canonical headline-metric set, the derived metrics
|
||||
(average pace/speed, moving vs. elapsed time), and the reusable stat-row
|
||||
presentation used across the feed, detail, and profile surfaces.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- None at the requirement level. The feed (`activity-feed`) and profile
|
||||
(`public-profiles`) specs describe aggregation/listing, not the metric set;
|
||||
the shared presentation is a new capability they consume. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Journal app**: a new shared `StatRow` (and a small `formatStat`/derivation
|
||||
helper); the feed, activity-detail, route-detail, and profile views adopt it;
|
||||
their loaders expose the fields needed to derive the metrics (already mostly
|
||||
present: distance, duration, startedAt, elevation, plus `gpx` for moving
|
||||
time).
|
||||
- **i18n**: `journal.stats.*` keys (en + de) for labels and units.
|
||||
- **Builds on** `activity-sport-type` for the pace-vs-speed choice (graceful
|
||||
fallback if that change is not yet merged).
|
||||
- No schema or wire-contract change; metrics are derived from existing fields.
|
||||
52
openspec/changes/activity-stats/specs/activity-stats/spec.md
Normal file
52
openspec/changes/activity-stats/specs/activity-stats/spec.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Reusable stat row
|
||||
The UI SHALL render activity and route headline metrics through one shared stat-row component (value, localized label, unit) used on the feed card, the activity and route detail pages, and the profile activity list.
|
||||
|
||||
#### Scenario: Same component across surfaces
|
||||
- **WHEN** distance is shown on the feed card, the detail page, and the profile list
|
||||
- **THEN** all three render it via the shared stat-row component with the same label, unit, and formatting
|
||||
|
||||
#### Scenario: Compact subset keeps order
|
||||
- **WHEN** a surface renders a subset of the headline metrics
|
||||
- **THEN** the subset preserves the canonical order and labels (no reordering or relabeling)
|
||||
|
||||
### Requirement: Canonical headline metric set
|
||||
The system SHALL define the canonical headline metric set, in order, as distance, time, average pace or speed, ascent, and descent.
|
||||
|
||||
#### Scenario: Detail page shows the full set
|
||||
- **WHEN** a user views an activity with distance, duration, and elevation data
|
||||
- **THEN** distance, time, average pace/speed, ascent, and descent are shown in that order
|
||||
|
||||
### Requirement: Average pace or speed, chosen by sport
|
||||
The system SHALL compute an average rate from distance and time and SHALL present it as pace for foot sports (hike, walk, run) and as speed for wheeled or ski sports (ride, gravel, mtb, ski), defaulting to speed when the sport type is unset.
|
||||
|
||||
#### Scenario: Pace for a run
|
||||
- **WHEN** an activity with sport type `run` is shown
|
||||
- **THEN** the rate is presented as pace (min/km)
|
||||
|
||||
#### Scenario: Speed for a ride
|
||||
- **WHEN** an activity with sport type `ride` is shown
|
||||
- **THEN** the rate is presented as speed (km/h)
|
||||
|
||||
#### Scenario: Default when sport is unset
|
||||
- **WHEN** an activity has no sport type
|
||||
- **THEN** the rate is presented as speed
|
||||
|
||||
### Requirement: Moving time and elapsed time
|
||||
The system SHALL display elapsed time and SHALL additionally display moving time when it can be derived from GPX trackpoint timestamps; when timestamps are unavailable, only elapsed time SHALL be shown.
|
||||
|
||||
#### Scenario: Both shown when trackpoint times exist
|
||||
- **WHEN** an activity's GPX has trackpoint timestamps with stationary periods
|
||||
- **THEN** both moving time and elapsed time are shown, and moving time is not greater than elapsed time
|
||||
|
||||
#### Scenario: Elapsed only when no timestamps
|
||||
- **WHEN** an activity has no trackpoint timestamps
|
||||
- **THEN** only elapsed time is shown
|
||||
|
||||
### Requirement: Localized metric formatting
|
||||
The system SHALL format metric values and unit suffixes through localized strings, covering distance, elevation, time, speed, and pace.
|
||||
|
||||
#### Scenario: German units
|
||||
- **WHEN** the UI locale is German
|
||||
- **THEN** metric labels and unit suffixes render in German
|
||||
27
openspec/changes/activity-stats/tasks.md
Normal file
27
openspec/changes/activity-stats/tasks.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
## 1. Helpers
|
||||
|
||||
- [ ] 1.1 Add a pure `movingTime(gpx)` helper to `@trails-cool/gpx` (sum inter-point intervals, exclude sub-threshold/stationary segments, max-gap guard); unit-test against fixtures with and without trackpoint times.
|
||||
- [ ] 1.2 Add a `deriveRate({ distance, time, sportType })` helper returning `{ kind: "pace" | "speed", value }` with the sport→kind rule and speed default.
|
||||
- [ ] 1.3 Add a `formatStat` helper (distance/elevation/time/speed/pace) routing labels + units through i18n.
|
||||
|
||||
## 2. Component
|
||||
|
||||
- [ ] 2.1 Build the shared `StatRow` (ordered `{ value, label, unit }` items; value-prominent layout; no data fetching).
|
||||
- [ ] 2.2 Define the canonical headline order as a single exported constant the surfaces draw from.
|
||||
|
||||
## 3. Adopt across surfaces
|
||||
|
||||
- [ ] 3.1 Activity detail: replace the hand-rolled stats block with `StatRow` (full set incl. pace/speed + moving/elapsed).
|
||||
- [ ] 3.2 Route detail: adopt `StatRow` (distance, ascent, descent; no time/pace).
|
||||
- [ ] 3.3 Feed card: adopt `StatRow` compact subset (distance · time · pace/speed); expose the needed fields in the feed loader.
|
||||
- [ ] 3.4 Profile activity list: adopt `StatRow` compact subset.
|
||||
|
||||
## 4. i18n
|
||||
|
||||
- [ ] 4.1 Add `journal.stats.*` keys (labels + unit suffixes for distance, elevation, time, speed, pace) to en + de.
|
||||
|
||||
## 5. Tests & checks
|
||||
|
||||
- [ ] 5.1 Unit: `movingTime` (with/without timestamps; moving ≤ elapsed), `deriveRate` (pace/speed/default), `formatStat`.
|
||||
- [ ] 5.2 Component: `StatRow` renders ordered items; subset preserves order.
|
||||
- [ ] 5.3 `pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2e` green.
|
||||
Loading…
Add table
Add a link
Reference in a new issue