uhryniuk 6f1bfc753a 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>
2026-08-17 14:09:11 -05:00
2026-08-17 14:09:11 -05:00
2026-08-17 14:09:11 -05:00
2026-08-17 14:09:11 -05:00
2026-08-17 14:09:11 -05:00
2026-08-17 14:09:11 -05:00
2026-08-17 10:41:35 -05:00

Rippr

A GPS ride recorder for motorcycles, on Android and iOS. Records telemetry into a local SQLite database, renders the traversed path on a map afterwards, and exports to GPX/GeoJSON.

Built for one specific use: hit start, put the phone in a pocket, ride, hit stop. Most of the design follows from that.

This is a Flutter port of the native Android app in ~/dojo/rippr (v1 → v2.0.1). The port is feature-complete; see docs/port/PARITY-AUDIT.md for exactly what is verified and what is not.

Flutter 3.47 · Dart 3.13        Android minSdk 26 · iOS 12+
~4,600 lines Dart               175 tests (171 unit/widget, 4 integration)

Status

Feature Status
GPS recording, foreground on Android / background mode on iOS Implemented; not yet ridden
Trips with pause / resume / stop / discard Working, 50 tests
Live speed + elapsed clock Working
Path on OpenStreetMap, per-segment, speed-coloured Working; colouring unverifiable without a real ride
Ride statistics + charts Working; bit-identical to the Kotlin original
Rename / delete / merge rides Working
GPX + GeoJSON export Working; byte-identical to the Kotlin original
Upload to a REST endpoint Implemented, no UI (as in the native app)
Notification actions (Pause/Resume in the shade) Absent — the one capability lost
Live map while recording · group ride Deferred to v3

The port has never recorded a real ride. No simulator produces velocity, so max speed, moving time, speed colouring, elevation against real GPS error, battery, and iOS stationary suspension are all unverified. That is docs/port/REAL-RIDE-CHECKLIST.md, and it is the next thing that should happen.

Quick start

Requires JDK 17–21 for Android (AGP rejects 25) and Xcode for iOS.

flutter pub get
dart run build_runner build          # Drift codegen
flutter analyze && flutter test
flutter test integration_test -d <device-id>
flutter run

Watch the disk. A full dual-platform build cycle costs roughly 10 GB, and the Android emulator needs 7.4 GB free just to boot. Run flutter clean before booting it.

Cross-language parity

The strongest guarantee in this repo. It compiles the real Kotlin sources from the native app and diffs them against the Dart port across twenty fixtures:

brew install kotlin
JAVA_HOME=<jdk-21> ./tool/parity/run.sh

Everything matches to the last digit — including the elevation accumulator at 38.959594555022136 and byte-identical GPX/GeoJSON. The single expected difference is speed-derived values, where Kotlin's 32-bit Float widens with artefacts Dart's uniform double does not reproduce.

Documentation

Document Contents
docs/ARCHITECTURE.md Why it is built this way, and what changed from the native app
docs/v3/ v3 tickets — one file per feature, ready to pick up
docs/BACKLOG.md The v3/v4 reasoning behind those tickets
docs/LAUNCH.md Getting from "on my phone" to the app stores, in stages, with costs
docs/port/IOS-VERIFICATION.md What a Mac can prove about the iPhone build, and what cannot
docs/port/PLAN.md The 28-task migration plan
docs/port/PROGRESS.md What actually happened, including every bug found
docs/port/PARITY-AUDIT.md Feature-by-feature, with the evidence behind each claim
docs/port/REAL-RIDE-CHECKLIST.md The outstanding work
docs/port/RELEASE-IOS.md App Review readiness
docs/PORT_RESEARCH.md The original research this plan was built on

The native repo's docs/v1/, docs/v2/ and docs/v3/BACKLOG.md remain the record of how the app got here and where it is going.

Two bugs this port found in the native app

Crash recovery measured the dead time as distance. restoreAfterProcessDeath says it resumes into a new segment; it actually adopts the segment a crash left open, so the authoritative recomputation measures straight through the gap. Reproduced at 111 km of phantom distance. Fixed here.

The elevation regression guard passes on seed luck. On a shared fixture the algorithm yields ~39 m, which would fail the native test's own 35 m bound. It passes only because kotlin.random.Random(42) happens to draw a benign sequence.

Before shipping

  • Switch the application id from com.rippr.port back to com.rippr. The suffix exists so the native app can be installed alongside during the port — keep it until the real ride comparison is done.
  • Replace the default Flutter app icon; the native artwork is in ~/dojo/rippr/design/.
  • Build and test a release build. Everything so far has been debug.
Description
No description provided
Readme 176 MiB
Languages
Dart 98.6%
Kotlin 0.7%
Shell 0.4%
Ruby 0.2%
Swift 0.1%