@sailingrotevista/rotevista-dash 7.0.9 → 7.0.10

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 (4) hide show
  1. package/app.js +232 -194
  2. package/charts.js +7 -4
  3. package/index.js +697 -642
  4. package/package.json +1 -1
package/app.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * ==========================================================================
3
- * Signal K Wind Dashboard - Pro Version 6.0 (Dynamic Envelope Architecture)
4
- * ==========================================================================
5
- * Autore: Sailing Rotevista
6
- * Motore di calcolo tattico per navigazione e crociera.
7
- * Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
8
- * Memoria UI persistente, Modalità Hercules, Focus Split Screen e
9
- * Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
10
- *file app.js
11
- */
2
+ * ==========================================================================
3
+ * Signal K Wind Dashboard - Pro Version 6.0 (Dynamic Envelope Architecture)
4
+ * ==========================================================================
5
+ * Autore: Sailing Rotevista
6
+ * Motore di calcolo tattico per navigazione e crociera.
7
+ * Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
8
+ * Memoria UI persistente, Modalità Hercules, Focus Split Screen e
9
+ * Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
10
+ * file app.js
11
+ */
12
12
 
13
13
  // ==========================================================================
14
14
  // 1. CONFIGURAZIONE E DEFAULT
@@ -106,9 +106,9 @@ const ui = {
106
106
  // ==========================================================================
107
107
 
108
108
  /**
109
- * Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
110
- * Mantiene sempre almeno 60 minuti di memoria locale per il calcolo della bussola meteo.
111
- */
109
+ * Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
110
+ * Mantiene sempre almeno 60 minuti di memoria locale per il calcolo della bussola meteo.
111
+ */
112
112
  function safePush(buffer, val, time) {
113
113
  if (val === null || val === undefined || isNaN(val)) return;
114
114
 
@@ -197,16 +197,19 @@ function checkDepthAlarm(m) {
197
197
  ui.depth.classList.remove('alarm-warning', 'alarm-danger', 'blink-alarm');
198
198
  if (m < CONFIG.alarms.depthDanger) {
199
199
  ui.depth.classList.add('alarm-danger', 'blink-alarm');
200
- playBingBing();
200
+ // Suona solo se siamo attivamente in navigazione (Harbor Silence acustico quando fermi)
201
+ if (isNavigating) {
202
+ playBingBing();
203
+ }
201
204
  } else if (m < CONFIG.alarms.depthWarning) {
202
205
  ui.depth.classList.add('alarm-warning');
203
206
  }
204
207
  }
205
208
 
206
209
  /**
207
- * computeTrueWind: Calcola TWS, TWA e TWD strategico.
208
- * Gestisce il fallback separato (Split-Fallback) in caso di dati parzialmente nativi di bordo.
209
- */
210
+ * computeTrueWind: Calcola TWS, TWA e TWD strategico.
211
+ * Gestisce il fallback separato (Split-Fallback) in caso di dati parzialmente nativi di bordo.
212
+ */
210
213
  function computeTrueWind() {
211
214
  const aws = store.raw["environment.wind.speedApparent"], awa = store.raw["environment.wind.angleApparent"];
212
215
  const stw = store.raw["navigation.speedThroughWater"] || 0, sog = store.raw["navigation.speedOverGround"] || 0;
@@ -267,9 +270,9 @@ function computeTrueWind() {
267
270
  // ==========================================================================
268
271
 
269
272
  /**
270
- * Assegna un punteggio di qualità statico alla sorgente basato sull'hardware.
271
- * Più alto è il punteggio, maggiore è la priorità del sensore.
272
- */
273
+ * Assegna un punteggio di qualità statico alla sorgente basato sull'hardware.
274
+ * Più alto è il punteggio, maggiore è la priorità del sensore.
275
+ */
273
276
  function getSourcePriorityScore(sourceName) {
274
277
  if (!sourceName) return 0;
275
278
  const name = sourceName.toLowerCase();
@@ -293,6 +296,20 @@ function getSourcePriorityScore(sourceName) {
293
296
  }
294
297
 
295
298
  function processIncomingData(path, val, source, timeMs) {
299
+ // 1. Filtro anti-spike vento (> 100 nodi / 51.44 m/s)
300
+ if ((path === "environment.wind.speedApparent" || path === "environment.wind.speedTrue") && val > 51.44) {
301
+ return;
302
+ }
303
+
304
+ // 2. Filtro anti-spike velocità barca STW/SOG (> 50 nodi / 25.72 m/s)
305
+ if ((path === "navigation.speedThroughWater" || path === "navigation.speedOverGround") && val > 25.72) {
306
+ return;
307
+ }
308
+
309
+ // 3. Filtro validità profondità (ignora errori negativi e letture > 500m per lost-echo)
310
+ if (path === "environment.depth.belowTransducer" && (val < -2.0 || val > 500)) {
311
+ return;
312
+ }
296
313
  // Usiamo il tempo reale del pacchetto del server per eliminare lo sfasamento
297
314
  const now = timeMs || Date.now();
298
315
  const score = getSourcePriorityScore(source);
@@ -380,6 +397,16 @@ function processIncomingData(path, val, source, timeMs) {
380
397
  if (path === "navigation.courseOverGroundTrue") {
381
398
  safePush(store.smoothBuf.cog, val, now);
382
399
  safePush(store.longBuf.cog, val, now);
400
+
401
+ // Se non è installata alcuna bussola fisica sulla rete e la barca è in movimento stabile (> 1.5 nodi),
402
+ // emuliamo la Prua usando il COG per attivare il TWD, il mini-compass e la bussola radar.
403
+ const hasCompass = store.raw["navigation.headingTrue"] !== undefined || store.raw["navigation.headingMagnetic"] !== undefined;
404
+ const sog = store.raw["navigation.speedOverGround"] || 0;
405
+ if (!hasCompass && sog > 0.77) { // 0.77 m/s = 1.5 nodi
406
+ store.raw["navigation.headingTrue"] = val;
407
+ safePush(store.smoothBuf.hdg, val, now);
408
+ safePush(store.longBuf.hdg, val, now);
409
+ }
383
410
  }
384
411
 
385
412
  const twPaths = [
@@ -532,12 +559,12 @@ function updateWindTrend() {
532
559
  // ==========================================================================
533
560
 
534
561
  /**
535
- * upUI: Aggiornamento valori digitali
536
- */
562
+ * upUI: Aggiornamento valori digitali
563
+ */
537
564
  const upUI = (el, obj, instantRaw, isCompass = false) => {
538
565
  if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
539
- el.innerHTML = "---&deg;";
540
- el.classList.remove('unstable-data');
566
+ el.innerHTML = "---&deg;";
567
+ el.classList.remove('unstable-data');
541
568
  } else {
542
569
  let valDeg = Math.round(radToDeg(obj.val));
543
570
  let mainVal = (isCompass ? ((valDeg + 360) % 360).toString().padStart(3, '0') : valDeg) + "&deg;";
@@ -551,8 +578,8 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
551
578
  };
552
579
 
553
580
  /**
554
- * Loop principale di aggiornamento interfaccia (1Hz)
555
- */
581
+ * Loop principale di aggiornamento interfaccia (1Hz)
582
+ */
556
583
  function startDisplayLoop() {
557
584
  renderInterval = setInterval(() => {
558
585
  const now = Date.now();
@@ -567,43 +594,54 @@ function startDisplayLoop() {
567
594
  updateWindTrend();
568
595
 
569
596
  // --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
570
- const isSocketOpen = socket && socket.readyState === WebSocket.OPEN;
571
-
572
- if (isSocketOpen) {
573
- ui.status.className = "online"; // Colore Verde
574
- const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
575
- const requiredMs = viewportMinutes * 60000;
576
- const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
577
-
578
- if (oldestStw) {
579
- const availableMs = now - oldestStw.time;
580
- if (availableMs >= requiredMs) {
581
- ui.status.innerText = `ONLINE ${viewportMinutes}min`;
582
- } else {
583
- const availableMin = Math.max(1, Math.floor(availableMs / 60000));
584
- ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
585
- }
586
- } else {
587
- ui.status.innerText = `ONLINE`;
588
- }
597
+ const isSocketOpen = socket && socket.readyState === WebSocket.OPEN;
598
+
599
+ if (isSocketOpen) {
600
+ ui.status.className = "online"; // Colore Verde
601
+ const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
602
+ const requiredMs = viewportMinutes * 60000;
603
+ const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
604
+
605
+ if (oldestStw) {
606
+ const availableMs = now - oldestStw.time;
607
+ if (availableMs >= requiredMs) {
608
+ ui.status.innerText = `ONLINE ${viewportMinutes}min`;
589
609
  } else {
590
- ui.status.className = "offline"; // Colore Rosso
591
- ui.status.innerText = "OFFLINE"; // Chirurgico: Forza il testo a OFFLINE se il socket è chiuso, evitando scritte verdi in rosso
610
+ const availableMin = Math.max(1, Math.floor(availableMs / 60000));
611
+ ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
592
612
  }
613
+ } else {
614
+ ui.status.innerText = `ONLINE`;
615
+ }
616
+ } else {
617
+ ui.status.className = "offline"; // Colore Rosso
618
+ ui.status.innerText = "OFFLINE"; // Chirurgico: Forza il testo a OFFLINE se il socket è chiuso, evitando scritte verdi in rosso
619
+ }
593
620
 
594
621
  // --- WATCHDOG: CONTROLLO TIMEOUT ---
595
- const watch = {
596
- "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
597
- "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
598
- "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
599
- "environment.wind.speedTrue": ui.tws
600
- };
601
- for (let p in watch) {
602
- if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
603
- safeSetText(watch[p], "---"); // Sostituito innerText con la funzione protetta!
604
- delete store.raw[p];
622
+ const watch = {
623
+ "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
624
+ "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
625
+ "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
626
+ "environment.wind.speedTrue": ui.tws
627
+ };
628
+ for (let p in watch) {
629
+ if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
630
+ safeSetText(watch[p], "---"); // Sostituito innerText con la funzione protetta!
631
+ delete store.raw[p];
632
+
633
+ // Forza lo scorrimento dei dati fuori dallo schermo per i sensori in timeout
634
+ if (p === "navigation.speedThroughWater") {
635
+ refreshGraph('stw');
636
+ } else if (p === "navigation.speedOverGround") {
637
+ refreshGraph('sog');
638
+ } else if (p === "environment.depth.belowTransducer") {
639
+ refreshGraph('depth');
640
+ } else if (p === "environment.wind.speedApparent" || p === "environment.wind.speedTrue") {
641
+ refreshGraph('tws');
605
642
  }
606
643
  }
644
+ }
607
645
 
608
646
  // --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
609
647
  if (store.raw["navigation.speedThroughWater"] !== undefined) {
@@ -642,95 +680,95 @@ function startDisplayLoop() {
642
680
  }
643
681
 
644
682
  // --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
645
- if (store.raw["environment.depth.belowTransducer"] !== undefined) {
646
- safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
647
- checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
648
- manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
649
- }
683
+ if (store.raw["environment.depth.belowTransducer"] !== undefined) {
684
+ safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
685
+ checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
686
+ manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
687
+ }
650
688
 
651
- // --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
652
-
653
- // Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
654
- const rawTws = store.raw["environment.wind.speedTrue"];
655
- const rawAws = store.raw["environment.wind.speedApparent"];
656
-
657
- const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
658
- const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
689
+ // --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
659
690
 
660
- if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
661
- if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
662
-
663
- // Disegno testo casella destra (TWS o AWS)
664
- if (rawTws !== undefined || rawAws !== undefined) {
665
- const labelWind = document.getElementById('tws-aws-label');
666
- const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
667
-
668
- safeSetText(ui.tws, currentWind.toFixed(1));
669
- if (labelWind) labelWind.textContent = displayModeTws;
670
-
671
- if (currentWind >= CONFIG.graphs.reef2) {
672
- ui.tws.style.setProperty('color', '#ff3b30', 'important');
673
- } else if (currentWind >= CONFIG.graphs.reef1) {
674
- ui.tws.style.setProperty('color', '#ff9800', 'important');
675
- } else {
676
- if (displayModeTws === 'AWS') {
677
- ui.tws.style.setProperty('color', '#5c6bc0', 'important');
678
- } else {
679
- const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
680
- ui.tws.style.setProperty('color', navyNight, 'important');
681
- }
682
- }
691
+ // Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
692
+ const rawTws = store.raw["environment.wind.speedTrue"];
693
+ const rawAws = store.raw["environment.wind.speedApparent"];
694
+
695
+ const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
696
+ const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
697
+
698
+ if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
699
+ if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
700
+
701
+ // Disegno testo casella destra (TWS o AWS)
702
+ if (rawTws !== undefined || rawAws !== undefined) {
703
+ const labelWind = document.getElementById('tws-aws-label');
704
+ const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
705
+
706
+ safeSetText(ui.tws, currentWind.toFixed(1));
707
+ if (labelWind) labelWind.textContent = displayModeTws;
708
+
709
+ if (currentWind >= CONFIG.graphs.reef2) {
710
+ ui.tws.style.setProperty('color', '#ff3b30', 'important');
711
+ } else if (currentWind >= CONFIG.graphs.reef1) {
712
+ ui.tws.style.setProperty('color', '#ff9800', 'important');
713
+ } else {
714
+ if (displayModeTws === 'AWS') {
715
+ ui.tws.style.setProperty('color', '#5c6bc0', 'important');
716
+ } else {
717
+ const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
718
+ ui.tws.style.setProperty('color', navyNight, 'important');
683
719
  }
720
+ }
721
+ }
684
722
 
685
723
  // --- AGGIORNAMENTO DELLA BUSSOLA CENTRALE (BATTERY SAVER A 1Hz) ---
686
- if (activeInstrument === 'gauge') {
687
- updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
688
- }
689
-
690
- // --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
691
- if (lastAvgUIUpdate++ % 10 === 0) {
692
- saveDashboardState();
693
- }
724
+ if (activeInstrument === 'gauge') {
725
+ updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal);
726
+ }
727
+
728
+ // --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
729
+ if (lastAvgUIUpdate++ % 10 === 0) {
730
+ saveDashboardState();
731
+ }
694
732
 
695
- if (lastAvgUIUpdate % 3 === 0) {
696
- let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
697
- let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
698
- let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true);
699
- let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
700
- let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
701
-
702
- upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
703
- upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
704
- upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
705
- upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
706
- upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
707
-
708
- if (hObj && twdObj) {
709
- const reflectAngle = (targetRad, axisRad) => {
710
- const dS = Math.sin(axisRad - targetRad);
711
- const dC = Math.cos(axisRad - targetRad);
712
- return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
713
- };
714
- const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
715
- if (!isNavigating) ui.tackHdg.innerHTML = "---&deg;";
716
- else if (unstableH) { ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data'); }
717
- else {
718
- const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
719
- ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
720
- ui.tackHdg.classList.remove('unstable-data');
721
- }
722
- if (cObj) {
723
- const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
724
- if (!isNavigating) ui.tackCog.innerHTML = "---&deg;";
725
- else if (unstableC) { ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data'); }
726
- else {
727
- const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
728
- ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
729
- ui.tackCog.classList.remove('unstable-data');
730
- }
731
- }
733
+ if (lastAvgUIUpdate % 3 === 0) {
734
+ let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
735
+ let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
736
+ let awObj = getCircularAverageFromBuffer(store.longBuf.awa, CONFIG.averaging.longWindow, true);
737
+ let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
738
+ let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
739
+
740
+ upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
741
+ upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
742
+ upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
743
+ upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
744
+ upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
745
+
746
+ if (hObj && twdObj) {
747
+ const reflectAngle = (targetRad, axisRad) => {
748
+ const dS = Math.sin(axisRad - targetRad);
749
+ const dC = Math.cos(axisRad - targetRad);
750
+ return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
751
+ };
752
+ const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
753
+ if (!isNavigating) ui.tackHdg.innerHTML = "---&deg;";
754
+ else if (unstableH) { ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data'); }
755
+ else {
756
+ const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
757
+ ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
758
+ ui.tackHdg.classList.remove('unstable-data');
759
+ }
760
+ if (cObj) {
761
+ const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
762
+ if (!isNavigating) ui.tackCog.innerHTML = "---&deg;";
763
+ else if (unstableC) { ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data'); }
764
+ else {
765
+ const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
766
+ ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
767
+ ui.tackCog.classList.remove('unstable-data');
732
768
  }
733
769
  }
770
+ }
771
+ }
734
772
  }, RENDER_INTERVAL_MS);
735
773
  }
736
774
 
@@ -741,8 +779,8 @@ function startDisplayLoop() {
741
779
  let currentConfigString = ""; // Memoria per rilevare cambiamenti nei settings
742
780
 
743
781
  /**
744
- * Risolve dinamicamente l'URL dell'API del Cerbo GX se siamo in locale su Mac/PC
745
- */
782
+ * Risolve dinamicamente l'URL dell'API del Cerbo GX se siamo in locale su Mac/PC
783
+ */
746
784
  function getApiUrl(path) {
747
785
  if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
748
786
  return `http://${CONFIG.server.fallbackIp}${path}`;
@@ -751,8 +789,8 @@ function getApiUrl(path) {
751
789
  }
752
790
 
753
791
  /**
754
- * Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
755
- */
792
+ * Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
793
+ */
756
794
  function applyConfigData(data) {
757
795
  Object.assign(CONFIG.alarms, data.alarms || {});
758
796
  Object.assign(CONFIG.graphs, data.graphs || {});
@@ -772,8 +810,8 @@ function applyConfigData(data) {
772
810
  }
773
811
 
774
812
  /**
775
- * Recupera la configurazione iniziale al caricamento della pagina
776
- */
813
+ * Recupera la configurazione iniziale al caricamento della pagina
814
+ */
777
815
  async function fetchServerConfig() {
778
816
  try {
779
817
  const response = await fetch(getApiUrl('/rotevista-config'));
@@ -791,8 +829,8 @@ async function fetchServerConfig() {
791
829
  }
792
830
 
793
831
  /**
794
- * Watchdog: Controlla in background se le impostazioni sul server sono cambiate
795
- */
832
+ * Watchdog: Controlla in background se le impostazioni sul server sono cambiate
833
+ */
796
834
  async function watchConfigChanges() {
797
835
  try {
798
836
  const response = await fetch(getApiUrl('/rotevista-config'));
@@ -822,8 +860,8 @@ async function watchConfigChanges() {
822
860
  }
823
861
 
824
862
  /**
825
- * Recupera lo storico dei grafici e dei radar pre-popolato dal server Signal K (Pro v6.0)
826
- */
863
+ * Recupera lo storico dei grafici e dei radar pre-popolato dal server Signal K (Pro v6.0)
864
+ */
827
865
  async function fetchServerHistory() {
828
866
  try {
829
867
  const response = await fetch(getApiUrl('/rotevista-history'));
@@ -862,12 +900,12 @@ async function fetchServerHistory() {
862
900
  }
863
901
 
864
902
  /**
865
- * manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
866
- * Integrazioni:
867
- * 1. Strict undefined check per lastUpdates.
868
- * 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
869
- * 3. Clamping di sicurezza (no negativi, no Infinity).
870
- */
903
+ * manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
904
+ * Integrazioni:
905
+ * 1. Strict undefined check per lastUpdates.
906
+ * 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
907
+ * 3. Clamping di sicurezza (no negativi, no Infinity).
908
+ */
871
909
  function manageHistory(type, value) {
872
910
  // --- 1. VALIDAZIONE INPUT RIGOROSA ---
873
911
  if (value === undefined || value === null || !isFinite(value)) return;
@@ -1133,50 +1171,50 @@ async function init() {
1133
1171
  initRadarTicks(); // Tacche del Wind Radar storico (weather-radar.js)
1134
1172
 
1135
1173
  // 1. COMANDO TATTICO: Gestore Box TWD (Pressione prolungata -> Radar | Tocco rapido in modalità Radar -> Torna a Gauge)
1136
- const twdBox = document.querySelector('.box-twd');
1137
- if (twdBox) {
1138
- let twdPressTimer = null;
1139
- let longPressTriggered = false; // Flag di controllo della pressione prolungata
1140
-
1141
- twdBox.addEventListener('pointerdown', (e) => {
1142
- longPressTriggered = false;
1143
- if (activeInstrument === 'gauge') {
1144
- twdPressTimer = setTimeout(() => {
1145
- activeInstrument = 'radar';
1146
- document.getElementById('wind-gauge').style.display = 'none';
1147
- document.getElementById('wind-radar').style.display = 'block';
1148
- renderRadar(); // Disegna immediatamente il radar all'attivazione
1149
- twdPressTimer = null;
1150
- longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
1151
- }, 1000);
1152
- }
1153
- });
1154
- twdBox.addEventListener('pointerup', () => {
1155
- if (activeInstrument === 'gauge') {
1156
- if (twdPressTimer) {
1157
- clearTimeout(twdPressTimer);
1158
- twdPressTimer = null;
1159
- }
1160
- } else if (activeInstrument === 'radar') {
1161
- if (longPressTriggered) {
1162
- // Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
1163
- longPressTriggered = false;
1164
- } else {
1165
- // Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
1166
- activeInstrument = 'gauge';
1167
- document.getElementById('wind-radar').style.display = 'none';
1168
- document.getElementById('wind-gauge').style.display = 'block';
1169
- }
1170
- }
1171
- });
1172
- twdBox.addEventListener('pointerleave', () => {
1174
+ const twdBox = document.querySelector('.box-twd');
1175
+ if (twdBox) {
1176
+ let twdPressTimer = null;
1177
+ let longPressTriggered = false; // Flag di controllo della pressione prolungata
1178
+
1179
+ twdBox.addEventListener('pointerdown', (e) => {
1180
+ longPressTriggered = false;
1181
+ if (activeInstrument === 'gauge') {
1182
+ twdPressTimer = setTimeout(() => {
1183
+ activeInstrument = 'radar';
1184
+ document.getElementById('wind-gauge').style.display = 'none';
1185
+ document.getElementById('wind-radar').style.display = 'block';
1186
+ renderRadar(); // Disegna immediatamente il radar all'attivazione
1187
+ twdPressTimer = null;
1188
+ longPressTriggered = true; // Segnala che la transizione al radar è avvenuta con successo
1189
+ }, 1000);
1190
+ }
1191
+ });
1192
+ twdBox.addEventListener('pointerup', () => {
1193
+ if (activeInstrument === 'gauge') {
1173
1194
  if (twdPressTimer) {
1174
1195
  clearTimeout(twdPressTimer);
1175
1196
  twdPressTimer = null;
1176
1197
  }
1177
- longPressTriggered = false;
1178
- });
1179
- }
1198
+ } else if (activeInstrument === 'radar') {
1199
+ if (longPressTriggered) {
1200
+ // Se l'evento di rilascio appartiene al tocco prolungato che ha appena attivato il radar, lo ignoriamo
1201
+ longPressTriggered = false;
1202
+ } else {
1203
+ // Altrimenti è un tocco rapido indipendente: torna alla bussola analogica
1204
+ activeInstrument = 'gauge';
1205
+ document.getElementById('wind-radar').style.display = 'none';
1206
+ document.getElementById('wind-gauge').style.display = 'block';
1207
+ }
1208
+ }
1209
+ });
1210
+ twdBox.addEventListener('pointerleave', () => {
1211
+ if (twdPressTimer) {
1212
+ clearTimeout(twdPressTimer);
1213
+ twdPressTimer = null;
1214
+ }
1215
+ longPressTriggered = false;
1216
+ });
1217
+ }
1180
1218
 
1181
1219
  // 2. COMANDO TATTICO: Click in qualsiasi punto del radar per tornare all'analogico
1182
1220
  const windRadarSvg = document.getElementById('wind-radar');
package/charts.js CHANGED
@@ -156,14 +156,14 @@ function refreshGraph(t) {
156
156
  // Genera fisicamente le curve e le aree SVG
157
157
  function drawGraph(d, id, min, max, isTws, isHercules) {
158
158
  const svg = document.getElementById(id);
159
- if (!svg || d.length < 2) return;
159
+ if (!svg) return;
160
160
 
161
161
  const w = 200, h = 40;
162
162
  const range = max - min || 1;
163
163
  const isDepth = (id === 'depth-graph');
164
164
 
165
- const latestPoint = d[d.length - 1];
166
- const now = latestPoint ? latestPoint.time : Date.now();
165
+ // Utilizza sempre il tempo reale corrente per permettere lo scorrimento continuo dei dati
166
+ const now = Date.now();
167
167
 
168
168
  const box = svg.closest('.data-box');
169
169
  const isFocused = isFocusActive && box && box.classList.contains('is-focused');
@@ -173,7 +173,10 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
173
173
  const viewportStart = now - viewportMs;
174
174
 
175
175
  const visibleData = d.filter(p => p.time >= viewportStart);
176
- if (visibleData.length < 2) return;
176
+ if (visibleData.length < 2) {
177
+ svg.innerHTML = ""; // Svuota completamente l'SVG se il sensore è spento e i dati sono scivolati fuori scala
178
+ return;
179
+ }
177
180
 
178
181
  const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
179
182
  const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";