@sailingrotevista/rotevista-dash 5.0.5 → 5.0.7

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 +292 -101
  2. package/debug.html +104 -56
  3. package/package.json +1 -1
package/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ==========================================================================
3
- * Signal K Wind Dashboard - Pro Version 3.5 (Time-Based Architecture)
3
+ * Signal K Wind Dashboard - Pro Version 3.8 (Dynamic Envelope Architecture)
4
4
  * ==========================================================================
5
5
  * Autore: Sailing Rotevista
6
6
  * Motore di calcolo tattico per navigazione e crociera.
@@ -24,19 +24,18 @@ let CONFIG = {
24
24
  },
25
25
  graphs: { reef1: 10, reef2: 15, historyMinutes: 10, samples: 60 },
26
26
  scales: {
27
- stw: { stdMax: 4, hercSpan: 4, step: 2 },
28
- sog: { stdMax: 4, hercSpan: 4, step: 2 },
29
- tws: { stdMax: 15, hercSpan: 10, step: 5 },
30
- depth: { stdMax: 8, hercSpan: 5, step: 5 }
27
+ stw: { stdMax: 4, hercSpan: 2, step: 1 },
28
+ sog: { stdMax: 4, hercSpan: 2, step: 1 },
29
+ tws: { stdMax: 15, hercSpan: 2, step: 1 },
30
+ depth: { stdMax: 8, hercSpan: 1, step: 1 }
31
31
  },
32
32
  server: { fallbackIp: "192.168.111.240:3000" }
33
33
  };
34
34
 
35
35
  const RENDER_INTERVAL_MS = 1000;
36
- const TIMEOUT_MS = 5000;
36
+ const TIMEOUT_MS = 15000;
37
37
  const SIM_SAMPLE_INTERVAL = 1000;
38
- const DASH_VERSION = "3.7"; // Major Update: Time-Based storage and GAP handling
39
- const sourceLocks = {};
38
+ const DASH_VERSION = "3.8"; // Major Update: Smart Source Locking & Breathing Hercules Scale
40
39
 
41
40
  // ==========================================================================
42
41
  // 2. STATO GLOBALE E RIFERIMENTI UI
@@ -59,6 +58,8 @@ let pressTimer, isFocusActive = false;
59
58
  let emaTwaSin = 0;
60
59
  let emaTwaCos = 0;
61
60
  let firstEmaRun = true;
61
+ // MACCHINA A STATI ALLARME STRAMBATA (Filtro Isteresi Anti-Brandeggio)
62
+ let lastGybeSide = null;
62
63
 
63
64
  const graphModes = {
64
65
  stw: 'standard',
@@ -67,13 +68,17 @@ const graphModes = {
67
68
  depth: 'standard'
68
69
  };
69
70
 
71
+ // GESTIONE SMART LOCK DELLE SORGENTI (Zero-Config)
72
+ const sourceLocks = {};
73
+
70
74
  // Database centrale dello store dati
71
75
  const store = {
72
76
  raw: {},
73
77
  timestamps: {},
78
+ depthProtectedActive: false, // Memoria per lo stato della protezione profondità
79
+ herculesScales: {}, // Memoria per i limiti attivi della modalità Hercules
74
80
  smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
75
81
  longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
76
- // histories ora conterrà array di oggetti: { time: Date.now(), val: numero }
77
82
  histories: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
78
83
  graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [], aws: [] },
79
84
  lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0 }
@@ -326,14 +331,62 @@ function computeTrueWind() {
326
331
  }
327
332
  }
328
333
 
