@meri-imperiumi/signalk-dead-reckoning 0.14.0 → 0.14.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
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.14.2] - 2026-10-19
11
+
12
+ ### Fixed
13
+ - **Routes and tracks crossing the antimeridian no longer draw a line around the planet.** An active-route leg straddling ±180° longitude (Fiji↔Samoa, Tonga's eastern islands, the Bering Strait) rendered as a horizontal stroke spanning the whole chart width — Leaflet joins two raw longitudes straight through the primary world frame instead of hopping the seam. Route lines draw unwrapped (`vm.unwrapLineLongitudes`: every point shifted ±360° onto the previous point's longitude strip) and repeat on the ±360° world copies the view can sit over (`vm.worldCopies`), so the leg renders as the short hop over the dateline with waypoint markers and the "next" emphasis intact; the ghost (DR) and GPS tracks get the same treatment, as a boat logging fixes across the seam produced the identical artifact in its track.
14
+
15
+ ## [0.14.1] - 2026-10-19
16
+
17
+ ### Added
18
+ - **Warning region shapes on the chart (signalk-passage-briefing work doc #48).** The passage-briefing plugin now publishes METAREA and CAP warning areas as `regions` resources (native Polygon/MultiPolygon features paired with their point notes) — the chart draws them: fetched with the same view-scoped query as the notes (`/signalk/v2/api/resources/regions`, matched by geometry-bbox distance), rendered on a "Regions" layers-control toggle mounted by default, in the plugin console's visual language — severity red for `GALE`/`HURRICANE`/`DENSE FOG`/… keyword texts (the shared highlighter), bulletin orange otherwise, heavier stroke for severe, and a dashed outline for the briefing plugin's own bbox-approximate areas (the honesty cue that the shape is the queried bbox). Rings unwrap against the view's longitude frame so a METAREA spanning the dateline draws as one polygon, and both published geometry shapes are accepted (standard-nested and the bulletin engine's flat coordinate chains). A note whose region shape draws stays hidden — the extent renders once — and clicking a shape opens the pick menu at the clicked point with the href-paired note's detail (title, body, Edit/Delete), the same surface as clicking the note marker.
19
+ - **The chart pick menu shows the clicked coordinates** (helm request): the picked position renders in the watchkeeper's own position format (decimal/DM/DMS, shared with the sight panel) above the bearing and distance rows.
20
+
21
+ ### Fixed
22
+ - **Radar-range chart picks seed the clicked object's coordinates again.** `seedObjectPosition` looked the coord fieldset up at shadow-root level, and both the vertical (distance-CPL) and radar forms carry a `data-prefix="center"` fieldset — the query returned the vertical form's (first in document order), so a "Radar range at" pick switched to the radar tab but filled the hidden distance-CPL form, leaving the radar form's Object position empty. The lookup is now scoped to the switched-to form.
23
+
24
+ ### Changed
25
+ - **Chartplotter zoom and follow-mode handling (helm feedback).** Wheel zoom is now continuous and fast: Leaflet's stock ScrollWheelZoom — which quantizes each debounced wheel burst to whole zoom levels and plays an animation per level (a jerky staircase), and whose sigmoid curve is meaningless under fractional zoom, making a detent move the chart ~0.01 levels — is replaced by a custom rAF-batched handler: wheel deltas accumulate per animation frame and apply as an un-animated fractional zoom at stock Leaflet speed (~one level per detent) with no jumps and no debounce lag. The anchor is follow-aware across every zoom gesture and locked to the BOAT'S POSITION rather than the map center: the wheel handler re-centers each animation frame on the DR position (the center can drift from the vessel between ticks — keyboard pans never fire dragstart, extensions may recenter — and anchoring the center would freeze that drift for the whole gesture, which read as "zoom snaps to whatever was the map center"), while pinch snaps the boat to center at touchstart (before Leaflet captures the gesture anchor) and double-click snaps back after Leaflet's center jump; with follow off the gestures anchor at the cursor as before, and follow is authoritative over the view while engaged: the ◎ re-center pans to the follow target un-animated (immediately) — the live DR position, falling back to the GNSS fix while the DR engine is idle so the button never engages into a silent no-op, with the camera locked onto DR once its positions flow (`vm.followTarget`: a live DR position always wins, so GPS fixes ticking faster than the DR engine cannot drag the view between the two marks; a stale DR position transfers to the fix exactly once, and DR resuming takes the camera back), the keyboard pan handler stands down while following (arrow pans never fire dragstart, so the view would otherwise drift off the vessel and snap back at the next DR tick), and the plotter-extension `map.center`/`map.fitBounds` affordances are refused with a `VIEW_LOCKED` error while follow is engaged (the host reads the lock through a dedicated `getFollow` accessor — `getMap` keeps returning the raw Leaflet map the overlay machinery hooks) — freeform view moves happen only once follow is released. Also moved `customElements.define("dr-map-view")` after the `NauticalScaleControl` class declaration — the synchronous upgrade of an in-DOM element runs `connectedCallback` → `initMap` → `new NauticalScaleControl()`, which threw "Cannot access before initialization" on any page where the element exists before the module evaluates (found by driving the map headless). Follow mode is now **off by default** — the map still opens centered on the vessel (one-shot fit on the first GPS fix, or the first DR position when no fix has arrived) but the helm keeps control of the view. The ◎ button is a real toggle: click to engage follow (and pan to the DR position, clearing the settle window so it always takes effect at once), click again to let go; dragging the chart lets go as before. The zoom-dependent re-renders (range rings, laylines, downstream note refetch) run on a trailing debounce instead of per zoom step.
26
+
10
27
  ## [0.14.0] - 2026-10-18
11
28
 
12
29
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meri-imperiumi/signalk-dead-reckoning",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Offline-first dead reckoning and sensor fusion engine for Signal K",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
package/public/dr-app.js CHANGED
@@ -140,10 +140,7 @@ template.innerHTML = /* html */ `
140
140
  look, the glance gets the gist. */
141
141
  .dr-status {
142
142
  margin: 0;
143
- max-width: 22rem;
144
143
  white-space: nowrap;
145
- overflow: hidden;
146
- text-overflow: ellipsis;
147
144
  font-family: ui-monospace, "Fira Code", monospace;
148
145
  font-size: 0.75rem;
149
146
  color: var(--text-muted);
@@ -158,8 +155,7 @@ template.innerHTML = /* html */ `
158
155
  --theme-color: var(--color-green);
159
156
  color: var(--color-green);
160
157
  }
