@meri-imperiumi/signalk-dead-reckoning 0.1.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.
Files changed (77) hide show
  1. package/.editorconfig +5 -0
  2. package/.github/workflows/publish.yml +34 -0
  3. package/.github/workflows/signalk-ci.yml +14 -0
  4. package/.github/workflows/test.yml +17 -0
  5. package/CHANGELOG.md +411 -0
  6. package/README.md +5 -0
  7. package/SPEC.md +494 -0
  8. package/biome.json +6 -0
  9. package/doc/dr-bearing-to.png +0 -0
  10. package/doc/dr-celestial.png +0 -0
  11. package/doc/dr-map.png +0 -0
  12. package/package.json +53 -0
  13. package/plugin/bins.js +85 -0
  14. package/plugin/celestial.js +478 -0
  15. package/plugin/current.js +292 -0
  16. package/plugin/db.js +958 -0
  17. package/plugin/divergence.js +137 -0
  18. package/plugin/engine.js +163 -0
  19. package/plugin/fix-pipeline.js +408 -0
  20. package/plugin/fixes.js +622 -0
  21. package/plugin/geo.js +196 -0
  22. package/plugin/ground-track.js +124 -0
  23. package/plugin/index.js +2349 -0
  24. package/plugin/logbook.js +397 -0
  25. package/plugin/matrix.js +271 -0
  26. package/plugin/star-almanac.js +109 -0
  27. package/plugin/training.js +572 -0
  28. package/plugin/uncertainty.js +196 -0
  29. package/public/dr-app.js +913 -0
  30. package/public/dr-coord-fields.js +229 -0
  31. package/public/dr-current-panel.js +312 -0
  32. package/public/dr-detail-popover.js +409 -0
  33. package/public/dr-fix-panel.js +479 -0
  34. package/public/dr-history.js +135 -0
  35. package/public/dr-map-view.js +668 -0
  36. package/public/dr-pending-list.js +446 -0
  37. package/public/dr-position-format.js +246 -0
  38. package/public/dr-sight-panel.js +873 -0
  39. package/public/dr-signalk-stream.js +187 -0
  40. package/public/dr-theme.js +200 -0
  41. package/public/dr-viewmodel.js +865 -0
  42. package/public/icon.png +0 -0
  43. package/public/index.html +18 -0
  44. package/public/styles.css +61 -0
  45. package/public/vendor/leaflet/LICENSE +26 -0
  46. package/public/vendor/leaflet/README.md +9 -0
  47. package/public/vendor/leaflet/images/layers-2x.png +0 -0
  48. package/public/vendor/leaflet/images/layers.png +0 -0
  49. package/public/vendor/leaflet/images/marker-icon-2x.png +0 -0
  50. package/public/vendor/leaflet/images/marker-icon.png +0 -0
  51. package/public/vendor/leaflet/images/marker-shadow.png +0 -0
  52. package/public/vendor/leaflet/leaflet.css +661 -0
  53. package/public/vendor/leaflet/leaflet.js +7349 -0
  54. package/tests/bins.test.js +54 -0
  55. package/tests/celestial.test.js +324 -0
  56. package/tests/current.test.js +243 -0
  57. package/tests/db.test.js +536 -0
  58. package/tests/divergence.test.js +156 -0
  59. package/tests/dr-coord-fields.test.js +109 -0
  60. package/tests/dr-current.test.js +88 -0
  61. package/tests/dr-history.test.js +115 -0
  62. package/tests/dr-position-format.test.js +158 -0
  63. package/tests/dr-stopwatch.test.js +76 -0
  64. package/tests/dr-theme.test.js +93 -0
  65. package/tests/dr-viewmodel.test.js +721 -0
  66. package/tests/engine.test.js +92 -0
  67. package/tests/fake-app.js +167 -0
  68. package/tests/fix-pipeline.test.js +656 -0
  69. package/tests/fixes.test.js +363 -0
  70. package/tests/geo.test.js +80 -0
  71. package/tests/ground-track.test.js +107 -0
  72. package/tests/logbook.test.js +326 -0
  73. package/tests/matrix.test.js +169 -0
  74. package/tests/plugin.test.js +2762 -0
  75. package/tests/star-almanac.test.js +64 -0
  76. package/tests/training.test.js +523 -0
  77. package/tests/uncertainty.test.js +173 -0
