Commit graph

801 commits

Author SHA1 Message Date
581cf1f1cd
feat(ui): tabs folding into the title, in Learn's and Train's Profile (ADR 0030, D60)
halkyon_ui's FoldingTabs: the tab row under the app bar's title at the
top, scrolling away with the content, the chosen tab's name then small
under the title, which, tapped, scrolls back up to the row, as Strava's.
Learn's Profile (Profile, Statistics) is built on it, and Train's
Profile in tabs: Progress (your body, the last 12 weeks, this year),
Days, and Plans where the device doesn't record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 19:12:11 +01:00
a552b7a0fa
feat(train): Home opens on today's figures from the band (ADR 0030, D60)
While the Loop gave figures for today, Home shows them above the week:
steps, active time, last night's sleep and Nightly Recharge, as Profile
lists a day, tapped opening the day's page. Shown above Home's message
too, before a first workout; the week waits for one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 18:52:32 +01:00
5cfab59bd2
feat(train): Profile opens on your body, the last 12 weeks and this year (ADR 0030, D60)
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>
2026-10-31 18:46:26 +01:00
f04388225b
feat(train): Record has a card for each activity, its Start opening the sheet for it (ADR 0030, D60)
All checks were successful
CI / check (push) Successful in 41m1s
CI / deploy (push) Successful in 56s
The Record tab shows a card per activity it records, the interval plans
below. A card's Start opens the Record sheet for that activity, which
keeps every other choice; the sheet no longer chooses the activity, and
so offers the plans and routes for it or for any. While a recording is
under way or waits to be saved, the cards wait, the bar leading back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 18:00:45 +01:00
4c78c7d2c5
docs(deploy): the home end's check of the tunnel with nc's own timeout (ADR 0032)
All checks were successful
CI / check (push) Successful in 40m51s
CI / deploy (push) Successful in 57s
Run on the home end, the check that nothing new from the tunnel reaches
the instance's server failed at once with "missing port number" instead
of trying. OpenBSD's nc, as the WireGuard image has it, ends the try with
its own -w, without timeout or a shell around it; checked on the home end
on 31 Oct 2026, where it gave up after 5 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 16:47:35 +01:00
82fa7aec1b
fix(deploy): the home end dials the instance's server by its address (ADR 0032, D11)
All checks were successful
CI / check (push) Successful in 41m12s
CI / deploy (push) Successful in 56s
A tunnel that fails to come up takes Jellyfin's internet with it, as
linuxserver/wireguard's client mode deletes the default route. Named by
learn.halkyon.eu, the endpoint needed DNS to come up, which the home end
starting or waking hasn't always got yet; by its address the tunnel comes
up whatever the network does. Closes ADR 0032's open point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 15:16:05 +01:00
f015792927
docs(deploy): a failed tunnel takes Jellyfin's internet with it (ADR 0032)
All checks were successful
CI / check (push) Successful in 41m43s
CI / deploy (push) Has been skipped
linuxserver/wireguard's client mode deletes the default route when wg0.conf
fails to come up, so Jellyfin, in its namespace, loses TMDb and scans get
frames for posters. The README's step 4 says so and how to recover; ADR
0032 holds the ways out, open for the developer to decide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 14:26:44 +01:00
220816884c
feat(train): Maps shows every route on the map, above the list or beside it (ADR 0030, D60)
All checks were successful
CI / check (push) Successful in 41m20s
CI / deploy (push) Successful in 58s
The instance's map with every route drawn, fitted to them all, a route
tapped opening its planner. On phones the map stands above the list;
from expanded windows on the two are panes side by side, as Material's
list-detail layout. Without a route to draw, the list alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 12:25:38 +01:00
064c44bfa5
feat(train): Home opens on this week, its totals and its seven days (ADR 0030, D60)
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>
2026-10-31 12:20:39 +01:00
7473366f66
feat(train): a recording under way above the bar on every tab, and a saved one opens its page (ADR 0030, D60)
halkyon_ui's NavigationFrame takes a status, standing above the floating
bar below medium windows and at the foot of the screen's panel from there
on. Train's RecordingBar fills it while a recording runs or waits to be
saved: its activity, moving time and distance, paused or stopped, a tap
back to it. Saving now opens the workout's page in Home's tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 12:12:55 +01:00
25191055ae
feat(train): Settings over the tabs from a cog on Home and Profile, the account near its foot (ADR 0030, D60)
As Learn's: a cog atop Home and Profile, with the count of accounts
waiting for whoever operates the instance, opens Settings over the tabs,
where an account sheet had held the account and the training's screens.
Grouped in cards: the operator's card, the training (your body, Health
Connect, the Polar Loop, music, those the device has), the account near
the foot, About Halkyon Train last, and the version small at the very
bottom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-31 00:16:01 +01:00
d4d749eb9f
feat(train): Home, Maps, Record, Groups and Profile in Learn's frame, washed in carrot (ADR 0030, D60)
Train's tabs are Strava's five where one screen with four tabs at its
top had held workouts, days, plans and routes: Home the workouts, Maps
the routes, Record recording and the interval plans, Groups a screen
saying they come later, Profile the days and the way to the account.
A device that doesn't record has no Record tab, its plans beside the
days in Profile.

