@sailingrotevista/rotevista-dash 4.0.21 → 5.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.
Files changed (4) hide show
  1. package/app.js +383 -137
  2. package/index.html +4 -1
  3. package/index.js +3 -3
  4. package/package.json +2 -2
package/app.js CHANGED
@@ -17,11 +17,11 @@ let CONFIG = {
17
17
  smoothWindow: 2000,
18
18
  longWindow: 30000,
19
19
  stabilityTolerance: 2000,
20
- stabilityThreshold: 0.95,
20
+ stabilityThreshold: 0.99,
21
21
  minSpeed: 0,
22
22
  stabilityBreakout: 15
23
23
  },
24
- graphs: { reef1: 8, reef2: 10.0, historyMinutes: 30, samples: 60 },
24
+ graphs: { reef1: 10, reef2: 15, historyMinutes: 10, samples: 60 },
25
25
  scales: {
26
26
  stw: { stdMax: 8, hercSpan: 4, step: 2 },
27
27
  sog: { stdMax: 8, hercSpan: 4, step: 2 },
@@ -34,7 +34,7 @@ let CONFIG = {
34
34
  const RENDER_INTERVAL_MS = 1000;
35
35
  const TIMEOUT_MS = 5000;
36
36
  const SIM_SAMPLE_INTERVAL = 1000;
37
- const DASH_VERSION = "2.4"; // Versione per la gestione della memoria locale
37
+ const DASH_VERSION = "3.0"; // Versione della memoria locale
38
38
  const sourceLocks = {};
39
39
 
40
40
 
@@ -42,7 +42,8 @@ const sourceLocks = {};
42
42
  // 2. STATO GLOBALE E RIFERIMENTI UI
43
43
  // ==========================================================================
44
44
  let simulationMode = false;
45
- let displayModeSog = 'SOG'; // Può essere 'SOG' o 'VMG'
45
+ let displayModeSog = 'SOG';
46
+ let displayModeTws = 'TWS';
46
47
  let socket, renderInterval, simInterval;
47
48
  let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
48
49
  let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
@@ -73,10 +74,10 @@ const store = {
73
74
  timestamps: {},
74
75
  smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
75
76
  longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
76
- histories: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
77
+ histories: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
77
78
  // Buffer temporaneo per il calcolo della media dell'intervallo del grafico
78
- graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
79
- lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0 }
79
+ graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
80
+ lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 }
80
81
  };
81
82
 
82
83
  // Riferimenti agli elementi DOM mappati all'avvio
