@skylord123/node-red-pebble-timeline 1.3.0 → 1.4.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,81 @@
1
+ name: Publish to npm
2
+
3
+ # Publishes the package to npm whenever a GitHub Release is published.
4
+ # It publishes the exact commit the release tag points to, so a pre-release
5
+ # can be cut from any branch (e.g. a beta off `dev`) without that branch
6
+ # having to be merged into master first.
7
+ #
8
+ # The release tag is the source of truth for the version:
9
+ # - Stable tag (e.g. v1.2.3) -> published to the "latest"
10
+ # dist-tag; the version bump is
11
+ # committed back to master.
12
+ # - Pre-release tag (e.g. v1.2.3-beta.1) -> published to a matching dist-tag
13
+ # ("beta", "rc", ...); does NOT
14
+ # become "latest" and is NOT
15
+ # committed back to master.
16
+ #
17
+ # Authentication uses npm Trusted Publishing (OIDC) - no token or secret is
18
+ # needed. Configure a trusted publisher for this package on npmjs.com:
19
+ # Repository: skylord123/node-red-pebble-timeline
20
+ # Workflow: publish.yml
21
+
22
+ on:
23
+ release:
24
+ types: [published]
25
+
26
+ jobs:
27
+ publish:
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ contents: write # commit the version bump back to master
31
+ id-token: write # npm Trusted Publishing (OIDC) + provenance
32
+ steps:
33
+ - name: Check out the released commit
34
+ uses: actions/checkout@v4
35
+
36
+ - name: Set up Node.js
37
+ uses: actions/setup-node@v4
38
+ with:
39
+ node-version: 22
40
+ registry-url: https://registry.npmjs.org
41
+
42
+ - name: Update npm
43
+ # Trusted Publishing requires npm 11.5.1 or newer; Node 22 ships npm 10.
44
+ run: npm install -g npm@latest
45
+
46
+ - name: Determine version and dist-tag
47
+ id: ver
48
+ run: |
49
+ VERSION="${GITHUB_REF_NAME#v}"
50
+ if [[ "$VERSION" == *-* ]]; then
51
+ # pre-release, e.g. 1.0.0-beta.1 -> dist-tag "beta"
52
+ DIST_TAG="${VERSION#*-}"
53
+ DIST_TAG="${DIST_TAG%%.*}"
54
+ PRERELEASE=true
55
+ else
56
+ DIST_TAG=latest
57
+ PRERELEASE=false
58
+ fi
59
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
60
+ echo "dist_tag=$DIST_TAG" >> "$GITHUB_OUTPUT"
61
+ echo "prerelease=$PRERELEASE" >> "$GITHUB_OUTPUT"
62
+ echo "Publishing $VERSION to npm dist-tag '$DIST_TAG' (prerelease=$PRERELEASE)"
63
+
64
+ - name: Set version
65
+ run: npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version
66
+
67
+ - name: Publish to npm
68
+ run: npm publish --provenance --access public --tag "${{ steps.ver.outputs.dist_tag }}"
69
+
70
+ - name: Commit version bump back to master
71
+ if: steps.ver.outputs.prerelease == 'false'
72
+ run: |
73
+ if git diff --quiet; then
74
+ echo "package.json already at ${{ steps.ver.outputs.version }}; nothing to commit."
75
+ exit 0
76
+ fi
77
+ git config user.name "github-actions[bot]"
78
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
79
+ git commit -am "Set version to ${{ steps.ver.outputs.version }}"
80
+ git push origin HEAD:master \
81
+ || echo "::warning::Could not push the version bump to master (branch protection?). The package was still published."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skylord123/node-red-pebble-timeline",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Node-RED nodes for interacting with the Pebble Timeline API",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,5 +27,3 @@
27
27
  "fs-extra": "^11.1.0"
28
28
  }
29
29
  }
30
-
31
-
@@ -1,7 +1,6 @@
1
1
  const axios = require('axios');
2
- const fs = require('fs-extra');
3
- const path = require('path');
4
2
  const { pinValid } = require('./pebble-timeline-validation');
3
+ const store = require('./pebble-timeline-store');
5
4
 
6
5
  /**
7
6
  * Node-RED node for adding pins to the Pebble Timeline API
@@ -29,20 +28,7 @@ module.exports = function(RED) {
29
28
  return;
30
29
  }
31
30
 
32
- // Make sure storage directory exists
33
- const storageDir = path.join(RED.settings.userDir, 'pebble-timeline');
34
- fs.ensureDirSync(storageDir);
35
- const pinsFile = path.join(storageDir, 'timeline-pins.json');
36
-
37
- // Load existing pins (organized by token)
38
- let pinsData = {};
39
- try {
40
- if (fs.existsSync(pinsFile)) {
41
- pinsData = JSON.parse(fs.readFileSync(pinsFile, 'utf8'));
42
- }
43
- } catch (error) {
44
- node.warn(`Error loading pins file: ${error.message}`);
45
- }
31
+ store.init(RED.settings.userDir);
46
32
 
47
33
  node.on('input', async function(msg, send, done) {
48
34
  // Backwards compatibility with Node-RED 0.x
@@ -291,8 +277,12 @@ module.exports = function(RED) {
291
277
  }
292
278
 
293
279
  // Pin is valid - store it locally
280
+ try {
281
+ await store.addPin(store.resolveKey(configNode, tokenOverride), pin);
282
+ } catch (e) {
283
+ node.warn(`Error saving pin to local storage: ${e.message}`);
284
+ }
294
285
  node.status({fill: "green", shape: "dot", text: "OK (local)"});
295
- storePin(pin, timelineToken || 'local');
296
286
 
297
287
  msg.payload = {
298
288
  success: true,
@@ -327,13 +317,17 @@ module.exports = function(RED) {
327
317
  'X-User-Token': timelineToken
328
318
  }
329
319
  })
330
- .then(response => {
320
+ .then(async response => {
321
+ // Store the pin in our local storage
322
+ try {
323
+ await store.addPin(store.resolveKey(configNode, tokenOverride), pin);
324
+ } catch (e) {
325
+ node.warn(`Error saving pin to local storage: ${e.message}`);
326
+ }
327
+
331
328
  // Set successful status - using "OK" as requested
332
329
  node.status({fill: "green", shape: "dot", text: "OK"});
333
330
 
334
- // Store the pin in our local storage
335
- storePin(pin, timelineToken);
336
-
337
331
  // Prepare the output message
338
332
  msg.payload = {
339
333
  success: true,
@@ -380,82 +374,6 @@ module.exports = function(RED) {
380
374
  }
381
375
  });
382
376
 
383
- // Helper to store a pin in local storage
384
- function storePin(pin, timelineToken) {
385
- // Ensure we have a valid token
386
- if (!timelineToken) {
387
- node.warn("Cannot store pin: No valid timeline token provided");
388
- return;
389
- }
390
-
391
- // Convert token to string to ensure it can be used as an object key
392
- timelineToken = String(timelineToken);
393
-
394
- // Initialize the token's pins array if it doesn't exist
395
- if (!pinsData[timelineToken]) {
396
- pinsData[timelineToken] = [];
397
- }
398
-
399
- // Remove any existing pin with the same ID for this token
400
- pinsData[timelineToken] = pinsData[timelineToken].filter(p => p.id !== pin.id);
401
-
402
- // Add the new pin with a timestamp for when it was added
403
- pinsData[timelineToken].push({
404
- ...pin,
405
- _stored: new Date().toISOString()
406
- });
407
-
408
- // Clean up old pins (older than 1 month) from all tokens
409
- cleanupOldPins();
410
-
411
- // Write the pins to the file
412
- try {
413
- fs.writeFileSync(pinsFile, JSON.stringify(pinsData, null, 2));
414
- } catch (error) {
415
- node.warn(`Error saving pins to file: ${error.message}`);
416
- }
417
- }
418
-
419
- // Helper to clean up pins older than 1 month
420
- function cleanupOldPins() {
421
- const oneMonthAgo = new Date();
422
- oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
423
- let changed = false;
424
-
425
- // Iterate through all tokens
426
- Object.keys(pinsData).forEach(token => {
427
- // Make sure the token's data is an array
428
- if (!Array.isArray(pinsData[token])) {
429
- pinsData[token] = [];
430
- return;
431
- }
432
-
433
- // Filter out pins older than 1 month
434
- const initialCount = pinsData[token].length;
435
- pinsData[token] = pinsData[token].filter(pin => {
436
- // Make sure pin has _stored property
437
- if (!pin || !pin._stored) return false;
438
-
439
- try {
440
- const storedDate = new Date(pin._stored);
441
- return storedDate >= oneMonthAgo;
442
- } catch (e) {
443
- // If date parsing fails, remove the pin
444
- return false;
445
- }
446
- });
447
-
448
- // Log if pins were removed
449
- if (pinsData[token].length < initialCount) {
450
- node.debug(`Removed ${initialCount - pinsData[token].length} old pins for token ${token.substring(0, 8)}...`);
451
- changed = true;
452
- }
453
- });
454
-
455
- // No need to save here as the calling function will save the file
456
- return changed;
457
- }
458
-
459
377
  // Apply node configuration to the pin
460
378
  async function applyNodeConfiguration(pin, config, msg, node) {
461
379
  try {
@@ -1,41 +1,26 @@
1
1
  const axios = require('axios');
2
- const fs = require('fs-extra');
3
- const path = require('path');
2
+ const store = require('./pebble-timeline-store');
4
3
 
5
4
  module.exports = function(RED) {
6
5
  function PebbleTimelineDeleteNode(config) {
7
6
  RED.nodes.createNode(this, config);
8
7
  const node = this;
9
8
 
10
- // Get the config node
11
9
  const configNode = RED.nodes.getNode(config.config);
12
10
  if (!configNode) {
13
11
  node.error("No Pebble Timeline configuration found");
14
12
  return;
15
13
  }
16
14
 
17
- // Make sure storage directory exists
18
- const storageDir = path.join(RED.settings.userDir, 'pebble-timeline');
19
- fs.ensureDirSync(storageDir);
20
- const pinsFile = path.join(storageDir, 'timeline-pins.json');
21
-
22
- // Load existing pins (organized by token)
23
- let pinsData = {};
24
- try {
25
- if (fs.existsSync(pinsFile)) {
26
- pinsData = JSON.parse(fs.readFileSync(pinsFile, 'utf8'));
27
- }
28
- } catch (error) {
29
- node.warn(`Error loading pins file: ${error.message}`);
30
- }
15
+ store.init(RED.settings.userDir);
31
16
 
32
17
  node.on('input', function(msg, send, done) {
33
- // Backwards compatibility with Node-RED 0.x
34
18
  send = send || function() { node.send.apply(node, arguments) };
35
19
 
36
- // Get the pin ID to delete
37
20
  let pinId;
38
- // Process parameters in sequence
21
+ let apiUrlOverride = null;
22
+ let tokenOverride = null;
23
+
39
24
  Promise.all([
40
25
  new Promise(resolve => {
41
26
  RED.util.evaluateNodeProperty(config.pinId, config.pinIdType, node, msg, (err, result) => {
@@ -56,12 +41,11 @@ module.exports = function(RED) {
56
41
  });
57
42
  }),
58
43
 
59
- // Check for server override options
60
44
  new Promise(resolve => {
61
45
  if (config.apiUrl) {
62
46
  RED.util.evaluateNodeProperty(config.apiUrl, config.apiUrlType, node, msg, (err, result) => {
63
47
  if (!err && result) {
64
- node.apiUrlOverride = result;
48
+ apiUrlOverride = result;
65
49
  }
66
50
  resolve();
67
51
  });
@@ -74,7 +58,7 @@ module.exports = function(RED) {
74
58
  if (config.token) {
75
59
  RED.util.evaluateNodeProperty(config.token, config.tokenType, node, msg, (err, result) => {
76
60
  if (!err && result) {
77
- node.tokenOverride = result;
61
+ tokenOverride = result;
78
62
  }
79
63
  resolve();
80
64
  });
@@ -82,20 +66,21 @@ module.exports = function(RED) {
82
66
  resolve();
83
67
  }
84
68
  })
85
- ]).then(() => {
86
- // Use overrides if provided, otherwise use config node values
87
- const baseApiUrl = node.apiUrlOverride || configNode.apiUrl;
88
- const timelineToken = node.tokenOverride || configNode.credentials.timelineToken;
69
+ ]).then(async () => {
70
+ const baseApiUrl = apiUrlOverride || configNode.apiUrl;
71
+ const timelineToken = tokenOverride || configNode.credentials.timelineToken;
72
+ const storeKey = store.resolveKey(configNode, tokenOverride);
89
73
 
90
- // Check if we're in local emulation mode (empty API URL)
91
74
  const isLocalMode = !baseApiUrl || baseApiUrl.trim() === '';
92
75
 
93
76
  if (isLocalMode) {
94
- // Local emulation mode - delete from local storage only
95
77
  node.debug(`Local emulation mode - deleting pin locally`);
96
78
 
97
- // Remove the pin from our local storage
98
- removePin(pinId, timelineToken || 'local');
79
+ try {
80
+ await store.removePin(storeKey, pinId);
81
+ } catch (e) {
82
+ node.warn(`Error removing pin from local storage: ${e.message}`);
83
+ }
99
84
 
100
85
  node.status({fill: "green", shape: "dot", text: "Pin deleted (local)"});
101
86
 
@@ -109,7 +94,6 @@ module.exports = function(RED) {
109
94
  send(msg);
110
95
  if (done) done();
111
96
  } else {
112
- // Remote API mode
113
97
  const apiUrl = `${baseApiUrl}/v1/user/pins/${pinId}`;
114
98
 
115
99
  if (!timelineToken) {
@@ -123,13 +107,15 @@ module.exports = function(RED) {
123
107
  'X-User-Token': timelineToken
124
108
  }
125
109
  })
126
- .then(response => {
110
+ .then(async response => {
127
111
  node.status({fill: "green", shape: "dot", text: "Pin deleted"});
128
112
 
129
- // Remove the pin from our local storage
130
- removePin(pinId, timelineToken);
113
+ try {
114
+ await store.removePin(storeKey, pinId);
115
+ } catch (e) {
116
+ node.warn(`Error removing pin from local storage: ${e.message}`);
117
+ }
131
118
 
132
- // Prepare the output message
133
119
  msg.payload = {
134
120
  success: true,
135
121
  pinId: pinId,
@@ -139,14 +125,16 @@ module.exports = function(RED) {
139
125
  send(msg);
140
126
  if (done) done();
141
127
  })
142
- .catch(error => {
143
- // Handle 404 - pin already deleted
128
+ .catch(async error => {
144
129
  if (error.response && error.response.status === 404) {
145
130
  node.warn(`Pin ${pinId} not found on server (404) - assuming already deleted`);
146
131
  node.status({fill: "yellow", shape: "dot", text: "Pin already deleted"});
147
132
 
148
- // Remove from local storage anyway
149
- removePin(pinId, timelineToken);
133
+ try {
134
+ await store.removePin(storeKey, pinId);
135
+ } catch (e) {
136
+ node.warn(`Error removing pin from local storage: ${e.message}`);
137
+ }
150
138
 
151
139
  msg.payload = {
152
140
  success: true,
@@ -158,7 +146,6 @@ module.exports = function(RED) {
158
146
  send(msg);
159
147
  if (done) done();
160
148
  } else {
161
- // Other errors
162
149
  node.status({fill: "red", shape: "dot", text: "Error: " + (error.response ? error.response.status : error.message)});
163
150
 
164
151
  msg.payload = {
@@ -178,40 +165,7 @@ module.exports = function(RED) {
178
165
  });
179
166
  });
180
167
 
181
- // Helper to remove a pin from local storage
182
- function removePin(pinId, timelineToken) {
183
- // Ensure we have a valid token
184
- if (!timelineToken) {
185
- node.warn("Cannot remove pin: No valid timeline token provided");
186
- return;
187
- }
188
-
189
- // Convert token to string to ensure it can be used as an object key
190
- timelineToken = String(timelineToken);
191
-
192
- // Check if this token has any pins
193
- if (!pinsData[timelineToken]) {
194
- return; // No pins for this token
195
- }
196
-
197
- // Remove the pin with the specified ID from this token's pins
198
- const initialCount = pinsData[timelineToken].length;
199
- pinsData[timelineToken] = pinsData[timelineToken].filter(p => p.id !== pinId);
200
-
201
- // Note: Cleanup of old pins is handled in the add node
202
-
203
- // Only write if we actually removed something
204
- if (pinsData[timelineToken].length !== initialCount) {
205
- try {
206
- fs.writeFileSync(pinsFile, JSON.stringify(pinsData, null, 2));
207
- } catch (error) {
208
- node.warn(`Error saving pins to file: ${error.message}`);
209
- }
210
- }
211
- }
212
-
213
168
  node.on('close', function() {
214
- // Clean up any resources
215
169
  });
216
170
  }
217
171
 
@@ -1,25 +1,19 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
1
+ const store = require('./pebble-timeline-store');
3
2
 
4
3
  module.exports = function(RED) {
5
4
  function PebbleTimelineListNode(config) {
6
5
  RED.nodes.createNode(this, config);
7
6
  const node = this;
8
7
 
9
- // Get the config node
10
8
  const configNode = RED.nodes.getNode(config.config);
11
9
  if (!configNode) {
12
10
  node.error("No Pebble Timeline configuration found");
13
11
  return;
14
12
  }
15
13
 
16
- // Make sure storage directory exists
17
- const storageDir = path.join(RED.settings.userDir, 'pebble-timeline');
18
- fs.ensureDirSync(storageDir);
19
- const pinsFile = path.join(storageDir, 'timeline-pins.json');
14
+ store.init(RED.settings.userDir);
20
15
 
21
16
  node.on('input', function(msg, send, done) {
22
- // Backwards compatibility with Node-RED 0.x
23
17
  send = send || function() { node.send.apply(node, arguments) };
24
18
 
25
19
  let startTime = null;
@@ -27,7 +21,6 @@ module.exports = function(RED) {
27
21
  let apiUrlOverride = null;
28
22
  let tokenOverride = null;
29
23
 
30
- // Process filter parameters in sequence
31
24
  Promise.resolve()
32
25
  .then(() => {
33
26
  return new Promise((resolve) => {
@@ -94,58 +87,15 @@ module.exports = function(RED) {
94
87
  });
95
88
  })
96
89
  .then(() => {
97
- // Load the pins
98
- let pinsData = {};
99
- try {
100
- if (fs.existsSync(pinsFile)) {
101
- pinsData = JSON.parse(fs.readFileSync(pinsFile, 'utf8'));
102
- }
103
- } catch (error) {
104
- node.warn(`Error loading pins file: ${error.message}`);
105
- }
106
-
107
- // Get the timeline token to use
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
- }
90
+ const key = store.resolveKey(configNode, tokenOverride);
91
+ const pins = store.getPins(key);
115
92
 
116
- // Convert token to string to ensure it can be used as an object key
117
- timelineToken = String(timelineToken);
118
-
119
- // Get pins for this token only
120
- let pins = [];
121
- if (pinsData[timelineToken] && Array.isArray(pinsData[timelineToken])) {
122
- pins = pinsData[timelineToken];
123
- }
124
-
125
- // Apply filters
126
93
  const filteredPins = pins.filter(pin => {
127
- let include = true;
128
-
129
- if (startTime !== null) {
130
- const pinTime = new Date(pin.time);
131
- if (pinTime < startTime) {
132
- include = false;
133
- }
134
- }
135
-
136
- if (endTime !== null) {
137
- const pinTime = new Date(pin.time);
138
- if (pinTime > endTime) {
139
- include = false;
140
- }
141
- }
142
-
143
- return include;
94
+ if (startTime !== null && new Date(pin.time) < startTime) return false;
95
+ if (endTime !== null && new Date(pin.time) > endTime) return false;
96
+ return true;
144
97
  });
145
98
 
146
- // Note: Cleanup of old pins is handled in the add node
147
-
148
- // Create output message
149
99
  msg.payload = filteredPins;
150
100
  msg.count = filteredPins.length;
151
101
 
@@ -161,7 +111,6 @@ module.exports = function(RED) {
161
111
  });
162
112
 
163
113
  node.on('close', function() {
164
- // Clean up any resources
165
114
  });
166
115
  }
167
116
 
@@ -0,0 +1,93 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ let pinsFile = null;
5
+ let pinsData = null;
6
+ let writeQueue = Promise.resolve();
7
+
8
+ function init(userDir) {
9
+ if (pinsFile) return;
10
+ const storageDir = path.join(userDir, 'pebble-timeline');
11
+ fs.ensureDirSync(storageDir);
12
+ pinsFile = path.join(storageDir, 'timeline-pins.json');
13
+ try {
14
+ pinsData = fs.existsSync(pinsFile)
15
+ ? JSON.parse(fs.readFileSync(pinsFile, 'utf8'))
16
+ : {};
17
+ } catch (e) {
18
+ pinsData = {};
19
+ }
20
+ }
21
+
22
+ // Compute the storage bucket key for a given config node, preferring the
23
+ // timeline token (or a per-message override) and falling back to the config
24
+ // node's own id so each config is isolated even without a token.
25
+ function resolveKey(configNode, override) {
26
+ const token = override
27
+ || (configNode && configNode.credentials && configNode.credentials.timelineToken);
28
+ if (token) return String(token);
29
+ if (configNode && configNode.id) return String(configNode.id);
30
+ return 'local';
31
+ }
32
+
33
+ function getPins(key) {
34
+ if (!pinsData) return [];
35
+ return Array.isArray(pinsData[key]) ? pinsData[key].slice() : [];
36
+ }
37
+
38
+ function addPin(key, pin) {
39
+ return enqueue(() => {
40
+ if (!Array.isArray(pinsData[key])) pinsData[key] = [];
41
+ pinsData[key] = pinsData[key].filter(p => p.id !== pin.id);
42
+ pinsData[key].push({ ...pin, _stored: new Date().toISOString() });
43
+ cleanupOldPins();
44
+ return writeFile();
45
+ });
46
+ }
47
+
48
+ function removePin(key, pinId) {
49
+ return enqueue(() => {
50
+ if (!Array.isArray(pinsData[key])) return false;
51
+ const before = pinsData[key].length;
52
+ pinsData[key] = pinsData[key].filter(p => p.id !== pinId);
53
+ if (pinsData[key].length === before) return false;
54
+ return writeFile().then(() => true);
55
+ });
56
+ }
57
+
58
+ // Serialize all mutating operations through a single promise chain so
59
+ // concurrent add/delete invocations cannot race on the read-modify-write.
60
+ function enqueue(fn) {
61
+ const next = writeQueue.then(fn, fn);
62
+ writeQueue = next.catch(() => {});
63
+ return next;
64
+ }
65
+
66
+ function cleanupOldPins() {
67
+ const oneMonthAgo = new Date();
68
+ oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
69
+ for (const k of Object.keys(pinsData)) {
70
+ if (!Array.isArray(pinsData[k])) {
71
+ pinsData[k] = [];
72
+ continue;
73
+ }
74
+ pinsData[k] = pinsData[k].filter(pin => {
75
+ if (!pin || !pin._stored) return false;
76
+ const d = new Date(pin._stored);
77
+ return !isNaN(d.getTime()) && d >= oneMonthAgo;
78
+ });
79
+ }
80
+ }
81
+
82
+ function writeFile() {
83
+ const tmp = pinsFile + '.tmp';
84
+ const data = JSON.stringify(pinsData, null, 2);
85
+ return new Promise((resolve, reject) => {
86
+ fs.writeFile(tmp, data, (err) => {
87
+ if (err) return reject(err);
88
+ fs.rename(tmp, pinsFile, (err2) => err2 ? reject(err2) : resolve());
89
+ });
90
+ });
91
+ }
92
+
93
+ module.exports = { init, resolveKey, getPins, addPin, removePin };
@@ -1,11 +0,0 @@
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
- }
@@ -1,124 +0,0 @@
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
- ]