334
+ // ==========================================================================
335
+ // GESTIONE SMART LOCK DELLE SORGENTI (Zero-Config)
336
+ // ==========================================================================
337
+
338
+ /**
339
+ * Assegna un punteggio di qualità statico alla sorgente basato sull'hardware.
340
+ * Più alto è il punteggio, maggiore è la priorità del sensore.
341
+ */
342
+ function getSourcePriorityScore(sourceName) {
343
+ if (!sourceName) return 0;
344
+ const name = sourceName.toLowerCase();
345
+
346
+ // TIER 3: AIS / Trasmissioni lente (es. yacht_device.AI, VDO)
347
+ if (name.includes('.ai') || name.includes('ais') || name.includes('vdo')) {
348
+ return 10;
349
+ }
350
+
351
+ // TIER 2: GPS USB del Cerbo GX / Victron / Sistemi locali di servizio
352
+ if (name.includes('venus') || name.includes('victron') || name.includes('ttyacm') || name.includes('ttyusb') || name.includes('system')) {
353
+ return 50;
354
+ }
355
+
356
+ // TIER 1: Strumentazione ufficiale di navigazione (NMEA 2000, Yacht Devices GP/YD/AP, Gateway, ecc.)
357
+ if (name.includes('yacht_device') || name.includes('n2k') || name.includes('actisense') || name.includes('can') || name.includes('nmea') || name.includes('raymarine') || name.includes('garmin') || name.includes('simrad') || name.includes('b&g')) {
358
+ return 100;
359
+ }
360
+
361
+ return 30; // Punteggio standard per sorgenti sconosciute
362
+ }
363
+
329
364
  function processIncomingData(path, val, source) {
330
365
  const now = Date.now();
366
+ const score = getSourcePriorityScore(source);
331
367
 
332
- // FILTRO SORGENTE STICKY
333
- if (!sourceLocks[path] || sourceLocks[path].label === source || (now - sourceLocks[path].lastSeen > 2000)) {
334
- sourceLocks[path] = { label: source, lastSeen: now };
368
+ // Gestione dello Smart Lock
369
+ if (!sourceLocks[path]) {
370
+ // Nessun blocco attivo: aggancia la sorgente corrente
371
+ sourceLocks[path] = { label: source, score: score, lastSeen: now };
335
372
  } else {
336
- return;
373
+ const currentLock = sourceLocks[path];
374
+ const isSameSource = (currentLock.label === source);
375
+ const isLockExpired = (now - currentLock.lastSeen > 12000); // Scadenza a 12 secondi per tollerare il GPS dello Yacht Devices
376
+ const hasHigherPriority = (score > currentLock.score);
377
+
378
+ if (isSameSource) {
379
+ // Stessa sorgente: aggiorna il timestamp e mantiene il blocco
380
+ currentLock.lastSeen = now;
381
+ currentLock.score = score;
382
+ } else if (isLockExpired || hasHigherPriority) {
383
+ // Ruba il blocco se la sorgente precedente è scaduta o se questa ha priorità superiore
384
+ sourceLocks[path] = { label: source, score: score, lastSeen: now };
385
+ console.log(`🔌 [Smart Lock] Path "${path}" switched to: ${source} (Score: ${score})`);
386
+ } else {
387
+ // Rifiuta i dati da sorgenti a priorità inferiore se quella principale è attiva
388
+ return;
389
+ }
337
390
  }
338
391
 
339
392
  store.timestamps[path] = now;
@@ -373,57 +426,16 @@ function processIncomingData(path, val, source) {
373
426
  // ==========================================================================
374
427
  function updateWindTrend() {
375
428
  const now = Date.now();
429
+
430
+ // --- 6.1 TREND TATTICO (AWA/TWA) ---
376
431
  const twaNow = getCircularAverageFromBuffer(store.longBuf.twa, 2000, true);
377
432
  const twaRef = getCircularAverageFromBuffer(store.longBuf.twa, 10000, true);
378
-
379
- const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
380
- const multiplier = isNavigating ? 1 : 2;
381
- const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
382
- const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
383
-
384
- if (!twaNow || !twaRef || !twdNow || !twdRef) return;
385
-
386
- const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
387
433
  const gaugeDots = { cw: document.getElementById('trend-gauge-cw'), ccw: document.getElementById('trend-gauge-ccw') };
388
434
 
389
- // TREND METEO
390
- let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
391
- if (Math.abs(deltaMeteo) > 6.0) {
392
- const isSouth = store.raw["navigation.position"]?.latitude < 0;
393
- let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#27ae60" : "#c0392b") : (deltaMeteo > 0 ? "#27ae60" : "#c0392b");
394
- if (deltaMeteo > 0) {
395
- compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor);
396
- compassDots.ccw.classList.remove('is-trending');
397
- } else {
398
- compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor);
399
- compassDots.cw.classList.remove('is-trending');
400
- }
401
- } else { [compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
402
-
403
- // TREND TATTICO
404
- let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
405
- const curTwaDeg = radToDeg(twaNow.val);
406
- if (Math.abs(deltaTac) > 3.0) {
407
- let absTwa = Math.abs(curTwaDeg);
408
- let tacticColor;
409
- if (absTwa > 75 && absTwa < 105) {
410
- tacticColor = "#bbb";
411
- } else {
412
- let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
413
- if (absTwa >= 90) isLift = !isLift;
414
- tacticColor = isLift ? "#27ae60" : "#c0392b";
415
- }
416
- if (deltaTac > 0) {
417
- gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor);
418
- gaugeDots.ccw.classList.remove('is-trending');
419
- } else {
420
- gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor);
421
- gaugeDots.cw.classList.remove('is-trending');
422
- }
423
- } else { [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }}); }
424
-
425
- // ALLARME STRAMBATA (EMA FILTERED)
435
+ // --- 6.2 ALLARME STRAMBATA CON ISTERESI (MACCHINA A STATI ANTI-BRANDEGGIO) ---
426
436
  const instTwaRad = store.raw["environment.wind.angleTrueWater"];
