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>
4.7 KiB
V3-01 — Activity type per ride
Phase Foundations · Depends on nothing · Size M · Status Done
Goal
Every ride records what it was done on — motorcycle, bicycle, scooter, skateboard, running, walking, other — and that choice drives per-activity defaults rather than just labelling the row.
Context
The recording pipeline never cared what you were sitting on: it records positions, speeds and altitudes. Only the UI says "motorcycle". Dylan rides both bikes and motorcycles.
This is also a positioning decision (see ../LAUNCH.md): it determines the store category, the screenshots, and who ever finds the app. Cyclists are a much larger audience and already pay for ride apps. Cheaper to settle before a store listing exists.
This ticket ships the port's first real migration. That matters more than the feature.
Design
Text enum column on Trip, exactly like TripState:
motorcycle · bicycle · scooter · skateboard · running · walking · other
Type drives defaults, not labels. Introduce an ActivityProfile rather than scattering
if (activity == …):
| Setting | Today | Why it must vary |
|---|---|---|
| Speed noise floor | 1.5 km/h | Right for a motorcycle; walking lives near it |
| Histogram bucket | 10 km/h | Useless for running — one bucket. Wants ~1 km/h |
| Accuracy gate | 50 m | A bike at speed tolerates looser fixes than a walker |
| Elevation smoothing window | 15 samples | Tuned for 2 Hz at road speed |
| Map fit zoom | — | A 2 km walk and a 200 km ride differ |
No picker in front of Start. The founding premise is press-and-go with gloves on. Default to the last activity used; make it editable on trip detail next to rename.
Implementation
Activityenum indomain/models.dart;activityfield onTrip- Drift column with
.withDefault(Constant('motorcycle')) - Migration:
schemaVersion1 → 2,m.addColumn(trips, trips.activity) ActivityProfileinstats/holding the constants above; thread it throughcomputeSummary,speedHistogram,Accumulator,isUsableFix- Persist last-used activity in
Config - Trip detail: activity row, editable via the same pattern as rename
- Trips list: show the activity icon on each tile
Acceptance criteria
- A new ride records an activity; existing rides read
motorcycle - A v1 database opens, migrates, and keeps every ride and point
- Changing a trip's activity recomputes its aggregates under the new profile
- Start still takes exactly one tap
flutter analyzeclean, all existing tests still pass
Tests
- Migration test — build a v1 database, migrate, assert rides and points survive.
Drift's
MigrationTestHelperwith a generated v1 schema. - Profile selection: each activity yields its own noise floor and bucket size
- A running-activity ride produces a histogram with more than one bucket
- Round-trip the enum through the database
Risks
- The migration is the risk. Getting it wrong destroys real rides, and the destructive fallback is deliberately gone. Write the migration test first.
- Recomputing aggregates on activity change is easy to forget — a ride switched from motorcycle to walking keeps a wrong moving time otherwise.
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.lastActivityfield. "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 tomotorcyclewhen the database is empty. ActivityProfilelives indomain/activity_profile.dart, not folded intomodels.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 andstats//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).