Detect a LiveTrack session ending the moment Garmin confirms it, not 30min later
CI / check (push) Successful in 18s
CI / deploy (push) Successful in 12s

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:
randy committed 2026-09-20 16:55:57 +02:00
1 parent 3106f8c103
commit b0c93d10d7
2 files changed
+41 -13

No files matched your search

+25 -13
View File
@@ -56,16 +56,25 @@ module Maptool
# doesn't set `end` until the activity actually stops — disproven
# against a real still-recording session (2026-09-20): `end` was
# 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`
# instead reflects its real end — ~2h after start for the 2026-09-06
# session this was originally, and only, verified against). Recency of
# the last track point is a directly-observable signal instead of
# trying to reverse-engineer what a given `end` value means: Garmin
# posts one roughly every `postTrackPointFrequency` seconds (~15,
# observed) while genuinely still recording, so a point older than
# ACTIVE_WITHIN means the activity has actually stopped (or, at worst,
# lost signal for a while — same "looks ended until it isn't" behavior
# Garmin's own LiveTrack page shows in that case).
# expiry, not an actual end time. Two signals now, both verified live
# against that same session:
#
# * `end` itself, once it's actually in the past — watched the same
# session update it from that 24h placeholder to within a second of
# the last point's own time, immediately after the activity was
# saved on the watch. Authoritative and near-instant when present,
# so this is checked first.
# * 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).
#
# 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
def self.summarize(result)
@@ -76,16 +85,19 @@ module Maptool
"activity_type" => last["activityType"],
"distance_meters" => last["totalDistanceMeters"],
"duration_secs" => last["totalDurationSecs"],
"active" => active?(last["dateTime"]),
"active" => active?(last["dateTime"], session["end"]),
"start" => session["start"],
"end" => session["end"],
}
end
def self.active?(last_point_time)
def self.active?(last_point_time, session_end)
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
false
end