Like BRouter's and Photon's: timeouts on connecting and reading, a check answer up to 64 KB and a file up to 64 MB, no redirect but to https, one fetch per file at a time, and 10 checks and downloads a minute for a person (tooOften). An answer that names no update properly is unreachable, no longer no update. The relay is the pod's, as routing's and search's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Serverpod's default device for a companion app is the web server, which
Train doesn't build for, so the launch failed. The app is run by hand on
the emulator or a phone with `flutter run -d <device> -t lib/driver.dart`.
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>
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>
deploy/compose.train.yaml adds Train's server at train.halkyon.eu, a
member of Learn's register, with its database halkyon_train in the
instance's PostgreSQL, and BRouter beside it alone (v1.7.10, checked by
SHA-256, no log), its segments of the operator's region kept current
monthly. Nothing changes until deploy/.env names the compose file:
redeploy.sh and backup.sh take Train in only then, and the operator's
steps are in sidecars/README.md. restic now forgets snapshots grouped by
host alone, so adding Train's dump leaves no group behind for good.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The routing endpoint plans a route through the waypoints for one of six
profiles, for the people signed in, at most 50 waypoints and 30 plans a
person a minute, nothing kept or logged; refused without BROUTER_URL.
BRouter's GeoJSON is read into points, elevations, climb and descent
above 3 m, turn hints and stretches by surface. Train's running profile
for BRouter, halkyon-run.brf, is a first guess not yet tried on BRouter.
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>
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>
The server gains Train's synced `day`, `day_series` (a day's heart
rate, deleted with its day) and `night`, each a sealed payload alone;
the migration `days` only adds their tables, indexes and foreign keys
(to their sync space, and a chunk's to its day, deferred), and the
person's deletion takes them with their spaces.
On Android, `LoopDays.kt` reads a band's days from a date to today,
framed by the SDK's sync start and stop, into lines of JSON, then
deletes on the band what the app kept up to the date it names, never
today; `LoopBridge` answers `readDays` with them, one band call at a
time, and `background` schedules `LoopWorker`, a WorkManager job every
four hours that appends the same lines to the person's
`loop_days.txt` while the app doesn't hold the band.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A person's profile, one row per person (its ID derived from theirs),
sealed like a workout: sex, birth date, height, weight, resting and
maximum heart rate, VO2max, training background, typical day and sleep
goal, each optional. "Your body" in the account sheet edits it; the
Loop's setup is to reuse its form. The migration only adds the synced
table `profile`, deleted with the person's sync space.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Train's first endpoint of its own, map.source, names the map workouts
are drawn on, for people signed in only, as a provider's key may be in
its style's address: OpenFreeMap's Liberty style where the operator sets
nothing, else MAP_STYLE_URL (HTTPS only) with MAP_SOURCE_NAME,
MAP_ATTRIBUTION and MAP_ABOUT_URL. A model without a table: no
migration. D22 is recorded in ADR 0030.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Train's server syncs with serverpod_offline_sync as Learn's does, and
holds three synced tables, every payload sealed on the devices:
content_key (the person's data key, sealed to the account), workout (its
type, times, name and totals, changed when edited) and workout_series
(chunks of a workout's samples, written once). A person's space goes
with them, with everything in it.
The migration only adds: the three tables, each indexed on spaceId and
cascading with its space (a series also with its workout), and the sync
engine's own tables.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
apps/train with halkyon_train_server, _client and _flutter, as Todo's:
halkyon_base configured by trainBase(), its own register alone and a
member of the instance's where the operator names one, the app on
halkyon_app's gates with Train's frame, account sheet and About, texts
in British English and German that speak of workouts, app.halkyon.train
on Android, iOS, Linux, macOS and Windows.
Where it differs from Todo: development ports of its own (8180 to 8190),
so it runs beside Learn's server; one initial migration with the schema
Todo's migrations end at (the same tables, compared line by line), not
Todo's seventeen; and Todo's icon as a placeholder until the icon tool
of ADR 0031 draws Train's bike. The workspace, the licences, the READMEs
and AGENTS.md name it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>