@skylord123/node-red-pebble-timeline 1.0.0 → 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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(xargs:*)",
5
+ "Bash(find:*)",
6
+ "WebSearch",
7
+ "WebFetch(domain:developer.rebble.io)",
8
+ "WebFetch(domain:github.com)"
9
+ ]
10
+ }
11
+ }
@@ -0,0 +1,124 @@
1
+ [
2
+ {
3
+ "id": "sync-flow-tab",
4
+ "type": "tab",
5
+ "label": "Rebble Timeline Sync",
6
+ "disabled": false,
7
+ "info": "Flow to fetch timeline pins from Rebble sync endpoint"
8
+ },
9
+ {
10
+ "id": "inject-token",
11
+ "type": "inject",
12
+ "z": "sync-flow-tab",
13
+ "name": "Set Access Token",
14
+ "props": [
15
+ {
16
+ "p": "access_token",
17
+ "v": "YOUR_ACCESS_TOKEN_HERE",
18
+ "vt": "str"
19
+ }
20
+ ],
21
+ "repeat": "",
22
+ "crontab": "",
23
+ "once": false,
24
+ "onceDelay": 0.1,
25
+ "topic": "",
26
+ "x": 140,
27
+ "y": 100,
28
+ "wires": [
29
+ ["setup-request"]
30
+ ]
31
+ },
32
+ {
33
+ "id": "setup-request",
34
+ "type": "function",
35
+ "z": "sync-flow-tab",
36
+ "name": "Setup Request",
37
+ "func": "msg.url = 'https://timeline-sync.rebble.io/v1/sync';\nmsg.headers = {\n 'Authorization': 'Bearer ' + msg.access_token\n};\n\n// Optional: pass timeline/glance cursor for incremental sync\nif (msg.timeline) {\n msg.url += '?timeline=' + msg.timeline;\n if (msg.glance) {\n msg.url += '&glance=' + msg.glance;\n }\n} else if (msg.glance) {\n msg.url += '?glance=' + msg.glance;\n}\n\nreturn msg;",
38
+ "outputs": 1,
39
+ "timeout": "",
40
+ "noerr": 0,
41
+ "initialize": "",
42
+ "finalize": "",
43
+ "libs": [],
44
+ "x": 340,
45
+ "y": 100,
46
+ "wires": [
47
+ ["http-request"]
48
+ ]
49
+ },
50
+ {
51
+ "id": "http-request",
52
+ "type": "http request",
53
+ "z": "sync-flow-tab",
54
+ "name": "GET /v1/sync",
55
+ "method": "GET",
56
+ "ret": "obj",
57
+ "paytoqs": "ignore",
58
+ "url": "",
59
+ "tls": "",
60
+ "persist": false,
61
+ "proxy": "",
62
+ "insecureHTTPParser": false,
63
+ "authType": "",
64
+ "senderr": false,
65
+ "headers": [],
66
+ "x": 530,
67
+ "y": 100,
68
+ "wires": [
69
+ ["debug-output", "parse-response"]
70
+ ]
71
+ },
72
+ {
73
+ "id": "debug-output",
74
+ "type": "debug",
75
+ "z": "sync-flow-tab",
76
+ "name": "Sync Response",
77
+ "active": true,
78
+ "tosidebar": true,
79
+ "console": false,
80
+ "tostatus": false,
81
+ "complete": "payload",
82
+ "targetType": "msg",
83
+ "statusVal": "",
84
+ "statusType": "auto",
85
+ "x": 740,
86
+ "y": 60,
87
+ "wires": []
88
+ },
89
+ {
90
+ "id": "parse-response",
91
+ "type": "function",
92
+ "z": "sync-flow-tab",
93
+ "name": "Parse Updates",
94
+ "func": "// Extract updates array and syncURL for next request\nlet response = msg.payload;\n\nif (response.updates && response.updates.length > 0) {\n node.status({fill: 'green', shape: 'dot', text: response.updates.length + ' updates'});\n} else {\n node.status({fill: 'yellow', shape: 'ring', text: 'No updates'});\n}\n\n// Store syncURL for subsequent requests\nmsg.syncURL = response.syncURL;\nmsg.updates = response.updates || [];\n\nreturn msg;",
95
+ "outputs": 1,
96
+ "timeout": "",
97
+ "noerr": 0,
98
+ "initialize": "",
99
+ "finalize": "",
100
+ "libs": [],
101
+ "x": 740,
102
+ "y": 140,
103
+ "wires": [
104
+ ["debug-updates"]
105
+ ]
106
+ },
107
+ {
108
+ "id": "debug-updates",
109
+ "type": "debug",
110
+ "z": "sync-flow-tab",
111
+ "name": "Parsed Updates",
112
+ "active": true,
113
+ "tosidebar": true,
114
+ "console": false,
115
+ "tostatus": false,
116
+ "complete": "true",
117
+ "targetType": "full",
118
+ "statusVal": "",
119
+ "statusType": "auto",
120
+ "x": 940,
121
+ "y": 140,
122
+ "wires": []
123
+ }
124
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylord123/node-red-pebble-timeline",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Node-RED nodes for interacting with the Pebble Timeline API",
5
5
  "keywords": [
6
6
  "node-red",
@@ -19,7 +19,9 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "axios": "^1.3.4",
22
+ "axios": "1.12.0",
23
23
  "fs-extra": "^11.1.0"
24
24
  }
25
25
  }
