@sailingrotevista/rotevista-dash 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.js CHANGED
@@ -313,7 +313,7 @@ function connect() {
313
313
  }
314
314
 
315
315
  try {
316
- socket = new WebSocket(`ws://${serverAddress}/signalk/v1/stream?subscribe=all`);
316
+ socket = new WebSocket(`ws://${serverAddress}/signalk/v1/stream?subscribe=self`);
317
317
 
318
318
  socket.onopen = () => {
319
319
  ui.status.className = "online";
package/debug.html ADDED
@@ -0,0 +1,411 @@
1
+ <!DOCTYPE html>
2
+ <html lang="it">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SignalK Data Debugger & Logger CSV</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: 20px; display: flex; gap: 10px; align-items: center; flex-shrink: 0; flex-wrap: wrap; }
23
+ input[type="text"], select { padding: 8px; background: #222; color: #fff; border: 1px solid #444; }
24
+ input[type="text"] { width: 200px; }
25
+ button { padding: 8px 15px; background: #4CAF50; color: white; border: none; cursor: pointer; font-weight: bold; border-radius: 4px; }
26
+ button:hover { opacity: 0.8; }
27
+ button.stop { background: #f44336; }
28
+ button.secondary { background: #555; }
29
+ button.action { background: #2196F3; } /* Tasto Copia Blu */
30
+ #status { font-weight: bold; color: #f44336; margin-left: auto; }
31
+ #status.online { color: #4CAF50; }
32
+
33
+ /* Layout a due pannelli (Sopra Tabella, Sotto Log) */
34
+ .panels-container {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 20px;
38
+ flex-grow: 1;
39
+ min-height: 0; /* Essenziale per far funzionare lo scroll nel flexbox */
40
+ }
41
+
42
+ /* Pannello Superiore: Tabella Valori Attuali */
43
+ .summary-panel {
44
+ flex: 0 0 auto;
45
+ background: #1a1a1a;
46
+ border: 1px solid #333;
47
+ border-radius: 8px;
48
+ padding: 10px;
49
+ overflow-x: auto;
50
+ }
51
+ table { width: 100%; border-collapse: collapse; font-size: 14px; }
52
+ th, td { border: 1px solid #333; padding: 8px; text-align: left; }
53
+ th { background-color: #222; color: #aaa; text-transform: uppercase; position: sticky; top: 0; }
54
+ .val-cell { min-width: 120px; }
55
+
56
+ /* Stili Testo Tabella */
57
+ .value { color: #64ffda; font-weight: bold; font-size: 16px; }
58
+ .unit { color: #888; font-size: 12px; margin-left: 5px; }
59
+ .source { color: #ffb74d; font-size: 12px; }
60
+ .time { color: #90caf9; font-size: 12px; }
61
+ .raw { color: #777; font-size: 11px; display: block; margin-top: 4px; }
62
+
63
+ /* Pannello Inferiore: Log Sequenziale in Formato Testo (simil-CSV) */
64
+ .log-panel {
65
+ flex: 1 1 auto;
66
+ background: #0a0a0a;
67
+ border: 1px solid #333;
68
+ border-radius: 8px;
69
+ display: flex;
70
+ flex-direction: column;
71
+ min-height: 200px;
72
+ }
73
+ .log-header {
74
+ background: #222;
75
+ padding: 8px 15px;
76
+ border-bottom: 1px solid #333;
77
+ display: flex;
78
+ justify-content: space-between;
79
+ align-items: center;
80
+ border-radius: 8px 8px 0 0;
81
+ font-size: 14px;
82
+ font-weight: bold;
83
+ color: #aaa;
84
+ }
85
+
86
+ /* Area di testo del Log (Non più div separati, ma un unico blocco di testo preformattato) */
87
+ textarea#log-container {
88
+ flex-grow: 1;
89
+ background: transparent;
90
+ color: #ddd;
91
+ border: none;
92
+ padding: 10px;
93
+ font-family: monospace;
94
+ font-size: 12px;
95
+ line-height: 1.4;
96
+ resize: none;
97
+ outline: none;
98
+ white-space: pre;
99
+ overflow-wrap: normal;
100
+ overflow-x: auto;
101
+ }
102
+
103
+ /* Notifica di Copia Riuscita */
104
+ #copy-notification {
105
+ color: #4CAF50;
106
+ font-size: 12px;
107
+ margin-right: 10px;
108
+ opacity: 0;
109
+ transition: opacity 0.5s;
110
+ }
111
+ </style>
112
+ </head>
113
+ <body>
114
+
115
+ <h1>SignalK Data Debugger & Logger CSV</h1>
116
+
117
+ <div class="controls">
118
+ <label>Server IP:</label>
119
+ <input type="text" id="server-ip" value="192.168.111.240:3000">
120
+ <button id="btn-connect" onclick="toggleConnection()">Connetti</button>
121
+
122
+ <span style="margin-left: 20px; color:#aaa;">Filtra Log:</span>
123
+ <select id="log-filter">
124
+ <option value="all">Tutti i dati monitorati</option>
125
+ <option value="navigation.speedOverGround">Solo SOG (Velocità sul fondo)</option>
126
+ <option value="navigation.courseOverGroundTrue">Solo COG (Rotta sul fondo)</option>
127
+ <option value="wind">Solo Vento (TWS/TWA/AWS/AWA)</option>
128
+ <option value="depth">Solo Profondità</option>
129
+ </select>
130
+
131
+ <span id="status">DISCONNESSO</span>
132
+ </div>
133
+
134
+ <div class="panels-container">
135
+
136
+ <!-- PANNELLO TABELLA RIASSUNTIVA -->
137
+ <div class="summary-panel">
138
+ <table>
139
+ <thead>
140
+ <tr>
141
+ <th width="30%">Path (Percorso)</th>
142
+ <th width="20%">Valore Convertito</th>
143
+ <th width="30%">Sorgente (Sensore)</th>
144
+ <th width="20%">Ultimo Ricevuto</th>
145
+ </tr>
146
+ </thead>
147
+ <tbody id="data-table">
148
+ <!-- Generate via JS -->
149
+ </tbody>
150
+ </table>
151
+ </div>
152
+
153
+ <!-- PANNELLO LOG SEQUENZIALE (CSV) -->
154
+ <div class="log-panel">
155
+ <div class="log-header">
156
+ <span>Event Log CSV (Timestamp, Path, Value, Unit, Source, Spike)</span>
157
+ <div style="display: flex; align-items: center;">
158
+ <span id="copy-notification">Copiato!</span>
159
+ <button class="action" onclick="copyLogCSV()" style="padding: 4px 10px; font-size: 11px; margin-right: 10px;">Copia CSV</button>
160
+ <button class="secondary" onclick="clearLog()" style="padding: 4px 10px; font-size: 11px;">Pulisci</button>
161
+ </div>
162
+ </div>
163
+
164
+ <!-- Usiamo una textarea readonly per permettere la selezione e lo scroll perfetto di testi lunghi -->
165
+ <textarea id="log-container" readonly></textarea>
166
+ </div>
167
+
168
+ </div>
169
+
170
+ <script>
171
+ // Percorsi da monitorare
172
+ const pathsToWatch = [
173
+ "navigation.speedThroughWater",
174
+ "navigation.speedOverGround",
175
+ "navigation.headingTrue",
176
+ "navigation.courseOverGroundTrue",
177
+ "environment.wind.speedApparent",
178
+ "environment.wind.angleApparent",
179
+ "environment.wind.speedTrue",
180
+ "environment.wind.angleTrueWater",
181
+ "environment.depth.belowTransducer"
182
+ ];
183
+
184
+ let socket = null;
185
+ let logLinesArray = []; // Array per conservare le righe in memoria
186
+ const MAX_LOG_LINES = 1000; // Aumentato a 1000 righe per analisi lunghe in Excel
187
+
188
+ // Memoria per rilevare i picchi anomali (Spikes)
189
+ let lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
190
+
191
+ // Funzioni di conversione matematica
192
+ const radToDeg = (rad) => rad * (180 / Math.PI);
193
+ const msToKts = (ms) => ms * 1.94384;
194
+
195
+ // Inizializza la Tabella Riassuntiva
196
+ const tbody = document.getElementById('data-table');
197
+ pathsToWatch.forEach(path => {
198
+ const tr = document.createElement('tr');
199
+ tr.id = `row-${path.replace(/\./g, '-')}`;
200
+
201
+ tr.innerHTML = `
202
+ <td><strong>${path}</strong></td>
203
+ <td id="val-${path.replace(/\./g, '-')}" class="val-cell"><span class="value">---</span></td>
204
+ <td id="src-${path.replace(/\./g, '-')}"><span class="source">---</span></td>
205
+ <td id="time-${path.replace(/\./g, '-')}"><span class="time">---</span></td>
206
+ `;
207
+ tbody.appendChild(tr);
208
+ });
209
+
210
+ // Intestazione fissa del file CSV nel Log
211
+ const csvHeader = "Timestamp,Path,Value,Unit,Source,IsSpike";
212
+ document.getElementById('log-container').value = csvHeader + "\n";
213
+
214
+ // =========================================================
215
+ // GESTIONE CONNESSIONE WEBSOCKET
216
+ // =========================================================
217
+ function toggleConnection() {
218
+ const btn = document.getElementById('btn-connect');
219
+ const status = document.getElementById('status');
220
+ const ip = document.getElementById('server-ip').value;
221
+
222
+ if (socket && socket.readyState === WebSocket.OPEN) {
223
+ socket.close();
224
+ btn.innerText = "Connetti";
225
+ btn.className = "";
226
+ status.innerText = "DISCONNESSO";
227
+ status.className = "";
228
+ return;
229
+ }
230
+
231
+ btn.innerText = "Disconnetti";
232
+ btn.className = "stop";
233
+ status.innerText = "CONNESSIONE IN CORSO...";
234
+ status.className = "";
235
+
236
+ // Resetting spike logic on new connection
237
+ lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
238
+
239
+ socket = new WebSocket(`ws://${ip}/signalk/v1/stream?subscribe=self`);
240
+
241
+ socket.onopen = () => {
242
+ status.innerText = "CONNESSO (In ascolto...)";
243
+ status.className = "online";
244
+ addLogEntry(new Date().toISOString(), "SYSTEM", "Connected", "-", ip, false);
245
+ };
246
+
247
+ socket.onmessage = (event) => {
248
+ const data = JSON.parse(event.data);
249
+
250
+ if (data.updates) {
251
+ data.updates.forEach(update => {
252
+ const sourceName = update.source ? update.source.label : "Unknown";
253
+ // ISO String è lo standard perfetto per i file CSV ed Excel (es. 2023-10-25T17:00:56.123Z)
254
+ const timeISO = new Date(update.timestamp).toISOString();
255
+ const timeUI = new Date(update.timestamp).toLocaleTimeString() + '.' + new Date(update.timestamp).getMilliseconds().toString().padStart(3, '0');
256
+
257
+ if (update.values) {
258
+ update.values.forEach(v => {
259
+ if (pathsToWatch.includes(v.path)) {
260
+ processData(v.path, v.value, sourceName, timeISO, timeUI);
261
+ }
262
+ });
263
+ }
264
+ });
265
+ }
266
+ };
267
+
268
+ socket.onclose = () => {
269
+ btn.innerText = "Connetti";
270
+ btn.className = "";
271
+ status.innerText = "DISCONNESSO";
272
+ status.className = "";
273
+ addLogEntry(new Date().toISOString(), "SYSTEM", "Disconnected", "-", "-", false);
274
+ };
275
+
276
+ socket.onerror = (error) => {
277
+ status.innerText = "ERRORE";
278
+ addLogEntry(new Date().toISOString(), "SYSTEM", "Error", "-", "-", true);
279
+ };
280
+ }
281
+
282
+ // =========================================================
283
+ // ELABORAZIONE DATI E SPIKE DETECTION
284
+ // =========================================================
285
+ function processData(path, rawValue, source, timeISO, timeUI) {
286
+ let formattedVal = rawValue;
287
+ let unit = "";
288
+ let isSpike = false;
289
+
290
+ // Conversioni
291
+ if (rawValue !== null && rawValue !== undefined) {
292
+ if (path.includes("speed")) {
293
+ formattedVal = msToKts(rawValue).toFixed(3);
294
+ unit = "kts";
295
+
296
+ // Rilevamento Spike SOG (> 5 nodi in un colpo solo)
297
+ if (path === "navigation.speedOverGround") {
298
+ let diff = Math.abs(formattedVal - lastValues[path]);
299
+ if (diff > 5.0 && lastValues[path] > 0) isSpike = true;
300
+ lastValues[path] = formattedVal;
301
+ }
302
+
303
+ } else if (path.includes("angle") || path.includes("heading") || path.includes("course")) {
304
+ formattedVal = radToDeg(rawValue).toFixed(2);
305
+ unit = "deg";
306
+
307
+ // Rilevamento Spike COG (> 45 gradi improvviso)
308
+ if (path === "navigation.courseOverGroundTrue") {
309
+ let diff = Math.abs(formattedVal - lastValues[path]);
310
+ if (diff > 180) diff = 360 - diff;
311
+ if (diff > 45 && lastValues[path] > 0) isSpike = true;
312
+ lastValues[path] = formattedVal;
313
+ }
314
+
315
+ } else if (path.includes("depth")) {
316
+ formattedVal = rawValue.toFixed(2);
317
+ unit = "m";
318
+ }
319
+ }
320
+
321
+ // 1. Aggiorna la Tabella in alto
322
+ updateTableRow(path, formattedVal, unit, rawValue, source, timeUI);
323
+
324
+ // 2. Aggiungi riga al Log CSV (rispettando il filtro della Dropdown)
325
+ const filter = document.getElementById('log-filter').value;
326
+ if (filter === "all" || path.includes(filter)) {
327
+ addLogEntry(timeISO, path, formattedVal, unit, source, isSpike);
328
+ }
329
+ }
330
+
331
+ function updateTableRow(path, val, unit, raw, source, timeUI) {
332
+ const safeId = path.replace(/\./g, '-');
333
+ document.getElementById(`val-${safeId}`).innerHTML = `<span class="value">${val}</span><span class="unit">${unit}</span><span class="raw">Raw: ${raw.toFixed(4)}</span>`;
334
+ document.getElementById(`src-${safeId}`).innerHTML = `<span class="source">${source}</span>`;
335
+ document.getElementById(`time-${safeId}`).innerHTML = `<span class="time">${timeUI}</span>`;
336
+
337
+ const row = document.getElementById(`row-${safeId}`);
338
+ row.style.backgroundColor = "#334a33";
339
+ setTimeout(() => row.style.backgroundColor = "", 150);
340
+ }
341
+
342
+ // =========================================================
343
+ // GESTIONE DEL LOG CSV E COPIA APPUNTI
344
+ // =========================================================
345
+ function addLogEntry(timeISO, path, value, unit, source, isSpike) {
346
+ // Formattazione stringa CSV pulita (niente spazi inutili)
347
+ // Se c'è una virgola nel nome della sorgente, la mettiamo tra virgolette per non rompere il CSV
348
+ const safeSource = source.includes(',') ? `"${source}"` : source;
349
+ const spikeFlag = isSpike ? "TRUE" : "FALSE";
350
+
351
+ const csvLine = `${timeISO},${path},${value},${unit},${safeSource},${spikeFlag}`;
352
+
353
+ logLinesArray.push(csvLine);
354
+
355
+ // Mantieni la memoria leggera
356
+ if (logLinesArray.length > MAX_LOG_LINES) {
357
+ logLinesArray.shift(); // Rimuove la più vecchia
358
+ }
359
+
360
+ renderLogTextArea();
361
+ }
362
+
363
+ function renderLogTextArea() {
364
+ const textArea = document.getElementById('log-container');
365
+
366
+ // Ricostruisci il testo unendo l'intestazione e l'array con \n (a capo)
367
+ textArea.value = csvHeader + "\n" + logLinesArray.join("\n");
368
+
369
+ // Auto-scroll verso il basso
370
+ textArea.scrollTop = textArea.scrollHeight;
371
+ }
372
+
373
+ function clearLog() {
374
+ logLinesArray = [];
375
+ renderLogTextArea();
376
+ }
377
+
378
+ function copyLogCSV() {
379
+ const textArea = document.getElementById('log-container');
380
+ const notif = document.getElementById('copy-notification');
381
+
382
+ // Seleziona il testo
383
+ textArea.select();
384
+ textArea.setSelectionRange(0, 999999); // Per mobile
385
+
386
+ try {
387
+ // Copia negli appunti (API moderna Navigator o execCommand classico)
388
+ if (navigator.clipboard) {
389
+ navigator.clipboard.writeText(textArea.value).then(() => showCopyNotif());
390
+ } else {
391
+ document.execCommand('copy');
392
+ showCopyNotif();
393
+ }
394
+ } catch (err) {
395
+ console.error("Copia fallita: ", err);
396
+ alert("Errore nella copia degli appunti. Usa CTRL+C / CMD+C manualmente.");
397
+ }
398
+ }
399
+
400
+ function showCopyNotif() {
401
+ const notif = document.getElementById('copy-notification');
402
+ notif.style.opacity = 1;
403
+ setTimeout(() => notif.style.opacity = 0, 2000);
404
+
405
+ // Deseleziona il testo
406
+ window.getSelection().removeAllRanges();
407
+ }
408
+
409
+ </script>
410
+ </body>
411
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Dashboard pubblica per Bavaria 39 Rotevista",
5
5
  "main": "index.html",
6
6
  "publishConfig": {
package/style.css CHANGED
@@ -170,7 +170,7 @@ body {
170
170
  /* ==========================================================================
171
171
  8. RESPONSIVE LAYOUT (Schermi < 960px - Tablet Verticali e Smartphone)
172
172
  ========================================================================== */
173
- @media (max-width: 959px) {
173
+ @media (max-aspect-ratio: 40/50) {
174
174
  /* Il Vento viene forzato come primo elemento e occupa mezza altezza */
175
175
  .center-panel {
176
176
  order: 1;