@meri-imperiumi/signalk-passage-briefing 0.2.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/.editorconfig +5 -0
- package/.github/workflows/publish.yml +34 -0
- package/.github/workflows/signalk-ci.yml +11 -0
- package/.github/workflows/test.yml +21 -0
- package/CHANGELOG.md +423 -0
- package/README.md +86 -0
- package/SPEC.md +488 -0
- package/bin/backfill-report.js +218 -0
- package/bin/backtest-cli.js +132 -0
- package/biome.json +6 -0
- package/doc/here-brief.png +0 -0
- package/package.json +48 -0
- package/plugin/backtest.js +742 -0
- package/plugin/brief-ext.js +173 -0
- package/plugin/bulletin-engine.js +746 -0
- package/plugin/bulletin-source.js +150 -0
- package/plugin/celestial-source.js +417 -0
- package/plugin/fetch-engine.js +717 -0
- package/plugin/history-backfill.js +540 -0
- package/plugin/index.js +1479 -0
- package/plugin/logbook-source.js +463 -0
- package/plugin/notes-publisher.js +228 -0
- package/plugin/notes-store.js +241 -0
- package/plugin/raster-convert.js +168 -0
- package/plugin/sails-configuration.js +111 -0
- package/plugin/spool-watcher.js +218 -0
- package/plugin/sqlite-db.js +378 -0
- package/plugin/state-machine.js +249 -0
- package/plugin/statustilesexamples.js +101 -0
- package/plugin/synoptic-map.json +45 -0
- package/plugin/synoptic-source.js +227 -0
- package/plugin/zone-source.js +339 -0
- package/public/app.js +18 -0
- package/public/brief-ext-model.js +64 -0
- package/public/brief-ext-widget.html +14 -0
- package/public/brief-ext-widget.js +294 -0
- package/public/components/backfill-controls.js +100 -0
- package/public/components/comfort-info.js +94 -0
- package/public/components/conditions-here.js +234 -0
- package/public/components/horizon-sparkline.js +77 -0
- package/public/components/models.mjs +372 -0
- package/public/components/passage-outlook.js +368 -0
- package/public/components/sk-api.js +146 -0
- package/public/components/sk-base-css.js +177 -0
- package/public/components/strategic-outlook.js +248 -0
- package/public/components/synoptic-chart.js +48 -0
- package/public/components/tactical-dashboard.js +172 -0
- package/public/css/visuals.css +292 -0
- package/public/gmdss-zones-min.json +287 -0
- package/public/icon.png +0 -0
- package/public/index.html +13 -0
- package/public/polar.mjs +303 -0
- package/public/route-sim.mjs +750 -0
- package/public/sereno-physics.mjs +592 -0
- package/public/tack-gybe.js +174 -0
- package/public/vendor/plotterext-bus/LICENSE +21 -0
- package/public/vendor/plotterext-bus/README.md +17 -0
- package/public/vendor/plotterext-bus/chunk-4W6N34SD.js +333 -0
- package/public/vendor/plotterext-bus/chunk-7XRFPDQL.js +263 -0
- package/public/vendor/plotterext-bus/chunk-RED55KML.js +117 -0
- package/public/vendor/plotterext-bus/extension.js +29 -0
- package/public/vendor/plotterext-bus/host.js +28 -0
- package/public/vendor/utif/LICENSE +21 -0
- package/public/vendor/utif/UTIF.js +1171 -0
- package/public/worker.js +35 -0
- package/status-tiles-examples.json +55 -0
- package/tests/backtest.test.js +461 -0
- package/tests/brief-ext.test.js +194 -0
- package/tests/bulletin-engine.test.js +361 -0
- package/tests/celestial-source.test.js +265 -0
- package/tests/fetch-engine.test.js +297 -0
- package/tests/history-backfill.test.js +341 -0
- package/tests/logbook-source.test.js +324 -0
- package/tests/notes-publisher.test.js +161 -0
- package/tests/notes-store.test.js +138 -0
- package/tests/openmeteo-mock.js +100 -0
- package/tests/plugin.test.js +1344 -0
- package/tests/route-sim.test.js +533 -0
- package/tests/sereno-physics.test.js +333 -0
- package/tests/sqlite-db.test.js +164 -0
- package/tests/state-machine.test.js +182 -0
- package/tests/statustilesexamples.test.js +86 -0
- package/tests/synoptic-source.test.js +195 -0
- package/tests/tack-gybe.test.js +211 -0
- package/tests/webapp.test.js +257 -0
- package/tests/zone-source.test.js +226 -0
package/.editorconfig
ADDED
|
@@ -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@v3.0.2
|
|
17
|
+
- uses: actions/setup-node@v3.1.1
|
|
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@v3.0.2
|
|
29
|
+
- uses: actions/setup-node@v3.1.1
|
|
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,21 @@
|
|
|
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
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
node-version: [24.x]
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v3.0.2
|
|
14
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
15
|
+
uses: actions/setup-node@v3.1.1
|
|
16
|
+
with:
|
|
17
|
+
node-version: ${{ matrix.node-version }}
|
|
18
|
+
- run: npm install
|
|
19
|
+
- run: npm test
|
|
20
|
+
env:
|
|
21
|
+
CI: true
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.2.0] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- The plotter widget HTML and JS are cache-busted with the plugin
|
|
10
|
+
version, so widget updates actually reach the host's iframe —
|
|
11
|
+
a stale cached copy was showing a removed Open button.
|
|
12
|
+
|
|
13
|
+
- The plotter tile drops its tap-to-open attempts entirely: the host
|
|
14
|
+
sandbox blocks pop-ups AND top-frame navigation (even
|
|
15
|
+
user-activated — verified on board, `allow-top-navigation-by-user-
|
|
16
|
+
activation` is not set), so the only "escape" was crushing the
|
|
17
|
+
webapp into the 1×1 frame. The tile is a pure mini summary; the
|
|
18
|
+
full briefing opens from the host app list or a host panel, and
|
|
19
|
+
the v1 open-panel request remains the spec-level fix.
|
|
20
|
+
|
|
21
|
+
- Warning notes are placed at the point of the warning area nearest
|
|
22
|
+
the vessel (clamped into the bounding box, nearest vertex for
|
|
23
|
+
axis lines) instead of the box center — a quarter-ocean area's
|
|
24
|
+
center sits a thousand miles from the crew, outside any useful
|
|
25
|
+
near-me query radius on resources/notes.
|
|
26
|
+
- Notes are schema-conservative (title/description/position/url/
|
|
27
|
+
mimeType/properties/timestamp only) — unknown top-level fields are
|
|
28
|
+
the classic resources write rejection — and provenance rides in
|
|
29
|
+
`properties.sourcePlugin`. Notes write failures are logged with
|
|
30
|
+
the server's reason and retried against the signalk-resources
|
|
31
|
+
provider id.
|
|
32
|
+
|
|
33
|
+
- The plotter tile is now a mini summary: prominent colored comfort
|
|
34
|
+
tier, route, age with a STALE marker when the briefing is past its
|
|
35
|
+
window, and an `Open ↗` link (`target="_top"`) to the full webapp —
|
|
36
|
+
replacing tap handling that could only ever navigate the widget's
|
|
37
|
+
own 1×1 frame inside the host sandbox (pop-ups and top navigation
|
|
38
|
+
are both blocked there, verified on board).
|
|
39
|
+
|
|
40
|
+
- The plotter tile received no values when its iframe connected
|
|
41
|
+
after the last compile: the tile paths are now re-emitted on every
|
|
42
|
+
60s cron tick (delta cost is five small values), the widget
|
|
43
|
+
subscribes to the new stale/ageHours paths too, and its tap tries
|
|
44
|
+
pop-up, then the parent window, then takes over its own frame —
|
|
45
|
+
whatever the host sandbox permits.
|
|
46
|
+
|
|
47
|
+
- The BoM radiofax hosts stall connections from the boat outright
|
|
48
|
+
(no response, fetch abort): zones 10 and 14 are parked under an
|
|
49
|
+
`_unverified` map section so refreshes no longer stall on doomed
|
|
50
|
+
fetches — zone 14 rides chartless until a reachable South Pacific
|
|
51
|
+
product is verified against
|
|
52
|
+
[otherfax.txt](https://tgftp.nws.noaa.gov/fax/otherfax.txt).
|
|
53
|
+
- Synoptic fetch timeout capped at 8s per candidate mirror.
|
|
54
|
+
|
|
55
|
+
- A half-migrated synoptic-source candidate-list change shipped a
|
|
56
|
+
`pick.urls is not iterable` error that failed every here refresh on
|
|
57
|
+
board. Consistent again, with per-candidate mirrors and loud
|
|
58
|
+
failure records (zone, url, error) instead of silent drops.
|
|
59
|
+
- The published tile comfort tier is computed with the webapp's own
|
|
60
|
+
model (`models.mjs` hereHourly) and stored on the payload, which
|
|
61
|
+
the conditions-here view also reads — one implementation, one
|
|
62
|
+
cached value, no drift between tile and view.
|
|
63
|
+
- Tile tap: when the host sandbox blocks the pop-up entirely, the
|
|
64
|
+
widget navigates its own frame to the brief webapp as the final
|
|
65
|
+
fallback.
|
|
66
|
+
|
|
67
|
+
- Bulletin geography parsing, corrected against a live NFFN
|
|
68
|
+
bulletin: two-coordinate trough axes now parse as open lines (the
|
|
69
|
+
NFFN style omits `TO` separators, so `10S 160E 12S 166E` produced
|
|
70
|
+
no geometry at all and the discard rule passed the block
|
|
71
|
+
unfiltered); `WITHIN 100 NAUTICAL MILES` now matches the band
|
|
72
|
+
regex (only `NM` did); `SOUTH OF 10S` built its box on the wrong
|
|
73
|
+
side (lat −10..90 instead of −90..−10), dropping the area block
|
|
74
|
+
that contained the vessel while keeping far-away bands. Wrapped
|
|
75
|
+
chains unfold before band expansion. Regression test uses the live
|
|
76
|
+
bulletin text.
|
|
77
|
+
- The SBDB comet query negotiates its field list against the live
|
|
78
|
+
endpoint (the documented `r` field is rejected for `sb-kind=c`);
|
|
79
|
+
when current distances are unavailable the parse falls back to a
|
|
80
|
+
perihelion-brightness estimate, labelled as such in Sky Notes.
|
|
81
|
+
|
|
82
|
+
- The forecast request listed `precipitable_water`, which Open-Meteo
|
|
83
|
+
rejects (`400 Cannot initialize ... Variable`) — every forecast
|
|
84
|
+
fetch failed with it, so nothing ever cached on board. Removed;
|
|
85
|
+
the upper-air fields (CAPE, K-index, RH/wind at pressure levels)
|
|
86
|
+
are unaffected. Doc #3 Phase 2's precipitable-water sky gate needs
|
|
87
|
+
a different source when Phase 2 lands.
|
|
88
|
+
- `GET /api/briefing` returns `200` with an empty payload
|
|
89
|
+
(`{mode, payload: null, cached: false}`) when nothing is cached
|
|
90
|
+
instead of a 404, so the webapp renders its refresh strip rather
|
|
91
|
+
than logging a failed request.
|
|
92
|
+
|
|
93
|
+
- The webapp's "Fetch now" button sent a GET to the POST-only
|
|
94
|
+
`/api/briefing/refresh` route (fetchJson had no method option), so
|
|
95
|
+
the button 404ed against a real server; it now issues a POST.
|
|
96
|
+
|
|
97
|
+
- On-board first-run issues: the webapp now fetches its REST API from
|
|
98
|
+
the absolute `/plugins/signalk-passage-briefing/api` mount (SK v2
|
|
99
|
+
serves the webapp itself under `/@<scope>/<name>/`, where the old
|
|
100
|
+
relative `api/…` base resolved against the wrong root and every
|
|
101
|
+
route 404ed — same mount as the energy-predictor webapp on this
|
|
102
|
+
server); refresh failures land in the plugin *error* state instead
|
|
103
|
+
of the status line; and failed internet fetches now include the
|
|
104
|
+
response body (e.g. Open-Meteo's `reason`) in the error so the
|
|
105
|
+
status says why, not just which URL returned which code.
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
|
|
109
|
+
- `GET /api/brief-meta` serves the current tile view (comfort tier,
|
|
110
|
+
route, generatedAt, staleness, age hours, hasNew) — the plotter
|
|
111
|
+
widget pulls it on connect so the mini summary is populated right
|
|
112
|
+
away instead of waiting for the next delta emission.
|
|
113
|
+
|
|
114
|
+
- The plugin registers as the server's `notes` resource provider
|
|
115
|
+
(there is none by default on SK v2, so writes were failing and
|
|
116
|
+
queries returned nothing). The provider is read-only and serves
|
|
117
|
+
only the metarea warnings the bulletin pipeline publishes — other
|
|
118
|
+
clients' notes belong to their own future providers — with
|
|
119
|
+
position/distance/bbox/limit query filtering per the resources
|
|
120
|
+
query docs, durable in the plugin data dir. The publisher writes
|
|
121
|
+
into the store directly and prunes expired warnings.
|
|
122
|
+
- METAREA warnings as Signal K Notes (work doc #12): after each
|
|
123
|
+
bulletin filter pass the placeable blocks are published as
|
|
124
|
+
georeferenced `resources/notes` — title from the first sentence,
|
|
125
|
+
verbatim description, representative position (antimeridian-safe),
|
|
126
|
+
category/subject/zone/source properties, timestamp from the
|
|
127
|
+
bulletin. Notes link the cached synoptic chart when one exists for
|
|
128
|
+
the zone. Ids are content-addressed (republish updates in place),
|
|
129
|
+
blocks that drop out of the filtered set have their notes deleted,
|
|
130
|
+
start re-syncs manifest vs server, and a
|
|
131
|
+
`publish_metarea_notes` toggle (default on) clears owned notes
|
|
132
|
+
when disabled. Publishing is local-only, never internet-gated.
|
|
133
|
+
|
|
134
|
+
- Status Tiles integration (work doc #13): the tile paths gain
|
|
135
|
+
`navigation.briefing.stale` (boolean freshness verdict — 3h here,
|
|
136
|
+
26h for route briefings — recomputed on every emission) and
|
|
137
|
+
`navigation.briefing.ageHours`; the ticker re-emits the tile paths
|
|
138
|
+
every 5th tick so widgets connecting after the last compile still
|
|
139
|
+
receive values. A read-only `statusTileExamples` resource provider
|
|
140
|
+
ships a copyable Comfort tile (tier colors, amber on stale,
|
|
141
|
+
Age/Route footer).
|
|
142
|
+
- The conditions-here view renders warnings, the synoptic chart and
|
|
143
|
+
the celestial-events card as siblings instead of cards nested
|
|
144
|
+
inside the conditions card.
|
|
145
|
+
- Plotter tile tap: pop-up first, then navigate the widget frame
|
|
146
|
+
itself — the host sandbox blocks pop-ups, and the probe-then-open
|
|
147
|
+
dance only added failure modes.
|
|
148
|
+
|
|
149
|
+
- Zone 14 synoptic chart repointed to the BoM difacs web host
|
|
150
|
+
(www.bom.gov.au/difacs/IDX0032/IDX0532.gif) after the anon FTP
|
|
151
|
+
hosts proved unreachable from the boat; GIF charts are cached and
|
|
152
|
+
served as-is (browsers render them natively), so the omggif
|
|
153
|
+
decoder is not needed and was removed again. TIFF charts still
|
|
154
|
+
convert to grayscale PNG via the vendored UTIF.
|
|
155
|
+
|
|
156
|
+
- Styling pass per the house UI spec: shadow-DOM components carried
|
|
157
|
+
no panel styling (document-level visuals.css cannot pierce a
|
|
158
|
+
shadow root), so the webapp rendered as unstyled text. A shared
|
|
159
|
+
`sk-base-css.js` subset — panels with 2px corner brackets, theme
|
|
160
|
+
tints, tracked uppercase headings, hardware buttons/selects/inputs
|
|
161
|
+
with 48px touch targets, data tables, consoles — now precedes every
|
|
162
|
+
component's own styles. Palette custom properties still inherit
|
|
163
|
+
from the host page for day/night reactivity.
|
|
164
|
+
|
|
165
|
+
- The plotter tile shows the current comfort tier:
|
|
166
|
+
`navigation.briefing.comfort` is published alongside the other tile
|
|
167
|
+
paths, computed at compile time from the payload's first forecast
|
|
168
|
+
step through the Sereno comfort model at SOG 0 (same math as the
|
|
169
|
+
conditions-here view), seeded from the cache after a restart.
|
|
170
|
+
- Bulletin blocks now carry the extracted `geometry` (bbox
|
|
171
|
+
coordinates, polygon/axis-line rings with the WITHIN-nm buffer)
|
|
172
|
+
alongside the geometry type, and `BETWEEN 165W AND 135W` longitude
|
|
173
|
+
pairs parse as area bounds — the NFFN swell statement east of a
|
|
174
|
+
vessel at 174W is now correctly discarded.
|
|
175
|
+
- The tile's tap opens the brief webapp by probing the SK v2 app
|
|
176
|
+
mount (`/@<scope>/<name>/`) before the v1 `/plugins/` mount.
|
|
177
|
+
|
|
178
|
+
- Synoptic surface-analysis chart in the strategic screen (work doc
|
|
179
|
+
#11): a bundled `synoptic-map.json` maps GMDSS zone integers to
|
|
180
|
+
per-agency chart URLs (NOAA TGFTP backbone, BoM radiofax for the
|
|
181
|
+
Tasman/South Pacific) with 00Z/12Z time-aware selection and static
|
|
182
|
+
entries; charts download on the same online-transition gate as the
|
|
183
|
+
weather, convert from TIFF to compact grayscale PNG via the
|
|
184
|
+
vendored pure-JS UTIF decoder plus a hand-rolled zlib PNG encoder
|
|
185
|
+
(no native image dependency), cache as `synoptic-<zone>.png` with
|
|
186
|
+
metadata, and skip re-downloads for the already-cached valid hour.
|
|
187
|
+
`GET /api/synoptic` serves the position-zone chart; the strategic
|
|
188
|
+
screen renders it in a figure that inverts for the night palette
|
|
189
|
+
and stays omitted when nothing is cached. Zones without a usable
|
|
190
|
+
chart (e.g. the unverified Chile source) are absent from the map
|
|
191
|
+
and a logged no-op. `biome.json` added to keep the vendored decoder
|
|
192
|
+
out of formatting.
|
|
193
|
+
|
|
194
|
+
- An ℹ️ next to the comfort tier (tactical dashboard and
|
|
195
|
+
conditions-here) expanding an explainer for the Sereno comfort
|
|
196
|
+
scale — what each tier means and the apparent-wind / vertical-motion
|
|
197
|
+
lines that drop it — with the current tier highlighted. The scale
|
|
198
|
+
data (`COMFORT_SCALE_INFO`) lives in the shared physics module.
|
|
199
|
+
|
|
200
|
+
- Logbook backfill controls in the webapp root (collapsed by
|
|
201
|
+
default, available in every mode including conditions-here): runs
|
|
202
|
+
`POST /api/backfill` (optional from/to date range) from the
|
|
203
|
+
browser session and shows the learned summary. The route is
|
|
204
|
+
auth-gated server-side, so the webapp session is the interface;
|
|
205
|
+
the backfill report stays available as `bin/backfill-report.js`.
|
|
206
|
+
|
|
207
|
+
- Zone-targeted bulletin sources wired end to end (work doc #9): the
|
|
208
|
+
NOAA TGFTP fast path now activates through a configurable
|
|
209
|
+
station→zone table (`bulletin_stations`, seeded with the doc's
|
|
210
|
+
FQPS01/NFFN for NAVAREA XIV) fetched before the GMDSS portal
|
|
211
|
+
fallback; the UKHO Admiralty MSI JSON is fetched per resolved zone
|
|
212
|
+
(`msi.admiralty.co.uk/api/Warnings/Area/{zone}`), stored raw and
|
|
213
|
+
parsed tolerantly (`parseUkhoWarnings` — canonical shape
|
|
214
|
+
fixture-tested, response shape and `[lat, lon]` coordinate order
|
|
215
|
+
need one on-board verification) into structured blocks that skip
|
|
216
|
+
the regex pipeline entirely. `/api/bulletin` and the payload
|
|
217
|
+
`metareaBulletin` now merge track-filtered blocks from the newest
|
|
218
|
+
cached entries across ingestion paths (text and structured),
|
|
219
|
+
deduped by text. `/api/bulletin/refresh` performs the zone-targeted
|
|
220
|
+
pull against the vessel position before the configured extra feeds.
|
|
221
|
+
|
|
222
|
+
- Plotter-extension brief tile (work doc #8): the plugin registers a
|
|
223
|
+
read-only `plotterExtensions` resource provider (API v1 manifest,
|
|
224
|
+
1x1 iframe widget, `whileEnabled`) and serves the widget assets
|
|
225
|
+
from a public, non-admin-gated `/plotterext/<id>/` prefix (minimal
|
|
226
|
+
static handler, traversal-guarded). The plugin publishes
|
|
227
|
+
`navigation.briefing.generatedAt` / `.route` / `.hasNew` flat
|
|
228
|
+
paths over the Signal K stream — the tile is bus-only — seeded
|
|
229
|
+
from the cache after a restart; a `signalk.put` to
|
|
230
|
+
`navigation.briefing.acknowledgedAt` (persisted across restarts)
|
|
231
|
+
clears the NEW badge. The widget state machine lives in the pure
|
|
232
|
+
`brief-ext-model.js` (muted / available / new with age string);
|
|
233
|
+
tap opens the brief webapp in a new browser context (documented
|
|
234
|
+
fallback until the v1 widget→host open-panel request is settled);
|
|
235
|
+
long-press asks the host for config/remove; night mode supported
|
|
236
|
+
when offered. `signalk-plotterext-bus` 0.11.0 dist is vendored
|
|
237
|
+
under `public/vendor/plotterext-bus/` (MIT), same policy as the
|
|
238
|
+
dead-reckoning plugin. The webapp gains an `?embed=1` compact
|
|
239
|
+
chrome (header hidden) for plotter dialogs.
|
|
240
|
+
|
|
241
|
+
- Celestial & space weather, Phase 1 (work doc #3):
|
|
242
|
+
`plugin/celestial-source.js` fetches the NOAA SWPC planetary
|
|
243
|
+
K-index forecast and the JPL Small-Body Database comet query during
|
|
244
|
+
the internet window and attaches coarse-gated `spaceEvents` to both
|
|
245
|
+
briefing payloads (route departure position and here). Aurora
|
|
246
|
+
alerts require a predicted Kp ≥ 5, a magnetic latitude equatorward
|
|
247
|
+
reach matching the Kp (dipole approximation, ~65° at Kp 5 down to
|
|
248
|
+
~45° at Kp 9), and local night at the vessel — "Aurora possible:
|
|
249
|
+
Kp 7 predicted tonight. Look south." Naked-eye comets (apparent
|
|
250
|
+
magnitude from M1/K1/r/Δ brighter than 6.0) surface as strategic
|
|
251
|
+
sky notes. Each source degrades independently; blocked hosts cost
|
|
252
|
+
nothing. Tactical banners and a strategic "Sky Notes" block render
|
|
253
|
+
them; the conditions-here view lists them in its events block.
|
|
254
|
+
|
|
255
|
+
- Planned tacks & gybes (work doc #5): the simulation records per-step
|
|
256
|
+
heading, wind direction and distance made good, and a pure
|
|
257
|
+
`tack-gybe.js` module classifies signed-TWA crossings between
|
|
258
|
+
established wind sides — through the bow as tacks, through the
|
|
259
|
+
stern as gybes — with a 25° wobble guard and interpolated crossing
|
|
260
|
+
position, distance and ETA. Maneuvers merge into the sail-event
|
|
261
|
+
queue (time-sorted), ride the tactical sail-action cards ("Tack to
|
|
262
|
+
starboard ~14:20, 12 kt"), and surface as a whole-route "Sail
|
|
263
|
+
Work" timeline in the strategic outlook. Motoring and drift legs
|
|
264
|
+
are never maneuvers.
|
|
265
|
+
|
|
266
|
+
- Empty-state conditions view (work doc #7): with no active or
|
|
267
|
+
explicitly requested route, `/api/briefing` serves a **here
|
|
268
|
+
payload** — the UnifiedWeatherPayload shape with a single waypoint
|
|
269
|
+
at the vessel's position and 24 forward hourly steps, cached as
|
|
270
|
+
`weather/here.json` with a 3 h staleness flag. The cron/oneshot
|
|
271
|
+
fetch keeps it fresh while moored or anchored (`POST
|
|
272
|
+
/api/briefing/refresh` without a route re-fetches it), and
|
|
273
|
+
bulletin filtering runs against the position alone. The webapp
|
|
274
|
+
derives the mode from the served payload: an explicit "Conditions
|
|
275
|
+
here" entry in the route picker leads it whenever no route is
|
|
276
|
+
being sailed, rendering the new `<conditions-here>` view (no
|
|
277
|
+
tabs): position, conditions now (wind/gust/sea/current/pressure
|
|
278
|
+
trend), the 24 h comfort sparkline evaluated at SOG 0 (Sereno
|
|
279
|
+
apparent wind ≈ true wind at anchor), filtered warnings, and —
|
|
280
|
+
once work doc #3 lands — celestial/space events.
|
|
281
|
+
- Plugin skeleton: Signal K lifecycle (`plugin/index.js`) with the SPEC
|
|
282
|
+
§2.1 configuration schema, delta subscriptions for
|
|
283
|
+
`network.internet.state`, `navigation.state` and house state of
|
|
284
|
+
charge, and a one-minute cron ticker.
|
|
285
|
+
- Connection & navigation state machine (`plugin/state-machine.js`,
|
|
286
|
+
SPEC §2.2): OFFLINE / TRIGGER_ONESHOT / STANDBY_OFFSHORE /
|
|
287
|
+
PERSISTENT_CRON with edge-triggered oneshot fetches, the four UTC
|
|
288
|
+
publication windows (02:15, 08:15, 14:15, 20:15) and the execution
|
|
289
|
+
guard that disables cron fetching while sailing.
|
|
290
|
+
- SQLite engine (`plugin/sqlite-db.js`, SPEC §4.1): WAL-mode
|
|
291
|
+
`node:sqlite` store with the logbook sail events, wind history cache
|
|
292
|
+
and learned sail preference matrix tables, plus the SPEC §3.2/§4.2
|
|
293
|
+
binning and EMA helpers. Extended beyond the SPEC schema with a
|
|
294
|
+
`night` column on the sail events and matrix bins: the crew reefs
|
|
295
|
+
deeper at the evening watch change than conditions alone require,
|
|
296
|
+
and the day/night behaviors are learned separately.
|
|
297
|
+
- Sereno comfort & monohull motion physics
|
|
298
|
+
(`public/sereno-physics.mjs`, SPEC §5.2): apparent wind and vertical
|
|
299
|
+
acceleration comfort tiers, encounter period with the surf guard,
|
|
300
|
+
heel and waterline-pitch resonance multipliers, steepness ratio and
|
|
301
|
+
the learned-matrix sail suggestion lookup, plus dependency-free sun
|
|
302
|
+
altitude for the day/night bucket. Shared between the browser worker
|
|
303
|
+
and the server as a plain ES module.
|
|
304
|
+
- Logbook sail-change event source (`plugin/logbook-source.js`):
|
|
305
|
+
reads `signalk-logbook`'s on-disk YAML day files, parses the
|
|
306
|
+
`Sails set:` / `Sailing with` / `Motor stopped, sailing with` /
|
|
307
|
+
`Sails down` entries (including manually edited ones) into
|
|
308
|
+
REEF_INCREASE / REEF_DECREASE / SAIL_CHANGE events with per-entry
|
|
309
|
+
wind and position, filtering free-text noise against the
|
|
310
|
+
`@signalk/sailsconfiguration` inventory when available. This module
|
|
311
|
+
is the swap point for the coming logbook Resource API.
|
|
312
|
+
- Sail preference backfill (`plugin/history-backfill.js`, SPEC §4.2):
|
|
313
|
+
wind window statistics (TWS avg/peak, circular-mean TWA) from
|
|
314
|
+
either the logbook's own wind snapshots (default, works ashore) or
|
|
315
|
+
the Signal K History API (for the on-board run), cached and folded
|
|
316
|
+
into the learned matrix with the α = 0.2 EMA; idempotent via the
|
|
317
|
+
wind history cache. REST routes `GET /api/matrix`,
|
|
318
|
+
`GET /api/events`, `GET /api/logbook-events` and
|
|
319
|
+
`POST /api/backfill`.
|
|
320
|
+
- Backfill report CLI (`bin/backfill-report.js`): runs the backfill
|
|
321
|
+
over a logbook store and reports the conditions per sail combination
|
|
322
|
+
(day/night) next to the whole-sail wind limits from
|
|
323
|
+
`@signalk/sailsconfiguration`, plus the learned matrix.
|
|
324
|
+
- Sail inventory reader (`plugin/sails-configuration.js`): reads the
|
|
325
|
+
`@signalk/sailsconfiguration` store (m/s wind limits, reef
|
|
326
|
+
configurations as remaining areas in m²) for priors and annotation.
|
|
327
|
+
- Fetch engine (`plugin/fetch-engine.js`, SPEC §3.1): builds the
|
|
328
|
+
UnifiedWeatherPayload along route waypoints sampled evenly from the
|
|
329
|
+
route geometry (great-circle interpolation). Surface wind, gusts,
|
|
330
|
+
pressure, CAPE and pressure-layer fields come from the Open-Meteo
|
|
331
|
+
forecast API (K-index computed from T850/T700/T500 + dewpoints),
|
|
332
|
+
the combined sea and its wind sea/swell partitions from GFS-Wave,
|
|
333
|
+
surface current from SMOC. Marine and current endpoints degrade
|
|
334
|
+
gracefully; per-attempt timeouts and retry with backoff on 429/5xx.
|
|
335
|
+
- Payload cache: fetched briefings are persisted per route
|
|
336
|
+
(`weather/latest-<route>.json` plus dated snapshots, pruned to the
|
|
337
|
+
newest eight) so the boat can run ~23h offline on the last fetch
|
|
338
|
+
window; REST routes `GET /api/routes`, `GET /api/briefing` (cached,
|
|
339
|
+
works offline), `GET /api/cached` and `POST /api/briefing/refresh`
|
|
340
|
+
(internet only, refuses while offline). Cron/oneshot triggers
|
|
341
|
+
prefer the route currently being sailed
|
|
342
|
+
(`navigation.course.activeRoute`, resolved the same way as in the
|
|
343
|
+
dead-reckoning plugin) and fall back to the last briefed route.
|
|
344
|
+
- Step-forward isochrone simulation (`public/route-sim.mjs`, SPEC
|
|
345
|
+
§5.1): hourly advance along the sampled route with the §5.1 speed
|
|
346
|
+
decision tree (polar sailing / drift mode / motoring), current set
|
|
347
|
+
and drift added to the boat vector, partial-hour arrivals, Sereno
|
|
348
|
+
comfort per hour, learned sail-change suggestions anchored to the
|
|
349
|
+
watch-change day/night buckets, steep-sea and convective anomaly
|
|
350
|
+
detection, hazard-note alerts (polygon containment or 5 nm point
|
|
351
|
+
radius) and a three-run TWS perturbation pseudo-ensemble producing
|
|
352
|
+
ETA p10/p50/p90 plus the 24 h energy balance.
|
|
353
|
+
- Polar performance lookup (`public/polar.mjs`): consumes the
|
|
354
|
+
vessel's canonical `polars` resource table (SI axes, bilinear with
|
|
355
|
+
the pinch/hull-speed edge semantics shared with signalk-polar-tools)
|
|
356
|
+
and the `polars.performanceFactor` derating, falling back to a
|
|
357
|
+
built-in conservative monohull polar when no polar is active. REST
|
|
358
|
+
route `GET /api/polar` resolves the active polar server-side.
|
|
359
|
+
- Simulation web worker (`public/worker.js`): runs the passage
|
|
360
|
+
simulation off the main thread and replies with the result plus its
|
|
361
|
+
pre-filtered exception views (next-24h blocks, passage summary).
|
|
362
|
+
- Two-screen webapp (SPEC §6): root `<passage-outlook>` shell with
|
|
363
|
+
hash-based tabs (tactical 24h / strategic passage), route picker
|
|
364
|
+
preselecting the active route, offline pill from the Signal K
|
|
365
|
+
stream, and a stale-briefing strip offering a refresh when the
|
|
366
|
+
link is up. `<tactical-dashboard>` shows the current comfort tier,
|
|
367
|
+
the 24h `<horizon-sparkline>` (comfort-tier colors, AWS heights)
|
|
368
|
+
and exception-only sail/energy/hazard alerts;
|
|
369
|
+
`<strategic-outlook>` the ETA percentile table, motor plan,
|
|
370
|
+
macro sea-state and convective warnings plus the METAREA bulletin
|
|
371
|
+
console. Day/night reactive per `environment.mode` (throttled
|
|
372
|
+
delta subscription), exponential-backoff reconnects, granular DOM
|
|
373
|
+
updates only, zero dependencies. Pure view models
|
|
374
|
+
(`public/components/models.mjs`) are Node-tested; `GET /api/config`
|
|
375
|
+
serves the simulation-relevant plugin settings to the worker.
|
|
376
|
+
- Backtest & calibration CLI (`bin/backtest-cli.js` +
|
|
377
|
+
`plugin/backtest.js`, SPEC §7): replays the vessel's history
|
|
378
|
+
through the Sereno motion model — attitude component paths from
|
|
379
|
+
the History API (`/signalk/v2/api/history/values`, same contract
|
|
380
|
+
as the signalk-polar-tools replays) are reconstructed into
|
|
381
|
+
measured RMS vertical acceleration per 15-minute sliding window,
|
|
382
|
+
Nelder-Mead tunes (k_heel, k_pitch) on the MAE loss, and a 5×5
|
|
383
|
+
predicted-vs-measured comfort confusion matrix reports the fit.
|
|
384
|
+
Sea state falls back to a Pierson-Moskowitz wind-sea
|
|
385
|
+
approximation when no wave history is recorded; the report JSON
|
|
386
|
+
records resolution, sample and window counts.
|
|
387
|
+
- GMDSS bulletin filtering engine (`plugin/bulletin-engine.js`,
|
|
388
|
+
work doc #4): strips ZCZC/NNNN and routing headers, filters on the
|
|
389
|
+
NAVTEX B_2 subject indicator (B_1 station letter vs B_2 subject
|
|
390
|
+
distinguished - `ZCZC GA14` is subject A), segments on GMDSS
|
|
391
|
+
section anchors including NWS `.WARNINGS.` style, extracts
|
|
392
|
+
coordinate chains and cardinal bounds into geometry (antimeridian-
|
|
393
|
+
safe: seam-spanning polygons and unwrapped cardinal boxes), and
|
|
394
|
+
drops blocks whose warning area does not intersect the route
|
|
395
|
+
track. Axis-line warnings ("WITHIN 120NM EAST OF AXIS") expand by
|
|
396
|
+
the declared band before the test.
|
|
397
|
+
- Zone resolution & sources (`plugin/zone-source.js` +
|
|
398
|
+
`public/gmdss-zones-min.json`, work doc #9): bundled low-res
|
|
399
|
+
GeoJSON zone polygons resolve the active NAVAREA/METAREA zones
|
|
400
|
+
from the route (smallest-containing-polygon wins on overlap,
|
|
401
|
+
routes straddling a boundary activate both zones), and only those
|
|
402
|
+
zones are fetched - NOAA TGFTP raw text when the station is
|
|
403
|
+
configured (NFFN for XIV), WMO GMDSS portal as fallback, UKHO MSI
|
|
404
|
+
JSON URL available for NAVAREA warnings. Antimeridian routes
|
|
405
|
+
(Tonga to Opua) verified end to end in tests.
|
|
406
|
+
- Bulletin ingestion & cache (`plugin/bulletin-source.js`): online-
|
|
407
|
+
gated like the weather fetches, raw texts cached on disk
|
|
408
|
+
(`weather/bulletins.json`, newest 20 kept) so warnings stay
|
|
409
|
+
available through the offline hours; per-source failures skip
|
|
410
|
+
without losing the rest. Extra custom feeds configurable via
|
|
411
|
+
`bulletin_urls`.
|
|
412
|
+
- Briefing integration: the freshest cached bulletin is filtered
|
|
413
|
+
against the route track and attached to the payload as
|
|
414
|
+
`metareaBulletin` (with segmented `blocks`), spliced into older
|
|
415
|
+
cached briefings at serve time; REST routes `GET /api/bulletin`
|
|
416
|
+
and `POST /api/bulletin/refresh`.
|
|
417
|
+
- Strategic screen rendering: filtered warning blocks in a
|
|
418
|
+
scrolling console with severe-keyword highlighting (GALE, STORM,
|
|
419
|
+
SQUALL, ROUGH SEAS, ...), raw bulletin text as fallback.
|
|
420
|
+
- README with credits; the `yaml` runtime dependency for reading the
|
|
421
|
+
logbook store.
|
|
422
|
+
- Smoketests for the physics, the logbook source, the backfill, the
|
|
423
|
+
state machine, the SQLite store and the plugin lifecycle.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# signalk-passage-briefing
|
|
2
|
+
|
|
3
|
+
Offshore passage daily briefing webapp for Signal K: a plugin that plans
|
|
4
|
+
and reviews passages for a cruising sailing vessel.
|
|
5
|
+
|
|
6
|
+
The plugin fetches multi-model weather along the planned route (online,
|
|
7
|
+
or via a GRIB/text spool when offline offshore), runs a step-forward
|
|
8
|
+
isochrone simulation with a monohull comfort model, learns the crew's
|
|
9
|
+
sail preferences from the electronic logbook, and serves a two-screen
|
|
10
|
+
webapp: a 24-hour tactical dashboard and a strategic passage summary.
|
|
11
|
+
See [SPEC.md](SPEC.md) for the full design.
|
|
12
|
+
|
|
13
|
+
Part of the Lille Ø offshore suite, alongside
|
|
14
|
+
[@meri-imperiumi/signalk-energy-predictor](https://github.com/meri-imperiumi/signalk-energy-predictator)
|
|
15
|
+
and [@meri-imperiumi/signalk-logbook](https://github.com/meri-imperiumi/signalk-logbook).
|
|
16
|
+
|
|
17
|
+
## Data sources
|
|
18
|
+
|
|
19
|
+
### Signal K
|
|
20
|
+
|
|
21
|
+
- `navigation.course.activeRoute` — the route being sailed; wins over
|
|
22
|
+
the last briefed route when the cron/oneshot fetch window opens
|
|
23
|
+
- `navigation.position` — vessel position for the conditions-here
|
|
24
|
+
empty state and position-only bulletin filtering
|
|
25
|
+
- `network.internet.state` — connectivity gating (weather, bulletins
|
|
26
|
+
and backfill only fetch while `online` or `metered`)
|
|
27
|
+
- `navigation.state`, `electrical.batteries.house.capacity.stateOfCharge`
|
|
28
|
+
— state machine inputs (moored/anchored/sailing, publication windows)
|
|
29
|
+
- `polars.activePolar`, `polars.performanceFactor` — the canonical
|
|
30
|
+
polar resource consumed for boat speed (falls back to a bundled
|
|
31
|
+
default table)
|
|
32
|
+
- Resources API — route geometries and polar tables
|
|
33
|
+
- History API (on board) — wind/attempt snapshots for the sail-event
|
|
34
|
+
backfill
|
|
35
|
+
- Published tile paths — `navigation.briefing.generatedAt` / `.route` /
|
|
36
|
+
`.hasNew` / `.comfort` drive the plotter-extension tile
|
|
37
|
+
(`.acknowledgedAt` is writable to clear the NEW badge)
|
|
38
|
+
- [signalk-logbook](https://github.com/meri-imperiumi/signalk-logbook)
|
|
39
|
+
store — crewed sail events (reefs, sail changes) that train the
|
|
40
|
+
preference matrix
|
|
41
|
+
- [@signalk/sailsconfiguration](https://www.npmjs.com/package/@signalk/sailsconfiguration)
|
|
42
|
+
— sail inventory used to filter free-text noise out of log entries
|
|
43
|
+
|
|
44
|
+
### External
|
|
45
|
+
|
|
46
|
+
- [Open-Meteo Forecast API](https://open-meteo.com/en/docs) — surface
|
|
47
|
+
wind, gusts, MSL pressure, CAPE and the pressure-layer fields behind
|
|
48
|
+
the K-index (best-match model)
|
|
49
|
+
- [NOAA SWPC planetary K-index forecast](https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json)
|
|
50
|
+
— geomagnetic activity behind the aurora advisories
|
|
51
|
+
- [JPL SBDB query API](https://ssd-api.jpl.nasa.gov/doc/sbdb_query.html)
|
|
52
|
+
— comet brightness parameters (M1/K1) behind the Sky Notes comets
|
|
53
|
+
- [NOAA TGFTP radiofax tree](https://tgftp.nws.noaa.gov/fax/) and the
|
|
54
|
+
[BoM difacs charts](http://www.bom.gov.au/difacs/) — synoptic
|
|
55
|
+
surface-analysis charts per METAREA zone (work doc #11), per the
|
|
56
|
+
schedule in
|
|
57
|
+
[otherfax.txt](https://tgftp.nws.noaa.gov/fax/otherfax.txt)
|
|
58
|
+
- [Open-Meteo Marine API](https://open-meteo.com/en/docs) —
|
|
59
|
+
NOAA GFS-Wave 0.25° combined sea/wind sea/swell partitions, and
|
|
60
|
+
Météo-France SMOC surface currents
|
|
61
|
+
- NOAA TGFTP (`tgftp.nws.noaa.gov`) — raw METAREA bulletin text for
|
|
62
|
+
the resolved GMDSS zone's station (e.g. `FQPS01 NFFN` for XIV)
|
|
63
|
+
- WMO GMDSS portal (`weather.gmdss.org`) — per-zone bulletin pages,
|
|
64
|
+
fallback when the TGFTP station is not configured
|
|
65
|
+
- api.weather.gov product API — US High Seas Forecast texts (default
|
|
66
|
+
`bulletin_urls`: HSF NP/EP1/EP2); extra sources can be added via the
|
|
67
|
+
`bulletin_urls` configuration (plain text or api.weather.gov
|
|
68
|
+
product URLs; UKHO MSI JSON works too)
|
|
69
|
+
|
|
70
|
+
All external fetches are online-gated and cached to the plugin data
|
|
71
|
+
directory, so the last payloads survive the offline hours.
|
|
72
|
+
|
|
73
|
+
## Acknowledgments
|
|
74
|
+
|
|
75
|
+
The comfort model and the whole idea of "what will each departure
|
|
76
|
+
actually feel like" come from SV Sabado's
|
|
77
|
+
[passage-weather](https://github.com/sailing12388/passage-weather)
|
|
78
|
+
ensemble departure planner by Ray Hendricks. The Sereno comfort scale
|
|
79
|
+
(Champagne, Easy, Coffee, Rough, Sick), the encounter-period motion
|
|
80
|
+
math, and the ISO 2631-1 comfort bands are adapted from it for a
|
|
81
|
+
monohull (heel and waterline-pitch resonance instead of catamaran
|
|
82
|
+
beam-roll and bridgedeck slam).
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
EUPL-1.2
|