@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
@@ -24,7 +24,7 @@ function ODBCclient(_data, _logger, _events) {
24
24
  * initialize the device type
25
25
  */
26
26
  this.init = function (_type) {
27
- console.error('Not supported!');
27
+ console.error('Not supported! (odbc.init)');
28
28
  }
29
29
 
30
30
  /**
@@ -165,14 +165,14 @@ function ODBCclient(_data, _logger, _events) {
165
165
  * Return Tags values array { id: <name>, value: <value> }
166
166
  */
167
167
  this.getValues = function () {
168
- console.error('Not supported!');
168
+ console.error('Not supported! (odbc.getValues)');
169
169
  }
170
170
 
171
171
  /**
172
172
  * Return Tag value { id: <name>, value: <value>, ts: <lastTimestampValue> }
173
173
  */
174
174
  this.getValue = function (tagid) {
175
- console.error('Not supported!');
175
+ console.error(`Not supported! (odbc.getValue) (TagID: ${tagid})`);
176
176
  }
177
177
 
178
178
  /**
@@ -186,14 +186,14 @@ function ODBCclient(_data, _logger, _events) {
186
186
  * Return Tag property to show in frontend
187
187
  */
188
188
  this.getTagProperty = function (tagid) {
189
- console.error('Not supported!');
189
+ console.error(`Not supported! (odbc.getTagProperty) (TagID: ${tagid})`);
190
190
  }
191
191
 
192
192
  /**
193
193
  * Set the Tag value to device
194
194
  */
195
195
  this.setValue = function (tagid, value) {
196
- console.error('Not supported!');
196
+ console.error(`Not supported! (odbc.setValue) (TagID: ${tagid}, Value: ${value})`);
197
197
  }
198
198
 
199
199
  /**
@@ -14,7 +14,7 @@ function DeviceTemplate(_data, _logger, _events) {
14
14
  * initialize the device type
15
15
  */
16
16
  this.init = function (_type) {
17
- console.error('Not supported!');
17
+ console.error('Not supported! (template.init)');
18
18
  }
19
19
 
20
20
  /**
@@ -22,7 +22,7 @@ function DeviceTemplate(_data, _logger, _events) {
22
22
  * Emit connection status to clients, clear all Tags values
23
23
  */
24
24
  this.connect = function () {
25
- console.error('Not supported!');
25
+ console.error('Not supported! (template.connect)');
26
26
  // events.emit('device-status:changed', { id: data.id, status: 'connect-ok' });
27
27
  // events.emit('device-status:changed', { id: data.id, status: 'connect-error' });
28
28
  }
@@ -33,7 +33,7 @@ function DeviceTemplate(_data, _logger, _events) {
33
33
  * Emit connection status to clients, clear all Tags values
34
34
  */
35
35
  this.disconnect = function () {
36
- console.error('Not supported!');
36
+ console.error('Not supported! (template.disconnect)');
37
37
  // events.emit('device-status:changed', { id: data.id, status: 'connect-off' });
38
38
  }
39
39
 
@@ -42,7 +42,7 @@ function DeviceTemplate(_data, _logger, _events) {
42
42
  * Update the tags values list, save in DAQ if value changed or in interval and emit values to clients
43
43
  */
44
44
  this.polling = async function () {
45
- console.error('Not supported!');
45
+ console.error('Not supported! (template.polling)');
46
46
  // events.emit('device-value:changed', { id: data.name, values: values });
47
47
  }
48
48
 
@@ -50,49 +50,49 @@ function DeviceTemplate(_data, _logger, _events) {
50
50
  * Load Tags attribute to read with polling
51
51
  */
52
52
  this.load = function (_data) {
53
- console.error('Not supported!');
53
+ console.error(`Not supported! (template.load) (Data: ${_data})`);
54
54
  }
55
55
 
56
56
  /**
57
57
  * Return Tags values array { id: <name>, value: <value> }
58
58
  */
59
59
  this.getValues = function () {
60
- console.error('Not supported!');
60
+ console.error('Not supported! (template.getValues)');
61
61
  }
62
62
 
63
63
  /**
64
64
  * Return Tag value { id: <name>, value: <value>, ts: <lastTimestampValue> }
65
65
  */
66
66
  this.getValue = function (tagid) {
67
- console.error('Not supported!');
67
+ console.error(`Not supported! (template.getValue) (TagID: ${tagid})`);
68
68
  }
69
69
 
70
70
  /**
71
71
  * Return connection status 'connect-off', 'connect-ok', 'connect-error', 'connect-busy'
72
72
  */
73
73
  this.getStatus = function () {
74
- console.error('Not supported!');
74
+ console.error('Not supported! (template.getStatus)');
75
75
  }
76
76
 
77
77
  /**
78
78
  * Return Tag property to show in frontend
79
79
  */
80
80
  this.getTagProperty = function (tagid) {
81
- console.error('Not supported!');
81
+ console.error(`Not supported! (template.getTagProperty) (TagID: ${tagid})`);
82
82
  }
83
83
 
84
84
  /**
85
85
  * Set the Tag value to device
86
86
  */
87
87
  this.setValue = function (tagid, value) {
88
- console.error('Not supported!');
88
+ console.error(`Not supported! (template.setValue) (TagID: ${tagid}, Value: ${value})`);
89
89
  }
90
90
 
91
91
  /**
92
92
  * Return if device is connected
93
93
  */
94
94
  this.isConnected = function () {
95
- console.error('Not supported!');
95
+ console.error('Not supported! (template.isConnected)');
96
96
  }
97
97
 
98
98
  /**
@@ -107,7 +107,7 @@ function DeviceTemplate(_data, _logger, _events) {
107
107
  * @returns
108
108
  */
109
109
  this.lastReadTimestamp = () => {
110
- console.error('Not supported!');
110
+ console.error('Not supported! (template.lastReadTimestamp)');
111
111
  }
112
112
 
113
113
  /**
@@ -115,7 +115,7 @@ function DeviceTemplate(_data, _logger, _events) {
115
115
  * @returns
116
116
  */
117
117
  this.getTagDaqSettings = (tagId) => {
118
- console.error('Not supported!');
118
+ console.error(`Not supported! (template.getTagDaqSettings) (TagID: ${tagId})`);
119
119
  }
120
120
 
121
121
  /**
@@ -123,7 +123,7 @@ function DeviceTemplate(_data, _logger, _events) {
123
123
  * @returns
124
124
  */
125
125
  this.setTagDaqSettings = (tagId, settings) => {
126
- console.error('Not supported!');
126
+ console.error(`Not supported! (template.setTagDaqSettings) (TagID: ${tagId}, Settings: ${settings})`);
127
127
  }
128
128
  }
129
129
 
@@ -21,6 +21,7 @@ var logger;
21
21
  var daqDB = {}; // list of daqDB node: SQlite one pro device, influxDB only one
22
22
  var currentStorateDB;
23
23
  var runtime;
24
+ var questDbModule = null;
24
25
 
25
26
  function init(_settings, _log, _runtime) {
26
27
  settings = _settings;
@@ -42,7 +43,7 @@ function reset() {
42
43
  function addDaqNode(_id, fncgetprop) {
43
44
  var id = _id;
44
45
  const dbType = _getDbType();
45
- if (dbType === DaqStoreTypeEnum.influxDB || dbType === DaqStoreTypeEnum.influxDB18 || dbType === DaqStoreTypeEnum.TDengine) {
46
+ if (dbType === DaqStoreTypeEnum.influxDB || dbType === DaqStoreTypeEnum.influxDB18 || dbType === DaqStoreTypeEnum.TDengine || dbType === DaqStoreTypeEnum.questDB) {
46
47
  id = dbType;
47
48
  }
48
49
  if (!daqDB[id]) {
@@ -50,6 +51,14 @@ function addDaqNode(_id, fncgetprop) {
50
51
  daqDB[id] = InfluxDB.create(settings, logger, currentStorateDB);
51
52
  } else if(id === DaqStoreTypeEnum.TDengine){
52
53
  daqDB[id] = TDengine.create(settings, logger, currentStorateDB);
54
+ } else if(id === DaqStoreTypeEnum.questDB){
55
+ const QuestDB = _getQuestDbModule();
56
+ if (QuestDB) {
57
+ daqDB[id] = QuestDB.create(settings, logger, currentStorateDB);
58
+ } else {
59
+ logger.warn('daqstorage: QuestDB storage selected but package dependencies are not installed. Falling back to SQLite.');
60
+ daqDB[id] = SqliteDB.create(settings, logger, id, currentStorateDB);
61
+ }
53
62
  } else {
54
63
  daqDB[id] = SqliteDB.create(settings, logger, id, currentStorateDB);
55
64
  }
@@ -160,16 +169,33 @@ function _getDaqNode(tagid) {
160
169
 
161
170
  function _getDbType() {
162
171
  if (settings.daqstore && settings.daqstore.type) {
172
+ if (settings.daqstore.type === 'QuestDB') {
173
+ return DaqStoreTypeEnum.questDB;
174
+ }
163
175
  return settings.daqstore.type;
164
176
  }
165
177
  return DaqStoreTypeEnum.SQlite;
166
178
  }
167
179
 
180
+ function _getQuestDbModule() {
181
+ if (questDbModule) {
182
+ return questDbModule;
183
+ }
184
+ try {
185
+ questDbModule = require("./questdb");
186
+ } catch (err) {
187
+ questDbModule = null;
188
+ logger.warn(`daqstorage: QuestDB module unavailable (${err.message})`);
189
+ }
190
+ return questDbModule;
191
+ }
192
+
168
193
  var DaqStoreTypeEnum = {
169
194
  SQlite: 'SQlite',
170
195
  influxDB: 'influxDB',
171
196
  influxDB18: 'influxDB18',
172
197
  TDengine: 'TDengine',
198
+ questDB: 'questDB',
173
199
  }
174
200
 
175
201
  function _getValue(value) {
@@ -187,4 +213,4 @@ module.exports = {
187
213
  getNodesValues: getNodesValues,
188
214
  checkRetention: checkRetention,
189
215
  getCurrentStorageFnc: getCurrentStorageFnc,
190
- };
216
+ };
@@ -123,7 +123,7 @@ function Influx(_settings, _log, _currentStorate) {
123
123
  } else {
124
124
  const point = new Point(tag.id)
125
125
  .tag('id', tag.id)
126
- .tag('name', tag.name || tag.tagref.name)
126
+ .tag('name', tag.name || tag.tagref?.name)
127
127
  .tag('type', tag.type)
128
128
  .timestamp(new Date(tag.timestamp || new Date().getTime()));
129
129
  if (deviceName) {
@@ -0,0 +1,224 @@
1
+ 'use strict'
2
+
3
+ const { Pool } = require('pg');
4
+ const { Sender } = require('@questdb/nodejs-client');
5
+ let utils = require('../../utils');
6
+
7
+ function QuestDB(_settings, _log, _currentStorage) {
8
+ let settings = _settings; // Application settings
9
+ const logger = _log; // Application logger
10
+ const currentStorage = _currentStorage; // Database to set the last value (current)
11
+ let pool = null;
12
+ let sender = null;
13
+ const tableName = getTableName();
14
+ let writeQueue = Promise.resolve();
15
+ let initPromise = Promise.resolve();
16
+
17
+ this.setCall = function (_fncGetProp) {
18
+ fncGetTagProp = _fncGetProp;
19
+ return this.addDaqValues;
20
+ }
21
+ var fncGetTagProp = null;
22
+
23
+ this.init = async function () {
24
+ try {
25
+ pool = new Pool(getQueryClientConfig());
26
+ sender = await Sender.fromConfig(getIngestConfigString());
27
+ await ensureSchema();
28
+ logger.info('QuestDB connected');
29
+ } catch (error) {
30
+ logger.error(`questdb-init failed! ${error}`);
31
+ }
32
+ }
33
+
34
+ this.addDaqValues = function (tagsValues, deviceName, deviceId) {
35
+ var dataToRestore = [];
36
+ var rowsToWrite = [];
37
+
38
+ for (const tagid in tagsValues) {
39
+ let tag = tagsValues[tagid];
40
+ if (!tag.daq || utils.isNullOrUndefined(tag.value) || Number.isNaN(tag.value)) {
41
+ if (tag.daq && tag.daq.restored) {
42
+ dataToRestore.push({ id: tag.id, deviceId: deviceId, value: tag.value });
43
+ }
44
+ if (tag.daq && !tag.daq.enabled) {
45
+ continue;
46
+ }
47
+ }
48
+
49
+ rowsToWrite.push({
50
+ tagid,
51
+ deviceId,
52
+ deviceName: deviceName || '',
53
+ unsPath: normalizeUnsPath(tag.unsPath),
54
+ value: tag.value,
55
+ timestamp: tag.timestamp || Date.now(),
56
+ });
57
+ }
58
+
59
+ if (rowsToWrite.length) {
60
+ writeQueue = writeQueue.then(async () => {
61
+ await initPromise;
62
+ if (!sender) {
63
+ return;
64
+ }
65
+
66
+ for (const row of rowsToWrite) {
67
+ const parsedValue = normalizeValue(row.value);
68
+ let line = sender
69
+ .table(tableName)
70
+ .symbol('tag_id', row.tagid)
71
+ .symbol('device_id', row.deviceId)
72
+ .stringColumn('device_name', row.deviceName);
73
+ if (!utils.isNullOrUndefined(row.unsPath)) {
74
+ line = line.stringColumn('uns_path', row.unsPath);
75
+ }
76
+
77
+ if (!utils.isNullOrUndefined(parsedValue.numberValue)) {
78
+ line = line.floatColumn('number_value', parsedValue.numberValue);
79
+ }
80
+ if (!utils.isNullOrUndefined(parsedValue.stringValue)) {
81
+ line = line.stringColumn('string_value', parsedValue.stringValue);
82
+ }
83
+ await line.at(Number(row.timestamp), 'ms');
84
+ }
85
+ await sender.flush();
86
+ }).catch((error) => {
87
+ logger.error(`questdb-addDaqValues failed! ${error}`);
88
+ });
89
+ }
90
+
91
+ if (dataToRestore.length && currentStorage) {
92
+ currentStorage.setValues(dataToRestore);
93
+ }
94
+ }
95
+
96
+ this.getDaqValue = function (tagid, fromts, tots) {
97
+ return new Promise(function (resolve, reject) {
98
+ initPromise.then(() => {
99
+ if (!pool) {
100
+ resolve([]);
101
+ return;
102
+ }
103
+
104
+ const query = `SELECT timestamp, number_value, string_value
105
+ FROM ${tableName}
106
+ WHERE tag_id = $1
107
+ AND timestamp >= $2
108
+ AND timestamp < $3
109
+ ORDER BY timestamp`;
110
+ const params = [tagid, new Date(fromts), new Date(tots)];
111
+
112
+ pool.query(query, params)
113
+ .then((result) => {
114
+ let data = [];
115
+ result.rows.forEach((row) => {
116
+ const value = !utils.isNullOrUndefined(row.number_value) ? Number(row.number_value) : row.string_value;
117
+ data.push({ dt: new Date(row.timestamp).getTime(), value });
118
+ });
119
+ resolve(data)
120
+ })
121
+ .catch((error) => {
122
+ logger.error(`questdb-getDaqValue failed! ${error}`)
123
+ reject(error)
124
+ })
125
+ }).catch((error) => {
126
+ logger.error(`questdb-getDaqValue failed! ${error}`)
127
+ reject(error)
128
+ });
129
+ })
130
+ }
131
+
132
+ this.close = function () {
133
+ if (sender) {
134
+ sender.close().catch((error) => {
135
+ logger.error(`questdb-close sender failed! ${error}`);
136
+ });
137
+ sender = null;
138
+ }
139
+ if (pool) {
140
+ pool.end().catch((error) => {
141
+ logger.error(`questdb-close pool failed! ${error}`);
142
+ });
143
+ pool = null;
144
+ }
145
+ }
146
+
147
+ this.getDaqMap = function (tagid) {
148
+ var dummy = {};
149
+ dummy[tagid] = true;
150
+ return dummy;
151
+ }
152
+
153
+ async function ensureSchema() {
154
+ if (!pool) {
155
+ return;
156
+ }
157
+ await pool.query(`CREATE TABLE IF NOT EXISTS ${tableName} (
158
+ timestamp TIMESTAMP,
159
+ tag_id SYMBOL,
160
+ number_value FLOAT,
161
+ string_value STRING,
162
+ device_id SYMBOL,
163
+ device_name STRING,
164
+ uns_path SYMBOL
165
+ ) TIMESTAMP(timestamp) PARTITION BY DAY`);
166
+ }
167
+
168
+ function getIngestConfigString() {
169
+ return settings.daqstore.configurationString || 'http::addr=localhost:9000;';
170
+ }
171
+
172
+ function getQueryClientConfig() {
173
+ return {
174
+ host: settings.daqstore.host || '127.0.0.1',
175
+ port: 8812, // Standard port
176
+ database: 'qdb', // Standard database
177
+ user: settings.daqstore.credentials?.username || 'admin',
178
+ password: settings.daqstore.credentials?.password || 'quest',
179
+ max: 10, // Pool with 10 connections
180
+ idleTimeoutMillis: 30000, // 30s
181
+ };
182
+ }
183
+
184
+ function getTableName() {
185
+ const name = (settings.daqstore.tableName || 'meters').trim();
186
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
187
+ return name.toLowerCase();
188
+ }
189
+ logger.warn(`questdb invalid tableName "${name}", fallback to meters`);
190
+ return 'meters';
191
+ }
192
+
193
+ function normalizeValue(value) {
194
+ if (utils.isNullOrUndefined(value)) {
195
+ return { numberValue: null, stringValue: null };
196
+ }
197
+ if (utils.isBoolean(value)) {
198
+ return {
199
+ numberValue: value ? 1 : 0,
200
+ stringValue: null
201
+ };
202
+ }
203
+ if (typeof value === 'number' && Number.isFinite(value)) {
204
+ return { numberValue: value, stringValue: null };
205
+ }
206
+ return { numberValue: null, stringValue: String(value) };
207
+ }
208
+
209
+ function normalizeUnsPath(value) {
210
+ if (utils.isNullOrUndefined(value)) {
211
+ return null;
212
+ }
213
+ const normalized = String(value).trim();
214
+ return normalized.length ? normalized : null;
215
+ }
216
+
217
+ initPromise = this.init();
218
+ }
219
+
220
+ module.exports = {
221
+ create: function (data, logger, currentStorage) {
222
+ return new QuestDB(data, logger, currentStorage);
223
+ }
224
+ };
package/runtime/utils.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const os = require('os');
2
2
  const ip = require('ip');
3
+ const crypto = require('crypto');
3
4
 
4
5
  'use strict';
5
6
  var utils = module.exports = {
@@ -361,5 +362,9 @@ var utils = module.exports = {
361
362
  }
362
363
  }
363
364
  return target;
365
+ },
366
+
367
+ generateSecretCode: function(byteLength = 32) {
368
+ return crypto.randomBytes(byteLength).toString('hex');
364
369
  }
365
370
  }
@@ -24,6 +24,7 @@ module.exports = {
24
24
  // - 'common': Less detailed than 'combined', omitting the referrer and user-agent.
25
25
  // - 'short': Shorter format that includes the remote address and request details.
26
26
  // - 'tiny': Minimalist format, showing just the method, URL, status, response length, and response time.
27
+ // - 'none': Completely disables HTTP request logging to clean the console for custom debugging scripts and reduce I/O overhead.
27
28
  //
28
29
  // Default Value:
29
30
  // - 'combined': By default, logApiLevel is set to 'combined', providing detailed logs suitable for thorough tracking and analysis.
@@ -91,8 +92,10 @@ module.exports = {
91
92
 
92
93
  // Used to enable security, authentication and authorization and crypt Token
93
94
  //secureEnabled: true,
94
- //secretCode: 'frangoteam751',
95
- //tokenExpiresIn: '1h' // '1h'=1hour, 60=60seconds, '1d'=1day
95
+ //secretCode: '<set-a-strong-random-secret>',
96
+ //tokenExpiresIn: '1h', // '1h'=1hour, 60=60seconds, '1d'=1day
97
+ //enableRefreshCookieAuth: false, // if true, use refresh token HttpOnly cookie flow
98
+ //refreshTokenExpiresIn: '7d' // '7d'=7days, 12h=12hours, 3600=3600seconds
96
99
 
97
100
  // Heartbeat interval in seconds (1-20)
98
101
  heartbeatIntervalSec: 10,
@@ -110,4 +113,11 @@ module.exports = {
110
113
  swaggerEnabled: false,
111
114
 
112
115
  nodeRedEnabled: false,
113
- }
116
+
117
+ // Node-RED access mode: "secure" (auth required) or "legacy-open" (no auth)
118
+ nodeRedAuthMode: "secure",
119
+
120
+ // Node-RED: allow unsafe stdlib modules in functionGlobalContext
121
+ // WARNING: Enabling this exposes modules like child_process/net to flows.
122
+ nodeRedUnsafeModules: false,
123
+ }