halkyon/apps/train/AGENTS.md
Tizian 「ティツィアーン」 405577050d
feat(train): the workouts listed, and each one's page with its map (ADR 0030, D22-D24)
Home lists the person's workouts newest first under their months, each
with its activity's icon, its name (else the time of day's and the
activity's), when it started, its distance, moving time and pace or
speed; those this device can't open come last, locked. A workout's page
shows its track on the instance's map source (flutter_map with
vector_map_tiles, pinned at 9.0.0-beta.13), in pieces between its
pauses, start and end marked; its totals with the elevation gained;
its splits per kilometre on foot, per 5 km cycling; pace or speed and
elevation over the distance, drawn by the app's own painter; and what
the map's source sees. Its name and notes are edited there; deleting
asks first and can be undone for a moment from home (halkyon_app's
DelayedDeletions, TrainPerson.deletions).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:50 +01:00

8.4 KiB

Halkyon Train

What holds for every app and package of this repository is in the root AGENTS.md; it applies here in full, and Learn's patterns (apps/learn/AGENTS.md) wherever Train needs the same thing.

  • A shell on the shared packages, Todo's copied and renamed (decided 2026-10-28, ADR 0030, D12; what Train is to do: ADR 0030): signing in, the instances, the account and the look work, and since step 5a (28 Oct 2026) a person's workouts are kept and synced with serverpod_offline_sync, every row sealed (ADR 0030, D7, D14, D15), and on Android a workout is recorded (5b: ADR 0030, D13, D16, D18 to D21), and since 5c home lists them and each has its page with its map (D22 to D24). Packages: halkyon_train_server, halkyon_train_client (generated, never edited), halkyon_train_flutter; app ID app.halkyon.train on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; LICENSE.md).
  • The server includes halkyon_base (ADR 0014) and configures it in server.dart (trainBase(), lib/src/train_base.dart): its basics say it serves train (never change it once apps use it), sign-in mails name Halkyon Train, the source is this repository, minimumAppBuild 1, and deleteTrainData (lib/src/train_data.dart) as its deletePersonData hook, which deletes the person's sync spaces and so, by the sync engine's cascades, every row of theirs (test/integration/workout/person_deletion_test.dart). Authentication through the module's builders: a password, SimpleLogin on the register, email codes proving an address, server-side sessions; no anonymous accounts (ADR 0020, D25), nor GitHub (D36). Accounts (ADR 0016): Train holds its own register when it runs alone, and is a member of the instance's register (Learn's server on learn.halkyon.eu) where the operator sets ACCOUNT_REGISTER_URL (trainBase(accounts: AccountsMode.fromEnvironment())) with the instance's shared INSTANCE_SECRET (instanceSecret); then sign-in, handles and profiles are the register's, the email settings are the register's alone, and an identified person's deletion goes to the register first. addInstanceRoutes serves the calls between the services on its web server, and the server doesn't start without the secret where it needs it. schedulePruning (lib/src/pruning.dart) keeps the module's pruning scheduled, one entry each however often the server starts (and cancels the one under the pruning's name before ADR 0020, D25): the anonymous accounts left from before daily, sign-up attempts and codes hourly, as the register the deletions its members haven't confirmed hourly, as a member the people the register no longer knows daily (each a no-op in the other mode). Session logs are never persisted, in any run mode. Migrations (2026-10-28): an initial one with Serverpod's tables, its auth modules' and the module's as they stand (Todo's grew one by one with the module; Train starts where they ended), then workouts, purely additive: the sync engine's tables and Train's first three. Synced models (database: sync, the module offline_sync, experimental_features: databaseSync in config/generator.yaml): content_key (the person's key sealed to the account), workout and workout_series (a workout's series in chunks of at most ten minutes, deleted with it), each a sealed payload with its keyId and nonce, nothing else readable. Tests (test/integration/train_server_test.dart) call the module's endpoints through the server's own dispatch (halkyon_base.<endpoint>), since the generated test tools expose the project's endpoints alone.
  • The app is halkyon_app's InstanceGate and SessionGate with Train's frame (trainFrame: "Train" under "Halkyon", its icon, train.example.org, its own sentence) and ServerpodInstanceChecker(app: 'train'); TrainDependencies (AppDependencies<TrainPerson>) on one instance, TrainPerson per person: their own sync database in their folder (sync.sqlite, halkyon_app's PersonFolders, one person's at a time), halkyon_app's SyncService behind AppCompatibility (the module's basics' minimumAppBuild, against the build package_info_plus reads), their key (TrainKeys, lib/src/core/privacy/: one key per person, IDs train1.<ms>.<nonce>, the oldest-made writing, kept in content_key and opened only in Train's form, ADR 0030, D15) and their workouts (WorkoutRepository, lib/src/feature/workout/: halkyon_app's PayloadEnvelope seals a workout for its row and a series chunk for its row and its workout; payloads are versioned JSON, a series' columns delta-coded, gzipped before sealing; a deletion blanks every payload first); the gateways' tests run on a sync database opened without a server, the repository's on memory gateways and real libsodium; a person's app whose home lists their workouts (WorkoutsCubit, with TrainPerson.deletions, halkyon_app's DelayedDeletions) and opens each one's page (WorkoutCubit: analyse in workout_analysis.dart reads its series into the track's pieces, splits, elevation and charts, drawn by workout_chart.dart; the map with flutter_map on the instance's source, MapSourceRepository asking map.source, its tiles from TrainPerson.mapTiles, vector_map_tiles in the app and none in widget tests), and the account sheet: halkyon_app's AccountTiles (whose account on which instance, Switch account, Sign out, Delete account, which asks first) in Train's words, and halkyon_app's About (showAppAbout: the source of what the instance runs, from TrainDependencies.sourceCodeUrl, and the licence list). Texts through gen-l10n in British English (the template), German (informal du), an empty en_US and en_GB (test/l10n/arb_files_test.dart keeps them in step). The look follows the system's theme mode and language until Train has settings. Countries in where an instance keeps its data are named by halkyon_app's CLDR names (AssetCountryNames), its language picked by halkyon_app's resolveAppLocale. Widget tests start Train's whole tree with halkyon_app's fakes (test/app/train_app_test.dart).
  • Recording (ADR 0030, D13, D16, D18 to D21) is Android's alone for now (TrainPerson.recording null elsewhere): RecorderService (Kotlin, android/app/src/main/kotlin/app/halkyon/train/), a foreground service of the types location and health started from the open app, takes fixes from LocationManager's GPS (no Play services) and steps from the step detector, decides auto-pause (AutoPause.kt) and appends each sample as a line to the person's recording.txt (format in recording_file.dart), flushed every 5 s; MainActivity asks the permissions (never background location) and bridges app.halkyon.train/recorder (methods) and …/recorder/lines (the file so far, then each line). The app (lib/src/feature/recording/) follows those lines with RecordingBloc, works out what the screen and the notification show (liveOf, the saved totals' functions), and saves a recording as a workout and its series (recordedWorkout) or throws it away; one cut off, or stopped and not saved, is found as the person's app opens. Tests: the Dart side with test/support/fake_recorder_platform.dart; the Kotlin auto-pause on the JVM, JAVA_HOME=<Android Studio's jbr> ./gradlew :app:testDebugUnitTest in android/ after a first flutter build apk (which writes gradlew), not part of tool/check.dart, as CI's runner has no Android SDK.
  • The icon is Lucide's bike fading from cobalt through violet and strawberry into carrot (ADR 0031), drawn with the other apps' by the repository's tool/app_icons.py (python3 -I tool/app_icons.py train from the root): assets/icon/halkyon_train_icon.png, Lucide's ISC licence beside it, and every platform's launcher icon through flutter_launcher_icons. registerLucideLicense puts Lucide's notice in the licence list.
  • serverpod start runs in apps/train/halkyon_train_server (it launches the app from lib/driver.dart); .mcp.json's --server-dir names it when a session works on Train. Its tests need throwaway passwords in its git-ignored config/passwords.yaml (database, serviceSecret, serverSideSessionKeyHashPepper, emailSignInPepper) or from the environment, as CI gives them. Train's development server has ports of its own (API 8180, Insights 8181, web 8182, PostgreSQL 8190; the app's assets/config.json), so it runs beside Learn's, as a member of Learn's register; its tests use config/test.yaml's ports, as Learn's and Todo's do, which Melos runs one package at a time.