A vest keeps its weight and, if adjustable, how far it goes. At a place
that has one, a set of bodyweight work offers it beside the weight, a
tap writing its weight in.
Whats-New: Add a weight vest to your equipment, and put its weight on a bodyweight set with a tap.
Whats-New-De: Trage eine Gewichtsweste bei deiner Ausrüstung ein und übernimm ihr Gewicht mit einem Tipp in einen Satz mit Körpergewicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A home trainer's or an exercise bike's ride had no place: Cycling waits
for GPS and pauses by speed. Cycling indoors needs no GPS, route or
device: the time, then whichever strap, speed and cadence sensor or
power meter there is. It goes to Health Connect as stationary biking,
and another app's stationary ride comes in as one.
With no GPS, steps or device the recorder has no foreground service
type, which Android 14 on doesn't allow, so it asks for the physical
activity permission and doesn't start without it.
Whats-New: Record a ride on a home trainer or exercise bike with Cycling indoors, without GPS.
Whats-New-De: Zeichne eine Fahrt auf dem Heimtrainer mit Radfahren drinnen auf, ganz ohne GPS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The skin temperature's line ran backwards in places: the band's samples
aren't always in time order, and neither the Loop's lines, nor the kept
day, nor the chart sorted them. They are sorted now, a time given twice
kept once, as the lines are read and as a kept day is opened, and
WorkoutChart draws any points in the order of their x, from the least
to the most.
The heart rate spiked down: the band writes 0 where it lost contact,
and each run's mean, the chart's one point a run, counted those in. A
value that isn't a heart's rate is 0, none read, in its place (so the
samples after keep their times), left out of every mean, days kept
before included; the chart draws the samples a point a minute, averaged
across runs, rather than one mean a run, which zigzagged where runs lay
close. ADR 0030, D65.
Whats-New: A day's skin temperature no longer runs backwards, and its heart rate no longer drops where the band lost contact.
Whats-New-De: Die Hauttemperatur eines Tages läuft nicht mehr rückwärts, und sein Puls fällt nicht mehr ab, wo das Band den Kontakt verlor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
As Strava's: over the last 6 % below the highest the sheet goes, its
top corners square off from 28 to 0 and its handle folds away, so the
bar above, opaque by then, meets it without the map showing through
the corners. The bar names the activity rather than the workout, whose
name already heads the sheet: it was in sight twice. sheetSquared in
track_camera.dart, tested beside barShown. ADR 0030, D65.
Whats-New: A workout's page pulled all the way up meets the bar square, which names the activity.
Whats-New-De: Ganz hochgezogen schließt die Seite eines Trainings eckig an die Leiste an, die die Sportart nennt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
On a fresh install (GrapheneOS) a walk recorded no GPS on the screen,
which waited for GPS throughout though the file was whole. The app
listens to the recorder's lines as soon as it asked for the start, and
Android starts the service a moment later: listening first, it got
nothing of the file, and the service then wrote the header straight to
the file, past what hands each line on. The reader takes the first line
it gets as the header, found none, and dropped every line after.
The service now writes its header through write() as every line, and
the app takes the header from the file's first line where lines came
without one (RecordingReader.recover, on the next second), reading the
lines kept since after it, so either order and either recorder version
is followed. The fake recorder can now begin late, as the service does.
On foot, auto-pause by steps paused after 5 s and went on only with a
step where the phone has no step detector or one switched off
(GrapheneOS' Sensors): GPS speed decides there, and until the first
step and once the detector has been silent 30 s. A minute without any
fix, the recording screen asks whether location is on and precise.
ADR 0030, D65.
Whats-New: A recording follows its GPS again on a phone where it waited for GPS throughout.
Whats-New: Auto-pause on foot goes by GPS speed where the phone counts no steps.
Whats-New: After a minute without GPS, the recording screen asks whether location is on and precise.
Whats-New-De: Eine Aufzeichnung folgt ihrem GPS wieder auf Handys, auf denen sie die ganze Zeit auf GPS wartete.
Whats-New-De: Die automatische Pause zu Fuß richtet sich nach der GPS-Geschwindigkeit, wo das Handy keine Schritte zählt.
Whats-New-De: Nach einer Minute ohne GPS fragt die Aufzeichnung, ob der Standort an und genau ist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Every weight is still kept in kilograms. It is shown and entered in the
person's unit, chosen in Your body (Weight in: kg | lb, the profile's
new weightUnit), else the region's: pounds in the US, Liberia and
Myanmar. The entry takes 44 to 880 lb, the same 20 to 400 kg. A weight
left untouched in Your body is saved exactly as it was, not rounded
through pounds. The other figures stay metric.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
What a switch or a section explains, rather than what is the person's
own, is one tap away: `HelpTitle`, a tile's title with halkyon_ui's
`HelpButton` beside it, in place of the explaining subtitles of Moving
more's reminders and suggestions, the music's playlists and smaller
copies, and the Loop's detection, firmware check and firmware through
the instance. Health Connect's short hints stay (they name what each
switch does, and its "taken back" state takes their place), as do its
rationale, the Loop's warnings before an update or a reset and the
settings' one-line hints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A person's weights are each a sealed `body_weight` row, as the days and
the places: the weight in grams, when it was measured (UTC), where it
came from and, for Health Connect's, the record's id and app are the
payload; the server sees the space and the ciphertext alone.
Progress gets a Weight card after Your body: the latest and its day,
its line over time (held, each weight and its day), the plus to enter
one (a comma taken as a point, 20 to 400 kg, when measured, today by
default) and All, the list to delete one. The latest weight is also
Your body's, which the band's and the zones' figures work from. In
kilograms alone: Train is metric throughout and has no units setting
yet.
Health Connect's weight records are read in when switched on in its
screen (READ_WEIGHT, asked as the others): each record once, its id kept
in health_connect.json also after it is deleted here, never Train's own,
and none within 10 minutes and 0.1 kg of a weight entered in Train.
The migration (20261103204323683-body-weights):
- creates (or replaces, unchanged) `gen_random_uuid_v7()`, the
default of every UUID id, as each of Serverpod's migrations does;
- CREATE TABLE "body_weight": "id" uuid primary key defaulting to a
UUID v7, "spaceId" bigint (the sync space), "keyId" text (the key
that sealed it), "payload" bytea NOT NULL (the sealed content),
"nonce" bytea;
- CREATE INDEX "body_weight_space_idx" on "spaceId", for a space's
rows as the sync reads them;
- a foreign key from "spaceId" to "offline_sync_spaces"("id"),
ON DELETE CASCADE, so that an account's deletion takes its weights
with its space;
- the version rows of Train's and every module's migrations.
Nothing existing changes, so nothing is lost.
Tests: the row written and read back in its owner's space, and the
denials: another person reads none of them, and writing one into
another's space throws OfflineSyncSpaceMembershipException and leaves
it empty; an account's deletion takes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
WorkoutChart, the line of every chart in Train (a day's heart rate and skin
temperature, a workout's, a route's height): held, a line marks the point
nearest the finger and follows it as it moves, its value beside the title,
with its moment where the chart says how to write it (a day's heart rate:
the time of day). A tap does nothing; lifting the finger clears it (the
developer's review of 3 Nov 2026, as stock charts do).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The night's chart threw mid-paint where Polar's last phase starts at the
night's end or after it: its right edge was clamped between left + 1 and
the row's width, the lower bound then above the upper (an ArgumentError).
A throw mid-paint leaves the canvas moved, so the rows below and the charts
after them were drawn out of place, the skin temperature's among them. Each
phase now stays within its row, a pixel wide at least.
Nightly Recharge showed Polar's marks for a figure it has none of (-1 for
the recovery and the intervals, -100 for the nervous system's status, 0 for
a baseline not yet learnt) as the person's own. Recharge.known leaves them
out, on the page and in the days' list; the figures are rows, each with the
person's usual below it, and Polar's explanation of them (the status from
-10 to +10, what each measures) is behind halkyon_ui's question mark.
The page is a screen of its own over the tabs, without the bar and with the
way back at its top left, its parts in cards (the developer's review of
3 Nov 2026; D60 had it inside Profile).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
- A workout with a track: its map to the foot of the screen under a sheet
with its name; once the sheet settles, the map fits the track anew, eased,
into what is left free; pulled up, the round buttons over the map fold
into an app bar whose chevron lowers the sheet again. From expanded
windows on, the map beside the rest. The page stays in Home but opens
over the shell, without the floating bar.
- What the map's source sees moves behind a question mark.
- The map's credit on the app's theme: flutter_map's, built on
flutter/material, never saw it (material_ui's Theme is a class of its
own) and drew the theme's text on white; map_tiles read that Theme too,
and so never chose the instance's dark map (D50).
- Places named in the app's language (name:<language>) where OpenFreeMap's
styles ask for the English name.
- The route planner's hint behind a question mark; a new route's map opens
at zoom 14 where the phone was last known to be, else where the newest
workout with a track started; its details beside the map from expanded
windows on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The band's screen shows whether the Loop detects training by itself and
switches it, with the shortest session it keeps (10, 15, 20 or 30
minutes), so a session nobody recorded still comes in afterwards as a
workout (ADR 0030, D39). Read on each connection and after the band's
setup through the SDK's getUserDeviceSettings, set with
setAutomaticTrainingDetectionSettings; its sensitivity stays as the band
has it, as the SDK documents no range for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Settings' Moving more, on Android: a reminder to get up after an idle
time, and a suggested walk or run, each off until turned on (then every
30 minutes, weekdays 9:00 to 18:00, not 12:00 to 13:00, each value the
person's), turning on asking for the activity and notification
permissions.
A WorkManager job every 15 minutes reads the phone's step counter and
keeps two hours of readings. It reminds where fewer than 50 steps came
over the idle time, all of it within the hours and outside the break,
on the person's days, never while Train records or the phone is in a
car (Android's car mode, or a car's Bluetooth audio or hands-free). It
suggests a walk or a run where the last span averaged a walking or
running cadence, once an hour; tapped, the Record sheet for it opens.
The decisions are pure Kotlin (Movement.kt), tested on the JVM; the
notifications' words are the app's, written into the job's
configuration as the person's app opens.
Polar's SDK sets neither the band's inactivity alert nor tells of one
without an all-day connection, hence the phone's steps (ADR 0037).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A workout with a track gets the weather over it as its page opens, once
while it has none: the hours from the one it started in to the one it
ended in, kept in its sealed payload (temperature, feels-like, humidity,
precipitation, the WMO code, the wind, its gusts and direction) and shown
under its totals with the link to Open-Meteo its licence asks for.
Train's server asks Open-Meteo in the person's place (the `weather`
endpoint), so Open-Meteo never sees their address: the place rounded to
two decimals (also on the phone), a span of at most 48 hours, the
forecast API for recent spans and the archive for older ones, for people
signed in, at most 30 requests a person a minute, nothing kept or
logged. On by default; `WEATHER=off` switches it off, and
`OPEN_METEO_API_KEY` moves it to the paid hosts for an instance used
commercially.
Health Connect's fingerprint leaves the weather out, so keeping it
writes nothing again. No table and no migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Strength plans of exercises from a catalogue or the person's own, in
blocks of one exercise or several done in turn (supersets, circuits),
each with its sets, reps or seconds, weights and rests; each a sealed
`strength_plan` row. Plans are marked with what the place chosen lacks,
and a session offers swaps moving the same way that the place allows.
A session runs on every device, set by set with the rest counted down,
each weight made up from the place's plates, bars and fixed weights with
the plate changes between sets, kept in strength_session.json after each
change so one cut off is carried on, and saved as a workout of the new
activity Strength with its sets, listed on its page; strength training
in Health Connect and FIT, no GPX.
Migration 20261102113104496-strength-plans is purely additive: it
creates the table strength_plan (id uuid v7, spaceId, keyId, the sealed
payload and its nonce), its index on spaceId, and a foreign key to
offline_sync_spaces with ON DELETE CASCADE, so deleting a person's sync
spaces deletes their plans; then the modules' version rows. The
gen_random_uuid_v7 function is created or replaced as in every
migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The places a person trains at (home, a gym), each a sealed, synced
`training_place` row with its equipment: bars and handles with their own
weight, plates by weight and count, dumbbells and kettlebells, the rest
by count, the person's own by name. A new place starts from what a home
or a gym usually has. The place last chosen is remembered on the device,
never its location.
The plate calculator loads each weight both sides alike, the heaviest
load not above it with the fewest plates, heavier inside, and over a
series of sets picks the loads making the fewest plate moves, listing
what comes off and goes on. Equipment is in Settings; strength plans
are to build on it.
The migration only creates the table, its space index and its cascade
to the sync engine's spaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Above the days: a card with your body's weight and resting and maximal
heart rate, tapped opening Your body; the last 12 weeks' time trained as
bars, a week tapped showing its time, distance and workouts; and this
calendar year by activity. Home's week card reads its time as the days do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The week's time, distance and workouts above Home's list, and a strip of
its days, each as tall as the time trained on it, starting on the day the
person's language starts weeks on. Home's empty hint points to the Record
tab, where its button had gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A day's, night's and heart chunk's ID was a UUID of version 5 of the
person and the date, which the instance can hash for every date itself:
each ID told it which days the band was worn. halkyon_keys' keyedRowId
hashes the name under the account's X25519 secret key (BLAKE2b, a UUID of
version 8), the same on every device of the account and unreadable
without its keys; ContentCipher.rowId and TrainKeys.rowId hand it on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The measure was held to GPS making 90 % of the wheel's way at the
circumference taken, 2105 mm to start: a real wheel under some 1.9 m fell
short of it on every ride and was never learnt. A ride now measures where
precise fixes followed it for 95 % of its moving time, its quotient between
1 and 3 m as one typed may be.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
An http address to another machine is refused as typed, saying why: over it the password, the token and what the person listens to went through the network in clear, and the music job, held by Android to https, retried for ever without a track. A Jellyfin on the phone (localhost, 127.0.0.1) stays reachable over http, the network security config allowing those two alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Relayed by the instance, the version asked again before downloading was written even where it wasn't the one shown and agreed to, and the band recorded as at the agreed one. Another version now writes nothing and is offered instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A trainer ride exported heart rates alone (or nothing) and a ride through tunnels the wheel's totals over GPS's distances; GPX's type came before its description and TCX's watts before the cadence, against their schemas. The wheel's size was learnt from rides GPS covered only in part. The live screen parsed the whole recording again on every sensor frame; now it reads each line once and shows sensors on the second's tick. The offline maps' requests time out after 20 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Smaller copies, on by default on the music screen, has the music job
fetch a file above 256 kbps as Opus at 128 kbps in WebM, but for the
lossy codecs (MP3, AAC, Vorbis, Opus) at any rate. Each track keeps
which copy its file is; switching fetches the other copy without
working out the tempo again, the file kept until the new one is whole.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The app reads the playlists as before and hands the missing tracks to
MusicWorker.kt, which runs every 6 hours and once whenever the list
changes, on unmetered networks with the battery and storage not low.
MusicFetcher.kt downloads each with the token from JellyfinTokens, works
out its tempo and appends a line to fetched.txt, which the app takes in
as it opens and as each track comes. The app's own download is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The token moves from flutter_secure_storage into JellyfinTokens.kt: a
file per person in the no-backup folder, sealed with AES-GCM under a key
of Android's Keystore, the person's ID as associated data, so that the
music job to come can read it as the app does. A token the keyring held
is moved over as it is first read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Health Connect gets the power meter's watts, the crank's cadence
(CyclingPedalingCadenceRecord), a run's step cadence (StepsCadenceRecord)
and, where the wheel measured the ride, the wheel's speed. The cadences'
permissions are asked with writing's and each record is left out where
its permission isn't granted; healthRecordsVersion in the fingerprint
writes every workout once more. On a bike, the wheel's speed decides
auto-pause while its sensor sends, GPS's once it has been silent 5 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A `search` endpoint asks the instance's Photon (compose.search.yaml, Europe's
database from GraphHopper, renewed monthly) for people signed in, limited and
unlogged. The planner gets a field over its map: the places found nearest the
map's centre first, in the app's language; one chosen is the next waypoint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Protomaps' basemap cut for Europe and served by pmtiles beside Train's server
(compose.maps.yaml), drawn by Train's own style, light and dark, which the
server serves. Only people signed in get tiles: map.source hands a key that
Caddy has the server check before each tile; nothing logs a tile.
Also fixes .dockerignore, which left apps/train/ out of the build context, so
Train's server's and BRouter's images couldn't have been built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The speed sensor's wheel gives a ride's distance, splits, progress and live
speed where GPS made less than 90 % of its way (a turbo trainer, a tunnel);
its circumference starts at 2105 mm, is learnt from rides with 2 km of GPS
or typed on the Record sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
11d, its sensors: a ride's Record sheet offers the CSC sensor (0x1816)
and the power meter (0x1818), found nearby and remembered as the strap
is; the person's devices are now one map of DeviceKind to DeviceStore.
The service parses CSC and Cycling Power measurements (JVM-tested,
counters' rollover, coasting), writes the wheel's revolutions, the
crank's cadence and the watts to the file and to series of their own.
The recording screen shows cadence and watts; the page the power's
chart, average, highest and normalised power and the cadence's chart and
average; GPX, TCX and FIT carry them. A plan's watts target reads the
power meter too.
The wheel's speed and distance follow. Not tried with a real sensor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
11c: where a recording has GPS and the phone a barometer, the service
averages its readings over 5 s into the file's `b` lines and the
workout's new `pressure` series (tenths of a pascal). The page, Health
Connect's totals and the exported files take the fixes' altitudes from
it: the standard atmosphere's height at the pressure, levelled by the
median of GPS's altitudes less those; climbs count above 1 m. Workouts
recorded before keep GPS's altitudes; an older app skips the series.
Not tried on a phone yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
11b: Export on a workout's page hands it to the share sheet as GPX (with
a track only; heart rate and cadence in Garmin's TrackPointExtension),
TCX (laps, heart rate, cadence, watts, a row's distance) or FIT (file
ID, records, timer events at the pauses, laps with their intensities,
session, activity), all from the same points: the precise fixes while
moving, else the rower's frames, else the heart rates; a plan's steps
are the laps. The FIT writer is our own (protocol 1.0, CRC-16/ARC),
tested by a reader written from the specification.
Not tried yet with Strava, intervals.icu or Garmin Connect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Step 11 takes up what OpenTracks does that Train didn't (D49). 11a: a
recording announces each kilometre on foot and each 5 km by bike, never
on a rower, where the Record sheet's switch is on for the activity
(remembered in the person's announce.json, off at first): the distance,
the moving time, the split's pace or speed and the heart rate. The app
hands the recorder the words in its language; Announcer.kt counts the
moving time and the distance, Cues.kt speaks the line after a cue still
speaking.
Not tried on a phone yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A workout keeps its cadence as a series of its own (steps a minute, every
10 s while moving). The Record sheet offers Music, Metronome or Off on
foot, remembered; Music once a kept track has a tempo. The recorder
measures the cadence over the last 30 s and plays a fitting track (its
tempo, half or double, within 6 %) through Media3's ExoPlayer, retuning
the track playing where it still fits, else crossfading over 3 s; a
plan's new step picks anew. A run starts at the cadence learnt from the
person's runs, which also gives a plan's steps their cadence.
Media3 1.10.1, pinned by Gradle's checksums (trusted on first download
from Google's Maven, 29 Oct 2026), for review:
media3-common aar 39686f54ff6baf8cbc841199afd55619533eaef2ab6cd0efc32b8a3c55dd88c3
module a81b2f0ebc256fe0e09f05d93c5259ade5bf44707748b9295343bee67575d9c2
media3-container aar a198b62c577fa434941282fbfa31eb66ff0ff308a6573edf0b5dd01f4e38445d
module e2a1c62c7e5d1013b906bc6ff5056db3e797cda4505e2d7d229524b24ca67a0e
media3-database aar 520e6f814e42a7d428db187ac073b453c0fcba0f4f5f4edd1cf33fafd2cfb740
module c380fea161e80d8074215a20e86e3dc2c2d825de964ca87aee66c8331f5ae252
media3-datasource aar e22f9f779047ce951152c793af094593e7cbb52475a82d56fa4f869fd319e700
module 9d32d2ec6a983ee9f418bcc05777aa23eee4db37bf794f44fb66a05d6f22e963
media3-decoder aar a9fb2b8fa1d54b4a8d6bda98a1c655b04d59c74b3ace7e2503e9e39830f13aee
module 388d5226366edd1facf8eec42cb5420871a46032d545286e90eaea8454789580
media3-exoplayer aar 19090f829afbbb595d67618f59c212f72b433ac01dc72b47594cf42552c8ed61
module ba0b9ef325fa114baa0f2fbdb4caf54029606924be1999f4af02bed5f079756b
media3-extractor aar 3c823279356b64f4215a8c6cb1e6bfa60c533351a651786ad6fa69ad001ef1b1
module b3eb755d7373e4fd199ff80814005f3b303693dfb8ace16b5b9482655d49d493
Not tried on a phone yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Music's second part: the marked playlists' tracks are downloaded as
Jellyfin keeps them, on unmetered networks only, while the app is open
or resumes, and deleted once no marked playlist holds them. Each
track's tempo is worked out once on the phone: a BPM tag where the file
has one, else a minute decoded with MediaCodec and its spectral flux
autocorrelated (Tempo.kt, tested on the JVM). The music screen shows
what is kept and deletes it after asking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Music's decisions as D47, and its first part: the music screen from the
account sheet, on Android, signs in to the person's Jellyfin with Quick
Connect, else a password, and lists their playlists of music to mark
for running. The token is in the keyring, the server and the marks in
the person's music.json, nothing on Train's server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A route's page shares it as GPX 1.1 (Android's share sheet, else saved
where the person chooses): its way a track with elevations, its
waypoints and turns as named points. A GPX file comes in from the Routes
tab or, on Android, opened with Train or shared to it; it is planned
along the paths through its points or kept as drawn, then named and
saved as a new route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A saved route's page has Keep the map offline: the tiles within 2 km of
its way at zooms 10 to 16 (never above the source's own), shared between
routes, kept again as the route changes and let go with it. The style,
its sprites and the instance's map source are kept as last read, and
every map reads a kept tile before the network, so a map draws with no
network at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The Record sheet offers the routes with a way planned; the recorder's
service follows the one chosen offline (RouteGuide.kt): turns told 50 m
before them (100 m on a bike) after a tone, off the route past 40 m for
10 s a cue with the way back, told again on rejoining, the end told.
The recording file keeps the route and the guidance; the recording
screen shows the next turn or the way back, and swipes to a map of the
route, the track and the position, north or the heading up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Routes are synced `route` rows (the model `SavedRoute`, as Flutter has a
`Route`), sealed as plans are; the migration `routes` only creates the
table, its space index and its cascade to the sync engine's spaces. A
route's payload holds its name, activity, profile, waypoints and the way
planned, points and elevations delta-coded and gzipped.
Home gets a fourth tab, Routes, and the planner: waypoints tapped on the
map, held and dragged, the last undone, a new route's first where the
phone is (GPS through the recorder's channel), the way planned by the
instance's BRouter after each change, an earlier answer dropped; its
distance, climb, descent, elevation and surfaces under the map.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
As a recording with a plan is saved, each step run becomes a lap in the
workout's sealed payload: the step, its round, when it started and ended,
its moving time and distance, with the highest heart rate the zones ran
with. The page lists the steps with their pace, speed or split, stroke
rate, watts and heart rate, marks whether each held its target, and the
charts shade the work steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The plan chosen on the Record sheet goes to the recording service spelled
out: each step's end, its target (zones as the heart rates they span for
the profile's highest rate) and its spoken line in the app's language.
PlanRunner.kt moves through the steps on the moving time and distance,
counts a timed step's last seconds down and cues off target from a 30 s
window; PlanCues.kt gives a tone, a vibration and the voice, ducking
music. The file's plan and step lines bring it back, and the recording
screen shows the step, what is left of it, its target, the next step and
Lap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Train's server gains the synced `plan` table, a sealed payload alone;
the migration `plans` only adds it, its space index and the foreign key
to its sync space, and a person's deletion takes it with their space.
The app keeps plans as `plan` rows sealed for their row: a name, what
they are for, whether a step off target is told, and blocks of steps
repeated, each ending after a time, a distance or a lap tap and aiming
at a pace, speed, heart rate or zone, or the rower's split, watts or
stroke rate. Home's Plans tab lists them, a new one starts from nothing
or from a template (4×4 minutes, Tabata, 10×400 m), the editor changes
steps on a sheet, and a deletion asks first and can be undone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A recording with a Loop set up reads its heart rate through Polar's SDK
while no strap sends: the strap first, the Loop where there is none or
while it is lost. The SDK is one for the process, held by the open
app's bridge and by the recording service, so that the band stays
connected with the app gone; the app reopening finds it connected. The
recording file gains `loop` state lines; the rate is kept alone, as `h`
lines, and the Record sheet names the source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Train's server relays Polar's firmware service for the people signed in
(`firmware`: offered, check, download), a fetched file kept in memory a
day per hardware and version, off with LOOP_FIRMWARE_RELAY=off. The app
asks on the day's first connection and on the band's screen, from Polar
or the instance as the person chose, never Polar in the instance's
place; an update runs only after asking, reads and keeps the band
first, and sets it up again with its figures and clock after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Each read of the band lists the sessions it holds, reads those from the
read's first day on (`LoopDays.kt`, one line per exercise: its start
with the band's offset, length, Polar's sport ID, figures and heart
rate at its interval), and deletes those up to the day kept, as the
days. The app keeps each as a workout of origin Polar Loop with its
heart rate (`takeLoopSessions`), Polar's running and cycling as
Train's and every other sport as the new `Activity.other`, which a
workout's edit sheet now changes; a session overlapping a workout Train
recorded gives it its heart rate where it has none instead. The
sessions taken are remembered in `loop.json`, so one deleted in Train
isn't brought back. Health Connect writes the Loop's workouts as
Train's own.
The SDK gives sessions as its protobuf messages: protobuf-javalite
3.20.0, the SDK's own, is named for compiling only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
What the band read brings, from the bridge or the background job's
file (`LoopLines`), is kept by `DayRepository`: one sealed `day` and
`night` a date and the day's heart rate in chunks of three hours, every
ID made from the person and the date, a later read replacing what it
brought and keeping the rest, nothing rewritten unchanged; versioned,
gzipped JSON (`day_payload.dart`). `PolarLoop` reads the band as each
connection is ready (every half hour at most, or when asked), takes in
the background file as the app opens, and remembers up to which day
the days are sealed, which the next read then deletes on the band;
it keeps the background job on the band and from the right day, and
`Let the band go` stops it.
Home gains a Days tab, each day with its steps, active time, sleep and
Nightly Recharge, pulled to read the band; a day's page shows its
figures, its time in each class, its heart rate, the night's phases
and rating, Nightly Recharge and skin temperature.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>