@frangoteam/fuxa-min 1.2.10 → 1.3.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 (60) hide show
  1. package/api/auth/index.js +141 -3
  2. package/api/command/index.js +10 -4
  3. package/api/diagnose/index.js +12 -4
  4. package/api/index.js +41 -8
  5. package/api/jwt-helper.js +15 -2
  6. package/api/path-helper.js +41 -0
  7. package/api/projects/index.js +27 -14
  8. package/api/reports/reports.service.ts +12 -2
  9. package/api/resources/index.js +30 -9
  10. package/api/scheduler/index.js +21 -1
  11. package/dist/3rdpartylicenses.txt +139 -7
  12. package/dist/assets/i18n/de.json +10 -0
  13. package/dist/assets/i18n/en.json +17 -3
  14. package/dist/assets/i18n/es.json +12 -0
  15. package/dist/assets/i18n/fr.json +10 -0
  16. package/dist/assets/i18n/ja.json +15 -6
  17. package/dist/assets/i18n/ko.json +12 -0
  18. package/dist/assets/i18n/pt.json +9 -2
  19. package/dist/assets/i18n/ru.json +11 -0
  20. package/dist/assets/i18n/sv.json +10 -1
  21. package/dist/assets/i18n/tr.json +8 -1
  22. package/dist/assets/i18n/ua.json +9 -2
  23. package/dist/assets/i18n/zh-cn.json +10 -0
  24. package/dist/assets/i18n/zh-tw.json +11 -1
  25. package/dist/index.html +2 -2
  26. package/dist/main.bafae830903d548e.js +329 -0
  27. package/dist/polyfills.d7de05f9af2fb559.js +1 -0
  28. package/dist/reports.service.js +11 -1
  29. package/dist/{runtime.8ef63094e52a66ba.js → runtime.9136a61a9b98f987.js} +1 -1
  30. package/dist/{scripts.40b60f02658462e4.js → scripts.d9e6ee984bf6f3b7.js} +1 -1
  31. package/dist/styles.545e37beb3e671ba.css +1 -0
  32. package/integrations/node-red/index.js +91 -24
  33. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.html +56 -5
  34. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-daq.js +8 -2
  35. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.html +56 -5
  36. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-change.js +12 -12
  37. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.html +56 -5
  38. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag-daq-settings.js +14 -10
  39. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.html +56 -5
  40. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-get-tag.js +8 -2
  41. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.html +56 -5
  42. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag-daq-settings.js +24 -20
  43. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.html +56 -5
  44. package/integrations/node-red/node-red-contrib-fuxa/nodes/fuxa-set-tag.js +8 -2
  45. package/main.js +41 -17
  46. package/package.json +10 -5
  47. package/runtime/devices/adsclient/index.js +1 -1
  48. package/runtime/devices/bacnet/index.js +66 -32
  49. package/runtime/devices/ethernetip/index.js +1 -1
  50. package/runtime/devices/gpio/index.js +1 -1
  51. package/runtime/devices/odbc/index.js +5 -5
  52. package/runtime/devices/template/index.js +14 -14
  53. package/runtime/storage/daqstorage.js +28 -2
  54. package/runtime/storage/influxdb/index.js +1 -1
  55. package/runtime/storage/questdb/index.js +224 -0
  56. package/runtime/utils.js +5 -0
  57. package/settings.default.js +13 -3
  58. package/dist/main.020ca34630a3363a.js +0 -329
  59. package/dist/polyfills.c8e7db9850a3ad8b.js +0 -1
  60. package/dist/styles.03cc550382689976.css +0 -1
@@ -34,8 +34,8 @@ async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger,
34
34
  // Minimal Node-RED settings; extend only what is really needed
35
35
  const redSettings = {
36
36
  httpAdminRoot: '/nodered/',
37
- // Let Node-RED own the HTTP space at root; dashboard will live under /dashboard
38
- httpNodeRoot: '/',
37
+ // Serve Node-RED HTTP nodes under /dashboard to avoid intercepting FUXA routes
38
+ httpNodeRoot: '/dashboard',
39
39
  userDir,
40
40
  nodesDir: [path.join(__dirname, 'node-red-contrib-fuxa')],
41
41
  flowFile: 'flows.json',
@@ -44,7 +44,7 @@ async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger,
44
44
  tours: { enabled: false },
45
45
  },
