@sailingrotevista/rotevista-dash 4.0.18 → 4.0.20

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 (3) hide show
  1. package/app.js +239 -86
  2. package/package.json +1 -1
  3. package/style.css +6 -0
package/app.js CHANGED
@@ -17,7 +17,7 @@ let CONFIG = {
17
17
  smoothWindow: 2000,
18
18
  longWindow: 30000,
19
19
  stabilityTolerance: 2000,
20
- stabilityThreshold: 0.85,
20
+ stabilityThreshold: 0.9,
21
21
  minSpeed: 0,
22
22
  stabilityBreakout: 15
23
23
  },
@@ -35,6 +35,8 @@ const RENDER_INTERVAL_MS = 1000;
35
35
  const TIMEOUT_MS = 5000;
36
36
  const SIM_SAMPLE_INTERVAL = 1000;
37
37
  const DASH_VERSION = "2.4"; // Versione per la gestione della memoria locale
38
+ const sourceLocks = {};
39
+
38
40
 
39
41
  // ==========================================================================
40
42
  // 2. STATO GLOBALE E RIFERIMENTI UI
@@ -53,6 +55,10 @@ let twDirty = false, isNavigating = false, reconnectDelay = 1000;
53
55
 
54
56
  let pressTimer, isFocusActive = false;
55
57
 
58
+ let emaTwaSin = 0;
59
+ let emaTwaCos = 0;
60
+ let firstEmaRun = true;
61
+
56
62
  // Stato dei singoli grafici (Standard vs Hercules Zoom)
