sailkick-boat 0.14.3 → 0.14.5
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 +45 -4
- package/index.js +54 -20
- package/lib/proxy/index.js +21 -5
- package/lib/sync/index.js +35 -4
- 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
|
|
@@ -171,6 +180,12 @@ That is the whole handshake. The plugin resolves everything else locally (`bucke
|
|
|
171
180
|
`<slug>_raw`, `org` = `sailkick`) and never calls the app for configuration, so setup
|
|
172
181
|
works with no internet and there is nothing to re-fetch after a restart.
|
|
173
182
|
|
|
183
|
+
> **Upgrading from before 0.14?** Old versions had visible `influxUrl` / `sailkickUrl`
|
|
184
|
+
> fields. Whatever was typed into them is still in your saved config, and since 0.14.4
|
|
185
|
+
> those leftovers are **ignored** — the plugin says so in the log and the status line
|
|
186
|
+
> rather than silently obeying a dev address. To keep your own endpoints deliberately,
|
|
187
|
+
> set `sync.selfHosted: true` / `proxy.selfHosted: true` in the config JSON.
|
|
188
|
+
>
|
|
174
189
|
> **Ignore the "Influx URL", "Organization" and "Bucket" on that screen** — they are for
|
|
175
190
|
> the community `signalk-to-influxdb-v2` plugin. This plugin always writes to
|
|
176
191
|
> `https://sync.sailkick.io`; the endpoint is fleet-wide and cannot be set from the UI,
|
|
@@ -231,6 +246,32 @@ a VE.Can↔Micro-C drop cable; leave the spare RJ45 unterminated) or a USB GPS/N
|
|
|
231
246
|
gateway. Without a position source, the live boat / trends / area-download stay
|
|
232
247
|
idle — energy telemetry still syncs to the cloud.
|
|
233
248
|
|
|
249
|
+
## Troubleshooting: is telemetry actually leaving the boat?
|
|
250
|
+
The plugin logs one unconditional line at startup naming its real target:
|
|
251
|
+
```
|
|
252
|
+
[sailkick-boat] sync -> https://sync.sailkick.io org=sailkick bucket=<slug>_raw
|
|
253
|
+
```
|
|
254
|
+
If that shows anything other than the endpoint you expect, a stale config field is the
|
|
255
|
+
usual cause — see the upgrade note above.
|
|
256
|
+
|
|
257
|
+
Failures reach the **normal server log**, not just the debug channel:
|
|
258
|
+
- `cannot write to <url> — unreachable …; N failed attempt(s), telemetry is buffering
|
|
259
|
+
on disk` — once per outage, then every 5 min while it lasts, then a `recovered` line.
|
|
260
|
+
- `batch REJECTED (HTTP 401) and quarantined …` — the write token is not valid for that
|
|
261
|
+
bucket. These are never retried, so they are always logged.
|
|
262
|
+
- `sync: ⚠ writing to <url> — a private address` — a loopback/RFC1918 target, i.e.
|
|
263
|
+
nothing is reaching the cloud.
|
|
264
|
+
|
|
265
|
+
Nothing at all in the log means the plugin never started; check it is enabled. The
|
|
266
|
+
status line in Plugin Config carries the same information continuously.
|
|
267
|
+
|
|
268
|
+
To prove credentials independently of the plugin:
|
|
269
|
+
```bash
|
|
270
|
+
curl -i -XPOST "https://sync.sailkick.io/api/v2/write?org=sailkick&bucket=<slug>_raw&precision=ns" \
|
|
271
|
+
-H "Authorization: Token $TOKEN" --data-binary "probe,context=vessels.self,self=true,source=manual value=1"
|
|
272
|
+
```
|
|
273
|
+
`204` = good, `401` = token not valid for that bucket.
|
|
274
|
+
|
|
234
275
|
## Dev / tests
|
|
235
276
|
```bash
|
|
236
277
|
npm install && npm test # proxy: mirror/cache/offline + Express route; sync: subscribe+buffer
|
package/index.js
CHANGED
|
@@ -14,10 +14,18 @@ const { resolveAccountConfig } = require('./lib/account')
|
|
|
14
14
|
// Kept as separate modules so a proxy fault can't wedge the data-critical sync.
|
|
15
15
|
|
|
16
16
|
// Fixed cloud endpoints. Boats never choose these, so they are constants rather than
|
|
17
|
-
// config fields
|
|
18
|
-
//
|
|
17
|
+
// config fields, and NOTHING in a saved config may override them unless `selfHosted` is
|
|
18
|
+
// explicitly set (see resolveEndpoint).
|
|
19
|
+
//
|
|
20
|
+
// v0.14.3 kept `sync.influxUrl` / `proxy.sailkickUrl` as a silent escape hatch, on the
|
|
21
|
+
// reasoning that a value could only get there by deliberate hand-editing. That was
|
|
22
|
+
// wrong: both were VISIBLE FIELDS before v0.14.0, so every upgraded install still has
|
|
23
|
+
// whatever was typed into them — and since they are no longer in the schema, the owner
|
|
24
|
+
// can neither see nor clear them from the UI. One boat spent a day POSTing valid cloud
|
|
25
|
+
// credentials at a dev LAN address for exactly this reason. Stale wins over intent, so
|
|
26
|
+
// leftovers are now ignored and reported rather than obeyed.
|
|
19
27
|
const SAILKICK_APP_URL = 'https://www.sailkick.io' // signup + mirror upstream
|
|
20
|
-
const SAILKICK_INFLUX_URL = 'https://sync.sailkick.io' // data egress
|
|
28
|
+
const SAILKICK_INFLUX_URL = 'https://sync.sailkick.io' // data egress
|
|
21
29
|
|
|
22
30
|
// Tuning that has a right answer. Deliberately NOT on the config page: a boat owner has
|
|
23
31
|
// no basis to choose these, and a wrong value silently degrades sync or the cache.
|
|
@@ -45,17 +53,32 @@ const HISTORY_TUNING = {
|
|
|
45
53
|
ringSampleSec: 15
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
// A *cloud* endpoint on loopback means telemetry never leaves the
|
|
49
|
-
// is indistinguishable from a normal offline backlog — the spool
|
|
50
|
-
// to be called out explicitly.
|
|
51
|
-
//
|
|
52
|
-
|
|
56
|
+
// A *cloud* endpoint on a loopback or private address means telemetry never leaves the
|
|
57
|
+
// LAN. On the wire that is indistinguishable from a normal offline backlog — the spool
|
|
58
|
+
// just grows — so it has to be called out explicitly. Covers RFC1918 as well as
|
|
59
|
+
// loopback: the case that actually bit was a 192.168.x dev box, which a loopback-only
|
|
60
|
+
// check sailed straight past. (The local history DB is legitimately on 127.0.0.1; this
|
|
61
|
+
// is only ever applied to the sync path.)
|
|
62
|
+
function isPrivateHostUrl (u) {
|
|
53
63
|
try {
|
|
54
64
|
const h = new URL(u).hostname.replace(/^\[|\]$/g, '')
|
|
55
|
-
|
|
65
|
+
if (h === 'localhost' || h === '::1' || /^127\./.test(h)) return true
|
|
66
|
+
if (/^10\./.test(h) || /^192\.168\./.test(h)) return true
|
|
67
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true
|
|
68
|
+
return false
|
|
56
69
|
} catch { return false }
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
// Endpoint resolution with one rule: the constant wins unless self-hosting is declared.
|
|
73
|
+
// Returns { url, ignored } — `ignored` is the stale value we refused, so the caller can
|
|
74
|
+
// say so out loud instead of leaving the owner to guess why nothing arrives.
|
|
75
|
+
function resolveEndpoint (configured, constant, selfHosted) {
|
|
76
|
+
const v = typeof configured === 'string' ? configured.trim() : ''
|
|
77
|
+
if (!v || v === constant) return { url: constant, ignored: null }
|
|
78
|
+
if (selfHosted) return { url: v, ignored: null }
|
|
79
|
+
return { url: constant, ignored: v }
|
|
80
|
+
}
|
|
81
|
+
|
|
59
82
|
module.exports = function (app) {
|
|
60
83
|
let sync = null
|
|
61
84
|
let proxy = null
|
|
@@ -151,17 +174,17 @@ module.exports = function (app) {
|
|
|
151
174
|
const store = p.dataDir || p.storeDir || undefined
|
|
152
175
|
const spoolDir = s.spoolDir || (p.dataDir ? path.join(p.dataDir, 'spool') : undefined)
|
|
153
176
|
|
|
177
|
+
// Self-hosting is opt-in and explicit. Without it, a saved endpoint is treated as a
|
|
178
|
+
// leftover from an older version rather than as intent.
|
|
179
|
+
const selfHosted = s.selfHosted === true || p.selfHosted === true
|
|
180
|
+
const influx = resolveEndpoint(s.influxUrl, SAILKICK_INFLUX_URL, selfHosted)
|
|
181
|
+
const upstream = resolveEndpoint(p.sailkickUrl, SAILKICK_APP_URL, selfHosted)
|
|
182
|
+
|
|
154
183
|
// --- sync module (isolated: its failure must not affect the proxy) ---
|
|
155
|
-
// Pairing wins over hand-written fields; hand-written fields win over the constants.
|
|
156
184
|
if (s.enabled !== false) {
|
|
157
185
|
const syncOpts = {
|
|
158
186
|
...SYNC_TUNING,
|
|
159
|
-
|
|
160
|
-
// never set it — the app's signup screen hands out its own http://localhost:8086,
|
|
161
|
-
// and a boat that believed it would spool to loopback forever, looking healthy.
|
|
162
|
-
// `sync.influxUrl` survives only as a self-hosting escape hatch: it is not in
|
|
163
|
-
// the schema, so it cannot arrive by pasting, only by editing the config JSON.
|
|
164
|
-
influxUrl: s.influxUrl || SAILKICK_INFLUX_URL,
|
|
187
|
+
influxUrl: influx.url,
|
|
165
188
|
org: b.org || s.org || SYNC_TUNING.org,
|
|
166
189
|
bucket: b.bucket || s.bucket,
|
|
167
190
|
token: b.writeToken || s.token,
|
|
@@ -174,9 +197,17 @@ module.exports = function (app) {
|
|
|
174
197
|
retryMinMs: s.retryMinMs || SYNC_TUNING.retryMinMs,
|
|
175
198
|
retryMaxMs: s.retryMaxMs || SYNC_TUNING.retryMaxMs
|
|
176
199
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
// One unconditional line naming the actual target. This is the single most useful
|
|
201
|
+
// thing in the log: it would have shown the stale 192.168.x address instantly,
|
|
202
|
+
// instead of a day spent inferring it from an absence of errors.
|
|
203
|
+
console.log(`[sailkick-boat] sync -> ${syncOpts.influxUrl} org=${syncOpts.org} bucket=${syncOpts.bucket || '(none)'}${selfHosted ? ' [self-hosted]' : ''}`)
|
|
204
|
+
|
|
205
|
+
if (influx.ignored) {
|
|
206
|
+
syncWarning = `sync: ignoring stale influxUrl ${influx.ignored} — using ${influx.url}`
|
|
207
|
+
;(app.error || console.error)(`[sailkick-boat] ignoring sync.influxUrl "${influx.ignored}" left over from an older config — using ${influx.url}. Set sync.selfHosted:true to keep your own endpoint.`)
|
|
208
|
+
} else if (isPrivateHostUrl(syncOpts.influxUrl)) {
|
|
209
|
+
syncWarning = `sync: ⚠ writing to ${syncOpts.influxUrl} — a private address, telemetry is NOT reaching the cloud`
|
|
210
|
+
;(app.error || console.error)(`[sailkick-boat] sync target ${syncOpts.influxUrl} is a loopback/private address — telemetry will not reach the cloud`)
|
|
180
211
|
}
|
|
181
212
|
try { sync = createSync(app, syncOpts); sync.start() } catch (e) {
|
|
182
213
|
(app.error || console.error)('[sailkick-boat] sync start failed: ' + e.message)
|
|
@@ -189,8 +220,11 @@ module.exports = function (app) {
|
|
|
189
220
|
const oldSeed = p.seed || {}
|
|
190
221
|
const oldPrefetch = p.prefetch || {}
|
|
191
222
|
const oldHistory = p.history || {}
|
|
223
|
+
if (upstream.ignored) {
|
|
224
|
+
;(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
|
+
}
|
|
192
226
|
const pOpts = {
|
|
193
|
-
sailkickUrl:
|
|
227
|
+
sailkickUrl: upstream.url,
|
|
194
228
|
storeDir: store,
|
|
195
229
|
proxyPort: p.proxyPort == null ? 8080 : p.proxyPort,
|
|
196
230
|
localSignalkUrl: p.localSignalkUrl || 'http://127.0.0.1:3000',
|
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 }
|
package/lib/sync/index.js
CHANGED
|
@@ -13,6 +13,12 @@ const { deltaToLines } = require('./lineprotocol')
|
|
|
13
13
|
|
|
14
14
|
function createSync (app, options) {
|
|
15
15
|
const log = (m) => (app.debug ? app.debug('[sync] ' + m) : console.log('[sailkick-boat:sync]', m))
|
|
16
|
+
// Anything that stops telemetry reaching the cloud goes to `warn`, which lands in the
|
|
17
|
+
// normal server log. `log` is behind the plugin's debug key and is for detail only —
|
|
18
|
+
// an outage must never be visible ONLY in the status line, which is what made a
|
|
19
|
+
// day-long silent failure possible.
|
|
20
|
+
const warn = (m) => (app.error ? app.error('[sailkick-boat:sync] ' + m) : console.error('[sailkick-boat:sync]', m))
|
|
21
|
+
const WARN_REPEAT_MS = 300000 // re-state an ongoing outage every 5 min, not every retry
|
|
16
22
|
let state = null
|
|
17
23
|
|
|
18
24
|
function start () {
|
|
@@ -24,8 +30,9 @@ function createSync (app, options) {
|
|
|
24
30
|
timeoutMs: options.requestTimeoutMs || 30000
|
|
25
31
|
}
|
|
26
32
|
if (!cfg.influxUrl || !cfg.bucket || !cfg.token) {
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
const missing = [!cfg.influxUrl && 'influxUrl', !cfg.bucket && 'bucket', !cfg.token && 'write token'].filter(Boolean)
|
|
34
|
+
warn(`not started — missing ${missing.join(' + ')}. Telemetry is NOT being sent.`)
|
|
35
|
+
state = { statusLine: `sync: not configured (missing ${missing.join(', ')})`, stopped: true }
|
|
29
36
|
return
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -39,7 +46,26 @@ function createSync (app, options) {
|
|
|
39
46
|
flushTimer: null, pumpTimer: null, stopped: false, unsubscribes: [],
|
|
40
47
|
retryMin: options.retryMinMs || 1000, retryMax: options.retryMaxMs || 60000,
|
|
41
48
|
backoff: options.retryMinMs || 1000, idlePoll: Math.max(500, options.flushIntervalMs || 1000),
|
|
42
|
-
lastOkAt: null, pumping: false, statusLine: 'sync: starting'
|
|
49
|
+
lastOkAt: null, pumping: false, statusLine: 'sync: starting',
|
|
50
|
+
failing: false, failCount: 0, lastWarnAt: 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Outage transitions, not per-retry spam: the first failure, then at most one line
|
|
54
|
+
// every WARN_REPEAT_MS while it persists, then a recovery line. A boat that can
|
|
55
|
+
// never reach its endpoint used to produce no log output at all.
|
|
56
|
+
const noteFailure = (res) => {
|
|
57
|
+
state.failCount++
|
|
58
|
+
const now = Date.now()
|
|
59
|
+
if (state.failing && now - state.lastWarnAt < WARN_REPEAT_MS) return
|
|
60
|
+
state.failing = true
|
|
61
|
+
state.lastWarnAt = now
|
|
62
|
+
const why = res && res.status ? `HTTP ${res.status}` : `unreachable${res && res.error ? ` (${res.error})` : ''}`
|
|
63
|
+
warn(`cannot write to ${cfg.influxUrl} — ${why}; ${state.failCount} failed attempt(s), telemetry is buffering on disk`)
|
|
64
|
+
}
|
|
65
|
+
const noteSuccess = () => {
|
|
66
|
+
if (state.failing) warn(`recovered — writing to ${cfg.influxUrl} again after ${state.failCount} failed attempt(s)`)
|
|
67
|
+
state.failing = false
|
|
68
|
+
state.failCount = 0
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
const handleDelta = (delta) => {
|
|
@@ -73,10 +99,15 @@ function createSync (app, options) {
|
|
|
73
99
|
const res = await writeLines(cfg, body)
|
|
74
100
|
if (res.ok) {
|
|
75
101
|
await spool.remove(file); state.lastOkAt = new Date(); state.backoff = state.retryMin
|
|
102
|
+
noteSuccess()
|
|
76
103
|
} else if (res.retryable) {
|
|
104
|
+
noteFailure(res)
|
|
77
105
|
state.pumping = false; scheduleRetry(res); return
|
|
78
106
|
} else {
|
|
79
|
-
|
|
107
|
+
// 4xx: bad credentials or malformed data. Never retried, so if this is not
|
|
108
|
+
// loud the batch is simply gone.
|
|
109
|
+
warn(`batch REJECTED (HTTP ${res.status}) and quarantined to ${state.spool.deadDir} — ${res.status === 401 ? 'the write token is not valid for this bucket' : 'check bucket/org and the data'}`)
|
|
110
|
+
await spool.quarantine(file)
|
|
80
111
|
}
|
|
81
112
|
}
|
|
82
113
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sailkick-boat",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.5",
|
|
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": {
|