26
+
27
+
@@ -187,6 +187,7 @@
187
187
  label: function() {
188
188
  return this.name || "Add Timeline Pin";
189
189
  },
190
+ paletteLabel: "Add Timeline Pin",
190
191
  oneditprepare: function() {
191
192
  // Setup TypedInput for server override options
192
193
  $("#node-input-apiUrl").typedInput({
@@ -288,7 +288,7 @@ module.exports = function(RED) {
288
288
  node.status({fill: "green", shape: "dot", text: "OK"});
289
289
 
290
290
  // Store the pin in our local storage
291
- storePin(pin);
291
+ storePin(pin, timelineToken);
292
292
 
293
293
  // Prepare the output message
294
294
  msg.payload = {
@@ -336,8 +336,15 @@ module.exports = function(RED) {
336
336
  });
337
337
 
338
338
  // Helper to store a pin in local storage
339
- function storePin(pin) {
340
- const timelineToken = tokenOverride || configNode.credentials.timelineToken;
339
+ function storePin(pin, timelineToken) {
340
+ // Ensure we have a valid token
341
+ if (!timelineToken) {
342
+ node.warn("Cannot store pin: No valid timeline token provided");
343
+ return;
344
+ }
345
+
346
+ // Convert token to string to ensure it can be used as an object key
347
+ timelineToken = String(timelineToken);
341
348
 
342
349
  // Initialize the token's pins array if it doesn't exist
343
350
  if (!pinsData[timelineToken]) {
@@ -368,21 +375,40 @@ module.exports = function(RED) {
368
375
  function cleanupOldPins() {
369
376
  const oneMonthAgo = new Date();
370
377
  oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
378
+ let changed = false;
371
379
 
372
380
  // Iterate through all tokens
373
381
  Object.keys(pinsData).forEach(token => {
382
+ // Make sure the token's data is an array
383
+ if (!Array.isArray(pinsData[token])) {
384
+ pinsData[token] = [];
385
+ return;
386
+ }
387
+
374
388
  // Filter out pins older than 1 month
375
389
  const initialCount = pinsData[token].length;
376
390
  pinsData[token] = pinsData[token].filter(pin => {
377
- const storedDate = new Date(pin._stored);
378
- return storedDate >= oneMonthAgo;
391
+ // Make sure pin has _stored property
392
+ if (!pin || !pin._stored) return false;
393
+
394
+ try {
395
+ const storedDate = new Date(pin._stored);
396
+ return storedDate >= oneMonthAgo;
397
+ } catch (e) {
398
+ // If date parsing fails, remove the pin
399
+ return false;
400
+ }
379
401
  });
380
402
 
381
403
  // Log if pins were removed
382
404
  if (pinsData[token].length < initialCount) {
383
405
  node.debug(`Removed ${initialCount - pinsData[token].length} old pins for token ${token.substring(0, 8)}...`);
406
+ changed = true;
384
407
  }
385
408
  });
409
+
410
+ // No need to save here as the calling function will save the file
411
+ return changed;
386
412
  }
387
413
 
388
414
  // Apply node configuration to the pin
@@ -23,6 +23,7 @@
23
23
  label: function () {
24
24
  return this.name || "Delete Timeline Pin";
25
25
  },
26
+ paletteLabel: "Delete Timeline Pin",
26
27
  oneditprepare: function () {
27
28
  // Setup TypedInput for server override options
28
29
  $("#node-input-apiUrl").typedInput({
@@ -102,7 +102,7 @@ module.exports = function(RED) {
102
102
  node.status({fill: "green", shape: "dot", text: "Pin deleted"});
103
103
 
104
104
  // Remove the pin from our local storage
105
- removePin(pinId);
105
+ removePin(pinId, timelineToken);
106
106
 
107
107
  // Prepare the output message
108
108
  msg.payload = {
@@ -115,17 +115,37 @@ module.exports = function(RED) {
115
115
  if (done) done();
116
116
  })
117
117
  .catch(error => {
118
- node.status({fill: "red", shape: "dot", text: "Error: " + (error.response ? error.response.status : error.message)});
119
-
120
- msg.payload = {
121
- success: false,
122
- pinId: pinId,
123
- error: error.message,
124
- response: error.response ? error.response.data : null
125
- };
126
-
127
- send(msg);
128
- if (done) done(error);
118
+ // Handle 404 - pin already deleted
119
+ if (error.response && error.response.status === 404) {
120
+ node.warn(`Pin ${pinId} not found on server (404) - assuming already deleted`);
121
+ node.status({fill: "yellow", shape: "dot", text: "Pin already deleted"});
122
+
123
+ // Remove from local storage anyway
124
+ removePin(pinId, timelineToken);
125
+
126
+ msg.payload = {
127
+ success: true,
128
+ pinId: pinId,
129
+ alreadyDeleted: true,
130
+ message: "Pin not found on server, removed from local storage"
131
+ };
132
+
133
+ send(msg);
134
+ if (done) done();
135
+ } else {
136
+ // Other errors
137
+ node.status({fill: "red", shape: "dot", text: "Error: " + (error.response ? error.response.status : error.message)});
138
+
139
+ msg.payload = {
140
+ success: false,
141
+ pinId: pinId,
142
+ error: error.message,
143
+ response: error.response ? error.response.data : null
144
+ };
145
+
146
+ send(msg);
147
+ if (done) done(error);
148
+ }
129
149
  });
130
150
  }).catch(err => {
131
151
  if (done) done(err);
@@ -133,8 +153,15 @@ module.exports = function(RED) {
133
153
  });
134
154
 
135
155
  // Helper to remove a pin from local storage
136
- function removePin(pinId) {
137
- const timelineToken = node.tokenOverride || configNode.credentials.timelineToken;
156
+ function removePin(pinId, timelineToken) {
157
+ // Ensure we have a valid token
158
+ if (!timelineToken) {
159
+ node.warn("Cannot remove pin: No valid timeline token provided");
160
+ return;
161
+ }
162
+
163
+ // Convert token to string to ensure it can be used as an object key
164
+ timelineToken = String(timelineToken);
138
165
 
139
166
  // Check if this token has any pins
140
167
  if (!pinsData[timelineToken]) {
@@ -145,8 +172,7 @@ module.exports = function(RED) {
145
172
  const initialCount = pinsData[timelineToken].length;
146
173
  pinsData[timelineToken] = pinsData[timelineToken].filter(p => p.id !== pinId);
147
174
 
148
- // Clean up old pins (older than 1 month) from all tokens
149
- cleanupOldPins();
175
+ // Note: Cleanup of old pins is handled in the add node
150
176
 
151
177
  // Only write if we actually removed something
152
178
  if (pinsData[timelineToken].length !== initialCount) {
@@ -158,27 +184,6 @@ module.exports = function(RED) {
158
184
  }
159
185
  }
160
186
 
161
- // Helper to clean up pins older than 1 month
162
- function cleanupOldPins() {
163
- const oneMonthAgo = new Date();
164
- oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
165
-
166
- // Iterate through all tokens
167
- Object.keys(pinsData).forEach(token => {
168
- // Filter out pins older than 1 month
169
- const initialCount = pinsData[token].length;
170
- pinsData[token] = pinsData[token].filter(pin => {
171
- const storedDate = new Date(pin._stored);
172
- return storedDate >= oneMonthAgo;
173
- });
174
-
175
- // Log if pins were removed
176
- if (pinsData[token].length < initialCount) {
177
- node.debug(`Removed ${initialCount - pinsData[token].length} old pins for token ${token.substring(0, 8)}...`);
178
- }
179
- });
180
- }
181
-
182
187
  node.on('close', function() {
183
188
  // Clean up any resources
184
189
  });
@@ -26,6 +26,7 @@
26
26
  label: function () {
27
27
  return this.name || "List Timeline Pins";
28
28
  },
29
+ paletteLabel: "List Timeline Pins",
29
30
  oneditprepare: function () {
30
31
  // Setup TypedInput for server override options
31
32
  $("#node-input-apiUrl").typedInput({
@@ -107,7 +108,7 @@
107
108
  </script>
108
109
 
109
110
  <script type="text/html" data-help-name="pebble-timeline-list">
110
- <p>Lists pins that have been added to the Pebble Timeline.</p>
111
+ <p>Lists pins from the local storage file that tracks pins added via Node-RED.</p>
111
112
 
112
113
  <h3>Inputs</h3>
113
114
  <dl class="message-properties">
@@ -128,9 +129,13 @@
128
129
  </dl>
129
130
 
130
131
  <h3>Details</h3>
131
- <p>This node lists pins that have been successfully added to the Pebble Timeline. The pins are stored locally in the
132
- Node-RED installation directory, organized by timeline token.</p>
133
- <p>Only pins associated with the current timeline token will be listed. Each token maintains its own separate list of pins.</p>
132
+ <p><strong>Important:</strong> This node does <strong>not</strong> fetch pins from the remote Pebble Timeline server.
133
+ Instead, it reads from a local file (<code>timeline-pins.json</code>) stored in the Node-RED user directory.</p>
134
+ <p>The local file is automatically updated whenever you use the <strong>pebble-timeline-add</strong> or
135
+ <strong>pebble-timeline-delete</strong> nodes. This provides a local record of pins that have been
136
+ added or removed via Node-RED.</p>
137
+ <p>Pins are organized by timeline token in the storage file. Only pins associated with the current timeline
138
+ token will be listed. Each token maintains its own separate list of pins.</p>
134
139
  <p>You can filter the pins by start and end times. The times should be in ISO date-time format (e.g.,
135
140
  2023-01-01T12:00:00Z).</p>
136
141
  <p>By default, if no filters are specified, all pins for the current token will be returned.</p>
@@ -138,9 +143,10 @@
138
143
 
139
144
  <h4>Example Use Cases</h4>
140
145
  <ul>
141
- <li>List all upcoming events for the day</li>
146
+ <li>List all upcoming events for the day that were added via Node-RED</li>
142
147
  <li>Filter pins for a specific date range for reporting</li>
143
148
  <li>Show pins for a specific event type based on time criteria</li>
149
+ <li>Track what pins have been sent to the Pebble Timeline</li>
144
150
  </ul>
145
151
 
146
152
  <h3>References</h3>
@@ -105,11 +105,20 @@ module.exports = function(RED) {
105
105
  }
106
106
 
107
107
  // Get the timeline token to use
108
- const timelineToken = tokenOverride || configNode.credentials.timelineToken;
108
+ let timelineToken = tokenOverride || configNode.credentials.timelineToken;
109
+
110
+ // Ensure we have a valid token
111
+ if (!timelineToken) {
112
+ node.warn("No valid timeline token provided");
113
+ timelineToken = "default"; // Use a default key to avoid errors
114
+ }
115
+
116
+ // Convert token to string to ensure it can be used as an object key
117
+ timelineToken = String(timelineToken);
109
118
 
110
119
  // Get pins for this token only
111
120
  let pins = [];
112
- if (pinsData[timelineToken]) {
121
+ if (pinsData[timelineToken] && Array.isArray(pinsData[timelineToken])) {
113
122
  pins = pinsData[timelineToken];
114
123
  }
115
124
 
@@ -134,8 +143,7 @@ module.exports = function(RED) {
134
143
  return include;
135
144
  });
136
145
 
137
- // Clean up old pins (older than 1 month) from all tokens
138
- cleanupOldPins(pinsData);
146
+ // Note: Cleanup of old pins is handled in the add node
139
147
 
140
148
  // Create output message
141
149
  msg.payload = filteredPins;
@@ -152,38 +160,6 @@ module.exports = function(RED) {
152
160
  });
153
161
  });
154
162
 
155
- // Helper to clean up pins older than 1 month
156
- function cleanupOldPins(pinsData) {
157
- const oneMonthAgo = new Date();
158
- oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
159
- let changed = false;
160
-
161
- // Iterate through all tokens
162
- Object.keys(pinsData).forEach(token => {
163
- // Filter out pins older than 1 month
164
- const initialCount = pinsData[token].length;
165
- pinsData[token] = pinsData[token].filter(pin => {
166
- const storedDate = new Date(pin._stored);
167
- return storedDate >= oneMonthAgo;
168
- });
169
-
170
- // Log if pins were removed
171
- if (pinsData[token].length < initialCount) {
172
- node.debug(`Removed ${initialCount - pinsData[token].length} old pins for token ${token.substring(0, 8)}...`);
173
- changed = true;
174
- }
175
- });
176
-
177
- // Save the updated pins data if any pins were removed
178
- if (changed) {
179
- try {
180
- fs.writeFileSync(pinsFile, JSON.stringify(pinsData, null, 2));
181
- } catch (error) {
182
- node.warn(`Error saving pins to file: ${error.message}`);
183
- }
184
- }
185
- }
186
-
187
163
  node.on('close', function() {
188
164
  // Clean up any resources
189
165
  });