V3-01: activity type per ride

Adds Activity (motorcycle/bicycle/scooter/skateboard/running/walking/other) as
a column on Trip, and threads it through as real behaviour rather than a label:
ActivityProfile (domain/activity_profile.dart) carries a noise floor, accuracy
gate, histogram bucket width, and elevation smoothing window/threshold per
activity, consumed by sanitizeSpeedKmh, isUsableFix, computeSummary, Accumulator
and speedHistogram. The motorcycle profile reproduces the exact constants the
app shipped with before this existed, and a test asserts they never drift apart.

This is the port's first real migration: schemaVersion 1 -> 2,
m.addColumn(trips, trips.activity) with a motorcycle default so every existing
row survives unmodified. Proven with a hand-built v1 SQLite file (raw sqlite3,
not drift_dev's schema tooling) that a real database with real rides upgrades
and keeps every trip, segment and point.

No picker in front of Start: a new trip defaults to whichever activity was most
recently used, derived live from the trips table rather than duplicated into a
Config field. Editable afterwards on trip detail, which recomputes aggregates
under the new profile immediately -- a walking pace that reads as noise under a
motorcycle's floor reads as real movement once the activity is corrected, and
there's a test proving exactly that transition.

The widget-test pass for the activity-picker sheet caught a real overflow bug:
seven options overflowed a Column-based bottom sheet the same way the record
screen once did. Fixed with a scrollable ListView + isScrollControlled, same
shape as that earlier fix.

188 tests (171 -> 188): 2 migration, 5 ActivityProfile, 3 profile-threading
proofs in computeSummary, 5 repository, 2 widget. Analyze clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 14:09:11 -05:00
parent 342a8f8382
commit 6f1bfc753a
20 changed files with 876 additions and 36 deletions

View File

@@ -1,6 +1,6 @@
# V3-01 — Activity type per ride
**Phase** Foundations · **Depends on** nothing · **Size** M · **Status** Not started
**Phase** Foundations · **Depends on** nothing · **Size** M · **Status** Done
## Goal
Every ride records what it was done on — motorcycle, bicycle, scooter, skateboard,
@@ -67,3 +67,31 @@ Default to the last activity used; make it editable on trip detail next to renam
## Out of scope
Per-activity totals or a stats screen. GPX `<type>` export (see V3-14).
## Outcome
Shipped as designed, with two deliberate deviations from the ticket text, both
recorded here rather than silently:
- **No `Config.lastActivity` field.** "Default to the last activity used" is instead
derived live from the trips table itself (`AppDatabase.mostRecentTrip()` /
`TripRepository._lastUsedActivity()`) rather than duplicated into a separate
preference. One source of truth, no write path to keep in sync, and it degrades
correctly to `motorcycle` when the database is empty.
- **`ActivityProfile` lives in `domain/activity_profile.dart`**, not folded into
`models.dart` — kept the domain model (`Trip.activity`) separate from the behavioural
defaults built on top of it, and avoided a dependency cycle between the domain layer
and `stats/`/`recording/`.
Map-fit zoom (mentioned in the ticket's defaults table) turned out to need no work:
`RideMap` already fits to the ride's actual recorded bounds, which is activity-agnostic
by construction.
The widget-test pass caught a real overflow bug independent of activity type: the
7-item activity picker sheet overflowed a `Column`-based `showModalBottomSheet` the same
way the record screen once did (see `docs/port/PROGRESS.md`, Phase 4). Fixed with a
scrollable `ListView` + `isScrollControlled: true`, the same shape as that earlier fix.
188 tests total (171 → 188): migration (2), `ActivityProfile` (5), `computeSummary`
profile-threading proof (3), repository (5), widget (2).