@sailingrotevista/rotevista-dash 2.0.20 → 2.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.js CHANGED
@@ -3,7 +3,14 @@
3
3
  // ==========================================================================
4
4
  let CONFIG = {
5
5
  alarms: { depthDanger: 2.5, depthWarning: 5.0 },
6
- averages: { smoothWindow: 2000, longWindow: 60000, stabilityTolerance: 2000, stabilityThreshold: 0.90, minSpeed: 0.5 },
6
+ // Ottimizzato per mare formato: 30s di media e soglia stabilità 85%
7
+ averages: {
8
+ smoothWindow: 2000,
9
+ longWindow: 30000,
10
+ stabilityTolerance: 2000,
11
+ stabilityThreshold: 0.85,
12
+ minSpeed: 0.5
13
+ },
7
14
  graphs: { reef1: 15.0, reef2: 20.0, historyMinutes: 5, samples: 60 },
8
15
  scales: {
9
16
  stw: { stdMax: 12, hercSpan: 4, step: 2 },
@@ -28,7 +35,7 @@ let lastAvgUIUpdate = 0, audioCtx = null, lastAlarmTime = 0;
28
35
  let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
29
36
  let curBoatCompassRot = 0, curWindCompassRot = 0;
30
37
 
31
- let smoothedLeeway = 0, rotationTrend = 0;
38
+ let smoothedLeeway = 0, rotationTrend = 0, meteoTrend = 0;
32
39
  let lastShortAvgVal = null, lastInstantTwa = null;
33
40
  let lastTrendTime = Date.now(), lastGybeAlarmTime = 0, lastTWCompute = 0;
34
41
  let twDirty = false, isNavigating = false, reconnectDelay = 1000;
@@ -47,6 +54,8 @@ const store = {
47
54
  smoothBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
48
55
  longBuf: { hdg: [], cog: [], awa: [], twa: [], twd: [] },
49
56
  histories: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
57
+ // Buffer temporaneo per calcolare la media dell'intervallo nei grafici
58
+ graphTempBuf: { stw: [], sog: [], depth: [], tws: [], vmg: [] },
50
59
  lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0 }
51
60
  };
52
61
 
@@ -168,9 +177,14 @@ function processIncomingData(path, val) {
168
177
  // ==========================================================================
169
178
  // 5. TREND VENTO E SICUREZZA
170
179
  // ==========================================================================
180
+ /**
181
+ * Analizza i trend di rotazione del vento su due scale temporali:
182
+ * 1. Tattica (veloce): per la regolazione delle vele (sulla lancetta TWA)
183
+ * 2. Strategica (lenta): per le previsioni meteo a lungo termine (bussola TWD)
184
+ */
171
185
  function updateWindTrend() {
172
186
  const now = Date.now();
173
- const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true);
187
+ const twaAvgObj = getCircularAverageFromBuffer(store.longBuf.twa, 30000, true);
174
188
  const shortAvg = getCircularAverageFromBuffer(store.longBuf.twd, 5000, false);
175
189
  const instantTwaRad = store.raw["environment.wind.angleTrueWater"];
176
190
 
@@ -180,6 +194,7 @@ function updateWindTrend() {
180
194
  if (lastShortAvgVal === null) { lastShortAvgVal = shortAvgDeg; lastInstantTwa = instantTwaDeg; return; }
181
195
  const dt = (now - lastTrendTime) / 1000; lastTrendTime = now;
182
196
 
197
+ // ALLARME STRAMBATA
183
198
  const gybeDetected = (Math.abs(instantTwaDeg) > 155 && Math.sign(instantTwaDeg) !== Math.sign(lastInstantTwa));
184
199
  lastInstantTwa = instantTwaDeg;
185
200
 
@@ -192,32 +207,47 @@ function updateWindTrend() {
192
207
  return;
193
208
  }
194
209
 
210
+ // CALCOLO TREND
195
211
  let diff = (shortAvgDeg - lastShortAvgVal + 540) % 360 - 180; lastShortAvgVal = shortAvgDeg;
196
212
  if (dt > 0) {
197
213
  let rate = Math.max(-10, Math.min(10, diff / dt));
198
- const alpha = Math.min(1, dt / 5);
199
- rotationTrend = rotationTrend * (1 - alpha) + rate * alpha;
214
+ // Tattico (veloce ~15s)
215
+ const alphaT = Math.min(1, dt / 15);
216
+ rotationTrend = rotationTrend * (1 - alphaT) + rate * alphaT;
217
+ // Strategico (molto lento ~8-10min)
218
+ const alphaM = Math.min(1, dt / 500);
219
+ meteoTrend = meteoTrend * (1 - alphaM) + rate * alphaM;
200
220
  }
201
221
 
202
- if (Math.abs(rotationTrend) > 1.0) {
203
- const pos = store.raw["navigation.position"], isSouth = pos && pos.latitude < 0;
204
- let meteoColor = (!isSouth) ? (rotationTrend < 0 ? "#00ff00" : "#ff0000") : (rotationTrend > 0 ? "#00ff00" : "#ff0000");
205
- let isLift = (twaAvgDeg > 0) ? (rotationTrend > 0) : (rotationTrend < 0);
206
- const absTwa = Math.abs(twaAvgDeg);
207
- if (absTwa >= 90 && absTwa < 160) isLift = !isLift; else if (absTwa >= 160) isLift = false;
208
- const tacticColor = isLift ? "#00ff00" : "#ff0000";
222
+ // VISUALIZZAZIONE METEO (Bussola Centrale)
223
+ if (Math.abs(meteoTrend) > 0.2) {
224
+ const isSouth = store.raw["navigation.position"]?.latitude < 0;
225
+ let meteoColor = (!isSouth) ? (meteoTrend < 0 ? "#27ae60" : "#c0392b") : (meteoTrend > 0 ? "#27ae60" : "#c0392b");
226
+ if (meteoTrend > 0) {
227
+ if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
228
+ if (compassDots.ccw) { compassDots.ccw.classList.remove('is-trending'); compassDots.ccw.setAttribute('fill', '#bbb'); }
229
+ } else {
230
+ if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
231
+ if (compassDots.cw) { compassDots.cw.classList.remove('is-trending'); compassDots.cw.setAttribute('fill', '#bbb'); }
232
+ }
233
+ } else {
234
+ [compassDots.cw, compassDots.ccw].forEach(el => { if (el) { el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
235
+ }
209
236
 
237
+ // VISUALIZZAZIONE TATTICA (Lancetta Vento)
238
+ if (Math.abs(rotationTrend) > 3.0) {
239
+ let isLift = (twaAvgDeg > 0) ? (rotationTrend > 0) : (rotationTrend < 0);
240
+ if (Math.abs(twaAvgDeg) >= 90) isLift = !isLift;
241
+ const tacticColor = isLift ? "#27ae60" : "#c0392b";
210
242
  if (rotationTrend > 0) {
211
- if (compassDots.cw) { compassDots.cw.classList.add('is-trending'); compassDots.cw.setAttribute('fill', meteoColor); }
212
243
  if (gaugeDots.cw) { gaugeDots.cw.classList.add('is-trending'); gaugeDots.cw.setAttribute('fill', tacticColor); }
213
- [compassDots.ccw, gaugeDots.ccw].forEach(el => { if(el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
244
+ if (gaugeDots.ccw) { gaugeDots.ccw.classList.remove('is-trending'); gaugeDots.ccw.setAttribute('fill', '#bbb'); }
214
245
  } else {
215
- if (compassDots.ccw) { compassDots.ccw.classList.add('is-trending'); compassDots.ccw.setAttribute('fill', meteoColor); }
216
246
  if (gaugeDots.ccw) { gaugeDots.ccw.classList.add('is-trending'); gaugeDots.ccw.setAttribute('fill', tacticColor); }
217
- [compassDots.cw, gaugeDots.cw].forEach(el => { if(el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
247
+ if (gaugeDots.cw) { gaugeDots.cw.classList.remove('is-trending'); gaugeDots.cw.setAttribute('fill', '#bbb'); }
218
248
  }
219
249
  } else {
220
- [compassDots.cw, compassDots.ccw, gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.remove('is-trending', 'is-gybing'); el.setAttribute('fill', '#ffffff'); }});
250
+ [gaugeDots.cw, gaugeDots.ccw].forEach(el => { if (el) { el.classList.remove('is-trending'); el.setAttribute('fill', '#bbb'); }});
221
251
  }
222
252
  }
223
253
 
@@ -249,11 +279,16 @@ function startDisplayLoop() {
249
279
  if (store.raw["navigation.speedOverGround"] !== undefined) {
250
280
  const twaRad = store.raw["environment.wind.angleTrueWater"], vmgKts = (twaRad !== undefined) ? Math.abs(stwKts * Math.cos(twaRad)) : 0;
251
281
  manageHistory('vmg', vmgKts); manageHistory('sog', sogKts);
252
- if (displayModeSog === 'VMG') { ui.sog.innerText = vmgKts.toFixed(1); ui.sog.style.color = "#64ffda"; document.getElementById('sog-vmg-label').textContent = 'VMG'; }
253
- else { ui.sog.innerText = sogKts.toFixed(1); ui.sog.style.color = (sogKts-stwKts > 0.3) ? "#2ecc71" : (sogKts-stwKts < -0.3 ? "#e74c3c" : "#fff"); document.getElementById('sog-vmg-label').textContent = 'SOG'; }
282
+ if (displayModeSog === 'VMG') {
283
+ ui.sog.innerText = vmgKts.toFixed(1); ui.sog.style.color = "#16a085"; document.getElementById('sog-vmg-label').textContent = 'VMG';
284
+ } else {
285
+ ui.sog.innerText = sogKts.toFixed(1);
286
+ ui.sog.style.color = (sogKts - stwKts > 0.3) ? "#27ae60" : (sogKts - stwKts < -0.3 ? "#c0392b" : "#000");
287
+ document.getElementById('sog-vmg-label').textContent = 'SOG';
288
+ }
254
289
  }
255
290
  if (store.raw["environment.depth.belowTransducer"] !== undefined) { const d = store.raw["environment.depth.belowTransducer"]; ui.depth.innerText = d.toFixed(1); checkDepthAlarm(d); manageHistory('depth', d); }
256
- if (store.raw["environment.wind.speedTrue"] !== undefined) { ui.tws.innerText = msToKts(store.raw["environment.wind.speedTrue"]).toFixed(1); ui.tws.style.color = (msToKts(store.raw["environment.wind.speedTrue"]) >= CONFIG.graphs.reef2) ? "#e74c3c" : (msToKts(store.raw["environment.wind.speedTrue"]) >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff"); manageHistory('tws', msToKts(store.raw["environment.wind.speedTrue"])); }
291
+ if (store.raw["environment.wind.speedTrue"] !== undefined) { ui.tws.innerText = msToKts(store.raw["environment.wind.speedTrue"]).toFixed(1); ui.tws.style.color = (msToKts(store.raw["environment.wind.speedTrue"]) >= CONFIG.graphs.reef2) ? "#e74c3c" : (msToKts(store.raw["environment.wind.speedTrue"]) >= CONFIG.graphs.reef1 ? "#e67e22" : "#000"); manageHistory('tws', msToKts(store.raw["environment.wind.speedTrue"])); }
257
292
  if (store.raw["environment.wind.speedApparent"] !== undefined) ui.awsSvg.textContent = msToKts(store.raw["environment.wind.speedApparent"]).toFixed(1);
258
293
 
259
294
  const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
@@ -265,20 +300,18 @@ function startDisplayLoop() {
265
300
  let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (Math.PI * 2) - Math.PI);
266
301
  if (sogKts < CONFIG.averages.minSpeed) smoothedLeeway = 0; else smoothedLeeway = (smoothedLeeway * 0.9) + (driftDeg * 0.1);
267
302
  curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway); ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
268
- ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#f39c12" : "#fff";
303
+ ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#e67e22" : "#000";
269
304
  updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
270
305
  }
271
306
 
272
307
  updateWindTrend();
273
-
274
- // HEAVY TIER (2s)
275
308
  if (tick % 2 === 0) { refreshGraph('stw'); refreshGraph(displayModeSog === 'VMG' ? 'vmg' : 'sog'); refreshGraph('depth'); refreshGraph('tws'); }
276
309
 
277
310
  // SLOW TIER (3s)
278
311
  if (tick % 3 === 0) {
279
- let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, 60000, false), cObj = getCircularAverageFromBuffer(store.longBuf.cog, 60000, false),
280
- awObj = getCircularAverageFromBuffer(store.longBuf.awa, 60000, true), twObj = getCircularAverageFromBuffer(store.longBuf.twa, 60000, true),
281
- twdObj = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false);
312
+ let hObj = getCircularAverageFromBuffer(store.longBuf.hdg, 30000, false), cObj = getCircularAverageFromBuffer(store.longBuf.cog, 30000, false),
313
+ awObj = getCircularAverageFromBuffer(store.longBuf.awa, 30000, true), twObj = getCircularAverageFromBuffer(store.longBuf.twa, 30000, true),
314
+ twdObj = getCircularAverageFromBuffer(store.longBuf.twd, 30000, false);
282
315
 
283
316
  const upUI = (el, obj, isCompass = false) => {
284
317
  if (!obj || obj.val === null) { el.innerHTML = "---&deg;"; el.classList.remove('unstable-data'); }
@@ -290,10 +323,10 @@ function startDisplayLoop() {
290
323
  upUI(ui.hdg, hObj, true); upUI(ui.cog, cObj, true); upUI(ui.awaAvg, awObj, false); upUI(ui.twaAvg, twObj, false); upUI(ui.twdAvg, twdObj, true);
291
324
 
292
325
  if (hObj && twObj) {
293
- const tackHdgDeg = radToDeg((hObj.val - twObj.val * 2 + Math.PI * 2) % (Math.PI * 2));
326
+ const tackHdgDeg = radToDeg((hObj.val + twObj.val * 2 + Math.PI * 2) % (Math.PI * 2));
294
327
  ui.tackHdg.innerHTML = `${Math.round((tackHdgDeg + 360) % 360).toString().padStart(3, '0')}&deg;`;
295
328
  if (cObj) {
296
- const tackCogDeg = radToDeg((cObj.val - twObj.val * 2 + Math.PI * 2) % (Math.PI * 2));
329
+ const tackCogDeg = radToDeg((cObj.val + twObj.val * 2 + Math.PI * 2) % (Math.PI * 2));
297
330
  ui.tackCog.innerHTML = `${Math.round((tackCogDeg + 360) % 360).toString().padStart(3, '0')}&deg;`;
298
331
  }
299
332
  }
@@ -335,8 +368,19 @@ async function fetchServerConfig() {
335
368
  }
336
369
 
337
370
  function manageHistory(t, v) {
338
- const n = Date.now(); const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
339
- if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) { store.histories[t].push(v); if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift(); store.lastUpdates[t] = n; }
371
+ const n = Date.now();
372
+ const interval = simulationMode ? SIM_SAMPLE_INTERVAL : (CONFIG.graphs.historyMinutes * 60000) / CONFIG.graphs.samples;
373
+ if (!store.graphTempBuf[t]) store.graphTempBuf[t] = [];
374
+ store.graphTempBuf[t].push(v);
375
+
376
+ if (n - store.lastUpdates[t] > interval || store.histories[t].length === 0) {
377
+ const sum = store.graphTempBuf[t].reduce((a, b) => a + b, 0);
378
+ const avg = sum / store.graphTempBuf[t].length;
379
+ store.histories[t].push(avg);
380
+ if (store.histories[t].length > CONFIG.graphs.samples) store.histories[t].shift();
381
+ store.graphTempBuf[t] = [];
382
+ store.lastUpdates[t] = n;
383
+ }
340
384
  }
341
385
 
342
386
  function calculateScale(type, data, mode) {
@@ -350,20 +394,24 @@ function updateScaleLabels(t, min, max) { const el = document.getElementById(t +
350
394
  function drawGraph(d, id, min, max, isTws, isHercules) {
351
395
  const svg = document.getElementById(id); if (!svg || d.length < 2) return;
352
396
  const w = 200, h = 40, range = max - min || 1;
353
- let grids = ""; [0.25, 0.5, 0.75].forEach(p => { grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(255,255,255,0.08)" stroke-width="0.5" />`; });
354
- for (let m = 1; m < CONFIG.graphs.historyMinutes; m++) { const x = w - (m / CONFIG.graphs.historyMinutes) * w; grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(255,255,255,0.05)" stroke-width="0.5" />`; }
397
+ let grids = "";
398
+ [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" />`; });
399
+ for (let m = 1; m < CONFIG.graphs.historyMinutes; m++) {
400
+ const x = w - (m / CONFIG.graphs.historyMinutes) * w;
401
+ grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.5" />`;
402
+ }
355
403
  let pD = ""; let cS = "";
356
404
  d.forEach((v, i) => {
357
405
  const x = (i/(CONFIG.graphs.samples-1))*w, y = h-(Math.max(0,Math.min(1,(v-min)/range))*h); pD += `${i===0?'M':'L'} ${x} ${y} `;
358
406
  if (isTws && i > 0) {
359
407
  const px = ((i-1)/(CONFIG.graphs.samples-1))*w, py = h-(Math.max(0,Math.min(1,(d[i-1]-min)/range))*h);
360
- let c = (v >= CONFIG.graphs.reef2) ? "#e74c3c" : (v >= CONFIG.graphs.reef1 ? "#e67e22" : "#fff");
408
+ let c = (v >= CONFIG.graphs.reef2) ? "#e74c3c" : (v >= CONFIG.graphs.reef1 ? "#e67e22" : "#000");
361
409
  cS += `<line x1="${px}" y1="${py}" x2="${x}" y2="${y}" stroke="${c}" class="${isHercules?'line-hercules':''}" />`;
362
410
  }
363
411
  });
364
- const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#ffffff' };
365
- const colorKey = id === 'sog-graph' && displayModeSog === 'VMG' ? '#64ffda' : clrs[id];
366
- svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(255,255,255,0.08)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${colorKey}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${colorKey}" />`;
412
+ const clrs = { 'stw-graph': '#2ecc71', 'sog-graph': '#f39c12', 'depth-graph': '#3498db', 'tws-graph': '#000' };
413
+ const colorKey = id === 'sog-graph' && displayModeSog === 'VMG' ? '#16a085' : clrs[id];
414
+ svg.innerHTML = isTws ? `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="rgba(0,0,0,0.05)" stroke="none" />${cS}` : `${grids}<path d="${pD} L ${((d.length-1)/(CONFIG.graphs.samples-1))*w} ${h} L 0 ${h} Z" fill="${colorKey}22" stroke="none" /><path d="${pD}" class="${isHercules?'line-hercules':''}" fill="none" stroke="${colorKey}" />`;
367
415
  }
368
416
 
369
417
  // ==========================================================================
@@ -384,7 +432,7 @@ function toggleFocusMode(type, element) {
384
432
  clearTimeout(pressTimer); if (isLongPressActive) return;
385
433
  const currentTime = new Date().getTime(), tapDelay = currentTime - lastTapTime;
386
434
  if (tapDelay < 300 && tapDelay > 0) { clearTimeout(tapTimeout); if (!isFocusActive) { graphModes[type] = graphModes[type] === 'standard' ? 'hercules' : 'standard'; localStorage.setItem('mode_' + type, graphModes[type]); } lastTapTime = 0; }
387
- else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(100, 255, 218, 0.1)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
435
+ else { lastTapTime = currentTime; tapTimeout = setTimeout(() => { if (isFocusActive && el.classList.contains('is-focused')) toggleFocusMode(type, el); else if (!isFocusActive && type === 'sog') { displayModeSog = (displayModeSog === 'SOG') ? 'VMG' : 'SOG'; el.style.backgroundColor = "rgba(0, 0, 0, 0.05)"; setTimeout(() => el.style.backgroundColor = "", 150); } }, 250); }
388
436
  });
389
437
  el.addEventListener('pointerleave', () => clearTimeout(pressTimer));
390
438
  });
@@ -414,24 +462,19 @@ function startDynamicSimulation() {
414
462
  let sim = { hdg: 45, tws: 12, twd: 90, depth: 12, stw: 5, leeway: 0, currentSpeed: 1.5, currentDir: 90, startTime: Date.now() };
415
463
  simInterval = setInterval(() => {
416
464
  const elapsed = (Date.now() - sim.startTime) / 1000;
417
- if (elapsed > 120 && elapsed < 121) sim.twd = Math.random() * 360;
418
465
  sim.twd = (sim.twd + (Math.sin(elapsed / 20) * 0.5) + 360) % 360;
419
- const inGust = (elapsed % 40) < 5; const targetTws = (inGust ? 18 : 10) + Math.sin(elapsed / 10) * 2;
466
+ const targetTws = 10 + Math.sin(elapsed / 10) * 2;
420
467
  sim.tws += (targetTws - sim.tws) * 0.05; sim.hdg = (sim.hdg + (Math.random() - 0.5) * 1 + 360) % 360;
421
468
  let twaRel = (sim.twd - sim.hdg + 360) % 360; if (twaRel > 180) twaRel -= 360;
422
- let targetStw = 3 + (4 * Math.sin((Math.abs(twaRel) - 45) * Math.PI / 125)); sim.stw += (Math.max(3, Math.min(8, targetStw)) - sim.stw) * 0.05;
423
- const rawLeeway = Math.sin(degToRad(twaRel)) * 4; sim.leeway += (rawLeeway - sim.leeway) * 0.05;
424
-
425
- const bX = sim.stw * Math.sin(degToRad(sim.hdg + sim.leeway)), bY = sim.stw * Math.cos(degToRad(sim.hdg + sim.leeway));
426
- const cX = sim.currentSpeed * Math.sin(degToRad(sim.currentDir)), cY = sim.currentSpeed * Math.cos(degToRad(sim.currentDir));
427
- const sog = Math.sqrt(Math.pow(bX + cX, 2) + Math.pow(bY + cY, 2)), cog = (radToDeg(Math.atan2(bX + cX, bY + cY)) + 360) % 360;
428
-
469
+ let targetStw = 5; sim.stw += (targetStw - sim.stw) * 0.05;
470
+ const bX = sim.stw * Math.sin(degToRad(sim.hdg)), bY = sim.stw * Math.cos(degToRad(sim.hdg));
471
+ const sog = sim.stw, cog = sim.hdg;
429
472
  const twaRad = degToRad(twaRel), aws = Math.sqrt(Math.pow(sim.stw, 2) + Math.pow(sim.tws, 2) + 2 * sim.stw * sim.tws * Math.cos(twaRad));
430
473
  const awa = Math.atan2(sim.tws * Math.sin(twaRad), sim.stw + sim.tws * Math.cos(twaRad));
431
474
  processIncomingData("environment.wind.speedApparent", ktsToMs(aws)); processIncomingData("environment.wind.angleApparent", awa);
432
475
  processIncomingData("environment.depth.belowTransducer", sim.depth); processIncomingData("navigation.headingTrue", degToRad(sim.hdg));
433
476
  processIncomingData("navigation.speedThroughWater", ktsToMs(sim.stw)); processIncomingData("navigation.speedOverGround", ktsToMs(sog));
434
- processIncomingData("navigation.courseOverGroundTrue", degToRad(cog)); processIncomingData("navigation.position", { latitude: 45.0, longitude: 12.0 });
477
+ processIncomingData("navigation.courseOverGroundTrue", degToRad(cog));
435
478
  }, 1000);
436
479
  }
437
480
 
@@ -439,6 +482,6 @@ function startDynamicSimulation() {
439
482
  // 10. INIT
440
483
  // ==========================================================================
441
484
  window.addEventListener('contextmenu', e => e.preventDefault(), true);
442
- (function genTicks() { const c = document.getElementById('ticks'); if (c) { for (let i = 0; i < 360; i += 10) { const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50)); l.setAttribute("stroke", m ? "#fff" : "#666"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
485
+ (function genTicks() { const c = document.getElementById('ticks'); if (c) { for (let i = 0; i < 360; i += 10) { const l = document.createElementNS("http://www.w3.org/2000/svg", "line"); const m = i % 30 === 0; l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50)); l.setAttribute("stroke", m ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1"); l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l); } } })();
443
486
  async function init() { await fetchServerConfig(); startDisplayLoop(); connect(); }
444
487
  window.addEventListener('load', init);
package/index.html CHANGED
@@ -122,23 +122,24 @@
122
122
 
123
123
  <g id="ticks"></g>
124
124
 
125
- <g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="hanging" font-family="Arial" font-weight="bold">
126
- <!-- Etichette Principali -->
127
- <text font-size="16" transform="translate(200, 65)">0</text>
128
- <text font-size="16" transform="translate(335, 200) rotate(90)">90</text>
129
- <text font-size="16" transform="translate(65, 200) rotate(-90)">90</text>
130
- <text font-size="16" transform="translate(200, 335) rotate(180)">180</text>
125
+ <!-- Etichette riposizionate con raggio ridotto per un contatto perfetto con la ghiera -->
126
+ <g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
127
+ <!-- Etichette Principali (Distanza 126px) -->
128
+ <text font-size="16" transform="translate(200, 74)">0</text>
129
+ <text font-size="16" transform="translate(326, 200) rotate(90)">90</text>
130
+ <text font-size="16" transform="translate(74, 200) rotate(-90)">90</text>
131
+ <text font-size="16" transform="translate(200, 326) rotate(180)">180</text>
131
132
 
132
- <!-- Etichette Intermedie (30-150 Gradi) -->
133
- <text font-size="11" transform="translate(267.5, 83) rotate(30)">30</text>
134
- <text font-size="11" transform="translate(317, 132.5) rotate(60)">60</text>
135
- <text font-size="11" transform="translate(317, 267.5) rotate(120)">120</text>
136
- <text font-size="11" transform="translate(267.5, 317) rotate(150)">150</text>
133
+ <!-- Etichette Intermedie (Distanza 125px) -->
134
+ <text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
135
+ <text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
136
+ <text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
137
+ <text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
137
138
 
138
- <text font-size="11" transform="translate(132.5, 83) rotate(-30)">30</text>
139
- <text font-size="11" transform="translate(83, 132.5) rotate(-60)">60</text>
140
- <text font-size="11" transform="translate(83, 267.5) rotate(-120)">120</text>
141
- <text font-size="11" transform="translate(132.5, 317) rotate(-150)">150</text>
139
+ <text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
140
+ <text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
141
+ <text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
142
+ <text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
142
143
  </g>
143
144
 
144
145
  <g id="track-pointer" transform="rotate(0, 200, 200)">
package/index.js CHANGED
@@ -63,8 +63,8 @@ module.exports = function (app) {
63
63
  longWindow: {
64
64
  type: 'number',
65
65
  title: 'Long Average Window (ms)',
66
- description: "Time buffer used for 'MEAN' values (e.g., 60000ms = 1 min). Higher values provide more stability but slower reaction.",
67
- default: 60000
66
+ description: "Time buffer for 'MEAN' values. Larger windows produce smoother numbers but increase the 'Unstable' (orange) alerts during maneuvers or in gusty conditions, as data coherence decreases over time.",
67
+ default: 30000
68
68
  },
69
69
  smoothWindow: {
70
70
  type: 'number',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "2.0.20",
3
+ "version": "2.0.22",
4
4
  "description": "Public Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
package/settings.json CHANGED
@@ -8,11 +8,11 @@
8
8
  "red": 20.0
9
9
  },
10
10
  "averages": {
11
- "long_window": 60000,
11
+ "long_window": 3000,
12
12
  "smooth_window": 2000
13
13
  },
14
14
  "hercules_ranges": {
15
15
  "speed": 4.0,
16
16
  "wind": 10.0
17
17
  }
18
- }
18
+ }
package/style.css CHANGED
@@ -1,16 +1,15 @@
1
1
  /* ==========================================================================
2
- 1. BASE E RESET DI SISTEMA
2
+ 1. BASE E RESET DI SISTEMA (GIORNO: SFONDO CHIARO)
3
3
  ========================================================================== */
4
4
  body {
5
- background-color: #000;
6
- color: #fff;
5
+ background-color: #fff;
6
+ color: #000;
7
7
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
8
8
  margin: 0;
9
9
  padding: 0;
10
10
  height: 100vh;
11
11
  width: 100vw;
12
12
  overflow: hidden;
13
- /* Inibisce i gesti di sistema per favorire Long Press e Swipe della dashboard */
14
13
  -webkit-touch-callout: none;
15
14
  -webkit-user-select: none;
16
15
  user-select: none;
@@ -27,7 +26,6 @@ body {
27
26
  padding: 5px;
28
27
  box-sizing: border-box;
29
28
  gap: 8px;
30
- /* Rapporto Standard: Lati flessibili, Centro bilanciato a 1.5fr */
31
29
  grid-template-columns: minmax(180px, 1fr) minmax(auto, 1.5fr) minmax(180px, 1fr);
32
30
  grid-template-rows: 100%;
33
31
  transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
@@ -37,18 +35,18 @@ body {
37
35
  .side-panel {
38
36
  display: flex;
39
37
  flex-direction: column;
40
- background: rgba(255, 255, 255, 0.03);
38
+ background: rgba(0, 0, 0, 0.03);
41
39
  border-radius: 12px;
42
40
  height: 100%;
43
41
  overflow: hidden;
44
- gap: 2px; /* Massimizza lo spazio per i 5 box verticali */
42
+ gap: 2px;
45
43
  }
46
44
 
47
45
  .left-panel { grid-column: 1; }
48
46
 
49
47
  .center-panel {
50
48
  grid-column: 2;
51
- position: relative; /* Indispensabile per posizionare l'etichetta STATUS in alto a dx */
49
+ position: relative;
52
50
  display: flex;
53
51
  flex-direction: column;
54
52
  align-items: center;
@@ -64,34 +62,32 @@ body {
64
62
  }
65
63
 
66
64
  /* ==========================================================================
67
- 3. DATA-BOX E TIPOGRAFIA (UNITA' ELASTICHE CQH/CLAMP)
65
+ 3. DATA-BOX E TIPOGRAFIA
68
66
  ========================================================================== */
69
67
  .data-box {
70
68
  position: relative;
71
- border-bottom: 1px solid #222;
69
+ border-bottom: 1px solid #eee;
72
70
  padding: 2px 4px;
73
71
  display: flex;
74
72
  flex-direction: column;
75
73
  width: 100%;
76
74
  box-sizing: border-box;
77
- container-type: size; /* Permette l'uso di cqh per i font */
78
- flex: 1 1 0px; /* Distribuzione equa dello spazio verticale disponibile */
75
+ container-type: size;
76
+ flex: 1 1 0px;
79
77
  min-height: 0;
80
78
  overflow: hidden;
81
79
  transition: background-color 0.2s ease;
82
80
  }
83
81
 
84
- /* Allineamento speculare dei testi tra colonna SX e DX */
85
82
  .left-panel .data-box { align-items: flex-start; text-align: left; }
86
83
  .right-panel .data-box { align-items: flex-end; text-align: right; }
87
84
 
88
85
  .label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 2px; width: 100%; }
89
- .label { color: #666; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
90
- .unit { color: #888; font-size: 0.6rem; font-weight: bold; }
86
+ .label { color: #888; font-size: 0.65rem; font-weight: bold; text-transform: uppercase; }
87
+ .unit { color: #aaa; font-size: 0.6rem; font-weight: bold; }
91
88
 
92
- /* Font dinamico per i valori: massimo 22% dell'altezza del box */
93
89
  .value {
94
- color: #fff;
90
+ color: #000;
95
91
  font-size: clamp(1.2rem, 22cqh, 3rem);
96
92
  font-weight: 600;
97
93
  line-height: 0.9;
@@ -99,7 +95,6 @@ body {
99
95
  padding-bottom: 5px;
100
96
  }
101
97
 
102
- /* Centratura verticale automatica per i box senza grafico */
103
98
  .value-large, .dual-value-container, .value-with-compass {
104
99
  margin-top: auto;
105
100
  margin-bottom: auto;
@@ -107,42 +102,36 @@ body {
107
102
 
108
103
  .value-large { font-size: clamp(1.5rem, 35cqh, 4.5rem); line-height: 0.85; }
109
104
 
110
- /* --- 5. BUSSOLA TWD (FIX ALLINEAMENTO) --- */
111
105
  .value-with-compass {
112
106
  display: flex;
113
- justify-content: space-between; /* Bussola a SX, Valore a DX */
107
+ justify-content: space-between;
114
108
  align-items: center;
115
109
  width: 100%;
116
- align-self: stretch; /* Impedisce al pannello DX di schiacciare il widget a destra */
110
+ align-self: stretch;
117
111
  gap: 5px;
118
112
  }
119
113
 
120
- /* TACK (Mure opposte) Layout */
121
114
  .dual-value-container { display: flex; justify-content: space-between; width: 100%; }
122
115
  .dual-value-col { display: flex; flex-direction: column; width: 48%; }
123
- .dual-label { color: #666; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
116
+ .dual-label { color: #888; font-size: 0.55rem; font-weight: bold; text-transform: uppercase; margin-bottom: 2px; }
124
117
  .value.dual-val { font-size: clamp(1rem, 22cqh, 2rem); padding-bottom: 0; line-height: 0.9; }
125
118
 
126
119
  /* ==========================================================================
127
- 4. TACTICAL FOCUS MODE (DUAL SCREEN 60/40)
120
+ 4. TACTICAL FOCUS MODE
128
121
  ========================================================================== */
129
122
  .focus-active .side-panel:not(.has-focus) { display: none !important; }
130
-
131
- /* Split: 60% per il grafico scelto, 40% per il vento centrale */
132
123
  .focus-active.focus-side-left { grid-template-columns: 3fr 2fr !important; }
133
124
  .focus-active.focus-side-left .side-panel.has-focus { grid-column: 1 !important; }
134
125
  .focus-active.focus-side-left .center-panel { grid-column: 2 !important; justify-content: center !important; align-items: center !important; }
135
-
136
126
  .focus-active.focus-side-right { grid-template-columns: 2fr 3fr !important; }
137
127
  .focus-active.focus-side-right .center-panel { grid-column: 1 !important; justify-content: center !important; align-items: center !important; }
138
128
  .focus-active.focus-side-right .side-panel.has-focus { grid-column: 2 !important; }
139
129
 
140
- /* Espansione box e tipografia massiccia in Focus Mode */
141
130
  .focus-active .has-focus .data-box:not(.is-focused) { display: none !important; }
142
131
  .focus-active .has-focus .data-box.is-focused {
143
132
  height: 100vh !important;
144
133
  border: none;
145
- background: rgba(255, 255, 255, 0.05);
134
+ background: rgba(0, 0, 0, 0.02);
146
135
  padding: 2vw 3vw !important;
147
136
  display: flex;
148
137
  flex-direction: column;
@@ -153,15 +142,14 @@ body {
153
142
  .focus-active .is-focused .label-row .label { font-size: 2rem !important; }
154
143
  .focus-active .is-focused .label-row .unit { font-size: 2rem !important; }
155
144
  .focus-active .is-focused .sparkline path { stroke-width: 2.5px !important; }
156
-
157
145
  .focus-active .center-panel svg#wind-gauge { max-width: 95% !important; max-height: 85vh !important; }
158
146
 
159
147
  /* ==========================================================================
160
148
  5. GRAFICI, SCALE E HERCULES MODE
161
- ========================================================================== */
149
+ ========================================================================= */
162
150
  .graph-wrapper {
163
151
  position: relative; width: 100%; flex-grow: 1; min-height: 0; margin-top: 4px;
164
- display: flex; align-items: stretch; background: rgba(255, 255, 255, 0.03);
152
+ display: flex; align-items: stretch; background: rgba(0, 0, 0, 0.04);
165
153
  border-radius: 4px; gap: 0px !important;
166
154
  }
167
155
 
@@ -173,7 +161,7 @@ body {
173
161
 
174
162
  .scale-labels {
175
163
  display: flex; flex-direction: column; justify-content: space-between;
176
- font-size: 8px; color: #777; font-weight: bold; min-width: 12px;
164
+ font-size: 8px; color: #444; font-weight: bold; min-width: 12px;
177
165
  height: 100%; line-height: 1; padding: 0; border: none !important;
178
166
  }
179
167
 
@@ -182,10 +170,9 @@ body {
182
170
  .right-panel .scale-labels { order: 1; text-align: right; padding-right: 4px; }
183
171
  .right-panel .sparkline { order: 2; }
184
172
 
185
- /* Hercules Mode Visuals */
186
173
  .line-hercules { filter: drop-shadow(0 0 5px #ff0000); stroke-width: 1.8px !important; }
187
- .box-hercules { background: rgba(255, 0, 0, 0.08) !important; }
188
- .box-hercules .scale-labels { color: #ff8888; }
174
+ .box-hercules { background: rgba(255, 0, 0, 0.05) !important; }
175
+ .box-hercules .scale-labels { color: #ff5555; }
189
176
  .box-hercules .unit::before, .box-hercules .unit::after, .box-hercules .label::before {
190
177
  font-size: 7px; color: #ff4444; font-weight: 900; letter-spacing: 1px; text-transform: uppercase;
191
178
  }
@@ -193,47 +180,67 @@ body {
193
180
  .right-panel .box-hercules .unit::after { content: " HERCULES"; }
194
181
  .right-panel .box-hercules .label:only-child::after { content: " HERCULES"; }
195
182
 
196
- /* Colori standard grafici */
197
183
  #stw-graph { stroke: #2ecc71; fill: rgba(46, 204, 113, 0.12); }
198
184
  #sog-graph { stroke: #f39c12; fill: rgba(243, 156, 18, 0.12); }
199
185
  #depth-graph { stroke: #3498db; fill: rgba(52, 152, 219, 0.12); }
200
- #tws-graph { stroke: #ffffff; fill: rgba(255, 255, 255, 0.08); }
186
+ #tws-graph { stroke: #000000; fill: rgba(0, 0, 0, 0.08); }
201
187
 
202
188
  /* ==========================================================================
203
- 6. WIDGETS E STATUS
189
+ 6. WIDGETS E STATUS (GIORNO)
204
190
  ========================================================================== */
205
191
  #status {
206
192
  position: absolute; top: 10px; right: 10px;
207
193
  font-size: 0.6rem; font-weight: 900;
208
194
  text-transform: uppercase; z-index: 1000;
209
- letter-spacing: 1px; background: rgba(0,0,0,0.4);
195
+ letter-spacing: 1px; background: rgba(0,0,0,0.05);
210
196
  padding: 2px 6px; border-radius: 4px;
211
197
  }
212
- .online { color: #2ecc71; opacity: 0.5; }
213
- .offline { color: #e74c3c; font-weight: bold; }
198
+ .online { color: #27ae60; opacity: 0.8; }
199
+ .offline { color: #c0392b; font-weight: bold; }
214
200
 
215
201
  .mini-compass {
216
202
  width: min(80cqh, 42cqw); height: min(80cqh, 42cqw); aspect-ratio: 1 / 1;
217
- flex-shrink: 0; background: #000; border-radius: 50%; border: 1.5px solid #333;
218
- box-shadow: inset 0 0 10px rgba(0,0,0,0.8); transition: all 0.4s ease;
203
+ flex-shrink: 0; background: #f0f0f0;
204
+ border-radius: 50%; border: 1.5px solid #ddd;
205
+ box-shadow: inset 0 0 10px rgba(0,0,0,0.05); transition: all 0.4s ease;
219
206
  }
220
207
 
208
+ /* Inversione elementi Mini Bussola TWD - Giorno */
209
+ .mini-compass text:last-of-type { fill: #000 !important; opacity: 0.4; } /* La "S" nera */
210
+ #twd-boat-wrap path { fill: #000 !important; opacity: 0.3; } /* Barca interna nera */
211
+ #twd-wind-chevron { stroke: #000 !important; } /* Forza il triangolino vento a nero in giorno */
212
+
221
213
  #awa-pointer, #twa-pointer, #track-pointer, #twd-arrow, #twd-boat-wrap {
222
214
  transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
223
215
  }
224
216
 
225
217
  #wind-gauge { width: 100%; height: 100%; max-height: 100%; object-fit: contain; }
226
218
 
219
+ /* Quadrante Vento Centrale - Giorno */
220
+ #wind-gauge circle[fill="#050505"] { fill: #fcfcfc; }
221
+ #wind-gauge circle[fill="#121212"] { fill: #f0f0f0; }
222
+ #boat-icon { fill: #000 !important; opacity: 1; }
223
+ #tick-labels { fill: #444 !important; }
224
+ #aws-val-svg { fill: #000 !important; }
225
+ #aws-display-group text { fill: #333 !important; }
226
+ #fullscreen-hotspot { fill: #eee !important; stroke: #ccc !important; }
227
+ #ticks line[stroke="#fff"] { stroke: #000 !important; }
228
+ #ticks line[stroke="#666"] { stroke: #bbb !important; }
229
+
230
+ /* Leeway Container - Giorno */
231
+ rect[fill="#222"] { fill: #eee !important; }
232
+ #leeway-val { color: #000 !important; }
233
+
227
234
  /* ==========================================================================
228
- 7. STATI DI ALLARME E INSTABILITÀ
235
+ 7. STATI DI ALLARME
229
236
  ========================================================================== */
230
- .unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #f39c12 !important; }
231
- .alarm-warning { color: #f1c40f !important; }
237
+ .unstable-data { animation: blink-unstable 1.5s infinite ease-in-out; color: #e67e22 !important; }
238
+ .alarm-warning { color: #f39c12 !important; }
232
239
  .alarm-danger { color: #e74c3c !important; font-weight: 900; animation: blink-unstable 1s infinite; }
233
240
  @keyframes blink-unstable { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
234
241
 
235
242
  /* ==========================================================================
236
- 8. RESPONSIVE (PORTRAIT MODE)
243
+ 8. RESPONSIVE
237
244
  ========================================================================== */
238
245
  @media (max-aspect-ratio: 0.9 / 1) {
239
246
  .main-container { grid-template-columns: 1fr 1fr !important; grid-template-rows: 45vh calc(55vh - 8px) !important; }
@@ -250,38 +257,46 @@ body {
250
257
  9. NIGHT MODE (TACTICAL RED)
251
258
  ========================================================================== */
252
259
  body.night-mode { background-color: #000 !important; color: #ff0000 !important; }
253
- .night-mode .side-panel { background: rgba(20, 0, 0, 0.4); border: 1px solid #330000; }
260
+ .night-mode .side-panel { background: rgba(20, 0, 0, 0.4) !important; border: 1px solid #330000; }
254
261
  .night-mode .data-box { border-bottom-color: #260000; }
255
262
  .night-mode .label, .night-mode .unit, .night-mode .dual-label { color: #800000 !important; }
256
263
  .night-mode .value, .night-mode .value-large { color: #ff3333 !important; text-shadow: 0 0 8px rgba(255, 0, 0, 0.4); }
257
264
 
258
- /* Grafici Night Mode: Solo linea, no riempimento */
259
265
  .night-mode .graph-wrapper { background: rgba(30, 0, 0, 0.3) !important; }
260
266
  .night-mode .sparkline path:first-of-type { display: none !important; }
261
267
  .night-mode .sparkline path { fill: none !important; stroke: #ff3333 !important; stroke-width: 1.8px !important; opacity: 1 !important; filter: drop-shadow(0 0 2px rgba(255, 0, 0, 0.4)); }
262
- .night-mode .sparkline line { stroke: #4d0000 !important; stroke-width: 0.7px !important; }
268
+
269
+ .night-mode .sparkline line { stroke: rgba(255, 0, 0, 0.15) !important; stroke-width: 0.7px !important; }
263
270
  .night-mode #tws-graph line:not([stroke*="rgba"]) { stroke: #ff3333 !important; stroke-width: 1.8px !important; }
264
271
  .night-mode .scale-labels { color: #660000 !important; }
265
272
 
266
- /* Hercules & special in Night */
267
273
  .night-mode .box-hercules { background: rgba(60, 0, 0, 0.2) !important; }
268
274
  .night-mode .line-hercules { filter: drop-shadow(0 0 6px #ff0000) !important; }
269
275
 
270
- /* Wind Gauge Night */
276
+ /* Fix Bussola Centrale Night */
271
277
  .night-mode #wind-gauge circle { stroke: #330000; }
278
+ .night-mode #wind-gauge circle[fill="#050505"] { fill: #050505 !important; }
279
+ .night-mode #wind-gauge circle[fill="#121212"] { fill: #121212 !important; }
280
+ .night-mode #fullscreen-hotspot { fill: #000 !important; stroke: #330000 !important; }
281
+
272
282
  .night-mode #ticks line { stroke: #4d0000 !important; }
273
283
  .night-mode #tick-labels { fill: #800000 !important; }
274
- .night-mode #boat-icon { fill: #330000 !important; opacity: 0.6; }
284
+ .night-mode #boat-icon { fill: #330000 !important; opacity: 0.6 !important; }
275
285
  .night-mode #aws-val-svg { fill: #ff3333 !important; }
276
286
  .night-mode #aws-display-group text { fill: #ff3333 !important; }
277
287
  .night-mode #center-glow feDropShadow { flood-color: #ff0000 !important; flood-opacity: 0.6 !important; }
278
288
 
279
- /* Settori Vento Night (Destra tratteggiata) */
289
+ /* Bussola TWD Night */
290
+ .night-mode .mini-compass { border-color: #330000; background: #000; }
291
+ .night-mode .mini-compass text { fill: #800000 !important; }
292
+ .night-mode .mini-compass text:last-of-type { fill: #800000 !important; opacity: 1; } /* La "S" torna rossa */
293
+ .night-mode #twd-boat-wrap path { fill: #ff3333 !important; opacity: 0.4 !important; } /* Barca torna rossa */
294
+ .night-mode #twd-arrow #twd-wind-chevron { stroke: #ff3333 !important; stroke-width: 3px !important; opacity: 1 !important; filter: drop-shadow(0 0 4px #ff0000); transform-origin: center; }
295
+
296
+ /* Vento e Leeway Night */
280
297
  .night-mode #wind-gauge path[stroke="#ff0000"] { stroke: #660000 !important; opacity: 0.8; }
281
298
  .night-mode #wind-gauge path[stroke="#00ff00"] { stroke: #660000 !important; stroke-dasharray: 4, 3; opacity: 0.8; }
282
299
  .night-mode #wind-gauge path[stroke="#ff8800"] { stroke: #330000 !important; stroke-width: 8; }
283
-
284
- /* Leeway & Pointer Night */
285
300
  .night-mode rect[fill="url(#leeway-grad)"] { fill: url(#leeway-night-grad) !important; }
286
301
  .night-mode #leeway-val { fill: #ff3333 !important; }
287
302
  .night-mode g[stroke="#555"] line { stroke: #4d0000 !important; }
@@ -292,30 +307,21 @@ body.night-mode { background-color: #000 !important; color: #ff0000 !important;
292
307
  .night-mode #twa-pointer path { fill: #800000; stroke: #000; }
293
308
  .night-mode #track-pointer path { fill: #ff0000; stroke: #fff; stroke-width: 0.5; }
294
309
 
295
- /* Bussola TWD Night */
296
- .night-mode .mini-compass { border-color: #330000; background: #000; }
297
- .night-mode .mini-compass text { fill: #800000 !important; }
298
- .night-mode #twd-arrow #twd-wind-chevron { stroke: #ff3333 !important; stroke-width: 3px !important; opacity: 1 !important; filter: drop-shadow(0 0 4px #ff0000); transform-origin: center; }
299
- .night-mode #twd-boat-wrap path { fill: #ff3333 !important; opacity: 0.4 !important; }
300
-
301
310
  /* ==========================================================================
302
- 11. TREND VENTO E ALLARME STRAMBATA (GYBE)
311
+ 10. TREND VENTO E ALLARME STRAMBATA
303
312
  ========================================================================== */
304
313
  @keyframes blink-trend { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
305
314
 
306
- /* Stato base dei pallini di trend: discreti al 30% */
307
315
  #trend-dot-cw, #trend-dot-ccw, #trend-gauge-cw, #trend-gauge-ccw {
308
316
  opacity: 0.3;
309
317
  transition: opacity 0.3s ease;
310
318
  }
311
319
 
312
- /* Quando attivi: brillano al 100% e lampeggiano */
313
320
  .is-trending {
314
321
  opacity: 1 !important;
315
322
  animation: blink-trend 1s infinite !important;
316
323
  }
317
324
 
318
- /* Allarme Strambata: Rosso fisso con bagliore neon massimo */
319
325
  .is-gybing {
320
326
  opacity: 1 !important;
321
327
  animation: none !important;
package/test.html CHANGED
@@ -68,4 +68,3 @@
68
68
  </script>
69
69
  </body>
70
70
  </html>
71
- ---Fine Codice ---