signalk-chiplog 2.14.0 → 2.15.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 +14 -1
- package/README.md +12 -6
- package/index.js +7 -0
- package/lib/api.js +7 -0
- package/lib/passage-summary.js +10 -2
- package/lib/summary-mailer.js +12 -1
- package/lib/track.js +9 -1
- package/package.json +1 -1
- package/public/app.css +21 -0
- package/public/js/components/PassageView.mjs +7 -0
- package/public/js/components/TideCard.mjs +21 -3
- package/public/js/components/Timeline.mjs +27 -23
- package/public/js/components/common.mjs +31 -0
- package/public/js/i18n.mjs +4 -0
- package/public/js/tide.mjs +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ All notable changes to Chiplog are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [2.15.0] - 2026-10-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Delete an instrument reading.** An administrator can delete any reading from a passage's journal (trash icon on its
|
|
14
|
+
row), including those taken at departure and arrival. Any log line, automatic ones included, can now be deleted from
|
|
15
|
+
the journal too.
|
|
16
|
+
- **Link to the passage in the summary emails.** A new **Server address for the summary link** setting; each summary
|
|
17
|
+
then ends with a link that opens the passage in the webapp. Without it, no link.
|
|
18
|
+
- **Icon buttons in the journal**: edit and delete are a pencil and a bin instead of words.
|
|
19
|
+
- **Hour scale on the tide graph**: the passage page's curve is graduated every three hours, in the logbook's time zone.
|
|
20
|
+
|
|
9
21
|
## [2.14.0] - 2026-10-25
|
|
10
22
|
|
|
11
23
|
### Added
|
|
@@ -523,7 +535,8 @@ First release.
|
|
|
523
535
|
- REST API under `/plugins/signalk-chiplog/api`, documented in [docs/API.md](docs/API.md).
|
|
524
536
|
- Single SQLite database through Node's built-in `node:sqlite`: no native module to build.
|
|
525
537
|
|
|
526
|
-
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.
|
|
538
|
+
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.15.0...HEAD
|
|
539
|
+
[2.15.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.14.0...v2.15.0
|
|
527
540
|
[2.14.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.13.0...v2.14.0
|
|
528
541
|
[2.13.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.12.0...v2.13.0
|
|
529
542
|
[2.12.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.11.0...v2.12.0
|
package/README.md
CHANGED
|
@@ -217,12 +217,13 @@ Open **Chiplog** from the Signal K webapps, or `/signalk-chiplog/`. Reading need
|
|
|
217
217
|
counter at departure and arrival and the hours run, and the tank levels and battery charge, voltage and current noted
|
|
218
218
|
at departure), and the log: every reading and event in order, including handwritten notes. A passage in progress
|
|
219
219
|
refreshes every minute. Each line's comment can be edited (read/write access); a manoeuvre or note the crew logged
|
|
220
|
-
themselves can also be **redated**
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
grey, its bearing and distance from the
|
|
220
|
+
themselves can also be **redated** — automatic lines (alarms, autopilot, weather, corrections) can only be annotated.
|
|
221
|
+
Any line can be deleted (with the instrument reading taken when it was logged), and so can a reading on its own, which
|
|
222
|
+
needs an administrator. Correcting a time reads the position back from the track at that time, and leaves it empty
|
|
223
|
+
when the track has nothing within 2 minutes of it, since the entry was not made where it happened. A time outside the
|
|
224
|
+
passage, or in the future, is refused, and the departure manoeuvre that opened a passage is moved by changing that
|
|
225
|
+
passage's departure time in **Corrections** below. Under each position, in grey, its bearing and distance from the
|
|
226
|
+
nearest landmark.
|
|
226
227
|
- **Corrections** (read/write access):
|
|
227
228
|
- rename the departure, or the arrival once the passage is closed — a passage in progress has none yet to rename;
|
|
228
229
|
- switch an engine period to sail or back;
|
|
@@ -482,6 +483,7 @@ In the Signal K admin, **Apps & Plugins → Configuration → Chiplog**.
|
|
|
482
483
|
| SMTP port | 587 | 587 for submission with STARTTLS, 465 for implicit TLS, 25 for a local relay. |
|
|
483
484
|
| SMTP security | STARTTLS | STARTTLS, TLS, or none for a relay on the boat itself. |
|
|
484
485
|
| SMTP user name / password | — | Leave empty if the relay needs no authentication. |
|
|
486
|
+
| Server address for the summary link | — | The address the server is reached at by whoever reads the summary, e.g. `https://boat.example.org`. Each summary then links to its passage in the webapp. Empty leaves the link out. |
|
|
485
487
|
| 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. |
|
|
486
488
|
| Logbook language (PDF and summary emails) | en | Language of the PDF logbooks on the USB drive and of the summary emails (English or French). |
|
|
487
489
|
| 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`. |
|
|
@@ -595,6 +597,10 @@ summary of every passage once it is over for good.
|
|
|
595
597
|
distance, the duration, the time under way, the time under engine and under sail, the average and maximum speed, and
|
|
596
598
|
the maximum wind — the same figures and wording as the passage page, in the **logbook language** and **ship's time
|
|
597
599
|
zone** set below.
|
|
600
|
+
- **A link to the passage.** Set **Server address for the summary link** to the address the server is reached at
|
|
601
|
+
(`https://boat.example.org`, or a LAN address if the readers are aboard) and each summary ends with a link that opens
|
|
602
|
+
the passage in the webapp. Without it the summary carries no link, as the plugin cannot know its own address from
|
|
603
|
+
outside.
|
|
598
604
|
- **The map** is drawn on the server and travels inside the message, so it shows up in the mail rather than as an
|
|
599
605
|
attachment to open. Tiles come from OpenStreetMap by default; set your own tile server, or clear the setting to get
|
|
600
606
|
the track drawn on a plain background and send nothing to a tile server.
|
package/index.js
CHANGED
|
@@ -261,6 +261,12 @@ module.exports = function (app) {
|
|
|
261
261
|
title: 'SMTP password',
|
|
262
262
|
format: 'password'
|
|
263
263
|
},
|
|
264
|
+
summaryMailServerUrl: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
title: 'Server address for the summary link',
|
|
267
|
+
description:
|
|
268
|
+
'The address the server is reached at from the recipients’ side, e.g. https://boat.example.org or http://192.168.1.10:3000. Each summary then links to its passage in the logbook; empty leaves the link out'
|
|
269
|
+
},
|
|
264
270
|
summaryMailTileUrl: {
|
|
265
271
|
type: 'string',
|
|
266
272
|
title: 'Map tiles for the summary',
|
|
@@ -673,6 +679,7 @@ module.exports = function (app) {
|
|
|
673
679
|
: SUMMARY_MAIL_DEFAULTS.smtpSecurity,
|
|
674
680
|
smtpUsername: config.smtpUsername || null,
|
|
675
681
|
smtpPassword: config.smtpPassword || null,
|
|
682
|
+
summaryMailServerUrl: config.summaryMailServerUrl || null,
|
|
676
683
|
summaryMailTileUrl:
|
|
677
684
|
config.summaryMailTileUrl === undefined
|
|
678
685
|
? SUMMARY_MAIL_DEFAULTS.summaryMailTileUrl
|
package/lib/api.js
CHANGED
|
@@ -369,6 +369,13 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
369
369
|
handle(({ db }, req) => track.listObservations(db, entryId(req), v.parsePagination(req.query)))
|
|
370
370
|
);
|
|
371
371
|
|
|
372
|
+
admin.delete(
|
|
373
|
+
'/api/observations/:id',
|
|
374
|
+
handle(({ db }, req) => {
|
|
375
|
+
track.deleteObservation(db, v.parseId(req.params.id));
|
|
376
|
+
})
|
|
377
|
+
);
|
|
378
|
+
|
|
372
379
|
readonly.get(
|
|
373
380
|
'/api/entries/:id/propulsion',
|
|
374
381
|
handle(({ db }, req) => propulsion.listSegments(db, entryId(req), v.parsePagination(req.query)))
|
package/lib/passage-summary.js
CHANGED
|
@@ -20,6 +20,7 @@ const STYLE = {
|
|
|
20
20
|
table: 'width:100%;border-collapse:collapse;margin-top:16px;font-size:14px;',
|
|
21
21
|
label: 'padding:6px 12px 6px 0;color:#52606d;white-space:nowrap;',
|
|
22
22
|
value: 'padding:6px 0;text-align:right;font-weight:bold;color:#102a43;',
|
|
23
|
+
link: 'margin:16px 0 0;font-size:14px;',
|
|
23
24
|
footer: 'margin:18px 0 0;font-size:12px;color:#7b8794;'
|
|
24
25
|
};
|
|
25
26
|
|
|
@@ -103,10 +104,11 @@ function htmlRow(label, value) {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
// `mapAvailable` says whether an image part accompanies the message; without
|
|
106
|
-
// one the <img> would show as a broken attachment.
|
|
107
|
+
// one the <img> would show as a broken attachment. `passageUrl`, when given, is
|
|
108
|
+
// where the passage can be opened in the webapp.
|
|
107
109
|
async function renderPassageSummary(
|
|
108
110
|
{ entry, trackPoints = [] },
|
|
109
|
-
{ language = 'en', timeZone, vesselName = null, mapAvailable = false } = {}
|
|
111
|
+
{ language = 'en', timeZone, vesselName = null, mapAvailable = false, passageUrl = null } = {}
|
|
110
112
|
) {
|
|
111
113
|
const { i18n, format: formatModule } = await loadShared();
|
|
112
114
|
const t = i18n.createTranslator(language);
|
|
@@ -139,6 +141,7 @@ async function renderPassageSummary(
|
|
|
139
141
|
'',
|
|
140
142
|
...facts.map(([label, value]) => `${label}: ${value}`),
|
|
141
143
|
...(trackPoints.length === 0 ? ['', t('passage.noTrack')] : []),
|
|
144
|
+
...(passageUrl ? ['', `${t('mail.link')}: ${passageUrl}`] : []),
|
|
142
145
|
'',
|
|
143
146
|
t('mail.footer')
|
|
144
147
|
].join('\n');
|
|
@@ -158,6 +161,11 @@ ${
|
|
|
158
161
|
${lines.map(({ label, place, when: at }) => htmlRow(label, at ? `${place} — ${at}` : place)).join('\n')}
|
|
159
162
|
${facts.map(([label, value]) => htmlRow(label, value)).join('\n')}
|
|
160
163
|
</table>
|
|
164
|
+
${
|
|
165
|
+
passageUrl
|
|
166
|
+
? `<p style="${STYLE.link}"><a href="${escapeHtml(passageUrl)}">${escapeHtml(t('mail.link'))}</a></p>`
|
|
167
|
+
: ''
|
|
168
|
+
}
|
|
161
169
|
<p style="${STYLE.footer}">${escapeHtml(t('mail.footer'))}</p>
|
|
162
170
|
</div>
|
|
163
171
|
</body></html>
|
package/lib/summary-mailer.js
CHANGED
|
@@ -79,6 +79,16 @@ function writeOffStale(db, now) {
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Where a passage can be opened in the webapp, from the address the crew reaches
|
|
83
|
+
// the server at; nothing when none is configured -- the plugin cannot know its
|
|
84
|
+
// own address from the outside.
|
|
85
|
+
function passageUrl(baseUrl, entryId) {
|
|
86
|
+
const base = String(baseUrl ?? '')
|
|
87
|
+
.trim()
|
|
88
|
+
.replace(/\/+$/, '');
|
|
89
|
+
return /^https?:\/\/\S+$/i.test(base) ? `${base}/signalk-chiplog/#/passages/${entryId}` : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
function configurationProblem(settings) {
|
|
83
93
|
if (!settings.smtpHost) {
|
|
84
94
|
return 'no SMTP server is configured';
|
|
@@ -135,7 +145,8 @@ function createSummaryMailer({
|
|
|
135
145
|
language: settings.logbookLanguage,
|
|
136
146
|
timeZone: settings.logbookTimeZone ?? undefined,
|
|
137
147
|
vesselName: vessel,
|
|
138
|
-
mapAvailable: map !== null
|
|
148
|
+
mapAvailable: map !== null,
|
|
149
|
+
passageUrl: passageUrl(settings.summaryMailServerUrl, entryId)
|
|
139
150
|
}
|
|
140
151
|
);
|
|
141
152
|
|
package/lib/track.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { requireEntryRow } = require('./entries');
|
|
2
|
+
const { notFound } = require('./errors');
|
|
2
3
|
const { toPosition, paginate } = require('./rows');
|
|
3
4
|
|
|
4
5
|
function toTrackPoint(row) {
|
|
@@ -68,4 +69,11 @@ function listObservations(db, entryId, { limit, offset }) {
|
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
function deleteObservation(db, id) {
|
|
73
|
+
const { changes } = db.prepare('DELETE FROM observations WHERE id = ?').run(id);
|
|
74
|
+
if (changes === 0) {
|
|
75
|
+
throw notFound('observation', id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { allTrackPoints, allObservations, listObservations, deleteObservation };
|
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -78,6 +78,16 @@ button.link-button {
|
|
|
78
78
|
padding: 0.2rem 0.4rem;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
button.icon-button {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
padding: 0.25rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
button.icon-button.danger {
|
|
88
|
+
border: none;
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
button.danger {
|
|
82
92
|
background: none;
|
|
83
93
|
color: var(--danger);
|
|
@@ -735,6 +745,17 @@ main {
|
|
|
735
745
|
stroke-width: 1;
|
|
736
746
|
}
|
|
737
747
|
|
|
748
|
+
.tide-tick {
|
|
749
|
+
stroke: var(--border);
|
|
750
|
+
stroke-width: 1;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.tide-hour {
|
|
754
|
+
fill: var(--muted);
|
|
755
|
+
font-size: 11px;
|
|
756
|
+
text-anchor: middle;
|
|
757
|
+
}
|
|
758
|
+
|
|
738
759
|
.tide-area {
|
|
739
760
|
fill: var(--accent);
|
|
740
761
|
opacity: 0.12;
|
|
@@ -373,6 +373,12 @@ export function PassageView({ id }) {
|
|
|
373
373
|
await request('DELETE', `/events/${event.id}`);
|
|
374
374
|
reload();
|
|
375
375
|
});
|
|
376
|
+
const deleteReading = (reading) =>
|
|
377
|
+
confirm(t('timeline.deleteReadingConfirm')) &&
|
|
378
|
+
act(async () => {
|
|
379
|
+
await request('DELETE', `/observations/${reading.id}`);
|
|
380
|
+
reload();
|
|
381
|
+
});
|
|
376
382
|
|
|
377
383
|
const sameDay =
|
|
378
384
|
entry.endTime && dayKey(new Date(entry.startTime)) === dayKey(new Date(entry.endTime));
|
|
@@ -539,6 +545,7 @@ export function PassageView({ id }) {
|
|
|
539
545
|
busy=${busy}
|
|
540
546
|
onEditEvent=${editEvent}
|
|
541
547
|
onDelete=${deleteEvent}
|
|
548
|
+
onDeleteReading=${deleteReading}
|
|
542
549
|
/>
|
|
543
550
|
</section>
|
|
544
551
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { html } from '../../vendor/preact-htm.mjs';
|
|
2
2
|
import { useLocale } from '../context.mjs';
|
|
3
|
-
import { smoothPath, tideExtremes } from '../tide.mjs';
|
|
3
|
+
import { hourTicks, smoothPath, tideExtremes } from '../tide.mjs';
|
|
4
4
|
|
|
5
5
|
const CHART_WIDTH = 600;
|
|
6
|
-
const CHART_HEIGHT =
|
|
7
|
-
|
|
6
|
+
const CHART_HEIGHT = 180;
|
|
7
|
+
// The bottom margin holds the hour scale.
|
|
8
|
+
const PADDING = { top: 16, right: 12, bottom: 32, left: 12 };
|
|
9
|
+
const TICK_LENGTH = 4;
|
|
8
10
|
|
|
9
11
|
// Pixel coordinates for the curve, an area fill down to the baseline, and the
|
|
10
12
|
// scales used to place the high/low markers at the same spots.
|
|
@@ -37,6 +39,7 @@ export function TideCard({ tide, placeName }) {
|
|
|
37
39
|
}
|
|
38
40
|
const chart = layout(points);
|
|
39
41
|
const extremes = tideExtremes(points);
|
|
42
|
+
const ticks = hourTicks(points, format.time);
|
|
40
43
|
|
|
41
44
|
return html`
|
|
42
45
|
<section class="card tide-card">
|
|
@@ -57,6 +60,21 @@ export function TideCard({ tide, placeName }) {
|
|
|
57
60
|
y2=${chart.baseline}
|
|
58
61
|
class="tide-baseline"
|
|
59
62
|
/>
|
|
63
|
+
${ticks.map(
|
|
64
|
+
(tick) =>
|
|
65
|
+
html`<g key=${tick.time}>
|
|
66
|
+
<line
|
|
67
|
+
x1=${chart.x(tick.time)}
|
|
68
|
+
y1=${chart.baseline}
|
|
69
|
+
x2=${chart.x(tick.time)}
|
|
70
|
+
y2=${chart.baseline + TICK_LENGTH}
|
|
71
|
+
class="tide-tick"
|
|
72
|
+
/>
|
|
73
|
+
<text x=${chart.x(tick.time)} y=${chart.baseline + TICK_LENGTH + 13} class="tide-hour">
|
|
74
|
+
${tick.label}
|
|
75
|
+
</text>
|
|
76
|
+
</g>`
|
|
77
|
+
)}
|
|
60
78
|
<path d=${chart.areaPath} class="tide-area" />
|
|
61
79
|
<path d=${chart.linePath} class="tide-line" />
|
|
62
80
|
${extremes.map(
|
|
@@ -2,6 +2,7 @@ import { html, useEffect, useRef, useState } from '../../vendor/preact-htm.mjs';
|
|
|
2
2
|
import { useLocale } from '../context.mjs';
|
|
3
3
|
import { dayKey } from '../days.mjs';
|
|
4
4
|
import { landmarkLine } from '../landmarks.mjs';
|
|
5
|
+
import { IconButton } from './common.mjs';
|
|
5
6
|
import { EDITABLE_EVENT_TYPES, buildRows, describeEvent } from '../log-lines.mjs';
|
|
6
7
|
|
|
7
8
|
// A dot — the stroke of an i or a full stop — has a single point, which a
|
|
@@ -68,10 +69,9 @@ export function EventRemark({ event, manoeuvreLabels }) {
|
|
|
68
69
|
// the comment on anything the plugin logged itself (SPEC: "an edited comment");
|
|
69
70
|
// what the crew logged (`EDITABLE_EVENT_TYPES`) can also be redated, which makes
|
|
70
71
|
// the server re-read its position from the track. Deleting is offered for what
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// corrections) can only be annotated.
|
|
72
|
+
// every line — an alarm (`sk_alarm`) takes its paired line, the raise or the
|
|
73
|
+
// clearing, with it (lib/events.js `pairedAlarmEvent`). The server decides who
|
|
74
|
+
// may: an administrator for a reading, whoever has write access for a line.
|
|
75
75
|
function EventLine({ event, bounds, manoeuvreLabels, busy, onEditEvent, onDelete }) {
|
|
76
76
|
const { t, format } = useLocale();
|
|
77
77
|
const [editing, setEditing] = useState(false);
|
|
@@ -143,25 +143,19 @@ function EventLine({ event, bounds, manoeuvreLabels, busy, onEditEvent, onDelete
|
|
|
143
143
|
return html`
|
|
144
144
|
<${EventRemark} event=${event} manoeuvreLabels=${manoeuvreLabels} />
|
|
145
145
|
<div class="timeline-actions">
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
<${IconButton}
|
|
147
|
+
icon="edit"
|
|
148
|
+
label=${t('common.edit')}
|
|
149
149
|
disabled=${busy}
|
|
150
150
|
onClick=${() => setEditing(true)}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
disabled=${busy}
|
|
160
|
-
onClick=${() => onDelete(event)}
|
|
161
|
-
>
|
|
162
|
-
${t('common.delete')}
|
|
163
|
-
</button>`
|
|
164
|
-
}
|
|
151
|
+
/>
|
|
152
|
+
<${IconButton}
|
|
153
|
+
icon="delete"
|
|
154
|
+
label=${t('common.delete')}
|
|
155
|
+
danger
|
|
156
|
+
disabled=${busy}
|
|
157
|
+
onClick=${() => onDelete(event)}
|
|
158
|
+
/>
|
|
165
159
|
</div>
|
|
166
160
|
`;
|
|
167
161
|
}
|
|
@@ -174,7 +168,8 @@ export function Timeline({
|
|
|
174
168
|
manoeuvreLabels,
|
|
175
169
|
busy,
|
|
176
170
|
onEditEvent,
|
|
177
|
-
onDelete
|
|
171
|
+
onDelete,
|
|
172
|
+
onDeleteReading
|
|
178
173
|
}) {
|
|
179
174
|
const { t, format } = useLocale();
|
|
180
175
|
const rows = buildRows(events, observations);
|
|
@@ -260,7 +255,16 @@ export function Timeline({
|
|
|
260
255
|
onEditEvent=${onEditEvent}
|
|
261
256
|
onDelete=${onDelete}
|
|
262
257
|
/>`
|
|
263
|
-
: html`<span class="muted">${t(`observation.${readings.reason}`)}</span
|
|
258
|
+
: html`<span class="muted">${t(`observation.${readings.reason}`)}</span>
|
|
259
|
+
<div class="timeline-actions">
|
|
260
|
+
<${IconButton}
|
|
261
|
+
icon="delete"
|
|
262
|
+
label=${t('common.delete')}
|
|
263
|
+
danger
|
|
264
|
+
disabled=${busy}
|
|
265
|
+
onClick=${() => onDeleteReading(readings)}
|
|
266
|
+
/>
|
|
267
|
+
</div>`
|
|
264
268
|
}
|
|
265
269
|
</td>
|
|
266
270
|
</tr>
|
|
@@ -9,6 +9,37 @@ export function SignInLink({ label }) {
|
|
|
9
9
|
return html`<a href="/admin/#/login" target="_blank" rel="noopener">${label}</a>`;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const ICON_PATHS = {
|
|
13
|
+
edit: 'M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z',
|
|
14
|
+
delete: 'M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14M10 10v6M14 10v6'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// A button drawn as an icon, named for screen readers and as a tooltip.
|
|
18
|
+
export function IconButton({ icon, label, danger, disabled, onClick }) {
|
|
19
|
+
return html`<button
|
|
20
|
+
type="button"
|
|
21
|
+
class=${`link-button icon-button${danger ? ' danger' : ''}`}
|
|
22
|
+
disabled=${disabled}
|
|
23
|
+
title=${label}
|
|
24
|
+
aria-label=${label}
|
|
25
|
+
onClick=${onClick}
|
|
26
|
+
>
|
|
27
|
+
<svg
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
width="16"
|
|
30
|
+
height="16"
|
|
31
|
+
fill="none"
|
|
32
|
+
stroke="currentColor"
|
|
33
|
+
stroke-width="2"
|
|
34
|
+
stroke-linecap="round"
|
|
35
|
+
stroke-linejoin="round"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
>
|
|
38
|
+
<path d=${ICON_PATHS[icon]} />
|
|
39
|
+
</svg>
|
|
40
|
+
</button>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
12
43
|
// Failed actions waiting for the link to come back, so the toast can say, once
|
|
13
44
|
// it does, that there is something to do again.
|
|
14
45
|
const unfinished = new Set();
|
package/public/js/i18n.mjs
CHANGED
|
@@ -219,6 +219,7 @@ export const MESSAGES = {
|
|
|
219
219
|
'mail.subjectVessel': '{vessel} — {route}',
|
|
220
220
|
'mail.when': '{date} ({zone})',
|
|
221
221
|
'mail.mapAlt': 'The track of the passage on the map',
|
|
222
|
+
'mail.link': 'Open the passage in the logbook',
|
|
222
223
|
'mail.footer': 'Sent automatically by Chiplog, this vessel’s logbook.',
|
|
223
224
|
|
|
224
225
|
'map.seamarks': 'Seamarks',
|
|
@@ -305,6 +306,7 @@ export const MESSAGES = {
|
|
|
305
306
|
'timeline.timeFromTrack': 'Changing the time reads the position back from the track.',
|
|
306
307
|
'timeline.deleteConfirm': 'Delete this log line? This cannot be undone.',
|
|
307
308
|
'timeline.deleteAlarmConfirm': 'Delete this alarm and its resolution? This cannot be undone.',
|
|
309
|
+
'timeline.deleteReadingConfirm': 'Delete this instrument reading? This cannot be undone.',
|
|
308
310
|
|
|
309
311
|
// Where the position is from the landmark, the way a logbook gives it.
|
|
310
312
|
'landmark.bearing': '{distance} {cardinal} ({bearing}) — {name}',
|
|
@@ -801,6 +803,7 @@ export const MESSAGES = {
|
|
|
801
803
|
'mail.subjectVessel': '{vessel} — {route}',
|
|
802
804
|
'mail.when': 'Le {date} ({zone})',
|
|
803
805
|
'mail.mapAlt': 'La trace de la navigation sur la carte',
|
|
806
|
+
'mail.link': 'Ouvrir la navigation dans le journal',
|
|
804
807
|
'mail.footer': 'Envoyé automatiquement par Chiplog, le journal de bord de ce navire.',
|
|
805
808
|
|
|
806
809
|
'map.seamarks': 'Balisage',
|
|
@@ -888,6 +891,7 @@ export const MESSAGES = {
|
|
|
888
891
|
'timeline.timeFromTrack': 'Changer l’heure relit la position dans la trace.',
|
|
889
892
|
'timeline.deleteConfirm': 'Supprimer cette ligne du journal ? C’est irréversible.',
|
|
890
893
|
'timeline.deleteAlarmConfirm': 'Supprimer cette alarme et sa résolution ? C’est irréversible.',
|
|
894
|
+
'timeline.deleteReadingConfirm': 'Supprimer ce relevé des instruments ? C’est irréversible.',
|
|
891
895
|
|
|
892
896
|
// Le relèvement inverse : où se trouve le bateau par rapport à l’amer.
|
|
893
897
|
// Sans préposition, qui varierait avec l’aire de vent (au NE, à l’E).
|
package/public/js/tide.mjs
CHANGED
|
@@ -17,6 +17,17 @@ export function tideExtremes(points) {
|
|
|
17
17
|
return extremes;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// The hour scale under the curve: the samples whose local hour is a multiple of
|
|
21
|
+
// `step`, labelled by `label` (the formatter's `time`, so the zone and the
|
|
22
|
+
// 24-hour clock are the reader's). Samples are used as they are rather than
|
|
23
|
+
// recomputed from the clock, so a zone that is not on a whole hour still lines
|
|
24
|
+
// up with the curve.
|
|
25
|
+
export function hourTicks(points, label, step = 3) {
|
|
26
|
+
return points
|
|
27
|
+
.map((point) => ({ time: point.time, label: label(point.time) }))
|
|
28
|
+
.filter((tick) => Number(tick.label.slice(0, 2)) % step === 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
// A smoothed SVG path through the points, quadratic Bezier segments via
|
|
21
32
|
// midpoints -- simple, and hourly samples are dense enough that it reads as
|
|
22
33
|
// a real tide curve rather than a jagged line.
|