signalk-github-pages
v1.0.3
Published
Publish Signal K vessel telemetry to a GitHub Pages site — a static tracker, updated from the boat, with no server ashore.
Maintainers
Readme
GitHub Pages Vessel Tracker
Your boat publishes its own website.
signalk-github-pages: a Signal K plugin that turns live vessel data into a static GitHub Pages site — position, tracks, instruments and conditions — with no server ashore and no git checkout on board.
Underway, the boat publishes every two minutes. At the dock, hourly. Ashore, anyone with the link sees where you are, where you have been, what the wind is doing and how the batteries are holding up — served by GitHub Pages, which does not go down when the hotspot does, and costs nothing.
Tip: Buy a domain and point it at the Pages site, so the link you give people is
myboat.comrather thanyou.github.io. In the repository's Settings → Pages, set the custom domain, add the DNS records GitHub lists at your registrar, and tick Override site address on the plugin's config page so link previews resolve against the new address. Use a domain attached to Pages, not an HTTP redirect: a redirect changes the address in the browser to thegithub.ioone, which defeats the point, and breaks the installed home-screen app's scope. Costs about $12 a year for a.com.
Signal K server GitHub Ashore
┌────────────────────┐ ┌──────────────────┐ ┌──────────────┐
│ navigation.* │ in-process │ Git Data API │ Pages │ │
│ environment.* ├─────────────▶│ blobs → tree ├───────▶│ your-site │
│ electrical.* │ plugin │ → commit → ref │ │ .github.io │
└─────────┬──────────┘ └──────────────────┘ └──────────────┘
│
│ navigation.state
│ sets the cadence
▼
plugin data dir
(rolling state)No git binary. No working copy to corrupt. No systemd unit. Nothing to
hand-edit: every setting is either on the plugin's config page or read from
the server. The plugin reads the self tree in-process, keeps its rolling
state in app.getDataDirPath(), and each cycle is one commit built on the
live HEAD.
What you get
For people ashore
| | |
|---|---|
| Where the boat is | Live position on a map, redacted inside your privacy zones |
| Where it has been | A track per local calendar day, as GPX, listed on a Voyages tab. Each voyage has its own link to share, and its own sail log if you write one |
| Where it is going | A passage banner from the waypoint or route active on the plotter, gone on arrival |
| How it is doing | Instruments colored by the zones set on the server, sparklines from your history provider, active notifications and how often each has fired |
| Conditions | Tides from the nearest NOAA station and a 48-hour wind, swell and temperature forecast |
| Your own links | Buttons to anything else the boat has online, from site.customLinks |
How it behaves
- Adaptive cadence. Fast underway, slow at anchor, straight off
navigation.state, with a publish the moment it changes. - Nothing hand-edited. A value the boat knows is read from Signal K: name, registrations, units, alarm zones, the active polar, the passage. A value you choose is a field on the config page.
- Shares the repository. The plugin writes only the paths it declares in
.tracker-manifest.json. Everything else in the repository is yours. - Measures its own cost. Every cycle logs bytes on the wire, API calls and rate limit left, and warns when the instrument log gets expensive.
- On-boat console. A Signal K webapp that previews the site from live data, publishes on request and prunes old voyages.
Live example
mermug.com is this plugin running on a 1994 Beneteau First 42s7 on a Raspberry Pi 5, served from a custom domain. It shows the whole thing: the position map, daily tracks on the Voyages tab, instruments with sparklines, tides and the 48-hour forecast.
Quick start
1. A repository with Pages on. The plugin publishes to <you>.github.io
unless you override the name; anything else is served at /<repo>/.
Settings → Pages → Deploy from a branch → main / (root).
Use a repository that holds only the tracker. The token can rewrite every file
in it, and the site is served from your github.io origin, so a leaked token
means someone else's JavaScript on your page. A dedicated repository keeps that
to the tracker. The plugin shares a repository politely, but the token does not.
2. A token. Make a fine-grained PAT and tick exactly this much:
| In the token form | Set it to |
|---|---|
| Resource owner | You, or the organization that owns the repository |
| Repository access | Only select repositories → the Pages repository |
| Repository permissions → Contents | Read and write |
| Repository permissions → Metadata | Read-only — added for you, cannot be removed |
| Expiration | 90 days, with a calendar reminder to rotate. Publishing stops with a 401 the day it lapses, and the plugin raises notifications.tracker.publishFailed after half an hour of failures |
Nothing else is needed: no Actions, no Pages, no account permissions. The commonly missed one is Contents, because a token without it reads the repository perfectly and fails on the first commit with a 403.
A classic token works too if you prefer one: the repo scope, or
public_repo if the repository is public. It is a blunter instrument — it
reaches every repository you can push to — so use a fine-grained one unless
you have a reason not to.
The plugin reads the failure status back to you: 401 says the token is wrong or expired, 403 says it lacks Contents: read and write, 404 says it cannot see the repository.
3. Install it. In the Signal K admin UI, Appstore → Available, search for GitHub Pages, install, and restart the server when it asks. From a shell, the same thing is:
cd ~/.signalk
npm install signalk-github-pages
sudo systemctl restart signalk # or however you run the serverTo run unreleased code from main, install
github:zackphillips/signalk-github-pages instead; npm clones it and runs the
TypeScript build through the package's prepare script.
Then open Server → Plugin Config → GitHub Pages Vessel Tracker, fill in the repository owner and the token, enable. Everything else has a working default or is derived.
The first cycle writes the whole site — HTML, CSS, JS, icons — then telemetry only. Give Pages a minute, then open the URL.
Tip: Install signalk-autostate if you have not. It sets
navigation.state, which is what makes the cadence adaptive. Without it every cycle uses the stationary interval.
Configuration
Two fields are required: github.owner and github.token. The ones you
are most likely to change:
| Field | Default | What it does |
|---|---|---|
| interval.underwayMinutes | 2 | Cadence when sailing or motoring |
| interval.stationaryMinutes | 60 | Cadence moored, anchored, or state unknown |
| privacyZones[] | empty | {name, lat, lon, radius_m}; see Privacy |
| paths.hide | one server notice | Paths never published, notifications included |
| instrumentLog.hours | 1 | How far back the sparklines reach; 0 publishes none |
| site.logo, site.icon | empty | The boat's logo, and the tab and home-screen icon |
The repository name, branch, site URL, timezone and tide station are derived, and each has an override. The polar comes from Polar Management. The full field list is in Configuration.
Warning: A token typed into the config page is stored as plain JSON under
~/.signalk/plugin-config-data/, and Signal K's admin API returns plugin configuration to any admin session. It is readable by anyone with a shell on the server, anyone logged in as a Signal K admin, and anything that backs up that directory. Scope it to the one repository, and rotate it if the Pi ever leaves your hands.To keep it off the config page, put it in a file and point the plugin at it (Token from a file).
Privacy
GitHub Pages sites are public. Anything the plugin publishes can be read by anyone who has the URL.
- Privacy zones. A position inside a zone is published as the zone's
center and left out of the track entirely. Every position in the snapshot
is checked,
navigation.anchor.positionincluded. Correcting a zone also trims tracks that are already published. - Positions go through one path. The instrument log never asks for a position, and the passage banner publishes names, never coordinates.
- Zones start empty. Nothing is hidden until you say what to hide. Put the center off your slip and make the radius reach past it with room to spare.
- Notifications are free text written by whichever plugin raised them,
and they are published verbatim.
notifications.publishturns them off, andpaths.hidedrops any path or subtree.
Details are in Privacy.
Documentation
| | | |---|---| | Configuration | Every field, and the five overrides | | Track and instruments | Track thinning, the history provider, sparkline windows, instrument paths and bandwidth | | Privacy | Privacy zones in full | | The site | Units and alarm zones, notifications, branding, what is read from Signal K, the passage banner, tides, polars | | The repository and the console | What the plugin writes, and the on-boat console | | Troubleshooting | The cycle log, "Data unavailable", a plugin missing from the list |
Trade-offs worth knowing
The plugin runs inside the server process. A bug here can affect the navigation data hub. Every cycle is wrapped: an exception skips one update, is reported in the admin UI, and never reaches the server's event loop. That is the cost of reading the tree in-process, and it buys away an HTTP poll that could freeze the site on stale data.
It needs a data link. The site updates when the boat has one. Offshore it shows the last publish and how long ago that was.
The token sits in plain text, on the page or in the file you point the plugin at. The file keeps it out of the admin API and config backups, not off the Pi's disk.
One commit per cycle. The repository grows at the rate you publish. Exclude the instrument paths you do not look at.
Nothing is served from the boat. That is KIP's job. This site is for people ashore, and it stays up when the boat's link does not.
Development
npm install
npm test # vitest
npm run typecheck # tsc --noEmit
npm run build # tsc → dist/
npm run dev # site/ + sample/ on http://localhost:8000npm run dev serves the real frontend against fixture telemetry, so no boat
is required. Publisher.runCycle() takes a self tree and returns what it
did without calling back into the server, so a full publish cycle is tested
without Signal K and without a network. The source layout and the rules that
are not obvious are in AGENTS.md.
License
BSD 3-Clause. See LICENSE.