46
46
  // Dashboard will be exposed at /dashboard/...
47
- ui: { path: '/dashboard' },
47
+ ui: { path: '/' },
48
48
  // Values used by FlowFuse dashboard's ui_base.js for layout saves
49
49
  // These mirror the FUXA HTTP bind address so Node-RED can call its own /nodered/flows API
50
50
  uiHost: settings.uiHost,
@@ -99,10 +99,8 @@ async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger,
99
99
  path: require('path'),
100
100
  util: require('util'),
101
101
  os: require('os'),
102
- child_process: require('child_process'),
103
102
  http: require('http'),
104
103
  https: require('https'),
105
- net: require('net'),
106
104
  dgram: require('dgram'),
107
105
  dns: require('dns'),
108
106
  url: require('url'),
@@ -114,40 +112,109 @@ async function mountNodeRedIfInstalled({ app, server, settings, runtime, logger,
114
112
  buffer: require('buffer'),
115
113
  sqlite3: require('sqlite3'),
116
114
  serialport: require('serialport'),
115
+ // Dangerous modules are opt-in only (see settings.nodeRedUnsafeModules)
116
+ ...(settings.nodeRedUnsafeModules ? {
117
+ child_process: require('child_process'),
118
+ net: require('net'),
119
+ } : {}),
117
120
  },
118
121
  };
119
122
 
120
123
  // Initialize Node-RED on the existing HTTP server (must be done before server.listen)
121
124
  RED.init(server, redSettings);
122
125
 
