@meri-imperiumi/signalk-passage-briefing 0.3.0 → 0.5.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 (55) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +39 -66
  3. package/SPEC.md +117 -0
  4. package/package.json +8 -1
  5. package/plugin/bulletin-engine.js +310 -15
  6. package/plugin/celestial-ephemeris.js +561 -0
  7. package/plugin/celestial-source.js +98 -20
  8. package/plugin/energy-source.js +167 -0
  9. package/plugin/fetch-engine.js +3 -0
  10. package/plugin/hazard-source.js +410 -0
  11. package/plugin/index.js +412 -7
  12. package/plugin/maritime-zones-source.js +366 -0
  13. package/plugin/notes-publisher.js +88 -1
  14. package/plugin/satellite-source.js +295 -0
  15. package/plugin/spool-watcher.js +87 -0
  16. package/plugin/weather-source.js +311 -0
  17. package/public/components/conditions-here.js +111 -2
  18. package/public/components/departure-control.js +170 -0
  19. package/public/components/horizon-sparkline.js +28 -7
  20. package/public/components/models.mjs +739 -59
  21. package/public/components/passage-outlook.js +300 -21
  22. package/public/components/passage-timeline.js +141 -0
  23. package/public/components/sk-api.js +108 -1
  24. package/public/components/strategic-outlook.js +87 -117
  25. package/public/components/tactical-dashboard.js +98 -59
  26. package/public/lines-of-interest.js +227 -0
  27. package/public/route-sim.mjs +525 -20
  28. package/public/sereno-physics.mjs +189 -5
  29. package/tests/bulletin-engine.test.js +493 -0
  30. package/tests/celestial-ephemeris.test.js +215 -0
  31. package/tests/celestial-source.test.js +57 -7
  32. package/tests/fetch-engine.test.js +1 -0
  33. package/tests/fixtures/bulletins/fqau23-ammc-western.txt +91 -0
  34. package/tests/fixtures/bulletins/fqps01-nffn.txt +33 -0
  35. package/tests/fixtures/bulletins/fqps43-nzkl-subtropic.txt +35 -0
  36. package/tests/fixtures/bulletins/fznt01-kwbc-hsf-at1.txt +139 -0
  37. package/tests/fixtures/bulletins/fzpn01-kwbc-hsf-ep1.txt +178 -0
  38. package/tests/fixtures/bulletins/fzpn03-knhc-hsf-ep2.txt +85 -0
  39. package/tests/fixtures/bulletins/fzpn40-phfo-hsf-np.txt +74 -0
  40. package/tests/fixtures/bulletins/fzps40-phfo-hsf-sp.txt +77 -0
  41. package/tests/fixtures/bulletins/hsfat2-knhc-2026-10-03.txt +55 -0
  42. package/tests/fixtures/bulletins/wtpz23-knhc-tcmep3.txt +157 -0
  43. package/tests/fixtures/energy-forecast-hourly.json +156 -0
  44. package/tests/fixtures/gdacs-rss-sample.xml +391 -0
  45. package/tests/hazard-source.test.js +239 -0
  46. package/tests/lines-of-interest.test.js +160 -0
  47. package/tests/maritime-zones-source.test.js +249 -0
  48. package/tests/notes-publisher.test.js +16 -0
  49. package/tests/openmeteo-mock.js +1 -0
  50. package/tests/plugin.test.js +480 -2
  51. package/tests/route-sim.test.js +419 -5
  52. package/tests/satellite-source.test.js +149 -0
  53. package/tests/sereno-physics.test.js +165 -0
  54. package/tests/weather-source.test.js +272 -0
  55. package/tests/webapp.test.js +731 -90
