@sailingrotevista/rotevista-dash 6.0.9 → 6.0.12
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.
- package/app.js +182 -61
- package/package.json +1 -1
- package/style.css +1 -3
package/app.js
CHANGED
|
@@ -115,6 +115,17 @@ function getShortestRotation(curr, target) {
|
|
|
115
115
|
return curr + diff;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Scrive il testo nel DOM/SVG solo se il valore è realmente cambiato.
|
|
120
|
+
* Utilizza innerHTML perché è l'unico metodo sicuro al 100% per forzare il ridisegno dei testi negli SVG.
|
|
121
|
+
*/
|
|
122
|
+
function safeSetText(el, text) {
|
|
123
|
+
if (!el) return;
|
|
124
|
+
if (el.innerHTML !== text) {
|
|
125
|
+
el.innerHTML = text;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
118
129
|
/**
|
|
119
130
|
* Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
|
|
120
131
|
*/
|
|
@@ -600,21 +611,22 @@ function startDisplayLoop() {
|
|
|
600
611
|
ui.status.className = (socket && socket.readyState === WebSocket.OPEN) ? "online" : "offline";
|
|
601
612
|
|
|
602
613
|
// --- WATCHDOG: CONTROLLO TIMEOUT ---
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
614
|
+
const watch = {
|
|
615
|
+
"navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
|
|
616
|
+
"navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
|
|
617
|
+
"environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
|
|
618
|
+
"environment.wind.speedTrue": ui.tws
|
|
619
|
+
};
|
|
620
|
+
for (let p in watch) {
|
|
621
|
+
if (!store.timestamps[p] || (now - store.timestamps[p] > TIMEOUT_MS)) {
|
|
622
|
+
safeSetText(watch[p], "---"); // Sostituito innerText con la funzione protetta!
|
|
623
|
+
delete store.raw[p];
|
|
624
|
+
}
|
|
612
625
|
}
|
|
613
|
-
}
|
|
614
626
|
|
|
615
627
|
// --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
|
|
616
628
|
if (store.raw["navigation.speedThroughWater"] !== undefined) {
|
|
617
|
-
ui.stw
|
|
629
|
+
safeSetText(ui.stw, stwKts.toFixed(1));
|
|
618
630
|
ui.stw.style.color = ""; // Neutro
|
|
619
631
|
manageHistory('stw', stwKts);
|
|
620
632
|
}
|
|
@@ -627,11 +639,11 @@ function startDisplayLoop() {
|
|
|
627
639
|
|
|
628
640
|
const labelSogVmg = document.getElementById('sog-vmg-label');
|
|
629
641
|
if (displayModeSog === 'VMG') {
|
|
630
|
-
ui.sog
|
|
642
|
+
safeSetText(ui.sog, vmgVal.toFixed(1));
|
|
631
643
|
ui.sog.style.setProperty('color', '#00b8d4', 'important'); // Cyan
|
|
632
644
|
if (labelSogVmg) labelSogVmg.textContent = 'VMG';
|
|
633
645
|
} else {
|
|
634
|
-
ui.sog
|
|
646
|
+
safeSetText(ui.sog, sogKts.toFixed(1));
|
|
635
647
|
if (labelSogVmg) labelSogVmg.textContent = 'SOG';
|
|
636
648
|
|
|
637
649
|
if (isNavigating) {
|
|
@@ -650,23 +662,29 @@ function startDisplayLoop() {
|
|
|
650
662
|
|
|
651
663
|
// --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
|
|
652
664
|
if (store.raw["environment.depth.belowTransducer"] !== undefined) {
|
|
653
|
-
ui.depth
|
|
665
|
+
safeSetText(ui.depth, store.raw["environment.depth.belowTransducer"].toFixed(1));
|
|
654
666
|
checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
|
|
655
667
|
manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
|
|
656
668
|
}
|
|
657
669
|
|
|
658
|
-
// --- GESTIONE VENTO (TWS / AWS SWITCH) ---
|
|
659
|
-
|
|
660
|
-
|
|
670
|
+
// --- GESTIONE VENTO (TWS / AWS SWITCH & BUSSOLA) ---
|
|
671
|
+
|
|
672
|
+
// Estrazione dati sicura: controlliamo esplicitamente se il dato esiste, altrimenti 0
|
|
673
|
+
const rawTws = store.raw["environment.wind.speedTrue"];
|
|
674
|
+
const rawAws = store.raw["environment.wind.speedApparent"];
|
|
675
|
+
|
|
676
|
+
const twsVal = (rawTws !== undefined && rawTws !== null) ? msToKts(rawTws) : 0;
|
|
677
|
+
const awsVal = (rawAws !== undefined && rawAws !== null) ? msToKts(rawAws) : 0;
|
|
661
678
|
|
|
662
|
-
if (
|
|
663
|
-
if (
|
|
679
|
+
if (rawTws !== undefined && rawTws !== null) manageHistory('tws', twsVal);
|
|
680
|
+
if (rawAws !== undefined && rawAws !== null) manageHistory('aws', awsVal);
|
|
664
681
|
|
|
665
|
-
|
|
682
|
+
// Disegno testo casella destra (TWS o AWS)
|
|
683
|
+
if (rawTws !== undefined || rawAws !== undefined) {
|
|
666
684
|
const labelWind = document.getElementById('tws-aws-label');
|
|
667
685
|
const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
|
|
668
686
|
|
|
669
|
-
ui.tws
|
|
687
|
+
safeSetText(ui.tws, currentWind.toFixed(1));
|
|
670
688
|
if (labelWind) labelWind.textContent = displayModeTws;
|
|
671
689
|
|
|
672
690
|
if (currentWind >= CONFIG.graphs.reef2) {
|
|
@@ -683,8 +701,21 @@ function startDisplayLoop() {
|
|
|
683
701
|
}
|
|
684
702
|
}
|
|
685
703
|
|
|
686
|
-
|
|
687
|
-
|
|
704
|
+
// --- SBLOCCO FORZATO TESTO BUSSOLA (AWS) ---
|
|
705
|
+
if (rawAws !== undefined && rawAws !== null && !isNaN(awsVal)) {
|
|
706
|
+
const strVal = awsVal.toFixed(1);
|
|
707
|
+
// Recupero robusto dell'elemento
|
|
708
|
+
let awsSvgEl = document.getElementById('aws-val-svg');
|
|
709
|
+
|
|
710
|
+
if (awsSvgEl) {
|
|
711
|
+
// Trucco anti-congelamento SVG: se il testo non combacia, lo forziamo usando
|
|
712
|
+
// textContent (standard SVG) invece di innerHTML (standard HTML).
|
|
713
|
+
if (awsSvgEl.textContent !== strVal) {
|
|
714
|
+
awsSvgEl.textContent = strVal;
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
console.error("ERRORE: Elemento SVG bussola ('aws-val-svg') non trovato nel documento!");
|
|
718
|
+
}
|
|
688
719
|
}
|
|
689
720
|
|
|
690
721
|
// --- PUNTATORI ANALOGICI ---
|
|
@@ -702,9 +733,8 @@ function startDisplayLoop() {
|
|
|
702
733
|
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
703
734
|
}
|
|
704
735
|
|
|
705
|
-
// ---
|
|
706
|
-
if (lastAvgUIUpdate++ %
|
|
707
|
-
['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
|
|
736
|
+
// --- SLOW TIER (Salvataggio stato ogni 10 secondi) ---
|
|
737
|
+
if (lastAvgUIUpdate++ % 10 === 0) {
|
|
708
738
|
saveDashboardState();
|
|
709
739
|
}
|
|
710
740
|
|
|
@@ -949,6 +979,10 @@ function manageHistory(type, value) {
|
|
|
949
979
|
|
|
950
980
|
// --- 7. STORAGE STORICO ---
|
|
951
981
|
store.histories[type].push({ val: finalValue, time: now });
|
|
982
|
+
|
|
983
|
+
// --- TRIGGER STRATEGIA 1 (EVENT-DRIVEN) ---
|
|
984
|
+
// Ridisegnamo lo strumento SOLO nell'esatto istante in cui viene generato un nuovo punto storico!
|
|
985
|
+
refreshGraph(type);
|
|
952
986
|
|
|
953
987
|
// --- 8. PRUNING DINAMICO ---
|
|
954
988
|
const maxViewportMinutes = historyMinutes * 2;
|
|
@@ -958,9 +992,8 @@ function manageHistory(type, value) {
|
|
|
958
992
|
store.histories[type].shift();
|
|
959
993
|
}
|
|
960
994
|
|
|
961
|
-
// Reset per il prossimo bucket
|
|
995
|
+
// Reset per il prossimo bucket (sincronizzato)
|
|
962
996
|
store.graphTempBuf[type] = [];
|
|
963
|
-
// Spostiamo il timer esattamente al confine del secchiello assoluto appena concluso
|
|
964
997
|
store.lastUpdates[type] = Math.floor(now / bucketIntervalMs) * bucketIntervalMs;
|
|
965
998
|
}
|
|
966
999
|
|
|
@@ -1018,30 +1051,36 @@ function calculateScale(type, data, mode) {
|
|
|
1018
1051
|
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
1019
1052
|
}
|
|
1020
1053
|
|
|
1021
|
-
// --- 1.2 HERCULES PROFONDITÀ (0 IN
|
|
1054
|
+
// --- 1.2 HERCULES PROFONDITÀ (BASE 0 IN ACQUE BASSE, FLUTTUANTE AL LARGO) ---
|
|
1022
1055
|
if (mode === 'hercules') {
|
|
1023
1056
|
const roundStep = s.hercSpan; // Passo di griglia selezionato dall'utente
|
|
1057
|
+
const shallowThreshold = Math.max(s.stdMax, 10); // Es. 20m
|
|
1024
1058
|
|
|
1025
1059
|
let targetMin = 0;
|
|
1060
|
+
// Se siamo in acque profonde, lasciamo che il minimo fluttui per mostrare i micro-dettagli
|
|
1061
|
+
if (localMin > shallowThreshold) {
|
|
1062
|
+
targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1026
1065
|
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
1027
1066
|
|
|
1028
|
-
// Impediamo
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
targetMax = absoluteMinSpan;
|
|
1067
|
+
// Impediamo uno span inferiore a 4 metri per evitare grafici piatti
|
|
1068
|
+
if (targetMax - targetMin < 4) {
|
|
1069
|
+
targetMax = targetMin + 4;
|
|
1032
1070
|
}
|
|
1033
1071
|
|
|
1034
|
-
// Regola asimmetrica per il MAX (Espansione istantanea, contrazione a 2 minuti)
|
|
1035
|
-
if (currentVal > currentScale.max) {
|
|
1036
|
-
currentScale.
|
|
1072
|
+
// Regola asimmetrica per il MIN e il MAX (Espansione istantanea, contrazione a 2 minuti)
|
|
1073
|
+
if (currentVal < currentScale.min || currentVal > currentScale.max) {
|
|
1074
|
+
currentScale.min = Math.min(currentScale.min, targetMin);
|
|
1075
|
+
currentScale.max = Math.max(currentScale.max, targetMax);
|
|
1037
1076
|
} else {
|
|
1038
|
-
const allStableInTarget = recentVals.every(val => val <= targetMax);
|
|
1077
|
+
const allStableInTarget = recentVals.every(val => val >= targetMin && val <= targetMax);
|
|
1039
1078
|
if (allStableInTarget) {
|
|
1079
|
+
currentScale.min = targetMin;
|
|
1040
1080
|
currentScale.max = targetMax;
|
|
1041
1081
|
}
|
|
1042
1082
|
}
|
|
1043
1083
|
|
|
1044
|
-
currentScale.min = 0;
|
|
1045
1084
|
return { min: currentScale.min, max: currentScale.max };
|
|
1046
1085
|
}
|
|
1047
1086
|
}
|
|
@@ -1100,10 +1139,21 @@ function updateScaleLabels(t, min, max) {
|
|
|
1100
1139
|
}
|
|
1101
1140
|
|
|
1102
1141
|
/**
|
|
1103
|
-
* refreshGraph: Recupero dati, switch AWS/TWS e passaggio a motore grafico.
|
|
1142
|
+
* refreshGraph: Recupero dati, switch AWS/TWS/VMG e passaggio a motore grafico.
|
|
1143
|
+
* Redirige e protegge i canali secondari (AWS/VMG) evitando ridisegni inutili.
|
|
1104
1144
|
*/
|
|
1105
1145
|
function refreshGraph(t) {
|
|
1106
|
-
|
|
1146
|
+
// Redirezione di sicurezza dei canali secondari (AWS / VMG) verso i contenitori principali
|
|
1147
|
+
if (t === 'aws') {
|
|
1148
|
+
if (displayModeTws !== 'AWS') return; // Se non stiamo visualizzando AWS a schermo, ignora il rinfresco
|
|
1149
|
+
t = 'tws';
|
|
1150
|
+
}
|
|
1151
|
+
if (t === 'vmg') {
|
|
1152
|
+
if (displayModeSog !== 'VMG') return; // Se non stiamo visualizzando VMG a schermo, ignora il rinfresco
|
|
1153
|
+
t = 'sog';
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const boxType = t;
|
|
1107
1157
|
let rawData;
|
|
1108
1158
|
|
|
1109
1159
|
if (t === 'tws' && displayModeTws === 'AWS') {
|
|
@@ -1128,6 +1178,7 @@ function refreshGraph(t) {
|
|
|
1128
1178
|
/**
|
|
1129
1179
|
* drawGraph: Motore SVG con Timeline Reale e Gestione GAP
|
|
1130
1180
|
* Risolve i conflitti di orologio (Clock Drift) tra Cerbo GX e Tablet.
|
|
1181
|
+
* Integra la reattività dello spessore della curva in modalità Dual Screen (Focus).
|
|
1131
1182
|
*/
|
|
1132
1183
|
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
1133
1184
|
const svg = document.getElementById(id);
|
|
@@ -1142,6 +1193,10 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
1142
1193
|
const latestPoint = d[d.length - 1];
|
|
1143
1194
|
const now = latestPoint ? latestPoint.time : Date.now();
|
|
1144
1195
|
|
|
1196
|
+
// --- RILEVAMENTO DINAMICO DUAL SCREEN BLINDATO ---
|
|
1197
|
+
const box = svg.closest('.data-box');
|
|
1198
|
+
const isFocused = isFocusActive && box && box.classList.contains('is-focused');
|
|
1199
|
+
|
|
1145
1200
|
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
1146
1201
|
const viewportMs = visibleMinutes * 60000;
|
|
1147
1202
|
const viewportStart = now - viewportMs;
|
|
@@ -1153,15 +1208,20 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
1153
1208
|
const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
|
|
1154
1209
|
|
|
1155
1210
|
const getColorProps = (val) => {
|
|
1156
|
-
|
|
1211
|
+
// Se siamo in Dual Screen (focus), aumentiamo lo spessore base della curva di un filino (da 1.6 a 2.2)
|
|
1212
|
+
const baseStroke = isFocused ? "4.2" : "1.6";
|
|
1213
|
+
const alertStroke = isFocused ? "4.8" : "2.2";
|
|
1214
|
+
const warnStroke = isFocused ? "4.4" : "1.8";
|
|
1215
|
+
|
|
1216
|
+
let color = colTws, opacity = "0.15", stroke = baseStroke;
|
|
1157
1217
|
if (isTws) {
|
|
1158
1218
|
const baseWind = (displayModeTws === 'AWS') ? colAws : colTws;
|
|
1159
|
-
if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke =
|
|
1160
|
-
else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke =
|
|
1219
|
+
if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
|
|
1220
|
+
else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
|
|
1161
1221
|
else color = baseWind;
|
|
1162
1222
|
} else if (isDepth) {
|
|
1163
|
-
if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke =
|
|
1164
|
-
else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke =
|
|
1223
|
+
if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
|
|
1224
|
+
else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
|
|
1165
1225
|
else color = colDepth;
|
|
1166
1226
|
} else {
|
|
1167
1227
|
if (id === 'stw-graph') color = colStw;
|
|
@@ -1171,14 +1231,37 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
1171
1231
|
};
|
|
1172
1232
|
|
|
1173
1233
|
let grids = "";
|
|
1174
|
-
//
|
|
1175
|
-
|
|
1176
|
-
[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" />`);
|
|
1234
|
+
// --- GRIGLIE ORIZZONTALI (Spessore fisso a 0.5px, non deformabile) ---
|
|
1235
|
+
[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" vector-effect="non-scaling-stroke" />`);
|
|
1177
1236
|
|
|
1237
|
+
// --- GRIGLIE VERTICALI (Spessore fisso a 0.4px, non deformabile) ---
|
|
1178
1238
|
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
1179
1239
|
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
1180
1240
|
const x = w - ((m / visibleMinutes) * w);
|
|
1181
|
-
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.
|
|
1241
|
+
grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.4" vector-effect="non-scaling-stroke" />`;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// --- DISEGNO LINEE DI SICUREZZA ALLARME PROFONDITÀ (DANGER & WARNING) ---
|
|
1245
|
+
if (isDepth) {
|
|
1246
|
+
const dangerVal = CONFIG.alarms.depthDanger; // Es. 2.5m
|
|
1247
|
+
const warningVal = CONFIG.alarms.depthWarning; // Es. 3.5m
|
|
1248
|
+
const marginX = 4; // Margine per non toccare i bordi esterni
|
|
1249
|
+
|
|
1250
|
+
// Spessore dinamico per gli allarmi: 4.8px in Dual Screen, 1.8px nella griglia normale
|
|
1251
|
+
const alarmStrokeWidth = isFocused ? "4.8" : "1.8";
|
|
1252
|
+
|
|
1253
|
+
// Linea Rossa Viva (Spessore dinamico, con il tuo tratteggio alternato personalizzato)
|
|
1254
|
+
if (dangerVal >= min && dangerVal <= max) {
|
|
1255
|
+
const p = (dangerVal - min) / range;
|
|
1256
|
+
const y = h - (p * h);
|
|
1257
|
+
grids += `<line x1="${marginX}" y1="${y}" x2="${w - marginX}" y2="${y}" stroke="rgba(255, 59, 48, 0.95)" stroke-width="${alarmStrokeWidth}" stroke-dasharray="12, 6, 2, 6" vector-effect="non-scaling-stroke" />`;
|
|
1258
|
+
}
|
|
1259
|
+
// Linea Giallo Oro Viva (Spessore dinamico, con il tuo tratteggio alternato personalizzato)
|
|
1260
|
+
if (warningVal >= min && warningVal <= max) {
|
|
1261
|
+
const p = (warningVal - min) / range;
|
|
1262
|
+
const y = h - (p * h);
|
|
1263
|
+
grids += `<line x1="${marginX}" y1="${y}" x2="${w - marginX}" y2="${y}" stroke="rgba(255, 204, 0, 0.95)" stroke-width="${alarmStrokeWidth}" stroke-dasharray="6 , 2, 6, 12" vector-effect="non-scaling-stroke" />`;
|
|
1264
|
+
}
|
|
1182
1265
|
}
|
|
1183
1266
|
|
|
1184
1267
|
let gradientStops = "", lines = "", areaPath = "";
|
|
@@ -1208,7 +1291,8 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
|
1208
1291
|
started = false;
|
|
1209
1292
|
}
|
|
1210
1293
|
} else {
|
|
1211
|
-
|
|
1294
|
+
// Curva del grafico: spessore reattivo al focus e protezione da deformazione (non-scaling)
|
|
1295
|
+
lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" vector-effect="non-scaling-stroke" />`;
|
|
1212
1296
|
if (!started) {
|
|
1213
1297
|
areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
|
|
1214
1298
|
started = true;
|
|
@@ -1235,8 +1319,20 @@ function toggleFocusMode(type, element) {
|
|
|
1235
1319
|
const container = document.querySelector('.main-container');
|
|
1236
1320
|
const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
|
|
1237
1321
|
isFocusActive = !isFocusActive;
|
|
1238
|
-
if (isFocusActive) {
|
|
1239
|
-
|
|
1322
|
+
if (isFocusActive) {
|
|
1323
|
+
container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right');
|
|
1324
|
+
element.classList.add('is-focused');
|
|
1325
|
+
} else {
|
|
1326
|
+
container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right');
|
|
1327
|
+
document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused'));
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// --- FORZATURA REDRAW IMMEDIATO DUAL SCREEN ---
|
|
1331
|
+
// Quando entriamo o usciamo dal Focus, ridisegnamo subito lo strumento
|
|
1332
|
+
// interessato per applicare istantaneamente il cambio di spessore della linea!
|
|
1333
|
+
if (['stw', 'sog', 'tws', 'depth'].includes(type)) {
|
|
1334
|
+
refreshGraph(type);
|
|
1335
|
+
}
|
|
1240
1336
|
}
|
|
1241
1337
|
|
|
1242
1338
|
['stw', 'sog', 'tws', 'depth'].forEach(type => {
|
|
@@ -1275,8 +1371,10 @@ function toggleFocusMode(type, element) {
|
|
|
1275
1371
|
} else if (!isFocusActive) {
|
|
1276
1372
|
if (type === 'sog') {
|
|
1277
1373
|
displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
|
|
1374
|
+
refreshGraph('sog'); // --- FORZA RINFRESCO ISTANTANEO AL CAMBIO SOG/VMG ---
|
|
1278
1375
|
} else if (type === 'tws') {
|
|
1279
1376
|
displayModeTws = (displayModeTws === 'TWS') ? 'AWS' : 'TWS';
|
|
1377
|
+
refreshGraph('tws'); // --- FORZA RINFRESCO ISTANTANEO AL CAMBIO TWS/AWS ---
|
|
1280
1378
|
}
|
|
1281
1379
|
el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
|
|
1282
1380
|
setTimeout(() => el.style.backgroundColor = "", 150);
|
|
@@ -1404,22 +1502,45 @@ async function init() {
|
|
|
1404
1502
|
// RICONNESSIONE RAPIDA AL RISVEGLIO (VISIBILITY WATCHDOG)
|
|
1405
1503
|
// ==========================================================================
|
|
1406
1504
|
|
|
1407
|
-
// Quando sblocchi l'iPad o riapri
|
|
1408
|
-
// WebSocket orfano. Questo farà scattare immediatamente connect() e la sincronizzazione.
|
|
1505
|
+
// Quando sblocchi l'iPad o riapri la scheda, sincronizziamo in modo intelligente
|
|
1409
1506
|
document.addEventListener('visibilitychange', () => {
|
|
1410
1507
|
if (document.visibilityState === 'visible') {
|
|
1411
|
-
|
|
1412
|
-
if (socket) {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1508
|
+
// 1. Se la connessione è già attiva e sana, scarichiamo solo lo storico senza disconnetterci
|
|
1509
|
+
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
1510
|
+
console.log("⏰ Schermo sbloccato: connessione attiva, sincronizzazione dello storico.");
|
|
1511
|
+
fetchServerHistory().then(() => {
|
|
1512
|
+
['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
|
|
1513
|
+
}).catch(err => {});
|
|
1514
|
+
}
|
|
1515
|
+
// 2. Se la connessione è persa o morta, forzatura riconnessione rapida
|
|
1516
|
+
else {
|
|
1517
|
+
console.log("⏰ Schermo sbloccato: connessione assente, forzatura riconnessione rapida.");
|
|
1518
|
+
if (socket) {
|
|
1519
|
+
try {
|
|
1520
|
+
socket.close();
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
connect();
|
|
1523
|
+
}
|
|
1524
|
+
} else {
|
|
1416
1525
|
connect();
|
|
1417
1526
|
}
|
|
1418
|
-
} else {
|
|
1419
|
-
connect();
|
|
1420
1527
|
}
|
|
1421
1528
|
}
|
|
1422
1529
|
});
|
|
1530
|
+
|
|
1531
|
+
// Rileva se il dispositivo è andato in sospensione misurando il ritardo dei secondi
|
|
1532
|
+
let lastHeartbeat = Date.now();
|
|
1533
|
+
setInterval(() => {
|
|
1534
|
+
const now = Date.now();
|
|
1535
|
+
const diff = now - lastHeartbeat;
|
|
1536
|
+
lastHeartbeat = now;
|
|
1537
|
+
|
|
1538
|
+
// Se passano più di 6 secondi tra un ciclo e l'altro (invece di 1 secondo),
|
|
1539
|
+
// significa che il PC era in sospensione. Avviamo il risveglio.
|
|
1540
|
+
if (diff > 6000) {
|
|
1541
|
+
handleWakeUp();
|
|
1542
|
+
}
|
|
1543
|
+
}, 1000);
|
|
1423
1544
|
}
|
|
1424
1545
|
|
|
1425
1546
|
|
package/package.json
CHANGED
package/style.css
CHANGED
|
@@ -175,8 +175,7 @@ body.night-mode .alarm-danger {
|
|
|
175
175
|
|
|
176
176
|
.focus-active .is-focused .sparkline path,
|
|
177
177
|
.focus-active .is-focused .sparkline line {
|
|
178
|
-
|
|
179
|
-
}
|
|
178
|
+
}
|
|
180
179
|
|
|
181
180
|
.focus-active.focus-side-left {
|
|
182
181
|
grid-template-columns: 3fr 2fr !important;
|
|
@@ -415,7 +414,6 @@ body.night-mode .alarm-danger {
|
|
|
415
414
|
rect[fill="#222"] { fill: #eee !important; }
|
|
416
415
|
#leeway-val { color: #000 !important; }
|
|
417
416
|
|
|
418
|
-
#awa-pointer, #twa-pointer, #track-pointer, #twd-arrow, #twd-boat-wrap { transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
419
417
|
#wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
|
|
420
418
|
|
|
421
419
|
/* ==========================================================================
|