@sailingrotevista/rotevista-dash 5.0.2 → 5.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 (2) hide show
  1. package/app.js +323 -484
  2. package/package.json +1 -1
package/app.js CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * ==========================================================================
3
- * Signal K Wind Dashboard - Pro Version 2.4 (Definitive)
3
+ * Signal K Wind Dashboard - Pro Version 3.5 (Time-Based Architecture)
4
4
  * ==========================================================================
5
5
  * Autore: Sailing Rotevista
6
6
  * Motore di calcolo tattico per navigazione e crociera.
7
- * Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico a 15min,
8
- * Memoria UI persistente, Modalità Hercules e Focus Split Screen.
7
+ * Gestisce: Medie Vettoriali, Deviazione Standard, Trend Strategico dinamico,
8
+ * Memoria UI persistente, Modalità Hercules, Focus Split Screen e
9
+ * Rendering Grafico basato sul Tempo Reale (Timeline e Gap Handling).
9
10
  */
10
11
 
11
12
  // ==========================================================================
@@ -18,13 +19,13 @@ let CONFIG = {
18
19
  longWindow: 30000,
19
20
  stabilityTolerance: 2000,
20
21
  stabilityThreshold: 0.99,
21
- minSpeed: 0,
22
+ minSpeed: 0.5,
22
23
  stabilityBreakout: 15
23
24
  },
24
25
  graphs: { reef1: 10, reef2: 15, historyMinutes: 10, samples: 60 },
25
26
  scales: {
26
- stw: { stdMax: 8, hercSpan: 4, step: 2 },
27
- sog: { stdMax: 8, hercSpan: 4, step: 2 },
27
+ stw: { stdMax: 4, hercSpan: 4, step: 2 },
28
+ sog: { stdMax: 4, hercSpan: 4, step: 2 },
28
29
  tws: { stdMax: 15, hercSpan: 10, step: 5 },
29
30
  depth: { stdMax: 8, hercSpan: 5, step: 5 }
30
31
  },
