signalk-chiplog 2.12.0 → 2.13.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 +18 -1
- package/README.md +8 -5
- package/index.js +28 -2
- package/lib/api.js +24 -4
- package/lib/cloud-pairing.js +27 -13
- package/lib/cloud-restore.js +13 -8
- package/lib/cloud-sync.js +14 -6
- package/lib/network-error.js +72 -0
- package/package.json +1 -1
- package/public/app.css +0 -6
- package/public/js/components/CloudBackup.mjs +14 -18
- package/public/js/i18n.mjs +10 -6
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ All notable changes to Chiplog are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.13.0] - 2026-10-24
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Remove the pairing** from the Export page, after a confirmation: the plugin forgets its token and the backup stops.
|
|
14
|
+
What is already on the service stays there.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Online backup says why the service cannot be reached.** Instead of "fetch failed", pairing, backup and restore name
|
|
19
|
+
the host and the cause (name not resolved, connection refused, certificate refused, no answer in time), with a hint
|
|
20
|
+
for the common ones — a wrong clock behind a certificate refused. A pairing that cannot start is written to the server
|
|
21
|
+
log.
|
|
22
|
+
- **Pairing no longer asks for the service address.** It is `https://app.tibord.com` for a new plugin; another one, for
|
|
23
|
+
testing or development, is set in the plugin configuration.
|
|
24
|
+
|
|
9
25
|
## [2.12.0] - 2026-10-24
|
|
10
26
|
|
|
11
27
|
### Added
|
|
@@ -495,7 +511,8 @@ First release.
|
|
|
495
511
|
- REST API under `/plugins/signalk-chiplog/api`, documented in [docs/API.md](docs/API.md).
|
|
496
512
|
- Single SQLite database through Node's built-in `node:sqlite`: no native module to build.
|
|
497
513
|
|
|
498
|
-
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.
|
|
514
|
+
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.13.0...HEAD
|
|
515
|
+
[2.13.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.12.0...v2.13.0
|
|
499
516
|
[2.12.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.11.0...v2.12.0
|
|
500
517
|
[2.11.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.10.0...v2.11.0
|
|
501
518
|
[2.10.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.9.0...v2.10.0
|
package/README.md
CHANGED
|
@@ -537,14 +537,17 @@ Keep a copy of the logbook off the boat, on the Ti Bord online service. What it
|
|
|
537
537
|
and how you can share your passages and voyages from it, is at [tibord.com](https://tibord.com), which the **Online
|
|
538
538
|
backup** card links to as well.
|
|
539
539
|
|
|
540
|
-
1. On the **Export** page, under **Online backup**,
|
|
540
|
+
1. On the **Export** page, under **Online backup**, choose **Get a pairing code**.
|
|
541
541
|
2. On the service, signed in to your account, enter the code shown (e.g. `K7QF-3MXB`) and choose your boat — or scan the
|
|
542
542
|
QR code beside it with your phone, which opens the service with the code filled in. The code is valid for 15 minutes.
|
|
543
|
-
3. The page soon says the boat is paired: Chiplog has saved
|
|
544
|
-
|
|
543
|
+
3. The page soon says the boat is paired: Chiplog has saved its own token in the plugin configuration and turned the
|
|
544
|
+
backup on.
|
|
545
545
|
|
|
546
|
-
You can also paste a token by hand: turn **Back the logbook up online** on in the plugin configuration
|
|
547
|
-
**Online service address**
|
|
546
|
+
You can also paste a token by hand: turn **Back the logbook up online** on in the plugin configuration and fill in the
|
|
547
|
+
**Device token**. The **Online service address** is `https://app.tibord.com`; change it only to test another instance.
|
|
548
|
+
|
|
549
|
+
**Remove the pairing**, on the same card, asks for a confirmation, then forgets the token and stops the backup. What is
|
|
550
|
+
already on the service stays there; revoke the device from your account on the service to stop it for good.
|
|
548
551
|
|
|
549
552
|
From then on, whenever the boat has a connection, Chiplog sends:
|
|
550
553
|
|
package/index.js
CHANGED
|
@@ -278,7 +278,9 @@ module.exports = function (app) {
|
|
|
278
278
|
cloudSyncUrl: {
|
|
279
279
|
type: 'string',
|
|
280
280
|
title: 'Online service address',
|
|
281
|
-
description:
|
|
281
|
+
description:
|
|
282
|
+
'The address of the Ti Bord service. Leave it as it is unless you test another instance',
|
|
283
|
+
default: CLOUD_SYNC_DEFAULTS.cloudSyncUrl
|
|
282
284
|
},
|
|
283
285
|
cloudSyncToken: {
|
|
284
286
|
type: 'string',
|
|
@@ -591,6 +593,28 @@ module.exports = function (app) {
|
|
|
591
593
|
});
|
|
592
594
|
}
|
|
593
595
|
|
|
596
|
+
// Forgets the pairing from the Export page: the token leaves the plugin configuration and the
|
|
597
|
+
// backup turns off; the service address stays. What the service holds stays there.
|
|
598
|
+
function removePairing() {
|
|
599
|
+
const next = { ...savedConfig, cloudSyncEnabled: false };
|
|
600
|
+
delete next.cloudSyncToken;
|
|
601
|
+
return new Promise((resolve, reject) => {
|
|
602
|
+
app.savePluginOptions(next, (err) => {
|
|
603
|
+
if (err) {
|
|
604
|
+
reject(err);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
savedConfig = next;
|
|
608
|
+
if (settings) {
|
|
609
|
+
settings.cloudSyncEnabled = false;
|
|
610
|
+
settings.cloudSyncToken = null;
|
|
611
|
+
}
|
|
612
|
+
cloudPairing?.cancel();
|
|
613
|
+
resolve();
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
594
618
|
// Declares the link costly or cheap from the Export page, saved like the rest of the
|
|
595
619
|
// configuration. Back to cheap, a run starts at once: full passages have been waiting.
|
|
596
620
|
function saveCostlyLink(costly) {
|
|
@@ -654,7 +678,7 @@ module.exports = function (app) {
|
|
|
654
678
|
? SUMMARY_MAIL_DEFAULTS.summaryMailTileUrl
|
|
655
679
|
: config.summaryMailTileUrl,
|
|
656
680
|
cloudSyncEnabled: config.cloudSyncEnabled ?? CLOUD_SYNC_DEFAULTS.cloudSyncEnabled,
|
|
657
|
-
cloudSyncUrl: config.cloudSyncUrl ||
|
|
681
|
+
cloudSyncUrl: config.cloudSyncUrl || CLOUD_SYNC_DEFAULTS.cloudSyncUrl,
|
|
658
682
|
cloudSyncToken: config.cloudSyncToken || null,
|
|
659
683
|
cloudSyncIntervalMinutes:
|
|
660
684
|
config.cloudSyncIntervalMinutes ?? CLOUD_SYNC_DEFAULTS.cloudSyncIntervalMinutes,
|
|
@@ -765,6 +789,7 @@ module.exports = function (app) {
|
|
|
765
789
|
});
|
|
766
790
|
cloudPairing = createCloudPairing({
|
|
767
791
|
db: database,
|
|
792
|
+
serviceUrl: () => settings.cloudSyncUrl,
|
|
768
793
|
userAgent: `signalk-chiplog/${version}`,
|
|
769
794
|
log,
|
|
770
795
|
onPaired: ({ url, token }) => savePairing(url, token)
|
|
@@ -886,6 +911,7 @@ module.exports = function (app) {
|
|
|
886
911
|
cloudSync,
|
|
887
912
|
nudgeCloudSync: () => cloudSchedule?.nudge(),
|
|
888
913
|
saveCostlyLink,
|
|
914
|
+
removePairing,
|
|
889
915
|
cloudPairing,
|
|
890
916
|
cloudRestore,
|
|
891
917
|
replayJob,
|
package/lib/api.js
CHANGED
|
@@ -778,16 +778,36 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
778
778
|
})
|
|
779
779
|
);
|
|
780
780
|
|
|
781
|
+
// Forgets the pairing: the device token is removed from the plugin configuration and the
|
|
782
|
+
// backup turns off. The service keeps what it holds, and the device until revoked there.
|
|
783
|
+
admin.delete(
|
|
784
|
+
'/api/cloud-sync/token',
|
|
785
|
+
handle(async ({ cloudSync, cloudPairing, cloudRestore, removePairing }) => {
|
|
786
|
+
try {
|
|
787
|
+
await removePairing();
|
|
788
|
+
} catch (err) {
|
|
789
|
+
throw conflict('cloud_sync_save_failed', err.message);
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
...cloudSync.status(),
|
|
793
|
+
pairing: cloudPairing.status(),
|
|
794
|
+
restore: cloudRestore.status()
|
|
795
|
+
};
|
|
796
|
+
})
|
|
797
|
+
);
|
|
798
|
+
|
|
781
799
|
// Asks the service for a pairing code, then waits in the background for someone to claim
|
|
782
|
-
// it; GET /cloud-sync follows the pairing.
|
|
800
|
+
// it; GET /cloud-sync follows the pairing. The service is the configured one unless `url`
|
|
801
|
+
// names another.
|
|
783
802
|
admin.post(
|
|
784
803
|
'/api/cloud-sync/pairing',
|
|
785
804
|
handle(async ({ cloudPairing }, req) => {
|
|
786
|
-
|
|
787
|
-
|
|
805
|
+
const url = req.body?.url;
|
|
806
|
+
if (url !== undefined && (typeof url !== 'string' || url.trim() === '')) {
|
|
807
|
+
throw badRequest('url must be a non-empty string');
|
|
788
808
|
}
|
|
789
809
|
try {
|
|
790
|
-
return await cloudPairing.start(
|
|
810
|
+
return await cloudPairing.start(url?.trim());
|
|
791
811
|
} catch (err) {
|
|
792
812
|
if (err.code === 'invalid_url') {
|
|
793
813
|
throw badRequest(err.message);
|
package/lib/cloud-pairing.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { getLogbookId } = require('./cloud-sync');
|
|
2
2
|
const { qrCode } = require('./qr');
|
|
3
|
+
const { fetchExplained } = require('./network-error');
|
|
3
4
|
|
|
4
5
|
const REQUEST_TIMEOUT_MS = 30 * 1000;
|
|
5
6
|
// When the service is out of reach mid-pairing: keep asking, as long as the code lives.
|
|
@@ -32,6 +33,8 @@ function serviceUrl(text) {
|
|
|
32
33
|
// given the service address and the token, to save them in the plugin configuration.
|
|
33
34
|
function createCloudPairing({
|
|
34
35
|
db,
|
|
36
|
+
// The address to pair with when none is given: the one in the plugin configuration.
|
|
37
|
+
serviceUrl: configuredUrl = () => null,
|
|
35
38
|
userAgent,
|
|
36
39
|
onPaired,
|
|
37
40
|
log,
|
|
@@ -44,16 +47,20 @@ function createCloudPairing({
|
|
|
44
47
|
let timer = null;
|
|
45
48
|
|
|
46
49
|
async function call(url, path, body) {
|
|
47
|
-
const response = await
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'
|
|
52
|
-
|
|
50
|
+
const response = await fetchExplained(
|
|
51
|
+
fetch,
|
|
52
|
+
`${url}/v1${path}`,
|
|
53
|
+
{
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'content-type': 'application/json',
|
|
57
|
+
'user-agent': userAgent,
|
|
58
|
+
'x-chiplog-version': userAgent.split('/')[1] ?? ''
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify(body)
|
|
53
61
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
62
|
+
REQUEST_TIMEOUT_MS
|
|
63
|
+
);
|
|
57
64
|
const data = await response.json().catch(() => ({}));
|
|
58
65
|
if (!response.ok) {
|
|
59
66
|
throw new PairingError(data.message ?? `The service answered ${response.status}`, {
|
|
@@ -87,6 +94,7 @@ function createCloudPairing({
|
|
|
87
94
|
state = { state: 'expired', at: new Date(clock()).toISOString() };
|
|
88
95
|
} else if (!(error instanceof PairingError)) {
|
|
89
96
|
// Out of reach for a moment: the code is still good.
|
|
97
|
+
log('debug', `Online backup pairing: ${error.message}; retrying`);
|
|
90
98
|
schedule(current, RETRY_MS);
|
|
91
99
|
} else {
|
|
92
100
|
attempt = null;
|
|
@@ -119,16 +127,22 @@ function createCloudPairing({
|
|
|
119
127
|
|
|
120
128
|
return {
|
|
121
129
|
// Asks the service for a code and starts waiting for it to be claimed. A pairing already
|
|
122
|
-
// under way is dropped: its code was not used.
|
|
123
|
-
async start(text) {
|
|
124
|
-
const url = serviceUrl(text);
|
|
130
|
+
// under way is dropped: its code was not used. Without an address, the configured one.
|
|
131
|
+
async start(text = configuredUrl()) {
|
|
132
|
+
const url = serviceUrl(text ?? '');
|
|
125
133
|
if (!url) {
|
|
126
134
|
throw new PairingError('The service address must be an http or https URL', {
|
|
127
135
|
code: 'invalid_url'
|
|
128
136
|
});
|
|
129
137
|
}
|
|
130
138
|
this.cancel();
|
|
131
|
-
|
|
139
|
+
let pairing;
|
|
140
|
+
try {
|
|
141
|
+
pairing = await call(url, '/pairings', { logbookId: getLogbookId(db) });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
log('error', `Online backup pairing could not start: ${error.message}`);
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
132
146
|
const current = {
|
|
133
147
|
url,
|
|
134
148
|
pairingId: pairing.pairingId,
|
package/lib/cloud-restore.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { withTransaction } = require('./database');
|
|
2
2
|
const { getLogbookId, isRestoring } = require('./cloud-sync');
|
|
3
3
|
const { TRACK_READINGS, OBSERVATION_READINGS } = require('./passage-import');
|
|
4
|
+
const { fetchExplained } = require('./network-error');
|
|
4
5
|
|
|
5
6
|
const REQUEST_TIMEOUT_MS = 2 * 60 * 1000;
|
|
6
7
|
|
|
@@ -250,15 +251,19 @@ function createCloudRestore({
|
|
|
250
251
|
const isEmpty = () => db.prepare('SELECT count(*) AS n FROM log_entries').get().n === 0;
|
|
251
252
|
|
|
252
253
|
async function call(path) {
|
|
253
|
-
const response = await
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
'
|
|
258
|
-
|
|
254
|
+
const response = await fetchExplained(
|
|
255
|
+
fetch,
|
|
256
|
+
`${settings.cloudSyncUrl.replace(/\/+$/, '')}/v1${path}`,
|
|
257
|
+
{
|
|
258
|
+
method: 'GET',
|
|
259
|
+
headers: {
|
|
260
|
+
authorization: `Bearer ${settings.cloudSyncToken}`,
|
|
261
|
+
'user-agent': userAgent,
|
|
262
|
+
'x-chiplog-version': userAgent.split('/')[1] ?? ''
|
|
263
|
+
}
|
|
259
264
|
},
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
REQUEST_TIMEOUT_MS
|
|
266
|
+
);
|
|
262
267
|
if (!response.ok) {
|
|
263
268
|
let detail = {};
|
|
264
269
|
try {
|
package/lib/cloud-sync.js
CHANGED
|
@@ -9,9 +9,13 @@ const { allSegments } = require('./propulsion');
|
|
|
9
9
|
const { getTideForecast } = require('./tide-forecaster');
|
|
10
10
|
const { allObservations, allTrackPoints } = require('./track');
|
|
11
11
|
const { getWeatherForecast } = require('./weather-forecaster');
|
|
12
|
+
const { fetchExplained } = require('./network-error');
|
|
12
13
|
|
|
13
14
|
const CLOUD_SYNC_DEFAULTS = {
|
|
14
15
|
cloudSyncEnabled: false,
|
|
16
|
+
// Where every boat backs up; another address is for testing and development, set in the
|
|
17
|
+
// plugin configuration.
|
|
18
|
+
cloudSyncUrl: 'https://app.tibord.com',
|
|
15
19
|
cloudSyncIntervalMinutes: 15,
|
|
16
20
|
cloudSyncCostlyLink: false
|
|
17
21
|
};
|
|
@@ -180,12 +184,16 @@ function createCloudSync({
|
|
|
180
184
|
headers['content-encoding'] = 'gzip';
|
|
181
185
|
payload = zlib.gzipSync(JSON.stringify(body));
|
|
182
186
|
}
|
|
183
|
-
const response = await
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
const response = await fetchExplained(
|
|
188
|
+
fetch,
|
|
189
|
+
`${base()}${path}`,
|
|
190
|
+
{
|
|
191
|
+
method,
|
|
192
|
+
headers,
|
|
193
|
+
body: payload
|
|
194
|
+
},
|
|
195
|
+
REQUEST_TIMEOUT_MS
|
|
196
|
+
);
|
|
189
197
|
if (!response.ok) {
|
|
190
198
|
let detail = {};
|
|
191
199
|
try {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Node's fetch says only "fetch failed" when no answer came back; the reason — a name that does
|
|
2
|
+
// not resolve, a refused connection, a certificate refused — is in `cause`, which the plugin
|
|
3
|
+
// would otherwise lose. These turn such a failure into one sentence for the log and the page.
|
|
4
|
+
|
|
5
|
+
// A hint for the causes a boat meets most, where the fix is not obvious from the code alone.
|
|
6
|
+
const HINTS = [
|
|
7
|
+
[/^(ENOTFOUND|EAI_AGAIN)$/, 'check the address and that this server can resolve names (DNS)'],
|
|
8
|
+
[
|
|
9
|
+
/^(CERT_|UNABLE_TO_VERIFY|SELF_SIGNED|DEPTH_ZERO|ERR_TLS_CERT)/,
|
|
10
|
+
"check this server's clock: a wrong date makes valid certificates look invalid"
|
|
11
|
+
],
|
|
12
|
+
[/^(ECONNREFUSED|ECONNRESET|EHOSTUNREACH|ENETUNREACH)$/, 'check the address and the network']
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function describeCause(cause) {
|
|
16
|
+
if (!cause) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
// Several addresses tried (IPv4 and IPv6): each has its own reason.
|
|
20
|
+
if (Array.isArray(cause.errors) && cause.errors.length > 0) {
|
|
21
|
+
const parts = [
|
|
22
|
+
...new Set(cause.errors.map((each) => describeCause(each)?.text).filter(Boolean))
|
|
23
|
+
];
|
|
24
|
+
return { code: cause.code ?? cause.errors[0].code ?? null, text: parts.join('; ') };
|
|
25
|
+
}
|
|
26
|
+
const message = cause.message || String(cause);
|
|
27
|
+
const code = cause.code ?? null;
|
|
28
|
+
const text = code && !message.includes(code) ? `${code}: ${message}` : message;
|
|
29
|
+
return { code, text };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The error a failed `fetch(url)` threw, explained. An answer the service gave is not this
|
|
33
|
+
// function's: only failures where no answer came back are rewritten; anything else is returned
|
|
34
|
+
// as it was.
|
|
35
|
+
function networkError(error, url, timeoutMs) {
|
|
36
|
+
let host;
|
|
37
|
+
try {
|
|
38
|
+
host = new URL(url).host;
|
|
39
|
+
} catch {
|
|
40
|
+
host = url;
|
|
41
|
+
}
|
|
42
|
+
if (error?.name === 'TimeoutError') {
|
|
43
|
+
const result = new Error(
|
|
44
|
+
`No answer from ${host} within ${Math.round(timeoutMs / 1000)} s: check the network`
|
|
45
|
+
);
|
|
46
|
+
result.code = 'timeout';
|
|
47
|
+
result.cause = error;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
if (!(error instanceof TypeError) || error.message !== 'fetch failed') {
|
|
51
|
+
return error;
|
|
52
|
+
}
|
|
53
|
+
const cause = describeCause(error.cause);
|
|
54
|
+
const hint = cause?.code && HINTS.find(([pattern]) => pattern.test(cause.code))?.[1];
|
|
55
|
+
const result = new Error(
|
|
56
|
+
`Cannot reach ${host}${cause ? ` (${cause.text})` : ''}${hint ? `: ${hint}` : ''}`
|
|
57
|
+
);
|
|
58
|
+
result.code = cause?.code ?? 'fetch_failed';
|
|
59
|
+
result.cause = error;
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// `fetch`, with what went wrong said when no answer came back.
|
|
64
|
+
async function fetchExplained(fetch, url, options, timeoutMs) {
|
|
65
|
+
try {
|
|
66
|
+
return await fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs) });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw networkError(error, url, timeoutMs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { networkError, fetchExplained };
|
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -1153,12 +1153,6 @@ button.preset-on {
|
|
|
1153
1153
|
margin-bottom: 1rem;
|
|
1154
1154
|
}
|
|
1155
1155
|
|
|
1156
|
-
.pairing-form label {
|
|
1157
|
-
display: grid;
|
|
1158
|
-
gap: 0.25rem;
|
|
1159
|
-
flex: 1 1 16rem;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
1156
|
.pairing-code .code {
|
|
1163
1157
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1164
1158
|
font-size: 2rem;
|
|
@@ -105,27 +105,16 @@ function PairingState({ pairing, onCancel }) {
|
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function PairingForm({
|
|
108
|
+
function PairingForm({ onStart, busy }) {
|
|
109
109
|
const { t } = useLocale();
|
|
110
|
-
const [url, setUrl] = useState(initialUrl ?? '');
|
|
111
110
|
return html`
|
|
112
111
|
<form
|
|
113
112
|
class="pairing-form"
|
|
114
113
|
onSubmit=${(event) => {
|
|
115
114
|
event.preventDefault();
|
|
116
|
-
onStart(
|
|
115
|
+
onStart();
|
|
117
116
|
}}
|
|
118
117
|
>
|
|
119
|
-
<label>
|
|
120
|
-
${t('cloud.serviceUrl')}
|
|
121
|
-
<input
|
|
122
|
-
type="url"
|
|
123
|
-
required
|
|
124
|
-
placeholder="https://"
|
|
125
|
-
value=${url}
|
|
126
|
-
onInput=${(event) => setUrl(event.currentTarget.value)}
|
|
127
|
-
/>
|
|
128
|
-
</label>
|
|
129
118
|
<button type="submit" disabled=${busy}>${t('cloud.pair')}</button>
|
|
130
119
|
</form>
|
|
131
120
|
`;
|
|
@@ -279,6 +268,17 @@ export function CloudBackup() {
|
|
|
279
268
|
onStart=${(logbookId) => act('POST', '/cloud-sync/restore', { logbookId })}
|
|
280
269
|
/>`
|
|
281
270
|
}
|
|
271
|
+
${
|
|
272
|
+
status?.configured &&
|
|
273
|
+
html`<button
|
|
274
|
+
type="button"
|
|
275
|
+
class="danger"
|
|
276
|
+
disabled=${busy}
|
|
277
|
+
onClick=${() => confirm(t('cloud.unpairConfirm')) && act('DELETE', '/cloud-sync/token')}
|
|
278
|
+
>
|
|
279
|
+
${t('cloud.unpair')}
|
|
280
|
+
</button>`
|
|
281
|
+
}
|
|
282
282
|
${
|
|
283
283
|
status?.pairing &&
|
|
284
284
|
html`<${PairingState}
|
|
@@ -293,11 +293,7 @@ export function CloudBackup() {
|
|
|
293
293
|
<details open=${!ready}>
|
|
294
294
|
<summary>${ready ? t('cloud.pairAgain') : t('cloud.pairTitle')}</summary>
|
|
295
295
|
<p>${t('cloud.pairIntro')}</p>
|
|
296
|
-
<${PairingForm}
|
|
297
|
-
initialUrl=${status.url}
|
|
298
|
-
busy=${busy}
|
|
299
|
-
onStart=${(url) => act('POST', '/cloud-sync/pairing', { url })}
|
|
300
|
-
/>
|
|
296
|
+
<${PairingForm} busy=${busy} onStart=${() => act('POST', '/cloud-sync/pairing', {})} />
|
|
301
297
|
</details>
|
|
302
298
|
`
|
|
303
299
|
}
|
package/public/js/i18n.mjs
CHANGED
|
@@ -452,11 +452,13 @@ export const MESSAGES = {
|
|
|
452
452
|
'cloud.restored': 'Logbook restored: {count} passages brought back.',
|
|
453
453
|
'cloud.restoreRefused': '{count} could not be written; see the server log.',
|
|
454
454
|
'cloud.restoreFailed': 'The restore stopped: {message}',
|
|
455
|
-
'cloud.pairAgain': 'Pair with another account
|
|
455
|
+
'cloud.pairAgain': 'Pair with another account',
|
|
456
|
+
'cloud.unpair': 'Remove the pairing',
|
|
457
|
+
'cloud.unpairConfirm':
|
|
458
|
+
'Remove the pairing? The backup stops and this plugin forgets its token. What is already on Ti Bord stays there; revoke the device from your account to stop it for good.',
|
|
456
459
|
'cloud.pairIntro':
|
|
457
|
-
'
|
|
460
|
+
'Ask for a code, then enter it on Ti Bord, signed in to your account. The backup turns on as soon as it is claimed.',
|
|
458
461
|
'cloud.learnMore': 'More about the backups, and about sharing your passages and voyages:',
|
|
459
|
-
'cloud.serviceUrl': 'Service address',
|
|
460
462
|
'cloud.pair': 'Get a pairing code',
|
|
461
463
|
'cloud.enterCode': 'Enter this code on the service:',
|
|
462
464
|
'cloud.openClaim': 'Open the service to enter it',
|
|
@@ -1037,12 +1039,14 @@ export const MESSAGES = {
|
|
|
1037
1039
|
'cloud.restored': 'Journal restauré : {count} navigations récupérées.',
|
|
1038
1040
|
'cloud.restoreRefused': '{count} n’ont pas pu être écrites ; voir le journal du serveur.',
|
|
1039
1041
|
'cloud.restoreFailed': 'La restauration s’est arrêtée : {message}',
|
|
1040
|
-
'cloud.pairAgain': 'Appairer avec un autre compte
|
|
1042
|
+
'cloud.pairAgain': 'Appairer avec un autre compte',
|
|
1043
|
+
'cloud.unpair': 'Supprimer l’appairage',
|
|
1044
|
+
'cloud.unpairConfirm':
|
|
1045
|
+
'Supprimer l’appairage ? La sauvegarde s’arrête et ce plugin oublie son jeton. Ce qui est déjà sur Ti Bord y reste ; révoquez l’appareil depuis votre compte pour l’arrêter définitivement.',
|
|
1041
1046
|
'cloud.pairIntro':
|
|
1042
|
-
'
|
|
1047
|
+
'Demandez un code, puis saisissez-le sur Ti Bord, connecté à votre compte. La sauvegarde s’active dès qu’il est utilisé.',
|
|
1043
1048
|
'cloud.learnMore':
|
|
1044
1049
|
'Plus d’informations sur les sauvegardes et sur le partage de vos navigations et de vos voyages :',
|
|
1045
|
-
'cloud.serviceUrl': 'Adresse du service',
|
|
1046
1050
|
'cloud.pair': 'Obtenir un code d’appairage',
|
|
1047
1051
|
'cloud.enterCode': 'Saisissez ce code sur le service :',
|
|
1048
1052
|
'cloud.openClaim': 'Ouvrir le service pour le saisir',
|