@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 +13 -0
- package/README.md +72 -16
- package/nodes/config/processlink-config.html +1 -1
- package/nodes/files/processlink-files-upload.html +45 -37
- package/nodes/files/processlink-files-upload.js +19 -8
- package/nodes/system/processlink-system-info.html +101 -0
- package/nodes/system/processlink-system-info.js +297 -0
- package/package.json +58 -54
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
|
-
##
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```
|
|
124
|
-
[
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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)
|
|
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
|
|
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:
|
|
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: "
|
|
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"> </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>
|
|
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">
|
|
65
|
-
<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>
|
|
82
|
+
<dd>Same as above (convenience property).</dd>
|
|
68
83
|
<dt>statusCode <span class="property-type">number</span></dt>
|
|
69
|
-
<dd>
|
|
84
|
+
<dd>201</dd>
|
|
70
85
|
</dl>
|
|
71
86
|
|
|
72
|
-
<
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
</
|
|
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> -
|
|
85
|
-
<li><strong>400</strong> - Bad request
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"> </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
|
|
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
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
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
|
+
}
|