Add Garmin LiveTrack session tracking
A Gmail-watching Apps Script (documented in docs/deployment.adoc) posts the session ID from each LiveTrack email to a new webhook, which stores it in data/livetrack.json; a new sidebar panel lists what's been received.
This commit is contained in:
1 parent
60170efda2
commit
f9ddf860ea
11 files changed
+279
No files matched your search
@@ -430,3 +430,33 @@ html, body {
|
||||
background: #2a9d8f;
|
||||
box-shadow: 0 0 0 2px rgba(42, 157, 143, 0.4);
|
||||
}
|
||||
|
||||
.livetrack-panel__list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.livetrack-panel__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.livetrack-panel__id {
|
||||
font-family: monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.livetrack-panel__time {
|
||||
flex-shrink: 0;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -47,3 +47,9 @@ export const WAYPOINT_ICON = `
|
||||
</svg>`;
|
||||
|
||||
export const TRACK_ICON = GPX_ICON;
|
||||
|
||||
export const LIVETRACK_ICON = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 21s7-7.58 7-12a7 7 0 0 0-14 0c0 4.42 7 12 7 12z"/>
|
||||
<path d="M8.5 9a3.5 3.5 0 0 1 7 0" stroke-dasharray="1.5 2.2"/>
|
||||
</svg>`;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { LIVETRACK_ICON } from "/js/icons.js";
|
||||
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
|
||||
|
||||
const SESSIONS_URL = "/api/livetrack/sessions";
|
||||
|
||||
// A sidebar icon that opens a side panel listing Garmin LiveTrack session
|
||||
// IDs received via the webhook (see docs/deployment.adoc) — read-only, just
|
||||
// so they're visible somewhere instead of only in data/livetrack.json.
|
||||
export class LiveTrackPanelControl {
|
||||
mount(sidebar) {
|
||||
this._container = document.createElement("div");
|
||||
this._container.className = "sidebar-item";
|
||||
this._container.innerHTML = `<button type="button" class="sidebar-icon" title="LiveTrack" aria-label="LiveTrack">${LIVETRACK_ICON}</button>`;
|
||||
this._icon = this._container.querySelector(".sidebar-icon");
|
||||
this._icon.addEventListener("click", () => this._togglePanel());
|
||||
|
||||
this._panel = document.getElementById("livetrack-panel");
|
||||
this._list = this._panel.querySelector(".livetrack-panel__list");
|
||||
this._panel
|
||||
.querySelector(".basemap-panel__close")
|
||||
.addEventListener("click", () => this._togglePanel(false));
|
||||
|
||||
onlyOneSidePanelOpen(this._panel, () => this._icon.classList.remove("active"));
|
||||
|
||||
sidebar.appendChild(this._container);
|
||||
}
|
||||
|
||||
_togglePanel(force) {
|
||||
const open = force === undefined ? this._panel.hidden : force;
|
||||
this._panel.hidden = !open;
|
||||
this._icon.classList.toggle("active", open);
|
||||
if (open) {
|
||||
notifySidePanelOpen(this._panel);
|
||||
this._loadSessions();
|
||||
}
|
||||
}
|
||||
|
||||
async _loadSessions() {
|
||||
const response = await fetch(SESSIONS_URL);
|
||||
const { sessions } = await response.json();
|
||||
this._renderSessions(sessions);
|
||||
}
|
||||
|
||||
_renderSessions(sessions) {
|
||||
this._list.innerHTML = "";
|
||||
|
||||
if (sessions.length === 0) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "gpx-tree__empty";
|
||||
li.textContent = "No sessions yet";
|
||||
this._list.appendChild(li);
|
||||
return;
|
||||
}
|
||||
|
||||
sessions.forEach(({ id, received_at }) => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "livetrack-panel__item";
|
||||
|
||||
const idSpan = document.createElement("span");
|
||||
idSpan.className = "livetrack-panel__id";
|
||||
idSpan.textContent = id;
|
||||
|
||||
const timeSpan = document.createElement("span");
|
||||
timeSpan.className = "livetrack-panel__time";
|
||||
timeSpan.textContent = new Date(received_at).toLocaleString();
|
||||
|
||||
li.append(idSpan, timeSpan);
|
||||
this._list.appendChild(li);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { RadarControl } from "/js/radar.js";
|
||||
import { GpxPanelControl } from "/js/gpx.js";
|
||||
import { GnssControl } from "/js/gnss.js";
|
||||
import { LiveTrackPanelControl } from "/js/livetrack.js";
|
||||
import { LAYERS_ICON } from "/js/icons.js";
|
||||
import { notifySidePanelOpen, onlyOneSidePanelOpen } from "/js/panels.js";
|
||||
|
||||
@@ -127,6 +128,7 @@ layerToggle.setActive("online");
|
||||
new GpxPanelControl(map).mount(sidebar);
|
||||
new RadarControl(map).mount(sidebar);
|
||||
new GnssControl(map).mount(sidebar);
|
||||
new LiveTrackPanelControl().mount(sidebar);
|
||||
|
||||
function applyStyle(name) {
|
||||
layerToggle.setActive(name);
|
||||
|
||||
Reference in new issue
Block a user