sailkick-boat 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -482,8 +482,21 @@ Point your chart app / browser at:
482
482
  ```bash
483
483
  cd ~/.signalk && npm install sailkick-boat # or a packed tarball
484
484
  ```
485
- Enable + configure under **Server Plugin Config "Sailkick boat companion"**.
486
- Set **Data directory** to a path on the SSD (or leave blank for the plugin data dir).
485
+ The plugin **enables itself on install** and appears in Signal K's **Webapps** menu as
486
+ **Sailkick** a launcher with two entries, "Open Sailkick" (full app) and "Open on
487
+ phone" (mobile view). Both point at the mirror on this boat, so nothing has to be typed
488
+ by hand. One npm package can only produce one menu item — Signal K dedupes webapps by
489
+ package name — hence one launcher rather than two entries.
490
+
491
+ Nothing is uploaded by enabling it. Telemetry sync needs an account write token and
492
+ refuses to start without one; AIS upload and the backfill are off by default; and the
493
+ worldwide base-map seed is skipped entirely on an unpaired boat, so a fresh install
494
+ downloads only what you actually look at. **The whole app works with no account** —
495
+ charts, instruments, trends, AIS, routes and polars are all served from the boat. An
496
+ account adds cloud sync, off-boat access and long-term history.
497
+
498
+ Set **Data directory** to a path on the SSD (or leave blank for the plugin data dir) —
499
+ that is the one setting worth changing straight away.
487
500
 
488
501
  ### Victron GX / Venus OS (Cerbo, Ekrano)
489
502
  Works on Venus OS Large (Signal K enabled). Point **Data directory** at USB/SD storage
