feat(train): the Loop's days read on the phone (ADR 0030, D37, D38)

The server gains Train's synced `day`, `day_series` (a day's heart
rate, deleted with its day) and `night`, each a sealed payload alone;
the migration `days` only adds their tables, indexes and foreign keys
(to their sync space, and a chunk's to its day, deferred), and the
person's deletion takes them with their spaces.

On Android, `LoopDays.kt` reads a band's days from a date to today,
framed by the SDK's sync start and stop, into lines of JSON, then
deletes on the band what the app kept up to the date it names, never
today; `LoopBridge` answers `readDays` with them, one band call at a
time, and `background` schedules `LoopWorker`, a WorkManager job every
four hours that appends the same lines to the person's
`loop_days.txt` while the app doesn't hold the band.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 19:53:04 +01:00
commit 88e8693025
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 24637 additions and 52 deletions

View file

@ -0,0 +1,21 @@
### A day as the person's Polar Loop counted it (ADR 0030, D37): its date, its
### steps, distance, calories and time in each activity class, its Nightly
### Recharge and skin temperature, all in its sealed payload (D7). One row per
### person and date, its ID made from both on the device, so a day read again
### (today, as it goes on) or on another phone writes the same row, the last
### writer winning. Its heart rate is its series (`day_series`). Synced.
class: Day
table: day
database: sync
fields:
### The key it is sealed with (ADR 0002's envelope); never null in Train.
keyId: String?
### The content, sealed: gzipped UTF-8 JSON with a `schemaVersion` once
### opened.
payload: ByteData
### The sealing's nonce (ADR 0002's envelope).
nonce: ByteData?
indexes:
# Deleting a space finds its rows through this index (see content_key).
day_space_idx:
fields: spaceId

View file

@ -0,0 +1,25 @@
### A chunk of a day's 24/7 heart rate from the Polar Loop (ADR 0030, D37):
### up to three hours of samples, in its sealed payload (D7) as a workout's
### series are, sealed for its day (`bound`). Its ID is made from its day's and
### its place in the day, so the day read again rewrites the same chunks.
### Synced; it goes with its day.
class: DaySeries
table: day_series
database: sync
fields:
### The day it is part of.
day: Day?, relation(onDelete=Cascade, deferred)
### The key it is sealed with (ADR 0002's envelope); never null in Train.
keyId: String?
### The content, sealed: gzipped UTF-8 JSON with a `schemaVersion` once
### opened.
payload: ByteData
### The sealing's nonce (ADR 0002's envelope).
nonce: ByteData?
indexes:
# Deleting a space finds its rows through this index (see content_key).
day_series_space_idx:
fields: spaceId
# A day's chunks, to read them together.
day_series_day_idx:
fields: dayId

View file

@ -0,0 +1,20 @@
### A night's sleep as the person's Polar Loop analysed it (ADR 0030, D37):
### when it began and ended, its wake phases and cycles, its rating and its
### skin temperature, all in its sealed payload (D7). One row per person and
### the date the sleep counts for, its ID made from both on the device, the
### last writer winning. Synced.
class: Night
table: night
database: sync
fields:
### The key it is sealed with (ADR 0002's envelope); never null in Train.
keyId: String?
### The content, sealed: gzipped UTF-8 JSON with a `schemaVersion` once
### opened.
payload: ByteData
### The sealing's nonce (ADR 0002's envelope).
nonce: ByteData?
indexes:
# Deleting a space finds its rows through this index (see content_key).
night_space_idx:
fields: spaceId