57
63
  const graphModes = {
58
64
  stw: 'standard',
@@ -110,39 +116,87 @@ function getShortestRotation(curr, target) {
110
116
  /**
111
117
  * Inserisce un dato nel buffer circolare limitandolo a 2000 campioni (30 min)
112
118
  */
113
- function safePush(buffer, val, time, maxLen = 2000) {
114
- buffer.push({ val: val, time: time });
115
- if (buffer.length > maxLen) { buffer.shift(); }
116
- }
117
-
118
119
  /**
119
- * Media Circolare Vettoriale: Calcola angolo medio, stabilità R e deviazione standard (±)
120
+ * Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
120
121
  */
121
- function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false) {
122
- const now = Date.now();
123
- const validData = bufferArray.filter(item => (now - item.time) <= windowMs);
124
- if (validData.length === 0) return null;
125
-
126
- let sSin = 0, sCos = 0;
127
- validData.forEach(item => {
128
- sSin += Math.sin(item.val);
129
- sCos += Math.cos(item.val);
122
+ function safePush(buffer, val, time) {
123
+ buffer.push({
124
+ val: val,
125
+ time: time,
126
+ sin: Math.sin(val),
127
+ cos: Math.cos(val)
130
128
  });
131
-
132
- let R = Math.sqrt(sSin * sSin + sCos * sCos) / validData.length;
133
- let historyDuration = (validData.length > 2) ? (validData[validData.length - 1].time - validData[0].time) : 0;
134
-
135
- // Un dato è stabile se abbiamo abbastanza storia e la coerenza vettoriale R è alta
136
- let isStable = (historyDuration > 10000) && (R > CONFIG.averaging.stabilityThreshold);
137
- let avgRad = Math.atan2(sSin, sCos);
138
129
 
139
- // Calcolo della Deviazione Standard Circolare (±) in gradi
140
- let deviation = (R < 1 && R > 0) ? Math.round(Math.sqrt(-2 * Math.log(R)) * (180 / Math.PI)) : 0;
130
+ // Teniamo sempre in memoria il DOPPIO della storia impostata (per la modalità ancoraggio)
131
+ // + 1 minuto di margine
132
+ const maxHistoryMs = (CONFIG.graphs.historyMinutes * 60000 * 2) + 60000;
133
+
134
+ while (buffer.length > 0 && (time - buffer[0].time) > maxHistoryMs) {
135
+ buffer.shift();
136
+ }
137
+
138
+ // Tetto massimo di campioni per sicurezza (circa 2 ore a 5Hz)
139
+ if (buffer.length > 36000) buffer.shift();
140
+ }
141
+
142
+ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
143
+ now = now || Date.now();
144
+
145
+ const len = bufferArray.length;
146
+ if (len === 0) return null;
147
+
148
+ let sSin = 0;
149
+ let sCos = 0;
150
+ let count = 0;
151
+
152
+ let newestTime = 0;
153
+ let oldestTime = 0;
154
+
155
+ for (let i = len - 1; i >= 0; i--) {
156
+ const item = bufferArray[i];
157
+
158
+ if ((now - item.time) > windowMs) break;
159
+
160
+ sSin += item.sin;
161
+ sCos += item.cos;
162
+
163
+ if (count === 0) newestTime = item.time;
164
+ oldestTime = item.time;
165
+
166
+ count++;
167
+ }
168
+
169
+ if (count === 0) return null;
170
+
171
+ const R = Math.hypot(sSin, sCos) / count;
172
+
173
+ const avgRad = Math.atan2(sSin, sCos);
174
+
175
+ const finalVal = signed
176
+ ? avgRad
177
+ : (avgRad + Math.PI * 2) % (Math.PI * 2);
178
+
179
+ const historyDuration =
180
+ (count > 2)
181
+ ? (newestTime - oldestTime)
182
+ : 0;
183
+
184
+ const isStable =
185
+ historyDuration > 10000 &&
186
+ R > CONFIG.averaging.stabilityThreshold;
187
+
188
+ const safeR = Math.max(R, 1e-9);
141
189
 
142
190
  return {
143
- val: signed ? avgRad : (avgRad + 2 * Math.PI) % (2 * Math.PI),
191
+ val: finalVal,
144
192
  stable: isStable,
145
- dev: deviation
193
+ dev: (R < 1)
194
+ ? Math.round(
195
+ Math.sqrt(-2 * Math.log(safeR)) *
196
+ (180 / Math.PI)
197
+ )
198
+ : 0,
199
+ samples: count
146
200
  };
147
201
  }
148
202
 
@@ -291,14 +345,58 @@ function computeTrueWind() {
291
345
  }
292
346
  }
293
347
 
294
- function processIncomingData(path, val) {
295
- const now = Date.now(); store.timestamps[path] = now; store.raw[path] = val;
296
- if (path === "environment.wind.angleApparent") { safePush(store.smoothBuf.awa, val, now); safePush(store.longBuf.awa, val, now); }
297
- const twPaths = ["environment.wind.speedApparent", "environment.wind.angleApparent", "navigation.speedThroughWater", "navigation.speedOverGround", "navigation.headingTrue", "navigation.courseOverGroundTrue"];
298
- if (twPaths.includes(path)) twDirty = true;
299
- if (twDirty && (now - lastTWCompute > 100)) { computeTrueWind(); lastTWCompute = now; twDirty = false; }
300
- if (path === "navigation.headingTrue") { safePush(store.smoothBuf.hdg, val, now); safePush(store.longBuf.hdg, val, now); }
301
- if (path === "navigation.courseOverGroundTrue") { safePush(store.smoothBuf.cog, val, now); safePush(store.longBuf.cog, val, now); }
348
+ function processIncomingData(path, val, source) {
349
+ const now = Date.now();
350
+
351
+ // --- 1. FILTRO SORGENTE STICKY (Elimina conflitti yacht_device vs Unknown) ---
352
+ if (!sourceLocks[path] || sourceLocks[path].label === source || (now - sourceLocks[path].lastSeen > 2000)) {
353
+ sourceLocks[path] = { label: source, lastSeen: now };
354
+ } else {
355
+ // Se arriva un dato da un'altra sorgente mentre il lock è attivo, lo scartiamo
356
+ return;
357
+ }
358
+
359
+ // --- 2. AGGIORNAMENTO DATI (Solo per la sorgente eletta) ---
360
+ store.timestamps[path] = now;
361
+ store.raw[path] = val;
362
+
363
+ // Buffer per AWA (Vento Apparente)
364
+ if (path === "environment.wind.angleApparent") {
365
+ safePush(store.smoothBuf.awa, val, now);
366
+ safePush(store.longBuf.awa, val, now);
367
+ }
368
+
369
+ // Buffer per HDG (Prua)
370
+ if (path === "navigation.headingTrue") {
371
+ safePush(store.smoothBuf.hdg, val, now);
372
+ safePush(store.longBuf.hdg, val, now);
373
+ }
374
+
375
+ // Buffer per COG (Rotta Fondo)
376
+ if (path === "navigation.courseOverGroundTrue") {
377
+ safePush(store.smoothBuf.cog, val, now);
378
+ safePush(store.longBuf.cog, val, now);
379
+ }
380
+
381
+ // --- 3. TRIGGER CALCOLO VENTO REALE (TWA/TWS/TWD) ---
382
+ const twPaths = [
383
+ "environment.wind.speedApparent",
384
+ "environment.wind.angleApparent",
385
+ "navigation.speedThroughWater",
386
+ "navigation.speedOverGround",
387
+ "navigation.headingTrue",
388
+ "navigation.courseOverGroundTrue"
389
+ ];
390
+
391
+ if (twPaths.includes(path)) {
392
+ twDirty = true;
393
+ // Calcolo limitato a 10Hz per non pesare sulla CPU
394
+ if (twDirty && (now - lastTWCompute > 100)) {
395
+ computeTrueWind();
396
+ lastTWCompute = now;
397
+ twDirty = false;
398
+ }
399
+ }
302
400
  }
303
401
 
304
402
  // ==========================================================================
@@ -312,8 +410,9 @@ function updateWindTrend() {
312
410
 
313
411
  // STRATEGIA (Bussola TWD): 1 min vs 30 minuti (tendenza meteo profonda)
314
412
  const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
315
- const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, 1800000, false); // 1.800.000 ms = 30 min
316
-
413
+ const multiplier = isNavigating ? 1 : 2;
414
+ const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
415
+ const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
317
416
  if (!twaNow || !twaRef || !twdNow || !twdRef) return;
318
417
  const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
319
418
  const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
@@ -357,12 +456,41 @@ function updateWindTrend() {
357
456
  }
358
457
  } else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
359
458
 
360
- // ALLARME STRAMBATA (GYBE)
361
- const instTwa = radToDeg(store.raw["environment.wind.angleTrueWater"] || 0);
362
- if (Math.abs(instTwa) > 155 && Math.sign(instTwa) !== Math.sign(lastInstantTwa)) {
363
- if (isNavigating && (now - lastGybeAlarmTime > 5000)) { lastGybeAlarmTime = now; playGybeAlarm(); }
364
- }
365
- lastInstantTwa = instTwa;
459
+ // --- LOGICA ALLARME STRAMBATA (GYBE) FILTRATA ---
460
+ const instTwaRad = store.raw["environment.wind.angleTrueWater"];
461
+
462
+ if (instTwaRad !== undefined) {
463
+ // Calcolo Alfa basato sulla Steering Precision (più precisione = filtro più lento e stabile)
464
+ const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
465
+
466
+ const currentSin = Math.sin(instTwaRad);
467
+ const currentCos = Math.cos(instTwaRad);
468
+
469
+ if (firstEmaRun) {
470
+ emaTwaSin = currentSin;
471
+ emaTwaCos = currentCos;
472
+ firstEmaRun = false;
473
+ } else {
474
+ // Media Esponenziale Vettoriale
475
+ emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
476
+ emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
477
+ }
478
+
479
+ // Angolo risultante filtrato
480
+ const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
481
+
482
+ // Verifichiamo il cambio di mure (> 155° e inversione di segno confermata dal filtro)
483
+ if (Math.abs(smoothedTwaDeg) > 155) {
484
+ if (Math.sign(smoothedTwaDeg) !== Math.sign(lastInstantTwa) && lastInstantTwa !== null) {
485
+ if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
486
+ lastGybeAlarmTime = now;
487
+ playGybeAlarm();
488
+ console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
489
+ }
490
+ }
491
+ }
492
+ lastInstantTwa = smoothedTwaDeg;
493
+ }
366
494
  }
367
495
 
368
496
  // ==========================================================================
@@ -390,7 +518,7 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
390
518
  } else {
391
519
  let valDeg = Math.round(radToDeg(obj.val));
392
520
  let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "&deg;";
393
- let dev = (obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.35em; opacity: 0.5; margin-left: 4px; vertical-align: middle;">&plusmn;${obj.dev}</span>` : "";
521
+ let dev = (obj.dev > 1 && obj.dev < 90) ? `<span style="font-size: 0.8em; opacity: 0.4; margin-left: 6px;">&plusmn;${obj.dev}</span>` : "";
394
522
  el.innerHTML = mainVal + dev;
395
523
 
396
524
  let diff = Math.abs((radToDeg(instantRaw) - radToDeg(obj.val) + 540) % 360 - 180);
@@ -512,30 +640,39 @@ function startDisplayLoop() {
512
640
  upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
513
641
  upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
514
642
 
515
- // --- LOGICA TACK STRATEGICA (Riflessione geometrica su TWD) ---
643
+ // --- LOGICA TACK STRATEGICA (VETTORIALE) ---
516
644
  if (hObj && twdObj) {
517
- const tH = radToDeg((2 * twdObj.val - hObj.val + Math.PI * 2) % (Math.PI * 2));
518
- const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout || twdObj.dev > CONFIG.averaging.stabilityBreakout;
645
+ // Funzione interna per riflettere un angolo rispetto all'asse del vento (TWD)
646
+ const reflectAngle = (targetRad, axisRad) => {
647
+ const diffSin = Math.sin(axisRad - targetRad);
648
+ const diffCos = Math.cos(axisRad - targetRad);
649
+ return Math.atan2(Math.sin(axisRad) * diffCos + Math.cos(axisRad) * diffSin,
650
+ Math.cos(axisRad) * diffCos - Math.sin(axisRad) * diffSin);
651
+ };
652
+
653
+ const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
519
654
 
520
655
  if (!isNavigating) {
521
- ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.remove('unstable-data');
656
+ ui.tackHdg.innerHTML = "---&deg;";
522
657
  } else if (unstableH) {
523
658
  ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data');
524
659
  } else {
525
- ui.tackHdg.innerHTML = `${Math.round((tH + 360) % 360).toString().padStart(3, '0')}&deg;`;
660
+ const reflectedH = reflectAngle(hObj.val, twdObj.val);
661
+ const outH = (radToDeg(reflectedH) + 360) % 360;
662
+ ui.tackHdg.innerHTML = `${Math.round(outH).toString().padStart(3, '0')}&deg;`;
526
663
  ui.tackHdg.classList.remove('unstable-data');
527
664
  }
528
665
 
529
666
  if (cObj) {
530
- const tC = radToDeg((2 * twdObj.val - cObj.val + Math.PI * 2) % (Math.PI * 2));
531
- const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout || twdObj.dev > CONFIG.averaging.stabilityBreakout;
532
-
667
+ const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
533
668
  if (!isNavigating) {
534
- ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.remove('unstable-data');
669
+ ui.tackCog.innerHTML = "---&deg;";
535
670
  } else if (unstableC) {
536
671
  ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data');
537
672
  } else {
538
- ui.tackCog.innerHTML = `${Math.round((tC + 360) % 360).toString().padStart(3, '0')}&deg;`;
673
+ const reflectedC = reflectAngle(cObj.val, twdObj.val);
674
+ const outC = (radToDeg(reflectedC) + 360) % 360;
675
+ ui.tackCog.innerHTML = `${Math.round(outC).toString().padStart(3, '0')}&deg;`;
539
676
  ui.tackCog.classList.remove('unstable-data');
540
677
  }
541
678
  }
@@ -616,6 +753,10 @@ function updateScaleLabels(t, min, max) {
616
753
  * drawGraph: Disegna i grafici con griglia temporale intelligente
617
754
  * Usa un Gradiente Lineare SVG dinamico per eliminare le giunzioni dei poligoni.
618
755
  */
756
+ /**
757
+ * drawGraph: Disegna i grafici con griglia temporale intelligente
758
+ * Versione bilanciata: 1.5px per allarmi critici, 1px per il resto.
759
+ */
619
760
  function drawGraph(d, id, min, max, isTws, isHercules) {
620
761
  const svg = document.getElementById(id);
621
762
  if (!svg || d.length < 2) return;
@@ -625,7 +766,7 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
625
766
  const isDepth = (id === 'depth-graph');
626
767
  const samples = CONFIG.graphs.samples;
627
768
 
628
- // 1. Griglia
769
+ // 1. Griglia (Sottile 0.5px)
629
770
  let grids = "";
630
771
  [0.25, 0.5, 0.75].forEach(p => {
631
772
  grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(0,0,0,0.12)" stroke-width="0.5" />`;
@@ -636,7 +777,7 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
636
777
  grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
637
778
  }
638
779
 
639
- // 2. Colori (Tavolozza Vivida)
780
+ // 2. Tavolozza Colori Vividi
640
781
  const baseColorTws = "#2c3e50";
641
782
  const baseColorDepth = "#0088cc";
642
783
 
@@ -656,49 +797,46 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
656
797
  let areaPath = `M 0 ${h} `;
657
798
 
658
799
  for (let i = 1; i < d.length; i++) {
659
- // Calcolo Percentuali per il gradiente
660
800
  const percentPrev = ((i - 1) / (samples - 1)) * 100;
661
801
  const percentCurr = (i / (samples - 1)) * 100;
662
802
 
663
- // Coordinate geometriche
664
803
  const x1 = ((i - 1) / (samples - 1)) * w;
665
804
  const y1 = h - (Math.max(0, Math.min(1, (d[i - 1] - min) / range)) * h);
666
805
  const x2 = (i / (samples - 1)) * w;
667
806
  const y2 = h - (Math.max(0, Math.min(1, (d[i] - min) / range)) * h);
668
807
 
669
808
  const color = getColor(d[i]);
670
-
671
- // Assegnazione specifica di Opacità e Spessore Linea
672
- let fillOpacity = "0.15"; // Default per valori base
673
- let strokeWidth = "1";
674
-
675
- if (color === "#ff3b30") {
676
- // ROSSO (Danger / Reef 2): Molto solido
677
- fillOpacity = "0.85";
678
- strokeWidth = "1.5";
679
- } else if (color === "#ff9800") {
680
- // ARANCIONE (Warning / Reef 1): Più trasparente (Velo)
681
- fillOpacity = "0.45";
682
- strokeWidth = "1"; // Manteniamo la linea spessa per leggerla bene
683
- }
809
+
810
+ // Logica Opacità e Spessore (Tua configurazione)
811
+ let fillOpacity = "0.15";
812
+ let strokeWidth = "1";
813
+
814
+ if (color === "#ff3b30") {
815
+ fillOpacity = "0.85"; // Rosso: Molto visibile
816
+ strokeWidth = "1.5"; // Rosso: Più marcato
817
+ } else if (color === "#ff9800") {
818
+ fillOpacity = "0.45"; // Arancio: Velo medio
819
+ strokeWidth = "1"; // Arancio: Sottile
820
+ }
684
821
 
685
- // GRADIENTE: Due stop alla stessa percentuale per creare uno stacco di colore netto (no sfumature)
822
+ // Costruzione Gradiente (stacchi netti tra i colori)
686
823
  gradientStops += `<stop offset="${percentPrev}%" stop-color="${color}" stop-opacity="${fillOpacity}" />`;
687
824
  gradientStops += `<stop offset="${percentCurr}%" stop-color="${color}" stop-opacity="${fillOpacity}" />`;
688
825
 
689
- // LINEE: Disegnate normalmente sopra l'area
826
+ // Disegno Linea con precisione geometrica
690
827
  lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
691
- style="stroke: ${color}; stroke-width: ${strokeWidth}; stroke-linecap: round;" />`;
828
+ style="stroke: ${color}; stroke-width: ${strokeWidth}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
692
829
 
693
- // AGGIORNAMENTO PATH UNICO
694
830
  if (i === 1) areaPath += `L ${x1} ${y1} `;
695
- areaPath += `L ${x2} ${y2} `;
696
- }
697
-
698
- // Chiusura del path dell'area
699
- areaPath += `L ${w} ${h} Z`;
831
+ areaPath += `L ${x2} ${y2} `;
832
+
833
+ // Salviamo l'ultima coordinata X calcolata per chiudere correttamente il path
834
+ if (i === d.length - 1) {
835
+ areaPath += `L ${x2} ${h} Z`;
836
+ }
837
+ }
700
838
 
701
- // 4. Iniezione del <defs> (Gradiente) nell'SVG
839
+ // 4. Iniezione del Gradiente
702
840
  const gradId = `grad-${id}`;
703
841
  const defs = `
704
842
  <defs>
@@ -709,12 +847,12 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
709
847
  `;
710
848
 
711
849
  // 5. Render Finale
712
- // Se è Depth o Tws applichiamo il gradiente, altrimenti colore standard fisso (per SOG/STW)
713
850
  if (isTws || isDepth) {
714
851
  svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
715
852
  } else {
716
- const standardColor = getColor(d[d.length - 1]);
717
- svg.innerHTML = `${grids}<path d="${areaPath}" fill="${standardColor}" fill-opacity="0.15" stroke="none" />${lines}`;
853
+ // Per STW/SOG usiamo il colore dell'ultimo punto con area fissa 0.15
854
+ const currentPathColor = getColor(d[d.length - 1]);
855
+ svg.innerHTML = `${grids}<path d="${areaPath}" fill="${currentPathColor}" fill-opacity="0.15" stroke="none" />${lines}`;
718
856
  }
719
857
  }
720
858
 
@@ -777,7 +915,22 @@ function connect() {
777
915
  try {
778
916
  socket = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=self`);
779
917
  socket.onopen = () => { ui.status.className = "online"; ui.status.innerText = "ONLINE"; reconnectDelay = 1000; };
780
- socket.onmessage = (e) => { const d = JSON.parse(e.data); if (d.updates) d.updates.forEach(u => u.values && u.values.forEach(v => processIncomingData(v.path, v.value))); };
918
+
919
+ socket.onmessage = (e) => {
920
+ const d = JSON.parse(e.data);
921
+ if (d.updates) {
922
+ d.updates.forEach(u => {
923
+ // 1. Estraiamo il nome del sensore/sorgente (es. "yacht_device" o "Unknown")
924
+ const sourceLabel = u.source ? (u.source.label || u.source.talker || "Unknown") : "Unknown";
925
+
926
+ // 2. Passiamo il nome della sorgente come TERZO parametro
927
+ if (u.values) {
928
+ u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
929
+ }
930
+ });
931
+ }
932
+ };
933
+
781
934
  socket.onclose = () => { if (!simulationMode) { ui.status.className = "offline"; ui.status.innerText = "RECONNECTING..."; setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 1.5, 10000); } };
782
935
  } catch (e) { setTimeout(connect, reconnectDelay); }
783
936
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "4.0.18",
3
+ "version": "4.0.20",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/style.css CHANGED
@@ -122,6 +122,12 @@ body {
122
122
  .box-twd #twd-avg { font-size: clamp(1.4rem, 45cqh, 20cqw) !important; }
123
123
  }
124
124
 
125
+ /* --- OTTIMIZZAZIONE SPECIFICA TWD PER EVITARE ANDATA A CAPO in landscape--- */
126
+ .box-twd .value-large {
127
+ /* Riduciamo il limite di larghezza (18cqw) per far spazio al simbolo ± rimpicciolendo il testo se serve */
128
+ font-size: clamp(1.2rem, 55cqh, 18cqw) !important;
129
+ }
130
+
125
131
  /* --- BOX TACK (Mure Opposte) --- */
126
132
  .box-tack .dual-value-container { display: flex; flex-direction: row; justify-content: space-between; align-items: center; width: 100%; height: 100%; }
127
133
  .box-tack .dual-value-col { display: flex; flex-direction: column; justify-content: center; }