Add Garmin LiveTrack session tracking
CI / check (push) Successful in 6m49s
CI / deploy (push) Successful in 23s

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:
randy committed 2026-09-06 21:05:35 +02:00
1 parent 60170efda2
commit f9ddf860ea
11 files changed
+279

No files matched your search

+67
View File
@@ -107,6 +107,73 @@ id/secret to store as the two `OAUTH_CLIENT_*` secrets above.
usernames, e.g. `randy,friend1,friend2` — not their emails, the
`login` field from Gitea's `/api/v1/user`.
== Garmin LiveTrack session webhook
`POST /api/livetrack/session` (`session_id`, `secret` form params) records a
Garmin LiveTrack session ID to `data/livetrack.json`; `GET
/api/livetrack/sessions` lists them, newest first. Intended caller: a Google
Apps Script bound to the Gmail account that receives LiveTrack emails,
triggered on a timer, extracting the session ID from the email and posting
it here — the endpoint itself doesn't know anything about email or Garmin's
API, just the ID string.
`secret` must match the `LIVETRACK_WEBHOOK_SECRET` repo secret (added to
`.env` the same way as the others above). Unlike the other secrets, this one
is optional — leaving it unset simply disables the endpoint (`halt 403` on
every call) rather than failing the deploy, since LiveTrack tracking isn't
core to the app.
The caller side is a Google Apps Script bound to the Gmail account that
receives the LiveTrack emails (script.google.com, "New project"), run on a
time-driven trigger:
[source,javascript]
----
const WEBHOOK_URL = "https://map.priesol.net/api/livetrack/session";
const WEBHOOK_SECRET = "PASTE_SAME_VALUE_AS_LIVETRACK_WEBHOOK_SECRET_IN_.env";
const PROCESSED_LABEL = "livetrack-processed";
function checkLiveTrackEmail() {
const label = GmailApp.getUserLabelByName(PROCESSED_LABEL) || GmailApp.createLabel(PROCESSED_LABEL);
const threads = GmailApp.search(`from:noreply@garmin.com subject:LiveTrack -label:${PROCESSED_LABEL}`);
threads.forEach(thread => {
// getBody() (raw HTML), not getPlainBody(): the email has no text/plain
// part, only text/html, and the link only exists as an href attribute
// — the auto-generated plain-text conversion isn't reliable for it.
const body = thread.getMessages()[0].getBody();
const match = body.match(/https:\/\/livetrack\.garmin\.com\/session\/([\w-]+)/);
if (match) {
UrlFetchApp.fetch(WEBHOOK_URL, {
method: "post",
payload: { session_id: match[1], secret: WEBHOOK_SECRET }
});
}
thread.addLabel(label); // stops it being reprocessed next run
});
}
----
The Gmail label (rather than marking the message read/unread) tracks which
threads are already handled without touching the user's own inbox state.
`subject:LiveTrack` matches regardless of the account's language — Garmin
keeps "LiveTrack" as-is in the subject even in a localized
("Sledujte LiveTrack používateľa ...") one.
Set the trigger from the editor: clock icon on the left (Triggers) → *Add
Trigger* → function `checkLiveTrackEmail`, time-driven, "Minutes timer",
every 5 minutes.
Verified against a real "started an activity" LiveTrack email
(2026-09-06): sender `noreply@garmin.com`, link
`https://livetrack.garmin.com/session/<uuid>/token/<token>` — the regex
above only captures the `<uuid>` part, stopping at the next `/`. Garmin
also sends other LiveTrack notifications (e.g. activity ended) that
weren't checked — if one turns out to use a different link shape, the
webhook simply won't receive that session ID; nothing breaks.
== CI deploy job
`.gitea/workflows/ci.yml`'s `deploy` job runs after `check` passes, only