437
+ let smoothedTwaDeg = null;
438
+
427
439
  if (instTwaRad !== undefined) {
428
440
  const dynamicAlpha = Math.max(0.05, 1.1 - CONFIG.averaging.stabilityThreshold);
429
441
  const currentSin = Math.sin(instTwaRad);
@@ -436,19 +448,98 @@ function updateWindTrend() {
436
448
  emaTwaCos = (currentCos * dynamicAlpha) + (emaTwaCos * (1 - dynamicAlpha));
437
449
  }
438
450
 
439
- const smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
451
+ smoothedTwaDeg = radToDeg(Math.atan2(emaTwaSin, emaTwaCos));
452
+ const absTwaDeg = Math.abs(smoothedTwaDeg);
453
+
454
+ // Allarme VISIVO: Se siamo in zona di pericolo poppa profonda (> 155°), accendi entrambi i LED di rosso pulsante
455
+ if (absTwaDeg > 155) {
456
+ if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-gybing'); gaugeDots.cw.setAttribute('fill', '#ff3b30'); }
457
+ if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-gybing'); gaugeDots.ccw.setAttribute('fill', '#ff3b30'); }
458
+
459
+ // LOGICA MACCHINA A STATI CON ISTERESI:
460
+ // Rileviamo le mure solo se siamo fuori dalla zona cieca di poppa secca (> 170°).
461
+ // Se oscilliamo tra -175° e +178° (brandeggio), lastGybeSide NON cambia e l'allarme acustico tace.
462
+ let currentTack = null;
463
+ if (smoothedTwaDeg > 155 && smoothedTwaDeg < 170) {
464
+ currentTack = 'starboard';
465
+ } else if (smoothedTwaDeg < -155 && smoothedTwaDeg > -170) {
466
+ currentTack = 'port';
467
+ }
440
468
 
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)}°`);
469
+ if (currentTack !== null) {
470
+ if (lastGybeSide === null) {
471
+ // Inizializzazione al primo ingresso nella zona di controllo
472
+ lastGybeSide = currentTack;
473
+ } else if (lastGybeSide !== currentTack) {
474
+ // Abbiamo eseguito una vera strambata stabile e siamo usciti dalla zona di poppa secca!
475
+ lastGybeSide = currentTack;
476
+
477
+ // Attivazione allarme acustico con blocco temporale di sicurezza (60 secondi)
478
+ if (isNavigating && (now - lastGybeAlarmTime > 60000)) {
479
+ lastGybeAlarmTime = now;
480
+ playGybeAlarm();
481
+ console.log(`⚠️ GYBE ALARM TRIGGERED: Tack switched to ${currentTack} (TWA: ${smoothedTwaDeg.toFixed(1)}°)`);
482
+ }
447
483
  }
448
484
  }
485
+ } else {
486
+ // Se usciamo dalla poppa profonda (< 155°), disattiva l'allarme visivo e resetta lo stato delle mure
487
+ if (gaugeDots.cw) gaugeDots.cw.classList.remove('is-gybing');
488
+ if (gaugeDots.ccw) gaugeDots.ccw.classList.remove('is-gybing');
489
+ lastGybeSide = null; // Reset per la prossima poppa
449
490
  }
450
491
  lastInstantTwa = smoothedTwaDeg;
451
492
  }
493
+
494
+ // Gestione normale dei Trend Tattici (Lifts/Headers) se NON siamo in allarme strambata
495
+ if (twaNow && twaRef && (smoothedTwaDeg === null || Math.abs(smoothedTwaDeg) <= 155)) {
496
+ let deltaTac = radToDeg((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI) - Math.PI);
497
+ const curTwaDeg = radToDeg(twaNow.val);
498
+ if (Math.abs(deltaTac) > 3.0) {
499
+ let absTwa = Math.abs(curTwaDeg);
500
+ let tacticColor;
501
+ if (absTwa > 75 && absTwa < 105) {
502
+ tacticColor = "#bbb";
503
+ } else {
504
+ let isLift = (curTwaDeg > 0) ? (deltaTac > 0) : (deltaTac < 0);
505
+ if (absTwa >= 90) isLift = !isLift;
506
+ tacticColor = isLift ? "#27ae60" : "#c0392b";
507
+ }
508
+ if (deltaTac > 0) {
509
+ if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
510
+ if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-trending'); }
511
+ } else {
512
+ if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor); }
513
+ if (gaugeDots.cw) { gaugeDots.cw.classList.remove('is-trending'); }
514
+ }
515
+ } else {
516
+ [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
517
+ }
518
+ }
519
+
520
+ // --- 6.3 TREND METEO STRATEGICO (TWD) ---
521
+ const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
522
+ const multiplier = isNavigating ? 1 : 2;
523
+ const strategicWindowMs = CONFIG.graphs.historyMinutes * 60000 * multiplier;
524
+ const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false);
525
+ const compassDots = { cw: document.getElementById('trend-dot-cw'), ccw: document.getElementById('trend-dot-ccw') };
526
+
527
+ if (twdNow && twdRef) {
528
+ let deltaMeteo = radToDeg((twdNow.val - twdRef.val + Math.PI * 3) % (Math.PI * 2) - Math.PI);
529
+ if (Math.abs(deltaMeteo) > 6.0) {
530
+ const isSouth = store.raw["navigation.position"]?.latitude < 0;
531
+ let meteoColor = (!isSouth) ? (deltaMeteo < 0 ? "#27ae60" : "#c0392b") : (deltaMeteo > 0 ? "#27ae60" : "#c0392b");
532
+ if (deltaMeteo > 0) {
533
+ if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
534
+ if (compassDots.ccw) { compassDots.ccw.classList.remove('is-trending'); }
535
+ } else {
536
+ if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
537
+ if (compassDots.cw) { compassDots.cw.classList.remove('is-trending'); }
538
+ }
539
+ } else {
540
+ [compassDots.cw, compassDots.ccw].forEach(el => { if(el){ el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
541
+ }
542
+ }
452
543
  }
453
544
 
454
545
  // ==========================================================================
@@ -487,6 +578,9 @@ function startDisplayLoop() {
487
578
 
488
579
  isNavigating = stwKts > CONFIG.averaging.minSpeed || sogKts > CONFIG.averaging.minSpeed;
489
580
 
581
+ // --- CALCOLO TREND VENTO & ALLARME STRAMBATA ---
582
+ updateWindTrend();
583
+
490
584
  // --- AGGIORNAMENTO STATUS CON CONTEGGIO MINUTI REALE ---
491
585
  const viewportMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
492
586
  const requiredMs = viewportMinutes * 60000;
@@ -766,7 +860,6 @@ function manageHistory(type, value) {
766
860
  const tempBuf = store.graphTempBuf[type];
767
861
 
768
862
  // --- 3. ANTI-DROPOUT DINAMICO (Auto-scaling) ---
769
- // Ignora cadute a zero se il valore precedente era superiore al 50% del primo Reef
770
863
  if ((type === 'tws' || type === 'aws') && value < 0.05 && tempBuf.length > 0) {
771
864
  const lastPoint = tempBuf[tempBuf.length - 1];
772
865
  const glitchThreshold = (CONFIG.graphs.reef1 || 15) * 0.5;
@@ -814,11 +907,10 @@ function manageHistory(type, value) {
814
907
  }
815
908
 
816
909
  // --- 6. CLAMPING E VALIDAZIONE FINALE ---
817
- // Protezione contro valori negativi (fisicamente impossibili per questi dati) e non finiti
818
910
  if (!isFinite(finalValue)) return;
819
911
  finalValue = Math.max(0, finalValue);
820
912
 
821
- // --- 7. STORAGE STORICO (Keys: val, time) ---
913
+ // --- 7. STORAGE STORICO ---
822
914
  store.histories[type].push({ val: finalValue, time: now });
823
915
 
824
916
  // --- 8. PRUNING DINAMICO ---
@@ -833,13 +925,126 @@ function manageHistory(type, value) {
833
925
  store.graphTempBuf[type] = [];
834
926
  store.lastUpdates[type] = now;
835
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
+ */
836
933
  function calculateScale(type, data, mode) {
837
- const s = CONFIG.scales[type]; let aMin = Math.min(...data), aMax = Math.max(...data);
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
958
+
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;
1012
+ }
1013
+ }
1014
+
1015
+ currentScale.min = 0;
1016
+ return { min: currentScale.min, max: currentScale.max };
1017
+ }
1018
+ }
1019
+
1020
+ // ==========================================================================
1021
+ // ALTRI GRAFICI (STW, SOG, TWS): MANTENGONO IL COMPORTAMENTO ORIGINALE
1022
+ // ==========================================================================
1023
+ let aMin = Math.min(...data), aMax = Math.max(...data);
1024
+
838
1025
  if (mode === 'hercules') {
839
- let avg = (aMin + aMax) / 2; let span = Math.max(s.hercSpan, Math.ceil(aMax - aMin));
840
- if (span % 2 !== 0) span += 1; let min = Math.max(0, Math.floor(avg - (span / 2)));
841
- return { min, max: min + span };
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 };
842
1045
  }
1046
+
1047
+ // Scala Standard (Autocompressione a scatti)
843
1048
  return { min: 0, max: Math.max(s.stdMax, Math.ceil(aMax / s.step) * s.step) };
844
1049
  }
845
1050
 
@@ -863,7 +1068,6 @@ function refreshGraph(t) {
863
1068
 
864
1069
  if (!rawData || rawData.length < 2) return;
865
1070
 
866
- // ESTRAZIONE SOLO VALORI NUMERICI per calculateScale()
867
1071
  const values = rawData.map(p => p.val);
868
1072
  const mode = graphModes[boxType];
869
1073
  const cfg = calculateScale(boxType, values, mode);
@@ -872,8 +1076,6 @@ function refreshGraph(t) {
872
1076
  if (box) box.classList.toggle('box-hercules', mode === 'hercules');
873
1077
 
874
1078
  updateScaleLabels(boxType, cfg.min, cfg.max);
875
-
876
- // Passiamo tutto l'array (oggetti) al nuovo motore
877
1079
  drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
878
1080
  }
879
1081
 
@@ -889,16 +1091,13 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
889
1091
  const isDepth = (id === 'depth-graph');
890
1092
  const now = Date.now();
891
1093
 
892
- // VIEWPORT TEMPORALE DINAMICO
893
1094
  const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
894
1095
  const viewportMs = visibleMinutes * 60000;
895
1096
  const viewportStart = now - viewportMs;
896
1097
 
897
- // FILTRO DATI VISIBILI (Gestisce Compressione/Zoom in modo naturale)
898
1098
  const visibleData = d.filter(p => p.time >= viewportStart);
899
1099
  if (visibleData.length < 2) return;
900
1100
 
901
- // COLORI
902
1101
  const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
903
1102
  const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
904
1103
 
@@ -920,18 +1119,15 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
920
1119
  return { color, opacity, stroke };
921
1120
  };
922
1121
 
923
- // GRIGLIA ORIZZONTALE
924
1122
  let grids = "";
925
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" />`);
926
1124
 