The frame is halkyon_ui's NavigationFrame, driven by go_router as
Learn's: a workout's page opens in Home and a day's in Profile with the
tabs beside them; the route and plan editors and the recording cover
them, their content handed over as it is. Every tab washes down from the
top in carrot, the warm end of Train's icon, as Learn's in cobalt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 23:52:48 +01:00
b45db6ea7c
feat(ui): every app's frame around its tabs, Learn's moved into halkyon_ui
NavigationFrame is Learn's AppShell without its router and its tabs: a
navigation rail from medium windows on with the app's icon on top,
leading to the first tab, a bar floating above the foot below, the
background of the screen in view around it, and a tick on every choice.
TabsStack is Learn's container of the tabs' navigators, every tab kept
alive and told whether it shows. Learn's shell is built on both, its
tests unchanged; Train's is next.

The rail's icon is drawn from 120 px at most in memory, as Learn's was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 23:52:39 +01:00
57262ebf73
feat(train): a route's line pulled out into a new waypoint, and any waypoint taken off (ADR 0030, D59)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 23:17:59 +01:00
32c94aa601
fix(train): the maps stop at zoom 18, beyond which they drew nothing
vector_map_tiles draws its tiles as pictures and stops at zoom 18, while
the maps let people zoom further, onto a grey map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 23:17:58 +01:00
d8c827eab6
feat(deploy): Jellyfin at home, published through WireGuard (ADR 0032)
Some checks failed
CI / check (push) Failing after 41m12s
CI / deploy (push) Has been skipped
The instance serves jellyfin.halkyon.eu and its Caddy proxies through a
WireGuard tunnel to a Jellyfin on the operator's own machine, which dials
out, so nothing names the home network's address and its router opens no
port. compose.jellyfin-home.yaml takes the sidecar's place, with its
site's rules unchanged (jellyfin.caddy's upstream is now a setting), and
leaves JELLYFIN_URL unset, so the instance suggests it only while it
answers.

