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:
2026-08-11 08:30:25 -05:00
parent 4debdfa518
commit 280fd7f988
106 changed files with 10377 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
# T03 — Trip repository + reactive state
**Phase** 1 · **Depends on** T02 · **Status** Done
## Goal
Own trip lifecycle transitions in one place and make recording state a function of the
database rather than an in-memory flag. Done when `RecordingState` is deleted, the UI
observes trip state through a Flow, and killing the app process no longer loses track of
whether a ride is in progress.
## State as of T02
`TripDao`/`SegmentDao` already expose every query this task needs (`observeActive`, `getActive`, `openSegment`, `close`, `setState`, `deleteById`).
T02 left **temporary scaffolding** that this task and T06 replace:
`TrackingService.openTripAndSegment()` opens a trip and segment inline via the DAOs, and
`@Volatile currentTripId/currentSegmentId` are read by the location callback. The callback
drops fixes while those are 0 so a point can never violate the FK — **preserve that guard**.
## Context
v1 keeps state in a process-wide singleton in
`app/src/main/java/com/rippr/Telemetry.kt`:
```kotlin
object RecordingState {
private val _isRecording = MutableStateFlow(false)
…
}
```
This was already an improvement over the Activity-local `isRecording` the original spec
used, but it still lies after process death: `START_STICKY` restarts the service with a
null intent and the flag resets to `false` while a ride is genuinely underway.
The fix is that the database already knows. A row in `trips` with `endedAt IS NULL` *is*
the fact of an in-progress ride, and it survives anything short of uninstall.
**Keep the rest of `Telemetry.kt` exactly as it is.** `msToKmh`, `sanitizeSpeedKmh`,
`isUsableFix`, `formatDuration`, and `encodeBatch` are pure, already unit-tested by
`TelemetryTest`, and have no reason to change. Only `RecordingState` goes.
`RecordingState.lastUploadError` is also used by `TelemetryUploader` and surfaced on the
Record screen — that one is genuinely ephemeral UI state and can stay as a small
`UploadStatus` object, or move onto the repository. Do not silently drop it.
## Design
```kotlin
class TripRepository(
private val tripDao: TripDao,
private val segmentDao: SegmentDao,
private val pointDao: TrackPointDao,
) {
fun observeActiveTrip(): Flow<Trip?>
fun observeTrip(id: Long): Flow<Trip?>
fun observeCompletedTrips(): Flow<List<Trip>>
suspend fun startTrip(now: Long): Trip // creates trip + first segment
suspend fun pauseTrip(now: Long) // closes open segment, state=PAUSED
suspend fun resumeTrip(now: Long) // opens new segment, state=RECORDING
suspend fun completeTrip(now: Long) // closes segment, endedAt, COMPLETED
suspend fun discardTrip() // deletes active trip (CASCADE)
}
```
Every transition is idempotent and safe to call from an unexpected state — the service
can be restarted by the OS at any moment, so `resumeTrip` on an already-recording trip
must be a no-op rather than opening a duplicate segment.
**Single instance.** v1 uses a `@Volatile` double-checked singleton for `AppDatabase`;
follow the same pattern for the repository rather than introducing a DI framework. The
app is small and Hilt would be more ceremony than it earns.
## Implementation
1. Create `data/TripRepository.kt` with the interface above.
2. Implement each transition as a single Room `@Transaction` so a crash mid-transition
cannot leave a trip with two open segments.
3. Guard each transition on current state; log and no-op on nonsensical transitions.
4. Delete `RecordingState` from `Telemetry.kt`.
5. Relocate upload-error state so the Record screen keeps its indicator.
6. Update `TelemetryUploader` and `TrackingService` references.
## Acceptance criteria
- [x] `RecordingState` no longer exists
- [x] `observeActiveTrip()` reflects reality after a simulated process death
- [x] Double `startTrip()` adopts rather than duplicating
- [x] `resumeTrip()` while already RECORDING is a no-op
- [x] Upload-error surfacing still works, now via `UploadStatus`
- [x] `TelemetryTest` still green (the pure functions are untouched)
## Tests
Instrumented, against a real in-memory database:
- start → pause → resume → complete produces exactly two segments
- discard removes the trip and all its points
- transitions from wrong states are no-ops
- `observeActiveTrip` emits null after complete
## Risks / gotchas
- **Transitions race the writer loop.** The service writes points continuously; pausing
mid-flush must not orphan points into a closed segment. T06 handles ordering — pause
drains the channel *before* closing the segment.
- **Do not delete the pure Telemetry functions.** They are load-bearing and tested.
## Out of scope
Wiring transitions to service actions (T06) and aggregate maths (T07).