@vitormnm/node-red-simple-opcua 1.4.3 → 1.5.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.
Files changed (32) hide show
  1. package/client/icons/opcua.svg +132 -132
  2. package/client/lib/opcua-client-browser.js +368 -330
  3. package/client/lib/opcua-client-method-service.js +88 -88
  4. package/client/lib/opcua-client-read-service.js +15 -15
  5. package/client/lib/opcua-client-subscription-id-service.js +24 -24
  6. package/client/lib/opcua-client-subscription-service.js +170 -170
  7. package/client/lib/opcua-client-write-service.js +146 -146
  8. package/client/opcua-client-config.html +80 -80
  9. package/client/opcua-client-utils.js +12 -11
  10. package/client/opcua-client.html +140 -140
  11. package/client/view/opcua-client.js +1144 -1140
  12. package/icons/opcua.svg +132 -132
  13. package/icons/opcua2.svg +132 -132
  14. package/package.json +3 -3
  15. package/server/icons/opcua.svg +132 -132
  16. package/server/lib/opcua-address-space-alarm.js +341 -341
  17. package/server/lib/opcua-address-space-builder.js +1603 -1485
  18. package/server/lib/opcua-config.js +677 -546
  19. package/server/lib/opcua-constants.js +119 -109
  20. package/server/lib/opcua-server-events-child.js +139 -139
  21. package/server/lib/opcua-server-runtime-child.js +873 -819
  22. package/server/lib/opcua-server-runtime.js +376 -311
  23. package/server/lib/opcua-server-status-child.js +187 -187
  24. package/server/lib/server-node-utils.js +16 -16
  25. package/server/opcua-server-io.html +346 -346
  26. package/server/opcua-server-io.js +497 -496
  27. package/server/opcua-server-registry.js +270 -270
  28. package/server/opcua-server.css +265 -265
  29. package/server/opcua-server.html +155 -1643
  30. package/server/opcua-server.js +24 -13
  31. package/server/view/opcua-server.css +492 -0
  32. package/server/view/opcua-server.js +1435 -0
@@ -0,0 +1,1435 @@
1
+
2
+ (function () {
3
+ var editorState = { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
4
+ var expansionState = {};
5
+ var selectedPath = "";
6
+ var pendingCreate = null;
7
+ var pendingPasswordHashes = 0;
8
+ var authGroups = [];
9
+ var authUsers = [];
10
+ var treeSearchValue = "";
11
+ var treeSearchTerm = "";
12
+ var isSyncing = false;
13
+ var DEFAULT_NAMESPACE_ID = 2;
14
+
15
+ function syncModalBodyClass() {
16
+ $("body").toggleClass("opcua-tree-modal-open", $("#node-input-tree-modal").is(":visible") || $("#node-input-auth-modal").is(":visible"));
17
+ }
18
+ function openTreeModal() { $("#node-input-tree-modal").show(); syncModalBodyClass(); }
19
+ function closeTreeModal() { $("#node-input-tree-modal").hide(); syncModalBodyClass(); }
20
+ function openAuthModal() { $("#node-input-auth-modal").show(); syncModalBodyClass(); renderAuthEditor(); }
21
+ function closeAuthModal() { $("#node-input-auth-modal").hide(); syncModalBodyClass(); }
22
+
23
+ function parseTree(rawValue, strict) {
24
+ if (!rawValue) return { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
25
+ if (typeof rawValue === "object") return rawValue;
26
+ try { var parsed = JSON.parse(rawValue); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed; }
27
+ catch (error) { if (strict) throw error; }
28
+ return { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
29
+ }
30
+
31
+ function parseCredentialArray(rawValue) {
32
+ if (!rawValue) return [];
33
+ if (typeof rawValue === "object") return Array.isArray(rawValue) ? rawValue : [];
34
+ try {
35
+ var parsed = JSON.parse(rawValue);
36
+ return Array.isArray(parsed) ? parsed : [];
37
+ } catch (error) {
38
+ return [];
39
+ }
40
+ }
41
+
42
+ function normalizeAuthGroup(group) {
43
+ if (typeof group === "string") {
44
+ return group.trim();
45
+ }
46
+ if (group && typeof group.name === "string") {
47
+ return group.name.trim();
48
+ }
49
+ return "";
50
+ }
51
+
52
+ function normalizeAuthGroups(groups) {
53
+ var seen = {};
54
+ return parseCredentialArray(groups).map(normalizeAuthGroup).filter(function (groupName) {
55
+ if (!groupName || seen[groupName]) return false;
56
+ seen[groupName] = true;
57
+ return true;
58
+ });
59
+ }
60
+
61
+ function normalizeAuthUser(user) {
62
+ user = user || {};
63
+ return {
64
+ username: user.username ? String(user.username).trim() : "",
65
+ password: user.password ? String(user.password) : "",
66
+ passwordHash: user.passwordHash ? String(user.passwordHash) : "",
67
+ group: user.group ? String(user.group).trim() : (user.role ? String(user.role).trim() : "")
68
+ };
69
+ }
70
+
71
+ function normalizeAuthUsers(users) {
72
+ return parseCredentialArray(users).map(normalizeAuthUser);
73
+ }
74
+
75
+ function reconcileAuthGroupsFromUsers() {
76
+ authUsers.forEach(function (user) {
77
+ var groupName = String((user && user.group) || "").trim();
78
+ if (groupName && authGroups.indexOf(groupName) === -1) {
79
+ authGroups.push(groupName);
80
+ }
81
+ });
82
+ }
83
+
84
+
85
+
86
+ function syncAuthCredentialFields() {
87
+ reconcileAuthGroupsFromUsers();
88
+ $("#node-input-groups").val(JSON.stringify(authGroups));
89
+ $("#node-input-users").val(JSON.stringify(authUsers.map(function (user) {
90
+ return {
91
+ username: user.username,
92
+ password: user.password,
93
+ passwordHash: user.passwordHash,
94
+ group: user.group
95
+ };
96
+ })));
97
+ }
98
+
99
+
100
+
101
+ function normalizeNamespaceId(value) {
102
+ var parsed = Number(value);
103
+ return Number.isInteger(parsed) && parsed >= DEFAULT_NAMESPACE_ID ? parsed : DEFAULT_NAMESPACE_ID;
104
+ }
105
+
106
+ function normalizeNamespaceDefinition(namespaceItem) {
107
+ namespaceItem = namespaceItem || {};
108
+ return {
109
+ id: normalizeNamespaceId(namespaceItem.id),
110
+ name: namespaceItem.name ? String(namespaceItem.name) : ""
111
+ };
112
+ }
113
+
114
+ function normalizeAccessPermissionValues(values) {
115
+ if (values === undefined || values === null || values === "") {
116
+ values = ["public"];
117
+ }
118
+ if (typeof values === "string") {
119
+ values = values.indexOf(",") >= 0 ? values.split(",") : [values];
120
+ }
121
+ if (!Array.isArray(values)) {
122
+ values = ["public"];
123
+ }
124
+ var seen = {};
125
+ var normalized = values.map(function (value) {
126
+ return String(value || "").trim().toLowerCase();
127
+ }).filter(function (value) {
128
+ if (!value || seen[value]) return false;
129
+ seen[value] = true;
130
+ return true;
131
+ });
132
+ return normalized.length ? normalized : ["public"];
133
+ }
134
+
135
+ function ensureNamespaces(tree) {
136
+ if (!Array.isArray(tree.nameSpaces)) tree.nameSpaces = [];
137
+ var hasDefaultNamespace = tree.nameSpaces.some(function (item) { return normalizeNamespaceId(item.id) === DEFAULT_NAMESPACE_ID; });
138
+ if (!hasDefaultNamespace) {
139
+ tree.nameSpaces.unshift(normalizeNamespaceDefinition({
140
+ id: DEFAULT_NAMESPACE_ID,
141
+ name: $("#node-input-namespaceUri").val() || "urn:node-red:opc-ua-server"
142
+ }));
143
+ }
144
+
145
+ tree.nameSpaces = tree.nameSpaces
146
+ .map(normalizeNamespaceDefinition)
147
+ .sort(function (left, right) { return left.id - right.id; });
148
+
149
+ return tree;
150
+ }
151
+
152
+ function normalizeVariable(variable) {
153
+ return {
154
+ name: variable && variable.name ? String(variable.name) : "",
155
+ type: variable && variable.type ? String(variable.type) : "Int32",
156
+ value: variable && variable.value !== undefined ? variable.value : "",
157
+ access: variable && variable.access ? String(variable.access).toLowerCase() : "readwrite",
158
+ description: variable && variable.description ? String(variable.description) : "",
159
+ displayName: variable && variable.displayName ? String(variable.displayName) : "",
160
+ nodeId: variable && variable.nodeId ? String(variable.nodeId) : "",
161
+ namespaceId: normalizeNamespaceId(variable && variable.namespaceId),
162
+ accessPermission: normalizeAccessPermissionValues(variable && (variable.accessPermission || variable.accessPermissions))
163
+ };
164
+ }
165
+ function normalizeAlarm(alarm) {
166
+ alarm = alarm || {};
167
+ var type = alarm.type ? String(alarm.type) : "levelAlarm";
168
+ return {
169
+ name: alarm.name ? String(alarm.name) : "",
170
+ variableNodeId: alarm.variableNodeId ? String(alarm.variableNodeId) : "",
171
+ type: type,
172
+ enabled: alarm.enabled !== undefined ? !!alarm.enabled : true,
173
+ severity: alarm.severity !== undefined ? alarm.severity : 500,
174
+ description: alarm.description ? String(alarm.description) : "",
175
+ displayName: alarm.displayName ? String(alarm.displayName) : "",
176
+ nodeId: alarm.nodeId ? String(alarm.nodeId) : "",
177
+ namespaceId: normalizeNamespaceId(alarm.namespaceId),
178
+ accessPermission: normalizeAccessPermissionValues(alarm.accessPermission || alarm.accessPermissions),
179
+ highHighLimit: alarm.highHighLimit !== undefined ? alarm.highHighLimit : 100,
180
+ highHighMessage: alarm.highHighMessage ? String(alarm.highHighMessage) : "High High alarm",
181
+ highLimit: alarm.highLimit !== undefined ? alarm.highLimit : 80,
182
+ highMessage: alarm.highMessage ? String(alarm.highMessage) : "High alarm",
183
+ lowLimit: alarm.lowLimit !== undefined ? alarm.lowLimit : 20,
184
+ lowMessage: alarm.lowMessage ? String(alarm.lowMessage) : "Low alarm",
185
+ lowLowLimit: alarm.lowLowLimit !== undefined ? alarm.lowLowLimit : 0,
186
+ lowLowMessage: alarm.lowLowMessage ? String(alarm.lowLowMessage) : "Low Low alarm",
187
+ normalStateValue: alarm.normalStateValue !== undefined ? alarm.normalStateValue : 0,
188
+ digitalMessage: alarm.digitalMessage ? String(alarm.digitalMessage) : "Digital alarm"
189
+ };
190
+ }
191
+
192
+ function normalizeMethodArg(arg) {
193
+ arg = arg || {};
194
+ return {
195
+ name: arg.name ? String(arg.name) : "",
196
+ type: arg.type ? String(arg.type) : "Float",
197
+ displayName: arg.displayName ? String(arg.displayName) : "",
198
+ description: arg.description ? String(arg.description) : ""
199
+ };
200
+ }
201
+
202
+ function normalizeMethod(method) {
203
+ method = method || {};
204
+ return {
205
+ name: method.name ? String(method.name) : "",
206
+ description: method.description ? String(method.description) : "",
207
+ displayName: method.displayName ? String(method.displayName) : "",
208
+ nodeId: method.nodeId ? String(method.nodeId) : "",
209
+ namespaceId: normalizeNamespaceId(method.namespaceId),
210
+ accessPermission: normalizeAccessPermissionValues(method.accessPermission || method.accessPermissions),
211
+ inputs: Array.isArray(method.inputs)
212
+ ? method.inputs.map(normalizeMethodArg)
213
+ : Array.isArray(method.inputArguments)
214
+ ? method.inputArguments.map(normalizeMethodArg)
215
+ : [],
216
+ outputs: Array.isArray(method.outputs)
217
+ ? method.outputs.map(normalizeMethodArg)
218
+ : Array.isArray(method.outputArguments)
219
+ ? method.outputArguments.map(normalizeMethodArg)
220
+ : []
221
+ };
222
+ }
223
+
224
+ function normalizeBranch(branch) {
225
+ branch = branch || {};
226
+ return {
227
+ name: branch.name ? String(branch.name) : "",
228
+ displayName: branch.displayName ? String(branch.displayName) : "",
229
+ description: branch.description ? String(branch.description) : "",
230
+ nodeId: branch.nodeId ? String(branch.nodeId) : "",
231
+ namespaceId: normalizeNamespaceId(branch.namespaceId),
232
+ accessPermission: normalizeAccessPermissionValues(branch.accessPermission || branch.accessPermissions),
233
+ objectsType: branch.objectsType ? String(branch.objectsType) : (branch.objectType ? String(branch.objectType) : ""),
234
+ folders: Array.isArray(branch.folders) ? branch.folders.map(normalizeBranch) : [],
235
+ objects: Array.isArray(branch.objects) ? branch.objects.map(normalizeBranch) : [],
236
+ variables: Array.isArray(branch.variables) ? branch.variables.map(normalizeVariable) : [],
237
+ alarms: Array.isArray(branch.alarms) ? branch.alarms.map(normalizeAlarm) : [],
238
+ methods: Array.isArray(branch.methods) ? branch.methods.map(normalizeMethod) : (Array.isArray(branch.method) ? branch.method.map(normalizeMethod) : []),
239
+ objectsTypes: Array.isArray(branch.objectsTypes) ? branch.objectsTypes.map(normalizeBranch) : []
240
+ };
241
+ }
242
+
243
+ function normalizeTree(tree) {
244
+ tree = ensureNamespaces(tree || {});
245
+ return ensureNamespaces({
246
+ objects: Array.isArray(tree.objects) ? tree.objects.map(normalizeBranch) : [],
247
+ folders: Array.isArray(tree.folders) ? tree.folders.map(normalizeBranch) : [],
248
+ objectsTypes: Array.isArray(tree.objectsTypes) ? tree.objectsTypes.map(normalizeBranch) : (Array.isArray(tree.objectTypes) ? tree.objectTypes.map(normalizeBranch) : []),
249
+ nameSpaces: Array.isArray(tree.nameSpaces) ? tree.nameSpaces.map(normalizeNamespaceDefinition) : (Array.isArray(tree.namespaces) ? tree.namespaces.map(normalizeNamespaceDefinition) : [])
250
+ });
251
+ }
252
+
253
+ function prettyTree(tree) { return JSON.stringify(normalizeTree(tree), null, 2); }
254
+ function cloneTree(tree) { return JSON.parse(prettyTree(tree)); }
255
+ function pathToTokens(path) { return String(path || "").split(".").filter(function (t) { return t !== ""; }); }
256
+
257
+ function getAtPath(tree, path) {
258
+ return pathToTokens(path).reduce(function (current, token) {
259
+ if (current === undefined || current === null) return undefined;
260
+ if (/^\d+$/.test(token)) return current[Number(token)];
261
+ return current[token];
262
+ }, tree);
263
+ }
264
+
265
+ function removeAtPath(tree, path) {
266
+ var tokens = pathToTokens(path);
267
+ var lastToken = tokens.pop();
268
+ var parent = getAtPath(tree, tokens.join("."));
269
+ if (parent === undefined || parent === null || lastToken === undefined) return;
270
+ if (/^\d+$/.test(lastToken) && Array.isArray(parent)) parent.splice(Number(lastToken), 1);
271
+ else delete parent[lastToken];
272
+ }
273
+
274
+ function updateTreeField(serializedTree, notifyChange) {
275
+ var field = $("#node-input-tree");
276
+ var previousValue = field.val();
277
+ field.val(serializedTree);
278
+ if (notifyChange && previousValue !== serializedTree) field.trigger("change");
279
+ }
280
+
281
+ function syncStateToJson(notifyChange) {
282
+ if (isSyncing) return;
283
+ isSyncing = true;
284
+ enforceFixedNodeIdsInObjectTypeModels(editorState);
285
+ var json = prettyTree(editorState);
286
+ updateTreeField(json, false);
287
+ $("#node-input-tree-editor").typedInput("value", json);
288
+ if (notifyChange) $("#node-input-tree").trigger("change");
289
+ isSyncing = false;
290
+ }
291
+
292
+ function normalizeSearchTerm(value) { return String(value || "").trim().toLowerCase(); }
293
+ function isExpanded(path, defaultValue) { if (expansionState[path] === undefined) expansionState[path] = !!defaultValue; return expansionState[path]; }
294
+ function nodeClassFromPath(path) {
295
+ var tokens = pathToTokens(path);
296
+ if (!tokens.length) return "Object";
297
+
298
+ var collectionToken = tokens.length > 1 ? tokens[tokens.length - 2] : tokens[0];
299
+ if (collectionToken === "variables") return "Variable";
300
+ if (collectionToken === "methods") return "Method";
301
+ if (collectionToken === "alarms") return "Alarm";
302
+ if (collectionToken === "objectsTypes" || collectionToken === "objectTypes") return "ObjectType";
303
+ if (collectionToken === "nameSpaces" || collectionToken === "namespaces") return "Namespace";
304
+ if (collectionToken === "folders") return "Folder";
305
+ return "Object";
306
+ }
307
+ function getNodeDisplayName(path) { var item = getAtPath(editorState, path); return item ? (item.name || "(unnamed)") : ""; }
308
+ function getNamespaceOptions() {
309
+ return Array.isArray(editorState.nameSpaces) ? editorState.nameSpaces.slice().sort(function (left, right) { return left.id - right.id; }) : [];
310
+ }
311
+
312
+ function getDefinedObjectTypeNames() {
313
+ var names = [];
314
+ (editorState.objectsTypes || []).forEach(function (ot) {
315
+ if (ot && ot.name) names.push(String(ot.name));
316
+ });
317
+ return names;
318
+ }
319
+
320
+
321
+ function buildObjectTypeSelect(id, currentValue) {
322
+ var names = getDefinedObjectTypeNames();
323
+ var cv = String(currentValue || "");
324
+ var opts = "";
325
+ var noneSelected = (cv === "") ? " selected" : "";
326
+ opts += "<option value=\"\"" + noneSelected + ">\u2014 none \u2014</option>";
327
+ names.forEach(function (n) {
328
+ var esc = n.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
329
+ var sel = (n === cv) ? " selected" : "";
330
+ opts += "<option value=\"" + esc + "\"" + sel + ">" + esc + "</option>";
331
+ });
332
+ return "<select id=\"" + id + "\">" + opts + "</select>";
333
+ }
334
+
335
+ function getAvailableAccessPermissionOptions() {
336
+ var values = ["public"].concat(authGroups || []);
337
+ return normalizeAccessPermissionValues(values);
338
+ }
339
+
340
+ function buildAccessPermissionSelect(id, currentValues) {
341
+ var values = normalizeAccessPermissionValues(currentValues);
342
+ var currentValue = values.length ? values[0] : "public";
343
+ var options = getAvailableAccessPermissionOptions();
344
+
345
+ if (options.indexOf(currentValue) === -1) {
346
+ options.push(currentValue);
347
+ }
348
+
349
+ var html = '<select id="' + id + '">';
350
+ options.forEach(function (value) {
351
+ var escaped = escapeHtml(value);
352
+ var selected = value === currentValue ? " selected" : "";
353
+ html += '<option value="' + escaped + '"' + selected + '>' + escaped + "</option>";
354
+ });
355
+ html += "</select>";
356
+ return html;
357
+ }
358
+
359
+ function getNamespaceLabel(namespaceId) {
360
+ var match = getNamespaceOptions().find(function (item) { return item.id === normalizeNamespaceId(namespaceId); });
361
+ return match ? String(match.id) + " - " + match.name : String(normalizeNamespaceId(namespaceId));
362
+ }
363
+ function getNodeNamespaceId(path) {
364
+ var item = getAtPath(editorState, path);
365
+ return normalizeNamespaceId(item && item.namespaceId);
366
+ }
367
+ function getNodeIdPrefix(namespaceId) {
368
+ return "ns=" + normalizeNamespaceId(namespaceId) + ";s=";
369
+ }
370
+
371
+ function buildDefaultNodeIdSuffixFromEditorPath(path) {
372
+ var tokens = pathToTokens(path);
373
+ var current = editorState;
374
+ var parts = [];
375
+ // var preservedCollections = { alarms: true, methods: true, objectsTypes: true };
376
+ var preservedCollections = { alarms: true, methods: true };
377
+
378
+ tokens.forEach(function (token) {
379
+ if (/^\d+$/.test(token)) {
380
+ current = current ? current[Number(token)] : null;
381
+ if (current && current.name) parts.push(current.name);
382
+ return;
383
+ }
384
+
385
+ current = current ? current[token] : null;
386
+ if (preservedCollections[token]) parts.push(token);
387
+ });
388
+
389
+ return parts.join(".");
390
+ }
391
+
392
+
393
+ function nodeIdSuffixFromValue(nodeId, defaultSuffix) {
394
+ var raw = String(nodeId || "").trim();
395
+ if (!raw) return defaultSuffix;
396
+ var match = /^ns=\d+;s=(.*)$/.exec(raw);
397
+ if (match) return match[1];
398
+ return raw;
399
+ }
400
+ function isObjectTypeModelPath(path) {
401
+ var tokens = pathToTokens(path);
402
+ return tokens.length > 0 && tokens[0] === "objectsTypes";
403
+ }
404
+ function buildGeneratedNodeIdForPath(path) {
405
+ return getNodeIdPrefix(getNodeNamespaceId(path)) + buildDefaultNodeIdSuffixFromEditorPath(path);
406
+ }
407
+ function assignFixedNodeIdsToBranch(path) {
408
+ var branch = getAtPath(editorState, path);
409
+ if (!branch || typeof branch !== "object") return;
410
+ branch.nodeId = buildGeneratedNodeIdForPath(path);
411
+ (branch.folders || []).forEach(function (_, index) { assignFixedNodeIdsToBranch(path + ".folders." + index); });
412
+ (branch.objects || []).forEach(function (_, index) { assignFixedNodeIdsToBranch(path + ".objects." + index); });
413
+ (branch.variables || []).forEach(function (item, index) {
414
+ if (!item) return;
415
+ item.nodeId = buildGeneratedNodeIdForPath(path + ".variables." + index);
416
+ });
417
+ (branch.methods || []).forEach(function (item, index) {
418
+ if (!item) return;
419
+ item.nodeId = buildGeneratedNodeIdForPath(path + ".methods." + index);
420
+ });
421
+ (branch.alarms || []).forEach(function (item, index) {
422
+ if (!item) return;
423
+ item.nodeId = buildGeneratedNodeIdForPath(path + ".alarms." + index);
424
+ });
425
+ (branch.objectsTypes || []).forEach(function (_, index) { assignFixedNodeIdsToBranch(path + ".objectsTypes." + index); });
426
+ }
427
+ function enforceFixedNodeIdsInObjectTypeModels(tree) {
428
+ (tree.objectsTypes || []).forEach(function (_, index) {
429
+ assignFixedNodeIdsToBranch("objectsTypes." + index);
430
+ });
431
+ return tree;
432
+ }
433
+ function buildDisplayNodeIdFromEditorPath(path) {
434
+ var item = getAtPath(editorState, path);
435
+ if (isObjectTypeModelPath(path)) return buildGeneratedNodeIdForPath(path);
436
+ var customNodeId = item && item.nodeId ? String(item.nodeId).trim() : "";
437
+ var suffix = nodeIdSuffixFromValue(customNodeId, buildDefaultNodeIdSuffixFromEditorPath(path));
438
+ return getNodeIdPrefix(getNodeNamespaceId(path)) + suffix;
439
+ }
440
+ function normalizeCustomNodeIdFromSuffix(path, suffix) {
441
+ if (isObjectTypeModelPath(path)) return "";
442
+ var nextSuffix = String(suffix || "").trim();
443
+ var defaultSuffix = buildDefaultNodeIdSuffixFromEditorPath(path);
444
+ if (!nextSuffix || nextSuffix === defaultSuffix) return "";
445
+ return getNodeIdPrefix(getNodeNamespaceId(path)) + nextSuffix;
446
+ }
447
+ function copyNodeIdValue(nodeId) {
448
+ if (!nodeId) {
449
+ RED.notify("NodeId not found for the selected item.", "warning");
450
+ return;
451
+ }
452
+
453
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
454
+ navigator.clipboard.writeText(nodeId).then(function () {
455
+ RED.notify("NodeId copied.", "success");
456
+ }).catch(function () {
457
+ RED.notify("Failed to copy NodeId.", "error");
458
+ });
459
+ return;
460
+ }
461
+
462
+ var input = $("<textarea readonly></textarea>").val(nodeId).css({
463
+ position: "fixed",
464
+ left: "-9999px",
465
+ top: "0"
466
+ });
467
+ $("body").append(input);
468
+ input[0].select();
469
+ try {
470
+ document.execCommand("copy");
471
+ RED.notify("NodeId copied.", "success");
472
+ } catch (error) {
473
+ RED.notify("Failed to copy NodeId.", "error");
474
+ }
475
+ input.remove();
476
+ }
477
+
478
+ function getChildrenByPath(path) {
479
+ var item = getAtPath(editorState, path);
480
+ if (!item) return [];
481
+ var children = [];
482
+ (item.folders || []).forEach(function (_, i) { children.push(path + ".folders." + i); });
483
+ (item.objects || []).forEach(function (_, i) { children.push(path + ".objects." + i); });
484
+ (item.variables || []).forEach(function (_, i) { children.push(path + ".variables." + i); });
485
+ (item.methods || []).forEach(function (_, i) { children.push(path + ".methods." + i); });
486
+ (item.alarms || []).forEach(function (_, i) { children.push(path + ".alarms." + i); });
487
+ (item.objectsTypes || []).forEach(function (_, i) { children.push(path + ".objectsTypes." + i); });
488
+ return children;
489
+ }
490
+
491
+ function getTopLevelPaths() {
492
+ var paths = [];
493
+ (editorState.folders || []).forEach(function (_, i) { paths.push("folders." + i); });
494
+ (editorState.objects || []).forEach(function (_, i) { paths.push("objects." + i); });
495
+ (editorState.objectsTypes || []).forEach(function (_, i) { paths.push("objectsTypes." + i); });
496
+ (editorState.nameSpaces || []).forEach(function (_, i) { paths.push("nameSpaces." + i); });
497
+ return paths;
498
+ }
499
+
500
+ function selectNode(path) {
501
+ selectedPath = path || "";
502
+ renderVisualEditor();
503
+ }
504
+
505
+ function nodeMatchesSearch(path) {
506
+ if (!treeSearchTerm) return true;
507
+ var item = getAtPath(editorState, path);
508
+ if (!item) return false;
509
+ var values = [path, item.name, item.displayName, item.description, nodeClassFromPath(path), item.type, item.value, item.id, item.namespaceId];
510
+ return values.some(function (x) { return String(x || "").toLowerCase().indexOf(treeSearchTerm) !== -1; });
511
+ }
512
+
513
+ function branchHasSearchMatch(path) {
514
+ if (nodeMatchesSearch(path)) return true;
515
+ return getChildrenByPath(path).some(branchHasSearchMatch);
516
+ }
517
+
518
+ function iconForNodeClass(nodeClass) {
519
+ if (nodeClass === "Folder") return "fa-folder";
520
+ if (nodeClass === "Object") return "fa-cube";
521
+ if (nodeClass === "Variable") return "fa-tag";
522
+ if (nodeClass === "ObjectType") return "fa-cubes";
523
+ if (nodeClass === "Namespace") return "fa-sitemap";
524
+ if (nodeClass === "Alarm") return "fa-bell";
525
+ if (nodeClass === "Method") return "fa-cog";
526
+ return "fa-tag";
527
+ }
528
+
529
+ // ── performance helpers ───────────────────────────────────────────
530
+ var _renderTreePending = false;
531
+
532
+ function debounce(fn, delay) {
533
+ var timer;
534
+ return function () { var ctx = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(ctx, args); }, delay); };
535
+ }
536
+
537
+ function escapeHtml(v) {
538
+ return String(v || "").replace(/[&<>"']/g, function (c) {
539
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
540
+ });
541
+ }
542
+
543
+ function appendNodeToFrag(frag, path, depth, ancestorMatched) {
544
+ var item = getAtPath(editorState, path);
545
+ if (!item) return;
546
+ var nodeClass = nodeClassFromPath(path);
547
+ var hasChildren = nodeClass !== "Variable" && nodeClass !== "Alarm" && nodeClass !== "Namespace" && getChildrenByPath(path).length > 0;
548
+ var expanded = isExpanded(path, depth < 1 || !!treeSearchTerm);
549
+ var subtreeVisible = !!ancestorMatched || nodeMatchesSearch(path);
550
+
551
+ var indents = "";
552
+ for (var i = 0; i < depth; i++) indents += '<span class="opcua-tree-indent"></span>';
553
+
554
+ var row = document.createElement("div");
555
+ row.className = "opcua-tree-row" + (path === selectedPath ? " is-selected" : "");
556
+ row.setAttribute("data-path", path);
557
+ row.innerHTML = indents
558
+ + '<span class="opcua-tree-twisty">' + (hasChildren ? '<i class="fa ' + (expanded ? "fa-caret-down" : "fa-caret-right") + '"></i>' : "") + "</span>"
559
+ + '<span class="opcua-tree-icon"><i class="fa ' + iconForNodeClass(nodeClass) + '"></i></span>'
560
+ + '<span class="opcua-tree-label">' + escapeHtml(item.name || "(unnamed)") + "</span>"
561
+ + '<span class="opcua-tree-type">' + escapeHtml(nodeClass) + "</span>";
562
+ frag.appendChild(row);
563
+
564
+ if (hasChildren && expanded) {
565
+ getChildrenByPath(path).forEach(function (childPath) {
566
+ if (!treeSearchTerm || subtreeVisible || branchHasSearchMatch(childPath)) {
567
+ appendNodeToFrag(frag, childPath, depth + 1, subtreeVisible);
568
+ }
569
+ });
570
+ }
571
+ }
572
+
573
+ function renderTree() {
574
+ if (_renderTreePending) return;
575
+ _renderTreePending = true;
576
+ setTimeout(function () {
577
+ _renderTreePending = false;
578
+ var list = document.getElementById("node-input-object-list");
579
+ if (!list) return;
580
+ var frag = document.createDocumentFragment();
581
+ var roots = getTopLevelPaths();
582
+ if (!roots.length) {
583
+ var empty = document.createElement("div");
584
+ empty.className = "opcua-tree-empty";
585
+ empty.textContent = "No OPC UA items. Use Add folder, Add object, or Add namespace.";
586
+ frag.appendChild(empty);
587
+ } else {
588
+ roots.forEach(function (path) {
589
+ if (!treeSearchTerm || branchHasSearchMatch(path)) appendNodeToFrag(frag, path, 0, false);
590
+ });
591
+ }
592
+ list.innerHTML = "";
593
+ list.appendChild(frag);
594
+ }, 0);
595
+ }
596
+
597
+ function renderBreadcrumbs() {
598
+ var el = $("#opcua-tree-breadcrumbs");
599
+ if (!selectedPath) { el.text("No selection"); return; }
600
+ var tokens = pathToTokens(selectedPath);
601
+ var cursor = [];
602
+ var parts = [];
603
+ tokens.forEach(function (token) {
604
+ cursor.push(token);
605
+ if (/^\d+$/.test(token)) parts.push(getNodeDisplayName(cursor.join(".")) || ("#" + token));
606
+ });
607
+ el.text(parts.join("."));
608
+ }
609
+
610
+ function updateNode(path, patch) {
611
+ var item = getAtPath(editorState, path);
612
+ if (!item) return;
613
+ Object.keys(patch || {}).forEach(function (k) { item[k] = patch[k]; });
614
+ syncStateToJson(false);
615
+ // Avoid rebuilding the details form on every keystroke to preserve input focus.
616
+ renderTree();
617
+ renderBreadcrumbs();
618
+ }
619
+
620
+ function openCreateForm(path, kind) {
621
+ if (!path) return;
622
+ if (nodeClassFromPath(path) === "Variable" || nodeClassFromPath(path) === "Namespace") {
623
+ RED.notify("Selected item cannot have children", "warning");
624
+ return;
625
+ }
626
+ pendingCreate = {
627
+ parentPath: path,
628
+ kind: kind,
629
+ name: kind === "variable" ? "newVariable" : kind === "folder" ? "newFolder" : kind === "objecttype" ? "newObjectType" : kind === "alarm" ? "newAlarm" : kind === "method" ? "newMethod" : "newObject",
630
+ displayName: "",
631
+ dataType: "Int32",
632
+ value: "",
633
+ access: "readwrite",
634
+ accessPermission: ["public"],
635
+ objectsType: "",
636
+ alarmType: "levelAlarm",
637
+ variableNodeId: "",
638
+ severity: 500,
639
+ highHighLimit: 100,
640
+ highHighMessage: "High High alarm",
641
+ highLimit: 80,
642
+ highMessage: "High alarm",
643
+ lowLimit: 20,
644
+ lowMessage: "Low alarm",
645
+ lowLowLimit: 0,
646
+ lowLowMessage: "Low Low alarm",
647
+ normalStateValue: 0,
648
+ digitalMessage: "Digital alarm"
649
+ };
650
+ renderDetails();
651
+ }
652
+
653
+ function saveCreateForm() {
654
+ if (!pendingCreate) return;
655
+ var parentPath = pendingCreate.parentPath;
656
+ var kind = pendingCreate.kind;
657
+ var branchTargetPath = kind === "variable"
658
+ ? (parentPath + ".variables")
659
+ : kind === "folder"
660
+ ? (parentPath + ".folders")
661
+ : kind === "objecttype"
662
+ ? (parentPath + ".objectsTypes")
663
+ : kind === "alarm"
664
+ ? (parentPath + ".alarms")
665
+ : kind === "method"
666
+ ? (parentPath + ".methods")
667
+ : (parentPath + ".objects");
668
+ var target = getAtPath(editorState, branchTargetPath);
669
+ if (!Array.isArray(target)) return;
670
+ if (kind === "variable") {
671
+ target.push(normalizeVariable({
672
+ name: pendingCreate.name,
673
+ displayName: pendingCreate.displayName || "",
674
+ type: pendingCreate.dataType,
675
+ value: pendingCreate.value,
676
+ access: pendingCreate.access || "readwrite",
677
+ accessPermission: pendingCreate.accessPermission
678
+ }));
679
+ } else if (kind === "folder") {
680
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
681
+ } else if (kind === "objecttype") {
682
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", objectsType: pendingCreate.objectsType || "", accessPermission: pendingCreate.accessPermission }));
683
+ target[target.length - 1].nodeId = buildGeneratedNodeIdForPath(branchTargetPath + "." + (target.length - 1));
684
+ } else if (kind === "alarm") {
685
+ target.push(normalizeAlarm({
686
+ displayName: pendingCreate.displayName || "",
687
+ name: pendingCreate.name,
688
+ accessPermission: pendingCreate.accessPermission,
689
+ type: pendingCreate.alarmType,
690
+ variableNodeId: pendingCreate.variableNodeId,
691
+ severity: Number(pendingCreate.severity || 500),
692
+ highHighLimit: pendingCreate.highHighLimit,
693
+ highHighMessage: pendingCreate.highHighMessage,
694
+ highLimit: pendingCreate.highLimit,
695
+ highMessage: pendingCreate.highMessage,
696
+ lowLimit: pendingCreate.lowLimit,
697
+ lowMessage: pendingCreate.lowMessage,
698
+ lowLowLimit: pendingCreate.lowLowLimit,
699
+ lowLowMessage: pendingCreate.lowLowMessage,
700
+ normalStateValue: pendingCreate.normalStateValue,
701
+ digitalMessage: pendingCreate.digitalMessage
702
+ }));
703
+ } else if (kind === "method") {
704
+ target.push(normalizeMethod({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
705
+ } else {
706
+ target.push(normalizeBranch({ name: pendingCreate.name, displayName: pendingCreate.displayName || "", accessPermission: pendingCreate.accessPermission }));
707
+ }
708
+ expansionState[parentPath] = true;
709
+ pendingCreate = null;
710
+ syncStateToJson(true);
711
+ renderVisualEditor();
712
+ }
713
+
714
+ function cancelCreateForm() {
715
+ pendingCreate = null;
716
+ renderDetails();
717
+ }
718
+
719
+ function renderDetails() {
720
+ var panel = $("#opcua-tree-details");
721
+ panel.empty();
722
+ if (pendingCreate) {
723
+ panel.append('<div class="form-row"><label>Parent</label><input type="text" id="opcua-create-parent" readonly></div>');
724
+ panel.append('<div class="form-row"><label>Type</label><input type="text" id="opcua-create-kind" readonly></div>');
725
+ panel.append('<div class="form-row"><label>Name</label><input type="text" id="opcua-create-name"></div>');
726
+ panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-create-displayname" placeholder="Leave blank to use browseName"></div>');
727
+ panel.append('<div class="form-row"><label>accessPermission</label>' + buildAccessPermissionSelect("opcua-create-accesspermission", pendingCreate.accessPermission || ["public"]) + '</div>');
728
+ if (pendingCreate.kind === "variable") {
729
+ panel.append('<div class="form-row"><label>dataType</label><select id="opcua-create-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
730
+ panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-create-value"></div>');
731
+ panel.append('<div class="form-row"><label>Access</label><select id="opcua-create-access"><option value="readwrite">readwrite</option><option value="readonly">readonly</option></select></div>');
732
+ }
733
+ if (pendingCreate.kind === "objecttype") {
734
+ panel.append('<div class="form-row"><label>objectsType</label>' + buildObjectTypeSelect("opcua-create-objectstype", pendingCreate.objectsType || "") + '</div>');
735
+ }
736
+ if (pendingCreate.kind === "alarm") {
737
+ panel.append('<div class="form-row"><label>alarmType</label><select id="opcua-create-alarm-type"><option value="levelAlarm">levelAlarm</option><option value="digitalAlarm">digitalAlarm</option></select></div>');
738
+ panel.append('<div class="form-row"><label>variablePath</label><input type="text" id="opcua-create-variable-nodeid"></div>');
739
+ panel.append('<div class="form-row"><label>severity</label><input type="number" id="opcua-create-severity"></div>');
740
+ if (pendingCreate.alarmType === "levelAlarm") {
741
+ panel.append('<div class="form-row"><label>highHighLimit</label><input type="number" id="opcua-create-highhighlimit"></div>');
742
+ panel.append('<div class="form-row"><label>highHighMessage</label><input type="text" id="opcua-create-highhighmessage"></div>');
743
+ panel.append('<div class="form-row"><label>highLimit</label><input type="number" id="opcua-create-highlimit"></div>');
744
+ panel.append('<div class="form-row"><label>highMessage</label><input type="text" id="opcua-create-highmessage"></div>');
745
+ panel.append('<div class="form-row"><label>lowLimit</label><input type="number" id="opcua-create-lowlimit"></div>');
746
+ panel.append('<div class="form-row"><label>lowMessage</label><input type="text" id="opcua-create-lowmessage"></div>');
747
+ panel.append('<div class="form-row"><label>lowLowLimit</label><input type="number" id="opcua-create-lowlowlimit"></div>');
748
+ panel.append('<div class="form-row"><label>lowLowMessage</label><input type="text" id="opcua-create-lowlowmessage"></div>');
749
+ } else {
750
+ panel.append('<div class="form-row"><label>normalStateValue</label><input type="number" id="opcua-create-normalstatevalue"></div>');
751
+ panel.append('<div class="form-row"><label>digitalMessage</label><input type="text" id="opcua-create-digitalmessage"></div>');
752
+ }
753
+ }
754
+ panel.append('<div class="form-row"><label style="width:90px;">Actions</label><div><a href="#" id="opcua-create-save" class="editor-button editor-button-small"><i class="fa fa-save"></i> Save</a> <a href="#" id="opcua-create-cancel" class="editor-button editor-button-small"><i class="fa fa-times"></i> Cancel</a></div></div>');
755
+ $("#opcua-create-parent").val(pendingCreate.parentPath);
756
+ $("#opcua-create-kind").val(pendingCreate.kind);
757
+ $("#opcua-create-name").val(pendingCreate.name);
758
+ $("#opcua-create-displayname").val(pendingCreate.displayName || "");
759
+ $("#opcua-create-accesspermission").val(normalizeAccessPermissionValues(pendingCreate.accessPermission));
760
+ $("#opcua-create-type").val(pendingCreate.dataType);
761
+ $("#opcua-create-value").val(pendingCreate.value);
762
+ $("#opcua-create-objectstype").val(pendingCreate.objectsType);
763
+ $("#opcua-create-access").val(pendingCreate.access || "readwrite");
764
+ $("#opcua-create-alarm-type").val(pendingCreate.alarmType);
765
+ $("#opcua-create-variable-nodeid").val(pendingCreate.variableNodeId);
766
+ $("#opcua-create-severity").val(pendingCreate.severity);
767
+ $("#opcua-create-highhighlimit").val(pendingCreate.highHighLimit);
768
+ $("#opcua-create-highhighmessage").val(pendingCreate.highHighMessage);
769
+ $("#opcua-create-highlimit").val(pendingCreate.highLimit);
770
+ $("#opcua-create-highmessage").val(pendingCreate.highMessage);
771
+ $("#opcua-create-lowlimit").val(pendingCreate.lowLimit);
772
+ $("#opcua-create-lowmessage").val(pendingCreate.lowMessage);
773
+ $("#opcua-create-lowlowlimit").val(pendingCreate.lowLowLimit);
774
+ $("#opcua-create-lowlowmessage").val(pendingCreate.lowLowMessage);
775
+ $("#opcua-create-normalstatevalue").val(pendingCreate.normalStateValue);
776
+ $("#opcua-create-digitalmessage").val(pendingCreate.digitalMessage);
777
+ return;
778
+ }
779
+ if (!selectedPath) { panel.append('<div class="opcua-tree-empty">Select a node to edit browseName, namespace, nodeId, and description.</div>'); return; }
780
+ var item = getAtPath(editorState, selectedPath);
781
+ if (!item) { panel.append('<div class="opcua-tree-empty">Selected node not found.</div>'); return; }
782
+ var nodeClass = nodeClassFromPath(selectedPath);
783
+ var nodeId = buildDisplayNodeIdFromEditorPath(selectedPath);
784
+ var nodeIdSuffix = nodeIdSuffixFromValue(item.nodeId, buildDefaultNodeIdSuffixFromEditorPath(selectedPath));
785
+ var nodeIdLocked = isObjectTypeModelPath(selectedPath);
786
+ var namespaceId = getNodeNamespaceId(selectedPath);
787
+ var namespaceOptions = getNamespaceOptions();
788
+ if (nodeClass === "Namespace") {
789
+ panel.append('<div class="form-row"><label>namespaceId</label><input type="number" id="opcua-detail-namespace-entry-id" min="2"></div>');
790
+ panel.append('<div class="form-row"><label>name</label><input type="text" id="opcua-detail-namespace-entry-name"></div>');
791
+ panel.append('<div class="form-row"><label style="width:90px;">Actions</label><div>' + (normalizeNamespaceId(item.id) === DEFAULT_NAMESPACE_ID ? '' : '<a href="#" id="opcua-detail-remove" class="editor-button editor-button-small"><i class="fa fa-trash"></i> Remove</a>') + '</div></div>');
792
+ $("#opcua-detail-namespace-entry-id").val(item.id !== undefined ? item.id : DEFAULT_NAMESPACE_ID);
793
+ $("#opcua-detail-namespace-entry-name").val(item.name || "");
794
+ if (normalizeNamespaceId(item.id) === DEFAULT_NAMESPACE_ID) $("#opcua-detail-namespace-entry-id").prop("disabled", true);
795
+ return;
796
+ }
797
+ panel.append('<div class="form-row"><label>browseName</label><input type="text" id="opcua-detail-name"></div>');
798
+ panel.append('<div class="form-row"><label>nodeClass</label><input type="text" id="opcua-detail-class" readonly></div>');
799
+ panel.append('<div class="form-row"><label>namespace</label><select id="opcua-detail-namespace"></select></div>');
800
+ panel.append('<div class="form-row"><label>nodeId</label><div class="opcua-nodeid-field"><span class="opcua-nodeid-prefix">' + getNodeIdPrefix(namespaceId) + '</span><input type="text" id="opcua-detail-nodeid"' + (nodeIdLocked ? ' readonly title="Generated automatically for object type models."' : '') + '><a href="#" id="opcua-detail-copy-nodeid" class="editor-button editor-button-small"><i class="fa fa-copy"></i> Copy</a></div></div>');
801
+ panel.append('<div class="form-row"><label>Description</label><input type="text" id="opcua-detail-description"></div>');
802
+ panel.append('<div class="form-row"><label>displayName</label><input type="text" id="opcua-detail-displayname" placeholder="Leave blank to use browseName"></div>');
803
+ panel.append('<div class="form-row"><label>accessPermission</label>' + buildAccessPermissionSelect("opcua-detail-accesspermission", item.accessPermission || ["public"]) + '</div>');
804
+ if (nodeClass === "ObjectType") {
805
+ panel.append('<div class="form-row"><label>objectsType</label>' + buildObjectTypeSelect("opcua-detail-objectstype", item.objectsType || "") + '</div>');
806
+ }
807
+ if (nodeClass === "Variable") {
808
+ panel.append('<div class="form-row"><label>dataType</label><select id="opcua-detail-type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option><option value="ByteString">ByteString</option></select></div>');
809
+ panel.append('<div class="form-row"><label>Value</label><input type="text" id="opcua-detail-value"></div>');
810
+ panel.append('<div class="form-row"><label>Access</label><select id="opcua-detail-access"><option value="readwrite">readwrite</option><option value="readonly">readonly</option></select></div>');
811
+ }
812
+ if (nodeClass === "Method") {
813
+ panel.append('<hr style="margin:8px 0; border-color:#e3e3e3;">');
814
+ panel.append('<div style="font-size:11px;font-weight:700;text-transform:uppercase;color:#666;margin-bottom:4px;">Inputs</div>');
815
+ var inputsDiv = $('<div id="opcua-detail-inputs"></div>').appendTo(panel);
816
+ (item.inputs || []).forEach(function (arg, idx) {
817
+ var argPath = selectedPath + ".inputs." + idx;
818
+ var argBlock = $('<div style="border:1px solid #e3e3e3;border-radius:4px;padding:6px;margin-bottom:4px;"></div>');
819
+ argBlock.append('<div class="form-row"><label>name</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="name"></div>');
820
+ argBlock.append('<div class="form-row"><label>type</label><select class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
821
+ argBlock.append('<div class="form-row"><label>displayName</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="displayName"></div>');
822
+ argBlock.append('<div class="form-row"><label>description</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="description"></div>');
823
+ argBlock.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small opcua-method-arg-remove" data-arg-path="' + argPath + '"><i class="fa fa-trash"></i> Remove</a></div>');
824
+ argBlock.find('[data-field="name"]').val(arg.name || "");
825
+ argBlock.find('[data-field="type"]').val(arg.type || "Float");
826
+ argBlock.find('[data-field="displayName"]').val(arg.displayName || "");
827
+ argBlock.find('[data-field="description"]').val(arg.description || "");
828
+ inputsDiv.append(argBlock);
829
+ });
830
+ panel.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small" id="opcua-method-add-input"><i class="fa fa-plus"></i> Add input</a></div>');
831
+ panel.append('<hr style="margin:8px 0; border-color:#e3e3e3;">');
832
+ panel.append('<div style="font-size:11px;font-weight:700;text-transform:uppercase;color:#666;margin-bottom:4px;">Outputs</div>');
833
+ var outputsDiv = $('<div id="opcua-detail-outputs"></div>').appendTo(panel);
834
+ (item.outputs || []).forEach(function (arg, idx) {
835
+ var argPath = selectedPath + ".outputs." + idx;
836
+ var argBlock = $('<div style="border:1px solid #e3e3e3;border-radius:4px;padding:6px;margin-bottom:4px;"></div>');
837
+ argBlock.append('<div class="form-row"><label>name</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="name"></div>');
838
+ argBlock.append('<div class="form-row"><label>type</label><select class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="type"><option value="Int16">Int16</option><option value="Int32">Int32</option><option value="Float">Float</option><option value="Boolean">Boolean</option><option value="String">String</option></select></div>');
839
+ argBlock.append('<div class="form-row"><label>displayName</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="displayName"></div>');
840
+ argBlock.append('<div class="form-row"><label>description</label><input type="text" class="opcua-method-arg-bind" data-arg-path="' + argPath + '" data-field="description"></div>');
841
+ argBlock.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small opcua-method-arg-remove" data-arg-path="' + argPath + '"><i class="fa fa-trash"></i> Remove</a></div>');
842
+ argBlock.find('[data-field="name"]').val(arg.name || "");
843
+ argBlock.find('[data-field="type"]').val(arg.type || "Float");
844
+ argBlock.find('[data-field="displayName"]').val(arg.displayName || "");
845
+ argBlock.find('[data-field="description"]').val(arg.description || "");
846
+ outputsDiv.append(argBlock);
847
+ });
848
+ panel.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small" id="opcua-method-add-output"><i class="fa fa-plus"></i> Add output</a></div>');
849
+ }
850
+ if (nodeClass === "Alarm") {
851
+ panel.append('<div class="form-row"><label>alarmType</label><select id="opcua-detail-alarm-type"><option value="levelAlarm">levelAlarm</option><option value="digitalAlarm">digitalAlarm</option></select></div>');
852
+ panel.append('<div class="form-row"><label>variablePath</label><input type="text" id="opcua-detail-variable-nodeid"></div>');
853
+ panel.append('<div class="form-row"><label>severity</label><input type="number" id="opcua-detail-severity"></div>');
854
+ if ((item.type || "levelAlarm") === "levelAlarm") {
855
+ panel.append('<div class="form-row"><label>highHighLimit</label><input type="number" id="opcua-detail-highhighlimit"></div>');
856
+ panel.append('<div class="form-row"><label>highHighMessage</label><input type="text" id="opcua-detail-highhighmessage"></div>');
857
+ panel.append('<div class="form-row"><label>highLimit</label><input type="number" id="opcua-detail-highlimit"></div>');
858
+ panel.append('<div class="form-row"><label>highMessage</label><input type="text" id="opcua-detail-highmessage"></div>');
859
+ panel.append('<div class="form-row"><label>lowLimit</label><input type="number" id="opcua-detail-lowlimit"></div>');
860
+ panel.append('<div class="form-row"><label>lowMessage</label><input type="text" id="opcua-detail-lowmessage"></div>');
861
+ panel.append('<div class="form-row"><label>lowLowLimit</label><input type="number" id="opcua-detail-lowlowlimit"></div>');
862
+ panel.append('<div class="form-row"><label>lowLowMessage</label><input type="text" id="opcua-detail-lowlowmessage"></div>');
863
+ } else {
864
+ panel.append('<div class="form-row"><label>normalStateValue</label><input type="number" id="opcua-detail-normalstatevalue"></div>');
865
+ panel.append('<div class="form-row"><label>digitalMessage</label><input type="text" id="opcua-detail-digitalmessage"></div>');
866
+ }
867
+ }
868
+ panel.append('<div class="form-row"><label style="width:90px;">Actions</label><div><a href="#" id="opcua-detail-edit" class="editor-button editor-button-small"><i class="fa fa-pencil"></i> Edit</a> <a href="#" id="opcua-detail-remove" class="editor-button editor-button-small"><i class="fa fa-trash"></i> Remove</a></div></div>');
869
+ $("#opcua-detail-name").val(item.name || "");
870
+ $("#opcua-detail-class").val(nodeClass);
871
+ $("#opcua-detail-nodeid").val(nodeIdLocked ? buildDefaultNodeIdSuffixFromEditorPath(selectedPath) : nodeIdSuffix);
872
+ $("#opcua-detail-description").val(item.description || "");
873
+ namespaceOptions.forEach(function (option) {
874
+ $("#opcua-detail-namespace").append($("<option></option>").val(option.id).text(getNamespaceLabel(option.id)));
875
+ });
876
+ $("#opcua-detail-namespace").val(String(namespaceId));
877
+ $("#opcua-detail-displayname").val(item.displayName || "");
878
+ $("#opcua-detail-accesspermission").val(normalizeAccessPermissionValues(item.accessPermission));
879
+ if (nodeClass === "ObjectType") { $("#opcua-detail-objectstype").val(item.objectsType || ""); }
880
+ if (nodeClass === "Variable") { $("#opcua-detail-type").val(item.type || "Int32"); $("#opcua-detail-value").val(item.value !== undefined ? item.value : ""); $("#opcua-detail-access").val(item.access || "readwrite"); }
881
+ if (nodeClass === "Alarm") {
882
+ $("#opcua-detail-alarm-type").val(item.type || "levelAlarm");
883
+ $("#opcua-detail-variable-nodeid").val(item.variableNodeId || "");
884
+ $("#opcua-detail-severity").val(item.severity !== undefined ? item.severity : 500);
885
+ $("#opcua-detail-highhighlimit").val(item.highHighLimit !== undefined ? item.highHighLimit : 100);
886
+ $("#opcua-detail-highhighmessage").val(item.highHighMessage || "High High alarm");
887
+ $("#opcua-detail-highlimit").val(item.highLimit !== undefined ? item.highLimit : 80);
888
+ $("#opcua-detail-highmessage").val(item.highMessage || "High alarm");
889
+ $("#opcua-detail-lowlimit").val(item.lowLimit !== undefined ? item.lowLimit : 20);
890
+ $("#opcua-detail-lowmessage").val(item.lowMessage || "Low alarm");
891
+ $("#opcua-detail-lowlowlimit").val(item.lowLowLimit !== undefined ? item.lowLowLimit : 0);
892
+ $("#opcua-detail-lowlowmessage").val(item.lowLowMessage || "Low Low alarm");
893
+ $("#opcua-detail-normalstatevalue").val(item.normalStateValue !== undefined ? item.normalStateValue : 0);
894
+ $("#opcua-detail-digitalmessage").val(item.digitalMessage || "Digital alarm");
895
+ }
896
+ }
897
+
898
+ function addNode(path, explicitKind) {
899
+ var kind = explicitKind || "object";
900
+ openCreateForm(path, kind);
901
+ }
902
+
903
+ function removeNode(path) {
904
+ if (!path) return;
905
+ if (nodeClassFromPath(path) === "Namespace") {
906
+ var namespaceItem = getAtPath(editorState, path);
907
+ if (normalizeNamespaceId(namespaceItem && namespaceItem.id) === DEFAULT_NAMESPACE_ID) {
908
+ RED.notify("Namespace 2 is fixed and cannot be removed.", "warning");
909
+ return;
910
+ }
911
+ }
912
+ removeAtPath(editorState, path);
913
+ if (selectedPath === path) selectedPath = "";
914
+ syncStateToJson(true);
915
+ renderVisualEditor();
916
+ }
917
+
918
+ function buildGroupOptions(selectedGroup) {
919
+ var currentGroup = String(selectedGroup || "");
920
+ var options = authGroups.map(function (groupName) {
921
+ var selected = groupName === currentGroup ? " selected" : "";
922
+ return '<option value="' + escapeHtml(groupName) + '"' + selected + '>' + escapeHtml(groupName) + "</option>";
923
+ });
924
+ if (currentGroup && authGroups.indexOf(currentGroup) === -1) {
925
+ options.unshift('<option value="' + escapeHtml(currentGroup) + '" selected>' + escapeHtml(currentGroup) + "</option>");
926
+ }
927
+ if (!options.length) {
928
+ options.push('<option value="">No groups available</option>');
929
+ }
930
+ return options.join("");
931
+ }
932
+
933
+ function ensureDefaultAuthGroup() {
934
+ if (!authGroups.length) {
935
+ authGroups.push("operator");
936
+ }
937
+ }
938
+
939
+ function addAuthGroup() {
940
+ var baseName = "group";
941
+ var suffix = authGroups.length + 1;
942
+ var candidate = baseName + suffix;
943
+ while (authGroups.indexOf(candidate) !== -1) {
944
+ suffix += 1;
945
+ candidate = baseName + suffix;
946
+ }
947
+ authGroups.push(candidate);
948
+ authUsers.forEach(function (user) {
949
+ if (!user.group) {
950
+ user.group = candidate;
951
+ }
952
+ });
953
+ syncAuthCredentialFields();
954
+ renderAuthEditor();
955
+ }
956
+
957
+ function addAuthUser() {
958
+ ensureDefaultAuthGroup();
959
+ authUsers.push({
960
+ username: "",
961
+ password: "",
962
+ passwordHash: "",
963
+ group: authGroups[0] || ""
964
+ });
965
+ syncAuthCredentialFields();
966
+ renderAuthEditor();
967
+ }
968
+
969
+ function removeAuthGroup(index) {
970
+ var groupName = authGroups[index];
971
+ var inUse = authUsers.some(function (user) { return user.group === groupName; });
972
+ if (inUse) {
973
+ RED.notify("Reassign users before removing this group.", "warning");
974
+ return;
975
+ }
976
+ authGroups.splice(index, 1);
977
+ syncAuthCredentialFields();
978
+ renderAuthEditor();
979
+ }
980
+
981
+ function removeAuthUser(index) {
982
+ authUsers.splice(index, 1);
983
+ syncAuthCredentialFields();
984
+ renderAuthEditor();
985
+ }
986
+
987
+ function renderAuthGroups() {
988
+ var container = $("#opcua-auth-groups");
989
+ container.empty();
990
+ if (!authGroups.length) {
991
+ container.append('<div class="opcua-tree-empty">No groups configured.</div>');
992
+ return;
993
+ }
994
+
995
+ authGroups.forEach(function (groupName, index) {
996
+ var card = $('<div class="opcua-auth-card"></div>');
997
+ card.append('<div class="form-row"><label>Name</label><input type="text" class="opcua-auth-group-name" data-index="' + index + '" data-previous="' + escapeHtml(groupName) + '"></div>');
998
+ card.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small opcua-auth-group-remove" data-index="' + index + '"><i class="fa fa-trash"></i> Remove</a></div>');
999
+ card.find(".opcua-auth-group-name").val(groupName);
1000
+ container.append(card);
1001
+ });
1002
+ }
1003
+
1004
+ function renderAuthUsers() {
1005
+ var container = $("#opcua-auth-users");
1006
+ container.empty();
1007
+ if (!authUsers.length) {
1008
+ container.append('<div class="opcua-tree-empty">No users configured.</div>');
1009
+ return;
1010
+ }
1011
+
1012
+ authUsers.forEach(function (user, index) {
1013
+ var card = $('<div class="opcua-auth-card"></div>');
1014
+ card.append('<div class="form-row"><label>Username</label><input type="text" class="opcua-auth-user-username" data-index="' + index + '"></div>');
1015
+ card.append('<div class="form-row"><label>Password</label><input type="password" class="opcua-auth-user-password" data-index="' + index + '" autocomplete="new-password"></div>');
1016
+ card.append('<div class="form-row"><label>Group</label><select class="opcua-auth-user-group" data-index="' + index + '">' + buildGroupOptions(user.group) + '</select></div>');
1017
+ card.append('<div class="form-row"><label></label><a href="#" class="editor-button editor-button-small opcua-auth-user-remove" data-index="' + index + '"><i class="fa fa-trash"></i> Remove</a></div>');
1018
+ card.find(".opcua-auth-user-username").val(user.username || "");
1019
+ card.find(".opcua-auth-user-password").val(user.password || "");
1020
+ card.find(".opcua-auth-user-group").val(user.group || "");
1021
+ container.append(card);
1022
+ });
1023
+ }
1024
+
1025
+ function renderAuthEditor() {
1026
+ renderAuthGroups();
1027
+ renderAuthUsers();
1028
+
1029
+ }
1030
+
1031
+ function validateAuthState() {
1032
+ var seenUsers = {};
1033
+ var seenGroups = {};
1034
+
1035
+ for (var i = 0; i < authGroups.length; i += 1) {
1036
+ var groupName = String(authGroups[i] || "").trim();
1037
+ if (!groupName) {
1038
+ RED.notify("Group names cannot be empty.", "warning");
1039
+ return false;
1040
+ }
1041
+ if (seenGroups[groupName]) {
1042
+ RED.notify("Group names must be unique.", "warning");
1043
+ return false;
1044
+ }
1045
+ seenGroups[groupName] = true;
1046
+ }
1047
+
1048
+ for (var j = 0; j < authUsers.length; j += 1) {
1049
+ var user = authUsers[j] || {};
1050
+ var username = String(user.username || "").trim();
1051
+ if (!username) {
1052
+ RED.notify("Usernames cannot be empty.", "warning");
1053
+ return false;
1054
+ }
1055
+ if (seenUsers[username]) {
1056
+ RED.notify("Usernames must be unique.", "warning");
1057
+ return false;
1058
+ }
1059
+ if (!user.password && !user.passwordHash) {
1060
+ RED.notify("Each user requires a password.", "warning");
1061
+ return false;
1062
+ }
1063
+ if (!user.group) {
1064
+ RED.notify("Each user requires a group.", "warning");
1065
+ return false;
1066
+ }
1067
+ seenUsers[username] = true;
1068
+ }
1069
+
1070
+ return true;
1071
+ }
1072
+
1073
+ function renderVisualEditor() {
1074
+ renderTree();
1075
+ renderDetails();
1076
+ renderBreadcrumbs();
1077
+
1078
+ }
1079
+
1080
+ function getNextNamespaceId() {
1081
+ var ids = getNamespaceOptions().map(function (item) { return normalizeNamespaceId(item.id); });
1082
+ var nextId = DEFAULT_NAMESPACE_ID;
1083
+ while (ids.indexOf(nextId) !== -1) nextId += 1;
1084
+ return nextId;
1085
+ }
1086
+
1087
+ function addItem(parentPath, kind) {
1088
+ var target = getAtPath(editorState, parentPath);
1089
+ if (!Array.isArray(target)) return;
1090
+ if (kind === "object" || kind === "folder" || kind === "objectTypeDefinition") {
1091
+ target.push(normalizeBranch());
1092
+ if (kind === "objectTypeDefinition") {
1093
+ target[target.length - 1].nodeId = buildGeneratedNodeIdForPath(parentPath + "." + (target.length - 1));
1094
+ }
1095
+ }
1096
+ if (kind === "namespace") {
1097
+ var namespaceId = getNextNamespaceId();
1098
+ target.push(normalizeNamespaceDefinition({
1099
+ id: namespaceId,
1100
+ name: "urn:namespace:" + namespaceId
1101
+ }));
1102
+ }
1103
+ }
1104
+
1105
+ $(document).on("change", "#node-input-tree", function () {
1106
+ var jsonText = $(this).val();
1107
+ if (!jsonText) {
1108
+ editorState = normalizeTree({ objects: [], folders: [], objectsTypes: [], nameSpaces: [] });
1109
+ updateTreeField(prettyTree(editorState), true);
1110
+ renderVisualEditor();
1111
+ return;
1112
+ }
1113
+ try {
1114
+ editorState = cloneTree(normalizeTree(parseTree(jsonText, true)));
1115
+ renderVisualEditor();
1116
+ } catch (error) { RED.notify("Invalid JSON: " + error.message, "error"); }
1117
+ });
1118
+
1119
+ $(document).on("click", ".opcua-tree-row", function (event) {
1120
+ var path = $(this).attr("data-path");
1121
+ if ($(event.target).closest(".opcua-tree-twisty").length) {
1122
+ expansionState[path] = !isExpanded(path, false);
1123
+ renderTree();
1124
+ return;
1125
+ }
1126
+ selectNode(path);
1127
+ });
1128
+
1129
+ $(document).on("contextmenu", ".opcua-tree-row", function (event) {
1130
+ event.preventDefault();
1131
+ selectNode($(this).attr("data-path"));
1132
+ $("#opcua-tree-context-menu").css({ left: event.clientX + "px", top: event.clientY + "px" }).show();
1133
+ });
1134
+
1135
+ $(document).on("click", function () { $("#opcua-tree-context-menu").hide(); });
1136
+ $(document).on("click", "#opcua-tree-context-menu a", function (event) {
1137
+ event.preventDefault();
1138
+ var action = $(this).attr("data-action");
1139
+ if (action === "add-folder") addNode(selectedPath, "folder");
1140
+ if (action === "add-object") addNode(selectedPath, "object");
1141
+ if (action === "add-variable") addNode(selectedPath, "variable");
1142
+ if (action === "add-objecttype") addNode(selectedPath, "objecttype");
1143
+ if (action === "add-alarm") addNode(selectedPath, "alarm");
1144
+ if (action === "add-method") addNode(selectedPath, "method");
1145
+ if (action === "add-method") addNode(selectedPath, "method");
1146
+ if (action === "remove") removeNode(selectedPath);
1147
+ if (action === "edit") renderDetails();
1148
+ $("#opcua-tree-context-menu").hide();
1149
+ });
1150
+
1151
+ $(document).on("input change", ".opcua-method-arg-bind", function () {
1152
+ var el = $(this);
1153
+ var argPath = el.attr("data-arg-path");
1154
+ var field = el.attr("data-field");
1155
+ var arg = getAtPath(editorState, argPath);
1156
+ if (!arg) return;
1157
+ arg[field] = el.val();
1158
+ syncStateToJson(false);
1159
+ });
1160
+
1161
+ $(document).on("click", ".opcua-method-arg-remove", function (e) {
1162
+ e.preventDefault();
1163
+ var argPath = $(this).attr("data-arg-path");
1164
+ removeAtPath(editorState, argPath);
1165
+ syncStateToJson(false);
1166
+ renderDetails();
1167
+ });
1168
+
1169
+ $(document).on("click", "#opcua-method-add-input", function (e) {
1170
+ e.preventDefault();
1171
+ var item = getAtPath(editorState, selectedPath);
1172
+ if (!item) return;
1173
+ if (!Array.isArray(item.inputs)) item.inputs = [];
1174
+ item.inputs.push(normalizeMethodArg());
1175
+ syncStateToJson(false);
1176
+ renderDetails();
1177
+ });
1178
+
1179
+ $(document).on("click", "#opcua-method-add-output", function (e) {
1180
+ e.preventDefault();
1181
+ var item = getAtPath(editorState, selectedPath);
1182
+ if (!item) return;
1183
+ if (!Array.isArray(item.outputs)) item.outputs = [];
1184
+ item.outputs.push(normalizeMethodArg());
1185
+ syncStateToJson(false);
1186
+ renderDetails();
1187
+ });
1188
+
1189
+ $(document).on("input", "#opcua-detail-displayname", function () { updateNode(selectedPath, { displayName: $(this).val() }); });
1190
+
1191
+ $(document).on("input", "#opcua-detail-name", function () { updateNode(selectedPath, { name: $(this).val() }); });
1192
+ $(document).on("change", "#opcua-detail-namespace", function () {
1193
+ var nextNamespaceId = normalizeNamespaceId($(this).val());
1194
+ var item = getAtPath(editorState, selectedPath);
1195
+ if (!item) return;
1196
+ item.namespaceId = nextNamespaceId;
1197
+ item.nodeId = normalizeCustomNodeIdFromSuffix(selectedPath, $("#opcua-detail-nodeid").val());
1198
+ syncStateToJson(false);
1199
+ renderTree();
1200
+ renderBreadcrumbs();
1201
+ renderDetails();
1202
+ });
1203
+ $(document).on("input", "#opcua-detail-nodeid", function () { updateNode(selectedPath, { nodeId: normalizeCustomNodeIdFromSuffix(selectedPath, $(this).val()) }); });
1204
+ $(document).on("click", "#opcua-detail-copy-nodeid", function (event) {
1205
+ event.preventDefault();
1206
+ copyNodeIdValue(buildDisplayNodeIdFromEditorPath(selectedPath));
1207
+ });
1208
+ $(document).on("input", "#opcua-detail-namespace-entry-id", function () {
1209
+ var nextId = normalizeNamespaceId($(this).val());
1210
+ var currentPath = selectedPath;
1211
+ var duplicate = (editorState.nameSpaces || []).some(function (namespaceItem, index) {
1212
+ return currentPath !== ("nameSpaces." + index) && normalizeNamespaceId(namespaceItem.id) === nextId;
1213
+ });
1214
+ if (duplicate) {
1215
+ RED.notify("Namespace id must be unique.", "warning");
1216
+ return;
1217
+ }
1218
+ updateNode(selectedPath, { id: nextId });
1219
+ });
1220
+ $(document).on("input", "#opcua-detail-namespace-entry-name", function () {
1221
+ var nextName = $(this).val();
1222
+ updateNode(selectedPath, { name: nextName });
1223
+ var namespaceItem = getAtPath(editorState, selectedPath);
1224
+ if (normalizeNamespaceId(namespaceItem && namespaceItem.id) === DEFAULT_NAMESPACE_ID) {
1225
+ $("#node-input-namespaceUri").val(nextName);
1226
+ }
1227
+ });
1228
+ $(document).on("input", "#opcua-detail-description", function () { updateNode(selectedPath, { description: $(this).val() }); });
1229
+ $(document).on("change", "#opcua-detail-accesspermission", function () {
1230
+ updateNode(selectedPath, { accessPermission: normalizeAccessPermissionValues($(this).val()) });
1231
+ });
1232
+ $(document).on("change", "#opcua-detail-objectstype", function () { updateNode(selectedPath, { objectsType: $(this).val() }); });
1233
+ $(document).on("change", "#opcua-detail-type", function () { updateNode(selectedPath, { type: $(this).val() }); });
1234
+ $(document).on("change", "#opcua-detail-access", function () { updateNode(selectedPath, { access: $(this).val() }); });
1235
+ $(document).on("input", "#opcua-detail-value", function () { updateNode(selectedPath, { value: $(this).val() }); });
1236
+ $(document).on("change", "#opcua-detail-alarm-type", function () {
1237
+ updateNode(selectedPath, { type: $(this).val() });
1238
+ renderDetails();
1239
+ });
1240
+ $(document).on("input", "#opcua-detail-variable-nodeid", function () { updateNode(selectedPath, { variableNodeId: $(this).val() }); });
1241
+ $(document).on("input", "#opcua-detail-severity", function () { updateNode(selectedPath, { severity: Number($(this).val() || 0) }); });
1242
+ $(document).on("input", "#opcua-detail-highhighlimit", function () { updateNode(selectedPath, { highHighLimit: Number($(this).val() || 0) }); });
1243
+ $(document).on("input", "#opcua-detail-highhighmessage", function () { updateNode(selectedPath, { highHighMessage: $(this).val() }); });
1244
+ $(document).on("input", "#opcua-detail-highlimit", function () { updateNode(selectedPath, { highLimit: Number($(this).val() || 0) }); });
1245
+ $(document).on("input", "#opcua-detail-highmessage", function () { updateNode(selectedPath, { highMessage: $(this).val() }); });
1246
+ $(document).on("input", "#opcua-detail-lowlimit", function () { updateNode(selectedPath, { lowLimit: Number($(this).val() || 0) }); });
1247
+ $(document).on("input", "#opcua-detail-lowmessage", function () { updateNode(selectedPath, { lowMessage: $(this).val() }); });
1248
+ $(document).on("input", "#opcua-detail-lowlowlimit", function () { updateNode(selectedPath, { lowLowLimit: Number($(this).val() || 0) }); });
1249
+ $(document).on("input", "#opcua-detail-lowlowmessage", function () { updateNode(selectedPath, { lowLowMessage: $(this).val() }); });
1250
+ $(document).on("input", "#opcua-detail-normalstatevalue", function () { updateNode(selectedPath, { normalStateValue: Number($(this).val() || 0) }); });
1251
+ $(document).on("input", "#opcua-detail-digitalmessage", function () { updateNode(selectedPath, { digitalMessage: $(this).val() }); });
1252
+ $(document).on("input", "#opcua-create-name", function () { if (pendingCreate) pendingCreate.name = $(this).val(); });
1253
+ $(document).on("input", "#opcua-create-displayname", function () { if (pendingCreate) pendingCreate.displayName = $(this).val(); });
1254
+ $(document).on("change", "#opcua-create-accesspermission", function () { if (pendingCreate) pendingCreate.accessPermission = normalizeAccessPermissionValues($(this).val()); });
1255
+ $(document).on("change", "#opcua-create-type", function () { if (pendingCreate) pendingCreate.dataType = $(this).val(); });
1256
+ $(document).on("input", "#opcua-create-value", function () { if (pendingCreate) pendingCreate.value = $(this).val(); });
1257
+ $(document).on("change", "#opcua-create-objectstype", function () { if (pendingCreate) pendingCreate.objectsType = $(this).val(); });
1258
+ $(document).on("change", "#opcua-create-access", function () { if (pendingCreate) pendingCreate.access = $(this).val(); });
1259
+ $(document).on("change", "#opcua-create-alarm-type", function () {
1260
+ if (pendingCreate) {
1261
+ pendingCreate.alarmType = $(this).val();
1262
+ renderDetails();
1263
+ }
1264
+ });
1265
+ $(document).on("input", "#opcua-create-variable-nodeid", function () { if (pendingCreate) pendingCreate.variableNodeId = $(this).val(); });
1266
+ $(document).on("input", "#opcua-create-severity", function () { if (pendingCreate) pendingCreate.severity = Number($(this).val() || 0); });
1267
+ $(document).on("input", "#opcua-create-highhighlimit", function () { if (pendingCreate) pendingCreate.highHighLimit = Number($(this).val() || 0); });
1268
+ $(document).on("input", "#opcua-create-highhighmessage", function () { if (pendingCreate) pendingCreate.highHighMessage = $(this).val(); });
1269
+ $(document).on("input", "#opcua-create-highlimit", function () { if (pendingCreate) pendingCreate.highLimit = Number($(this).val() || 0); });
1270
+ $(document).on("input", "#opcua-create-highmessage", function () { if (pendingCreate) pendingCreate.highMessage = $(this).val(); });
1271
+ $(document).on("input", "#opcua-create-lowlimit", function () { if (pendingCreate) pendingCreate.lowLimit = Number($(this).val() || 0); });
1272
+ $(document).on("input", "#opcua-create-lowmessage", function () { if (pendingCreate) pendingCreate.lowMessage = $(this).val(); });
1273
+ $(document).on("input", "#opcua-create-lowlowlimit", function () { if (pendingCreate) pendingCreate.lowLowLimit = Number($(this).val() || 0); });
1274
+ $(document).on("input", "#opcua-create-lowlowmessage", function () { if (pendingCreate) pendingCreate.lowLowMessage = $(this).val(); });
1275
+ $(document).on("input", "#opcua-create-normalstatevalue", function () { if (pendingCreate) pendingCreate.normalStateValue = Number($(this).val() || 0); });
1276
+ $(document).on("input", "#opcua-create-digitalmessage", function () { if (pendingCreate) pendingCreate.digitalMessage = $(this).val(); });
1277
+ $(document).on("click", "#opcua-create-save", function (e) { e.preventDefault(); saveCreateForm(); });
1278
+ $(document).on("click", "#opcua-create-cancel", function (e) { e.preventDefault(); cancelCreateForm(); });
1279
+ $(document).on("click", "#opcua-detail-edit", function (e) { e.preventDefault(); renderDetails(); });
1280
+ $(document).on("click", "#opcua-detail-remove", function (e) { e.preventDefault(); removeNode(selectedPath); });
1281
+ $(document).on("input", ".opcua-auth-group-name", function () {
1282
+ var input = $(this);
1283
+ var index = Number(input.attr("data-index"));
1284
+ var previousGroup = String(input.attr("data-previous") || "");
1285
+ var nextGroup = String(input.val() || "").trim();
1286
+ authGroups[index] = nextGroup;
1287
+ authUsers.forEach(function (user) {
1288
+ if (user.group === previousGroup) {
1289
+ user.group = nextGroup;
1290
+ }
1291
+ });
1292
+ input.attr("data-previous", nextGroup);
1293
+ syncAuthCredentialFields();
1294
+
1295
+ renderAuthUsers();
1296
+ });
1297
+ $(document).on("click", ".opcua-auth-group-remove", function (event) {
1298
+ event.preventDefault();
1299
+ removeAuthGroup(Number($(this).attr("data-index")));
1300
+ });
1301
+ $(document).on("input", ".opcua-auth-user-username", function () {
1302
+ var index = Number($(this).attr("data-index"));
1303
+ authUsers[index].username = $(this).val();
1304
+ syncAuthCredentialFields();
1305
+
1306
+ });
1307
+ $(document).on("input", ".opcua-auth-user-password", function () {
1308
+ var index = Number($(this).attr("data-index"));
1309
+ authUsers[index].password = $(this).val();
1310
+ authUsers[index].passwordHash = "";
1311
+ syncAuthCredentialFields();
1312
+ });
1313
+ $(document).on("change", ".opcua-auth-user-group", function () {
1314
+ var index = Number($(this).attr("data-index"));
1315
+ authUsers[index].group = $(this).val();
1316
+ syncAuthCredentialFields();
1317
+
1318
+ });
1319
+ $(document).on("click", ".opcua-auth-user-remove", function (event) {
1320
+ event.preventDefault();
1321
+ removeAuthUser(Number($(this).attr("data-index")));
1322
+ });
1323
+
1324
+ RED.nodes.registerType("opc-ua-server", {
1325
+ category: "network",
1326
+ color: "#d9edf7",
1327
+ credentials: { username: { type: "text" }, password: { type: "password" }, users: { type: "text" }, groups: { type: "text" } },
1328
+ defaults: {
1329
+ name: { value: "" }, resourcePath: { value: "/" }, serverName: { value: "Node-RED OPC UA Server", required: true }, allowAnonymous: { value: true },
1330
+ port: { value: 4840, required: true, validate: function (value) { var port = Number(value); return Number.isInteger(port) && port > 0 && port < 65536; } },
1331
+ maxConnections: { value: 10, required: true, validate: function (value) { var n = Number(value); return Number.isInteger(n) && n > 0; } },
1332
+ securityPolicy: { value: "None", required: true }, securityMode: { value: "None", required: true }, namespaceUri: { value: "urn:node-red:opc-ua-server", required: true },
1333
+ tree: {
1334
+ value: "{\n \"folders\": [],\n \"objects\": [],\n \"objectsTypes\": [],\n \"nameSpaces\": [\n {\n \"id\": 2,\n \"name\": \"urn:node-red:opc-ua-server\"\n }\n ]\n}",
1335
+ validate: function (value) {
1336
+ try { var parsed = parseTree(value, true); return Array.isArray(parsed.objects || []) && Array.isArray(parsed.folders || []) && Array.isArray(parsed.objectsTypes || parsed.objectTypes || []) && Array.isArray(parsed.nameSpaces || parsed.namespaces || []); }
1337
+ catch (error) { return false; }
1338
+ }
1339
+ }
1340
+ },
1341
+ inputs: 1,
1342
+ outputs: 1,
1343
+ icon: "opcua.svg",
1344
+ label: function () { return this.name || this.serverName || "opc-ua-server"; },
1345
+ oneditprepare: function () {
1346
+ var node = this;
1347
+ editorState = cloneTree(normalizeTree(parseTree(node.tree)));
1348
+ authGroups = normalizeAuthGroups($("#node-input-groups").val());
1349
+ authUsers = normalizeAuthUsers($("#node-input-users").val());
1350
+ var defaultNamespaceEntry = (editorState.nameSpaces || []).find(function (item) { return normalizeNamespaceId(item.id) === DEFAULT_NAMESPACE_ID; });
1351
+ if (defaultNamespaceEntry && defaultNamespaceEntry.name) {
1352
+ $("#node-input-namespaceUri").val(defaultNamespaceEntry.name);
1353
+ }
1354
+ syncAuthCredentialFields();
1355
+ updateTreeField(prettyTree(editorState), false);
1356
+ $("#node-input-tree-editor").typedInput({ type: "json", types: ["json"] });
1357
+ $("#node-input-tree-editor").typedInput("value", prettyTree(editorState));
1358
+ $("#node-input-tree-editor").on("change", function () {
1359
+ if (isSyncing) return;
1360
+ var val = $(this).typedInput("value");
1361
+ $("#node-input-tree").val(val).trigger("change");
1362
+ });
1363
+
1364
+ treeSearchValue = ""; treeSearchTerm = ""; selectedPath = "";
1365
+ $("#node-input-tree-search").val(""); $("#node-input-tree-search-clear").hide();
1366
+ renderVisualEditor();
1367
+ renderAuthEditor();
1368
+
1369
+ $("#node-input-open-tree-modal").off("click").on("click", function (event) { event.preventDefault(); openTreeModal(); });
1370
+ $("#node-input-close-tree-modal").off("click").on("click", function (event) { event.preventDefault(); closeTreeModal(); });
1371
+ $("#node-input-tree-modal").off("click").on("click", function (event) { if (event.target === this) closeTreeModal(); });
1372
+ $("#node-input-open-auth-modal").off("click").on("click", function (event) { event.preventDefault(); openAuthModal(); });
1373
+ $("#node-input-close-auth-modal").off("click").on("click", function (event) { event.preventDefault(); closeAuthModal(); });
1374
+ $("#node-input-auth-modal").off("click").on("click", function (event) { if (event.target === this) closeAuthModal(); });
1375
+ $("#node-input-add-auth-group").off("click").on("click", function (event) { event.preventDefault(); addAuthGroup(); });
1376
+ $("#node-input-add-auth-user").off("click").on("click", function (event) { event.preventDefault(); addAuthUser(); });
1377
+
1378
+ $("#node-input-tree-search").off("input").on("input", debounce(function () {
1379
+ treeSearchValue = $(this).val(); treeSearchTerm = normalizeSearchTerm(treeSearchValue);
1380
+ $("#node-input-tree-search-clear").toggle(!!treeSearchTerm); renderTree();
1381
+ }, 200));
1382
+ $("#node-input-tree-search-clear").off("click").on("click", function (event) {
1383
+ event.preventDefault(); treeSearchValue = ""; treeSearchTerm = "";
1384
+ $("#node-input-tree-search").val(""); $(this).hide(); renderTree();
1385
+ });
1386
+ $("#node-input-namespaceUri").off("input.opcuaNamespaceDefault").on("input.opcuaNamespaceDefault", function () {
1387
+ var namespaceEntry = (editorState.nameSpaces || []).find(function (item) { return normalizeNamespaceId(item.id) === DEFAULT_NAMESPACE_ID; });
1388
+ if (!namespaceEntry) return;
1389
+ namespaceEntry.name = $(this).val() || "urn:node-red:opc-ua-server";
1390
+ syncStateToJson(false);
1391
+ if (selectedPath && nodeClassFromPath(selectedPath) === "Namespace") renderDetails();
1392
+ renderTree();
1393
+ });
1394
+ $(document).off("keydown.opcuaTreeModal").on("keydown.opcuaTreeModal", function (event) {
1395
+ if (event.key !== "Escape") return;
1396
+ if ($("#node-input-auth-modal").is(":visible")) {
1397
+ closeAuthModal();
1398
+ return;
1399
+ }
1400
+ closeTreeModal();
1401
+ });
1402
+
1403
+ $("#node-input-add-object").off("click").on("click", function (event) { event.preventDefault(); addItem("objects", "object"); syncStateToJson(true); renderVisualEditor(); });
1404
+ $("#node-input-add-folder").off("click").on("click", function (event) { event.preventDefault(); addItem("folders", "folder"); syncStateToJson(true); renderVisualEditor(); });
1405
+ $("#node-input-add-object-type").off("click").on("click", function (event) { event.preventDefault(); addItem("objectsTypes", "objectTypeDefinition"); syncStateToJson(true); renderVisualEditor(); });
1406
+ $("#node-input-add-namespace").off("click").on("click", function (event) { event.preventDefault(); addItem("nameSpaces", "namespace"); syncStateToJson(true); renderVisualEditor(); });
1407
+ $("#node-input-expand-all").off("click").on("click", function (event) {
1408
+ event.preventDefault();
1409
+ function walk(path) { expansionState[path] = true; getChildrenByPath(path).forEach(walk); }
1410
+ getTopLevelPaths().forEach(walk); renderTree();
1411
+ });
1412
+ $("#node-input-collapse-all").off("click").on("click", function (event) { event.preventDefault(); expansionState = {}; renderTree(); });
1413
+ syncStateToJson(false);
1414
+ },
1415
+ oneditsave: function () {
1416
+ var editorValue = $("#node-input-tree-editor").typedInput("value");
1417
+ $("#node-input-tree").val(editorValue);
1418
+ var jsonText = $("#node-input-tree").val().trim();
1419
+ try { editorState = cloneTree(normalizeTree(parseTree(jsonText, true))); }
1420
+ catch (error) { RED.notify("Invalid OPC UA tree JSON: " + error.message, "error"); return; }
1421
+ if (pendingPasswordHashes > 0) { RED.notify("Wait for password hashing to finish before saving.", "warning"); return; }
1422
+ if (!validateAuthState()) { return; }
1423
+ syncAuthCredentialFields();
1424
+ updateTreeField(prettyTree(editorState), true);
1425
+ closeAuthModal();
1426
+ closeTreeModal();
1427
+ $(document).off("keydown.opcuaTreeModal");
1428
+ },
1429
+ oneditcancel: function () {
1430
+ closeAuthModal();
1431
+ closeTreeModal();
1432
+ $(document).off("keydown.opcuaTreeModal");
1433
+ }
1434
+ });
1435
+ })();