@@ -34,10 +35,9 @@ let CONFIG = {
34
35
  const RENDER_INTERVAL_MS = 1000;
35
36
  const TIMEOUT_MS = 5000;
36
37
  const SIM_SAMPLE_INTERVAL = 1000;
37
- const DASH_VERSION = "3.0"; // Versione della memoria locale
38
+ const DASH_VERSION = "3.5"; // Major Update: Time-Based storage and GAP handling
38
39
  const sourceLocks = {};
39
40
 
40
-
41
41
  // ==========================================================================
42
42
  // 2. STATO GLOBALE E RIFERIMENTI UI
43
43
  // ==========================================================================
@@ -60,7 +60,6 @@ let emaTwaSin = 0;
60
60
  let emaTwaCos = 0;
61
61
  let firstEmaRun = true;
62
62
 
63
- // Stato dei singoli grafici (Standard vs Hercules Zoom)
64
63
  const graphModes = {
65
64
  stw: 'standard',
66
65
  sog: 'standard',
@@ -74,8 +73,8 @@ const store = {
74
73
  timestamps: {},
75
74
  smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
76
75
  longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
76
+ // histories ora conterrà array di oggetti: { time: Date.now(), val: numero }
77
77
  histories: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
78
- // Buffer temporaneo per il calcolo della media dell'intervallo del grafico
79
78
  graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
80
79
  lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 }
81
80
  };
@@ -97,16 +96,13 @@ const ui = {
97
96
  };
98
97
 
99
98
  // ==========================================================================
100
- // 3. UTILITIES (MATEMATICA, BUFFER, AUDIO, MEMORIA)
99
+ // 3. UTILITIES (MATEMATICA, BUFFER E MEMORIA)
101
100
  // ==========================================================================
102
101
  function radToDeg(rad) { return rad * (180 / Math.PI); }
103
102
  function degToRad(deg) { return deg * (Math.PI / 180); }
104
103
  function msToKts(ms) { return ms * 1.94384; }
105
104
  function ktsToMs(kts) { return kts / 1.94384; }
106
105
 
107
- /**
108
- * Calcola il percorso più breve per la rotazione di un puntatore (evita giri di 360°)
109
- */
110
106
  function getShortestRotation(curr, target) {
111
107
  let diff = (target - curr) % 360;
112
108
  if (diff > 180) diff -= 360;
@@ -114,15 +110,10 @@ function getShortestRotation(curr, target) {
114
110
  return curr + diff;
115
111
  }
116
112
 
117
- /**
118
- * Inserisce un dato nel buffer circolare limitandolo a 2000 campioni (30 min)
119
- */
120
113
  /**
121
114
  * Inserimento sicuro nel buffer con trigonometria precaricata e pruning automatico
122
115
  */
123
116
  function safePush(buffer, val, time) {
124
- // --- PROTEZIONE ANTI-NaN (FONDAMENTALE ALL'AVVIO) ---
125
- // Se il valore è nullo, non definito o non è un numero, ignoriamo l'inserimento
126
117
  if (val === null || val === undefined || isNaN(val)) return;
127
118
 
128
119
  buffer.push({
@@ -132,21 +123,15 @@ function safePush(buffer, val, time) {
132
123
  cos: Math.cos(val)
133
124
  });
134
125
 
135
- // Teniamo sempre in memoria il DOPPIO della storia impostata (per la modalità ancoraggio)
136
- // + 1 minuto di margine
137
126
  const maxHistoryMs = (CONFIG.graphs.historyMinutes * 60000 * 2) + 60000;
138
-
139
127
  while (buffer.length > 0 && (time - buffer[0].time) > maxHistoryMs) {
140
128
  buffer.shift();
141
129
  }
142
-
143
- // Tetto massimo di campioni per sicurezza (circa 2 ore a 5Hz)
144
130
  if (buffer.length > 36000) buffer.shift();
145
131
  }
146
132
 
147
133
  /**
148
134
  * Media Circolare Vettoriale - Versione "Soft Outlier Rejection"
149
- * Riduce l'impatto degli sbalzi limitando il loro angolo massimo di discostamento.
150
135
  */
151
136
  function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now) {
152
137
  now = now || Date.now();
@@ -156,7 +141,6 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
156
141
  let sSin = 0, sCos = 0, count = 0;
157
142
  let newestTime = 0, oldestTime = 0;
158
143
 
159
- // 1. MEDIA PILOTA: Guardiamo l'ultima frazione di dati (es. max 15 campioni) per sapere dove punta "ora"
160
144
  let pilotSin = 0, pilotCos = 0;
161
145
  const pilotSamples = Math.min(len, 15);
162
146
  for (let i = len - 1; i >= len - pilotSamples; i--) {
@@ -164,34 +148,21 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
164
148
  pilotCos += bufferArray[i].cos;
165
149
  }
166
150
  const pilotRad = Math.atan2(pilotSin, pilotCos);
167
-
168
- // Il limite elastico in radianti (basato sul tuo stabilityBreakout in gradi)
169
151
  const limitRad = (CONFIG.averaging.stabilityBreakout || 15) * (Math.PI / 180);
170
152
 
171
- // 2. CALCOLO AMMORTIZZATO
172
153
  for (let i = len - 1; i >= 0; i--) {
173
154
  const item = bufferArray[i];
174
155
  if ((now - item.time) > windowMs) break;
175
156
 
176
- // Troviamo la differenza angolare (da -Pi a +Pi) tra il dato e la Media Pilota
177
- let diffRad = Math.atan2(
178
- Math.sin(item.val - pilotRad),
179
- Math.cos(item.val - pilotRad)
180
- );
181
-
157
+ let diffRad = Math.atan2(Math.sin(item.val - pilotRad), Math.cos(item.val - pilotRad));
182
158
  let finalSin, finalCos;
183
159
 
184
- // Se lo scarto è maggiore del limite, "Pattiniamo" (Ammortizzazione)
185
160
  if (Math.abs(diffRad) > limitRad) {
186
- // Tronchiamo la differenza al limite massimo consentito (mantenendo il segno)
187
161
  const clampedDiff = Math.sign(diffRad) * limitRad;
188
- // Ricalcoliamo l'angolo ammortizzato
189
162
  const clampedRad = pilotRad + clampedDiff;
190
-
191
163
  finalSin = Math.sin(clampedRad);
192
164
  finalCos = Math.cos(clampedRad);
193
165
  } else {
194
- // Il dato è buono, usiamo i valori precalcolati
195
166
  finalSin = item.sin;
196
167
  finalCos = item.cos;
197
168
  }
@@ -220,9 +191,6 @@ function getCircularAverageFromBuffer(bufferArray, windowMs, signed = false, now
220
191
  };
221
192
  }
222
193
 
223
- /**
224
- * Salva lo stato attuale della dashboard (dati e preferenze UI) nel browser
225
- */
226
194
  function saveDashboardState() {
227
195
  try {
228
196
  const focusedBox = document.querySelector('.data-box.is-focused');
@@ -248,9 +216,6 @@ function saveDashboardState() {
248
216
  } catch (e) { console.error("Save error:", e); }
249
217
  }
250
218
 
251
- /**
252
- * Carica e ripristina lo stato salvato (entro i 20 minuti di vecchiaia)
253
- */
254
219
  function loadDashboardState() {
255
220
  const saved = localStorage.getItem('rotevista_dash_state');
256
221
  if (!saved) return;
@@ -263,31 +228,24 @@ function loadDashboardState() {
263
228
  if (state.longBuf) Object.assign(store.longBuf, state.longBuf);
264
229
  if (state.graphModes) Object.assign(graphModes, state.graphModes);
265
230
 
266
- // Ripristino SOG/VMG
267
231
  if (state.displayModeSog) {
268
232
  displayModeSog = state.displayModeSog;
269
233
  const labelEl = document.getElementById('sog-vmg-label');
270
234
  if (labelEl) labelEl.textContent = displayModeSog;
271
235
  }
272
-
273
- // Ripristino TWS/AWS ---
274
236
  if (state.displayModeTws) {
275
237
  displayModeTws = state.displayModeTws;
276
238
  const labelEl = document.getElementById('tws-aws-label');
277
239
  if (labelEl) labelEl.textContent = displayModeTws;
278
240
  }
279
-
280
- // Ripristino Tema Notte
281
241
  if (state.isNightMode) document.body.classList.add('night-mode');
282
242
 
283
- // Ripristino Focus (Dual Screen)
284
243
  if (state.isFocusActive && state.focusedBoxType) {
285
244
  setTimeout(() => {
286
245
  const el = document.querySelector(`.box-${state.focusedBoxType}`);
287
246
  if (el) { isFocusActive = false; toggleFocusMode(state.focusedBoxType, el); }
288
247
  }, 200);
289
248
  }
290
- console.log("Stato ripristinato con successo dalla cache.");
291
249
  }
292
250
  } catch (e) { localStorage.removeItem('rotevista_dash_state'); }
293
251
  }
@@ -323,13 +281,10 @@ function playGybeAlarm() {
323
281
  }
324
282
 
325
283
  function checkDepthAlarm(m) {
326
- // Rimuoviamo sempre le classi prima di riapplicarle
327
284
  ui.depth.classList.remove('alarm-warning', 'alarm-danger', 'blink-alarm');
328
-
329
- // Logica di confronto dinamica
330
285
  if (m < CONFIG.alarms.depthDanger) {
331
286
  ui.depth.classList.add('alarm-danger', 'blink-alarm');
332
- playBingBing(); // Il tuo suono di allarme
287
+ playBingBing();
333
288
  } else if (m < CONFIG.alarms.depthWarning) {
334
289
  ui.depth.classList.add('alarm-warning');
335
290
  }
@@ -350,11 +305,9 @@ function computeTrueWind() {
350
305
  const hdg = store.raw["navigation.headingTrue"] || 0, cog = store.raw["navigation.courseOverGroundTrue"] || 0;
351
306
  if (aws === undefined || awa === undefined) return;
352
307
 
353
- // Vento Reale Rispetto all'acqua (TWA/TWS Water)
354
308
  const tw_water_x = aws * Math.cos(awa) - stw, tw_water_y = aws * Math.sin(awa);
355
309
  const tws_water = Math.sqrt(tw_water_x * tw_water_x + tw_water_y * tw_water_y);
356
310
 
357
- // Vento Reale Rispetto al fondo (TWD Ground)
358
311
  const drift = (cog - hdg + Math.PI * 3) % (2 * Math.PI) - Math.PI;
359
312
  const tw_ground_x = aws * Math.cos(awa) - sog * Math.cos(drift), tw_ground_y = aws * Math.sin(awa) - sog * Math.sin(drift);
360
313
  const tws_ground = Math.sqrt(tw_ground_x * tw_ground_x + tw_ground_y * tw_ground_y);
@@ -376,49 +329,37 @@ function computeTrueWind() {
376
329
  function processIncomingData(path, val, source) {
377
330
  const now = Date.now();
378
331
 
379
- // --- 1. FILTRO SORGENTE STICKY (Elimina conflitti yacht_device vs Unknown) ---
332
+ // FILTRO SORGENTE STICKY
380
333
  if (!sourceLocks[path] || sourceLocks[path].label === source || (now - sourceLocks[path].lastSeen > 2000)) {
381
334
  sourceLocks[path] = { label: source, lastSeen: now };
382
335
  } else {
383
- // Se arriva un dato da un'altra sorgente mentre il lock è attivo, lo scartiamo
384
336
  return;
385
337
  }
386
338
 
387
- // --- 2. AGGIORNAMENTO DATI (Solo per la sorgente eletta) ---
388
339
  store.timestamps[path] = now;
389
340
  store.raw[path] = val;
390
341
 
391
- // Buffer per AWA (Vento Apparente)
392
342
  if (path === "environment.wind.angleApparent") {
393
343
  safePush(store.smoothBuf.awa, val, now);
394
344
  safePush(store.longBuf.awa, val, now);
395
345
  }
396
-
397
- // Buffer per HDG (Prua)
398
346
  if (path === "navigation.headingTrue") {
399
347
  safePush(store.smoothBuf.hdg, val, now);
400
348
  safePush(store.longBuf.hdg, val, now);
401
349
  }
402
-
403
- // Buffer per COG (Rotta Fondo)
404
350
  if (path === "navigation.courseOverGroundTrue") {
405
351
  safePush(store.smoothBuf.cog, val, now);
406
352
  safePush(store.longBuf.cog, val, now);
407
353
  }
408
354
 
409
- // --- 3. TRIGGER CALCOLO VENTO REALE (TWA/TWS/TWD) ---
410
355
  const twPaths = [
411
- "environment.wind.speedApparent",
412
- "environment.wind.angleApparent",
413
- "navigation.speedThroughWater",
414
- "navigation.speedOverGround",
415
- "navigation.headingTrue",
416
- "navigation.courseOverGroundTrue"
356
+ "environment.wind.speedApparent", "environment.wind.angleApparent",
357
+ "navigation.speedThroughWater", "navigation.speedOverGround",
358
+ "navigation.headingTrue", "navigation.courseOverGroundTrue"
417
359
  ];
418
360
 
419
361
  if (twPaths.includes(path)) {
420
362
  twDirty = true;
421
- // Calcolo limitato a 10Hz per non pesare sulla CPU
422
363
  if (twDirty && (now - lastTWCompute > 100)) {
423
364
  computeTrueWind();
424
365
  lastTWCompute = now;
@@ -432,20 +373,20 @@ function processIncomingData(path, val, source) {
432
373
  // ==========================================================================
433
374
  function updateWindTrend() {
434
375
  const now = Date.now();
435
- // TATTICA (Lancetta): 2s vs 10s (reazione rapida per trim)
436
376
  const twaNow = getCircularAverageFromBuffer(store.longBuf.twa, 2000, true);
437
377
  const twaRef = getCircularAverageFromBuffer(store.longBuf.twa, 10000, true);
438
378
 
439
- // STRATEGIA (Bussola TWD): 1 min vs 30 minuti (tendenza meteo profonda)
440
379
  const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
441
380
  const multiplier = isNavigating ? 1 : 2;
442
381
  const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
443
382
  const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
383
+
444
384
  if (!twaNow || !twaRef || !twdNow || !twdRef) return;
385
+
445
386
  const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
446
387
  const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
447
388
 
448
- // TREND METEO (Bussola Centrale TWD)
389
+ // TREND METEO
449
390
  let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
450
391
  if (Math.abs(deltaMeteo) > 6.0) {
451
392
  const isSouth = store.raw["navigation.position"]?.latitude < 0;
@@ -459,20 +400,17 @@ function updateWindTrend() {
459
400
  }
460
401
  } else { [compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
461
402
 
462
- // TREND TATTICO (Lancetta Arancione)
403
+ // TREND TATTICO
463
404
  let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
464
405
  const curTwaDeg = radToDeg(twaNow.val);
465
406
  if (Math.abs(deltaTac) > 3.0) {
466
- // Calcolo logica tattica: LIFT (Verde), HEADER (Rosso) o NEUTRO (Grigio)
467
407
  let absTwa = Math.abs(curTwaDeg);
468
408
  let tacticColor;
469
-
470
- // Se siamo tra 75° e 105° (Traverso), il cambio è considerato neutro
471
409
  if (absTwa > 75 && absTwa < 105) {
472
- tacticColor = "#bbb"; // Grigio/Bianco sporco neutro
410
+ tacticColor = "#bbb";
473
411
  } else {
474
412
  let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
475
- if (absTwa >= 90) isLift = !isLift; // Inversione logica per andature portanti
413
+ if (absTwa >= 90) isLift = !isLift;
476
414
  tacticColor = isLift ? "#27ae60" : "#c0392b";
477
415
  }
478
416
  if (deltaTac > 0) {
@@ -484,100 +422,45 @@ function updateWindTrend() {
484
422
  }
485
423
  } else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
486
424
 
487
- // --- LOGICA ALLARME STRAMBATA (GYBE) FILTRATA ---
488
- const instTwaRad = store.raw["environment.wind.angleTrueWater"];
489
-
490
- if (instTwaRad !== undefined) {
491
- // Calcolo Alfa basato sulla Steering Precision (più precisione = filtro più lento e stabile)
492
- const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
493
-
494
- const currentSin = Math.sin(instTwaRad);
495
- const currentCos = Math.cos(instTwaRad);
425
+ // ALLARME STRAMBATA (EMA FILTERED)
426
+ const instTwaRad = store.raw["environment.wind.angleTrueWater"];
427
+ if (instTwaRad !== undefined) {
428
+ const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
429
+ const currentSin = Math.sin(instTwaRad);
430
+ const currentCos = Math.cos(instTwaRad);
496
431
 
497
- if (firstEmaRun) {
498
- emaTwaSin = currentSin;
499
- emaTwaCos = currentCos;
500
- firstEmaRun = false;
501
- } else {
502
- // Media Esponenziale Vettoriale
503
- emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
504
- emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
505
- }
432
+ if (firstEmaRun) {
433
+ emaTwaSin = currentSin; emaTwaCos = currentCos; firstEmaRun = false;
434
+ } else {
435
+ emaTwaSin = (currentSin * dynamicAlpha) + (emaTwaSin * (1 - dynamicAlpha));
436
+ emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
437
+ }
506
438
 
507
- // Angolo risultante filtrato
508
- const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
439
+ const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
509
440
 
510
- // Verifichiamo il cambio di mure (> 155° e inversione di segno confermata dal filtro)
511
- if (Math.abs(smoothedTwaDeg) > 155) {
512
- if (Math.sign(smoothedTwaDeg) !== Math.sign(lastInstantTwa) && lastInstantTwa !== null) {
513
- if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
514
- lastGybeAlarmTime = now;
515
- playGybeAlarm();
516
- console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
517
- }
441
+ if (Math.abs(smoothedTwaDeg) > 155) {
442
+ if (Math.sign(smoothedTwaDeg) !== Math.sign(lastInstantTwa) && lastInstantTwa !== null) {
443
+ if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
444
+ lastGybeAlarmTime = now;
445
+ playGybeAlarm();
446
+ console.log(`⚠️ GYBE ALARM: TWA ${smoothedTwaDeg.toFixed(1)}°`);
518
447
  }
519
448
  }
520
- lastInstantTwa = smoothedTwaDeg;
521
449
  }
450
+ lastInstantTwa = smoothedTwaDeg;
451
+ }
522
452
  }
523
453
 
524
454
  // ==========================================================================
525
455
  // 7. RENDERING ENGINE E AGGIORNAMENTO UI
526
456
  // ==========================================================================
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
- */
533
- function refreshGraph(t) {
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);
553
-
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
- }
559
-
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
- );
573
- }
574
457
 
575
458
  /**
576
- * upUI: Aggiornamento valori digitali con logica anti-ritardo (Istantaneo vs Media)
459
+ * upUI: Aggiornamento valori digitali
577
460
  */
578
461
  const upUI = (el, obj, instantRaw, isCompass = false) => {
579
462
  if (!obj || obj.val === null || isNaN(obj.val) || instantRaw === undefined) {
580
- el.innerHTML = "---&deg;";
463
+ el.innerHTML = "---&deg;";
581
464
  el.classList.remove('unstable-data');
582
465
  } else {
583
466
  let valDeg = Math.round(radToDeg(obj.val));
@@ -586,7 +469,6 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
586
469
  el.innerHTML = mainVal + dev;
587
470
 
588
471
  let diff = Math.abs((radToDeg(instantRaw) - radToDeg(obj.val) + 540) % 360 - 180);
589
- // Allarme lampeggio solo se in navigazione E (R bassa O deviazione alta O salto istantaneo brusco)
590
472
  if (isNavigating && (!obj.stable || obj.dev > CONFIG.averaging.stabilityBreakout || diff > CONFIG.averaging.stabilityBreakout)) el.classList.add('unstable-data');
591
473
  else el.classList.remove('unstable-data');
592
474
  }
@@ -594,29 +476,40 @@ const upUI = (el, obj, instantRaw, isCompass = false) => {
594
476
 
595
477
  /**
596
478
  * Loop principale di aggiornamento interfaccia (1Hz)
597
- * Gestisce la gerarchia di aggiornamento Live (1s), Heavy (2s) e Slow (3s).
598
479
  */
599
480
  function startDisplayLoop() {
600
481
  renderInterval = setInterval(() => {
601
482
  const now = Date.now();
602
483
  const isNight = document.body.classList.contains('night-mode');
603
484
 
604
- // Conversione velocità da m/s a Nodi
605
485
  const stwKts = msToKts(store.raw["navigation.speedThroughWater"] || 0);
606
486
  const sogKts = msToKts(store.raw["navigation.speedOverGround"] || 0);
607
487
 
608
- // Verifica stato navigazione basato su soglia impostata (minSpeed)
609
488
  isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
610
489
 
611
- // --- TIER LIVE (1s): CONTROLLO TIMEOUT DATI ---
612
- // Se un dato non arriva da più di 5 secondi, mostra i trattini
490
+ // --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
491
+ const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
492
+ const requiredMs = viewportMinutes * 60000;
493
+ const oldestStw = store.histories.stw ? store.histories.stw[0] : null;
494
+
495
+ if (oldestStw) {
496
+ const availableMs = now - oldestStw.time;
497
+ if (availableMs >= requiredMs) {
498
+ ui.status.innerText = `ONLINE ${viewportMinutes}min`;
499
+ } else {
500
+ const availableMin = Math.max(1, Math.floor(availableMs / 60000));
501
+ ui.status.innerText = `ONLINE ${availableMin}/${viewportMinutes}min`;
502
+ }
503
+ } else {
504
+ ui.status.innerText = `ONLINE`;
505
+ }
506
+ ui.status.className = (socket && socket.readyState === WebSocket.OPEN) ? "online" : "offline";
507
+
508
+ // --- WATCHDOG: CONTROLLO TIMEOUT ---
613
509
  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,
510
+ "navigation.speedThroughWater": ui.stw, "navigation.speedOverGround": ui.sog,
511
+ "navigation.headingTrue": ui.hdg, "navigation.courseOverGroundTrue": ui.cog,
512
+ "environment.wind.speedApparent": ui.awsSvg, "environment.depth.belowTransducer": ui.depth,
620
513
  "environment.wind.speedTrue": ui.tws
621
514
  };
622
515
  for (let p in watch) {
@@ -628,51 +521,34 @@ function startDisplayLoop() {
628
521
  // --- AGGIORNAMENTO VELOCITÀ SULL'ACQUA (STW) ---
629
522
  if (store.raw["navigation.speedThroughWater"] !== undefined) {
630
523
  ui.stw.innerText = stwKts.toFixed(1);
631
- // Colore neutro (ereditato dal tema) perché non ha switch di modalità
632
- ui.stw.style.color = "";
524
+ ui.stw.style.color = ""; // Neutro
633
525
  manageHistory('stw', stwKts);
634
526
  }
635
527
 
636
- // --- LOGICA SOG / VMG (COLORI DINAMICI E FILTRO NAVIGAZIONE) ---
528
+ // --- LOGICA SOG / VMG ---
637
529
  if (store.raw["navigation.speedOverGround"] !== undefined) {
638
- // Calcolo VMG istantanea per il display numerico
639
- const vmg = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
640
-
641
- // Registriamo i dati nelle storie (per i grafici)
642
- manageHistory('vmg', vmg);
530
+ const vmgVal = Math.abs(stwKts * Math.cos(store.raw["environment.wind.angleTrueWater"] || 0));
531
+ manageHistory('vmg', vmgVal);
643
532
  manageHistory('sog', sogKts);
644
533
 
645
- const labelEl = document.getElementById('sog-vmg-label');
646
-
534
+ const labelSogVmg = document.getElementById('sog-vmg-label');
647
535
  if (displayModeSog === 'VMG') {
648
- // MODALITÀ VMG: Mostra valore istantaneo, colore Cyan fisso per identificare la modalità
649
- ui.sog.innerText = vmg.toFixed(1);
650
- ui.sog.style.setProperty('color', '#00b8d4', 'important');
651
- if (labelEl) labelEl.textContent = 'VMG';
536
+ ui.sog.innerText = vmgVal.toFixed(1);
537
+ ui.sog.style.setProperty('color', '#00b8d4', 'important'); // Cyan
538
+ if (labelSogVmg) labelSogVmg.textContent = 'VMG';
652
539
  } else {
653
- // MODALITÀ SOG: Mostra valore istantaneo
654
540
  ui.sog.innerText = sogKts.toFixed(1);
655
- if (labelEl) labelEl.textContent = 'SOG';
541
+ if (labelSogVmg) labelSogVmg.textContent = 'SOG';
656
542
 
657
- // --- LOGICA COLORE DINAMICO (Solo se in navigazione reale) ---
658
543
  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
- }
544
+ const lastSog = store.histories.sog.length > 0 ? store.histories.sog[store.histories.sog.length - 1].val : sogKts;
545
+ const lastStw = store.histories.stw.length > 0 ? store.histories.stw[store.histories.stw.length - 1].val : stwKts;
546
+ const drift = lastSog - lastStw;
547
+
548
+ if (drift < -0.3) ui.sog.style.setProperty('color', '#ff3b30', 'important'); // Contro
549
+ else if (drift > 0.3) ui.sog.style.setProperty('color', '#00C851', 'important'); // Favore
550
+ else ui.sog.style.setProperty('color', '#ffbb33', 'important'); // Neutro SOG
674
551
  } else {
675
- // NON IN NAVIGAZIONE: Riportiamo il colore al default neutro
676
552
  ui.sog.style.color = "";
677
553
  }
678
554
  }
@@ -681,65 +557,50 @@ function startDisplayLoop() {
681
557
  // --- AGGIORNAMENTO PROFONDITÀ (DEPTH) ---
682
558
  if (store.raw["environment.depth.belowTransducer"] !== undefined) {
683
559
  ui.depth.innerText = store.raw["environment.depth.belowTransducer"].toFixed(1);
684
- // Il colore neutro/allarme è gestito internamente dalla funzione checkDepthAlarm
685
560
  checkDepthAlarm(store.raw["environment.depth.belowTransducer"]);
686
561
  manageHistory('depth', store.raw["environment.depth.belowTransducer"]);
687
562
  }
688
563
 
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;
564
+ // --- GESTIONE VENTO (TWS / AWS SWITCH) ---
565
+ const twsVal = store.raw["environment.wind.speedTrue"] ? msToKts(store.raw["environment.wind.speedTrue"]) : 0;
566
+ const awsVal = store.raw["environment.wind.speedApparent"] ? msToKts(store.raw["environment.wind.speedApparent"]) : 0;
692
567
 
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);
568
+ if (store.raw["environment.wind.speedTrue"] !== undefined) manageHistory('tws', twsVal);
569
+ if (store.raw["environment.wind.speedApparent"] !== undefined) manageHistory('aws', awsVal);
696
570
 
697
571
  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;
572
+ const labelWind = document.getElementById('tws-aws-label');
573
+ const currentWind = (displayModeTws === 'AWS') ? awsVal : twsVal;
700
574
 
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
575
+ ui.tws.innerText = currentWind.toFixed(1);
576
+ if (labelWind) labelWind.textContent = displayModeTws;
577
+
578
+ if (currentWind >= CONFIG.graphs.reef2) {
579
+ ui.tws.style.setProperty('color', '#ff3b30', 'important');
580
+ } else if (currentWind >= CONFIG.graphs.reef1) {
581
+ ui.tws.style.setProperty('color', '#ff9800', 'important');
709
582
  } else {
710
- // Colore di base differenziato per modalità (Indaco per AWS, Navy per TWS)
711
583
  if (displayModeTws === 'AWS') {
712
- ui.tws.style.setProperty('color', '#5c6bc0', 'important'); // Indigo
584
+ ui.tws.style.setProperty('color', '#5c6bc0', 'important');
713
585
  } 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');
586
+ const navyNight = isNight ? '#6c8ea0' : '#2c3e50';
587
+ ui.tws.style.setProperty('color', navyNight, 'important');
717
588
  }
718
589
  }
719
590
  }
720
591
 
721
- // --- AGGIORNAMENTO NUMERO AWS CENTRALE (Bussola) ---
722
592
  if (store.raw["environment.wind.speedApparent"] !== undefined) {
723
- const awsVal = msToKts(store.raw["environment.wind.speedApparent"]);
724
593
  ui.awsSvg.textContent = awsVal.toFixed(1);
725
594
  }
726
595
 
727
- // --- PUNTATORI ANALOGICI (Smoothing 2s) ---
596
+ // --- PUNTATORI ANALOGICI ---
728
597
  const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
729
598
  const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
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
- }
599
+ if (smAwa) ui.awa.setAttribute('transform', `rotate(${curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val))}, 200, 200)`);
600
+ if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
738
601
 
739
- // --- CALCOLO LEEWAY E TRACK POINTER ---
740
602
  if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
741
603
  let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
742
- // Filtraggio scarroccio: azzeramento se barca ferma, altrimenti smoothing
743
604
  smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
744
605
  curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
745
606
  ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
@@ -747,16 +608,12 @@ function startDisplayLoop() {
747
608
  updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
748
609
  }
749
610
 
750
- // Aggiorna i trend meteo/tattici (pallini) e l'allarme strambata
751
- updateWindTrend();
752
-
753
- // TIER HEAVY (2s) - Aggiornamento Grafici e Salvataggio Stato
611
+ // --- HEAVY TIER (2s) E SLOW TIER (3s) ---
754
612
  if (lastAvgUIUpdate++ % 2 === 0) {
755
613
  ['stw', 'sog', 'depth', 'tws'].forEach(refreshGraph);
756
614
  saveDashboardState();
757
615
  }
758
616
 
759
- // TIER SLOW (3s) - Calcolo Medie Lunghe e Tack Strategico
760
617
  if (lastAvgUIUpdate % 3 === 0) {
761
618
  let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, CONFIG.averaging.longWindow * 2, false);
762
619
  let cObj = getCircularAverageFromBuffer(store.longBuf.cog, CONFIG.averaging.longWindow, false);
@@ -764,41 +621,31 @@ function startDisplayLoop() {
764
621
  let twObj = getCircularAverageFromBuffer(store.longBuf.twa, CONFIG.averaging.longWindow, true);
765
622
  let twdObj = getCircularAverageFromBuffer(store.longBuf.twd, CONFIG.averaging.longWindow, false);
766
623
 
767
- // Aggiornamento interfaccia per i valori mediati (Heading, Cog, Awa, Twa, Twd)
768
624
  upUI(ui.hdg, hObj, store.raw["navigation.headingTrue"], true);
769
625
  upUI(ui.cog, cObj, store.raw["navigation.courseOverGroundTrue"], true);
770
626
  upUI(ui.awaAvg, awObj, store.raw["environment.wind.angleApparent"], false);
771
627
  upUI(ui.twaAvg, twObj, store.raw["environment.wind.angleTrueWater"], false);
772
628
  upUI(ui.twdAvg, twdObj, store.raw["environment.wind.directionTrue"], true);
773
629
 
774
- // --- LOGICA TACK STRATEGICA (VETTORIALE) ---
775
630
  if (hObj && twdObj) {
776
631
  const reflectAngle = (targetRad, axisRad) => {
777
632
  const dS = Math.sin(axisRad - targetRad);
778
633
  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);
634
+ return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
781
635
  };
782
-
783
636
  const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > CONFIG.averaging.stabilityBreakout;
784
-
785
- if (!isNavigating) {
786
- ui.tackHdg.innerHTML = "---&deg;";
787
- } else if (unstableH) {
788
- ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data');
789
- } else {
637
+ if (!isNavigating) ui.tackHdg.innerHTML = "---&deg;";
638
+ else if (unstableH) { ui.tackHdg.innerHTML = "---&deg;"; ui.tackHdg.classList.add('unstable-data'); }
639
+ else {
790
640
  const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
791
641
  ui.tackHdg.innerHTML = `${Math.round(rH).toString().padStart(3, '0')}&deg;`;
792
642
  ui.tackHdg.classList.remove('unstable-data');
793
643
  }
794
-
795
644
  if (cObj) {
796
645
  const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > CONFIG.averaging.stabilityBreakout;
797
- if (!isNavigating) {
798
- ui.tackCog.innerHTML = "---&deg;";
799
- } else if (unstableC) {
800
- ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data');
801
- } else {
646
+ if (!isNavigating) ui.tackCog.innerHTML = "---&deg;";
647
+ else if (unstableC) { ui.tackCog.innerHTML = "---&deg;"; ui.tackCog.classList.add('unstable-data'); }
648
+ else {
802
649
  const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
803
650
  ui.tackCog.innerHTML = `${Math.round(rC).toString().padStart(3, '0')}&deg;`;
804
651
  ui.tackCog.classList.remove('unstable-data');
@@ -806,7 +653,6 @@ function startDisplayLoop() {
806
653
  }
807
654
  }
808
655
 
809
- // Rotazione Mini-Icone nella bussola TWD (Mini-Bussole)
810
656
  const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
811
657
  const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
812
658
  if (smHdgIcons && smTwdIcons) {
@@ -820,10 +666,34 @@ function startDisplayLoop() {
820
666
  }
821
667
 
822
668
  // ==========================================================================
823
- // 8. CONFIGURAZIONE E GRAFICI UTILS
669
+ // 8. CONFIGURAZIONE, AGGIORNAMENTO LIVE E GRAFICI UTILS
824
670
  // ==========================================================================
671
+
672
+ let currentConfigString = ""; // Memoria per rilevare cambiamenti nei settings
673
+
674
+ /**
675
+ * Funzione Helper: Applica fisicamente i dati JSON all'oggetto CONFIG globale
676
+ */
677
+ function applyConfigData(data) {
678
+ Object.assign(CONFIG.alarms, data.alarms || {});
679
+ Object.assign(CONFIG.graphs, data.graphs || {});
680
+ Object.assign(CONFIG.averaging, data.averaging || {});
681
+
682
+ // Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85).
683
+ // Aggiornato a 0.95 per ottimizzazione filtri.
684
+ if (CONFIG.averaging.stabilityThreshold <= 0.85) {
685
+ CONFIG.averaging.stabilityThreshold = 0.95;
686
+ }
687
+
688
+ if (data.scales) {
689
+ for (let key in data.scales) {
690
+ if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
691
+ }
692
+ }
693
+ }
694
+
825
695
  /**
826
- * Recupera la configurazione dal server e applica migrazioni automatiche per le vecchie versioni
696
+ * Recupera la configurazione iniziale al caricamento della pagina
827
697
  */
828
698
  async function fetchServerConfig() {
829
699
  try {
@@ -831,44 +701,75 @@ async function fetchServerConfig() {
831
701
  if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
832
702
  const data = await response.json();
833
703
 
834
- // Stampa di debug per verificare cosa riceve il client
835
- console.log("🔍 Configurazione ricevuta dal Server:", data);
704
+ // Salviamo l'impronta digitale della configurazione per i confronti futuri
705
+ currentConfigString = JSON.stringify(data);
706
+
707
+ applyConfigData(data);
708
+ console.log("✅ Configurazione iniziale applicata con successo.");
709
+ } catch (err) {
710
+ console.warn("⚠️ Impossibile raggiungere il server per le configurazioni. Utilizzo default locali. Motivo:", err.message);
711
+ }
712
+ }
836
713
 
837
- // Merge intelligente dei dati ricevuti
838
- Object.assign(CONFIG.alarms, data.alarms || {});
839
- Object.assign(CONFIG.graphs, data.graphs || {});
840
- Object.assign(CONFIG.averaging, data.averaging || {});
714
+ /**
715
+ * Watchdog: Controlla in background se le impostazioni sul server sono cambiate
716
+ */
717
+ async function watchConfigChanges() {
718
+ try {
719
+ const response = await fetch('/rotevista-config');
720
+ if (!response.ok) return;
721
+ const data = await response.json();
841
722
 
842
- // --- LOGICA DI MIGRAZIONE SILENZIOSA ---
843
- // Se il valore ricevuto è il vecchio default (0.85) o inferiore, lo portiamo al nuovo standard 0.95.
844
- // Questo è necessario perché con i nuovi filtri "Soft" lo 0.85 non farebbe quasi mai lampeggiare gli allarmi.
845
- if (CONFIG.averaging.stabilityThreshold <= 0.85) {
846
- CONFIG.averaging.stabilityThreshold = 0.95;
847
- console.log("♻️ Migrazione Silenziosa: Rilevato vecchio parametro stabilità (<= 0.85). Aggiornato a 0.95 per ottimizzazione filtri.");
848
- }
723
+ const newConfigString = JSON.stringify(data);
849
724
 
850
- // Per le scale, siccome sono nidificate, facciamo un loop di merge profondo
851
- if (data.scales) {
852
- for (let key in data.scales) {
853
- if (CONFIG.scales[key]) Object.assign(CONFIG.scales[key], data.scales[key]);
854
- }
725
+ // Se l'impronta digitale è diversa, l'utente ha salvato nuovi settings!
726
+ if (newConfigString !== currentConfigString) {
727
+ console.log("🔄 Rilevato cambio impostazioni su Signal K! Aggiornamento in tempo reale...");
728
+
729
+ // Applichiamo i nuovi settings al volo
730
+ applyConfigData(data);
731
+
732
+ // Aggiorniamo l'impronta
733
+ currentConfigString = newConfigString;
734
+
735
+ // FEEDBACK VISIVO: Avvisiamo l'utente dell'aggiornamento
736
+ ui.status.innerText = "CONFIG UPDATED!";
737
+ ui.status.style.color = "#00C851"; // Verde Neon
738
+ setTimeout(() => { ui.status.style.color = ""; }, 4000);
855
739
  }
856
-
857
- console.log("✅ Configurazione applicata. Stabilità attiva:", CONFIG.averaging.stabilityThreshold);
858
740
  } catch (err) {
859
- console.warn("⚠️ Utilizzo default locali. Motivo:", err.message);
741
+ // Ignoriamo gli errori di rete nel watchdog per non intasare la console in caso di disconnessione
860
742
  }
861
743
  }
862
744
 
745
+ // --------------------------------------------------------------------------
746
+
747
+ /**
748
+ * Rielaborazione Storica Time-Based (Rimuove accumulo e gestisce il Pruning dinamicamente)
749
+ */
863
750
  function manageHistory(t, v) {
864
- const n = Date.now(), interval = (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
751
+ const n = Date.now();
752
+ const interval = (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
753
+
865
754
  if (!store.graphTempBuf[t]) store.graphTempBuf[t] = [];
866
755
  store.graphTempBuf[t].push(v);
756
+
867
757
  if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
868
758
  const avg = store.graphTempBuf[t].reduce((a, b) => a + b, 0) / store.graphTempBuf[t].length;
869
- store.histories[t].push(avg);
870
- if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
871
- store.graphTempBuf[t] = []; store.lastUpdates[t] = n;
759
+
760
+ // NUOVO STORAGE TIME-BASED
761
+ store.histories[t].push({ time: n, val: avg });
762
+
763
+ // PRUNING DINAMICO
764
+ const maxViewportMinutes = CONFIG.graphs.historyMinutes * 2;
765
+ const maxHistoryMs = (maxViewportMinutes * 60000) + 30000;
766
+
767
+ while (store.histories[t].length > 0 && (n - store.histories[t][0].time) > maxHistoryMs) {
768
+ store.histories[t].shift();
769
+ }
770
+
771
+ store.graphTempBuf[t] = [];
772
+ store.lastUpdates[t] = n;
872
773
  }
873
774
  }
874
775
 
@@ -888,261 +789,202 @@ function updateScaleLabels(t, min, max) {
888
789
  }
889
790
 
890
791
  /**
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à.
792
+ * refreshGraph: Recupero dati, switch AWS/TWS e passaggio a motore grafico.
900
793
  */
794
+ function refreshGraph(t) {
795
+ const boxType = (t === 'vmg') ? 'sog' : t;
796
+ let rawData;
797
+
798
+ if (t === 'tws' && displayModeTws === 'AWS') {
799
+ rawData = store.histories['aws'];
800
+ } else {
801
+ rawData = store.histories[t];
802
+ }
803
+
804
+ if (!rawData || rawData.length < 2) return;
805
+
806
+ // ESTRAZIONE SOLO VALORI NUMERICI per calculateScale()
807
+ const values = rawData.map(p => p.val);
808
+ const mode = graphModes[boxType];
809
+ const cfg = calculateScale(boxType, values, mode);
810
+
811
+ const box = document.querySelector(`.box-${boxType}`);
812
+ if (box) box.classList.toggle('box-hercules', mode === 'hercules');
813
+
814
+ updateScaleLabels(boxType, cfg.min, cfg.max);
815
+
816
+ // Passiamo tutto l'array (oggetti) al nuovo motore
817
+ drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
818
+ }
819
+
901
820
  /**
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à.
821
+ * drawGraph: Motore SVG con Timeline Reale e Gestione GAP
915
822
  */
916
823
  function drawGraph(d, id, min, max, isTws, isHercules) {
917
824
  const svg = document.getElementById(id);
918
-
919
- // Uscita di sicurezza se l'elemento non esiste o non ci sono abbastanza dati
920
825
  if (!svg || d.length < 2) return;
921
826
 
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
827
+ const w = 200, h = 40;
828
+ const range = max - min || 1;
829
+ const isDepth = (id === 'depth-graph');
830
+ const now = Date.now();
831
+
832
+ // VIEWPORT TEMPORALE DINAMICO
833
+ const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
834
+ const viewportMs = visibleMinutes * 60000;
835
+ const viewportStart = now - viewportMs;
951
836
 
952
- // A. LOGICA PER IL VENTO (TWS o AWS)
837
+ // FILTRO DATI VISIBILI (Gestisce Compressione/Zoom in modo naturale)
838
+ const visibleData = d.filter(p => p.time >= viewportStart);
839
+ if (visibleData.length < 2) return;
840
+
841
+ // COLORI
842
+ const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
843
+ const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
844
+
845
+ const getColorProps = (val) => {
846
+ let color = colTws, opacity = "0.15", stroke = "1";
953
847
  if (isTws) {
954
- // Scegliamo il colore di base a seconda se stiamo guardando Reale o Apparente
955
848
  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
- }
849
+ if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke = "1.2"; }
850
+ else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke = "1"; }
851
+ else color = baseWind;
852
+ } else if (isDepth) {
853
+ if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke = "1.2"; }
854
+ else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke = "1"; }
855
+ else color = colDepth;
856
+ } else {
857
+ if (id === 'stw-graph') color = colStw;
858
+ else if (id === 'sog-graph') color = (displayModeSog === 'VMG') ? colVmg : colSog;
983
859
  }
984
860
  return { color, opacity, stroke };
985
861
  };
986
862
 
987
- // --- 3. COSTRUZIONE DELLE GRIGLIE DI RIFERIMENTO ---
863
+ // GRIGLIA ORIZZONTALE
988
864
  let grids = "";
989
- // Linee orizzontali di livello (25%, 50%, 75%)
990
- [0.25, 0.5, 0.75].forEach(p => {
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" />`;
992
- });
993
- // Linee verticali temporali (dinamiche in base alla storia impostata)
994
- const gridInterval = (CONFIG.graphs.historyMinutes <= 15) ? 1 : 5;
995
- for (let m = gridInterval; m < CONFIG.graphs.historyMinutes; m += gridInterval) {
996
- const x = w - (m / CONFIG.graphs.historyMinutes) * w;
865
+ [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" />`);
866
+
867
+ // GRIGLIA VERTICALE VERA
868
+ const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
869
+ for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
870
+ const x = w - ((m / visibleMinutes) * w);
997
871
  grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
998
872
  }
999
873
 
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
1004
-
1005
- for (let i = 1; i < d.length; i++) {
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;
1009
- const x1 = ((i - 1) / (samples - 1)) * w;
1010
- const y1 = h - (Math.max(0, Math.min(1, (d[i - 1] - min) / range)) * h);
1011
- const x2 = (i / (samples - 1)) * w;
1012
- const y2 = h - (Math.max(0, Math.min(1, (d[i] - min) / range)) * h);
1013
-
1014
- // Otteniamo lo stile per questo specifico segmento
1015
- const props = getColorProps(d[i]);
1016
-
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}" />`;
1022
-
1023
- // DISEGNO DELLA LINEA SUPERIORE:
1024
- // Usiamo linee individuali per poter gestire spessori e colori diversi (se necessario in futuro)
1025
- lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"
1026
- style="stroke: ${props.color}; stroke-width: ${props.stroke}; stroke-linecap: round; shape-rendering: geometricPrecision;" />`;
1027
-
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`;
874
+ // RENDER TIME-BASED
875
+ let gradientStops = "", lines = "", areaPath = "";
876
+ let started = false;
877
+
878
+ for (let i = 1; i < visibleData.length; i++) {
879
+ const pA = visibleData[i - 1];
880
+ const pB = visibleData[i];
881
+
882
+ // POSIZIONE X ESATTA AL MILLISECONDO
883
+ const x1 = ((pA.time - viewportStart) / viewportMs) * w;
884
+ const x2 = ((pB.time - viewportStart) / viewportMs) * w;
885
+ const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
886
+ const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
887
+
888
+ const props = getColorProps(pB.val);
889
+
890
+ // GESTIONE GAP TEMPORALI (Network loss)
891
+ const deltaTime = pB.time - pA.time;
892
+ const expectedInterval = viewportMs / CONFIG.graphs.samples;
893
+ const isGap = deltaTime > (expectedInterval * 2.5);
894
+
895
+ // STOPS GRADIENTE
896
+ const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
897
+ gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
898
+ gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
899
+
900
+ // --- FIX: GESTIONE AREA E LINEE DURANTE I GAP ---
901
+ if (isGap) {
902
+ // Se c'è un buco nei dati e avevamo già iniziato a disegnare...
903
+ if (started) {
904
+ // Chiudiamo il pezzo di area precedente scendendo verticalmente (Z non serve qui)
905
+ areaPath += `L ${x1} ${h} `;
906
+ started = false; // Resettiamo il flag per far ripartire l'area al prossimo punto
907
+ }
908
+ } else {
909
+ // I dati sono continui, disegniamo la linea superiore
910
+ lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" />`;
911
+
912
+ // Gestione Area
913
+ if (!started) {
914
+ // Iniziamo un nuovo pezzo di area dal fondo, saliamo a y1
915
+ areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
916
+ started = true;
917
+ }
918
+ // Aggiungiamo il punto attuale
919
+ areaPath += `L ${x2} ${y2} `;
1037
920
  }
1038
921
  }
1039
922
 
1040
- // --- 5. CREAZIONE DEL GRADIENTE LINEARE DINAMICO ---
1041
- const gradId = `grad-${id}`; // ID unico basato sul box (es: grad-tws-graph)
923
+ // CHIUSURA FINALE DELL'AREA (Solo se non siamo finiti dentro un gap)
924
+ if (started) {
925
+ const last = visibleData[visibleData.length - 1];
926
+ const lastX = ((last.time - viewportStart) / viewportMs) * w;
927
+ areaPath += `L ${lastX} ${h} Z`; // Z chiude automaticamente il path tornando al punto 'M' iniziale
928
+ }
929
+
930
+ // GRADIENTE
931
+ const gradId = `grad-${id}`;
932
+ const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
1042
933
 
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
- */
1048
- const defs = `
1049
- <defs>
1050
- <linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">
1051
- ${gradientStops}
1052
- </linearGradient>
1053
- </defs>
1054
- `;
1055
-
1056
- // --- 6. AGGIORNAMENTO FINALE DEL DOM ---
1057
- // Inseriamo tutto nell'elemento SVG: Defs (Gradienti) -> Griglie -> Area (Fill) -> Linee (Stroke)
934
+ // RENDER FINALE
1058
935
  svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
1059
936
  }
1060
937
 
1061
938
  // ==========================================================================
1062
939
  // 9. INTERAZIONI E GESTI
1063
940
  // ==========================================================================
1064
- /**
1065
- * toggleFocusMode: Gestisce l'attivazione della modalità Split-Screen (Focus).
1066
- * Ingrandisce un box specifico e mantiene visibile la bussola centrale.
1067
- */
1068
941
  function toggleFocusMode(type, element) {
1069
942
  const container = document.querySelector('.main-container');
1070
-
1071
- // Identifica se il box appartiene alla colonna sinistra (per il layout CSS)
1072
943
  const isLeft = ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(type);
1073
-
1074
- // Inverte lo stato del Focus
1075
944
  isFocusActive = !isFocusActive;
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
- }
945
+ if (isFocusActive) { container.classList.add('focus-active', isLeft ? 'focus-side-left' : 'focus-side-right'); element.classList.add('is-focused'); }
946
+ else { container.classList.remove('focus-active', 'focus-side-left', 'focus-side-right'); document.querySelectorAll('.data-box').forEach(b => b.classList.remove('is-focused')); }
1086
947
  }
1087
948
 
1088
- /**
1089
- * Inizializzazione Gesti e Interazioni sui box con grafico.
1090
- * Gestisce: Singolo Tap (Switch), Doppio Tap (Zoom), Long Press (Focus).
1091
- */
1092
949
  ['stw', 'sog', 'tws', 'depth'].forEach(type => {
1093
950
  const el = document.getElementById(type + '-graph').closest('.data-box');
1094
951
  let lastTapTime = 0, tapTimeout, isLongPressActive = false;
1095
952
 
1096
- // --- GESTIONE PRESSIONE (Inizio) ---
1097
953
  el.addEventListener('pointerdown', (e) => {
1098
954
  isLongPressActive = false;
1099
- // Timer per attivare il Focus Mode dopo 1 secondo di pressione continua
1100
955
  pressTimer = setTimeout(() => {
1101
956
  if (!isFocusActive) {
1102
957
  isLongPressActive = true;
1103
958
  toggleFocusMode(type, el);
1104
- lastTapTime = 0; // Evita che al rilascio scatti un click
959
+ lastTapTime = 0;
1105
960
  }
1106
961
  }, 1000);
1107
962
  });
1108
963
 
1109
- // --- GESTIONE RILASCIO (Fine Gesto) ---
1110
964
  el.addEventListener('pointerup', (e) => {
1111
- clearTimeout(pressTimer); // Cancella il timer del long press
1112
- if (isLongPressActive) return; // Se è scattato il focus, non fare altro
965
+ clearTimeout(pressTimer);
966
+ if (isLongPressActive) return;
1113
967
 
1114
968
  const currentTime = new Date().getTime();
1115
969
  const tapDelay = currentTime - lastTapTime;
1116
970
 
1117
- // 1. GESTIONE DOPPIO CLICK (Zoom Hercules)
1118
971
  if (tapDelay < 300 && tapDelay > 0) {
1119
972
  clearTimeout(tapTimeout);
1120
- // Switch tra modalità scala Standard e Zoom Hercules
1121
973
  graphModes[type] = (graphModes[type] === 'standard') ? 'hercules' : 'standard';
1122
974
  localStorage.setItem('mode_' + type, graphModes[type]);
1123
975
  refreshGraph(type);
1124
976
  lastTapTime = 0;
1125
- }
1126
- // 2. GESTIONE SINGOLO CLICK (Switch Dati o Esci dal Focus)
1127
- else {
977
+ } else {
1128
978
  lastTapTime = currentTime;
1129
979
  tapTimeout = setTimeout(() => {
1130
- // Se il box è in focus, il singolo click lo chiude
1131
980
  if (isFocusActive && el.classList.contains('is-focused')) {
1132
981
  toggleFocusMode(type, el);
1133
- }
1134
- // Se siamo in visualizzazione standard, gestiamo gli switch dati
1135
- else if (!isFocusActive) {
1136
- // Switch SOG <-> VMG
982
+ } else if (!isFocusActive) {
1137
983
  if (type === 'sog') {
1138
984
  displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG';
1139
- }
1140
- // Switch TWS <-> AWS (Nuova Logica)
1141
- else if (type === 'tws') {
985
+ } else if (type === 'tws') {
1142
986
  displayModeTws = (displayModeTws === 'TWS') ? 'AWS' : 'TWS';
1143
987
  }
1144
-
1145
- // Feedback visivo (lampeggio leggero) dell'avvenuto switch
1146
988
  el.style.backgroundColor = "rgba(0, 0, 0, 0.05)";
1147
989
  setTimeout(() => el.style.backgroundColor = "", 150);
1148
990
  }
@@ -1150,7 +992,6 @@ function toggleFocusMode(type, element) {
1150
992
  }
1151
993
  });
1152
994
 
1153
- // --- GESTIONE USCITA (Se l'utente trascina il dito fuori) ---
1154
995
  el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
1155
996
  });
1156
997
 
@@ -1194,10 +1035,7 @@ function connect() {
1194
1035
  const d = JSON.parse(e.data);
1195
1036
  if (d.updates) {
1196
1037
  d.updates.forEach(u => {
1197
- // 1. Estraiamo il nome del sensore/sorgente (es. "yacht_device" o "Unknown")
1198
1038
  const sourceLabel = u.source ? (u.source.label || u.source.talker || "Unknown") : "Unknown";
1199
-
1200
- // 2. Passiamo il nome della sorgente come TERZO parametro
1201
1039
  if (u.values) {
1202
1040
  u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
1203
1041
  }
@@ -1231,6 +1069,7 @@ async function init() {
1231
1069
  await fetchServerConfig();
1232
1070
  startDisplayLoop();
1233
1071
  connect();
1072
+ setInterval(watchConfigChanges, 10000);
1234
1073
  }
1235
1074
 
1236
1075
  window.addEventListener('load', init);