430 lines
12 KiB
Markdown
430 lines
12 KiB
Markdown
# Architecture
|
|
|
|
## 1. System boundary
|
|
|
|
Trainlog separates capture, durable history, transport, and presentation.
|
|
|
|
```text
|
|
Android capture client
|
|
|
|
|
| local SQLite
|
|
|
|
|
+-- automatic mobile snapshot
|
|
|
|
|
v
|
|
Android shared storage
|
|
Download/Trainlog
|
|
|
|
|
| direct MTP
|
|
v
|
|
shared desktop sync engine
|
|
/ \
|
|
/ \
|
|
desktop SQLite directional PC artifacts
|
|
| |
|
|
v v
|
|
desktop TUI Android
|
|
```
|
|
|
|
The desktop SQLite database is the canonical long-term history.
|
|
|
|
Android local SQLite is a capture store, not a synchronization format.
|
|
|
|
## 2. Components
|
|
|
|
### Android application
|
|
|
|
Responsibilities:
|
|
|
|
- exercise catalog entry;
|
|
- stable-ID exercise rename/editing;
|
|
- workout-session recording;
|
|
- performed set entry;
|
|
- continuous-activity entry;
|
|
- body measurement entry;
|
|
- local history/detail;
|
|
- automatic mobile snapshot generation;
|
|
- PC catalog application;
|
|
- synchronization request creation;
|
|
- synchronization receipt display.
|
|
|
|
Android is not responsible for canonical long-term analytics.
|
|
|
|
### Desktop core
|
|
|
|
The C17 core owns:
|
|
|
|
- desktop SQLite persistence;
|
|
- exercise/catalog rules;
|
|
- profile-aware session data;
|
|
- body data;
|
|
- ID and time helpers;
|
|
- USB discovery;
|
|
- direct MTP operations;
|
|
- the shared bidirectional synchronization engine.
|
|
|
|
### 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.
|
|
|
|
It consumes core services for:
|
|
|
|
- session entry/editing;
|
|
- history;
|
|
- exercise performance;
|
|
- body tracking;
|
|
- graphs;
|
|
- manual synchronization;
|
|
- synchronization log/detail display.
|
|
|
|
### `trainlog-syncd`
|
|
|
|
`trainlog-syncd` is a small user-session agent.
|
|
|
|
It polls for a new Android request and invokes the same shared C synchronization
|
|
engine used by the TUI.
|
|
|
|
It does not implement a second synchronization algorithm.
|
|
|
|
## 3. Exercise model
|
|
|
|
Trainlog is metadata-driven:
|
|
|
|
```text
|
|
recording_mode = SETS | CONTINUOUS
|
|
tracking_mode = REPS | DURATION
|
|
data_fields = SPEED_KMH | DISTANCE_KM
|
|
```
|
|
|
|
Valid model-v1 combinations:
|
|
|
|
```text
|
|
SETS + REPS
|
|
SETS + DURATION
|
|
CONTINUOUS + DURATION
|
|
```
|
|
|
|
Load mode is session-specific:
|
|
|
|
```text
|
|
none
|
|
external
|
|
assistance
|
|
```
|
|
|
|
Continuous work is persisted separately from performed sets.
|
|
|
|
## 4. Persistence ownership
|
|
|
|
### Desktop
|
|
|
|
Desktop SQLite schema v10 is canonical long-term history. Its v9 -> v10
|
|
migration losslessly rebuilds only `performed_sets` so actual `weight_kg` may
|
|
be finite `>= 0`; the column already existed and targets/max results retain
|
|
their strictly-positive contracts. `session_exercises`
|
|
stores a stable occurrence `entry_id`; a catalogue `exercise_id` can therefore
|
|
occur more than once in one session without identity fusion.
|
|
|
|
Main tables:
|
|
|
|
```text
|
|
exercises
|
|
sessions
|
|
session_exercises
|
|
performed_sets
|
|
continuous_activity
|
|
max_results
|
|
body_observations
|
|
custom_equipment
|
|
```
|
|
|
|
### Android
|
|
|
|
Android has an independent local SQLite schema, currently v9. Completed and
|
|
draft MAX values use one-to-one `max_results` and `draft_max_results` rows;
|
|
resuming a completed Test max records its stable source session in the one
|
|
durable draft.
|
|
|
|
It mirrors domain concepts needed for capture, but its schema version is not
|
|
coupled to the desktop schema.
|
|
|
|
Synchronization exchanges domain artifacts rather than database files.
|
|
|
|
`TrainlogRepository` owns a singleton active-session draft, its ordered exercise
|
|
and actual-value children, and raw form text. Compose sends meaningful mutations
|
|
to that repository; lifecycle callbacks are not the sole persistence boundary.
|
|
Normal navigation never deletes the draft. Home restores the resume affordance
|
|
from SQLite after process recreation.
|
|
|
|
Drafts use separate tables from completed sessions and are never export sources.
|
|
Finalization inserts the completed session and deletes the draft in one
|
|
transaction; failures retain the draft. Catalog row references preserve draft
|
|
identity through existing PC-catalog reconciliation. Missing editing-selection
|
|
recovery preserves the raw fields and added exercises with a specific warning.
|
|
|
|
`TrainlogRepository.editExercise()` owns all Android exercise edits. It changes
|
|
the display name and normalized form in the existing catalog row identified by
|
|
`exercise_id`; foreign-key ownership consequently preserves completed history
|
|
and active drafts. A profile edit is admitted only before that row is referenced
|
|
by either completed or active-draft data. Same-ID reconciliation applies name
|
|
metadata in place. A different-ID normalized-name collision may merge only for
|
|
equal recording/tracking modes, compatible represented invariants, and
|
|
`data_fields` masks comparable by inclusion. Desktop ownership wins on both
|
|
sides; the richer mask is retained and every occurrence/draft reference moves
|
|
transactionally. Otherwise synchronization reports a conflict.
|
|
|
|
Compose presentation has one `TrainlogScreen` header component for every page.
|
|
It uses the TUI's compact accent `◆ TRAINLOG ◆` plaque and muted context line;
|
|
screen navigation and data ownership remain independent from the header.
|
|
|
|
## 5. Compatibility boundaries
|
|
|
|
### Frozen Trainlog JSON v1
|
|
|
|
`TRAINLOG_FORMAT_V1` is frozen and remains a compatibility boundary for its
|
|
existing set-based session contract.
|
|
|
|
### Synchronization artifacts
|
|
|
|
Synchronization uses separate formats:
|
|
|
|
```text
|
|
trainlog-mobile-export v1
|
|
trainlog-mobile-export v2
|
|
trainlog-pc-catalog v1
|
|
trainlog-equipment-associations v2
|
|
trainlog-equipment-definitions v1
|
|
trainlog-sync-request v1
|
|
trainlog-sync-receipt v1
|
|
```
|
|
|
|
A new domain requirement must not be forced into frozen v1 by using notes,
|
|
synthetic sets, or data loss.
|
|
|
|
The active occurrence-aware session exchange remains
|
|
`trainlog-mobile-export` v2. The separate directional
|
|
`trainlog-equipment-definitions` v1 artifacts carry user-created equipment
|
|
definitions: `trainlog-mobile-equipment-definitions-v1.json` travels from
|
|
Android to PC and `trainlog-pc-equipment-definitions-v1.json` travels from PC
|
|
to Android. The filename identifies direction; the JSON format and version do
|
|
not change. V1 historical artifacts remain readable under their frozen
|
|
contracts.
|
|
|
|
## 6. Direct MTP transport
|
|
|
|
Linux transport:
|
|
|
|
```text
|
|
physical Android USB device
|
|
|
|
|
v
|
|
libudev discovery
|
|
|
|
|
| bus + device number
|
|
v
|
|
libmtp exact raw-device access
|
|
|
|
|
v
|
|
Android internal storage
|
|
```
|
|
|
|
No GVFS/FUSE mount is required.
|
|
|
|
Canonical exchange directory:
|
|
|
|
```text
|
|
Download/Trainlog
|
|
```
|
|
|
|
## 7. Shared synchronization engine
|
|
|
|
Both user-trigger paths call:
|
|
|
|
```text
|
|
trainlog_sync_run()
|
|
```
|
|
|
|
Manual path:
|
|
|
|
```text
|
|
TUI -> trainlog_sync_run()
|
|
```
|
|
|
|
Android-triggered path:
|
|
|
|
```text
|
|
Android request
|
|
-> trainlog-syncd
|
|
-> trainlog_sync_run()
|
|
-> receipt
|
|
```
|
|
|
|
The same engine owns all supported directions. Its explicit modes are:
|
|
|
|
```text
|
|
a = Android -> PC only
|
|
p = PC -> Android only
|
|
b = Android -> PC, then PC -> Android
|
|
```
|
|
|
|
The Android-to-PC direction receives the mobile equipment-definitions v1
|
|
artifact, mobile-export v2, and equipment-associations v2. The PC-to-Android
|
|
direction publishes PC equipment-definitions v1 before dependent artifacts,
|
|
then publishes the PC catalog v1, PC mobile-export v2 (including completed
|
|
sessions and body observations), and equipment-associations v2.
|
|
|
|
The engine performs the applicable direction steps:
|
|
|
|
```text
|
|
1. direct-MTP device/storage discovery
|
|
2. exchange-folder resolution
|
|
3. definition artifact transfer and reconciliation before dependent V2 data
|
|
4. strict transactional Android -> PC import when selected
|
|
5. PC catalog, mobile/body, and association export when selected
|
|
6. direct-MTP publication of the selected PC -> Android artifacts
|
|
7. optional request receipt publication
|
|
8. structured run-history recording
|
|
```
|
|
|
|
Synchronization is additive and reconciliatory: artifact absence never implies
|
|
deletion of local content. Equal same-ID definitions are idempotent; divergent
|
|
same-ID definitions, unknown references, and association conflicts are reported
|
|
explicitly. The affected persisted content is preserved on conflict; the engine
|
|
does not silently overwrite it.
|
|
|
|
There is no exercise, session, body-observation, or equipment-definition
|
|
tombstone in the current formats. Omitting one of those objects from a later
|
|
snapshot is not a deletion request. The only explicit removal signal is
|
|
equipment-association V2 `state: cleared`, scoped to one
|
|
`(session_id, entry_id)` occurrence.
|
|
|
|
## 8. Synchronization concurrency
|
|
|
|
The shared engine serializes synchronization with:
|
|
|
|
```text
|
|
$XDG_DATA_HOME/trainlog/sync.lock
|
|
```
|
|
|
|
The TUI waits for an active transaction.
|
|
|
|
Daemon polling uses non-blocking acquisition and retries later.
|
|
|
|
A request ID already successfully consumed is not processed as a new request.
|
|
|
|
## 9. Synchronization history
|
|
|
|
Every actual run gets a stable:
|
|
|
|
```text
|
|
sy_<uuid-v4>
|
|
```
|
|
|
|
Structured history is stored under:
|
|
|
|
```text
|
|
$XDG_DATA_HOME/trainlog/sync_runs/
|
|
```
|
|
|
|
The TUI exposes list/detail semantics comparable to:
|
|
|
|
```text
|
|
git log
|
|
git show
|
|
```
|
|
|
|
## 10. Error philosophy
|
|
|
|
Trainlog prefers explicit failure over silent corruption.
|
|
|
|
Hard validation or persistence failure aborts the relevant transaction.
|
|
|
|
The synchronization layer records useful failure detail rather than masking
|
|
known errors with generic placeholders.
|
|
|
|
## 11. Layering rule
|
|
|
|
```text
|
|
Notcurses / Compose rendering
|
|
|
|
|
v
|
|
application workflow
|
|
|
|
|
+-- domain model
|
|
+-- synchronization
|
|
+-- validation
|
|
|
|
|
v
|
|
persistence / MTP transport
|
|
```
|
|
|
|
Rendering does not own persistence rules.
|
|
|
|
Persistence and MTP code do not depend on Notcurses rendering.
|
|
|
|
## 12. Body analytics boundary
|
|
|
|
Body analytics belong to the desktop analysis layer.
|
|
|
|
```text
|
|
Android
|
|
real measurements only
|
|
|
|
|
v
|
|
desktop body_observations
|
|
|
|
|
v
|
|
pure derived analytics
|
|
|
|
|
v
|
|
TUI display
|
|
```
|
|
|
|
The analytics layer does not modify canonical observations.
|
|
|
|
A desktop-only configuration file stores the estimation profile:
|
|
|
|
```text
|
|
$XDG_CONFIG_HOME/trainlog/body_analytics.conf
|
|
```
|
|
|
|
with `~/.config` fallback.
|
|
|
|
This profile is not synchronized to Android and does not require a SQLite
|
|
schema change.
|
|
|
|
## 13. Audited evolution constraints
|
|
|
|
Each mutating importer has strict validation and a SQLite transaction. The V2
|
|
association companion is a strict corroboration of the equipment already
|
|
carried by the mobile snapshot and does not rewrite divergent state. However,
|
|
one definitions/mobile/associations publication has no common generation
|
|
manifest and is not one cross-artifact database transaction. A late association
|
|
conflict can therefore follow a successfully committed definitions or mobile
|
|
import; replay remains idempotent and existing conflicting content is not
|
|
overwritten. A future batch protocol must be separately versioned rather than
|
|
retrofitted into frozen formats.
|
|
|
|
Android and desktop also have different stored name-normalization behavior:
|
|
Android removes diacritics, while the frozen desktop/Python rule preserves them
|
|
through NFC plus Unicode case folding. Correcting existing Android keys requires
|
|
an explicit migration and collision policy. Finally, Android retains supplied
|
|
exercise/equipment relationship tables, but its current picker searches the
|
|
complete definition catalog; applying those relations as recommendations is
|
|
future gym-catalog behavior.
|
|
|
|
Custom-equipment reconciliation is deliberately ID-based. Different custom
|
|
IDs are not merged by equal or similar display names because their load
|
|
semantics and existing occurrence references may differ. The current PC
|
|
catalog V1 Android reader also validates its required semantic fields without
|
|
the exact-key rejection used by newer V2/companion readers; stricter V1 parsing
|
|
needs an explicit compatibility review.
|
|
|
|
The desktop model/API currently permits bounded supplemental field masks on a
|
|
`SETS` profile, while Android creation and V2 inbox validation require those
|
|
masks to be zero for `SETS`. The shipped catalog uses supplemental speed and
|
|
distance only with `CONTINUOUS`; defining cross-platform behavior for a future
|
|
set-based supplemental field is a model-contract decision, not part of this
|
|
reconciliation.
|