signalk-underkeel 0.6.1 → 0.6.2

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
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to signalk-underkeel are documented here.
4
4
 
5
+ ## [0.6.2] - 2026-11-13
6
+
7
+ ### Fixed
8
+
9
+ - Closing the details panel no longer logs an error in the Signal K server log. The panel
10
+ subscribed to `notifications.navigation.underKeel` itself, and on close Freeboard sent
11
+ Signal K an unsubscribe for that one path, which Signal K refuses (it takes only an
12
+ unsubscribe of everything). The background page, which never closes, now holds that
13
+ subscription and passes the message to the panel.
14
+
5
15
  ## [0.6.1] - 2026-11-13
6
16
 
7
17
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-underkeel",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Under-keel clearance awareness for Signal K: look-ahead grounding risk, route checks and a living chart.",
5
5
  "main": "lib/index.js",
6
6
  "files": [
package/public/panel.js CHANGED
@@ -145,19 +145,45 @@ async function refreshRoutes() {
145
145
  }
146
146
  }
147
147
 
148
- if (client.hasCapability("signalk.stream")) {
149
- await client.signalk.subscribe(["notifications.navigation.underKeel"], (ev) => {
150
- text("message", ev.value && typeof ev.value === "object" ? ev.value.message : "");
151
- if (!document.hidden) {
152
- void refreshStatus();
148
+ /** Reads overlap as the status ones do: only the latest may update the message. */
149
+ let noticeRequest = 0;
150
+
151
+ /** The notification's message, which the background page keeps in the extension state. */
152
+ async function refreshNotice() {
153
+ const id = ++noticeRequest;
154
+ try {
155
+ const { ukcNotice } = await client.state.get(["ukcNotice"], "extension");
156
+ if (id !== noticeRequest) {
157
+ return;
158
+ }
159
+ text("message", ukcNotice && typeof ukcNotice.message === "string" ? ukcNotice.message : "");
160
+ } catch (err) {
161
+ if (id !== noticeRequest) {
162
+ return;
153
163
  }
154
- });
164
+ // An old message left on screen could say normal over a warning; the poll reads it again.
165
+ text("message", "Under-keel message unavailable: the chart host did not answer.");
166
+ console.warn("under-keel notice", err);
167
+ }
155
168
  }
169
+
170
+ // No Signal K subscription of its own: the host would unsubscribe it path by path when the
171
+ // panel closes, which Signal K refuses and logs as an error. The background page relays it.
156
172
  await client.subscribe(["state.changed"], (_name, params) => {
157
- if (params && params.scope === "extension" && params.keys.includes("routeCheck")) {
173
+ if (!params || params.scope !== "extension") {
174
+ return;
175
+ }
176
+ if (params.keys.includes("routeCheck")) {
158
177
  void refreshRoutes();
159
178
  }
179
+ if (params.keys.includes("ukcNotice")) {
180
+ void refreshNotice();
181
+ if (!document.hidden) {
182
+ void refreshStatus();
183
+ }
184
+ }
160
185
  });
186
+ void refreshNotice();
161
187
  // The background runtime answers the button, over an event the host must carry.
162
188
  const recheck = document.getElementById("recheck");
163
189
  recheck.hidden = !["events.publish", "routes", "background.iframe"].every((c) => client.hasCapability(c));
@@ -176,6 +202,7 @@ function checkStatusAge() {
176
202
  function poll() {
177
203
  void refreshStatus();
178
204
  void refreshRoutes();
205
+ void refreshNotice();
179
206
  }
180
207
 
181
208
  // A hidden page polls nothing; back on screen an old status reads unknown until the poll answers.
package/public/runtime.js CHANGED
@@ -427,11 +427,64 @@ if (has("routes")) {
427
427
  }, RECHECK_MS);
428
428
  }
429
429
 
430
+ // ---- the notification, relayed to the details panel ----
431
+ /**
432
+ * The panel opens and closes, and the chart host unsubscribes a closed page's Signal K paths
433
+ * one by one, which Signal K refuses: it only takes an unsubscribe of everything, and logs the
434
+ * rest as errors. So this page, which never closes, holds the notification's subscription and
435
+ * keeps its message in the extension state, where the panel reads it.
436
+ */
437
+ const NOTICE = "notifications.navigation.underKeel";
438
+ /** A write that fails is tried again this many times, each wait longer by this much. */
439
+ const NOTICE_RETRIES = 5;
440
+ const NOTICE_RETRY_MS = 2000;
441
+ /** The last notice received and not given up on, as JSON; a newer one replaces a retry of an older one. */
442
+ let latestNotice;
443
+ let noticeRetry;
444
+
445
+ function relayNotice(value) {
446
+ const n = value && typeof value === "object" ? value : {};
447
+ const notice = { message: typeof n.message === "string" ? n.message : "", state: n.state };
448
+ const key = JSON.stringify(notice);
449
+ if (key === latestNotice) {
450
+ return;
451
+ }
452
+ latestNotice = key;
453
+ clearTimeout(noticeRetry);
454
+ void writeNotice(notice, key, 0);
455
+ }
456
+
457
+ /**
458
+ * Writes the notice to the extension state. A notification is sent only when it changes, so
459
+ * a write that fails is tried again rather than left until the next one.
460
+ */
461
+ async function writeNotice(notice, key, tries) {
462
+ try {
463
+ await client.state.set({ ukcNotice: notice }, "extension");
464
+ } catch (err) {
465
+ if (key !== latestNotice) {
466
+ return;
467
+ }
468
+ if (tries < NOTICE_RETRIES) {
469
+ noticeRetry = setTimeout(() => void writeNotice(notice, key, tries + 1), NOTICE_RETRY_MS * (tries + 1));
470
+ } else {
471
+ // Given up: the same notice, sent again on a reconnect, is written rather than dropped.
472
+ latestNotice = undefined;
473
+ console.warn("under-keel notice", err);
474
+ }
475
+ }
476
+ }
477
+
430
478
  /** Without the Signal K stream, or when subscribing to it fails, the envelope is polled. */
431
479
  let streaming = false;
432
480
  if (has("signalk.stream")) {
433
481
  try {
434
- await client.signalk.subscribe(Object.keys(onPath), (ev) => {
482
+ await client.signalk.subscribe([...Object.keys(onPath), NOTICE], (ev) => {
483
+ // A notification is sent only when it changes, so its age says nothing about the plugin.
484
+ if (ev.path === NOTICE) {
485
+ relayNotice(ev.value);
486
+ return;
487
+ }
435
488
  const left = fresh(ev.path, ev.timestamp, Date.now());
436
489
  if (left > 0) {
437
490
  onPath[ev.path](ev.value, left);