signalk-edge-link 2.6.0 → 2.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/README.md +31 -8
- package/lib/connection-config.js +1 -4
- package/lib/index.js +22 -16
- package/lib/instance.js +8 -49
- package/lib/pipeline.js +1 -215
- package/lib/routes/config.js +2 -1
- package/lib/routes/connections.js +6 -8
- package/lib/shared/connection-schema.js +0 -7
- package/lib/values-snapshot.js +79 -9
- package/package.json +165 -165
- package/public/982.fb1b6560eada159d88ee.js +2 -0
- package/public/982.fb1b6560eada159d88ee.js.map +1 -0
- package/public/remoteEntry.js +1 -1
- package/public/remoteEntry.js.map +1 -1
- package/public/982.078efbd502a09820e418.js +0 -2
- package/public/982.078efbd502a09820e418.js.map +0 -1
package/lib/values-snapshot.js
CHANGED
|
@@ -72,13 +72,63 @@ function walkValues(node, pathParts, onLeaf) {
|
|
|
72
72
|
walkValues(node[key], pathParts.concat(key), onLeaf);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Build a lookup map from $source reference string → structured source object
|
|
77
|
+
* using the top-level `sources` section of the SK full model tree.
|
|
78
|
+
*
|
|
79
|
+
* signalk-server stores sources as sources[provider][key] = { label, type, ... }
|
|
80
|
+
* and formats $source references as "provider.key". This function inverts that
|
|
81
|
+
* structure so we can attach the correct source object to each synthetic update.
|
|
82
|
+
*
|
|
83
|
+
* Falls back to a minimal { label } entry derived from the $source string if
|
|
84
|
+
* the sources section is absent or the exact reference is not found there.
|
|
85
|
+
*/
|
|
86
|
+
function buildSourceLookup(tree) {
|
|
87
|
+
const lookup = new Map();
|
|
88
|
+
const sourcesNode = tree.sources;
|
|
89
|
+
if (!isRecord(sourcesNode)) {
|
|
90
|
+
return lookup;
|
|
91
|
+
}
|
|
92
|
+
for (const [provider, providerNode] of Object.entries(sourcesNode)) {
|
|
93
|
+
if (!isRecord(providerNode)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
for (const [key, sourceObj] of Object.entries(providerNode)) {
|
|
97
|
+
if (isRecord(sourceObj)) {
|
|
98
|
+
lookup.set(`${provider}.${key}`, sourceObj);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return lookup;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve the source object for a $source reference string. Tries the exact
|
|
106
|
+
* reference in the lookup, then falls back to a { label } derived from the
|
|
107
|
+
* part of the reference before the first ".".
|
|
108
|
+
*/
|
|
109
|
+
function resolveSource(sourceRef, lookup) {
|
|
110
|
+
const found = lookup.get(sourceRef);
|
|
111
|
+
if (found) {
|
|
112
|
+
return found;
|
|
113
|
+
}
|
|
114
|
+
// Fallback: provider label is the part before the first "."
|
|
115
|
+
const dotIdx = sourceRef.indexOf(".");
|
|
116
|
+
const label = dotIdx > 0 ? sourceRef.slice(0, dotIdx) : sourceRef;
|
|
117
|
+
return label ? { label } : undefined;
|
|
118
|
+
}
|
|
75
119
|
/**
|
|
76
120
|
* Build synthetic deltas for every value currently in the Signal K tree.
|
|
77
121
|
*
|
|
78
|
-
* Returns one delta per `(context, source)`
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
122
|
+
* Returns one delta per `(context, source, timestamp)` triple so that the
|
|
123
|
+
* original per-path measurement time is preserved. Values from the same
|
|
124
|
+
* source that were last updated at different times (e.g. GPS speed vs
|
|
125
|
+
* autopilot settings) end up in separate updates rather than being collapsed
|
|
126
|
+
* under the latest timestamp of the group.
|
|
127
|
+
*
|
|
128
|
+
* Each update carries both `$source` (the reference string) and `source`
|
|
129
|
+
* (the structured source object looked up from the SK sources tree) so that
|
|
130
|
+
* `handleMessageBySource` can call `app.handleMessage(source.label, delta)`
|
|
131
|
+
* with the original instrument label rather than an empty provider ID.
|
|
82
132
|
*
|
|
83
133
|
* Returns [] when `app.signalk` isn't exposed (older signalk-server) or the
|
|
84
134
|
* tree is empty.
|
|
@@ -98,7 +148,13 @@ function collectValuesSnapshot(app) {
|
|
|
98
148
|
catch {
|
|
99
149
|
return [];
|
|
100
150
|
}
|
|
101
|
-
//
|
|
151
|
+
// Build $source → source object lookup from the top-level sources tree so
|
|
152
|
+
// each synthetic update can carry the correct source.label for attribution.
|
|
153
|
+
const sourceLookup = buildSourceLookup(tree);
|
|
154
|
+
// Group leaves by (context, source, timestamp) so each distinct measurement
|
|
155
|
+
// time gets its own update. This preserves per-path timestamps: paths from
|
|
156
|
+
// the same source that were last updated at different times (e.g. GPS vs
|
|
157
|
+
// autopilot settings) are not collapsed under the same (latest) timestamp.
|
|
102
158
|
const grouped = new Map();
|
|
103
159
|
for (const contextGroup of Object.keys(tree)) {
|
|
104
160
|
if (SK_NON_CONTEXT_KEYS.has(contextGroup)) {
|
|
@@ -115,13 +171,23 @@ function collectValuesSnapshot(app) {
|
|
|
115
171
|
}
|
|
116
172
|
const context = `${contextGroup}.${contextId}`;
|
|
117
173
|
walkValues(contextNode, [], (leaf) => {
|
|
118
|
-
|
|
174
|
+
// Skip values that this plugin injected from remote instances.
|
|
175
|
+
// SK stores them under "signalk-edge-link.*" $source keys. Including
|
|
176
|
+
// them in the snapshot would loop remote data back to its origin and
|
|
177
|
+
// propagate wrong source labels (the fallback label derived from the
|
|
178
|
+
// "signalk-edge-link" prefix is never the original sensor label).
|
|
179
|
+
// Live streaming handles relay correctly via subscription callbacks,
|
|
180
|
+
// which SK populates with the full source object automatically.
|
|
181
|
+
const src = leaf.source ?? "";
|
|
182
|
+
if (src === "signalk-edge-link" ||
|
|
183
|
+
src.startsWith("signalk-edge-link.") ||
|
|
184
|
+
src.startsWith("signalk-edge-link:")) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const key = `${context}|${leaf.source ?? ""}|${leaf.timestamp}`;
|
|
119
188
|
const existing = grouped.get(key);
|
|
120
189
|
if (existing) {
|
|
121
190
|
existing.values.push({ path: leaf.path, value: leaf.value });
|
|
122
|
-
if (leaf.timestamp > existing.timestamp) {
|
|
123
|
-
existing.timestamp = leaf.timestamp;
|
|
124
|
-
}
|
|
125
191
|
}
|
|
126
192
|
else {
|
|
127
193
|
grouped.set(key, {
|
|
@@ -145,6 +211,10 @@ function collectValuesSnapshot(app) {
|
|
|
145
211
|
};
|
|
146
212
|
if (entry.source) {
|
|
147
213
|
update.$source = entry.source;
|
|
214
|
+
const sourceObj = resolveSource(entry.source, sourceLookup);
|
|
215
|
+
if (sourceObj) {
|
|
216
|
+
update.source = sourceObj;
|
|
217
|
+
}
|
|
148
218
|
}
|
|
149
219
|
deltas.push({ context: entry.context, updates: [update] });
|
|
150
220
|
}
|
package/package.json
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "signalk-edge-link",
|
|
3
|
-
"version": "2.6.
|
|
4
|
-
"description": "SignalK Edge Link. Secure UDP link for data exchange.",
|
|
5
|
-
"main": "lib/index.js",
|
|
6
|
-
"files": [
|
|
7
|
-
"lib/",
|
|
8
|
-
"public/"
|
|
9
|
-
],
|
|
10
|
-
"keywords": [
|
|
11
|
-
"signalk-node-server-plugin",
|
|
12
|
-
"signalk-category-network",
|
|
13
|
-
"signalk-webapp",
|
|
14
|
-
"signalk-category-utility",
|
|
15
|
-
"signalk-plugin-configurator"
|
|
16
|
-
],
|
|
17
|
-
"signalk": {
|
|
18
|
-
"appIcon": "./icons/icon-72x72.png",
|
|
19
|
-
"displayName": "Edge Link Configuration"
|
|
20
|
-
},
|
|
21
|
-
"signalk-plugin-enabled-by-default": false,
|
|
22
|
-
"scripts": {
|
|
23
|
-
"clean:lib": "node -e \"const fs=require('fs');if(fs.existsSync('lib'))fs.rmSync('lib',{recursive:true,force:true});\"",
|
|
24
|
-
"clean:public": "node -e \"const fs=require('fs');if(fs.existsSync('public'))fs.rmSync('public',{recursive:true,force:true});\"",
|
|
25
|
-
"build": "npm run build:ts && npm run build:web",
|
|
26
|
-
"build:web": "npm run clean:public && webpack --mode production",
|
|
27
|
-
"build:ts": "npm run clean:lib && tsc",
|
|
28
|
-
"check:ts": "tsc --noEmit",
|
|
29
|
-
"check:release-docs": "node scripts/check-release-truth.js",
|
|
30
|
-
"dev": "webpack --mode development --watch",
|
|
31
|
-
"test": "jest --runInBand",
|
|
32
|
-
"test:v2": "jest __tests__/v2/",
|
|
33
|
-
"test:integration": "jest test/integration/",
|
|
34
|
-
"test:watch": "jest --watch",
|
|
35
|
-
"test:coverage": "jest --coverage",
|
|
36
|
-
"lint": "eslint .",
|
|
37
|
-
"lint:fix": "eslint . --fix",
|
|
38
|
-
"format": "prettier --write \"**/*.{js,ts,json,md}\"",
|
|
39
|
-
"migrate:config": "node lib/scripts/migrate-config.js",
|
|
40
|
-
"cli": "node lib/bin/edge-link-cli.js",
|
|
41
|
-
"prepare": "husky"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"@msgpack/msgpack": "^3.0.0",
|
|
45
|
-
"@rjsf/core": "^5.18.4",
|
|
46
|
-
"@rjsf/utils": "^5.18.4",
|
|
47
|
-
"@rjsf/validator-ajv8": "^5.18.4",
|
|
48
|
-
"ping-monitor": "^0.8.2"
|
|
49
|
-
},
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"@babel/core": "^7.22.0",
|
|
52
|
-
"@babel/preset-env": "^7.22.0",
|
|
53
|
-
"@babel/preset-react": "^7.22.0",
|
|
54
|
-
"@testing-library/jest-dom": "^5.17.0",
|
|
55
|
-
"@testing-library/react": "^12.1.5",
|
|
56
|
-
"@types/node": "^25.3.5",
|
|
57
|
-
"@types/react": "^16.14.0",
|
|
58
|
-
"@types/react-dom": "^16.9.0",
|
|
59
|
-
"babel-loader": "^9.1.2",
|
|
60
|
-
"copy-webpack-plugin": "^14.0.0",
|
|
61
|
-
"css-loader": "^6.8.1",
|
|
62
|
-
"eslint": "^8.57.1",
|
|
63
|
-
"eslint-plugin-react": "^7.37.5",
|
|
64
|
-
"html-webpack-plugin": "^5.5.3",
|
|
65
|
-
"husky": "^9.1.7",
|
|
66
|
-
"jest": "^29.7.0",
|
|
67
|
-
"jest-environment-jsdom": "^30.3.0",
|
|
68
|
-
"lint-staged": "^15.4.3",
|
|
69
|
-
"mini-css-extract-plugin": "^2.7.6",
|
|
70
|
-
"prettier": "^3.6.2",
|
|
71
|
-
"react": "^16.13.1",
|
|
72
|
-
"react-dom": "^16.13.1",
|
|
73
|
-
"react-test-renderer": "^16.14.0",
|
|
74
|
-
"style-loader": "^3.3.3",
|
|
75
|
-
"ts-jest": "^29.4.6",
|
|
76
|
-
"ts-loader": "^9.5.4",
|
|
77
|
-
"typescript": "^5.9.3",
|
|
78
|
-
"webpack": "^5.102.1",
|
|
79
|
-
"webpack-cli": "^5.1.4"
|
|
80
|
-
},
|
|
81
|
-
"engines": {
|
|
82
|
-
"node": ">=16"
|
|
83
|
-
},
|
|
84
|
-
"author": "Karl-Erik Gustafsson",
|
|
85
|
-
"repository": "https://github.com/KEGustafsson/signalk-edge-link",
|
|
86
|
-
"homepage": "https://github.com/KEGustafsson/signalk-edge-link#readme",
|
|
87
|
-
"bugs": {
|
|
88
|
-
"url": "https://github.com/KEGustafsson/signalk-edge-link/issues"
|
|
89
|
-
},
|
|
90
|
-
"license": "MIT",
|
|
91
|
-
"jest": {
|
|
92
|
-
"testEnvironment": "node",
|
|
93
|
-
"coverageDirectory": "coverage",
|
|
94
|
-
"collectCoverageFrom": [
|
|
95
|
-
"lib/**/*.js",
|
|
96
|
-
"!lib/webapp/**",
|
|
97
|
-
"!lib/components/**",
|
|
98
|
-
"!lib/utils/**"
|
|
99
|
-
],
|
|
100
|
-
"testMatch": [
|
|
101
|
-
"**/__tests__/**/*.js",
|
|
102
|
-
"**/*.test.js",
|
|
103
|
-
"**/*.spec.js"
|
|
104
|
-
],
|
|
105
|
-
"transform": {
|
|
106
|
-
"^.+\\.js$": [
|
|
107
|
-
"babel-jest",
|
|
108
|
-
{
|
|
109
|
-
"presets": [
|
|
110
|
-
[
|
|
111
|
-
"@babel/preset-env",
|
|
112
|
-
{
|
|
113
|
-
"targets": {
|
|
114
|
-
"node": "current"
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
]
|
|
118
|
-
]
|
|
119
|
-
}
|
|
120
|
-
],
|
|
121
|
-
".+\\.tsx$": [
|
|
122
|
-
"ts-jest",
|
|
123
|
-
{
|
|
124
|
-
"tsconfig": "tsconfig.webapp.json",
|
|
125
|
-
"diagnostics": false
|
|
126
|
-
}
|
|
127
|
-
],
|
|
128
|
-
"^.+\\.ts$": "ts-jest"
|
|
129
|
-
},
|
|
130
|
-
"moduleFileExtensions": [
|
|
131
|
-
"ts",
|
|
132
|
-
"tsx",
|
|
133
|
-
"js",
|
|
134
|
-
"json",
|
|
135
|
-
"node"
|
|
136
|
-
],
|
|
137
|
-
"testPathIgnorePatterns": [
|
|
138
|
-
"/node_modules/",
|
|
139
|
-
"/public/"
|
|
140
|
-
],
|
|
141
|
-
"coverageThreshold": {
|
|
142
|
-
"global": {
|
|
143
|
-
"branches": 60,
|
|
144
|
-
"functions": 65,
|
|
145
|
-
"lines": 65,
|
|
146
|
-
"statements": 65
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
"bin": {
|
|
151
|
-
"edge-link-cli": "lib/bin/edge-link-cli.js"
|
|
152
|
-
},
|
|
153
|
-
"lint-staged": {
|
|
154
|
-
"*.js": [
|
|
155
|
-
"prettier --write",
|
|
156
|
-
"eslint --fix"
|
|
157
|
-
],
|
|
158
|
-
"*.ts": [
|
|
159
|
-
"prettier --write"
|
|
160
|
-
],
|
|
161
|
-
"*.{json,md}": [
|
|
162
|
-
"prettier --write"
|
|
163
|
-
]
|
|
164
|
-
}
|
|
165
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "signalk-edge-link",
|
|
3
|
+
"version": "2.6.2",
|
|
4
|
+
"description": "SignalK Edge Link. Secure UDP link for data exchange.",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"lib/",
|
|
8
|
+
"public/"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"signalk-node-server-plugin",
|
|
12
|
+
"signalk-category-network",
|
|
13
|
+
"signalk-webapp",
|
|
14
|
+
"signalk-category-utility",
|
|
15
|
+
"signalk-plugin-configurator"
|
|
16
|
+
],
|
|
17
|
+
"signalk": {
|
|
18
|
+
"appIcon": "./icons/icon-72x72.png",
|
|
19
|
+
"displayName": "Edge Link Configuration"
|
|
20
|
+
},
|
|
21
|
+
"signalk-plugin-enabled-by-default": false,
|
|
22
|
+
"scripts": {
|
|
23
|
+
"clean:lib": "node -e \"const fs=require('fs');if(fs.existsSync('lib'))fs.rmSync('lib',{recursive:true,force:true});\"",
|
|
24
|
+
"clean:public": "node -e \"const fs=require('fs');if(fs.existsSync('public'))fs.rmSync('public',{recursive:true,force:true});\"",
|
|
25
|
+
"build": "npm run build:ts && npm run build:web",
|
|
26
|
+
"build:web": "npm run clean:public && webpack --mode production",
|
|
27
|
+
"build:ts": "npm run clean:lib && tsc",
|
|
28
|
+
"check:ts": "tsc --noEmit",
|
|
29
|
+
"check:release-docs": "node scripts/check-release-truth.js",
|
|
30
|
+
"dev": "webpack --mode development --watch",
|
|
31
|
+
"test": "jest --runInBand",
|
|
32
|
+
"test:v2": "jest __tests__/v2/",
|
|
33
|
+
"test:integration": "jest test/integration/",
|
|
34
|
+
"test:watch": "jest --watch",
|
|
35
|
+
"test:coverage": "jest --coverage",
|
|
36
|
+
"lint": "eslint .",
|
|
37
|
+
"lint:fix": "eslint . --fix",
|
|
38
|
+
"format": "prettier --write \"**/*.{js,ts,json,md}\"",
|
|
39
|
+
"migrate:config": "node lib/scripts/migrate-config.js",
|
|
40
|
+
"cli": "node lib/bin/edge-link-cli.js",
|
|
41
|
+
"prepare": "husky"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@msgpack/msgpack": "^3.0.0",
|
|
45
|
+
"@rjsf/core": "^5.18.4",
|
|
46
|
+
"@rjsf/utils": "^5.18.4",
|
|
47
|
+
"@rjsf/validator-ajv8": "^5.18.4",
|
|
48
|
+
"ping-monitor": "^0.8.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@babel/core": "^7.22.0",
|
|
52
|
+
"@babel/preset-env": "^7.22.0",
|
|
53
|
+
"@babel/preset-react": "^7.22.0",
|
|
54
|
+
"@testing-library/jest-dom": "^5.17.0",
|
|
55
|
+
"@testing-library/react": "^12.1.5",
|
|
56
|
+
"@types/node": "^25.3.5",
|
|
57
|
+
"@types/react": "^16.14.0",
|
|
58
|
+
"@types/react-dom": "^16.9.0",
|
|
59
|
+
"babel-loader": "^9.1.2",
|
|
60
|
+
"copy-webpack-plugin": "^14.0.0",
|
|
61
|
+
"css-loader": "^6.8.1",
|
|
62
|
+
"eslint": "^8.57.1",
|
|
63
|
+
"eslint-plugin-react": "^7.37.5",
|
|
64
|
+
"html-webpack-plugin": "^5.5.3",
|
|
65
|
+
"husky": "^9.1.7",
|
|
66
|
+
"jest": "^29.7.0",
|
|
67
|
+
"jest-environment-jsdom": "^30.3.0",
|
|
68
|
+
"lint-staged": "^15.4.3",
|
|
69
|
+
"mini-css-extract-plugin": "^2.7.6",
|
|
70
|
+
"prettier": "^3.6.2",
|
|
71
|
+
"react": "^16.13.1",
|
|
72
|
+
"react-dom": "^16.13.1",
|
|
73
|
+
"react-test-renderer": "^16.14.0",
|
|
74
|
+
"style-loader": "^3.3.3",
|
|
75
|
+
"ts-jest": "^29.4.6",
|
|
76
|
+
"ts-loader": "^9.5.4",
|
|
77
|
+
"typescript": "^5.9.3",
|
|
78
|
+
"webpack": "^5.102.1",
|
|
79
|
+
"webpack-cli": "^5.1.4"
|
|
80
|
+
},
|
|
81
|
+
"engines": {
|
|
82
|
+
"node": ">=16"
|
|
83
|
+
},
|
|
84
|
+
"author": "Karl-Erik Gustafsson",
|
|
85
|
+
"repository": "https://github.com/KEGustafsson/signalk-edge-link",
|
|
86
|
+
"homepage": "https://github.com/KEGustafsson/signalk-edge-link#readme",
|
|
87
|
+
"bugs": {
|
|
88
|
+
"url": "https://github.com/KEGustafsson/signalk-edge-link/issues"
|
|
89
|
+
},
|
|
90
|
+
"license": "MIT",
|
|
91
|
+
"jest": {
|
|
92
|
+
"testEnvironment": "node",
|
|
93
|
+
"coverageDirectory": "coverage",
|
|
94
|
+
"collectCoverageFrom": [
|
|
95
|
+
"lib/**/*.js",
|
|
96
|
+
"!lib/webapp/**",
|
|
97
|
+
"!lib/components/**",
|
|
98
|
+
"!lib/utils/**"
|
|
99
|
+
],
|
|
100
|
+
"testMatch": [
|
|
101
|
+
"**/__tests__/**/*.js",
|
|
102
|
+
"**/*.test.js",
|
|
103
|
+
"**/*.spec.js"
|
|
104
|
+
],
|
|
105
|
+
"transform": {
|
|
106
|
+
"^.+\\.js$": [
|
|
107
|
+
"babel-jest",
|
|
108
|
+
{
|
|
109
|
+
"presets": [
|
|
110
|
+
[
|
|
111
|
+
"@babel/preset-env",
|
|
112
|
+
{
|
|
113
|
+
"targets": {
|
|
114
|
+
"node": "current"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
".+\\.tsx$": [
|
|
122
|
+
"ts-jest",
|
|
123
|
+
{
|
|
124
|
+
"tsconfig": "tsconfig.webapp.json",
|
|
125
|
+
"diagnostics": false
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"^.+\\.ts$": "ts-jest"
|
|
129
|
+
},
|
|
130
|
+
"moduleFileExtensions": [
|
|
131
|
+
"ts",
|
|
132
|
+
"tsx",
|
|
133
|
+
"js",
|
|
134
|
+
"json",
|
|
135
|
+
"node"
|
|
136
|
+
],
|
|
137
|
+
"testPathIgnorePatterns": [
|
|
138
|
+
"/node_modules/",
|
|
139
|
+
"/public/"
|
|
140
|
+
],
|
|
141
|
+
"coverageThreshold": {
|
|
142
|
+
"global": {
|
|
143
|
+
"branches": 60,
|
|
144
|
+
"functions": 65,
|
|
145
|
+
"lines": 65,
|
|
146
|
+
"statements": 65
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"bin": {
|
|
151
|
+
"edge-link-cli": "lib/bin/edge-link-cli.js"
|
|
152
|
+
},
|
|
153
|
+
"lint-staged": {
|
|
154
|
+
"*.js": [
|
|
155
|
+
"prettier --write",
|
|
156
|
+
"eslint --fix"
|
|
157
|
+
],
|
|
158
|
+
"*.ts": [
|
|
159
|
+
"prettier --write"
|
|
160
|
+
],
|
|
161
|
+
"*.{json,md}": [
|
|
162
|
+
"prettier --write"
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";(self.webpackChunksignalk_edge_link=self.webpackChunksignalk_edge_link||[]).push([[982],{4353(e,t,n){n.r(t),n.d(t,{default:()=>M});var r=n(4147),i=n(6718),o=n(4810),a=n(7936);const s="Management token required/invalid.",l={token:null,localStorageKey:"signalkEdgeLinkManagementToken",queryParam:"edgeLinkToken",includeTokenInQuery:!1,headerMode:"both"};function c(e,t={}){const n=function(){if("undefined"==typeof window)return l;const e=window.__EDGE_LINK_AUTH__;return e&&"object"==typeof e?{...l,...e}:l}(),r=function(e){if(e.token)return String(e.token).trim();if("undefined"==typeof window)return"";if(e.includeTokenInQuery&&e.queryParam){const t=new URLSearchParams(window.location.search).get(e.queryParam);if(t)return t.trim()}if(e.localStorageKey&&window.localStorage){const t=window.localStorage.getItem(e.localStorageKey);if(t)return t.trim()}return""}(n),i=new Headers(t.headers||{});return function(e,t,n){if(!t)return e;const r=(n||"both").toLowerCase();"x-edge-link-token"!==r&&"token"!==r&&"both"!==r||e.set("X-Edge-Link-Token",t),"authorization"!==r&&"bearer"!==r&&"both"!==r||e.set("Authorization",`Bearer ${t}`)}(i,r,n.headerMode),fetch(e,{...t,headers:i})}const d={name:{type:"string",title:"Connection Name",description:"Human-readable label for this connection (e.g. 'Shore Server', 'Sat Client'). Used to namespace config files and Signal K metrics paths.",default:"connection",maxLength:40},serverType:{type:"string",title:"Operation Mode",description:"Select Server to receive data, or Client to send data.",default:"client",oneOf:[{const:"server",title:"Server Mode – Receive Data"},{const:"client",title:"Client Mode – Send Data"}]},udpPort:{type:"number",title:"UDP Port",description:"UDP port for data transmission (must match on both ends).",default:4446,minimum:1024,maximum:65535},secretKey:{type:"string",title:"Encryption Key",description:"32-byte secret key: 32-character ASCII, 64-character hex, or 44-character base64.",minLength:32,maxLength:64,pattern:"^(?:.{32}|[0-9a-fA-F]{64}|[A-Za-z0-9+/]{43}=?)$"},stretchAsciiKey:{type:"boolean",title:"Stretch 32-char ASCII Key (PBKDF2)",description:`When the secretKey is 32-character ASCII, route it through PBKDF2-SHA256 (${6e5.toLocaleString("en-US")} iterations) to raise it to full 256-bit AES strength. Hex and base64 keys are unaffected. BOTH ENDS OF THE CONNECTION MUST USE THE SAME SETTING — otherwise authentication will fail and every packet will be dropped.`,default:!1},useMsgpack:{type:"boolean",title:"Use MessagePack",description:"Binary serialization for smaller payloads (must match on both ends).",default:!1},usePathDictionary:{type:"boolean",title:"Use Path Dictionary",description:"Encode paths as numeric IDs for bandwidth savings (must match on both ends).",default:!1},protocolVersion:{type:"number",title:"Protocol Version",description:"v1: encrypted UDP. v2 adds reliable delivery and metrics. v3 keeps the v2 data path and authenticates control packets (ACK/NAK/HEARTBEAT/HELLO). Must match on both ends.",default:1,oneOf:[{const:1,title:"v1 – Standard encrypted UDP"},{const:2,title:"v2 – Reliability, congestion control, bonding, metrics"},{const:3,title:"v3 - v2 features with authenticated control packets"}]}},m={testAddress:{type:"string",title:"Connectivity Test Address (v1 only)",description:"Host used for reachability checks (e.g. 8.8.8.8). v1 only.",default:"127.0.0.1"},testPort:{type:"number",title:"Connectivity Test Port (v1 only)",description:"Port used for reachability checks (e.g. 53, 80, or 443). v1 only.",default:80,minimum:1,maximum:65535},pingIntervalTime:{type:"number",title:"Check Interval (minutes, v1 only)",description:"Frequency of network reachability checks. v1 only.",default:1,minimum:.1,maximum:60}},u={udpAddress:{type:"string",title:"Server Address",description:"IP address or hostname of the remote Signal K endpoint.",default:"127.0.0.1"},helloMessageSender:{type:"integer",title:"Heartbeat Interval (seconds)",description:"Send periodic heartbeat messages to keep NAT/firewall mappings alive.",default:60,minimum:10,maximum:3600},heartbeatInterval:{type:"number",title:"NAT Keepalive Heartbeat Interval (ms)",description:"v2/v3 only. How often to send UDP heartbeat packets for NAT traversal. Typical NAT timeouts range from 30s to 120s.",default:25e3,minimum:5e3,maximum:12e4}},p={type:"object",title:"Reliability Settings (v2/v3 only)",description:"Requires Protocol v2 or v3. Controls retransmit queue behavior and packet retry limits.",properties:{retransmitQueueSize:{type:"number",title:"Retransmit Queue Size",description:"Maximum number of sent packets stored for potential retransmission.",default:5e3,minimum:100,maximum:5e4},maxRetransmits:{type:"number",title:"Max Retransmit Attempts",description:"Maximum resend attempts before a packet is dropped from the retransmit queue.",default:3,minimum:1,maximum:20},retransmitMaxAge:{type:"number",title:"Retransmit Max Age (ms)",description:"Expire stale unacknowledged packets older than this age.",default:12e4,minimum:1e3,maximum:3e5},retransmitMinAge:{type:"number",title:"Retransmit Min Age (ms)",description:"Minimum packet age before expiration is allowed.",default:1e4,minimum:200,maximum:3e4},retransmitRttMultiplier:{type:"number",title:"RTT Expiry Multiplier",description:"Dynamic expiry age = RTT × this multiplier.",default:12,minimum:2,maximum:20},ackIdleDrainAge:{type:"number",title:"ACK Idle Drain Age (ms)",description:"If ACKs are idle longer than this, expiry becomes more aggressive.",default:2e4,minimum:500,maximum:3e4},forceDrainAfterAckIdle:{type:"boolean",title:"Force Drain After ACK Idle",description:"When enabled, clear retransmit queue if no ACKs arrive for too long.",default:!1},forceDrainAfterMs:{type:"number",title:"Force Drain Timeout (ms)",description:"ACK idle duration before force-draining retransmit queue to zero.",default:45e3,minimum:2e3,maximum:12e4},recoveryBurstEnabled:{type:"boolean",title:"Recovery Burst Enabled",description:"When ACKs return after outage, rapidly retransmit queued packets to catch up.",default:!0},recoveryBurstSize:{type:"number",title:"Recovery Burst Size",description:"Max queued packets to retransmit per recovery burst cycle.",default:100,minimum:10,maximum:1e3},recoveryBurstIntervalMs:{type:"number",title:"Recovery Burst Interval (ms)",description:"Interval between recovery burst cycles while backlog exists.",default:200,minimum:50,maximum:5e3},recoveryAckGapMs:{type:"number",title:"Recovery ACK Gap (ms)",description:"Minimum ACK silence before triggering fast recovery bursts.",default:4e3,minimum:500,maximum:12e4}}},g={type:"boolean",title:"Request Full Status on Server Start (v2/v3 only)",description:"When enabled, the server sends a request to each client on first contact asking it to replay its complete current values snapshot. This rebuilds the server's state immediately after a restart instead of waiting for incremental deltas to arrive.",default:!1},f={type:"object",title:"Reliability Settings (v2/v3 only)",description:"Requires Protocol v2 or v3. Controls ACK/NAK timing for reliable delivery.",properties:{ackInterval:{type:"number",title:"ACK Interval (ms)",description:"How often server sends cumulative ACK updates.",default:100,minimum:20,maximum:5e3},ackResendInterval:{type:"number",title:"ACK Resend Interval (ms)",description:"Re-send duplicate ACK periodically to recover from lost ACK packets.",default:1e3,minimum:100,maximum:1e4},nakTimeout:{type:"number",title:"NAK Timeout (ms)",description:"Delay before requesting retransmission for missing sequence numbers.",default:100,minimum:20,maximum:5e3}}},y={type:"object",title:"Dynamic Congestion Control (v2/v3 only)",description:"Requires Protocol v2 or v3. AIMD algorithm to dynamically adjust send rate based on network conditions.",properties:{enabled:{type:"boolean",title:"Enable Congestion Control",description:"Automatically adjust delta timer based on RTT and packet loss.",default:!1},targetRTT:{type:"number",title:"Target RTT (ms)",description:"RTT threshold above which send rate is reduced.",default:200,minimum:50,maximum:2e3},nominalDeltaTimer:{type:"number",title:"Nominal Delta Timer (ms)",description:"Preferred steady-state send interval.",default:1e3,minimum:100,maximum:1e4},minDeltaTimer:{type:"number",title:"Minimum Delta Timer (ms)",description:"Fastest allowed send interval.",default:100,minimum:50,maximum:1e3},maxDeltaTimer:{type:"number",title:"Maximum Delta Timer (ms)",description:"Slowest allowed send interval.",default:5e3,minimum:1e3,maximum:3e4}}},b={type:"object",title:"Connection Bonding (v2/v3 only)",description:"Requires Protocol v2 or v3. Dual-link bonding with automatic failover between primary and backup connections.",properties:{enabled:{type:"boolean",title:"Enable Connection Bonding",description:"Enable dual-link bonding with automatic failover.",default:!1},mode:{type:"string",title:"Bonding Mode",description:"Bonding operating mode.",default:"main-backup",oneOf:[{const:"main-backup",title:"Main/Backup – Failover to backup when primary degrades"}]},primary:{type:"object",title:"Primary Link",description:"Primary connection (e.g. LTE modem).",properties:{address:{type:"string",title:"Server Address",default:"127.0.0.1"},port:{type:"number",title:"UDP Port",default:4446,minimum:1024,maximum:65535},interface:{type:"string",title:"Bind Interface (optional)",description:"Network interface IP to bind to."}}},backup:{type:"object",title:"Backup Link",description:"Backup connection (e.g. Starlink, satellite).",properties:{address:{type:"string",title:"Server Address",default:"127.0.0.1"},port:{type:"number",title:"UDP Port",default:4447,minimum:1024,maximum:65535},interface:{type:"string",title:"Bind Interface (optional)",description:"Network interface IP to bind to."}}},failover:{type:"object",title:"Failover Thresholds",description:"Configure when failover is triggered.",properties:{rttThreshold:{type:"number",title:"RTT Threshold (ms)",default:500,minimum:100,maximum:5e3},lossThreshold:{type:"number",title:"Packet Loss Threshold (0-1)",default:.1,minimum:.01,maximum:.5},healthCheckInterval:{type:"number",title:"Health Check Interval (ms)",default:1e3,minimum:500,maximum:1e4},failbackDelay:{type:"number",title:"Failback Delay (ms)",default:3e4,minimum:5e3,maximum:3e5},heartbeatTimeout:{type:"number",title:"Heartbeat Timeout (ms)",default:5e3,minimum:1e3,maximum:3e4}}}}},h={type:"boolean",title:"Enable Signal K Notifications",description:"Emit Signal K notifications for alerts and failover events.",default:!1},k={type:"boolean",title:"Skip Plugin's Own Data",description:"Do not forward data this plugin publishes locally over the link. Strips entries under 'networking.edgeLink.*' and the v1 RTT path 'networking.modem.rtt' / 'networking.modem.<id>.rtt'; other 'networking.modem.*' paths from external providers are left intact. Also suppresses the v2/v3 client telemetry packet that mirrors local link metrics to the receiver.",default:!1},v={type:"object",title:"Monitoring Alert Thresholds (v2/v3 only)",description:"Customize warning/critical thresholds for network monitoring alerts.",properties:{rtt:{type:"object",title:"RTT Thresholds",properties:{warning:{type:"number",title:"Warning RTT (ms)",default:300},critical:{type:"number",title:"Critical RTT (ms)",default:800}}},packetLoss:{type:"object",title:"Packet Loss Thresholds",properties:{warning:{type:"number",title:"Warning Loss Ratio",default:.03},critical:{type:"number",title:"Critical Loss Ratio",default:.1}}},retransmitRate:{type:"object",title:"Retransmit Rate Thresholds",properties:{warning:{type:"number",title:"Warning Retransmit Ratio",default:.05},critical:{type:"number",title:"Critical Retransmit Ratio",default:.15}}},jitter:{type:"object",title:"Jitter Thresholds",properties:{warning:{type:"number",title:"Warning Jitter (ms)",default:100},critical:{type:"number",title:"Critical Jitter (ms)",default:300}}},queueDepth:{type:"object",title:"Queue Depth Thresholds",properties:{warning:{type:"number",title:"Warning Queue Depth",default:100},critical:{type:"number",title:"Critical Queue Depth",default:500}}}}};function x(e,t){const n=Number(t)>=2,r={...d},i=["serverType","udpPort","secretKey"];return e?(Object.assign(r,u),r.enableNotifications=h,r.skipOwnData=k,i.push("udpAddress"),n?(r.reliability=p,r.congestionControl=y,r.bonding=b,r.alertThresholds=v):(Object.assign(r,m),i.push("testAddress","testPort"))):n&&(r.requestFullStatusOnRestart=g,r.reliability=f),{type:"object",required:i,properties:r}}const T="/plugins/signalk-edge-link";let w=0;function A(){return`skel-${Date.now()}-${++w}`}function S(e){const t=A();return{_id:t,connectionId:t,name:e||"client",serverType:"client",udpPort:4446,secretKey:"",stretchAsciiKey:!1,useMsgpack:!1,usePathDictionary:!1,enableNotifications:!1,skipOwnData:!1,protocolVersion:1,udpAddress:"127.0.0.1",helloMessageSender:60,testAddress:"127.0.0.1",testPort:80,pingIntervalTime:1}}function E(e){const t=A();return{_id:t,connectionId:t,name:e||"server",serverType:"server",udpPort:4446,secretKey:"",stretchAsciiKey:!1,useMsgpack:!1,usePathDictionary:!1,protocolVersion:1}}function C(e){const t="string"==typeof e.connectionId&&e.connectionId.trim()?e.connectionId.trim():e._id||A();return{...e,_id:e._id||t,connectionId:t}}function I(e){const t=x("server"!==e.serverType,e.protocolVersion),{_id:n,...r}=e;return{...(0,a.NV)(o.Ay,t,r),_id:n}}function P(e){if(null===e||"object"!=typeof e)return JSON.stringify(e);if(Array.isArray(e))return"["+e.map(P).join(",")+"]";const t=e;return"{"+Object.keys(t).sort().map(e=>JSON.stringify(e)+":"+P(t[e])).join(",")+"}"}const N={"ui:order":["name","serverType","udpAddress","udpPort","secretKey","stretchAsciiKey","protocolVersion","useMsgpack","usePathDictionary","testAddress","testPort","pingIntervalTime","helloMessageSender","heartbeatInterval","reliability","congestionControl","bonding","skipOwnData","enableNotifications","alertThresholds"],secretKey:{"ui:widget":"password","ui:help":"Use 32-character ASCII, 64-character hex, or 44-character base64"},stretchAsciiKey:{"ui:help":"Only applies to 32-char ASCII keys. Must match on both peers."},serverType:{"ui:widget":"select"},reliability:{"ui:classNames":"skel-optional-group"},congestionControl:{"ui:classNames":"skel-optional-group"},bonding:{"ui:classNames":"skel-optional-group"},alertThresholds:{"ui:classNames":"skel-optional-group"}},D={"ui:order":["name","serverType","udpPort","secretKey","stretchAsciiKey","useMsgpack","usePathDictionary","protocolVersion","requestFullStatusOnRestart","reliability"],secretKey:{"ui:widget":"password","ui:help":"Use 32-character ASCII, 64-character hex, or 44-character base64"},stretchAsciiKey:{"ui:help":"Only applies to 32-char ASCII keys. Must match on both peers."},serverType:{"ui:widget":"select"}},K=["name","udpPort","secretKey","stretchAsciiKey","useMsgpack","usePathDictionary","protocolVersion"];function R({conn:e,index:t,totalCount:n,expanded:a,onToggle:s,onChange:l,onRemove:c}){const d="server"!==e.serverType,m=x(d,e.protocolVersion),u=d?N:D,p=d?"Client":"Server",g=(e.name||`Connection ${t+1}`).trim(),{_id:f,...y}=e;return r.createElement("div",{className:"skel-card"},r.createElement("div",{className:"skel-card-header",onClick:s,role:"button","aria-expanded":a},r.createElement("span",{className:"skel-badge "+(d?"skel-badge-client":"skel-badge-server")},p),r.createElement("span",{className:"skel-card-title"},g),r.createElement("span",{className:"skel-expand-icon"},a?"▲":"▼"),r.createElement("button",{className:"skel-btn-remove",disabled:n<=1,onClick:e=>{e.stopPropagation(),c()},title:n<=1?"Cannot remove the only connection":"Remove this connection"},"Remove")),a&&r.createElement("div",{className:"skel-card-body"},r.createElement(i.Ay,{schema:m,uiSchema:u,formData:y,validator:o.Ay,onChange:function(t){const n=t.formData;if(!n)return;if(n.serverType&&n.serverType!==e.serverType){const t={..."server"===n.serverType?E(n.name):S(n.name),_id:e._id,connectionId:e.connectionId||e._id};for(const e of K)void 0!==n[e]&&(t[e]=n[e]);return t.serverType=n.serverType,void l(t)}const r={...n,_id:e._id,connectionId:n.connectionId||e.connectionId||e._id};"server"!==r.serverType&&(r.protocolVersion??1)>=2&&(delete r.testAddress,delete r.testPort,delete r.pingIntervalTime);const{_id:i,...o}=r,{_id:a,...s}=e;(function(e,t){const n=Object.keys(e),r=Object.keys(t);if(n.length!==r.length)return!1;for(const r of n){if(!Object.prototype.hasOwnProperty.call(t,r))return!1;const n=e[r],i=t[r];if(n!==i){if(null===n||null===i||"object"!=typeof n||"object"!=typeof i)return!1;if(P(n)!==P(i))return!1}}return!0})(o,s)||l(r)},onSubmit:()=>{},liveValidate:!1},r.createElement("div",null))))}const M=function(e){const[t,n]=(0,r.useState)([]),[i,o]=(0,r.useState)(""),[a,l]=(0,r.useState)(!1),[d,m]=(0,r.useState)(!0),[u,p]=(0,r.useState)(null),[g,f]=(0,r.useState)(null),[y,b]=(0,r.useState)(null),[h,k]=(0,r.useState)(0),[v,x]=(0,r.useState)(!1),w=(0,r.useRef)(!1);(0,r.useEffect)(()=>{!async function(){try{const e=await c(`${T}/plugin-config`);if(401===e.status)throw new Error(s);if(!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);const t=await e.json();if(!t.success)throw new Error(t.error||"Failed to load configuration");const r=t.configuration||{};let i;i=Array.isArray(r.connections)&&r.connections.length>0?r.connections.map(e=>I(C(e))):r.serverType?[I(C(r))]:[S()],n(i),o("string"==typeof r.managementApiToken?r.managementApiToken:""),l(!0===r.requireManagementApiToken),k(0),x(!1)}catch(e){p(e instanceof Error?e.message:String(e))}finally{m(!1)}}()},[]);const A=t.filter(e=>"server"===e.serverType).map(e=>e.udpPort),P=new Set(A.filter((e,t)=>A.indexOf(e)!==t));function N(){x(!0),f(null),b(null)}const D=(0,r.useCallback)(async()=>{if(!w.current){if(0===t.length)return b("At least one connection is required before saving."),void f({type:"error",message:"Cannot save an empty configuration. Add at least one connection."});if(b(null),P.size>0)f({type:"error",message:`Duplicate server ports detected: ${[...P].join(", ")}. Each server must use a unique UDP port.`});else{w.current=!0,f({type:"saving",message:"Saving configuration..."});try{const e=t.map(({_id:e,...t})=>({...t,connectionId:"string"==typeof t.connectionId&&t.connectionId.trim()?t.connectionId.trim():e})),n=await c(`${T}/plugin-config`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({connections:e,managementApiToken:i,requireManagementApiToken:a})});if(401===n.status)throw new Error(s);const r=await n.json();if(!n.ok||!r.success)throw new Error(r.error||"Failed to save");f({type:"success",message:r.message||"Configuration saved. Plugin restarting..."}),x(!1)}catch(e){f({type:"error",message:e instanceof Error?e.message:String(e)})}finally{w.current=!1}}}},[t,P,i,a]);return d?r.createElement("div",{style:{padding:"20px",textAlign:"center"}},"Loading configuration..."):u?r.createElement("div",{style:{padding:"20px"}},r.createElement("div",{className:"skel-alert skel-alert-error"},r.createElement("strong",null,"Error loading configuration:")," ",u)):r.createElement("div",{className:"skel-config"},r.createElement("style",null,'\n.skel-config { font-family: inherit; }\n.skel-dirty-banner {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 14px;\n background: #fff3cd;\n color: #664d03;\n border: 1px solid #ffe69c;\n border-radius: 4px;\n margin-bottom: 12px;\n font-size: 0.88rem;\n}\n.skel-card {\n border: 1px solid #dee2e6;\n border-radius: 6px;\n margin-bottom: 12px;\n overflow: hidden;\n}\n.skel-card-header {\n display: flex;\n align-items: center;\n padding: 10px 14px;\n background: #f8f9fa;\n cursor: pointer;\n user-select: none;\n gap: 10px;\n}\n.skel-card-header:hover { background: #e9ecef; }\n.skel-badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 12px;\n font-size: 0.75rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n}\n.skel-badge-server { background: #cfe2ff; color: #084298; }\n.skel-badge-client { background: #d1e7dd; color: #0a3622; }\n.skel-card-title { font-weight: 600; flex: 1; }\n.skel-expand-icon { font-size: 0.8rem; color: #6c757d; }\n.skel-btn-remove {\n background: none;\n border: 1px solid #dc3545;\n color: #dc3545;\n border-radius: 4px;\n padding: 2px 8px;\n font-size: 0.8rem;\n cursor: pointer;\n}\n.skel-btn-remove:hover { background: #dc3545; color: white; }\n.skel-btn-remove:disabled { opacity: 0.4; cursor: default; border-color: #aaa; color: #aaa; }\n.skel-btn-remove:disabled:hover { background: none; }\n.skel-card-body { padding: 16px; border-top: 1px solid #dee2e6; }\n.skel-toolbar {\n display: flex;\n gap: 10px;\n align-items: center;\n margin-top: 16px;\n padding-top: 16px;\n border-top: 1px solid #dee2e6;\n flex-wrap: wrap;\n}\n.skel-btn {\n padding: 7px 16px;\n border-radius: 4px;\n font-size: 0.95rem;\n cursor: pointer;\n border: none;\n}\n.skel-btn-primary { background: #0d6efd; color: white; }\n.skel-btn-primary:hover { background: #0b5ed7; }\n.skel-btn-primary:disabled { background: #6c757d; cursor: default; }\n.skel-btn-secondary { background: white; color: #0d6efd; border: 1px solid #0d6efd; }\n.skel-btn-secondary:hover { background: #e7f0ff; }\n.skel-alert {\n padding: 10px 14px;\n border-radius: 4px;\n margin-bottom: 14px;\n font-size: 0.9rem;\n}\n.skel-alert-success { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; }\n.skel-alert-error { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; }\n.skel-alert-saving { background: #fff3cd; color: #664d03; border: 1px solid #ffe69c; }\n.skel-dup-warn { font-size: 0.8rem; color: #dc3545; margin-top: 4px; }\n.skel-plugin-settings {\n border: 1px solid #dee2e6;\n border-radius: 6px;\n margin-bottom: 20px;\n padding: 16px;\n background: #f8f9fa;\n}\n.skel-plugin-settings h3 {\n margin: 0 0 12px;\n font-size: 1rem;\n font-weight: 600;\n}\n.skel-field-group {\n margin-bottom: 14px;\n}\n.skel-field-group label {\n display: block;\n font-weight: 500;\n margin-bottom: 4px;\n font-size: 0.9rem;\n}\n.skel-field-group input[type="text"],\n.skel-field-group input[type="password"] {\n width: 100%;\n max-width: 420px;\n padding: 6px 10px;\n border: 1px solid #ced4da;\n border-radius: 4px;\n font-size: 0.9rem;\n}\n.skel-field-group input[type="checkbox"] {\n margin-right: 6px;\n}\n.skel-field-desc {\n font-size: 0.8rem;\n color: #5c6773;\n margin-top: 3px;\n}\n.skel-config .field-description {\n color: #5c6773;\n font-size: 0.83rem;\n line-height: 1.35;\n}\n.skel-config legend,\n.skel-config label {\n line-height: 1.2;\n overflow-wrap: anywhere;\n}\n.skel-optional-group {\n margin-top: 12px;\n border: 1px dashed #ccd5df;\n border-radius: 6px;\n padding: 10px 12px 4px;\n background: #fbfcfe;\n}\n.skel-optional-group legend {\n font-size: 0.92rem;\n margin-bottom: 6px;\n}\n.skel-optional-group .form-group {\n margin-bottom: 10px;\n}\n.skel-optional-group .form-control {\n max-width: 340px;\n}\n'),v&&"saving"!==g?.type&&r.createElement("div",{className:"skel-dirty-banner"},r.createElement("span",null,"⚠"),r.createElement("span",null,"You have unsaved changes.")),g&&r.createElement("div",{className:"skel-alert skel-alert-"+("saving"===g.type?"saving":"success"===g.type?"success":"error")},g.message),r.createElement("div",{className:"skel-plugin-settings"},r.createElement("h3",null,"Plugin Security Settings"),r.createElement("div",{className:"skel-field-group"},r.createElement("label",{htmlFor:"skel-mgmt-token"},"Management API Token"),r.createElement("input",{id:"skel-mgmt-token",type:"password",value:i,placeholder:"Leave empty for open access",onChange:e=>{o(e.target.value),N()},autoComplete:"new-password"}),r.createElement("div",{className:"skel-field-desc"},"Shared secret to protect the management API endpoints. Strongly recommended for production. Can also be set via the ",r.createElement("code",null,"SIGNALK_EDGE_LINK_MANAGEMENT_TOKEN")," ","environment variable (env var takes priority). Leave empty to allow open access.")),r.createElement("div",{className:"skel-field-group"},r.createElement("label",null,r.createElement("input",{type:"checkbox",checked:a,onChange:e=>{l(e.target.checked),N()}}),"Require Management API Token"),r.createElement("div",{className:"skel-field-desc"},"When enabled, all management API requests are rejected if no token is configured (fail-closed). When disabled, requests are allowed if no token is set (open access)."))),t.map((e,i)=>r.createElement("div",{key:e._id},r.createElement(R,{conn:e,index:i,totalCount:t.length,expanded:h===i,onToggle:()=>function(e){k(t=>t===e?null:e)}(i),onChange:e=>function(e,t){n(n=>n.map((n,r)=>r===e?t:n)),N()}(i,e),onRemove:()=>function(e){n(t=>{if(t.length<=1)return t;const n=t.filter((t,n)=>n!==e);return k(t=>null!==t&&t>=e&&t>0?t-1:t),n}),N()}(i)}),"server"===e.serverType&&P.has(e.udpPort)&&r.createElement("div",{className:"skel-dup-warn"},"Port ",e.udpPort," is used by multiple server connections. Each server requires a unique port."))),r.createElement("div",{className:"skel-toolbar"},r.createElement("button",{className:"skel-btn skel-btn-secondary",onClick:function(){n(e=>{const t=[...e,E(`server-${e.length+1}`)];return k(t.length-1),t}),N()}},"+ Add Server"),r.createElement("button",{className:"skel-btn skel-btn-secondary",onClick:function(){n(e=>{const t=[...e,S(`client-${e.length+1}`)];return k(t.length-1),t}),N()}},"+ Add Client"),r.createElement("button",{className:"skel-btn skel-btn-primary",onClick:D,disabled:g&&"saving"===g.type||0===t.length},v?"Save Changes":"Save Configuration"),y&&r.createElement("span",{style:{color:"#dc3545",fontSize:"0.85rem",fontWeight:500}},y),r.createElement("span",{style:{fontSize:"0.85rem",color:"#6c757d"}},t.length," connection",1!==t.length?"s":""," · ",t.filter(e=>"server"===e.serverType).length," server",1!==t.filter(e=>"server"===e.serverType).length?"s":"",", ",t.filter(e=>"server"!==e.serverType).length," client",1!==t.filter(e=>"server"!==e.serverType).length?"s":"")))}}}]);
|
|
2
|
+
//# sourceMappingURL=982.fb1b6560eada159d88ee.js.map
|