@sailingrotevista/rotevista-dash 6.0.2 → 6.0.4

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 +253 -13
  2. package/index.html +1 -1
  3. package/index.js +13 -11
  4. package/package.json +1 -1
package/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ==========================================================================
3
- * Signal K Wind Dashboard - Pro Version 3.8 (Dynamic Envelope Architecture)
3
+ * Signal K Wind Dashboard - Pro Version 6.0 (Dynamic Envelope Architecture)
4
4
  * ==========================================================================
5
5
  * Autore: Sailing Rotevista
6
6
  * Motore di calcolo tattico per navigazione e crociera.
@@ -35,7 +35,7 @@ let CONFIG = {
35
35
  const RENDER_INTERVAL_MS = 1000;
36
36
  const TIMEOUT_MS = 15000;
37
37
  const SIM_SAMPLE_INTERVAL = 1000;
38
- const DASH_VERSION = "6.0"; // Major Update: Smart Source Locking & Breathing Hercules Scale
38
+ const DASH_VERSION = "6.0"; // Major Update: Server-Side History RAM Logging (Pro v6.0)
39
39
 
40
40
  // ==========================================================================
41
41
  // 2. STATO GLOBALE E RIFERIMENTI UI
@@ -616,11 +616,14 @@ function startDisplayLoop() {
616
616
  if (store.raw["navigation.speedThroughWater"] !== undefined) {
617
617
  ui.stw.innerText = stwKts.toFixed(1);
618
618
  ui.stw.style.color = ""; // Neutro
619
+ manageHistory('stw', stwKts);
619
620
  }
620
621
 
621
622
  // --- LOGICA SOG / VMG ---
622
623
  if (store.raw["navigation.speedOverGround"] !== undefined) {
623
624
  const vmgVal = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
625
+ manageHistory('vmg', vmgVal);
626
+ manageHistory('sog', sogKts);
624
627
 
625
628
  const labelSogVmg = document.getElementById('sog-vmg-label');
626
629
  if (displayModeSog === 'VMG') {
@@ -649,12 +652,16 @@ function startDisplayLoop() {
649
652
  if (store.raw["environment.depth.belowTransducer"] !== undefined) {
650
653
  ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
651
654
  checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
655
+ manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
652
656
  }
653
657
 
654
658
  // --- GESTIONE VENTO (TWS / AWS SWITCH) ---
655
659
  const twsVal = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
656
660
  const awsVal = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
657
661
 
662
+ if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsVal);
663
+ if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsVal);
664
+
658
665
  if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
659
666
  const labelWind = document.getElementById('tws-aws-label');
660
667
  const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
@@ -687,7 +694,7 @@ function startDisplayLoop() {
687
694
  if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
688
695
 
689
696
  if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
690
- let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
697
+ let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (2 * Math.PI) - Math.PI);
691
698
  smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
692
699
  curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
693
700
  ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
@@ -758,6 +765,16 @@ function startDisplayLoop() {
758
765
 
759
766
  let currentConfigString = ""; // Memoria per rilevare cambiamenti nei settings
760
767
 
768
+ /**
769
+ * Risolve dinamicamente l'URL dell'API del Cerbo GX se siamo in locale su Mac/PC
770
+ */
771
+ function getApiUrl(path) {
772
+ if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
773
+ return `http://${CONFIG.server.fallbackIp}${path}`;
774
+ }
775
+ return path;
776
+ }
777
+
761
778
  /**
762
779
  * Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
763
780
  */
@@ -784,7 +801,7 @@ function applyConfigData(data) {
784
801
  */
785
802
  async function fetchServerConfig() {
786
803
  try {
787
- const response = await fetch('/rotevista-config');
804
+ const response = await fetch(getApiUrl('/rotevista-config'));
788
805
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
789
806
  const data = await response.json();
790
807
 
@@ -803,7 +820,7 @@ async function fetchServerConfig() {
803
820
  */
804
821
  async function watchConfigChanges() {
805
822
  try {
806
- const response = await fetch('/rotevista-config');
823
+ const response = await fetch(getApiUrl('/rotevista-config'));
807
824
  if (!response.ok) return;
808
825
  const data = await response.json();
809
826
 
@@ -834,7 +851,7 @@ async function watchConfigChanges() {
834
851
  */
835
852
  async function fetchServerHistory() {
836
853
  try {
837
- const response = await fetch('/rotevista-history');
854
+ const response = await fetch(getApiUrl('/rotevista-history'));
838
855
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
839
856
  const data = await response.json();
840
857
 
@@ -852,6 +869,226 @@ async function fetchServerHistory() {
852
869
  }
853
870
  }
854
871
 
872
+ /**
873
+ * manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
874
+ * Integrazioni:
875
+ * 1. Strict undefined check per lastUpdates.
876
+ * 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
877
+ * 3. Clamping di sicurezza (no negativi, no Infinity).
878
+ */
879
+ function manageHistory(type, value) {
880
+ // --- 1. VALIDAZIONE INPUT RIGOROSA ---
881
+ if (value === undefined || value === null || !isFinite(value)) return;
882
+
883
+ const now = Date.now();
884
+ const historyMinutes = Math.max(1, CONFIG.graphs.historyMinutes || 10);
885
+ const samples = Math.max(2, CONFIG.graphs.samples || 60);
886
+ const bucketIntervalMs = (historyMinutes * 60000) / samples;
887
+
888
+ // --- 2. INIT SICURO (Strict Check) ---
889
+ if (!store.graphTempBuf[type]) store.graphTempBuf[type] = [];
890
+ if (!store.histories[type]) store.histories[type] = [];
891
+ if (store.lastUpdates[type] === undefined) store.lastUpdates[type] = 0;
892
+
893
+ const tempBuf = store.graphTempBuf[type];
894
+
895
+ // --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
896
+ if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
897
+ const lastPoint = tempBuf[tempBuf.length - 1];
898
+ const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
899
+ if (lastPoint && lastPoint.val > glitchThreshold) return;
900
+ }
901
+
902
+ // --- 4. STORAGE TEMPORANEO ---
903
+ tempBuf.push({ val: value, time: now });
904
+
905
+ // Controllo finestra temporale
906
+ const bucketReady = (now - store.lastUpdates[type] > bucketIntervalMs) || store.histories[type].length === 0;
907
+ if (!bucketReady) return;
908
+
909
+ // --- 5. AGGREGAZIONE SEMANTICA ---
910
+ let finalValue = value;
911
+
912
+ if (tempBuf.length > 0) {
913
+ // A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
914
+ if (type === 'tws' || type === 'aws') {
915
+ const tauMs = 2500;
916
+ let ema = tempBuf[0].val;
917
+ let maxSustained = ema;
918
+
919
+ for (let i = 1; i < tempBuf.length; i++) {
920
+ const dt = Math.max(1, tempBuf[i].time - tempBuf[i - 1].time);
921
+ const alpha = 1 - Math.exp(-dt / tauMs);
922
+ ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
923
+ if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
924
+ }
925
+ finalValue = maxSustained;
926
+ }
927
+ // B. PROFONDITÀ -> MINIMO
928
+ else if (type === 'depth') {
929
+ const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
930
+ if (vals.length > 0) finalValue = Math.min(...vals);
931
+ }
932
+ // C. VELOCITÀ -> MEDIA
933
+ else {
934
+ const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
935
+ if (vals.length > 0) {
936
+ const sum = vals.reduce((a, b) => a + b, 0);
937
+ finalValue = sum / tempBuf.length;
938
+ }
939
+ }
940
+ }
941
+
942
+ // --- 6. CLAMPING E VALIDAZIONE FINALE ---
943
+ if (!isFinite(finalValue)) return;
944
+ finalValue = Math.max(0, finalValue);
945
+
946
+ // --- 7. STORAGE STORICO ---
947
+ store.histories[type].push({ val: finalValue, time: now });
948
+
949
+ // --- 8. PRUNING DINAMICO ---
950
+ const maxViewportMinutes = historyMinutes * 2;
951
+ const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
952
+
953
+ while (store.histories[type].length > 0 && (now - store.histories[type][0].time) > maxHistoryMs) {
954
+ store.histories[type].shift();
955
+ }
956
+
957
+ // Reset per il prossimo bucket
958
+ store.graphTempBuf[type] = [];
959
+ store.lastUpdates[type] = now;
960
+ }
961
+
962
+ /**
963
+ * Gestione dinamica delle scale dei grafici (Involucro Elastico Snapped e Safety Zoom)
964
+ * Implementa lo "Snap a Griglia" basato sull'hercSpan senza padding per tutti i sensori.
965
+ */
966
+ function calculateScale(type, data, mode) {
967
+ const s = CONFIG.scales[type];
968
+ const currentVal = data[data.length - 1];
969
+
970
+ // Fallback di emergenza se il buffer è momentaneamente vuoto
971
+ if (currentVal === undefined || currentVal === null) {
972
+ return { min: 0, max: s ? s.stdMax : 10 };
973
+ }
974
+
975
+ // Inizializzazione della memoria delle scale nello store se non esiste
976
+ if (!store.herculesScales) store.herculesScales = {};
977
+ if (!store.herculesScales[type]) {
978
+ store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
979
+ }
980
+ let currentScale = store.herculesScales[type];
981
+
982
+ // ==========================================================================
983
+ // 1. SEZIONE PROFONDITÀ (DEDICATA A 2 MINUTI DI SICUREZZA, MINIMO FISSO A 0)
984
+ // ==========================================================================
985
+ if (type === 'depth') {
986
+ const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
987
+ if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
988
+
989
+ const now = Date.now();
990
+ const depthSafetyWindowMs = 120000; // 2 minuti fissi per la sicurezza
991
+
992
+ // Estrazione dati reali degli ultimi 2 minuti con timestamp
993
+ const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
994
+ const recentVals = recentPoints.map(p => p.val);
995
+
996
+ const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
997
+ const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
998
+
999
+ // --- 1.1 NORMALE PROFONDITÀ (CON SOGLIA STANDARD E FILTRO 2 MINUTI) ---
1000
+ if (mode !== 'hercules') {
1001
+ if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
1002
+ store.depthProtectedActive = true;
1003
+ }
1004
+ if (store.depthProtectedActive) {
1005
+ if (localMin > shallowThreshold) {
1006
+ store.depthProtectedActive = false;
1007
+ }
1008
+ }
1009
+ if (store.depthProtectedActive) {
1010
+ return { min: 0, max: shallowThreshold };
1011
+ }
1012
+ const maxHistorico = Math.max(...data);
1013
+ return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
1014
+ }
1015
+
1016
+ // --- 1.2 HERCULES PROFONDITÀ (0 IN BASSO, SNAP SUL MAX SENZA PADDING) ---
1017
+ if (mode === 'hercules') {
1018
+ const roundStep = s.hercSpan; // Passo di griglia selezionato dall'utente
1019
+
1020
+ let targetMin = 0;
1021
+ let targetMax = Math.ceil(localMax / roundStep) * roundStep;
1022
+
1023
+ // Impediamo una scala inferiore a 4 metri per sicurezza visiva
1024
+ const absoluteMinSpan = 4;
1025
+ if (targetMax < absoluteMinSpan) {
1026
+ targetMax = absoluteMinSpan;
1027
+ }
1028
+
1029
+ // Regola asimmetrica per il MAX (Espansione istantanea, contrazione a 2 minuti)
1030
+ if (currentVal > currentScale.max) {
1031
+ currentScale.max = targetMax;
1032
+ } else {
1033
+ const allStableInTarget = recentVals.every(val => val <= targetMax);
1034
+ if (allStableInTarget) {
1035
+ currentScale.max = targetMax;
1036
+ }
1037
+ }
1038
+
1039
+ currentScale.min = 0;
1040
+ return { min: currentScale.min, max: currentScale.max };
1041
+ }
1042
+ }
1043
+
1044
+ // ==========================================================================
1045
+ // 2. ALTRI GRAFICI (STW, SOG, TWS): COMPORTAMENTO ADATTIVO SU TERZO DEL GRAFICO
1046
+ // ==========================================================================
1047
+
1048
+ // --- 2.1 MODALITÀ NORMALE ---
1049
+ if (mode !== 'hercules') {
1050
+ const maxHistorico = Math.max(...data);
1051
+ return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
1052
+ }
1053
+
1054
+ // --- 2.2 MODALITÀ HERCULES AD ALTO CONTRASTO (SNAP SENZA PADDING) ---
1055
+ if (mode === 'hercules') {
1056
+ const roundStep = s.hercSpan; // Passo di griglia (ex hercSpan)
1057
+
1058
+ const oneThirdCount = Math.max(1, Math.floor(data.length / 3));
1059
+ const recentThirdData = data.slice(-oneThirdCount);
1060
+
1061
+ const localMin = Math.min(...recentThirdData);
1062
+ const localMax = Math.max(...recentThirdData);
1063
+
1064
+ // Applichiamo lo snap diretto ai multipli della griglia
1065
+ let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
1066
+ let targetMax = Math.ceil(localMax / roundStep) * roundStep;
1067
+
1068
+ // Se lo span calcolato è nullo (es. velocità costante), forziamo lo span minimo
1069
+ if (targetMax - targetMin === 0) {
1070
+ targetMax = targetMin + roundStep;
1071
+ }
1072
+
1073
+ // APPLICAZIONE REGOLE ASIMMETRICHE ANTI-CLIPPING
1074
+ // A. Espansione ISTANTANEA in alto o in basso (sicurezza)
1075
+ if (currentVal < currentScale.min || currentVal > currentScale.max) {
1076
+ currentScale.min = Math.min(currentScale.min, targetMin);
1077
+ currentScale.max = Math.max(currentScale.max, targetMax);
1078
+ }
1079
+ // B. Contrazione RITARDATA (solo se tutto il terzo recente si è assestato nel target)
1080
+ else {
1081
+ const allStableInTarget = recentThirdData.every(val => val >= targetMin && val <= targetMax);
1082
+ if (allStableInTarget) {
1083
+ currentScale.min = targetMin;
1084
+ currentScale.max = targetMax;
1085
+ }
1086
+ }
1087
+
1088
+ return { min: currentScale.min, max: currentScale.max };
1089
+ }
1090
+ }
1091
+
855
1092
  function updateScaleLabels(t, min, max) {
856
1093
  const el = document.getElementById(t + '-scale');
857
1094
  if (el) el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`;
@@ -885,6 +1122,7 @@ function refreshGraph(t) {
885
1122
 
886
1123
  /**
887
1124
  * drawGraph: Motore SVG con Timeline Reale e Gestione GAP
1125
+ * Risolve i conflitti di orologio (Clock Drift) tra Cerbo GX e Tablet.
888
1126
  */
889
1127
  function drawGraph(d, id, min, max, isTws, isHercules) {
890
1128
  const svg = document.getElementById(id);
@@ -893,7 +1131,11 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
893
1131
  const w = 200, h = 40;
894
1132
  const range = max - min || 1;
895
1133
  const isDepth = (id === 'depth-graph');
896
- const now = Date.now();
1134
+
1135
+ // --- RISOLUZIONE DISALLINEAMENTO ORARIO ---
1136
+ // Usiamo il tempo del sensore (l'ultimo dato ricevuto) invece dell'orologio del tablet
1137
+ const latestPoint = d[d.length - 1];
1138
+ const now = latestPoint ? latestPoint.time : Date.now();
897
1139
 
898
1140
  const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
899
1141
  const viewportMs = visibleMinutes * 60000;
@@ -1123,19 +1365,17 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
1123
1365
  async function init() {
1124
1366
  loadDashboardState();
1125
1367
 
1126
- // Prova a caricare lo storico dal server. Se fallisce (test offline), gestisce il fallback
1368
+ // Prova a caricare lo storico reale dal Cerbo GX tramite l'API deviata
1127
1369
  try {
1128
1370
  await fetchServerHistory();
1129
1371
  } catch (err) {
1130
- if (simulationMode) {
1131
- // Se sei in modalità simulazione locale, puoi generare dati finti qui (opzionale)
1132
- console.log("🎮 Modalità Simulazione Locale attiva.");
1133
- }
1372
+ console.warn("⚠️ Impossibile caricare lo storico dal server.");
1134
1373
  }
1135
1374
 
1136
1375
  await fetchServerConfig();
1137
1376
  startDisplayLoop();
1138
- connect();
1377
+ connect(); // Si collegherà in tempo reale al WebSocket del Cerbo (usando l'IP di fallback se sei su Mac)
1378
+
1139
1379
  setInterval(watchConfigChanges, 10000);
1140
1380
  }
1141
1381
 
package/index.html CHANGED
@@ -210,6 +210,6 @@
210
210
  </div>
211
211
  </div>
212
212
 
213
- <script src="app.js"></script>
213
+ <script src="app.js?v=6.0"></script>
214
214
  </body>
215
215
  </html>
package/index.js CHANGED
@@ -37,17 +37,19 @@ module.exports = function (app) {
37
37
  lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 };
38
38
  raw = {};
39
39
 
40
- // 2. Registra le rotte API solo la prima volta
41
- if (!routeRegistered) {
42
- app.get('/rotevista-config', (req, res) => {
43
- res.json(currentConfig);
44
- });
45
- app.get('/rotevista-history', (req, res) => {
46
- res.json(histories);
47
- });
48
- routeRegistered = true;
49
- app.debug('Public API endpoints registered at /rotevista-config and /rotevista-history');
50
- }
40
+ // 2. Registra le rotte API solo la prima volta (Abilitate per CORS remoto)
41
+ if (!routeRegistered) {
42
+ app.get('/rotevista-config', (req, res) => {
43
+ res.header("Access-Control-Allow-Origin", "*"); // Sblocca la Dashboard locale su Mac
44
+ res.json(currentConfig);
45
+ });
46
+ app.get('/rotevista-history', (req, res) => {
47
+ res.header("Access-Control-Allow-Origin", "*"); // Sblocca la Dashboard locale su Mac
48
+ res.json(histories);
49
+ });
50
+ routeRegistered = true;
51
+ app.debug('Public API endpoints registered at /rotevista-config and /rotevista-history');
52
+ }
51
53
 
52
54
  // 3. Iscrizione ai dati dei sensori di bordo tramite Signal K
53
55
  const localSubscription = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "6.0.2",
3
+ "version": "6.0.4",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {