@sailingrotevista/rotevista-dash 5.0.7 → 6.0.1

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 +28 -212
  2. package/index.js +214 -17
  3. package/package.json +1 -1
package/app.js CHANGED
@@ -616,14 +616,11 @@ 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);
620
619
  }
621
620
 
622
621
  // --- LOGICA SOG / VMG ---
623
622
  if (store.raw["navigation.speedOverGround"] !== undefined) {
624
623
  const vmgVal = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
625
- manageHistory('vmg', vmgVal);
626
- manageHistory('sog', sogKts);
627
624
 
628
625
  const labelSogVmg = document.getElementById('sog-vmg-label');
629
626
  if (displayModeSog === 'VMG') {
@@ -652,16 +649,12 @@ function startDisplayLoop() {
652
649
  if (store.raw["environment.depth.belowTransducer"] !== undefined) {
653
650
  ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
654
651
  checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
655
- manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
656
652
  }
657
653
 
658
654
  // --- GESTIONE VENTO (TWS / AWS SWITCH) ---
659
655
  const twsVal = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
660
656
  const awsVal = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
661
657
 
662
- if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsVal);
663
- if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsVal);
664
-
665
658
  if (store.raw["environment.wind.speedTrue"] !== undefined || store.raw["environment.wind.speedApparent"] !== undefined) {
666
659
  const labelWind = document.getElementById('tws-aws-label');
667
660
  const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
@@ -801,7 +794,7 @@ async function fetchServerConfig() {
801
794
  applyConfigData(data);
802
795
  console.log("✅ Configurazione iniziale applicata con successo.");
803
796
  } catch (err) {
804
- console.warn("⚠️ Impossibile raggiungere il server per le configurazioni. Utilizzo default locali. Motivo:", err.message);
797
+ console.warn("⚠️ Impossibile raggiungere le configurazioni dal server. Utilizzo default.");
805
798
  }
806
799
  }
807
800
 
@@ -837,215 +830,26 @@ async function watchConfigChanges() {
837
830
  }
838
831
 
839
832
  /**
840
- * manageHistory v3.7 - Aggregazione semantica "Pro-Grade"
841
- * Integrazioni:
842
- * 1. Strict undefined check per lastUpdates.
843
- * 2. Anti-dropout dinamico tarato sul 50% del Reef 1.
844
- * 3. Clamping di sicurezza (no negativi, no Infinity).
833
+ * Recupera lo storico dei grafici pre-popolato dal server Signal K (Pro v6.0)
845
834
  */
846
- function manageHistory(type, value) {
847
- // --- 1. VALIDAZIONE INPUT RIGOROSA ---
848
- if (value === undefined || value === null || !isFinite(value)) return;
849
-
850
- const now = Date.now();
851
- const historyMinutes = Math.max(1, CONFIG.graphs.historyMinutes || 10);
852
- const samples = Math.max(2, CONFIG.graphs.samples || 60);
853
- const bucketIntervalMs = (historyMinutes * 60000) / samples;
854
-
855
- // --- 2. INIT SICURO (Strict Check) ---
856
- if (!store.graphTempBuf[type]) store.graphTempBuf[type] = [];
857
- if (!store.histories[type]) store.histories[type] = [];
858
- if (store.lastUpdates[type] === undefined) store.lastUpdates[type] = 0;
859
-
860
- const tempBuf = store.graphTempBuf[type];
861
-
862
- // --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
863
- if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
864
- const lastPoint = tempBuf[tempBuf.length - 1];
865
- const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
866
- if (lastPoint && lastPoint.val > glitchThreshold) return;
867
- }
868
-
869
- // --- 4. STORAGE TEMPORANEO ---
870
- tempBuf.push({ val: value, time: now });
871
-
872
- // Controllo finestra temporale
873
- const bucketReady = (now - store.lastUpdates[type] > bucketIntervalMs) || store.histories[type].length === 0;
874
- if (!bucketReady) return;
875
-
876
- // --- 5. AGGREGAZIONE SEMANTICA ---
877
- let finalValue = value;
878
-
879
- if (tempBuf.length > 0) {
880
- // A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
881
- if (type === 'tws' || type === 'aws') {
882
- const tauMs = 2500;
883
- let ema = tempBuf[0].val;
884
- let maxSustained = ema;
885
-
886
- for (let i = 1; i < tempBuf.length; i++) {
887
- const dt = Math.max(1, tempBuf[i].time - tempBuf[i - 1].time);
888
- const alpha = 1 - Math.exp(-dt / tauMs);
889
- ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
890
- if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
891
- }
892
- finalValue = maxSustained;
893
- }
894
- // B. PROFONDITÀ -> MINIMO
895
- else if (type === 'depth') {
896
- const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
897
- if (vals.length > 0) finalValue = Math.min(...vals);
898
- }
899
- // C. VELOCITÀ -> MEDIA
900
- else {
901
- const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
902
- if (vals.length > 0) {
903
- const sum = vals.reduce((a, b) => a + b, 0);
904
- finalValue = sum / tempBuf.length;
905
- }
906
- }
907
- }
908
-
909
- // --- 6. CLAMPING E VALIDAZIONE FINALE ---
910
- if (!isFinite(finalValue)) return;
911
- finalValue = Math.max(0, finalValue);
912
-
913
- // --- 7. STORAGE STORICO ---
914
- store.histories[type].push({ val: finalValue, time: now });
915
-
916
- // --- 8. PRUNING DINAMICO ---
917
- const maxViewportMinutes = historyMinutes * 2;
918
- const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
919
-
920
- while (store.histories[type].length > 0 && (now - store.histories[type][0].time) > maxHistoryMs) {
921
- store.histories[type].shift();
922
- }
923
-
924
- // Reset per il prossimo bucket
925
- store.graphTempBuf[type] = [];
926
- store.lastUpdates[type] = now;
927
- }
928
-
929
- /**
930
- * Gestione dinamica delle scale dei grafici (Involucro Elastico e Safety Zoom)
931
- * Implementa la logica di sicurezza disaccoppiata a 2 minuti per la profondità.
932
- */
933
- function calculateScale(type, data, mode) {
934
- const s = CONFIG.scales[type];
935
- const currentVal = data[data.length - 1];
936
-
937
- // Fallback di emergenza se il buffer è momentaneamente vuoto
938
- if (currentVal === undefined || currentVal === null) {
939
- return { min: 0, max: s ? s.stdMax : 10 };
940
- }
941
-
942
- // Inizializzazione della memoria delle scale nello store se non esiste
943
- if (!store.herculesScales) store.herculesScales = {};
944
- if (!store.herculesScales[type]) {
945
- store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
946
- }
947
- let currentScale = store.herculesScales[type];
948
-
949
- // ==========================================================================
950
- // SEZIONE PROFONDITÀ (REGOLA DI SICUREZZA DISACCOPPIATA A 2 MINUTI)
951
- // ==========================================================================
952
- if (type === 'depth') {
953
- const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
954
- if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
955
-
956
- const now = Date.now();
957
- const depthSafetyWindowMs = 120000; // 2 minuti di stabilizzazione fissi per la sicurezza
835
+ async function fetchServerHistory() {
836
+ try {
837
+ const response = await fetch('/rotevista-history');
838
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
839
+ const data = await response.json();
958
840
 
959
- // Estrazione dati reali degli ultimi 2 minuti con timestamp
960
- const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
961
- const recentVals = recentPoints.map(p => p.val);
962
-
963
- const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
964
- const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
965
-
966
- // --- NORMALE PROFONDITÀ (CON SOGLIA STANDARD E FILTRO 2 MINUTI) ---
967
- if (mode !== 'hercules') {
968
- // Entrata istantanea sotto lo Standard Max (sicurezza immediata)
969
- if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
970
- store.depthProtectedActive = true;
971
- }
972
-
973
- // Uscita ritardata: usciamo solo se il minimo degli ultimi 2 minuti è sopra soglia
974
- if (store.depthProtectedActive) {
975
- if (localMin > shallowThreshold) {
976
- store.depthProtectedActive = false;
977
- }
978
- }
979
-
980
- if (store.depthProtectedActive) {
981
- return { min: 0, max: shallowThreshold }; // Blocco a [0 - 20m]
982
- }
983
-
984
- // Se siamo fuori, scala dinamica normale basata sull'intero buffer passato
985
- const maxHistorico = Math.max(...data);
986
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
987
- }
988
-
989
- // --- HERCULES PROFONDITÀ (0 IN BASSO, ZOOM SUL MASSIMO DEI 2 MINUTI) ---
990
- if (mode === 'hercules') {
991
- const padding = 1.0; // 1 metro di margine sopra il fondo
992
-
993
- // Calcoliamo i limiti ideali basati solo sugli ultimi 2 minuti
994
- let targetMin = 0;
995
- let targetMax = Math.ceil(localMax + padding);
996
-
997
- // Impediamo una scala troppo stretta (minimo 4 metri di range totale per sicurezza)
998
- const absoluteMinSpan = 4;
999
- if (targetMax < absoluteMinSpan) {
1000
- targetMax = absoluteMinSpan;
1001
- }
1002
-
1003
- // Regola asimmetrica di aggiornamento
1004
- if (currentVal > currentScale.max) {
1005
- // Se andiamo verso il fondo profondo, allarghiamo istantaneamente
1006
- currentScale.max = targetMax;
1007
- } else {
1008
- // Stringiamo lo zoom solo se tutti i dati degli ultimi 2 minuti sono inferiori al target
1009
- const allStableInTarget = recentVals.every(val => val <= targetMax);
1010
- if (allStableInTarget) {
1011
- currentScale.max = targetMax;
841
+ if (data && typeof data === 'object') {
842
+ for (let key in data) {
843
+ if (store.histories[key] !== undefined) {
844
+ store.histories[key] = data[key];
1012
845
  }
1013
846
  }
1014
-
1015
- currentScale.min = 0;
1016
- return { min: currentScale.min, max: currentScale.max };
847
+ console.log("📈 Storico dei grafici pre-popolato caricato dal server.");
1017
848
  }
849
+ } catch (err) {
850
+ console.warn("⚠️ Impossibile caricare lo storico dal server. Utilizzo dati vuoti/simulati.");
851
+ throw err; // Rilancia l'errore per far attivare il fallback nella funzione init()
1018
852
  }
1019
-
1020
- // ==========================================================================
1021
- // ALTRI GRAFICI (STW, SOG, TWS): MANTENGONO IL COMPORTAMENTO ORIGINALE
1022
- // ==========================================================================
1023
- let aMin = Math.min(...data), aMax = Math.max(...data);
1024
-
1025
- if (mode === 'hercules') {
1026
- const padding = (type === 'stw' || type === 'sog') ? 0.5 : 1.0;
1027
-
1028
- let min = Math.max(0, Math.floor(aMin - padding));
1029
- let max = Math.ceil(aMax + padding);
1030
-
1031
- // Garantisce lo span minimo
1032
- const currentSpan = max - min;
1033
- if (currentSpan < s.hercSpan) {
1034
- const diff = s.hercSpan - currentSpan;
1035
- min = Math.max(0, min - Math.floor(diff / 2));
1036
- max = min + s.hercSpan;
1037
- }
1038
-
1039
- // Arrotonda la griglia numerica a step prefissati
1040
- const roundStep = (type === 'stw' || type === 'sog') ? 0.5 : 1.0;
1041
- min = Math.floor(min / roundStep) * roundStep;
1042
- max = Math.ceil(max / roundStep) * roundStep;
1043
-
1044
- return { min, max };
1045
- }
1046
-
1047
- // Scala Standard (Autocompressione a scatti)
1048
- return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
1049
853
  }
1050
854
 
1051
855
  function updateScaleLabels(t, min, max) {
@@ -1120,7 +924,8 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
1120
924
  };
1121
925
 
1122
926
  let grids = "";
1123
- [0.25, 0.5, 0.75].forEach(p => 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" />`);
927
+ // Una sola linea di griglia centrale al 50%, matematicamente sempre intera e allineata all'etichetta
928
+ [0.5].forEach(p => 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" />`);
1124
929
 
1125
930
  const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
1126
931
  for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
@@ -1317,6 +1122,17 @@ window.addEventListener('contextmenu', e => e.preventDefault(), true);
1317
1122
 
1318
1123
  async function init() {
1319
1124
  loadDashboardState();
1125
+
1126
+ // Prova a caricare lo storico dal server. Se fallisce (test offline), gestisce il fallback
1127
+ try {
1128
+ await fetchServerHistory();
1129
+ } 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
+ }
1134
+ }
1135
+
1320
1136
  await fetchServerConfig();
1321
1137
  startDisplayLoop();
1322
1138
  connect();
package/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * ==========================================================================
3
- * Rotevista Dash Configuration Plugin
3
+ * Rotevista Dash Configuration & History Plugin (Pro v6.0)
4
4
  * ==========================================================================
5
5
  * Definisce l'interfaccia di configurazione in Signal K Admin e crea
6
- * l'endpoint pubblico per la comunicazione con la Dashboard.
6
+ * gli endpoint pubblici per la Dashboard, mantenendo lo storico in RAM.
7
7
  */
8
8
 
9
9
  module.exports = function (app) {
@@ -14,40 +14,213 @@ module.exports = function (app) {
14
14
 
15
15
  let currentConfig = {};
16
16
  let routeRegistered = false;
17
+ let unsubscribes = [];
18
+
19
+ // Database dello storico in RAM sul server
20
+ let histories = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
21
+ let graphTempBuf = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
22
+ let lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 };
23
+ let raw = {};
17
24
 
18
25
  /**
19
26
  * plugin.start: Inizializza il plugin.
20
27
  * Viene chiamato all'avvio e OGNI VOLTA che clicchi "Save" nelle impostazioni.
21
28
  */
22
29
  plugin.start = function (options) {
23
- // 1. Aggiorna la configurazione in memoria (per l'endpoint pubblico)
30
+ // 1. Aggiorna la configurazione in memoria
24
31
  currentConfig = options;
25
-
26
- // 2. Log di debug nel server Signal K
27
32
  app.debug(`${plugin.name} started/updated with new options`);
28
33
 
29
- // 3. Registra la rotta API solo la prima volta
34
+ // Reset dello storico al riavvio del plugin per evitare incoerenze
35
+ histories = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
36
+ graphTempBuf = { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] };
37
+ lastUpdates = { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 };
38
+ raw = {};
39
+
40
+ // 2. Registra le rotte API solo la prima volta
30
41
  if (!routeRegistered) {
31
42
  app.get('/rotevista-config', (req, res) => {
32
43
  res.json(currentConfig);
33
44
  });
45
+ app.get('/rotevista-history', (req, res) => {
46
+ res.json(histories);
47
+ });
34
48
  routeRegistered = true;
35
- app.debug('Public API endpoint registered at /rotevista-config');
49
+ app.debug('Public API endpoints registered at /rotevista-config and /rotevista-history');
36
50
  }
51
+
52
+ // 3. Iscrizione ai dati dei sensori di bordo tramite Signal K
53
+ const localSubscription = {
54
+ context: 'vessels.self',
55
+ subscribe: [
56
+ { path: 'navigation.speedThroughWater' },
57
+ { path: 'navigation.speedOverGround' },
58
+ { path: 'environment.depth.belowTransducer' },
59
+ { path: 'environment.wind.speedApparent' },
60
+ { path: 'environment.wind.angleApparent' },
61
+ { path: 'navigation.headingTrue' },
62
+ { path: 'navigation.courseOverGroundTrue' }
63
+ ]
64
+ };
65
+
66
+ app.subscriptionmanager.subscribe(
67
+ localSubscription,
68
+ unsubscribes,
69
+ subscriptionError => {
70
+ app.error('Subscription error: ' + subscriptionError);
71
+ },
72
+ delta => {
73
+ if (delta.updates) {
74
+ delta.updates.forEach(update => {
75
+ if (update.values) {
76
+ update.values.forEach(v => {
77
+ processIncomingDelta(v.path, v.value);
78
+ });
79
+ }
80
+ });
81
+ }
82
+ }
83
+ );
37
84
  };
38
85
 
39
86
  /**
40
- * plugin.stop: Chiamato quando il plugin viene disattivato o prima di un aggiornamento.
87
+ * plugin.stop: Chiamato quando il plugin viene disattivato.
41
88
  */
42
89
  plugin.stop = function () {
90
+ unsubscribes.forEach(f => f());
91
+ unsubscribes = [];
43
92
  app.debug(`${plugin.name} stopped`);
44
93
  };
45
94
 
46
- // Se desideri avere una funzione plugin.debug personalizzata (opzionale)
47
95
  plugin.debug = function(msg) {
48
96
  app.debug(msg);
49
97
  };
50
98
 
99
+ /**
100
+ * processIncomingDelta: Decodifica i dati dei sensori in Knots/Meters ed esegue l'aggregazione
101
+ */
102
+ function processIncomingDelta(path, val) {
103
+ if (val === null || val === undefined) return;
104
+ raw[path] = val;
105
+
106
+ // Elaborazione e invio alla macchina a stati temporali dello storico
107
+ if (path === 'navigation.speedThroughWater') {
108
+ manageHistory('stw', val * 1.94384);
109
+ }
110
+ else if (path === 'navigation.speedOverGround') {
111
+ manageHistory('sog', val * 1.94384);
112
+ }
113
+ else if (path === 'environment.depth.belowTransducer') {
114
+ manageHistory('depth', val);
115
+ }
116
+ else if (path === 'environment.wind.speedApparent') {
117
+ manageHistory('aws', val * 1.94384);
118
+ }
119
+
120
+ // Calcolo combinato del Vento Reale (TWS) e della VMG a livello Server
121
+ const aws = raw["environment.wind.speedApparent"];
122
+ const awa = raw["environment.wind.angleApparent"];
123
+ const stw = raw["navigation.speedThroughWater"] || 0;
124
+ const sog = raw["navigation.speedOverGround"] || 0;
125
+ const hdg = raw["navigation.headingTrue"] || 0;
126
+ const cog = raw["navigation.courseOverGroundTrue"] || 0;
127
+
128
+ if (aws !== undefined && awa !== undefined) {
129
+ const awsKts = aws * 1.94384;
130
+ const stwKts = stw * 1.94384;
131
+ const tw_water_x = awsKts * Math.cos(awa) - stwKts;
132
+ const tw_water_y = awsKts * Math.sin(awa);
133
+ const tws = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
134
+
135
+ manageHistory('tws', tws);
136
+
137
+ const twa = Math.atan2(tw_water_y, tw_water_x);
138
+ const vmg = Math.abs(stwKts * Math.cos(twa));
139
+ manageHistory('vmg', vmg);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * manageHistory: Versione Server-side dell'aggregatore matematico tattico
145
+ */
146
+ function manageHistory(type, value) {
147
+ if (value === undefined || value === null || !isFinite(value)) return;
148
+
149
+ const now = Date.now();
150
+ const historyMinutes = currentConfig.graphs ? currentConfig.graphs.historyMinutes : 5;
151
+ const samples = 60;
152
+ const bucketIntervalMs = (historyMinutes * 60000) / samples;
153
+
154
+ if (!graphTempBuf[type]) graphTempBuf[type] = [];
155
+ if (!histories[type]) histories[type] = [];
156
+ if (lastUpdates[type] === undefined) lastUpdates[type] = 0;
157
+
158
+ const tempBuf = graphTempBuf[type];
159
+
160
+ // Anti-dropout dinamico sul vento forte
161
+ if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
162
+ const lastPoint = tempBuf[tempBuf.length - 1];
163
+ const reef1 = currentConfig.graphs ? currentConfig.graphs.reef1 : 15;
164
+ const glitchThreshold = reef1 * 0.5;
165
+ if (lastPoint && lastPoint.val > glitchThreshold) return;
166
+ }
167
+
168
+ tempBuf.push({ val: value, time: now });
169
+
170
+ // Controllo avanzamento del secchiello temporale (Bucket)
171
+ const bucketReady = (now - lastUpdates[type] > bucketIntervalMs) || histories[type].length === 0;
172
+ if (!bucketReady) return;
173
+
174
+ let finalValue = value;
175
+
176
+ if (tempBuf.length > 0) {
177
+ // A. VENTO -> SUSTAINED PEAK (EMA Time-Aware)
178
+ if (type === 'tws' || type === 'aws') {
179
+ const tauMs = 2500;
180
+ let ema = tempBuf[0].val;
181
+ let maxSustained = ema;
182
+
183
+ for (let i = 1; i < tempBuf.length; i++) {
184
+ const dt = Math.max(1, tempBuf[i].time - tempBuf[i-1].time);
185
+ const alpha = 1 - Math.exp(-dt / tauMs);
186
+ ema = (tempBuf[i].val * alpha) + (ema * (1 - alpha));
187
+ if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
188
+ }
189
+ finalValue = maxSustained;
190
+ }
191
+ // B. PROFONDITÀ -> MINIMO
192
+ else if (type === 'depth') {
193
+ const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
194
+ if (vals.length > 0) finalValue = Math.min(...vals);
195
+ }
196
+ // C. VELOCITÀ -> MEDIA
197
+ else {
198
+ const vals = tempBuf.map(p => p.val).filter(v => isFinite(v));
199
+ if (vals.length > 0) {
200
+ const sum = vals.reduce((a, b) => a + b, 0);
201
+ finalValue = sum / tempBuf.length;
202
+ }
203
+ }
204
+ }
205
+
206
+ if (!isFinite(finalValue)) return;
207
+ finalValue = Math.max(0, finalValue);
208
+
209
+ // Salvataggio nel ring buffer dello storico principale
210
+ histories[type].push({ val: finalValue, time: now });
211
+
212
+ // Pruning automatico basato sulle impostazioni di timeline
213
+ const maxViewportMinutes = historyMinutes * 2;
214
+ const maxHistoryMs = (maxViewportMinutes * 60000) + 60000;
215
+
216
+ while (histories[type].length > 0 && (now - histories[type][0].time) > maxHistoryMs) {
217
+ histories[type].shift();
218
+ }
219
+
220
+ graphTempBuf[type] = [];
221
+ lastUpdates[type] = now;
222
+ }
223
+
51
224
  /**
52
225
  * plugin.schema: Definisce l'interfaccia grafica in Signal K Admin.
53
226
  */
@@ -127,10 +300,10 @@ module.exports = function (app) {
127
300
  default: 0.5
128
301
  },
129
302
  stabilityThreshold: {
130
- type: 'number',
131
- title: 'Steering Precision (Sensitivity)',
132
- description: "How strictly the system judges data coherence (0.0 to 1.0). Due to internal smoothing, 0.97-0.98 requires racing precision; 0.93-0.95 is ideal for cruising in waves. Below this, the display rarely alerts for instability.",
133
- default: 0.95
303
+ type: 'number',
304
+ title: 'Steering Precision (Sensitivity)',
305
+ description: "How strictly the system judges data coherence (0.0 to 1.0). Due to internal smoothing, 0.97-0.98 requires racing precision; 0.93-0.95 is ideal for cruising in waves. Below this, the display rarely alerts for instability.",
306
+ default: 0.95
134
307
  },
135
308
  stabilityBreakout: {
136
309
  type: 'number',
@@ -152,7 +325,13 @@ module.exports = function (app) {
152
325
  properties: {
153
326
  stdMax: { type: 'number', title: 'Standard Max', description: "Default top limit of the graph.", default: 12 },
154
327
  step: { type: 'number', title: 'Scale Jump', description: "Amount the scale increases when you exceed the limit.", default: 2 },
155
- hercSpan: { type: 'number', title: 'Hercules Zoom Span', description: "Width of the zoom window around your current speed.", default: 4 }
328
+ hercSpan: {
329
+ type: 'number',
330
+ title: 'Hercules Grid Step (Resolution)',
331
+ description: "Select the multiplier step for the Hercules zoom. The scale boundaries will always snap to multiples of this value.",
332
+ enum: [0.5, 1.0, 2.0, 3.0],
333
+ default: 1.0
334
+ }
156
335
  }
157
336
  },
158
337
  sog: {
@@ -161,7 +340,13 @@ module.exports = function (app) {
161
340
  properties: {
162
341
  stdMax: { type: 'number', title: 'Standard Max', default: 12 },
163
342
  step: { type: 'number', title: 'Scale Jump', default: 2 },
164
- hercSpan: { type: 'number', title: 'Hercules Zoom Span', default: 4 }
343
+ hercSpan: {
344
+ type: 'number',
345
+ title: 'Hercules Grid Step (Resolution)',
346
+ description: "Select the multiplier step for the Hercules zoom. The scale boundaries will always snap to multiples of this value.",
347
+ enum: [0.5, 1.0, 2.0, 3.0],
348
+ default: 1.0
349
+ }
165
350
  }
166
351
  },
167
352
  tws: {
@@ -170,7 +355,13 @@ module.exports = function (app) {
170
355
  properties: {
171
356
  stdMax: { type: 'number', title: 'Standard Max', default: 25 },
172
357
  step: { type: 'number', title: 'Scale Jump', default: 5 },
173
- hercSpan: { type: 'number', title: 'Hercules Zoom Span', default: 10 }
358
+ hercSpan: {
359
+ type: 'number',
360
+ title: 'Hercules Grid Step (Resolution)',
361
+ description: "Select the multiplier step for the Hercules zoom. The scale boundaries will always snap to multiples of this value.",
362
+ enum: [1, 2, 3, 5, 10],
363
+ default: 2
364
+ }
174
365
  }
175
366
  },
176
367
  depth: {
@@ -179,7 +370,13 @@ module.exports = function (app) {
179
370
  properties: {
180
371
  stdMax: { type: 'number', title: 'Standard Max', default: 20 },
181
372
  step: { type: 'number', title: 'Scale Jump', default: 10 },
182
- hercSpan: { type: 'number', title: 'Hercules Zoom Span', default: 10 }
373
+ hercSpan: {
374
+ type: 'number',
375
+ title: 'Hercules Grid Step (Resolution)',
376
+ description: "Select the multiplier step for the Hercules zoom. The scale boundaries will always snap to multiples of this value.",
377
+ enum: [1, 2, 3, 5, 10],
378
+ default: 2
379
+ }
183
380
  }
184
381
  }
185
382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "5.0.7",
3
+ "version": "6.0.1",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {