node-red-contrib-uos-nats 1.0.1 → 1.0.3

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/lib/provider.js CHANGED
@@ -2,8 +2,8 @@ import { connect } from 'nats';
2
2
  import { requestToken } from './auth.js';
3
3
  import { CLIENT_NAME, NATS_SERVER, PROVIDER_ID, PUBLISH_INTERVAL_MS, VARIABLE_DEFINITIONS, } from './config.js';
4
4
  import { SimulationEngine } from './simulation.js';
5
- import { buildProviderDefinitionEvent, buildReadVariablesResponse, buildVariablesChangedEvent, } from './payloads.js';
6
- import { providerDefinitionChanged, readVariablesQuery, varsChangedEvent } from './subjects.js';
5
+ import { buildProviderDefinitionEvent, buildReadVariablesResponse, buildVariablesChangedEvent, parseRegistryStateEvent, } from './payloads.js';
6
+ import { providerDefinitionChanged, readVariablesQuery, varsChangedEvent, registryStateEvent } from './subjects.js';
7
7
  async function main() {
8
8
  console.log(`[provider] Node Provider für ${PROVIDER_ID} wird gestartet…`);
9
9
  const token = await requestToken();
@@ -15,8 +15,28 @@ async function main() {
15
15
  });
16
16
  console.log('[provider] NATS verbunden:', nc.getServer());
17
17
  const simulation = new SimulationEngine(VARIABLE_DEFINITIONS);
18
- const { payload: definitionPayload, fingerprint } = buildProviderDefinitionEvent(VARIABLE_DEFINITIONS);
18
+
19
+ // Initial Publish
20
+ let { payload: definitionPayload, fingerprint } = buildProviderDefinitionEvent(VARIABLE_DEFINITIONS);
19
21
  await nc.publish(providerDefinitionChanged(PROVIDER_ID), definitionPayload);
22
+
23
+ // Registry State Listener (Keep Alive)
24
+ nc.subscribe(registryStateEvent(), {
25
+ callback: async (err, msg) => {
26
+ if (err) return;
27
+ try {
28
+ const state = parseRegistryStateEvent(msg.data);
29
+ if (state === 1) { // 1 = RUNNING
30
+ console.log('[provider] Registry restart detected. Re-publishing definition...');
31
+ const { payload: newPayload } = buildProviderDefinitionEvent(VARIABLE_DEFINITIONS);
32
+ await nc.publish(providerDefinitionChanged(PROVIDER_ID), newPayload);
33
+ }
34
+ } catch (e) {
35
+ console.error('[provider] Error handling registry state:', e);
36
+ }
37
+ }
38
+ });
39
+
20
40
  const readSubject = readVariablesQuery(PROVIDER_ID);
21
41
  const sub = nc.subscribe(readSubject, {
22
42
  callback: async (err, msg) => {
@@ -276,17 +276,17 @@ module.exports = function (RED) {
276
276
 
277
277
  this.status({ fill: 'green', shape: 'dot', text: 'ready' });
278
278
 
279
- // Heartbeat Removed: Periodic republishing causes UI flickering/refresh in DataHub.
280
- // The definition should only be sent on start or when it actually changes.
281
- /*
279
+ // Heartbeat Restored: Provider disappears after ~20 minutes if definition is not refreshed.
280
+ // Set to 15 minutes (900000ms) to avoid frequent UI flickering but ensure persistence.
282
281
  const outputHeartbeat = setInterval(() => {
283
282
  if (nc && !nc.isClosed()) {
283
+ console.log('[DataHub Output] Sending Definition Heartbeat...');
284
284
  sendDefinitionUpdate(payloads, subjects).catch(err => {
285
285
  this.warn(`Heartbeat error: ${err.message}`);
286
286
  });
287
287
  }
288
- }, 10000); // Every 10 seconds
289
- */
288
+ }, 900000);
289
+
290
290
 
291
291
  this.on('input', async (msg, send, done) => {
292
292
  try {
@@ -28,13 +28,12 @@
28
28
  const $inputId = $('#node-input-variableId');
29
29
  const $inputKey = $('#node-input-variableKey');
30
30
 
31
- // Styles matching Read Node (Simplified: Icon, ID, Name)
32
- // Was: 60px 1fr 80px 80px
33
- // New: 30px 60px 1fr
31
+ // Styles matching Read Node exactly
34
32
  const rowStyle = "display:grid; grid-template-columns: 30px 60px 1fr; align-items:center; padding:4px 0; border-bottom:1px solid #eee; font-family:'Helvetica Neue', Arial, sans-serif; font-size:12px; cursor:pointer;";
35
33
  const idStyle = "background:#eee; color:#555; padding:1px 4px; border-radius:3px; font-family:monospace; text-align:center; font-size:11px;";
36
34
  const keyStyle = "overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px;";
37
- const iconStyle = "text-align:center; color:#ccc;";
35
+
36
+ // Removed iconStyle, using checkbox instead
38
37
 
39
38
  let currentVariables = [];
40
39
 
@@ -48,53 +47,76 @@
48
47
 
49
48
  vars.forEach(v => {
50
49
  const safeId = (v.id !== undefined) ? v.id : (v.Id !== undefined ? v.Id : 'ERR');
51
- // Type/Access removed from display as per request
52
-
53
50
  const isSelected = String(safeId) === String($inputId.val());
54
51
 
55
52
  const row = $('<div>', { class: 'var-row', style: rowStyle });
56
- if (isSelected) {
57
- row.css('background', '#dcefff');
58
- }
59
53
 
60
- // Icon (Selection Indicator)
61
- const iconCol = $('<div>', { style: iconStyle }).html(isSelected ? '<i class="fa fa-dot-circle-o" style="color:#2196f3; font-size:14px;"></i>' : '<i class="fa fa-circle-o"></i>');
54
+ // Checkbox Container
55
+ const cbContainer = $('<div>', { style: 'text-align:center;' });
56
+ // Use actual checkbox to match Read Node visual
57
+ const cb = $('<input type="checkbox" class="var-checkbox">')
58
+ .prop('checked', isSelected)
59
+ .css('cursor', 'pointer');
60
+
61
+ cbContainer.append(cb);
62
62
 
63
63
  // ID
64
64
  const idCol = $('<div>', { style: 'text-align:center;' }).append($('<span>', { style: idStyle }).text(safeId));
65
65
  // Key
66
66
  const keyCol = $('<div>', { style: keyStyle, title: v.key }).text(v.key);
67
67
 
68
- row.append(iconCol).append(idCol).append(keyCol);
69
-
70
- // Click Handler - Fill Inputs
71
- row.on('click', function () {
72
- // Reset all rows
73
- $('.var-row').css('background', 'transparent');
74
- $('.var-row i.fa').removeClass('fa-dot-circle-o').addClass('fa-circle-o').css('color', '#ccc');
68
+ row.append(cbContainer).append(idCol).append(keyCol);
75
69
 
76
- // Highlight this one
77
- $(this).css('background', '#dcefff');
78
- $(this).find('i.fa').removeClass('fa-circle-o').addClass('fa-dot-circle-o').css('color', '#2196f3');
70
+ // Click Handler (Row or Checkbox)
71
+ const selectRow = () => {
72
+ // Uncheck all others (Single Force)
73
+ $('.var-checkbox').prop('checked', false);
74
+ cb.prop('checked', true);
79
75
 
80
76
  $inputId.val(safeId !== 'ERR' ? safeId : '');
81
77
  $inputKey.val(v.key);
82
-
83
- // Trigger validation visual update if needed
84
78
  $inputId.trigger('change');
79
+ };
80
+
81
+ // Toggle behavior if clicking already selected?
82
+ // Usually for single select, clicking selected does nothing or keeps it selected.
83
+ // If user clicks checkbox directly:
84
+ cb.on('click', function (e) {
85
+ e.stopPropagation(); // prevent row click double bubble
86
+ if ($(this).prop('checked')) {
87
+ selectRow();
88
+ } else {
89
+ // Allowing deserialize? imply batch mode if none selected
90
+ $inputId.val('');
91
+ $inputKey.val('');
92
+ $inputId.trigger('change');
93
+ }
94
+ });
95
+
96
+ row.on('click', function () {
97
+ // If not checked, check it.
98
+ if (!cb.prop('checked')) {
99
+ selectRow();
100
+ } else {
101
+ // If already checked, uncheck?
102
+ cb.prop('checked', false);
103
+ $inputId.val('');
104
+ $inputKey.val('');
105
+ $inputId.trigger('change');
106
+ }
85
107
  });
86
108
 
87
- // Hover effect
88
- row.on('mouseenter', function () { if ($(this).css('background-color') !== 'rgb(220, 239, 255)') $(this).css('background', '#f7f7f7'); });
89
- row.on('mouseleave', function () { if ($(this).css('background-color') !== 'rgb(220, 239, 255)') $(this).css('background', 'transparent'); });
109
+ // Hover effect (Gray, not Blue, matching Read Node)
110
+ row.on('mouseenter', function () { $(this).css('background', '#f7f7f7'); });
111
+ row.on('mouseleave', function () { $(this).css('background', 'transparent'); });
90
112
 
91
113
  $listContainer.append(row);
92
114
  });
93
115
 
94
- // Scroll to selection if possible
95
- const selectedRow = $listContainer.find('.var-row').filter(function () { return $(this).css('background-color') === 'rgb(220, 239, 255)'; });
96
- if (selectedRow.length) {
97
- $listContainer.scrollTop(selectedRow.offset().top - $listContainer.offset().top + $listContainer.scrollTop() - 40);
116
+ // Scroll to selection
117
+ const selectedCb = $listContainer.find('.var-checkbox:checked');
118
+ if (selectedCb.length) {
119
+ $listContainer.scrollTop(selectedCb.closest('.var-row').offset().top - $listContainer.offset().top + $listContainer.scrollTop() - 40);
98
120
  }
99
121
  };
100
122
 
@@ -102,7 +124,7 @@
102
124
  $searchInput.on('keyup', function () {
103
125
  const term = $(this).val().toLowerCase();
104
126
  $listContainer.find('.var-row').each(function () {
105
- const text = $(this).find('div:nth-child(3)').text().toLowerCase(); // Key is 3rd column now
127
+ const text = $(this).find('div:nth-child(3)').text().toLowerCase(); // Key is 3rd column
106
128
  $(this).toggle(text.indexOf(term) > -1);
107
129
  });
108
130
  });
@@ -207,7 +229,7 @@
207
229
  </div>
208
230
 
209
231
  <div style="display:grid; grid-template-columns: 30px 60px 1fr; background:#eee; font-size:11px; font-weight:bold; padding:4px 0; border-bottom:1px solid #ccc;">
210
- <div style="text-align:center;"></div>
232
+ <div style="text-align:center;"><i class="fa fa-check-square-o"></i></div>
211
233
  <div style="text-align:center;">ID</div>
212
234
  <div style="padding-left:10px;">Name</div>
213
235
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-uos-nats",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read, write, and provide variables via NATS protocol with OAuth2 authentication. Features: Variable Key resolution, custom icons, example flows, and provider definition caching.",
5
5
  "author": {
6
6
  "name": "IoTUeli",