@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.
Files changed (86) hide show
  1. package/.editorconfig +5 -0
  2. package/.github/workflows/publish.yml +34 -0
  3. package/.github/workflows/signalk-ci.yml +11 -0
  4. package/.github/workflows/test.yml +21 -0
  5. package/CHANGELOG.md +423 -0
  6. package/README.md +86 -0
  7. package/SPEC.md +488 -0
  8. package/bin/backfill-report.js +218 -0
  9. package/bin/backtest-cli.js +132 -0
  10. package/biome.json +6 -0
  11. package/doc/here-brief.png +0 -0
  12. package/package.json +48 -0
  13. package/plugin/backtest.js +742 -0
  14. package/plugin/brief-ext.js +173 -0
  15. package/plugin/bulletin-engine.js +746 -0
  16. package/plugin/bulletin-source.js +150 -0
  17. package/plugin/celestial-source.js +417 -0
  18. package/plugin/fetch-engine.js +717 -0
  19. package/plugin/history-backfill.js +540 -0
  20. package/plugin/index.js +1479 -0
  21. package/plugin/logbook-source.js +463 -0
  22. package/plugin/notes-publisher.js +228 -0
  23. package/plugin/notes-store.js +241 -0
  24. package/plugin/raster-convert.js +168 -0
  25. package/plugin/sails-configuration.js +111 -0
  26. package/plugin/spool-watcher.js +218 -0
  27. package/plugin/sqlite-db.js +378 -0
  28. package/plugin/state-machine.js +249 -0
  29. package/plugin/statustilesexamples.js +101 -0
  30. package/plugin/synoptic-map.json +45 -0
  31. package/plugin/synoptic-source.js +227 -0
  32. package/plugin/zone-source.js +339 -0
  33. package/public/app.js +18 -0
  34. package/public/brief-ext-model.js +64 -0
  35. package/public/brief-ext-widget.html +14 -0
  36. package/public/brief-ext-widget.js +294 -0
  37. package/public/components/backfill-controls.js +100 -0
  38. package/public/components/comfort-info.js +94 -0
  39. package/public/components/conditions-here.js +234 -0
  40. package/public/components/horizon-sparkline.js +77 -0
  41. package/public/components/models.mjs +372 -0
  42. package/public/components/passage-outlook.js +368 -0
  43. package/public/components/sk-api.js +146 -0
  44. package/public/components/sk-base-css.js +177 -0
  45. package/public/components/strategic-outlook.js +248 -0
  46. package/public/components/synoptic-chart.js +48 -0
  47. package/public/components/tactical-dashboard.js +172 -0
  48. package/public/css/visuals.css +292 -0
  49. package/public/gmdss-zones-min.json +287 -0
  50. package/public/icon.png +0 -0
  51. package/public/index.html +13 -0
  52. package/public/polar.mjs +303 -0
  53. package/public/route-sim.mjs +750 -0
  54. package/public/sereno-physics.mjs +592 -0
  55. package/public/tack-gybe.js +174 -0
  56. package/public/vendor/plotterext-bus/LICENSE +21 -0
  57. package/public/vendor/plotterext-bus/README.md +17 -0
  58. package/public/vendor/plotterext-bus/chunk-4W6N34SD.js +333 -0
  59. package/public/vendor/plotterext-bus/chunk-7XRFPDQL.js +263 -0
  60. package/public/vendor/plotterext-bus/chunk-RED55KML.js +117 -0
  61. package/public/vendor/plotterext-bus/extension.js +29 -0
  62. package/public/vendor/plotterext-bus/host.js +28 -0
  63. package/public/vendor/utif/LICENSE +21 -0
  64. package/public/vendor/utif/UTIF.js +1171 -0
  65. package/public/worker.js +35 -0
  66. package/status-tiles-examples.json +55 -0
  67. package/tests/backtest.test.js +461 -0
  68. package/tests/brief-ext.test.js +194 -0
  69. package/tests/bulletin-engine.test.js +361 -0
  70. package/tests/celestial-source.test.js +265 -0
  71. package/tests/fetch-engine.test.js +297 -0
  72. package/tests/history-backfill.test.js +341 -0
  73. package/tests/logbook-source.test.js +324 -0
  74. package/tests/notes-publisher.test.js +161 -0
  75. package/tests/notes-store.test.js +138 -0
  76. package/tests/openmeteo-mock.js +100 -0
  77. package/tests/plugin.test.js +1344 -0
  78. package/tests/route-sim.test.js +533 -0
  79. package/tests/sereno-physics.test.js +333 -0
  80. package/tests/sqlite-db.test.js +164 -0
  81. package/tests/state-machine.test.js +182 -0
  82. package/tests/statustilesexamples.test.js +86 -0
  83. package/tests/synoptic-source.test.js +195 -0
  84. package/tests/tack-gybe.test.js +211 -0
  85. package/tests/webapp.test.js +257 -0
  86. package/tests/zone-source.test.js +226 -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@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,11 @@
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
@@ -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