@@ -236,6 +237,7 @@ function saveDashboardState() {
236
237
  histories: store.histories,
237
238
  longBuf: store.longBuf,
238
239
  displayModeSog: displayModeSog,
240
+ displayModeTws: displayModeTws,
239
241
  graphModes: graphModes,
240
242
  isNightMode: document.body.classList.contains('night-mode'),
241
243
  isFocusActive: isFocusActive,
@@ -267,6 +269,13 @@ function loadDashboardState() {
267
269
  const labelEl = document.getElementById('sog-vmg-label');
268
270
  if (labelEl) labelEl.textContent = displayModeSog;
269
271
  }
272
+
273
+ // Ripristino TWS/AWS ---
274
+ if (state.displayModeTws) {
275
+ displayModeTws = state.displayModeTws;
276
+ const labelEl = document.getElementById('tws-aws-label');
277
+ if (labelEl) labelEl.textContent = displayModeTws;
278
+ }
270
279
 
271
280
  // Ripristino Tema Notte
272
281
  if (state.isNightMode) document.body.classList.add('night-mode');
@@ -515,17 +524,52 @@ function updateWindTrend() {
515
524
  // ==========================================================================
516
525
  // 7. RENDERING ENGINE E AGGIORNAMENTO UI
517
526
  // ==========================================================================
527
+ /**
528
+ * refreshGraph: Recupera i dati corretti dallo store e coordina il disegno del grafico.
529
+ * Gestisce lo switch tra TWS/AWS e la mappatura VMG -> SOG.
530
+ *
531
+ * @param {string} t - Il tipo di dato da aggiornare ('stw', 'sog', 'depth', 'tws', 'vmg')
532
+ */
518
533
  function refreshGraph(t) {
519
- const type = (t === 'vmg') ? 'sog' : t;
520
- const data = store.histories[t]; if (!data || data.length < 2) return;
521
- const mode = graphModes[type], cfg = calculateScale(type, data, mode);
534
+ // 1. Mappatura del box UI: il VMG condivide il riquadro fisico del SOG
535
+ const boxType = (t === 'vmg') ? 'sog' : t;
536
+
537
+ // 2. Selezione della sorgente dati corretta
538
+ let data;
539
+ if (t === 'tws' && displayModeTws === 'AWS') {
540
+ // Se siamo nel box vento e la modalità è AWS, carichiamo la storia dell'apparente
541
+ data = store.histories['aws'];
542
+ } else {
543
+ // Altrimenti carichiamo la storia standard (TWS, STW, SOG, Depth, VMG)
544
+ data = store.histories[t];
545
+ }
546
+
547
+ // 3. Controllo integrità: se non ci sono dati sufficienti, non disegniamo nulla
548
+ if (!data || data.length < 2) return;
549
+
550
+ // 4. Configurazione Scala: recupera la modalità (standard/hercules) e calcola min/max
551
+ const mode = graphModes[boxType];
552
+ const cfg = calculateScale(boxType, data, mode);
522
553
 
523
- // Gestione visualizzazione Hercules Zoom (sfondo rosso)
524
- const box = document.querySelector(`.box-${type}`);
525
- if (box) box.classList.toggle('box-hercules', mode === 'hercules');
554
+ // 5. Aggiornamento estetico del Box: aggiunge lo sfondo speciale se in modalità Hercules
555
+ const box = document.querySelector(`.box-${boxType}`);
556
+ if (box) {
557
+ box.classList.toggle('box-hercules', mode === 'hercules');
558
+ }
526
559
 
527
- updateScaleLabels(type, cfg.min, cfg.max);
528
- drawGraph(data, type + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
560
+ // 6. Aggiornamento etichette numeriche della scala (Y-axis)
561
+ updateScaleLabels(boxType, cfg.min, cfg.max);
562
+
563
+ // 7. Render finale del grafico SVG
564
+ // Il parametro 't === tws' indica a drawGraph di attivare la logica dei colori Reef (Rosso/Arancio)
565
+ drawGraph(
566
+ data,
567
+ boxType + '-graph',
568
+ cfg.min,
569
+ cfg.max,
570
+ t === 'tws',
571
+ mode === 'hercules'
572
+ );
529
573
  }
530
574
 
531
575
  /**
@@ -548,9 +592,6 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
548
592
  }
549
593
  };
550
594
 
551
- /**
552
- * Loop principale di aggiornamento interfaccia (1Hz)
553
- */
554
595
  /**
555
596
  * Loop principale di aggiornamento interfaccia (1Hz)
556
597
  * Gestisce la gerarchia di aggiornamento Live (1s), Heavy (2s) e Slow (3s).
@@ -558,95 +599,164 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
558
599
  function startDisplayLoop() {
559
600
  renderInterval = setInterval(() => {
560
601
  const now = Date.now();
602
+ const isNight = document.body.classList.contains('night-mode');
603
+
604
+ // Conversione velocità da m/s a Nodi
561
605
  const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
562
606
  const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
563
607
 
564
- // Verifica stato navigazione basato su soglia impostata
608
+ // Verifica stato navigazione basato su soglia impostata (minSpeed)
565
609
  isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
566
610
 
567
611
  // --- TIER LIVE (1s): CONTROLLO TIMEOUT DATI ---
568
- const watch = { "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog, "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog, "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth, "environment.wind.speedTrue": ui.tws };
612
+ // Se un dato non arriva da più di 5 secondi, mostra i trattini
613
+ const watch = {
614
+ "navigation.speedThroughWater": ui.stw,
615
+ "navigation.speedOverGround": ui.sog,
616
+ "navigation.headingTrue": ui.hdg,
617
+ "navigation.courseOverGroundTrue": ui.cog,
618
+ "environment.wind.speedApparent": ui.awsSvg,
619
+ "environment.depth.belowTransducer": ui.depth,
620
+ "environment.wind.speedTrue": ui.tws
621
+ };
569
622
  for (let p in watch) {
570
623
  if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
571
624
  watch[p].innerText = "---"; delete store.raw[p];
572
625
  }
573
626
  }
574
627
 
575
- // --- AGGIORNAMENTO DATI ISTANTANEI ---
628
+ // --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
576
629
  if (store.raw["navigation.speedThroughWater"] !== undefined) {
577
- ui.stw.innerText = stwKts.toFixed(1); manageHistory('stw', stwKts);
630
+ ui.stw.innerText = stwKts.toFixed(1);
631
+ // Colore neutro (ereditato dal tema) perché non ha switch di modalità
632
+ ui.stw.style.color = "";
633
+ manageHistory('stw', stwKts);
578
634
  }
579
635
 
580
- // --- LOGICA SOG / VMG E COLORI DINAMICI ---
636
+ // --- LOGICA SOG / VMG (COLORI DINAMICI E FILTRO NAVIGAZIONE) ---
581
637
  if (store.raw["navigation.speedOverGround"] !== undefined) {
638
+ // Calcolo VMG istantanea per il display numerico
582
639
  const vmg = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
583
- manageHistory('vmg', vmg); manageHistory('sog', sogKts);
640
+
641
+ // Registriamo i dati nelle storie (per i grafici)
642
+ manageHistory('vmg', vmg);
643
+ manageHistory('sog', sogKts);
584
644
 
585
645
  const labelEl = document.getElementById('sog-vmg-label');
646
+
586
647
  if (displayModeSog === 'VMG') {
648
+ // MODALITÀ VMG: Mostra valore istantaneo, colore Cyan fisso per identificare la modalità
587
649
  ui.sog.innerText = vmg.toFixed(1);
588
- // AGGIORNATO AL NUOVO COLORE CYAN VIBRANTE (#00b8d4)
589
650
  ui.sog.style.setProperty('color', '#00b8d4', 'important');
590
651
  if (labelEl) labelEl.textContent = 'VMG';
591
652
  } else {
653
+ // MODALITÀ SOG: Mostra valore istantaneo
592
654
  ui.sog.innerText = sogKts.toFixed(1);
593
655
  if (labelEl) labelEl.textContent = 'SOG';
594
656
 
595
- // Colore Corrente: Giallo Caldo per corrente a favore, Rosso per corrente contraria
596
- // (Allineiamo il colore della corrente a favore con il #ffbb33 usato nei grafici)
597
- if (sogKts - stwKts > 0.3) ui.sog.style.setProperty('color', '#ffbb33', 'important');
598
- else if (sogKts - stwKts < -0.3) ui.sog.style.setProperty('color', '#ff3b30', 'important'); // Rosso vivido
599
- else ui.sog.style.color = ""; // Torna normale
657
+ // --- LOGICA COLORE DINAMICO (Solo se in navigazione reale) ---
658
+ if (isNavigating) {
659
+ // Usiamo la media dell'ultimo punto del grafico per stabilizzare il colore (anti-onda)
660
+ const lastAvgSog = store.histories.sog.length > 0 ? store.histories.sog[store.histories.sog.length - 1] : sogKts;
661
+ const lastAvgStw = store.histories.stw.length > 0 ? store.histories.stw[store.histories.stw.length - 1] : stwKts;
662
+ const deltaCurrent = lastAvgSog - lastAvgStw;
663
+
664
+ if (deltaCurrent < -0.3) {
665
+ // CORRENTE CONTRO: Rosso Vivido
666
+ ui.sog.style.setProperty('color', '#ff3b30', 'important');
667
+ } else if (deltaCurrent > 0.3) {
668
+ // CORRENTE A FAVORE: Verde Neon (Feedback Positivo)
669
+ ui.sog.style.setProperty('color', '#00C851', 'important');
670
+ } else {
671
+ // NEUTRO: Amber (Colore base della linea SOG)
672
+ ui.sog.style.setProperty('color', '#ffbb33', 'important');
673
+ }
674
+ } else {
675
+ // NON IN NAVIGAZIONE: Riportiamo il colore al default neutro
676
+ ui.sog.style.color = "";
677
+ }
600
678
  }
601
679
  }
602
680
 
681
+ // --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
603
682
  if (store.raw["environment.depth.belowTransducer"] !== undefined) {
604
683
  ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
684
+ // Il colore neutro/allarme è gestito internamente dalla funzione checkDepthAlarm
605
685
  checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
606
686
  manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
607
687
  }
608
688
 
609
- if (store.raw["environment.wind.speedTrue"] !== undefined) {
610
- const twsKts = msToKts(store.raw["environment.wind.speedTrue"]);
611
- ui.tws.innerText = twsKts.toFixed(1);
612
-
613
- // Colore Reef: se normale usiamo "", il CSS metterà Nero (giorno) o Rosso (notte)
614
- if (twsKts >= CONFIG.graphs.reef2) ui.tws.style.setProperty('color', '#e74c3c', 'important');
615
- else if (twsKts >= CONFIG.graphs.reef1) ui.tws.style.setProperty('color', '#e67e22', 'important');
616
- else ui.tws.style.color = "";
689
+ // --- GESTIONE VENTO (TWS / AWS SWITCH CON COLORI COORDINATI) ---
690
+ const twsKts = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
691
+ const awsKts = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
692
+
693
+ // Registriamo sempre entrambe le storie per permettere lo switch fluido dei grafici
694
+ if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsKts);
695
+ if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsKts);
617
696
 
618
- manageHistory('tws', twsKts);
697
+ if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
698
+ const labelEl = document.getElementById('tws-aws-label');
699
+ const currentWindValue = (displayModeTws === 'AWS') ? awsKts : twsKts;
700
+
701
+ ui.tws.innerText = currentWindValue.toFixed(1);
702
+ if (labelEl) labelEl.textContent = displayModeTws;
703
+
704
+ // Logica Colore Testo Vento (Priorità ai Reef, poi colore di base modalità)
705
+ if (currentWindValue >= CONFIG.graphs.reef2) {
706
+ ui.tws.style.setProperty('color', '#ff3b30', 'important'); // Rosso Reef 2
707
+ } else if (currentWindValue >= CONFIG.graphs.reef1) {
708
+ ui.tws.style.setProperty('color', '#ff9800', 'important'); // Arancio Reef 1
709
+ } else {
710
+ // Colore di base differenziato per modalità (Indaco per AWS, Navy per TWS)
711
+ if (displayModeTws === 'AWS') {
712
+ ui.tws.style.setProperty('color', '#5c6bc0', 'important'); // Indigo
713
+ } else {
714
+ // Per il TWS, schiariamo il Navy in modalità notte per renderlo leggibile
715
+ const twsColor = isNight ? '#6c8ea0' : '#2c3e50';
716
+ ui.tws.style.setProperty('color', twsColor, 'important');
717
+ }
718
+ }
619
719
  }
620
720
 
721
+ // --- AGGIORNAMENTO NUMERO AWS CENTRALE (Bussola) ---
621
722
  if (store.raw["environment.wind.speedApparent"] !== undefined) {
622
- ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
723
+ const awsVal = msToKts(store.raw["environment.wind.speedApparent"]);
724
+ ui.awsSvg.textContent = awsVal.toFixed(1);
623
725
  }
624
726
 
625
727
  // --- PUNTATORI ANALOGICI (Smoothing 2s) ---
626
728
  const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
627
729
  const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
628
- if (smAwa) { curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val)); ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`); }
629
- if (smTwa) { curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val)); ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`); }
730
+ if (smAwa) {
731
+ curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val));
732
+ ui.awa.setAttribute('transform', `rotate(${curAwaRot}, 200, 200)`);
733
+ }
734
+ if (smTwa) {
735
+ curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val));
736
+ ui.twa.setAttribute('transform', `rotate(${curTwaRot}, 200, 200)`);
737
+ }
630
738
 
631
739
  // --- CALCOLO LEEWAY E TRACK POINTER ---
632
740
  if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
633
741
  let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
634
- // Azzeramento sotto soglia minima impostata
742
+ // Filtraggio scarroccio: azzeramento se barca ferma, altrimenti smoothing
635
743
  smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
636
- curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
637
- ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#e67e22" : "";
744
+ curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
745
+ ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
746
+ ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#ff9800" : "";
638
747
  updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
639
748
  }
640
749
 
750
+ // Aggiorna i trend meteo/tattici (pallini) e l'allarme strambata
641
751
  updateWindTrend();
642
752
 
643
- // TIER HEAVY (2s) - Grafici e Persistenza Browser
753
+ // TIER HEAVY (2s) - Aggiornamento Grafici e Salvataggio Stato
644
754
  if (lastAvgUIUpdate++ % 2 === 0) {
645
755
  ['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
646
756
  saveDashboardState();
647
757
  }
648
758
 
649
- // TIER SLOW (3s) - Medie Lunghe e Calcolo TACK
759
+ // TIER SLOW (3s) - Calcolo Medie Lunghe e Tack Strategico
650
760
  if (lastAvgUIUpdate % 3 === 0) {
651
761
  let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
652
762
  let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
@@ -654,6 +764,7 @@ function startDisplayLoop() {
654
764
  let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
655
765
  let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
656
766
 
767
+ // Aggiornamento interfaccia per i valori mediati (Heading, Cog, Awa, Twa, Twd)
657
768
  upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
658
769
  upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
659
770
  upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
@@ -662,27 +773,25 @@ function startDisplayLoop() {
662
773
 
663
774
  // --- LOGICA TACK STRATEGICA (VETTORIALE) ---
664
775
  if (hObj && twdObj) {
665
- // Funzione interna per riflettere un angolo rispetto all'asse del vento (TWD)
666
776
  const reflectAngle = (targetRad, axisRad) => {
667
- const diffSin = Math.sin(axisRad - targetRad);
668
- const diffCos = Math.cos(axisRad - targetRad);
669
- return Math.atan2(Math.sin(axisRad) * diffCos + Math.cos(axisRad) * diffSin,
670
- Math.cos(axisRad) * diffCos - Math.sin(axisRad) * diffSin);
777
+ const dS = Math.sin(axisRad - targetRad);
778
+ const dC = Math.cos(axisRad - targetRad);
779
+ return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS,
780
+ Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
671
781
  };
672
-
782
+
673
783
  const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
674
-
784
+
675
785
  if (!isNavigating) {
676
786
  ui.tackHdg.innerHTML = "---&deg;";
677
787
  } else if (unstableH) {
678
788
  ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data');
679
789
  } else {
680
- const reflectedH = reflectAngle(hObj.val, twdObj.val);
681
- const outH = (radToDeg(reflectedH) + 360) % 360;
682
- ui.tackHdg.innerHTML = `${Math.round(outH).toString().padStart(3, '0')}&deg;`;
790
+ const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
791
+ ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
683
792
  ui.tackHdg.classList.remove('unstable-data');
684
793
  }
685
-
794
+
686
795
  if (cObj) {
687
796
  const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
688
797
  if (!isNavigating) {
@@ -690,20 +799,21 @@ function startDisplayLoop() {
690
799
  } else if (unstableC) {
691
800
  ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data');
692
801
  } else {
693
- const reflectedC = reflectAngle(cObj.val, twdObj.val);
694
- const outC = (radToDeg(reflectedC) + 360) % 360;
695
- ui.tackCog.innerHTML = `${Math.round(outC).toString().padStart(3, '0')}&deg;`;
802
+ const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
803
+ ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
696
804
  ui.tackCog.classList.remove('unstable-data');
697
805
  }
698
806
  }
699
807
  }
700
808
 
701
- // Rotazione Mini-Bussole
809
+ // Rotazione Mini-Icone nella bussola TWD (Mini-Bussole)
702
810
  const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
703
811
  const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
704
812
  if (smHdgIcons && smTwdIcons) {
705
- curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val)); ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
706
- curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val)); ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
813
+ curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
814
+ ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
815
+ curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val));
816
+ ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
707
817
  }
708
818
  }
709
819
  }, RENDER_INTERVAL_MS);
@@ -778,133 +888,269 @@ function updateScaleLabels(t, min, max) {
778
888
  }
779
889
 
780
890
  /**
781
- * drawGraph: Disegna i grafici con griglia temporale intelligente
782
- * Usa un Gradiente Lineare SVG dinamico per eliminare le giunzioni dei poligoni.
891
+ * ==========================================================================
892
+ * drawGraph: Motore di Rendering SVG "Tactical Precision" (Integrale)
893
+ * ==========================================================================
894
+ * Caratteristiche:
895
+ * - Linea dinamica: 1.0px (base) / 1.5px (allerta).
896
+ * - Area segmentata: colore preciso sotto i picchi (0.15 / 0.45 / 0.85).
897
+ * - Fix Colore Bleeding: Usa 'userSpaceOnUse' per mantenere i colori al loro posto.
898
+ * - Fix Diagonale: Chiusura verticale sull'ultimo punto reale.
899
+ * - Palette Vivid Glass: Colori distinti per ogni modalità.
783
900
  */
784
901
  /**
785
- * drawGraph: Disegna i grafici con griglia temporale intelligente
786
- * Versione bilanciata: 1.5px per allarmi critici, 1px per il resto.
902
+ * ==========================================================================
903
+ * drawGraph: Motore di Rendering Grafico SVG "Tactical Glass" (Versione Pro)
904
+ * ==========================================================================
905
+ * Questa funzione trasforma un array di dati in una sparkline SVG dinamica.
906
+ *
907
+ * Caratteristiche principali:
908
+ * 1. LINEA CHIRURGICA: Mantiene uno spessore di 1px costante per un look tecnico.
909
+ * 2. AREA DINAMICA: L'area sottesa cambia colore solo sotto i picchi di allerta.
910
+ * 3. ZERO ARTEFATTI:
911
+ * - Usa LinearGradients con 'userSpaceOnUse' per evitare trascinamenti di colore.
912
+ * - Elimina le righe verticali di giunzione tra i segmenti.
913
+ * - Chiude il tracciato verticalmente eliminando la "coda" diagonale finale.
914
+ * 4. MULTI-MODALITÀ: Gestisce switch AWS/TWS, SOG/VMG e allarmi Profondità.
787
915
  */
788
916
  function drawGraph(d, id, min, max, isTws, isHercules) {
789
917
  const svg = document.getElementById(id);
918
+
919
+ // Uscita di sicurezza se l'elemento non esiste o non ci sono abbastanza dati
790
920
  if (!svg || d.length < 2) return;
791
921
 
792
- const w = 200, h = 40;
793
- const range = max - min || 1;
794
- const isDepth = (id === 'depth-graph');
795
- const samples = CONFIG.graphs.samples;
922
+ // --- 1. CONFIGURAZIONE GEOMETRICA E SCALA ---
923
+ const w = 200; // Larghezza fissa del box grafico
924
+ const h = 40; // Altezza fissa del box grafico
925
+ const range = max - min || 1; // Range dei valori per il calcolo dell'altezza Y
926
+ const isDepth = (id === 'depth-graph'); // Flag specifico per il box profondità
927
+ const samples = CONFIG.graphs.samples; // Numero di campioni previsti (asse X)
928
+
929
+ // --- 2. DEFINIZIONE TAVOLOZZA COLORI "VIVID GLASS" ---
930
+ // Colori di Allerta
931
+ const colDanger = "#ff3b30"; // Rosso Vivido (Apple/Alert style)
932
+ const colWarning = "#ff9800"; // Arancio Fluo (High visibility)
933
+
934
+ // Colori Base (Sotto le soglie di allarme)
935
+ const colTws = "#2c3e50"; // Navy Slate (Vento Reale)
936
+ const colAws = "#5c6bc0"; // Electric Indigo (Vento Apparente)
937
+ const colDepth = "#0088cc"; // Ocean Blue (Profondità)
938
+ const colStw = "#00C851"; // Emerald Neon (Velocità Acqua)
939
+ const colSog = "#ffbb33"; // Amber (Velocità Fondo)
940
+ const colVmg = "#00b8d4"; // Cyan Vibrant (VMG)
941
+
942
+ /**
943
+ * getColorProps: Funzione interna per determinare lo stile di ogni punto.
944
+ * Restituisce un oggetto con {colore, opacità area, spessore linea}.
945
+ */
946
+ const getColorProps = (val) => {
947
+ // Inizializziamo con i valori di default (Dati Normali)
948
+ let color = colTws;
949
+ let opacity = "0.15";
950
+ let stroke = "1"; // Spessore fisso a 1px richiesto
951
+
952
+ // A. LOGICA PER IL VENTO (TWS o AWS)
953
+ if (isTws) {
954
+ // Scegliamo il colore di base a seconda se stiamo guardando Reale o Apparente
955
+ const baseWind = (displayModeTws === 'AWS') ? colAws : colTws;
956
+
957
+ if (val >= CONFIG.graphs.reef2) {
958
+ color = colDanger; opacity = "0.55"; // Zona Pericolo
959
+ } else if (val >= CONFIG.graphs.reef1) {
960
+ color = colWarning; opacity = "0.45"; // Zona Attenzione
961
+ } else {
962
+ color = baseWind; // Zona Sicura
963
+ }
964
+ }
965
+ // B. LOGICA PER LA PROFONDITÀ (Inversa: allerta se il valore scende)
966
+ else if (isDepth) {
967
+ if (val < CONFIG.alarms.depthDanger) {
968
+ color = colDanger; opacity = "0.55";
969
+ } else if (val < CONFIG.alarms.depthWarning) {
970
+ color = colWarning; opacity = "0.45";
971
+ } else {
972
+ color = colDepth;
973
+ }
974
+ }
975
+ // C. LOGICA PER LE VELOCITÀ (STW, SOG, VMG)
976
+ else {
977
+ if (id === 'stw-graph') {
978
+ color = colStw;
979
+ } else if (id === 'sog-graph') {
980
+ // Il box SOG cambia colore se l'utente switcha in modalità VMG
981
+ color = (displayModeSog === 'VMG') ? colVmg : colSog;
982
+ }
983
+ }
984
+ return { color, opacity, stroke };
985
+ };
796
986
 
797
- // 1. Griglia (Sottile 0.5px)
987
+ // --- 3. COSTRUZIONE DELLE GRIGLIE DI RIFERIMENTO ---
798
988
  let grids = "";
989
+ // Linee orizzontali di livello (25%, 50%, 75%)
799
990
  [0.25, 0.5, 0.75].forEach(p => {
800
991
  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" />`;
801
992
  });
993
+ // Linee verticali temporali (dinamiche in base alla storia impostata)
802
994
  const gridInterval = (CONFIG.graphs.historyMinutes <= 15) ? 1 : 5;
803
995
  for (let m = gridInterval; m < CONFIG.graphs.historyMinutes; m += gridInterval) {
804
996
  const x = w - (m / CONFIG.graphs.historyMinutes) * w;
805
997
  grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
806
998
  }
807
999
 
808
- // 2. Tavolozza Colori Vividi
809
- const baseColorTws = "#2c3e50";
810
- const baseColorDepth = "#0088cc";
811
-
812
- const getColor = (val) => {
813
- if (isTws) return (val >= CONFIG.graphs.reef2) ? "#ff3b30" : (val >= CONFIG.graphs.reef1 ? "#ff9800" : baseColorTws);
814
- if (isDepth) return (val < CONFIG.alarms.depthDanger) ? "#ff3b30" : (val < CONFIG.alarms.depthWarning ? "#ff9800" : baseColorDepth);
815
- switch(id) {
816
- case 'stw-graph': return "#00C851";
817
- case 'sog-graph': return (displayModeSog === 'VMG') ? "#00b8d4" : "#ffbb33";
818
- default: return baseColorTws;
819
- }
820
- };
821
-
822
- // 3. Creazione Gradiente e Linee
823
- let gradientStops = "";
824
- let lines = "";
825
- let areaPath = `M 0 ${h} `;
1000
+ // --- 4. CICLO DI ELABORAZIONE DEI DATI ---
1001
+ let gradientStops = ""; // Contiene i cambi di colore per l'area
1002
+ let lines = ""; // Contiene i segmenti della linea superiore
1003
+ let areaPath = `M 0 ${h} `; // Inizio del path del riempimento dal fondo sinistro
826
1004
 
827
1005
  for (let i = 1; i < d.length; i++) {
828
- const percentPrev = ((i - 1) / (samples - 1)) * 100;
829
- const percentCurr = (i / (samples - 1)) * 100;
830
-
1006
+ // Calcolo delle posizioni percentuali (per il gradiente) e pixel (per il disegno)
1007
+ const p1 = ((i - 1) / (samples - 1)) * 100;
1008
+ const p2 = (i / (samples - 1)) * 100;
831
1009
  const x1 = ((i - 1) / (samples - 1)) * w;
832
1010
  const y1 = h - (Math.max(0, Math.min(1, (d[i - 1] - min) / range)) * h);
833
1011
  const x2 = (i / (samples - 1)) * w;
834
1012
  const y2 = h - (Math.max(0, Math.min(1, (d[i] - min) / range)) * h);
835
1013
 
836
- const color = getColor(d[i]);
837
-
838
- // Logica Opacità e Spessore (Tua configurazione)
839
- let fillOpacity = "0.15";
840
- let strokeWidth = "1";
841
-
842
- if (color === "#ff3b30") {
843
- fillOpacity = "0.85"; // Rosso: Molto visibile
844
- strokeWidth = "1.5"; // Rosso: Più marcato
845
- } else if (color === "#ff9800") {
846
- fillOpacity = "0.45"; // Arancio: Velo medio
847
- strokeWidth = "1"; // Arancio: Sottile
848
- }
1014
+ // Otteniamo lo stile per questo specifico segmento
1015
+ const props = getColorProps(d[i]);
849
1016
 
850
- // Costruzione Gradiente (stacchi netti tra i colori)
851
- gradientStops += `<stop offset="${percentPrev}%" stop-color="${color}" stop-opacity="${fillOpacity}" />`;
852
- gradientStops += `<stop offset="${percentCurr}%" stop-color="${color}" stop-opacity="${fillOpacity}" />`;
1017
+ // AGGIUNTA STOP AL GRADIENTE:
1018
+ // Usiamo due stop identici alla stessa percentuale per creare stacchi di colore netti,
1019
+ // evitando sfumature tra una zona sicura e una di allarme.
1020
+ gradientStops += `<stop offset="${p1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
1021
+ gradientStops += `<stop offset="${p2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
853
1022
 
854
- // Disegno Linea con precisione geometrica
1023
+ // DISEGNO DELLA LINEA SUPERIORE:
1024
+ // Usiamo linee individuali per poter gestire spessori e colori diversi (se necessario in futuro)
855
1025
  lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
856
- style="stroke: ${color}; stroke-width: ${strokeWidth}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
1026
+ style="stroke: ${props.color}; stroke-width: ${props.stroke}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
857
1027
 
858
- if (i === 1) areaPath += `L ${x1} ${y1} `;
859
- areaPath += `L ${x2} ${y2} `;
860
-
861
- // Salviamo l'ultima coordinata X calcolata per chiudere correttamente il path
862
- if (i === d.length - 1) {
863
- areaPath += `L ${x2} ${h} Z`;
864
- }
1028
+ // AGGIORNAMENTO DEL PATH DELL'AREA:
1029
+ if (i === 1) areaPath += `L ${x1} ${y1} `; // Primo collegamento con la base
1030
+ areaPath += `L ${x2} ${y2} `; // Tracciato dei picchi
1031
+
1032
+ // --- FIX ARTEFATTO DIAGONALE FINALE ---
1033
+ // Se siamo arrivati all'ultimo dato disponibile nel buffer, chiudiamo il path
1034
+ // scendendo verticalmente verso l'asse zero (h), poi chiudiamo con 'Z'.
1035
+ if (i === d.length - 1) {
1036
+ areaPath += `L ${x2} ${h} Z`;
865
1037
  }
1038
+ }
866
1039
 
867
- // 4. Iniezione del Gradiente
868
- const gradId = `grad-${id}`;
1040
+ // --- 5. CREAZIONE DEL GRADIENTE LINEARE DINAMICO ---
1041
+ const gradId = `grad-${id}`; // ID unico basato sul box (es: grad-tws-graph)
1042
+
1043
+ /**
1044
+ * IMPORTANTE: gradientUnits="userSpaceOnUse" fissa il gradiente alle coordinate
1045
+ * assolute (0-200px). Questo impedisce al colore di allarme di "allungarsi"
1046
+ * erroneamente se il grafico è solo a metà schermo.
1047
+ */
869
1048
  const defs = `
870
1049
  <defs>
871
- <linearGradient id="${gradId}" x1="0%" y1="0%" x2="100%" y2="0%">
1050
+ <linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">
872
1051
  ${gradientStops}
873
1052
  </linearGradient>
874
1053
  </defs>
875
1054
  `;
876
1055
 
877
- // 5. Render Finale
878
- if (isTws || isDepth) {
879
- svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
880
- } else {
881
- // Per STW/SOG usiamo il colore dell'ultimo punto con area fissa 0.15
882
- const currentPathColor = getColor(d[d.length - 1]);
883
- svg.innerHTML = `${grids}<path d="${areaPath}" fill="${currentPathColor}" fill-opacity="0.15" stroke="none" />${lines}`;
884
- }
1056
+ // --- 6. AGGIORNAMENTO FINALE DEL DOM ---
1057
+ // Inseriamo tutto nell'elemento SVG: Defs (Gradienti) -> Griglie -> Area (Fill) -> Linee (Stroke)
1058
+ svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
885
1059
  }
886
1060
 
887
1061
  // ==========================================================================
888
1062
  // 9. INTERAZIONI E GESTI
889
1063
  // ==========================================================================
1064
+ /**
1065
+ * toggleFocusMode: Gestisce l'attivazione della modalità Split-Screen (Focus).
1066
+ * Ingrandisce un box specifico e mantiene visibile la bussola centrale.
1067
+ */
890
1068
  function toggleFocusMode(type, element) {
891
1069
  const container = document.querySelector('.main-container');
1070
+
1071
+ // Identifica se il box appartiene alla colonna sinistra (per il layout CSS)
892
1072
  const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
1073
+
1074
+ // Inverte lo stato del Focus
893
1075
  isFocusActive = !isFocusActive;
894
- if (isFocusActive) { container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right'); element.classList.add('is-focused'); }
895
- else { container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right'); document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused')); }
1076
+
1077
+ if (isFocusActive) {
1078
+ // Applica le classi per dividere lo schermo e mettere in risalto il box
1079
+ container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right');
1080
+ element.classList.add('is-focused');
1081
+ } else {
1082
+ // Rimuove tutte le classi di focus e torna alla griglia standard
1083
+ container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
1084
+ document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
1085
+ }
896
1086
  }
897
1087
 
1088
+ /**
1089
+ * Inizializzazione Gesti e Interazioni sui box con grafico.
1090
+ * Gestisce: Singolo Tap (Switch), Doppio Tap (Zoom), Long Press (Focus).
1091
+ */
898
1092
  ['stw', 'sog', 'tws', 'depth'].forEach(type => {
899
1093
  const el = document.getElementById(type + '-graph').closest('.data-box');
900
1094
  let lastTapTime = 0, tapTimeout, isLongPressActive = false;
901
- el.addEventListener('pointerdown', (e) => { isLongPressActive = false; pressTimer = setTimeout(() => { if (!isFocusActive) { isLongPressActive = true; toggleFocusMode(type, el); lastTapTime = 0; } }, 1000); });
1095
+
1096
+ // --- GESTIONE PRESSIONE (Inizio) ---
1097
+ el.addEventListener('pointerdown', (e) => {
1098
+ isLongPressActive = false;
1099
+ // Timer per attivare il Focus Mode dopo 1 secondo di pressione continua
1100
+ pressTimer = setTimeout(() => {
1101
+ if (!isFocusActive) {
1102
+ isLongPressActive = true;
1103
+ toggleFocusMode(type, el);
1104
+ lastTapTime = 0; // Evita che al rilascio scatti un click
1105
+ }
1106
+ }, 1000);
1107
+ });
1108
+
1109
+ // --- GESTIONE RILASCIO (Fine Gesto) ---
902
1110
  el.addEventListener('pointerup', (e) => {
903
- clearTimeout(pressTimer); if (isLongPressActive) return;
904
- const currentTime = new Date().getTime(), tapDelay = currentTime - lastTapTime;
905
- if (tapDelay < 300 && tapDelay > 0) { clearTimeout(tapTimeout); graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); refreshGraph(type); lastTapTime = 0; }
906
- else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(0, 0, 0, 0.05)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
1111
+ clearTimeout(pressTimer); // Cancella il timer del long press
1112
+ if (isLongPressActive) return; // Se è scattato il focus, non fare altro
1113
+
1114
+ const currentTime = new Date().getTime();
1115
+ const tapDelay = currentTime - lastTapTime;
1116
+
1117
+ // 1. GESTIONE DOPPIO CLICK (Zoom Hercules)
1118
+ if (tapDelay < 300 && tapDelay > 0) {
1119
+ clearTimeout(tapTimeout);
1120
+ // Switch tra modalità scala Standard e Zoom Hercules
1121
+ graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard';
1122
+ localStorage.setItem('mode_' + type, graphModes[type]);
1123
+ refreshGraph(type);
1124
+ lastTapTime = 0;
1125
+ }
1126
+ // 2. GESTIONE SINGOLO CLICK (Switch Dati o Esci dal Focus)
1127
+ else {
1128
+ lastTapTime = currentTime;
1129
+ tapTimeout = setTimeout(() => {
1130
+ // Se il box è in focus, il singolo click lo chiude
1131
+ if (isFocusActive && el.classList.contains('is-focused')) {
1132
+ toggleFocusMode(type, el);
1133
+ }
1134
+ // Se siamo in visualizzazione standard, gestiamo gli switch dati
1135
+ else if (!isFocusActive) {
1136
+ // Switch SOG <-> VMG
1137
+ if (type === 'sog') {
1138
+ displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
1139
+ }
1140
+ // Switch TWS <-> AWS (Nuova Logica)
1141
+ else if (type === 'tws') {
1142
+ displayModeTws = (displayModeTws === 'TWS') ? 'AWS' : 'TWS';
1143
+ }
1144
+
1145
+ // Feedback visivo (lampeggio leggero) dell'avvenuto switch
1146
+ el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
1147
+ setTimeout(() => el.style.backgroundColor = "", 150);
1148
+ }
1149
+ }, 250);
1150
+ }
907
1151
  });
1152
+
1153
+ // --- GESTIONE USCITA (Se l'utente trascina il dito fuori) ---
908
1154
  el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
909
1155
  });
910
1156
 
package/index.html CHANGED
@@ -168,7 +168,10 @@
168
168
  </div>
169
169
 
170
170
  <div class="data-box box-tws">
171
- <div class="label-row"><span class="label">TWS</span><span class="unit">kts</span></div>
171
+ <div class="label-row">
172
+ <span class="label" id="tws-aws-label">TWS</span>
173
+ <span class="unit">kts</span>
174
+ </div>
172
175
  <span class="value" id="tws">0.0</span>
173
176
  <div class="graph-wrapper">
174
177
  <div class="scale-labels" id="tws-scale"></div>
package/index.js CHANGED
@@ -84,19 +84,19 @@ module.exports = function (app) {
84
84
  reef1: {
85
85
  type: 'number',
86
86
  title: '1st Reef Alert (Orange)',
87
- description: "Wind speed (TWS) at which the graph turns orange, suggesting it's time to prepare for the first sail reduction.",
87
+ description: "Wind speed at which the graph turns orange. This threshold applies to the active mode: in TWS it indicates weather intensity, in AWS it indicates pressure on sails/rigging.",
88
88
  default: 15.0
89
89
  },
90
90
  reef2: {
91
91
  type: 'number',
92
92
  title: '2nd Reef Alert (Red)',
93
- description: "Wind speed (TWS) at which the graph turns red, indicating urgent need for sail reduction.",
93
+ description: "Critical wind speed at which the graph turns red. This threshold applies to the active mode: in TWS it warns of high sea state, in AWS it warns of excessive load on the mast/sails.",
94
94
  default: 20.0
95
95
  },
96
96
  historyMinutes: {
97
97
  type: 'number',
98
98
  title: 'Strategic Timeline (Minutes)',
99
- description: "Total duration shown in the charts. Vertical grid lines mark 1-minute intervals for short durations and 5-minute intervals for long ones.",
99
+ description: "Sets the time duration for all charts. It also defines the comparison window for the Strategic Weather Trend (the dot in the TWD compass) to detect long-term wind shifts.",
100
100
  default: 5,
101
101
  enum: [5, 10, 15, 30, 60]
102
102
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "4.0.21",
4
- "description": "Public Wind Dashboard with navigation and course aids",
3
+ "version": "5.0.2",
4
+ "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
7
7
  "access": "public"