release: prepare Trainlog 0.1.1

This commit is contained in:
fy59 2026-09-16 07:46:11 +02:00
parent d4b56a3aa5
commit 7a12dd3728
12 changed files with 628 additions and 72 deletions

5
.gitignore vendored
View file

@ -8,6 +8,11 @@ meson-private/
.gradle/
local.properties
**/build/
*.jks
*.keystore
/android/release-signing.properties
/android/release-signing.env
/android/keystore.properties
# Editors
*.swp

View file

@ -1,10 +1,60 @@
# Changelog
- Documented stable-release distribution through GitHub Releases, including
## 0.1.1 — 2026-09-16
- Completed `TRAINLOG_ANDROID_FIELD_INPUT_REFINEMENT_V1`: active and completed
occurrence handles now keep one stable `entry_id`-keyed pointer gesture alive,
accumulate total drag distance, jump directly across multiple positions, show
residual pointer-following motion, restore the origin on cancellation, and
commit only once at completion where persistence applies. Decimal distance,
speed, MAX, target-load, and set-load editors use decimal keyboards, accept
both `.` and `,`, preserve incomplete typing states, and parse finite
locale-independent values only at commit. Units, schemas, formats, identities,
active/completed ownership, and version 0.1.1 remain unchanged.
- Completed `TRAINLOG_ANDROID_COMPLETED_SESSION_REORDER_V1`: the Android
completed-session correction form reuses the dedicated localized right-side
drag handle and accessibility move actions. Reordering remains transient until
Save; Cancel/Back writes nothing, while Save atomically replaces positions and
any factual corrections under the same session/entry/exercise identities.
Stable Compose keys keep editor state attached to `entry_id`; equipment,
targets, actuals, MAX, feedback revisions, and J+1 ownership survive. Existing
V3 synchronization reconciles the same desktop session without duplication.
No schema, exchange format, active-session behavior, or version changed.
- Completed `TRAINLOG_ANDROID_FIELD_WORKFLOW_REFINEMENT_V1`: Android now uses a
full-width semantic-success **Add to session** action, provides a dedicated
accessible right-side drag handle for durable active-draft reordering, and
supports atomic completed-session factual correction. Reorder and correction
retain stable session, occurrence, exercise, equipment, plan, actual, feedback,
and follow-up ownership; corrected facts continue through the existing V3
synchronization identity. Continuous values reject non-finite input and show
explicit km, km/h, and seconds units. No SQLite schema, exchange format,
desktop/TUI behavior, or product version changed.
- Documented stable-release distribution through GitHub and Forgejo Releases,
including
prebuilt Android and architecture-specific dynamically linked Linux assets,
SHA-256 verification, runtime/build/optional dependencies, and the release
signing and exact-tag requirements. No v0.1.0 release is claimed until a
signed APK and authenticated GitHub publication path are available.
signing and exact-tag requirements. The Android artifact is built with the
durable Trainlog release identity and is verified before publication.
- Completed `TRAINLOG_I18N_V0_1_1`: Trainlog 0.1.1 is one synchronized Android
and desktop product version, with French as the default interface language
and English selectable through **Settings → Language** on both surfaces.
The preference is local presentation state (dedicated Android
SharedPreferences; desktop XDG `trainlog/presentation.conf`) and successful
selection updates the interface immediately. Localized Trainlog-owned text,
BODY ZONE labels selected by stable IDs, and visible number/date formatting
do not change training data, user exercise/catalogue names, IDs, SQLite,
schemas, exchange/protocol artifacts, AI, MAX, or feedback. Desktop and
Android synchronization views render local typed status/counters; raw
core/receipt/history summaries remain opaque protocol/operational bytes and
are never injected as cross-device French text. Final validation passed
60/60 normal and 60/60 ASan/UBSan desktop tests, Android 202 total tests
(198 passed, 4 skipped, 0 failed), both validators, source-derived
`TRANSLATABLE_UI=0`, and Android resource parity. Real device/manual visual
switching remains an explicit smoke validation.
- Completed `TRAINLOG_FULL_REPOSITORY_DOCUMENTATION_AUDIT_V2`: added the
canonical documentation index, made README a concise entry point, separated

View file

@ -1,9 +1,10 @@
# Trainlog
Trainlog is a local-first workout and body-data system. Its native Android app
is the lightweight field companion for fast capture and glanceable summaries;
its C17/Notcurses desktop TUI is the detailed surface for correction,
catalogue management, analytics, graphs, and long-term follow-up.
Trainlog is a local-first workout and body-data system. Trainlog Core owns
business truth and canonical desktop persistence. Its native Android app is the
field companion, its C17/Notcurses desktop TUI is the administration and
technical surface, and its documented future local Web sibling is the analysis,
visualization, and program/session-preparation surface.
Android and desktop each own a local SQLite database. The desktop database is
the canonical long-term history. Trainlog synchronizes versioned JSON artifacts
@ -13,26 +14,41 @@ over direct MTP; it never copies SQLite database files between devices.
| Surface | Primary responsibility |
|---|---|
| Android | Capture sets, repetitions, loads, durations, continuous activities, feedback, J+1 follow-ups, body measurements, active drafts, and AI proposals; trigger sync; show quick summaries. |
| Desktop TUI | Consult and correct canonical history; manage exercises and equipment; analyze body data, MAX results, training statistics, body zones, calendar periods, and longitudinal graphs. |
| Android | Capture and quickly correct sets, repetitions, loads, durations, and continuous activities; reorder active and completed-session occurrences; capture feedback, J+1 follow-ups, body measurements, and AI proposals; trigger sync; show quick summaries. |
| Desktop TUI | Administer, inspect, maintain, import/export, correct canonical history, and provide technical tools. |
| Local Web (planned) | Analyze and visualize canonical data; prepare programs and sessions through typed Trainlog Core services. |
In short: **Android captures and summarizes. The TUI analyzes and tracks over
time.** Android intentionally remains lightweight for analytics.
No interface reconstructs business truth from SQLite tables. The local Web is
a sibling adapter, not an extension of the TUI; `TRAINLOG_WEB_V1` is currently
contract-only and has no implementation.
## Releases
Prebuilt Trainlog releases are published on both official mirrors:
When a Trainlog release is published, its prebuilt assets are available from
both official mirrors:
- [GitHub Releases](https://github.com/labfytools/trainlog/releases)
- [Forgejo Releases](https://git.labfytools.com/fy59/trainlog/releases)
Each stable release provides:
Each published stable release provides:
- a signed Android APK;
- a Linux x86-64 TUI executable;
- SHA-256 checksums.
Both mirrors publish the same Trainlog version and release assets.
Both mirrors publish the same Trainlog product version and release assets. The
implemented product version is **0.1.1** on Android and desktop; this is one
shared Trainlog version, not separate interface versions. Version 0.1.1 is the
current stable release.
Version 0.1.1 presents Trainlog in French by default, with English selectable
from **Settings → Language** on Android and the desktop TUI. The selection is
local to that installation and updates the visible interface immediately; it
does not translate user exercise/catalogue names or change training data,
stable IDs, databases, schemas, exchange artifacts, synchronization protocols,
AI, MAX, or feedback. Synchronization status is rendered locally from typed
status and counters; raw protocol and operational summaries are not translated
or injected into the other surface.
See the [documentation](docs/README.md) and [changelog](CHANGELOG.md) for more
details.
@ -41,7 +57,7 @@ details.
### Download a prebuilt release
You can download Trainlog from either official mirror:
For a published release, download Trainlog from either official mirror:
- [GitHub](https://github.com/labfytools/trainlog/releases)
- [Forgejo](https://git.labfytools.com/fy59/trainlog/releases)
@ -55,8 +71,8 @@ not a universally portable Linux binary: the host must provide compatible
runtime libraries listed under [Dependencies](#dependencies).
```bash
chmod +x trainlog-tui-linux-x86_64-v0.1.0
./trainlog-tui-linux-x86_64-v0.1.0
chmod +x trainlog-tui-linux-x86_64-v<version>
./trainlog-tui-linux-x86_64-v<version>
```
The executable can remain in the download directory. Moving it into a directory
@ -94,6 +110,19 @@ JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew assembleRelease
is configured by the release operator. The repository does not contain signing
credentials and does not silently generate them.
Release hosts provide `TRAINLOG_RELEASE_STORE_FILE`,
`TRAINLOG_RELEASE_STORE_PASSWORD`, `TRAINLOG_RELEASE_KEY_ALIAS`, and
`TRAINLOG_RELEASE_KEY_PASSWORD`, or a private
`~/.config/trainlog/release-signing.properties` file containing the equivalent
`storeFile`, `storePassword`, `keyAlias`, and `keyPassword` keys. Environment
variables take precedence. Keystores and local credential files are ignored by
Git; neither belongs in source control, build logs, or release notes.
The Android release key is the permanent update identity for the application.
The release operator must preserve an encrypted external backup of both the
keystore and its credentials. Losing either prevents future compatible Android
updates; generating a replacement key is not a normal release procedure.
## Dependencies
### Linux runtime dependencies
@ -200,6 +229,7 @@ As of 2026-09-15:
- `APP_SHELL_V1=IMPLEMENTED_AWAITING_VISUAL_REVIEW_2`;
- `TRAINLOG_AI_SESSION_DRAFT_V1=VALIDATION_PENDING` pending a real Drive plus
Android-triggered bidirectional smoke test.
- `TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_NOT_STARTED`.
The latest executable result belongs in
[current state](docs/current_state.md), not in multiple README narratives.

View file

@ -1,8 +1,40 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.plugin.compose")
}
/*
* CONTRACT: release-signing material is machine-local. Environment variables
* take precedence over the private properties file so CI and release hosts can
* inject secrets without copying them into the source tree.
*/
val releaseSigningProperties = Properties()
val releaseSigningPropertiesPath =
providers.environmentVariable("TRAINLOG_RELEASE_CREDENTIALS_FILE").orNull
?: "${System.getProperty("user.home")}/.config/trainlog/release-signing.properties"
val releaseSigningPropertiesFile = file(releaseSigningPropertiesPath)
if (releaseSigningPropertiesFile.isFile) {
releaseSigningPropertiesFile.inputStream().use(releaseSigningProperties::load)
}
fun releaseSigningValue(environmentName: String, propertyName: String): String? =
providers.environmentVariable(environmentName).orNull
?.takeIf { it.isNotBlank() }
?: releaseSigningProperties.getProperty(propertyName)?.takeIf { it.isNotBlank() }
val releaseStoreFile = releaseSigningValue("TRAINLOG_RELEASE_STORE_FILE", "storeFile")
val releaseStorePassword = releaseSigningValue("TRAINLOG_RELEASE_STORE_PASSWORD", "storePassword")
val releaseKeyAlias = releaseSigningValue("TRAINLOG_RELEASE_KEY_ALIAS", "keyAlias")
val releaseKeyPassword = releaseSigningValue("TRAINLOG_RELEASE_KEY_PASSWORD", "keyPassword")
val releaseSigningComplete = listOf(
releaseStoreFile,
releaseStorePassword,
releaseKeyAlias,
releaseKeyPassword,
).all { !it.isNullOrBlank() }
android {
namespace = "com.labfytools.trainlog"
compileSdk = 37
@ -12,12 +44,31 @@ android {
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "0.1.0"
versionCode = 2
versionName = "0.1.1"
testInstrumentationRunner =
"androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
if (releaseSigningComplete) {
create("release") {
storeFile = file(requireNotNull(releaseStoreFile))
storePassword = requireNotNull(releaseStorePassword)
keyAlias = requireNotNull(releaseKeyAlias)
keyPassword = requireNotNull(releaseKeyPassword)
}
}
}
buildTypes {
getByName("release") {
if (releaseSigningComplete) {
signingConfig = signingConfigs.getByName("release")
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
@ -53,6 +104,27 @@ android {
}
}
/* WHY: Android Gradle otherwise emits app-release-unsigned.apk successfully,
* which is build evidence but can be mistaken for a publishable artifact.
* INVARIANT: every task that packages a release fails before packaging unless
* all four external values exist and the configured keystore is a regular
* file. Gradle/apksigner still perform the cryptographic validation. */
tasks.matching { it.name == "packageRelease" }.configureEach {
doFirst {
if (!releaseSigningComplete) {
throw GradleException(
"Trainlog release signing is not configured. Set the four " +
"TRAINLOG_RELEASE_* variables or provide the private " +
"release-signing.properties file.",
)
}
val configuredStore = file(requireNotNull(releaseStoreFile))
if (!configuredStore.isFile) {
throw GradleException("Trainlog release keystore does not exist: $configuredStore")
}
}
}
dependencies {
val composeBom =
platform("androidx.compose:compose-bom:2026.08.00")

View file

@ -18,6 +18,9 @@ evidence and chronology.
glanceable summaries, permissions, and Android interaction rules.
- [Desktop TUI](tui.md) owns Notcurses navigation, correction, detailed
consultation, analytics, graphs, and long-term follow-up behavior.
- [Architecture](architecture.md) owns the contract for the planned local Web
sibling, including Core/API boundaries, loopback networking, shell,
Dashboard, layout, build/runtime separation, and security invariants.
## Data model

View file

@ -8,6 +8,50 @@ It is a native Kotlin/Jetpack Compose application with local SQLite persistence.
The desktop remains the canonical long-term history and analytics store.
## Interface language (`TRAINLOG_I18N_V0_1_1`)
Trainlog version 0.1.1 is synchronized with the desktop TUI as one product
version; Android does not have an independent interface version. French is the
default interface language. The only alternate language is English, selected
from **Settings → Language**. The selection updates the Compose UI immediately.
`LanguageSettingsOwner` stores exactly one language tag in its dedicated
`trainlog_presentation_settings` SharedPreferences file. An absent or invalid
value deterministically selects French. This is presentation state only: it
does not call the repository and is never stored in Android SQLite, exported,
imported, or synchronized.
Translations apply to Trainlog-owned UI text, accessibility text, and visible
number/date formatting. They never translate user exercise or catalogue names,
stable IDs, JSON/protocol literals, training data, schemas, AI proposals, MAX,
or feedback. Recognized BODY ZONES are presented from their stable IDs; an
unknown future ID deliberately retains its catalogue label rather than changing
domain data.
The current synchronization view renders local typed status and counters. It
does not translate, reinterpret, or inject a raw desktop/core/receipt/history
summary: those protocol and operational bytes remain opaque. This presentation
boundary changes no Android import, export, receipt, or history format.
The active-session editor presents its add action as a full-width semantic
success button. Existing occurrences can be moved from a dedicated right-side
drag handle; the resulting list is persisted once the gesture completes. The
permutation retains `session_id`, `entry_id`, `exercise_id`, equipment, targets,
actual form values, MAX results, and feedback ownership. A failed write reloads
the last durable ordering instead of leaving presentation and SQLite divergent.
The pointer gesture is keyed by stable `entry_id`, accumulates its full vertical
delta, and calculates a direct bounded destination from the gesture origin. A
single drag can therefore cross several positions while the handle follows the
residual pointer offset and surrounding occurrences move away. Cancellation
restores the origin permutation; completion performs one draft write.
Decimal capture fields use a decimal-capable Android keyboard and retain raw
editing text. Both `.` and `,` are accepted independently of interface language,
including intermediate forms such as `0.`, `0,`, `.3`, and `,3`. Commit replaces
`,` with `.` and parses a finite locale-independent number before applying the
existing positive/non-negative domain rule. Distance remains explicitly km; no
unit guessing or automatic correction occurs.
## Session exchange V3
Completed session occurrences persist an `entry_id`; it is never regenerated
@ -79,6 +123,21 @@ Completed Session Detail exposes **Modifier la séance**. Its bounded form is
initialized from occurrence snapshots and writes nothing before Save. Save
replaces all children in one transaction while retaining `session_id`, every
retained `entry_id`, feedback roots/revisions, and the session follow-up parent.
The form edits only facts allowed by each stored occurrence profile, including
repetitions, set load, set duration, continuous duration, distance in kilometres,
and speed in km/h. The existing V3 snapshot republishes those corrected facts,
so synchronization updates the same stable session and occurrences rather than
creating correction IDs.
The same dedicated right-side handle can reorder completed occurrences in this
form. Its list permutation remains transient until Save; Cancel or Back performs
no repository mutation. Stable `entry_id` keys keep Compose editor/focus state
with the same occurrence even when duplicate exercises exchange positions. Save
persists facts and positions together through the same atomic replacement, and
a failed Save restores the original completed order with all child ownership.
Completed decimal fields likewise retain keyed raw text until Save, so typing a
separator is not collapsed into a premature numeric value. Invalid incomplete,
malformed, non-finite, or negative final values block the transaction with a
localized error.
Set and occurrence removal require explicit confirmation; Cancel writes
nothing. Exercise Detail separately exposes **Modifier l'exercice**. A
confirmed incompatible catalogue edit controls future Android occurrences;
@ -607,8 +666,9 @@ trainlog-sync-receipt-v1.json
The receipt is matched by `request_id`.
On success Android then applies the latest PC catalog and displays the final
result.
On success Android then applies the latest PC catalog and renders the final
local typed status/counters. Raw receipt summaries remain opaque operational
bytes and are not displayed as injected cross-device text.
Before applying the PC catalog or its V2 artifacts, Android applies
`trainlog-pc-equipment-definitions-v1.json`. Thus custom definitions are known

View file

@ -2,10 +2,13 @@
## 1. System boundary
Trainlog separates capture, durable history, transport, and presentation.
Trainlog Core is the product authority and separates capture, durable history,
business logic, transport, and presentation. The TUI, Web, and Android are
sibling adapters with different product responsibilities; none owns a parallel
business model.
```text
Android capture client
Android field client
|
| local SQLite
|
@ -23,10 +26,16 @@ Android capture client
desktop SQLite directional PC artifacts
| |
v v
desktop TUI Android
Trainlog Core Android
|
+-- desktop TUI
+-- local Web adapter -> browser
```
The desktop SQLite database is the canonical long-term history.
The desktop SQLite database is the canonical local source of truth and
long-term history. Core owns data, persistence, business logic, calculations,
read models, command services, and versioned contracts. An interface must not
query tables directly to reconstruct its own business rules.
Android local SQLite is a capture store, not a synchronization format.
@ -48,14 +57,23 @@ Responsibilities:
- PC catalog application;
- synchronization request creation;
- synchronization receipt display.
- retrieval of a session prepared through Trainlog;
- future sensor-data acquisition, including BLE heart rate.
Android is not responsible for canonical long-term analytics.
Android is the field adapter for gym use, actual-work capture, feedback, and
returning observations to Trainlog. It is not responsible for canonical
long-term analytics or business truth and never accesses desktop SQLite
directly.
### Desktop core
The C17 core owns:
- data and domain rules;
- desktop SQLite persistence;
- business calculations;
- typed read models and transactional command services;
- versioned contracts;
- exercise/catalog rules;
- generated body-zone taxonomy and relation APIs;
- profile-aware session data;
@ -67,9 +85,10 @@ The C17 core owns:
### TUI
The Notcurses layer owns interaction and rendering. It is confined to the
desktop executable; persistence, synchronization, and core services have no
terminal-library dependency.
The TUI is the administration, inspection, maintenance, import/export, and
technical-tooling adapter. The Notcurses layer owns terminal interaction and
rendering. It is confined to the desktop executable; persistence,
synchronization, and core services have no terminal-library dependency.
It consumes core services for:
@ -100,6 +119,145 @@ layout is compact from 72x20, has a sidebar from 100x26, and expands it from
and run-scoped terminal planes keep Notcurses infrastructure separate from
product semantics.
The current Dashboard implementation retains one known layering violation:
`tui.c` uses SQLite and `database_internal.h` directly to build Dashboard facts
and calculations. This is a blocking debt for the Web Dashboard. Before any
Dashboard HTTP endpoint exists, characterization tests must capture the current
behavior, the query and calculations must move incrementally into a typed Core
read model, and the TUI must consume that read model. This requirement does not
authorize a global `tui.c` refactor.
### Local Web application
`TRAINLOG_WEB_V1` is a sibling adapter of the TUI and Android, never an
extension of the TUI. It is launched by either:
```text
trainlog -w
trainlog --web
```
Trainlog serves the Web application and its versioned HTTP API to a browser.
The Web owns analysis, visualization, program preparation, and session
preparation. Initial API introduction is read-only; later program and session
mutations must call explicit transactional Core command services. Generic CRUD
over SQLite tables is forbidden.
The V1 network contract is:
```text
address = 127.0.0.1
default port = 8080
explicit override = trainlog -w --port <port>
IPv6 = out of scope
```
A requested occupied port is an explicit startup failure. Trainlog never
selects another port silently and never binds to a LAN address implicitly.
The architectural boundary is:
```text
Browser
|
| HTTP / independently versioned /api/v1/
v
Web adapter
|
| typed Core services
v
Trainlog Core
|
v
desktop SQLite
```
The following dependencies are forbidden:
```text
Browser -> SQLite
React -> SQL
Web route -> database_internal.h
Web route -> duplicated TUI calculation
```
The Web API version is independent of the SQLite schema,
`TRAINLOG_FORMAT_V1`, mobile/synchronization/export formats, and the Trainlog
product version. Web evolution must not alter a frozen exchange format.
V1 preserves serialized Core access: one process-owned `TrainlogDatabase` is
opened and migrated once before HTTP listening, used without concurrent direct
SQLite-handle access, and closed by the process owner after server shutdown. A
single-threaded event loop is the preferred server architecture.
Every Web page retains one application shell:
- Header navigation: Dashboard, Analyse, Programmes, Séances, Exercices;
- Body: responsive tile-organized page content;
- Footer: user, date of the latest completed session, and all primary BODY
ZONES worked in that completed session.
The Unix login is the V1 user fallback until Trainlog owns an explicit display
name. Footer zones come only from the latest completed session; missing facts
remain unavailable rather than being invented. Header and Footer remain
present during navigation.
The sole V1 theme is Catppuccin Mocha with Lavender as its primary accent.
There is no V1 light theme. The browser is the client and Electron is outside
the architecture. React, TypeScript, Vite, and Apache ECharts are the intended
frontend stack. Node and npm are build-only dependencies; installed Trainlog
must not require either. All production assets are local, built ahead of time,
and incorporated into the `trainlog` binary from the sibling root `web/`
directory. The grid library remains open pending a dedicated spike.
The Dashboard is a canonical 12-column grid containing exactly seven V1 tiles:
1. Prochaine séance;
2. Activité;
3. Progression;
4. Dernière séance;
5. Records / MAX;
6. Répartition musculaire;
7. Cardio / récupération.
V1 supports drag and drop, horizontal and vertical resizing, tile-specific
minimum/maximum sizes, automatic reorganization, responsive projection,
persistence, and restoration of the Trainlog default. Dragging and resizing
are enabled only by the explicit `Modifier l'agencement` mode. A larger tile
may reveal more information, but tile size never changes a metric's business
definition.
The single persisted source layout is the canonical desktop 12-column layout:
```text
$XDG_CONFIG_HOME/trainlog/web/dashboard-layout-v1.json
```
with fallback:
```text
$HOME/.config/trainlog/web/dashboard-layout-v1.json
```
Its separately versioned format is interface configuration, not training data.
It does not enter business SQLite V1, `TRAINLOG_FORMAT_V1`, scientific or AI
exports, or session synchronization. Small-screen layouts are derived
responsive projections, not a second persisted truth.
Dashboard values come only from real Trainlog data or documented deterministic
Core calculations. Missing data produces an unavailable/empty state. Prochaine
séance displays only a genuinely prepared and persisted session; a generated,
AI, hidden, or unaccepted proposal is not a validated session. Cardio /
récupération remains unavailable until real relevant data exists. The exact
Activité and Progression mathematics remain open and must be specified before
their read models are implemented.
V1 security invariants are: loopback-only binding, Host validation, no
permissive CORS, foreign-origin/CSRF protection for mutations, explicit bounds
for headers, bodies, connections and timeouts, local assets only, no CDN, no
implicit telemetry, a Content Security Policy, and orderly `SIGINT`/`SIGTERM`
shutdown.
Android's `AppNavigationController` is the corresponding route owner. Its
Material 3 drawer exposes the same seven roots and derives drawer selection
from the route. It uses local vector resources, a sans-serif hierarchy and
@ -463,20 +621,21 @@ known errors with generic placeholders.
## 11. Layering rule
```text
Notcurses / Compose rendering
|
v
application workflow
|
+-- domain model
+-- synchronization
+-- validation
|
v
persistence / MTP transport
Notcurses / Web / Compose adapters
|
v
typed Core application services
|
+-- domain model and calculations
+-- read models and command services
+-- synchronization and validation
|
v
persistence / MTP transport
```
Rendering does not own persistence rules.
Rendering does not own persistence rules, SQL, or duplicate business
calculations.
Persistence and MTP code do not depend on Notcurses rendering.

View file

@ -1,6 +1,6 @@
# Current implementation state
Snapshot date: **2026-09-15**.
Snapshot date: **2026-09-16**.
This document owns the current implemented state. Historical checkpoints and
closed incidents belong in [reviews](reviews/) and the
@ -10,11 +10,14 @@ closed incidents belong in [reviews](reviews/) and the
Android is the field companion: it captures training and body data, preserves
the active draft, receives AI proposals, triggers synchronization, and shows
quick summaries. The C17/Notcurses TUI is the detailed consultation,
correction, catalogue, analytics, graphing, and long-term tracking surface.
quick summaries. The C17/Notcurses TUI is the administration, inspection,
maintenance, import/export, and technical-tooling surface. The documented but
not yet implemented local Web sibling will own analysis, visualization, and
program/session preparation through Trainlog Core.
Android captures and summarizes. The TUI analyzes and tracks over time. The
desktop SQLite database remains the canonical long-term history.
Trainlog Core owns business truth. The desktop SQLite database remains the
canonical local source of truth and long-term history; no interface owns a
parallel implementation of its rules.
## Versions and compatibility
@ -25,16 +28,36 @@ desktop SQLite database remains the canonical long-term history.
| Android SQLite | schema v17 |
| Mobile snapshot | V3 active; V1/V2 readable legacy inputs |
| Desktop terminal backend | Notcurses only |
| Trainlog product version | `0.1.1` stable, synchronized across Android and desktop (no independent interface versions) |
| Interface language | `TRAINLOG_I18N_V0_1_1=PASS`: French default; English selectable in Settings → Language on both surfaces |
| AI history export | `TRAINLOG_AI_EXPORT_V1` active |
| AI session proposals | `TRAINLOG_AI_SESSION_DRAFT_V1=VALIDATION_PENDING` |
| Training knowledge | `TRAINING_KNOWLEDGE_V1=PASS` |
| Session generator | `SESSION_GENERATOR_V1=PASS`, hidden pending V2 |
| Application shell | `APP_SHELL_V1=IMPLEMENTED_AWAITING_VISUAL_REVIEW_2` |
| Statistics | `STATS_V1=IMPLEMENTED` |
| Local Web | `TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_NOT_STARTED` |
Desktop and Android schema numbers are independent. Neither changes the frozen
Trainlog JSON V1 contract.
Language is device-local presentation state. Android keeps it in dedicated
SharedPreferences; desktop keeps it in the XDG configuration file
`trainlog/presentation.conf`. A successful selection updates the UI
immediately. It does not enter either SQLite database, training data, stable
IDs, schemas, exchange artifacts, synchronization protocols, AI proposals,
MAX, or feedback. User-defined exercise and catalogue names are never
automatically translated. Where a recognized stable body-zone ID has a local
presentation label, only that label is localized; unknown IDs retain their
catalogue label. Numbers, dates, and other visible formatting are owned by the
selected presentation language.
Synchronization remains a protocol boundary: core, receipt, and persisted
history summaries stay opaque operational bytes. Android and the TUI render
their current synchronization state from local typed status and counters; they
never inject a raw cross-device French summary into the visible interface. No
raw exchange or protocol format changed for interface language support.
## Storage and synchronization
- Android stores capture data in its private SQLite database.
@ -59,9 +82,12 @@ Android currently provides:
- metadata-driven SETS+REPS, SETS+DURATION, and CONTINUOUS+DURATION capture;
- actual per-set values, optional occurrence equipment, and explicit MAX rows;
- exactly one durable active session draft with resume, confirmed discard, and
atomic completion;
- completed-session consultation and correction;
- exactly one durable active session draft with identity-preserving continuous
multi-position drag reorder,
resume, confirmed discard, and atomic completion;
- completed-session consultation, transient drag reorder, and atomic factual or
order correction while retaining session/occurrence/feedback identities for
synchronization;
- exercise catalogue editing with stable `exercise_id` and referenced-profile
protection;
- supplied/custom equipment and machine-exercise metadata;
@ -70,8 +96,10 @@ Android currently provides:
follow-ups;
- pending AI proposal collection, explicit start/delete, and durable tombstones;
- 7/30/90-day, year, and all-history quick statistics;
- direct-storage snapshot publication, sync triggering, receipt display, and
companion application.
- direct-storage snapshot publication, sync triggering, locally rendered typed
receipt status/counters, and companion application.
- French-default/English-selectable presentation from **Settings → Language**,
applied immediately without changing repository or synchronization state.
Android does not own canonical long-term analytics. The V1 session generator is
implemented but intentionally hidden from normal navigation pending V2.
@ -90,10 +118,25 @@ The desktop is a strict C17 application using Notcurses. It currently provides:
- statistics derived from canonical occurrence-owned actual work, including
exact calendar-week/month buckets, performance, body, frequency, exercise,
and BODY ZONES views;
- detailed synchronization status and history through the shared engine.
- detailed locally rendered typed synchronization status/history through the
shared engine.
- French-default/English-selectable presentation from **Settings → Language**,
including selected-language formatting without changing canonical data.
Rendering does not own SQL or business rules. Statistics are read-only
projections; they are not persisted as facts.
Rendering is not permitted to own SQL or business rules. Statistics are
read-only projections; they are not persisted as facts. The current TUI
Dashboard still has a known exception: its direct SQLite/
`database_internal.h` fact projection must be characterized, extracted into a
typed Core read model, and consumed by the TUI before any Web Dashboard
endpoint is created. This is an incremental extraction, not authorization for
a global `tui.c` refactor.
## Local Web
`TRAINLOG_WEB_V1` architecture, API independence, local-network boundary,
browser shell, Dashboard, layout ownership, build/runtime separation, and
security invariants are now canonical. No Web server, frontend, CLI option,
HTTP endpoint, embedded asset, or layout persistence is implemented yet.
## Data semantics
@ -113,10 +156,19 @@ projections; they are not persisted as facts.
## Validation status
The durable commands are owned by [tests.md](tests.md). On 2026-09-15 this audit
passed desktop compilation, **58/58 Meson tests**, Android unit tests, Android
debug assembly, the JSON validator, and the import-contract validator. Link and
diff safety checks also pass in the final audit evidence.
The durable commands are owned by [tests.md](tests.md). On 2026-09-15 final
validation passed desktop compilation, **60/60 normal Meson tests** and
**60/60 ASan/UBSan Meson tests**, Android debug assembly and **202 Android
tests (198 passed, 4 skipped, 0 failed)**, the JSON validator, and the
import-contract validator. The source-derived TUI
`TRANSLATABLE_UI=0` check and Android resource parity also passed. Link and
diff safety checks pass in the final audit evidence.
`TRAINLOG_I18N_V0_1_1=PASS` is covered by desktop presentation, persistence,
formatting, layout-invariance and source-derived text-boundary tests, plus
Android resource-parity, language-owner, typed sync-presentation, and
stable-data presentation tests. A real-device/manual visual language-switch
smoke remains a manual validation; it does not change the validated status.
Hardware-dependent MTP and the final real Drive plus Android-triggered AI-draft
smoke test are not automated. The latter is why
@ -124,6 +176,8 @@ smoke test are not automated. The latter is why
## Active limitations
- `TRAINLOG_WEB_V1` is contract-only; its implementation starts with Dashboard
characterization and Core extraction, not React or HTTP infrastructure.
- `APP_SHELL_V1` still awaits the recorded human visual/accessibility review.
- AI proposal exchange still awaits one real Drive plus Android-triggered
bidirectional smoke test.

View file

@ -7,9 +7,10 @@ in [reviews](reviews/) and [CHANGELOG.md](../CHANGELOG.md).
## Product boundary
```text
Android captures and summarizes.
The TUI analyzes and tracks over time.
Desktop SQLite is canonical long-term history.
Trainlog Core owns business truth and canonical desktop persistence.
The TUI administers, inspects, maintains, and imports/exports.
The local Web analyzes, visualizes, and prepares programs/sessions.
Android executes in the field, captures actual work/feedback, and returns data.
```
Future work must preserve the frozen Trainlog JSON V1 contract, stable
@ -23,16 +24,45 @@ Desktop schema v18, Android schema v17, Notcurses, direct
Zones V1, Training Feedback V1/V2, STATS V1, and Session Generator V1 are
implemented. Session Generator V1 is hidden pending V2. AI session-draft
exchange and APP_SHELL_V1 retain their explicit manual validation/review gates.
`TRAINLOG_WEB_V1=CONTRACT_FROZEN / IMPLEMENTATION_NOT_STARTED`; its local-only
architecture is canonical, but no Web code exists yet.
## Current cursor
```text
CURRENT_OPERATIONAL_CURSOR=REAL_DATA_BASELINE_V1
CURRENT_OPERATIONAL_CURSOR=WEB_DASHBOARD_CHARACTERIZATION_V1
```
Collect real body observations and training sessions through Android, sync them
to desktop, and record MAX only through explicit `max_test` results. Do not seed
fictitious user history into canonical databases.
Characterize the existing TUI Dashboard before moving its direct SQLite and
`database_internal.h` projection into a typed Core read model. Then adapt the
TUI to consume that read model. This incremental sequence is a blocking
prerequisite for a Web Dashboard endpoint and does not authorize a global
`tui.c` refactor.
## TRAINLOG_WEB_V1 implementation gate
Implementation order is fixed as:
```text
WEB_DASHBOARD_CHARACTERIZATION_V1
-> WEB_DASHBOARD_CORE_READ_MODEL_V1
-> WEB_TUI_READ_MODEL_ADOPTION_V1
-> WEB_CLI_HTTP_INFRASTRUCTURE_V1
-> WEB_FRONTEND_SHELL_V1
-> WEB_DASHBOARD_V1
```
The Web must not start with React. Characterization proves current behavior;
Core extraction establishes one business implementation; TUI adoption proves
reuse; only then may `trainlog -w`, the loopback HTTP adapter, embedded assets,
and browser UI be introduced. Initial API delivery is read-only. Later program
and session writes require explicit transactional Core command services.
`TRAINLOG_WEB_V1` preserves `/api/v1/` as an independently versioned boundary,
the `127.0.0.1:8080` default, explicit `--port`, no silent port fallback, a
single serialized Core/SQLite owner, build-only Node/npm, and no change to
business SQLite or frozen exchange formats. Dashboard Activité and Progression
mathematics remain specification gates before their read models.
## Next
@ -75,7 +105,11 @@ normal capture.
## Canonical order
```text
REAL_DATA_BASELINE_V1
WEB_DASHBOARD_CHARACTERIZATION_V1
-> WEB_DASHBOARD_CORE_READ_MODEL_V1
-> WEB_TUI_READ_MODEL_ADOPTION_V1
-> TRAINLOG_WEB_V1
-> REAL_DATA_BASELINE_V1
-> GYM_CATALOG_V1
-> EXERCISE_METADATA_V1
-> SESSION_GENERATOR_V2
@ -101,3 +135,6 @@ filesystem, exercise-name identity heuristics, fake sets for continuous work,
synthetic uniform actuals, ordinary training promoted to measured MAX,
assistance interpreted as external load, or estimates presented as direct
measurements. Incompatible exchange semantics require a new version.
Web UI configuration remains outside training data, scientific/AI exports,
session synchronization, and business SQLite. Browser, React, TUI, and Android
must not query desktop tables or duplicate Core calculations.

View file

@ -29,6 +29,23 @@ It validates:
A negative fixture passes only when Trainlog rejects it.
Android field-workflow regression coverage additionally permutes distinct and
duplicate active occurrences, checks stable IDs/equipment/targets/actuals and raw
form state, recreates the repository, injects a transactional write failure, and
finalizes the reordered draft. Completed-session tests edit reps, loads, set and
continuous durations, distance, and speed; preserve feedback/follow-up roots;
exercise atomic rollback; and inspect the corrected V3 export identity.
Completed-order coverage also exercises duplicate exercises, SETS, CONTINUOUS,
MAX, planning, equipment, feedback revisions, J+1 follow-up, Cancel/no-write,
successful position persistence, injected Save rollback, and desktop V3
reconciliation without a duplicate canonical session.
Field-input coverage maps one accumulated drag directly from index 5 to 1 and
between both list extremes, restores the origin permutation on cancellation,
and checks decimal editing/final parsing for both separators. It covers `0.3`,
`0,3`, quarter-unit values, whole values, incomplete separator states, malformed
text, non-finite spellings, negative final distance, stored numeric equality,
and locale-independent V3 JSON publication.
Semantic validation includes:
```text
@ -118,6 +135,7 @@ body_analytics
terminal_input_event_type_policy
tui_workflows
app_shell
presentation_i18n
```
The desktop suite includes APP_SHELL_V1 production-transition coverage and the
@ -150,6 +168,26 @@ The desktop executable is additionally smoke-checked in isolated tmux PTYs at
recover before a clean keyboard quit. Notcurses is verified as the executable's
direct terminal dependency with `readelf -d`.
`presentation_i18n` covers the deterministic French default, French/English
catalogue lookup, immediate in-process language switching, selected-language
decimal/date formatting even under a contrary host numeric locale, unchanged
layout at the 72x20 minimum, and local XDG configuration persistence/failure
semantics. It also proves that persisted catalogue names are not translation
keys, the source-derived `TRANSLATABLE_UI=0` boundary, and that synchronization
presentation uses local typed status/counters rather than raw opaque
core/receipt/history summaries.
Android Robolectric coverage verifies both resource locales, Settings language
ownership and dedicated SharedPreferences persistence, French fallback for an
absent/invalid preference, and localized presentation of known stable BODY ZONE
IDs without changing unknown catalogue labels or repository values. Resource
parity and typed synchronization-receipt presentation are also covered; raw
protocol/operational summaries remain opaque. Final i18n validation passed
60/60 normal desktop tests, 60/60 ASan/UBSan desktop tests, and 202 Android
tests (198 passed, 4 skipped, 0 failed), with both JSON validators passing.
Automated coverage does not replace a real device/manual visual language-switch
smoke; that remains explicit manual validation.
Notable regression coverage:
- transactional persisted-session replacement;
@ -238,6 +276,24 @@ Notable regression coverage:
Stable-release validation additionally builds the signed release variant and
checks the packaged product versions and binary linkage:
The release machine supplies all four `TRAINLOG_RELEASE_*` environment
variables, or a mode-0600 private file at
`~/.config/trainlog/release-signing.properties` with this shape:
```text
storeFile=/absolute/private/path/trainlog-release.jks
storePassword=<local secret>
keyAlias=<local alias>
keyPassword=<local secret>
```
`TRAINLOG_RELEASE_CREDENTIALS_FILE` may select another private properties file.
Environment variables take precedence over file values. The keystore and
credentials are the durable Android update identity: back them up through an
approved encrypted external mechanism, never Git or release assets. Missing
configuration makes `packageRelease` fail explicitly rather than producing an
apparently publishable unsigned APK.
```bash
meson compile -C build
meson test -C build --print-errorlogs
@ -246,10 +302,13 @@ cd android
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew test
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew assembleRelease
file trainlog-tui-linux-x86_64-v0.1.0
ldd trainlog-tui-linux-x86_64-v0.1.0
sha256sum trainlog-android-v0.1.0.apk \
trainlog-tui-linux-x86_64-v0.1.0
apksigner verify --verbose --print-certs \
app/build/outputs/apk/release/app-release.apk
file trainlog-tui-linux-x86_64-v<version>
ldd trainlog-tui-linux-x86_64-v<version>
sha256sum trainlog-android-v<version>.apk \
trainlog-tui-linux-x86_64-v<version>
```
An unsigned release APK is build evidence only and must not be published as the

View file

@ -115,6 +115,32 @@ Minimum terminal size:
Smaller terminals display a clear fallback instead of corrupt layout.
## Interface language (`TRAINLOG_I18N_V0_1_1`)
Trainlog version 0.1.1 is synchronized with Android as one product version;
the TUI has no independent interface version. French is the default. English is
the only alternate language and is selected through **Settings → Language**.
The active TUI rerenders immediately after a successful selection.
The choice is local presentation state in the XDG configuration file
`trainlog/presentation.conf` (under `$XDG_CONFIG_HOME`, or
`$HOME/.config` when the XDG variable is unavailable). Invalid, absent, or
unreadable preference data falls back to French. A failed persistence attempt
does not claim a committed choice. The setting never enters the desktop SQLite
database, training data, stable IDs, schemas, exchange artifacts, transport
protocols, AI proposals, MAX, or feedback.
Trainlog-owned strings and selected-language number/date formatting are
localized. Persisted/user exercise and catalogue names are byte-exact display
data, never translation keys. Stable BODY ZONE IDs select localized known-zone
labels where applicable; unknown IDs retain their catalogue label. Layout and
minimum-terminal behavior remain language-invariant.
The current synchronization screen renders local typed status and counters. It
does not translate, reinterpret, or inject Android/core/receipt/history raw
summaries: those protocol and operational bytes remain opaque. No exchange,
receipt, or synchronization-history format changed for this presentation work.
## 3. Visual rules
The TUI uses centralized semantic theme roles.
@ -174,7 +200,8 @@ Unicode-aware normalized-name uniqueness prevents duplicate logical names.
`3 Exercices` also owns Body Zones V1. Creation opens a keyboard-only manifest
selector: `p` chooses the sole primary, Space toggles secondaries, `n` selects
the explicit unclassified state and clears all relations, Enter validates and Escape
cancels without mutation. Only French display names are shown; stable IDs such
cancels without mutation. Visible names use the selected presentation language
where Trainlog owns the label; stable IDs such
as `chest` remain internal. New set-based exercises require a primary zone.
The exercise list supports `/` prefix search and `z` cycling through all

View file

@ -1,7 +1,7 @@
project(
'trainlog',
'c',
version: '0.1.0',
version: '0.1.1',
meson_version: '>=1.3.0',
default_options: [
'c_std=c17',