diff --git a/public/js/livetrack.js b/public/js/livetrack.js index 13c9822..0dbbd58 100644 --- a/public/js/livetrack.js +++ b/public/js/livetrack.js @@ -5,6 +5,10 @@ import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js"; const SESSIONS_URL = "/api/livetrack/sessions"; const TRACK_SOURCE = "livetrack-track"; const TRACK_COLOR = "#7209b7"; +// Garmin devices post a new point roughly every 15s (postTrackPointFrequency +// in the session data) — polling twice that often would just re-fetch the +// same 400+KB page for nothing most of the time. +const REFRESH_INTERVAL_MS = 30000; // A sidebar icon that opens a side panel listing Garmin LiveTrack sessions // received via the webhook (see docs/deployment.adoc). Clicking one fetches @@ -17,6 +21,7 @@ export class LiveTrackPanelControl { this._geojson = null; this._hoverPopup = null; this._pointInteractivityRegistered = false; + this._refreshTimer = null; // setStyle() (switching the Online/Offline base map) tears down all // custom sources and layers — put the track back afterwards, same as @@ -109,13 +114,7 @@ export class LiveTrackPanelControl { button.classList.add("loading"); try { - const response = await fetch(`/api/livetrack/sessions/${encodeURIComponent(id)}/track`); - if (!response.ok) { - const { error } = await response.json().catch(() => ({})); - window.alert(error || "Could not load track."); - return; - } - this._geojson = await response.json(); + this._geojson = await this._fetchTrack(id); this._showTrack(this._geojson); this._activeSessionId = id; button.classList.add("active"); @@ -123,11 +122,48 @@ export class LiveTrackPanelControl { // (see LiveTrackClient.summarize) — reload so this row picks it up // instead of showing the raw id from here on. this._loadSessions(); + this._scheduleRefresh(id); + } catch (err) { + window.alert(err.message); } finally { button.classList.remove("loading"); } } + async _fetchTrack(id) { + const response = await fetch(`/api/livetrack/sessions/${encodeURIComponent(id)}/track`); + if (!response.ok) { + const { error } = await response.json().catch(() => ({})); + throw new Error(error || "Could not load track."); + } + return response.json(); + } + + // Re-fetches on a timer for as long as the session is still recording + // (properties.active on the Point feature — see LiveTrackClient), so an + // in-progress activity's line keeps growing instead of staying frozen at + // whatever it looked like when first opened. Stops on its own once the + // session ends, or immediately if the session is switched away from or + // toggled off in the meantime. + _scheduleRefresh(id) { + if (!this._geojson.features[1].properties.active) return; + + this._refreshTimer = setTimeout(async () => { + if (this._activeSessionId !== id) return; + try { + const geojson = await this._fetchTrack(id); + if (this._activeSessionId !== id) return; + this._geojson = geojson; + this._map.getSource(TRACK_SOURCE)?.setData(geojson); + this._loadSessions(); + } catch { + // Transient fetch failure — leave the track as-is and try again + // next tick rather than interrupting with an alert. + } + this._scheduleRefresh(id); + }, REFRESH_INTERVAL_MS); + } + _showTrack(geojson, { fit = true } = {}) { if (this._map.getSource(TRACK_SOURCE)) return; @@ -202,6 +238,8 @@ export class LiveTrackPanelControl { } _removeTrack() { + clearTimeout(this._refreshTimer); + this._refreshTimer = null; this._activeSessionId = null; this._geojson = null; this._hoverPopup?.remove();