diff --git a/lib/maptool/livetrack_client.rb b/lib/maptool/livetrack_client.rb index 52465f7..6679622 100644 --- a/lib/maptool/livetrack_client.rb +++ b/lib/maptool/livetrack_client.rb @@ -43,6 +43,11 @@ module Maptool # Distance/duration/activity type come from the last track point (its # totals are cumulative for the whole session), name/type from the # session details — nil-safe throughout since either can be missing. + # + # "active": Garmin doesn't set a session's `end` until the activity is + # actually stopped, so its absence means the session is still recording + # — inferred from the one (already-ended) session this was built + # against, not verified against a session actually caught mid-activity. def self.summarize(result) last = result.points.last || {} session = result.session || {} @@ -51,6 +56,7 @@ module Maptool "activity_type" => last["activityType"], "distance_meters" => last["totalDistanceMeters"], "duration_secs" => last["totalDurationSecs"], + "active" => session["end"].nil?, "start" => session["start"], "end" => session["end"], } @@ -145,6 +151,11 @@ if __FILE__ == $0 raise "expected $undefined cleaned to nil" unless result.points[0]["totalDistanceMeters"].nil? raise "expected summary from last point" unless summary["activity_type"] == "CYCLING" && summary["distance_meters"] == 5000 raise "expected session name in summary" unless summary["session_name"] == "Test Ride" + raise "expected inactive: session has an end" if summary["active"] + + ongoing_session = session.merge("end" => nil) + ongoing_summary = Maptool::LiveTrackClient.summarize(Maptool::LiveTrackClient::Result.new(points: result.points, session: ongoing_session)) + raise "expected active: session has no end" unless ongoing_summary["active"] no_points = client.send(:search, client.send(:parse_payloads, "")) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) } raise "expected nil when no trackPoints present" unless no_points.nil? diff --git a/public/css/style.css b/public/css/style.css index e31ba0f..72ef81f 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -446,8 +446,7 @@ html, body { .livetrack-panel__row { display: flex; align-items: center; - justify-content: space-between; - gap: 8px; + gap: 6px; width: 100%; padding: 6px 4px; border: none; @@ -472,6 +471,8 @@ html, body { } .livetrack-panel__label { + flex: 1; + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -481,6 +482,23 @@ html, body { font-family: monospace; } +.livetrack-badge { + flex-shrink: 0; + padding: 2px 6px; + border-radius: 3px; + background: #e5e5e5; + color: #666; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.livetrack-badge--live { + background: #d7f3e3; + color: #1b7a43; +} + .livetrack-panel__time { flex-shrink: 0; color: #888; diff --git a/public/js/livetrack.js b/public/js/livetrack.js index f0f779e..13c9822 100644 --- a/public/js/livetrack.js +++ b/public/js/livetrack.js @@ -90,7 +90,11 @@ export class LiveTrackPanelControl { timeSpan.className = "livetrack-panel__time"; timeSpan.textContent = new Date(received_at).toLocaleString(); - button.append(labelSpan, timeSpan); + if (summary) { + button.append(statusBadge(summary.active), labelSpan, timeSpan); + } else { + button.append(labelSpan, timeSpan); + } button.addEventListener("click", () => this._toggleTrack(id, button)); li.appendChild(button); this._list.appendChild(li); @@ -213,8 +217,9 @@ export class LiveTrackPanelControl { // data — session_name in particular is user-supplied (whatever the LiveTrack // session was named on the watch/app) — so escape before it lands in HTML, // same as GpxPanelControl's popups do for GPX file contents. -function popupHtml({ session_name, activity_type, distance_meters, duration_secs, time, speed_meters_per_sec, heart_rate_beats_per_min }) { - const parts = [`${escapeHtml(session_name || "LiveTrack session")}`]; +function popupHtml({ session_name, activity_type, distance_meters, duration_secs, active, time, speed_meters_per_sec, heart_rate_beats_per_min }) { + const status = active ? `Live` : `Ended`; + const parts = [`${escapeHtml(session_name || "LiveTrack session")} ${status}`]; const stats = []; if (activity_type) stats.push(escapeHtml(titleCase(activity_type))); @@ -243,6 +248,14 @@ function escapeHtml(text) { return div.innerHTML; } +function statusBadge(active) { + const badge = document.createElement("span"); + badge.className = "livetrack-badge"; + badge.classList.toggle("livetrack-badge--live", !!active); + badge.textContent = active ? "Live" : "Ended"; + return badge; +} + function summaryLabel({ session_name, activity_type, distance_meters, duration_secs }) { const parts = []; if (activity_type) parts.push(titleCase(activity_type));