@sailingrotevista/rotevista-dash 7.0.6 → 7.0.8

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.
Files changed (2) hide show
  1. package/debug.html +642 -147
  2. package/package.json +1 -1
package/debug.html CHANGED
@@ -1,9 +1,9 @@
1
1
  <!DOCTYPE html>
2
- <html lang="it">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SignalK Data Debugger & Logger CSV (Pro v6.0)</title>
6
+ <title>SignalK Data Debugger & CSV Dumper (Pro v6.0)</title>
7
7
  <style>
8
8
  body {
9
9
  background-color: #121212;
@@ -26,76 +26,169 @@
26
26
  button.stop { background: #f44336; }
27
27
  button.secondary { background: #555; }
28
28
  button.action { background: #2196F3; } /* Tasto Copia Blu */
29
+
30
+ button:disabled {
31
+ background: #2a2a2a !important;
32
+ color: #666 !important;
33
+ cursor: not-allowed !important;
34
+ border-color: #333 !important;
35
+ }
36
+
29
37
  #status { font-weight: bold; color: #f44336; margin-left: auto; }
30
38
  #status.online { color: #4CAF50; }
39
+ #status.scanning { color: #ff9800; }
31
40
 
32
- /* Pannello dei filtri avanzati */
41
+ /* GRIGLIA WORKSPACE: AFFIANCA SELEZIONE E TABELLA VALORI */
42
+ .workspace-grid {
43
+ display: flex;
44
+ flex-direction: row;
45
+ flex-shrink: 0;
46
+ margin-bottom: 15px;
47
+ min-height: 0;
48
+ height: 380px; /* Altezza fissa dei pannelli superiori */
49
+ }
50
+
51
+ /* COLONNA SINISTRA: Pannello dei filtri ad albero */
33
52
  .filter-container {
34
53
  background: #1a1a1a;
35
54
  border: 1px solid #333;
36
55
  border-radius: 8px;
37
56
  padding: 15px;
38
- margin-bottom: 20px;
39
- flex-shrink: 0;
57
+ box-sizing: border-box;
58
+ width: 45%; /* Larghezza iniziale di default */
59
+ min-width: 200px;
60
+ max-width: 80%;
61
+ display: flex;
62
+ flex-direction: column;
63
+ overflow: hidden; /* Forza lo scorrimento dei figli all'interno dell'altezza fissa */
64
+ flex: none; /* Impedisce a Flexbox di sovrascrivere la larghezza impostata via JS durante il drag */
40
65
  }
41
66
  .presets {
42
67
  margin: 10px 0;
43
68
  display: flex;
44
69
  gap: 10px;
45
70
  flex-wrap: wrap;
71
+ flex-shrink: 0;
46
72
  }
47
- .checkbox-grid {
48
- display: grid;
49
- grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
50
- gap: 8px;
51
- margin-top: 12px;
73
+
74
+ /* INPUT BARRA DI RICERCA VELOCE */
75
+ .search-input {
76
+ width: 100%;
77
+ padding: 8px 12px;
78
+ background: #111;
79
+ color: #fff;
80
+ border: 1px solid #333;
81
+ border-radius: 6px;
82
+ box-sizing: border-box;
83
+ font-family: monospace;
84
+ font-size: 11px;
85
+ margin-top: 10px;
86
+ outline: none;
87
+ transition: border-color 0.2s;
88
+ flex-shrink: 0;
89
+ }
90
+ .search-input:focus {
91
+ border-color: #4CAF50;
92
+ }
93
+
94
+ /* DIVISIONE ORIZZONTALE RIDIMENSIONABILE A PIACIMENTO (SPLITTER DRAG BAR) */
95
+ .splitter {
96
+ width: 6px;
97
+ background: #222;
98
+ cursor: col-resize;
99
+ flex-shrink: 0;
100
+ transition: background 0.15s ease;
101
+ border-radius: 3px;
102
+ margin: 0 5px;
103
+ touch-action: none; /* Impedisce lo scorrimento della pagina web su tablet durante il drag */
104
+ }
105
+ .splitter:hover, .splitter.dragging {
106
+ background: #4CAF50;
107
+ }
108
+
109
+ /* COLONNA DESTRA: Pannello Tabella Valori Attuali */
110
+ .summary-panel {
111
+ background: #1a1a1a;
112
+ border: 1px solid #333;
113
+ border-radius: 8px;
114
+ padding: 15px;
115
+ box-sizing: border-box;
116
+ flex-grow: 1; /* Occupa tutto lo spazio a destra dello splitter */
117
+ min-width: 200px;
118
+ overflow-y: auto;
119
+ }
120
+
121
+ /* STRUTTURA AD ALBERO NIDIFICATO INTERNO */
122
+ .trees-container {
123
+ margin-top: 15px;
52
124
  border-top: 1px solid #222;
53
- padding-top: 10px;
125
+ padding-top: 15px;
126
+ flex-grow: 1; /* Occupa tutto lo spazio interno rimasto nel contenitore fisso */
127
+ overflow-y: auto;
128
+ display: block; /* Cambiato a block per garantire il ricalcolo dello scroll interno */
54
129
  }
55
- .checkbox-item {
130
+ .tree-node {
131
+ display: flex;
132
+ flex-direction: column;
133
+ margin-left: 18px;
134
+ }
135
+ .tree-node.root-level {
136
+ margin-left: 0;
137
+ margin-bottom: 6px;
138
+ background: #0d0d0d;
139
+ border: 1px solid #222;
140
+ border-radius: 6px;
141
+ overflow: hidden;
142
+ }
143
+ .node-row {
56
144
  display: flex;
57
145
  align-items: center;
58
146
  gap: 8px;
59
- font-size: 11px;
60
- color: #ccc;
61
- cursor: pointer;
147
+ padding: 6px 10px;
148
+ font-size: 12px;
149
+ user-select: none;
62
150
  }
63
- .checkbox-item input {
151
+ .root-level > .node-row {
152
+ background: #181818;
153
+ font-weight: bold;
154
+ color: #4CAF50;
155
+ border-bottom: 1px solid #222;
156
+ }
157
+ .node-toggle {
64
158
  cursor: pointer;
159
+ color: #888;
160
+ font-weight: bold;
161
+ width: 24px;
162
+ text-align: center;
163
+ font-size: 11px;
65
164
  }
66
-
67
- /* Layout a due pannelli (Sopra Tabella, Sotto Log) */
68
- .panels-container {
165
+ .node-toggle:hover {
166
+ color: #4CAF50;
167
+ }
168
+ .node-label {
169
+ color: #ccc;
170
+ font-size: 11px;
171
+ }
172
+ .node-children {
69
173
  display: flex;
70
174
  flex-direction: column;
71
- gap: 20px;
72
- flex-grow: 1;
73
- min-height: 0; /* Essenziale per far funzionare lo scroll nel flexbox */
175
+ border-left: 1px dashed #2c2c2c;
176
+ margin-left: 11px;
177
+ padding-left: 5px;
74
178
  }
75
179
 
76
- /* Pannello Superiore: Tabella Valori Attuali */
77
- .summary-panel {
78
- flex: 0 0 auto;
79
- background: #1a1a1a;
80
- border: 1px solid #333;
81
- border-radius: 8px;
82
- padding: 10px;
83
- overflow-x: auto;
84
- max-height: 250px;
85
- overflow-y: auto;
86
- }
87
180
  table {
88
181
  width: 100%;
89
182
  border-collapse: collapse;
90
183
  font-size: 13px;
91
- table-layout: fixed; /* BLOCCO RIGIDO DELLE COLONNE (Evita ridimensionamenti continui) */
184
+ table-layout: fixed; /* BLOCCO RIGIDO DELLE COLONNE */
92
185
  }
93
186
  th, td {
94
187
  border: 1px solid #333;
95
188
  padding: 6px 8px;
96
189
  text-align: left;
97
190
  overflow: hidden;
98
- word-wrap: break-word; /* Forza l'andamento a capo dei testi lunghi dentro le colonne fisse */
191
+ word-wrap: break-word; /* Forza l'andamento a capo dei testi lunghi */
99
192
  }
100
193
  th { background-color: #222; color: #aaa; text-transform: uppercase; position: sticky; top: 0; z-index: 10; }
101
194
  .val-cell { min-width: 120px; }
@@ -107,15 +200,15 @@
107
200
  .time { color: #90caf9; font-size: 11px; }
108
201
  .raw { color: #777; font-size: 10px; display: block; margin-top: 2px; }
109
202
 
110
- /* Pannello Inferiore: Log Sequenziale in Formato Testo (simil-CSV) */
203
+ /* PANNELLO INFERIORE: Log Sequenziale in Formato Testo (Spazio flessibile rimanente) */
111
204
  .log-panel {
112
- flex: 1 1 auto;
205
+ flex-grow: 1; /* Occupa tutto lo spazio verticale rimanente della finestra */
113
206
  background: #0a0a0a;
114
207
  border: 1px solid #333;
115
208
  border-radius: 8px;
116
209
  display: flex;
117
210
  flex-direction: column;
118
- min-height: 200px;
211
+ min-height: 150px;
119
212
  }
120
213
  .log-header {
121
214
  background: #222;
@@ -159,41 +252,50 @@
159
252
  </head>
160
253
  <body>
161
254
 
162
- <h1>SignalK Data Debugger & Logger CSV (Pro v6.0)</h1>
255
+ <h1>SignalK Data Debugger & CSV Dumper (Pro v6.0)</h1>
163
256
 
164
257
  <div class="controls">
165
258
  <label>Server IP:</label>
166
- <input type="text" id="server-ip" value="192.168.111.240:3000">
167
- <button id="btn-connect" onclick="toggleConnection()">Connetti</button>
168
- <span id="status">DISCONNESSO</span>
259
+ <input type="text" id="server-ip" value="venus.local:3000">
260
+ <button id="btn-scan" onclick="startScan()">Scan Network (10s)</button>
261
+ <button id="btn-listen" onclick="toggleListening()" class="secondary" disabled>Listen</button>
262
+ <span id="status">DISCONNECTED</span>
169
263
  </div>
170
264
 
171
- <!-- AREA FILTRI AVANZATA CON CHECKBOX E PRESET -->
172
- <div class="filter-container">
173
- <span style="font-weight: bold; color: #4CAF50; font-size: 13px;">Seleziona percorsi da scrivere nel Log CSV:</span>
174
- <div class="presets">
175
- <button class="secondary" onclick="applyPreset('all')">Seleziona Tutto</button>
176
- <button class="secondary" style="background-color: #2c3e50;" onclick="applyPreset('twd')">Analisi Bussola Meteo (TWD/Vento/Rotta)</button>
177
- <button class="secondary" style="background-color: #0088cc;" onclick="applyPreset('gps')">Solo GPS / Rotte</button>
178
- <button class="secondary" onclick="applyPreset('none')">Deseleziona Tutto</button>
179
- </div>
180
- <div id="path-checkboxes" class="checkbox-grid">
181
- <!-- Generate via JS -->
265
+ <!-- CONTENITORE PRINCIPALE DI LAVORO CON STRUTTURA FLUIDA ED ELEMENTI INTERATTIVI -->
266
+ <div class="workspace-grid" id="grid-container">
267
+
268
+ <!-- COLONNA SINISTRA: AREA FILTRI AVANZATA CON ALBERO GERARCHICO -->
269
+ <div class="filter-container" id="pane-left">
270
+ <span style="font-weight: bold; color: #4CAF50; font-size: 13px;">Select paths to write to CSV Dump (Complete Hierarchical Tree):</span>
271
+ <div class="presets">
272
+ <button class="secondary" onclick="applyPreset('all')">Select All</button>
273
+ <button class="secondary" style="background-color: #2c3e50;" onclick="applyPreset('twd')">Weather Compass Analysis (TWD/Wind/Course)</button>
274
+ <button class="secondary" style="background-color: #0088cc;" onclick="applyPreset('gps')">GPS / Course Only</button>
275
+ <button class="secondary" onclick="applyPreset('none')">Deselect All</button>
276
+ </div>
277
+
278
+ <!-- INPUT BARRA DI RICERCA VELOCE -->
279
+ <input type="text" id="tree-search" class="search-input" placeholder="Search path... (e.g. solar, temp, gps, batteries)" oninput="filterTree(this.value)">
280
+
281
+ <div id="path-trees-container" class="trees-container">
282
+ <!-- Categorie ed alberi gerarchici autogenerati dinamicamente via JS al termine della scansione -->
283
+ </div>
182
284
  </div>
183
- </div>
184
285
 
185
- <div class="panels-container">
186
-
187
- <!-- PANNELLO TABELLA RIASSUNTIVA -->
188
- <div class="summary-panel">
286
+ <!-- DIVISIONE ORIZZONTALE RIDIMENSIONABILE A PIACIMENTO (SPLITTER DRAG BAR) -->
287
+ <div class="splitter" id="drag-splitter"></div>
288
+
289
+ <!-- COLONNA DESTRA: PANNELLO TABELLA RIASSUNTIVA VALORI IN TEMPO REALE -->
290
+ <div class="summary-panel" id="pane-right">
189
291
  <table>
190
292
  <thead>
191
293
  <tr>
192
294
  <!-- BLOCCO RIGIDO DELLE LARGHEZZE DELLE COLONNE -->
193
- <th width="25%">Path (Percorso)</th>
194
- <th width="31.5%">Valore Convertito</th>
195
- <th width="31.5%">Sorgente (Sensore - Dettagliato)</th>
196
- <th width="12%">Ultimo Ricevuto</th>
295
+ <th width="30%">Path</th>
296
+ <th width="33%">Converted Value</th>
297
+ <th width="22%">Source (Detailed Sensor)</th>
298
+ <th width="15%">Last Received</th>
197
299
  </tr>
198
300
  </thead>
199
301
  <tbody id="data-table">
@@ -202,56 +304,54 @@
202
304
  </table>
203
305
  </div>
204
306
 
205
- <!-- PANNELLO LOG SEQUENZIALE (CSV) -->
206
- <div class="log-panel">
207
- <div class="log-header">
208
- <span>Event Log CSV (Timestamp, Path, Value, Unit, Source, Spike)</span>
209
- <div style="display: flex; align-items: center;">
210
- <span id="copy-notification">Copiato!</span>
211
- <button class="action" onclick="copyLogCSV()" style="padding: 4px 10px; font-size: 11px; margin-right: 10px;">Copia CSV</button>
212
- <button class="secondary" onclick="clearLog()" style="padding: 4px 10px; font-size: 11px;">Pulisci</button>
213
- </div>
307
+ </div>
308
+
309
+ <!-- PANNELLO LOG SEQUENZIALE (CSV) - POSIZIONATO A TUTTA LARGHEZZA SUL FONDO -->
310
+ <div class="log-panel">
311
+ <div class="log-header">
312
+ <span>CSV Event Dump (Timestamp, Path, Value, Unit, Source, Spike)</span>
313
+ <div style="display: flex; align-items: center;">
314
+ <span id="copy-notification">Copied!</span>
315
+ <button class="action" onclick="copyLogCSV()" style="padding: 4px 10px; font-size: 11px; margin-right: 10px;">Copy Dump</button>
316
+ <button class="secondary" onclick="clearLog()" style="padding: 4px 10px; font-size: 11px;">Clear</button>
214
317
  </div>
215
-
216
- <textarea id="log-container" readonly></textarea>
217
318
  </div>
218
-
319
+
320
+ <textarea id="log-container" readonly></textarea>
219
321
  </div>
220
322
 
221
323
  <script>
222
- const pathsToWatch = [
223
- "navigation.position",
224
- "navigation.position.latitude",
225
- "navigation.position.longitude",
226
- "navigation.speedThroughWater",
227
- "navigation.speedOverGround",
228
- "navigation.headingTrue",
229
- "navigation.courseOverGroundTrue",
230
- "environment.wind.speedApparent",
231
- "environment.wind.angleApparent",
232
- "environment.wind.speedTrue",
233
- "environment.wind.angleTrueWater",
234
- "environment.wind.directionTrue",
235
- "environment.depth.belowTransducer"
236
- ];
324
+ // Registro dello stato dati e del database ad albero
325
+ const registeredPaths = new Set();
326
+ let pathTree = {}; // Albero gerarchico nidificato in memoria RAM
237
327
 
238
328
  let socket = null;
239
329
  let logLinesArray = [];
240
330
  const MAX_LOG_LINES = 1000;
241
331
 
332
+ // Stato del sistema
333
+ let isScanning = false;
334
+ let isListening = false;
335
+ let scanTimer = null;
336
+ let scanCountdown = 10;
337
+
242
338
  let lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
243
339
 
244
340
  const radToDeg = (rad) => rad * (180 / Math.PI);
245
341
  const msToKts = (ms) => ms * 1.94384;
246
342
 
247
- // Inizializza la Tabella Riassuntiva e genera le Checkbox
248
- const tbody = document.getElementById('data-table');
249
- const checkboxGrid = document.getElementById('path-checkboxes');
343
+ const csvHeader = "Timestamp,Path,Value,Unit,Source,IsSpike";
344
+ document.getElementById('log-container').value = csvHeader + "\n";
345
+
346
+ // Inserisce e mappa dinamicamente un nuovo percorso nella tabella e nella struttura ad albero logica
347
+ function registerNewPath(path) {
348
+ if (registeredPaths.has(path)) return;
349
+ registeredPaths.add(path);
250
350
 
251
- pathsToWatch.forEach(path => {
252
351
  const safeId = path.replace(/\./g, '-');
253
352
 
254
- // 1. Creazione righe Tabella
353
+ // 1. Creazione dinamica della riga nella tabella riassuntiva
354
+ const tbody = document.getElementById('data-table');
255
355
  const tr = document.createElement('tr');
256
356
  tr.id = `row-${safeId}`;
257
357
  tr.innerHTML = `
@@ -262,24 +362,240 @@
262
362
  `;
263
363
  tbody.appendChild(tr);
264
364
 
265
- // 2. Creazione Checkbox
266
- const label = document.createElement('label');
267
- label.className = 'checkbox-item';
268
- label.innerHTML = `
269
- <input type="checkbox" id="chk-${safeId}" checked>
270
- <span>${path}</span>
271
- `;
272
- checkboxGrid.appendChild(label);
365
+ // 2. Costruzione della struttura gerarchica in memoria (RAM) per la generazione successiva
366
+ const parts = path.split('.');
367
+ let current = pathTree;
368
+ parts.forEach((part, index) => {
369
+ if (!current[part]) {
370
+ current[part] = {
371
+ _name: part,
372
+ _fullPath: parts.slice(0, index + 1).join('.'),
373
+ _isLeaf: (index === parts.length - 1),
374
+ _children: {}
375
+ };
376
+ }
377
+ current = current[part]._children;
378
+ });
379
+ }
380
+
381
+ // Disegna ricorsivamente l'albero gerarchico nel DOM (Lazy Rendering)
382
+ function renderTreeDOM(node, container, isRoot = false) {
383
+ const keys = Object.keys(node).sort();
384
+
385
+ keys.forEach(key => {
386
+ const item = node[key];
387
+ const hasChildren = Object.keys(item._children).length > 0;
388
+
389
+ const nodeDiv = document.createElement('div');
390
+ nodeDiv.className = `tree-node ${isRoot ? 'root-level' : ''} ${hasChildren ? 'tree-branch' : 'tree-leaf'}`;
391
+
392
+ const rowDiv = document.createElement('div');
393
+ rowDiv.className = 'node-row';
394
+
395
+ // 1. Pulsante Espandi/Collassa per i rami dotati di figli
396
+ if (hasChildren) {
397
+ const toggleSpan = document.createElement('span');
398
+ toggleSpan.className = 'node-toggle';
399
+ toggleSpan.innerText = '[+]'; // Compresso di default all'avvio
400
+ toggleSpan.onclick = (e) => {
401
+ e.stopPropagation();
402
+ const childContainer = nodeDiv.querySelector(':scope > .node-children');
403
+ if (childContainer.style.display === 'none') {
404
+ childContainer.style.display = 'flex';
405
+ toggleSpan.innerText = '[-]';
406
+ } else {
407
+ childContainer.style.display = 'none';
408
+ toggleSpan.innerText = '[+]';
409
+ }
410
+ };
411
+ rowDiv.appendChild(toggleSpan);
412
+ } else {
413
+ const spacer = document.createElement('span');
414
+ spacer.className = 'node-toggle';
415
+ rowDiv.appendChild(spacer);
416
+ }
417
+
418
+ // 2. Checkbox di selezione
419
+ const chk = document.createElement('input');
420
+ chk.type = 'checkbox';
421
+ chk.checked = false; // Deselezionato di default per focalizzare lo spazio
422
+ chk.className = 'node-checkbox';
423
+
424
+ if (!hasChildren) {
425
+ const safeId = item._fullPath.replace(/\./g, '-');
426
+ chk.id = `chk-${safeId}`;
427
+ chk.dataset.path = item._fullPath;
428
+ } else {
429
+ const category = item._fullPath;
430
+ chk.id = `master-chk-${category.replace(/\./g, '-')}`;
431
+ chk.dataset.branchCategory = category;
432
+ }
433
+
434
+ rowDiv.appendChild(chk);
435
+
436
+ // 3. Etichetta del Nodo
437
+ const labelSpan = document.createElement('span');
438
+ labelSpan.className = 'node-label';
439
+ labelSpan.innerText = key;
440
+ rowDiv.appendChild(labelSpan);
441
+
442
+ nodeDiv.appendChild(rowDiv);
443
+
444
+ // 4. Creazione ricorsiva dei nodi figli (Sotto-Albero nascosto di default all'avvio)
445
+ if (hasChildren) {
446
+ const childrenDiv = document.createElement('div');
447
+ childrenDiv.className = 'node-children';
448
+ childrenDiv.style.display = 'none'; // Chiuso di default all'avvio
449
+ renderTreeDOM(item._children, childrenDiv, false);
450
+ nodeDiv.appendChild(childrenDiv);
451
+ }
452
+
453
+ container.appendChild(nodeDiv);
454
+ });
455
+ }
456
+
457
+ // DELEGAZIONE DEGLI EVENTI SUL CONTENITORE: Gestisce lo scorrimento e la propagazione degli stati
458
+ document.getElementById('path-trees-container').addEventListener('change', (e) => {
459
+ const chk = e.target;
460
+ if (!chk || chk.type !== 'checkbox') return;
461
+
462
+ // 1. PROPAGAZIONE VERSO IL BASSO: Se clicchi un nodo padre, spunta/disattiva tutti i suoi discendenti
463
+ const nodeDiv = chk.closest('.tree-node');
464
+ if (nodeDiv) {
465
+ const childCheckboxes = nodeDiv.querySelectorAll('.node-checkbox');
466
+ childCheckboxes.forEach(c => {
467
+ c.checked = chk.checked;
468
+ });
469
+ }
470
+
471
+ // 2. CASCATA VERSO L'ALTO: Ricalcola gli stati dei padri (incluso lo stato indeterminato "-")
472
+ updateAllTreeMasterCheckboxes();
473
+
474
+ // 3. FILTRAGGIO DINAMICO DELLA TABELLA: Aggiorna istantaneamente la visibilità dei valori
475
+ updateTableVisibility();
476
+
477
+ // 4. FILTRAGGIO DINAMICO DEL DUMP CSV: Aggiorna lo schermo dei log al cambio di selezione
478
+ renderLogTextArea();
273
479
  });
274
480
 
275
- const csvHeader = "Timestamp,Path,Value,Unit,Source,IsSpike";
276
- document.getElementById('log-container').value = csvHeader + "\n";
481
+ // Filtra l'albero gerarchico visivo in tempo reale con espansione automatica dei rami corrispondenti
482
+ function filterTree(query) {
483
+ const q = query.toLowerCase().trim();
484
+ const leaves = document.querySelectorAll('.tree-leaf');
485
+ const branches = Array.from(document.querySelectorAll('.tree-branch')).reverse(); // Scansione invertita per propagare lo stato ai nonni
486
+
487
+ if (q === "") {
488
+ // Ripristina la visibilità totale e richiude i rami se la query viene cancellata
489
+ leaves.forEach(leaf => {
490
+ leaf.style.display = '';
491
+ });
492
+ branches.forEach(branch => {
493
+ branch.style.display = '';
494
+ const childContainer = branch.querySelector(':scope > .node-children');
495
+ const toggleSpan = branch.querySelector(':scope > .node-row > .node-toggle');
496
+ if (childContainer && toggleSpan) {
497
+ childContainer.style.display = 'none';
498
+ toggleSpan.innerText = '[+]';
499
+ }
500
+ });
501
+ updateTableVisibility();
502
+ renderLogTextArea();
503
+ return;
504
+ }
505
+
506
+ // 1. Filtra tutte le foglie (Parametri finali) e AUTO-SELEZIONA quelle corrispondenti
507
+ leaves.forEach(leaf => {
508
+ const labelEl = leaf.querySelector('.node-label');
509
+ const chk = leaf.querySelector('input');
510
+ if (!labelEl || !chk) return;
511
+
512
+ const label = labelEl.innerText.toLowerCase();
513
+ const path = (chk.dataset.path || "").toLowerCase();
514
+
515
+ if (label.includes(q) || path.includes(q)) {
516
+ leaf.style.display = ''; // Corrispondenza trovata
517
+ chk.checked = true; // AUTO-SELEZIONE dei parametri corrispondenti!
518
+ } else {
519
+ leaf.style.display = 'none'; // Nasconde se non corrisponde
520
+ }
521
+ });
522
+
523
+ // 2. Filtra e auto-espande i rami genitori dal basso verso l'alto
524
+ branches.forEach(branch => {
525
+ const childContainer = branch.querySelector(':scope > .node-children');
526
+ const toggleSpan = branch.querySelector(':scope > .node-row > .node-toggle');
527
+ if (!childContainer) return;
528
+
529
+ // Conta quanti sotto-nodi (sia rami che foglie) sono rimasti visibili
530
+ const visibleChildren = childContainer.querySelectorAll('.tree-node:not([style*="display: none"])');
531
+
532
+ if (visibleChildren.length > 0) {
533
+ branch.style.display = ''; // Mostra il ramo genitore poiché contiene elementi corrispondenti
534
+ if (toggleSpan) {
535
+ childContainer.style.display = 'flex'; // Auto-espande la cartella per mostrare il match
536
+ toggleSpan.innerText = '[-]';
537
+ }
538
+ } else {
539
+ branch.style.display = 'none'; // Nasconde l'intera cartella se nessun figlio corrisponde alla ricerca
540
+ }
541
+ });
542
+
543
+ // 3. Ricalcola gli stati indeterminati dei genitori e aggiorna la tabella in tempo reale
544
+ updateAllTreeMasterCheckboxes();
545
+ updateTableVisibility();
546
+ renderLogTextArea(); // Filtra il Dump CSV sullo schermo ad ogni input della ricerca!
547
+ }
548
+
549
+ // Aggiorna istantaneamente la visibilità dei valori della tabella in base alle spunte e alla barra di ricerca
550
+ function updateTableVisibility() {
551
+ const queryInput = document.getElementById('tree-search');
552
+ const q = queryInput ? queryInput.value.toLowerCase().trim() : "";
553
+
554
+ registeredPaths.forEach(path => {
555
+ const safeId = path.replace(/\./g, '-');
556
+ const chk = document.getElementById(`chk-${safeId}`);
557
+ const row = document.getElementById(`row-${safeId}`);
558
+
559
+ if (row) {
560
+ if (isScanning) {
561
+ // Durante la scansione mostriamo tutte le righe scoperte in tempo reale
562
+ row.style.display = '';
563
+ } else {
564
+ // Fuori scansione la riga è visibile solo se è spuntata E se il percorso corrisponde alla ricerca attiva (se presente)
565
+ const matchesQuery = q === "" || path.toLowerCase().includes(q);
566
+ const isChecked = chk && chk.checked;
567
+
568
+ if (isChecked && matchesQuery) {
569
+ row.style.display = ''; // Mostra
570
+ } else {
571
+ row.style.display = 'none'; // Nasconde
572
+ }
573
+ }
574
+ }
575
+ });
576
+ }
577
+
578
+ // Ricalcola ricorsivamente gli stati (Checked, Unchecked, Indeterminate) dal basso verso l'alto
579
+ function updateAllTreeMasterCheckboxes() {
580
+ const branchNodes = Array.from(document.querySelectorAll('.tree-branch')).reverse();
581
+
582
+ branchNodes.forEach(branchNode => {
583
+ const masterCheckbox = branchNode.querySelector(':scope > .node-row > .node-checkbox');
584
+ if (!masterCheckbox) return;
585
+
586
+ const childCheckboxes = branchNode.querySelectorAll('.tree-leaf input[type="checkbox"]');
587
+ const checkedCount = Array.from(childCheckboxes).filter(c => c.checked).length;
588
+
589
+ masterCheckbox.checked = (checkedCount === childCheckboxes.length);
590
+ masterCheckbox.indeterminate = (checkedCount > 0 && checkedCount < childCheckboxes.length);
591
+ });
592
+ }
277
593
 
278
594
  // =========================================================
279
- // PRESET RAPIDI DI SELEZIONE
595
+ // PRESET RAPIDI DI SELEZIONE (Mappati sulle foglie attive)
280
596
  // =========================================================
281
597
  function applyPreset(preset) {
282
- pathsToWatch.forEach(path => {
598
+ registeredPaths.forEach(path => {
283
599
  const safeId = path.replace(/\./g, '-');
284
600
  const chk = document.getElementById(`chk-${safeId}`);
285
601
  if (!chk) return;
@@ -289,15 +605,16 @@
289
605
  } else if (preset === 'none') {
290
606
  chk.checked = false;
291
607
  } else if (preset === 'gps') {
292
- // Solo GPS, SOG e COG
293
608
  const isGps = path.includes('position') || path === 'navigation.speedOverGround' || path === 'navigation.courseOverGroundTrue';
294
609
  chk.checked = isGps;
295
610
  } else if (preset === 'twd') {
296
- // Analisi strategica bussola vento ( Heading, COG, AWA, TWA, TWD, TWS, AWS )
297
- const isTwd = path.includes('wind') || path === 'navigation.headingTrue' || path === 'navigation.courseOverGroundTrue';
611
+ const isTwd = path.includes('wind') || path === 'navigation.headingTrue' || path === 'navigation.courseOverGroundTrue' || path === 'navigation.headingMagnetic';
298
612
  chk.checked = isTwd;
299
613
  }
300
614
  });
615
+ updateAllTreeMasterCheckboxes();
616
+ updateTableVisibility(); // Sincronizza la tabella visiva al cambio di preset
617
+ renderLogTextArea(); // Filtra dinamicamente anche i log CSV in base al preset!
301
618
  }
302
619
 
303
620
  // =========================================================
@@ -318,41 +635,148 @@
318
635
  }
319
636
 
320
637
  // =========================================================
321
- // GESTIONE CONNESSIONE WEBSOCKET
638
+ // FASE 1: AVVIO SCANSIONE DI SCOPERTA (DISCOVERY - 10s)
322
639
  // =========================================================
323
- function toggleConnection() {
324
- const btn = document.getElementById('btn-connect');
640
+ function startScan() {
641
+ if (socket && socket.readyState === WebSocket.OPEN) {
642
+ socket.close();
643
+ }
644
+
645
+ // Pulisce l'albero logico e l'interfaccia prima del nuovo avvio
646
+ registeredPaths.clear();
647
+ pathTree = {};
648
+ document.getElementById('path-trees-container').innerHTML = '';
649
+ document.getElementById('data-table').innerHTML = '';
650
+ document.getElementById('tree-search').value = ''; // Resetta il testo della barra di ricerca
651
+
652
+ const btnScan = document.getElementById('btn-scan');
653
+ const btnListen = document.getElementById('btn-listen');
325
654
  const status = document.getElementById('status');
326
655
  const ip = document.getElementById('server-ip').value;
327
656
 
328
- if (socket && socket.readyState === WebSocket.OPEN) {
329
- socket.close();
330
- btn.innerText = "Connetti";
331
- btn.className = "";
332
- status.innerText = "DISCONNESSO";
333
- status.className = "";
657
+ isScanning = true;
658
+ isListening = false;
659
+ scanCountdown = 10;
660
+
661
+ btnScan.disabled = true;
662
+ btnScan.innerText = `Scanning... ${scanCountdown}s`;
663
+ btnListen.disabled = true;
664
+ btnListen.className = "secondary";
665
+ btnListen.innerText = "Listen";
666
+
667
+ status.innerText = "SCAN IN PROGRESS";
668
+ status.className = "scanning";
669
+
670
+ socket = new WebSocket(`ws://${ip}/signalk/v1/stream?subscribe=self`);
671
+
672
+ socket.onopen = () => {
673
+ addLogEntry(new Date().toISOString(), "SYSTEM", "Scan Phase Started", "-", ip, false);
674
+ };
675
+
676
+ scanTimer = setInterval(() => {
677
+ scanCountdown--;
678
+ if (scanCountdown > 0) {
679
+ btnScan.innerText = `Scanning... ${scanCountdown}s`;
680
+ } else {
681
+ // Scansione terminata! Generazione ricorsiva dell'albero gerarchico reale
682
+ clearInterval(scanTimer);
683
+ scanTimer = null;
684
+
685
+ isScanning = false; // 1. Imposta lo stato a false PRIMA di chiudere la connessione
686
+ socket.close(); // 2. Scatena l'evento onclose (che ora ignorerà l'interruzione di scansione)
687
+
688
+ const treeContainer = document.getElementById('path-trees-container');
689
+ renderTreeDOM(pathTree, treeContainer, true); // Genera ricorsivamente l'albero
690
+ updateAllTreeMasterCheckboxes(); // Allinea gli stati dei padri
691
+ updateTableVisibility(); // Inizializza i filtri della tabella (Nasconde tutto all'avvio)
692
+
693
+ btnScan.disabled = false;
694
+ btnScan.innerText = "Scan Network (10s)";
695
+
696
+ btnListen.disabled = false;
697
+ btnListen.className = "";
698
+
699
+ status.innerText = "SCAN COMPLETED";
700
+ status.className = "online";
701
+ addLogEntry(new Date().toISOString(), "SYSTEM", "Scan Phase Finished", "-", "-", false);
702
+ }
703
+ }, 1000);
704
+
705
+ socket.onmessage = (event) => {
706
+ try {
707
+ const data = JSON.parse(event.data);
708
+ if (data.updates) {
709
+ data.updates.forEach(update => {
710
+ const sourceName = extractSourceIdentifier(update);
711
+ const timeISO = new Date(update.timestamp).toISOString();
712
+ const timeUI = new Date(update.timestamp).toLocaleTimeString() + '.' + new Date(update.timestamp).getMilliseconds().toString().padStart(3, '0');
713
+
714
+ if (update.values) {
715
+ update.values.forEach(v => {
716
+ // Popola e accumula i percorsi in memoria ad albero
717
+ registerNewPath(v.path);
718
+ processData(v.path, v.value, sourceName, timeISO, timeUI, false); // Nessun log scritto
719
+ });
720
+ }
721
+ });
722
+ }
723
+ } catch (err) {
724
+ console.error("Errore scansione:", err);
725
+ }
726
+ };
727
+
728
+ socket.onclose = () => {
729
+ if (isScanning) {
730
+ clearInterval(scanTimer);
731
+ isScanning = false;
732
+ btnScan.disabled = false;
733
+ btnScan.innerText = "Scan Network (10s)";
734
+ status.innerText = "SCAN FAILED";
735
+ status.className = "";
736
+ }
737
+ };
738
+ }
739
+
740
+ // =========================================================
741
+ // FASE 2: ASCOLTO SELETTIVO (LOG ATTIVI SOLO SE SPUNTATI)
742
+ // =========================================================
743
+ function toggleListening() {
744
+ const btnListen = document.getElementById('btn-listen');
745
+ const btnScan = document.getElementById('btn-scan');
746
+ const status = document.getElementById('status');
747
+ const ip = document.getElementById('server-ip').value;
748
+
749
+ if (isListening) {
750
+ if (socket) socket.close();
751
+ isListening = false;
752
+ btnListen.innerText = "Listen";
753
+ btnListen.className = "";
754
+ btnScan.disabled = false;
755
+ status.innerText = "LISTENING STOPPED";
756
+ status.className = "online";
757
+ addLogEntry(new Date().toISOString(), "SYSTEM", "Listening Stopped", "-", "-", false);
334
758
  return;
335
759
  }
336
760
 
337
- btn.innerText = "Disconnetti";
338
- btn.className = "stop";
339
- status.innerText = "CONNESSIONE IN CORSO...";
340
- status.className = "";
761
+ isListening = true;
762
+ btnScan.disabled = true;
763
+ btnListen.innerText = "Stop Dump";
764
+ btnListen.className = "stop";
765
+
766
+ status.innerText = "DUMPING VALUES...";
767
+ status.className = "online";
341
768
 
342
769
  lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
343
770
 
344
771
  socket = new WebSocket(`ws://${ip}/signalk/v1/stream?subscribe=self`);
345
772
 
346
773
  socket.onopen = () => {
347
- status.innerText = "CONNESSO (In ascolto...)";
348
- status.className = "online";
349
- addLogEntry(new Date().toISOString(), "SYSTEM", "Connected", "-", ip, false);
774
+ addLogEntry(new Date().toISOString(), "SYSTEM", "Selective Listening Started", "-", ip, false);
350
775
  };
351
776
 
352
777
  socket.onmessage = (event) => {
353
778
  try {
354
779
  const data = JSON.parse(event.data);
355
-
356
780
  if (data.updates) {
357
781
  data.updates.forEach(update => {
358
782
  const sourceName = extractSourceIdentifier(update);
@@ -361,36 +785,58 @@
361
785
 
362
786
  if (update.values) {
363
787
  update.values.forEach(v => {
364
- if (pathsToWatch.includes(v.path)) {
365
- processData(v.path, v.value, sourceName, timeISO, timeUI);
788
+ const safeId = v.path.replace(/\./g, '-');
789
+ const chk = document.getElementById(`chk-${safeId}`);
790
+
791
+ // Verifica sia la spunta sia l'effettiva visibilità a schermo (non nascosto da ricerca!)
792
+ let isLogged = false;
793
+ if (chk && chk.checked) {
794
+ const leaf = chk.closest('.tree-leaf');
795
+ if (leaf && leaf.style.display !== 'none') {
796
+ isLogged = true;
797
+ }
798
+ }
799
+
800
+ if (isLogged) {
801
+ // Scrive e logga nel CSV solo se la checkbox corrispondente è spuntata e visibile!
802
+ processData(v.path, v.value, sourceName, timeISO, timeUI, true);
803
+ } else {
804
+ // Se non è spuntata o è nascosta, aggiorna passivamente la tabella silenziando i log
805
+ if (registeredPaths.has(v.path)) {
806
+ processData(v.path, v.value, sourceName, timeISO, timeUI, false);
807
+ }
366
808
  }
367
809
  });
368
810
  }
369
811
  });
370
812
  }
371
813
  } catch (err) {
372
- console.error("Errore elaborazione messaggio: ", err);
814
+ console.error("Errore ascolto:", err);
373
815
  }
374
816
  };
375
817
 
376
818
  socket.onclose = () => {
377
- btn.innerText = "Connetti";
378
- btn.className = "";
379
- status.innerText = "DISCONNESSO";
380
- status.className = "";
381
- addLogEntry(new Date().toISOString(), "SYSTEM", "Disconnected", "-", "-", false);
819
+ if (isListening) {
820
+ isListening = false;
821
+ btnListen.innerText = "Listen";
822
+ btnListen.className = "";
823
+ btnScan.disabled = false;
824
+ status.innerText = "DISCONNESSO";
825
+ status.className = "";
826
+ addLogEntry(new Date().toISOString(), "SYSTEM", "Connection Closed", "-", "-", false);
827
+ }
382
828
  };
383
-
384
- socket.onerror = (error) => {
385
- status.innerText = "ERRORE";
386
- addLogEntry(new Date().toISOString(), "SYSTEM", "Error", "-", "-", true);
829
+
830
+ socket.onerror = () => {
831
+ status.innerText = "CONNECTION ERROR";
832
+ addLogEntry(new Date().toISOString(), "SYSTEM", "Connection Error", "-", "-", true);
387
833
  };
388
834
  }
389
835
 
390
836
  // =========================================================
391
837
  // ELABORAZIONE DATI E SPIKE DETECTION
392
838
  // =========================================================
393
- function processData(path, rawValue, source, timeISO, timeUI) {
839
+ function processData(path, rawValue, source, timeISO, timeUI, writeToCsv) {
394
840
  let formattedVal = "---";
395
841
  let unit = "";
396
842
  let isSpike = false;
@@ -398,7 +844,7 @@
398
844
  if (rawValue !== null && rawValue !== undefined) {
399
845
  if (path === "navigation.position") {
400
846
  if (typeof rawValue === 'object' && rawValue.latitude !== undefined && rawValue.longitude !== undefined) {
401
- formattedVal = `${rawValue.latitude.toFixed(6)};${rawValue.longitude.toFixed(6)}`;
847
+ formattedVal = `${rawValue.latitude.toFixed(6)}`;
402
848
  unit = "lat;lon";
403
849
  } else {
404
850
  formattedVal = "Invalid Position";
@@ -439,10 +885,8 @@
439
885
  // 1. Aggiorna la Tabella in alto
440
886
  updateTableRow(path, formattedVal, unit, rawValue, source, timeUI);
441
887
 
442
- // 2. Aggiungi riga al Log CSV (Solo ed esclusivamente se la checkbox di questo percorso è spuntata!)
443
- const safeId = path.replace(/\./g, '-');
444
- const chk = document.getElementById(`chk-${safeId}`);
445
- if (chk && chk.checked) {
888
+ // 2. Aggiungi riga al Log CSV
889
+ if (writeToCsv) {
446
890
  addLogEntry(timeISO, path, formattedVal, unit, source, isSpike);
447
891
  }
448
892
  }
@@ -471,7 +915,7 @@
471
915
  srcCell.innerHTML = `<span class="source">${source}</span>`;
472
916
  }
473
917
 
474
- const timeCell = document.getElementById(`time-${safeId}`);
918
+ const timeCell = document.getElementById('time-' + safeId);
475
919
  if (timeCell) {
476
920
  timeCell.innerHTML = `<span class="time">${timeUI}</span>`;
477
921
  }
@@ -502,9 +946,28 @@
502
946
  renderLogTextArea();
503
947
  }
504
948
 
949
+ // Filtra ricorsivamente i log visualizzati nel Dump CSV in tempo reale in base alla selezione attiva e visibile
505
950
  function renderLogTextArea() {
506
951
  const textArea = document.getElementById('log-container');
507
- textArea.value = csvHeader + "\n" + logLinesArray.join("\n");
952
+ if (!textArea) return;
953
+
954
+ const filteredLines = logLinesArray.filter(line => {
955
+ const parts = line.split(',');
956
+ const path = parts[1]; // L'indice 1 corrisponde al percorso dati del pacchetto
957
+ if (!path || path === "Path" || path === "SYSTEM") return true; // Preserva le intestazioni e i messaggi di connessione di sistema
958
+
959
+ const safeId = path.replace(/\./g, '-');
960
+ const chk = document.getElementById(`chk-${safeId}`);
961
+
962
+ // Filtro integrato: la linea viene visualizzata solo se spuntata E visibile (non esclusa dalla ricerca)
963
+ if (chk && chk.checked) {
964
+ const leaf = chk.closest('.tree-leaf');
965
+ return leaf && leaf.style.display !== 'none';
966
+ }
967
+ return false;
968
+ });
969
+
970
+ textArea.value = csvHeader + "\n" + filteredLines.join("\n");
508
971
  textArea.scrollTop = textArea.scrollHeight;
509
972
  }
510
973
 
@@ -527,7 +990,7 @@
527
990
  }
528
991
  } catch (err) {
529
992
  console.error("Copia fallita: ", err);
530
- alert("Errore nella copia degli appunti. Usa CTRL+C / CMD+C manualmente.");
993
+ alert("Error copying to clipboard. Please use CTRL+C / CMD+C manually.");
531
994
  }
532
995
  }
533
996
 
@@ -537,6 +1000,38 @@
537
1000
  setTimeout(() => notif.style.opacity = 0, 2000);
538
1001
  window.getSelection().removeAllRanges();
539
1002
  }
1003
+
1004
+ // =========================================================
1005
+ // LOGICA DI GESTIONE DELLO SPLITTER ORIZZONTALE (DRAG BAR)
1006
+ // =========================================================
1007
+ const splitter = document.getElementById('drag-splitter');
1008
+ const leftPane = document.getElementById('pane-left');
1009
+ const gridContainer = document.getElementById('grid-container');
1010
+
1011
+ splitter.addEventListener('pointerdown', (e) => {
1012
+ e.preventDefault();
1013
+ splitter.classList.add('dragging');
1014
+
1015
+ const startX = e.clientX;
1016
+ const startWidth = leftPane.getBoundingClientRect().width;
1017
+ const containerWidth = gridContainer.getBoundingClientRect().width;
1018
+
1019
+ const onPointerMove = (moveEvent) => {
1020
+ const deltaX = moveEvent.clientX - startX;
1021
+ // Limita il ridimensionamento tra 200px e l'80% dello schermo per sicurezza
1022
+ const newWidth = Math.max(200, Math.min(containerWidth * 0.8, startWidth + deltaX));
1023
+ leftPane.style.width = `${(newWidth / containerWidth) * 100}%`;
1024
+ };
1025
+
1026
+ const onPointerUp = () => {
1027
+ splitter.classList.remove('dragging');
1028
+ document.removeEventListener('pointermove', onPointerMove);
1029
+ document.removeEventListener('pointerup', onPointerUp);
1030
+ };
1031
+
1032
+ document.addEventListener('pointermove', onPointerMove);
1033
+ document.addEventListener('pointerup', onPointerUp);
1034
+ });
540
1035
  </script>
541
1036
  </body>
542
1037
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "7.0.6",
3
+ "version": "7.0.8",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {