@meri-imperiumi/signalk-dead-reckoning 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/package.json +1 -1
- package/plugin/db.js +43 -8
- package/plugin/index.js +109 -7
- package/public/dr-app.js +327 -19
- package/public/dr-ext-host.js +59 -10
- package/public/dr-history.js +40 -4
- package/public/dr-map-view.js +853 -36
- package/public/dr-note-panel.js +187 -0
- package/public/dr-viewmodel.js +758 -13
- package/tests/db.test.js +97 -0
- package/tests/dr-app-layout.test.js +29 -0
- package/tests/dr-chartwork.test.js +30 -5
- package/tests/dr-cpa.test.js +269 -0
- package/tests/dr-ext-host.test.js +34 -1
- package/tests/dr-history.test.js +66 -2
- package/tests/dr-laylines.test.js +135 -0
- package/tests/dr-notes.test.js +144 -0
- package/tests/dr-plotter-map.test.js +223 -0
- package/tests/dr-plotter.test.js +128 -0
- package/tests/dr-track-style.test.js +8 -7
- package/tests/plugin.test.js +214 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,116 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-09-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Chartplotter UX (work doc #30, north-up phase):** the DR webapp
|
|
14
|
+
navigates like a plotter, not a map viewer:
|
|
15
|
+
- **Own-ship boat glyphs** replace the GPS dot and the bare DR X —
|
|
16
|
+
a pointed-hull vessel shape rotated to COG (GPS) and to the DR
|
|
17
|
+
course (DR), the navigator's X kept as a small detail inside the
|
|
18
|
+
DR hull. Dot/X fallbacks when course is unknown.
|
|
19
|
+
- **10-minute predictor vectors** (layers control, "Vectors"): a
|
|
20
|
+
dashed COG×SOG line from the GPS position and a DR-course×DR-speed
|
|
21
|
+
line from the DR position, with tick marks at 2-minute intervals;
|
|
22
|
+
the AIS velocity leader extends from the 6-minute to the same
|
|
23
|
+
10-minute convention so every predictor on the chart reads as one
|
|
24
|
+
family.
|
|
25
|
+
- **Nautical scale bar** (bottom-left): a custom control snapped to
|
|
26
|
+
a 0.1–1000 NM ladder (80–200 px band), metric fallback at deep
|
|
27
|
+
zooms; no `1:x` numeric readout.
|
|
28
|
+
- **Range rings** (layers control, "Range rings"): three
|
|
29
|
+
concentric rings centered on the GPS position, spacing derived
|
|
30
|
+
from the zoom ladder, re-spaced on zoom.
|
|
31
|
+
- **Wind laylines** (layers control, "Laylines"): two rays from the
|
|
32
|
+
DR vessel at TWD ± beat/gybe angle (the polar performance plugin's
|
|
33
|
+
angles — the established feed), port tack red / starboard green
|
|
34
|
+
per the navigation-light convention, fixed screen-relative length.
|
|
35
|
+
Gated honestly: renders only while `navigation.state` is
|
|
36
|
+
`sailing`, and only when true wind and the angle this point of
|
|
37
|
+
sail needs are actually published.
|
|
38
|
+
- **Pick menu is now the target surface:** every pick shows bearing
|
|
39
|
+
& distance from BOTH own-ship references (DR and GPS — the
|
|
40
|
+
most-wanted helm readout, absent-source rows hidden); AIS picks
|
|
41
|
+
add the target details panel (type, flag, dimensions, destination
|
|
42
|
+
& ETA — Freeboard-SK's field set, new static paths in the AIS
|
|
43
|
+
subscription folded into the store) plus **CPA/TCPA for both
|
|
44
|
+
references** (DR-based = conservative, GPS-based = conventional);
|
|
45
|
+
a target closing inside the CPA watch limits (0.5 nm / 20 min)
|
|
46
|
+
turns its glyph red and carries the CPA figure in its tooltip.
|
|
47
|
+
- **Measure tool** ("Measure from here…" in the pick menu): tap
|
|
48
|
+
points, get true bearing + distance leg by leg with a running
|
|
49
|
+
total in a floating readout; double-click / right-click / Esc ends.
|
|
50
|
+
- **Signal K notes on the chart**: positioned notes from the v2
|
|
51
|
+
resources API render as orange pin markers on a toggleable
|
|
52
|
+
"Notes" layer; clicking/right-clicking opens the detail surface
|
|
53
|
+
(title, body rendered per mimeType with a minimal safe markdown
|
|
54
|
+
renderer, timestamp, DR/GPS bearings, all pick actions). The pick
|
|
55
|
+
menu gains "New note at…" — a one-handed hazard-marking form
|
|
56
|
+
(Hazard preset, position pre-seeded) that POSTs to the v2
|
|
57
|
+
resources API, with edit/delete from the note's menu and refetch
|
|
58
|
+
on stream reconnect. Failed writes surface in the form, never
|
|
59
|
+
silently.
|
|
60
|
+
|
|
61
|
+
- **Fixes over a day old carry their date on the chart** —
|
|
62
|
+
"Fix 18.9. 02:36Z" instead of the ambiguous "Fix 02:36Z". With the
|
|
63
|
+
new 7-day history window most fixes on the chart aren't from
|
|
64
|
+
today, so the time alone no longer identifies the fix. Under 24 h
|
|
65
|
+
the label is unchanged.
|
|
66
|
+
- **History-aware chart window: tracks, overlays and trip
|
|
67
|
+
boundaries.** The webapp now shows the last
|
|
68
|
+
`max(7 days, since trip start)` on the chart instead of nothing:
|
|
69
|
+
both the GPS track and the DR ghost track backfill from the Signal K
|
|
70
|
+
History API over the window (10-minute resolution — ~1000 points per
|
|
71
|
+
track on a full week), and the persisted chartwork overlays (fixes,
|
|
72
|
+
LOPs, CPLs, snap vectors) are window-filtered via a new `since`
|
|
73
|
+
query parameter on the REST endpoints (`/fixes`, `/observations`,
|
|
74
|
+
`/corrections`; ISO-8601 or epoch ms, invalid values ignored).
|
|
75
|
+
Two bugs fixed on the way: the DR track backfill silently never
|
|
76
|
+
worked — the history provider emits plugin-published positions as
|
|
77
|
+
`{latitude, longitude}` objects where the parser only accepted
|
|
78
|
+
GeoJSON `[lon, lat]` pairs — and the 6-hour fetch window was
|
|
79
|
+
replaced by the proper window.
|
|
80
|
+
- **Trip log now actually resets at trip boundaries (SPEC §9.2).**
|
|
81
|
+
`engine.resetTrip()` existed but nothing ever called it, so
|
|
82
|
+
`navigation.deadReckoning.trip.log` accumulated since install (the
|
|
83
|
+
production log read 703 nm — more than the last two trips
|
|
84
|
+
combined). A sustained `navigation.state` transition from
|
|
85
|
+
`anchored`/`moored` to underway (5-minute debounce, configurable
|
|
86
|
+
via `tripBoundary.sustainS`/`clearS`) now zeroes the trip log and
|
|
87
|
+
records the trip start, which is persisted across restarts
|
|
88
|
+
(mid-trip server restarts keep the boundary) and exposed via
|
|
89
|
+
GET /status as `tripStartMs` — the webapp uses it to anchor its
|
|
90
|
+
history window. A flapping autostate source can't zero the log:
|
|
91
|
+
blips shorter than the debounce are ignored.
|
|
92
|
+
- **The headline log figure is now the trip log.** The bottom-right
|
|
93
|
+
readout shows `navigation.deadReckoning.trip.log` ("Trip log")
|
|
94
|
+
instead of the cumulative water-track log — the watchkeeper's
|
|
95
|
+
glance figure is distance-since-departure; the cumulative total
|
|
96
|
+
rides along as the figure's hover tooltip. Falls back to the
|
|
97
|
+
cumulative log until a first trip boundary is ever observed.
|
|
98
|
+
|
|
99
|
+
### Fixed
|
|
100
|
+
- **Hosted plotter widgets showed as empty black squares with no
|
|
101
|
+
data** (sea trial 2026-09-21). Two independent bugs, both in the
|
|
102
|
+
widget host, both reproducible with a locally served webapp against
|
|
103
|
+
a stub Signal K server:
|
|
104
|
+
- The bus port captured the widget iframe's `contentWindow` at
|
|
105
|
+
context creation — but a browser replaces that Window object when
|
|
106
|
+
the frame navigates from `about:blank` to its `src` (and the
|
|
107
|
+
frame is still detached, window `null`, when the context is
|
|
108
|
+
created). Every message from the loaded widget was silently
|
|
109
|
+
dropped: the handshake never completed and the tile sat on its
|
|
110
|
+
placeholder grid. The port now resolves the live window per
|
|
111
|
+
message (`liveWindowPort`).
|
|
112
|
+
- A placement restored from layout storage (page reload) rendered
|
|
113
|
+
an empty dark cell forever: the area element renders its cells
|
|
114
|
+
before discovery has created the widget contexts, and the iframe
|
|
115
|
+
was only adopted at cell creation. Adoption is now idempotent —
|
|
116
|
+
every render adopts a context's iframe into its cell unless it's
|
|
117
|
+
already home (never re-parenting a live iframe, which would
|
|
118
|
+
reload it).
|
|
119
|
+
|
|
10
120
|
## [0.11.2] - 2026-09-21
|
|
11
121
|
|
|
12
122
|
### Added
|
package/package.json
CHANGED
package/plugin/db.js
CHANGED
|
@@ -654,10 +654,14 @@ function dequeueLogbookPending(db, pendingId) {
|
|
|
654
654
|
|
|
655
655
|
/**
|
|
656
656
|
* Lists recent confirmed fixes for the UI (SPEC §14.1 fix points).
|
|
657
|
+
* The webapp's history window filters via `sinceMs` (epoch ms): rows
|
|
658
|
+
* older than the boundary are left out so the chart shows current
|
|
659
|
+
* chartwork, not everything since install.
|
|
657
660
|
*
|
|
658
661
|
* @param {import("node:sqlite").DatabaseSync} db
|
|
659
662
|
* @param {object} [q]
|
|
660
663
|
* @param {number} [q.limit=100]
|
|
664
|
+
* @param {number|null} [q.sinceMs] - include rows at/after this epoch ms
|
|
661
665
|
* @returns {Array<object>} newest-first
|
|
662
666
|
*/
|
|
663
667
|
function listFixes(db, q = {}) {
|
|
@@ -666,9 +670,35 @@ function listFixes(db, q = {}) {
|
|
|
666
670
|
`SELECT fix_id, timestamp, source_type, latitude, longitude,
|
|
667
671
|
estimated_error_radius, confirmed_by, resets_dr_origin,
|
|
668
672
|
derived_from_fix_id
|
|
669
|
-
FROM fixes ORDER BY fix_id DESC LIMIT ?`,
|
|
673
|
+
FROM fixes ${sinceClause(q)} ORDER BY fix_id DESC LIMIT ?`,
|
|
670
674
|
)
|
|
671
|
-
.all(q.limit ?? 100);
|
|
675
|
+
.all(...sinceArgs(q), q.limit ?? 100);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* WHERE fragment for the optional `sinceMs` window filter, shared by
|
|
680
|
+
* the overlay list queries — fixes, LOPs, CPLs and corrections all
|
|
681
|
+
* carry the same ISO-8601 `timestamp` column. Compares via julianday()
|
|
682
|
+
* rather than string comparison: stored timestamps vary in shape
|
|
683
|
+
* (`…00Z` vs `…00.001Z` vs `…00.000Z`), and lexicographic order is
|
|
684
|
+
* wrong across those shapes (a bare `Z` sorts AFTER a fraction).
|
|
685
|
+
*
|
|
686
|
+
* @param {object} q - the query options carrying `sinceMs`
|
|
687
|
+
* @returns {string} "" or the WHERE clause (args from sinceArgs)
|
|
688
|
+
*/
|
|
689
|
+
function sinceClause(q) {
|
|
690
|
+
return q.sinceMs != null ? "WHERE julianday(timestamp) >= julianday(?)" : "";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Positional args matching {@link sinceClause}; callers pass them to
|
|
695
|
+
* .all() BEFORE the limit.
|
|
696
|
+
*
|
|
697
|
+
* @param {object} q
|
|
698
|
+
* @returns {Array<unknown>}
|
|
699
|
+
*/
|
|
700
|
+
function sinceArgs(q) {
|
|
701
|
+
return q.sinceMs != null ? [new Date(q.sinceMs).toISOString()] : [];
|
|
672
702
|
}
|
|
673
703
|
|
|
674
704
|
/**
|
|
@@ -709,10 +739,12 @@ function getCircularPositionLine(db, id) {
|
|
|
709
739
|
|
|
710
740
|
/**
|
|
711
741
|
* Lists persisted lines of position for the UI (SPEC §14.1 LOP overlay).
|
|
742
|
+
* `sinceMs` window-filters as in listFixes.
|
|
712
743
|
*
|
|
713
744
|
* @param {import("node:sqlite").DatabaseSync} db
|
|
714
745
|
* @param {object} [q]
|
|
715
746
|
* @param {number} [q.limit=100]
|
|
747
|
+
* @param {number|null} [q.sinceMs]
|
|
716
748
|
* @returns {Array<object>} newest-first
|
|
717
749
|
*/
|
|
718
750
|
function listLinesOfPosition(db, q = {}) {
|
|
@@ -720,9 +752,9 @@ function listLinesOfPosition(db, q = {}) {
|
|
|
720
752
|
.prepare(
|
|
721
753
|
`SELECT lop_id, timestamp, lop_type, assumed_lat, assumed_lon,
|
|
722
754
|
azimuth_true, intercept_nm, body_or_object, used_in_fix_id
|
|
723
|
-
FROM lines_of_position ORDER BY lop_id DESC LIMIT ?`,
|
|
755
|
+
FROM lines_of_position ${sinceClause(q)} ORDER BY lop_id DESC LIMIT ?`,
|
|
724
756
|
)
|
|
725
|
-
.all(q.limit ?? 100);
|
|
757
|
+
.all(...sinceArgs(q), q.limit ?? 100);
|
|
726
758
|
}
|
|
727
759
|
|
|
728
760
|
/**
|
|
@@ -732,6 +764,7 @@ function listLinesOfPosition(db, q = {}) {
|
|
|
732
764
|
* @param {import("node:sqlite").DatabaseSync} db
|
|
733
765
|
* @param {object} [q]
|
|
734
766
|
* @param {number} [q.limit=100]
|
|
767
|
+
* @param {number|null} [q.sinceMs]
|
|
735
768
|
* @returns {Array<object>} newest-first
|
|
736
769
|
*/
|
|
737
770
|
function listCircularPositionLines(db, q = {}) {
|
|
@@ -739,18 +772,20 @@ function listCircularPositionLines(db, q = {}) {
|
|
|
739
772
|
.prepare(
|
|
740
773
|
`SELECT cpl_id, timestamp, center_lat, center_lon, radius_nm,
|
|
741
774
|
source_object, used_in_fix_id
|
|
742
|
-
FROM circular_position_lines ORDER BY cpl_id DESC LIMIT ?`,
|
|
775
|
+
FROM circular_position_lines ${sinceClause(q)} ORDER BY cpl_id DESC LIMIT ?`,
|
|
743
776
|
)
|
|
744
|
-
.all(q.limit ?? 100);
|
|
777
|
+
.all(...sinceArgs(q), q.limit ?? 100);
|
|
745
778
|
}
|
|
746
779
|
|
|
747
780
|
/**
|
|
748
781
|
* Lists recent snap-to-fix corrections for the UI (SPEC §9.3/§14.1 —
|
|
749
782
|
* dashed vector from pre-snap ghost position to confirmed fix).
|
|
783
|
+
* `sinceMs` window-filters as in listFixes.
|
|
750
784
|
*
|
|
751
785
|
* @param {import("node:sqlite").DatabaseSync} db
|
|
752
786
|
* @param {object} [q]
|
|
753
787
|
* @param {number} [q.limit=20]
|
|
788
|
+
* @param {number|null} [q.sinceMs]
|
|
754
789
|
* @returns {Array<object>} newest-first
|
|
755
790
|
*/
|
|
756
791
|
function listCorrections(db, q = {}) {
|
|
@@ -759,9 +794,9 @@ function listCorrections(db, q = {}) {
|
|
|
759
794
|
`SELECT correction_id, timestamp, dr_lat, dr_lon, fix_lat, fix_lon,
|
|
760
795
|
deviation_nm, deviation_bearing, dr_elapsed_seconds,
|
|
761
796
|
sail_state, sea_state
|
|
762
|
-
FROM dr_corrections ORDER BY correction_id DESC LIMIT ?`,
|
|
797
|
+
FROM dr_corrections ${sinceClause(q)} ORDER BY correction_id DESC LIMIT ?`,
|
|
763
798
|
)
|
|
764
|
-
.all(q.limit ?? 20);
|
|
799
|
+
.all(...sinceArgs(q), q.limit ?? 20);
|
|
765
800
|
}
|
|
766
801
|
|
|
767
802
|
// -------------------------------------------------------------------------
|
package/plugin/index.js
CHANGED
|
@@ -323,6 +323,18 @@ const DEFAULT_CONFIG = {
|
|
|
323
323
|
sustainS: 10,
|
|
324
324
|
clearS: 10,
|
|
325
325
|
},
|
|
326
|
+
/**
|
|
327
|
+
* Trip-boundary hysteresis (SPEC §9.2): how long navigation.state
|
|
328
|
+
* must STAY underway (leaving moored/anchored) before the transition
|
|
329
|
+
* counts as a trip start and resets the trip log. Longer than the
|
|
330
|
+
* sensor-health windows on purpose — untying, anchor aweigh and
|
|
331
|
+
* lunch stops all bounce the state for a few minutes — long enough
|
|
332
|
+
* that only real departure zeroes the trip log.
|
|
333
|
+
*/
|
|
334
|
+
tripBoundary: {
|
|
335
|
+
sustainS: 300,
|
|
336
|
+
clearS: 300,
|
|
337
|
+
},
|
|
326
338
|
/**
|
|
327
339
|
* §3.1 inertial-polar fallback (work doc #18): running-average window
|
|
328
340
|
* and staleness cutoff for the polar speed — both the
|
|
@@ -610,6 +622,27 @@ module.exports = (app) => {
|
|
|
610
622
|
/** @type {number|null} monotonic seconds counter for the training loop */
|
|
611
623
|
let clockS = 0;
|
|
612
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Trip boundary tracking (SPEC §9.2): the debounced moored/anchored
|
|
627
|
+
* → underway transition resets the trip log and records the trip
|
|
628
|
+
* start. Debounced symmetric hysteresis (createFlagState/flagTick)
|
|
629
|
+
* so a flapping autostate source can't zero the trip log spuriously.
|
|
630
|
+
* `raw` is this tick's underway verdict; the flag RAISES on the
|
|
631
|
+
* sustained underway edge and never drops mid-trip — the boundary
|
|
632
|
+
* event is the transition INTO a trip, not the trip's end.
|
|
633
|
+
* @type {ReturnType<typeof createFlagState>|null}
|
|
634
|
+
*/
|
|
635
|
+
let tripFlag = null;
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Trip start timestamp (ms) of the current/last trip — persisted in
|
|
639
|
+
* dr_state_store so a mid-trip restart keeps the boundary (and the
|
|
640
|
+
* webapp's history window: max(7 days, since trip start)). Null when
|
|
641
|
+
* no boundary has ever been observed.
|
|
642
|
+
* @type {number|null}
|
|
643
|
+
*/
|
|
644
|
+
let tripStartMs = null;
|
|
645
|
+
|
|
613
646
|
/** @type {ReturnType<typeof createDivergenceState>|null} §7.3 divergence monitor */
|
|
614
647
|
let divergence = null;
|
|
615
648
|
|
|
@@ -859,6 +892,10 @@ module.exports = (app) => {
|
|
|
859
892
|
...DEFAULT_CONFIG.sensorHealth,
|
|
860
893
|
...(opts.sensorHealth ?? {}),
|
|
861
894
|
};
|
|
895
|
+
config.tripBoundary = {
|
|
896
|
+
...DEFAULT_CONFIG.tripBoundary,
|
|
897
|
+
...(opts.tripBoundary ?? {}),
|
|
898
|
+
};
|
|
862
899
|
config.logbook = {
|
|
863
900
|
...DEFAULT_CONFIG.logbook,
|
|
864
901
|
...(opts.logbook ?? {}),
|
|
@@ -955,6 +992,14 @@ module.exports = (app) => {
|
|
|
955
992
|
shadowGate = deps.createPublishGate({
|
|
956
993
|
everyTicks: config.publish.everyTicks,
|
|
957
994
|
});
|
|
995
|
+
// Trip boundary tracking (SPEC §9.2): fresh debounce state per
|
|
996
|
+
// start; the trip start itself survives restarts via dr_state_store
|
|
997
|
+
// so the webapp's history window keeps the trip boundary across a
|
|
998
|
+
// mid-trip restart.
|
|
999
|
+
tripFlag = deps.createFlagState();
|
|
1000
|
+
const savedTripStartMs = Number(deps.getState(db, "dr_trip_start_ms"));
|
|
1001
|
+
tripStartMs = Number.isFinite(savedTripStartMs) ? savedTripStartMs : null;
|
|
1002
|
+
|
|
958
1003
|
polarState = deps.createPolarSpeedState();
|
|
959
1004
|
polarModel = null;
|
|
960
1005
|
polarLoadedId = null;
|
|
@@ -1461,6 +1506,30 @@ module.exports = (app) => {
|
|
|
1461
1506
|
const navState = deltaState.get("navigation.state");
|
|
1462
1507
|
const underway = navState !== "anchored" && navState !== "moored";
|
|
1463
1508
|
|
|
1509
|
+
// Trip boundary (SPEC §9.2): a sustained moored/anchored → underway
|
|
1510
|
+
// transition starts a trip — resets the trip log and records the
|
|
1511
|
+
// start. The flag raises once on the sustained edge and never drops
|
|
1512
|
+
// (a trip ends at the NEXT raise, not on the underway→moored edge),
|
|
1513
|
+
// so an afternoon of anchoring for lunch doesn't zero the log. Only
|
|
1514
|
+
// a reset that actually moves the count is recorded — restarting
|
|
1515
|
+
// underway (no prior observed boundary) must not fabricate one.
|
|
1516
|
+
const tripEdge = deps.flagTick(
|
|
1517
|
+
tripFlag,
|
|
1518
|
+
underway,
|
|
1519
|
+
config.tickIntervalMs / 1000,
|
|
1520
|
+
config.tripBoundary,
|
|
1521
|
+
);
|
|
1522
|
+
if (tripEdge.transition === "raise") {
|
|
1523
|
+
if (engine.tripLogNm > 0) {
|
|
1524
|
+
engine.resetTrip();
|
|
1525
|
+
tripStartMs = Date.now();
|
|
1526
|
+
deps.setState(db, "dr_trip_start_ms", String(tripStartMs));
|
|
1527
|
+
app.debug(
|
|
1528
|
+
`Trip boundary: navigation.state sustained underway — trip log reset, trip started ${new Date(tripStartMs).toISOString()}`,
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1464
1533
|
// GPS-derived motion (independent of the water-track sensors):
|
|
1465
1534
|
// updated every tick, used to detect "idle but making way" below.
|
|
1466
1535
|
updateGpsMotion(gps);
|
|
@@ -2725,6 +2794,11 @@ module.exports = (app) => {
|
|
|
2725
2794
|
}
|
|
2726
2795
|
deps.setState(db, "dr_log_nm", String(engine.logNm));
|
|
2727
2796
|
deps.setState(db, "dr_trip_log_nm", String(engine.tripLogNm));
|
|
2797
|
+
// Trip boundary (SPEC §9.2) survives restarts: persisted at the
|
|
2798
|
+
// boundary tick AND on every flush (the boundary itself can't be
|
|
2799
|
+
// re-derived — a restart mid-trip sees only "underway").
|
|
2800
|
+
if (tripStartMs != null)
|
|
2801
|
+
deps.setState(db, "dr_trip_start_ms", String(tripStartMs));
|
|
2728
2802
|
deps.setState(db, "dr_log_since_origin", String(engine.logNmSinceOrigin));
|
|
2729
2803
|
// Sea trial 2026-09-06: elapsed time drives the current-knowledge
|
|
2730
2804
|
// term of the uncertainty cone — a restart must not zero it, or the
|
|
@@ -2746,6 +2820,25 @@ module.exports = (app) => {
|
|
|
2746
2820
|
|
|
2747
2821
|
// --- REST API ----------------------------------------------------------
|
|
2748
2822
|
|
|
2823
|
+
/**
|
|
2824
|
+
* Parses the optional `since` query parameter (ISO-8601 string or
|
|
2825
|
+
* epoch ms) of the overlay endpoints into the db layer's `sinceMs`
|
|
2826
|
+
* option. Invalid values are ignored (unfiltered) — the window is a
|
|
2827
|
+
* display concern, never a reason to 4xx the chart.
|
|
2828
|
+
*
|
|
2829
|
+
* @param {object} req - Express request
|
|
2830
|
+
* @returns {{sinceMs?: number}}
|
|
2831
|
+
*/
|
|
2832
|
+
function sinceOpt(req) {
|
|
2833
|
+
const raw = req.query?.since;
|
|
2834
|
+
if (raw == null || raw === "") return {};
|
|
2835
|
+
const n = Number(raw);
|
|
2836
|
+
if (Number.isFinite(n) && n > 0) return { sinceMs: n };
|
|
2837
|
+
const t = Date.parse(String(raw));
|
|
2838
|
+
if (Number.isFinite(t)) return { sinceMs: t };
|
|
2839
|
+
return {};
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2749
2842
|
/**
|
|
2750
2843
|
* Registers REST routes on the plugin router.
|
|
2751
2844
|
*
|
|
@@ -2766,6 +2859,9 @@ module.exports = (app) => {
|
|
|
2766
2859
|
origin: engine.origin,
|
|
2767
2860
|
logNm: engine.logNm,
|
|
2768
2861
|
tripLogNm: engine.tripLogNm,
|
|
2862
|
+
// SPEC §9.2 trip boundary (null when none observed yet): the
|
|
2863
|
+
// webapp's history window is max(7 days, since trip start).
|
|
2864
|
+
tripStartMs,
|
|
2769
2865
|
elapsedSinceOriginS: engine.elapsedSinceOriginS,
|
|
2770
2866
|
underwaySinceOriginS: engine.underwaySinceOriginS,
|
|
2771
2867
|
binCount: matrix.count(),
|
|
@@ -2794,8 +2890,9 @@ module.exports = (app) => {
|
|
|
2794
2890
|
});
|
|
2795
2891
|
|
|
2796
2892
|
/**
|
|
2797
|
-
* GET /fixes —
|
|
2798
|
-
*
|
|
2893
|
+
* GET /fixes — confirmed fixes for the map overlay (SPEC §14.1 fix
|
|
2894
|
+
* points). `limit` caps the result (default 100); `since` (ISO-8601
|
|
2895
|
+
* or epoch ms) window-filters to the webapp's history window.
|
|
2799
2896
|
*/
|
|
2800
2897
|
router.get("/fixes", (req, res) => {
|
|
2801
2898
|
if (!engine || !db) {
|
|
@@ -2806,13 +2903,14 @@ module.exports = (app) => {
|
|
|
2806
2903
|
1,
|
|
2807
2904
|
Math.min(1000, Number(req.query?.limit) || 100),
|
|
2808
2905
|
);
|
|
2809
|
-
res.json({ fixes: deps.listFixes(db, { limit }) });
|
|
2906
|
+
res.json({ fixes: deps.listFixes(db, { limit, ...sinceOpt(req) }) });
|
|
2810
2907
|
});
|
|
2811
2908
|
|
|
2812
2909
|
/**
|
|
2813
2910
|
* GET /observations — persisted LOPs and CPLs for the map overlay
|
|
2814
2911
|
* (SPEC §14.1 geometric primitives). Unused/unresolved observations
|
|
2815
2912
|
* (used_in_fix_id IS NULL) are marked so the UI can emphasize them.
|
|
2913
|
+
* `since` window-filters to the webapp's history window.
|
|
2816
2914
|
*/
|
|
2817
2915
|
router.get("/observations", (req, res) => {
|
|
2818
2916
|
if (!engine || !db) {
|
|
@@ -2823,15 +2921,17 @@ module.exports = (app) => {
|
|
|
2823
2921
|
1,
|
|
2824
2922
|
Math.min(1000, Number(req.query?.limit) || 100),
|
|
2825
2923
|
);
|
|
2924
|
+
const since = sinceOpt(req);
|
|
2826
2925
|
res.json({
|
|
2827
|
-
lops: deps.listLinesOfPosition(db, { limit }),
|
|
2828
|
-
cpls: deps.listCircularPositionLines(db, { limit }),
|
|
2926
|
+
lops: deps.listLinesOfPosition(db, { limit, ...since }),
|
|
2927
|
+
cpls: deps.listCircularPositionLines(db, { limit, ...since }),
|
|
2829
2928
|
});
|
|
2830
2929
|
});
|
|
2831
2930
|
|
|
2832
2931
|
/**
|
|
2833
2932
|
* GET /corrections — recent snap-to-fix corrections for the dashed
|
|
2834
|
-
* vector overlay (SPEC §9.3, §14.1).
|
|
2933
|
+
* vector overlay (SPEC §9.3, §14.1). `since` window-filters to the
|
|
2934
|
+
* webapp's history window.
|
|
2835
2935
|
*/
|
|
2836
2936
|
router.get("/corrections", (req, res) => {
|
|
2837
2937
|
if (!engine || !db) {
|
|
@@ -2839,7 +2939,9 @@ module.exports = (app) => {
|
|
|
2839
2939
|
return;
|
|
2840
2940
|
}
|
|
2841
2941
|
const limit = Math.max(1, Math.min(200, Number(req.query?.limit) || 20));
|
|
2842
|
-
res.json({
|
|
2942
|
+
res.json({
|
|
2943
|
+
corrections: deps.listCorrections(db, { limit, ...sinceOpt(req) }),
|
|
2944
|
+
});
|
|
2843
2945
|
});
|
|
2844
2946
|
|
|
2845
2947
|
/**
|