package/CHANGELOG.md CHANGED
@@ -2,6 +2,118 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.5.0] - 2026-10-03
6
+
7
+ ### Added
8
+ - Daylight-anchored departure time (work doc #15): while the boat is moored, the whole forecast schedule anchors to a realistic departure instead of "now" — nobody casts off at 02:40 because the forecast said so. Auto mode: underway sails from now; night at the start position waits for next first light (civil dawn, −6° sun altitude at the route's first waypoint); day adds the prep delay (1.5 h, configurable 0.5–3) unless the sun would set within it, in which case the anchor is the next dawn — never an immediate departure right before dark (the recorded decision). The pure helper (`assumedDepartureTime` in sereno-physics) detects a rising sun crossing with 5-minute steps bounded at 18 h and states what it assumed (`reason`), including a polar `no_dawn` fallback. The tactical and strategic views share a departure control (Auto / Now / First light / +1 h / +2 h / custom) whose manual choice overrides auto until it is put back; the root owns the state, re-checks the anchor every 10 minutes, and re-simulates only when the anchor moved more than 10 minutes so the view doesn't flap around dawn. The tactical chip states the anchor ("First light 10-05 06:00 +13"), the strategic ETA header says "Assumed departure first light …" ("Underway — from now" when sailing), sparkline titles read wall-clock stamps while shifted, and a delayed departure appears in the timeline as a ⚓ event ("Departure at first light" / "Departure after prep"). The served payload carries the same `departure` metadata so external consumers see numbers consistent with the auto assumption. SPEC §2.1/§3.1 updated.
9
+ - Energy forecast consumption (work doc #10): the briefing finally powers its energy views — in the predictor's own terms. signalk-energy-predictor's hourly forecast (`electrical.energy.prediction.forecast.hourly`, the whole 48 h series as one delta value) and its outlook paths (`status`, `net`, `surplus` + window, `timeToEmpty`) are subscribed like any other Signal K path; the hourly series adapts into the frozen `energyHourly` consumer contract at compile time (the contract's `solarWh` carries total ideal generation: solar + wind + hydro + alternator) and rides the briefing payload so the offline hours re-derive from the cache. The 24 h solar-yield strip and the deficit banner work for the first time; the conditions-here view gains an "Energy 24h" line (the passage strip's here-mode sibling at SOG 0). New in the timeline: the predictor's own energy events — **surplus** is forecast curtailment (battery full while yield continues; the event carries the curtailed Wh and its window, "run opportunistic loads"), **deficit** and **critical** come from the outlook status (the event carries the 24 h net and, when the trajectory names one, the depletion time) — no independent re-derivation with invented thresholds.
10
+ - Territorial waters transitions along the route (work doc #17): the briefing now says where the route crosses the boundaries that change the practical picture on board — entering or leaving a country's internal, archipelagic or 12 NM waters (the metered-ocean data boundary, customs and discharge rules). Built on `@openwaters/maritime-zones` (MIT) over the Marine Regions Maritime Boundaries Geodatabase (VLIZ, CC-BY 4.0 — the "not for navigation, no legal value" disclaimer rides the payload and renders wherever zones do). Corridor tiles (about 8 MB for a 900 nm crossing) download during the online window into the plugin data directory and answer offline afterwards; the boundary walk resamples the route at 1 nm so short territorial hops are not missed. The route simulation timestamps each crossing against the passage schedule, the unified timeline renders enter/leave events (leave events carry the "ocean data rules beyond this point" connectivity note), and here mode reports the waters the vessel sits in right now in the conditions view. Missing tiles, older Node (the package needs ≥ 24, the plugin allows 22.5) or a failed download degrade to no zone data — never a failed briefing. — entering or leaving a country's internal, archipelagic or 12 NM waters (the metered-ocean data boundary, customs and discharge rules). Built on `@openwaters/maritime-zones` (MIT) over the Marine Regions Maritime Boundaries Geodatabase (VLIZ, CC-BY 4.0 — the "not for navigation, no legal value" disclaimer rides the payload and renders wherever zones do). Corridor tiles (about 8 MB for a 900 nm crossing) download during the online window into the plugin data directory and answer offline afterwards; the boundary walk resamples the route at 1 nm so short territorial hops are not missed. The route simulation timestamps each crossing against the passage schedule, the unified timeline renders enter/leave events (leave events carry the "ocean data rules beyond this point" connectivity note), and here mode reports the waters the vessel sits in right now in the conditions view. Missing tiles, older Node (the package needs ≥ 24, the plugin allows 22.5) or a failed download degrade to no zone data — never a failed briefing.
11
+ - GDACS hazard events also surface in the conditions-here view: earthquakes, cyclones and floods near the vessel are critical while stationary, so the here view lists them (Red events highlighted) alongside the passage timeline.
12
+ - GDACS hazard event feed (work doc #22): earthquakes, tropical cyclones, floods and volcanic activity from the EU JRC's `gdacs.org/xml/rss.xml`, fetched during the online window and cached in `weather/hazards.json` with id-based dedup (repolls update in place). Events filter route-relative — alert level at or above the configured minimum (default Orange), within 500 nm of the route corridor or 1000 nm of the vessel, and no older than 72 h — and ride the briefing payload as `hazardEvents`, render in the unified timeline as `hazard` events with the alert level setting the severity, and publish as georeferenced `resources/notes` chart notes (category `hazard-event`, provenance `GDACS`) with scoped expiry that leaves the METAREA notes and other clients' notes untouched. Zero XML dependencies: a targeted extractor reads the stable RSS shape; malformed items skip without costing the feed.
13
+ - Slatting & roll-dampening penalty in the Sereno comfort index (work doc #14): a residual swell (combined Hs ≥ 0.6 m) with too little wind to keep the sails loaded — below 7 kn upwind, below 12 kn downwind — now scores as the wash machine it is: the vertical acceleration carries a 2.5× discomfort multiplier before banding and the hour is forced to at least Rough. The hourly rows tag `slatting: true`, and the tactical dashboard's sparkline renders those Rough blocks with a diagonal hatch (alternating `--comfort-rough` over the card background) so the skipper sees the discomfort is light-air slatting, not heavy weather. SPEC §5.2 amended.
14
+ - Real-world bulletin fixtures (work doc #4 follow-up): ten received-as-is High Seas texts — NHC Atlantic T1/T2, OPC East Pacific EP1, NHC EP2, Honolulu North and South Pacific (NP/SP), Fiji NAVAREA XIV, MetService NZ Subtropic, Australian BoM Western X and an NHC hurricane advisory — live under `tests/fixtures/bulletins/` with a per-family regression suite asserting issue metadata, issuer and geography extraction.
15
+ - The bulletin engine now parses the NHC/OPC/PHFO High Seas families: compact coordinate pairs (`13N67W`, no separator) normalize into the spaced form; a hemisphere-less second number that is not the dateline shorthand is rejected as prose ("07N TO 31N" is two latitudes, not 7N 31E); issue lines from NHC (`0430 UTC SAT OCT 3 2026`), PHFO (`1130 UTC TUE FEB 17 2026`), Fiji (`OCT 022000 UTC`, mixed case `Oct` from the WMO bulletin sets), MetService New Zealand (`Wellington issued at 021856UTC`) and the Australian BoM (`commencing 2300 UTC 2 October 2026`) all parse to real timestamps; `.FORECASTER <NAME>.` lines identify the issuer when no `ISSUED BY` exists, and a leading article is trimmed ("Issued by the Australian Government Bureau of Meteorology").
16
+ - NWS's renamed Gulf is normalized back to the crew's name: "GULF OF AMERICA" reads "GULF OF MEXICO" everywhere the cleaned bulletin text is shown or matched.
17
+ - Lines of interest (work doc #1): the briefing detects where the planned passage crosses the traditional ceremonial lines — the equator (Shellback ceremony), the tropics, the polar circles (Blue Nose / Red Nose), the prime meridian and the antimeridian (Domain of the Golden Dragon, with the calendar skip/repeat note) — reading the simulated track, so each crossing's position, distance from start and ETA come from the boat's own schedule rather than the forecast window. The crossings ride the unified passage timeline as `line` events (⌀ glyph, the general form the timeline was built for) instead of a separate strategic block; `passageSummary.linesOfInterest` carries the data, `lines_of_interest_enabled` (default on) turns the feature off.
18
+ - Celestial ephemeris engine (work doc #3 Phase 2): the sky is computed, not fetched. A new `plugin/celestial-ephemeris.js` module built on `astronomy-engine` (MIT, zero transitive dependencies — the dependency decision is made) derives planetary conjunctions and oppositions during nautical night, and meteor shower peaks (static annual calendar) gated to dark, clear, visible nights: moon set or a crescent, radiant able to clear 20° from the vessel's latitude, forecast cloud cover under 30 %. Bright station passes (ISS, Tiangong) are propagated locally from CelesTrak two-line elements with `satellite.js` (MIT — the SGP4 propagator call resolved) in `plugin/satellite-source.js`, reported when the pass peaks over 15° in nautical night under a clear enough sky. Every event passes the full tactical visibility gate the work document specifies — nautical night, altitude over 15° to clear the marine boundary-layer haze, cloud cover, moonlight — and the Phase-1 aurora alert is upgraded onto the same gate (nautical night replaces the sunset-based check; moonlit or overcast nights no longer raise aurora banners). Briefing payloads gain `celestialNights` — per-night twilight times (civil, nautical, astronomical) and moon phase/illumination — and `TimeStepForecast` gains `surface.cloudCover` (Open-Meteo `cloud_cover`; the Weather API provider publishes none and maps null, which leaves the cloud gate open).
19
+ - The timeline's night stamps show the actual moon phase instead of a fixed crescent: `mergeTimeline` picks one of the eight moon glyphs (🌑🌒🌓🌔🌕🌖🌗🌘) for each night-flagged event from the payload's `celestialNights`, falling back to ☾ for payloads that predate the field. The strategic ETA table's night-arrival markers (P10/P50/P90) get the same treatment.
20
+ - **METAREA notes now carry provenance: who published, when.** Every note the bulletin pipeline serves in `resources/notes` is stamped in its `properties` with `publishedBy` — the issuing authority parsed from the bulletin header ("ISSUED BY FIJI METEOROLOGICAL SERVICE …" → `FIJI METEOROLOGICAL SERVICE`), falling back to `signalk-passage-briefing` when the bulletin names no service (structured UKHO warnings, some NWS products) — and `publishedAt`, the bulletin's issue time. Chart plotters can show the line "Published on 10-03 12:00Z by FIJI METEOROLOGICAL SERVICE" straight from the resource; the DR webapp's note detail surface does.
21
+
22
+ ## [0.4.0] - 2026-10-03
23
+
24
+ ### Added
25
+
26
+ - Sail changes are scheduled when the crew can act on them:
27
+ recommendation-driven canvas changes anchor to the *previous watch
28
+ handover* when a watch schedule is running (signalk-watch-schedule;
29
+ boundaries are extrapolated across the forecast horizon from the
30
+ rotation cycle, and the watch boundary wins over sunlight),
31
+ otherwise to the next sunrise/sunset. Several detections between
32
+ boundaries collapse into one change carrying the last suggested
33
+ state, no-op re-rigs are dropped, and the timeline says which
34
+ anchor applied ("watch change" / "at dusk" / "at dawn"). Tacks and
35
+ gybes stay at their tactical times.
36
+ - Unified passage timeline: a new `<passage-timeline>` component and
37
+ `mergeTimeline()` view model render every event source — sail
38
+ changes, planned tacks and gybes, convective risk, macro sea state,
39
+ territorial waters transitions, sky events and hazard notes — as
40
+ one chronological list with a time gutter, kind glyph and severity
41
+ colour. The tactical dashboard renders the 24 h slice, the
42
+ strategic outlook the whole passage; the per-type blocks (Sail
43
+ Work, Convective Risk, Macro Sea State, Sky Notes) and the tactical
44
+ hazard banners are replaced by it. The exception view now carries
45
+ the whole-route hazard list in `passageSummary.hazards` (the 24 h
46
+ `next24h.sailChanges` and `next24h.hazards` slices are gone — the
47
+ timeline slices itself).
48
+ - Sail-change events in the timeline carry the forecast conditions
49
+ at the change point (nearest simulated hour): true wind, sea state
50
+ and Sereno comfort tier — "Main 1 reef — 14.2 kn TWS · Hs 1.5 m ·
51
+ coffee" — so the crew knows what they are rigging into. The
52
+ simulation's hourly rows now include wave height and period.
53
+ - Ship's time display: the webapp reads the vessel's published
54
+ timezone (`environment.time.timezoneOffset` / `.timezoneRegion`, as
55
+ served by signalk-ships-time) over the REST API and the delta
56
+ stream, and renders briefing stamps in ship's time (`MM-DD HH:MM
57
+ +13`) instead of UTC, falling back to UTC `Z` when no offset is
58
+ published. The offset rides on every stamp so a zone crossing
59
+ mid-passage reads honestly; a header pill names the zone (IANA
60
+ region when known, else the offset).
61
+ - Weather source selection (`weather_source`: `auto` / `weather-api` /
62
+ `open-meteo`, default `auto`): when the server has the Weather API
63
+ and a provider answers — signalk-weather-router-plus serving its
64
+ decoded ECMWF run — waypoint forecasts for the here and route
65
+ briefing windows are read from it in-process (`app.weatherApi`)
66
+ instead of Open-Meteo, so the briefing reasons from the same
67
+ forecast the router planned with and offshore fetches stay local.
68
+ Provider responses (Signal K units) map onto the payload
69
+ conventions (knots, degrees true, hPa); combined sea only — swell
70
+ and wind-sea partitions and the upper-air fields behind the
71
+ convective warnings degrade to absent. A failed Weather API fetch
72
+ falls back to Open-Meteo for that window; the forced choices never
73
+ fall back.
74
+
75
+ ### Changed
76
+
77
+ - Convective warnings read as episodes, not hourly spam: consecutive
78
+ anomalies (and steep-sea anomalies alike) merge into one timeline
79
+ event with a time range and peak values — "CAPE 713 J/kg · K 28.6 ·
80
+ until 10-05 05:38Z". Severity follows the peak: CAPE ≥ 400 J/kg (the
81
+ bar where weather services start coloring the index) or K-index ≥ 30
82
+ is a red alert, while the unstable-air band below it (K ≥ 28 with
83
+ low CAPE) still shows as an orange warning with units and sane
84
+ precision instead of being dropped or inflated.
85
+
86
+ ### Fixed
87
+
88
+ - The webapp flags stale cached briefings: when a served payload was
89
+ compiled more than a day ago, a banner above the briefing shows the
90
+ compile stamp and age with a Fetch now affordance, instead of
91
+ presenting a multi-day-old timeline's `+Xh` labels as upcoming.
92
+ - The plugin re-fetches stale briefings without waiting for an edge
93
+ trigger: the oneshot fires only when the internet state changes and
94
+ cron windows only run while moored and charged, so a server that
95
+ stays up for days while the machine sits in STANDBY_OFFSHORE kept
96
+ serving a briefing sliding into the past. The one-minute ticker now
97
+ re-fetches (trigger `stale`) when online and the cached briefing
98
+ (active route, else last briefed, else here) is older than the
99
+ route TTL — at most once per six hours, and only when something is
100
+ actually cached.
101
+
102
+ - Bulletin geography now resolves named synoptic features in area
103
+ bounds: `SOUTH OF 09S AND WEST OF CF` and `SOUTH OF 10S, BETWEEN
104
+ 150W AND CF` compose a polygon from the cold front's defining
105
+ chain (clipped to the stated latitude bounds, closed across the
106
+ antimeridian with a margin so western-Pacific vessels stay in
107
+ west-of-front areas) instead of falling back to a hemisphere-wide
108
+ box that matched every vessel south of the bound.
109
+
110
+ - Coordinate chains accept the Fiji/NFFN bulletin conventions the
111
+ strict parser dropped: the dateline written as bare `180` (`TROUGH
112
+ T3 12S 175E 14S 180 15S 177W`) and the equator written as `EQT`
113
+ (`EQT 177E`). Prose numbers (`280600 UTC`, `20 TO 30 KNOTS`) are
114
+ still rejected, and a rejected span no longer swallows a following
115
+ coordinate pair.
116
+
5
117
  ## [0.3.0] - 2026-09-28
6
118
 
7
119
  ### Added
package/README.md CHANGED
@@ -1,85 +1,58 @@
1
1
  # signalk-passage-briefing
2
2
 
3
- Offshore passage daily briefing webapp for Signal K: a plugin that plans
4
- and reviews passages for a cruising sailing vessel.
3
+ Offshore passage daily briefing webapp for Signal K: a plugin that plans and reviews passages for a cruising sailing vessel.
5
4
 
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.
5
+ The plugin fetches weather along the planned route — from the server's Weather API when a provider answers, Open-Meteo otherwise; fetched online, or via a GRIB/text spool when offline offshore. It runs a step-forward isochrone simulation with a monohull comfort model, learns the crew's sail preferences from the electronic logbook, and serves a two-screen webapp: a 24-hour tactical dashboard and a strategic passage summary. See [SPEC.md](SPEC.md) for the full design.
12
6
 
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).
7
+ Part of the Lille Ø offshore suite, alongside [@meri-imperiumi/signalk-energy-predictor](https://github.com/meri-imperiumi/signalk-energy-predictator) and [@meri-imperiumi/signalk-logbook](https://github.com/meri-imperiumi/signalk-logbook).
16
8
 
17
9
  ## Data sources
18
10
 
19
11
  ### Signal K
20
12
 
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)
13
+ - `navigation.course.activeRoute` — the route being sailed; wins over the last briefed route when the cron/oneshot fetch window opens
14
+ - `navigation.position` — vessel position for the conditions-here empty state and position-only bulletin filtering
15
+ - `network.internet.state` — connectivity gating (weather, bulletins and backfill only fetch while `online` or `metered`)
16
+ - `navigation.state`, `electrical.batteries.house.capacity.stateOfCharge` — state machine inputs (moored/anchored/sailing, publication windows)
17
+ - `polars.activePolar`, `polars.performanceFactor` — the canonical polar resource consumed for boat speed (falls back to a bundled default table)
32
18
  - 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
19
+ - History API (on board) — wind/attempt snapshots for the sail-event backfill
20
+ - Published tile paths — `navigation.briefing.generatedAt` / `.route` / `.hasNew` / `.comfort` drive the plotter-extension tile (`.acknowledgedAt` is writable to clear the NEW badge)
21
+ - [signalk-logbook](https://github.com/meri-imperiumi/signalk-logbook) store — crewed sail events (reefs, sail changes) that train the preference matrix
22
+ - [signalk-ships-time](https://github.com/meri-imperiumi/signalk-ships-time) — `environment.time.timezoneOffset` / `.timezoneRegion`, the vessel's published timezone: briefing stamps render in ship's time when available (the offset rides on every stamp), falling back to UTC `Z` when nothing is published
23
+ - [signalk-watch-schedule](https://github.com/hoeken/signalk-watch-schedule) — `watch.state.onWatch`, `watch.state.startedAt`, `watch.system` and `watch.schedule`: the running watch rotation. While a watch is running, planned sail changes anchor to the previous watch handover (both teams awake) instead of sunrise/sunset; boundaries are extrapolated across the forecast horizon from the rotation cycle
24
+ - [@signalk/sailsconfiguration](https://www.npmjs.com/package/@signalk/sailsconfiguration) — sail inventory used to filter free-text noise out of log entries
25
+ - Weather API (`app.weatherApi`) — waypoint forecasts from the registered provider, the preferred source (`weather_source: auto`) whenever one answers; [signalk-weather-router-plus](https://github.com/motamman/signalk-weather-router-plus) serves it from its decoded ECMWF run, so briefing numbers match what the router planned with. That source carries combined sea only: swell/wind-sea partitions and the upper-air fields behind the convective warnings degrade to absent rather than being invented
43
26
 
44
27
  ### External
45
28
 
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.
29
+ The Open-Meteo entries below are the fallback weather source: used when no Weather API provider answers (or `weather_source` is `open-meteo`), and the only source of the partition and upper-air fields the Weather API providers don't publish.
30
+
31
+ - [Open-Meteo Forecast API](https://open-meteo.com/en/docs) — surface wind, gusts, MSL pressure, CAPE and the pressure-layer fields behind the K-index (best-match model)
32
+ - [NOAA SWPC planetary K-index forecast](https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json) — geomagnetic activity behind the aurora advisories
33
+ - [JPL SBDB query API](https://ssd-api.jpl.nasa.gov/doc/sbdb_query.html) — comet brightness parameters (M1/K1) behind the Sky Notes comets
34
+ - [CelesTrak](https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle) — two-line elements for the crewed stations (ISS, Tiangong); bright satellite passes are propagated locally from the fetched TLEs with SGP4, gated to nautical night, clear sky and a 15° haze line
35
+ - [NOAA TGFTP radiofax tree](https://tgftp.nws.noaa.gov/fax/) and the [BoM difacs charts](http://www.bom.gov.au/difacs/) — synoptic surface-analysis charts per METAREA zone (work doc #11), per the schedule in [otherfax.txt](https://tgftp.nws.noaa.gov/fax/otherfax.txt)
36
+ - [UKHO Admiralty MSI API](https://msi.admiralty.co.uk/) — structured NAVAREA navigational warnings for the resolved zone; the third rung of the GMDSS bulletin fetch ladder (TGFTP raw text, WMO GMDSS portal, then UKHO MSI JSON) and also accepted verbatim as a `bulletin_urls` source
37
+ - [GDACS RSS](https://www.gdacs.org/xml/rss.xml) — EU JRC global disaster alerts (earthquakes, tropical cyclones, floods, volcanic activity), filtered route-relative by alert level and corridor distance: they ride the unified passage timeline and publish as georeferenced chart notes (work doc #22)
38
+ - [Open-Meteo Marine API](https://open-meteo.com/en/docs) — NOAA GFS-Wave 0.25° combined sea/wind sea/swell partitions, and Météo-France SMOC surface currents
39
+ - NOAA TGFTP (`tgftp.nws.noaa.gov`) — raw METAREA bulletin text for the resolved GMDSS zone's station (e.g. `FQPS01 NFFN` for XIV)
40
+ - WMO GMDSS portal (`weather.gmdss.org`) — per-zone bulletin pages, fallback when the TGFTP station is not configured
41
+ - api.weather.gov product API — US High Seas Forecast texts (default `bulletin_urls`: HSF NP/EP1/EP2); extra sources can be added via the `bulletin_urls` configuration (plain text or api.weather.gov product URLs; UKHO MSI JSON works too)
42
+
43
+ All external fetches are online-gated and cached to the plugin data directory, so the last payloads survive the offline hours.
44
+
45
+ ## Pairing with Weather Router Plus
46
+
47
+ [signalk-weather-router-plus](https://github.com/motamman/signalk-weather-router-plus) plans the passage: isochrone routing against the vessel's polar on the ECMWF open-data run it keeps decoded on disk, with map overlays, tides and currents. Activate the route it publishes to the Resources API, and this plugin briefs it — `navigation.course.activeRoute` wins over the last briefed route at the next fetch window, and with `weather_source: auto` (the default) the briefing reads its forecasts from the router's Weather API provider. One forecast on board, planner and briefing in agreement: the router for planning and visualization, the briefing for the underway daily routine (comfort, sail changes, bulletins, energy).
48
+
49
+ ## Pairing with Watch Schedule
50
+
51
+ [signalk-watch-schedule](https://github.com/hoeken/signalk-watch-schedule) runs the crew's watch rotation and publishes it under `watch.*`. When a watch is running, this plugin reads the schedule at app load and moves planned sail changes (reefs, canvas work) to the *previous* watch handover — the moment both teams are awake on deck — rather than sunrise/sunset; tacks and gybes stay at their tactical times, since course work can't wait for a handover. The watch boundary wins over sunlight. Without a running watch, canvas work anchors to the next sunrise/sunset. Webapp-side only: no configuration, and the briefing works unchanged when the plugin is absent.
72
52
 
73
53
  ## Acknowledgments
74
54
 
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).
55
+ The comfort model and the whole idea of "what will each departure actually feel like" come from SV Sabado's [passage-weather](https://github.com/sailing12388/passage-weather) ensemble departure planner by Ray Hendricks. The Sereno comfort scale (Champagne, Easy, Coffee, Rough, Sick), the encounter-period motion math, and the ISO 2631-1 comfort bands are adapted from it for a monohull (heel and waterline-pitch resonance instead of catamaran beam-roll and bridgedeck slam).
83
56
 
84
57
  ## License
85
58
 
package/SPEC.md CHANGED
@@ -77,6 +77,52 @@ signalk-passage-outlook/
77
77
  "type": "number",
78
78
  "title": "Pitching Acceleration Multiplier Constant",
79
79
  "default": 0.40
80
+ },
81
+ "lines_of_interest_enabled": {
82
+ "type": "boolean",
83
+ "title": "Lines of Interest (ceremonial crossings)",
84
+ "default": true
85
+ },
86
+ "hazard_events_enabled": {
87
+ "type": "boolean",
88
+ "title": "GDACS Hazard Events",
89
+ "default": true
90
+ },
91
+ "hazard_min_alert_level": {
92
+ "type": "string",
93
+ "enum": ["green", "orange", "red"],
94
+ "title": "Minimum GDACS Alert Level",
95
+ "default": "orange"
96
+ },
97
+ "hazard_radius_offroute_nm": {
98
+ "type": "number",
99
+ "title": "Hazard Off-route Radius (nm)",
100
+ "default": 500
101
+ },
102
+ "hazard_radius_ahead_nm": {
103
+ "type": "number",
104
+ "title": "Hazard Vessel Radius (nm)",
105
+ "default": 1000
106
+ },
107
+ "hazard_max_age_hours": {
108
+ "type": "number",
109
+ "title": "Hazard Event Aging (hours)",
110
+ "default": 72
111
+ },
112
+ "departure_daylight_auto": {
113
+ "type": "boolean",
114
+ "title": "Anchor Departure to Daylight (auto)",
115
+ "default": true
116
+ },
117
+ "departure_prep_hours": {
118
+ "type": "number",
119
+ "title": "Departure Prep Delay (hours)",
120
+ "default": 1.5
121
+ },
122
+ "departure_dawn_altitude_deg": {
123
+ "type": "number",
124
+ "title": "First Light Sun Altitude (degrees)",
125
+ "default": -6
80
126
  }
81
127
  }
82
128
  }
@@ -144,6 +190,63 @@ interface UnifiedWeatherPayload {
144
190
  issuedAt: string;
145
191
  bulletinText: string;
146
192
  };
193
+ spaceEvents?: {
194
+ kind: string; // 'aurora' | 'comet' | 'conjunction' | 'opposition' | 'meteor'
195
+ timestamp: string; // ISO timestamp
196
+ tactical: boolean; // 24h dashboard banner vs strategic sky note
197
+ description: string;
198
+ }[];
199
+ celestialNights?: {
200
+ timestamp: string; // ISO timestamp (night anchor)
201
+ moonPhaseDeg: number; // 0 = new, 90 = first quarter, 180 = full, 270 = third quarter
202
+ moonIllumination: number; // fraction 0-1
203
+ civilDusk: string | null; // ISO timestamps, null beyond polar day/night
204
+ nauticalDusk: string | null;
205
+ astronomicalDusk: string | null;
206
+ astronomicalDawn: string | null;
207
+ nauticalDawn: string | null;
208
+ civilDawn: string | null;
209
+ }[];
210
+ hazardEvents?: {
211
+ id: string; // GDACS event id
212
+ type: string; // 'EQ' | 'TC' | 'FL' | 'VO' | 'DR' | 'WF'
213
+ alertLevel: string; // 'green' | 'orange' | 'red'
214
+ title: string;
215
+ description: string;
216
+ timestamp: string; // ISO, newer of pubDate/datemodified
217
+ lat: number;
218
+ lon: number;
219
+ distanceNm: number; // from the admitting reference (vessel or route)
220
+ bearingDeg: number; // degrees true from the same reference
221
+ link: string | null; // GDACS event page
222
+ }[];
223
+ zoneTransitions?: {
224
+ kind: 'enter' | 'leave'; // boundary crossing direction
225
+ territory: { name: string; iso_ter: string };
226
+ lat: number; // crossing position
227
+ lon: number;
228
+ distanceFromStartNm: number;
229
+ connectivity?: 'ocean'; // leave events: metered-ocean rules beyond
230
+ }[];
231
+ zonesHere?: { // here mode: waters the vessel sits in
232
+ layer: string; // 'internal' | 'archipelagic' | '12nm'
233
+ name: string;
234
+ iso_ter: string;
235
+ territory: string;
236
+ }[];
237
+ zoneDisclaimer?: string; // Marine Regions attribution + no-navigation notice
238
+ energyHourly?: {
239
+ timestamp: string; // ISO hour stamp
240
+ solarWh: number; // total ideal generation (solar+wind+hydro+alternator)
241
+ loadWh: number; // house consumption
242
+ netWh: number; // solarWh - loadWh
243
+ soc: number | null; // ideal SoC 0-1 at hour end
244
+ }[]; // signalk-energy-predictor forecast, 48 h
245
+ departure?: {
246
+ assumed: boolean; // auto daylight anchor vs crew override
247
+ time: string; // ISO — the schedule's anchor instant
248
+ reason: string; // 'underway' | 'next_dawn' | 'daylight_prep' | 'no_dawn' | 'manual'
249
+ };
147
250
  }
148
251
 
149
252
  interface TimeStepForecast {
@@ -152,6 +255,7 @@ interface TimeStepForecast {
152
255
  tws: number; // knots
153
256
  twd: number; // degrees true
154
257
  mslp: number; // hPa
258
+ cloudCover: number | null; // percent (0-100); null when the source carries none
155
259
  };
156
260
  marine: {
157
261
  hsCombined: number; // meters
@@ -351,6 +455,19 @@ $$a_z = a_{z,\text{base}} \cdot \mu_{\text{heel}} \cdot \mu_{\text{pitch}}$$
351
455
  * $0.630 \le a_z < 1.250\text{ m/s}^2 \implies \mathbf{Rough}$
352
456
  * $a_z \ge 1.250\text{ m/s}^2 \implies \mathbf{Sick}$
353
457
 
458
+ #### Slatting & Roll-Dampening Penalty (work doc #14)
459
+
460
+ On a monohull, aerodynamic pressure in the sails dampens roll. When
461
+ that pressure is lost but a residual swell remains, the snap-roll and
462
+ boom shock-loading can be worse than a gale — yet raw $a_z$ scores it
463
+ calm, because the sea state itself is modest. After the base rating:
464
+
465
+ 1. **Glassy-calm gate:** if $H_{s,\text{combined}} < 0.6\text{ m}$ there is not enough wave energy to roll the hull — the penalty is skipped entirely.
466
+ 2. **Dampening-loss thresholds:** otherwise the sails have lost their dampening pressure when
467
+ * upwind / reaching ($|TWA| < 90°$): $TWS < 7.0\text{ kt}$
468
+ * downwind / running ($|TWA| \ge 90°$): $TWS < 12.0\text{ kt}$ — the boat sails away from its wind, dropping apparent wind below flow-attachment.
469
+ 3. **Washing-machine penalty:** when the regime is detected, the vertical acceleration is multiplied by $\mu_{\text{slat}} = 2.5$ before ISO banding, and the hour's tier is forced to at least **Rough**. The hourly row carries a `slatting` tag; the tactical dashboard renders slatting-triggered Rough blocks with a diagonal hatch (alternating `--comfort-rough` over the card background) so the skipper sees the discomfort is light-air slatting, not heavy weather — alter course for a better wave angle, drop the main and motor, or lock the boom down.
470
+
354
471
 
355
472
 
356
473
  ### 5.3 Hazard Proximity Ray-Casting Algorithm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meri-imperiumi/signalk-passage-briefing",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Offshore passage daily briefing webapp for Signal K",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
@@ -34,12 +34,19 @@
34
34
  "@meri-imperiumi/signalk-energy-predictor",
35
35
  "@meri-imperiumi/signalk-internet",
36
36
  "signalk-polar-management"
37
+ ],
38
+ "recommends": [
39
+ "signalk-watch-schedule",
40
+ "signalk-ships-time"
37
41
  ]
38
42
  },
39
43
  "engines": {
40
44
  "node": ">=22.5.0"
41
45
  },
42
46
  "dependencies": {
47
+ "@openwaters/maritime-zones": "^0.2.1",
48
+ "astronomy-engine": "^2.1.19",
49
+ "satellite.js": "^7.1.0",
43
50
  "yaml": "^2.9.0"
44
51
  },
45
52
  "devDependencies": {