@meri-imperiumi/signalk-passage-briefing 0.9.0 → 0.11.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 (40) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -6
  3. package/SPEC.md +17 -7
  4. package/bin/blog-backtest.js +2509 -0
  5. package/bin/blog-gust-backfill.js +231 -0
  6. package/bin/blog-llm-labels.js +264 -0
  7. package/bin/blog-sail-state-join.js +263 -0
  8. package/package.json +1 -1
  9. package/plugin/bulletin-engine.js +63 -3
  10. package/plugin/celestial-ephemeris.js +81 -11
  11. package/plugin/fetch-engine.js +10 -3
  12. package/plugin/index.js +69 -10
  13. package/plugin/sea-legs.js +236 -0
  14. package/plugin/weather-source.js +4 -0
  15. package/public/brief-ext-widget.js +2 -2
  16. package/public/components/comfort-info.js +61 -2
  17. package/public/components/conditions-here.js +4 -1
  18. package/public/components/models.mjs +157 -8
  19. package/public/components/passage-outlook.js +20 -9
  20. package/public/components/passage-timeline.js +1 -0
  21. package/public/components/tactical-dashboard.js +5 -4
  22. package/public/css/visuals.css +4 -4
  23. package/public/route-sim.mjs +206 -1
  24. package/public/sereno-physics.mjs +164 -15
  25. package/public/tack-gybe.js +31 -0
  26. package/status-tiles-examples.json +2 -2
  27. package/tests/backtest.test.js +1 -1
  28. package/tests/blog-backtest.test.js +201 -0
  29. package/tests/blog-sail-state-join.test.js +76 -0
  30. package/tests/bulletin-engine.test.js +71 -2
  31. package/tests/fetch-engine.test.js +8 -0
  32. package/tests/openmeteo-mock.js +2 -0
  33. package/tests/plugin.test.js +99 -4
  34. package/tests/route-sim.test.js +171 -2
  35. package/tests/sea-legs.test.js +130 -0
  36. package/tests/sereno-physics.test.js +173 -8
  37. package/tests/statustilesexamples.test.js +1 -1
  38. package/tests/tack-gybe.test.js +15 -0
  39. package/tests/weather-source.test.js +3 -0
  40. package/tests/webapp.test.js +170 -15
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.0] - 2026-10-15
6
+
7
+ ### Added
8
+ - Blog backtest pilot (`bin/blog-backtest.js`): replays sailed days from the Lille Ø blog through the Sereno comfort model and sets the result against the crew's own words — the day window comes from consecutive blog `created` stamps, the crewed log entries (sail states, the crew's Douglas sea-state observations decoded from the logbook's Beaufort code) come from the logentries resource API, wind/speed/attitude/current/depth come from onboard history at the stored resolution, historical ERA5 waves and surface currents come from the Open-Meteo Marine archive at four track points (Pierson-Moskowitz wind-sea approximation as the labeled fallback), hours underway (hourly mean SOG ≥ 1.5 kn, overridable by sparse `navigation.state`) render per hour with archive-wave and PM tiers, the AWS/motion sub-tiers, measured attitude-reconstructed acceleration (steady hours only), and relative sea, current and land-lee angles, followed by day histograms, an inshore/offshore regime classification from sounding plus maritime-zones distance-to-land, and the quoted blog text. Summarize mode (`--summarize`) aggregates confusion matrices model-vs-narrative and model-vs-crew, the harshness-axis decomposition, slatting-by-AWS-and-wave-angle buckets, sea-legs passage-day gaps and current-opposition bins. Smoketests in `tests/blog-backtest.test.js`.
9
+ - LLM labeling pass (`bin/blog-llm-labels.js`): the one-time structured reading of every blog entry through the Cortecs API — comfort tier for the sailing parts only, narrative scope (sailing/anchored/mixed/land), slatting mentions with mitigation flag, fatigue, and the verbatim condition words — as the careful reading the keyword regexes cannot do; the labels join the summarize step via `--labels` for a scope-aware model-vs-LLM comparison.
10
+ - METAREA console area gating (bergie session, Tonga fix): the console headline belongs to the newest bulletin whose blocks actually match the vessel's waters, not to the newest cache entry whatever ocean it came from — a North Pacific synopsis ("LOW 59N167W … N OF 50N BETWEEN 170W AND 180W") no longer replaces the Fiji METAREA XIV warnings in Tonga. Entries and blocks whose own coordinates place them elsewhere are discarded even when severity keywords would have rescued unplaceable text; the standalone /api/bulletin route filters against the vessel position instead of an empty track; and the cardinal-bounds parser now accepts the NWS "N OF"/"S OF" abbreviations, so latitude bands no longer default pole-to-pole.
11
+ - Polar day/night naming (work doc #40 session feedback, prompted by signalk-energy-predictor #6): the celestial night entries classify each span explicitly (`polarNight`/`midnightSun` via an hourly altitude scan — no event-based no-crossing branch), the sky line says "polar night" / "midnight sun" instead of staying silent, and the route timeline fires entries when the sun regime begins or ends as the boat sails. The moon's once-a-month missing crossings read as "up all night" / "down all night" instead of stamps borrowed from a neighbouring night (the confusing new-moon numbers), and the night anchor falls back to civil dusk where nautical dusk does not exist.
12
+ - Sea-legs passage counter (work doc #40, informational): the plugin derives a restart-safe passage start by scanning the History API's navigation.state backward for the last stopped-to-underway transition (a stopgap behind a swappable seam until Signal K's trips resource lands), serves it on GET /api/sea-legs, and the route timeline gains neutral "Day N at sea" checkpoints on each passage day. No threshold math: the 470-day backtest found no support for exposure-based comfort easing — the crew-facing context stands on its own.
13
+ - Squall-risk episodes in the route timeline (`public/route-sim.mjs`, `public/components/models.mjs`): hours where the forecasted gust reaches 16 kn and at least double the base wind merge into a "Squall risk" timeline entry carrying the peak gust factor — the forecast itself saying the air is squally, where canvas may need to come off for the gusts and the reduced canvas then slats in the lulls. Gusts ride the hourly rows but deliberately never enter the comfort model: the 470-day backtest showed gust-peak tier evaluation overcalls the trades (hours with gusts 2+ bands above the mean sit predominantly on champagne/easy days).
14
+ - Slatting three-way rule in `public/sereno-physics.mjs` (`slattingRisk`): drawing sails suppress the forced-Rough flag, motoring keeps it only into steep head seas (Hs/Tp² at or over 0.02 — the 2022-09-12 broken-cars family), unknown sail state keeps the legacy behavior; sail state rides in `vessel.sailState`. Validated on the 470-day backtest harness: 53 days improved against the LLM narrative readings, 0 genuinely regressed (6 apparent regressions were histogram tie-break artifacts), harsher-rate down 10pp offshore / 4pp coastal / 2pp archipelago, protected gale days held at sporty-or-worse on motion alone. Sub-3 s encounter-period floor (`encounterPeriodFloorSeconds`) from the same harness pass: waves shorter than the waterline are bridged, not followed — 254 Baltic chop hours softened a tier, protected set untouched.
15
+ - Archive gust backfill (`bin/blog-gust-backfill.js`): ERA5 `wind_speed_10m`/`wind_gusts_10m` per hour onto the blog backtest dataset at the four track points — the gust-peak ground truth the onboard gust column cannot provide for the early years, plus the per-hour gust factor and an archive-vs-onboard sensor sanity check. Compare mode (`--compare baseline candidate`) turns the tool into the regression harness for comfort-model changes: protected-set violations (gale and crew-D5+ days must not read calmer), per-regime harsher-rate deltas, and LLM-label day-agreement movement.
16
+
17
+ ## [0.10.0] - 2026-10-14
18
+
19
+ ### Added
20
+ - Rain-shower episodes on the unified timeline (probable squalls): Open-Meteo `precipitation` (hourly mm/h) and `precipitation_probability` join the fetch, ride `weatherAt`'s interpolation under the same unknown-stays-null rule as cloud cover, and hours at or over the shower bar (≥ 0.5 mm/h rate or ≥ 50 % probability — whichever the model still publishes at that horizon) merge into contiguous `rainEpisodes` carrying the peak rate and probability. They render as info-level 🌧 timeline entries (“Rain showers · 2.5 mm/h · 80 % · until …”) — tropical rain rarely arrives without squalls, so probable rain is the crew's probable-squall advisory without claiming convection's warning weight. The sky line on tactical and here gains the now rain segment above the same bars (🌧 65 %, or 🌧 0.8 mm/h when the model publishes only a rate — drier hours stay quiet); Weather API provider mode publishes no precipitation fields and degrades to absent rather than inventing them.
21
+
22
+ ### Changed
23
+ - Two comfort tiers renamed for the project's modified Sereno scale — `coffee` is now `sporty` ("Lively enough that a hot coffee needs a firm grip.") and `sick` is now `miserable` ("Nobody is enjoying this. Seasickness very likely.") — the canonical identifiers everywhere: physics module, CSS tier tokens (`--comfort-sporty`/`--comfort-miserable`), the embed widget's color map, the status-tile example mapping, and the Signal K value the plugin publishes. Briefings compiled before the rename serve raw old tier names — unstyled, never wrong-colored — until their next compile rewrites them; no compatibility mapping, one unambiguous scale.
24
+ - Sail-change and maneuver timeline entries name the point of sail: `Genoa 1 + Main — broad reach · 9.5 kn TWS · Hs 1.1 m · coffee · at dawn`. The signed TWA at the change point (nearest hourly step, wind-from-starboard positive, same convention as the maneuver tack calls) rides the event's conditions block as `twaDeg`; the timeline renders it as the standard angle-band text (close-hauled / close reach / beam reach / broad reach / running, sign-blind since the maneuver label already calls the tack). Payloads compiled before the field degrade to the previous detail line without inventing an angle.
25
+
26
+ ### Fixed
27
+ - Conditions here showed two "Update now" buttons stacked when no cached conditions existed: the no-cached-conditions strip carried its own button while the conditions card head directly below always carries one. The strip now stays text-only in here mode (the card-head button is the refresh affordance there, present with and without cached data); the route-mode strip keeps its button, as the tab views have no other on-screen refresh.
28
+ - The comfort-scale legend (the ℹ️ popover) opened past the right edge of a phone screen, dragging the page into a sideways scroll with half the legend unreachable. While open it now pins to the viewport — clamped inside the window on both axes, flipping above the summary when the space below runs out, internally scrollable when taller than the screen, and repositioning on scroll, resize and tier-highlight changes.
29
+ - The conditions-here celestial & space events card rendered raw ISO timestamps (`2026-10-12T07:06:12.650Z Tiangong pass: …`) instead of the app's formatted stamps: the renderer read a `stamp` field the plugin never sets, falling through to the raw event timestamp. Events now render through the shared ship's-time formatter like every other view (`MM-DD HH:MM` with the zone offset, `MM-DD HH:MMZ` in UTC when the vessel publishes no timezone), per the Signal K visuals time-formatting rules.
30
+ - Energy timeline events rendered their detail stamps in UTC (`battery depleted by 06-22 03:00Z`) while the same rows' timeline stamp renders in ship's time — a leftover from before the ship's-time work, forcing the crew to do arithmetic between two renderings of the same instant. The `until`/`battery depleted by` details now use the shared ship's-time formatter like every other stamp.
31
+
5
32
  ## [0.9.0] - 2026-10-12
6
33
 
7
34
  ### Added
package/README.md CHANGED
@@ -7,14 +7,14 @@ Offshore passage daily briefing webapp for Signal K: a plugin that plans and rev
7
7
  - Simulates the passage hourly from the boat's actual position: speed from the vessel's polar, current added, ETA percentiles for the remaining route
8
8
  - Recommends sail changes hour by hour from what the crew actually did, learned from the electronic logbook
9
9
  - Rates every hour on the Sereno comfort scale and flags when light air in a residual swell will be worse than a gale
10
- - Renders one unified timeline: sail work, tacks and gybes, clock changes, territorial waters, official alerts, sky events and hazards in order
10
+ - Renders one unified timeline: sail work, tacks and gybes, clock changes, territorial waters, official alerts, sky events, rain showers and hazards in order
11
11
  - Fetches bulletins, official structured alerts, global disaster events and synoptic charts for the waters the route actually crosses
12
12
  - Keeps working offline: everything fetches in the online window and survives the other 23 hours from the cache
13
13
  - Shows a mini summary on the chart plotter and learns from every logbook entry the crew writes
14
14
 
15
15
  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.
16
16
 
17
- 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).
17
+ Part of the Lille Ø offshore suite, alongside [@meri-imperiumi/signalk-energy-predictor](https://github.com/meri-imperiumi/signalk-energy-predictor) and [@meri-imperiumi/signalk-logbook](https://github.com/meri-imperiumi/signalk-logbook).
18
18
 
19
19
  ## Data sources
20
20
 
@@ -32,13 +32,13 @@ Part of the Lille Ø offshore suite, alongside [@meri-imperiumi/signalk-energy-p
32
32
  - [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
33
33
  - [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
34
34
  - [@signalk/sailsconfiguration](https://www.npmjs.com/package/@signalk/sailsconfiguration) — sail inventory used to filter free-text noise out of log entries
35
- - 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
35
+ - 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, the upper-air fields behind the convective warnings and the precipitation fields behind the rain-shower episodes degrade to absent rather than being invented
36
36
 
37
37
  ### External
38
38
 
39
- 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.
39
+ 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, upper-air and precipitation fields the Weather API providers don't publish.
40
40
 
41
- - [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)
41
+ - [Open-Meteo Forecast API](https://open-meteo.com/en/docs) — surface wind, gusts, MSL pressure, CAPE, precipitation (hourly rate and probability) and the pressure-layer fields behind the K-index (best-match model)
42
42
  - [NOAA SWPC planetary K-index forecast](https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json) — geomagnetic activity behind the aurora advisories
43
43
  - [JPL SBDB query API](https://ssd-api.jpl.nasa.gov/doc/sbdb_query.html) — comet brightness parameters (M1/K1) behind the Sky Notes comets
44
44
  - [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. The same service's per-catalog queries (`gp.php?CATNR=…`) supply the current elements of decaying satellites named by Space-Track's reentry predictions
@@ -64,7 +64,7 @@ All external fetches are online-gated and cached to the plugin data directory, s
64
64
 
65
65
  ## Acknowledgments
66
66
 
67
- 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).
67
+ 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. Ours is a modified Sereno comfort scale, not a direct copy: the thresholds and the encounter-period motion math are adapted for a monohull (heel and waterline-pitch resonance instead of catamaran beam-roll and bridgedeck slam), the tier names with their meanings are our own — Champagne, Easy, Sporty, Rough, Miserable — and the slatting penalty (light air over a residual swell snap-rolling the boat, which raw acceleration scores as calm) is a vector upstream lacks entirely; a catamaran slats far less, and few models bother with light-air discomfort. The ISO 2631-1 comfort bands complete the model.
68
68
 
69
69
  ## License
70
70
 
package/SPEC.md CHANGED
@@ -287,6 +287,8 @@ interface TimeStepForecast {
287
287
  twd: number; // degrees true
288
288
  mslp: number; // hPa
289
289
  cloudCover: number | null; // percent (0-100); null when the source carries none
290
+ precip: number | null; // mm/h; null when the source carries none
291
+ precipProbability: number | null; // percent (0-100); null when unknown
290
292
  };
291
293
  marine: {
292
294
  hsCombined: number; // meters
@@ -439,15 +441,17 @@ Executed inside `public/worker.js` for time steps $\Delta t = 1.0\text{ hour}$:
439
441
 
440
442
  ### 5.2 Monohull Sereno Comfort Index Formulation
441
443
 
444
+ A modified Sereno comfort scale, not a direct copy of the upstream planner's: the band thresholds and motion math derive from passage-weather, the tier names and their one-line meanings are this project's own, and the slatting & roll-dampening penalty below is a vector upstream has nothing of — a catamaran slats far less, and light-air discomfort rarely gets modeled.
445
+
442
446
  #### Vector 1: Apparent Wind Speed Rating
443
447
 
444
448
  $$AWS = \sqrt{TWS^2 + STW^2 + 2 \cdot TWS \cdot STW \cdot \cos(TWA)}$$
445
449
 
446
450
  * $AWS < 12\text{ kt} \implies \mathbf{Champagne}$
447
451
  * $12\text{ kt} \le AWS < 18\text{ kt} \implies \mathbf{Easy}$
448
- * $18\text{ kt} \le AWS < 23\text{ kt} \implies \mathbf{Coffee}$
452
+ * $18\text{ kt} \le AWS < 23\text{ kt} \implies \mathbf{Sporty}$
449
453
  * $23\text{ kt} \le AWS < 33\text{ kt} \implies \mathbf{Rough}$
450
- * $AWS \ge 33\text{ kt} \implies \mathbf{Sick}$
454
+ * $AWS \ge 33\text{ kt} \implies \mathbf{Miserable}$
451
455
 
452
456
  #### Vector 2: Monohull Vertical Acceleration ($a_z$)
453
457
 
@@ -482,9 +486,9 @@ $$a_z = a_{z,\text{base}} \cdot \mu_{\text{heel}} \cdot \mu_{\text{pitch}}$$
482
486
 
483
487
  * $a_z < 0.15\text{ m/s}^2 \implies \mathbf{Champagne}$
484
488
  * $0.15 \le a_z < 0.315\text{ m/s}^2 \implies \mathbf{Easy}$
485
- * $0.315 \le a_z < 0.630\text{ m/s}^2 \implies \mathbf{Coffee}$
489
+ * $0.315 \le a_z < 0.630\text{ m/s}^2 \implies \mathbf{Sporty}$
486
490
  * $0.630 \le a_z < 1.250\text{ m/s}^2 \implies \mathbf{Rough}$
487
- * $a_z \ge 1.250\text{ m/s}^2 \implies \mathbf{Sick}$
491
+ * $a_z \ge 1.250\text{ m/s}^2 \implies \mathbf{Miserable}$
488
492
 
489
493
  #### Slatting & Roll-Dampening Penalty (work doc #14)
490
494
 
@@ -527,9 +531,9 @@ $$\text{IntersectCount} = \sum_{j=1}^{k} \text{RayCrossesSegment}(P_{\text{inter
527
531
  --color-fg: #ffcc00;
528
532
  --color-champagne: #00ffcc;
529
533
  --color-easy: #00ff00;
530
- --color-coffee: #ffcc00;
534
+ --color-sporty: #ffcc00;
531
535
  --color-rough: #ff6600;
532
- --color-sick: #ff0000;
536
+ --color-miserable: #ff0000;
533
537
  --font-family: 'Courier New', Courier, monospace;
534
538
  }
535
539
 
@@ -565,6 +569,12 @@ function filterExceptions(simulationResult) {
565
569
  totalFuelGal: simulationResult.fuelConsumptionGal,
566
570
  macroSeaAnomalies: simulationResult.seaStateAnomalies.filter(a => a.steepnessRatio < 3.28),
567
571
  convectiveWarnings: simulationResult.upperAirAnomalies.filter(u => u.cape > 1000 || u.kIndex > 28),
572
+ // Rain-shower episodes: hours at or over the shower bar
573
+ // (precip ≥ 0.5 mm/h or probability ≥ 50 %) merged into one
574
+ // entry each with the peak rate and probability. The crew reads
575
+ // probable rain as probable squalls in the tropics — info-level
576
+ // timeline entries, not warnings
577
+ rainEpisodes: mergeEpisodes(simulationResult.rainHours),
568
578
  // Meteor-shower nights evaluated along the track (work doc #37):
569
579
  // per (shower, night) spaceEvents gated at the night's nautical-dusk
570
580
  // anchor, the radiant checked at the track's forecasted position;
@@ -605,7 +615,7 @@ Displays:
605
615
 
606
616
  #### `<horizon-sparkline>` (SVG Renderer)
607
617
 
608
- Renders a 24-column SVG bar chart where bar background color maps to `comfortLevel` (Champagne, Easy, Coffee, Rough, Sick) and bar height represents $AWS$.
618
+ Renders a 24-column SVG bar chart where bar background color maps to `comfortLevel` (Champagne, Easy, Sporty, Rough, Miserable) and bar height represents $AWS$.
609
619
 
610
620
  ---
611
621