927
- // GRIGLIA VERTICALE VERA
928
1125
  const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
929
1126
  for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
930
1127
  const x = w - ((m / visibleMinutes) * w);
931
1128
  grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
932
1129
  }
933
1130
 
934
- // RENDER TIME-BASED
935
1131
  let gradientStops = "", lines = "", areaPath = "";
936
1132
  let started = false;
937
1133
 
@@ -939,59 +1135,43 @@ function drawGraph(d, id, min, max, isTws, isHercules) {
939
1135
  const pA = visibleData[i - 1];
940
1136
  const pB = visibleData[i];
941
1137
 
942
- // POSIZIONE X ESATTA AL MILLISECONDO
943
1138
  const x1 = ((pA.time - viewportStart) / viewportMs) * w;
944
1139
  const x2 = ((pB.time - viewportStart) / viewportMs) * w;
945
1140
  const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
946
1141
  const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
947
1142
 
948
1143
  const props = getColorProps(pB.val);
949
-
950
- // GESTIONE GAP TEMPORALI (Network loss)
951
1144
  const deltaTime = pB.time - pA.time;
952
1145
  const expectedInterval = viewportMs / CONFIG.graphs.samples;
953
1146
  const isGap = deltaTime > (expectedInterval * 2.5);
954
1147
 
955
- // STOPS GRADIENTE
956
1148
  const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
957
1149
  gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
958
1150
  gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
959
1151
 
960
- // --- FIX: GESTIONE AREA E LINEE DURANTE I GAP ---
961
1152
  if (isGap) {
962
- // Se c'è un buco nei dati e avevamo già iniziato a disegnare...
963
1153
  if (started) {
964
- // Chiudiamo il pezzo di area precedente scendendo verticalmente (Z non serve qui)
965
1154
  areaPath += `L ${x1} ${h} `;
966
- started = false; // Resettiamo il flag per far ripartire l'area al prossimo punto
1155
+ started = false;
967
1156
  }
968
1157
  } else {
969
- // I dati sono continui, disegniamo la linea superiore
970
1158
  lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" />`;
971
-
972
- // Gestione Area
973
1159
  if (!started) {
974
- // Iniziamo un nuovo pezzo di area dal fondo, saliamo a y1
975
1160
  areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
976
1161
  started = true;
977
1162
  }
978
- // Aggiungiamo il punto attuale
979
1163
  areaPath += `L ${x2} ${y2} `;
980
1164
  }
981
1165
  }
