signalk-webhook-bridge 2.0.0 → 2.1.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 +28 -0
- package/README.md +2 -0
- package/package.json +2 -2
- package/plugin/index.js +81 -17
- package/plugin/schedule.js +21 -0
- package/public/651.main.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ All notable changes to Webhook Bridge will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
and this project uses [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [2.1.0] - 2026-09-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Live **Current Payload Preview** in the plugin configuration page.
|
|
13
|
+
- Preview uses the current live Signal K values and shows the exact JSON structure that will be sent to the receiving webhook.
|
|
14
|
+
- Preview updates automatically when Signal K paths, webhook field names or output units are changed.
|
|
15
|
+
- Preview supports structured Signal K values such as `navigation.position`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Payload documentation in the configuration interface is now complemented by a live example using the current configuration.
|
|
20
|
+
|
|
21
|
+
## [2.0.1] - 2026-09-13
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Webhook captures are now aligned to the server clock rather than being timed from when the plugin starts.
|
|
26
|
+
- A 60-minute interval now captures on the hour.
|
|
27
|
+
- A 30-minute interval captures at `:00` and `:30`.
|
|
28
|
+
- A 15-minute interval captures at `:00`, `:15`, `:30` and `:45`.
|
|
29
|
+
- The next capture time is recalculated after every snapshot to avoid gradual timer drift.
|
|
30
|
+
- Updated configuration help text to explain clock-aligned scheduling.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Automated tests for clock-aligned capture scheduling, including hourly, half-hourly, quarter-hourly, five-minute and day-boundary behaviour.
|
|
35
|
+
|
|
8
36
|
## [2.0.0] - 2026-09-13
|
|
9
37
|
|
|
10
38
|
### Added
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Choose the Signal K data paths you want to send, assign simple field names, sele
|
|
|
6
6
|
|
|
7
7
|
If the webhook is unavailable, data is stored locally in a persistent queue and automatically delivered in order when the connection returns.
|
|
8
8
|
|
|
9
|
+
Captures are aligned to the Signal K server's local clock. For example, a 60-minute interval captures on the hour, while a 15-minute interval captures at :00, :15, :30 and :45.
|
|
10
|
+
|
|
9
11
|
## Features
|
|
10
12
|
|
|
11
13
|
- Send selected Signal K data to any HTTP webhook
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-webhook-bridge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Send selected Signal K data paths to an external webhook with unit conversion, persistent queueing and automatic retry.",
|
|
5
5
|
"main": "plugin/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -60,4 +60,4 @@
|
|
|
60
60
|
"webpack": "^5.110.3",
|
|
61
61
|
"webpack-cli": "^7.2.3"
|
|
62
62
|
}
|
|
63
|
-
}
|
|
63
|
+
}
|
package/plugin/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Signal K Webhook Bridge.
|
|
5
5
|
*
|
|
6
|
-
* Captures configured Signal K values at
|
|
6
|
+
* Captures configured Signal K values at clock-aligned intervals,
|
|
7
7
|
* stores every snapshot in a persistent SQLite FIFO queue,
|
|
8
8
|
* and delivers queued entries to the configured webhook in order.
|
|
9
9
|
* Adds output-unit labels, retries backlog on startup and after each capture,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const { getAvailablePaths } = require("./paths");
|
|
14
14
|
const { convertValue, getOutputUnitLabel } = require("./units");
|
|
15
15
|
const { sendWebhook } = require("./webhook");
|
|
16
|
+
const { getNextAlignedCaptureTime } = require("./schedule");
|
|
16
17
|
const storage = require("./storage");
|
|
17
18
|
|
|
18
19
|
module.exports = function (app) {
|
|
@@ -124,6 +125,36 @@ module.exports = function (app) {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Schedule one capture at the next clock boundary.
|
|
130
|
+
*
|
|
131
|
+
* A new timeout is calculated after every capture rather than using
|
|
132
|
+
* setInterval(), preventing gradual timing drift.
|
|
133
|
+
*/
|
|
134
|
+
function scheduleNextCapture() {
|
|
135
|
+
if (stopped || !currentSettings) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const intervalMinutes = Number(currentSettings.sendFreq) || 10;
|
|
140
|
+
|
|
141
|
+
const nextCapture = getNextAlignedCaptureTime(intervalMinutes);
|
|
142
|
+
|
|
143
|
+
const delay = Math.max(0, nextCapture.getTime() - Date.now());
|
|
144
|
+
|
|
145
|
+
app.debug(
|
|
146
|
+
`Next webhook capture scheduled for ${nextCapture.toISOString()}`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
captureTimer = setTimeout(() => {
|
|
150
|
+
captureTimer = null;
|
|
151
|
+
|
|
152
|
+
captureSnapshot();
|
|
153
|
+
|
|
154
|
+
scheduleNextCapture();
|
|
155
|
+
}, delay);
|
|
156
|
+
}
|
|
157
|
+
|
|
127
158
|
/**
|
|
128
159
|
* Deliver queued entries in strict FIFO order.
|
|
129
160
|
*
|
|
@@ -187,7 +218,6 @@ module.exports = function (app) {
|
|
|
187
218
|
* This preserves FIFO order and prevents newer entries
|
|
188
219
|
* overtaking an entry that has not been delivered.
|
|
189
220
|
*/
|
|
190
|
-
|
|
191
221
|
lastError = error.message;
|
|
192
222
|
deliveryState = "waiting";
|
|
193
223
|
|
|
@@ -213,17 +243,22 @@ module.exports = function (app) {
|
|
|
213
243
|
}
|
|
214
244
|
}
|
|
215
245
|
|
|
216
|
-
|
|
217
|
-
|
|
246
|
+
/*
|
|
247
|
+
* Complete deferred cleanup after any in-flight request
|
|
248
|
+
* has settled.
|
|
249
|
+
*/
|
|
218
250
|
if (stopped) {
|
|
219
251
|
storage.close();
|
|
220
252
|
}
|
|
221
253
|
}
|
|
222
254
|
}
|
|
223
255
|
|
|
224
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Open storage, attempt backlog delivery and schedule the first
|
|
258
|
+
* clock-aligned capture.
|
|
259
|
+
*/
|
|
225
260
|
plugin.start = function (settings) {
|
|
226
|
-
app.debug("Starting
|
|
261
|
+
app.debug("Starting Webhook Bridge");
|
|
227
262
|
|
|
228
263
|
currentSettings = settings || {};
|
|
229
264
|
stopped = false;
|
|
@@ -248,34 +283,36 @@ module.exports = function (app) {
|
|
|
248
283
|
* sendFreq is configured in minutes.
|
|
249
284
|
*/
|
|
250
285
|
const sendFreq = Number(currentSettings.sendFreq) || 10;
|
|
251
|
-
const intervalMilliseconds = sendFreq * 60 * 1000;
|
|
252
286
|
|
|
253
287
|
app.debug(
|
|
254
288
|
`Webhook capture interval set to ${sendFreq} minute${
|
|
255
289
|
sendFreq === 1 ? "" : "s"
|
|
256
|
-
}`,
|
|
290
|
+
}, aligned to the clock`,
|
|
257
291
|
);
|
|
258
292
|
|
|
259
293
|
/*
|
|
260
294
|
* Immediately attempt to clear any backlog left from a previous
|
|
261
295
|
* offline period or server restart.
|
|
262
296
|
*
|
|
263
|
-
* We do NOT capture a new snapshot immediately.
|
|
264
|
-
*
|
|
297
|
+
* We do NOT capture a new snapshot immediately. The first new
|
|
298
|
+
* snapshot is taken at the next clock-aligned boundary.
|
|
265
299
|
*/
|
|
266
300
|
processQueue();
|
|
267
301
|
|
|
268
|
-
|
|
302
|
+
scheduleNextCapture();
|
|
269
303
|
};
|
|
270
304
|
|
|
271
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Stop future captures; an in-flight delivery finishes before its
|
|
307
|
+
* worker closes storage.
|
|
308
|
+
*/
|
|
272
309
|
plugin.stop = function () {
|
|
273
|
-
app.debug("Stopping
|
|
310
|
+
app.debug("Stopping Webhook Bridge");
|
|
274
311
|
|
|
275
312
|
stopped = true;
|
|
276
313
|
|
|
277
314
|
if (captureTimer) {
|
|
278
|
-
|
|
315
|
+
clearTimeout(captureTimer);
|
|
279
316
|
captureTimer = null;
|
|
280
317
|
}
|
|
281
318
|
|
|
@@ -325,6 +362,7 @@ module.exports = function (app) {
|
|
|
325
362
|
});
|
|
326
363
|
}
|
|
327
364
|
});
|
|
365
|
+
|
|
328
366
|
router.get("/status", (req, res) => {
|
|
329
367
|
try {
|
|
330
368
|
const queueCount = storage.count();
|
|
@@ -368,11 +406,37 @@ module.exports = function (app) {
|
|
|
368
406
|
});
|
|
369
407
|
}
|
|
370
408
|
});
|
|
409
|
+
|
|
410
|
+
router.post("/preview", (req, res) => {
|
|
411
|
+
try {
|
|
412
|
+
const previewSettings = {
|
|
413
|
+
paths: Array.isArray(req.body?.paths) ? req.body.paths : [],
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const snapshot = buildSnapshot(previewSettings);
|
|
417
|
+
|
|
418
|
+
res.status(200).json({
|
|
419
|
+
timestamp: new Date().toISOString(),
|
|
420
|
+
...snapshot,
|
|
421
|
+
});
|
|
422
|
+
} catch (error) {
|
|
423
|
+
app.error(
|
|
424
|
+
`Unable to generate webhook preview: ${error.message}`,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
res.status(500).json({
|
|
428
|
+
error: "Unable to generate webhook preview",
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
});
|
|
371
432
|
};
|
|
372
433
|
|
|
373
434
|
/**
|
|
374
|
-
* Describe webhook settings and path mappings for the schema-based
|
|
375
|
-
*
|
|
435
|
+
* Describe webhook settings and path mappings for the schema-based
|
|
436
|
+
* configuration form.
|
|
437
|
+
*
|
|
438
|
+
* Path choices are discovered when requested; the custom panel
|
|
439
|
+
* filters units by metadata.
|
|
376
440
|
*/
|
|
377
441
|
plugin.schema = function () {
|
|
378
442
|
const availablePaths = getAvailablePaths(app);
|
|
@@ -405,7 +469,7 @@ module.exports = function (app) {
|
|
|
405
469
|
type: "number",
|
|
406
470
|
title: "Send Interval",
|
|
407
471
|
description:
|
|
408
|
-
"
|
|
472
|
+
"Capture interval in minutes, aligned to the server clock. For example, 60 sends on the hour and 15 sends at :00, :15, :30 and :45.",
|
|
409
473
|
default: 10,
|
|
410
474
|
minimum: 1,
|
|
411
475
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function getNextAlignedCaptureTime(intervalMinutes, now = new Date()) {
|
|
2
|
+
const interval = Number(intervalMinutes);
|
|
3
|
+
|
|
4
|
+
if (!Number.isFinite(interval) || interval <= 0) {
|
|
5
|
+
throw new Error("Capture interval must be greater than zero");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const startOfDay = new Date(now);
|
|
9
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
10
|
+
|
|
11
|
+
const intervalMs = interval * 60 * 1000;
|
|
12
|
+
const elapsedToday = now.getTime() - startOfDay.getTime();
|
|
13
|
+
|
|
14
|
+
const nextElapsed = Math.floor(elapsedToday / intervalMs + 1) * intervalMs;
|
|
15
|
+
|
|
16
|
+
return new Date(startOfDay.getTime() + nextElapsed);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
getNextAlignedCaptureTime,
|
|
21
|
+
};
|
package/public/651.main.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! For license information please see 651.main.js.LICENSE.txt */
|
|
2
|
-
"use strict";(self.webpackChunksignalk_webhook_bridge=self.webpackChunksignalk_webhook_bridge||[]).push([[651],{651(e,t,a){a.r(t),a.d(t,{default:()=>s});var r=a.cw(function(e,t){var a=Symbol.for("react.transitional.element"),r=Symbol.for("react.fragment");function i(e,t,r){var i=null;if(void 0!==r&&(i=""+r),void 0!==t.key&&(i=""+t.key),"key"in t)for(var n in r={},t)"key"!==n&&(r[n]=t[n]);else r=t;return t=r.ref,{$$typeof:a,type:e,key:i,ref:void 0!==t?t:null,props:r}}t.Fragment=r,t.jsx=i,t.jsxs=i}),i=a.cw(function(e,t){e.exports=r()}),n=a(231);function s({configuration:e={},save:t}){const[a,i]=(0,n.useState)(e.webhookUrl||""),[s,l]=(0,n.useState)(e.authKey||""),[o,d]=(0,n.useState)(e.sendFreq||10),[h,u]=(0,n.useState)(Array.isArray(e.paths)?e.paths:[]),[c,g]=(0,n.useState)([]),[m,p]=(0,n.useState)(!0),[y,v]=(0,n.useState)(""),[b,x]=(0,n.useState)(null),[j,f]=(0,n.useState)(null),[k,w]=(0,n.useState)(""),[S,C]=(0,n.useState)(!1);(0,n.useEffect)(()=>{fetch("/plugins/signalk-webhook-bridge/paths").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}`);return e.json()}).then(e=>{g(Array.isArray(e.paths)?e.paths:[]),p(!1)}).catch(e=>{console.error("Unable to load Signal K paths:",e),v("Unable to load available Signal K paths."),p(!1)})},[]),(0,n.useEffect)(()=>{let e=!0;const t=async()=>{try{const t=await fetch("/plugins/signalk-webhook-bridge/status");if(!t.ok)throw new Error(`HTTP ${t.status}`);const a=await t.json();e&&(f(a),w(""))}catch(t){console.error("Unable to load webhook status:",t),e&&w("Unable to load webhook status.")}};t();const a=setInterval(t,5e3);return()=>{e=!1,clearInterval(a)}},[]);const B=(e,t,a)=>{u(r=>r.map((r,i)=>i===e?{...r,[t]:a}:r))};return(0,r().jsxs)("div",{style:{padding:"1rem",maxWidth:"900px"},children:[(0,r().jsx)("h2",{children:"Webhook Bridge"}),(0,r().jsx)("div",{style:{marginBottom:"1.25rem",maxWidth:"750px",lineHeight:"1.5",opacity:.8},children:"Send selected Signal K data to an external webhook at a regular interval. Choose the data paths to include, give each value a webhook field name, and select the units you want to send. If delivery is unavailable, updates are stored locally and automatically sent in order when the connection returns."}),(0,r().jsxs)("div",{style:{marginBottom:"1.5rem",padding:"1rem",borderRadius:"6px",transition:"background 0.2s ease, border 0.2s ease",...(e=>{switch(e){case"queued":case"waiting":return{background:"rgba(255, 152, 0, 0.10)",border:"1px solid rgba(255, 152, 0, 0.40)"};case"error":return{background:"rgba(244, 67, 54, 0.10)",border:"1px solid rgba(244, 67, 54, 0.40)"};default:return{background:"rgba(33, 150, 243, 0.08)",border:"1px solid rgba(33, 150, 243, 0.35)"}}})(j?.deliveryState)},children:[(0,r().jsxs)("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"1rem",marginBottom:"0.75rem"},children:[(0,r().jsxs)("div",{children:[(0,r().jsx)("h3",{style:{margin:0},children:"Status"}),(0,r().jsx)("div",{style:{marginTop:"0.25rem",fontSize:"0.9rem",opacity:.75},children:"Current delivery and queue status."})]}),(0,r().jsx)("button",{type:"button",onClick:async()=>{C(!0);try{const e=await fetch("/plugins/signalk-webhook-bridge/retry",{method:"POST"});if(!e.ok)throw new Error(`HTTP ${e.status}`);const t=await e.json();f(t),w("")}catch(e){console.error("Unable to retry webhook delivery:",e),w("Unable to retry webhook delivery.")}finally{C(!1)}},disabled:S,style:{padding:"0.5rem 0.8rem",cursor:S?"default":"pointer",opacity:S?.7:1},children:S?"Retrying...":"Retry Now"})]}),k&&(0,r().jsx)("div",{style:{marginBottom:"0.75rem",padding:"0.65rem 0.8rem",border:"1px solid rgba(244, 67, 54, 0.45)",borderRadius:"6px",background:"rgba(244, 67, 54, 0.12)"},children:k}),!j&&!k&&(0,r().jsx)("div",{style:{opacity:.75},children:"Loading status..."}),j&&(0,r().jsxs)("div",{style:{display:"grid",gridTemplateColumns:"180px 1fr",gap:"0.5rem 1rem"},children:[(0,r().jsx)("div",{style:{fontWeight:"600"},children:"Delivery"}),(0,r().jsx)("div",{children:"connected"===j.deliveryState?"Connected":"delivering"===j.deliveryState?"Delivering":"waiting"===j.deliveryState?"Waiting to retry":"queued"===j.deliveryState?"Queued":"error"===j.deliveryState?"Error":"Idle"}),(0,r().jsx)("div",{style:{fontWeight:"600"},children:"Queue"}),(0,r().jsxs)("div",{children:[j.queueCount," ",1===j.queueCount?"entry":"entries"," ","waiting"]}),(0,r().jsx)("div",{style:{fontWeight:"600"},children:"Last capture"}),(0,r().jsx)("div",{children:j.lastCapture?new Date(j.lastCapture).toLocaleString():"Not yet"}),(0,r().jsx)("div",{style:{fontWeight:"600"},children:"Last delivery"}),(0,r().jsx)("div",{children:j.lastDelivery?new Date(j.lastDelivery).toLocaleString():"Not yet"}),j.lastError&&(0,r().jsxs)(r().Fragment,{children:[(0,r().jsx)("div",{style:{fontWeight:"600"},children:"Last error"}),(0,r().jsx)("div",{style:{wordBreak:"break-word"},children:j.lastError})]})]})]}),(0,r().jsxs)("div",{style:{marginBottom:"1rem"},children:[(0,r().jsx)("label",{htmlFor:"webhookUrl",style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Webhook URL"}),(0,r().jsx)("input",{id:"webhookUrl",type:"url",value:a,onChange:e=>i(e.target.value),placeholder:"https://example.com/webhook",style:{width:"100%",padding:"0.5rem"}})]}),(0,r().jsxs)("div",{style:{marginBottom:"1rem"},children:[(0,r().jsx)("label",{htmlFor:"authKey",style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Authentication Key"}),(0,r().jsx)("input",{id:"authKey",type:"text",value:s,onChange:e=>l(e.target.value),placeholder:"Optional",style:{width:"100%",padding:"0.5rem"}})]}),(0,r().jsxs)("div",{style:{marginBottom:"1.5rem"},children:[(0,r().jsx)("label",{htmlFor:"sendFreq",style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Send Interval"}),(0,r().jsx)("input",{id:"sendFreq",type:"number",min:"1",value:o,onChange:e=>d(e.target.value),style:{width:"150px",padding:"0.5rem"}}),(0,r().jsx)("div",{style:{marginTop:"0.35rem",fontSize:"0.9rem",opacity:.75},children:"Minutes between webhook updates."})]}),(0,r().jsx)("hr",{style:{margin:"1.5rem 0"}}),(0,r().jsx)("div",{style:{marginBottom:"1rem"},children:(0,r().jsxs)("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"1rem"},children:[(0,r().jsxs)("div",{children:[(0,r().jsx)("h3",{style:{marginBottom:"0.25rem"},children:"Signal K Data Paths"}),(0,r().jsx)("div",{style:{fontSize:"0.9rem",opacity:.75},children:"Choose the Signal K values to include in each webhook update."})]}),(0,r().jsx)("button",{type:"button",onClick:()=>{u(e=>[...e,{path:"",fieldName:"",units:"native"}])},style:{padding:"0.5rem 0.8rem",cursor:"pointer"},children:"Add Path"})]})}),0===h.length&&(0,r().jsx)("div",{style:{padding:"1rem",border:"1px solid rgba(128,128,128,0.35)",borderRadius:"6px",marginBottom:"1rem",opacity:.8},children:"No Signal K paths added yet."}),h.map((e,t)=>{const a=c.find(t=>t.path===e.path),i=function(e){switch(e){case"m/s":return[{value:"native",label:"Native (m/s)"},{value:"knots",label:"Knots"},{value:"kmh",label:"km/h"},{value:"mph",label:"mph"}];case"rad":return[{value:"native",label:"Native (rad)"},{value:"degrees",label:"Degrees"}];case"m":return[{value:"native",label:"Native (m)"},{value:"metres",label:"Metres"},{value:"feet",label:"Feet"}];case"K":return[{value:"native",label:"Native (K)"},{value:"celsius",label:"Celsius"},{value:"fahrenheit",label:"Fahrenheit"}];case"Pa":return[{value:"native",label:"Native (Pa)"},{value:"hpa",label:"hPa"},{value:"mbar",label:"mbar"}];default:return[{value:"native",label:e?`Native (${e})`:"Native"}]}}(a?.units);return(0,r().jsx)("div",{style:{border:"1px solid rgba(128,128,128,0.35)",borderRadius:"6px",padding:"1rem",marginBottom:"1rem"},children:(0,r().jsxs)("div",{style:{display:"grid",gridTemplateColumns:"minmax(220px, 2fr) minmax(180px, 1.2fr) minmax(140px, 1fr) auto",gap:"0.75rem",alignItems:"end"},children:[(0,r().jsxs)("div",{children:[(0,r().jsx)("label",{style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Signal K Path"}),(0,r().jsx)("input",{type:"text",list:`signalk-paths-${t}`,value:e.path,onChange:e=>B(t,"path",e.target.value),placeholder:m?"Loading Signal K paths...":"Search Signal K paths...",disabled:m,style:{width:"100%",padding:"0.5rem"}}),(0,r().jsx)("datalist",{id:`signalk-paths-${t}`,children:c.map(e=>(0,r().jsx)("option",{value:e.path},e.path))}),y&&(0,r().jsx)("div",{style:{marginTop:"0.35rem",fontSize:"0.85rem"},children:y})]}),(0,r().jsxs)("div",{children:[(0,r().jsx)("label",{style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Webhook Field Name"}),(0,r().jsx)("input",{type:"text",value:e.fieldName,onChange:e=>B(t,"fieldName",e.target.value),placeholder:"speed",style:{width:"100%",padding:"0.5rem"}})]}),(0,r().jsxs)("div",{children:[(0,r().jsx)("label",{style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Output Units"}),(0,r().jsx)("select",{value:i.some(t=>t.value===e.units)?e.units:"native",onChange:e=>B(t,"units",e.target.value),style:{width:"100%",padding:"0.5rem"},children:i.map(e=>(0,r().jsx)("option",{value:e.value,children:e.label},e.value))})]}),(0,r().jsx)("button",{type:"button",onClick:()=>{return e=t,void u(t=>t.filter((t,a)=>a!==e));var e},style:{padding:"0.5rem 0.75rem",cursor:"pointer"},children:"Remove"})]})},t)}),(0,r().jsxs)("details",{style:{marginTop:"1.5rem",marginBottom:"1.5rem"},children:[(0,r().jsx)("summary",{style:{cursor:"pointer",fontWeight:"600"},children:"What does Webhook Bridge send?"}),(0,r().jsxs)("div",{style:{marginTop:"1rem",lineHeight:"1.5"},children:[(0,r().jsxs)("p",{children:["Webhook Bridge sends an HTTP POST request containing a JSON object. Each configured Signal K path is sent using the ",(0,r().jsx)("strong",{children:"Webhook Field Name"})," you specify above, with values converted to your selected output units."]}),(0,r().jsxs)("p",{children:["Each request also contains the time the update was captured and a ",(0,r().jsx)("code",{children:"units"})," object describing the units used."]}),(0,r().jsx)("pre",{style:{padding:"1rem",overflowX:"auto",borderRadius:"4px",background:"rgba(128,128,128,0.10)"},children:'{\n "timestamp": "2026-09-13T10:15:00.000Z",\n "speed": 5.64,\n "depth": 12.8,\n "units": {\n "speed": "kn",\n "depth": "m"\n }\n}'}),(0,r().jsxs)("p",{children:["Your webhook should return an HTTP"," ",(0,r().jsx)("strong",{children:"2xx response"})," when the data has been successfully received. If delivery fails, the update remains in the local queue and will be sent again later in its original order."]}),(0,r().jsxs)("p",{style:{marginBottom:0},children:["If an Authentication Key is configured, it is sent as a"," ",(0,r().jsx)("strong",{children:"Bearer token"})," in the"," ",(0,r().jsx)("code",{children:"Authorization"})," header."]})]})]}),(0,r().jsx)("button",{type:"button",onClick:async()=>{x("saving");try{await t({...e,webhookUrl:a,authKey:s,sendFreq:Number(o),paths:h}),x("saved"),setTimeout(()=>{x(null)},4e3)}catch(e){console.error("Unable to save configuration:",e),x("error")}},disabled:"saving"===b,style:{padding:"0.55rem 1rem",cursor:"saving"===b?"default":"pointer",marginTop:"0.5rem",opacity:"saving"===b?.7:1},children:"saving"===b?"Saving...":"Save Configuration"}),"saved"===b&&(0,r().jsx)("div",{style:{marginTop:"0.75rem",padding:"0.65rem 0.8rem",border:"1px solid rgba(76, 175, 80, 0.45)",borderRadius:"6px",background:"rgba(76, 175, 80, 0.12)"},children:"Configuration saved successfully."}),"error"===b&&(0,r().jsx)("div",{style:{marginTop:"0.75rem",padding:"0.65rem 0.8rem",border:"1px solid rgba(244, 67, 54, 0.45)",borderRadius:"6px",background:"rgba(244, 67, 54, 0.12)"},children:"Unable to save configuration. Please try again."})]})}i()}}]);
|
|
2
|
+
"use strict";(self.webpackChunksignalk_webhook_bridge=self.webpackChunksignalk_webhook_bridge||[]).push([[651],{651(e,t,r){r.r(t),r.d(t,{default:()=>l});var a=r.cw(function(e,t){var r=Symbol.for("react.transitional.element"),a=Symbol.for("react.fragment");function i(e,t,a){var i=null;if(void 0!==a&&(i=""+a),void 0!==t.key&&(i=""+t.key),"key"in t)for(var n in a={},t)"key"!==n&&(a[n]=t[n]);else a=t;return t=a.ref,{$$typeof:r,type:e,key:i,ref:void 0!==t?t:null,props:a}}t.Fragment=a,t.jsx=i,t.jsxs=i}),i=r.cw(function(e,t){e.exports=a()}),n=r(231);function l({configuration:e={},save:t}){const[r,i]=(0,n.useState)(e.webhookUrl||""),[l,s]=(0,n.useState)(e.authKey||""),[o,d]=(0,n.useState)(e.sendFreq||10),[u,h]=(0,n.useState)(Array.isArray(e.paths)?e.paths:[]),[c,g]=(0,n.useState)([]),[m,p]=(0,n.useState)(!0),[v,y]=(0,n.useState)(""),[b,x]=(0,n.useState)(null),[f,j]=(0,n.useState)(null),[k,w]=(0,n.useState)(""),[S,C]=(0,n.useState)(!1),[T,B]=(0,n.useState)(null),[K,N]=(0,n.useState)(!1),[W,U]=(0,n.useState)("");(0,n.useEffect)(()=>{fetch("/plugins/signalk-webhook-bridge/paths").then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}`);return e.json()}).then(e=>{g(Array.isArray(e.paths)?e.paths:[]),p(!1)}).catch(e=>{console.error("Unable to load Signal K paths:",e),y("Unable to load available Signal K paths."),p(!1)})},[]),(0,n.useEffect)(()=>{let e=!0;const t=async()=>{try{const t=await fetch("/plugins/signalk-webhook-bridge/status");if(!t.ok)throw new Error(`HTTP ${t.status}`);const r=await t.json();e&&(j(r),w(""))}catch(t){console.error("Unable to load webhook status:",t),e&&w("Unable to load webhook status.")}};t();const r=setInterval(t,5e3);return()=>{e=!1,clearInterval(r)}},[]),(0,n.useEffect)(()=>{const e=u.filter(e=>e&&e.path);if(0===e.length)return B(null),U(""),void N(!1);const t=setTimeout(()=>{N(!0),U(""),fetch("/plugins/signalk-webhook-bridge/preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({paths:e})}).then(async e=>{if(!e.ok){const t=await e.text();throw new Error(t||`HTTP ${e.status}`)}return e.json()}).then(e=>{B(e),N(!1)}).catch(e=>{console.error("Unable to generate webhook preview:",e),U("Unable to generate the payload preview."),N(!1)})},350);return()=>clearTimeout(t)},[u]);const P=(e,t,r)=>{h(a=>a.map((a,i)=>i===e?{...a,[t]:r}:a))};return(0,a().jsxs)("div",{style:{padding:"1rem",maxWidth:"900px"},children:[(0,a().jsx)("h2",{children:"Webhook Bridge"}),(0,a().jsx)("div",{style:{marginBottom:"1.25rem",maxWidth:"750px",lineHeight:"1.5",opacity:.8},children:"Send selected Signal K data to an external webhook at a regular interval. Choose the data paths to include, give each value a webhook field name, and select the units you want to send. If delivery is unavailable, updates are stored locally and automatically sent in order when the connection returns."}),(0,a().jsxs)("div",{style:{marginBottom:"1.5rem",padding:"1rem",borderRadius:"6px",transition:"background 0.2s ease, border 0.2s ease",...(e=>{switch(e){case"queued":case"waiting":return{background:"rgba(255, 152, 0, 0.10)",border:"1px solid rgba(255, 152, 0, 0.40)"};case"error":return{background:"rgba(244, 67, 54, 0.10)",border:"1px solid rgba(244, 67, 54, 0.40)"};default:return{background:"rgba(33, 150, 243, 0.08)",border:"1px solid rgba(33, 150, 243, 0.35)"}}})(f?.deliveryState)},children:[(0,a().jsxs)("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"1rem",marginBottom:"0.75rem"},children:[(0,a().jsxs)("div",{children:[(0,a().jsx)("h3",{style:{margin:0},children:"Status"}),(0,a().jsx)("div",{style:{marginTop:"0.25rem",fontSize:"0.9rem",opacity:.75},children:"Current delivery and queue status."})]}),(0,a().jsx)("button",{type:"button",onClick:async()=>{C(!0);try{const e=await fetch("/plugins/signalk-webhook-bridge/retry",{method:"POST"});if(!e.ok)throw new Error(`HTTP ${e.status}`);const t=await e.json();j(t),w("")}catch(e){console.error("Unable to retry webhook delivery:",e),w("Unable to retry webhook delivery.")}finally{C(!1)}},disabled:S,style:{padding:"0.5rem 0.8rem",cursor:S?"default":"pointer",opacity:S?.7:1},children:S?"Retrying...":"Retry Now"})]}),k&&(0,a().jsx)("div",{style:{marginBottom:"0.75rem",padding:"0.65rem 0.8rem",border:"1px solid rgba(244, 67, 54, 0.45)",borderRadius:"6px",background:"rgba(244, 67, 54, 0.12)"},children:k}),!f&&!k&&(0,a().jsx)("div",{style:{opacity:.75},children:"Loading status..."}),f&&(0,a().jsxs)("div",{style:{display:"grid",gridTemplateColumns:"180px 1fr",gap:"0.5rem 1rem"},children:[(0,a().jsx)("div",{style:{fontWeight:"600"},children:"Delivery"}),(0,a().jsx)("div",{children:"connected"===f.deliveryState?"Connected":"delivering"===f.deliveryState?"Delivering":"waiting"===f.deliveryState?"Waiting to retry":"queued"===f.deliveryState?"Queued":"error"===f.deliveryState?"Error":"Idle"}),(0,a().jsx)("div",{style:{fontWeight:"600"},children:"Queue"}),(0,a().jsxs)("div",{children:[f.queueCount," ",1===f.queueCount?"entry":"entries"," ","waiting"]}),(0,a().jsx)("div",{style:{fontWeight:"600"},children:"Last capture"}),(0,a().jsx)("div",{children:f.lastCapture?new Date(f.lastCapture).toLocaleString():"Not yet"}),(0,a().jsx)("div",{style:{fontWeight:"600"},children:"Last delivery"}),(0,a().jsx)("div",{children:f.lastDelivery?new Date(f.lastDelivery).toLocaleString():"Not yet"}),f.lastError&&(0,a().jsxs)(a().Fragment,{children:[(0,a().jsx)("div",{style:{fontWeight:"600"},children:"Last error"}),(0,a().jsx)("div",{style:{wordBreak:"break-word"},children:f.lastError})]})]})]}),(0,a().jsxs)("div",{style:{marginBottom:"1rem"},children:[(0,a().jsx)("label",{htmlFor:"webhookUrl",style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Webhook URL"}),(0,a().jsx)("input",{id:"webhookUrl",type:"url",value:r,onChange:e=>i(e.target.value),placeholder:"https://example.com/webhook",style:{width:"100%",padding:"0.5rem"}})]}),(0,a().jsxs)("div",{style:{marginBottom:"1rem"},children:[(0,a().jsx)("label",{htmlFor:"authKey",style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Authentication Key"}),(0,a().jsx)("input",{id:"authKey",type:"text",value:l,onChange:e=>s(e.target.value),placeholder:"Optional",style:{width:"100%",padding:"0.5rem"}})]}),(0,a().jsxs)("div",{style:{marginBottom:"1.5rem"},children:[(0,a().jsx)("label",{htmlFor:"sendFreq",style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Send Interval"}),(0,a().jsx)("input",{id:"sendFreq",type:"number",min:"1",value:o,onChange:e=>d(e.target.value),style:{width:"150px",padding:"0.5rem"}}),(0,a().jsx)("div",{style:{marginTop:"0.35rem",fontSize:"0.9rem",opacity:.75},children:"Minutes between webhook updates."})]}),(0,a().jsx)("hr",{style:{margin:"1.5rem 0"}}),(0,a().jsx)("div",{style:{marginBottom:"1rem"},children:(0,a().jsxs)("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"1rem"},children:[(0,a().jsxs)("div",{children:[(0,a().jsx)("h3",{style:{marginBottom:"0.25rem"},children:"Signal K Data Paths"}),(0,a().jsx)("div",{style:{fontSize:"0.9rem",opacity:.75},children:"Choose the Signal K values to include in each webhook update."})]}),(0,a().jsx)("button",{type:"button",onClick:()=>{h(e=>[...e,{path:"",fieldName:"",units:"native"}])},style:{padding:"0.5rem 0.8rem",cursor:"pointer"},children:"Add Path"})]})}),0===u.length&&(0,a().jsx)("div",{style:{padding:"1rem",border:"1px solid rgba(128,128,128,0.35)",borderRadius:"6px",marginBottom:"1rem",opacity:.8},children:"No Signal K paths added yet."}),u.map((e,t)=>{const r=c.find(t=>t.path===e.path),i=function(e){switch(e){case"m/s":return[{value:"native",label:"Native (m/s)"},{value:"knots",label:"Knots"},{value:"kmh",label:"km/h"},{value:"mph",label:"mph"}];case"rad":return[{value:"native",label:"Native (rad)"},{value:"degrees",label:"Degrees"}];case"m":return[{value:"native",label:"Native (m)"},{value:"metres",label:"Metres"},{value:"feet",label:"Feet"}];case"K":return[{value:"native",label:"Native (K)"},{value:"celsius",label:"Celsius"},{value:"fahrenheit",label:"Fahrenheit"}];case"Pa":return[{value:"native",label:"Native (Pa)"},{value:"hpa",label:"hPa"},{value:"mbar",label:"mbar"}];default:return[{value:"native",label:e?`Native (${e})`:"Native"}]}}(r?.units);return(0,a().jsx)("div",{style:{border:"1px solid rgba(128,128,128,0.35)",borderRadius:"6px",padding:"1rem",marginBottom:"1rem"},children:(0,a().jsxs)("div",{style:{display:"grid",gridTemplateColumns:"minmax(220px, 2fr) minmax(180px, 1.2fr) minmax(140px, 1fr) auto",gap:"0.75rem",alignItems:"end"},children:[(0,a().jsxs)("div",{children:[(0,a().jsx)("label",{style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Signal K Path"}),(0,a().jsx)("input",{type:"text",list:`signalk-paths-${t}`,value:e.path,onChange:e=>P(t,"path",e.target.value),placeholder:m?"Loading Signal K paths...":"Search Signal K paths...",disabled:m,style:{width:"100%",padding:"0.5rem"}}),(0,a().jsx)("datalist",{id:`signalk-paths-${t}`,children:c.map(e=>(0,a().jsx)("option",{value:e.path},e.path))}),v&&(0,a().jsx)("div",{style:{marginTop:"0.35rem",fontSize:"0.85rem"},children:v})]}),(0,a().jsxs)("div",{children:[(0,a().jsx)("label",{style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Webhook Field Name"}),(0,a().jsx)("input",{type:"text",value:e.fieldName,onChange:e=>P(t,"fieldName",e.target.value),placeholder:"speed",style:{width:"100%",padding:"0.5rem"}})]}),(0,a().jsxs)("div",{children:[(0,a().jsx)("label",{style:{display:"block",fontWeight:"600",marginBottom:"0.35rem"},children:"Output Units"}),(0,a().jsx)("select",{value:i.some(t=>t.value===e.units)?e.units:"native",onChange:e=>P(t,"units",e.target.value),style:{width:"100%",padding:"0.5rem"},children:i.map(e=>(0,a().jsx)("option",{value:e.value,children:e.label},e.value))})]}),(0,a().jsx)("button",{type:"button",onClick:()=>{return e=t,void h(t=>t.filter((t,r)=>r!==e));var e},style:{padding:"0.5rem 0.75rem",cursor:"pointer"},children:"Remove"})]})},t)}),(0,a().jsxs)("div",{style:{marginTop:"1.5rem",marginBottom:"1.5rem",border:"1px solid rgba(128,128,128,0.35)",borderRadius:"6px",overflow:"hidden"},children:[(0,a().jsxs)("div",{style:{padding:"0.9rem 1rem",borderBottom:"1px solid rgba(128,128,128,0.25)"},children:[(0,a().jsx)("div",{style:{fontWeight:"600",fontSize:"1.05rem"},children:"Current Payload Preview"}),(0,a().jsx)("div",{style:{marginTop:"0.25rem",fontSize:"0.9rem",opacity:.75},children:"This is the JSON structure your webhook will receive using the paths, field names and units currently configured above. Values shown are the current live Signal K values."})]}),(0,a().jsxs)("div",{style:{padding:"1rem"},children:[0===u.filter(e=>e&&e.path).length&&(0,a().jsx)("div",{style:{opacity:.7},children:"Add a Signal K path to see the outgoing payload."}),K&&!T&&(0,a().jsx)("div",{style:{opacity:.7},children:"Loading current Signal K values..."}),W&&(0,a().jsx)("div",{style:{padding:"0.75rem",borderRadius:"4px",background:"rgba(244, 67, 54, 0.10)",border:"1px solid rgba(244, 67, 54, 0.35)"},children:W}),T&&(0,a().jsxs)(a().Fragment,{children:[(0,a().jsx)("pre",{style:{margin:0,padding:"1rem",overflowX:"auto",borderRadius:"4px",background:"rgba(128,128,128,0.10)",fontSize:"0.9rem",lineHeight:"1.5"},children:JSON.stringify(T,null,2)}),K&&(0,a().jsx)("div",{style:{marginTop:"0.5rem",fontSize:"0.85rem",opacity:.6},children:"Updating preview..."})]})]})]}),(0,a().jsx)("button",{type:"button",onClick:async()=>{x("saving");try{await t({...e,webhookUrl:r,authKey:l,sendFreq:Number(o),paths:u}),x("saved"),setTimeout(()=>{x(null)},4e3)}catch(e){console.error("Unable to save configuration:",e),x("error")}},disabled:"saving"===b,style:{padding:"0.55rem 1rem",cursor:"saving"===b?"default":"pointer",marginTop:"0.5rem",opacity:"saving"===b?.7:1},children:"saving"===b?"Saving...":"Save Configuration"}),"saved"===b&&(0,a().jsx)("div",{style:{marginTop:"0.75rem",padding:"0.65rem 0.8rem",border:"1px solid rgba(76, 175, 80, 0.45)",borderRadius:"6px",background:"rgba(76, 175, 80, 0.12)"},children:"Configuration saved successfully."}),"error"===b&&(0,a().jsx)("div",{style:{marginTop:"0.75rem",padding:"0.65rem 0.8rem",border:"1px solid rgba(244, 67, 54, 0.45)",borderRadius:"6px",background:"rgba(244, 67, 54, 0.12)"},children:"Unable to save configuration. Please try again."})]})}i()}}]);
|