161
- .dr-status.alert,
162
- .dr-status.retrying {
158
+ .dr-status.alert {
163
159
  --theme-color: var(--color-red);
164
160
  color: var(--color-red);
165
161
  font-weight: 600;
@@ -179,29 +175,44 @@ template.innerHTML = /* html */ `
179
175
  background: var(--color-teal);
180
176
  color: var(--bg-base);
181
177
  }
182
- /* Failover control — top-right panel, inline with the status
183
- badge: one row, same vertical size as the tools panel. Alternate-
184
- power semantics: orange, red when engaged (DR authoritative).
185
- The button keeps its minimum width — SPEC §14.1: prominent,
186
- always human-initiated. */
178
+ /* Stream link badge — [ONLINE]/[OFFLINE], color-coded; the full
179
+ connection detail (reconnecting…, which endpoint) rides the
180
+ badge's tooltip. */
181
+ .dr-link {
182
+ font-family: ui-monospace, "Fira Code", monospace;
183
+ font-size: 0.75rem;
184
+ color: var(--color-orange);
185
+ }
186
+ .dr-link.online {
187
+ color: var(--color-green);
188
+ }
189
+ .dr-link.offline {
190
+ color: var(--color-red);
191
+ font-weight: 600;
192
+ }
193
+ /* Failover control — top-right panel, one compact badge row:
194
+ [ONLINE] DR[ON] GNSS[OK] [OVERRIDE]. Alternate-power semantics:
195
+ orange, red when engaged (DR authoritative). The button stays
196
+ prominent (SPEC §14.1: always human-initiated) without eating
197
+ the top bar. */
187
198
  .dr-gps {
188
199
  display: flex;
189
200
  flex-direction: row;
190
201
  align-items: center;
191
202
  justify-content: flex-end;
192
- gap: 0.75rem;
203
+ gap: 0.5rem;
193
204
  flex-wrap: wrap;
194
205
  }
195
206
  .dr-override {
196
207
  display: flex;
197
208
  align-items: center;
198
209
  justify-content: flex-end;
199
- gap: 0.75rem;
210
+ gap: 0.4rem;
200
211
  flex-wrap: wrap;
201
212
  }
202
213
  .dr-override button {
203
214
  --theme-color: var(--color-orange);
204
- min-width: 13rem;
215
+ min-width: 5.5rem;
205
216
  }
206
217
  .dr-override button.engaged,
207
218
  .dr-override button.engaged:hover,
@@ -218,6 +229,9 @@ template.innerHTML = /* html */ `
218
229
  letter-spacing: 0.08em;
219
230
  color: var(--text-muted);
220
231
  }
232
+ .dr-override #dr-override-state.off {
233
+ color: var(--color-orange);
234
+ }
221
235
  /* Pending-observations drawer: docked left, below the entry
222
236
  tools; the list scrolls inside, the panel never grows past the
223
237
  viewport. The whole box (toggle included) disappears when there
@@ -357,7 +371,6 @@ template.innerHTML = /* html */ `
357
371
  }
358
372
  .dr-status {
359
373
  font-size: 0.7rem;
360
- max-width: 14rem;
361
374
  }
362
375
  .dr-override button {
363
376
  min-width: 11rem;
@@ -421,19 +434,20 @@ template.innerHTML = /* html */ `
421
434
  <div class="dr-top-col">
