signalk-chiplog 1.1.0 → 2.0.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 +29 -1
- package/README.md +63 -23
- package/index.js +71 -1
- package/lib/api.js +38 -6
- package/lib/database.js +85 -0
- package/lib/detection.js +11 -2
- package/lib/entries.js +44 -0
- package/lib/event-watcher.js +1 -1
- package/lib/events.js +11 -3
- package/lib/formats.js +13 -1
- package/lib/influx-history.js +480 -0
- package/lib/logbook-pdf.js +14 -1
- package/lib/observation-recorder.js +126 -3
- package/lib/pdf/writer.js +17 -9
- package/lib/replay-job.js +174 -0
- package/lib/replay.js +231 -0
- package/lib/track-recorder.js +4 -2
- package/lib/track.js +1 -0
- package/package.json +4 -1
- package/public/app.css +49 -7
- package/public/entry/entry.css +90 -0
- package/public/entry/js/components/Icons.mjs +89 -0
- package/public/entry/js/components/SketchPanel.mjs +184 -45
- package/public/entry/js/main.mjs +5 -3
- package/public/entry/js/strokes.mjs +85 -5
- package/public/js/components/PassageView.mjs +96 -20
- package/public/js/components/ReplayView.mjs +194 -0
- package/public/js/components/Timeline.mjs +11 -7
- package/public/js/components/TrackMap.mjs +46 -2
- package/public/js/components/TrackScrubber.mjs +64 -0
- package/public/js/context.mjs +4 -1
- package/public/js/format.mjs +9 -0
- package/public/js/i18n.mjs +107 -2
- package/public/js/log-lines.mjs +29 -0
- package/public/js/main.mjs +9 -2
- package/public/js/track.mjs +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to Chiplog are documented here. The format follows [Keep a C
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.0.0] - 2026-09-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- The passage page has a **Boat status** card: each engine's hour counter, at departure and arrival, and every tank's level (and volume) and every battery's charge, voltage and current as noted at departure. Tanks and batteries are noted once on the passage as it opens (`startTanks`, `startBatteries`), whether detection or the crew opens it, and included in the JSON export.
|
|
12
|
+
- Merging two entries now keeps a record of the stop between them: a `stopover` line on the surviving passage's timeline, naming the place and its position — previously that information was silently lost once the merge took the later entry's arrival as its own.
|
|
13
|
+
- The passage page's track map now shows a small boat marker at the selected point, pointing along its heading, and a scrubber under the map to step back and forth through the track's history — it defaults to the latest point, doubling as the current position on a passage in progress. A band below it shows that point's time, SOG, COG, STW, TWS, TWD, TWA and AWA. Speed through water now rides along with every track point like wind and heading already did, not just the hourly instrument snapshot.
|
|
14
|
+
- Retrospective analysis: a new **Retrospective** page reconstructs past passages for a date range from a [signalk-to-influxdb](https://github.com/tkurki/signalk-to-influxdb) history (InfluxDB 1.x, local or remote — a new recommended companion plugin), through the exact same detection pipeline used live rather than a separate implementation. Runs in the background with a progress bar, refuses a range that overlaps a passage already on record, refuses to run at all while a passage is under way (it would be driving that same live passage through the replay's detector at the same time), and can be cancelled mid-way without losing what it already reconstructed. Data is matched to this server's own vessel identity by default, overridable (`influxSelfContext`) for running it from a different Signal K server than the one that wrote the history; a mismatch fails with the vessel contexts actually found, rather than reconstructing nothing with no explanation. An InfluxDB that never answers — unreachable, or overloaded — fails after 30 seconds with the actual connection problem, rather than hanging indefinitely on a generic "fetch failed". A quick first pass reads one mean speed per minute to find when the boat moved, and only those stretches are then fetched — one value per track interval — and replayed, committing once per ten simulated minutes, and the page sums up what a run added (passages, distance, engine and sail time, track points, events): a month now takes seconds rather than the best part of an hour, and no longer holds every raw reading of the range in memory. Requests stay bounded (a week for the scan, six hours for a stretch) with a short pause between them, so a multi-week reconstruction cannot overwhelm a database sharing a resource-constrained host (a Raspberry Pi) with Signal K itself; cancelling works at any stage. A replay wakes the place-naming lookup immediately once it finishes, rather than leaving newly-reconstructed departures and arrivals waiting out whatever backoff that chain was already in. Signal K alarms are not reconstructed, since a typical InfluxDB history does not archive notifications the way it does a numeric reading, nor are weather events while the boat lay still between passages, nor the extra track points live recording adds on turns and speed changes.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- The arrival instrument snapshot (`entry_end`) is now dated from the moment the passage actually ended, not from the later tick that found out about it once the stop had held past the closure threshold (up to `stopClosureMinutes`) — it could otherwise sort after an hourly reading taken during that wait, even though the passage had already ended before that reading was taken.
|
|
19
|
+
|
|
20
|
+
## [1.2.0] - 2026-09-15
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- The tablet app's handwriting pad now fills the whole screen and has a toolbar: fine pen, thick pen, highlighter, eraser, undo and a choice of colour (kept to the theme's colour in night mode). The eraser removes only the points it touches, splitting a stroke instead of deleting all of it; undo now steps back through erasing too, not just strokes. A stroke's colour and tool travel with it to the webapp's timeline and the PDF export, not just the tablet.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- The tablet app's stylus canvas now prevents the default action on every contact, not just the pen's — a resting palm's touch was left to the browser, which could hijack it as a gesture and cancel the pen's in-progress stroke, or show a native text-selection highlight over the canvas. iOS Safari's long-press selection callout on the canvas needed the whole entry app, not just the canvas, to opt out of selection to reliably stay away, plus blocking `selectstart`/`contextmenu`/`dragstart` directly since the CSS alone is unreliable on some iOS versions.
|
|
29
|
+
- Quickly lifting and reapplying the pen could have its next stroke silently dropped: the previous contact's pointerup can arrive after the next one's pointerdown, which read as "still drawing" and refused to start the new stroke.
|
|
30
|
+
- Worked around an iPadOS Safari/Scribble bug that could swallow a pen's pointer events mid-stroke, dropping strokes or having them mistakenly typed into the comment field, by also preventing the canvas's underlying touch events directly, not just the pointer ones.
|
|
31
|
+
- An autopilot engagement, disengagement or mode change now takes an instrument snapshot like every other automatic event, instead of logging the change with no conditions attached.
|
|
32
|
+
|
|
7
33
|
## [1.1.0] - 2026-09-15
|
|
8
34
|
|
|
9
35
|
### Added
|
|
@@ -76,6 +102,8 @@ First release.
|
|
|
76
102
|
- REST API under `/plugins/signalk-chiplog/api`, documented in [docs/API.md](docs/API.md).
|
|
77
103
|
- Single SQLite database through Node's built-in `node:sqlite`: no native module to build.
|
|
78
104
|
|
|
79
|
-
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/
|
|
105
|
+
[Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.0.0...HEAD
|
|
106
|
+
[2.0.0]: https://github.com/ricard33/signalk-chiplog/compare/v1.2.0...v2.0.0
|
|
107
|
+
[1.2.0]: https://github.com/ricard33/signalk-chiplog/compare/v1.1.0...v1.2.0
|
|
80
108
|
[1.1.0]: https://github.com/ricard33/signalk-chiplog/compare/v1.0.0...v1.1.0
|
|
81
109
|
[1.0.0]: https://github.com/ricard33/signalk-chiplog/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ English and French, chosen from the browser's language.
|
|
|
23
23
|
- [Configuration](#configuration)
|
|
24
24
|
- [Signal K data used](#signal-k-data-used)
|
|
25
25
|
- [Backups and abandon ship](#backups-and-abandon-ship)
|
|
26
|
+
- [Retrospective analysis](#retrospective-analysis)
|
|
26
27
|
- [Privacy and online services](#privacy-and-online-services)
|
|
27
28
|
- [Troubleshooting](#troubleshooting)
|
|
28
29
|
- [Limitations](#limitations)
|
|
@@ -68,7 +69,7 @@ A **passage** is one logbook entry: from leaving a berth or anchorage to arrivin
|
|
|
68
69
|
- **Power cuts and restarts.** If the server comes back after the boat has been still for longer than the tolerance, the passage is closed at its last movement. A short restart carries on with the same passage.
|
|
69
70
|
- **Under way or stopped** comes from `navigation.state` when signalk-autostate provides it. Otherwise Chiplog averages speed over ground over 3 minutes: under way above 1 knot, stopped below half a knot. This keeps a boat swinging at anchor from starting passages.
|
|
70
71
|
|
|
71
|
-
A passage that was split in two — a stop just longer than the tolerance, for instance — can be merged back from the logbook webapp.
|
|
72
|
+
A passage that was split in two — a stop just longer than the tolerance, for instance — can be merged back from the logbook webapp. The stop the merge folds away is kept on the timeline as its own line, naming the place, since it would otherwise leave no trace once the merge takes the later passage's arrival as its own.
|
|
72
73
|
|
|
73
74
|
### Track and distance
|
|
74
75
|
|
|
@@ -120,7 +121,7 @@ Open **Chiplog** from the Signal K webapps, or `/signalk-chiplog/`. Reading need
|
|
|
120
121
|
|
|
121
122
|
- **Status bar** — under way under sail or engine, stopped, or waiting for data, with a link to the passage in progress. A warning shows when detection works from speed alone because signalk-autostate is missing.
|
|
122
123
|
- **Logbook** — a summary above the list (number of passages, total distance, total time, across every passage logged, not just what is loaded), then passages grouped by day, newest first, with times, departure and arrival, distance, duration and an engine/sail bar. A passage across midnight appears on both days. Provisional place names are shown as such.
|
|
123
|
-
- **Passage page** — summary (distance, duration, average speed, and the highest speed and wind seen), map of the track (OpenStreetMap with OpenSeaMap seamarks, which can be hidden), the tide forecast near the departure (place, high/low times and heights, height curve) when one was fetched, the engine and sail periods
|
|
124
|
+
- **Passage page** — summary (distance, duration, average speed, and the highest speed and wind seen), map of the track (OpenStreetMap with OpenSeaMap seamarks, which can be hidden) with a small boat marker at the selected point, a scrubber under the map to step back and forth through its history (defaulting to the latest point, so it shows the current position on a passage in progress) with a band of that point's time, SOG, COG, STW, TWS, TWD, TWA and AWA, the tide forecast near the departure (place, high/low times and heights, height curve) when one was fetched, the engine and sail periods, the boat's status (each engine's hour counter at departure and arrival and the hours run, and the tank levels and battery charge, voltage and current noted at departure), and the log: every reading and event in order, including handwritten notes. A passage in progress refreshes every minute. Each line's comment can be edited (read/write access); a manoeuvre or note the crew logged themselves can also be deleted — automatic lines (alarms, autopilot, weather, corrections) can only be annotated.
|
|
124
125
|
- **Corrections** (read/write access):
|
|
125
126
|
- rename the departure, or the arrival once the passage is closed — a passage in progress has none yet to rename;
|
|
126
127
|
- switch an engine period to sail or back;
|
|
@@ -128,6 +129,7 @@ Open **Chiplog** from the Signal K webapps, or `/signalk-chiplog/`. Reading need
|
|
|
128
129
|
- merge with the previous or next passage;
|
|
129
130
|
- delete a passage (admin).
|
|
130
131
|
- **Export** — download the whole logbook or a date range as a PDF logbook to print, JSON, CSV or GPX, and write the abandon-ship copy to the USB drive now (admin). The PDF is written in the webapp's language and the device's time zone.
|
|
132
|
+
- **Retrospective** (admin) — reconstruct past passages for a date range from an InfluxDB history (see [Retrospective analysis](#retrospective-analysis)).
|
|
131
133
|
|
|
132
134
|
**Helm entry** in the top bar opens the tablet entry app.
|
|
133
135
|
|
|
@@ -153,9 +155,9 @@ Other entries made with no passage open go to the last passage if the boat is st
|
|
|
153
155
|
### Notes and handwriting
|
|
154
156
|
|
|
155
157
|
- **Note** — type and tap **Log it**.
|
|
156
|
-
- **Handwriting** —
|
|
158
|
+
- **Handwriting** — takes over the whole screen, with a toolbar above the pad: fine pen, thick pen, highlighter, eraser, undo, and a choice of colour (kept to the theme's colour in night mode, to spare night vision). Pen pressure also sets the line width. The eraser removes only what it actually touches, splitting a stroke rather than deleting all of it; undo steps back through strokes and erasing alike. Once a stylus has touched the pad, fingers are ignored, so a palm resting on the screen does not draw. Add a comment and tap **Log it** to send.
|
|
157
159
|
|
|
158
|
-
Handwritten notes appear as drawn in the logbook's timeline.
|
|
160
|
+
Handwritten notes appear as drawn — colour, pen or highlighter included — in the logbook's timeline and in the PDF export, not just on the tablet.
|
|
159
161
|
|
|
160
162
|
### Latest entries
|
|
161
163
|
|
|
@@ -187,25 +189,31 @@ To revoke it, delete the device under **Security → Devices**: the tablet asks
|
|
|
187
189
|
|
|
188
190
|
In the Signal K admin, **Apps & Plugins → Configuration → Chiplog**.
|
|
189
191
|
|
|
190
|
-
| Setting | Default | What it does
|
|
191
|
-
| -------------------------------------------------- | --------------------------------------------- |
|
|
192
|
-
| Stop duration that ends a passage | 30 min | Shorter stops stay within the same passage.
|
|
193
|
-
| Under-way speed without navigation.state | 1 kn | Used only without signalk-autostate: under way above it, stopped below half of it.
|
|
194
|
-
| Propulsion assumed without engine data | sail | When nothing says whether the engine is running. Set to engine on a motorboat.
|
|
195
|
-
| Instrument snapshot interval | 60 min | Readings on this clock boundary during a passage.
|
|
196
|
-
| Track point interval | 15 s | A track point at least this often while moving.
|
|
197
|
-
| Place matching radius | 200 m | A departure or arrival this close to a known place takes its name.
|
|
198
|
-
| Name departures and arrivals with online geocoding | on | Turn off to never send positions online; places are then named after their coordinates until corrected.
|
|
199
|
-
| Geocoding service | `https://nominatim.openstreetmap.org` | Any Nominatim-compatible service, e.g. a self-hosted one.
|
|
200
|
-
| Fetch the tide forecast at departure | on | Turn off to never send the departure position online; the passage page then shows no tide.
|
|
201
|
-
| Tide service | `https://marine-api.open-meteo.com/v1/marine` | Any Open-Meteo Marine-compatible service, e.g. a self-hosted one.
|
|
202
|
-
| USB export directory | — | Where the abandon-ship copy is written, e.g. `/media/usb`. Empty turns the USB copy off.
|
|
203
|
-
| Automatic USB copy interval | 15 min | How often the USB copy is brought up to date. 0 turns the periodic copy off.
|
|
204
|
-
| Copy to the USB drive at each arrival | on | Brings the USB copy up to date as soon as a passage ends.
|
|
205
|
-
| Logbook language (PDF) | en | Language of the PDF logbooks on the USB drive (English or French).
|
|
206
|
-
| Ship's time zone (PDF) | the server's | Time zone of the PDF logbooks on the USB drive, e.g. `Europe/Paris`.
|
|
207
|
-
| Wind speed thresholds | 20, 30 kn | Logged when the 2-minute average true wind crosses them.
|
|
208
|
-
| Barometric drop warning | 4 hPa / 3 h | 0 turns it off.
|
|
192
|
+
| Setting | Default | What it does |
|
|
193
|
+
| -------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
194
|
+
| Stop duration that ends a passage | 30 min | Shorter stops stay within the same passage. |
|
|
195
|
+
| Under-way speed without navigation.state | 1 kn | Used only without signalk-autostate: under way above it, stopped below half of it. |
|
|
196
|
+
| Propulsion assumed without engine data | sail | When nothing says whether the engine is running. Set to engine on a motorboat. |
|
|
197
|
+
| Instrument snapshot interval | 60 min | Readings on this clock boundary during a passage. |
|
|
198
|
+
| Track point interval | 15 s | A track point at least this often while moving. |
|
|
199
|
+
| Place matching radius | 200 m | A departure or arrival this close to a known place takes its name. |
|
|
200
|
+
| Name departures and arrivals with online geocoding | on | Turn off to never send positions online; places are then named after their coordinates until corrected. |
|
|
201
|
+
| Geocoding service | `https://nominatim.openstreetmap.org` | Any Nominatim-compatible service, e.g. a self-hosted one. |
|
|
202
|
+
| Fetch the tide forecast at departure | on | Turn off to never send the departure position online; the passage page then shows no tide. |
|
|
203
|
+
| Tide service | `https://marine-api.open-meteo.com/v1/marine` | Any Open-Meteo Marine-compatible service, e.g. a self-hosted one. |
|
|
204
|
+
| USB export directory | — | Where the abandon-ship copy is written, e.g. `/media/usb`. Empty turns the USB copy off. |
|
|
205
|
+
| Automatic USB copy interval | 15 min | How often the USB copy is brought up to date. 0 turns the periodic copy off. |
|
|
206
|
+
| Copy to the USB drive at each arrival | on | Brings the USB copy up to date as soon as a passage ends. |
|
|
207
|
+
| Logbook language (PDF) | en | Language of the PDF logbooks on the USB drive (English or French). |
|
|
208
|
+
| Ship's time zone (PDF) | the server's | Time zone of the PDF logbooks on the USB drive, e.g. `Europe/Paris`. |
|
|
209
|
+
| Wind speed thresholds | 20, 30 kn | Logged when the 2-minute average true wind crosses them. |
|
|
210
|
+
| Barometric drop warning | 4 hPa / 3 h | 0 turns it off. |
|
|
211
|
+
| InfluxDB host (retrospective analysis) | — | Local or remote host of the InfluxDB 1.x database signalk-to-influxdb writes to. Empty turns the retrospective analysis page off. |
|
|
212
|
+
| InfluxDB port | 8086 | |
|
|
213
|
+
| InfluxDB database | — | |
|
|
214
|
+
| InfluxDB username / password | — | Leave empty if the database needs none. |
|
|
215
|
+
| InfluxDB protocol | http | `http` or `https`. |
|
|
216
|
+
| InfluxDB vessel context | this server's own | Only needed running the replay from a different Signal K server than the one that wrote the history, e.g. development pointed at a production database. |
|
|
209
217
|
|
|
210
218
|
## Signal K data used
|
|
211
219
|
|
|
@@ -216,6 +224,7 @@ None of these is required except position and speed over ground; each feature us
|
|
|
216
224
|
| Passages, track, distance | `navigation.position`, `navigation.speedOverGround`, `navigation.courseOverGroundTrue`, `navigation.state` |
|
|
217
225
|
| Engine or sail | `propulsion.*.revolutions`, `propulsion.*.state`, `navigation.state` |
|
|
218
226
|
| Readings | `navigation.headingTrue` (or `headingMagnetic` + `magneticVariation`), `navigation.speedThroughWater`, `environment.wind.*`, `environment.depth.belowSurface` (or `belowTransducer`), `environment.outside.pressure`, `environment.outside.temperature`, `environment.water.temperature`, `navigation.log`, `propulsion.*.runTime` |
|
|
227
|
+
| Boat status | `tanks.*.*.currentLevel`, `.currentVolume`, `.capacity`, `.name`, `electrical.batteries.*.voltage`, `.current`, `.capacity.stateOfCharge`, `.temperature`, `.name` |
|
|
219
228
|
| Events | `notifications.*`, `steering.autopilot.state`, `.mode`, `.engaged`, `.target`, `environment.wind.speedTrue`, `environment.outside.pressure` |
|
|
220
229
|
|
|
221
230
|
## Backups and abandon ship
|
|
@@ -227,11 +236,22 @@ None of these is required except position and speed over ground; each feature us
|
|
|
227
236
|
- The Export page shows the schedule, the last copy, the next one, and the last failure if any.
|
|
228
237
|
- **The database** — `chiplog.sqlite` in the plugin's data folder can be copied while the plugin is stopped.
|
|
229
238
|
|
|
239
|
+
## Retrospective analysis
|
|
240
|
+
|
|
241
|
+
Already have a history of the boat's Signal K data before Chiplog was installed, or from a period the plugin was stopped? The **Retrospective** page (admin access) reconstructs those passages from it, using the exact same detection Chiplog runs live — the same thresholds, so a reconstructed passage is one Chiplog would have logged had it been running at the time.
|
|
242
|
+
|
|
243
|
+
- **Requires [signalk-to-influxdb](https://github.com/tkurki/signalk-to-influxdb)** (a recommended companion plugin) already having written the boat's data into an InfluxDB 1.x database — local or on another machine, set in **Apps & Plugins → Configuration**: host, port, database, and a username/password if it needs one.
|
|
244
|
+
- Pick a **from** and **to** date on the Retrospective page and start it. It runs in the background — the page shows its progress — and can be cancelled at any point; a first quick pass finds when the boat moved, and only those stretches are then fetched and reconstructed, so weeks in port take next to no time. Once done, the page sums up what it added: passages, distance, time under engine and sail, track points and events; what was already reconstructed up to that point stays on record.
|
|
245
|
+
- **Refuses to run while a passage is under way**, whatever the date range asked for — it would be reconstructing history through the same detection that is simultaneously tracking the live passage.
|
|
246
|
+
- **Refuses a range that overlaps a passage already logged**, to avoid a duplicate or a conflicting one. Reconstruction only ever adds passages; it does not edit or merge into an existing one.
|
|
247
|
+
- **What is not reconstructed**: Signal K alarms and emergencies (`sk_alarm` events), since a typical InfluxDB history does not archive notifications the way it does a numeric reading; strong-wind and falling-barometer events while the boat lay still between passages; and the extra track points recorded live on turns and speed changes — a reconstructed track has one point per **Track point interval**. Everything else read from a continuously published path — position, speed, wind, engine, autopilot, depth, barometer — is reconstructed the same as live.
|
|
248
|
+
|
|
230
249
|
## Privacy and online services
|
|
231
250
|
|
|
232
251
|
- **Place names.** With geocoding on, the position of each departure and arrival that matches no known place is sent to the geocoding service — OpenStreetMap's public Nominatim by default. Nothing else is sent, and nothing at all when it is off.
|
|
233
252
|
- **Tide forecast.** With it on, the departure position of each passage is sent to the tide service — the public Open-Meteo by default — once, at departure. Nothing at all when it is off.
|
|
234
253
|
- **Maps.** The logbook webapp loads map tiles from OpenStreetMap and OpenSeaMap while the device viewing it is online. Offline, the track is still drawn, on a blank background.
|
|
254
|
+
- **Retrospective analysis.** Running one queries the InfluxDB database set in the plugin configuration — the boat's own, local or remote, never a third party — for the Signal K history in the requested range.
|
|
235
255
|
- **Nothing else** leaves the boat. There is no account, analytics or cloud service.
|
|
236
256
|
|
|
237
257
|
Map data and place names © OpenStreetMap contributors (ODbL); seamarks © OpenSeaMap; tide data © [Open-Meteo.com](https://open-meteo.com/) (CC BY 4.0).
|
|
@@ -279,6 +299,26 @@ The USB drive is not mounted at the configured directory, or cannot be written.
|
|
|
279
299
|
|
|
280
300
|
The server's clock is wrong — common on a Raspberry Pi without a real-time clock. Set it from GPS with `signalk-set-system-time`.
|
|
281
301
|
|
|
302
|
+
### Handwriting strokes are dropped or turn into typed text
|
|
303
|
+
|
|
304
|
+
On an iPad, this is Apple's **Scribble** intercepting the Apple Pencil before the page sees it — a known iPadOS/Safari limitation with no web-page-level fix (Scribble runs beneath the browser). If it happens often, turn Scribble off under **Settings → Apple Pencil → Scribble**; a tablet dedicated to Chiplog does not need it.
|
|
305
|
+
|
|
306
|
+
### A retrospective analysis finishes but reconstructs nothing
|
|
307
|
+
|
|
308
|
+
Signal K tags historical data with the vessel it came from; a replay only reads data tagged for its own vessel. This shows up running the replay from a different Signal K server than the one that wrote the history — a development instance pointed at a production database, typically — since each server has its own vessel identity by default. The replay's error names the vessel contexts it actually found in the database; set the matching one as **InfluxDB vessel context** in the plugin configuration.
|
|
309
|
+
|
|
310
|
+
### A retrospective analysis takes minutes then fails with no clear reason
|
|
311
|
+
|
|
312
|
+
The InfluxDB server did not answer — unreachable, overloaded, a firewall or a VPN not connected. Each query now gives up after 30 seconds with the connection problem it ran into, rather than hanging until some far longer, less informative failure; check that the server named in the plugin configuration is reachable from wherever Signal K runs, and that it is not overloaded.
|
|
313
|
+
|
|
314
|
+
### A retrospective analysis over several days makes the InfluxDB server unresponsive
|
|
315
|
+
|
|
316
|
+
The replay first reads one mean speed per minute, a week at a time, then fetches only the stretches where the boat moved, six hours at a time and already reduced to one value per track interval, with a short pause between requests, specifically so this does not happen — a boat's InfluxDB often shares a resource-constrained host (a Raspberry Pi) with Signal K itself, and one query spanning weeks across every path at once can overwhelm it. If it still struggles on a very small or busy host, run the reconstruction over shorter date ranges instead of the whole history at once.
|
|
317
|
+
|
|
318
|
+
### Reconstructed passages keep their provisional place names for a while
|
|
319
|
+
|
|
320
|
+
A replay wakes the geocoding lookup as soon as it finishes, but the lookup itself still needs internet access to succeed — the passage page shows the raw coordinates until it does. If the boat (or the Signal K server running the replay) has no internet access at the time, naming is retried on the same backoff as any other departure or arrival, up to an hour between attempts; nothing is lost, it just takes longer to resolve.
|
|
321
|
+
|
|
282
322
|
## Limitations
|
|
283
323
|
|
|
284
324
|
- **Not yet:** a places page, and editing manoeuvre shortcuts from the webapps.
|
package/index.js
CHANGED
|
@@ -3,9 +3,11 @@ const { openDatabase } = require('./lib/database');
|
|
|
3
3
|
const { createPassageDetector, DETECTION_DEFAULTS, TICK_INTERVAL_MS } = require('./lib/detection');
|
|
4
4
|
const { ApiError } = require('./lib/errors');
|
|
5
5
|
const { createEventWatcher, CHECK_INTERVAL_MS, EVENT_DEFAULTS } = require('./lib/event-watcher');
|
|
6
|
+
const { INFLUX_DEFAULTS } = require('./lib/influx-history');
|
|
6
7
|
const { OBSERVATION_DEFAULTS } = require('./lib/observation-recorder');
|
|
7
8
|
const { createPlaceNamer, GEOCODING_DEFAULTS } = require('./lib/place-names');
|
|
8
9
|
const { PROPULSION_DEFAULTS } = require('./lib/propulsion-detector');
|
|
10
|
+
const { createReplayJob } = require('./lib/replay-job');
|
|
9
11
|
const { createTideForecaster, TIDE_DEFAULTS } = require('./lib/tide-forecaster');
|
|
10
12
|
const { createTrackRecorder, SAMPLE_INTERVAL_MS, TRACK_DEFAULTS } = require('./lib/track-recorder');
|
|
11
13
|
const {
|
|
@@ -55,6 +57,7 @@ module.exports = function (app) {
|
|
|
55
57
|
let namer = null;
|
|
56
58
|
let tideForecaster = null;
|
|
57
59
|
let usbExport = null;
|
|
60
|
+
let replayJob = null;
|
|
58
61
|
let namingTimer = null;
|
|
59
62
|
let tideTimer = null;
|
|
60
63
|
let timers = [];
|
|
@@ -188,6 +191,43 @@ module.exports = function (app) {
|
|
|
188
191
|
'The log records a pressure fall of at least this much over three hours; 0 disables it',
|
|
189
192
|
default: EVENT_DEFAULTS.pressureDropThreshold,
|
|
190
193
|
minimum: 0
|
|
194
|
+
},
|
|
195
|
+
influxHost: {
|
|
196
|
+
type: 'string',
|
|
197
|
+
title: 'InfluxDB host (retrospective analysis)',
|
|
198
|
+
description:
|
|
199
|
+
'For reconstructing past passages from a signalk-to-influxdb history (InfluxDB 1.x), local or remote. Leave empty to turn that feature off'
|
|
200
|
+
},
|
|
201
|
+
influxPort: {
|
|
202
|
+
type: 'number',
|
|
203
|
+
title: 'InfluxDB port',
|
|
204
|
+
default: INFLUX_DEFAULTS.influxPort
|
|
205
|
+
},
|
|
206
|
+
influxDatabase: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
title: 'InfluxDB database'
|
|
209
|
+
},
|
|
210
|
+
influxUsername: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
title: 'InfluxDB username',
|
|
213
|
+
description: 'Leave empty if the database needs none'
|
|
214
|
+
},
|
|
215
|
+
influxPassword: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
title: 'InfluxDB password',
|
|
218
|
+
format: 'password'
|
|
219
|
+
},
|
|
220
|
+
influxProtocol: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
title: 'InfluxDB protocol',
|
|
223
|
+
enum: ['http', 'https'],
|
|
224
|
+
default: INFLUX_DEFAULTS.influxProtocol
|
|
225
|
+
},
|
|
226
|
+
influxSelfContext: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
title: 'InfluxDB vessel context',
|
|
229
|
+
description:
|
|
230
|
+
'Only needed running the replay from a different Signal K server than the one that wrote the history — e.g. a development instance pointed at a boat’s production database. The vessel context the data was tagged with, such as "vessels.urn:mrn:imo:mmsi:123456789"; a failed replay names the contexts actually found. Leave empty to use this server’s own (Signal K → Server → Vessel Identity)'
|
|
191
231
|
}
|
|
192
232
|
}
|
|
193
233
|
};
|
|
@@ -209,6 +249,18 @@ module.exports = function (app) {
|
|
|
209
249
|
}
|
|
210
250
|
}
|
|
211
251
|
|
|
252
|
+
// A retrospective replay can create newly-pending place names; wake the
|
|
253
|
+
// naming chain immediately rather than leave it to whatever backoff it
|
|
254
|
+
// was already in. Guarded against the plugin having stopped in the
|
|
255
|
+
// meantime, since a replay runs in the background and isn't awaited.
|
|
256
|
+
function nudgeNaming() {
|
|
257
|
+
if (!namer) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
clearTimeout(namingTimer);
|
|
261
|
+
namingTimer = setTimeout(runNaming, 0);
|
|
262
|
+
}
|
|
263
|
+
|
|
212
264
|
// Geocoding is a network call, so it runs as its own chain of timeouts rather
|
|
213
265
|
// than inside detection: each lookup says when the next one is due.
|
|
214
266
|
async function runNaming() {
|
|
@@ -304,7 +356,14 @@ module.exports = function (app) {
|
|
|
304
356
|
: 'en',
|
|
305
357
|
logbookTimeZone: config.logbookTimeZone || null,
|
|
306
358
|
windSpeedThresholds: config.windSpeedThresholds ?? EVENT_DEFAULTS.windSpeedThresholds,
|
|
307
|
-
pressureDropThreshold: config.pressureDropThreshold ?? EVENT_DEFAULTS.pressureDropThreshold
|
|
359
|
+
pressureDropThreshold: config.pressureDropThreshold ?? EVENT_DEFAULTS.pressureDropThreshold,
|
|
360
|
+
influxHost: config.influxHost || null,
|
|
361
|
+
influxPort: config.influxPort ?? INFLUX_DEFAULTS.influxPort,
|
|
362
|
+
influxDatabase: config.influxDatabase || null,
|
|
363
|
+
influxUsername: config.influxUsername || null,
|
|
364
|
+
influxPassword: config.influxPassword || null,
|
|
365
|
+
influxProtocol: config.influxProtocol || INFLUX_DEFAULTS.influxProtocol,
|
|
366
|
+
influxSelfContext: config.influxSelfContext || null
|
|
308
367
|
};
|
|
309
368
|
|
|
310
369
|
if (settings.logbookTimeZone && !isTimeZone(settings.logbookTimeZone)) {
|
|
@@ -347,6 +406,13 @@ module.exports = function (app) {
|
|
|
347
406
|
pdfOptions,
|
|
348
407
|
log: (level, message) => (level === 'error' ? app.error(message) : app.debug(message))
|
|
349
408
|
});
|
|
409
|
+
replayJob = createReplayJob({
|
|
410
|
+
db: database,
|
|
411
|
+
settings,
|
|
412
|
+
app,
|
|
413
|
+
log: (level, message) => (level === 'error' ? app.error(message) : app.debug(message)),
|
|
414
|
+
onDone: nudgeNaming
|
|
415
|
+
});
|
|
350
416
|
namingTimer = setTimeout(runNaming, FIRST_NAMING_DELAY_MS);
|
|
351
417
|
tideTimer = setTimeout(runTides, FIRST_TIDE_DELAY_MS);
|
|
352
418
|
lastStatus = null;
|
|
@@ -379,6 +445,8 @@ module.exports = function (app) {
|
|
|
379
445
|
tideForecaster = null;
|
|
380
446
|
usbExport?.stop();
|
|
381
447
|
usbExport = null;
|
|
448
|
+
replayJob?.cancel();
|
|
449
|
+
replayJob = null;
|
|
382
450
|
detector = null;
|
|
383
451
|
if (database) {
|
|
384
452
|
database.close();
|
|
@@ -404,7 +472,9 @@ module.exports = function (app) {
|
|
|
404
472
|
now: () => new Date().toISOString(),
|
|
405
473
|
vesselPosition: () => readVesselPosition(app),
|
|
406
474
|
observeEvent: (entryId, time) => detector.observeEvent(entryId, time),
|
|
475
|
+
noteDeparture: (entryId) => detector.noteDeparture(entryId),
|
|
407
476
|
usbExport,
|
|
477
|
+
replayJob,
|
|
408
478
|
pdfOptions,
|
|
409
479
|
detection: () => ({
|
|
410
480
|
mode: detector.mode(),
|
package/lib/api.js
CHANGED
|
@@ -269,22 +269,27 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
269
269
|
|
|
270
270
|
// Conditions at a manoeuvre, note or sketch belong in the log — but only as
|
|
271
271
|
// it happens; readings now say nothing about one logged after the fact.
|
|
272
|
-
|
|
272
|
+
// Likewise the boat's state, for a passage the crew opened by casting off.
|
|
273
|
+
const observeLiveEvent = (context, input, { event, created, openedEntry }) => {
|
|
273
274
|
if (created && events.CLIENT_EVENT_TYPES.includes(event.type) && input.time === undefined) {
|
|
274
|
-
observeEvent(event.entryId, event.time);
|
|
275
|
+
context.observeEvent(event.entryId, event.time);
|
|
276
|
+
if (openedEntry) {
|
|
277
|
+
context.noteDeparture(event.entryId);
|
|
278
|
+
}
|
|
275
279
|
}
|
|
276
280
|
};
|
|
277
281
|
|
|
278
282
|
readwrite.post(
|
|
279
283
|
'/api/entries/:id/events',
|
|
280
|
-
handle((
|
|
284
|
+
handle((context, req, res) => {
|
|
285
|
+
const { db, now, vesselPosition } = context;
|
|
281
286
|
const id = entryId(req);
|
|
282
287
|
const input = parseEventInput(v.requireBody(req.body, EVENT_FIELDS));
|
|
283
288
|
const outcome = events.createEvent(db, id, input, {
|
|
284
289
|
now: now(),
|
|
285
290
|
vesselPosition: vesselPosition()
|
|
286
291
|
});
|
|
287
|
-
observeLiveEvent(
|
|
292
|
+
observeLiveEvent(context, input, outcome);
|
|
288
293
|
res.status(outcome.created ? 201 : 200);
|
|
289
294
|
return outcome.event;
|
|
290
295
|
})
|
|
@@ -293,14 +298,15 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
293
298
|
// What the tablet posts: the server finds the passage the entry belongs to.
|
|
294
299
|
readwrite.post(
|
|
295
300
|
'/api/events',
|
|
296
|
-
handle((
|
|
301
|
+
handle((context, req, res) => {
|
|
302
|
+
const { db, config, now, vesselPosition } = context;
|
|
297
303
|
const input = parseEventInput(v.requireBody(req.body, EVENT_FIELDS));
|
|
298
304
|
const outcome = events.logCrewEvent(db, input, {
|
|
299
305
|
now: now(),
|
|
300
306
|
vesselPosition: vesselPosition(),
|
|
301
307
|
placeMatchRadius: config.placeMatchRadius
|
|
302
308
|
});
|
|
303
|
-
observeLiveEvent(
|
|
309
|
+
observeLiveEvent(context, input, outcome);
|
|
304
310
|
res.status(outcome.created ? 201 : 200);
|
|
305
311
|
return { ...outcome.event, openedEntry: outcome.openedEntry };
|
|
306
312
|
})
|
|
@@ -450,6 +456,32 @@ function registerRoutes(router, { getContext, logError }) {
|
|
|
450
456
|
return usbExport.run('manual');
|
|
451
457
|
})
|
|
452
458
|
);
|
|
459
|
+
|
|
460
|
+
readonly.get(
|
|
461
|
+
'/api/replay',
|
|
462
|
+
handle(({ replayJob }) => replayJob.status())
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
admin.post(
|
|
466
|
+
'/api/replay',
|
|
467
|
+
handle(({ replayJob }, req) => {
|
|
468
|
+
const from = v.parseTimestamp(req.body.from, 'from');
|
|
469
|
+
const to = v.parseTimestamp(req.body.to, 'to');
|
|
470
|
+
if (to <= from) {
|
|
471
|
+
throw badRequest('to must be after from');
|
|
472
|
+
}
|
|
473
|
+
return replayJob.start(from, to);
|
|
474
|
+
})
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
admin.post(
|
|
478
|
+
'/api/replay/cancel',
|
|
479
|
+
handle(({ replayJob }) => {
|
|
480
|
+
if (!replayJob.cancel()) {
|
|
481
|
+
throw conflict('replay_not_running', 'No retrospective replay is running');
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
);
|
|
453
485
|
}
|
|
454
486
|
|
|
455
487
|
module.exports = { registerRoutes };
|
package/lib/database.js
CHANGED
|
@@ -239,6 +239,23 @@ const MIGRATIONS = [
|
|
|
239
239
|
fetched_at TEXT NOT NULL,
|
|
240
240
|
points TEXT NOT NULL
|
|
241
241
|
);
|
|
242
|
+
`,
|
|
243
|
+
`
|
|
244
|
+
-- Speed through water at each track point, for the passage page's position
|
|
245
|
+
-- scrubber (SPEC §4.1) -- true wind angle is derived from twd and heading
|
|
246
|
+
-- instead of stored, since it needs no sensor of its own.
|
|
247
|
+
ALTER TABLE track_points ADD COLUMN stw REAL;
|
|
248
|
+
`,
|
|
249
|
+
// Another CHECK-widening rebuild, same reason and same shape as
|
|
250
|
+
// addPropulsionChangeEventType.
|
|
251
|
+
addStopoverEventType,
|
|
252
|
+
`
|
|
253
|
+
-- The boat's state noted as the passage opened (SPEC §4.5.1), as JSON
|
|
254
|
+
-- arrays: every tank's level, volume and capacity, every battery's voltage,
|
|
255
|
+
-- current, state of charge and temperature, in SI units. NULL when the boat
|
|
256
|
+
-- publishes none, or the passage was opened after the fact.
|
|
257
|
+
ALTER TABLE log_entries ADD COLUMN start_tanks TEXT;
|
|
258
|
+
ALTER TABLE log_entries ADD COLUMN start_batteries TEXT;
|
|
242
259
|
`
|
|
243
260
|
];
|
|
244
261
|
|
|
@@ -290,6 +307,56 @@ function addPropulsionChangeEventType(db, version) {
|
|
|
290
307
|
db.exec('PRAGMA foreign_keys = ON');
|
|
291
308
|
}
|
|
292
309
|
|
|
310
|
+
function addStopoverEventType(db, version) {
|
|
311
|
+
db.exec('PRAGMA foreign_keys = OFF');
|
|
312
|
+
withTransaction(db, () => {
|
|
313
|
+
db.exec(`
|
|
314
|
+
-- Merging two entries (SPEC §3.1) folds the earlier one's arrival into
|
|
315
|
+
-- the middle of the surviving passage, which would otherwise silently
|
|
316
|
+
-- drop the only record of where that stop was.
|
|
317
|
+
CREATE TABLE events_new (
|
|
318
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
319
|
+
entry_id INTEGER NOT NULL REFERENCES log_entries (id) ON DELETE CASCADE,
|
|
320
|
+
time TEXT NOT NULL,
|
|
321
|
+
type TEXT NOT NULL CHECK (
|
|
322
|
+
type IN (
|
|
323
|
+
'manoeuvre',
|
|
324
|
+
'text_annotation',
|
|
325
|
+
'handwritten_annotation',
|
|
326
|
+
'sk_alarm',
|
|
327
|
+
'autopilot',
|
|
328
|
+
'weather_threshold',
|
|
329
|
+
'manual_correction',
|
|
330
|
+
'propulsion_change',
|
|
331
|
+
'stopover'
|
|
332
|
+
)
|
|
333
|
+
),
|
|
334
|
+
subtype TEXT,
|
|
335
|
+
lat REAL,
|
|
336
|
+
lon REAL,
|
|
337
|
+
comment TEXT,
|
|
338
|
+
payload TEXT,
|
|
339
|
+
source TEXT NOT NULL DEFAULT 'manual' CHECK (source IN ('auto', 'manual')),
|
|
340
|
+
created_at TEXT NOT NULL,
|
|
341
|
+
client_ref TEXT
|
|
342
|
+
);
|
|
343
|
+
INSERT INTO events_new (
|
|
344
|
+
id, entry_id, time, type, subtype, lat, lon, comment, payload, source, created_at, client_ref
|
|
345
|
+
)
|
|
346
|
+
SELECT id, entry_id, time, type, subtype, lat, lon, comment, payload, source, created_at, client_ref
|
|
347
|
+
FROM events;
|
|
348
|
+
DROP TABLE events;
|
|
349
|
+
ALTER TABLE events_new RENAME TO events;
|
|
350
|
+
CREATE INDEX idx_events_entry_time ON events (entry_id, time);
|
|
351
|
+
CREATE INDEX idx_events_type ON events (type);
|
|
352
|
+
CREATE UNIQUE INDEX idx_events_client_ref ON events (client_ref)
|
|
353
|
+
WHERE client_ref IS NOT NULL;
|
|
354
|
+
`);
|
|
355
|
+
db.exec(`PRAGMA user_version = ${version + 1}`);
|
|
356
|
+
});
|
|
357
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
358
|
+
}
|
|
359
|
+
|
|
293
360
|
function seedBuiltinManoeuvreTypes(db) {
|
|
294
361
|
const insert = db.prepare(
|
|
295
362
|
`INSERT INTO manoeuvre_types (key, label, sort_order, builtin)
|
|
@@ -301,8 +368,24 @@ function seedBuiltinManoeuvreTypes(db) {
|
|
|
301
368
|
}
|
|
302
369
|
}
|
|
303
370
|
|
|
371
|
+
// Open transactions per connection. A call made inside another joins it
|
|
372
|
+
// rather than starting its own -- the retrospective replay wraps many
|
|
373
|
+
// detection ticks in one, so a long replay commits (and syncs) once per slice
|
|
374
|
+
// instead of once per tick. Only the outermost call commits or rolls back.
|
|
375
|
+
const transactionDepth = new WeakMap();
|
|
376
|
+
|
|
304
377
|
function withTransaction(db, fn) {
|
|
378
|
+
const depth = transactionDepth.get(db) ?? 0;
|
|
379
|
+
if (depth > 0) {
|
|
380
|
+
transactionDepth.set(db, depth + 1);
|
|
381
|
+
try {
|
|
382
|
+
return fn();
|
|
383
|
+
} finally {
|
|
384
|
+
transactionDepth.set(db, depth);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
305
387
|
db.exec('BEGIN');
|
|
388
|
+
transactionDepth.set(db, 1);
|
|
306
389
|
try {
|
|
307
390
|
const result = fn();
|
|
308
391
|
db.exec('COMMIT');
|
|
@@ -310,6 +393,8 @@ function withTransaction(db, fn) {
|
|
|
310
393
|
} catch (err) {
|
|
311
394
|
db.exec('ROLLBACK');
|
|
312
395
|
throw err;
|
|
396
|
+
} finally {
|
|
397
|
+
transactionDepth.delete(db);
|
|
313
398
|
}
|
|
314
399
|
}
|
|
315
400
|
|
package/lib/detection.js
CHANGED
|
@@ -360,7 +360,11 @@ function createPassageDetector({ db, readSelfPath, settings, clock = Date.now })
|
|
|
360
360
|
closePassage(entry, now);
|
|
361
361
|
// Closed well after the tolerance ran out means the plugin was not
|
|
362
362
|
// running: current conditions say nothing about that arrival.
|
|
363
|
-
closed = {
|
|
363
|
+
closed = {
|
|
364
|
+
id: entry.id,
|
|
365
|
+
endTime: entry.stopped_since,
|
|
366
|
+
late: stoppedFor > closureMs + LATE_CLOSURE_MARGIN_MS
|
|
367
|
+
};
|
|
364
368
|
entry = null;
|
|
365
369
|
} else if (current === 'underway') {
|
|
366
370
|
resumedAt = resumeTime(entry, now);
|
|
@@ -400,11 +404,16 @@ function createPassageDetector({ db, readSelfPath, settings, clock = Date.now })
|
|
|
400
404
|
stateIssue: () => stateIssue,
|
|
401
405
|
motion: () => motion,
|
|
402
406
|
propulsion: () => propulsionType,
|
|
403
|
-
observeEvent: (entryId, time) => observations.recordEvent(entryId, time, clock())
|
|
407
|
+
observeEvent: (entryId, time) => observations.recordEvent(entryId, time, clock()),
|
|
408
|
+
noteDeparture: (entryId) => observations.noteDeparture(entryId, clock())
|
|
404
409
|
};
|
|
405
410
|
}
|
|
406
411
|
|
|
407
412
|
module.exports = {
|
|
413
|
+
AUTOSTATE_SOURCE_PREFIX,
|
|
414
|
+
MAX_AGE_MS,
|
|
415
|
+
METRES_PER_SECOND_PER_KNOT,
|
|
416
|
+
UNDERWAY_STATES,
|
|
408
417
|
createFreshnessTracker,
|
|
409
418
|
createPassageDetector,
|
|
410
419
|
DETECTION_DEFAULTS,
|
package/lib/entries.js
CHANGED
|
@@ -29,6 +29,8 @@ function toEntry(row) {
|
|
|
29
29
|
engineDuration: row.engine_duration,
|
|
30
30
|
sailDuration: row.sail_duration,
|
|
31
31
|
openedByEventId: row.opened_by_event_id,
|
|
32
|
+
startTanks: row.start_tanks === null ? null : JSON.parse(row.start_tanks),
|
|
33
|
+
startBatteries: row.start_batteries === null ? null : JSON.parse(row.start_batteries),
|
|
32
34
|
createdAt: row.created_at,
|
|
33
35
|
updatedAt: row.updated_at
|
|
34
36
|
};
|
|
@@ -307,6 +309,16 @@ function mergeEntries(db, id, otherId, now) {
|
|
|
307
309
|
);
|
|
308
310
|
}
|
|
309
311
|
|
|
312
|
+
// The "entry_end" observation is dated the same as the entry's own end,
|
|
313
|
+
// so this is normally a no-op; it only matters for an entry closed
|
|
314
|
+
// before that was true, when the reading could be up to
|
|
315
|
+
// `stopClosureMinutes` later than end_time. Read before the reassignment
|
|
316
|
+
// below, while entry_id still tells earlier's own snapshot apart from
|
|
317
|
+
// later's.
|
|
318
|
+
const entryEndObservation = db
|
|
319
|
+
.prepare(`SELECT MAX(time) AS time FROM observations WHERE entry_id = ? AND reason = ?`)
|
|
320
|
+
.get(earlier.id, 'entry_end').time;
|
|
321
|
+
|
|
310
322
|
for (const table of CHILD_TABLES) {
|
|
311
323
|
db.prepare(`UPDATE ${table} SET entry_id = ? WHERE entry_id = ?`).run(earlier.id, later.id);
|
|
312
324
|
}
|
|
@@ -314,6 +326,38 @@ function mergeEntries(db, id, otherId, now) {
|
|
|
314
326
|
// index would otherwise reject the update below.
|
|
315
327
|
db.prepare('DELETE FROM log_entries WHERE id = ?').run(later.id);
|
|
316
328
|
|
|
329
|
+
// The merge below overwrites the earlier entry's end with the later
|
|
330
|
+
// entry's: without this, the place where the passage actually stopped —
|
|
331
|
+
// the whole reason a merge exists — would leave no trace on the timeline.
|
|
332
|
+
if (earlier.end_place_name !== null) {
|
|
333
|
+
// Placed no earlier than the entry_end reading, so it follows it on
|
|
334
|
+
// the timeline instead of appearing to predate the passage's own
|
|
335
|
+
// arrival. Falls back to the entry's end time when there is no
|
|
336
|
+
// reading to follow — closed by hand, or with no instrument data.
|
|
337
|
+
const time =
|
|
338
|
+
entryEndObservation && entryEndObservation > earlier.end_time
|
|
339
|
+
? entryEndObservation
|
|
340
|
+
: earlier.end_time;
|
|
341
|
+
// The comment carries the raw place name too, like an sk_alarm's
|
|
342
|
+
// message, so it still reads in a CSV export or before any
|
|
343
|
+
// translation is applied.
|
|
344
|
+
db.prepare(
|
|
345
|
+
`INSERT INTO events (entry_id, time, type, lat, lon, comment, payload, source, created_at)
|
|
346
|
+
VALUES (?, ?, 'stopover', ?, ?, ?, ?, 'auto', ?)`
|
|
347
|
+
).run(
|
|
348
|
+
earlier.id,
|
|
349
|
+
time,
|
|
350
|
+
earlier.end_lat,
|
|
351
|
+
earlier.end_lon,
|
|
352
|
+
earlier.end_place_name,
|
|
353
|
+
JSON.stringify({
|
|
354
|
+
placeName: earlier.end_place_name,
|
|
355
|
+
placePending: earlier.end_place_pending === 1
|
|
356
|
+
}),
|
|
357
|
+
now
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
317
361
|
db.prepare(
|
|
318
362
|
`UPDATE log_entries SET
|
|
319
363
|
state = ?, end_time = ?, stopped_since = ?,
|