Files
samplez/rippr-src/docs/v2/12-trip-management.md
uhryniuk 280fd7f988 Add Rippr source snapshot and full-history bundle
Two copies for two jobs. rippr-src/ is a browsable git archive export of
the tracked tree at 2f76983 - no build outputs, no local.properties, no
nested .git - which is convenient to read in gitea but carries no history
and will drift.

rippr-full-history.bundle is the real backup: all 18 commits, verified as
"records a complete history" and test-cloned before committing. This
matters because ~/dojo/rippr has no git remote and otherwise exists only
on one machine.

rippr-src/SNAPSHOT.md explains the difference and how to restore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 08:30:25 -05:00

4.5 KiB
Raw Blame History

T12 — Trip rename / delete / merge

Phase 4 · Depends on T10, T11 · Status Done

Goal

Let rides be renamed, deleted, and merged. Done when a ride can be given a meaningful name, junk test rides can be removed, and two trips can be combined into one with correct recomputed aggregates.

Context

Merge exists as a safety valve. It was originally promised to rescue a ride wrongly split by the migration heuristic; that heuristic is gone now that v1 data is being discarded, but merge keeps earning its place:

  • A ride accidentally stopped and restarted mid-route
  • Test rides that should be one logical trip
  • Any future automatic splitting (v3+) needs an undo

Delete is the mundane but most-used of the three — a session of testing leaves a dozen 30-second trips cluttering the list.

Trip.name is already nullable in the T02 schema, with the UI deriving a date label when it is null. CASCADE foreign keys mean delete is a single statement.

State after T08–T11

TripDetailViewModel already has rename(name) and delete(onDeleted); TripsViewModel has delete(tripId). The repository has renameTrip (which already collapses blank to null) and deleteTrip. What is missing is the merge operation and all of the UI: selection mode on the list, the rename affordance on detail, and the confirmation dialogs. ConfirmDialog exists in ui/components/Stats.kt.

Design

Rename

Inline edit on the detail header. Empty input clears back to null (date-derived label) rather than storing an empty string — those two states must not diverge.

Delete

From both the list (swipe or overflow) and detail. Confirmation stating what is lost. CASCADE removes segments and points. After deleting from detail, navigate back.

Merge

Selected from the trips list: long-press to enter selection mode, pick exactly two, then Merge.

Semantics:

  • The earlier trip (by startedAt) is the survivor
  • The later trip's segments are re-parented to the survivor
  • Segments are never joined — the boundary between the two rides becomes a segment boundary, exactly like a pause. This is correct: the rider genuinely was not recording in between, and joining them would draw a false straight line across the gap
  • endedAt becomes the later trip's endedAt
  • The now-empty later trip row is deleted
  • Aggregates are recomputed from scratch with RideStatistics.compute(), not summed
  • The survivor keeps its name if set; otherwise inherits the later trip's

Merge runs in a single @Transaction — a partial merge would leave orphaned segments.

Merging is not offered while either trip is active.

Implementation

  1. TripRepository.renameTrip(id, name: String?)
  2. TripRepository.deleteTrip(id) — CASCADE
  3. TripRepository.mergeTrips(survivorId, absorbedId) in one transaction: re-parent segments → update endedAt → delete absorbed row → recompute aggregates
  4. Selection mode in TripsScreen, enabled at exactly two selections
  5. Confirmation dialogs for delete and merge; merge states the resulting distance
  6. Inline rename on the detail header

Acceptance criteria

  • Rename persists; clearing reverts to the date label
  • Delete removes the trip and every point (verified by row count)
  • Merge re-parents all segments and deletes the absorbed row
  • Merged aggregates equal a fresh computation over the combined points
  • Merged trip shows a segment boundary at the join, not a joined line
  • Merge unavailable unless exactly two completed trips are selected
  • Merge is atomic — an interrupted merge leaves no orphans

Tests

Instrumented:

  • Delete cascades: zero segments, zero points remain
  • Merge: segment count is the sum of both; point count is the sum; no orphans
  • Merged aggregates match RideStatistics.compute() over combined points
  • Merge of trips out of chronological order still picks the earlier as survivor
  • Rename to empty stores null, not ""

Compose UI: selection mode enables Merge only at two selections. Not written — the UI-test debt from T08–T11 covers this too; tracked in T18.

Risks / gotchas

  • Do not sum aggregates on merge. Distance is not additive across the join — there is a real gap. Recompute.
  • Chronological survivor. Selection order is not ride order; sort by startedAt.
  • Deleting the active trip must go through the service's discard path (T06), not a raw delete, or the service keeps writing into a deleted trip.

Out of scope

Splitting a trip; bulk delete; undo.