422
435
  <section class="sk-floating dr-tools">
423
436
  <button id="btn-sight">⊕ Sight / LOP</button>
424
- <button id="btn-coord-fix" title="Confirm a fix at coordinates — prefilled from the current GNSS position, editable for offline/known-position fixes">⊙ Fix at coordinates</button>
437
+ <button id="btn-coord-fix" title="Fix at coordinates — prefilled from the current GNSS position, editable for offline/known-position fixes">⊙ FIX AT</button>
425
438
  <button id="btn-pending" aria-expanded="false" aria-controls="dr-pending-drawer" hidden>◧ Pending</button>
426
439
  </section>
427
440
  <dr-ext-widget-area anchor="top-left"></dr-ext-widget-area>
428
441
  </div>
429
442
  <div class="dr-top-col dr-top-col-right">
430
443
  <section class="sk-floating dr-gps">
431
- <div class="dr-status" id="dr-status-panel">
432
- <span id="dr-status-text">Connecting to Signal K…</span>
444
+ <span class="dr-link" id="dr-link" title="Connecting to Signal K…">[CONNECTING…]</span>
445
+ <div class="dr-status" id="dr-status-panel" title="No dead-reckoning data">
446
+ <span id="dr-status-text">DR[—]</span>
433
447
  </div>
434
448
  <div class="dr-override">
435
- <button id="dr-override-btn">Engage OVERRIDE</button>
436
- <span id="dr-override-state">NORMAL (GPS authoritative)</span>
449
+ <span id="dr-override-state" title="Normal — the GNSS fix is authoritative">GNSS[OK]</span>
450
+ <button id="dr-override-btn" title="Toggle DR-authoritative override — while engaged the dead-reckoned position steers the chart and the GNSS fix is not trusted">OVERRIDE</button>
437
451
  </div>
438
452
  </section>
439
453
  <dr-ext-widget-area anchor="top-right"></dr-ext-widget-area>
@@ -783,6 +797,11 @@ class DrApp extends HTMLElement {
783
797
  * source of truth is the server's v2 resources API; the cache lets
784
798
  * saves update markers without a full refetch. */
785
799
  this.notesById = new Map();
800
+ /** Signal K regions cache (passage-briefing work doc #48): id →
801
+ * region resource. The warning shapes published alongside the
802
+ * notes — fetched with the same view-scoped query, drawn on the
803
+ * chart as polygon extents. */
804
+ this.regionsById = new Map();
786
805
  /** Bounds the last in-view notes fetch covered (fetchNotesInView
787
806
  * margin guard) — null until the first view-driven fetch.
788
807
  * @type {{west: number, south: number, east: number, north: number}|null} */
@@ -906,6 +925,7 @@ class DrApp extends HTMLElement {
906
925
  // The chart, once it exists — map overlays composite onto it and
907
926
  // the map capability reads/drives its viewport.
908
927
  getMap: () => this.map?.map ?? null,
928
+ getFollow: () => this.map?.follow ?? false,
909
929
  });
