@processlink/node-red-contrib-processlink 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.4] - 2025-02-05
9
+
10
+ ### Added
11
+
12
+ - Timestamp prefix option: automatically prefix filenames with `YYYY-MM-DD_HH-MM-SS_`
13
+ - Improved filename documentation explaining how it appears in Process Link Files
14
+
15
+ ## [1.0.3] - 2025-02-05
16
+
17
+ ### Added
18
+
19
+ - Importable JSON example flow in README for easy customer onboarding
20
+
8
21
  ## [1.0.2] - 2025-02-05
9
22
 
10
23
  ### Changed
package/README.md CHANGED
@@ -1,9 +1,3 @@
1
- <p align="center">
2
- <a href="https://processlink.com.au">
3
- <img src="processlink-logo.png" alt="Process Link" width="80">
4
- </a>
5
- </p>
6
-
7
1
  <p align="center">
8
2
  <a href="https://processlink.com.au">
9
3
  <img src="processlink-banner.png" alt="Process Link" height="50">
@@ -116,17 +110,79 @@ Uploads files to the Process Link Files API.
116
110
  | 🟢 Green | Upload successful |
117
111
  | 🔴 Red | Error occurred |
118
112
 
119
- ## Examples
120
-
121
- ### Basic File Upload
122
-
123
- ```
124
- [File In] → [files upload] → [Debug]
113
+ ## Example Flow (Copy & Import)
114
+
115
+ Copy the JSON below and import it into Node-RED: **Menu → Import → Clipboard**
116
+
117
+ ```json
118
+ [
119
+ {
120
+ "id": "pl-inject",
121
+ "type": "inject",
122
+ "z": "",
123
+ "name": "Upload File",
124
+ "props": [],
125
+ "repeat": "",
126
+ "crontab": "",
127
+ "once": false,
128
+ "onceDelay": 0.1,
129
+ "topic": "",
130
+ "x": 110,
131
+ "y": 100,
132
+ "wires": [["pl-file-in"]]
133
+ },
134
+ {
135
+ "id": "pl-file-in",
136
+ "type": "file in",
137
+ "z": "",
138
+ "name": "Read File",
139
+ "filename": "/tmp/myfile.csv",
140
+ "filenameType": "str",
141
+ "format": "",
142
+ "chunk": false,
143
+ "sendError": false,
144
+ "encoding": "none",
145
+ "allProps": true,
146
+ "x": 270,
147
+ "y": 100,
148
+ "wires": [["pl-upload"]]
149
+ },
150
+ {
151
+ "id": "pl-upload",
152
+ "type": "processlink-files-upload",
153
+ "z": "",
154
+ "name": "Upload to Process Link",
155
+ "server": "",
156
+ "filename": "",
157
+ "timeout": "30000",
158
+ "apiUrl": "https://files.processlink.com.au/api/upload",
159
+ "x": 470,
160
+ "y": 100,
161
+ "wires": [["pl-debug"]]
162
+ },
163
+ {
164
+ "id": "pl-debug",
165
+ "type": "debug",
166
+ "z": "",
167
+ "name": "Result",
168
+ "active": true,
169
+ "tosidebar": true,
170
+ "console": false,
171
+ "tostatus": false,
172
+ "complete": "true",
173
+ "targetType": "full",
174
+ "x": 650,
175
+ "y": 100,
176
+ "wires": []
177
+ }
178
+ ]
125
179
  ```
126
180
 
127
- 1. Configure a **File In** node to read your file
128
- 2. Connect it to the **files upload** node
129
- 3. Add a **Debug** node to see the response
181
+ **After importing:**
182
+ 1. Double-click the **Read File** node → change the file path to your file
183
+ 2. Double-click the **Upload to Process Link** node click the pencil icon → enter your **Site ID** and **API Key**
184
+ 3. Click **Deploy**
185
+ 4. Click the inject button to upload
130
186
 
131
187
  ### Dynamic Filename
132
188
 
@@ -190,4 +246,4 @@ Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md)
190
246
 
191
247
  ## License
192
248
 
193
- [MIT](LICENSE) © [Process Link](https://processlink.com.au)
249
+ [MIT](LICENSE)
@@ -50,7 +50,7 @@
50
50
  <dt>Name <span class="property-type">string</span></dt>
51
51
  <dd>Optional friendly name for this configuration.</dd>
52
52
  <dt>Site ID <span class="property-type">string</span></dt>
53
- <dd>The UUID of your site from Process Link. Found in your site settings.</dd>
53
+ <dd>The UUID of your site from Process Link. Found in your organisation settings under Developer.</dd>
54
54
  <dt>API Key <span class="property-type">string</span></dt>
55
55
  <dd>
56
56
  The API key for authentication. Generate one from Developer → API Keys in the Process Link Portal.
@@ -6,11 +6,12 @@
6
6
  name: { value: "" },
7
7
  server: { value: "", type: "processlink-config", required: true },
8
8
  filename: { value: "" },
9
+ timestampPrefix: { value: false },
9
10
  timeout: { value: "30000" },
10
11
  apiUrl: { value: "https://files.processlink.com.au/api/upload" },
11
12
  },
12
13
  inputs: 1,
13
- outputs: 1,
14
+ outputs: 2,
14
15
  icon: "processlink.png",
15
16
  paletteLabel: "files upload",
16
17
  label: function () {
@@ -20,7 +21,7 @@
20
21
  return this.name ? "node_label_italic" : "";
21
22
  },
22
23
  inputLabels: "file buffer",
23
- outputLabels: "response",
24
+ outputLabels: ["success", "error"],
24
25
  });
25
26
  </script>
26
27
 
@@ -37,6 +38,11 @@
37
38
  <label for="node-input-filename"><i class="fa fa-file"></i> Filename</label>
38
39
  <input type="text" id="node-input-filename" placeholder="msg.filename (or specify default)" />
39
40
  </div>
41
+ <div class="form-row">
42
+ <label for="node-input-timestampPrefix">&nbsp;</label>
43
+ <input type="checkbox" id="node-input-timestampPrefix" style="width: auto; margin-right: 10px;">
44
+ <span>Prefix filename with timestamp (YYYY-MM-DD_HH-MM-SS_)</span>
45
+ </div>
40
46
  <div class="form-row">
41
47
  <label for="node-input-timeout"><i class="fa fa-clock-o"></i> Timeout</label>
42
48
  <input type="text" id="node-input-timeout" placeholder="30000" />
@@ -56,58 +62,60 @@
56
62
  <dt>payload <span class="property-type">buffer | string</span></dt>
57
63
  <dd>The file content to upload.</dd>
58
64
  <dt class="optional">filename <span class="property-type">string</span></dt>
59
- <dd>The filename to use. Defaults to the configured filename or "file.bin".</dd>
65
+ <dd>Filename for the upload. Can be set via <code>msg.filename</code> or in the node config. Defaults to "file.bin".</dd>
60
66
  </dl>
61
67
 
62
68
  <h3>Outputs</h3>
69
+ <p>This node has two outputs:</p>
70
+ <ol>
71
+ <li><strong>Success</strong> - Upload completed successfully (HTTP 201)</li>
72
+ <li><strong>Error</strong> - Upload failed (network error, API error, or timeout)</li>
73
+ </ol>
74
+
75
+ <h4>Success Output</h4>
63
76
  <dl class="message-properties">
64
- <dt>payload <span class="property-type">object</span></dt>
65
- <dd>The API response containing <code>ok</code>, <code>file_id</code>, and <code>created_at</code>.</dd>
77
+ <dt>payload.ok <span class="property-type">boolean</span></dt>
78
+ <dd><code>true</code></dd>
79
+ <dt>payload.file_id <span class="property-type">string</span></dt>
80
+ <dd>UUID of the uploaded file.</dd>
66
81
  <dt>file_id <span class="property-type">string</span></dt>
67
- <dd>The UUID of the uploaded file (convenience property).</dd>
82
+ <dd>Same as above (convenience property).</dd>
68
83
  <dt>statusCode <span class="property-type">number</span></dt>
69
- <dd>The HTTP status code (201 on success).</dd>
84
+ <dd>201</dd>
70
85
  </dl>
71
86
 
72
- <h3>Details</h3>
73
- <p>
74
- This node uploads files to your Process Link site. The file content should be passed in
75
- <code>msg.payload</code> as a Buffer (from a file-in node) or as a string.
76
- </p>
77
- <p>
78
- The filename can be set in <code>msg.filename</code> or configured in the node properties. If
79
- the filename contains a path, only the basename will be used.
80
- </p>
87
+ <h4>Error Output</h4>
88
+ <dl class="message-properties">
89
+ <dt>payload.error <span class="property-type">string</span></dt>
90
+ <dd>Error message from the API or network layer.</dd>
91
+ <dt>statusCode <span class="property-type">number</span></dt>
92
+ <dd>HTTP status code, or 0 for network/timeout errors.</dd>
93
+ </dl>
94
+
95
+ <h3>Configuration</h3>
96
+ <dl class="message-properties">
97
+ <dt>Filename <span class="property-type">string</span></dt>
98
+ <dd>Default filename if <code>msg.filename</code> is not set.</dd>
99
+ <dt>Prefix with timestamp <span class="property-type">boolean</span></dt>
100
+ <dd>Adds <code>YYYY-MM-DD_HH-MM-SS_</code> prefix to filename.</dd>
101
+ <dt>Timeout <span class="property-type">number</span></dt>
102
+ <dd>Request timeout in ms. Default: 30000.</dd>
103
+ </dl>
81
104
 
82
105
  <h3>Status Codes</h3>
83
106
  <ul>
84
- <li><strong>201</strong> - Upload successful</li>
85
- <li><strong>400</strong> - Bad request (missing file, invalid site ID)</li>
107
+ <li><strong>201</strong> - Success</li>
108
+ <li><strong>400</strong> - Bad request</li>
86
109
  <li><strong>401</strong> - Invalid API key</li>
87
- <li><strong>403</strong> - API access not enabled for this site</li>
110
+ <li><strong>403</strong> - API access not enabled</li>
88
111
  <li><strong>404</strong> - Site not found</li>
89
- <li><strong>429</strong> - Rate limit exceeded (max 30 uploads/minute)</li>
112
+ <li><strong>429</strong> - Rate limit exceeded</li>
90
113
  <li><strong>507</strong> - Storage limit exceeded</li>
91
114
  </ul>
92
115
 
93
- <h3>Example Flow</h3>
94
- <pre>
95
- [File In] → [Process Link Upload] → [Debug]
96
-
97
- The File In node reads a file and outputs msg.payload (Buffer)
98
- and msg.filename. Connect directly to this node to upload.</pre>
99
-
100
116
  <h3>References</h3>
101
117
  <ul>
102
- <li>
103
- <a href="https://portal.processlink.com.au" target="_blank">Process Link Portal</a> - Manage
104
- your sites and API keys
105
- </li>
106
- <li>
107
- <a href="https://github.com/process-link/node-red-contrib-processlink" target="_blank"
108
- >GitHub</a
109
- >
110
- - Source code and issues
111
- </li>
118
+ <li><a href="https://portal.processlink.com.au" target="_blank">Process Link Portal</a></li>
119
+ <li><a href="https://github.com/process-link/node-red-contrib-processlink" target="_blank">GitHub</a></li>
112
120
  </ul>
113
121
  </script>
@@ -55,7 +55,19 @@ module.exports = function (RED) {
55
55
 
56
56
  // Get filename
57
57
  const filename = msg.filename || config.filename || "file.bin";
58
- const basename = filename.split(/[\\/]/).pop();
58
+ let basename = filename.split(/[\\/]/).pop();
59
+
60
+ // Add timestamp prefix if enabled
61
+ if (config.timestampPrefix) {
62
+ const now = new Date();
63
+ const timestamp = now.getFullYear() + "-" +
64
+ String(now.getMonth() + 1).padStart(2, "0") + "-" +
65
+ String(now.getDate()).padStart(2, "0") + "_" +
66
+ String(now.getHours()).padStart(2, "0") + "-" +
67
+ String(now.getMinutes()).padStart(2, "0") + "-" +
68
+ String(now.getSeconds()).padStart(2, "0");
69
+ basename = timestamp + "_" + basename;
70
+ }
59
71
 
60
72
  // Build multipart form data
61
73
  const boundary = "----NodeREDProcessLink" + Date.now() + Math.random().toString(36).substring(2);
@@ -109,25 +121,24 @@ module.exports = function (RED) {
109
121
  msg.headers = res.headers;
110
122
 
111
123
  if (res.statusCode === 201 && parsedResponse.ok) {
112
- // Success
124
+ // Success - send to output 1
113
125
  msg.file_id = parsedResponse.file_id;
114
126
  node.status({
115
127
  fill: "green",
116
128
  shape: "dot",
117
129
  text: `uploaded: ${parsedResponse.file_id?.substring(0, 8)}...`,
118
130
  });
119
- send(msg);
131
+ send([msg, null]);
120
132
  done();
121
133
 
122
134
  // Clear status after 5 seconds
123
135
  setTimeout(() => node.status({}), 5000);
124
136
  } else {
125
- // API error
137
+ // API error - send to output 2
126
138
  const errorMsg = parsedResponse.error || parsedResponse.message || `HTTP ${res.statusCode}`;
127
139
  node.status({ fill: "red", shape: "dot", text: errorMsg });
128
140
 
129
- // Still send the message so users can handle errors in their flow
130
- send(msg);
141
+ send([null, msg]);
131
142
  done();
132
143
 
133
144
  // Clear status after 10 seconds
@@ -140,7 +151,7 @@ module.exports = function (RED) {
140
151
  node.status({ fill: "red", shape: "ring", text: "request failed" });
141
152
  msg.payload = { error: err.message };
142
153
  msg.statusCode = 0;
143
- send(msg);
154
+ send([null, msg]);
144
155
  done(err);
145
156
 
146
157
  setTimeout(() => node.status({}), 10000);
@@ -153,7 +164,7 @@ module.exports = function (RED) {
153
164
  node.status({ fill: "red", shape: "ring", text: "timeout" });
154
165
  msg.payload = { error: "Request timed out" };
155
166
  msg.statusCode = 0;
156
- send(msg);
167
+ send([null, msg]);
157
168
  done(new Error("Request timed out"));
158
169
 
159
170
  setTimeout(() => node.status({}), 10000);
@@ -0,0 +1,101 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("processlink-system-info", {
3
+ category: "Process Link",
4
+ color: "#f97316",
5
+ defaults: {
6
+ name: { value: "" },
7
+ sendOnDeploy: { value: true },
8
+ },
9
+ inputs: 1,
10
+ outputs: 1,
11
+ icon: "processlink.png",
12
+ paletteLabel: "system info",
13
+ label: function () {
14
+ return this.name || "system info";
15
+ },
16
+ labelStyle: function () {
17
+ return this.name ? "node_label_italic" : "";
18
+ },
19
+ inputLabels: "trigger",
20
+ outputLabels: "system info",
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="processlink-system-info">
25
+ <div class="form-row">
26
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
27
+ <input type="text" id="node-input-name" placeholder="Name" />
28
+ </div>
29
+ <div class="form-row">
30
+ <label for="node-input-sendOnDeploy">&nbsp;</label>
31
+ <input type="checkbox" id="node-input-sendOnDeploy" style="width: auto; margin-right: 10px;" checked>
32
+ <span>Send system info on deploy</span>
33
+ </div>
34
+ </script>
35
+
36
+ <script type="text/html" data-help-name="processlink-system-info">
37
+ <p>Outputs system information for diagnostics and monitoring.</p>
38
+
39
+ <h3>Triggers</h3>
40
+ <ul>
41
+ <li><strong>On deploy</strong> (if enabled) - Automatically sends when flow starts</li>
42
+ <li><strong>On input</strong> - Any incoming message triggers a fresh reading</li>
43
+ </ul>
44
+
45
+ <h3>Configuration</h3>
46
+ <dl class="message-properties">
47
+ <dt>Send on deploy <span class="property-type">boolean</span></dt>
48
+ <dd>When checked (default), outputs system info when the flow is deployed.</dd>
49
+ </dl>
50
+
51
+ <h3>Output</h3>
52
+ <p><code>msg.payload</code> contains:</p>
53
+
54
+ <table>
55
+ <tr><td><strong>timestamp</strong></td><td>ISO 8601 UTC timestamp</td></tr>
56
+ <tr><td><strong>localTime</strong></td><td>Device local time string</td></tr>
57
+ <tr><td><strong>timezone</strong></td><td>Timezone name (e.g., "Australia/Sydney")</td></tr>
58
+ <tr><td><strong>hostname</strong></td><td>Device hostname</td></tr>
59
+ <tr><td><strong>platform</strong></td><td>"win32", "linux", or "darwin"</td></tr>
60
+ <tr><td><strong>os</strong></td><td>OS name and version</td></tr>
61
+ <tr><td><strong>arch</strong></td><td>CPU architecture</td></tr>
62
+ <tr><td><strong>user</strong></td><td>User running Node-RED</td></tr>
63
+ <tr><td><strong>workingDirectory</strong></td><td>Node-RED working directory</td></tr>
64
+ <tr><td><strong>uptime</strong></td><td>System uptime (raw seconds, breakdown, formatted)</td></tr>
65
+ <tr><td><strong>cpu</strong></td><td>Model, cores, architecture</td></tr>
66
+ <tr><td><strong>memory</strong></td><td>Total, free, used (bytes + formatted), usedPercent</td></tr>
67
+ <tr><td><strong>disk</strong></td><td>Total, free, used (bytes + formatted), usedPercent</td></tr>
68
+ <tr><td><strong>network</strong></td><td>primaryIP, mac, interfaces</td></tr>
69
+ <tr><td><strong>nodeRed</strong></td><td>version, uptime</td></tr>
70
+ <tr><td><strong>nodejs</strong></td><td>version</td></tr>
71
+ <tr><td><strong>processMemory</strong></td><td>rss, heapTotal, heapUsed</td></tr>
72
+ </table>
73
+
74
+ <h3>Uptime Structure</h3>
75
+ <pre>{
76
+ "raw": 432000,
77
+ "breakdown": { "days": 5, "hours": 0, "minutes": 0, "seconds": 0 },
78
+ "formatted": "5d 0h 0m 0s"
79
+ }</pre>
80
+
81
+ <h3>Memory/Disk Structure</h3>
82
+ <pre>{
83
+ "total": { "bytes": 17179869184, "formatted": "16.00 GB" },
84
+ "free": { "bytes": 8589934592, "formatted": "8.00 GB" },
85
+ "used": { "bytes": 8589934592, "formatted": "8.00 GB" },
86
+ "usedPercent": 50
87
+ }</pre>
88
+
89
+ <h3>Notes</h3>
90
+ <ul>
91
+ <li><strong>disk</strong> may be unavailable on some systems due to permissions.</li>
92
+ <li><strong>nodeRed.uptime</strong> tracks time since flow was deployed.</li>
93
+ <li>Use <strong>usedPercent</strong> for easy threshold checks.</li>
94
+ </ul>
95
+
96
+ <h3>References</h3>
97
+ <ul>
98
+ <li><a href="https://portal.processlink.com.au" target="_blank">Process Link Portal</a></li>
99
+ <li><a href="https://github.com/process-link/node-red-contrib-processlink" target="_blank">GitHub</a></li>
100
+ </ul>
101
+ </script>
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Process Link System Info Node
3
+ * Outputs system information for diagnostics and monitoring
4
+ */
5
+
6
+ module.exports = function (RED) {
7
+ const os = require("os");
8
+ const { execSync } = require("child_process");
9
+
10
+ /**
11
+ * Format bytes to human-readable string
12
+ * @param {number} bytes
13
+ * @returns {string}
14
+ */
15
+ function formatBytes(bytes) {
16
+ if (bytes === 0) return "0 B";
17
+ const units = ["B", "KB", "MB", "GB", "TB"];
18
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
19
+ return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
20
+ }
21
+
22
+ /**
23
+ * Convert seconds to broken-down time object
24
+ * @param {number} totalSeconds
25
+ * @returns {object}
26
+ */
27
+ function formatUptime(totalSeconds) {
28
+ const days = Math.floor(totalSeconds / 86400);
29
+ const hours = Math.floor((totalSeconds % 86400) / 3600);
30
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
31
+ const seconds = Math.floor(totalSeconds % 60);
32
+
33
+ return {
34
+ raw: Math.floor(totalSeconds),
35
+ breakdown: {
36
+ days: days,
37
+ hours: hours,
38
+ minutes: minutes,
39
+ seconds: seconds,
40
+ },
41
+ formatted: `${days}d ${hours}h ${minutes}m ${seconds}s`,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Get primary network interface info (first non-internal IPv4)
47
+ * @returns {object}
48
+ */
49
+ function getNetworkInfo() {
50
+ const interfaces = os.networkInterfaces();
51
+ let primaryIP = null;
52
+ let primaryMAC = null;
53
+
54
+ // Find first non-internal IPv4 address
55
+ for (const name of Object.keys(interfaces)) {
56
+ for (const iface of interfaces[name]) {
57
+ if (iface.family === "IPv4" && !iface.internal) {
58
+ if (!primaryIP) {
59
+ primaryIP = iface.address;
60
+ primaryMAC = iface.mac;
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ return {
67
+ interfaces: interfaces,
68
+ primaryIP: primaryIP || "unknown",
69
+ mac: primaryMAC || "unknown",
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Get disk space information (cross-platform)
75
+ * @returns {object|null}
76
+ */
77
+ function getDiskInfo() {
78
+ try {
79
+ const platform = os.platform();
80
+ let total, free, used;
81
+
82
+ if (platform === "win32") {
83
+ // Windows: use wmic
84
+ const output = execSync("wmic logicaldisk where drivetype=3 get size,freespace", {
85
+ encoding: "utf8",
86
+ timeout: 5000,
87
+ });
88
+ const lines = output.trim().split("\n").filter((l) => l.trim());
89
+ if (lines.length > 1) {
90
+ // Sum all drives
91
+ total = 0;
92
+ free = 0;
93
+ for (let i = 1; i < lines.length; i++) {
94
+ const parts = lines[i].trim().split(/\s+/);
95
+ if (parts.length >= 2) {
96
+ free += parseInt(parts[0]) || 0;
97
+ total += parseInt(parts[1]) || 0;
98
+ }
99
+ }
100
+ used = total - free;
101
+ }
102
+ } else {
103
+ // Linux/Mac: use df
104
+ const output = execSync("df -B1 / | tail -1", {
105
+ encoding: "utf8",
106
+ timeout: 5000,
107
+ });
108
+ const parts = output.trim().split(/\s+/);
109
+ if (parts.length >= 4) {
110
+ total = parseInt(parts[1]) || 0;
111
+ used = parseInt(parts[2]) || 0;
112
+ free = parseInt(parts[3]) || 0;
113
+ }
114
+ }
115
+
116
+ if (total && total > 0) {
117
+ return {
118
+ total: {
119
+ bytes: total,
120
+ formatted: formatBytes(total),
121
+ },
122
+ free: {
123
+ bytes: free,
124
+ formatted: formatBytes(free),
125
+ },
126
+ used: {
127
+ bytes: used,
128
+ formatted: formatBytes(used),
129
+ },
130
+ usedPercent: Math.round((used / total) * 100),
131
+ };
132
+ }
133
+ } catch (e) {
134
+ // Disk info unavailable
135
+ }
136
+ return null;
137
+ }
138
+
139
+ /**
140
+ * Get Node-RED version
141
+ * @returns {string}
142
+ */
143
+ function getNodeRedVersion() {
144
+ try {
145
+ return RED.version();
146
+ } catch (e) {
147
+ return "unknown";
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Collect all system information
153
+ * @param {number} nodeRedStartTime - timestamp when Node-RED started
154
+ * @returns {object}
155
+ */
156
+ function collectSystemInfo(nodeRedStartTime) {
157
+ const now = new Date();
158
+ const totalMem = os.totalmem();
159
+ const freeMem = os.freemem();
160
+ const usedMem = totalMem - freeMem;
161
+ const cpus = os.cpus();
162
+ const processMemory = process.memoryUsage();
163
+ const networkInfo = getNetworkInfo();
164
+ const diskInfo = getDiskInfo();
165
+
166
+ // Calculate Node-RED uptime
167
+ const nodeRedUptimeSeconds = (Date.now() - nodeRedStartTime) / 1000;
168
+
169
+ const info = {
170
+ timestamp: now.toISOString(),
171
+ localTime: now.toLocaleString(),
172
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
173
+
174
+ hostname: os.hostname(),
175
+ platform: os.platform(),
176
+ os: `${os.type()} ${os.release()}`,
177
+ arch: os.arch(),
178
+ user: os.userInfo().username,
179
+ workingDirectory: process.cwd(),
180
+
181
+ uptime: formatUptime(os.uptime()),
182
+
183
+ cpu: {
184
+ model: cpus.length > 0 ? cpus[0].model : "unknown",
185
+ cores: cpus.length,
186
+ arch: os.arch(),
187
+ },
188
+
189
+ memory: {
190
+ total: {
191
+ bytes: totalMem,
192
+ formatted: formatBytes(totalMem),
193
+ },
194
+ free: {
195
+ bytes: freeMem,
196
+ formatted: formatBytes(freeMem),
197
+ },
198
+ used: {
199
+ bytes: usedMem,
200
+ formatted: formatBytes(usedMem),
201
+ },
202
+ usedPercent: Math.round((usedMem / totalMem) * 100),
203
+ },
204
+
205
+ network: networkInfo,
206
+
207
+ nodeRed: {
208
+ version: getNodeRedVersion(),
209
+ uptime: formatUptime(nodeRedUptimeSeconds),
210
+ },
211
+
212
+ nodejs: {
213
+ version: process.version,
214
+ },
215
+
216
+ processMemory: {
217
+ rss: {
218
+ bytes: processMemory.rss,
219
+ formatted: formatBytes(processMemory.rss),
220
+ },
221
+ heapTotal: {
222
+ bytes: processMemory.heapTotal,
223
+ formatted: formatBytes(processMemory.heapTotal),
224
+ },
225
+ heapUsed: {
226
+ bytes: processMemory.heapUsed,
227
+ formatted: formatBytes(processMemory.heapUsed),
228
+ },
229
+ },
230
+ };
231
+
232
+ // Add disk info if available
233
+ if (diskInfo) {
234
+ info.disk = diskInfo;
235
+ }
236
+
237
+ return info;
238
+ }
239
+
240
+ function ProcessLinkSystemInfoNode(config) {
241
+ RED.nodes.createNode(this, config);
242
+ const node = this;
243
+
244
+ // Record when this node was created (proxy for Node-RED start time)
245
+ const nodeRedStartTime = Date.now();
246
+
247
+ // Send on deploy if enabled
248
+ if (config.sendOnDeploy) {
249
+ // Small delay to let Node-RED fully initialize
250
+ setTimeout(() => {
251
+ const info = collectSystemInfo(nodeRedStartTime);
252
+ node.send({ payload: info });
253
+ node.status({
254
+ fill: "green",
255
+ shape: "dot",
256
+ text: `sent @ ${new Date().toLocaleTimeString()}`,
257
+ });
258
+ setTimeout(() => node.status({}), 5000);
259
+ }, 1000);
260
+ }
261
+
262
+ node.on("input", function (msg, send, done) {
263
+ // For Node-RED 0.x compatibility
264
+ send = send || function () { node.send.apply(node, arguments); };
265
+ done = done || function (err) { if (err) node.error(err, msg); };
266
+
267
+ try {
268
+ node.status({ fill: "yellow", shape: "dot", text: "collecting..." });
269
+
270
+ const info = collectSystemInfo(nodeRedStartTime);
271
+ msg.payload = info;
272
+
273
+ node.status({
274
+ fill: "green",
275
+ shape: "dot",
276
+ text: `sent @ ${new Date().toLocaleTimeString()}`,
277
+ });
278
+
279
+ send(msg);
280
+ done();
281
+
282
+ // Clear status after 5 seconds
283
+ setTimeout(() => node.status({}), 5000);
284
+ } catch (err) {
285
+ node.status({ fill: "red", shape: "ring", text: "error" });
286
+ done(err);
287
+ setTimeout(() => node.status({}), 10000);
288
+ }
289
+ });
290
+
291
+ node.on("close", function () {
292
+ node.status({});
293
+ });
294
+ }
295
+
296
+ RED.nodes.registerType("processlink-system-info", ProcessLinkSystemInfoNode);
297
+ };
package/package.json CHANGED
@@ -1,54 +1,58 @@
1
- {
2
- "name": "@processlink/node-red-contrib-processlink",
3
- "publishConfig": {
4
- "access": "public"
5
- },
6
- "version": "1.0.2",
7
- "description": "Node-RED nodes for Process Link platform integration - upload files, send notifications, and connect to industrial automation systems",
8
- "keywords": [
9
- "node-red",
10
- "processlink",
11
- "process-link",
12
- "files",
13
- "upload",
14
- "api",
15
- "industrial",
16
- "iot",
17
- "automation",
18
- "manufacturing",
19
- "scada",
20
- "plc"
21
- ],
22
- "node-red": {
23
- "version": ">=2.0.0",
24
- "nodes": {
25
- "processlink-config": "nodes/config/processlink-config.js",
26
- "processlink-files-upload": "nodes/files/processlink-files-upload.js"
27
- }
28
- },
29
- "scripts": {
30
- "test": "echo \"No tests yet\" && exit 0"
31
- },
32
- "author": {
33
- "name": "Process Link",
34
- "email": "support@processlink.com.au",
35
- "url": "https://processlink.com.au"
36
- },
37
- "contributors": [],
38
- "license": "MIT",
39
- "repository": {
40
- "type": "git",
41
- "url": "git+https://github.com/process-link/node-red-contrib-processlink.git"
42
- },
43
- "bugs": {
44
- "url": "https://github.com/process-link/node-red-contrib-processlink/issues"
45
- },
46
- "homepage": "https://processlink.com.au",
47
- "funding": {
48
- "type": "individual",
49
- "url": "https://processlink.com.au"
50
- },
51
- "engines": {
52
- "node": ">=14.0.0"
53
- }
54
- }
1
+ {
2
+ "name": "@processlink/node-red-contrib-processlink",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "1.1.0",
7
+ "description": "Node-RED nodes for Process Link platform integration - upload files, send notifications, and connect to industrial automation systems",
8
+ "keywords": [
9
+ "node-red",
10
+ "processlink",
11
+ "process-link",
12
+ "files",
13
+ "upload",
14
+ "api",
15
+ "industrial",
16
+ "iot",
17
+ "automation",
18
+ "manufacturing",
19
+ "scada",
20
+ "plc",
21
+ "system-info",
22
+ "diagnostics",
23
+ "monitoring"
24
+ ],
25
+ "node-red": {
26
+ "version": ">=2.0.0",
27
+ "nodes": {
28
+ "processlink-config": "nodes/config/processlink-config.js",
29
+ "processlink-files-upload": "nodes/files/processlink-files-upload.js",
30
+ "processlink-system-info": "nodes/system/processlink-system-info.js"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "test": "echo \"No tests yet\" && exit 0"
35
+ },
36
+ "author": {
37
+ "name": "Process Link",
38
+ "email": "support@processlink.com.au",
39
+ "url": "https://processlink.com.au"
40
+ },
41
+ "contributors": [],
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/process-link/node-red-contrib-processlink.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/process-link/node-red-contrib-processlink/issues"
49
+ },
50
+ "homepage": "https://processlink.com.au",
51
+ "funding": {
52
+ "type": "individual",
53
+ "url": "https://processlink.com.au"
54
+ },
55
+ "engines": {
56
+ "node": ">=14.0.0"
57
+ }
58
+ }