package/index.js CHANGED
@@ -98,6 +98,8 @@ module.exports = function (app) {
98
98
  let ais = null
99
99
  let aisTargets = null
100
100
  let profile = null
101
+ let proxyPort = null // what the launcher page needs to build its links
102
+ let pairedSlug = null
101
103
  let statusTimer = null
102
104
  let accountStatus = null
103
105
  let syncWarning = null
@@ -264,7 +266,7 @@ module.exports = function (app) {
264
266
  const pOpts = {
265
267
  sailkickUrl: upstream.url,
266
268
  storeDir: store,
267
- proxyPort: p.proxyPort == null ? 8080 : p.proxyPort,
269
+ proxyPort: (proxyPort = p.proxyPort == null ? 8080 : p.proxyPort),
268
270
  localSignalkUrl: p.localSignalkUrl || 'http://127.0.0.1:3000',
269
271
  localPaths: (p.localPaths && p.localPaths.length) ? p.localPaths : PROXY_TUNING.localPaths,
270
272
  telemetryPath: p.telemetryPath || PROXY_TUNING.telemetryPath,
@@ -457,6 +459,8 @@ module.exports = function (app) {
457
459
  ais = null
458
460
  aisTargets = null
459
461
  profile = null
462
+ proxyPort = null
463
+ pairedSlug = null
460
464
  proxy = null
461
465
  accountStatus = null
462
466
  syncWarning = null
@@ -465,6 +469,19 @@ module.exports = function (app) {
465
469
  // Mounted by Signal K at /plugins/sailkick-boat. Handlers dispatch to the live
466
470
  // proxy module (created in start), so enable/disable works at request time.
467
471
  plugin.registerWithRouter = function (router) {
472
+ // Read by the launcher page (public/index.html), which Signal K serves at
473
+ // /sailkick-boat/. That page lives outside this process and cannot know which port
474
+ // the mirror was configured on, so it asks. `paired` drives the hint about cloud
475
+ // sync; `running` distinguishes "plugin off" from "mirror port disabled".
476
+ router.get('/info', (req, res) => {
477
+ res.json({
478
+ ok: true,
479
+ running: !!proxy,
480
+ port: proxyPort,
481
+ paired: !!pairedSlug,
482
+ version: (() => { try { return require('./package.json').version } catch { return null } })()
483
+ })
484
+ })
468
485
  router.get('/p/*', (req, res) => {
469
486
  if (proxy) proxy.handleGet(req, res)
470
487
  else res.status(503).send('proxy not enabled')
@@ -498,12 +498,19 @@ function createProxy (app, options) {
498
498
  if (clamped.length) log(`area prefetch: ${clamped.map(([l, v]) => `${l} capped at z${v.top}`).join(', ')} (upstream limit — those tiles do not exist)`)
499
499
  if (total > CAP) { area = { capped: true, total, radius, maxZoom }; log(`area prefetch: ~${total} tiles too large — reduce radius/detail`); return null }
500
500
  const paths = [...enumerateTiles(bbox, minZoom, perLayer)]
501
- area = { done: 0, total: paths.length, running: true, radius, maxZoom, perLayer }
501
+ // Hold our OWN reference and mutate that, never the `area` slot. A prefetch of tens
502
+ // of thousands of tiles outlives a plugin disable by minutes, and stop() sets
503
+ // area = null — so settling handlers that touched `area` threw
504
+ // "Cannot set properties of null (setting 'running')" on every disable/enable. The
505
+ // same reference also protects against a restart having installed a NEWER prefetch:
506
+ // the old run must not report its progress into the new one's counters.
507
+ const run = { done: 0, total: paths.length, running: true, radius, maxZoom, perLayer }
508
+ area = run
502
509
  log(`area prefetch: ${radius}nm around ${pos.latitude.toFixed(2)},${pos.longitude.toFixed(2)} to z${maxZoom} — ${paths.length} tiles`)
503
- area.promise = warmMany(paths, { concurrency: pf.concurrency || 4, onProgress: () => { area.done++ } })
504
- .then((r) => { area.running = false; area.result = r; log(`area prefetch done: ${r.cached} cached, ${r.empty} empty, ${r.failed} failed`); return r })
505
- .catch((e) => { area.running = false; log('area prefetch error: ' + e.message) })
506
- return area.promise
510
+ run.promise = warmMany(paths, { concurrency: pf.concurrency || 4, onProgress: () => { run.done++ } })
511
+ .then((r) => { run.running = false; run.result = r; log(`area prefetch done: ${r.cached} cached, ${r.empty} empty, ${r.failed} failed`); return r })
512
+ .catch((e) => { run.running = false; log('area prefetch error: ' + e.message) })
513
+ return run.promise
507
514
  }
508
515
 
509
516
  function areaStatus () {
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "sailkick-boat",
3
- "version": "0.19.0",
4
- "description": "Run the sailkick app on board with no internet: charts, weather, climatology, trends and AIS all served from the boat itself. With a sailkick account it also syncs your metrics to the cloud in real time. Alpha, invite-only info@sailkick.io",
3
+ "version": "0.20.0",
4
+ "description": "Run the sailkick app on board with no internet: charts, weather, climatology, trends and AIS all served from the boat itself. With a sailkick account it also syncs your metrics to the cloud in real time. Alpha, invite-only \u2014 info@sailkick.io",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "node --test"
8
8
  },
9
9
  "keywords": [
10
10
  "signalk-node-server-plugin",
11
+ "signalk-webapp",
11
12
  "signalk",
12
13
  "sailkick",
13
14
  "influxdb",
@@ -15,6 +16,7 @@
15
16
  "cache",
16
17
  "proxy"
17
18
  ],
19
+ "signalk-plugin-enabled-by-default": true,
18
20
  "author": "lauchat",
19
21
  "license": "MIT",
20
22
  "repository": {
@@ -35,6 +37,7 @@
35
37
  "files": [
36
38
  "index.js",
37
39
  "lib/",
40
+ "public/",
38
41
  "README.md",
39
42
  "LICENSE"
40
43
  ]
@@ -0,0 +1,97 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Sailkick</title>
7
+ <!--
8
+ Launcher for the Signal K admin menu.
9
+
10
+ Signal K mounts <package>/public/ at /<package-name> for any package carrying the
11
+ `signalk-webapp` keyword, and dedupes the menu by PACKAGE name (interfaces/webapps.js,
12
+ uniqBy(webapps,'name')) — so one package can only ever produce one entry. Hence one
13
+ launcher offering both surfaces rather than two menu items.
14
+
15
+ The app itself is not served from here: it lives on the plugin's mirror, on a port the
16
+ owner can change. This page therefore asks the plugin where it is, via the plugin
17
+ router at /plugins/sailkick-boat/info, and falls back to the default port if that
18
+ cannot be reached. The host is always taken from the current URL, so this works over
19
+ whatever address the browser reached Signal K on — LAN IP, .local, or tailnet.
20
+ -->
21
+ <style>
22
+ :root { color-scheme: light dark; --bg:#f6f7f9; --fg:#12161c; --mut:#5c6672; --card:#fff; --line:#e2e6eb; --accent:#0b6ea9 }
23
+ @media (prefers-color-scheme: dark) {
24
+ :root { --bg:#11151a; --fg:#e8edf2; --mut:#96a1ad; --card:#171c23; --line:#252c35; --accent:#4aa8e0 }
25
+ }
26
+ * { box-sizing: border-box }
27
+ body { margin:0; padding:2rem 1rem; background:var(--bg); color:var(--fg);
28
+ font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif }
29
+ main { max-width:44rem; margin:0 auto }
30
+ h1 { margin:0 0 .25rem; font-size:1.5rem; letter-spacing:-.01em }
31
+ .sub { color:var(--mut); margin:0 0 1.5rem }
32
+ .grid { display:grid; gap:1rem; grid-template-columns:repeat(auto-fit,minmax(15rem,1fr)) }
33
+ a.card { display:block; padding:1.1rem 1.2rem; background:var(--card); border:1px solid var(--line);
34
+ border-radius:.7rem; text-decoration:none; color:inherit; transition:border-color .15s, transform .15s }
35
+ a.card:hover { border-color:var(--accent); transform:translateY(-1px) }
36
+ a.card strong { display:block; font-size:1.05rem; margin-bottom:.2rem }
37
+ a.card span { color:var(--mut); font-size:.9rem }
38
+ .note { margin-top:1.5rem; padding:.85rem 1rem; background:var(--card); border:1px solid var(--line);
39
+ border-radius:.6rem; color:var(--mut); font-size:.9rem }
40
+ .note b { color:var(--fg); font-weight:600 }
41
+ code { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:.87em }
42
+ .warn { border-color:#b4791f }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <main>
47
+ <h1>Sailkick</h1>
48
+ <p class="sub">Charts, instruments and AIS, served from this boat.</p>
49
+
50
+ <div class="grid">
51
+ <a class="card" id="desktop" href="#"><strong>Open Sailkick</strong><span>Full app — chart, trends, weather</span></a>
52
+ <a class="card" id="mobile" href="#"><strong>Open on phone</strong><span>Mobile view — swipeable instrument decks</span></a>
53
+ </div>
54
+
55
+ <div class="note" id="note">Locating the mirror…</div>
56
+ </main>
57
+
58
+ <script>
59
+ (function () {
60
+ var DEFAULT_PORT = 8080;
61
+ var note = document.getElementById('note');
62
+
63
+ function apply (port, info) {
64
+ var base = location.protocol + '//' + location.hostname + (port ? ':' + port : '');
65
+ document.getElementById('desktop').href = base + '/';
66
+ document.getElementById('mobile').href = base + '/mobile.html';
67
+
68
+ if (info && info.running === false) {
69
+ note.className = 'note warn';
70
+ note.innerHTML = '<b>The mirror is not running.</b> Enable the plugin in ' +
71
+ 'Server → Plugin Config → Sailkick boat companion, then reload this page.';
72
+ return;
73
+ }
74
+ if (port === 0) {
75
+ note.className = 'note warn';
76
+ note.innerHTML = '<b>The mirror port is disabled</b> (set to 0 in the plugin config), ' +
77
+ 'so the app cannot be opened from here. Set a port — 8080 is the default.';
78
+ return;
79
+ }
80
+ var paired = info && info.paired;
81
+ note.innerHTML = 'Serving on <code>' + base + '</code>. Works with no account and no internet — ' +
82
+ 'live data comes from this boat’s own Signal K, and maps are cached locally.' +
83
+ (paired ? '' : ' <b>Cloud sync is off</b> until you add a sailkick account in the plugin config; ' +
84
+ 'nothing leaves the boat before then.');
85
+ }
86
+
87
+ fetch('/plugins/sailkick-boat/info', { credentials: 'same-origin' })
88
+ .then(function (r) { return r.ok ? r.json() : null })
89
+ .then(function (j) {
90
+ if (j && typeof j.port === 'number') apply(j.port, j);
91
+ else apply(DEFAULT_PORT, null);
92
+ })
93
+ .catch(function () { apply(DEFAULT_PORT, null); });
94
+ })();
95
+ </script>
96
+ </body>
97
+ </html>