sailkick-boat 0.20.1 → 0.21.1
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 +32 -9
- package/index.js +6 -3
- package/lib/account/index.js +19 -5
- package/lib/ais/index.js +10 -1
- package/lib/backfill/index.js +19 -3
- package/lib/sync/index.js +39 -4
- package/lib/sync/influxWrite.js +18 -5
- package/package.json +8 -1
- package/public/icon.png +0 -0
- package/public/index.html +3 -2
package/README.md
CHANGED
|
@@ -52,8 +52,13 @@ files to send later — nothing is lost in the gap.
|
|
|
52
52
|
|
|
53
53
|
- Network errors, `429` and `5xx` are retried with backoff (1 s → 60 s); the data stays
|
|
54
54
|
on disk.
|
|
55
|
-
- A `4xx` is quarantined to `spool/dead/`
|
|
56
|
-
|
|
55
|
+
- A malformed batch (`4xx` other than the three below) is quarantined to `spool/dead/`
|
|
56
|
+
rather than retried forever, because it would otherwise wedge the queue behind it.
|
|
57
|
+
- **`404`, `401` and `403` are held, not quarantined.** A missing bucket or a rejected
|
|
58
|
+
token rejects *every* batch identically, so quarantining would pour the whole telemetry
|
|
59
|
+
stream into `spool/dead/` while the plugin looked busy. Instead the spool is left
|
|
60
|
+
intact, the status line names the setting to fix, and retries continue slowly so a
|
|
61
|
+
correction recovers on its own.
|
|
57
62
|
- The buffer is bounded (500 MB). On overflow the **oldest** files are dropped and
|
|
58
63
|
logged — a long-offline boat fills its own disk otherwise.
|
|
59
64
|
- Timestamps are nanosecond-precise, so replaying after a reconnect overwrites rather
|
|
@@ -302,9 +307,15 @@ arriving now, and the spool only replays what it captured itself while offline.
|
|
|
302
307
|
|
|
303
308
|
Fill the **Copy older history to the cloud** section and save. It walks backwards in
|
|
304
309
|
one-hour windows (newest first, so recent history lands first), resumes after a restart
|
|
305
|
-
from a manifest, and stands aside whenever live telemetry has a backlog — the data-
|
|
310
|
+
from a manifest, and stands aside whenever live telemetry has a **backlog** — the data-
|
|
306
311
|
critical path is never starved by a bulk upload. Progress shows in the status line.
|
|
307
312
|
|
|
313
|
+
"Backlog" means several files waiting, not merely an upload in flight. Requiring an empty
|
|
314
|
+
spool was a race: live sync flushes every second, so once the round trip to the cloud grew
|
|
315
|
+
past a second the spool was never empty for an instant and the backfill stood down for
|
|
316
|
+
ever — silently, since that path logs nothing. It now yields at a real backlog, which an
|
|
317
|
+
outage produces within a minute, and logs both the stand-down and the resume.
|
|
318
|
+
|
|
308
319
|
It needs a **cloud read+write token**, not the write token from signup. Every hour it
|
|
309
320
|
uploads is verified by counting the destination, and a write-only token cannot read. A
|
|
310
321
|
`204` means InfluxDB accepted the bytes, not that every point landed — without the count
|
|
@@ -428,12 +439,24 @@ online boat is never worse off than before.
|
|
|
428
439
|
is needed — see above). The signup screen shows your boat's ingest credentials.
|
|
429
440
|
2. **Copy the "Write token"** — it is shown **once** and cannot be recovered. If you
|
|
430
441
|
lose it you need a new one minted.
|
|
431
|
-
3. In Plugin Config, fill the **Sailkick account** section — **
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
That is the whole handshake. The plugin resolves everything else locally (`
|
|
435
|
-
|
|
436
|
-
|
|
442
|
+
3. In Plugin Config, fill the **Sailkick account** section — **write token**, plus
|
|
443
|
+
either the **boat name** or the **data bucket** — and save.
|
|
444
|
+
|
|
445
|
+
That is the whole handshake. The plugin resolves everything else locally (`org` =
|
|
446
|
+
`sailkick`) and never calls the app for configuration, so setup works with no internet
|
|
447
|
+
and there is nothing to re-fetch after a restart.
|
|
448
|
+
|
|
449
|
+
**Data bucket.** Leave it blank and the bucket is derived as `<boat name>_raw`, which is
|
|
450
|
+
what older accounts use. Newer accounts are identified by a UUID instead — their bucket
|
|
451
|
+
looks like `1fcad258-c422-4e93-a6f9-6811938499f6_raw` — so paste that into **Data
|
|
452
|
+
bucket** and the boat name becomes optional. If your bucket is ever renamed, this is the
|
|
453
|
+
one field to change: the write token survives a rename, because InfluxDB scopes tokens by
|
|
454
|
+
bucket *id* rather than by name.
|
|
455
|
+
|
|
456
|
+
A bucket that does not exist is treated as a **configuration error, not bad data**:
|
|
457
|
+
telemetry is held on disk, nothing is quarantined, the status line reads
|
|
458
|
+
`sync: HELD — bucket "…" not found`, and it sends itself as soon as the name is
|
|
459
|
+
corrected — no restart needed. The same applies to a rejected token.
|
|
437
460
|
|
|
438
461
|
> **Upgrading from before 0.14?** Old versions had visible `influxUrl` / `sailkickUrl`
|
|
439
462
|
> fields. Whatever was typed into them is still in your saved config, and since 0.14.4
|
package/index.js
CHANGED
|
@@ -121,8 +121,9 @@ module.exports = function (app) {
|
|
|
121
121
|
title: 'Sailkick account',
|
|
122
122
|
description: 'Register your boat at www.sailkick.io first — the signup screen shows a write token. Paste it here together with your boat name. Keep that token safe: it is shown only once and cannot be recovered.',
|
|
123
123
|
properties: {
|
|
124
|
-
slug: { type: 'string', title: 'Boat name', description: 'Exactly as registered on the website.' },
|
|
125
|
-
writeToken: { type: 'string', title: 'Write token', description: 'From the signup screen ("Write token").
|
|
124
|
+
slug: { type: 'string', title: 'Boat name', description: 'Exactly as registered on the website. Newer accounts are identified by a data bucket instead — if you have one, fill that in below and leave this blank.' },
|
|
125
|
+
writeToken: { type: 'string', title: 'Write token', description: 'From the signup screen ("Write token"). The Influx URL and organization are fleet-wide and set for you.' },
|
|
126
|
+
bucket: { type: 'string', title: 'Data bucket', description: 'Leave blank to derive it from the boat name. Set it when your account has an explicit bucket — newer ones are a UUID, e.g. 1fcad258-c422-4e93-a6f9-6811938499f6_raw. Copy it exactly; a bucket that does not exist stops sync rather than losing data, and the status line will say so.' }
|
|
126
127
|
}
|
|
127
128
|
},
|
|
128
129
|
sync: {
|
|
@@ -191,7 +192,9 @@ module.exports = function (app) {
|
|
|
191
192
|
// Purely local: the boat registers on the website, the owner pastes the write
|
|
192
193
|
// token here. Nothing to fetch, so nothing that can fail while offline.
|
|
193
194
|
const r = resolveAccountConfig(app, opts.account)
|
|
194
|
-
|
|
195
|
+
// Name the BUCKET, not just the boat: a bucket that no longer exists (a rename, a
|
|
196
|
+
// typo) is the one misconfiguration that stops sync dead, and it was invisible here.
|
|
197
|
+
if (r.bundle) accountStatus = `account: ${r.bundle.slug || '(no name)'} -> ${r.bundle.bucket}`
|
|
195
198
|
else if (r.error) accountStatus = `account: ${r.error}`
|
|
196
199
|
else accountStatus = 'account: not configured — register at www.sailkick.io, then paste your write token'
|
|
197
200
|
startModules(opts, r.bundle)
|
package/lib/account/index.js
CHANGED
|
@@ -8,10 +8,19 @@
|
|
|
8
8
|
// is the whole handshake. The plugin deliberately does NOT sign up on its own —
|
|
9
9
|
// one registration path means no half-created accounts and no spent-invite dead ends.
|
|
10
10
|
//
|
|
11
|
-
// Everything else about the bundle
|
|
11
|
+
// Everything else about the bundle WAS derivable, so only two fields used to be asked for:
|
|
12
12
|
// bucket = "<slug>_raw" (the app's convention — server/auth/registry.js bucketFor)
|
|
13
13
|
// org = "sailkick" (single org, fleet-wide)
|
|
14
14
|
//
|
|
15
|
+
// That derivation is no longer reliable. The fleet is moving boat identity from a slug to
|
|
16
|
+
// a UUID, and buckets are being renamed with it — this boat went from "addiction_raw" to
|
|
17
|
+
// "1fcad258-c422-4e93-a6f9-6811938499f6_raw" while keeping the same token, because
|
|
18
|
+
// InfluxDB scopes tokens by bucket ID and a rename preserves it. So `bucket` is now an
|
|
19
|
+
// explicit field: blank keeps the old derivation (every pre-existing install is
|
|
20
|
+
// untouched), set wins verbatim. The bucket is the actual contract with InfluxDB, so it
|
|
21
|
+
// is the thing worth being able to state directly — it survives whatever identity the
|
|
22
|
+
// fleet settles on.
|
|
23
|
+
//
|
|
15
24
|
// This bundle deliberately carries NO Influx URL. "Which boat am I" and "where is the
|
|
16
25
|
// cloud" are separate questions, and the second one has a single fleet-wide answer that
|
|
17
26
|
// index.js owns. Letting a pasted or cached value set the host is how telemetry ends up
|
|
@@ -56,15 +65,20 @@ const clean = (v) => String(v == null ? '' : v).trim()
|
|
|
56
65
|
function resolveAccountConfig (app, account) {
|
|
57
66
|
const acct = account || {}
|
|
58
67
|
const slug = clean(acct.slug).toLowerCase()
|
|
68
|
+
// NOT lowercased: a bucket name is taken verbatim from whatever the server called it.
|
|
69
|
+
const bucket = clean(acct.bucket)
|
|
59
70
|
const writeToken = clean(acct.writeToken)
|
|
60
71
|
|
|
61
|
-
|
|
72
|
+
// Either identifier will do. A boat given an explicit bucket needs no slug at all —
|
|
73
|
+
// newer accounts are issued a UUID bucket and may never see a slug — while an older
|
|
74
|
+
// install that only has a boat name keeps deriving as before.
|
|
75
|
+
if (writeToken && (slug || bucket)) {
|
|
62
76
|
return {
|
|
63
77
|
source: 'config',
|
|
64
78
|
bundle: {
|
|
65
79
|
slug,
|
|
66
80
|
org: clean(acct.org) || DEFAULT_ORG,
|
|
67
|
-
bucket:
|
|
81
|
+
bucket: bucket || `${slug}_raw`,
|
|
68
82
|
writeToken
|
|
69
83
|
}
|
|
70
84
|
}
|
|
@@ -74,8 +88,8 @@ function resolveAccountConfig (app, account) {
|
|
|
74
88
|
if (cached) return { bundle: cached, source: 'cache' }
|
|
75
89
|
|
|
76
90
|
// Half-filled: say which half, so the status line is actionable.
|
|
77
|
-
if (writeToken) return { bundle: null, source: null, error: 'boat name is missing' }
|
|
78
|
-
if (slug) return { bundle: null, source: null, error: 'write token is missing' }
|
|
91
|
+
if (writeToken) return { bundle: null, source: null, error: 'boat name or data bucket is missing' }
|
|
92
|
+
if (slug || bucket) return { bundle: null, source: null, error: 'write token is missing' }
|
|
79
93
|
return { bundle: null, source: null }
|
|
80
94
|
}
|
|
81
95
|
|
package/lib/ais/index.js
CHANGED
|
@@ -205,7 +205,12 @@ function createAis (app, options) {
|
|
|
205
205
|
if (!body.trim()) { await state.spool.remove(file); continue }
|
|
206
206
|
const res = await writeLines({ influxUrl: cfg.influxUrl, org: cfg.org, bucket: cfg.bucket, token: cfg.token, timeoutMs: cfg.requestTimeoutMs }, body)
|
|
207
207
|
if (res.ok) {
|
|
208
|
-
await state.spool.remove(file); state.lastOkAt = new Date(); state.backoff = cfg.retryMinMs
|
|
208
|
+
await state.spool.remove(file); state.lastOkAt = new Date(); state.backoff = cfg.retryMinMs; state.configError = null
|
|
209
|
+
} else if (res.configError) {
|
|
210
|
+
// Same rule as telemetry: a missing bucket or bad token rejects everything, so
|
|
211
|
+
// hold rather than quarantine and let a corrected config drain the spool.
|
|
212
|
+
state.configError = res.status === 404 ? `bucket "${cfg.bucket}" not found` : `token rejected (HTTP ${res.status})`
|
|
213
|
+
state.pumping = false; scheduleRetry(res); return
|
|
209
214
|
} else if (res.retryable) {
|
|
210
215
|
state.pumping = false; scheduleRetry(res); return
|
|
211
216
|
} else {
|
|
@@ -238,6 +243,10 @@ function createAis (app, options) {
|
|
|
238
243
|
}
|
|
239
244
|
function refreshStatus (suffix) {
|
|
240
245
|
if (!state) return
|
|
246
|
+
if (state.configError) {
|
|
247
|
+
state.statusLine = `ais: HELD — ${state.configError}; ${state.forwarded} point(s) on disk`
|
|
248
|
+
return
|
|
249
|
+
}
|
|
241
250
|
state.statusLine = `ais: ${state.targets.size} target(s), ${state.forwarded} point(s)${state.dropped ? `, ${state.dropped} skipped` : ''}${suffix ? '; ' + suffix : ''}`
|
|
242
251
|
}
|
|
243
252
|
|
package/lib/backfill/index.js
CHANGED
|
@@ -41,6 +41,14 @@ const DEFAULTS = {
|
|
|
41
41
|
minWindowMs: 60000, // never subdivide below a minute
|
|
42
42
|
idleMs: 250, // pending() is the real backpressure; this is just politeness
|
|
43
43
|
backlogWaitMs: 15000, // how long to stand down when live sync has a backlog
|
|
44
|
+
// Stand down at a genuine BACKLOG, not at any activity. Requiring an EMPTY spool was a
|
|
45
|
+
// race rather than a design: live sync flushes every second, and a file exists for as
|
|
46
|
+
// long as its upload is in flight. Once the round trip to the cloud grew past the flush
|
|
47
|
+
// interval — which it did — the spool stopped being empty even for an instant (measured
|
|
48
|
+
// on the boat: non-empty in 30 of 30 one-second samples) and the backfill parked for
|
|
49
|
+
// ever, silently, because that path logs nothing. A real backlog is unmistakable by
|
|
50
|
+
// comparison: an outage puts hundreds of files on disk within a minute.
|
|
51
|
+
backlogFiles: 5,
|
|
44
52
|
maxErrorStreak: 5,
|
|
45
53
|
queryTimeoutMs: 120000
|
|
46
54
|
}
|
|
@@ -227,7 +235,11 @@ function createBackfill (app, options) {
|
|
|
227
235
|
const res = await writeLines({ influxUrl: cfg.dst.url, org: cfg.dst.org, bucket: cfg.dst.bucket, token: cfg.dst.token, timeoutMs: cfg.queryTimeoutMs }, body)
|
|
228
236
|
if (!res.ok) {
|
|
229
237
|
// 4xx is fatal: bad credentials or malformed data, and retrying cannot fix it.
|
|
230
|
-
if (!res.retryable) {
|
|
238
|
+
if (!res.retryable) {
|
|
239
|
+
// Name the destination: after a bucket rename this is the first thing to check.
|
|
240
|
+
warn(`write to ${cfg.dst.bucket} REJECTED (HTTP ${res.status}) — ${res.body ? String(res.body).slice(0, 160) : 'no detail'}`)
|
|
241
|
+
return 'fatal'
|
|
242
|
+
}
|
|
231
243
|
warn(`write ${iso(startMs)} failed — ${res.status ? 'HTTP ' + res.status : 'unreachable'}; will retry`)
|
|
232
244
|
return 'retry'
|
|
233
245
|
}
|
|
@@ -290,14 +302,18 @@ function createBackfill (app, options) {
|
|
|
290
302
|
const key = iso(startMs)
|
|
291
303
|
if (state.done[key]) continue
|
|
292
304
|
|
|
293
|
-
// Live telemetry is data-critical; this is not. Stand down while it is
|
|
305
|
+
// Live telemetry is data-critical; this is not. Stand down while it is BEHIND —
|
|
306
|
+
// which means a backlog, not merely an upload in flight. See cfg.backlogFiles.
|
|
307
|
+
let waited = false
|
|
294
308
|
while (!stopped && cfg.pending) {
|
|
295
309
|
let depth = 0
|
|
296
310
|
try { depth = (await cfg.pending()).count || 0 } catch {}
|
|
297
|
-
if (
|
|
311
|
+
if (depth < cfg.backlogFiles) break
|
|
312
|
+
if (!waited) { waited = true; log(`standing down — live sync is behind (${depth} file(s)); telemetry has the link`) }
|
|
298
313
|
statusLine = `backfill: paused — live sync backlog (${depth} file(s))`
|
|
299
314
|
await sleep(cfg.backlogWaitMs)
|
|
300
315
|
}
|
|
316
|
+
if (waited && !stopped) log('live sync caught up — resuming')
|
|
301
317
|
if (stopped) break
|
|
302
318
|
|
|
303
319
|
const tally = { written: 0 }
|
package/lib/sync/index.js
CHANGED
|
@@ -66,6 +66,27 @@ function createSync (app, options) {
|
|
|
66
66
|
if (state.failing) warn(`recovered — writing to ${cfg.influxUrl} again after ${state.failCount} failed attempt(s)`)
|
|
67
67
|
state.failing = false
|
|
68
68
|
state.failCount = 0
|
|
69
|
+
state.configError = null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A wrong bucket or token rejects every batch identically, so this is a standing
|
|
73
|
+
// condition rather than an outage: name the exact setting to fix, once, and keep the
|
|
74
|
+
// reason on the status line until it clears.
|
|
75
|
+
const noteConfigError = (res) => {
|
|
76
|
+
state.failCount++
|
|
77
|
+
const notFound = res.status === 404
|
|
78
|
+
state.configError = notFound
|
|
79
|
+
? `bucket "${cfg.bucket}" not found`
|
|
80
|
+
: `write token rejected (HTTP ${res.status})`
|
|
81
|
+
const now = Date.now()
|
|
82
|
+
if (state.failing && now - state.lastWarnAt < WARN_REPEAT_MS) return
|
|
83
|
+
state.failing = true
|
|
84
|
+
state.lastWarnAt = now
|
|
85
|
+
warn(`sync HELD — ${state.configError}. ` +
|
|
86
|
+
(notFound
|
|
87
|
+
? 'Set "Data bucket" in the plugin config to the bucket your account actually uses — newer accounts use a UUID. '
|
|
88
|
+
: 'Check the write token, and that it is valid for this bucket. ') +
|
|
89
|
+
'Telemetry is kept on disk (nothing quarantined) and will send itself once this is corrected.')
|
|
69
90
|
}
|
|
70
91
|
|
|
71
92
|
const handleDelta = (delta) => {
|
|
@@ -100,13 +121,21 @@ function createSync (app, options) {
|
|
|
100
121
|
if (res.ok) {
|
|
101
122
|
await spool.remove(file); state.lastOkAt = new Date(); state.backoff = state.retryMin
|
|
102
123
|
noteSuccess()
|
|
124
|
+
} else if (res.configError) {
|
|
125
|
+
// The settings are wrong, not this batch — every batch would be rejected the
|
|
126
|
+
// same way. Quarantining here would pour the entire telemetry stream into
|
|
127
|
+
// spool/dead/ while the plugin looked busy. Hold everything on disk, say so
|
|
128
|
+
// once, and keep retrying slowly so a corrected bucket or token recovers
|
|
129
|
+
// without a restart.
|
|
130
|
+
noteConfigError(res)
|
|
131
|
+
state.pumping = false; scheduleRetry(res); return
|
|
103
132
|
} else if (res.retryable) {
|
|
104
133
|
noteFailure(res)
|
|
105
134
|
state.pumping = false; scheduleRetry(res); return
|
|
106
135
|
} else {
|
|
107
|
-
// 4xx:
|
|
108
|
-
//
|
|
109
|
-
warn(`batch REJECTED (HTTP ${res.status}) and quarantined to ${state.spool.deadDir} —
|
|
136
|
+
// Other 4xx: malformed data. Never retried, so if this is not loud the batch
|
|
137
|
+
// is simply gone.
|
|
138
|
+
warn(`batch REJECTED (HTTP ${res.status}) and quarantined to ${state.spool.deadDir} — check the data`)
|
|
110
139
|
await spool.quarantine(file)
|
|
111
140
|
}
|
|
112
141
|
}
|
|
@@ -133,7 +162,13 @@ function createSync (app, options) {
|
|
|
133
162
|
try {
|
|
134
163
|
const { count, bytes } = await spool.stats()
|
|
135
164
|
const last = state.lastOkAt ? state.lastOkAt.toISOString() : 'never'
|
|
136
|
-
|
|
165
|
+
// A config error is the headline — it needs the owner, not just time.
|
|
166
|
+
const head = state.configError
|
|
167
|
+
? `sync: HELD — ${state.configError}; fix the plugin config`
|
|
168
|
+
: `sync: buffered ${count}f/${Math.round(bytes / 1024)}KB; last ok ${last}`
|
|
169
|
+
state.statusLine = state.configError
|
|
170
|
+
? `${head}; ${count}f/${Math.round(bytes / 1024)}KB held on disk`
|
|
171
|
+
: `${head}${suffix ? '; ' + suffix : ''}`
|
|
137
172
|
} catch {}
|
|
138
173
|
}
|
|
139
174
|
|
package/lib/sync/influxWrite.js
CHANGED
|
@@ -4,12 +4,22 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Return shape:
|
|
6
6
|
// { ok: true, status: 204 }
|
|
7
|
-
// { ok: false, retryable: true, networkError: true }
|
|
8
|
-
// { ok: false, retryable: true, status }
|
|
9
|
-
// { ok: false,
|
|
7
|
+
// { ok: false, retryable: true, networkError: true } connection failed
|
|
8
|
+
// { ok: false, retryable: true, status } 429 / 5xx (transient)
|
|
9
|
+
// { ok: false, configError: true, status, body } 404 / 401 / 403
|
|
10
|
+
// { ok: false, retryable: false, status, body } other 4xx (bad data)
|
|
10
11
|
//
|
|
11
|
-
// 4xx is non-retryable: retrying a malformed
|
|
12
|
-
//
|
|
12
|
+
// Most 4xx is non-retryable: retrying a malformed batch forever would wedge the queue,
|
|
13
|
+
// so the caller quarantines it instead.
|
|
14
|
+
//
|
|
15
|
+
// 404/401/403 are different in kind and must NOT be quarantined. They say "your settings
|
|
16
|
+
// are wrong", not "this batch is bad" — a missing bucket or a bad token rejects EVERY
|
|
17
|
+
// batch equally, so quarantining would feed the whole telemetry stream into spool/dead/
|
|
18
|
+
// while looking like normal operation. That is not hypothetical: the fleet renamed this
|
|
19
|
+
// boat's bucket from `addiction_raw` to a UUID, and a plugin left running through it
|
|
20
|
+
// would have shredded every point it collected. The caller holds these on disk instead
|
|
21
|
+
// and keeps retrying slowly, so correcting the config recovers on its own.
|
|
22
|
+
const CONFIG_ERROR_STATUS = new Set([401, 403, 404])
|
|
13
23
|
|
|
14
24
|
const zlib = require('zlib')
|
|
15
25
|
|
|
@@ -42,6 +52,9 @@ async function writeLines (cfg, body) {
|
|
|
42
52
|
|
|
43
53
|
let text = ''
|
|
44
54
|
try { text = await res.text() } catch {}
|
|
55
|
+
if (CONFIG_ERROR_STATUS.has(res.status)) {
|
|
56
|
+
return { ok: false, configError: true, retryable: false, status: res.status, body: text }
|
|
57
|
+
}
|
|
45
58
|
const retryable = res.status === 429 || res.status >= 500
|
|
46
59
|
return { ok: false, retryable, status: res.status, body: text }
|
|
47
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sailkick-boat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
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": {
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"keywords": [
|
|
10
10
|
"signalk-node-server-plugin",
|
|
11
11
|
"signalk-webapp",
|
|
12
|
+
"signalk-category-chart-plotters",
|
|
13
|
+
"signalk-category-instruments",
|
|
14
|
+
"signalk-category-ais",
|
|
12
15
|
"signalk",
|
|
13
16
|
"sailkick",
|
|
14
17
|
"influxdb",
|
|
@@ -17,6 +20,10 @@
|
|
|
17
20
|
"proxy"
|
|
18
21
|
],
|
|
19
22
|
"signalk-plugin-enabled-by-default": true,
|
|
23
|
+
"signalk": {
|
|
24
|
+
"appIcon": "./icon.png",
|
|
25
|
+
"displayName": "Sailkick"
|
|
26
|
+
},
|
|
20
27
|
"author": "lauchat",
|
|
21
28
|
"license": "MIT",
|
|
22
29
|
"repository": {
|
package/public/icon.png
ADDED
|
Binary file
|
package/public/index.html
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
body { margin:0; padding:2rem 1rem; background:var(--bg); color:var(--fg);
|
|
28
28
|
font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif }
|
|
29
29
|
main { max-width:44rem; margin:0 auto }
|
|
30
|
-
h1 { margin:0 0 .25rem; font-size:1.5rem; letter-spacing:-.01em }
|
|
30
|
+
h1 { margin:0 0 .25rem; font-size:1.5rem; letter-spacing:-.01em; display:flex; align-items:center; gap:.5rem }
|
|
31
|
+
h1 img { border-radius:.35rem }
|
|
31
32
|
.sub { color:var(--mut); margin:0 0 1.5rem }
|
|
32
33
|
.grid { display:grid; gap:1rem; grid-template-columns:repeat(auto-fit,minmax(15rem,1fr)) }
|
|
33
34
|
a.card { display:block; padding:1.1rem 1.2rem; background:var(--card); border:1px solid var(--line);
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
</head>
|
|
45
46
|
<body>
|
|
46
47
|
<main>
|
|
47
|
-
<h1>Sailkick</h1>
|
|
48
|
+
<h1><img src="icon.png" alt="" width="28" height="28">Sailkick</h1>
|
|
48
49
|
<p class="sub">Charts, instruments and AIS, served from this boat.</p>
|
|
49
50
|
|
|
50
51
|
<div class="grid">
|