Detect a LiveTrack session ending the moment Garmin confirms it, not 30min later
Watched the session from the last two commits actually get saved: Garmin updated its `end` field from the future 24h placeholder to within a second of the last point's own time, immediately — much faster than the recency-of-last-point fallback alone would have noticed (that only flips after ACTIVE_WITHIN with no new points, so a just-saved session with a last point seconds old would still read as active for up to 30 more minutes). Now checks end-already-in-the-past first (authoritative and near instant when Garmin has set it), falling back to point recency only when end still shows the future placeholder — the case an abandoned, never-saved session never updates.
This commit is contained in:
1 parent
3106f8c103
commit
b0c93d10d7
2 files changed
+40
-12
No files matched your search
@@ -56,16 +56,25 @@ module Maptool
|
|||||||
# doesn't set `end` until the activity actually stops — disproven
|
# doesn't set `end` until the activity actually stops — disproven
|
||||||
# against a real still-recording session (2026-09-20): `end` was
|
# against a real still-recording session (2026-09-20): `end` was
|
||||||
# already non-nil, set to exactly `start + 24h`, the share link's own
|
# already non-nil, set to exactly `start + 24h`, the share link's own
|
||||||
# expiry, not an actual end time (a genuinely ended session's `end`
|
# expiry, not an actual end time. Two signals now, both verified live
|
||||||
# instead reflects its real end — ~2h after start for the 2026-09-06
|
# against that same session:
|
||||||
# session this was originally, and only, verified against). Recency of
|
#
|
||||||
# the last track point is a directly-observable signal instead of
|
# * `end` itself, once it's actually in the past — watched the same
|
||||||
# trying to reverse-engineer what a given `end` value means: Garmin
|
# session update it from that 24h placeholder to within a second of
|
||||||
# posts one roughly every `postTrackPointFrequency` seconds (~15,
|
# the last point's own time, immediately after the activity was
|
||||||
# observed) while genuinely still recording, so a point older than
|
# saved on the watch. Authoritative and near-instant when present,
|
||||||
# ACTIVE_WITHIN means the activity has actually stopped (or, at worst,
|
# so this is checked first.
|
||||||
# lost signal for a while — same "looks ended until it isn't" behavior
|
# * failing that (still showing the future placeholder — an abandoned
|
||||||
|
# session never gets saved, so `end` never moves), recency of the
|
||||||
|
# last track point: Garmin posts one roughly every
|
||||||
|
# `postTrackPointFrequency` seconds (~15, observed) while genuinely
|
||||||
|
# still recording, so one older than ACTIVE_WITHIN means signal was
|
||||||
|
# lost a while ago (same "looks ended until it isn't" behavior
|
||||||
# Garmin's own LiveTrack page shows in that case).
|
# Garmin's own LiveTrack page shows in that case).
|
||||||
|
#
|
||||||
|
# Both must agree the session is live — `end` already in the past
|
||||||
|
# means stopped even if the last point is only seconds old (exactly
|
||||||
|
# the save-immediately-after-the-last-point case above).
|
||||||
ACTIVE_WITHIN = 30 * 60 # seconds
|
ACTIVE_WITHIN = 30 * 60 # seconds
|
||||||
|
|
||||||
def self.summarize(result)
|
def self.summarize(result)
|
||||||
@@ -76,16 +85,19 @@ module Maptool
|
|||||||
"activity_type" => last["activityType"],
|
"activity_type" => last["activityType"],
|
||||||
"distance_meters" => last["totalDistanceMeters"],
|
"distance_meters" => last["totalDistanceMeters"],
|
||||||
"duration_secs" => last["totalDurationSecs"],
|
"duration_secs" => last["totalDurationSecs"],
|
||||||
"active" => active?(last["dateTime"]),
|
"active" => active?(last["dateTime"], session["end"]),
|
||||||
"start" => session["start"],
|
"start" => session["start"],
|
||||||
"end" => session["end"],
|
"end" => session["end"],
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.active?(last_point_time)
|
def self.active?(last_point_time, session_end)
|
||||||
return false unless last_point_time
|
return false unless last_point_time
|
||||||
|
|
||||||
Time.parse(last_point_time) > Time.now.utc - ACTIVE_WITHIN
|
now = Time.now.utc
|
||||||
|
return false if session_end && Time.parse(session_end) <= now
|
||||||
|
|
||||||
|
Time.parse(last_point_time) > now - ACTIVE_WITHIN
|
||||||
rescue ArgumentError, TypeError
|
rescue ArgumentError, TypeError
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -71,6 +71,22 @@ class LiveTrackClientTest < Minitest::Test
|
|||||||
assert Maptool::LiveTrackClient.summarize(result)["active"]
|
assert Maptool::LiveTrackClient.summarize(result)["active"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Verified live the same day: Garmin updates a real session's `end` from
|
||||||
|
# the future placeholder to within a second of the last point's own
|
||||||
|
# time immediately once the activity is saved on the watch — so a fresh
|
||||||
|
# last point alone isn't enough, `end` already being in the past has to
|
||||||
|
# win even then.
|
||||||
|
def test_summarize_marks_inactive_once_end_is_in_the_past_even_with_a_fresh_point
|
||||||
|
payloads = @client.send(:parse_payloads, FAKE_PAGE)
|
||||||
|
points = @client.send(:clean, @client.send(:search, payloads) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) })
|
||||||
|
points[-1] = points[-1].merge("dateTime" => Time.now.utc.iso8601)
|
||||||
|
session = @client.send(:clean, @client.send(:search, payloads) { |n| n if n["sessionName"] })
|
||||||
|
.merge("end" => (Time.now.utc - 1).iso8601)
|
||||||
|
result = Maptool::LiveTrackClient::Result.new(points: points, session: session)
|
||||||
|
|
||||||
|
refute Maptool::LiveTrackClient.summarize(result)["active"]
|
||||||
|
end
|
||||||
|
|
||||||
def test_finds_the_planned_course_alongside_the_actual_track
|
def test_finds_the_planned_course_alongside_the_actual_track
|
||||||
payloads = @client.send(:parse_payloads, FAKE_PAGE)
|
payloads = @client.send(:parse_payloads, FAKE_PAGE)
|
||||||
course = @client.send(:clean, @client.send(:search, payloads) { |n| n["courses"].first if n["courses"].is_a?(Array) && !n["courses"].empty? })
|
course = @client.send(:clean, @client.send(:search, payloads) { |n| n["courses"].first if n["courses"].is_a?(Array) && !n["courses"].empty? })
|
||||||
|
|||||||
Reference in new issue
Block a user