sailkick-boat 0.14.7 → 0.17.2
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 +91 -41
- package/index.js +107 -40
- package/lib/ais/index.js +262 -0
- package/lib/backfill/index.js +348 -0
- package/lib/backfill/lineproto.js +114 -0
- package/lib/history/index.js +21 -138
- package/lib/sync/index.js +8 -1
- package/lib/sync/lineprotocol.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ One Signal K plugin, independently-toggleable modules — so the boat stays
|
|
|
27
27
|
same contracts as the cloud, offline-first):
|
|
28
28
|
- **`/ws/telemetry`** — the app's live telemetry bus, fed from local SignalK.
|
|
29
29
|
- **`/api/history/{series,track}`** — the app's Trends panel + track, served
|
|
30
|
-
from
|
|
30
|
+
from a live ring sampled on the boat — full local history, no database.
|
|
31
31
|
|
|
32
32
|
Kept as separate modules so a proxy fault can't wedge the data-critical sync path.
|
|
33
33
|
|
|
@@ -139,37 +139,92 @@ config passes through untouched. (Hand-edit `proxy.openAccess: false` to keep th
|
|
|
139
139
|
login gate — there is no toggle, since the gate cannot complete over the mirror anyway.)
|
|
140
140
|
|
|
141
141
|
## Local history (offline Trends + track)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
142
|
+
**One source: a live ring**, sampled from the same BoatState that feeds `/ws/telemetry`
|
|
143
|
+
— no database, works on a Victron GX with nothing else installed. `historyAvailable` is
|
|
144
|
+
forced on so the app shows the Trends panel, and the eight channels match what the cloud
|
|
145
|
+
serves, `stw` included.
|
|
146
|
+
|
|
147
|
+
There is deliberately **no way to point this at a local InfluxDB**. Until v0.15.0 a read
|
|
148
|
+
token did exactly that, and it was a trap: the app only ever asks for a *relative* window
|
|
149
|
+
clamped to 24 h, so aiming it at a bucket of older data matched nothing — Trends went
|
|
150
|
+
blank **and** the working live ring was switched off. If you still have a token in your
|
|
151
|
+
config it is now inert; the plugin logs `history -> live ring` regardless.
|
|
152
|
+
|
|
153
|
+
A local InfluxDB is not a competitor here anyway. The app never requests finer than
|
|
154
|
+
`every=5s` over a 24 h window, and the ring's floor at that window is 2 s — set
|
|
155
|
+
`ringSampleSec: 5` and it matches anything the UI can draw, from live state. What an old
|
|
156
|
+
database *is* good for is its contents ending up **in the cloud** — see below.
|
|
157
|
+
|
|
158
|
+
## Uploading AIS targets
|
|
159
|
+
The cloud app already draws other vessels, but its AIS source polls a SignalK server over
|
|
160
|
+
the LAN and keeps everything in memory — which cannot work once a boat is on a mobile
|
|
161
|
+
link. Enable **Upload AIS targets** and the boat pushes what its own receiver hears, so
|
|
162
|
+
the web app can show other boats, their heading and their trail from stored data.
|
|
163
|
+
|
|
164
|
+
Only **locally received** AIS is forwarded. A boat running an internet feed such as
|
|
165
|
+
`signalk-aisstream` would otherwise spend uplink bandwidth sending data the cloud can
|
|
166
|
+
fetch directly from the same API — known feeds are skipped automatically. The plugin logs
|
|
167
|
+
the AIS sources it sees, so you can name your own receiver in **Only this AIS source** if
|
|
168
|
+
you want to be explicit.
|
|
169
|
+
|
|
170
|
+
There is no radius or rate limit: a real AIS receiver is bounded by VHF line-of-sight,
|
|
171
|
+
which is the honest limiter, and offshore — where this data is most valuable, because
|
|
172
|
+
commercial feeds are blind there — it tends to zero. Vessel identity (name, dimensions,
|
|
173
|
+
ship type) repeats every few minutes and never changes, so it is re-sent at most hourly;
|
|
174
|
+
positions are never throttled.
|
|
175
|
+
|
|
176
|
+
Telemetry always wins the link. AIS buffers in its **own** spool with its own cap and
|
|
177
|
+
stands down completely whenever the telemetry spool has a backlog, so a busy anchorage
|
|
178
|
+
can never delay or evict your own boat's data.
|
|
179
|
+
|
|
180
|
+
> ⚠️ **Requires a cloud that filters history on `self`.** Every AIS row is tagged
|
|
181
|
+
> `self=false`, and the cloud's Trends and track queries must filter `self == "true"`.
|
|
182
|
+
> Without that, other ships' speed and heading appear in *your* charts. Leave this off
|
|
183
|
+
> until the server side is in place.
|
|
184
|
+
|
|
185
|
+
## Copying older history to the cloud (one-time)
|
|
186
|
+
If the boat recorded into its own InfluxDB before it started syncing — a
|
|
187
|
+
`signalk-to-influxdb-v2` bucket, or an imported logbook — the **backfill** copies it up
|
|
188
|
+
so the cloud holds your full history. Live sync can't do this: it only ever sees deltas
|
|
189
|
+
arriving now, and the spool only replays what it captured itself while offline.
|
|
190
|
+
|
|
191
|
+
Fill the **Copy older history to the cloud** section and save. It walks backwards in
|
|
192
|
+
one-hour windows (newest first, so recent history lands first), resumes after a restart
|
|
193
|
+
from a manifest, and stands aside whenever live telemetry has a backlog — the data-
|
|
194
|
+
critical path is never starved by a bulk upload. Progress shows in the status line.
|
|
195
|
+
|
|
196
|
+
It needs a **cloud read+write token**, not the write token from signup. Every hour it
|
|
197
|
+
uploads is verified by counting the destination, and a write-only token cannot read. A
|
|
198
|
+
`204` means InfluxDB accepted the bytes, not that every point landed — without the count
|
|
199
|
+
a partial write would be marked done and lost. The token is only needed while the
|
|
200
|
+
backfill runs: **revoke it afterwards**, live sync is unaffected.
|
|
201
|
+
|
|
202
|
+
Safe to re-run. Points are keyed by (measurement, tagset, nanosecond timestamp), so an
|
|
203
|
+
identical point overwrites rather than duplicating — an interrupted migration is simply
|
|
204
|
+
run again.
|
|
205
|
+
|
|
206
|
+
**Only this boat's data is copied**, and that is not configurable. The cloud's history
|
|
207
|
+
queries assume your bucket holds one vessel, so uploading an archive's AIS would put
|
|
208
|
+
other ships into your own SOG and heading charts. If the source holds several contexts
|
|
209
|
+
the plugin copies yours and logs which it skipped. If it holds exactly **one** context it
|
|
210
|
+
is copied whatever identity string it uses — a bucket with one vessel cannot be an AIS
|
|
211
|
+
collection, and this is what lets an archive recorded under an older Signal K UUID (or an
|
|
212
|
+
MMSI URN) still migrate. Hand-edit `backfill.context` to force a specific one.
|
|
213
|
+
|
|
214
|
+
A run that copies **zero** points is reported as a problem, not as success: that almost
|
|
215
|
+
always means the org or bucket is wrong rather than that the archive is empty.
|
|
216
|
+
|
|
217
|
+
**It starts below what live sync already covers.** The destination's own oldest point is
|
|
218
|
+
the moment cloud sync began, so the walk begins there rather than at *now*. Without that,
|
|
219
|
+
a source archive that is still being written — a `signalk-to-influxdb-v2` bucket still
|
|
220
|
+
recording — makes the first windows re-upload today's data. The timestamps are correct,
|
|
221
|
+
but it is data the cloud already has, and a lot of wasted uplink.
|
|
222
|
+
|
|
223
|
+
**Dense archives are subdivided.** A window is read whole and converted in memory, and a
|
|
224
|
+
busy boat can produce millions of points an hour (54M/day was measured on a real boat —
|
|
225
|
+
about 400 MB of CSV per hour). When a window holds more than `maxRowsPerChunk` points it
|
|
226
|
+
is halved until it fits, down to a one-minute floor. The count is already known before
|
|
227
|
+
the read, so this costs nothing extra.
|
|
173
228
|
|
|
174
229
|
**True wind comes from your instruments.** If the boat publishes
|
|
175
230
|
`environment.wind.speedTrue` / `directionTrue`, those are stored verbatim — a wind
|
|
@@ -190,14 +245,14 @@ water. (Before v0.14.6 the derivation used SOG, which in 3 kt of foul tide skewe
|
|
|
190
245
|
|
|
191
246
|
The sailkick app is deployment-agnostic about history: *central Influx in the
|
|
192
247
|
cloud, in-memory ring on a DB-less edge*. The boat is a third case — an edge that
|
|
193
|
-
serves the app's history endpoints from its **own** data
|
|
248
|
+
serves the app's history endpoints from its **own** live data:
|
|
194
249
|
```
|
|
195
250
|
GET /api/history/series?window=3600s&every=30s -> { series: { sog|heading|tws|… : [[tMs,val],…] } }
|
|
196
251
|
GET /api/history/track?window=3600s -> { track: [{ t, lat, lon }, …] }
|
|
197
252
|
```
|
|
198
253
|
Same JSON the cloud returns, so the browser can't tell the difference — but it
|
|
199
|
-
works **offline** with the boat's own data. Only when
|
|
200
|
-
|
|
254
|
+
works **offline** with the boat's own data. Only when no telemetry source is
|
|
255
|
+
available at all do these paths **fall through to the cloud mirror**, so an
|
|
201
256
|
online boat is never worse off than before.
|
|
202
257
|
|
|
203
258
|
## Setup: register on the web, then paste the token
|
|
@@ -237,21 +292,16 @@ in `index.js`.
|
|
|
237
292
|
|
|
238
293
|
- **Sailkick account**: `slug` (boat name), `writeToken`
|
|
239
294
|
- **Telemetry sync → cloud**: `enabled`
|
|
295
|
+
- **Upload AIS targets**: `enabled` (default off), `source`
|
|
240
296
|
- **Offline app & maps**: `enabled`, `proxyPort` (default 8080), `localSignalkUrl`
|
|
241
297
|
(default `http://127.0.0.1:3000`), `dataDir`, `seedEnabled`, `prefetchRadiusNm`,
|
|
242
298
|
`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`)
|
|
246
299
|
|
|
247
300
|
`dataDir` is the one storage location — cached maps, the telemetry spool and the
|
|
248
301
|
history ring log all live under it. **Put it on the SSD/USB disk, not the SD card.**
|
|
249
302
|
Leave it blank and each part falls back to its historical spot under the plugin data
|
|
250
303
|
dir.
|
|
251
304
|
|
|
252
|
-
`historyToken` is optional and only matters if the boat already runs its own InfluxDB.
|
|
253
|
-
Blank — the normal case — uses the built-in DB-less ring.
|
|
254
|
-
|
|
255
305
|
Cache-manifest polling is always on (no toggle): tile freshness comes from the cloud
|
|
256
306
|
announcing bakes, and without it a re-baked dataset would never refresh.
|
|
257
307
|
|
package/index.js
CHANGED
|
@@ -6,6 +6,8 @@ const { createSync } = require('./lib/sync')
|
|
|
6
6
|
const { createProxy } = require('./lib/proxy')
|
|
7
7
|
const { createTelemetry } = require('./lib/telemetry')
|
|
8
8
|
const { createHistory } = require('./lib/history')
|
|
9
|
+
const { createBackfill } = require('./lib/backfill')
|
|
10
|
+
const { createAis } = require('./lib/ais')
|
|
9
11
|
const { resolveAccountConfig } = require('./lib/account')
|
|
10
12
|
|
|
11
13
|
// sailkick-boat: one Signal K plugin, two independently-toggleable modules —
|
|
@@ -43,15 +45,8 @@ const PROXY_TUNING = { requestTimeoutMs: 20000, localPaths: ['/signalk'], teleme
|
|
|
43
45
|
const MANIFEST = { enabled: true, path: '/api/cache-manifest', pollIntervalSec: 300 }
|
|
44
46
|
const SEED_TUNING = { coastlineMaxZoom: 8, seabedMaxZoom: 6, concurrency: 4 }
|
|
45
47
|
const PREFETCH_TUNING = { concurrency: 4 }
|
|
46
|
-
const HISTORY_TUNING = {
|
|
47
|
-
|
|
48
|
-
org: 'signalk',
|
|
49
|
-
bucket: 'signalk',
|
|
50
|
-
requestTimeoutMs: 15000,
|
|
51
|
-
ringPersist: true,
|
|
52
|
-
ringWindowSec: 86400,
|
|
53
|
-
ringSampleSec: 15
|
|
54
|
-
}
|
|
48
|
+
const HISTORY_TUNING = { ringPersist: true, ringWindowSec: 86400, ringSampleSec: 15 }
|
|
49
|
+
const BACKFILL_SRC_DEFAULTS = { influxUrl: 'http://127.0.0.1:8086', org: 'signalk', bucket: 'signalk' }
|
|
55
50
|
|
|
56
51
|
// A *cloud* endpoint on a loopback or private address means telemetry never leaves the
|
|
57
52
|
// LAN. On the wire that is indistinguishable from a normal offline backlog — the spool
|
|
@@ -69,13 +64,12 @@ function isPrivateHostUrl (u) {
|
|
|
69
64
|
} catch { return false }
|
|
70
65
|
}
|
|
71
66
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
function pickHistoryField (flat, legacy, dflt) {
|
|
67
|
+
// The Signal K config UI writes every schema default on save, so a freshly pre-filled
|
|
68
|
+
// `backfillOrg: "signalk"` can appear on a boat whose archive has always lived in org
|
|
69
|
+
// "addiction" under the pre-0.15 `proxy.history` block — and the backfill would read an
|
|
70
|
+
// empty bucket. Rule: an explicitly-changed field wins; else a legacy value that differs
|
|
71
|
+
// from the default wins; else the default.
|
|
72
|
+
function pickField (flat, legacy, dflt) {
|
|
79
73
|
const f = String(flat == null ? '' : flat).trim()
|
|
80
74
|
const l = String(legacy == null ? '' : legacy).trim()
|
|
81
75
|
if (f && f !== dflt) return { value: f, from: 'config' }
|
|
@@ -98,6 +92,8 @@ module.exports = function (app) {
|
|
|
98
92
|
let proxy = null
|
|
99
93
|
let telemetry = null
|
|
100
94
|
let history = null
|
|
95
|
+
let backfill = null
|
|
96
|
+
let ais = null
|
|
101
97
|
let statusTimer = null
|
|
102
98
|
let accountStatus = null
|
|
103
99
|
let syncWarning = null
|
|
@@ -130,6 +126,28 @@ module.exports = function (app) {
|
|
|
130
126
|
enabled: { type: 'boolean', title: 'Enable telemetry sync', default: true }
|
|
131
127
|
}
|
|
132
128
|
},
|
|
129
|
+
ais: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
title: 'Upload AIS targets',
|
|
132
|
+
description: 'Send the AIS this boat\'s own receiver hears to the cloud, so the web app can show other vessels, their heading and their trail. Only locally received AIS is forwarded — an internet feed such as signalk-aisstream is skipped, since the cloud can fetch that itself without spending your uplink. REQUIRES a cloud that filters history on self; until then leave this off or the owner\'s SOG and heading charts will pick up other ships.',
|
|
133
|
+
properties: {
|
|
134
|
+
enabled: { type: 'boolean', title: 'Upload AIS targets', default: false },
|
|
135
|
+
source: { type: 'string', title: 'Only this AIS source', description: 'Blank forwards every source except known internet feeds. The plugin logs the AIS sources it sees — copy the one for your own receiver here if you want to be explicit.' }
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
backfill: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
title: 'Copy older history to the cloud (one-time)',
|
|
141
|
+
description: 'If this boat recorded data into its own InfluxDB before it started syncing — a signalk-to-influxdb-v2 bucket, or an imported logbook — this copies it up so the app can chart it. It runs in the background, resumes after a restart, verifies every hour it uploads, and stands aside whenever live telemetry is behind. Safe to re-run: identical points overwrite rather than duplicate.',
|
|
142
|
+
properties: {
|
|
143
|
+
enabled: { type: 'boolean', title: 'Run the backfill', default: false },
|
|
144
|
+
cloudToken: { type: 'string', title: 'Cloud read+write token', description: 'A token with READ and WRITE on your cloud bucket. Your normal write token cannot read, and reading is how each uploaded hour is verified. Needed only while the backfill runs — revoke it afterwards; live sync is unaffected.' },
|
|
145
|
+
sourceToken: { type: 'string', title: 'Local InfluxDB read token' },
|
|
146
|
+
sourceUrl: { type: 'string', title: '…local InfluxDB URL', default: 'http://127.0.0.1:8086' },
|
|
147
|
+
sourceOrg: { type: 'string', title: '…organization', default: 'signalk' },
|
|
148
|
+
sourceBucket: { type: 'string', title: '…bucket', default: 'signalk' }
|
|
149
|
+
}
|
|
150
|
+
},
|
|
133
151
|
proxy: {
|
|
134
152
|
type: 'object',
|
|
135
153
|
title: 'Offline app & maps',
|
|
@@ -154,11 +172,7 @@ module.exports = function (app) {
|
|
|
154
172
|
enum: [12, 13, 14, 15],
|
|
155
173
|
enumNames: ['Overview (z12)', 'Coastal (z13)', 'Detailed (z14)', 'Harbor (z15)'],
|
|
156
174
|
default: 13
|
|
157
|
-
}
|
|
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' }
|
|
175
|
+
}
|
|
162
176
|
}
|
|
163
177
|
}
|
|
164
178
|
}
|
|
@@ -237,21 +251,7 @@ module.exports = function (app) {
|
|
|
237
251
|
const oldSeed = p.seed || {}
|
|
238
252
|
const oldPrefetch = p.prefetch || {}
|
|
239
253
|
const oldHistory = p.history || {}
|
|
240
|
-
|
|
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
|
-
}
|
|
254
|
+
console.log('[sailkick-boat] history -> live ring')
|
|
255
255
|
|
|
256
256
|
if (upstream.ignored) {
|
|
257
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.`)
|
|
@@ -284,10 +284,6 @@ module.exports = function (app) {
|
|
|
284
284
|
history: {
|
|
285
285
|
...HISTORY_TUNING,
|
|
286
286
|
enabled: oldHistory.enabled !== false,
|
|
287
|
-
influxUrl: hUrl.value,
|
|
288
|
-
org: hOrg.value,
|
|
289
|
-
bucket: hBucket.value,
|
|
290
|
-
token: hToken,
|
|
291
287
|
ringPersist: oldHistory.ringPersist !== false,
|
|
292
288
|
ringWindowSec: oldHistory.ringWindowSec || HISTORY_TUNING.ringWindowSec,
|
|
293
289
|
ringSampleSec: oldHistory.ringSampleSec || HISTORY_TUNING.ringSampleSec,
|
|
@@ -328,6 +324,71 @@ module.exports = function (app) {
|
|
|
328
324
|
}
|
|
329
325
|
}
|
|
330
326
|
|
|
327
|
+
// --- AIS upload (isolated; yields to telemetry, its own spool) ---
|
|
328
|
+
const aisOpts = opts.ais || {}
|
|
329
|
+
if (aisOpts.enabled === true) {
|
|
330
|
+
if (!b.writeToken) {
|
|
331
|
+
(app.error || console.error)('[sailkick-boat] AIS upload needs a paired account for its destination bucket — skipped')
|
|
332
|
+
} else {
|
|
333
|
+
try {
|
|
334
|
+
ais = createAis(app, {
|
|
335
|
+
influxUrl: influx.url,
|
|
336
|
+
org: b.org || SYNC_TUNING.org,
|
|
337
|
+
bucket: b.bucket,
|
|
338
|
+
token: b.writeToken,
|
|
339
|
+
source: String(aisOpts.source || '').trim() || null,
|
|
340
|
+
spoolDir: store ? path.join(store, 'ais-spool') : undefined,
|
|
341
|
+
pending: sync ? sync.pending : null
|
|
342
|
+
})
|
|
343
|
+
ais.start()
|
|
344
|
+
console.log(`[sailkick-boat] ais -> ${influx.url} bucket=${b.bucket}${aisOpts.source ? ' source=' + aisOpts.source : ''}`)
|
|
345
|
+
} catch (e) {
|
|
346
|
+
(app.error || console.error)('[sailkick-boat] AIS start failed: ' + e.message)
|
|
347
|
+
ais = null
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// --- backfill (best-effort, isolated: it must never disturb sync or the proxy) ---
|
|
353
|
+
const bf = opts.backfill || {}
|
|
354
|
+
const oldHist = (opts.proxy || {}).history || {}
|
|
355
|
+
if (bf.enabled === true) {
|
|
356
|
+
// Pre-0.15 installs kept the archive's coordinates in proxy.history.* — reuse them
|
|
357
|
+
// so nobody retypes an org/bucket the plugin already knows.
|
|
358
|
+
const src = {
|
|
359
|
+
url: pickField(bf.sourceUrl, oldHist.influxUrl, BACKFILL_SRC_DEFAULTS.influxUrl).value,
|
|
360
|
+
org: pickField(bf.sourceOrg, oldHist.org, BACKFILL_SRC_DEFAULTS.org).value,
|
|
361
|
+
bucket: pickField(bf.sourceBucket, oldHist.bucket, BACKFILL_SRC_DEFAULTS.bucket).value,
|
|
362
|
+
token: String(bf.sourceToken || oldHist.token || '').trim()
|
|
363
|
+
}
|
|
364
|
+
const dst = { url: influx.url, org: b.org || SYNC_TUNING.org, bucket: b.bucket, token: String(bf.cloudToken || '').trim() }
|
|
365
|
+
if (!b.writeToken) {
|
|
366
|
+
(app.error || console.error)('[sailkick-boat] backfill needs a paired account for its destination bucket — skipped')
|
|
367
|
+
} else if (!dst.token) {
|
|
368
|
+
(app.error || console.error)('[sailkick-boat] backfill needs a cloud READ+WRITE token: every uploaded hour is verified by counting the destination, which the write-only sync token cannot do — skipped')
|
|
369
|
+
} else if (!src.token) {
|
|
370
|
+
(app.error || console.error)('[sailkick-boat] backfill needs a read token for the local InfluxDB — skipped')
|
|
371
|
+
} else {
|
|
372
|
+
try {
|
|
373
|
+
backfill = createBackfill(app, {
|
|
374
|
+
src,
|
|
375
|
+
dst,
|
|
376
|
+
// Never a config option: the plugin must not upload data that is not this
|
|
377
|
+
// boat's, and the cloud's history queries depend on that holding.
|
|
378
|
+
selfContext: app.selfContext || ('vessels.' + (app.selfId || 'self')),
|
|
379
|
+
context: String(bf.context || '').trim() || null, // hand-edit escape hatch
|
|
380
|
+
startBound: bf.startBound,
|
|
381
|
+
stateFile: path.join((app.getDataDirPath && app.getDataDirPath()) || '.', 'backfill.json'),
|
|
382
|
+
pending: sync ? sync.pending : null
|
|
383
|
+
})
|
|
384
|
+
backfill.start()
|
|
385
|
+
} catch (e) {
|
|
386
|
+
(app.error || console.error)('[sailkick-boat] backfill start failed: ' + e.message)
|
|
387
|
+
backfill = null
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
331
392
|
statusTimer = setInterval(updateStatus, 5000)
|
|
332
393
|
updateStatus()
|
|
333
394
|
}
|
|
@@ -341,6 +402,8 @@ module.exports = function (app) {
|
|
|
341
402
|
if (proxy) parts.push(proxy.status())
|
|
342
403
|
if (telemetry) parts.push(telemetry.status())
|
|
343
404
|
if (history) parts.push(history.status())
|
|
405
|
+
if (ais) parts.push(ais.status())
|
|
406
|
+
if (backfill) parts.push(backfill.status())
|
|
344
407
|
try { app.setPluginStatus(parts.join(' | ') || 'idle (both features off)') } catch {}
|
|
345
408
|
}
|
|
346
409
|
|
|
@@ -350,10 +413,14 @@ module.exports = function (app) {
|
|
|
350
413
|
try { if (sync) sync.stop() } catch {}
|
|
351
414
|
try { if (telemetry) telemetry.stop() } catch {}
|
|
352
415
|
try { if (history) history.stop() } catch {}
|
|
416
|
+
try { if (ais) ais.stop() } catch {}
|
|
417
|
+
try { if (backfill) backfill.stop() } catch {}
|
|
353
418
|
try { if (proxy) proxy.stop() } catch {}
|
|
354
419
|
sync = null
|
|
355
420
|
telemetry = null
|
|
356
421
|
history = null
|
|
422
|
+
backfill = null
|
|
423
|
+
ais = null
|
|
357
424
|
proxy = null
|
|
358
425
|
accountStatus = null
|
|
359
426
|
syncWarning = null
|