910
930
  // The host hooks the Leaflet events itself, but discovery can land
911
931
  // before the chart exists — forward the element's own view-change
@@ -1256,15 +1276,32 @@ class DrApp extends HTMLElement {
1256
1276
  async fetchNotes(view) {
1257
1277
  try {
1258
1278
  const query = view ? `?${vm.notesViewQuery(view)}` : "";
1259
- const res = await fetch(`/signalk/v2/api/resources/notes${query}`);
1260
- if (!res.ok) return;
1261
- const collection = await res.json();
1262
- if (view) {
1263
- for (const [id, note] of Object.entries(collection ?? {})) {
1264
- this.notesById.set(id, note);
1279
+ const [notesRes, regionsRes] = await Promise.all([
1280
+ fetch(`/signalk/v2/api/resources/notes${query}`),
1281
+ fetch(`/signalk/v2/api/resources/regions${query}`),
1282
+ ]);
1283
+ if (notesRes.ok) {
1284
+ const collection = await notesRes.json();
1285
+ if (view) {
1286
+ for (const [id, note] of Object.entries(collection ?? {})) {
1287
+ this.notesById.set(id, note);
1288
+ }
1289
+ } else {
1290
+ this.notesById = new Map(Object.entries(collection ?? {}));
1291
+ }
1292
+ }
1293
+ // Regions ride the same view-scoped query: the regions provider
1294
+ // matches by geometry-bbox distance (a region has no position
1295
+ // of its own — the paired note is the queryable anchor).
1296
+ if (regionsRes.ok) {
1297
+ const collection = await regionsRes.json();
1298
+ if (view) {
1299
+ for (const [id, region] of Object.entries(collection ?? {})) {
1300
+ this.regionsById.set(id, region);
1301
+ }
1302
+ } else {
1303
+ this.regionsById = new Map(Object.entries(collection ?? {}));
1265
1304
  }
1266
- } else {
1267
- this.notesById = new Map(Object.entries(collection ?? {}));
1268
1305
  }
1269
1306
  this.renderNotes();
1270
1307
  } catch {
@@ -1296,8 +1333,16 @@ class DrApp extends HTMLElement {
1296
1333
 
1297
1334
  /** @returns {void} */
1298
1335
  renderNotes() {
1299
- const collection = Object.fromEntries(this.notesById);
1300
- this.map?.renderNotes(vm.notesRenderSpecs(collection), this.notesById);
1336
+ const notes = Object.fromEntries(this.notesById);
1337
+ const regions = Object.fromEntries(this.regionsById);
1338
+ const referenceLon = this.map?.map?.getCenter()?.lng ?? null;
1339
+ const shapes = vm.regionShapes(regions, referenceLon);
1340
+ this.map?.renderRegions(shapes, this.regionsById);
1341
+ // A note whose region shape draws stays hidden: the region carries
1342
+ // the warning's true extent, the note is its anchored detail — the
1343
+ // pair renders once (href-linked, `*-area-` ids).
1344
+ const drawn = new Set(shapes.map((shape) => shape.id));
1345
+ this.map?.renderNotes(vm.notesRenderSpecs(notes, drawn), this.notesById);
1301
1346
  }
1302
1347
 
1303
1348
  /**
@@ -1964,18 +2009,21 @@ class DrApp extends HTMLElement {
1964
2009
  renderOverride(active) {
1965
2010
  if (!this.btn || !this.stateLabel) return;
1966
2011
  this.btn.classList.toggle("engaged", active);
1967
- this.btn.textContent = active ? "Release OVERRIDE" : "Engage OVERRIDE";
1968
- this.stateLabel.textContent = active
1969
- ? "OVERRIDE (DR authoritative)"
1970
- : "NORMAL (GPS authoritative)";
2012
+ this.stateLabel.classList.toggle("off", active);
2013
+ this.stateLabel.textContent = active ? "GNSS[OFF]" : "GNSS[OK]";
2014
+ this.stateLabel.title = active
2015
+ ? "Override engaged — the dead-reckoned position is authoritative; the GNSS fix is not trusted"
2016
+ : "Normal — the GNSS fix is authoritative";
1971
2017
  }
1972
2018
 
1973
2019
  /**
1974
- * Renders the DR engine state — explains why the readout is empty
1975
- * when idle (moored, no speed/heading) so the user isn't left
1976
- * guessing.
2020
+ * Renders the DR engine state as a compact badge (DR[ON], DR[IDLE],
2021
+ * DR[FOULED]…) with the full explanatory text on the badge's
2022
+ * tooltip — the helm reads the row at a glance and hovers for the
2023
+ * why.
1977
2024
  *
1978
- * @param {{status: string, reason?: string}|null} value
2025
+ * @param {{status: string, reason?: string, moving?: boolean,
2026
+ * fouled?: boolean, transient?: boolean}|null} value
1979
2027
  * @returns {void}
1980
2028
  */
1981
2029
  renderDrState(value) {
@@ -1983,43 +2031,54 @@ class DrApp extends HTMLElement {
1983
2031
  const text = this.shadowRoot.querySelector("#dr-status-text");
1984
2032
  if (!panel || !text) return;
1985
2033
  panel.classList.remove("idle", "underway", "alert", "transient");
1986
- if (!value) {
1987
- text.textContent = "No dead-reckoning data";
1988
- return;
2034
+ let title = "No dead-reckoning data";
2035
+ if (value) {
2036
+ if (value.status === "idle") {
2037
+ panel.classList.add("idle");
2038
+ if (value.moving) panel.classList.add("alert");
2039
+ } else if (value.status === "underway") {
2040
+ panel.classList.add("underway");
2041
+ if (value.fouled) panel.classList.add("alert");
2042
+ else if (value.transient) panel.classList.add("transient");
2043
+ } else if (value.status === "warm" && value.fouled) {
2044
+ // The engine runs warm on a tied-up boat (SPEC §5) — muted,
2045
+ // unless the speed sensor is crying wolf.
2046
+ panel.classList.add("alert");
2047
+ }
2048
+ title =
2049
+ vm.drStatusText({ ...value, navState: this.vesselNavState }) ?? title;
1989
2050
  }
1990
- if (value.status === "idle") {
1991
- panel.classList.add("idle");
1992
- if (value.moving) panel.classList.add("alert");
1993
- } else if (value.status === "underway") {
1994
- panel.classList.add("underway");
1995
- if (value.fouled) panel.classList.add("alert");
1996
- else if (value.transient) panel.classList.add("transient");
1997
- } else if (value.status === "warm") {
1998
- // Engine alive, boat tied up — muted, no status color crying wolf.
1999
- if (value.fouled) panel.classList.add("alert");
2000
- }
2001
- const next = vm.drStatusText({ ...value, navState: this.vesselNavState });
2002
- if (next != null) text.textContent = next;
2051
+ text.textContent = vm.drStatusBadge(value);
2052
+ panel.title = title;
2003
2053
  }
2004
2054
 
2005
2055
  /**
2006
- * Renders the stream link state so a dropped connection is visible
2007
- * immediately.
2056
+ * Renders the stream link state as a compact [ONLINE]/[OFFLINE]
2057
+ * badge (color-coded) with the full connection detail on its
2058
+ * tooltip — a dropped connection is visible immediately without
2059
+ * eating the top bar.
2008
2060
  *
2009
2061
  * @param {{state: "connecting"|"open"|"retrying"}} status
2010
2062
  * @returns {void}
2011
2063
  */
2012
2064
  renderLinkStatus(status) {
2013
- const panel = this.shadowRoot.querySelector("#dr-status-panel");
2014
- const text = this.shadowRoot.querySelector("#dr-status-text");
2015
- if (!panel || !text) return;
2016
- panel.classList.remove("retrying");
2017
- if (status.state === "connecting") {
2018
- text.textContent = "Connecting to Signal K…";
2019
- } else if (status.state === "retrying") {
2020
- panel.classList.add("retrying");
2021
- text.textContent = "Signal K link lost — reconnecting…";
2022
- } else if (status.state === "open") {
2065
+ const link = this.shadowRoot.querySelector("#dr-link");
2066
+ if (link) {
2067
+ link.classList.remove("online", "offline");
2068
+ if (status.state === "open") {
2069
+ link.textContent = "[ONLINE]";
2070
+ link.classList.add("online");
2071
+ link.title = "Signal K stream connected";
2072
+ } else if (status.state === "retrying") {
2073
+ link.textContent = "[OFFLINE]";
2074
+ link.classList.add("offline");
2075
+ link.title = "Signal K link lost — reconnecting…";
2076
+ } else {
2077
+ link.textContent = "[CONNECTING…]";
2078
+ link.title = "Connecting to Signal K…";
2079
+ }
2080
+ }
2081
+ if (status.state === "open") {
2023
2082
  // Re-seed AIS static data on (re)connect (work doc #23) — names
2024
2083
  // and MMSIs don't ride position deltas.
2025
2084
  this.bootstrapAis();
@@ -2028,7 +2087,6 @@ class DrApp extends HTMLElement {
2028
2087
  // while offline) reappear without a page reload.
2029
2088
  this.fetchNotes();
2030
2089
  }
2031
- // "open" — let the DR state panel take over once data flows.
2032
2090
  }
2033
2091
  }
2034
2092
 
@@ -89,6 +89,9 @@ export class PlotterExtHost {
89
89
  * @param {() => object|null} [opts.getMap] - the Leaflet map once
90
90
  * the chart exists (dr-app wires it to `<dr-map-view>`); null
91
91
  * until then — the overlay machinery no-ops without a chart
92
+ * @param {() => boolean} [opts.getFollow] - whether center-on-vessel
93
+ * (follow) is engaged: while it is, the view is locked and freeform
94
+ * extension moves are refused
92
95
  * @param {{depth: string}} [opts.units] - the user's preferred units
93
96
  * (`units.get`)
94
97
  * @param {Storage} [opts.storage]
@@ -99,6 +102,7 @@ export class PlotterExtHost {
99
102
  this.stream = opts.stream;
100
103
  this.mount = opts.mount;
101
104
  this.getMap = opts.getMap ?? (() => null);
105
+ this.getFollow = opts.getFollow ?? (() => false);
102
106
  this.units = opts.units ?? { depth: "m" };
103
107
  this.storage = opts.storage ?? globalThis.localStorage;
104
108
  this.fetchImpl = opts.fetchImpl ?? globalThis.fetch?.bind(globalThis);
@@ -629,6 +633,10 @@ export class PlotterExtHost {
629
633
  }
630
634
  const map = this.getMap?.();
631
635
  if (!map) throw notSupported("map");
636
+ // Center-on-vessel owns the view while engaged: the chart is
637
+ // locked on the DR position and freeform view moves (an
638
+ // extension's included) only happen once follow is released.
639
+ if (this.getFollow?.()) throw viewLocked("map.center");
632
640
  const zoom =
633
641
  params.zoom === undefined || params.zoom === null
634
642
  ? undefined
@@ -650,6 +658,7 @@ export class PlotterExtHost {
650
658
  }
651
659
  const map = this.getMap?.();
652
660
  if (!map) throw notSupported("map");
661
+ if (this.getFollow?.()) throw viewLocked("map.fitBounds");
653
662
  const [west, south, east, north] = b;
654
663
  // Antimeridian form (west > east): unwrap east so Leaflet's
655
664
  // bounds span the short way round.
@@ -1262,6 +1271,23 @@ function badParams(message) {
1262
1271
  });
1263
1272
  }
1264
1273
 
1274
+ /**
1275
+ * The chart is locked on the vessel (center-on-vessel engaged) — view
1276
+ * moves are refused until follow is released.
1277
+ *
1278
+ * @param {string} method - the refused affordance, for the message
1279
+ * @returns {RpcError}
1280
+ */
1281
+ function viewLocked(method) {
1282
+ return new RpcError(
1283
+ `${method} refused: the chart is locked on the vessel (center-on-vessel engaged) — release follow to move the view`,
1284
+ {
1285
+ code: RPC_ERRORS.HOST_ERROR,
1286
+ reason: "VIEW_LOCKED",
1287
+ },
1288
+ );
1289
+ }
1290
+
1265
1291
  /**
1266
1292
  * @param {unknown} keys
1267
1293
  * @returns {string[]|undefined}