Show Live/Ended status on LiveTrack sessions
A session's own end timestamp is absent until the activity is actually stopped, so its presence/absence is the status signal — no new data needed, just reading what was already there. Shown as a badge next to the session in the list and in its map hover popup.
This commit is contained in:
1 parent
55607aece9
commit
6b8ea27e22
3 files changed
+47
-5
No files matched your search
@@ -43,6 +43,11 @@ module Maptool
|
|||||||
# Distance/duration/activity type come from the last track point (its
|
# Distance/duration/activity type come from the last track point (its
|
||||||
# totals are cumulative for the whole session), name/type from the
|
# totals are cumulative for the whole session), name/type from the
|
||||||
# session details — nil-safe throughout since either can be missing.
|
# 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)
|
def self.summarize(result)
|
||||||
last = result.points.last || {}
|
last = result.points.last || {}
|
||||||
session = result.session || {}
|
session = result.session || {}
|
||||||
@@ -51,6 +56,7 @@ 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" => session["end"].nil?,
|
||||||
"start" => session["start"],
|
"start" => session["start"],
|
||||||
"end" => session["end"],
|
"end" => session["end"],
|
||||||
}
|
}
|
||||||
@@ -145,6 +151,11 @@ if __FILE__ == $0
|
|||||||
raise "expected $undefined cleaned to nil" unless result.points[0]["totalDistanceMeters"].nil?
|
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 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 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, "<html></html>")) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) }
|
no_points = client.send(:search, client.send(:parse_payloads, "<html></html>")) { |n| n["trackPoints"] if n["trackPoints"].is_a?(Array) }
|
||||||
raise "expected nil when no trackPoints present" unless no_points.nil?
|
raise "expected nil when no trackPoints present" unless no_points.nil?
|
||||||
|
|||||||
+20
-2
@@ -446,8 +446,7 @@ html, body {
|
|||||||
.livetrack-panel__row {
|
.livetrack-panel__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 6px;
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 6px 4px;
|
padding: 6px 4px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -472,6 +471,8 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.livetrack-panel__label {
|
.livetrack-panel__label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -481,6 +482,23 @@ html, body {
|
|||||||
font-family: monospace;
|
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 {
|
.livetrack-panel__time {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: #888;
|
color: #888;
|
||||||
|
|||||||
+16
-3
@@ -90,7 +90,11 @@ export class LiveTrackPanelControl {
|
|||||||
timeSpan.className = "livetrack-panel__time";
|
timeSpan.className = "livetrack-panel__time";
|
||||||
timeSpan.textContent = new Date(received_at).toLocaleString();
|
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));
|
button.addEventListener("click", () => this._toggleTrack(id, button));
|
||||||
li.appendChild(button);
|
li.appendChild(button);
|
||||||
this._list.appendChild(li);
|
this._list.appendChild(li);
|
||||||
@@ -213,8 +217,9 @@ export class LiveTrackPanelControl {
|
|||||||
// data — session_name in particular is user-supplied (whatever the LiveTrack
|
// 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,
|
// session was named on the watch/app) — so escape before it lands in HTML,
|
||||||
// same as GpxPanelControl's popups do for GPX file contents.
|
// 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 }) {
|
function popupHtml({ session_name, activity_type, distance_meters, duration_secs, active, time, speed_meters_per_sec, heart_rate_beats_per_min }) {
|
||||||
const parts = [`<strong>${escapeHtml(session_name || "LiveTrack session")}</strong>`];
|
const status = active ? `<span class="livetrack-badge livetrack-badge--live">Live</span>` : `<span class="livetrack-badge">Ended</span>`;
|
||||||
|
const parts = [`<strong>${escapeHtml(session_name || "LiveTrack session")}</strong> ${status}`];
|
||||||
|
|
||||||
const stats = [];
|
const stats = [];
|
||||||
if (activity_type) stats.push(escapeHtml(titleCase(activity_type)));
|
if (activity_type) stats.push(escapeHtml(titleCase(activity_type)));
|
||||||
@@ -243,6 +248,14 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
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 }) {
|
function summaryLabel({ session_name, activity_type, distance_meters, duration_secs }) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (activity_type) parts.push(titleCase(activity_type));
|
if (activity_type) parts.push(titleCase(activity_type));
|
||||||
|
|||||||
Reference in new issue
Block a user