@sailingrotevista/rotevista-dash 7.0.45 → 8.0.2

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.

Potentially problematic release.


This version of @sailingrotevista/rotevista-dash might be problematic. Click here for more details.

package/debug.html DELETED
@@ -1,1037 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>SignalK Data Debugger & CSV Dumper (Pro v6.0)</title>
7
- <style>
8
- body {
9
- background-color: #121212;
10
- color: #eee;
11
- font-family: monospace;
12
- margin: 0;
13
- padding: 20px;
14
- display: flex;
15
- flex-direction: column;
16
- height: 100vh;
17
- box-sizing: border-box;
18
- }
19
- h1 { color: #4CAF50; border-bottom: 1px solid #333; padding-bottom: 10px; margin-top: 0; flex-shrink: 0; }
20
-
21
- /* Controlli in alto */
22
- .controls { margin-bottom: 15px; display: flex; gap: 10px; align-items: center; flex-shrink: 0; flex-wrap: wrap; }
23
- input[type="text"] { padding: 8px; background: #222; color: #fff; border: 1px solid #444; width: 200px; }
24
- button { padding: 8px 15px; background: #4CAF50; color: white; border: none; cursor: pointer; font-weight: bold; border-radius: 4px; }
25
- button:hover { opacity: 0.8; }
26
- button.stop { background: #f44336; }
27
- button.secondary { background: #555; }
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
-
37
- #status { font-weight: bold; color: #f44336; margin-left: auto; }
38
- #status.online { color: #4CAF50; }
39
- #status.scanning { color: #ff9800; }
40
-
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 */
52
- .filter-container {
53
- background: #1a1a1a;
54
- border: 1px solid #333;
55
- border-radius: 8px;
56
- padding: 15px;
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 */
65
- }
66
- .presets {
67
- margin: 10px 0;
68
- display: flex;
69
- gap: 10px;
70
- flex-wrap: wrap;
71
- flex-shrink: 0;
72
- }
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;
124
- border-top: 1px solid #222;
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 */
129
- }
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 {
144
- display: flex;
145
- align-items: center;
146
- gap: 8px;
147
- padding: 6px 10px;
148
- font-size: 12px;
149
- user-select: none;
150
- }
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 {
158
- cursor: pointer;
159
- color: #888;
160
- font-weight: bold;
161
- width: 24px;
162
- text-align: center;
163
- font-size: 11px;
164
- }
165
- .node-toggle:hover {
166
- color: #4CAF50;
167
- }
168
- .node-label {
169
- color: #ccc;
170
- font-size: 11px;
171
- }
172
- .node-children {
173
- display: flex;
174
- flex-direction: column;
175
- border-left: 1px dashed #2c2c2c;
176
- margin-left: 11px;
177
- padding-left: 5px;
178
- }
179
-
180
- table {
181
- width: 100%;
182
- border-collapse: collapse;
183
- font-size: 13px;
184
- table-layout: fixed; /* BLOCCO RIGIDO DELLE COLONNE */
185
- }
186
- th, td {
187
- border: 1px solid #333;
188
- padding: 6px 8px;
189
- text-align: left;
190
- overflow: hidden;
191
- word-wrap: break-word; /* Forza l'andamento a capo dei testi lunghi */
192
- }
193
- th { background-color: #222; color: #aaa; text-transform: uppercase; position: sticky; top: 0; z-index: 10; }
194
- .val-cell { min-width: 120px; }
195
-
196
- /* Stili Testo Tabella */
197
- .value { color: #64ffda; font-weight: bold; font-size: 15px; }
198
- .unit { color: #888; font-size: 11px; margin-left: 5px; }
199
- .source { color: #ffb74d; font-size: 11px; word-break: break-all; }
200
- .time { color: #90caf9; font-size: 11px; }
201
- .raw { color: #777; font-size: 10px; display: block; margin-top: 2px; }
202
-
203
- /* PANNELLO INFERIORE: Log Sequenziale in Formato Testo (Spazio flessibile rimanente) */
204
- .log-panel {
205
- flex-grow: 1; /* Occupa tutto lo spazio verticale rimanente della finestra */
206
- background: #0a0a0a;
207
- border: 1px solid #333;
208
- border-radius: 8px;
209
- display: flex;
210
- flex-direction: column;
211
- min-height: 150px;
212
- }
213
- .log-header {
214
- background: #222;
215
- padding: 8px 15px;
216
- border-bottom: 1px solid #333;
217
- display: flex;
218
- justify-content: space-between;
219
- align-items: center;
220
- border-radius: 8px 8px 0 0;
221
- font-size: 13px;
222
- font-weight: bold;
223
- color: #aaa;
224
- }
225
-
226
- /* Area di testo del Log */
227
- textarea#log-container {
228
- flex-grow: 1;
229
- background: transparent;
230
- color: #ddd;
231
- border: none;
232
- padding: 10px;
233
- font-family: monospace;
234
- font-size: 12px;
235
- line-height: 1.4;
236
- resize: none;
237
- outline: none;
238
- white-space: pre;
239
- overflow-wrap: normal;
240
- overflow-x: auto;
241
- }
242
-
243
- /* Notifica di Copia Riuscita */
244
- #copy-notification {
245
- color: #4CAF50;
246
- font-size: 12px;
247
- margin-right: 10px;
248
- opacity: 0;
249
- transition: opacity 0.5s;
250
- }
251
- </style>
252
- </head>
253
- <body>
254
-
255
- <h1>SignalK Data Debugger & CSV Dumper (Pro v6.0)</h1>
256
-
257
- <div class="controls">
258
- <label>Server IP:</label>
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>
263
- </div>
264
-
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>
284
- </div>
285
-
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">
291
- <table>
292
- <thead>
293
- <tr>
294
- <!-- BLOCCO RIGIDO DELLE LARGHEZZE DELLE COLONNE -->
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>
299
- </tr>
300
- </thead>
301
- <tbody id="data-table">
302
- <!-- Generate via JS -->
303
- </tbody>
304
- </table>
305
- </div>
306
-
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>
317
- </div>
318
- </div>
319
-
320
- <textarea id="log-container" readonly></textarea>
321
- </div>
322
-
323
- <script>
324
- // Registro dello stato dati e del database ad albero
325
- const registeredPaths = new Set();
326
- let pathTree = {}; // Albero gerarchico nidificato in memoria RAM
327
-
328
- let socket = null;
329
- let logLinesArray = [];
330
- const MAX_LOG_LINES = 1000;
331
-
332
- // Stato del sistema
333
- let isScanning = false;
334
- let isListening = false;
335
- let scanTimer = null;
336
- let scanCountdown = 10;
337
-
338
- let lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
339
-
340
- const radToDeg = (rad) => rad * (180 / Math.PI);
341
- const msToKts = (ms) => ms * 1.94384;
342
-
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);
350
-
351
- const safeId = path.replace(/\./g, '-');
352
-
353
- // 1. Creazione dinamica della riga nella tabella riassuntiva
354
- const tbody = document.getElementById('data-table');
355
- const tr = document.createElement('tr');
356
- tr.id = `row-${safeId}`;
357
- tr.innerHTML = `
358
- <td><strong>${path}</strong></td>
359
- <td id="val-${safeId}" class="val-cell"><span class="value">---</span></td>
360
- <td id="src-${safeId}"><span class="source">---</span></td>
361
- <td id="time-${safeId}"><span class="time">---</span></td>
362
- `;
363
- tbody.appendChild(tr);
364
-
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();
479
- });
480
-
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
- }
593
-
594
- // =========================================================
595
- // PRESET RAPIDI DI SELEZIONE (Mappati sulle foglie attive)
596
- // =========================================================
597
- function applyPreset(preset) {
598
- registeredPaths.forEach(path => {
599
- const safeId = path.replace(/\./g, '-');
600
- const chk = document.getElementById(`chk-${safeId}`);
601
- if (!chk) return;
602
-
603
- if (preset === 'all') {
604
- chk.checked = true;
605
- } else if (preset === 'none') {
606
- chk.checked = false;
607
- } else if (preset === 'gps') {
608
- const isGps = path.includes('position') || path === 'navigation.speedOverGround' || path === 'navigation.courseOverGroundTrue';
609
- chk.checked = isGps;
610
- } else if (preset === 'twd') {
611
- const isTwd = path.includes('wind') || path === 'navigation.headingTrue' || path === 'navigation.courseOverGroundTrue' || path === 'navigation.headingMagnetic';
612
- chk.checked = isTwd;
613
- }
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!
618
- }
619
-
620
- // =========================================================
621
- // ESTRAZIONE AVANZATA DELLA SORGENTE (GPS / NMEA)
622
- // =========================================================
623
- function extractSourceIdentifier(update) {
624
- if (update.$source) return update.$source;
625
- if (update.source) {
626
- if (typeof update.source === 'object') {
627
- return update.source.label ||
628
- update.source.talker ||
629
- update.source.src ||
630
- (update.source.device ? `${update.source.device}:${update.source.pgn || ''}` : JSON.stringify(update.source));
631
- }
632
- return String(update.source);
633
- }
634
- return "Unknown-Source";
635
- }
636
-
637
- // =========================================================
638
- // FASE 1: AVVIO SCANSIONE DI SCOPERTA (DISCOVERY - 10s)
639
- // =========================================================
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');
654
- const status = document.getElementById('status');
655
- const ip = document.getElementById('server-ip').value;
656
-
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);
758
- return;
759
- }
760
-
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";
768
-
769
- lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
770
-
771
- socket = new WebSocket(`ws://${ip}/signalk/v1/stream?subscribe=self`);
772
-
773
- socket.onopen = () => {
774
- addLogEntry(new Date().toISOString(), "SYSTEM", "Selective Listening Started", "-", ip, false);
775
- };
776
-
777
- socket.onmessage = (event) => {
778
- try {
779
- const data = JSON.parse(event.data);
780
- if (data.updates) {
781
- data.updates.forEach(update => {
782
- const sourceName = extractSourceIdentifier(update);
783
- const timeISO = new Date(update.timestamp).toISOString();
784
- const timeUI = new Date(update.timestamp).toLocaleTimeString() + '.' + new Date(update.timestamp).getMilliseconds().toString().padStart(3, '0');
785
-
786
- if (update.values) {
787
- update.values.forEach(v => {
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
- }
808
- }
809
- });
810
- }
811
- });
812
- }
813
- } catch (err) {
814
- console.error("Errore ascolto:", err);
815
- }
816
- };
817
-
818
- socket.onclose = () => {
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
- }
828
- };
829
-
830
- socket.onerror = () => {
831
- status.innerText = "CONNECTION ERROR";
832
- addLogEntry(new Date().toISOString(), "SYSTEM", "Connection Error", "-", "-", true);
833
- };
834
- }
835
-
836
- // =========================================================
837
- // ELABORAZIONE DATI E SPIKE DETECTION
838
- // =========================================================
839
- function processData(path, rawValue, source, timeISO, timeUI, writeToCsv) {
840
- let formattedVal = "---";
841
- let unit = "";
842
- let isSpike = false;
843
-
844
- if (rawValue !== null && rawValue !== undefined) {
845
- if (path === "navigation.position") {
846
- if (typeof rawValue === 'object' && rawValue.latitude !== undefined && rawValue.longitude !== undefined) {
847
- formattedVal = `${rawValue.latitude.toFixed(6)}`;
848
- unit = "lat;lon";
849
- } else {
850
- formattedVal = "Invalid Position";
851
- }
852
- } else if (path.includes("speed")) {
853
- formattedVal = msToKts(rawValue).toFixed(3);
854
- unit = "kts";
855
-
856
- if (path === "navigation.speedOverGround") {
857
- let diff = Math.abs(formattedVal - lastValues[path]);
858
- if (diff > 5.0 && lastValues[path] > 0) isSpike = true;
859
- lastValues[path] = formattedVal;
860
- }
861
-
862
- } else if (path.includes("angle") || path.includes("heading") || path.includes("course") || path.includes("direction")) {
863
- formattedVal = radToDeg(rawValue).toFixed(2);
864
- unit = "deg";
865
-
866
- if (path === "navigation.courseOverGroundTrue") {
867
- let diff = Math.abs(formattedVal - lastValues[path]);
868
- if (diff > 180) diff = 360 - diff;
869
- if (diff > 45 && lastValues[path] > 0) isSpike = true;
870
- lastValues[path] = formattedVal;
871
- }
872
-
873
- } else if (path.includes("depth")) {
874
- formattedVal = rawValue.toFixed(2);
875
- unit = "m";
876
- } else {
877
- if (typeof rawValue === 'number') {
878
- formattedVal = rawValue.toFixed(6);
879
- } else {
880
- formattedVal = String(rawValue);
881
- }
882
- }
883
- }
884
-
885
- // 1. Aggiorna la Tabella in alto
886
- updateTableRow(path, formattedVal, unit, rawValue, source, timeUI);
887
-
888
- // 2. Aggiungi riga al Log CSV
889
- if (writeToCsv) {
890
- addLogEntry(timeISO, path, formattedVal, unit, source, isSpike);
891
- }
892
- }
893
-
894
- function updateTableRow(path, val, unit, raw, source, timeUI) {
895
- const safeId = path.replace(/\./g, '-');
896
-
897
- let rawStr = "---";
898
- if (raw !== null && raw !== undefined) {
899
- if (typeof raw === 'number') {
900
- rawStr = raw.toFixed(4);
901
- } else if (typeof raw === 'object') {
902
- rawStr = JSON.stringify(raw);
903
- } else {
904
- rawStr = String(raw);
905
- }
906
- }
907
-
908
- const valCell = document.getElementById(`val-${safeId}`);
909
- if (valCell) {
910
- valCell.innerHTML = `<span class="value">${val}</span><span class="unit">${unit}</span><span class="raw">Raw: ${rawStr}</span>`;
911
- }
912
-
913
- const srcCell = document.getElementById(`src-${safeId}`);
914
- if (srcCell) {
915
- srcCell.innerHTML = `<span class="source">${source}</span>`;
916
- }
917
-
918
- const timeCell = document.getElementById('time-' + safeId);
919
- if (timeCell) {
920
- timeCell.innerHTML = `<span class="time">${timeUI}</span>`;
921
- }
922
-
923
- const row = document.getElementById(`row-${safeId}`);
924
- if (row) {
925
- row.style.backgroundColor = "#334a33";
926
- setTimeout(() => row.style.backgroundColor = "", 150);
927
- }
928
- }
929
-
930
- // =========================================================
931
- // GESTIONE DEL LOG CSV E COPIA APPUNTI
932
- // =========================================================
933
- function addLogEntry(timeISO, path, value, unit, source, isSpike) {
934
- const safeValue = (value.includes(',') || value.includes(';')) ? `"${value}"` : value;
935
- const safeSource = (source.includes(',') || source.includes(';')) ? `"${source}"` : source;
936
- const spikeFlag = isSpike ? "TRUE" : "FALSE";
937
-
938
- const csvLine = `${timeISO},${path},${safeValue},${unit},${safeSource},${spikeFlag}`;
939
-
940
- logLinesArray.push(csvLine);
941
-
942
- if (logLinesArray.length > MAX_LOG_LINES) {
943
- logLinesArray.shift();
944
- }
945
-
946
- renderLogTextArea();
947
- }
948
-
949
- // Filtra ricorsivamente i log visualizzati nel Dump CSV in tempo reale in base alla selezione attiva e visibile
950
- function renderLogTextArea() {
951
- const textArea = document.getElementById('log-container');
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");
971
- textArea.scrollTop = textArea.scrollHeight;
972
- }
973
-
974
- function clearLog() {
975
- logLinesArray = [];
976
- renderLogTextArea();
977
- }
978
-
979
- function copyLogCSV() {
980
- const textArea = document.getElementById('log-container');
981
- textArea.select();
982
- textArea.setSelectionRange(0, 999999);
983
-
984
- try {
985
- if (navigator.clipboard) {
986
- navigator.clipboard.writeText(textArea.value).then(() => showCopyNotif());
987
- } else {
988
- document.execCommand('copy');
989
- showCopyNotif();
990
- }
991
- } catch (err) {
992
- console.error("Copia fallita: ", err);
993
- alert("Error copying to clipboard. Please use CTRL+C / CMD+C manually.");
994
- }
995
- }
996
-
997
- function showCopyNotif() {
998
- const notif = document.getElementById('copy-notification');
999
- notif.style.opacity = 1;
1000
- setTimeout(() => notif.style.opacity = 0, 2000);
1001
- window.getSelection().removeAllRanges();
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
- });
1035
- </script>
1036
- </body>
1037
- </html>