signalk-pypilot-newui 3.1.4 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.2.0 — 2026-10-15 — AIS rose rebuilt on real positions, tide popup, chart kept warm
4
+
5
+ Rolls up Rev470 → Rev482 on top of 3.1.4. Minor bump: new UI (tide
6
+ button + popup, "Chart in the background" card, anchor-watch visor
7
+ embedded in the rose) plus a set of AIS rose fixes found in port QA in
8
+ Moaña. No breaking changes.
9
+
10
+ ### Fixed — AIS targets on the rose (root cause)
11
+
12
+ - **The target position/SOG/COG cache never filled.** The REST calls
13
+ used `encodeURIComponent` on the vessel URN
14
+ (`urn%3Amrn%3Aimo%3Ammsi%3A…`) and Signal K answers 404 to that; the
15
+ raw URN answers 200. With an empty cache the hull fell back to
16
+ `cpaPositions.target` — the target's FUTURE position at CPA — so a
17
+ boat coming from 7 o'clock was drawn at 11 o'clock and "0 m" away.
18
+ The fallback is removed: no current position → no hull on the rose
19
+ (the infobox still shows the alarm). Cache refresh 30 s → 5 s.
20
+ (Rev474)
21
+ - **Hull and projected track were drawn 180° off.** The sprite has its
22
+ bow at +x, so a direction needs `rotate(angle − 90)`; Rev457 used
23
+ `+ 90`. (Rev470)
24
+ - **Moored boats.** A stopped target's COG is GPS noise: the hull is
25
+ oriented by `headingTrue` when available, by COG only when moving
26
+ (≥ 0.5 kn), otherwise it points at us; the dashed track is drawn only
27
+ for moving targets and always along COG. The "stationary" filter
28
+ (now 0.5 kn) is re-evaluated when the REST cache lands, not only on
29
+ the alarm delta. (Rev470, Rev471)
30
+ - **Targets drawn on top of our own hull.** Radial position is now a
31
+ log scale (50 m → 2 NM) over the free ring between our hull outline
32
+ and the compass ring, with a distance label ("40 m", "0.4 NM") under
33
+ each target. (Rev472)
34
+ - **Target name in the infobox** comes from the Signal K vessel `name`
35
+ first, then from the alarm text; "MMSI 123456789" is no longer shown
36
+ as a name. (Rev481)
37
+ - **AIS card:** the Harbour / At anchor / Sailing selector showed raw
38
+ i18n keys; all texts rewritten in plain language (EN/ES). (Rev473)
39
+ - **Unstyled flash on reload:** critical inline CSS in `<head>` and
40
+ `style.css?v=RevNNN` cache-busting from `deploy.ps1`. (Rev476)
41
+
42
+ ### Added
43
+
44
+ - **Tide button** above the bottom-right corner tile of the rose (shown
45
+ only when `signalk-mareas-ihm` is installed). Opens a popup with the
46
+ station, current height, rising/falling, coefficient, next HW/LW and
47
+ a −6 h → +12 h tide curve. "Open full Tides" opens the
48
+ signalk-mareas-ihm anchor-watch visor inside the rose, same chrome as
49
+ Freeboard (maximize / close). Data from `/signalk-mareas-ihm/api/extremes`
50
+ and `environment.tide.*`, fetched only when the popup opens.
51
+ (Rev478 → Rev480, Rev482)
52
+ - **Freeboard-SK kept loaded in the background** so a double-tap on the
53
+ rose opens it instantly with AIS names already in place. Own card
54
+ "Chart in the background" with a per-device switch and a "?" that
55
+ explains when to turn it off (lag / battery). (Rev475 → Rev477)
56
+
3
57
  ## 3.1.4 — 2026-10-14 — iOS "Add to Home Screen" PWA basics (issue #5)
4
58
 
5
59
  Rev469. Patch bump: no code changes, just the HTML shell. Thanks to
package/README.md CHANGED
@@ -277,7 +277,10 @@ on its own), but installing them unlocks the features listed.
277
277
  — Double-tap the compass rose to open Freeboard-SK embedded inside
278
278
  the rose box (side panels stay visible and usable). Lets you drop
279
279
  from the autopilot view to the full chart with every AIS target in
280
- one tap, without leaving the pypilot-newui session. **Pair with a
280
+ one tap, without leaving the pypilot-newui session. Freeboard is kept
281
+ loaded in the background so it opens instantly (per-device switch in
282
+ the "Chart in the background" card if a weak tablet prefers lazy
283
+ loading). **Pair with a
281
284
  Signal K chart provider (e.g. `@signalk/charts-plugin`) or the chart
282
285
  area will stay empty** — see
283
286
  [Chart provider is required](#chart-provider-is-required-for-the-embedded-plotter).
@@ -286,13 +289,24 @@ on its own), but installing them unlocks the features listed.
286
289
  - **[`signalk-collision-alerts`](https://www.npmjs.com/package/signalk-collision-alerts)**
287
290
  by @dirkwa — Provides the CPA/TCPA engine behind our AIS collision
288
291
  overlay. Pypilot-newui subscribes to
289
- `notifications.navigation.closestApproach.*` and renders a red /
290
- orange triangle on the rose + an infobox with ACK that routes
292
+ `notifications.navigation.closestApproach.*` and draws each target
293
+ on the rose at its real bearing and distance (log scale, distance
294
+ label), oriented by its heading / course with a projected track line
295
+ when it is moving, plus an infobox with ACK that routes
291
296
  through the Signal K v2 Notifications API, so an ACK from
292
297
  pypilot-newui is also seen by Freeboard-SK and every other SK
293
298
  client.
294
299
  See `docs/_feature_ais_overlay.md`.
295
300
 
301
+ - **[`signalk-mareas-ihm`](https://www.npmjs.com/package/signalk-mareas-ihm)**
302
+ ("AnchorWatch Pro: Smart Anchoring, AIS & Tides" in the App Store) —
303
+ **Recommended.** Unlocks the tide button on the rose (popup with the
304
+ tide curve, next HW/LW and coefficient) and opens its anchor-watch
305
+ visor inside the rose. It also fills AIS vessel names into the Signal
306
+ K tree from its own persistent AIS database, so pypilot-newui,
307
+ Freeboard-SK and collision-alerts show names instead of raw MMSIs
308
+ right after a server restart.
309
+
296
310
  - **[`pypilot-autopilot-provider`](https://www.npmjs.com/package/pypilot-autopilot-provider)**
297
311
  by Panaaj — Shares the single pypilot socket cleanly with our
298
312
  autopilot provider (see next section) so KIP, Freeboard's Autopilot
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ const trip_recorder_1 = require("./trip-recorder");
28
28
  const maneuver_trace_1 = require("./maneuver-trace");
29
29
  // Rev counter bumped on every build so the user can distinguish deploys
30
30
  // from the webapp header (feedback_revision_bump_each_build).
31
- const PLUGIN_REVISION = "Rev469";
31
+ const PLUGIN_REVISION = "Rev482";
32
32
  // Rev59: read package.json once at load time so /status can report the
33
33
  // npm package version alongside the internal Rev counter.
34
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -193,7 +193,7 @@ module.exports = function (app) {
193
193
  let _tackStatePrev = null;
194
194
  let _tackDirection = null;
195
195
  // Rev283: persistent servo error log. Writes JSONL to dataDir on
196
- // every off→on transition of a servo-* alarm rule and remembers the
196
+ // every off→on transition of a servo-* alarm rule and remembers the
197
197
  // 30 s pre-fault window from the historian.
198
198
  let servoErrorLog = null;
199
199
  // Rev286 (B2): profile advisor + last-published state for the SK
@@ -223,9 +223,9 @@ module.exports = function (app) {
223
223
  // Rev323 (Carlos, 2026-09-27): pseudo-modo visor-side. El visor lo
224
224
  // posta a `/maneuver-state` al arrancar / cerrar Aproado o Empopado.
225
225
  // Se usa para silenciar heading-deviation / cruise-drift / unable-
226
- // to-steer durante la maniobra, INDEPENDIENTE de `ap.tack.state` —
226
+ // to-steer durante la maniobra, INDEPENDIENTE de `ap.tack.state` —
227
227
  // en modo wind pypilot NO obedece y ap.tack.state se queda en "none"
228
- // aunque el visor esté pilotando la maniobra por otras vías.
228
+ // aunque el visor esté pilotando la maniobra por otras vías.
229
229
  let _maneuverPseudoActive = false;
230
230
  let lastNavState = null;
231
231
  let sessionSampleTimer = null;
@@ -382,24 +382,24 @@ module.exports = function (app) {
382
382
  sensorsIgnored: {
383
383
  type: "array",
384
384
  title: "Ignored sensor SK paths",
385
- description: "List of Signal K paths the sailor has marked as 'not on this boat' from Setup → Sensor Quality. Ignored sensors stop turning the Setup chip red without hiding the row. Managed from the visor; edit here only if you know the exact path names.",
385
+ description: "List of Signal K paths the sailor has marked as 'not on this boat' from Setup → Sensor Quality. Ignored sensors stop turning the Setup chip red without hiding the row. Managed from the visor; edit here only if you know the exact path names.",
386
386
  items: { type: "string" },
387
387
  default: [],
388
388
  },
389
389
  tripRecorderEnabled: {
390
390
  type: "boolean",
391
- title: "Record logbook (bitácora) trips",
392
- description: "When ON, the plugin auto-opens a trip on every leg away from moored, records position + wind + speed samples, and closes on return with a summary + KPI. Files stay on the Pi 5. Managed from Setup → Logbook.",
391
+ title: "Record logbook (bitácora) trips",
392
+ description: "When ON, the plugin auto-opens a trip on every leg away from moored, records position + wind + speed samples, and closes on return with a summary + KPI. Files stay on the Pi 5. Managed from Setup → Logbook.",
393
393
  default: true,
394
394
  },
395
395
  maneuverTraceEnabled: {
396
396
  type: "boolean",
397
397
  title: "Trace user maneuvers vs. pypilot response (sea-trial forensics)",
398
- description: "When ON, every maneuver-related action from the visor (aproado / empopado / tack / mode change / target PUT / engage) is logged along with pypilot state before and 300 ms after the command, so we can audit whether pypilot actually obeyed. Files land under <plugin data dir>/maneuver-trace/*.jsonl. Off by default — enable only during a sea trial.",
398
+ description: "When ON, every maneuver-related action from the visor (aproado / empopado / tack / mode change / target PUT / engage) is logged along with pypilot state before and 300 ms after the command, so we can audit whether pypilot actually obeyed. Files land under <plugin data dir>/maneuver-trace/*.jsonl. Off by default — enable only during a sea trial.",
399
399
  default: false,
400
400
  },
401
401
  // Rev299 (2026-09-24): per-rule alarm enable list. Managed from
402
- // the visor's Setup → Alarms card. NO schema default here on
402
+ // the visor's Setup → Alarms card. NO schema default here on
403
403
  // purpose: an existing install that upgraded from a pre-Rev299
404
404
  // plugin must not have its (implicit) alarms silenced by a
405
405
  // schema default of []. The visor is responsible for prompting
@@ -407,7 +407,7 @@ module.exports = function (app) {
407
407
  alarmsEnabled: {
408
408
  type: "array",
409
409
  title: "Alarms enabled (rule ids)",
410
- description: "List of alarm rule ids that are active. When absent the plugin keeps its legacy behaviour (every rule on). Managed from the visor's Setup → Alarms card; edit here only if you know the rule ids (heading-deviation, servo-overcurrent, low-voltage, etc.).",
410
+ description: "List of alarm rule ids that are active. When absent the plugin keeps its legacy behaviour (every rule on). Managed from the visor's Setup → Alarms card; edit here only if you know the rule ids (heading-deviation, servo-overcurrent, low-voltage, etc.).",
411
411
  items: { type: "string" },
412
412
  },
413
413
  // Rev167 (Carlos): the Smart Pilot toggles (auto-profile, gust
@@ -438,8 +438,8 @@ module.exports = function (app) {
438
438
  });
439
439
  // Rev271 (audit R05): propagate the allowWrites gate to the
440
440
  // single write path in PypilotClient.set(). Every route to a
441
- // pypilot write — provider interface, PUT handlers on action
442
- // paths, momentary switches — goes through set(), so this
441
+ // pypilot write — provider interface, PUT handlers on action
442
+ // paths, momentary switches — goes through set(), so this
443
443
  // check covers them all.
444
444
  client.setAllowWrites(props.allowWrites !== false);
445
445
  client.on("connect", () => {
@@ -511,7 +511,7 @@ module.exports = function (app) {
511
511
  if (changedField)
512
512
  pushAutopilotUpdate(changedField);
513
513
  }
514
- // Rev299 (I2): watch ap.tack.state for the tacking → none
514
+ // Rev299 (I2): watch ap.tack.state for the tacking → none
515
515
  // transition, which is the moment the sailor needs the
516
516
  // catch-up nudge. `ap.tack.direction` is watched separately
517
517
  // so we already know which side we tacked onto by the time
@@ -1275,7 +1275,7 @@ module.exports = function (app) {
1275
1275
  // reject embedded webapps served over Signal K. Routing tiles
1276
1276
  // through this backend lets us send the User-Agent that OSM's
1277
1277
  // policy requires (self-identifying app string) and centralises
1278
- // caching. Zero external dependencies — Node 18+ has fetch built
1278
+ // caching. Zero external dependencies — Node 18+ has fetch built
1279
1279
  // in, browser handles per-tile caching via Cache-Control.
1280
1280
  router.get("/tiles/:z/:x/:y.png", async (req, res) => {
1281
1281
  const z = parseInt(req.params.z, 10);
@@ -1300,7 +1300,7 @@ module.exports = function (app) {
1300
1300
  }
1301
1301
  const buf = Buffer.from(await upstream.arrayBuffer());
1302
1302
  res.setHeader("Content-Type", "image/png");
1303
- // Cache 24 h in the browser — OSM tiles are effectively
1303
+ // Cache 24 h in the browser — OSM tiles are effectively
1304
1304
  // immutable within a day; a hard reload still fetches fresh.
1305
1305
  res.setHeader("Cache-Control", "public, max-age=86400, immutable");
1306
1306
  res.send(buf);
@@ -1627,7 +1627,7 @@ module.exports = function (app) {
1627
1627
  // PUT /raw { name: "ap.tack.direction", value: "port|starboard" }
1628
1628
  // PUT /raw { name: "ap.tack.state", value: "begin" }
1629
1629
  // Rev378-380 put the virtual-tack FSM behind the POST /ap/tack/:dir
1630
- // endpoint, but the frontend never calls it — the raw path bypassed
1630
+ // endpoint, but the frontend never calls it — the raw path bypassed
1631
1631
  // our provider.tack() entirely (seen in trace-20261001-161853 QA
1632
1632
  // 2026-10-01 16:49: ap.tack.state=begin forwarded to pypilot
1633
1633
  // unchanged, mode stayed "wind" throughout, no virtual-tack).
@@ -1635,14 +1635,14 @@ module.exports = function (app) {
1635
1635
  // active, re-route it through the FSM and DO NOT forward the raw
1636
1636
  // write to pypilot (otherwise pypilot runs its own buggy wind
1637
1637
  // tack in parallel). The direction write that typically precedes
1638
- // "begin" by a few ms is harmless — pypilot stores it but our
1638
+ // "begin" by a few ms is harmless — pypilot stores it but our
1639
1639
  // FSM derives direction from its own argument.
1640
1640
  // Rev382 (Carlos, 2026-10-01): memorise the direction the
1641
1641
  // frontend sends in the ap.tack.direction raw write so the
1642
1642
  // state=begin that follows ~100ms later uses the FRESH value.
1643
1643
  // Rev381 QA on Tunatunes read ap.tack.direction from the
1644
1644
  // pypilot values cache which still carried the previous tack's
1645
- // "starboard" when the user had just pressed "port" — the
1645
+ // "starboard" when the user had just pressed "port" — the
1646
1646
  // intercept launched the wrong direction (16:58:42 journal).
1647
1647
  if (name === "ap.tack.direction" && (value === "port" || value === "starboard")) {
1648
1648
  app._pypilotNewuiLastTackDirection = {
@@ -1663,7 +1663,7 @@ module.exports = function (app) {
1663
1663
  ? dirCache
1664
1664
  : (dirRaw === "port" || dirRaw === "starboard" ? dirRaw : "starboard");
1665
1665
  // eslint-disable-next-line no-console
1666
- console.log(`[/raw tack intercept] mode=${modeStr} dir=${dir} (cache=${dirCache ?? "n/a"} cache=${dirRaw ?? "n/a"}) → virtual-tack`);
1666
+ console.log(`[/raw tack intercept] mode=${modeStr} dir=${dir} (cache=${dirCache ?? "n/a"} cache=${dirRaw ?? "n/a"}) → virtual-tack`);
1667
1667
  apProvider
1668
1668
  .toProviderInterface()
1669
1669
  .tack(dir, apProvider.deviceId)
@@ -1897,7 +1897,7 @@ module.exports = function (app) {
1897
1897
  // Rev416 (Carlos sea trial 2026-10-04, obs #12): accept an
1898
1898
  // optional `finalAngleRad` so the visor can trim the destination
1899
1899
  // AWA (closer-hauled or bear-away) at tack-time. Values outside
1900
- // (-π, π] or non-finite are discarded by startVirtualTack.
1900
+ // (-π, π] or non-finite are discarded by startVirtualTack.
1901
1901
  const rawFinalAngle = req.body?.finalAngleRad;
1902
1902
  const finalAngleRadOverride = typeof rawFinalAngle === "number" && Number.isFinite(rawFinalAngle)
1903
1903
  ? rawFinalAngle : null;
@@ -2567,7 +2567,7 @@ module.exports = function (app) {
2567
2567
  });
2568
2568
  // Rev291 (F1): APB target preview. Reads the current SK course
2569
2569
  // data via app.getCourse and returns what the AP *would* steer to
2570
- // if the plugin drove NAV directly. Diagnostic only — pypilot's
2570
+ // if the plugin drove NAV directly. Diagnostic only — pypilot's
2571
2571
  // own nav mode is still the one commanding the pilot.
2572
2572
  router.get("/nav/apb-preview", async (_req, res) => {
2573
2573
  try {
@@ -2603,7 +2603,7 @@ module.exports = function (app) {
2603
2603
  }
2604
2604
  });
2605
2605
  // Rev296 (Carlos, navigating - bug D): audit trail of profile
2606
- // switches. Answers "who cargó default?" — shows the last N
2606
+ // switches. Answers "who cargó default?" — shows the last N
2607
2607
  // changes with source attribution (user / auto-profile /
2608
2608
  // gust-heavy / doctor / external / etc).
2609
2609
  router.get("/profile-change-log", (_req, res) => {
@@ -2997,9 +2997,9 @@ module.exports = function (app) {
2997
2997
  // flaky 4G link. This endpoint accepts {events: [{kind, payload,
2998
2998
  // visorRev}, ...]} and records them in order. Malformed events
2999
2999
  // are skipped silently (never block the batch). The visor side
3000
- // (public/app.js _mtLog + _mtFlush) encola localmente y envía
3001
- // cada 2 s en vez de uno a uno — ~20x menos HTTP overhead en
3002
- // navegación con muchos deltas.
3000
+ // (public/app.js _mtLog + _mtFlush) encola localmente y envía
3001
+ // cada 2 s en vez de uno a uno — ~20x menos HTTP overhead en
3002
+ // navegación con muchos deltas.
3003
3003
  router.post("/maneuver-trace/events", (req, res) => {
3004
3004
  if (!maneuverTrace)
3005
3005
  return res.status(503).json({ error: "not initialised" });
@@ -3650,12 +3650,12 @@ module.exports = function (app) {
3650
3650
  rollFfTwaGateDeg: clampNumber(options.rollFfTwaGateDeg, 30, 179, 90),
3651
3651
  // Rev298 (H4): leeway adjustment. Practical range 9..12 for a
3652
3652
  // keelboat. 0 = disabled (nothing published). Upper cap 30 keeps
3653
- // a stray fat-finger config from producing a 40° leeway.
3653
+ // a stray fat-finger config from producing a 40° leeway.
3654
3654
  leewayAdjustment: clampNumber(options.leewayAdjustment, 0, 30, 0),
3655
3655
  // Rev299 (H2): failsafe boat speed in knots. Clamped 0..15;
3656
3656
  // 0 = disabled. Practical range 3..8 kn for a cruising keelboat.
3657
3657
  failSafeBspKn: clampNumber(options.failSafeBspKn, 0, 15, 0),
3658
- // Rev299 (I2): tack catch-up. Peak 0..15° (0 disables); tau
3658
+ // Rev299 (I2): tack catch-up. Peak 0..15° (0 disables); tau
3659
3659
  // 1..15 s. Defaults keep the module inert; sailor opts in
3660
3660
  // after sea trial validation.
3661
3661
  tackCatchupDeg: clampNumber(options.tackCatchupDeg, 0, 15, 0),
@@ -3665,7 +3665,7 @@ module.exports = function (app) {
3665
3665
  profileAdvisorRmsLowDeg: clampNumber(options.profileAdvisorRmsLowDeg, 0.1, 5, 1),
3666
3666
  profileAdvisorSustainSec: clampNumber(options.profileAdvisorSustainSec, 15, 600, 60),
3667
3667
  // Rev289 (B5): sanitise the persisted metadata against the current
3668
- // schema — a plugin downgrade could otherwise leave stray rows
3668
+ // schema — a plugin downgrade could otherwise leave stray rows
3669
3669
  // with unknown conditions.
3670
3670
  profileMetadata: (0, profile_metadata_1.loadMetadata)(options.profileMetadata),
3671
3671
  // Rev290 (E2/E3): threshold overrides, clamped to sane ranges.
@@ -3674,8 +3674,8 @@ module.exports = function (app) {
3674
3674
  alarmServoMotorTempC: clampNumber(options.alarmServoMotorTempC, 40, 90, 70),
3675
3675
  alarmPypilotDiscSec: clampNumber(options.alarmPypilotDiscSec, 3, 300, 15),
3676
3676
  // Rev299 (I1): attitude envelope thresholds. Ranges kept wide
3677
- // enough for coastal cruisers (25°) and racers who purposely
3678
- // put the rail under (60°). Defaults are conservative.
3677
+ // enough for coastal cruisers (25°) and racers who purposely
3678
+ // put the rail under (60°). Defaults are conservative.
3679
3679
  alarmAttitudeHeelDeg: clampNumber(options.alarmAttitudeHeelDeg, 15, 70, 45),
3680
3680
  alarmAttitudePitchDeg: clampNumber(options.alarmAttitudePitchDeg, 10, 45, 25),
3681
3681
  // Rev299: per-rule enable list. Preserve undefined so legacy
@@ -3707,14 +3707,14 @@ module.exports = function (app) {
3707
3707
  if (!apProvider)
3708
3708
  return;
3709
3709
  // Rev387 (Carlos, 2026-10-01, GPT/Gemini consult): REVERTED Rev386
3710
- // masking. Both LLMs flagged it as a bus pollution bug — other SK
3710
+ // masking. Both LLMs flagged it as a bus pollution bug — other SK
3711
3711
  // clients (KIP, derived-data, etc.) subscribe to steering.autopilot.*
3712
3712
  // and need the TRUTH about mode/target, not a convenient visor-side
3713
3713
  // lie. Masking belongs in the VISOR's presentation layer, consuming
3714
3714
  // the dedicated `steering.autopilot.virtualTack` path we now publish
3715
3715
  // alongside the canonical deltas. Rev388 frontend will do the
3716
3716
  // display-side override.
3717
- // Rev394: 'virtualTack' is a VT-only notification — skip the
3717
+ // Rev394: 'virtualTack' is a VT-only notification — skip the
3718
3718
  // autopilotUpdate call and the canonical delta block below, only
3719
3719
  // emit the virtualTack path itself (which may now carry `null`
3720
3720
  // telling the visor to drop its shield).
@@ -3745,8 +3745,8 @@ module.exports = function (app) {
3745
3745
  // publishes ONLY state/engaged/availableActions. This kills the
3746
3746
  // race where a parallel /engage + /target pair produced two deltas
3747
3747
  // each carrying both fields; whichever landed second would clobber
3748
- // the sibling with a stale copy (the "DIA jumps to 115° then back
3749
- // to 75°" Carlos reported on Rev82). "all" is still used by the
3748
+ // the sibling with a stale copy (the "DIA jumps to 115° then back
3749
+ // to 75°" Carlos reported on Rev82). "all" is still used by the
3750
3750
  // 30-s keep-alive and startup push.
3751
3751
  const values = [];
3752
3752
  if (fields === "all" || fields === "engaged") {
@@ -3787,7 +3787,7 @@ module.exports = function (app) {
3787
3787
  // Rev410 (fix T.1-A, Carlos 2026-10-04): prefer the authoritative
3788
3788
  // `remainingDeg` written by _runVirtualTack (distance to the FINAL
3789
3789
  // compass target, computed against the REAL heading after each
3790
- // step reached — accounts for Rev407 recompute). Fallback to the
3790
+ // step reached — accounts for Rev407 recompute). Fallback to the
3791
3791
  // old per-step stale calculation only when the FSM has not written
3792
3792
  // one yet (very first snapshot, pre-publishState race).
3793
3793
  const remainingDeg = (typeof vt.remainingDeg === "number")
@@ -3808,7 +3808,7 @@ module.exports = function (app) {
3808
3808
  direction: vt.direction,
3809
3809
  windMode: vt.windMode,
3810
3810
  // Rev394: backend-computed kind so visor doesn't misdetect
3811
- // from |AWA| vs 90°. "tack" = rotation crosses bow; "jibe"
3811
+ // from |AWA| vs 90°. "tack" = rotation crosses bow; "jibe"
3812
3812
  // = crosses stern.
3813
3813
  maneuverKind: g?.maneuverKind ?? null,
3814
3814
  finalWindTargetRad: g?.angleNewRad ?? null,
@@ -3892,7 +3892,7 @@ module.exports = function (app) {
3892
3892
  return;
3893
3893
  // Rev328 (Carlos, 2026-09-28): master switch. When the sailor
3894
3894
  // disables the logbook from Setup we neither open new trips nor
3895
- // record samples into an open one — but we DO close any trip that
3895
+ // record samples into an open one — but we DO close any trip that
3896
3896
  // was already recording so its summary lands cleanly on disk.
3897
3897
  if (props.tripRecorderEnabled === false) {
3898
3898
  if (tripRecorder.isRecording()) {
@@ -3908,7 +3908,7 @@ module.exports = function (app) {
3908
3908
  // Transitions.
3909
3909
  // Rev280 (audit T10): also open a trip when the plugin boots
3910
3910
  // with the vessel already off "moored". Previously the "start"
3911
- // path only fired on a moored → sailing transition, so restarting
3911
+ // path only fired on a moored → sailing transition, so restarting
3912
3912
  // Signal K mid-outing missed the entire session and stored zero
3913
3913
  // trips. Treat lastNavState==null (boot) the same as coming from
3914
3914
  // "moored" for the purpose of opening a new trip.
@@ -4223,7 +4223,7 @@ module.exports = function (app) {
4223
4223
  }
4224
4224
  const currentProfile = client.getValues()["profile"];
4225
4225
  if (currentProfile !== targetProfile) {
4226
- _autoProfileLastReason = `TWS avg ${avgKn.toFixed(1)} kn → band ${targetBand} → profile ${targetProfile}`;
4226
+ _autoProfileLastReason = `TWS avg ${avgKn.toFixed(1)} kn → band ${targetBand} → profile ${targetProfile}`;
4227
4227
  try {
4228
4228
  profileChangeLog.markPlannedWrite(targetProfile, "auto-profile", _autoProfileLastReason);
4229
4229
  client.set("profile", targetProfile);
@@ -4495,7 +4495,7 @@ module.exports = function (app) {
4495
4495
  };
4496
4496
  }
4497
4497
  // Rev322 (Carlos, 2026-09-27): snapshot pypilot + SK state for the
4498
- // maneuver trace log. Cheaper than collectSample() — no historian
4498
+ // maneuver trace log. Cheaper than collectSample() — no historian
4499
4499
  // aggregations, no derivations. Read once, fill the flat object,
4500
4500
  // return. Called on every trace event pre + 300 ms later.
4501
4501
  function _captureManeuverContext() {
@@ -4547,9 +4547,9 @@ module.exports = function (app) {
4547
4547
  // with state / method / message so any downstream SK client honours
4548
4548
  // them out of the box.
4549
4549
  // Rev299: apply the per-rule enable list to the AlarmEngine.
4550
- // undefined = legacy → every rule keeps its defaultEnabled (=true
4550
+ // undefined = legacy → every rule keeps its defaultEnabled (=true
4551
4551
  // for pre-Rev299 installs, so Tunatunes carries on unchanged).
4552
- // Array = user-selected → only listed ids are enabled, everything
4552
+ // Array = user-selected → only listed ids are enabled, everything
4553
4553
  // else is silenced. Empty array = fresh install = ALL silent.
4554
4554
  // Rev299 (I2): watch pypilot values that describe the tack state
4555
4555
  // machine so we can trigger the catch-up module on completion.
@@ -4628,10 +4628,10 @@ module.exports = function (app) {
4628
4628
  // Rev311 (Carlos sea trial 2026-09-25): flag para las reglas
4629
4629
  // de heading-error / drift / unable-to-steer. Durante una
4630
4630
  // virada nativa de pypilot (ap.tack.state != "none") el error
4631
- // instantáneo es enorme por diseño; silenciamos esas alarmas.
4632
- // `_tackStatePrev` lo mantiene observeTackTransition() al día en
4633
- // el mismo callback de client.on("value"), así que siempre
4634
- // refleja el último valor conocido de pypilot.
4631
+ // instantáneo es enorme por diseño; silenciamos esas alarmas.
4632
+ // `_tackStatePrev` lo mantiene observeTackTransition() al día en
4633
+ // el mismo callback de client.on("value"), así que siempre
4634
+ // refleja el último valor conocido de pypilot.
4635
4635
  maneuverInProgress: !!(_tackStatePrev && _tackStatePrev !== "none"),
4636
4636
  // Rev323 (Carlos, 2026-09-27): flag pseudo-modo del visor.
4637
4637
  maneuverPseudoActive: _maneuverPseudoActive,
@@ -4834,7 +4834,7 @@ module.exports = function (app) {
4834
4834
  }
4835
4835
  // Rev286 (B2): drive the profile advisor from the sampler tick.
4836
4836
  // Publishes a SK notification only on state transitions to avoid
4837
- // flooding — the same idempotency the AlarmEngine uses.
4837
+ // flooding — the same idempotency the AlarmEngine uses.
4838
4838
  function tickProfileAdvisor(nowMs) {
4839
4839
  if (!profileAdvisor || !kpis || !props.profileAdvisorEnabled)
4840
4840
  return;
@@ -4848,7 +4848,7 @@ module.exports = function (app) {
4848
4848
  publishProfileAdvisorNotification(ev);
4849
4849
  }
4850
4850
  else {
4851
- // No new event — but if we previously published an advisory and
4851
+ // No new event — but if we previously published an advisory and
4852
4852
  // the cooldown has expired, revert the notification to normal so
4853
4853
  // downstream SK clients stop showing the banner.
4854
4854
  const st = profileAdvisor.status(nowMs);
@@ -4931,7 +4931,7 @@ module.exports = function (app) {
4931
4931
  const meta = [
4932
4932
  { path: "steering.autopilot.pypilot.tuning.rollFf.gain", value: { description: "Roll FF gain (unitless, output = -gain*rollHp). 0 = off." } },
4933
4933
  { path: "steering.autopilot.pypilot.tuning.rollFf.rollHpRad", value: { units: "rad", description: "High-pass filtered roll (dynamic component only)." } },
4934
- { path: "steering.autopilot.pypilot.tuning.rollFf.deltaRad", value: { units: "rad", description: "Feed-forward heading delta this tick. Computed even when disabled — but only non-zero when gain>0, AP engaged, and |TWA| above the gate." } },
4934
+ { path: "steering.autopilot.pypilot.tuning.rollFf.deltaRad", value: { units: "rad", description: "Feed-forward heading delta this tick. Computed even when disabled — but only non-zero when gain>0, AP engaged, and |TWA| above the gate." } },
4935
4935
  { path: "steering.autopilot.pypilot.tuning.rollFf.appliedToAp", value: { description: "False until a later Rev flips the enable switch after sea trial." } },
4936
4936
  ];
4937
4937
  app.handleMessage(PLUGIN_ID, {
@@ -4949,7 +4949,7 @@ module.exports = function (app) {
4949
4949
  app.debug(`[roll-ff] publish failed: ${e?.message || e}`);
4950
4950
  }
4951
4951
  }
4952
- // Rev298 (H4) → Rev300: publish the leeway estimate on our own
4952
+ // Rev298 (H4) → Rev300: publish the leeway estimate on our own
4953
4953
  // namespace instead of the canonical `performance.leeway`. Two
4954
4954
  // reasons: (1) signalk-derived-data already publishes on that
4955
4955
  // canonical path with a different formula and the sailor was
@@ -4966,7 +4966,7 @@ module.exports = function (app) {
4966
4966
  const meta = [
4967
4967
  { path: LEEWAY_PATH, value: {
4968
4968
  units: "rad",
4969
- description: "Estimated leeway (side-slip) angle from the plugin's own estimator. Formula: drift_deg = adj * heel_deg / bsp_kn^2 with adj set by the sailor (Setup → Smart Pilot → Sensor helpers). Positive = drifting to port. Published only when leewayAdjustment > 0. Kept off the canonical performance.leeway path so it does not overwrite signalk-derived-data.",
4969
+ description: "Estimated leeway (side-slip) angle from the plugin's own estimator. Formula: drift_deg = adj * heel_deg / bsp_kn^2 with adj set by the sailor (Setup → Smart Pilot → Sensor helpers). Positive = drifting to port. Published only when leewayAdjustment > 0. Kept off the canonical performance.leeway path so it does not overwrite signalk-derived-data.",
4970
4970
  } },
4971
4971
  ];
4972
4972
  app.handleMessage(PLUGIN_ID, {
@@ -4990,7 +4990,7 @@ module.exports = function (app) {
4990
4990
  }
4991
4991
  // Rev299 (I2): publish the current tack catch-up delta as a
4992
4992
  // diagnostic path so downstream SK clients can watch the decay.
4993
- // NOT applied to the pilot yet — the sea trial gates that switch.
4993
+ // NOT applied to the pilot yet — the sea trial gates that switch.
4994
4994
  let tackCatchupMetaSent = false;
4995
4995
  function publishTackCatchupPaths() {
4996
4996
  if (!tackCatchup)
@@ -5344,7 +5344,7 @@ module.exports = function (app) {
5344
5344
  // refusal. The pre-check at the top of this handler covers
5345
5345
  // `!client.connected`, but `client.set` can still refuse on
5346
5346
  // zombie socket (no pong > 5s), coreOffline flag or
5347
- // writes-disabled config — all of which pre-Rev411 landed as
5347
+ // writes-disabled config — all of which pre-Rev411 landed as
5348
5348
  // "COMPLETED 200" to KIP/SK clients.
5349
5349
  if (!client.set(name, value)) {
5350
5350
  return { state: "COMPLETED", statusCode: 503, message: "pypilot write refused" };
@@ -6011,7 +6011,7 @@ module.exports = function (app) {
6011
6011
  // resident for subscribers; re-emitting the CURRENT
6012
6012
  // apProvider.data value periodically produced double-pushes
6013
6013
  // that raced with the real change delta and got collapsed by
6014
- // minPeriod:500 on the visor's subscription — the sailor saw
6014
+ // minPeriod:500 on the visor's subscription — the sailor saw
6015
6015
  // engaged=false held for ~10 s after an external engage.
6016
6016
  // The KIP-specific momentary + mode radio + engage switch
6017
6017
  // values still ship (KIP UI needs them refreshed to stay in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-pypilot-newui",
3
- "version": "3.1.4",
3
+ "version": "3.2.0",
4
4
  "description": "Signal K friendly UI for the pypilot autopilot, with first-class Signal K paths and Simple Switches so any downstream Signal K client can drive or monitor the pilot. Touch-first mobile visor + full pypilot data (servo, gains, calibration, tack, warnings). Coexists with pypilot-autopilot-provider.",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {