node-red-contrib-uos-nats 0.1.52 → 0.1.53

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.
@@ -357,77 +357,399 @@
357
357
  <div class="form-row">
358
358
  <label for="node-input-connection"><i class="fa fa-cube"></i> u-OS Config</label>
359
359
  <input type="text" id="node-input-connection">
360
- </div>
361
- <div class="form-row">
362
- <label><i class="fa fa-id-badge"></i> Provider</label>
363
- <div style="display:flex; align-items:center; flex:1; gap:5px;">
364
- <select id="datahub-provider-select" style="flex:1;"></select>
365
- <button type="button" id="datahub-provider-refresh" class="red-ui-button" title="Refresh Providers"><i class="fa fa-refresh"></i></button>
366
- <input type="hidden" id="node-input-providerId">
367
- <!-- Manual input fallback kept hidden/removed from flow per user request -->
368
- <input type="text" id="datahub-provider-manual" style="display:none;" disabled>
360
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
361
+ <input type="text" id="node-input-name" placeholder="Name">
369
362
  </div>
370
- </div>
371
- <div class="form-row">
372
- <label></label>
373
- <div id="datahub-provider-status" class="form-tips" style="display:none;"></div>
374
- </div>
375
- <div class="form-row" style="margin-bottom:0;">
376
- <label for="datahub-input-variables" style="width: auto;"><i class="fa fa-list"></i> Variables</label>
377
- <div style="display:inline-block; float:right;">
378
- <div class="btn-group" style="margin-right: 5px;">
379
- <button type="button" id="datahub-vars-all" class="red-ui-button red-ui-button-small" title="Select All"><i class="fa fa-check-square-o"></i></button>
380
- <button type="button" id="datahub-vars-none" class="red-ui-button red-ui-button-small" title="Deselect All"><i class="fa fa-square-o"></i></button>
363
+ <div class="form-row">
364
+ <label for="node-input-connection"><i class="fa fa-globe"></i> Config</label>
365
+ <input type="text" id="node-input-connection">
366
+ </div>
367
+ <div class="form-row">
368
+ <label for="node-input-providerId"><i class="fa fa-server"></i> Provider</label>
369
+ <div style="display: inline-flex; width: 70%;">
370
+ <select id="node-input-providerId" style="flex-grow: 1;"></select>
371
+ <button id="node-input-check-custom-provider" class="red-ui-button" style="margin-left: 5px;">Refresh</button>
381
372
  </div>
382
- <button type="button" id="datahub-vars-refresh" class="red-ui-button red-ui-button-small" title="Refresh Variables"><i class="fa fa-refresh"></i></button>
383
373
  </div>
384
- </div>
385
- <div class="form-row">
386
- <div style="border:1px solid #ccc; border-radius:4px; height:200px; overflow-y:scroll; padding:5px; width:100%; box-sizing:border-box;" id="datahub-variables-container">
387
- <!-- Checkboxes will be inserted here -->
388
- <div style="color:#aaa; font-style:italic; padding:5px;">Select a provider to load variables...</div>
374
+ <div class="form-row">
375
+ <label for="node-input-mode"><i class="fa fa-sliders"></i> Mode</label>
376
+ <select id="node-input-mode">
377
+ <option value="auto">Auto-Discovery (Select from List)</option>
378
+ <option value="manual_single">Manual: Single Variable</option>
379
+ <option value="manual_multi">Manual: Multi Variable (Table)</option>
380
+ </select>
381
+ </div>
382
+
383
+ <!-- Mode: Auto-Discovery -->
384
+ <div id="mode-auto-row" class="mode-row">
385
+ <div class="form-row" style="margin-bottom: 0px;">
386
+ <label><i class="fa fa-list"></i> Variables</label>
387
+ <div style="display: inline-block; width: 70%;">
388
+ <div style="margin-bottom: 5px;">
389
+ <button id="node-input-refresh-vars" class="red-ui-button red-ui-button-small"><i class="fa fa-refresh"></i> Refresh</button>
390
+ <button id="node-input-select-all" class="red-ui-button red-ui-button-small" style="margin-left: 5px;">Select All</button>
391
+ <button id="node-input-select-none" class="red-ui-button red-ui-button-small" style="margin-left: 5px;">None</button>
392
+ <span id="vars-status" style="margin-left: 10px; font-style: italic; color: #888;"></span>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ <div class="form-row">
397
+ <input type="hidden" id="node-input-variablesText">
398
+ <div id="node-input-variables-container" style="border: 1px solid #ccc; border-radius: 4px; height: 250px; overflow-y: scroll; padding: 5px; background: #fff; width: 100%; box-sizing: border-box;">
399
+ <!-- Checkboxes will be injected here -->
400
+ <div style="padding: 10px; text-align: center; color: #888;">
401
+ Select a Provider to load variables...
402
+ </div>
403
+ </div>
404
+ <div class="form-tips" style="margin-top: 5px;">
405
+ <i class="fa fa-info-circle"></i> Requires <b>Deploy</b> or active connection.
406
+ </div>
407
+ </div>
408
+ </div>
409
+
410
+ <!-- Mode: Manual Single -->
411
+ <div id="mode-manual-single-row" class="mode-row">
412
+ <div class="form-row">
413
+ <label for="node-input-singleName"><i class="fa fa-font"></i> Var Name</label>
414
+ <input type="text" id="node-input-singleName" placeholder="e.g. digital_nameplate.manufacturer_name">
415
+ </div>
416
+ <div class="form-row">
417
+ <label for="node-input-singleId"><i class="fa fa-hashtag"></i> Var ID</label>
418
+ <input type="number" id="node-input-singleId" placeholder="e.g. 0">
419
+ </div>
420
+ <div class="form-tips">
421
+ <i class="fa fa-info-circle"></i> <b>Manual Mode:</b> Use this if Auto-Discovery fails (Permissions/403). Enter the internal ID from your system.
422
+ </div>
423
+ </div>
424
+
425
+ <!-- Mode: Manual Multi -->
426
+ <div id="mode-manual-multi-row" class="mode-row">
427
+ <div class="form-row">
428
+ <label for="node-input-manualVariables-container"><i class="fa fa-table"></i> Mapping</label>
429
+ <div class="form-row node-input-manualVariables-container-row" style="width: 100%; margin-top: 8px;">
430
+ <ol id="node-input-manual-def-list"></ol>
431
+ </div>
432
+ <!-- Hidden field to store JSON string of definitions -->
433
+ <input type="hidden" id="node-input-manualVariables" />
434
+ </div>
435
+ <div class="form-tips">
436
+ <i class="fa fa-info-circle"></i> <b>Manual Table:</b> Map strictly "Name -> ID". Only variables listed here will be output.
437
+ </div>
438
+ </div>
439
+
440
+ <hr>
441
+
442
+ <div class="form-row">
443
+ <label for="node-input-pollingInterval"><i class="fa fa-clock-o"></i> Poll (ms)</label>
444
+ <input type="number" id="node-input-pollingInterval" placeholder="0 (Disabled)">
389
445
  </div>
390
- <input type="hidden" id="node-input-variablesText">
391
- </div>
392
- <div class="form-row">
393
- <label></label>
394
- <div id="datahub-variables-status" class="form-tips">Leave empty to listen to every variable.</div>
395
- </div>
396
- <div class="form-row" style="margin-bottom:0;">
397
- <label style="width:auto;"><i class="fa fa-wrench"></i> Manual Definition Map</label>
398
- <div style="float:right; font-size:12px; color:#888;">Map Variable Names to IDs manually (if Discovery fails)</div>
399
- </div>
400
- <div class="form-row node-input-manual-container-row">
401
- <ol id="node-input-manual-defs-container"></ol>
402
- </div>
403
- <input type="hidden" id="node-input-manualVariables">
404
- <div class="form-tips"><strong>Tip:</strong> If auto-discovery fails (permissions), enter the Name and ID exactly as configured in the server. See your Python config for reference.</div>
405
446
  </script>
406
447
 
407
448
  <script type="text/html" data-help-name="datahub-input">
408
- <p><strong>DataHub Input</strong> subscribes to a provider via the u-OS Data Hub and emits JSON messages.</p>
409
- <p><em>Note: This node is developed by <strong>iotueli</strong> and is NOT an official Weidmüller product.</em></p>
410
- <dl>
411
- <dt>u-OS Config</dt>
412
- <dd>Pick the connection node that holds host, port and OAuth credentials.</dd>
413
- <dt>Provider ID</dt>
414
- <dd>Provider to read. The drop-down lists every provider returned by the Data Hub API.</dd>
415
- </dl>
416
- <p><strong>Attributes:</strong></p>
417
- <dl>
418
- <dt>Provider & Variables</dt>
419
- <dd>Click the <strong>Refresh</strong> button to load the lists. Use the Input Port (e.g. inject node) to trigger manual snapshots.</dd>
420
- </dl>
421
- <p><strong>Troubleshooting:</strong></p>
422
- <ul>
423
- <li><strong>Empty Variable List:</strong> Ensure the Config Node is <strong>DEPLOYED</strong>. The "Test Connection" button only checks basic connectivity, but reading variable schemas requires a running flow.</li>
424
- <li><strong>Permission Information:</strong> If you don't see a specific provider, your OAuth Client might lack read permissions for it.</li>
425
- </ul>
426
- <p>Each message has <code>msg.payload</code> shaped like:</p>
427
- <pre>{
428
- "type": "snapshot" | "change",
429
- "variables": [
430
- { "name": "diagnostics.status_text", "value": "ready" }
431
- ]
432
- }</pre>
433
- </script>
449
+ <p>Reads variables from the u-OS Data Hub (Snapshot or Subscription).</p>
450
+
451
+ <h3>Configuration</h3>
452
+ <dl class="message-properties">
453
+ <dt>Mode</dt>
454
+ <dd>
455
+ <ul>
456
+ <li><b>Auto-Discovery:</b> Connects to the hub, downloads the variable list, and lets you select variables via checkboxes. Requires correct permissions (`hub.variables.readonly`).</li>
457
+ <li><b>Manual (Single):</b> For reading a single variable when Discovery is blocked. Enter Name and ID manually.</li>
458
+ <li><b>Manual (Multi):</b> For reading multiple variables when Discovery is blocked. Use the table to map Names to IDs.</li>
459
+ </ul>
460
+ </dd>
461
+
462
+ <dt>Provider</dt>
463
+ <dd>The Data Hub Provider ID (e.g. `u_os_sbm`).</dd>
464
+
465
+ <dt>Polling</dt>
466
+ <dd>Interval in ms to force-read values. 0 = Disabled (Wait for Change Events).</dd>
467
+ </dl>
468
+
469
+ <h3>Important: Permission Issues / 403</h3>
470
+ <p>
471
+ If you see "Empty List" or "Permission Violation", you must use one of the <b>Manual Modes</b>.
472
+ Auto-Discovery relies on endpoints that are often restricted.
473
+ In Manual Mode, you provide the `ID` (Number) which works even without Discovery permissions.
474
+ </p>
475
+
476
+ <div style="margin-top: 10px; font-size: 0.8em; color: #666; border-top: 1px solid #eee; padding-top: 5px;">
477
+ Developed by <a href="https://github.com/iotueli" target="_blank">iotueli</a>. Not an official Weidmüller product.
478
+ </div>
479
+ </script>
480
+
481
+ <script type="text/javascript">
482
+ RED.nodes.registerType('datahub-input', {
483
+ category: 'u-OS',
484
+ color: '#ff9900',
485
+ defaults: {
486
+ name: { value: "" },
487
+ connection: { type: "uos-config", required: true },
488
+ providerId: { value: "", required: true },
489
+ variablesText: { value: "" }, // Legacy / Auto-Mode list
490
+ manualVariables: { value: "" }, // Manual Table JSON/CSV
491
+ singleName: { value: "" }, // Manual Single Name
492
+ singleId: { value: "" }, // Manual Single ID
493
+ mode: { value: "auto" }, // auto | manual_single | manual_multi
494
+ pollingInterval: { value: 0, validate: RED.validators.number() }
495
+ },
496
+ inputs: 1,
497
+ outputs: 1,
498
+ icon: "datahub-input.svg",
499
+ label: function () {
500
+ if (this.name) return this.name;
501
+ if (this.mode === 'manual_single' && this.singleName) return this.singleName;
502
+ return "u-OS " + (this.providerId || "Data Hub");
503
+ },
504
+ paletteLabel: "u-OS Data Hub",
505
+ oneditprepare: function () {
506
+ const node = this;
507
+
508
+ // --- 1. Mode Visibility Logic ---
509
+ const modeSelect = $('#node-input-mode');
510
+ const autoRow = $('#mode-auto-row');
511
+ const manualSingleRow = $('#mode-manual-single-row');
512
+ const manualMultiRow = $('#mode-manual-multi-row');
513
+
514
+ // Set default if missing (backward compat)
515
+ if (!$('#node-input-mode').val()) {
516
+ $('#node-input-mode').val('auto');
517
+ }
518
+
519
+ const updateVisibility = () => {
520
+ const mode = modeSelect.val();
521
+ autoRow.hide();
522
+ manualSingleRow.hide();
523
+ manualMultiRow.hide();
524
+
525
+ if (mode === 'auto') autoRow.show();
526
+ else if (mode === 'manual_single') manualSingleRow.show();
527
+ else if (mode === 'manual_multi') manualMultiRow.show();
528
+ };
529
+
530
+ modeSelect.on('change', updateVisibility);
531
+ updateVisibility(); // Initial trigger
532
+
533
+ // --- 2. Existing "Auto" Logic (Providers & Checkboxes) ---
534
+ const $config = $('#node-input-connection');
535
+ const $providerRefresh = $('#node-input-check-custom-provider');
536
+ const $variables = $('input[type="checkbox"]');
537
+ const $variablesHidden = $('#node-input-variablesText');
538
+ const $varsRefresh = $('#node-input-refresh-vars');
539
+
540
+ let initialProvider = node.providerId;
541
+ let providerRequest = 0;
542
+
543
+ // Define Helpers (providerValue, providerLabel etc) from original code
544
+ const providerValue = (p) => p.id || p.providerId || p.name;
545
+ const providerLabel = (p) => p.name || p.displayName || p.providerId || p.id;
546
+
547
+ // --- 3. Existing Loading Logic ---
548
+ const setProviderStatus = (msg) => { /* No-op or custom div if needed */ };
549
+ const setProviderValue = (val) => { $('#node-input-providerId').val(val); };
550
+
551
+ const setVariableStatus = (msg) => { $('#vars-status').text(msg); };
552
+
553
+ const normalizeVariables = (payload) => {
554
+ if (Array.isArray(payload)) return payload;
555
+ if (payload && Array.isArray(payload.variables)) return payload.variables;
556
+ return [];
557
+ };
558
+
559
+ const normalizeProviders = (payload) => {
560
+ if (Array.isArray(payload)) return payload;
561
+ if (payload && Array.isArray(payload.providers)) return payload.providers;
562
+ return [];
563
+ };
564
+
565
+ const variableValue = (v) => v.key || v.name || v.path || v.id;
566
+ const variableLabel = (v) => v.tagName || v.key || v.name || v.path || v.id;
567
+
568
+ const syncVariablesField = () => {
569
+ const selected = [];
570
+ varsContainer.find('input[type="checkbox"]:checked').each(function () {
571
+ selected.push($(this).val());
572
+ });
573
+ varsHiddenInput.val(selected.join(','));
574
+ setVariableStatus(`${selected.length} selected`);
575
+ };
576
+
577
+ const loadVariables = (providerId, force = false) => {
578
+ const configId = $config.val();
579
+ if (!configId || !providerId) return;
580
+
581
+ setVariableStatus('Loading...');
582
+ varsContainer.empty().append('<div style="padding:10px; text-align:center; color:#888;"><i class="fa fa-spinner fa-spin"></i> Loading variables...</div>');
583
+
584
+ $.getJSON(`uos/providers/${configId}/${providerId}/variables`)
585
+ .done((payload) => {
586
+ const list = normalizeVariables(payload);
587
+ varsContainer.empty();
588
+ if (!list.length) {
589
+ varsContainer.append('<div style="padding:10px; text-align:center; color:#888;">No variables found for this provider.</div>');
590
+ setVariableStatus('No variables found');
591
+ return;
592
+ }
593
+
594
+ const currentVal = varsHiddenInput.val().split(',');
595
+ // Sort by name/key
596
+ list.sort((a, b) => (variableLabel(a) || '').localeCompare(variableLabel(b) || ''));
597
+
598
+ list.forEach((v) => {
599
+ const val = variableValue(v);
600
+ const lbl = variableLabel(v);
601
+ const checked = currentVal.includes(val) ? 'checked' : '';
602
+ const row = $('<div/>').appendTo(varsContainer);
603
+ $('<label/>').css({ display: 'block', cursor: 'pointer' })
604
+ .append($('<input type="checkbox"/>').val(val).prop('checked', !!checked).on('change', syncVariablesField))
605
+ .append($('<span/>').text(' ' + lbl))
606
+ .appendTo(row);
607
+ });
608
+ syncVariablesField();
609
+ })
610
+ .fail((xhr) => {
611
+ const error = xhr?.responseJSON?.error;
612
+ let msg = error || xhr.statusText || 'Failed to load variables.';
613
+ if (xhr.status === 404 && error === 'config not found') {
614
+ msg = 'Config not deployed yet. Please Deploy first.';
615
+ }
616
+ varsContainer.empty().append(`<div style="padding:10px; color:#d66;">${msg}</div>`);
617
+ setVariableStatus('Error loading variables');
618
+ });
619
+ };
620
+
621
+ const populateProviders = (payload) => {
622
+ const list = normalizeProviders(payload);
623
+ providerSelect.empty();
624
+ if (!list.length) {
625
+ providerSelect.append('<option value="">No providers found</option>').prop('disabled', true);
626
+ return;
627
+ }
628
+ let foundInitial = false;
629
+ list.forEach((prov) => {
630
+ const val = providerValue(prov);
631
+ if (!val) return;
632
+ const opt = $('<option/>').val(val).text(providerLabel(prov));
633
+ providerSelect.append(opt);
634
+ if (val === initialProvider) foundInitial = true;
635
+ });
636
+ providerSelect.prop('disabled', false);
637
+
638
+ if (initialProvider && foundInitial) {
639
+ providerSelect.val(initialProvider);
640
+ } else if (initialProvider) {
641
+ // Preserve unknown provider (e.g. from manual entry or offline)
642
+ providerSelect.append($('<option/>').val(initialProvider).text(initialProvider + ' (unknown)'));
643
+ providerSelect.val(initialProvider);
644
+ }
645
+
646
+ if (!providerSelect.val()) {
647
+ providerSelect.val(providerSelect.find('option:first').val());
648
+ }
649
+
650
+ // Trigger variable load for selected
651
+ const selected = providerSelect.val();
652
+ if (selected) loadVariables(selected);
653
+ };
654
+
655
+ const loadProviders = (force = false) => {
656
+ const configId = $config.val();
657
+ if (!configId) {
658
+ providerSelect.empty().append('<option value="">Select Config Node first</option>').prop('disabled', true);
659
+ return;
660
+ }
661
+
662
+ const seq = ++providerRequest;
663
+ // Cache logic omitted for brevity/safety - simplified direct load
664
+ providerSelect.prop('disabled', true);
665
+
666
+ $.getJSON(`uos/providers/${configId}`)
667
+ .done((payload) => {
668
+ if (seq !== providerRequest) return;
669
+ populateProviders(payload);
670
+ })
671
+ .fail((xhr) => {
672
+ const error = xhr?.responseJSON?.error;
673
+ const msg = error || 'Failed to load providers';
674
+ providerSelect.empty().append('<option value="">Error loading providers</option>').prop('disabled', true);
675
+ });
676
+ };
677
+
678
+ // Event Listeners
679
+ providerSelect.on('change', () => {
680
+ const val = providerSelect.val();
681
+ varsHiddenInput.val('');
682
+ syncVariablesField();
683
+ loadVariables(val);
684
+ });
685
+
686
+ $providerRefresh.on('click', () => loadProviders(true));
687
+ $varsRefresh.on('click', () => {
688
+ const v = providerSelect.val();
689
+ if (v) loadVariables(v, true);
690
+ });
691
+
692
+ $('#node-input-select-all').on('click', () => {
693
+ varsContainer.find('input[type="checkbox"]').prop('checked', true).trigger('change');
694
+ });
695
+ $('#node-input-select-none').on('click', () => {
696
+ varsContainer.find('input[type="checkbox"]').prop('checked', false).trigger('change');
697
+ });
698
+
699
+ $config.on('change', () => {
700
+ setTimeout(() => loadProviders(false), 50);
701
+ });
702
+
703
+ // Initial Load
704
+ setTimeout(() => loadProviders(false), 100);
705
+
706
+
707
+ // --- 4. Manual Table Logic ---
708
+ const $manualList = $('#node-input-manual-def-list');
709
+ const $manualHidden = $('#node-input-manualVariables');
710
+
711
+ $manualList.editableList({
712
+ addItem: function (row, index, data) {
713
+ row.css({ display: 'flex', alignItems: 'center', gap: '5px' });
714
+
715
+ $('<input/>', { class: 'node-input-manual-name', type: 'text', placeholder: 'Name', style: 'flex:1;' })
716
+ .val(data.name || '')
717
+ .appendTo(row);
718
+
719
+ $('<input/>', { class: 'node-input-manual-id', type: 'number', placeholder: 'ID', style: 'width:80px;' })
720
+ .val(data.id || '')
721
+ .appendTo(row);
722
+ },
723
+ removable: true,
724
+ header: $('<div></div>').append(
725
+ $.parseHTML('<div style="display:flex; gap:5px; padding-left:5px;"><div style="flex:1;">Name (String)</div><div style="width:80px;">ID (Int)</div></div>')
726
+ ),
727
+ addButton: 'Add Mapping'
728
+ });
729
+
730
+ // Populate Table
731
+ const currentManual = $manualHidden.val() || '';
732
+ if (currentManual) {
733
+ currentManual.split(',').forEach(entry => {
734
+ const parts = entry.split(':');
735
+ if (parts.length === 2) {
736
+ const n = parts[0].trim();
737
+ const i = parts[1].trim();
738
+ if (n && i) $manualList.editableList('addItem', { name: n, id: i });
739
+ }
740
+ });
741
+ }
742
+
743
+ },
744
+ oneditsave: function () {
745
+ const $manualList = $('#node-input-manual-def-list');
746
+ const items = [];
747
+ $manualList.editableList('items').each(function () {
748
+ const name = $(this).find('.node-input-manual-name').val().trim();
749
+ const id = $(this).find('.node-input-manual-id').val().trim();
750
+ if (name && id) {
751
+ items.push(`${name}:${id}`);
752
+ }
753
+ });
754
+ $('#node-input-manualVariables').val(items.join(','));
755
+ }
@@ -27,30 +27,53 @@ module.exports = function (RED) {
27
27
  }
28
28
 
29
29
  this.providerId = config.providerId || 'sampleprovider';
30
- this.pollingInterval = parseInt(config.pollingInterval, 10) || 0; // ms, 0 = disabled (default)
30
+ this.providerId = config.providerId || 'sampleprovider';
31
+ this.pollingInterval = parseInt(config.pollingInterval, 10) || 0; // ms
32
+ this.mode = config.mode || 'auto';
33
+
31
34
  const text = config.variablesText || '';
32
35
  const manualText = config.manualVariables || '';
36
+ const singleName = config.singleName || '';
37
+ const singleId = config.singleId || '';
33
38
 
34
- // Parse variables: Standard list of names to filter
35
- this.variables = text
36
- .split(',')
37
- .map((entry) => (entry ? String(entry).trim() : ''))
38
- .filter((entry) => entry.length > 0);
39
-
40
- // Parse Manual Definitions "Name:ID"
39
+ // Initialize containers
40
+ this.variables = [];
41
41
  this.manualDefs = [];
42
- if (manualText) {
43
- manualText.split(',').forEach(entry => {
44
- let trimmed = entry ? String(entry).trim() : '';
45
- if (trimmed.includes(':')) {
46
- const parts = trimmed.split(':');
47
- const name = parts[0].trim();
48
- const id = parseInt(parts[1].trim(), 10);
49
- if (name && !isNaN(id)) {
50
- this.manualDefs.push({ id, key: name });
51
- }
42
+
43
+ // --- Mode-Based Initialization ---
44
+ if (this.mode === 'manual_single') {
45
+ // Mode: Manual Single
46
+ // Strictly use Single Name/ID. Ignore others.
47
+ if (singleName && singleId !== '') {
48
+ const id = parseInt(singleId, 10);
49
+ if (!isNaN(id)) {
50
+ this.manualDefs.push({ id, key: String(singleName).trim() });
52
51
  }
53
- });
52
+ }
53
+ }
54
+ else if (this.mode === 'manual_multi') {
55
+ // Mode: Manual Multi (Table)
56
+ if (manualText) {
57
+ manualText.split(',').forEach(entry => {
58
+ let trimmed = entry ? String(entry).trim() : '';
59
+ if (trimmed.includes(':')) {
60
+ const parts = trimmed.split(':');
61
+ const name = parts[0].trim();
62
+ const id = parseInt(parts[1].trim(), 10);
63
+ if (name && !isNaN(id)) {
64
+ this.manualDefs.push({ id, key: name });
65
+ }
66
+ }
67
+ });
68
+ }
69
+ }
70
+ else {
71
+ // Mode: Auto (Default)
72
+ // Use the standard checkbox list
73
+ this.variables = text
74
+ .split(',')
75
+ .map((entry) => (entry ? String(entry).trim() : ''))
76
+ .filter((entry) => entry.length > 0);
54
77
  }
55
78
 
56
79
  let nc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-uos-nats",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "description": "Node-RED nodes for u-OS Data Hub via NATS",
5
5
  "repository": {
6
6
  "type": "git",