Add matched training signals, daily recovery and performance analysis

This commit is contained in:
Peter Stockings
2026-10-03 13:22:32 +10:00
parent e6e2f22479
commit 418e03583a
37 changed files with 1380 additions and 47 deletions
+18
View File
@@ -1,6 +1,24 @@
# Dokku deployment
App: `stride`; database: `stride-db`; domain: `stride.peterstockings.com`.
Training-signal releases migrate private daily recovery and session preferences
automatically. Back up PostgreSQL before release, then recompute older recordings:
```sh
dokku run stride env FLASK_APP=app flask backfill-analysis --batch-size 20
dokku run stride env FLASK_APP=app flask install-training-signals --email ACCOUNT
dokku run stride env FLASK_APP=app flask backfill-health --email ACCOUNT --days 90
```
The installer validates queries before saving, pins five analyses and preserves
existing edits on repeat runs. Pace and long-run thresholds are editable CLI
options and account queries. Recovery backfill uses the existing encrypted
session and account lock, enables recovery only for that account and leaves
missing watch observations missing. Other accounts can opt in through Settings;
recovery failures do not stop activity imports. Never run a local copy of the
same Garmin session concurrently with production. Analysis version 3 persists
continuous windows; no dashboard query loads full recording samples.
The Dockerfile installs locked dependencies on Python 3.12. Dokku runs database
migrations in the predeploy step, then one web container and one sync worker.
+112
View File
@@ -1,5 +1,117 @@
# Run analysis DSL
## Deep training signals
`intervals` reads persisted, non-overlapping continuous moving windows of up to
30 seconds. Recording breaks over ten seconds, distance resets, stops and
implausible movement end a window; windows never join across those breaks.
Filter `duration >= 25 s` to remove short fragments. `elapsed` and
`moving_offset` are minutes since recording start and usable moving start.
Elapsed offsets use relative time even when FIT timestamps are Unix timestamps.
```text
intervals
during last 16 weeks
reduce by run min 3 min
where elapsed >= 10 min
and duration >= 25 s
and pace between 6:30 and 7:00 /km
and grade between -2 and 2 %
and grade_variability <= 2 %
and pace_variability <= 15 %
and sensor_coverage >= 90 %
and hr_artifact_percent = 0 %
summarize mean(heart_rate) as "Matched HR"
by run
sort by date asc
compare none
chart line
```
Filters run **before** `reduce by run`. A session needs the specified total
matched moving minutes to contribute one observation. Within-session HR and
mechanics are weighted by usable sensor time; `mean` across reduced observations
weights each session equally. `count()` then counts independent sessions rather
than windows. Matched windows can be separated in time. This is different from
requiring one uninterrupted three-minute section. Charts show distinct-session
and usable-minute coverage, and run plots preserve calendar spacing.
`grade` is duration-weighted signed grade using the existing smoothed elevation
model; `grade_variability` is its population SD in percentage points. GAP and
grade require at least 80% terrain coverage. Window-level cadence, power,
stride length, vertical oscillation and ground contact include sensor coverage
metrics. HR outside 30–240 bpm or a jump exceeding 20 bpm per second is excluded
and counted in `hr_artifact_percent`. These coarse checks do not detect every
sensor artifact or cadence lock. Terrain, HR lag, weather and session intent
remain confounders; the matcher does not claim to remove them.
After reduction, `heart_rate_drift` is recomputed over matched moving-time
halves, excluding any filtered warm-up. Each half needs at least ten minutes of
joint usable HR and terrain, and 80% of the half's time. Halves must differ by
at most 10% in speed and one percentage point of mean grade. Otherwise the
result is missing. Positive means lower speed/HR efficiency later. This is a
descriptive comparison, not an overtraining diagnosis. Recording-level flags
require reduction to independent sessions; a baseline still needs ten runs.
`efforts` expands the persisted best elapsed windows. Use `where distance = 5 km`
and `min(effort_time)` for a 5 km progression. Whole runs expose `best_1km`,
`best_2km`, `best_5km`, and `best_10km`, which accept second/minute filters and
display elapsed min:sec. No attempt at that distance remains missing. These
efforts are windows inside runs, not standardized races.
```text
days
during last 16 weeks
long run distance >= 10 km
summarize latest(days_since_long_run), latest(rolling_28d_distance)
by day
compare none
chart line
display overlay
```
`days` includes rest days. `sessions`, `days_since_run`, `days_since_long_run`,
`run_day_streak`, and `long_run_share` expose continuity and concentration.
`long_run_share` is the largest individual run's share of the current seven-day
distance. Daily 7/28-day windows include the current local day plus the previous
6/27 local days, including history before the query. Whole-run rolling metrics
continue to use elapsed 168/672-hour windows. Missing prior long-run history
does not become a fabricated gap. `latest(field)` selects the latest input by
timestamp regardless of sort order, retaining missing values on that input;
it is useful for current dashboard values and cannot define a baseline band.
`health` reads owner-only Garmin resting HR, overnight HRV, sleep duration and
stress observations (`resting_hr`, `hrv`, `sleep_hours`, `stress`). Enable the
optional recovery sync in Settings. The worker refreshes today/yesterday and
backfills a bounded batch of earlier dates. Provider gaps and unsupported watch
observations stay missing; no readiness score is inferred. Daily inputs link
to their contributing runs when present; rest/recovery-only days do not invent
activity links. Daily sources also support calendar rolling calculations.
Private activity notes expose `perceived_effort` (1–10) and `fatigue` (0–10).
They are never shown on public or friend activity pages. Settings accepts two
personal HR boundaries. `lower_hr_minutes`, `middle_hr_minutes`, and
`upper_hr_minutes` sum actual usable moving sample time, rather than assigning
an entire run/window according to its average HR. Boundary HR belongs to the
higher band. Missing thresholds mean missing results; physiological zones are
never assumed.
`route similar to ID` restricts recordings to an approximate full-course match
against one of your own runs. The comparison resamples compact geometry along
distance, requires endpoints within 100 m, length within 10%, mean course
separation ≤100 m and 90th-percentile separation ≤200 m. Different directions
and recording breaks are excluded; geometry shorter than 500 m is unavailable.
This is an exploratory same-start course matcher, not exact named-segment
recognition. Always inspect the contributing courses.
Operators can run `flask --app app install-training-signals --email ACCOUNT`
to save editable examples and pin the first five. `--pace-low`, `--pace-high`
and `--long-run` customize matching. Re-running preserves existing library
edits. Recompute old recordings with `backfill-analysis` (version 3).
`backfill-health --email ACCOUNT --days 90` imports bounded recovery history
using the normal account session lock and encrypted token rotation; only this
account's recovery sync is enabled.
## Account library and live dashboard
Save an analysis with a name, investigation collection and notes. **Save as new**