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>
This commit is contained in:
109
rippr-src/docs/v2/12-trip-management.md
Normal file
109
rippr-src/docs/v2/12-trip-management.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# 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
|
||||
|
||||
- [x] Rename persists; clearing reverts to the date label
|
||||
- [x] Delete removes the trip and every point (verified by row count)
|
||||
- [x] Merge re-parents all segments and deletes the absorbed row
|
||||
- [x] Merged aggregates equal a fresh computation over the combined points
|
||||
- [x] Merged trip shows a segment boundary at the join, not a joined line
|
||||
- [x] Merge unavailable unless exactly two completed trips are selected
|
||||
- [x] 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.
|
||||
Reference in New Issue
Block a user