signalk-chiplog 2.11.0 → 2.12.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
@@ -6,6 +6,18 @@ All notable changes to Chiplog are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.12.0] - 2026-10-24
10
+
11
+ ### Added
12
+
13
+ - **Helm entry beside the chart.** Chartplotters supporting the Plotter Extensions API can open Chiplog from a toolbar
14
+ button. The iframe panel keeps drafts while hidden and follows the plotter's night mode when available. The standalone
15
+ tablet app and its offline queue remain available.
16
+
17
+ ### Fixed
18
+
19
+ - Event and crew deletion confirmations work inside sandboxed plotter panels.
20
+
9
21
  ## [2.11.0] - 2026-10-24
10
22
 
11
23
  ### Added
@@ -483,7 +495,8 @@ First release.
483
495
  - REST API under `/plugins/signalk-chiplog/api`, documented in [docs/API.md](docs/API.md).
484
496
  - Single SQLite database through Node's built-in `node:sqlite`: no native module to build.
485
497
 
486
- [Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.11.0...HEAD
498
+ [Unreleased]: https://github.com/ricard33/signalk-chiplog/compare/v2.12.0...HEAD
499
+ [2.12.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.11.0...v2.12.0
487
500
  [2.11.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.10.0...v2.11.0
488
501
  [2.10.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.9.0...v2.10.0
489
502
  [2.9.0]: https://github.com/ricard33/signalk-chiplog/compare/v2.8.0...v2.9.0
package/README.md CHANGED
@@ -437,6 +437,18 @@ Logging needs read/write access. The first time, the app shows **This tablet nee
437
437
  To revoke it, delete the device under **Security → Devices**: the tablet asks for access again. **Sign in instead** uses
438
438
  a regular Signal K user account.
439
439
 
440
+ ### From a chartplotter
441
+
442
+ In a chartplotter supporting the Signal K Plotter Extensions API (such as Freeboard-SK), the **Chiplog** toolbar button
443
+ opens the helm entry app in an iframe panel beside the chart. Closing and reopening it keeps notes and handwriting in
444
+ progress; reloading the plotter still discards unsent drafts. The usual offline queue and Signal K access rights apply.
445
+ The panel follows the plotter's night mode when available, and its night switch also changes the plotter's mode.
446
+ Standalone tablet use remains available at the same address.
447
+
448
+ The extension is advertised automatically while the plugin is enabled. A server without the resource-provider API can
449
+ still run the standalone apps. Journal and login navigation links are hidden in the extension: sign in through the
450
+ chartplotter, or use the panel's device access request.
451
+
440
452
  ## Configuration 🔧
441
453
 
442
454
  In the Signal K admin, **Apps & Plugins → Configuration → Chiplog**.
package/index.js CHANGED
@@ -8,6 +8,7 @@ const { INFLUX_DEFAULTS } = require('./lib/influx-history');
8
8
  const { createLandmarkFinder, LANDMARK_DEFAULTS } = require('./lib/landmark-finder');
9
9
  const { OBSERVATION_DEFAULTS } = require('./lib/observation-recorder');
10
10
  const { createPlaceNamer, GEOCODING_DEFAULTS } = require('./lib/place-names');
11
+ const { registerPlotterExtension } = require('./lib/plotter-extension');
11
12
  const { PROPULSION_DEFAULTS } = require('./lib/propulsion-detector');
12
13
  const { createReplayJob } = require('./lib/replay-job');
13
14
  const { createSummaryMailer, SUMMARY_MAIL_DEFAULTS } = require('./lib/summary-mailer');
@@ -706,6 +707,8 @@ module.exports = function (app) {
706
707
  throw err;
707
708
  }
708
709
 
710
+ registerPlotterExtension(app, () => database !== null);
711
+
709
712
  const readSelfPath = (path) => app.getSelfPath(path);
710
713
  detector = createPassageDetector({ db: database, readSelfPath, settings });
711
714
  const recorder = createTrackRecorder({ db: database, readSelfPath, settings });
@@ -0,0 +1,59 @@
1
+ const { version } = require('../package.json');
2
+
3
+ const EXTENSION_ID = 'signalk-chiplog';
4
+
5
+ function buildManifest() {
6
+ return {
7
+ name: 'Chiplog — Helm entry',
8
+ description: 'Log manoeuvres, notes and handwriting without leaving the chart.',
9
+ version,
10
+ apiVersion: '1',
11
+ requires: ['panels.iframe', 'buttons'],
12
+ optional: ['nightMode'],
13
+ panels: [
14
+ {
15
+ id: 'helm-entry',
16
+ title: 'Chiplog',
17
+ type: 'iframe',
18
+ url: '/signalk-chiplog/entry/?plotterExtension=1',
19
+ lifecycle: 'keepAlive'
20
+ }
21
+ ],
22
+ buttons: [
23
+ {
24
+ id: 'open-helm-entry',
25
+ title: 'Chiplog',
26
+ slot: 'mapToolbar',
27
+ icon: 'edit_note',
28
+ action: { type: 'togglePanel', panel: 'helm-entry' }
29
+ }
30
+ ]
31
+ };
32
+ }
33
+
34
+ function registerPlotterExtension(app, isRunning) {
35
+ // Older servers can still run the standalone logbook and tablet app.
36
+ if (typeof app.registerResourceProvider !== 'function') {
37
+ return;
38
+ }
39
+ app.registerResourceProvider({
40
+ type: 'plotterExtensions',
41
+ methods: {
42
+ listResources: async () => (isRunning() ? { [EXTENSION_ID]: buildManifest() } : {}),
43
+ getResource: async (id) => {
44
+ if (!isRunning() || id !== EXTENSION_ID) {
45
+ throw new Error(`No such plotterExtensions resource: ${id}`);
46
+ }
47
+ return buildManifest();
48
+ },
49
+ setResource: async () => {
50
+ throw new Error('Chiplog plotter extension is read-only');
51
+ },
52
+ deleteResource: async () => {
53
+ throw new Error('Chiplog plotter extension is read-only');
54
+ }
55
+ }
56
+ });
57
+ }
58
+
59
+ module.exports = { registerPlotterExtension };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-chiplog",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Nautical logbook for Signal K: automatic entries, handwritten notes",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -81,6 +81,7 @@
81
81
  "mediabunny": "1.58.0",
82
82
  "preact": "^10.29.8",
83
83
  "prettier": "^3.9.6",
84
+ "signalk-plotterext-bus": "^0.21.0",
84
85
  "three": "0.186.0"
85
86
  }
86
87
  }
@@ -749,6 +749,32 @@ input {
749
749
  border-radius: var(--radius);
750
750
  }
751
751
 
752
+ .confirmation-dialog {
753
+ width: min(40rem, calc(100% - 2rem));
754
+ max-height: calc(100% - 2rem);
755
+ padding: 0;
756
+ border: 0;
757
+ border-radius: var(--radius);
758
+ background: var(--surface);
759
+ color: var(--text);
760
+ }
761
+
762
+ .confirmation-dialog::backdrop {
763
+ background: rgb(0 0 0 / 55%);
764
+ }
765
+
766
+ .confirmation-dialog .sheet {
767
+ width: 100%;
768
+ }
769
+
770
+ :root[data-plotter-extension='true'] [data-standalone-link] {
771
+ display: none;
772
+ }
773
+
774
+ :root[data-plotter-extension='true'] .entry-main {
775
+ padding: 0.75rem 0.75rem 9rem;
776
+ }
777
+
752
778
  .sail-grid {
753
779
  display: grid;
754
780
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
@@ -63,7 +63,7 @@ export function AccessGate({ onGranted }) {
63
63
  ${t('access.request')}
64
64
  </button>`
65
65
  }
66
- <a class="big-button" href="/admin/#/login">${t('access.signIn')}</a>
66
+ <a class="big-button" data-standalone-link href="/admin/#/login">${t('access.signIn')}</a>
67
67
  </div>
68
68
  </section>
69
69
  `;
@@ -1,6 +1,7 @@
1
1
  import { html, useState } from '../../../vendor/preact-htm.mjs';
2
2
  import { useLocale } from '../../../js/context.mjs';
3
3
  import { PencilIcon, TrashIcon } from './Icons.mjs';
4
+ import { confirmAction } from '../confirm.mjs';
4
5
 
5
6
  // .big-button is a row flex container, so a bare <br/> between its children
6
7
  // would not stack them -- wrap name and role in their own column instead.
@@ -83,7 +84,7 @@ export function CrewDialog({
83
84
  };
84
85
 
85
86
  const removeMember = async (member) => {
86
- if (!confirm(t('entry.deleteCrewMemberConfirm', { name: member.name }))) {
87
+ if (!(await confirmAction(t('entry.deleteCrewMemberConfirm', { name: member.name }), t))) {
87
88
  return;
88
89
  }
89
90
  const removed = await onDeleteMember(member.id);
@@ -95,7 +95,7 @@ export function StatusHeader({
95
95
  >
96
96
  ${t('entry.night')}
97
97
  </button>
98
- <a class="tool-button" href="../">${t('entry.logbook')}</a>
98
+ <a class="tool-button" data-standalone-link href="../">${t('entry.logbook')}</a>
99
99
  </div>
100
100
  </header>
101
101
  `;
@@ -0,0 +1,42 @@
1
+ // Native confirm() is blocked in plotter-extension sandboxes. An HTML dialog
2
+ // works there, traps focus, supports Escape, and keeps the same confirmation
3
+ // in the standalone app. Text is never interpreted as HTML.
4
+ export function confirmAction(message, t) {
5
+ return new Promise((resolve) => {
6
+ const dialog = document.createElement('dialog');
7
+ dialog.className = 'confirmation-dialog';
8
+ dialog.setAttribute('aria-label', message);
9
+ const form = document.createElement('form');
10
+ form.method = 'dialog';
11
+ form.className = 'sheet';
12
+ const text = document.createElement('p');
13
+ text.textContent = message;
14
+ const actions = document.createElement('div');
15
+ actions.className = 'sheet-row';
16
+ for (const [value, label, className] of [
17
+ ['cancel', t('common.cancel'), 'big-button'],
18
+ ['confirm', t('common.delete'), 'big-button danger']
19
+ ]) {
20
+ const button = document.createElement('button');
21
+ button.type = 'submit';
22
+ button.value = value;
23
+ button.textContent = label;
24
+ button.className = className;
25
+ button.autofocus = value === 'cancel';
26
+ actions.append(button);
27
+ }
28
+ form.append(text, actions);
29
+ dialog.append(form);
30
+ dialog.addEventListener(
31
+ 'close',
32
+ () => {
33
+ const confirmed = dialog.returnValue === 'confirm';
34
+ dialog.remove();
35
+ resolve(confirmed);
36
+ },
37
+ { once: true }
38
+ );
39
+ document.body.append(dialog);
40
+ dialog.showModal();
41
+ });
42
+ }
@@ -12,6 +12,8 @@ import { randomId } from '../../js/ids.mjs';
12
12
  import { pickLanguage } from '../../js/i18n.mjs';
13
13
  import { createServerClock } from './clock.mjs';
14
14
  import { createJournal } from './journal.mjs';
15
+ import { confirmAction } from './confirm.mjs';
16
+ import { connectPlotter, isPlotterExtension } from './plotter.mjs';
15
17
  import { AccessGate } from './components/AccessGate.mjs';
16
18
  import { EventDialog, Toast } from './components/Dialogs.mjs';
17
19
  import { CrewDialog } from './components/CrewDialog.mjs';
@@ -106,6 +108,7 @@ function App({ journal }) {
106
108
  const [managingCrew, setManagingCrew] = useState(false);
107
109
  const [tab, setTab] = useState('note');
108
110
  const [night, setNight] = useState(readSetting(NIGHT_KEY) === 'on');
111
+ const [plotter, setPlotter] = useState(null);
109
112
  const [version, setVersion] = useState(0);
110
113
  const [busy, setBusy] = useState(false);
111
114
 
@@ -163,6 +166,43 @@ function App({ journal }) {
163
166
  writeSetting(NIGHT_KEY, night ? 'on' : 'off');
164
167
  }, [night]);
165
168
 
169
+ useEffect(() => {
170
+ if (!isPlotterExtension) {
171
+ return undefined;
172
+ }
173
+ let active = true;
174
+ let connection;
175
+ connectPlotter((enabled) => active && setNight(enabled))
176
+ .then((client) => {
177
+ if (!active) {
178
+ client.close();
179
+ return;
180
+ }
181
+ connection = client;
182
+ setPlotter(client);
183
+ })
184
+ // The tablet remains usable if the host cannot complete the handshake.
185
+ .catch(() => {});
186
+ return () => {
187
+ active = false;
188
+ connection?.close();
189
+ };
190
+ }, []);
191
+
192
+ const toggleNight = async () => {
193
+ const enabled = !night;
194
+ if (plotter?.hasCapability('nightMode')) {
195
+ try {
196
+ await plotter.call('nightMode.set', { enabled, auto: false });
197
+ setNight(enabled);
198
+ return;
199
+ } catch {
200
+ // Keep the local night switch usable if the host stops answering.
201
+ }
202
+ }
203
+ setNight(enabled);
204
+ };
205
+
166
206
  useEffect(() => {
167
207
  if (!toast || toast.kind === 'error') {
168
208
  return undefined;
@@ -307,7 +347,7 @@ function App({ journal }) {
307
347
  };
308
348
 
309
349
  const deleteEvent = async (event) => {
310
- if (!confirm(t('entry.deleteConfirm'))) {
350
+ if (!(await confirmAction(t('entry.deleteConfirm'), t))) {
311
351
  return;
312
352
  }
313
353
  try {
@@ -376,7 +416,7 @@ function App({ journal }) {
376
416
  onReconnect=${reconnect}
377
417
  queued=${queue.pending.length}
378
418
  night=${night}
379
- onToggleNight=${() => setNight((value) => !value)}
419
+ onToggleNight=${toggleNight}
380
420
  />
381
421
  <main class="entry-main">
382
422
  ${
@@ -498,6 +538,7 @@ if ('serviceWorker' in navigator && window.isSecureContext) {
498
538
  }
499
539
 
500
540
  const locale = createLocale(pickLanguage(navigator.languages, location.search));
541
+ document.documentElement.dataset.plotterExtension = String(isPlotterExtension);
501
542
  document.documentElement.lang = locale.language;
502
543
  document.title = locale.t('entry.title');
503
544
 
@@ -0,0 +1,30 @@
1
+ import { connectExtension, windowPort } from '../../vendor/plotterext.mjs';
2
+
3
+ // An explicit opt-in leaves standalone and ordinary iframe use unchanged.
4
+ export const isPlotterExtension =
5
+ window.parent !== window && new URLSearchParams(location.search).get('plotterExtension') === '1';
6
+
7
+ export async function connectPlotter(onNightChange) {
8
+ const client = await connectExtension({
9
+ port: windowPort(window.parent, { origin: location.origin }),
10
+ timeoutMs: 5000
11
+ });
12
+ try {
13
+ if (client.hasCapability('nightMode')) {
14
+ // Subscribe first so a change during the initial read cannot be lost.
15
+ let changed = false;
16
+ await client.subscribe(['nightMode.changed'], (_name, state) => {
17
+ changed = true;
18
+ onNightChange(state.enabled);
19
+ });
20
+ const state = await client.call('nightMode.get');
21
+ if (!changed) {
22
+ onNightChange(state.enabled);
23
+ }
24
+ }
25
+ return client;
26
+ } catch (error) {
27
+ client.close();
28
+ throw error;
29
+ }
30
+ }
@@ -13,6 +13,8 @@ const SHELL = [
13
13
  'icons/icon-512.png',
14
14
  'icons/apple-touch-icon.png',
15
15
  'js/main.mjs',
16
+ 'js/confirm.mjs',
17
+ 'js/plotter.mjs',
16
18
  'js/access.mjs',
17
19
  'js/clock.mjs',
18
20
  'js/journal.mjs',
@@ -43,7 +45,8 @@ const SHELL = [
43
45
  '../js/view-memory.mjs',
44
46
  '../js/components/common.mjs',
45
47
  '../js/components/Timeline.mjs',
46
- '../vendor/preact-htm.mjs'
48
+ '../vendor/preact-htm.mjs',
49
+ '../vendor/plotterext.mjs'
47
50
  ];
48
51
 
49
52
  const shellUrls = () => new Set(SHELL.map((file) => new URL(file, self.registration.scope).href));
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joel Kozikowski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ var f="plotterExt/1",o={PARSE_ERROR:-32700,INVALID_REQUEST:-32600,METHOD_NOT_FOUND:-32601,INVALID_PARAMS:-32602,INTERNAL_ERROR:-32603,HOST_ERROR:-32e3,TIMEOUT:-32001,CONNECTION_CLOSED:-32002},c=class l extends Error{code;data;constructor(e,s={}){super(e),this.name="RpcError",this.code=s.code??o.HOST_ERROR;let n={...s.data??{}};s.reason!==void 0&&(n.reason=s.reason),this.data=Object.keys(n).length>0?n:void 0}get reason(){return typeof this.data?.reason=="string"?this.data.reason:void 0}toErrorObject(){return{code:this.code,message:this.message,...this.data?{data:this.data}:{}}}static fromErrorObject(e){return new l(e.message,{code:e.code,data:e.data})}static from(e){return e instanceof l?e:e instanceof Error?new l(e.message,{code:o.INTERNAL_ERROR}):new l(String(e),{code:o.INTERNAL_ERROR})}},u="bus.ready",y="bus.handshake";function E(t,e){return t===e?!0:m(t.split("."),0,e.split("."),0)}function m(t,e,s,n){for(;e<t.length;){let i=t[e];if(i==="**"){if(e===t.length-1)return!0;for(let r=n;r<=s.length;r++)if(m(t,e+1,s,r))return!0;return!1}if(n>=s.length||i!=="*"&&i!==s[n])return!1;e++,n++}return n===s.length}function g(t,e){for(let s of t)if(E(s,e))return!0;return!1}function N(t){return{bus:f,msg:t}}function M(t){if(typeof t!="object"||t===null)return null;let e=t;return e.bus!==f?null:_(e.msg)?e.msg:null}function _(t){if(typeof t!="object"||t===null)return!1;let e=t;if(e.jsonrpc!=="2.0")return!1;if(typeof e.method=="string")return e.id===void 0||typeof e.id=="string"||typeof e.id=="number";if(!(typeof e.id=="string"||typeof e.id=="number"||e.id===null))return!1;let n="result"in e,i=e.error,r=typeof i=="object"&&i!==null&&typeof i.code=="number"&&typeof i.message=="string";return n?!("error"in e):r}function S(t){return"method"in t&&"id"in t&&t.id!==void 0}function A(t){return"method"in t&&(!("id"in t)||t.id===void 0)}function D(t){return!("method"in t)}function h(t,e={}){let s=e.listenWindow??globalThis,n=e.origin??s.location?.origin??"*";return{post(i,r){r?.length?t.postMessage(i,{targetOrigin:n,transfer:r}):t.postMessage(i,n)},listen(i){let r=a=>{a.source===t&&(n!=="*"&&a.origin!==n||i(a.data))};return s.addEventListener("message",r),()=>s.removeEventListener("message",r)}}}var L=1e4,p=class{callTimeoutMs;port;unlisten;onError;pending=new Map;methods=new Map;eventHandlers=new Set;idPrefix=Math.random().toString(36).slice(2,8);seq=0;closed=!1;constructor(t){this.port=t.port,this.callTimeoutMs=t.callTimeoutMs??L,this.onError=t.onError??(e=>console.warn("[plotterext-bus]",e)),this.unlisten=this.port.listen(e=>this.onData(e))}registerMethod(t,e){this.methods.set(t,e)}unregisterMethod(t){this.methods.delete(t)}onEvent(t,e){let s={patterns:t,fn:e};return this.eventHandlers.add(s),()=>this.eventHandlers.delete(s)}notify(t,e,s={}){this.send({jsonrpc:"2.0",method:t,...e!==void 0?{params:e}:{}},s.transfer)}call(t,e,s={}){if(this.closed)return Promise.reject(new c("Bus endpoint is closed",{code:o.CONNECTION_CLOSED,reason:"CLOSED"}));let n=`${this.idPrefix}-${++this.seq}`,i=s.timeoutMs??this.callTimeoutMs;return new Promise((r,a)=>{let d=i>0?setTimeout(()=>{this.pending.delete(n),a(new c(`Call timed out after ${i}ms: ${t}`,{code:o.TIMEOUT,reason:"TIMEOUT"}))},i):null;this.pending.set(n,{resolve:r,reject:a,timer:d}),this.send({jsonrpc:"2.0",id:n,method:t,...e!==void 0?{params:e}:{}},s.transfer)})}close(){if(!this.closed){this.closed=!0,this.unlisten();for(let[,t]of this.pending)t.timer&&clearTimeout(t.timer),t.reject(new c("Bus endpoint closed",{code:o.CONNECTION_CLOSED,reason:"CLOSED"}));this.pending.clear(),this.eventHandlers.clear()}}send(t,e){this.closed||this.port.post(N(t),e)}onData(t){let e=M(t);e&&(D(e)?this.onResponse(e):S(e)?this.onRequest(e):A(e)&&this.onNotification(e.method,e.params))}onResponse(t){if(t.id===null)return;let e=this.pending.get(t.id);e&&(this.pending.delete(t.id),e.timer&&clearTimeout(e.timer),"error"in t?e.reject(c.fromErrorObject(t.error)):e.resolve(t.result))}async onRequest(t){let e=this.methods.get(t.method);if(!e){this.send({jsonrpc:"2.0",id:t.id,error:{code:o.METHOD_NOT_FOUND,message:`Method not found: ${t.method}`}});return}try{let s=await e(t.params,{endpoint:this});this.send({jsonrpc:"2.0",id:t.id,result:s===void 0?null:s})}catch(s){this.send({jsonrpc:"2.0",id:t.id,error:c.from(s).toErrorObject()})}}onNotification(t,e){for(let s of[...this.eventHandlers])if(g(s.patterns,t))try{s.fn(t,e)}catch(n){this.onError(n)}}};var v=class{handshake;endpoint;constructor(t,e){this.endpoint=t,this.handshake=e}get context(){return this.handshake.context}get apiVersion(){return this.handshake.apiVersion}get capabilities(){return this.handshake.capabilities}hasCapability(t){return this.handshake.capabilities.includes(t)}call(t,e,s){return this.endpoint.call(t,e,s)}notify(t,e){this.endpoint.notify(t,e)}async subscribe(t,e){let s=this.endpoint.onEvent(t,e),n;try{n=(await this.call("events.subscribe",{patterns:t})).subscriptionId}catch(i){throw s(),i}return async()=>{s(),await this.call("events.unsubscribe",{subscriptionId:n}).catch(()=>{})}}async publish(t,e,s){await this.call("events.publish",{topic:t,...e!==void 0?{params:e}:{},...s?{scope:s}:{}})}state={get:async(t,e)=>(await this.call("state.get",{...e?{scope:e}:{},...t?{keys:t}:{}})).values??{},set:async(t,e)=>{await this.call("state.set",{...e?{scope:e}:{},values:t})}};signalk={subscribe:async(t,e)=>{let s=t.map(r=>`sk.${r}`),n=await this.subscribe(s,(r,a)=>e(a)),i;try{i=(await this.call("signalk.subscribe",{paths:t})).subscriptionId}catch(r){throw await n(),r}return async()=>{await n(),await this.call("signalk.unsubscribe",{subscriptionId:i}).catch(()=>{})}},put:(t,e)=>this.call("signalk.put",{path:t,value:e})};route={list:async()=>(await this.call("route.list")).routes??[],create:async t=>await this.call("route.create",t),get:async t=>await this.call("route.get",{routeId:t}),replace:async(t,e)=>await this.call("route.replace",{routeId:t,points:e}),save:async(t,e)=>await this.call("route.save",{routeId:t,...e??{}}),show:async t=>await this.call("route.show",{ref:t}),hide:async t=>{await this.call("route.hide",{routeId:t})},delete:async t=>{await this.call("route.delete",{routeId:t})}};chart={list:async()=>(await this.call("chart.list")).charts??[],setVisibility:async(t,e)=>{await this.call("chart.setVisibility",{ids:t,visible:e})},setOpacity:async(t,e)=>{await this.call("chart.setOpacity",{ids:t,opacity:e})},setOrder:async t=>{await this.call("chart.setOrder",{order:t})},setTime:async(t,e)=>{await this.call("chart.setTime",{ids:t,time:e})}};nightMode={get:async()=>await this.call("nightMode.get"),set:async t=>{await this.call("nightMode.set",t)}};panels={list:async()=>(await this.call("ui.listPanels")).panels};overlays={list:async()=>(await this.call("overlay.list")).overlays,setVisibility:async(t,e)=>{await this.call("overlay.setVisibility",{ids:t,visible:e})},setOpacity:async(t,e)=>{await this.call("overlay.setOpacity",{ids:t,opacity:e})},frame:async t=>await this.call("overlay.frame",t,{transfer:[t.image]}),refresh:async t=>{await this.call("overlay.refresh",t)},setData:async t=>{await this.call("overlay.setData",t)},setLayer:async t=>{await this.call("overlay.setLayer",t)},setState:async(t,e)=>{await this.call("overlay.setState",{overlayId:t,values:e})}};windows={open:async t=>await this.call("ui.openWindow",t),update:async t=>await this.call("ui.updateWindow",t),focus:async t=>{await this.call("ui.focusWindow",t===void 0?{}:{windowId:t})},close:async t=>{await this.call("ui.closeWindow",t===void 0?{}:{windowId:t})},list:async()=>(await this.call("ui.listWindows")).windows};resourceGroup={apply:async t=>await this.call("resourceGroup.apply",{id:t})};map={getView:async()=>await this.call("map.getView"),center:async(t,e)=>{await this.call("map.center",{position:t,...typeof e=="number"?{zoom:e}:{}})},fitBounds:async t=>{await this.call("map.fitBounds",{bounds:t})}};close(){this.endpoint.close()}};function R(t={}){let e=t.port??h(globalThis.parent,{origin:"*"}),s=new p({port:e,callTimeoutMs:t.callTimeoutMs,onError:t.onError}),n=t.id?{id:t.id}:void 0;return new Promise((i,r)=>{let a=!1,d=s.onEvent([y],(I,T)=>{a||(a=!0,w(),i(new v(s,T)))}),b=setInterval(()=>s.notify(u,n),t.readyIntervalMs??250),O=setTimeout(()=>{a||(a=!0,w(),s.close(),r(new c("Timed out waiting for host handshake",{code:o.TIMEOUT,reason:"HANDSHAKE_TIMEOUT"})))},t.timeoutMs??1e4),w=()=>{d(),clearInterval(b),clearTimeout(O)};s.notify(u,n)})}export{R as connectExtension,h as windowPort};