On the instance's server the tunnel reaches Jellyfin from Caddy alone and
nothing of the host (wg-jellyfin-rules.sh, set and removed with the
tunnel); the CI runner's unit denies it, and CI's Sandbox step fails if a
job reaches it. The home end keeps Jellyfin in WireGuard's network
namespace, its port on the loopback alone. The steps are in
deploy/sidecars/README.md, the rules held by test/deploy/jellyfin_home_test.dart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 23:17:57 +01:00
991af7af48
build(ci): the check job may take 60 minutes, as Train's packages brought a warm run to 42 of 45
All checks were successful
CI / check (push) Successful in 41m21s
CI / deploy (push) Successful in 2m19s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 18:56:44 +01:00
46b2b9bd09
build: CI and the snap build with Flutter v3.47.8, as the laptop does
Some checks failed
CI / check (push) Has been cancelled
CI / deploy (push) Has been skipped
A patch release; the release tool refuses to build with another Flutter
than CI tests with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 17:43:24 +01:00
97642b26aa
build: the F-Droid script takes back every APK released before it updates, so no laptop's run deletes one
Some checks failed
CI / deploy (push) Has been cancelled
CI / check (push) Has been cancelled
Its upload mirrors the working folder with --delete-after, and a folder on
another laptop held none of the APKs released from the first: its run would
have published an index without them and deleted them from halkyon.app.
Each APK the published indexes (repo and archive) name now comes from the
repository first unless the folder has it, its SHA-256 checked against the
index's either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 17:05:11 +01:00
cd3b88c174
build: releases fetch their signing keys from Proton Pass into memory, never onto the disk
tool/with_signing_keys.sh downloads the app's Android keystore and the
F-Droid index's from the vault Infra · Signing into a folder under
$XDG_RUNTIME_DIR, runs tool/release.dart with their paths, and removes
the folder however the run ends. The passwords are asked for as ever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 17:05:11 +01:00
48951c1e1a
build(train): Train is released as Learn is, with a key of its own, beside it in the F-Droid repository (ADR 0030, D58)
apps/train/release.dart configures the release tool for Train's APK on
train.halkyon.eu; its release builds sign with its own key when the tool
hands it over. Its F-Droid listing declares NonFreeDep for Polar's SDK and
NonFreeNet; the repository's script copies each app's icon. The key's
certificate is pinned once the key is made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 17:05:10 +01:00
0ec15d2fb2
fix(learn): nothing of Learn's goes to Android's backup or to another phone (ADR 0019, D9)
Some checks failed
CI / check (push) Has been cancelled
CI / deploy (push) Has been cancelled
The device keeps private studysets open in its database, which Android's
backup (Google's, or Seedvault where turned on) and a transfer to a new
phone took open. allowBackup and fullBackupContent are off, and from
Android 12 on the data extraction rules exclude every folder from the
transfer too, which allowBackup alone no longer stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 16:46:23 +01:00
f47c2767a3
docs(train): what the phone can't check of relayed firmware (ADR 0030, D55)
All checks were successful
CI / check (push) Successful in 42m14s
CI / deploy (push) Has been skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 16:14:56 +01:00
839d49c4cc
fix(train): no row's ID tells the instance a date: the days' IDs are keyed by the account (ADR 0030, D57)
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>
2026-10-30 16:12:01 +01:00
9b9b8d08ca
fix(train): the wheel is learnt from rides GPS covered, whatever its size (ADR 0030, D56)
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>
2026-10-30 16:12:01 +01:00
ca62ed1aee
docs(train): ADR 0030's D53–D55 as built, and the sidecars' downloads as they now retry
Some checks failed
CI / deploy (push) Has been cancelled
CI / check (push) Has been cancelled
What the branch's review of 30 Oct 2026 decided: no backup or transfer of Train's data, Jellyfin over https alone but on the phone, only the firmware agreed to and the relay bounded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:53:13 +01:00
ced82b1e75
fix(train): Jellyfin over https alone, but on the phone itself (ADR 0030, D54)
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>
2026-10-30 15:53:13 +01:00
071899ce44
fix(train): nothing of Train's goes to Android's backup or to another phone (ADR 0030, D53)
What Train keeps unsealed on the phone (a recording while it runs, the Loop's days before they are sealed, the body's figures the band was set up with, the maps along routes) went to the backup and to a new phone by default, outside the sealing. allowBackup and fullBackupContent are off, and from Android 12 on the extraction rules exclude every folder from both. The second WRITE_EXERCISE goes: a ride's cadence is written under the first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:53:12 +01:00
ba77248930
fix(deploy): the sidecars' downloads try again within the hour after a failed round
A failed BRouter segment or listing, a failed tile cut or an unwritable folder waited the whole refresh period, 30 days without routes or a map; they try again after an hour, doubling to a day. Photon's failed download backs off from a day to a week, and a dead Photon is noticed within a minute. The downloads' network is an ordinary bridge the three share, as the comments now say.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:53:12 +01:00
0240a80faf
fix(train): the firmware relay's calls to Polar time out, are bounded and limited per person (ADR 0030, D55)
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>
2026-10-30 15:53:11 +01:00
ac6cff179f
fix(train): only the firmware version the person agreed to is written (ADR 0030, D55)
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>
2026-10-30 15:53:11 +01:00
c8f16686ec
fix(train): a ride by the wheel goes out with its power, cadence and distance, and learns no wheel from tunnels (ADR 0030, D49, D51)
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>
2026-10-30 15:53:11 +01:00
236ac95eab
fix(train): a sensor's count reset is no rollover, and a plan's metres are the wheel's on a trainer (ADR 0030, D49, D51)
A speed sensor resetting its 32-bit count after sleep or a new battery had its drop read as a rollover, some 4.29 billion turns; a crank's after more than 64 s coasting a cadence of its wrapped time. A plan's distance steps and the km announced took GPS or the rower alone, so on a trainer they never came, and the rower's counted its paused metres; the fallback announcement said 0 km for half a kilometre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:53:10 +01:00
acb291afcb
feat(app): the recovery phrase's words typed back are suggested from BIP-39's list (ADR 0017)
Each of the three fields suggests the words of BIP-39's whole English
list beginning with what is typed, never the phrase's own, and refuses a
word not on it as its field is left, so that a typo is caught before the
words are compared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:53:10 +01:00
18f456eed9
feat(base,train): Train signs in with SimpleLogin, on ways back of its own (ADR 0020)
The forwarding page's table gains train-loopback (port 43825) and
train-scheme (app.halkyon.train://). Train's app had kept Todo's
loopback port 43824 from its shell; it now has its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:53:09 +01:00
d938dfa017
build(train): serverpod start leaves the app to be run on a device
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>
2026-10-30 15:53:09 +01:00
2a7cd927c4
feat(train): smaller copies of lossless tracks, transcoded by Jellyfin to Opus (ADR 0030, D52)
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>
2026-10-30 15:53:09 +01:00
409a2519f9
feat(train): the marked playlists downloaded by a WorkManager job, the app open or not (ADR 0030, D52)
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>
2026-10-30 15:53:08 +01:00
6bde74f0ba
feat(train): the Jellyfin token in Train's own store, sealed by the Keystore (ADR 0030, D52)
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>
2026-10-30 15:53:08 +01:00
64366d5420
feat(train): a ride's watts, cadences and wheel to Health Connect, and auto-pause by the wheel (ADR 0030, D51)
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>
2026-10-30 15:53:07 +01:00
edd2df79bd
feat(train): search for places in the route planner, through Photon (ADR 0030, D50)
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>
2026-10-30 15:53:07 +01:00
190c3872f4
feat(train): the instance's own map tiles, drawn by Train's style (ADR 0030, D50)
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>
2026-10-30 15:53:07 +01:00
a848b71bd4
feat(train): a ride's distance from the wheel where GPS falls short (ADR 0030, D49)
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>
2026-10-30 15:53:06 +01:00
5c13e6f3c5
feat(train): a ride reads the bike's speed and cadence sensor and power meter (ADR 0030, D49)
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>
2026-10-30 15:53:06 +01:00
57cbd97fd1
feat(train): elevation from the barometer, its pressure kept raw (ADR 0030, D49)
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>
2026-10-30 15:53:05 +01:00
128ba1f6c8
feat(train): a workout out as GPX, TCX or FIT (ADR 0030, D49)
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>
2026-10-30 15:53:05 +01:00
db7e0b7bcd
feat(train): each km announced on foot, each 5 km by bike (ADR 0030, D49)
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>
2026-10-30 15:53:05 +01:00
d5b367e930
feat(train): music by cadence in a recording, or the metronome (ADR 0030, D48)
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>
2026-10-30 15:53:04 +01:00
7f18f001f4
feat(train): marked playlists kept on the phone, their tempo worked out (ADR 0030, D47)
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>
2026-10-30 15:53:04 +01:00
3336fe4eca
feat(train): signing in to Jellyfin and marking playlists (ADR 0030, D47)
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>
2026-10-30 15:53:03 +01:00