Flag LiveTrack sessions whose map Garmin no longer serves
Ended sessions past the ~24h Garmin appears to keep a live-track page around for get verified once at app startup and, once confirmed dead, flagged so the panel greys them out instead of only failing after a click. Already-flagged sessions are skipped on later startups so this doesn't keep re-hitting Garmin for the same dead session.
This commit is contained in:
1 parent
141b7a08ce
commit
0c4ee97368
5 files changed
+98
-3
No files matched your search
@@ -325,6 +325,25 @@ module Maptool
|
|||||||
json sessions: LIVETRACK_STORE.list
|
json sessions: LIVETRACK_STORE.list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Garmin stops serving a session's live-track page some while after the
|
||||||
|
# activity ends (its own session/token URL, not the API — see
|
||||||
|
# LiveTrackClient) — the exact cutoff isn't documented, ~24h is what's
|
||||||
|
# been observed. Ended sessions old enough to likely be past that are
|
||||||
|
# verified once at startup (LiveTrackStore#stale_ended already skips
|
||||||
|
# ones already flagged, so this doesn't re-hit Garmin for the same
|
||||||
|
# dead session on every restart), so the panel can grey them out up
|
||||||
|
# front instead of only failing after a user clicks one and waits on
|
||||||
|
# a fetch.
|
||||||
|
LIVETRACK_MAP_STALE_AFTER = 24 * 60 * 60 # seconds
|
||||||
|
|
||||||
|
Thread.new do
|
||||||
|
LIVETRACK_STORE.stale_ended(LIVETRACK_MAP_STALE_AFTER).each do |session|
|
||||||
|
LiveTrackClient.fetch(session["id"], session["token"])
|
||||||
|
rescue LiveTrackClient::FetchError
|
||||||
|
LIVETRACK_STORE.mark_unavailable(session["id"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Garmin's own API (livetrack.garmin.com/api/v2/...) 403s a plain server
|
# Garmin's own API (livetrack.garmin.com/api/v2/...) 403s a plain server
|
||||||
# request no matter what headers it gets — see LiveTrackClient — so this
|
# request no matter what headers it gets — see LiveTrackClient — so this
|
||||||
# fetches the session page itself and pulls the track out of that.
|
# fetches the session page itself and pulls the track out of that.
|
||||||
|
|||||||
@@ -55,6 +55,31 @@ module Maptool
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Ended sessions (summary["active"] == false) that ended more than
|
||||||
|
# `stale_after` seconds ago and aren't already flagged unavailable —
|
||||||
|
# i.e. ones Garmin has very likely stopped serving the live-track page
|
||||||
|
# for, but that haven't been confirmed yet (see app.rb's startup
|
||||||
|
# check). Excludes already-flagged ones so that check doesn't re-hit
|
||||||
|
# Garmin, on every restart, for a session it already told us is gone.
|
||||||
|
def stale_ended(stale_after)
|
||||||
|
cutoff = Time.now.utc - stale_after
|
||||||
|
list.select { |s| stale_ended?(s, cutoff) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Flags a session's live-track page as no longer served by Garmin, once
|
||||||
|
# confirmed (see app.rb's startup check) — the summary itself (small,
|
||||||
|
# and a record of the activity, see the class comment) is left alone.
|
||||||
|
def mark_unavailable(session_id)
|
||||||
|
@mutex.synchronize do
|
||||||
|
sessions = read
|
||||||
|
session = sessions.find { |s| s["id"] == session_id }
|
||||||
|
return unless session
|
||||||
|
|
||||||
|
session["map_available"] = false
|
||||||
|
write(sessions)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Most recently received first.
|
# Most recently received first.
|
||||||
def list
|
def list
|
||||||
prune(read)
|
prune(read)
|
||||||
@@ -77,6 +102,20 @@ module Maptool
|
|||||||
Time.now.utc # malformed/missing timestamp: don't let it block pruning
|
Time.now.utc # malformed/missing timestamp: don't let it block pruning
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def stale_ended?(session, cutoff)
|
||||||
|
summary = session["summary"]
|
||||||
|
return false unless summary && summary["active"] == false && session["map_available"] != false
|
||||||
|
|
||||||
|
ended = ended_at(summary)
|
||||||
|
ended && ended < cutoff
|
||||||
|
end
|
||||||
|
|
||||||
|
def ended_at(summary)
|
||||||
|
Time.parse(summary["end"])
|
||||||
|
rescue ArgumentError, TypeError
|
||||||
|
nil # malformed/missing "end": can't tell how long it's been, so don't guess
|
||||||
|
end
|
||||||
|
|
||||||
def write(sessions)
|
def write(sessions)
|
||||||
File.write(@path, JSON.pretty_generate(sessions))
|
File.write(@path, JSON.pretty_generate(sessions))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -781,6 +781,11 @@ html, body {
|
|||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.livetrack-panel__row:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
.livetrack-panel__label {
|
.livetrack-panel__label {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -76,15 +76,18 @@ export class LiveTrackPanelControl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions.forEach(({ id, received_at, summary }) => {
|
sessions.forEach(({ id, received_at, summary, map_available }) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "livetrack-panel__item";
|
li.className = "livetrack-panel__item";
|
||||||
|
|
||||||
|
const unavailable = map_available === false;
|
||||||
|
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = "livetrack-panel__row";
|
button.className = "livetrack-panel__row";
|
||||||
button.classList.toggle("active", id === this._activeSessionId);
|
button.classList.toggle("active", id === this._activeSessionId);
|
||||||
button.title = id;
|
button.disabled = unavailable;
|
||||||
|
button.title = unavailable ? "Garmin no longer serves this session's map" : id;
|
||||||
|
|
||||||
const labelSpan = document.createElement("span");
|
const labelSpan = document.createElement("span");
|
||||||
labelSpan.className = "livetrack-panel__label";
|
labelSpan.className = "livetrack-panel__label";
|
||||||
@@ -100,7 +103,7 @@ export class LiveTrackPanelControl {
|
|||||||
} else {
|
} else {
|
||||||
button.append(labelSpan, timeSpan);
|
button.append(labelSpan, timeSpan);
|
||||||
}
|
}
|
||||||
button.addEventListener("click", () => this._toggleTrack(id, button));
|
if (!unavailable) button.addEventListener("click", () => this._toggleTrack(id, button));
|
||||||
li.appendChild(button);
|
li.appendChild(button);
|
||||||
this._list.appendChild(li);
|
this._list.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,6 +53,35 @@ class LiveTrackStoreTest < Minitest::Test
|
|||||||
assert_includes ids, "old_with_summary"
|
assert_includes ids, "old_with_summary"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_stale_ended_finds_old_ended_sessions_but_not_active_recent_or_already_flagged_ones
|
||||||
|
@store.add("old_ended", "tok1")
|
||||||
|
@store.set_summary("old_ended", { "active" => false, "end" => (Time.now.utc - 25 * 60 * 60).iso8601 })
|
||||||
|
|
||||||
|
@store.add("recent_ended", "tok2")
|
||||||
|
@store.set_summary("recent_ended", { "active" => false, "end" => Time.now.utc.iso8601 })
|
||||||
|
|
||||||
|
@store.add("still_active", "tok3")
|
||||||
|
@store.set_summary("still_active", { "active" => true })
|
||||||
|
|
||||||
|
@store.add("already_flagged", "tok4")
|
||||||
|
@store.set_summary("already_flagged", { "active" => false, "end" => (Time.now.utc - 25 * 60 * 60).iso8601 })
|
||||||
|
@store.mark_unavailable("already_flagged")
|
||||||
|
|
||||||
|
ids = @store.stale_ended(24 * 60 * 60).map { |s| s["id"] }
|
||||||
|
assert_equal ["old_ended"], ids
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_mark_unavailable_flags_the_session_without_touching_its_summary
|
||||||
|
@store.add("abc123", "tok-abc")
|
||||||
|
@store.set_summary("abc123", { "session_name" => "Test Ride" })
|
||||||
|
|
||||||
|
@store.mark_unavailable("abc123")
|
||||||
|
|
||||||
|
session = @store.find("abc123")
|
||||||
|
assert_equal false, session["map_available"]
|
||||||
|
assert_equal({ "session_name" => "Test Ride" }, session["summary"])
|
||||||
|
end
|
||||||
|
|
||||||
def test_data_survives_reload_from_disk
|
def test_data_survives_reload_from_disk
|
||||||
@store.add("abc123", "tok-abc")
|
@store.add("abc123", "tok-abc")
|
||||||
path = @store.instance_variable_get(:@path)
|
path = @store.instance_variable_get(:@path)
|
||||||
|
|||||||
Reference in new issue
Block a user