package/.editorconfig ADDED
@@ -0,0 +1,5 @@
1
+ [*]
2
+ end_of_line = lf
3
+ insert_final_newline = true
4
+ indent_style = space
5
+ indent_size = 2
@@ -0,0 +1,34 @@
1
+ name: Publish Node.js Package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "*"
7
+
8
+ permissions:
9
+ id-token: write # Required for OIDC
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: 24
20
+ package-manager-cache: false # never use caching in release builds
21
+ - run: npm install
22
+ - run: npm test
23
+
24
+ publish-npm:
25
+ needs: build
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-node@v4
30
+ with:
31
+ node-version: 24
32
+ registry-url: https://registry.npmjs.org/
33
+ package-manager-cache: false # never use caching in release builds
34
+ - run: npm publish
@@ -0,0 +1,14 @@
1
+ name: SignalK Plugin CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+
9
+ jobs:
10
+ test:
11
+ uses: SignalK/signalk-server/.github/workflows/plugin-ci.yml@master
12
+ with:
13
+ enable-armv7: false
14
+ enable-signalk-integration: true
@@ -0,0 +1,17 @@
1
+ name: Node CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ name: Run test suite
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-node@v4
12
+ with:
13
+ node-version: 24.x
14
+ - run: npm install
15
+ - run: npm test
16
+ env:
17
+ CI: true
package/CHANGELOG.md ADDED
@@ -0,0 +1,411 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-08-27
9
+
10
+ ### Changed
11
+ - **"Tactical sci-fi" visual theme** applied across the webapp (the
12
+ Lille Ø Signal K UI spec): the GitHub-dark palette is replaced by the
13
+ semantic neon token system (`--color-green/teal/orange/red/grey`, dark
14
+ canvas tokens) declared at the document `:root` and shared into every
15
+ shadow-root component via a new `public/dr-theme.js` module — flat
16
+ geometry (no radii, no shadows), 2px corner brackets on `.sk-card`
17
+ panels, hardware-style controls (transparent buttons with theme
18
+ borders that invert on use, 2px-bottom-rule monospace inputs, sharp
19
+ square checkboxes) with ≥48 px touch targets, massive tabular-nums
20
+ telemetry values, and uppercase tracked headers. Map overlays
21
+ (divergence chip, chart-pick menu, Leaflet controls/tooltips) use the
22
+ semi-transparent dark overlay treatment; the map aggressively fills
23
+ the desktop viewport with a 50 vh mobile floor. Map geometry colors
24
+ remapped to the semantic palette (GPS green, DR/ghost teal, LOP
25
+ orange, consumed grey). `dr-app` now subscribes to
26
+ `vessels.self.environment.mode` and reflects it as
27
+ `data-mode="night"|"day"` on `<html>`, lifting the canvas tokens for
28
+ daylight legibility.
29
+ - **"Fix at coordinates" uses the structured coordinate entry** — the
30
+ same deg/min/sec/hemisphere (or decimal) sub-fields as the sight
31
+ forms, driven by the server-configured position format. The fieldset
32
+ builder, seeding and reading logic now live in a shared
33
+ `public/dr-coord-fields.js` module used by both panels (the sight
34
+ panel's private copies were removed). Reading is now format-driven:
35
+ decimal mode reads the decimal field, DM/DMS assemble the visible
36
+ deg/min/sec/hem fields.
37
+
38
+ ### Added
39
+ - **Traditional sun-run-sun support: 36 h running-fix window** —
40
+ classic single-sight-per-day practice advances yesterday's LOP by a
41
+ full day's run, so consecutive sights land ~24 h apart; the old 6 h
42
+ buffer couldn't span them. The ground-track window is now
43
+ configurable (`groundTrackHours`, default 36 h ≈ 129 600 samples at
44
+ 1 Hz — a day plus drift margin for when the sight slips; the SQLite
45
+ persistence window follows automatically). Memory and database grow
46
+ ~3.6 MB per configured hour.
47
+ - **Restart survival for the DR ground track** (work doc #16, sea-trial
48
+ prep): the running-fix advancement buffer (6 h ring of 1 Hz DR
49
+ samples, SPEC §9.1) is now persisted to SQLite (`dr_track_samples`,
50
+ flushed incrementally on the 60 s state-flush cadence plus the
51
+ stop-time flush, pruned to the buffer window, `INSERT OR REPLACE`
52
+ keyed on timestamp to match `GroundTrack.append` semantics) and
53
+ re-seeded on plugin start — a mid-passage server restart no longer
54
+ leaves sights taken before the restart un-advanced. Verified
55
+ end-to-end: plugin run → stop → restart on the same db →
56
+ `POST /fix/resolve` advances a pre-restart sight along the persisted
57
+ DR run.
58
+ - **History-backed map restart survival** (work doc #16): a new
59
+ `public/dr-history.js` module speaks the Signal K History API
60
+ (`/signalk/v2/api/history`, no auth) — multi-path `/values` queries
61
+ with aggregation postfixes (`:last` is mandatory for non-numeric
62
+ paths like the divergence record; numbers may use `average`/`sma`/
63
+ `ema`). On load, `<dr-app>` now backfills in one request: the GPS
64
+ track, the DR ghost track (previously live-session only — a page
65
+ reload blanked it), and the divergence sparkline. Tracks merge
66
+ history → live-session points continuously.
67
+ - **Header set & drift readout + manual override** (SPEC §6.2 tier 1):
68
+ the webapp header shows the resolved current vector (`067° · 1.2 kn`)
69
+ themed by source (manual = orange with TTL countdown, weather/pilot
70
+ chart = teal, none = offline grey). A new `≋ Current` toolbar button
71
+ opens `<dr-current-panel>` — enter set (° true), drift (kn) and a TTL
72
+ (default 60 min) to `PUT /current/manual`, or clear the override
73
+ (`DELETE /current/manual`); the resolver honors the manual tier over
74
+ every automatic source while the TTL lasts. `GET /status` now also
75
+ reports the resolved `current` vector and any `manualCurrent`, so
76
+ the header can bootstrap and refresh between deltas (the manual TTL
77
+ caption counts down on the 30 s status poll).
78
+ - **Stopwatch sight-time entry ("N min N sec ago")**: each sight
79
+ form's time field gains an `ago` mode — enter the minutes/seconds
80
+ elapsed since the sight was taken (stopwatch method) and the offset
81
+ converts to clock time **at entry**: every keystroke re-bases
82
+ "now", so the committed instant is anchored to when the navigator
83
+ stopped the watch, not when the form is submitted. The converted
84
+ time lands in the regular sight-time field (still editable; the
85
+ local/UTC toggle re-expresses it). Pure conversion in
86
+ `vm.stopwatchToIso()`.
87
+ - **Running-fix visualization & interactive resolve** (work doc #13):
88
+ - `POST /fix/resolve` candidates now carry per-observation
89
+ `advancements` — the original reference point as taken, the
90
+ DR-transported advanced point, and the displacement used (null when
91
+ not advanced) — so the preview can show the transport instead of
92
+ only the final fix.
93
+ - **Pending observations are first-class**: new `<dr-pending-list>`
94
+ panel alongside the map (moved out of the sight-entry modal) with
95
+ per-row select (map highlight), edit and delete. A single pending
96
+ observation shows a "needs a partner" hint. "Preview selected"
97
+ resolves just the checked subset into a live candidate ring;
98
+ confirm stays the deliberate second step.
99
+ - **Advancement layer on the map**: for each previewed observation,
100
+ the faded original point, the dashed DR-run vector, the advanced
101
+ point and the advanced LOP line. Older observations that could not
102
+ be advanced (no DR track over the interval) render in a warning
103
+ style and the candidate ring flags "includes un-advanced
104
+ observation" — the honest failure made visible.
105
+ - **Map-click detail popover** (`<dr-detail-popover>`): click any
106
+ LOP, CPL or fix on the map for its full record, with Edit (LOP/CPL
107
+ → seeded sight form; fix → inline notes editor) and Delete actions.
108
+ - **Observation & fix CRUD** (REST + db): `DELETE`/`PUT
109
+ /fix/lop/:id`, `/fix/cpl/:id`, `/fix/:id`. LOPs/CPLs attached to a
110
+ confirmed fix are guarded (409 — delete the fix first); deleting a
111
+ fix un-confirms it: its observations return to pending, the
112
+ correction row and any queued logbook entry are dropped, and the
113
+ DR origin is not rewound. Fix edits allow only audit metadata
114
+ (notes, confirmed_by, estimated error radius) — position and
115
+ source_type are guarded.
116
+ - **Phone-first layout pass** for the fix workflow: dialogs become
117
+ bottom sheets on narrow viewports, tap targets ≥ 44 px, no
118
+ hover-only affordances.
119
+ - **"Fix at coordinates" dialog** (replaces the one-tap "Fix at GPS"
120
+ button): opens prefilled with the live GNSS position, editable before
121
+ confirming. Covers three point-fix workflows with one flow —
122
+ the GPS reality check (accept the prefill as-is), known-position fixes
123
+ (type a berth/dock position, works without GNSS), and offline fixes
124
+ from paper forms (backdated fix time, `backfill` source type). The
125
+ dialog shows GNSS fix-quality stats when the receiver publishes them
126
+ (`navigation.gnss.*`): system (GPS/GLONASS/…), fix method (2D/3D/DGNSS),
127
+ satellites in use/visible, and HDOP with a rough error estimate that
128
+ prefills the new estimated-error field. Coordinates accept decimal,
129
+ DM or DMS free text so they can be transcribed from paper exactly as
130
+ written; editing a prefilled GNSS coordinate switches the source to
131
+ manual automatically.
132
+ - `POST /fix` accepts optional `timestamp`, `notes` and
133
+ `estimated_error_nm` on point fixes (the fix pipeline already
134
+ supported them; the route now forwards them). Backfilled fixes are
135
+ recorded at their observation time — in `fixes`, `dr_corrections` and
136
+ the signalk-logbook write-through — not at entry time. `backfill`
137
+ fixes get a distinct map color.
138
+ - Sea-trial safety hardening — honest DR under sensor failure:
139
+ - **Idle-while-making-way detection**: when STW/heading drop but
140
+ GPS-derived motion shows the vessel still making way (fouled
141
+ paddlewheel, compass dropout), the frozen DR position is flagged
142
+ `moving: true` in `navigation.deadReckoning.state`, the uncertainty
143
+ polygon keeps growing by GPS-derived ground distance (instead of
144
+ freezing with the water track), and a §3.1 sensor-health alert
145
+ (`notifications.navigation.deadReckoning.status`) is raised:
146
+ "DR stopped tracking… position is stale". GPS remains authoritative
147
+ until proven faulty or OVERRIDE — the watchkeeper is informed, not
148
+ left with falsely-confident DR.
149
+ - **Paddlewheel fouling surfaced**: `detectFouling`'s verdict (STW≈0
150
+ while SOG/wind indicate motion) now raises the same §3.1 alert and
151
+ sets `fouled: true` on the state, instead of silently gating
152
+ training.
153
+ - **Transient flag**: the underway state now carries
154
+ `transient: true` during a tack/gybe — the UI explains an expected
155
+ divergence spike instead of reading it as a fault.
156
+ - **"Since last fix" headline**: `navigation.deadReckoning.elapsedSinceFix`
157
+ (s, per-tick) drives the previously-unwired UI figure — the
158
+ watchkeeper's fix-cadence cue (`elapsedText` formatter in the
159
+ view-model).
160
+ - UI status panel renders the new states with distinct styling:
161
+ stale-DR / fouled (red), maneuver-in-progress (amber).
162
+ - Signal K Weather API current (SPEC §6.2 tier 3): a new
163
+ `plugin/current.js` subsystem polls
164
+ `/signalk/v2/api/weather/forecasts/point` at the vessel position
165
+ (default every 30 min, off the 1 Hz hot path) and integrates the
166
+ point-forecast `current` — `set` (rad) / `drift` (m/s), converted and
167
+ u/v-interpolated between bracketing forecast entries — into the DR
168
+ solution as set/drift. Offshore this is typically backed by a GRIB
169
+ another process already downloaded, so it works without the plugin
170
+ itself having connectivity. `resolveCurrent` (moved from
171
+ `training.js`) resolves the full hierarchy: manual override (tier 1,
172
+ not yet wired to an input) → weather API (tier 3) → offline pilot
173
+ charts (tier 4, reserved hook) → zero vector (tier 5). A failed
174
+ fetch keeps the previous cache until its TTL lapses; the resolved
175
+ tier + source is published with `environment.current`. Config:
176
+ `weatherCurrent.enabled` (default on), `.intervalMs`. The endpoint
177
+ requires no authentication (verified against a live server), so no
178
+ token plumbing is needed.
179
+
180
+ ### Fixed
181
+ - **GPS track history backfill was silently 404ing**: the webapp
182
+ queried `/signalk/v1/history/values`, but the history API (and the
183
+ installed `signalk-history-sqlite` provider) serves
184
+ `/signalk/v2/api/history/values` — the fallback to the live-session
185
+ track always kicked in. The query now uses the v2 endpoint with the
186
+ `duration` parameter from the History API contract.
187
+ - Sight panel assumed-position seeding threw on every DR/GPS position
188
+ update: the `seedCoord` sub-field selector was missing its closing
189
+ `]` (invalid selector), so the celestial form's assumed position
190
+ never tracked the boat. Fixed in the shared `dr-coord-fields.js`
191
+ module with a regression test.
192
+ - Sight panel DM/DMS submissions could send a stale seeded decimal
193
+ value instead of the user-edited deg/min/sec fields: the form parser
194
+ preferred the (hidden) decimal field whenever it was non-empty.
195
+ Reading is now driven by the panel's `data-pos-format` attribute, so
196
+ only the fields the user actually sees are read.
197
+ - Windows CI: the plugin smoke tests leaked open SQLite handles in the
198
+ shared temp directory (four `makeStarted()` tests never called
199
+ `plugin.stop()`). On Linux/macOS an open file can still be unlinked; on
200
+ Windows the cleanup `rm` failed with `EBUSY: resource busy or locked`.
201
+ Those tests now stop the plugin, and the shared teardown retries the
202
+ removal (`maxRetries`/`retryDelay`, which `fs.rm` applies to EBUSY/EPERM
203
+ on Windows only) to ride out transient locks from AV scanners on CI
204
+ runners.
205
+ - `POST /fix/resolve` and `POST /fix` no longer return `observations not
206
+ resolvable` when called with only `lop_ids`/`cpl_ids` (the common sight
207
+ panel path). The pipeline now hydrates the observation bodies from the
208
+ database via the new `getLineOfPosition`/`getCircularPositionLine` db
209
+ helpers, then runs the geometric resolver on them.
210
+ - Bearing LOP no longer "runs through and past the object to the
211
+ opposite bearing." A bearing LOP is now drawn as a ray from the
212
+ charted object toward the navigator's side (the reciprocal of the
213
+ measured bearing), with a short stub past the object, instead of a
214
+ symmetric infinite line through the object. Celestial LOPs stay
215
+ symmetric infinite lines. `lopLineSpec` now exposes `lopType`, and
216
+ `extendLineSpec` renders bearing vs celestial LOPs differently.
217
+ - Sun-run-sun / running fix (SPEC §9.1): `resolveCandidateFix` now
218
+ advances earlier observations to the timestamp of the latest one along
219
+ the vessel's DR track before resolving, turning two LOPs taken at
220
+ different times into a fix. The displacement comes from a new
221
+ `GroundTrack` DR-history buffer (`plugin/ground-track.js`) fed by the
222
+ DR engine's water-track integration only — **never GPS** (celestial is
223
+ a GPS-independent position check; GPS is used only to calibrate DR
224
+ accuracy, not to advance celestial LOPs). Boats without water-track
225
+ sensors get no advance — the honest failure rather than a wrong fix.
226
+ New `advanceToLatest` + `input.advance` provider in the pipeline.
227
+ - "Fix at GPS" quick action in the DR toolbar: confirms a GPS point fix
228
+ (`source_type: "gps"`) at the current GNSS position — the GPS reality
229
+ check that snaps the DR origin to GPS when the watchkeeper judges GPS
230
+ good. Disabled when no GPS position is known.
231
+ - Noon Sun sight reduction (SPEC §13): `POST /celestial/sight` with
232
+ `noon: true` reduces a local-apparent-noon meridian-altitude sight to
233
+ latitude directly (Lat = Dec ± z) via `reduceNoonSight`, emitted as an
234
+ east-west LOP with zero intercept — a single-sight latitude fix that
235
+ crosses any other LOP/CPL normally through the existing pipeline.
236
+ - Observations logged to the logbook on creation (SPEC §9.5): a bearing
237
+ LOP, vertical-angle CPL, or celestial sight writes a `navigation` entry
238
+ via `composeObservationEntry` — taking the sight is itself a navigational
239
+ event, independent of whether it later resolves into a fix. The entry's
240
+ `position` is the assumed/object/charted position; `text` describes what
241
+ was observed (body name, Zn, intercept for celestial).
242
+ - Logbook write-through no longer loses entries in the approval window:
243
+ while no admin token is granted (access request pending, server
244
+ unreachable, or token expired mid-passage), every fix / tack /
245
+ observation entry is queued in a new `logbook_pending` SQLite table
246
+ (bounded to 200, ordered) and flushed oldest-first once a token lands —
247
+ nothing written during the approval window is dropped. The access flow
248
+ now distinguishes an open server (501/404 → unauthenticated writes) from
249
+ a transport failure (`unreachable` → queue + honest status, retry on
250
+ the next write instead of falsely claiming an open server), and a
251
+ tokenless write re-kicks `initLogbook()` so a lost/expired grant is
252
+ re-requested at write time, not only at startup. A denied access request
253
+ stops writes without re-request spam. Delayed fix deliveries mark their
254
+ `fixes` row logged (the confirm route only marks immediate writes).
255
+ - Initial project scaffold: package metadata, CI workflows, plugin entry
256
+ point with subscription/start/stop structure, SQLite schema layer,
257
+ dead-reckoning vector-integration engine, EMA matrix store, unified fix
258
+ model, and `<dr-map-view>` web component stub.
259
+ - DR web UI (SPEC §14.1): a pure, dependency-free view-model
260
+ (`public/dr-viewmodel.js` — TrackLog ring buffers, LOP/CPL/fix/correction
261
+ render specs, uncertainty + divergence helpers, sparkline reduction, and
262
+ a Signal K `resources/charts` tile-layer parser) backed by unit tests, and
263
+ a vendored-Leaflet `<dr-map-view>` adapter plus `<dr-app>` layout with
264
+ headline figures, dual Ghost/GPS track rendering, uncertainty polygon,
265
+ LOP/CPL/snap-vector overlays, the divergence chip + sparkline, and the
266
+ always-human-initiated Failover Override control. Tile-less by default
267
+ (offline-first); basemaps come from the server's configured charts
268
+ (`/signalk/v1/api/resources/charts`) via a Leaflet layers control, never
269
+ hardcoded OSM. New `GET /fixes`, `GET /observations`, `GET /corrections`
270
+ routes feed the persisted overlays.
271
+ - DR engine idle-state reporting: the plugin publishes
272
+ `navigation.deadReckoning.state` (`{status: "idle"|"underway", reason}`)
273
+ so the UI can explain why the readout is empty when moored/anchored or
274
+ lacking speed/heading, instead of leaving the user to guess. The
275
+ webapp shows a status banner (amber idle, green underway, red link-lost)
276
+ and the GPS boat marker is always drawn when a fix is available.
277
+ - Historical GPS track from the Signal K history API
278
+ (`/signalk/v1/history/values`, same pattern as signalk-logbook's Map):
279
+ fetched once on load to seed the track, then extended by live deltas —
280
+ shows where the boat has actually been, the baseline against which DR
281
+ divergence is measured. Falls back to the live-session track when no
282
+ history provider is configured (route 404s).
283
+ - Sight & LOP input panel (SPEC §14.1 "Manual LOP & Sight Input"):
284
+ a new `<dr-sight-panel>` web component (in a `<dialog>` opened from the
285
+ headline toolbar) with three modes — compass bearing (→ LOP through the
286
+ observer), vertical-angle sight (→ CPL by height/tan(angle) distance),
287
+ and celestial sight (→ LOP via Marcq St. Hilaire, with reduction
288
+ feedback: Hc/Ho/Zn/intercept/LHA). Observations collect in a pending
289
+ list; "Resolve" previews a candidate fix on the map (hollow yellow
290
+ ring); "Confirm" snaps the DR origin via the unified fix pipeline.
291
+ - **Chart pick**: right-click (or long-press) a charted object to get
292
+ a context menu ("Add bearing to here" / "Distance CPL at here") that
293
+ opens the sight dialog with the object position pre-seeded.
294
+ - **Pending list survives reload**: observations are persisted
295
+ server-side; reopening the dialog re-hydrates the pending list from
296
+ any unattached LOPs/CPLs (`used_in_fix_id IS NULL`), so a page
297
+ reload or dialog close doesn't lose work-in-progress. Each item
298
+ shows a readable label (e.g. "#5 · lighthouse brg 045°",
299
+ "#3 · lighthouse 0.46nm"). The candidate itself is a computed preview
300
+ (not persisted) but can be re-derived with "Resolve candidate".
301
+ - **Bearing LOP semantics corrected**: a bearing is taken to a known
302
+ charted object, so the form collects the *object's* position (not the
303
+ observer's assumed position). The view-model shaper rotates the
304
+ azimuth +90° so the engine's perpendicular-line convention yields a
305
+ line running along the bearing through the object (you are somewhere
306
+ on it), matching traditional nav practice.
307
+ - **Configurable position format** (`decimal` / `dm` / `dms`, default
308
+ DMS) set in the plugin config and served to the UI via a public
309
+ `GET /signalk/v2/api/signalk-dead-reckoning/configuration` endpoint
310
+ (mirrors signalk-status-tiles' pattern); a config-hash delta triggers
311
+ a live reload on server-side edits. Coordinate entry uses structured
312
+ deg/min/sec/hemisphere fields (not a single error-prone text field)
313
+ that show/hide based on the configured format. Assumed-position
314
+ defaults track the live DR (or GPS when moored) and re-seed on format
315
+ change.
316
+ - **Server-derived `confirmed_by`**: the watchkeeper is taken from the
317
+ `JAUTHENTICATION` cookie JWT (mirrors signalk-logbook), so there is
318
+ no manual "confirmed by" form field.
319
+ Pure view-model form→REST-body shapers (`bearingLopBody`,
320
+ `verticalAngleCplBody`, `celestialSightBody`, `bearingToTrue`,
321
+ `verticalAngleDistanceNm`) and the position formatter
322
+ (`formatCoord`/`parseCoord`/`coordParts`/`parseParts` + `setFormat`/
323
+ `fmt`/`fmtPos`) are unit-tested. New `GET /celestial/bodies` route
324
+ lists Sun/Moon/bundled stars + almanac validity for the body selector.
325
+ - Stream subscription hardened after signalk-status-tiles' st-stream.js:
326
+ `/signalk/v1/stream?subscribe=none&sendMeta=all` URL, `minPeriod: 1000`
327
+ throttle, auto-reconnect on link loss with immediate re-subscribe,
328
+ hello/ack filtering, and link-state reporting to the UI.
329
+ - Unified fix pipeline (SPEC §4.4, §9.1, §9.3): DB helpers for inserting
330
+ lines of position and circular position lines and attaching them to a
331
+ confirmed fix; a pure local-planar geometric resolver (Line×Line,
332
+ Line×Circle, Circle×Circle, with least-squares residual fallback for
333
+ the cocked-hat case) returning a candidate fix plus a residual spread
334
+ and any alternate Circle×Circle candidate; running-fix advance of an
335
+ observation along the DR track; and a `fix-pipeline.js` orchestrator
336
+ (`resolveCandidateFix` → human confirmation → `confirmFix`) that
337
+ writes the `fixes` row, attaches observations, records a
338
+ `dr_corrections` row on origin-reset, and snaps the DR engine origin
339
+ without flipping navigational authority.
340
+ - REST fix pipeline (SPEC §4.4, §9.1, §9.3): `POST /fix/lop` and
341
+ `POST /fix/cpl` persist lines/circular position lines and return their
342
+ ids; `POST /fix/resolve` previews a candidate fix (with cocked-hat
343
+ residual and any alternate Circle×Circle candidate) WITHOUT
344
+ confirming; `POST /fix` now routes both point fixes and LOP/CPL-resolved
345
+ fixes through the unified pipeline, attaching observations and recording
346
+ corrections. Point-fix request shape is unchanged (back-compat).
347
+ - Celestial sight reduction (SPEC §13): pure `plugin/celestial.js`
348
+ module (Marcq St. Hilaire / intercept method) producing a LOP ready
349
+ for the fix pipeline — Sun/Moon geographic positions via GMST + RA,
350
+ star positions via a bundled `plugin/star-almanac.js` (J2000 SHA/Dec
351
+ for ~23 navigational stars, with an explicit valid epoch and
352
+ `isExpired`/`daysUntilExpiry` for the §12-style startup check);
353
+ altitude corrections (index error, dip, Bennett refraction with a
354
+ 5° low-altitude cutoff, Sun/Moon limb semi-diameter, lunar parallax);
355
+ `reduceSight` carries the time-sync staleness indicator (§11) through
356
+ to the result. `POST /celestial/sight` REST endpoint reduces a raw
357
+ sight and persists the resulting celestial LOP, returning the
358
+ reduction details (Hc, Ho, intercept, Zn, LHA) for UI feedback.
359
+ - Uncertainty polygon (SPEC §8): pure `plugin/uncertainty.js` growth
360
+ model producing a confidence-weighted circular error region around
361
+ the DR position — empirical regime from an EWMA of recent
362
+ `dr_corrections` deviation rates (per-condition via sail/sea state,
363
+ converted to a per-distance rate so the radius scales with distance
364
+ run, not clock time), conservative angular-margin fallback for
365
+ low-confidence bins, and a continuous blend between them weighted by
366
+ the current matrix bin's effective hit count. Engine gained
367
+ `logNmSinceOrigin` (distance-since-last-snap) alongside
368
+ `elapsedSinceOriginS`, persisted in `dr_state_store` so a
369
+ mid-excursion restart continues the polygon. Published every tick as
370
+ `navigation.deadReckoning.uncertainty` `{radius_nm, method}` with
371
+ meta; recomputed per tick from the current bin so bin transitions
372
+ (tack, sail change) re-evaluate it. New `db.getDeviationRateStats`
373
+ reads recent per-condition correction rows for the model.
374
+ - Divergence advisory (SPEC §7.3, gradual band): pure
375
+ `plugin/divergence.js` monitor with sustained-interval hysteresis
376
+ (raises when DR-vs-GPS divergence exceeds 1.5× the uncertainty
377
+ polygon radius for 30s, clears on 30s sustained recovery; both
378
+ tunable via `start({divergence: {...}})`) — the "get a fix" nudge
379
+ the polygon was built to threshold. Publishes/clears
380
+ `notifications.navigation.deadReckoning.divergenceAdvisory` at
381
+ `alert` severity (visual method) with the divergence/expected
382
+ numbers in the message, suppressed at anchor/moored, held (not
383
+ progressed) when either position is missing, and cleared on plugin
384
+ stop if live. Also publishes
385
+ `navigation.deadReckoning.divergence` `{distance_nm, bearing_true}`
386
+ each tick — the §14.1 live divergence readout input.
387
+ - Logbook integration (SPEC §9.4, §9.5): `plugin/logbook.js` with
388
+ §9.5 field-mapped fix-entry composition (explicit `datetime`,
389
+ DR-log for `log` per §10.3, per-source_type text templates,
390
+ `origin: agent`, closed-schema observations), auto tack/gybe
391
+ entries, and a REST client sending auth as both Bearer header and
392
+ JAUTHENTICATION cookie (the signalk-dsc pattern; no `app.fetch` —
393
+ verified against signalk-server 2.29.0). Token acquisition via
394
+ the server's Access Requests flow: stable persisted clientId,
395
+ `permissions: "admin"` requested explicitly (plugin routes are
396
+ admin-gated — verified), 30s approval polling, DENIED stops
397
+ polling, 401/403 drops the token and re-requests; a config token
398
+ short-circuits and 501/404 falls back to unauthenticated writes.
399
+ Confirmed fixes write through fire-and-forget and mark
400
+ `logged_to_logbook`/`logbook_entry_ref` on success (`db.markFixLogged`);
401
+ the GPS auto-seed snap does not (not human-confirmed). Maneuver
402
+ classification from the AWA change across the §6.4 transient
403
+ window (pre-maneuver AWA captured at window open; tack = bow
404
+ through the wind within ±90°, gybe = stern through within ±60° of
405
+ downwind), debounced (default 120s) so a beat doesn't flood the
406
+ log; the window now also requires the heading itself to
407
+ re-stabilize (±5°) before closing, so the logged course is the
408
+ settled one. `environment.seaState` (and the logbook's actual
409
+ `environment.water.swell.state`) now feed `sea_state` everywhere —
410
+ bins, dr_corrections, and polygon rates become condition-specific
411
+ for real.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Signal K Dead Reckoning
2
+
3
+ An offline-first dead reckoning and sensor fusion engine for Signal K that maintains a continuously computed "shadow boat" position from water-track sensors (speed through water, compass heading, and learned leeway and current corrections), so you always have a navigational fallback when GPS becomes unreliable — whether from jamming, spoofing, or plain receiver failure. While GPS is trusted, the engine learns vessel-specific calibration corrections against ground truth and watches for GPS anomalies; when it isn't, the same learned model keeps the dead-reckoned position, its uncertainty polygon, and a water-track log going. Fixes from celestial sights, compass bearings, and vertical angles are entered through a unified pipeline and can snap dead reckoning back on track, with optional write-through to `signalk-logbook`. Historical data can be backfilled to train the calibration model and backtest it against past passages. Licensed under the EUPL-1.2.
4
+
5
+ **Note:** This is just a toy. Make your own navigation calculations and decisions.