123
- // Allow dashboard UI, its admin APIs and socket.io without extra JWT; enforce auth for the rest
126
+ const getCookieValue = (req, name) => {
127
+ const cookieHeader = req.headers.cookie;
128
+ if (!cookieHeader) return null;
129
+ const cookies = cookieHeader.split(';');
130
+ for (const cookie of cookies) {
131
+ const [key, ...rest] = cookie.trim().split('=');
132
+ if (key === name) {
133
+ return rest.join('=');
134
+ }
135
+ }
136
+ return null;
137
+ };
138
+
139
+ const verifyApiKey = (runtimeRef, apiKey) => {
140
+ return runtimeRef.apiKeys.getApiKeys().then(stored => {
141
+ const now = Date.now();
142
+ return stored.find(k => {
143
+ if (!k || k.key !== apiKey || k.enabled === false) {
144
+ return false;
145
+ }
146
+ if (!k.expires) {
147
+ return true;
148
+ }
149
+ const expiresAt = new Date(k.expires).getTime();
150
+ return !isNaN(expiresAt) && expiresAt > now;
151
+ });
152
+ });
153
+ };
154
+
155
+ // Allow public dashboard UI and socket.io; require JWT or API key for admin/editor/flows when security is enabled
124
156
  const allowDashboard = (req, res, next) => {
125
- const url = req.originalUrl || req.url || req.path;
157
+ const url = req.originalUrl || req.url || req.path;
126
158
 
127
- // Public dashboard UI and its HTTP APIs (served from httpNodeRoot/ui.path)
128
- if (url.includes('/dashboard') || url.includes('/socket.io')) return next();
159
+ // Public dashboard UI and its HTTP APIs (served from httpNodeRoot/ui.path)
160
+ if (url.includes('/dashboard') || url.includes('/socket.io')) return next();
129
161
 
130
- // Node-RED dashboard admin APIs used by the layout editor under /nodered/dashboard/...
131
- if (url.startsWith('/nodered/dashboard/')) return next();
162
+ if (!settings.secureEnabled || settings.nodeRedAuthMode === 'legacy-open') {
163
+ return next();
164
+ }
132
165
 
133
- // Internal Node-RED flows APIs used by FlowFuse layout saves and deployments
134
- if (url === '/nodered/flows' || url === '/nodered/flows/') return next();
135
- if (url === '/nodered/flows/state' || url === '/nodered/flows/state/') return next();
136
- if (url === '/nodered/flows/deploy' || url === '/nodered/flows/deploy/') return next();
166
+ const apiKey = req.headers['x-api-key'];
167
+ if (apiKey) {
168
+ return verifyApiKey(runtime, apiKey)
169
+ .then(validKey => {
170
+ if (!validKey) {
171
+ return res.status(401).json({ error: "unauthorized_error", message: "Invalid API Key" });
172
+ }
173
+ return next();
174
+ })
175
+ .catch(err => {
176
+ logger.error(`api-key validation failed: ${err}`);
177
+ return res.status(500).json({ error: "unexpected_error", message: "ApiKey validation failed" });
178
+ });
179
+ }
137
180
 
138
- const referer = req.headers.referer;
139
- if (referer) {
140
- const ok = ['/editor', '/viewer', '/lab', '/home', '/fuxa', '/flows', '/nodered']
141
- .some(p => referer.includes(p));
142
- if (ok) return next();
143
- }
144
- return authJwt.requireAuth(req, res, next);
181
+ const headerToken = req.headers['x-access-token'];
182
+ const queryToken = req.query?.token;
183
+ const cookieToken = getCookieValue(req, 'nodered_auth');
184
+ // Prefer explicit tokens over cookie to avoid stale cookie blocking valid logins
185
+ const token = headerToken || queryToken || cookieToken;
186
+ if (!token) {
187
+ return res.status(401).json({ error: "unauthorized_error", message: "Authentication required!" });
188
+ }
189
+
190
+ return authJwt.verify(token)
191
+ .then(() => {
192
+ if (queryToken) {
193
+ res.cookie('nodered_auth', token, {
194
+ httpOnly: true,
195
+ sameSite: 'lax',
196
+ secure: !!settings.https,
197
+ });
198
+ if (req.method === 'GET') {
199
+ const cleanUrl = new URL(req.originalUrl, `http://${req.headers.host}`);
200
+ cleanUrl.searchParams.delete('token');
201
+ return res.redirect(cleanUrl.pathname + cleanUrl.search);
202
+ }
203
+ }
204
+ return next();
205
+ })
206
+ .catch(() => {
207
+ if (cookieToken) {
208
+ res.clearCookie('nodered_auth');
209
+ }
210
+ return res.status(401).json({ error: "unauthorized_error", message: "Invalid token!" });
211
+ });
145
212
  };
146
213
 
147
214
  // Mount Node-RED admin/editor under /nodered; HTTP nodes (including dashboard)
148
- // are served from httpNodeRoot ('/') so they appear at /dashboard/... etc.
215
+ // are served from httpNodeRoot ('/dashboard') so they appear at /dashboard/... etc.
149
216
  app.use('/nodered', allowDashboard, RED.httpAdmin);
150
- app.use('/', allowDashboard, RED.httpNode);
217
+ app.use('/dashboard', allowDashboard, RED.httpNode);
151
218
 
152
219
  await RED.start();
153
220
 
@@ -4,7 +4,8 @@
4
4
  color: '#a6bbcf',
5
5
  defaults: {
6
6
  name: {value:""},
7
- tag: {value:"", required:true},
7
+ tag: {value:""}, // Keep: tag name for display and backward compatibility
8
+ tagId: {value:""}, // New: unique identifier (optional, for backward compatibility)
8
9
  from: {value:""},
9
10
  to: {value:""}
10
11
  },
@@ -12,17 +13,65 @@
12
13
  outputs:1,
13
14
  icon: "white-globe.png",
14
15
  label: function() {
15
- return this.name||this.tag||"get daq";
16
+ if (this.name) {
17
+ return this.name;
18
+ }
19
+ // Display tag name or tagId
20
+ return this.tag || this.tagId || "get daq";
16
21
  },
