sailkick-boat 0.14.4 → 0.14.7
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 +49 -5
- package/index.js +38 -5
- package/lib/history/ring.js +24 -4
- package/lib/proxy/index.js +21 -5
- package/lib/telemetry/signalk-map.js +27 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,10 +63,19 @@ comes from the cloud **announcing bakes**, not from a clock:
|
|
|
63
63
|
`X-Sailkick-Cache` reports `HIT` / `MISS` / `UPDATED` / `STALE` / `LIVE` per response.
|
|
64
64
|
|
|
65
65
|
**Static vs dynamic — two strategies.** Tiles and app assets are **cache-first**
|
|
66
|
-
(pinned, offline forever). Dynamic `/api/*` data (AIS, weather) is
|
|
67
|
-
fetched live every time online (so
|
|
68
|
-
response kept only as an **offline fallback** (`STALE`). Local `/api/history` and
|
|
69
|
-
patched `/api/config` are served specially (above).
|
|
66
|
+
(pinned, offline forever). Dynamic `/api/*` data (AIS, weather, lightning) is
|
|
67
|
+
**network-first**: fetched live every time online (so it never goes stale), with the
|
|
68
|
+
last response kept only as an **offline fallback** (`STALE`). Local `/api/history` and
|
|
69
|
+
the patched `/api/config` are served specially (above).
|
|
70
|
+
|
|
71
|
+
**Exception — velocity tiles are pinned.** The app serves its wind/current field from
|
|
72
|
+
`/api/velocity/tiles/<layer>/<runId>/<z>/<x>/<y>/<hour>.f32`. The forecast **run id is
|
|
73
|
+
in the path**, so a URL's bytes never change — a new run means new URLs. Those are
|
|
74
|
+
cache-first like map tiles: each tile downloads once instead of on every pan, and the
|
|
75
|
+
wind particles + storm field keep rendering **offline**. The velocity *manifest*
|
|
76
|
+
(`?run=latest`) stays network-first, or the boat would pin itself to a stale run
|
|
77
|
+
forever. Nothing prefetches velocity tiles yet, so offline wind covers only what you
|
|
78
|
+
have panned over, and old runs are not pruned.
|
|
70
79
|
|
|
71
80
|
**Offline circuit breaker.** Some boat routers return errors (or hang) for outbound
|
|
72
81
|
requests when the uplink is down, instead of failing cleanly. Once a fetch fails, the
|
|
@@ -137,6 +146,38 @@ Two ways, chosen automatically:
|
|
|
137
146
|
same BoatState feeding `/ws/telemetry`) — for boats with no local InfluxDB, e.g.
|
|
138
147
|
**SignalK on a Victron GX / Venus OS**. Same JSON contract, fully offline, no database.
|
|
139
148
|
(`historyAvailable` reports true either way, so the app shows the Trends panel.)
|
|
149
|
+
Same eight channels as the InfluxDB path, `stw` included.
|
|
150
|
+
|
|
151
|
+
**Serving an archive.** Set `historyToken` and the three `history*` fields point at any
|
|
152
|
+
InfluxDB holding `signalk-to-influxdb-v2`-schema data — a still-running local database,
|
|
153
|
+
or a bucket you imported an old logbook into. That is the archive's real value: history
|
|
154
|
+
from before this boat ever synced to the cloud. The token is the switch; blank means the
|
|
155
|
+
built-in ring. The plugin logs which source it chose at startup:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
[sailkick-boat] history -> archive http://127.0.0.1:8086 org=addiction bucket=bandg
|
|
159
|
+
[sailkick-boat] history -> built-in ring (no archive token set)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Upgrading from before 0.14.7, when these lived in a hidden `proxy.history` block: your
|
|
163
|
+
saved values still win over the newly pre-filled defaults, so an archive in a
|
|
164
|
+
non-default org/bucket is not silently lost when the config UI writes `signalk` over it.
|
|
165
|
+
The plugin says so in the log — copy the values into the visible fields when convenient.
|
|
166
|
+
|
|
167
|
+
**Do you actually need a local InfluxDB?** For *live* trends, usually not. The app never requests finer
|
|
168
|
+
than `every=5s` over a `window` of 24 h, and the ring's floor at that window is 2 s — so
|
|
169
|
+
set `ringSampleSec: 5` and it matches anything the UI can draw, from live state, with no
|
|
170
|
+
database to run. A local InfluxDB is worth it only for history predating cloud sync,
|
|
171
|
+
more than 30 days offline, or channels outside BoatState (engine, batteries) that the
|
|
172
|
+
Trends panel does not chart anyway.
|
|
173
|
+
|
|
174
|
+
**True wind comes from your instruments.** If the boat publishes
|
|
175
|
+
`environment.wind.speedTrue` / `directionTrue`, those are stored verbatim — a wind
|
|
176
|
+
system corrects for heel and leeway against water-referenced boat speed, and every other
|
|
177
|
+
display aboard shows its numbers. Only when the boat publishes none is true wind
|
|
178
|
+
derived, and then from **STW**, not SOG: true wind is relative to motion through the
|
|
179
|
+
water. (Before v0.14.6 the derivation used SOG, which in 3 kt of foul tide skewed TWD by
|
|
180
|
+
~12° and TWS by ~1.8 kt.) SOG is the last resort for boats with no paddlewheel.
|
|
140
181
|
- **Persistent (append-log):** the ring is saved as a JSONL append-log at
|
|
141
182
|
`<dataDir>/history/history-ring.jsonl` (on the SSD/USB with the tiles; override with
|
|
142
183
|
`proxy.history.ringDir`), so it **survives restarts**. Each sample appends one line; the file is
|
|
@@ -198,7 +239,10 @@ in `index.js`.
|
|
|
198
239
|
- **Telemetry sync → cloud**: `enabled`
|
|
199
240
|
- **Offline app & maps**: `enabled`, `proxyPort` (default 8080), `localSignalkUrl`
|
|
200
241
|
(default `http://127.0.0.1:3000`), `dataDir`, `seedEnabled`, `prefetchRadiusNm`,
|
|
201
|
-
`prefetchDetailZoom
|
|
242
|
+
`prefetchDetailZoom`
|
|
243
|
+
- **History archive** (optional): `historyToken`, `historyInfluxUrl`
|
|
244
|
+
(default `http://127.0.0.1:8086`), `historyOrg` + `historyBucket` (default `signalk`,
|
|
245
|
+
matching `signalk-to-influxdb-v2`)
|
|
202
246
|
|
|
203
247
|
`dataDir` is the one storage location — cached maps, the telemetry spool and the
|
|
204
248
|
history ring log all live under it. **Put it on the SSD/USB disk, not the SD card.**
|
package/index.js
CHANGED
|
@@ -69,6 +69,20 @@ function isPrivateHostUrl (u) {
|
|
|
69
69
|
} catch { return false }
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// A pre-filled schema default must not silently shadow a legacy value that differs. The
|
|
73
|
+
// Signal K config UI writes every default on save, so `historyOrg: "signalk"` appears on
|
|
74
|
+
// a boat whose archive has always lived in org "addiction" under the old nested
|
|
75
|
+
// proxy.history block — and the archive would go quiet with no explanation. Rule: an
|
|
76
|
+
// explicitly-changed field wins; otherwise a legacy value that differs from the default
|
|
77
|
+
// wins; otherwise the default.
|
|
78
|
+
function pickHistoryField (flat, legacy, dflt) {
|
|
79
|
+
const f = String(flat == null ? '' : flat).trim()
|
|
80
|
+
const l = String(legacy == null ? '' : legacy).trim()
|
|
81
|
+
if (f && f !== dflt) return { value: f, from: 'config' }
|
|
82
|
+
if (l && l !== dflt) return { value: l, from: 'legacy' }
|
|
83
|
+
return { value: f || l || dflt, from: 'default' }
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
// Endpoint resolution with one rule: the constant wins unless self-hosting is declared.
|
|
73
87
|
// Returns { url, ignored } — `ignored` is the stale value we refused, so the caller can
|
|
74
88
|
// say so out loud instead of leaving the owner to guess why nothing arrives.
|
|
@@ -141,7 +155,10 @@ module.exports = function (app) {
|
|
|
141
155
|
enumNames: ['Overview (z12)', 'Coastal (z13)', 'Detailed (z14)', 'Harbor (z15)'],
|
|
142
156
|
default: 13
|
|
143
157
|
},
|
|
144
|
-
historyToken: { type: 'string', title: '
|
|
158
|
+
historyToken: { type: 'string', title: 'History archive: read token', description: 'Leave blank for the built-in lightweight history, which is what most boats want and needs no database. Paste a read token to serve Trends and the track from a local InfluxDB instead — worth it for history recorded before this boat synced to the cloud, for more than 30 days offline, or for an imported archive. Setting a token switches the source; the three fields below say where to find it.' },
|
|
159
|
+
historyInfluxUrl: { type: 'string', title: '…InfluxDB URL', description: 'Where the archive lives. The default is an InfluxDB on this machine.', default: 'http://127.0.0.1:8086' },
|
|
160
|
+
historyOrg: { type: 'string', title: '…organization', description: 'Default matches signalk-to-influxdb-v2.', default: 'signalk' },
|
|
161
|
+
historyBucket: { type: 'string', title: '…bucket', description: 'Default matches signalk-to-influxdb-v2. An imported archive often uses its own name.', default: 'signalk' }
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
164
|
}
|
|
@@ -220,6 +237,22 @@ module.exports = function (app) {
|
|
|
220
237
|
const oldSeed = p.seed || {}
|
|
221
238
|
const oldPrefetch = p.prefetch || {}
|
|
222
239
|
const oldHistory = p.history || {}
|
|
240
|
+
// History source, resolved before the modules start so it can be reported. A token
|
|
241
|
+
// is the switch: present = query the archive, absent = the built-in ring.
|
|
242
|
+
const hUrl = pickHistoryField(p.historyInfluxUrl, oldHistory.influxUrl, HISTORY_TUNING.influxUrl)
|
|
243
|
+
const hOrg = pickHistoryField(p.historyOrg, oldHistory.org, HISTORY_TUNING.org)
|
|
244
|
+
const hBucket = pickHistoryField(p.historyBucket, oldHistory.bucket, HISTORY_TUNING.bucket)
|
|
245
|
+
const hToken = String(p.historyToken || oldHistory.token || '').trim()
|
|
246
|
+
const fromLegacy = [['URL', hUrl], ['org', hOrg], ['bucket', hBucket]].filter(([, v]) => v.from === 'legacy')
|
|
247
|
+
if (hToken) {
|
|
248
|
+
console.log(`[sailkick-boat] history -> archive ${hUrl.value} org=${hOrg.value} bucket=${hBucket.value}`)
|
|
249
|
+
if (fromLegacy.length) {
|
|
250
|
+
;(app.error || console.error)(`[sailkick-boat] history archive ${fromLegacy.map(([n]) => n).join(' + ')} taken from the old proxy.history config, not the new field(s) — copy the value(s) up to keep them after the next save`)
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
console.log('[sailkick-boat] history -> built-in ring (no archive token set)')
|
|
254
|
+
}
|
|
255
|
+
|
|
223
256
|
if (upstream.ignored) {
|
|
224
257
|
;(app.error || console.error)(`[sailkick-boat] ignoring proxy.sailkickUrl "${upstream.ignored}" left over from an older config — mirroring ${upstream.url}. Set proxy.selfHosted:true to keep your own server.`)
|
|
225
258
|
}
|
|
@@ -251,10 +284,10 @@ module.exports = function (app) {
|
|
|
251
284
|
history: {
|
|
252
285
|
...HISTORY_TUNING,
|
|
253
286
|
enabled: oldHistory.enabled !== false,
|
|
254
|
-
influxUrl:
|
|
255
|
-
org:
|
|
256
|
-
bucket:
|
|
257
|
-
token:
|
|
287
|
+
influxUrl: hUrl.value,
|
|
288
|
+
org: hOrg.value,
|
|
289
|
+
bucket: hBucket.value,
|
|
290
|
+
token: hToken,
|
|
258
291
|
ringPersist: oldHistory.ringPersist !== false,
|
|
259
292
|
ringWindowSec: oldHistory.ringWindowSec || HISTORY_TUNING.ringWindowSec,
|
|
260
293
|
ringSampleSec: oldHistory.ringSampleSec || HISTORY_TUNING.ringSampleSec,
|
package/lib/history/ring.js
CHANGED
|
@@ -24,12 +24,28 @@ const DEG = Math.PI / 180
|
|
|
24
24
|
const wrap360 = (d) => ((d % 360) + 360) % 360
|
|
25
25
|
const MAX_SAMPLES = 50000
|
|
26
26
|
|
|
27
|
-
// True wind (TWS kt, TWD ° the wind blows FROM)
|
|
27
|
+
// True wind (TWS kt, TWD ° the wind blows FROM).
|
|
28
|
+
//
|
|
29
|
+
// Prefer what the instruments publish. A wind system computes true wind from its own
|
|
30
|
+
// calibrated apparent wind with heel and leeway correction and a water-referenced boat
|
|
31
|
+
// speed — strictly better than anything derivable here, and it is what every other
|
|
32
|
+
// display on board shows, so deriving a second answer would just disagree with them.
|
|
33
|
+
//
|
|
34
|
+
// Only when the boat publishes no true wind do we derive it, and then from speed
|
|
35
|
+
// THROUGH WATER: true wind is defined relative to the boat's motion through the water,
|
|
36
|
+
// not over the ground. Using SOG (as this did before v0.14.6) skews both TWS and TWD in
|
|
37
|
+
// any tidal stream — several degrees and about a knot in 3 kt of tide, which is exactly
|
|
38
|
+
// the error you would be trimming against. SOG remains the last resort for boats with
|
|
39
|
+
// no paddlewheel, where it is at least right in slack water.
|
|
28
40
|
function trueWind (s) {
|
|
41
|
+
if (Number.isFinite(s.twsKt) && Number.isFinite(s.twdDeg)) {
|
|
42
|
+
return { tws: s.twsKt, twd: wrap360(s.twdDeg), measured: true }
|
|
43
|
+
}
|
|
29
44
|
if (!Number.isFinite(s.awsKt) || !Number.isFinite(s.awaDeg) || !Number.isFinite(s.headingDeg)) return null
|
|
45
|
+
const boatSpeed = Number.isFinite(s.stwKt) ? s.stwKt : (Number.isFinite(s.sogKt) ? s.sogKt : 0)
|
|
30
46
|
const a = s.awaDeg * DEG
|
|
31
|
-
const x = s.awsKt * Math.cos(a) -
|
|
32
|
-
return { tws: Math.hypot(x, y), twd: wrap360(s.headingDeg + Math.atan2(y, x) / DEG) }
|
|
47
|
+
const x = s.awsKt * Math.cos(a) - boatSpeed; const y = s.awsKt * Math.sin(a)
|
|
48
|
+
return { tws: Math.hypot(x, y), twd: wrap360(s.headingDeg + Math.atan2(y, x) / DEG), measured: false }
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
class RingHistoryProvider {
|
|
@@ -61,6 +77,7 @@ class RingHistoryProvider {
|
|
|
61
77
|
const row = {
|
|
62
78
|
t: Date.now(),
|
|
63
79
|
sog: num(s.sogKt), cog: num(s.cogDeg), heading: num(s.headingDeg),
|
|
80
|
+
stw: num(s.stwKt),
|
|
64
81
|
aws: num(s.awsKt), awa: num(s.awaDeg), depth: num(s.depthM),
|
|
65
82
|
tws: tw ? tw.tws : null, twd: tw ? tw.twd : null,
|
|
66
83
|
lat: num(s.lat), lon: num(s.lon)
|
|
@@ -80,7 +97,10 @@ class RingHistoryProvider {
|
|
|
80
97
|
|
|
81
98
|
getSeries ({ windowSec } = {}) {
|
|
82
99
|
const rows = this._window(windowSec)
|
|
83
|
-
|
|
100
|
+
// Matches the InfluxDB provider's channel set exactly (lib/history/index.js MAP), so
|
|
101
|
+
// the Trends panel looks the same whichever provider is serving. Empty channels are
|
|
102
|
+
// omitted, so a boat with no paddlewheel simply has no `stw` key.
|
|
103
|
+
const chans = ['tws', 'twd', 'aws', 'awa', 'sog', 'stw', 'heading', 'depth']
|
|
84
104
|
const series = {}
|
|
85
105
|
for (const c of chans) {
|
|
86
106
|
const pts = rows.filter((r) => r[c] != null).map((r) => [r.t, r[c]])
|
package/lib/proxy/index.js
CHANGED
|
@@ -18,6 +18,20 @@ const { countBboxTiles, bboxTiles, boxAround } = require('./tiles')
|
|
|
18
18
|
// get its live data from the boat's local SignalK and its charts/weather cached.
|
|
19
19
|
// Also exposes the auth'd /plugins/sailkick-boat/p/* router handlers.
|
|
20
20
|
|
|
21
|
+
// Everything under /api/ is dynamic and network-first — EXCEPT paths whose URL is
|
|
22
|
+
// immutable by construction. Velocity tiles embed the forecast run id
|
|
23
|
+
// (/api/velocity/tiles/<layer>/<runId>/<z>/<x>/<y>/<hour>.f32, see the app's
|
|
24
|
+
// public/engine/velocity-client.js), so a given URL's bytes never change: a new
|
|
25
|
+
// forecast run produces new URLs. Treating them as network-first re-downloaded the
|
|
26
|
+
// whole wind field on every pan while online, and left nothing usable offline —
|
|
27
|
+
// pinning them is what makes wind particles and the storm field work with no uplink.
|
|
28
|
+
//
|
|
29
|
+
// The velocity *manifest* is deliberately NOT in here: it resolves `run=latest` and
|
|
30
|
+
// must stay live, otherwise the boat pins itself to a stale forecast run forever.
|
|
31
|
+
const IMMUTABLE_API_PREFIXES = ['/api/velocity/tiles/']
|
|
32
|
+
const isImmutableApi = (p) => IMMUTABLE_API_PREFIXES.some((x) => p.startsWith(x))
|
|
33
|
+
const isNetworkFirst = (p) => p.startsWith('/api/') && !isImmutableApi(p)
|
|
34
|
+
|
|
21
35
|
function createProxy (app, options) {
|
|
22
36
|
const log = (m) => (app.debug ? app.debug('[proxy] ' + m) : console.log('[sailkick-boat:proxy]', m))
|
|
23
37
|
let cfg = null
|
|
@@ -138,8 +152,10 @@ function createProxy (app, options) {
|
|
|
138
152
|
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
139
153
|
const { buffer, contentType, cacheState } = await getResource({
|
|
140
154
|
storeDir: cfg.storeDir, upstream: cfg.upstream, reqPath: req.url, timeoutMs: cfg.timeoutMs,
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
// An immutable URL needs no invalidation: a bake announcement can't change
|
|
156
|
+
// bytes that are keyed by run id, so don't let an app rebuild churn them.
|
|
157
|
+
invalidatedAt: isImmutableApi(req.url) ? 0 : (manifest ? manifest.invalidatedAtFor(req.url) : 0),
|
|
158
|
+
networkFirst: isNetworkFirst(req.url), // live data (AIS, weather, lightning): fresh online, cache-fallback offline
|
|
143
159
|
health
|
|
144
160
|
})
|
|
145
161
|
res.setHeader('Content-Type', contentType)
|
|
@@ -242,8 +258,8 @@ function createProxy (app, options) {
|
|
|
242
258
|
try {
|
|
243
259
|
const { buffer, contentType, cacheState } = await getResource({
|
|
244
260
|
storeDir: cfg.storeDir, upstream: cfg.upstream, reqPath, timeoutMs: cfg.timeoutMs,
|
|
245
|
-
invalidatedAt: manifest ? manifest.invalidatedAtFor(reqPath) : 0,
|
|
246
|
-
networkFirst: reqPath
|
|
261
|
+
invalidatedAt: isImmutableApi(reqPath) ? 0 : (manifest ? manifest.invalidatedAtFor(reqPath) : 0),
|
|
262
|
+
networkFirst: isNetworkFirst(reqPath), health
|
|
247
263
|
})
|
|
248
264
|
res.set('Content-Type', contentType)
|
|
249
265
|
res.set('X-Sailkick-Cache', cacheState)
|
|
@@ -390,4 +406,4 @@ function createProxy (app, options) {
|
|
|
390
406
|
return { start, stop, status, handleGet, handlePrefetch, handlePrefetchRegion, handleClear, _serveMirror: serveMirror, _serveConfig: serveConfig, _manifest: () => manifest, _seeder: () => seeder, _startAreaPrefetch: startAreaPrefetch, _area: () => area }
|
|
391
407
|
}
|
|
392
408
|
|
|
393
|
-
module.exports = { createProxy }
|
|
409
|
+
module.exports = { createProxy, isNetworkFirst, isImmutableApi }
|
|
@@ -27,6 +27,9 @@ function signalkValuesToPatch (values) {
|
|
|
27
27
|
case 'navigation.speedOverGround':
|
|
28
28
|
if (Number.isFinite(v.value)) patch.sogKt = Math.max(0, v.value * MS_TO_KT)
|
|
29
29
|
break
|
|
30
|
+
case 'navigation.speedThroughWater':
|
|
31
|
+
if (Number.isFinite(v.value)) patch.stwKt = Math.max(0, v.value * MS_TO_KT)
|
|
32
|
+
break
|
|
30
33
|
case 'navigation.courseOverGroundTrue':
|
|
31
34
|
if (Number.isFinite(v.value)) patch.cogDeg = wrap360(v.value * RAD2DEG)
|
|
32
35
|
break
|
|
@@ -45,10 +48,34 @@ function signalkValuesToPatch (values) {
|
|
|
45
48
|
case 'environment.wind.angleApparent':
|
|
46
49
|
if (Number.isFinite(v.value)) patch.awaDeg = wrap180(v.value * RAD2DEG)
|
|
47
50
|
break
|
|
51
|
+
case 'environment.wind.speedTrue':
|
|
52
|
+
if (Number.isFinite(v.value)) patch.twsKt = Math.max(0, v.value * MS_TO_KT)
|
|
53
|
+
break
|
|
54
|
+
case 'environment.wind.directionTrue': // absolute compass direction (not off-bow)
|
|
55
|
+
if (Number.isFinite(v.value)) patch.twdDeg = wrap360(v.value * RAD2DEG)
|
|
56
|
+
break
|
|
48
57
|
case 'environment.depth.belowSurface':
|
|
49
58
|
case 'environment.depth.belowTransducer':
|
|
50
59
|
if (Number.isFinite(v.value)) patch.depthM = v.value
|
|
51
60
|
break
|
|
61
|
+
case 'steering.rudderAngle':
|
|
62
|
+
if (Number.isFinite(v.value)) patch.rudderDeg = v.value * RAD2DEG
|
|
63
|
+
break
|
|
64
|
+
case 'navigation.rateOfTurn':
|
|
65
|
+
if (Number.isFinite(v.value)) patch.rotDegMin = v.value * RAD2DEG * 60
|
|
66
|
+
break
|
|
67
|
+
case 'navigation.attitude': // object { roll, pitch, yaw } (rad)
|
|
68
|
+
if (v.value && Number.isFinite(v.value.roll)) patch.heelDeg = v.value.roll * RAD2DEG
|
|
69
|
+
break
|
|
70
|
+
case 'steering.autopilot.state':
|
|
71
|
+
if (typeof v.value === 'string') patch.apState = v.value
|
|
72
|
+
break
|
|
73
|
+
case 'steering.autopilot.target.headingTrue':
|
|
74
|
+
if (Number.isFinite(v.value)) patch.apTargetDeg = wrap360(v.value * RAD2DEG)
|
|
75
|
+
break
|
|
76
|
+
case 'steering.autopilot.target.windAngleApparent':
|
|
77
|
+
if (Number.isFinite(v.value)) patch.apTargetAwa = wrap180(v.value * RAD2DEG)
|
|
78
|
+
break
|
|
52
79
|
case 'navigation.datetime':
|
|
53
80
|
if (typeof v.value === 'string') patch.gpsTime = v.value
|
|
54
81
|
break
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sailkick-boat",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.7",
|
|
4
4
|
"description": "EARLY ALPHA — cloud telemetry + offline maps for sailkick boats (www.sailkick.io; register on the web, paste the write token). Gapless boat→cloud telemetry sync to InfluxDB, and a local proxy that keeps the sailkick app and its charts/maps working fully offline on board.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|