diff --git a/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-08-12-17-42-11_Running.tcx b/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-08-12-17-42-11_Running.tcx
new file mode 100644
index 0000000..3b8bd3e
--- /dev/null
+++ b/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-08-12-17-42-11_Running.tcx
@@ -0,0 +1,190 @@
+
+
+
+
+ 2026-08-12T15:42:11Z
+
+ 624
+ 983.6306355595589
+ 0
+ Active
+ Manual
+
+
+
+
+ 626
+ 980.7304027080536
+ 0
+ Active
+ Manual
+
+
+
+
+ RunnerUp Pixel 6a with barometer
+ 0
+ 0
+
+ 1
+ 0
+ 0
+ 0
+
+
+
+
+
+ org.runnerup - 2.11.0.1 - 23000372
+
+
+ 1
+ 0
+
+
+ en
+ 000-00000-00
+
+
\ No newline at end of file
diff --git a/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-09-15-00_Walking.tcx b/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-09-15-00_Walking.tcx
new file mode 100644
index 0000000..db26651
--- /dev/null
+++ b/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-09-15-00_Walking.tcx
@@ -0,0 +1,1071 @@
+
+
+
+
+ 2026-11-09T08:15:00Z
+
+ 1665
+ 2331.004
+ Active
+ Manual
+
+
+
+ kuhy workout_app
+ 0
+ 0
+
+ 1
+ 0
+
+
+
+
+
+ kuhy workout_app
+
+
+ 1
+ 0
+
+
+ en
+ 000-00000-00
+
+
\ No newline at end of file
diff --git a/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-17-30-00_Running.tcx b/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-17-30-00_Running.tcx
new file mode 100644
index 0000000..a0bddf5
--- /dev/null
+++ b/screen_locker/tests/fixtures/run_tcx/RunnerUp_2026-11-09-17-30-00_Running.tcx
@@ -0,0 +1,747 @@
+
+
+
+
+ 2026-11-09T16:30:00Z
+
+ 1560
+ 4212.014
+ Active
+ Manual
+
+
+
+ kuhy workout_app
+ 0
+ 0
+
+ 1
+ 0
+
+
+
+
+
+ kuhy workout_app
+
+
+ 1
+ 0
+
+
+ en
+ 000-00000-00
+
+
\ No newline at end of file
diff --git a/screen_locker/tests/fixtures/run_tcx/golden_tracks.json b/screen_locker/tests/fixtures/run_tcx/golden_tracks.json
new file mode 100644
index 0000000..1586631
--- /dev/null
+++ b/screen_locker/tests/fixtures/run_tcx/golden_tracks.json
@@ -0,0 +1,1391 @@
+{
+ "running": {
+ "file": "RunnerUp_2026-11-09-17-30-00_Running.tcx",
+ "sport": "Running",
+ "started_at": "2026-11-09T16:30:00Z",
+ "utc_offset_minutes": 60,
+ "total_seconds": 1560,
+ "distance_m": 4212.014,
+ "points": [
+ [
+ "2026-11-09T16:30:00Z",
+ 48.8584,
+ 2.2945,
+ 35.0,
+ 0.0
+ ],
+ [
+ "2026-11-09T16:30:20Z",
+ 48.858772,
+ 2.2949745,
+ 35.0,
+ 54.0
+ ],
+ [
+ "2026-11-09T16:30:40Z",
+ 48.8591285,
+ 2.2954757,
+ 35.3,
+ 107.999
+ ],
+ [
+ "2026-11-09T16:31:00Z",
+ 48.8594688,
+ 2.2960023,
+ 35.5,
+ 161.998
+ ],
+ [
+ "2026-11-09T16:31:20Z",
+ 48.8597926,
+ 2.2965524,
+ 35.8,
+ 215.997
+ ],
+ [
+ "2026-11-09T16:31:40Z",
+ 48.8600999,
+ 2.297124,
+ 36.1,
+ 269.998
+ ],
+ [
+ "2026-11-09T16:32:00Z",
+ 48.8603909,
+ 2.297715,
+ 36.3,
+ 324.001
+ ],
+ [
+ "2026-11-09T16:32:20Z",
+ 48.8606663,
+ 2.298323,
+ 36.6,
+ 378.001
+ ],
+ [
+ "2026-11-09T16:32:40Z",
+ 48.8609269,
+ 2.2989459,
+ 36.8,
+ 432.002
+ ],
+ [
+ "2026-11-09T16:33:00Z",
+ 48.861174,
+ 2.2995813,
+ 37.0,
+ 485.998
+ ],
+ [
+ "2026-11-09T16:33:20Z",
+ 48.8614092,
+ 2.3002272,
+ 37.2,
+ 540.003
+ ],
+ [
+ "2026-11-09T16:33:40Z",
+ 48.8616342,
+ 2.3008814,
+ 37.4,
+ 594.005
+ ],
+ [
+ "2026-11-09T16:34:00Z",
+ 48.8618509,
+ 2.301542,
+ 37.5,
+ 648.004
+ ],
+ [
+ "2026-11-09T16:34:20Z",
+ 48.8620615,
+ 2.3022071,
+ 37.7,
+ 702.0
+ ],
+ [
+ "2026-11-09T16:34:40Z",
+ 48.8622683,
+ 2.3028751,
+ 37.8,
+ 756.006
+ ],
+ [
+ "2026-11-09T16:35:00Z",
+ 48.8624735,
+ 2.3035441,
+ 37.9,
+ 810.002
+ ],
+ [
+ "2026-11-09T16:35:20Z",
+ 48.8626796,
+ 2.3042125,
+ 37.9,
+ 864.001
+ ],
+ [
+ "2026-11-09T16:35:40Z",
+ 48.862889,
+ 2.3048786,
+ 38.0,
+ 918.005
+ ],
+ [
+ "2026-11-09T16:36:00Z",
+ 48.8631038,
+ 2.3055406,
+ 38.0,
+ 972.001
+ ],
+ [
+ "2026-11-09T16:36:20Z",
+ 48.8633263,
+ 2.3061968,
+ 38.0,
+ 1026.003
+ ],
+ [
+ "2026-11-09T16:36:40Z",
+ 48.8635585,
+ 2.3068452,
+ 38.0,
+ 1080.006
+ ],
+ [
+ "2026-11-09T16:37:00Z",
+ 48.8638022,
+ 2.3074837,
+ 37.9,
+ 1134.004
+ ],
+ [
+ "2026-11-09T16:37:20Z",
+ 48.8640589,
+ 2.3081103,
+ 37.8,
+ 1188.001
+ ],
+ [
+ "2026-11-09T16:37:40Z",
+ 48.8643301,
+ 2.3087227,
+ 37.7,
+ 1242.002
+ ],
+ [
+ "2026-11-09T16:38:00Z",
+ 48.8646168,
+ 2.3093186,
+ 37.6,
+ 1296.005
+ ],
+ [
+ "2026-11-09T16:38:20Z",
+ 48.8649196,
+ 2.3098958,
+ 37.5,
+ 1350.008
+ ],
+ [
+ "2026-11-09T16:38:40Z",
+ 48.8652388,
+ 2.3104521,
+ 37.3,
+ 1404.004
+ ],
+ [
+ "2026-11-09T16:39:00Z",
+ 48.8655746,
+ 2.3109853,
+ 37.1,
+ 1457.999
+ ],
+ [
+ "2026-11-09T16:39:20Z",
+ 48.8659267,
+ 2.3114938,
+ 36.9,
+ 1512.002
+ ],
+ [
+ "2026-11-09T16:39:40Z",
+ 48.8662946,
+ 2.3119758,
+ 36.7,
+ 1566.007
+ ],
+ [
+ "2026-11-09T16:40:00Z",
+ 48.8666773,
+ 2.3124302,
+ 36.5,
+ 1620.004
+ ],
+ [
+ "2026-11-09T16:40:20Z",
+ 48.867074,
+ 2.3128561,
+ 36.2,
+ 1674.006
+ ],
+ [
+ "2026-11-09T16:40:40Z",
+ 48.8674834,
+ 2.3132533,
+ 36.0,
+ 1728.01
+ ],
+ [
+ "2026-11-09T16:41:00Z",
+ 48.8679042,
+ 2.3136218,
+ 35.7,
+ 1782.009
+ ],
+ [
+ "2026-11-09T16:41:20Z",
+ 48.8683351,
+ 2.3139623,
+ 35.4,
+ 1836.009
+ ],
+ [
+ "2026-11-09T16:41:40Z",
+ 48.8687747,
+ 2.314276,
+ 35.2,
+ 1890.008
+ ],
+ [
+ "2026-11-09T16:42:00Z",
+ 48.8692217,
+ 2.3145646,
+ 34.9,
+ 1944.009
+ ],
+ [
+ "2026-11-09T16:42:20Z",
+ 48.8696748,
+ 2.3148301,
+ 34.6,
+ 1998.004
+ ],
+ [
+ "2026-11-09T16:42:40Z",
+ 48.8701329,
+ 2.3150752,
+ 34.3,
+ 2052.005
+ ],
+ [
+ "2026-11-09T16:43:00Z",
+ 48.8705949,
+ 2.3153027,
+ 34.1,
+ 2106.005
+ ],
+ [
+ "2026-11-09T16:43:20Z",
+ 48.8710599,
+ 2.3155157,
+ 33.8,
+ 2160.006
+ ],
+ [
+ "2026-11-09T16:43:40Z",
+ 48.871527,
+ 2.3157179,
+ 33.6,
+ 2214.01
+ ],
+ [
+ "2026-11-09T16:44:00Z",
+ 48.8719954,
+ 2.3159128,
+ 33.3,
+ 2268.009
+ ],
+ [
+ "2026-11-09T16:44:20Z",
+ 48.8724644,
+ 2.3161042,
+ 33.1,
+ 2322.006
+ ],
+ [
+ "2026-11-09T16:44:40Z",
+ 48.8729334,
+ 2.316296,
+ 32.9,
+ 2376.01
+ ],
+ [
+ "2026-11-09T16:45:00Z",
+ 48.8734016,
+ 2.3164919,
+ 32.7,
+ 2430.007
+ ],
+ [
+ "2026-11-09T16:45:20Z",
+ 48.8738683,
+ 2.3166958,
+ 32.6,
+ 2484.002
+ ],
+ [
+ "2026-11-09T16:45:40Z",
+ 48.8743329,
+ 2.3169112,
+ 32.4,
+ 2538.012
+ ],
+ [
+ "2026-11-09T16:46:00Z",
+ 48.8747943,
+ 2.3171416,
+ 32.3,
+ 2592.013
+ ],
+ [
+ "2026-11-09T16:46:20Z",
+ 48.8752516,
+ 2.3173901,
+ 32.2,
+ 2646.013
+ ],
+ [
+ "2026-11-09T16:46:40Z",
+ 48.8757037,
+ 2.3176595,
+ 32.1,
+ 2700.007
+ ],
+ [
+ "2026-11-09T16:47:00Z",
+ 48.8761495,
+ 2.3179525,
+ 32.0,
+ 2754.011
+ ],
+ [
+ "2026-11-09T16:47:20Z",
+ 48.8765876,
+ 2.3182709,
+ 32.0,
+ 2808.004
+ ],
+ [
+ "2026-11-09T16:47:40Z",
+ 48.8770168,
+ 2.3186164,
+ 32.0,
+ 2862.005
+ ],
+ [
+ "2026-11-09T16:48:00Z",
+ 48.8774357,
+ 2.31899,
+ 32.0,
+ 2916.006
+ ],
+ [
+ "2026-11-09T16:48:20Z",
+ 48.8778429,
+ 2.3193923,
+ 32.1,
+ 2970.003
+ ],
+ [
+ "2026-11-09T16:48:40Z",
+ 48.8782372,
+ 2.3198234,
+ 32.1,
+ 3024.005
+ ],
+ [
+ "2026-11-09T16:49:00Z",
+ 48.8786174,
+ 2.3202829,
+ 32.2,
+ 3078.009
+ ],
+ [
+ "2026-11-09T16:49:20Z",
+ 48.8789825,
+ 2.3207698,
+ 32.3,
+ 3132.008
+ ],
+ [
+ "2026-11-09T16:49:40Z",
+ 48.8793318,
+ 2.3212829,
+ 32.5,
+ 3186.012
+ ],
+ [
+ "2026-11-09T16:50:00Z",
+ 48.8796647,
+ 2.3218205,
+ 32.6,
+ 3240.01
+ ],
+ [
+ "2026-11-09T16:50:20Z",
+ 48.879981,
+ 2.3223808,
+ 32.8,
+ 3294.008
+ ],
+ [
+ "2026-11-09T16:50:40Z",
+ 48.8802809,
+ 2.3229617,
+ 33.0,
+ 3348.013
+ ],
+ [
+ "2026-11-09T16:51:00Z",
+ 48.8805647,
+ 2.3235609,
+ 33.2,
+ 3402.011
+ ],
+ [
+ "2026-11-09T16:51:20Z",
+ 48.8808333,
+ 2.3241761,
+ 33.4,
+ 3456.009
+ ],
+ [
+ "2026-11-09T16:51:40Z",
+ 48.8810877,
+ 2.3248052,
+ 33.7,
+ 3510.013
+ ],
+ [
+ "2026-11-09T16:52:00Z",
+ 48.8813292,
+ 2.3254458,
+ 33.9,
+ 3564.008
+ ],
+ [
+ "2026-11-09T16:52:20Z",
+ 48.8815595,
+ 2.326096,
+ 34.2,
+ 3618.011
+ ],
+ [
+ "2026-11-09T16:52:40Z",
+ 48.8817805,
+ 2.3267536,
+ 34.4,
+ 3672.013
+ ],
+ [
+ "2026-11-09T16:53:00Z",
+ 48.8819942,
+ 2.3274167,
+ 34.7,
+ 3726.01
+ ],
+ [
+ "2026-11-09T16:53:20Z",
+ 48.8822028,
+ 2.3280836,
+ 35.0,
+ 3780.011
+ ],
+ [
+ "2026-11-09T16:53:40Z",
+ 48.8824086,
+ 2.3287525,
+ 35.2,
+ 3834.012
+ ],
+ [
+ "2026-11-09T16:54:00Z",
+ 48.8826139,
+ 2.3294217,
+ 35.5,
+ 3888.008
+ ],
+ [
+ "2026-11-09T16:54:20Z",
+ 48.8828212,
+ 2.3300896,
+ 35.8,
+ 3942.013
+ ],
+ [
+ "2026-11-09T16:54:40Z",
+ 48.8830327,
+ 2.3307543,
+ 36.0,
+ 3996.008
+ ],
+ [
+ "2026-11-09T16:55:00Z",
+ 48.8832508,
+ 2.3314142,
+ 36.3,
+ 4050.012
+ ],
+ [
+ "2026-11-09T16:55:20Z",
+ 48.8834774,
+ 2.3320674,
+ 36.5,
+ 4104.014
+ ],
+ [
+ "2026-11-09T16:55:40Z",
+ 48.8837145,
+ 2.3327118,
+ 36.8,
+ 4158.007
+ ],
+ [
+ "2026-11-09T16:56:00Z",
+ 48.883964,
+ 2.3333455,
+ 37.0,
+ 4212.014
+ ]
+ ],
+ "expect": {
+ "status": "verified",
+ "distance_km": 4.2,
+ "minutes": 26
+ }
+ },
+ "walking": {
+ "file": "RunnerUp_2026-11-09-09-15-00_Walking.tcx",
+ "sport": "Walking",
+ "started_at": "2026-11-09T08:15:00Z",
+ "utc_offset_minutes": 60,
+ "total_seconds": 1665,
+ "distance_m": 2331.004,
+ "points": [
+ [
+ "2026-11-09T08:15:00Z",
+ 48.8584,
+ 2.2945,
+ 35.0,
+ 0.0
+ ],
+ [
+ "2026-11-09T08:15:15Z",
+ 48.8585447,
+ 2.2946845,
+ 35.0,
+ 21.002
+ ],
+ [
+ "2026-11-09T08:15:30Z",
+ 48.8586833,
+ 2.2948794,
+ 35.3,
+ 41.997
+ ],
+ [
+ "2026-11-09T08:15:45Z",
+ 48.8588157,
+ 2.2950842,
+ 35.5,
+ 63.003
+ ],
+ [
+ "2026-11-09T08:16:00Z",
+ 48.8589416,
+ 2.2952981,
+ 35.8,
+ 83.999
+ ],
+ [
+ "2026-11-09T08:16:15Z",
+ 48.8590611,
+ 2.2955204,
+ 36.1,
+ 105.0
+ ],
+ [
+ "2026-11-09T08:16:30Z",
+ 48.8591742,
+ 2.2957502,
+ 36.3,
+ 125.995
+ ],
+ [
+ "2026-11-09T08:16:45Z",
+ 48.8592813,
+ 2.2959867,
+ 36.6,
+ 146.999
+ ],
+ [
+ "2026-11-09T08:17:00Z",
+ 48.8593827,
+ 2.2962289,
+ 36.8,
+ 168.001
+ ],
+ [
+ "2026-11-09T08:17:15Z",
+ 48.8594788,
+ 2.296476,
+ 37.0,
+ 189.0
+ ],
+ [
+ "2026-11-09T08:17:30Z",
+ 48.8595702,
+ 2.2967272,
+ 37.2,
+ 210.0
+ ],
+ [
+ "2026-11-09T08:17:45Z",
+ 48.8596577,
+ 2.2969816,
+ 37.4,
+ 231.001
+ ],
+ [
+ "2026-11-09T08:18:00Z",
+ 48.859742,
+ 2.2972385,
+ 37.5,
+ 252.002
+ ],
+ [
+ "2026-11-09T08:18:15Z",
+ 48.8598239,
+ 2.2974971,
+ 37.7,
+ 272.998
+ ],
+ [
+ "2026-11-09T08:18:30Z",
+ 48.8599043,
+ 2.2977569,
+ 37.8,
+ 294.002
+ ],
+ [
+ "2026-11-09T08:18:45Z",
+ 48.8599841,
+ 2.298017,
+ 37.9,
+ 314.997
+ ],
+ [
+ "2026-11-09T08:19:00Z",
+ 48.8600643,
+ 2.298277,
+ 37.9,
+ 336.004
+ ],
+ [
+ "2026-11-09T08:19:15Z",
+ 48.8601457,
+ 2.298536,
+ 38.0,
+ 357.002
+ ],
+ [
+ "2026-11-09T08:19:30Z",
+ 48.8602292,
+ 2.2987934,
+ 38.0,
+ 377.997
+ ],
+ [
+ "2026-11-09T08:19:45Z",
+ 48.8603158,
+ 2.2990486,
+ 38.0,
+ 399.003
+ ],
+ [
+ "2026-11-09T08:20:00Z",
+ 48.8604061,
+ 2.2993007,
+ 38.0,
+ 420.002
+ ],
+ [
+ "2026-11-09T08:20:15Z",
+ 48.8605008,
+ 2.299549,
+ 37.9,
+ 440.998
+ ],
+ [
+ "2026-11-09T08:20:30Z",
+ 48.8606007,
+ 2.2997927,
+ 37.8,
+ 462.003
+ ],
+ [
+ "2026-11-09T08:20:45Z",
+ 48.8607062,
+ 2.3000308,
+ 37.7,
+ 483.003
+ ],
+ [
+ "2026-11-09T08:21:00Z",
+ 48.8608176,
+ 2.3002625,
+ 37.6,
+ 503.997
+ ],
+ [
+ "2026-11-09T08:21:15Z",
+ 48.8609354,
+ 2.300487,
+ 37.5,
+ 525.004
+ ],
+ [
+ "2026-11-09T08:21:30Z",
+ 48.8610595,
+ 2.3007033,
+ 37.3,
+ 545.999
+ ],
+ [
+ "2026-11-09T08:21:45Z",
+ 48.8611901,
+ 2.3009107,
+ 37.1,
+ 567.001
+ ],
+ [
+ "2026-11-09T08:22:00Z",
+ 48.8613271,
+ 2.3011084,
+ 36.9,
+ 588.006
+ ],
+ [
+ "2026-11-09T08:22:15Z",
+ 48.8614701,
+ 2.3012958,
+ 36.7,
+ 609.001
+ ],
+ [
+ "2026-11-09T08:22:30Z",
+ 48.861619,
+ 2.3014725,
+ 36.5,
+ 630.006
+ ],
+ [
+ "2026-11-09T08:22:45Z",
+ 48.8617732,
+ 2.3016381,
+ 36.2,
+ 651.0
+ ],
+ [
+ "2026-11-09T08:23:00Z",
+ 48.8619324,
+ 2.3017925,
+ 36.0,
+ 671.999
+ ],
+ [
+ "2026-11-09T08:23:15Z",
+ 48.8620961,
+ 2.3019358,
+ 35.7,
+ 693.004
+ ],
+ [
+ "2026-11-09T08:23:30Z",
+ 48.8622636,
+ 2.3020682,
+ 35.4,
+ 713.997
+ ],
+ [
+ "2026-11-09T08:23:45Z",
+ 48.8624346,
+ 2.3021902,
+ 35.2,
+ 735.002
+ ],
+ [
+ "2026-11-09T08:24:00Z",
+ 48.8626084,
+ 2.3023024,
+ 34.9,
+ 755.998
+ ],
+ [
+ "2026-11-09T08:24:15Z",
+ 48.8627846,
+ 2.3024057,
+ 34.6,
+ 776.997
+ ],
+ [
+ "2026-11-09T08:24:30Z",
+ 48.8629628,
+ 2.302501,
+ 34.3,
+ 798.003
+ ],
+ [
+ "2026-11-09T08:24:45Z",
+ 48.8631425,
+ 2.3025894,
+ 34.1,
+ 819.005
+ ],
+ [
+ "2026-11-09T08:25:00Z",
+ 48.8633233,
+ 2.3026723,
+ 33.8,
+ 840.004
+ ],
+ [
+ "2026-11-09T08:25:15Z",
+ 48.8635049,
+ 2.3027509,
+ 33.6,
+ 860.999
+ ],
+ [
+ "2026-11-09T08:25:30Z",
+ 48.8636871,
+ 2.3028267,
+ 33.3,
+ 882.004
+ ],
+ [
+ "2026-11-09T08:25:45Z",
+ 48.8638695,
+ 2.3029011,
+ 33.1,
+ 903.004
+ ],
+ [
+ "2026-11-09T08:26:00Z",
+ 48.8640519,
+ 2.3029757,
+ 32.9,
+ 924.007
+ ],
+ [
+ "2026-11-09T08:26:15Z",
+ 48.864234,
+ 2.3030518,
+ 32.7,
+ 945.007
+ ],
+ [
+ "2026-11-09T08:26:30Z",
+ 48.8644155,
+ 2.3031311,
+ 32.6,
+ 966.006
+ ],
+ [
+ "2026-11-09T08:26:45Z",
+ 48.8645961,
+ 2.3032149,
+ 32.4,
+ 987.002
+ ],
+ [
+ "2026-11-09T08:27:00Z",
+ 48.8647755,
+ 2.3033044,
+ 32.3,
+ 1007.997
+ ],
+ [
+ "2026-11-09T08:27:15Z",
+ 48.8649534,
+ 2.303401,
+ 32.2,
+ 1029.003
+ ],
+ [
+ "2026-11-09T08:27:30Z",
+ 48.8651292,
+ 2.3035058,
+ 32.1,
+ 1050.001
+ ],
+ [
+ "2026-11-09T08:27:45Z",
+ 48.8653026,
+ 2.3036197,
+ 32.0,
+ 1071.005
+ ],
+ [
+ "2026-11-09T08:28:00Z",
+ 48.865473,
+ 2.3037435,
+ 32.0,
+ 1092.005
+ ],
+ [
+ "2026-11-09T08:28:15Z",
+ 48.8656399,
+ 2.3038778,
+ 32.0,
+ 1113.004
+ ],
+ [
+ "2026-11-09T08:28:30Z",
+ 48.8658028,
+ 2.3040231,
+ 32.0,
+ 1134.005
+ ],
+ [
+ "2026-11-09T08:28:45Z",
+ 48.8659611,
+ 2.3041795,
+ 32.1,
+ 1154.998
+ ],
+ [
+ "2026-11-09T08:29:00Z",
+ 48.8661145,
+ 2.3043471,
+ 32.1,
+ 1176.004
+ ],
+ [
+ "2026-11-09T08:29:15Z",
+ 48.8662623,
+ 2.3045258,
+ 32.2,
+ 1197.003
+ ],
+ [
+ "2026-11-09T08:29:30Z",
+ 48.8664043,
+ 2.3047151,
+ 32.3,
+ 1218.004
+ ],
+ [
+ "2026-11-09T08:29:45Z",
+ 48.8665401,
+ 2.3049146,
+ 32.5,
+ 1239.003
+ ],
+ [
+ "2026-11-09T08:30:00Z",
+ 48.8666696,
+ 2.3051236,
+ 32.6,
+ 1260.005
+ ],
+ [
+ "2026-11-09T08:30:15Z",
+ 48.8667926,
+ 2.3053414,
+ 32.8,
+ 1281.001
+ ],
+ [
+ "2026-11-09T08:30:30Z",
+ 48.8667926,
+ 2.3053414,
+ 33.0,
+ 1281.001
+ ],
+ [
+ "2026-11-09T08:30:45Z",
+ 48.8667926,
+ 2.3053414,
+ 33.2,
+ 1281.001
+ ],
+ [
+ "2026-11-09T08:31:30Z",
+ 48.8671059,
+ 2.306059,
+ 33.4,
+ 1281.001
+ ],
+ [
+ "2026-11-09T08:31:45Z",
+ 48.8672049,
+ 2.3063036,
+ 33.7,
+ 1302.008
+ ],
+ [
+ "2026-11-09T08:32:00Z",
+ 48.8672988,
+ 2.3065527,
+ 33.9,
+ 1323.008
+ ],
+ [
+ "2026-11-09T08:32:15Z",
+ 48.8673884,
+ 2.3068054,
+ 34.2,
+ 1344.006
+ ],
+ [
+ "2026-11-09T08:32:30Z",
+ 48.8674743,
+ 2.3070611,
+ 34.4,
+ 1365.007
+ ],
+ [
+ "2026-11-09T08:32:45Z",
+ 48.8675574,
+ 2.3073189,
+ 34.7,
+ 1386.006
+ ],
+ [
+ "2026-11-09T08:33:00Z",
+ 48.8676385,
+ 2.3075782,
+ 35.0,
+ 1407.007
+ ],
+ [
+ "2026-11-09T08:33:15Z",
+ 48.8677186,
+ 2.3078382,
+ 35.2,
+ 1428.006
+ ],
+ [
+ "2026-11-09T08:33:30Z",
+ 48.8677984,
+ 2.3080984,
+ 35.5,
+ 1449.005
+ ],
+ [
+ "2026-11-09T08:33:45Z",
+ 48.867879,
+ 2.308358,
+ 35.8,
+ 1470.002
+ ],
+ [
+ "2026-11-09T08:34:00Z",
+ 48.8679613,
+ 2.3086165,
+ 36.0,
+ 1491.008
+ ],
+ [
+ "2026-11-09T08:34:15Z",
+ 48.8680461,
+ 2.308873,
+ 36.3,
+ 1512.006
+ ],
+ [
+ "2026-11-09T08:34:30Z",
+ 48.8681342,
+ 2.309127,
+ 36.5,
+ 1533.009
+ ],
+ [
+ "2026-11-09T08:34:45Z",
+ 48.8682264,
+ 2.3093775,
+ 36.8,
+ 1554.005
+ ],
+ [
+ "2026-11-09T08:35:00Z",
+ 48.8683234,
+ 2.3096238,
+ 37.0,
+ 1575.002
+ ],
+ [
+ "2026-11-09T08:35:15Z",
+ 48.8684258,
+ 2.3098651,
+ 37.2,
+ 1596.005
+ ],
+ [
+ "2026-11-09T08:35:30Z",
+ 48.8685339,
+ 2.3101005,
+ 37.3,
+ 1617.004
+ ],
+ [
+ "2026-11-09T08:35:45Z",
+ 48.8686482,
+ 2.3103291,
+ 37.5,
+ 1638.007
+ ],
+ [
+ "2026-11-09T08:36:00Z",
+ 48.8687688,
+ 2.31055,
+ 37.6,
+ 1659.004
+ ],
+ [
+ "2026-11-09T08:36:15Z",
+ 48.8688959,
+ 2.3107624,
+ 37.8,
+ 1680.006
+ ],
+ [
+ "2026-11-09T08:36:30Z",
+ 48.8690294,
+ 2.3109656,
+ 37.9,
+ 1701.012
+ ],
+ [
+ "2026-11-09T08:36:45Z",
+ 48.8691691,
+ 2.3111587,
+ 37.9,
+ 1722.007
+ ],
+ [
+ "2026-11-09T08:37:00Z",
+ 48.8693148,
+ 2.3113414,
+ 38.0,
+ 1743.008
+ ],
+ [
+ "2026-11-09T08:37:15Z",
+ 48.8694661,
+ 2.3115131,
+ 38.0,
+ 1764.002
+ ],
+ [
+ "2026-11-09T08:37:30Z",
+ 48.8696227,
+ 2.3116737,
+ 38.0,
+ 1785.007
+ ],
+ [
+ "2026-11-09T08:37:45Z",
+ 48.8697839,
+ 2.3118231,
+ 38.0,
+ 1806.0
+ ],
+ [
+ "2026-11-09T08:38:00Z",
+ 48.8699494,
+ 2.3119615,
+ 37.9,
+ 1827.003
+ ],
+ [
+ "2026-11-09T08:38:15Z",
+ 48.8701186,
+ 2.3120892,
+ 37.8,
+ 1848.008
+ ],
+ [
+ "2026-11-09T08:38:30Z",
+ 48.8702909,
+ 2.3122067,
+ 37.7,
+ 1869.006
+ ],
+ [
+ "2026-11-09T08:38:45Z",
+ 48.8704659,
+ 2.3123147,
+ 37.6,
+ 1890.007
+ ],
+ [
+ "2026-11-09T08:39:00Z",
+ 48.870643,
+ 2.3124143,
+ 37.5,
+ 1911.004
+ ],
+ [
+ "2026-11-09T08:39:15Z",
+ 48.8708219,
+ 2.3125063,
+ 37.3,
+ 1932.004
+ ],
+ [
+ "2026-11-09T08:39:30Z",
+ 48.8710021,
+ 2.3125921,
+ 37.1,
+ 1953.001
+ ],
+ [
+ "2026-11-09T08:39:45Z",
+ 48.8711834,
+ 2.3126729,
+ 36.9,
+ 1974.009
+ ],
+ [
+ "2026-11-09T08:40:00Z",
+ 48.8713653,
+ 2.3127501,
+ 36.7,
+ 1995.009
+ ],
+ [
+ "2026-11-09T08:40:15Z",
+ 48.8715476,
+ 2.3128251,
+ 36.5,
+ 2016.009
+ ],
+ [
+ "2026-11-09T08:40:30Z",
+ 48.87173,
+ 2.3128994,
+ 36.2,
+ 2037.006
+ ],
+ [
+ "2026-11-09T08:40:45Z",
+ 48.8719123,
+ 2.3129745,
+ 36.0,
+ 2058.008
+ ],
+ [
+ "2026-11-09T08:41:00Z",
+ 48.8720942,
+ 2.3130519,
+ 35.7,
+ 2079.012
+ ],
+ [
+ "2026-11-09T08:41:15Z",
+ 48.8722753,
+ 2.313133,
+ 35.5,
+ 2100.005
+ ],
+ [
+ "2026-11-09T08:41:30Z",
+ 48.8724555,
+ 2.3132193,
+ 35.2,
+ 2121.012
+ ],
+ [
+ "2026-11-09T08:41:45Z",
+ 48.8726342,
+ 2.3133119,
+ 34.9,
+ 2142.005
+ ],
+ [
+ "2026-11-09T08:42:00Z",
+ 48.8728112,
+ 2.3134121,
+ 34.6,
+ 2163.007
+ ],
+ [
+ "2026-11-09T08:42:15Z",
+ 48.872986,
+ 2.3135209,
+ 34.4,
+ 2184.01
+ ],
+ [
+ "2026-11-09T08:42:30Z",
+ 48.8731581,
+ 2.3136392,
+ 34.1,
+ 2205.011
+ ],
+ [
+ "2026-11-09T08:42:45Z",
+ 48.873327,
+ 2.3137677,
+ 33.8,
+ 2226.012
+ ],
+ [
+ "2026-11-09T08:43:00Z",
+ 48.8734921,
+ 2.3139069,
+ 33.6,
+ 2247.005
+ ],
+ [
+ "2026-11-09T08:43:15Z",
+ 48.873653,
+ 2.3140572,
+ 33.4,
+ 2268.003
+ ],
+ [
+ "2026-11-09T08:43:30Z",
+ 48.8738092,
+ 2.3142187,
+ 33.1,
+ 2289.007
+ ],
+ [
+ "2026-11-09T08:43:45Z",
+ 48.8739601,
+ 2.3143914,
+ 32.9,
+ 2310.009
+ ],
+ [
+ "2026-11-09T08:44:00Z",
+ 48.8741053,
+ 2.3145749,
+ 32.8,
+ 2331.004
+ ]
+ ],
+ "expect": {
+ "status": "verified",
+ "legs": 1,
+ "distance_km": 2.3,
+ "moving_minutes": 28
+ }
+ }
+}
\ No newline at end of file
diff --git a/screen_locker/tests/test_run_tcx_golden.py b/screen_locker/tests/test_run_tcx_golden.py
new file mode 100644
index 0000000..eb12699
--- /dev/null
+++ b/screen_locker/tests/test_run_tcx_golden.py
@@ -0,0 +1,169 @@
+"""Cross-language golden test: the app's TCX is accepted by the real verifier.
+
+The workout app (Dart, ``lib/services/tcx_codec.dart``) writes the TCX files
+the PC credits. Each language's own round trip passes happily while the other
+side's parser disagrees, so the Dart twin
+(``stronglift_replacement/workout_app/test/services/tcx_golden_fixture_test.dart``)
+encodes ``fixtures/run_tcx/golden_tracks.json`` and compares the result byte
+for byte with the ``.tcx`` files checked in next to it. This test feeds those
+SAME files to the real ``RunnerUpVerificationMixin`` -- date glob, parser,
+run validation and walk summing -- and asserts the km / minutes the JSON
+promises. Everything runs on a temporary drop directory: nothing here reads
+or writes ``~/data/cloud/RunnerUp``.
+"""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+import shutil
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from screen_locker._runnerup_verification import RunnerUpVerificationMixin
+
+FIXTURES = Path(__file__).parent / "fixtures" / "run_tcx"
+SAMPLE = "RunnerUp_2026-08-12-17-42-11_Running.tcx"
+# ``_TCX_SPORT_TO_INT`` code for "Running" in screen_locker/_runnerup_tcx.py.
+SPORT_RUNNING = 0
+
+
+def _golden() -> dict[str, dict[str, object]]:
+ return json.loads((FIXTURES / "golden_tracks.json").read_text(encoding="utf-8"))
+
+
+class Verifier(RunnerUpVerificationMixin):
+ """The real mixin with no phone: only the WebDAV drop directory is read."""
+
+ def __init__(self) -> None:
+ self.adb = MagicMock(return_value=(False, ""))
+
+ def _has_adb_device(self) -> bool:
+ return False
+
+ def _run_adb(self, args: list[str]) -> tuple[bool, str]:
+ return self.adb(args)
+
+
+@pytest.fixture(name="inbox")
+def inbox_fixture(tmp_path: Path) -> Path:
+ """A temporary WebDAV inbox standing in for ``~/data/cloud/RunnerUp``."""
+ inbox = tmp_path / "cloud" / "RunnerUp"
+ inbox.mkdir(parents=True)
+ return inbox
+
+
+def _verify(inbox: Path, today: str) -> tuple[str, str] | None:
+ """Run the real file-based verification on ``inbox`` as if it were ``today``."""
+ with (
+ patch("screen_locker._runnerup_verification.RUNNERUP_WEBDAV_DIRS", (inbox,)),
+ patch("screen_locker._runnerup_verification.today_str", return_value=today),
+ ):
+ return Verifier()._verify_runnerup_via_files()
+
+
+def _drop(inbox: Path, name: str) -> None:
+ shutil.copyfile(FIXTURES / name, inbox / name)
+
+
+class TestGoldenRun:
+ """The app's run file passes the PC's run validation unchanged."""
+
+ def test_run_is_verified_with_the_promised_km_and_minutes(
+ self, inbox: Path
+ ) -> None:
+ """Date glob, parser and validation accept the app's run file."""
+ spec = _golden()["running"]
+ _drop(inbox, str(spec["file"]))
+ expect = spec["expect"]
+ assert isinstance(expect, dict)
+ result = _verify(inbox, "2026-11-09")
+ assert result == (
+ expect["status"],
+ f"Running: {expect['distance_km']} km in {expect['minutes']} min",
+ )
+
+ def test_parsed_fields_match_the_json(self, inbox: Path) -> None:
+ """The parser reads sport, duration and distance as the app wrote them."""
+ spec = _golden()["running"]
+ _drop(inbox, str(spec["file"]))
+ data = Verifier()._pull_and_parse_tcx(str(inbox / str(spec["file"])))
+ assert data is not None
+ assert data["sport"] == SPORT_RUNNING
+ assert data["walking"] is False
+ assert data["duration_seconds"] == spec["total_seconds"]
+ assert data["distance_m"] == pytest.approx(float(str(spec["distance_m"])))
+ assert data["activity_id"] == str(spec["started_at"])
+
+ def test_the_local_date_in_the_name_is_what_is_globbed(self, inbox: Path) -> None:
+ """Another day does not see the file: the stamp is local, the glob is too."""
+ spec = _golden()["running"]
+ _drop(inbox, str(spec["file"]))
+ assert _verify(inbox, "2026-11-10") is None
+
+
+class TestGoldenWalk:
+ """The app's walk file is a walk, and its moving time is what the PC sums."""
+
+ def test_walk_is_verified_with_the_promised_moving_minutes(
+ self, inbox: Path
+ ) -> None:
+ """A walk is summed per day: legs, km and moving minutes as promised."""
+ spec = _golden()["walking"]
+ _drop(inbox, str(spec["file"]))
+ expect = spec["expect"]
+ assert isinstance(expect, dict)
+ result = _verify(inbox, "2026-11-09")
+ assert result == (
+ expect["status"],
+ (
+ f"Walking: {expect['legs']} walk(s), {expect['distance_km']} km, "
+ f"{expect['moving_minutes']} min moving"
+ ),
+ )
+
+ def test_pc_moving_time_equals_the_apps_total_seconds(self, inbox: Path) -> None:
+ """Dart's moving-time rule and the PC's ``track_summary`` agree exactly."""
+ spec = _golden()["walking"]
+ _drop(inbox, str(spec["file"]))
+ data = Verifier()._pull_and_parse_tcx(str(inbox / str(spec["file"])))
+ assert data is not None
+ assert data["walking"] is True
+ assert data["moving_seconds"] == pytest.approx(
+ float(str(spec["total_seconds"]))
+ )
+ assert data["activity_id"] == str(spec["started_at"])
+
+ def test_walk_is_flagged_by_name_even_if_sport_is_not_walking(
+ self, inbox: Path
+ ) -> None:
+ """The app also writes ``Sport="Walking"``; either signal alone suffices."""
+ spec = _golden()["walking"]
+ name = str(spec["file"])
+ body = (FIXTURES / name).read_text(encoding="utf-8")
+ assert 'Sport="Walking"' in body
+ renamed = inbox / "activity.tcx"
+ renamed.write_text(body.replace('Sport="Walking"', 'Sport="Other"'))
+ data = Verifier()._pull_and_parse_tcx(str(renamed))
+ assert data is not None
+ assert data["walking"] is False
+ (inbox / name).write_text(body.replace('Sport="Walking"', 'Sport="Other"'))
+ by_name = Verifier()._pull_and_parse_tcx(str(inbox / name))
+ assert by_name is not None
+ assert by_name["walking"] is True
+
+
+class TestRunnerUpSample:
+ """The trimmed real RunnerUp export parses to the numbers Dart's decoder sees."""
+
+ def test_parser_sums_both_laps(self, inbox: Path) -> None:
+ """Same literals as the Dart decoder test: 624 + 626 s, two laps' metres."""
+ _drop(inbox, SAMPLE)
+ data = Verifier()._pull_and_parse_tcx(str(inbox / SAMPLE))
+ assert data is not None
+ assert data["sport"] == SPORT_RUNNING
+ assert data["walking"] is False
+ assert data["duration_seconds"] == 1250
+ assert data["distance_m"] == pytest.approx(983.6306 + 980.7304, abs=1e-3)
+ assert data["activity_id"] == "2026-08-12T15:42:11Z"
diff --git a/stronglift_replacement/workout_app/DOCS-workout-app-context.md b/stronglift_replacement/workout_app/DOCS-workout-app-context.md
index ec52cd6..3c893ae 100644
--- a/stronglift_replacement/workout_app/DOCS-workout-app-context.md
+++ b/stronglift_replacement/workout_app/DOCS-workout-app-context.md
@@ -14,3 +14,18 @@ Implementing design_v2.md improvements.
workout and shows the status-bar notification; notification presses go
through a durable prefs queue, never straight into sqflite (the CRDT
sync layer owns that database's ordering)
+
+## GPS runs (foundation, 2026-11-10)
+- The app records runs/walks itself and uploads a TCX the PC credits unchanged.
+ Contract = the PC parser, pinned by `screen_locker/tests/fixtures/run_tcx/`
+ (Dart `test/services/tcx_golden_fixture_test.dart` <-> pytest
+ `screen_locker/tests/test_run_tcx_golden.py`). Regenerate after a deliberate
+ codec change: `UPDATE_GOLDEN=1 flutter test test/services/tcx_golden_fixture_test.dart`.
+- File name `RunnerUp_YYYY-MM-DD-HH-MM-SS_.tcx`, stamp in
+ LOCAL time; ``/`