signalk-chiplog 2.15.0 โ 2.16.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/CHANGELOG.md +26 -1
- package/README.md +18 -0
- package/index.js +6 -0
- package/lib/api.js +351 -26
- package/lib/changes.js +83 -0
- package/lib/cloud-restore.js +74 -3
- package/lib/cloud-sync.js +28 -3
- package/lib/crew.js +1 -0
- package/lib/database.js +131 -0
- package/lib/defects.js +213 -0
- package/lib/detection.js +2 -1
- package/lib/entries.js +25 -3
- package/lib/events.js +6 -0
- package/lib/observation-recorder.js +14 -0
- package/lib/openapi.js +1155 -0
- package/lib/passage-import.js +4 -3
- package/lib/plans.js +240 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,30 @@ All notable changes to Chiplog are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.16.0] - 2026-11-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **An API for companion apps**, such as Chiplog Skipper, described in [docs/API.md](docs/API.md):
|
|
14
|
+
- **Versioned**: the public routes are also served under `/plugins/signalk-chiplog/api/v1`, described by an OpenAPI
|
|
15
|
+
document (`GET /api/v1/openapi.json`, also listed by the Signal K server). `GET /state` now says `apiVersions`,
|
|
16
|
+
`pluginVersion`, `features` and the `logbookId`.
|
|
17
|
+
- **Planned passages** (`/plans`): departure, arrival, Signal K route and fallback points, criteria, crew and watches;
|
|
18
|
+
linking a plan to the passage that opens puts the planned crew aboard.
|
|
19
|
+
- **Defects** (`/defects`), with severity, status and links to the passage, plan or journal line they came from.
|
|
20
|
+
- **Tanks at arrival** (`endTanks`): noted as a passage closes, by detection or by the crew, beside those noted at
|
|
21
|
+
departure; both can be set by hand with `PATCH /entries/:id`, and imported with `POST /entries`.
|
|
22
|
+
- **A changes feed** (`GET /changes?since=`), listing the passages, events, crew, plans and defects changed since a
|
|
23
|
+
cursor, deletions included.
|
|
24
|
+
- Every new route is open to read/write users; none needs admin.
|
|
25
|
+
- **Plans and defects in the online backup**, sent whole like the places to a service that keeps them, and brought back
|
|
26
|
+
by a restore.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- The first backup run after the update sends every passage's content again, without its track: a passage now carries
|
|
31
|
+
its tanks at arrival.
|
|
32
|
+
|
|
9
33
|
## [2.15.0] - 2026-10-30
|
|
10
34
|
|
|
11
35
|
### Added
|
|
@@ -535,7 +559,8 @@ First release.
|
|
|
535
559
|
- REST API under `/plugins/signalk-chiplog/api`, documented in [docs/API.md](docs/API.md).
|
|
536
560
|
- Single SQLite database through Node's built-in `node:sqlite`: no native module to build.
|
|
537
561
|
|
|
538
|
-
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.
|
|
562
|
+
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.16.0...HEAD
|
|
563
|
+
[2.16.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.15.0...v2.16.0
|
|
539
564
|
[2.15.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.14.0...v2.15.0
|
|
540
565
|
[2.14.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.13.0...v2.14.0
|
|
541
566
|
[2.13.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.12.0...v2.13.0
|
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ English and French, chosen from the browser's language.
|
|
|
33
33
|
- [Passage summary emails](#passage-summary-emails-)
|
|
34
34
|
- [Retrospective analysis](#retrospective-analysis-)
|
|
35
35
|
- [Importing from PostgSail](#importing-from-postgsail-)
|
|
36
|
+
- [Companion apps](#companion-apps-)
|
|
36
37
|
- [Privacy and online services](#privacy-and-online-services-)
|
|
37
38
|
- [Troubleshooting](#troubleshooting-)
|
|
38
39
|
- [Limitations](#limitations-)
|
|
@@ -682,6 +683,23 @@ node scripts/import-postgsail.js PostgSail_Trip.geojson --url http://boat.local:
|
|
|
682
683
|
and landmarks are then looked up in the background, like for any passage. Tide and weather forecasts are not fetched
|
|
683
684
|
for passages this old.
|
|
684
685
|
|
|
686
|
+
## Companion apps ๐งญ
|
|
687
|
+
|
|
688
|
+
Chiplog keeps what happened. A companion app, such as Chiplog Skipper on a tablet, can add what was planned and decided
|
|
689
|
+
around it, through the plugin's REST API โ nothing to set up in the plugin:
|
|
690
|
+
|
|
691
|
+
- **Planned passages**: departure, arrival, the Signal K route and fallback points, the crew and the watches. Once the
|
|
692
|
+
passage opens, the planned crew is the one recorded aboard.
|
|
693
|
+
- **Defects** noticed under way or after, with their severity and whether they are fixed.
|
|
694
|
+
- **Tanks at arrival**, noted as a passage ends next to those noted at departure, so that what a passage used can be
|
|
695
|
+
worked out. A boat without tank sensors can have them typed in.
|
|
696
|
+
- **Decisions, check-lists, briefings and signatures** appear in the passage's journal as notes.
|
|
697
|
+
|
|
698
|
+
The app connects with a Signal K device access request, like the tablet entry app, and needs read/write access, never
|
|
699
|
+
admin. Plans and defects go to the [online backup](#online-backup-) with the rest of the logbook, when the service keeps
|
|
700
|
+
them. For developers: the API is versioned (`/plugins/signalk-chiplog/api/v1`) and described by an OpenAPI document,
|
|
701
|
+
listed in the Signal K admin under **Documentation**, and in [docs/API.md](docs/API.md).
|
|
702
|
+
|
|
685
703
|
## Privacy and online services ๐
|
|
686
704
|
|
|
687
705
|
- **Place names.** With geocoding on, the position of each departure and arrival that matches no known place is sent to
|
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { registerRoutes } = require('./lib/api');
|
|
2
|
+
const { getOpenApi } = require('./lib/openapi');
|
|
2
3
|
const { openDatabase } = require('./lib/database');
|
|
3
4
|
const { createPassageDetector, DETECTION_DEFAULTS, TICK_INTERVAL_MS } = require('./lib/detection');
|
|
4
5
|
const { ApiError } = require('./lib/errors');
|
|
@@ -894,6 +895,10 @@ module.exports = function (app) {
|
|
|
894
895
|
}
|
|
895
896
|
};
|
|
896
897
|
|
|
898
|
+
// The versioned API's description, which the server lists with its own (Signal K admin,
|
|
899
|
+
// Documentation > OpenAPI).
|
|
900
|
+
plugin.getOpenApi = () => getOpenApi();
|
|
901
|
+
|
|
897
902
|
// The server registers routes once, before start() and even while the plugin
|
|
898
903
|
// is disabled, and never removes them: every request must check the database.
|
|
899
904
|
plugin.registerWithRouter = function (router) {
|
|
@@ -913,6 +918,7 @@ module.exports = function (app) {
|
|
|
913
918
|
vesselPosition: () => readVesselPosition(app),
|
|
914
919
|
observeEvent: (entryId, time) => detector.observeEvent(entryId, time),
|
|
915
920
|
noteDeparture: (entryId) => detector.noteDeparture(entryId),
|
|
921
|
+
noteArrival: (entryId) => detector.noteArrival(entryId),
|
|
916
922
|
usbExport,
|
|
917
923
|
summaryMailer,
|
|
918
924
|
cloudSync,
|
package/lib/api.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
const { version: pluginVersion } = require('../package.json');
|
|
2
|
+
const changes = require('./changes');
|
|
3
|
+
const { getLogbookId } = require('./cloud-sync');
|
|
1
4
|
const { getSchemaVersion } = require('./database');
|
|
2
5
|
const crew = require('./crew');
|
|
6
|
+
const defects = require('./defects');
|
|
3
7
|
const { ApiError, badRequest, conflict, notFound } = require('./errors');
|
|
4
8
|
const entries = require('./entries');
|
|
5
9
|
const events = require('./events');
|
|
@@ -9,7 +13,9 @@ const { isTimeZone, PDF_LANGUAGES } = require('./logbook-pdf');
|
|
|
9
13
|
const { toGeoJson, toGpx } = require('./formats');
|
|
10
14
|
const manoeuvreTypes = require('./manoeuvre-types');
|
|
11
15
|
const { importPassage, TRACK_READINGS, OBSERVATION_READINGS } = require('./passage-import');
|
|
16
|
+
const { getOpenApi } = require('./openapi');
|
|
12
17
|
const places = require('./places');
|
|
18
|
+
const plans = require('./plans');
|
|
13
19
|
const propulsion = require('./propulsion');
|
|
14
20
|
const { getTideForecast } = require('./tide-forecaster');
|
|
15
21
|
const { getWeatherForecast } = require('./weather-forecaster');
|
|
@@ -19,6 +25,14 @@ const v = require('./validation');
|
|
|
19
25
|
|
|
20
26
|
const SQLITE_CONSTRAINT = 19;
|
|
21
27
|
|
|
28
|
+
// The versioned contract (docs/API.md, Versioning). A field or route added stays in v1; a
|
|
29
|
+
// breaking change is v2, with v1 kept alongside for a while.
|
|
30
|
+
const V1_PREFIX = '/api/v1';
|
|
31
|
+
const API_VERSIONS = [1];
|
|
32
|
+
// What a client may rely on beyond what v1 first described: it sends a field or calls a route
|
|
33
|
+
// that belongs to a feature only once this list names it, since unknown fields are refused.
|
|
34
|
+
const FEATURES = ['plans', 'defects', 'changes', 'endTanks'];
|
|
35
|
+
|
|
22
36
|
// Servers predating router.access() only support admin-only plugin routes.
|
|
23
37
|
// Falling back keeps the plugin usable there, at the cost of an admin login.
|
|
24
38
|
function scoped(router, level) {
|
|
@@ -151,6 +165,17 @@ function parseTank(item, name) {
|
|
|
151
165
|
};
|
|
152
166
|
}
|
|
153
167
|
|
|
168
|
+
// A list of tanks, or null to clear them.
|
|
169
|
+
function parseTanks(value, name) {
|
|
170
|
+
if (value === null) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (!Array.isArray(value)) {
|
|
174
|
+
throw badRequest(`${name} must be an array or null`);
|
|
175
|
+
}
|
|
176
|
+
return value.map((item, index) => parseTank(item, `${name}[${index}]`));
|
|
177
|
+
}
|
|
178
|
+
|
|
154
179
|
// A battery as noted at departure: volts, amps (negative discharging), state of
|
|
155
180
|
// charge as a ratio, kelvin.
|
|
156
181
|
function parseBattery(item, name) {
|
|
@@ -188,10 +213,187 @@ function parsePropulsionPeriod(item, name) {
|
|
|
188
213
|
};
|
|
189
214
|
}
|
|
190
215
|
|
|
216
|
+
const nullableTimestamp = (value, name) => (value === null ? null : v.parseTimestamp(value, name));
|
|
217
|
+
const nullableId = (value, name) => (value === null ? null : v.parseId(value, name));
|
|
218
|
+
|
|
219
|
+
// The changes feed's cursor: the `cursor` of the previous page, 0 for everything.
|
|
220
|
+
function parseCursor(value) {
|
|
221
|
+
if (value === undefined) {
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (!/^(0|[1-9]\d*)$/.test(String(value))) {
|
|
225
|
+
throw badRequest('since must be a non-negative integer');
|
|
226
|
+
}
|
|
227
|
+
return Number(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseChangesLimit(value) {
|
|
231
|
+
const limit = value === undefined ? changes.DEFAULT_CHANGES_LIMIT : Number(value);
|
|
232
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > changes.MAX_CHANGES_LIMIT) {
|
|
233
|
+
throw badRequest(`limit must be an integer between 1 and ${changes.MAX_CHANGES_LIMIT}`);
|
|
234
|
+
}
|
|
235
|
+
return limit;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A Signal K resource, by the href its API gives it: /resources/<type>/<id>.
|
|
239
|
+
const resourceRef = (type) => (value, name) => {
|
|
240
|
+
const pattern = new RegExp(`^/resources/${type}/[^/\\s]{1,200}$`);
|
|
241
|
+
if (typeof value !== 'string' || !pattern.test(value)) {
|
|
242
|
+
throw badRequest(`${name} must be a Signal K resource reference: /resources/${type}/<id>`);
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const MAX_PLAN_LIST = 100;
|
|
248
|
+
|
|
249
|
+
function parseArray(value, name, parseItem) {
|
|
250
|
+
if (!Array.isArray(value) || value.length > MAX_PLAN_LIST) {
|
|
251
|
+
throw badRequest(`${name} must be an array of at most ${MAX_PLAN_LIST} items`);
|
|
252
|
+
}
|
|
253
|
+
return value.map((item, index) => parseItem(item, `${name}[${index}]`));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// A crew member a plan names: a roster member by id, or a name for this plan alone.
|
|
257
|
+
function parseMember(item, name, extraFields = []) {
|
|
258
|
+
if (!v.isPlainObject(item)) {
|
|
259
|
+
throw badRequest(`${name} must be an object`);
|
|
260
|
+
}
|
|
261
|
+
if ('crewMemberId' in item) {
|
|
262
|
+
const member = v.requireBody(item, ['crewMemberId', 'role', ...extraFields]);
|
|
263
|
+
return {
|
|
264
|
+
crewMemberId: v.parseId(member.crewMemberId, `${name}.crewMemberId`),
|
|
265
|
+
role: optional(member, 'role', nullableString(100))
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const member = v.requireBody(item, ['name', 'role', ...extraFields]);
|
|
269
|
+
return {
|
|
270
|
+
name: v.parseString(member.name, `${name}.name`, { maxLength: 100 }),
|
|
271
|
+
role: optional(member, 'role', nullableString(100))
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseWatch(item, name) {
|
|
276
|
+
const member = parseMember(item, name, ['start', 'end']);
|
|
277
|
+
const start = v.parseTimestamp(item.start, `${name}.start`);
|
|
278
|
+
const end = v.parseTimestamp(item.end, `${name}.end`);
|
|
279
|
+
if (end <= start) {
|
|
280
|
+
throw badRequest(`${name}.end must be after its start`);
|
|
281
|
+
}
|
|
282
|
+
return { start, end, ...member };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const PLAN_FIELDS = [
|
|
286
|
+
'clientRef',
|
|
287
|
+
'title',
|
|
288
|
+
'status',
|
|
289
|
+
'departureTime',
|
|
290
|
+
'departurePlaceName',
|
|
291
|
+
'departurePosition',
|
|
292
|
+
'arrivalTime',
|
|
293
|
+
'arrivalPlaceName',
|
|
294
|
+
'arrivalPosition',
|
|
295
|
+
'routeRef',
|
|
296
|
+
'fallbackRefs',
|
|
297
|
+
'criteria',
|
|
298
|
+
'crew',
|
|
299
|
+
'watches',
|
|
300
|
+
'entryId'
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
function parsePlanInput(body, { creating }) {
|
|
304
|
+
return withoutUndefined({
|
|
305
|
+
clientRef: creating ? optional(body, 'clientRef', requiredString(100)) : undefined,
|
|
306
|
+
title: creating
|
|
307
|
+
? v.parseString(body.title, 'title', { maxLength: 200 })
|
|
308
|
+
: optional(body, 'title', requiredString(200)),
|
|
309
|
+
status: optional(body, 'status', (value, name) =>
|
|
310
|
+
v.parseEnum(value, name, plans.PLAN_STATUSES)
|
|
311
|
+
),
|
|
312
|
+
departureTime: optional(body, 'departureTime', nullableTimestamp),
|
|
313
|
+
departurePlaceName: optional(body, 'departurePlaceName', nullableString(200)),
|
|
314
|
+
departurePosition: optional(body, 'departurePosition', v.parsePosition),
|
|
315
|
+
arrivalTime: optional(body, 'arrivalTime', nullableTimestamp),
|
|
316
|
+
arrivalPlaceName: optional(body, 'arrivalPlaceName', nullableString(200)),
|
|
317
|
+
arrivalPosition: optional(body, 'arrivalPosition', v.parsePosition),
|
|
318
|
+
routeRef: optional(body, 'routeRef', (value, name) =>
|
|
319
|
+
value === null ? null : resourceRef('routes')(value, name)
|
|
320
|
+
),
|
|
321
|
+
fallbackRefs: optional(body, 'fallbackRefs', (value, name) =>
|
|
322
|
+
parseArray(value, name, resourceRef('waypoints'))
|
|
323
|
+
),
|
|
324
|
+
criteria: optional(body, 'criteria', parsePayload),
|
|
325
|
+
crew: optional(body, 'crew', (value, name) => parseArray(value, name, parseMember)),
|
|
326
|
+
watches: optional(body, 'watches', (value, name) => parseArray(value, name, parseWatch)),
|
|
327
|
+
entryId: optional(body, 'entryId', nullableId)
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const DEFECT_FIELDS = [
|
|
332
|
+
'clientRef',
|
|
333
|
+
'title',
|
|
334
|
+
'description',
|
|
335
|
+
'severity',
|
|
336
|
+
'status',
|
|
337
|
+
'time',
|
|
338
|
+
'fixedAt',
|
|
339
|
+
'entryId',
|
|
340
|
+
'planId',
|
|
341
|
+
'eventId'
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
function parseDefectInput(body, { creating }) {
|
|
345
|
+
return withoutUndefined({
|
|
346
|
+
clientRef: creating ? optional(body, 'clientRef', requiredString(100)) : undefined,
|
|
347
|
+
title: creating
|
|
348
|
+
? v.parseString(body.title, 'title', { maxLength: 200 })
|
|
349
|
+
: optional(body, 'title', requiredString(200)),
|
|
350
|
+
description: optional(body, 'description', nullableString(10000)),
|
|
351
|
+
severity: optional(body, 'severity', (value, name) =>
|
|
352
|
+
v.parseEnum(value, name, defects.DEFECT_SEVERITIES)
|
|
353
|
+
),
|
|
354
|
+
status: optional(body, 'status', (value, name) =>
|
|
355
|
+
v.parseEnum(value, name, defects.DEFECT_STATUSES)
|
|
356
|
+
),
|
|
357
|
+
time: optional(body, 'time', v.parseTimestamp),
|
|
358
|
+
fixedAt: optional(body, 'fixedAt', nullableTimestamp),
|
|
359
|
+
entryId: optional(body, 'entryId', nullableId),
|
|
360
|
+
planId: optional(body, 'planId', nullableId),
|
|
361
|
+
eventId: optional(body, 'eventId', nullableId)
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const optionalQuery = (query, field, parse) =>
|
|
366
|
+
query[field] === undefined ? undefined : parse(query[field], field);
|
|
367
|
+
|
|
368
|
+
const ROUTE_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
|
|
369
|
+
|
|
370
|
+
// A public route is mounted twice: under /api, which the webapp uses and which may change
|
|
371
|
+
// freely, and under /api/v1, the versioned contract the OpenAPI document describes (see
|
|
372
|
+
// docs/API.md, Versioning). Each one is recorded, for the OpenAPI document to be checked
|
|
373
|
+
// against.
|
|
374
|
+
function versioned(registrar, level, routes) {
|
|
375
|
+
const twice = {};
|
|
376
|
+
for (const method of ROUTE_METHODS) {
|
|
377
|
+
twice[method] = (routePath, ...handlers) => {
|
|
378
|
+
registrar[method](routePath, ...handlers);
|
|
379
|
+
const v1Path = routePath.replace(/^\/api\//, `${V1_PREFIX}/`);
|
|
380
|
+
registrar[method](v1Path, ...handlers);
|
|
381
|
+
routes.push({ method: method.toUpperCase(), path: v1Path.slice(V1_PREFIX.length), level });
|
|
382
|
+
return twice;
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return twice;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Returns the public routes as { method, path, level }, path relative to /api/v1.
|
|
191
389
|
function registerRoutes(router, { getContext, logError }) {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
390
|
+
const publicRoutes = [];
|
|
391
|
+
const readonly = versioned(scoped(router, 'readonly'), 'readonly', publicRoutes);
|
|
392
|
+
const readwrite = versioned(scoped(router, 'readwrite'), 'readwrite', publicRoutes);
|
|
393
|
+
const admin = versioned(router, 'admin', publicRoutes);
|
|
394
|
+
// The webapp's own configuration and maintenance screens: never part of the contract.
|
|
395
|
+
const internalReadonly = scoped(router, 'readonly');
|
|
396
|
+
const internalAdmin = router;
|
|
195
397
|
|
|
196
398
|
const handle = (fn) => async (req, res) => {
|
|
197
399
|
try {
|
|
@@ -221,11 +423,31 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
221
423
|
motion,
|
|
222
424
|
propulsion: under,
|
|
223
425
|
stateIssue,
|
|
224
|
-
schemaVersion: getSchemaVersion(db)
|
|
426
|
+
schemaVersion: getSchemaVersion(db),
|
|
427
|
+
logbookId: getLogbookId(db),
|
|
428
|
+
apiVersions: API_VERSIONS,
|
|
429
|
+
pluginVersion,
|
|
430
|
+
features: FEATURES
|
|
225
431
|
};
|
|
226
432
|
})
|
|
227
433
|
);
|
|
228
434
|
|
|
435
|
+
readonly.get(
|
|
436
|
+
'/api/openapi.json',
|
|
437
|
+
handle(() => getOpenApi())
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
// Changes feed
|
|
441
|
+
readonly.get(
|
|
442
|
+
'/api/changes',
|
|
443
|
+
handle(({ db }, req) =>
|
|
444
|
+
changes.listChanges(db, {
|
|
445
|
+
since: parseCursor(req.query.since),
|
|
446
|
+
limit: parseChangesLimit(req.query.limit)
|
|
447
|
+
})
|
|
448
|
+
)
|
|
449
|
+
);
|
|
450
|
+
|
|
229
451
|
// Entries
|
|
230
452
|
|
|
231
453
|
readonly.get(
|
|
@@ -264,6 +486,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
264
486
|
'distance',
|
|
265
487
|
'startTanks',
|
|
266
488
|
'startBatteries',
|
|
489
|
+
'endTanks',
|
|
267
490
|
'trackPoints',
|
|
268
491
|
'observations',
|
|
269
492
|
'propulsion'
|
|
@@ -280,6 +503,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
280
503
|
distance: optional(body, 'distance', v.parseNonNegativeNumber),
|
|
281
504
|
startTanks: parseList(body, 'startTanks', parseTank),
|
|
282
505
|
startBatteries: parseList(body, 'startBatteries', parseBattery),
|
|
506
|
+
endTanks: parseList(body, 'endTanks', parseTank),
|
|
283
507
|
trackPoints: parseList(body, 'trackPoints', parseTrackPoint),
|
|
284
508
|
observations: parseList(body, 'observations', parseObservation),
|
|
285
509
|
propulsion: parseList(body, 'propulsion', parsePropulsionPeriod)
|
|
@@ -302,7 +526,9 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
302
526
|
'endPosition',
|
|
303
527
|
'startPlaceName',
|
|
304
528
|
'endPlaceName',
|
|
305
|
-
'distance'
|
|
529
|
+
'distance',
|
|
530
|
+
'startTanks',
|
|
531
|
+
'endTanks'
|
|
306
532
|
]);
|
|
307
533
|
const patch = {
|
|
308
534
|
startTime: optional(body, 'startTime', v.parseTimestamp),
|
|
@@ -311,7 +537,9 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
311
537
|
endPosition: optional(body, 'endPosition', v.parsePosition),
|
|
312
538
|
startPlaceName: optional(body, 'startPlaceName', nullableString(200)),
|
|
313
539
|
endPlaceName: optional(body, 'endPlaceName', nullableString(200)),
|
|
314
|
-
distance: optional(body, 'distance', v.parseNonNegativeNumber)
|
|
540
|
+
distance: optional(body, 'distance', v.parseNonNegativeNumber),
|
|
541
|
+
startTanks: optional(body, 'startTanks', parseTanks),
|
|
542
|
+
endTanks: optional(body, 'endTanks', parseTanks)
|
|
315
543
|
};
|
|
316
544
|
return entries.updateEntry(db, id, patch, {
|
|
317
545
|
placeMatchRadius: config.placeMatchRadius,
|
|
@@ -322,13 +550,16 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
322
550
|
|
|
323
551
|
readwrite.post(
|
|
324
552
|
'/api/entries/:id/close',
|
|
325
|
-
handle(({ db, config, now, vesselPosition }, req) =>
|
|
326
|
-
|
|
553
|
+
handle(({ db, config, now, vesselPosition, noteArrival }, req) => {
|
|
554
|
+
const id = entryId(req);
|
|
555
|
+
entries.closeEntry(db, id, {
|
|
327
556
|
now: now(),
|
|
328
557
|
position: vesselPosition(),
|
|
329
558
|
placeMatchRadius: config.placeMatchRadius
|
|
330
|
-
})
|
|
331
|
-
|
|
559
|
+
});
|
|
560
|
+
noteArrival(id);
|
|
561
|
+
return entries.getEntry(db, id);
|
|
562
|
+
})
|
|
332
563
|
);
|
|
333
564
|
|
|
334
565
|
readwrite.post(
|
|
@@ -674,6 +905,98 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
674
905
|
})
|
|
675
906
|
);
|
|
676
907
|
|
|
908
|
+
// Plans
|
|
909
|
+
|
|
910
|
+
readonly.get(
|
|
911
|
+
'/api/plans',
|
|
912
|
+
handle(({ db }, req) =>
|
|
913
|
+
plans.listPlans(db, {
|
|
914
|
+
status: optionalQuery(req.query, 'status', (value, name) =>
|
|
915
|
+
v.parseEnum(value, name, plans.PLAN_STATUSES)
|
|
916
|
+
),
|
|
917
|
+
entryId: optionalQuery(req.query, 'entryId', v.parseId),
|
|
918
|
+
...v.parsePagination(req.query)
|
|
919
|
+
})
|
|
920
|
+
)
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
readonly.get(
|
|
924
|
+
'/api/plans/:id',
|
|
925
|
+
handle(({ db }, req) => plans.getPlan(db, v.parseId(req.params.id)))
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
readwrite.post(
|
|
929
|
+
'/api/plans',
|
|
930
|
+
handle(({ db, now }, req, res) => {
|
|
931
|
+
const input = parsePlanInput(v.requireBody(req.body, PLAN_FIELDS), { creating: true });
|
|
932
|
+
const { plan, created } = plans.createPlan(db, input, now());
|
|
933
|
+
res.status(created ? 201 : 200);
|
|
934
|
+
return plan;
|
|
935
|
+
})
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
readwrite.patch(
|
|
939
|
+
'/api/plans/:id',
|
|
940
|
+
handle(({ db, now }, req) => {
|
|
941
|
+
const id = v.parseId(req.params.id);
|
|
942
|
+
const body = v.requireBody(
|
|
943
|
+
req.body,
|
|
944
|
+
PLAN_FIELDS.filter((field) => field !== 'clientRef')
|
|
945
|
+
);
|
|
946
|
+
return plans.updatePlan(db, id, parsePlanInput(body, { creating: false }), now());
|
|
947
|
+
})
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
readwrite.delete(
|
|
951
|
+
'/api/plans/:id',
|
|
952
|
+
handle(({ db }, req) => {
|
|
953
|
+
plans.deletePlan(db, v.parseId(req.params.id));
|
|
954
|
+
})
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
// Defects
|
|
958
|
+
|
|
959
|
+
readonly.get(
|
|
960
|
+
'/api/defects',
|
|
961
|
+
handle(({ db }, req) =>
|
|
962
|
+
defects.listDefects(db, {
|
|
963
|
+
status: optionalQuery(req.query, 'status', (value, name) =>
|
|
964
|
+
v.parseEnum(value, name, defects.DEFECT_STATUSES)
|
|
965
|
+
),
|
|
966
|
+
entryId: optionalQuery(req.query, 'entryId', v.parseId),
|
|
967
|
+
planId: optionalQuery(req.query, 'planId', v.parseId),
|
|
968
|
+
...v.parsePagination(req.query)
|
|
969
|
+
})
|
|
970
|
+
)
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
readonly.get(
|
|
974
|
+
'/api/defects/:id',
|
|
975
|
+
handle(({ db }, req) => defects.getDefect(db, v.parseId(req.params.id)))
|
|
976
|
+
);
|
|
977
|
+
|
|
978
|
+
readwrite.post(
|
|
979
|
+
'/api/defects',
|
|
980
|
+
handle(({ db, now }, req, res) => {
|
|
981
|
+
const input = parseDefectInput(v.requireBody(req.body, DEFECT_FIELDS), { creating: true });
|
|
982
|
+
const { defect, created } = defects.createDefect(db, input, now());
|
|
983
|
+
res.status(created ? 201 : 200);
|
|
984
|
+
return defect;
|
|
985
|
+
})
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
readwrite.patch(
|
|
989
|
+
'/api/defects/:id',
|
|
990
|
+
handle(({ db, now }, req) => {
|
|
991
|
+
const id = v.parseId(req.params.id);
|
|
992
|
+
const body = v.requireBody(
|
|
993
|
+
req.body,
|
|
994
|
+
DEFECT_FIELDS.filter((field) => field !== 'clientRef')
|
|
995
|
+
);
|
|
996
|
+
return defects.updateDefect(db, id, parseDefectInput(body, { creating: false }), now());
|
|
997
|
+
})
|
|
998
|
+
);
|
|
999
|
+
|
|
677
1000
|
// Export
|
|
678
1001
|
|
|
679
1002
|
readonly.get(
|
|
@@ -707,13 +1030,13 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
707
1030
|
})
|
|
708
1031
|
);
|
|
709
1032
|
|
|
710
|
-
|
|
1033
|
+
internalReadonly.get(
|
|
711
1034
|
'/api/export/usb',
|
|
712
1035
|
handle(({ config, usbExport }) => ({ directory: config.usbExportPath, ...usbExport.status() }))
|
|
713
1036
|
);
|
|
714
1037
|
|
|
715
1038
|
// Goes through the scheduler, so it never overlaps an automatic copy.
|
|
716
|
-
|
|
1039
|
+
internalAdmin.post(
|
|
717
1040
|
'/api/export/usb',
|
|
718
1041
|
handle(({ config, usbExport }) => {
|
|
719
1042
|
if (!config.usbExportPath) {
|
|
@@ -726,14 +1049,14 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
726
1049
|
})
|
|
727
1050
|
);
|
|
728
1051
|
|
|
729
|
-
|
|
1052
|
+
internalReadonly.get(
|
|
730
1053
|
'/api/summary-mail',
|
|
731
1054
|
handle(({ summaryMailer }) => summaryMailer.status())
|
|
732
1055
|
);
|
|
733
1056
|
|
|
734
1057
|
// Sends one passage's summary now, whether or not it already went out: how
|
|
735
1058
|
// the SMTP settings are tried without waiting for the next arrival.
|
|
736
|
-
|
|
1059
|
+
internalAdmin.post(
|
|
737
1060
|
'/api/entries/:id/summary-mail',
|
|
738
1061
|
handle(async ({ db, summaryMailer }, req) => {
|
|
739
1062
|
const id = entryId(req);
|
|
@@ -746,7 +1069,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
746
1069
|
})
|
|
747
1070
|
);
|
|
748
1071
|
|
|
749
|
-
|
|
1072
|
+
internalReadonly.get(
|
|
750
1073
|
'/api/cloud-sync',
|
|
751
1074
|
handle(({ cloudSync, cloudPairing, cloudRestore }) => ({
|
|
752
1075
|
...cloudSync.status(),
|
|
@@ -756,7 +1079,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
756
1079
|
);
|
|
757
1080
|
|
|
758
1081
|
// The earlier logbooks of this boat the service can give back, asked of the service now.
|
|
759
|
-
|
|
1082
|
+
internalReadonly.get(
|
|
760
1083
|
'/api/cloud-sync/restore',
|
|
761
1084
|
handle(async ({ cloudRestore }) => {
|
|
762
1085
|
try {
|
|
@@ -769,7 +1092,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
769
1092
|
|
|
770
1093
|
// Starts bringing one of them back into this logbook, which must be empty; the restore
|
|
771
1094
|
// goes on in the background and GET /cloud-sync follows it.
|
|
772
|
-
|
|
1095
|
+
internalAdmin.post(
|
|
773
1096
|
'/api/cloud-sync/restore',
|
|
774
1097
|
handle(({ cloudRestore }, req) => {
|
|
775
1098
|
const logbookId = req.body?.logbookId;
|
|
@@ -787,7 +1110,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
787
1110
|
|
|
788
1111
|
// Forgets the pairing: the device token is removed from the plugin configuration and the
|
|
789
1112
|
// backup turns off. The service keeps what it holds, and the device until revoked there.
|
|
790
|
-
|
|
1113
|
+
internalAdmin.delete(
|
|
791
1114
|
'/api/cloud-sync/token',
|
|
792
1115
|
handle(async ({ cloudSync, cloudPairing, cloudRestore, removePairing }) => {
|
|
793
1116
|
try {
|
|
@@ -806,7 +1129,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
806
1129
|
// Asks the service for a pairing code, then waits in the background for someone to claim
|
|
807
1130
|
// it; GET /cloud-sync follows the pairing. The service is the configured one unless `url`
|
|
808
1131
|
// names another.
|
|
809
|
-
|
|
1132
|
+
internalAdmin.post(
|
|
810
1133
|
'/api/cloud-sync/pairing',
|
|
811
1134
|
handle(async ({ cloudPairing }, req) => {
|
|
812
1135
|
const url = req.body?.url;
|
|
@@ -824,7 +1147,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
824
1147
|
})
|
|
825
1148
|
);
|
|
826
1149
|
|
|
827
|
-
|
|
1150
|
+
internalAdmin.delete(
|
|
828
1151
|
'/api/cloud-sync/pairing',
|
|
829
1152
|
handle(({ cloudPairing }) => {
|
|
830
1153
|
cloudPairing.cancel();
|
|
@@ -834,7 +1157,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
834
1157
|
|
|
835
1158
|
// Starts a backup run now rather than at the next interval; the run itself
|
|
836
1159
|
// goes on in the background, through its schedule, never alongside another.
|
|
837
|
-
|
|
1160
|
+
internalAdmin.post(
|
|
838
1161
|
'/api/cloud-sync',
|
|
839
1162
|
handle(({ cloudSync, nudgeCloudSync }) => {
|
|
840
1163
|
const status = cloudSync.status();
|
|
@@ -852,7 +1175,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
852
1175
|
);
|
|
853
1176
|
|
|
854
1177
|
// Declares the link costly, or cheap again (SPEC ยง4.17).
|
|
855
|
-
|
|
1178
|
+
internalAdmin.post(
|
|
856
1179
|
'/api/cloud-sync/costly-link',
|
|
857
1180
|
handle(async ({ cloudSync, saveCostlyLink }, req) => {
|
|
858
1181
|
if (typeof req.body?.costly !== 'boolean') {
|
|
@@ -867,12 +1190,12 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
867
1190
|
})
|
|
868
1191
|
);
|
|
869
1192
|
|
|
870
|
-
|
|
1193
|
+
internalReadonly.get(
|
|
871
1194
|
'/api/replay',
|
|
872
1195
|
handle(({ replayJob }) => replayJob.status())
|
|
873
1196
|
);
|
|
874
1197
|
|
|
875
|
-
|
|
1198
|
+
internalAdmin.post(
|
|
876
1199
|
'/api/replay',
|
|
877
1200
|
handle(({ replayJob }, req) => {
|
|
878
1201
|
const from = v.parseTimestamp(req.body.from, 'from');
|
|
@@ -884,7 +1207,7 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
884
1207
|
})
|
|
885
1208
|
);
|
|
886
1209
|
|
|
887
|
-
|
|
1210
|
+
internalAdmin.post(
|
|
888
1211
|
'/api/replay/cancel',
|
|
889
1212
|
handle(({ replayJob }) => {
|
|
890
1213
|
if (!replayJob.cancel()) {
|
|
@@ -892,6 +1215,8 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
892
1215
|
}
|
|
893
1216
|
})
|
|
894
1217
|
);
|
|
1218
|
+
|
|
1219
|
+
return publicRoutes;
|
|
895
1220
|
}
|
|
896
1221
|
|
|
897
|
-
module.exports = { registerRoutes };
|
|
1222
|
+
module.exports = { registerRoutes, API_VERSIONS, FEATURES, V1_PREFIX };
|