# 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](../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 1. `Activity` enum in `domain/models.dart`; `activity` field on `Trip` 2. Drift column with `.withDefault(Constant('motorcycle'))` 3. **Migration**: `schemaVersion` 1 → 2, `m.addColumn(trips, trips.activity)` 4. `ActivityProfile` in `stats/` holding the constants above; thread it through `computeSummary`, `speedHistogram`, `Accumulator`, `isUsableFix` 5. Persist last-used activity in `Config` 6. Trip detail: activity row, editable via the same pattern as rename 7. 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 analyze` clean, all existing tests still pass ## Tests - **Migration test** — build a v1 database, migrate, assert rides and points survive. Drift's `MigrationTestHelper` with 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 `` 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).