17
22
  oneditprepare: function() {
23
+ var node = this;
24
+ var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
25
+
18
26
  $.getJSON('/nodered/fuxa/devices', function(data) {
19
27
  var datalist = $('#fuxa-tags-daq');
20
28
  datalist.empty();
29
+
21
30
  data.forEach(function(device) {
22
31
  device.tags.forEach(function(tag) {
23
- datalist.append('<option value="' + tag.name + '">' + device.name + ' - ' + tag.name + '</option>');
32
+ var fullName = device.name + ' - ' + tag.name;
33
+ // datalist option value format: tag.name(tag.id)
34
+ var optionValue = tag.name + '(' + tag.id + ')';
35
+ datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
36
+ tagMap[tag.id] = {
37
+ name: tag.name,
38
+ deviceName: device.name,
39
+ fullName: fullName
40
+ };
24
41
  });
25
42
  });
43
+
44
+ // Listen for tagSelected input changes
45
+ $('#node-input-tagSelected').on('change', function() {
46
+ var selectedValue = $(this).val();
47
+ // Parse format: tag.name(tag.id)
48
+ var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
49
+ if (match) {
50
+ var tagName = match[1];
51
+ var tagId = match[2];
52
+
53
+ // Set the actual stored fields
54
+ $('#node-input-tag').val(tagName);
55
+ $('#node-input-tagId').val(tagId);
56
+ }
57
+ });
58
+
59
+ // Initialize display (when editing existing node)
60
+ if (node.tagId && tagMap[node.tagId]) {
61
+ // Has tagId, construct tagSelected value
62
+ var tagName = node.tag || tagMap[node.tagId].name;
63
+ $('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
64
+ } else if (node.tag && !node.tagId) {
65
+ // Old node: only has tag (tag.name), need to find corresponding tagId
66
+ // Note: if there are duplicate names, only the first match will be found
67
+ for (var tagId in tagMap) {
68
+ if (tagMap[tagId].name === node.tag) {
69
+ $('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
70
+ $('#node-input-tagId').val(tagId);
71
+ break;
72
+ }
73
+ }
74
+ }
26
75
  });
27
76
 
28
77
  // Convert timestamp format for datetime-local inputs
@@ -64,10 +113,12 @@
64
113
  <input type="text" id="node-input-name" placeholder="Name">
65
114
  </div>
66
115
  <div class="form-row">
67
- <label for="node-input-tag"><i class="fa fa-tag"></i> Tag</label>
68
- <input type="text" id="node-input-tag" list="fuxa-tags-daq" placeholder="Tag Name">
116
+ <label for="node-input-tagSelected"><i class="fa fa-tag"></i> Tag</label>
117
+ <input type="text" id="node-input-tagSelected" list="fuxa-tags-daq" placeholder="Select Tag">
69
118
  <datalist id="fuxa-tags-daq"></datalist>
70
119
  </div>
120
+ <input type="hidden" id="node-input-tag">
121
+ <input type="hidden" id="node-input-tagId">
71
122
  <div class="form-row">
72
123
  <label for="node-input-from"><i class="fa fa-clock-o"></i> From (timestamp)</label>
73
124
  <input type="datetime-local" id="node-input-from" placeholder="From timestamp (optional)">
@@ -6,7 +6,13 @@ module.exports = function(RED) {
6
6
 
7
7
  this.on('input', async function(msg) {
8
8
  try {
9
- var tagId = fuxa.getTagId(config.tag, null);
9
+ // Prefer config.tagId, fallback to config.tag for backward compatibility
10
+ var tagId = config.tagId;
11
+ if (!tagId && config.tag) {
12
+ // Backward compatibility: old nodes use tag.name, need to convert to tagId
13
+ tagId = fuxa.getTagId(config.tag, null);
14
+ }
15
+
10
16
  if (tagId) {
11
17
  var fromts = config.from || msg.from || Date.now() - 3600000; // default 1 hour ago
12
18
  var tots = config.to || msg.to || Date.now();
@@ -14,7 +20,7 @@ module.exports = function(RED) {
14
20
  msg.payload = data;
15
21
  node.send(msg);
16
22
  } else {
17
- node.error('Tag not found: ' + config.tag, msg);
23
+ node.error('Tag not found: ' + (config.tag || config.tagId), msg);
18
24
  }
19
25
  } catch (err) {
20
26
  node.error(err, msg);
@@ -4,23 +4,72 @@
4
4
  color: '#a6bbcf',
5
5
  defaults: {
6
6
  name: {value:""},
7
- tag: {value:"", required:true}
7
+ tag: {value:""}, // Keep: tag name for display and backward compatibility
8
+ tagId: {value:""} // New: unique identifier (optional, for backward compatibility)
8
9
  },
9
10
  inputs:0,
10
11
  outputs:1,
11
12
  icon: "white-globe.png",
12
13
  label: function() {
13
- return this.name||this.tag||"get tag change";
14
+ if (this.name) {
15
+ return this.name;
16
+ }
17
+ // Display tag name or tagId
18
+ return this.tag || this.tagId || "get tag change";
14
19
  },
15
20
  oneditprepare: function() {
21
+ var node = this;
22
+ var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
23
+
16
24
  $.getJSON('/nodered/fuxa/devices', function(data) {
17
25
  var datalist = $('#fuxa-change-tags');
18
26
  datalist.empty();
27
+
19
28
  data.forEach(function(device) {
20
29
  device.tags.forEach(function(tag) {
21
- datalist.append('<option value="' + tag.name + '">' + device.name + ' - ' + tag.name + '</option>');
30
+ var fullName = device.name + ' - ' + tag.name;
31
+ // datalist option value format: tag.name(tag.id)
32
+ var optionValue = tag.name + '(' + tag.id + ')';
33
+ datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
34
+ tagMap[tag.id] = {
35
+ name: tag.name,
36
+ deviceName: device.name,
37
+ fullName: fullName
38
+ };
22
39
  });
23
40
  });
41
+
42
+ // Listen for tagSelected input changes
43
+ $('#node-input-tagSelected').on('change', function() {
44
+ var selectedValue = $(this).val();
45
+ // Parse format: tag.name(tag.id)
46
+ var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
47
+ if (match) {
48
+ var tagName = match[1];
49
+ var tagId = match[2];
50
+
51
+ // Set the actual stored fields
52
+ $('#node-input-tag').val(tagName);
53
+ $('#node-input-tagId').val(tagId);
54
+ }
55
+ });
56
+
57
+ // Initialize display (when editing existing node)
58
+ if (node.tagId && tagMap[node.tagId]) {
59
+ // Has tagId, construct tagSelected value
60
+ var tagName = node.tag || tagMap[node.tagId].name;
61
+ $('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
62
+ } else if (node.tag && !node.tagId) {
63
+ // Old node: only has tag (tag.name), need to find corresponding tagId
64
+ // Note: if there are duplicate names, only the first match will be found
65
+ for (var tagId in tagMap) {
66
+ if (tagMap[tagId].name === node.tag) {
67
+ $('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
68
+ $('#node-input-tagId').val(tagId);
69
+ break;
70
+ }
71
+ }
72
+ }
24
73
  });
25
74
  }
26
75
  });
@@ -32,10 +81,12 @@
32
81
  <input type="text" id="node-input-name" placeholder="Name">
33
82
  </div>
34
83
  <div class="form-row">
35
- <label for="node-input-tag"><i class="fa fa-tag"></i> Tag</label>
36
- <input type="text" id="node-input-tag" list="fuxa-change-tags" placeholder="Tag Name">
84
+ <label for="node-input-tagSelected"><i class="fa fa-tag"></i> Tag</label>
85
+ <input type="text" id="node-input-tagSelected" list="fuxa-change-tags" placeholder="Select Tag">
37
86
  <datalist id="fuxa-change-tags"></datalist>
38
87
  </div>
88
+ <input type="hidden" id="node-input-tag">
89
+ <input type="hidden" id="node-input-tagId">
39
90
  </script>
40
91
 
41
92
  <script type="text/x-red" data-help-name="get-tag-change">
@@ -7,26 +7,26 @@ module.exports = function(RED) {
7
7
  // Store previous values to detect actual changes
8
8
  var previousValues = {};
9
9
 
10
+ // Prefer config.tagId, fallback to config.tag for backward compatibility
11
+ var configuredTagId = config.tagId;
12
+ if (!configuredTagId && config.tag) {
13
+ // Backward compatibility: old nodes use tag.name, need to convert to tagId
14
+ configuredTagId = fuxa.getTagId(config.tag, null);
15
+ }
16
+
10
17
  // Initialize previous value for the configured tag
11
- if (config.tag) {
12
- var tagId = fuxa.getTagId(config.tag, null);
13
- if (tagId) {
14
- // Initialize previous value
15
- var initialValue = fuxa.getTag(tagId);
16
- if (initialValue !== undefined) {
17
- previousValues[tagId] = initialValue;
18
- }
18
+ if (configuredTagId) {
19
+ var initialValue = fuxa.getTag(configuredTagId);
20
+ if (initialValue !== undefined) {
21
+ previousValues[configuredTagId] = initialValue;
19
22
  }
20
23
  }
21
24
 
22
- // Event listener for device value changes (raw events from devices)
25
+ // Event listener for device value changes (raw events from devices)
23
26
  var deviceEventListener = function(deviceEvent) {
24
27
  try {
25
28
  // deviceEvent format: { id: deviceId, values: { tagId: tagObject, ... } }
26
29
  if (deviceEvent && deviceEvent.values) {
27
- // Check if any of the changed values match our configured tag
28
- var configuredTagId = fuxa.getTagId(config.tag, null);
29
-
30
30
  if (configuredTagId && deviceEvent.values[configuredTagId]) {
31
31
  var tagData = deviceEvent.values[configuredTagId];
32
32
 
@@ -4,23 +4,72 @@
4
4
  color: '#a6bbcf',
5
5
  defaults: {
6
6
  name: {value:""},
7
- tag: {value:"", required:true}
7
+ tag: {value:""}, // Keep: tag name for display and backward compatibility
8
+ tagId: {value:""} // New: unique identifier (optional, for backward compatibility)
8
9
  },
9
10
  inputs:1,
10
11
  outputs:1,
11
12
  icon: "white-globe.png",
12
13
  label: function() {
13
- return this.name||this.tag||"get tag daq settings";
14
+ if (this.name) {
15
+ return this.name;
16
+ }
17
+ // Display tag name or tagId
18
+ return this.tag || this.tagId || "get tag daq settings";
14
19
  },
15
20
  oneditprepare: function() {
21
+ var node = this;
22
+ var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
23
+
16
24
  $.getJSON('/nodered/fuxa/devices', function(data) {
17
25
  var datalist = $('#fuxa-tags-daq-settings');
18
26
  datalist.empty();
27
+
19
28
  data.forEach(function(device) {
20
29
  device.tags.forEach(function(tag) {
21
- datalist.append('<option value="' + tag.name + '">' + device.name + ' - ' + tag.name + '</option>');
30
+ var fullName = device.name + ' - ' + tag.name;
31
+ // datalist option value format: tag.name(tag.id)
32
+ var optionValue = tag.name + '(' + tag.id + ')';
33
+ datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
34
+ tagMap[tag.id] = {
35
+ name: tag.name,
36
+ deviceName: device.name,
37
+ fullName: fullName
38
+ };
22
39
  });
23
40
  });
41
+
42
+ // Listen for tagSelected input changes
43
+ $('#node-input-tagSelected').on('change', function() {
44
+ var selectedValue = $(this).val();
45
+ // Parse format: tag.name(tag.id)
46
+ var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
47
+ if (match) {
48
+ var tagName = match[1];
49
+ var tagId = match[2];
50
+
51
+ // Set the actual stored fields
52
+ $('#node-input-tag').val(tagName);
53
+ $('#node-input-tagId').val(tagId);
54
+ }
55
+ });
56
+
57
+ // Initialize display (when editing existing node)
58
+ if (node.tagId && tagMap[node.tagId]) {
59
+ // Has tagId, construct tagSelected value
60
+ var tagName = node.tag || tagMap[node.tagId].name;
61
+ $('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
62
+ } else if (node.tag && !node.tagId) {
63
+ // Old node: only has tag (tag.name), need to find corresponding tagId
64
+ // Note: if there are duplicate names, only the first match will be found
65
+ for (var tagId in tagMap) {
66
+ if (tagMap[tagId].name === node.tag) {
67
+ $('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
68
+ $('#node-input-tagId').val(tagId);
69
+ break;
70
+ }
71
+ }
72
+ }
24
73
  });
25
74
  }
26
75
  });
@@ -32,10 +81,12 @@
32
81
  <input type="text" id="node-input-name" placeholder="Name">
33
82
  </div>
34
83
  <div class="form-row">
35
- <label for="node-input-tag"><i class="fa fa-tag"></i> Tag</label>
36
- <input type="text" id="node-input-tag" list="fuxa-tags-daq-settings" placeholder="Tag Name">
84
+ <label for="node-input-tagSelected"><i class="fa fa-tag"></i> Tag</label>
85
+ <input type="text" id="node-input-tagSelected" list="fuxa-tags-daq-settings" placeholder="Select Tag">
37
86
  <datalist id="fuxa-tags-daq-settings"></datalist>
38
87
  </div>
88
+ <input type="hidden" id="node-input-tag">
89
+ <input type="hidden" id="node-input-tagId">
39
90
  </script>
40
91
 
41
92
  <script type="text/x-red" data-help-name="get-tag-daq-settings">
@@ -6,18 +6,22 @@ module.exports = function(RED) {
6
6
 
7
7
  this.on('input', async function(msg) {
8
8
  try {
9
- var tagName = config.tag || msg.tag;
10
- if (tagName) {
11
- var tagId = fuxa.getTagId(tagName, null);
12
- if (tagId) {
13
- var settings = await fuxa.getTagDaqSettings(tagId);
14
- msg.payload = settings;
15
- node.send(msg);
16
- } else {
17
- node.error('Tag not found: ' + tagName, msg);
9
+ // Prefer config.tagId, fallback to config.tag or msg.tag for backward compatibility
10
+ var tagId = config.tagId;
11
+ if (!tagId) {
12
+ var tagName = config.tag || msg.tag;
13
+ if (tagName) {
14
+ // Backward compatibility: old nodes use tag.name, need to convert to tagId
15
+ tagId = fuxa.getTagId(tagName, null);
18
16
  }
17
+ }
18
+
19
+ if (tagId) {
20
+ var settings = await fuxa.getTagDaqSettings(tagId);
21
+ msg.payload = settings;
22
+ node.send(msg);
19
23
  } else {
20
- node.error('Tag name not specified', msg);
24
+ node.error('Tag not found: ' + (config.tag || msg.tag || config.tagId), msg);
21
25
  }
22
26
  } catch (err) {
23
27
  node.error(err, msg);
@@ -4,23 +4,72 @@
4
4
  color: '#a6bbcf',
5
5
  defaults: {
6
6
  name: {value:""},
7
- tag: {value:"", required:true}
7
+ tag: {value:""}, // Keep: tag name for display and backward compatibility
8
+ tagId: {value:""} // New: unique identifier (optional, for backward compatibility)
8
9
  },
9
10
  inputs:1,
10
11
  outputs:1,
11
12
  icon: "white-globe.png",
12
13
  label: function() {
13
- return this.name||this.tag||"get tag";
14
+ if (this.name) {
15
+ return this.name;
16
+ }
17
+ // Display tag name or tagId
18
+ return this.tag || this.tagId || "get tag";
14
19
  },
15
20
  oneditprepare: function() {
21
+ var node = this;
22
+ var tagMap = {}; // Use tag.id as key to avoid tag.name duplication issues
23
+
16
24
  $.getJSON('/nodered/fuxa/devices', function(data) {
17
25
  var datalist = $('#fuxa-tags');
18
26
  datalist.empty();
27
+
19
28
  data.forEach(function(device) {
20
29
  device.tags.forEach(function(tag) {
21
- datalist.append('<option value="' + tag.name + '">' + device.name + ' - ' + tag.name + '</option>');
30
+ var fullName = device.name + ' - ' + tag.name;
31
+ // datalist option value format: tag.name(tag.id)
32
+ var optionValue = tag.name + '(' + tag.id + ')';
33
+ datalist.append('<option value="' + optionValue + '">' + fullName + '</option>');
34
+ tagMap[tag.id] = {
35
+ name: tag.name,
36
+ deviceName: device.name,
37
+ fullName: fullName
38
+ };
22
39
  });
23
40
  });
41
+
42
+ // Listen for tagSelected input changes
43
+ $('#node-input-tagSelected').on('change', function() {
44
+ var selectedValue = $(this).val();
45
+ // Parse format: tag.name(tag.id)
46
+ var match = selectedValue.match(/^(.+)\(([^)]+)\)$/);
47
+ if (match) {
48
+ var tagName = match[1];
49
+ var tagId = match[2];
50
+
51
+ // Set the actual stored fields
52
+ $('#node-input-tag').val(tagName);
53
+ $('#node-input-tagId').val(tagId);
54
+ }
55
+ });
56
+
57
+ // Initialize display (when editing existing node)
58
+ if (node.tagId && tagMap[node.tagId]) {
59
+ // Has tagId, construct tagSelected value
60
+ var tagName = node.tag || tagMap[node.tagId].name;
61
+ $('#node-input-tagSelected').val(tagName + '(' + node.tagId + ')');
62
+ } else if (node.tag && !node.tagId) {
63
+ // Old node: only has tag (tag.name), need to find corresponding tagId
64
+ // Note: if there are duplicate names, only the first match will be found
65
+ for (var tagId in tagMap) {
66
+ if (tagMap[tagId].name === node.tag) {
67
+ $('#node-input-tagSelected').val(node.tag + '(' + tagId + ')');
68
+ $('#node-input-tagId').val(tagId);
69
+ break;
70
+ }
71
+ }
72
+ }
24
73
  });
25
74
  }
26
75
  });
@@ -32,10 +81,12 @@
32
81
  <input type="text" id="node-input-name" placeholder="Name">
33
82
  </div>
34
83
  <div class="form-row">
35
- <label for="node-input-tag"><i class="fa fa-tag"></i> Tag</label>
36
- <input type="text" id="node-input-tag" list="fuxa-tags" placeholder="Tag Name">
84
+ <label for="node-input-tagSelected"><i class="fa fa-tag"></i> Tag</label>
85
+ <input type="text" id="node-input-tagSelected" list="fuxa-tags" placeholder="Select Tag">
37
86
  <datalist id="fuxa-tags"></datalist>
38
87
  </div>
88
+ <input type="hidden" id="node-input-tag">
89
+ <input type="hidden" id="node-input-tagId">
39
90
  </script>
40
91
 
41
92
  <script type="text/x-red" data-help-name="get-tag">
@@ -7,14 +7,20 @@ module.exports = function(RED) {
7
7
 
8
8
  this.on('input', function(msg) {
9
9
  try {
10
- var tagId = fuxa.getTagId(config.tag, null);
10
+ // Prefer config.tagId, fallback to config.tag for backward compatibility
11
+ var tagId = config.tagId;
12
+ if (!tagId && config.tag) {
13
+ // Backward compatibility: old nodes use tag.name, need to convert to tagId
14
+ tagId = fuxa.getTagId(config.tag, null);
15
+ }
16
+
11
17
  if (tagId) {
12
18
  var value = fuxa.getTag(tagId);
13
19
  msg.payload = value;
14
20
  msg.topic = config.tag; // Set topic to tag name for join operations
15
21
  node.send(msg);
16
22
  } else {
17
- node.error('Tag not found: ' + config.tag, msg);
23
+ node.error('Tag not found: ' + (config.tag || config.tagId), msg);
18
24
  }
19
25
  } catch (err) {
20
26
  node.error(err, msg);