982
1166
 
983
- // CHIUSURA FINALE DELL'AREA (Solo se non siamo finiti dentro un gap)
984
1167
  if (started) {
985
1168
  const last = visibleData[visibleData.length - 1];
986
1169
  const lastX = ((last.time - viewportStart) / viewportMs) * w;
987
- areaPath += `L ${lastX} ${h} Z`; // Z chiude automaticamente il path tornando al punto 'M' iniziale
1170
+ areaPath += `L ${lastX} ${h} Z`;
988
1171
  }
989
1172
 
990
- // GRADIENTE
991
1173
  const gradId = `grad-${id}`;
992
1174
  const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
993
-
994
- // RENDER FINALE
995
1175
  svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
996
1176
  }
997
1177
 
@@ -1095,7 +1275,18 @@ function connect() {
1095
1275
  const d = JSON.parse(e.data);
1096
1276
  if (d.updates) {
1097
1277
  d.updates.forEach(u => {
1098
- const sourceLabel = u.source ? (u.source.label || u.source.talker || "Unknown") : "Unknown";
1278
+ // ESTRAZIONE AVANZATA DELLA SORGENTE (Gestisce $source e stringhe native)
1279
+ let sourceLabel = "Unknown";
1280
+ if (u.$source) {
1281
+ sourceLabel = u.$source;
1282
+ } else if (u.source) {
1283
+ if (typeof u.source === 'object') {
1284
+ sourceLabel = u.source.label || u.source.talker || u.source.src || "Unknown";
1285
+ } else {
1286
+ sourceLabel = String(u.source);
1287
+ }
1288
+ }
1289
+
1099
1290
  if (u.values) {
1100
1291
  u.values.forEach(v => processIncomingData(v.path, v.value, sourceLabel));
1101
1292
  }
package/debug.html CHANGED
@@ -56,7 +56,7 @@
56
56
  /* Stili Testo Tabella */
57
57
  .value { color: #64ffda; font-weight: bold; font-size: 16px; }
58
58
  .unit { color: #888; font-size: 12px; margin-left: 5px; }
59
- .source { color: #ffb74d; font-size: 12px; }
59
+ .source { color: #ffb74d; font-size: 12px; word-break: break-all; }
60
60
  .time { color: #90caf9; font-size: 12px; }
61
61
  .raw { color: #777; font-size: 11px; display: block; margin-top: 4px; }
62
62
 
@@ -83,7 +83,7 @@
83
83
  color: #aaa;
84
84
  }
85
85
 
86
- /* Area di testo del Log (Non più div separati, ma un unico blocco di testo preformattato) */
86
+ /* Area di testo del Log */
87
87
  textarea#log-container {
88
88
  flex-grow: 1;
89
89
  background: transparent;
@@ -122,6 +122,7 @@
122
122
  <span style="margin-left: 20px; color:#aaa;">Filtra Log:</span>
123
123
  <select id="log-filter">
124
124
  <option value="all">Tutti i dati monitorati</option>
125
+ <option value="navigation.position">Solo Posizione (GPS)</option>
125
126
  <option value="navigation.speedOverGround">Solo SOG (Velocità sul fondo)</option>
126
127
  <option value="navigation.courseOverGroundTrue">Solo COG (Rotta sul fondo)</option>
127
128
  <option value="wind">Solo Vento (TWS/TWA/AWS/AWA)</option>
@@ -140,7 +141,7 @@
140
141
  <tr>
141
142
  <th width="30%">Path (Percorso)</th>
142
143
  <th width="20%">Valore Convertito</th>
143
- <th width="30%">Sorgente (Sensore)</th>
144
+ <th width="30%">Sorgente (Sensore - Dettagliato)</th>
144
145
  <th width="20%">Ultimo Ricevuto</th>
145
146
  </tr>
146
147
  </thead>
@@ -161,15 +162,16 @@
161
162
  </div>
162
163
  </div>
163
164
 
164
- <!-- Usiamo una textarea readonly per permettere la selezione e lo scroll perfetto di testi lunghi -->
165
165
  <textarea id="log-container" readonly></textarea>
166
166
  </div>
167
167
 
168
168
  </div>
169
169
 
170
170
  <script>
171
- // Percorsi da monitorare
172
171
  const pathsToWatch = [
172
+ "navigation.position",
173
+ "navigation.position.latitude",
174
+ "navigation.position.longitude",
173
175
  "navigation.speedThroughWater",
174
176
  "navigation.speedOverGround",
175
177
  "navigation.headingTrue",
@@ -182,13 +184,11 @@
182
184
  ];
183
185
 
184
186
  let socket = null;
185
- let logLinesArray = []; // Array per conservare le righe in memoria
186
- const MAX_LOG_LINES = 1000; // Aumentato a 1000 righe per analisi lunghe in Excel
187
+ let logLinesArray = [];
188
+ const MAX_LOG_LINES = 1000;
187
189
 
188
- // Memoria per rilevare i picchi anomali (Spikes)
189
190
  let lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
190
191
 
191
- // Funzioni di conversione matematica
192
192
  const radToDeg = (rad) => rad * (180 / Math.PI);
193
193
  const msToKts = (ms) => ms * 1.94384;
194
194
 
@@ -207,10 +207,33 @@
207
207
  tbody.appendChild(tr);
208
208
  });
209
209
 
210
- // Intestazione fissa del file CSV nel Log
211
210
  const csvHeader = "Timestamp,Path,Value,Unit,Source,IsSpike";
212
211
  document.getElementById('log-container').value = csvHeader + "\n";
213
212
 
213
+ // =========================================================
214
+ // ESTRAZIONE AVANZATA DELLA SORGENTE (GPS / NMEA)
215
+ // =========================================================
216
+ function extractSourceIdentifier(update) {
217
+ // 1. Cerca il campo standard moderno $source (es. "can0.GP" o "dbus.gps")
218
+ if (update.$source) {
219
+ return update.$source;
220
+ }
221
+
222
+ // 2. Se non presente, analizza l'oggetto o la stringa source tradizionale
223
+ if (update.source) {
224
+ if (typeof update.source === 'object') {
225
+ // Estrae label, nome talker o identificativo bus hardware se disponibili
226
+ return update.source.label ||
227
+ update.source.talker ||
228
+ update.source.src ||
229
+ (update.source.device ? `${update.source.device}:${update.source.pgn || ''}` : JSON.stringify(update.source));
230
+ }
231
+ return String(update.source);
232
+ }
233
+
234
+ return "Unknown-Source";
235
+ }
236
+
214
237
  // =========================================================
215
238
  // GESTIONE CONNESSIONE WEBSOCKET
216
239
  // =========================================================
@@ -233,7 +256,6 @@
233
256
  status.innerText = "CONNESSIONE IN CORSO...";
234
257
  status.className = "";
235
258
 
236
- // Resetting spike logic on new connection
237
259
  lastValues = { "navigation.speedOverGround": 0, "navigation.courseOverGroundTrue": 0 };
238
260
 
239
261
  socket = new WebSocket(`ws://${ip}/signalk/v1/stream?subscribe=self`);
@@ -245,23 +267,27 @@
245
267
  };
246
268
 
247
269
  socket.onmessage = (event) => {
248
- const data = JSON.parse(event.data);
249
-
250
- if (data.updates) {
251
- data.updates.forEach(update => {
252
- const sourceName = update.source ? update.source.label : "Unknown";
253
- // ISO String è lo standard perfetto per i file CSV ed Excel (es. 2023-10-25T17:00:56.123Z)
254
- const timeISO = new Date(update.timestamp).toISOString();
255
- const timeUI = new Date(update.timestamp).toLocaleTimeString() + '.' + new Date(update.timestamp).getMilliseconds().toString().padStart(3, '0');
256
-
257
- if (update.values) {
258
- update.values.forEach(v => {
259
- if (pathsToWatch.includes(v.path)) {
260
- processData(v.path, v.value, sourceName, timeISO, timeUI);
261
- }
262
- });
263
- }
264
- });
270
+ try {
271
+ const data = JSON.parse(event.data);
272
+
273
+ if (data.updates) {
274
+ data.updates.forEach(update => {
275
+ // Cattura l'identificativo sorgente con la nuova logica avanzata
276
+ const sourceName = extractSourceIdentifier(update);
277
+ const timeISO = new Date(update.timestamp).toISOString();
278
+ const timeUI = new Date(update.timestamp).toLocaleTimeString() + '.' + new Date(update.timestamp).getMilliseconds().toString().padStart(3, '0');
279
+
280
+ if (update.values) {
281
+ update.values.forEach(v => {
282
+ if (pathsToWatch.includes(v.path)) {
283
+ processData(v.path, v.value, sourceName, timeISO, timeUI);
284
+ }
285
+ });
286
+ }
287
+ });
288
+ }
289
+ } catch (err) {
290
+ console.error("Errore elaborazione messaggio: ", err);
265
291
  }
266
292
  };
267
293
 
@@ -283,17 +309,22 @@
283
309
  // ELABORAZIONE DATI E SPIKE DETECTION
284
310
  // =========================================================
285
311
  function processData(path, rawValue, source, timeISO, timeUI) {
286
- let formattedVal = rawValue;
312
+ let formattedVal = "---";
287
313
  let unit = "";
288
314
  let isSpike = false;
289
315
 
290
- // Conversioni
291
316
  if (rawValue !== null && rawValue !== undefined) {
292
- if (path.includes("speed")) {
317
+ if (path === "navigation.position") {
318
+ if (typeof rawValue === 'object' && rawValue.latitude !== undefined && rawValue.longitude !== undefined) {
319
+ formattedVal = `${rawValue.latitude.toFixed(6)};${rawValue.longitude.toFixed(6)}`;
320
+ unit = "lat;lon";
321
+ } else {
322
+ formattedVal = "Invalid Position";
323
+ }
324
+ } else if (path.includes("speed")) {
293
325
  formattedVal = msToKts(rawValue).toFixed(3);
294
326
  unit = "kts";
295
327
 
296
- // Rilevamento Spike SOG (> 5 nodi in un colpo solo)
297
328
  if (path === "navigation.speedOverGround") {
298
329
  let diff = Math.abs(formattedVal - lastValues[path]);
299
330
  if (diff > 5.0 && lastValues[path] > 0) isSpike = true;
@@ -304,7 +335,6 @@
304
335
  formattedVal = radToDeg(rawValue).toFixed(2);
305
336
  unit = "deg";
306
337
 
307
- // Rilevamento Spike COG (> 45 gradi improvviso)
308
338
  if (path === "navigation.courseOverGroundTrue") {
309
339
  let diff = Math.abs(formattedVal - lastValues[path]);
310
340
  if (diff > 180) diff = 360 - diff;
@@ -315,6 +345,12 @@
315
345
  } else if (path.includes("depth")) {
316
346
  formattedVal = rawValue.toFixed(2);
317
347
  unit = "m";
348
+ } else {
349
+ if (typeof rawValue === 'number') {
350
+ formattedVal = rawValue.toFixed(6);
351
+ } else {
352
+ formattedVal = String(rawValue);
353
+ }
318
354
  }
319
355
  }
320
356
 
@@ -330,31 +366,54 @@
330
366
 
331
367
  function updateTableRow(path, val, unit, raw, source, timeUI) {
332
368
  const safeId = path.replace(/\./g, '-');
333
- document.getElementById(`val-${safeId}`).innerHTML = `<span class="value">${val}</span><span class="unit">${unit}</span><span class="raw">Raw: ${raw.toFixed(4)}</span>`;
334
- document.getElementById(`src-${safeId}`).innerHTML = `<span class="source">${source}</span>`;
335
- document.getElementById(`time-${safeId}`).innerHTML = `<span class="time">${timeUI}</span>`;
369
+
370
+ let rawStr = "---";
371
+ if (raw !== null && raw !== undefined) {
372
+ if (typeof raw === 'number') {
373
+ rawStr = raw.toFixed(4);
374
+ } else if (typeof raw === 'object') {
375
+ rawStr = JSON.stringify(raw);
376
+ } else {
377
+ rawStr = String(raw);
378
+ }
379
+ }
380
+
381
+ const valCell = document.getElementById(`val-${safeId}`);
382
+ if (valCell) {
383
+ valCell.innerHTML = `<span class="value">${val}</span><span class="unit">${unit}</span><span class="raw">Raw: ${rawStr}</span>`;
384
+ }
385
+
386
+ const srcCell = document.getElementById(`src-${safeId}`);
387
+ if (srcCell) {
388
+ srcCell.innerHTML = `<span class="source">${source}</span>`;
389
+ }
390
+
391
+ const timeCell = document.getElementById(`time-${safeId}`);
392
+ if (timeCell) {
393
+ timeCell.innerHTML = `<span class="time">${timeUI}</span>`;
394
+ }
336
395
 
337
396
  const row = document.getElementById(`row-${safeId}`);
338
- row.style.backgroundColor = "#334a33";
339
- setTimeout(() => row.style.backgroundColor = "", 150);
397
+ if (row) {
398
+ row.style.backgroundColor = "#334a33";
399
+ setTimeout(() => row.style.backgroundColor = "", 150);
400
+ }
340
401
  }
341
402
 
342
403
  // =========================================================
343
404
  // GESTIONE DEL LOG CSV E COPIA APPUNTI
344
405
  // =========================================================
345
406
  function addLogEntry(timeISO, path, value, unit, source, isSpike) {
346
- // Formattazione stringa CSV pulita (niente spazi inutili)
347
- // Se c'è una virgola nel nome della sorgente, la mettiamo tra virgolette per non rompere il CSV
348
- const safeSource = source.includes(',') ? `"${source}"` : source;
407
+ const safeValue = (value.includes(',') || value.includes(';')) ? `"${value}"` : value;
408
+ const safeSource = (source.includes(',') || source.includes(';')) ? `"${source}"` : source;
349
409
  const spikeFlag = isSpike ? "TRUE" : "FALSE";
350
410
 
351
- const csvLine = `${timeISO},${path},${value},${unit},${safeSource},${spikeFlag}`;
411
+ const csvLine = `${timeISO},${path},${safeValue},${unit},${safeSource},${spikeFlag}`;
352
412
 
353
413
  logLinesArray.push(csvLine);
354
414
 
355
- // Mantieni la memoria leggera
356
415
  if (logLinesArray.length > MAX_LOG_LINES) {
357
- logLinesArray.shift(); // Rimuove la più vecchia
416
+ logLinesArray.shift();
358
417
  }
359
418
 
360
419
  renderLogTextArea();
@@ -362,11 +421,7 @@
362
421
 
363
422
  function renderLogTextArea() {
364
423
  const textArea = document.getElementById('log-container');
365
-
366
- // Ricostruisci il testo unendo l'intestazione e l'array con \n (a capo)
367
424
  textArea.value = csvHeader + "\n" + logLinesArray.join("\n");
368
-
369
- // Auto-scroll verso il basso
370
425
  textArea.scrollTop = textArea.scrollHeight;
371
426
  }
372
427
 
@@ -377,14 +432,10 @@
377
432
 
378
433
  function copyLogCSV() {
379
434
  const textArea = document.getElementById('log-container');
380
- const notif = document.getElementById('copy-notification');
381
-
382
- // Seleziona il testo
383
435
  textArea.select();
384
- textArea.setSelectionRange(0, 999999); // Per mobile
436
+ textArea.setSelectionRange(0, 999999);
385
437
 
386
438
  try {
387
- // Copia negli appunti (API moderna Navigator o execCommand classico)
388
439
  if (navigator.clipboard) {
389
440
  navigator.clipboard.writeText(textArea.value).then(() => showCopyNotif());
390
441
  } else {
@@ -401,11 +452,8 @@
401
452
  const notif = document.getElementById('copy-notification');
402
453
  notif.style.opacity = 1;
403
454
  setTimeout(() => notif.style.opacity = 0, 2000);
404
-
405
- // Deseleziona il testo
406
455
  window.getSelection().removeAllRanges();
407
456
  }
408
-
409
457
  </script>
410
458
  </body>
411
459
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "5.0.5",
3
+ "version": "5.0.7",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {