# Persisted run analysis and compact routes Imports and recording refreshes now calculate summaries, GAP, terrain breakdowns, lap GAP and charts from the full-resolution samples, in the same transaction as the activity. `analysis_version` identifies the calculation/display version. Current-version empty results distinguish incomplete recordings from pending backfills. Raw recordings remain intact. The page reads `analysis_summary`; the detail JSON endpoint reads `display_data`. Both are deferred JSON columns so activity lists and record comparisons do not load them. No page request calculates full-resolution analysis or reads raw laps. Pending activities show unavailable derived metrics until backfilled. Dashboard maps use the authorized `/activities//route` endpoint, which reads only the persisted `route_geometry` and returns coordinates. The full detail data remains protected by the same owner/friend/public-link access checks as before. No shared caching is introduced for private responses. Route display combines Douglas-Peucker simplification (initial tolerance 3 m) with regular measurement samples for colours. Tolerance increases for complex routes to fit a target of 1,400 points across the recording. Each GPS segment's endpoints are retained, so heavily fragmented routes may exceed that target; gaps are never joined to meet a budget. Coordinates are rounded to six decimals. Map measurements use only distance, speed, HR and grade; other raw fields are omitted. Chart data retains up to 750 evenly spaced samples, including endpoints. Calculations and personal records always use full-resolution source data. The dashboard replay uses canvas rendering and binary search through route edges instead of a linear search on each animation frame. After the schema migration, backfill existing activities: ```sh flask --app app backfill-analysis --batch-size 25 # Dokku avoids interpreting Flask's --app option as a Dokku option: dokku run stride env FLASK_APP=app flask backfill-analysis --batch-size 25 ``` Each batch commits independently and uses PostgreSQL row locks. Reruns skip current-version activities. `--owner-id ID` scopes the job; `--force` rebuilds current results. The job fixes an upper activity ID at start; concurrent imports calculate their own results. No downloads or Garmin authentication are needed. Baseline measured on production on 2026-10-02, before this change: longest run 6,845 samples; median of three internal authenticated requests: dashboard HTML 8 ms, run HTML 156 ms, run JSON 213 ms / 2,654 KiB uncompressed. These timings exclude network transfer, browser rendering and map tile downloads. After deployment and backfill on 2026-10-02: all 19 activities at analysis version 1; rerun processed zero rows. Same longest run retained 6,845 raw samples and used 1,001 display route points. Persisted summary exactly matched a fresh full-resolution calculation. Median of three internal authenticated requests: | Response | Server time | Uncompressed size | | --- | ---: | ---: | | Dashboard HTML | 9 ms | 27 KiB | | Longest run HTML | 8 ms | 16 KiB | | Longest run JSON | 29 ms | 313 KiB | | Longest run route-only JSON | 7 ms | 23 KiB | Page and JSON requests issued zero raw sample/lap queries. HTTPS health returned HTTP 200 and `status: ok`; Dokku web and sync worker startup checks succeeded. The synthetic browser fixture verified dashboard replay, route colour switching and charts. Its in-memory SQLite server uses one request thread to avoid concurrent access to the shared SQLite connection.