docs(adr): ADR 0030's D13–D17, how Train records

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 11:04:53 +01:00
commit ff91f62303
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,7 +1,7 @@
# ADR 0030: Halkyon Train, what it does and where its data comes from
- **Status:** decided 27–28 Oct 2026, by the developer (D1–D12 below). Nothing is built
yet.
- **Status:** decided 27–28 Oct 2026, by the developer (D1–D17 below). The shell (D12)
is built; recording (D13–D17) is being built.
- **Numbering:** 0025 is held by the federation draft (stashed), 0026–0029 by the .NET
rewrite on the local branch `rewrite-dotnet`, abandoned on 27 Oct; Train takes the next
free number so that neither is ever read as this one.
@ -225,6 +225,65 @@ Strava. What was found, 27 Oct:
- **deployed once recording works** and the developer wants to use it, not as an
empty shell first.
## Recording (28 Oct 2026, the developer's)
- **D13 Positions are recorded by Train's own platform code** (recommended, over the
`geolocator` package and paid background-tracking packages): on Android a foreground
service of its own, as Learn's Pomodoro timer has, reading `LocationManager`'s GPS
provider as OpenTracks does and appending every fix to a recording file in the app's
folder, which the app follows while open and takes in when it opens again; on iOS
CoreLocation with background location; both behind one interface (`TrackRecorder`),
faked in tests. So a recording goes on with the app swiped away, and nothing of Google
Play services is in the app: `geolocator`'s Android part depends on Play services'
location library even where its `forceLocationManager` keeps it from being used, a
non-free library that F-Droid's main repository refuses, and its positions arrive in
Dart, which a swipe ends. The strap and the rower join the same service later.
- **D14 A workout and its series are kept apart** (recommended, over one row holding
every sample and over sealed GPX or FIT files in the instance's storage): `workout`
(type, start, end, name, notes, totals; changed when edited, the last writer winning, as
a studyset's payload) and `workout_series` (a chunk of one series, positions, heart rate,
strokes, pauses, of up to 10 minutes; written once as the workout is saved, never
changed, deleted with its workout, as Learn's review log), every payload sealed (D7)
and nothing readable in a column, not the type nor the start. Editing a workout touches
one small row, two devices never conflict over samples, and every source is one more
kind of series. A chunk's payload is JSON as every payload, each column (time,
latitude, longitude, altitude, accuracy, …) a list of whole-number differences from the
value before, gzipped before it is sealed, since sealed bytes compress no more. The
recording file is the device's own and never synced. UUID v7 ids carry their creation
time, which the sync engine's change times tell anyway (D7's list).
- **D15 Sealing: the envelope moves into halkyon_app, Train holds its key itself**
(recommended, over moving all of Learn's key management and over a copy of the
envelope in Train): Learn's `PayloadEnvelope` (a payload sealed for its table, row and
column; readable payloads signed by the account; what doesn't open `Locked`) moves
unchanged into halkyon_app, Learn importing it from there; Train has one key per person
in a synced `content_key` row, its sealed ID saying it is the person's, and a device
without the account's keys writes nothing, as in Learn. Learn's tree (private or
readable studysets, their records, rewrites) stays Learn's. Publishing a workout later
doesn't change it (asked by the developer): a published workout is a readable copy made
on the device (D7), which may leave out the heart rate or the first and last few hundred
metres, as Strava's privacy zones do, the sealed workout staying as it is; the envelope
can sign the copy with the account's key.
- **D16 Auto-pause by each activity's best signal, never discarding samples**
(recommended, over GPS alone and over manual pauses only): a pause, automatic or by
hand, is a marker in a series of its own, and moving time, pace and the totals leave
paused stretches out, so a misjudged pause is corrected from the data afterwards.
Running and walking stop with the phone's step detector (Android's step sensor, iOS's
pedometer; its permission asked for, GPS alone without it), confirmed by GPS; cycling
by GPS speed, paused below 3 km/h for 5 s and going on above 5 km/h; rowing by the
rower's stroke rate, no stroke for 5 s. On by default for running and cycling, the
thresholds adjustable once Train has settings.
- **D17 Running, walking, hiking, cycling and indoor rowing first, in this order of
parts** (recommended): 5a the envelope moved, Train's key holder, the two models and
their migration, the chunk codec and the first synced repository; 5b recording on
Android (the service, the file taken in after a swipe, the recording screen,
auto-pause, saving); 5c the workouts and a workout's page with its track on a map
(D11's `flutter_map` and the instance's tile source brought forward from route
planning, the developer's choice: a run without its track looks unfinished), totals,
splits and charts; 5d recording on iOS; 5e the heart-rate strap; 5f the WP 5000 over
FTMS, from frames recorded with nRF Connect. Health Connect (D6) and the Loop (D3)
follow recording. Each type maps onto one of Health Connect's exercise types, and is a
name in the sealed payload, so new ones need no migration.
## Consequences
- Train's sensors are one pattern: open profiles read by the app itself, on the phone;