signalk-chiplog 2.10.0 → 2.12.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 +36 -1
- package/README.md +26 -1
- package/index.js +22 -0
- package/lib/cloud-sync.js +19 -4
- package/lib/event-watcher.js +31 -1
- package/lib/plotter-extension.js +59 -0
- package/package.json +2 -1
- package/public/app.css +45 -0
- package/public/entry/entry.css +26 -0
- package/public/entry/js/components/AccessGate.mjs +1 -1
- package/public/entry/js/components/CrewDialog.mjs +2 -1
- package/public/entry/js/components/Dialogs.mjs +9 -1
- package/public/entry/js/components/StatusHeader.mjs +40 -7
- package/public/entry/js/confirm.mjs +42 -0
- package/public/entry/js/main.mjs +118 -41
- package/public/entry/js/plotter.mjs +30 -0
- package/public/entry/sw.js +5 -1
- package/public/js/api.mjs +18 -2
- package/public/js/components/LogView.mjs +1 -1
- package/public/js/components/StatusBar.mjs +26 -11
- package/public/js/components/common.mjs +114 -5
- package/public/js/connection.mjs +126 -0
- package/public/js/context.mjs +11 -0
- package/public/js/i18n.mjs +29 -2
- package/public/js/main.mjs +6 -0
- package/public/vendor/plotterext-LICENSE +21 -0
- package/public/vendor/plotterext.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,39 @@ All notable changes to Chiplog are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.12.0] - 2026-10-24
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Helm entry beside the chart.** Chartplotters supporting the Plotter Extensions API can open Chiplog from a toolbar
|
|
14
|
+
button. The iframe panel keeps drafts while hidden and follows the plotter's night mode when available. The standalone
|
|
15
|
+
tablet app and its offline queue remain available.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Event and crew deletion confirmations work inside sandboxed plotter panels.
|
|
20
|
+
|
|
21
|
+
## [2.11.0] - 2026-10-24
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **Alarms can be left out of the log.** **Log Signal K alarms** turns them off altogether, and **Alarms left out of the
|
|
26
|
+
log** lists the ones never to record, `*` standing for any characters: `notifications.chartplotter.unknown*` silences
|
|
27
|
+
a chart plotter raising `unknown114`, `unknown115` and so on. An alarm already in the log is still closed when it
|
|
28
|
+
clears.
|
|
29
|
+
- **Reconnecting after the server drops out.** When the Signal K server stops answering, or your login expires, the
|
|
30
|
+
webapp says which in a banner at the bottom of the screen, seen wherever you are on the page, and offers **Reconnect**
|
|
31
|
+
(with **Sign in** for an expired login, in another tab); once the server answers, pages reload on their own and the
|
|
32
|
+
banner says when a failed correction can be done again. On the tablet, **Not connected** gets a **Reconnect** button
|
|
33
|
+
that sends waiting entries at once, and corrections or crew changes that fail offline say so with the same button.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **The online backup sends only what the boat's plan keeps.** When the service says its plan keeps the passages from a
|
|
38
|
+
date on (Ti Bord's free plan: the last 30 days), older closed passages stay on board and are not sent, and the
|
|
39
|
+
passages counted for the backup's progress are those of that window. A passage under way is always sent; once the boat
|
|
40
|
+
is subscribed, everything is sent again, newest first.
|
|
41
|
+
|
|
9
42
|
## [2.10.0] - 2026-10-20
|
|
10
43
|
|
|
11
44
|
### Added
|
|
@@ -462,7 +495,9 @@ First release.
|
|
|
462
495
|
- REST API under `/plugins/signalk-chiplog/api`, documented in [docs/API.md](docs/API.md).
|
|
463
496
|
- Single SQLite database through Node's built-in `node:sqlite`: no native module to build.
|
|
464
497
|
|
|
465
|
-
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.
|
|
498
|
+
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.12.0...HEAD
|
|
499
|
+
[2.12.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.11.0...v2.12.0
|
|
500
|
+
[2.11.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.10.0...v2.11.0
|
|
466
501
|
[2.10.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.9.0...v2.10.0
|
|
467
502
|
[2.9.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.8.0...v2.9.0
|
|
468
503
|
[2.8.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.7.0...v2.8.0
|
package/README.md
CHANGED
|
@@ -128,7 +128,8 @@ an old value.
|
|
|
128
128
|
Added to the timeline without anyone touching anything:
|
|
129
129
|
|
|
130
130
|
- **Alarms** — any Signal K notification reaching `alarm` or `emergency` (man overboard, engine alarm, anchor watch…),
|
|
131
|
-
and when it clears.
|
|
131
|
+
and when it clears. They can be turned off altogether, or some left out by name: `notifications.chartplotter.unknown*`
|
|
132
|
+
skips `unknown114`, `unknown115` and every other path starting that way.
|
|
132
133
|
- **Autopilot** — engaged, disengaged, mode changes.
|
|
133
134
|
- **Wind** — true wind, averaged over 2 minutes, rising above 20 and 30 knots and falling back below them
|
|
134
135
|
(configurable).
|
|
@@ -260,6 +261,12 @@ shortcuts beside it; where any date is allowed, the calendar has an _Any date_ b
|
|
|
260
261
|
|
|
261
262
|
**Helm entry** in the top bar opens the tablet entry app.
|
|
262
263
|
|
|
264
|
+
**When the server drops out** — Wi-Fi gone, Signal K restarting — a banner at the bottom of the screen says the server
|
|
265
|
+
does not answer and offers **Reconnect**, wherever you are on the page. Once it answers again, pages reload what they
|
|
266
|
+
were showing, and the banner says when a correction that failed can be done again. If your Signal K login has expired
|
|
267
|
+
instead, the message says so, with **Sign in** opening the admin in another tab: sign in there, come back, and the page
|
|
268
|
+
picks up where you left it.
|
|
269
|
+
|
|
263
270
|
### Your own boat in the 3D animation ⛵
|
|
264
271
|
|
|
265
272
|
The 3D view draws a cartoon sailboat. To see yours instead, use **Boat → Use my own boat…** under the animation and pick
|
|
@@ -407,6 +414,10 @@ Keep logging. The header shows **Not connected** and how many entries are waitin
|
|
|
407
414
|
the time it was made, and sent in order as soon as the server answers again, with the position the track recorded at
|
|
408
415
|
that time. An entry sent just before the connection dropped is never logged twice.
|
|
409
416
|
|
|
417
|
+
The app tries again by itself every few seconds; **Reconnect** beside **Not connected** tries at once, and sends what is
|
|
418
|
+
waiting if the server answers. A correction or a crew change cannot wait like an entry: if it fails for want of a
|
|
419
|
+
connection, it says so with a **Reconnect** button, to do again once connected.
|
|
420
|
+
|
|
410
421
|
If the server refuses a waiting entry when it comes back — typically nothing to attach it to — it is shown in red in the
|
|
411
422
|
latest entries, to discard.
|
|
412
423
|
|
|
@@ -426,6 +437,18 @@ Logging needs read/write access. The first time, the app shows **This tablet nee
|
|
|
426
437
|
To revoke it, delete the device under **Security → Devices**: the tablet asks for access again. **Sign in instead** uses
|
|
427
438
|
a regular Signal K user account.
|
|
428
439
|
|
|
440
|
+
### From a chartplotter
|
|
441
|
+
|
|
442
|
+
In a chartplotter supporting the Signal K Plotter Extensions API (such as Freeboard-SK), the **Chiplog** toolbar button
|
|
443
|
+
opens the helm entry app in an iframe panel beside the chart. Closing and reopening it keeps notes and handwriting in
|
|
444
|
+
progress; reloading the plotter still discards unsent drafts. The usual offline queue and Signal K access rights apply.
|
|
445
|
+
The panel follows the plotter's night mode when available, and its night switch also changes the plotter's mode.
|
|
446
|
+
Standalone tablet use remains available at the same address.
|
|
447
|
+
|
|
448
|
+
The extension is advertised automatically while the plugin is enabled. A server without the resource-provider API can
|
|
449
|
+
still run the standalone apps. Journal and login navigation links are hidden in the extension: sign in through the
|
|
450
|
+
chartplotter, or use the panel's device access request.
|
|
451
|
+
|
|
429
452
|
## Configuration 🔧
|
|
430
453
|
|
|
431
454
|
In the Signal K admin, **Apps & Plugins → Configuration → Chiplog**.
|
|
@@ -459,6 +482,8 @@ In the Signal K admin, **Apps & Plugins → Configuration → Chiplog**.
|
|
|
459
482
|
| Map tiles for the summary | `https://tile.openstreetmap.org/{z}/{x}/{y}.png` | The tiles the summary map is drawn over, e.g. a self-hosted server. Empty draws the track on a plain background. |
|
|
460
483
|
| Logbook language (PDF and summary emails) | en | Language of the PDF logbooks on the USB drive and of the summary emails (English or French). |
|
|
461
484
|
| Ship's time zone (PDF and summary emails) | the server's | Time zone of the PDF logbooks on the USB drive and of the summary emails, e.g. `Europe/Paris`. |
|
|
485
|
+
| Log Signal K alarms | on | Turn off to leave every alarm out of the log. |
|
|
486
|
+
| Alarms left out of the log | — | Notification paths never logged; `*` stands for any characters, and the leading `notifications.` may be omitted. |
|
|
462
487
|
| Wind speed thresholds | 20, 30 kn | Logged when the 2-minute average true wind crosses them. |
|
|
463
488
|
| Barometric drop warning | 4 hPa / 3 h | 0 turns it off. |
|
|
464
489
|
| Log heading changes | on | Turn off to leave heading changes out of the log. |
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const { INFLUX_DEFAULTS } = require('./lib/influx-history');
|
|
|
8
8
|
const { createLandmarkFinder, LANDMARK_DEFAULTS } = require('./lib/landmark-finder');
|
|
9
9
|
const { OBSERVATION_DEFAULTS } = require('./lib/observation-recorder');
|
|
10
10
|
const { createPlaceNamer, GEOCODING_DEFAULTS } = require('./lib/place-names');
|
|
11
|
+
const { registerPlotterExtension } = require('./lib/plotter-extension');
|
|
11
12
|
const { PROPULSION_DEFAULTS } = require('./lib/propulsion-detector');
|
|
12
13
|
const { createReplayJob } = require('./lib/replay-job');
|
|
13
14
|
const { createSummaryMailer, SUMMARY_MAIL_DEFAULTS } = require('./lib/summary-mailer');
|
|
@@ -314,6 +315,21 @@ module.exports = function (app) {
|
|
|
314
315
|
description:
|
|
315
316
|
'IANA time zone the PDF logbook on the USB drive and the passage summary emails are kept in, e.g. Europe/Paris. Empty uses the server’s time zone; downloads from the webapp use the browser’s'
|
|
316
317
|
},
|
|
318
|
+
alarmsEnabled: {
|
|
319
|
+
type: 'boolean',
|
|
320
|
+
title: 'Log Signal K alarms',
|
|
321
|
+
description:
|
|
322
|
+
'Records every notification reaching alarm or emergency, when raised and when cleared; turn off to leave alarms out of the log',
|
|
323
|
+
default: EVENT_DEFAULTS.alarmsEnabled
|
|
324
|
+
},
|
|
325
|
+
ignoredAlarms: {
|
|
326
|
+
type: 'array',
|
|
327
|
+
title: 'Alarms left out of the log',
|
|
328
|
+
description:
|
|
329
|
+
'Notification paths never logged, * standing for any characters: notifications.chartplotter.unknown* leaves out unknown114, unknown115 and so on. The leading notifications. may be omitted',
|
|
330
|
+
items: { type: 'string' },
|
|
331
|
+
default: EVENT_DEFAULTS.ignoredAlarms
|
|
332
|
+
},
|
|
317
333
|
windSpeedThresholds: {
|
|
318
334
|
type: 'array',
|
|
319
335
|
title: 'Wind speed thresholds (knots)',
|
|
@@ -647,6 +663,10 @@ module.exports = function (app) {
|
|
|
647
663
|
? config.logbookLanguage
|
|
648
664
|
: 'en',
|
|
649
665
|
logbookTimeZone: config.logbookTimeZone || null,
|
|
666
|
+
alarmsEnabled: config.alarmsEnabled ?? EVENT_DEFAULTS.alarmsEnabled,
|
|
667
|
+
ignoredAlarms: Array.isArray(config.ignoredAlarms)
|
|
668
|
+
? config.ignoredAlarms
|
|
669
|
+
: EVENT_DEFAULTS.ignoredAlarms,
|
|
650
670
|
windSpeedThresholds: config.windSpeedThresholds ?? EVENT_DEFAULTS.windSpeedThresholds,
|
|
651
671
|
pressureDropThreshold: config.pressureDropThreshold ?? EVENT_DEFAULTS.pressureDropThreshold,
|
|
652
672
|
headingChangeEnabled: config.headingChangeEnabled ?? EVENT_DEFAULTS.headingChangeEnabled,
|
|
@@ -687,6 +707,8 @@ module.exports = function (app) {
|
|
|
687
707
|
throw err;
|
|
688
708
|
}
|
|
689
709
|
|
|
710
|
+
registerPlotterExtension(app, () => database !== null);
|
|
711
|
+
|
|
690
712
|
const readSelfPath = (path) => app.getSelfPath(path);
|
|
691
713
|
detector = createPassageDetector({ db: database, readSelfPath, settings });
|
|
692
714
|
const recorder = createTrackRecorder({ db: database, readSelfPath, settings });
|
package/lib/cloud-sync.js
CHANGED
|
@@ -160,6 +160,9 @@ function createCloudSync({
|
|
|
160
160
|
let progress = null;
|
|
161
161
|
// Set by a backup asked for by hand: the next run sends everything in full, costly link or not.
|
|
162
162
|
let fullRequested = false;
|
|
163
|
+
// The first moment of the logbook the boat's plan keeps on the service, as its sync state last
|
|
164
|
+
// said (`historyFrom`); null for all of it, and before the service has said anything.
|
|
165
|
+
let historyFrom = null;
|
|
163
166
|
|
|
164
167
|
const iso = (ms) => new Date(ms).toISOString();
|
|
165
168
|
const base = () => `${settings.cloudSyncUrl.replace(/\/+$/, '')}/v1/logbooks/${getLogbookId(db)}`;
|
|
@@ -260,15 +263,27 @@ function createCloudSync({
|
|
|
260
263
|
}
|
|
261
264
|
}
|
|
262
265
|
|
|
266
|
+
// A passage the service's plan keeps: begun from `historyFrom`, or still under way. The others
|
|
267
|
+
// stay on board and are not sent; the service keeps no more of them than their hashes anyway.
|
|
268
|
+
const kept = (row) =>
|
|
269
|
+
historyFrom === null || row.end_time === null || Date.parse(row.start_time) >= historyFrom;
|
|
270
|
+
|
|
263
271
|
async function synchronise(costly) {
|
|
264
|
-
const
|
|
265
|
-
|
|
272
|
+
const onBoardRows = db
|
|
273
|
+
.prepare('SELECT * FROM log_entries ORDER BY start_time DESC, id DESC')
|
|
274
|
+
.all();
|
|
275
|
+
// How many passages are on board for the service to keep: it tells its owner how far the
|
|
276
|
+
// backup is. Counted by the window it last gave, the first run by none.
|
|
266
277
|
const state = await call('GET', '/sync-state', undefined, {
|
|
267
|
-
'x-chiplog-passages': String(
|
|
278
|
+
'x-chiplog-passages': String(onBoardRows.filter(kept).length)
|
|
268
279
|
});
|
|
280
|
+
const from = typeof state.historyFrom === 'string' ? Date.parse(state.historyFrom) : NaN;
|
|
281
|
+
historyFrom = Number.isNaN(from) ? null : from;
|
|
282
|
+
const rows = onBoardRows.filter(kept);
|
|
269
283
|
const remote = state.passages;
|
|
270
284
|
const held = new Map(remote.map((item) => [item.originId, item]));
|
|
271
|
-
|
|
285
|
+
// Passages left out by the plan are still on board: the service is not told to delete them.
|
|
286
|
+
const onBoard = new Set(onBoardRows.map((row) => row.id));
|
|
272
287
|
for (const id of cache.keys()) {
|
|
273
288
|
if (!onBoard.has(id)) {
|
|
274
289
|
cache.delete(id);
|
package/lib/event-watcher.js
CHANGED
|
@@ -5,6 +5,8 @@ const { CRITICAL_STATES } = require('./events');
|
|
|
5
5
|
const METRES_PER_SECOND_PER_KNOT = 1852 / 3600;
|
|
6
6
|
|
|
7
7
|
const EVENT_DEFAULTS = {
|
|
8
|
+
alarmsEnabled: true,
|
|
9
|
+
ignoredAlarms: [],
|
|
8
10
|
windSpeedThresholds: [20, 30],
|
|
9
11
|
pressureDropThreshold: 4,
|
|
10
12
|
headingChangeEnabled: true,
|
|
@@ -88,8 +90,30 @@ function collectNotifications(node, path, found, depth = 0) {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
// A pattern names a notification path, `*` standing for any run of
|
|
94
|
+
// characters (dots included); the leading `notifications.` may be left out.
|
|
95
|
+
function compileAlarmPattern(pattern) {
|
|
96
|
+
const trimmed = String(pattern).trim();
|
|
97
|
+
if (trimmed === '') {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const full = trimmed.startsWith('notifications.') ? trimmed : `notifications.${trimmed}`;
|
|
101
|
+
const source = full
|
|
102
|
+
.split('*')
|
|
103
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
|
|
104
|
+
.join('.*');
|
|
105
|
+
return new RegExp(`^${source}$`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createAlarmFilter(patterns) {
|
|
109
|
+
const compiled = (patterns ?? []).map(compileAlarmPattern).filter(Boolean);
|
|
110
|
+
return (path) => compiled.some((pattern) => pattern.test(path));
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
function createEventWatcher({ db, readSelfPath, settings, observe, clock = Date.now }) {
|
|
92
114
|
const readFresh = createFreshnessTracker(readSelfPath);
|
|
115
|
+
const alarmsEnabled = settings.alarmsEnabled ?? EVENT_DEFAULTS.alarmsEnabled;
|
|
116
|
+
const isIgnoredAlarm = createAlarmFilter(settings.ignoredAlarms);
|
|
93
117
|
|
|
94
118
|
const seenNotifications = new Map();
|
|
95
119
|
const loggedNotifications = new Map();
|
|
@@ -173,6 +197,12 @@ function createEventWatcher({ db, readSelfPath, settings, observe, clock = Date.
|
|
|
173
197
|
|
|
174
198
|
const logged = lastLoggedState(path);
|
|
175
199
|
const critical = CRITICAL_STATES.has(state);
|
|
200
|
+
// Alarms turned off, or this one left out, are not raised in the log;
|
|
201
|
+
// one already logged is still closed when it clears, so no raise is
|
|
202
|
+
// left without its end.
|
|
203
|
+
if (critical && (!alarmsEnabled || isIgnoredAlarm(path))) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
176
206
|
// Log a critical state not yet logged, or the end of one that was; a
|
|
177
207
|
// clearing whose alarm never made it into the log is not news.
|
|
178
208
|
const worthLogging = critical ? logged !== state : CRITICAL_STATES.has(logged);
|
|
@@ -467,4 +497,4 @@ function createEventWatcher({ db, readSelfPath, settings, observe, clock = Date.
|
|
|
467
497
|
};
|
|
468
498
|
}
|
|
469
499
|
|
|
470
|
-
module.exports = { createEventWatcher, EVENT_DEFAULTS, CHECK_INTERVAL_MS };
|
|
500
|
+
module.exports = { createEventWatcher, createAlarmFilter, EVENT_DEFAULTS, CHECK_INTERVAL_MS };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const { version } = require('../package.json');
|
|
2
|
+
|
|
3
|
+
const EXTENSION_ID = 'signalk-chiplog';
|
|
4
|
+
|
|
5
|
+
function buildManifest() {
|
|
6
|
+
return {
|
|
7
|
+
name: 'Chiplog — Helm entry',
|
|
8
|
+
description: 'Log manoeuvres, notes and handwriting without leaving the chart.',
|
|
9
|
+
version,
|
|
10
|
+
apiVersion: '1',
|
|
11
|
+
requires: ['panels.iframe', 'buttons'],
|
|
12
|
+
optional: ['nightMode'],
|
|
13
|
+
panels: [
|
|
14
|
+
{
|
|
15
|
+
id: 'helm-entry',
|
|
16
|
+
title: 'Chiplog',
|
|
17
|
+
type: 'iframe',
|
|
18
|
+
url: '/signalk-chiplog/entry/?plotterExtension=1',
|
|
19
|
+
lifecycle: 'keepAlive'
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
buttons: [
|
|
23
|
+
{
|
|
24
|
+
id: 'open-helm-entry',
|
|
25
|
+
title: 'Chiplog',
|
|
26
|
+
slot: 'mapToolbar',
|
|
27
|
+
icon: 'edit_note',
|
|
28
|
+
action: { type: 'togglePanel', panel: 'helm-entry' }
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function registerPlotterExtension(app, isRunning) {
|
|
35
|
+
// Older servers can still run the standalone logbook and tablet app.
|
|
36
|
+
if (typeof app.registerResourceProvider !== 'function') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
app.registerResourceProvider({
|
|
40
|
+
type: 'plotterExtensions',
|
|
41
|
+
methods: {
|
|
42
|
+
listResources: async () => (isRunning() ? { [EXTENSION_ID]: buildManifest() } : {}),
|
|
43
|
+
getResource: async (id) => {
|
|
44
|
+
if (!isRunning() || id !== EXTENSION_ID) {
|
|
45
|
+
throw new Error(`No such plotterExtensions resource: ${id}`);
|
|
46
|
+
}
|
|
47
|
+
return buildManifest();
|
|
48
|
+
},
|
|
49
|
+
setResource: async () => {
|
|
50
|
+
throw new Error('Chiplog plotter extension is read-only');
|
|
51
|
+
},
|
|
52
|
+
deleteResource: async () => {
|
|
53
|
+
throw new Error('Chiplog plotter extension is read-only');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { registerPlotterExtension };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-chiplog",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "Nautical logbook for Signal K: automatic entries, handwritten notes",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"mediabunny": "1.58.0",
|
|
82
82
|
"preact": "^10.29.8",
|
|
83
83
|
"prettier": "^3.9.6",
|
|
84
|
+
"signalk-plotterext-bus": "^0.21.0",
|
|
84
85
|
"three": "0.186.0"
|
|
85
86
|
}
|
|
86
87
|
}
|
package/public/app.css
CHANGED
|
@@ -251,6 +251,51 @@ main {
|
|
|
251
251
|
border-color: transparent;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
.notice-actions {
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-wrap: wrap;
|
|
257
|
+
align-items: center;
|
|
258
|
+
gap: 0.75rem;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Above the map's controls, wherever the page is scrolled. */
|
|
262
|
+
.connection-toast {
|
|
263
|
+
position: fixed;
|
|
264
|
+
left: 50%;
|
|
265
|
+
bottom: 1rem;
|
|
266
|
+
z-index: 1100;
|
|
267
|
+
transform: translateX(-50%);
|
|
268
|
+
width: min(40rem, calc(100% - 2rem));
|
|
269
|
+
box-sizing: border-box;
|
|
270
|
+
display: flex;
|
|
271
|
+
flex-wrap: wrap;
|
|
272
|
+
align-items: center;
|
|
273
|
+
justify-content: space-between;
|
|
274
|
+
gap: 0.5rem 1rem;
|
|
275
|
+
padding: 0.75rem 1rem;
|
|
276
|
+
border-radius: var(--radius);
|
|
277
|
+
background: var(--error-bg);
|
|
278
|
+
color: var(--text);
|
|
279
|
+
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.25);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.connection-toast p {
|
|
283
|
+
margin: 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.connection-toast-ok {
|
|
287
|
+
background: var(--ok-bg);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.connection-toast .toast-close {
|
|
291
|
+
border: none;
|
|
292
|
+
background: none;
|
|
293
|
+
color: inherit;
|
|
294
|
+
font-size: 1.4rem;
|
|
295
|
+
line-height: 1;
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
}
|
|
298
|
+
|
|
254
299
|
.notice-ok {
|
|
255
300
|
background: var(--ok-bg);
|
|
256
301
|
border-color: transparent;
|
package/public/entry/entry.css
CHANGED
|
@@ -749,6 +749,32 @@ input {
|
|
|
749
749
|
border-radius: var(--radius);
|
|
750
750
|
}
|
|
751
751
|
|
|
752
|
+
.confirmation-dialog {
|
|
753
|
+
width: min(40rem, calc(100% - 2rem));
|
|
754
|
+
max-height: calc(100% - 2rem);
|
|
755
|
+
padding: 0;
|
|
756
|
+
border: 0;
|
|
757
|
+
border-radius: var(--radius);
|
|
758
|
+
background: var(--surface);
|
|
759
|
+
color: var(--text);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.confirmation-dialog::backdrop {
|
|
763
|
+
background: rgb(0 0 0 / 55%);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.confirmation-dialog .sheet {
|
|
767
|
+
width: 100%;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
:root[data-plotter-extension='true'] [data-standalone-link] {
|
|
771
|
+
display: none;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
:root[data-plotter-extension='true'] .entry-main {
|
|
775
|
+
padding: 0.75rem 0.75rem 9rem;
|
|
776
|
+
}
|
|
777
|
+
|
|
752
778
|
.sail-grid {
|
|
753
779
|
display: grid;
|
|
754
780
|
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
|
@@ -63,7 +63,7 @@ export function AccessGate({ onGranted }) {
|
|
|
63
63
|
${t('access.request')}
|
|
64
64
|
</button>`
|
|
65
65
|
}
|
|
66
|
-
<a class="big-button" href="/admin/#/login">${t('access.signIn')}</a>
|
|
66
|
+
<a class="big-button" data-standalone-link href="/admin/#/login">${t('access.signIn')}</a>
|
|
67
67
|
</div>
|
|
68
68
|
</section>
|
|
69
69
|
`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { html, useState } from '../../../vendor/preact-htm.mjs';
|
|
2
2
|
import { useLocale } from '../../../js/context.mjs';
|
|
3
3
|
import { PencilIcon, TrashIcon } from './Icons.mjs';
|
|
4
|
+
import { confirmAction } from '../confirm.mjs';
|
|
4
5
|
|
|
5
6
|
// .big-button is a row flex container, so a bare <br/> between its children
|
|
6
7
|
// would not stack them -- wrap name and role in their own column instead.
|
|
@@ -83,7 +84,7 @@ export function CrewDialog({
|
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
const removeMember = async (member) => {
|
|
86
|
-
if (!
|
|
87
|
+
if (!(await confirmAction(t('entry.deleteCrewMemberConfirm', { name: member.name }), t))) {
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
const removed = await onDeleteMember(member.id);
|
|
@@ -67,7 +67,7 @@ export function EventDialog({ title, event, bounds, onSave, onCancel }) {
|
|
|
67
67
|
// say the entry is in, a countdown shows the banner will go on its own, and the
|
|
68
68
|
// two follow-ups are secondary next to it — undoing is the destructive one, and
|
|
69
69
|
// says what it undoes, since on its own "undo" reads as dismissing the banner.
|
|
70
|
-
export function Toast({ toast, timeoutMs, onUndo, onComment, onClose }) {
|
|
70
|
+
export function Toast({ toast, timeoutMs, onUndo, onComment, onClose, onReconnect }) {
|
|
71
71
|
const { t, format } = useLocale();
|
|
72
72
|
if (!toast) {
|
|
73
73
|
return null;
|
|
@@ -85,6 +85,14 @@ export function Toast({ toast, timeoutMs, onUndo, onComment, onClose }) {
|
|
|
85
85
|
return html`
|
|
86
86
|
<div class="toast toast-error" role="alert">
|
|
87
87
|
<span class="toast-text">${toast.message}</span>
|
|
88
|
+
${
|
|
89
|
+
toast.reconnect &&
|
|
90
|
+
html`<span class="toast-actions">
|
|
91
|
+
<button type="button" class="tool-button" onClick=${onReconnect}>
|
|
92
|
+
${t('connection.reconnect')}
|
|
93
|
+
</button>
|
|
94
|
+
</span>`
|
|
95
|
+
}
|
|
88
96
|
${close}
|
|
89
97
|
</div>
|
|
90
98
|
`;
|
|
@@ -34,9 +34,40 @@ function passageLine(entry, t, format) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// While the server does not answer, the connection mark is a button that
|
|
38
|
+
// tries again at once, rather than at the next poll.
|
|
39
|
+
function ConnectionMark({ online, connecting, queued, onReconnect }) {
|
|
40
|
+
const { t } = useLocale();
|
|
39
41
|
const connection = online ? t('entry.online') : t('entry.offline');
|
|
42
|
+
const label = queued > 0 ? `${connection} · ${t('entry.queued', { count: queued })}` : connection;
|
|
43
|
+
if (online) {
|
|
44
|
+
return html`<span class="connection connection-online">
|
|
45
|
+
<span class="connection-dot" aria-hidden="true"></span>
|
|
46
|
+
${label}
|
|
47
|
+
</span>`;
|
|
48
|
+
}
|
|
49
|
+
return html`
|
|
50
|
+
<span class="connection connection-offline">
|
|
51
|
+
<span class="connection-dot" aria-hidden="true"></span>
|
|
52
|
+
${label}
|
|
53
|
+
<button type="button" class="tool-button" disabled=${connecting} onClick=${onReconnect}>
|
|
54
|
+
${connecting ? t('entry.reconnecting') : t('connection.reconnect')}
|
|
55
|
+
</button>
|
|
56
|
+
</span>
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function StatusHeader({
|
|
61
|
+
state,
|
|
62
|
+
entry,
|
|
63
|
+
online,
|
|
64
|
+
connecting,
|
|
65
|
+
queued,
|
|
66
|
+
night,
|
|
67
|
+
onToggleNight,
|
|
68
|
+
onReconnect
|
|
69
|
+
}) {
|
|
70
|
+
const { t, format } = useLocale();
|
|
40
71
|
return html`
|
|
41
72
|
<header class="entry-header">
|
|
42
73
|
<div class="entry-status">
|
|
@@ -50,10 +81,12 @@ export function StatusHeader({ state, entry, online, queued, night, onToggleNigh
|
|
|
50
81
|
}
|
|
51
82
|
</div>
|
|
52
83
|
<div class="entry-tools">
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
<${ConnectionMark}
|
|
85
|
+
online=${online}
|
|
86
|
+
connecting=${connecting}
|
|
87
|
+
queued=${queued}
|
|
88
|
+
onReconnect=${onReconnect}
|
|
89
|
+
/>
|
|
57
90
|
<button
|
|
58
91
|
type="button"
|
|
59
92
|
class="tool-button"
|
|
@@ -62,7 +95,7 @@ export function StatusHeader({ state, entry, online, queued, night, onToggleNigh
|
|
|
62
95
|
>
|
|
63
96
|
${t('entry.night')}
|
|
64
97
|
</button>
|
|
65
|
-
<a class="tool-button" href="../">${t('entry.logbook')}</a>
|
|
98
|
+
<a class="tool-button" data-standalone-link href="../">${t('entry.logbook')}</a>
|
|
66
99
|
</div>
|
|
67
100
|
</header>
|
|
68
101
|
`;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Native confirm() is blocked in plotter-extension sandboxes. An HTML dialog
|
|
2
|
+
// works there, traps focus, supports Escape, and keeps the same confirmation
|
|
3
|
+
// in the standalone app. Text is never interpreted as HTML.
|
|
4
|
+
export function confirmAction(message, t) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const dialog = document.createElement('dialog');
|
|
7
|
+
dialog.className = 'confirmation-dialog';
|
|
8
|
+
dialog.setAttribute('aria-label', message);
|
|
9
|
+
const form = document.createElement('form');
|
|
10
|
+
form.method = 'dialog';
|
|
11
|
+
form.className = 'sheet';
|
|
12
|
+
const text = document.createElement('p');
|
|
13
|
+
text.textContent = message;
|
|
14
|
+
const actions = document.createElement('div');
|
|
15
|
+
actions.className = 'sheet-row';
|
|
16
|
+
for (const [value, label, className] of [
|
|
17
|
+
['cancel', t('common.cancel'), 'big-button'],
|
|
18
|
+
['confirm', t('common.delete'), 'big-button danger']
|
|
19
|
+
]) {
|
|
20
|
+
const button = document.createElement('button');
|
|
21
|
+
button.type = 'submit';
|
|
22
|
+
button.value = value;
|
|
23
|
+
button.textContent = label;
|
|
24
|
+
button.className = className;
|
|
25
|
+
button.autofocus = value === 'cancel';
|
|
26
|
+
actions.append(button);
|
|
27
|
+
}
|
|
28
|
+
form.append(text, actions);
|
|
29
|
+
dialog.append(form);
|
|
30
|
+
dialog.addEventListener(
|
|
31
|
+
'close',
|
|
32
|
+
() => {
|
|
33
|
+
const confirmed = dialog.returnValue === 'confirm';
|
|
34
|
+
dialog.remove();
|
|
35
|
+
resolve(confirmed);
|
|
36
|
+
},
|
|
37
|
+
{ once: true }
|
|
38
|
+
);
|
|
39
|
+
document.body.append(dialog);
|
|
40
|
+
dialog.showModal();
|
|
41
|
+
});
|
|
42
|
+
}
|