signalk-mareas-ihm 2.1.4 → 2.1.5

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.
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "2.1.4",
3
- "timestamp": "20260609-0204",
2
+ "version": "2.1.5",
3
+ "timestamp": "20260609-0225",
4
4
  "gitHash": null,
5
5
  "gitDirty": true,
6
- "builtAt": "2026-06-09T00:04:55.082Z"
6
+ "builtAt": "2026-06-09T00:25:39.157Z"
7
7
  }
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ function isPositionValue(v) {
49
49
  // timestamp + git hash so we can verify exactly which build is running on the Pi
50
50
  // without ambiguity. ("¿Qué versión tengo deployada?" → /api/paths or landing.)
51
51
  const PLUGIN_VERSION = esmRequire("../package.json").version;
52
- const PLUGIN_REVISION = "Rev315";
52
+ const PLUGIN_REVISION = "Rev317";
53
53
  let _buildInfo = null;
54
54
  try {
55
55
  _buildInfo = esmRequire("./build-info.json");
@@ -3507,6 +3507,24 @@ export default function (app) {
3507
3507
  }
3508
3508
  });
3509
3509
  expressApp.get("/signalk-mareas-ihm/navtiles/:z/:x/:y.png", async (req, res) => {
3510
+ /* Rev317: guard res.headersSent en todos los error paths. Si la conexión
3511
+ upstream falla MID-STREAM (después de pipe()), no se puede llamar a
3512
+ res.status/send porque ya se enviaron headers → "Cannot set headers
3513
+ after they are sent". Solución: solo enviar 502 si headersSent=false;
3514
+ en otro caso destruir el socket para cortar el stream colgado. */
3515
+ const sendFallback = () => {
3516
+ if (res.headersSent) {
3517
+ try {
3518
+ res.end();
3519
+ }
3520
+ catch { /* socket already closed */ }
3521
+ return;
3522
+ }
3523
+ try {
3524
+ res.status(502).set("Content-Type", "image/png").send(TRANSPARENT_PNG);
3525
+ }
3526
+ catch { /* ignore */ }
3527
+ };
3510
3528
  try {
3511
3529
  const { token, bearer } = await fetchChartTokens();
3512
3530
  const z = req.params.z, x = req.params.x, y = req.params.y;
@@ -3519,16 +3537,24 @@ export default function (app) {
3519
3537
  "Origin": "https://maps.garmin.com"
3520
3538
  }
3521
3539
  }, (tileResp) => {
3540
+ if (res.headersSent) {
3541
+ try {
3542
+ tileResp.destroy();
3543
+ }
3544
+ catch { }
3545
+ return;
3546
+ }
3522
3547
  const ct = tileResp.headers["content-type"] || "image/png";
3523
3548
  res.set("Content-Type", ct);
3524
3549
  res.set("Cache-Control", "public, max-age=15552000, immutable");
3550
+ tileResp.on("error", sendFallback);
3525
3551
  tileResp.pipe(res);
3526
3552
  });
3527
- tileReq.on("error", () => { res.status(502).send(TRANSPARENT_PNG); });
3553
+ tileReq.on("error", sendFallback);
3528
3554
  tileReq.end();
3529
3555
  }
3530
3556
  catch {
3531
- res.status(502).send(TRANSPARENT_PNG);
3557
+ sendFallback();
3532
3558
  }
3533
3559
  });
3534
3560
  // ═══════════════ SSE (Server-Sent Events) for multi-device sync ═══════════════
package/dist/mobile.html CHANGED
@@ -1303,26 +1303,24 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1303
1303
  <!-- Rev296: botones izquierdos dentro de #m-leftbar (scroll vertical, mismo
1304
1304
  estilo que #m-sidebar derecha). -->
1305
1305
  <div id="m-leftbar">
1306
- <button id="m-ham" title="Menú" onclick="document.getElementById('m-menu').classList.add('open')"><span class="ico">☰</span></button>
1307
- <button id="m-cartas" title="Activar / desactivar audio" onclick="toggleAudioEnable&&toggleAudioEnable()">
1306
+ <button id="m-ham" data-i18n-title="lb_ham_tip" title="Menú" onclick="document.getElementById('m-menu').classList.add('open')"><span class="ico">☰</span></button>
1307
+ <button id="m-cartas" data-i18n-title="lb_audio_tip" title="Activar / desactivar audio" onclick="toggleAudioEnable&&toggleAudioEnable()">
1308
1308
  <span class="ico" id="m-mute-icon">🔊</span>
1309
1309
  <span class="lbl" data-i18n="m_silencio">Silencio</span>
1310
1310
  </button>
1311
- <button id="m-snooze" title="Snooze 5 min" onclick="m_toggleSnooze()">
1311
+ <button id="m-snooze" data-i18n-title="lb_snooze_tip" title="Snooze 5 min" onclick="m_toggleSnooze()">
1312
1312
  <span class="ico" id="m-snooze-icon">💤</span>
1313
1313
  <span class="lbl" id="m-snooze-lbl" data-i18n="m_snooze">Snooze</span>
1314
1314
  </button>
1315
- <button id="m-fav" title="Fondeo favorito" onclick="m_favClick()">
1315
+ <button id="m-fav" data-i18n-title="lb_fav_tip" title="Fondeo favorito" onclick="m_favClick()">
1316
1316
  <span class="ico">❤</span>
1317
1317
  <span class="lbl" data-i18n="m_favorito">Favorito</span>
1318
1318
  </button>
1319
- <!-- Rev296: acceso directo a Curvas y Mareas también desde la left bar
1320
- (entre Favorito y KIP). Mismos handlers que los del sidebar derecho. -->
1321
- <button id="m-lb-curvas" title="Curvas" onclick="openPopup('curvas-pop');fetchCurvas&&fetchCurvas()">
1319
+ <button id="m-lb-curvas" data-i18n-title="sb_curvas" title="Curvas" onclick="openPopup('curvas-pop');fetchCurvas&&fetchCurvas()">
1322
1320
  <span class="ico">〰️</span>
1323
1321
  <span class="lbl" data-i18n="sb_curvas">Curvas</span>
1324
1322
  </button>
1325
- <button id="m-lb-mareas" title="Mareas (TidesView)" onclick="m_openMareas&&m_openMareas()">
1323
+ <button id="m-lb-mareas" data-i18n-title="lb_mareas_tip" title="Mareas (TidesView)" onclick="m_openMareas&&m_openMareas()">
1326
1324
  <span class="ico" style="display:inline-flex;align-items:center;justify-content:center">
1327
1325
  <svg viewBox="0 0 44 44" width="30" height="30">
1328
1326
  <rect x="2" y="2" width="40" height="14" rx="2" fill="#ffd966"/>
@@ -1334,10 +1332,10 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1334
1332
  </span>
1335
1333
  <span class="lbl" data-i18n="sb_mareas">Mareas</span>
1336
1334
  </button>
1337
- <button id="m-kip" title="Abrir KIP dashboard" onclick="window.location.href = window.location.origin + '/@mxtommy/kip/'">
1335
+ <button id="m-kip" data-i18n-title="lb_kip_tip" title="Abrir KIP dashboard" onclick="window.location.href = window.location.origin + '/@mxtommy/kip/'">
1338
1336
  <span class="ico" style="font-weight:900;letter-spacing:1px">KIP</span>
1339
1337
  </button>
1340
- <button id="m-fb" title="Abrir Freeboard-SK" onclick="window.location.href = window.location.origin + '/@signalk/freeboard-sk/'">
1338
+ <button id="m-fb" data-i18n-title="lb_fb_tip" title="Abrir Freeboard-SK" onclick="window.location.href = window.location.origin + '/@signalk/freeboard-sk/'">
1341
1339
  <img src="assets/freeboard.png" alt="Freeboard"
1342
1340
  style="width:42px;height:42px;object-fit:contain;background:#fff;border-radius:6px;padding:2px;display:block;box-sizing:border-box"/>
1343
1341
  </button>
@@ -1389,7 +1387,7 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1389
1387
  <span class="vl" id="m-bb-wind-kt-txt">—<span style="font-size:11px;font-weight:700;opacity:.75;margin-left:2px">kts</span></span>
1390
1388
  </div>
1391
1389
  </div>
1392
- <div class="it" data-i18n-title="bb_sonda_tip" title="Profundidad bajo quilla · Sonda" onclick="openPopup&&openPopup('sonda-pop')"><span class="lb" data-i18n="bb_sonda">Sonda</span><span class="vl" id="m-bb-sonda">—</span><span id="m-bb-sonda-status" style="display:none;font-size:9px;font-weight:800;margin-top:2px;text-align:center;line-height:1.1"></span></div>
1390
+ <div class="it" data-i18n-title="bb_sonda_tip" title="Profundidad bajo quilla · Sonda" onclick="openPopup&&openPopup('sonda-pop');typeof fetchSondaData==='function'&&fetchSondaData()"><span class="lb" data-i18n="bb_sonda">Sonda</span><span class="vl" id="m-bb-sonda">—</span><span id="m-bb-sonda-status" style="display:none;font-size:9px;font-weight:800;margin-top:2px;text-align:center;line-height:1.1"></span></div>
1393
1391
  <div class="it" data-i18n-title="bb_cadena_tip" title="Cadena recomendada" onclick="m_openInfoModal&&m_openInfoModal()"><span class="lb" data-i18n="bb_cadena_rec">Cadena rec.</span><span class="vl" id="m-bb-cad">—</span></div>
1394
1392
  <div class="it" data-i18n-title="bb_dist_tip" title="Distancia al ancla" onclick="m_openInfoModal&&m_openInfoModal()"><span class="lb" data-i18n="bb_dist_ancla">Dist. ancla</span><span class="vl" id="m-bb-dist">—</span></div>
1395
1393
  <div class="it m-bb-graph" data-i18n-title="bb_presion_tip" title="Presión barométrica · línea naranja = AHORA · click → Meteo" onclick="toggleMeteo&&toggleMeteo()">
@@ -1593,18 +1591,20 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1593
1591
  </div>
1594
1592
  <!-- Popup info histórico ola: leyenda colores + tabla detallada. -->
1595
1593
  <div class="popup-overlay" id="wave-hist-info-pop" onclick="closePopup('wave-hist-info-pop')">
1596
- <div class="popup-box" onclick="event.stopPropagation()" style="width:620px;max-width:96vw;max-height:88vh;overflow-y:auto;padding:18px 22px">
1597
- <button onclick="closePopup('wave-hist-info-pop')" style="position:absolute;top:8px;right:12px;background:none;border:none;color:#aaa;font-size:24px;cursor:pointer;line-height:1">✕</button>
1598
- <h4 style="margin:0 0 8px 0;font-size:19px">Historial de olas — leyenda y detalle</h4>
1599
- <div style="font-size:14px;color:#9ad;margin-bottom:14px">Cada barra resume 15 minutos. Color = intensidad media medida por el IMU del barco. Altura = oscilación del bin.</div>
1600
- <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:18px">
1601
- <div style="text-align:center"><div style="width:100%;height:18px;background:#3aa856;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700" data-i18n="m_calma">Calma</div><div style="font-size:10px;color:#888">&lt;0.5°</div></div>
1602
- <div style="text-align:center"><div style="width:100%;height:18px;background:#a3c813;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700">Rizada</div><div style="font-size:10px;color:#888">0.5–1.5°</div></div>
1603
- <div style="text-align:center"><div style="width:100%;height:18px;background:#f3c812;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700">Moderada</div><div style="font-size:10px;color:#888">1.5–3°</div></div>
1604
- <div style="text-align:center"><div style="width:100%;height:18px;background:#f37812;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700">Agitada</div><div style="font-size:10px;color:#888">36°</div></div>
1605
- <div style="text-align:center"><div style="width:100%;height:18px;background:#e74c3c;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700">Fuerte</div><div style="font-size:10px;color:#888">&gt;6°</div></div>
1594
+ <!-- Rev316: ventana wave-hist-info ampliada (antes 620px / 88vh) y todo
1595
+ bilingüe. Estilo título alineado con resto de modales (.m-modal-title). -->
1596
+ <div class="popup-box" onclick="event.stopPropagation()" style="width:900px;max-width:96vw;max-height:92vh;overflow-y:auto;padding:28px 32px">
1597
+ <button onclick="closePopup('wave-hist-info-pop')" style="position:absolute;top:10px;right:14px;background:none;border:none;color:#aaa;font-size:28px;cursor:pointer;line-height:1">✕</button>
1598
+ <h4 style="margin:0 0 12px 0;font-size:24px;font-weight:900;color:#fff" data-i18n="sh_wh_title">Historial de olas — leyenda y detalle</h4>
1599
+ <div style="font-size:16px;color:#9ad;margin-bottom:18px" data-i18n="sh_wh_subtitle">Cada barra resume 15 minutos. Color = intensidad media medida por el IMU del barco. Altura = oscilación del bin.</div>
1600
+ <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:22px">
1601
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#3aa856;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="m_calma">Calma</div><div style="font-size:12px;color:#888">&lt;0.5°</div></div>
1602
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#a3c813;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="sh_wh_rizada">Rizada</div><div style="font-size:12px;color:#888">0.51.5°</div></div>
1603
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#f3c812;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="sh_wh_moderada">Moderada</div><div style="font-size:12px;color:#888">1.5–3°</div></div>
1604
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#f37812;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="sh_wh_agitada">Agitada</div><div style="font-size:12px;color:#888">3–6°</div></div>
1605
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#e74c3c;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="sh_wh_fuerte">Fuerte</div><div style="font-size:12px;color:#888">&gt;6°</div></div>
1606
1606
  </div>
1607
- <div id="wave-hist-info-table" style="font-size:13px;color:#cfd8dc"></div>
1607
+ <div id="wave-hist-info-table" style="font-size:15px;color:#cfd8dc"></div>
1608
1608
  </div>
1609
1609
  </div>
1610
1610
  <!-- Rev79: popup explicacion del % de proteccion -->
@@ -1776,6 +1776,12 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1776
1776
  <div class="alarm-info"><b style="color:var(--red)" data-i18n="alarma_sonda">🌊 Alarma Sonda</b><br><small style="color:#aaa" data-i18n="alarma_sonda_desc">Riesgo de varada por profundidad</small><div class="alarm-st" id="alarm-sonda-st"></div></div>
1777
1777
  <div class="alarm-sw"><input type="checkbox" id="chk-alarm-sonda" onclick="event.stopPropagation()" onchange="setAlarm('sonda',this.checked)"><label for="chk-alarm-sonda" onclick="event.stopPropagation()"></label></div>
1778
1778
  </div>
1779
+ <!-- Rev316: nuevo selector "Mala condición climática" — alerta cuando
1780
+ la previsión meteo en las próximas 6h tiene viento > 25 kt o ola > 1.5 m. -->
1781
+ <div class="alarm-row" id="ar-weather" onclick="var c=document.getElementById('chk-alarm-weather');c.checked=!c.checked;setAlarm('weather',c.checked)">
1782
+ <div class="alarm-info"><b style="color:#ffb23f" data-i18n="alarma_meteo">⛈️ Mala condición climática</b><br><small style="color:#aaa" data-i18n="alarma_meteo_desc">Aviso si previsión 6h supera viento 25 kt u ola 1.5 m</small><div class="alarm-st" id="alarm-weather-st"></div></div>
1783
+ <div class="alarm-sw"><input type="checkbox" id="chk-alarm-weather" onclick="event.stopPropagation()" onchange="setAlarm('weather',this.checked)"><label for="chk-alarm-weather" onclick="event.stopPropagation()"></label></div>
1784
+ </div>
1779
1785
  </div>
1780
1786
  <div id="alarm-active-list" style="margin-top:6px"></div>
1781
1787
  <!-- Rev184: orden correcto — debajo de alarmas va el VOLUMEN, luego
@@ -2078,6 +2084,31 @@ var _i18n={
2078
2084
  m_viento:{es:'Viento',en:'Wind'},
2079
2085
  m_olas:{es:'Olas',en:'Waves'},
2080
2086
  m_mar_calma:{es:'Mar en calma',en:'Calm sea'},
2087
+ /* Rev316: wave-hist-info popup */
2088
+ sh_wh_title:{es:'Historial de olas — leyenda y detalle',en:'Wave history — legend & details'},
2089
+ sh_wh_subtitle:{es:'Cada barra resume 15 minutos. Color = intensidad media medida por el IMU del barco. Altura = oscilación del bin.',en:'Each bar summarises 15 minutes. Color = average intensity measured by boat IMU. Bar height = bin oscillation.'},
2090
+ sh_wh_no_data:{es:'No hay datos todavía. Echa el ancla y espera al menos 15 min para que aparezcan barras.',en:'No data yet. Drop anchor and wait at least 15 min for bars to appear.'},
2091
+ sh_wh_inicio:{es:'Inicio',en:'Start'},
2092
+ sh_wh_intensidad:{es:'Intensidad',en:'Intensity'},
2093
+ sh_wh_periodo:{es:'Período',en:'Period'},
2094
+ sh_wh_altura:{es:'Altura',en:'Height'},
2095
+ sh_wh_rms:{es:'RMS',en:'RMS'},
2096
+ sh_wh_rizada:{es:'Rizada',en:'Rippled'},
2097
+ sh_wh_moderada:{es:'Moderada',en:'Moderate'},
2098
+ sh_wh_agitada:{es:'Agitada',en:'Rough'},
2099
+ sh_wh_fuerte:{es:'Fuerte',en:'Strong'},
2100
+ sh_wh_actual:{es:'ACTUAL',en:'CURRENT'},
2101
+ /* Rev316: tooltips left bar */
2102
+ lb_ham_tip:{es:'Menú',en:'Menu'},
2103
+ lb_audio_tip:{es:'Activar / desactivar audio',en:'Enable / disable audio'},
2104
+ lb_snooze_tip:{es:'Snooze 5 min',en:'Snooze 5 min'},
2105
+ lb_fav_tip:{es:'Fondeo favorito',en:'Favourite anchorage'},
2106
+ lb_mareas_tip:{es:'Mareas (TidesView)',en:'Tides (TidesView)'},
2107
+ lb_kip_tip:{es:'Abrir KIP dashboard',en:'Open KIP dashboard'},
2108
+ lb_fb_tip:{es:'Abrir Freeboard-SK',en:'Open Freeboard-SK'},
2109
+ /* Rev316: alarma meteo */
2110
+ alarma_meteo:{es:'⛈️ Mala condición climática',en:'⛈️ Bad weather conditions'},
2111
+ alarma_meteo_desc:{es:'Aviso si previsión 6h supera viento 25 kt u ola 1.5 m',en:'Warning if 6h forecast exceeds wind 25 kt or wave 1.5 m'},
2081
2112
  /* Rev313: modal guardar favorito */
2082
2113
  fav_guardar_title:{es:'❤ Guardar fondeo favorito',en:'❤ Save favourite anchorage'},
2083
2114
  fav_guardar_hint:{es:'Nombre del favorito (vacío = nombre automático por geolocalización):',en:'Favourite name (empty = auto name by geolocation):'},
@@ -4852,25 +4883,32 @@ function _renderWaveHistInfo(){
4852
4883
  var bins=Array.isArray(j.bins)?j.bins.slice():[];
4853
4884
  if(j.currentBin)bins.push(j.currentBin);
4854
4885
  if(bins.length===0){
4855
- el.innerHTML='<div style="color:#888;text-align:center;padding:12px">No hay datos todavía. Echa el ancla y espera al menos 15 min para que aparezcan barras.</div>';
4886
+ var _noDataMsg=(typeof T==='function')?T('sh_wh_no_data','No hay datos todavía. Echa el ancla y espera al menos 15 min para que aparezcan barras.'):'No hay datos todavía. Echa el ancla y espera al menos 15 min para que aparezcan barras.';
4887
+ el.innerHTML='<div style="color:#888;text-align:center;padding:12px">'+_noDataMsg+'</div>';
4856
4888
  return;
4857
4889
  }
4858
- /* Encabezado tabla */
4859
- var rows=['<div style="display:grid;grid-template-columns:80px 1fr 60px 70px 90px;gap:4px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.18);font-weight:800;color:#9ad;font-size:12px;letter-spacing:.8px;text-transform:uppercase">',
4860
- '<span>Inicio</span>','<span>Intensidad</span>','<span style="text-align:right">Período</span>','<span style="text-align:right">Altura</span>','<span style="text-align:right">RMS</span>',
4890
+ /* Rev316: headers i18n. */
4891
+ var _tw=function(k,fb){ return (typeof T==='function')?T(k,fb):fb; };
4892
+ var _hInicio=_tw('sh_wh_inicio','Inicio'), _hInten=_tw('sh_wh_intensidad','Intensidad'),
4893
+ _hPer=_tw('sh_wh_periodo','Período'), _hAlt=_tw('sh_wh_altura','Altura'), _hRms=_tw('sh_wh_rms','RMS');
4894
+ var _capCalma=_tw('m_calma','Calma'), _capRiz=_tw('sh_wh_rizada','Rizada'),
4895
+ _capMod=_tw('sh_wh_moderada','Moderada'), _capAgi=_tw('sh_wh_agitada','Agitada'), _capFue=_tw('sh_wh_fuerte','Fuerte');
4896
+ var _capMap={calma:_capCalma,rizada:_capRiz,moderada:_capMod,agitada:_capAgi,fuerte:_capFue};
4897
+ var _badgeActual=_tw('sh_wh_actual','ACTUAL');
4898
+ var rows=['<div style="display:grid;grid-template-columns:80px 1fr 60px 70px 90px;gap:4px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.18);font-weight:800;color:#9ad;font-size:13px;letter-spacing:.8px;text-transform:uppercase">',
4899
+ '<span>'+_hInicio+'</span>','<span>'+_hInten+'</span>','<span style="text-align:right">'+_hPer+'</span>','<span style="text-align:right">'+_hAlt+'</span>','<span style="text-align:right">'+_hRms+'</span>',
4861
4900
  '</div>'];
4862
- /* Más recientes arriba */
4863
4901
  var sorted=bins.slice().reverse();
4864
4902
  for(var i=0;i<sorted.length;i++){
4865
4903
  var b=sorted[i];
4866
4904
  var d=new Date(b.binStartMs);
4867
4905
  var hhmm=('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2);
4868
4906
  var color=_WAVE_INT_COLORS[b.intensityMode]||'#888';
4869
- var intCap=(b.intensityMode||'').charAt(0).toUpperCase()+(b.intensityMode||'').slice(1);
4907
+ var intCap=_capMap[b.intensityMode||'']||((b.intensityMode||'').charAt(0).toUpperCase()+(b.intensityMode||'').slice(1));
4870
4908
  var per=(typeof b.periodAvgSec==='number'&&b.periodAvgSec>0)?b.periodAvgSec.toFixed(1)+'s':'—';
4871
4909
  var alt=(typeof b.heightSigMaxM==='number'&&b.heightSigMaxM>0)?b.heightSigMaxM.toFixed(2).replace('.',',')+' m':'<5cm';
4872
4910
  var rms=(typeof b.rmsMaxDeg==='number'&&b.rmsMaxDeg>0)?b.rmsMaxDeg.toFixed(2)+'°':'—';
4873
- var curBadge=b.isCurrent?' <span style="font-size:10px;color:#ffeb3b;font-weight:800;margin-left:4px">ACTUAL</span>':'';
4911
+ var curBadge=b.isCurrent?' <span style="font-size:10px;color:#ffeb3b;font-weight:800;margin-left:4px">'+_badgeActual+'</span>':'';
4874
4912
  rows.push('<div style="display:grid;grid-template-columns:80px 1fr 60px 70px 90px;gap:4px;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.06);align-items:center">'+
4875
4913
  '<span style="color:#fff;font-weight:700">'+hhmm+'</span>'+
4876
4914
  '<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:'+color+';margin-right:6px;vertical-align:middle"></span>'+intCap+curBadge+'</span>'+
@@ -6701,8 +6739,44 @@ function setAlarm(type,on){
6701
6739
  document.getElementById('alarm-sonda-st').textContent=T('activa');document.getElementById('alarm-sonda-st').style.color='var(--grn)';
6702
6740
  }
6703
6741
  });
6742
+ }else if(type==='weather'){
6743
+ /* Rev316: alarma meteo — frontend-only por ahora. Estado en localStorage
6744
+ y check periódico contra _shelterCache.assessment.hours (próximas 6 h).
6745
+ Si viento > 25 kt u ola > 1.5 m → setAlarmActive('weather', true). */
6746
+ _alarmWeatherEnabled = on;
6747
+ localStorage.setItem('ihm-weather-alarm', on?'1':'0');
6748
+ var st = document.getElementById('alarm-weather-st');
6749
+ if (st) {
6750
+ st.textContent = on ? T('activa','activa') : T('desactivada','desactivada');
6751
+ st.style.color = on ? 'var(--grn)' : '#666';
6752
+ }
6753
+ if (!on) {
6754
+ try { setAlarmActive('weather', false); } catch(_){}
6755
+ } else {
6756
+ try { _checkWeatherAlarm(); } catch(_){}
6757
+ }
6704
6758
  }
6705
6759
  }
6760
+ /* Rev316: estado y comprobación de alarma meteo (frontend-only).
6761
+ Mira las próximas 6h de _shelterCache.assessment.hours buscando viento>25kt
6762
+ u ola>1.5m. Se invoca tras tocar el switch y cada 5 min mientras esté on. */
6763
+ var _alarmWeatherEnabled = (localStorage.getItem('ihm-weather-alarm')==='1');
6764
+ function _checkWeatherAlarm(){
6765
+ if (!_alarmWeatherEnabled) return;
6766
+ try {
6767
+ var asm = (typeof _shelterCache!=='undefined' && _shelterCache) ? _shelterCache.assessment : null;
6768
+ if (!asm || !asm.hours || !asm.hours.length) return;
6769
+ var bad = false;
6770
+ for (var i=0; i<Math.min(6, asm.hours.length); i++){
6771
+ var hr = asm.hours[i];
6772
+ var w = hr.windKt || hr.exposureKt || 0;
6773
+ var wave = hr.waveM || 0;
6774
+ if (w > 25 || wave > 1.5) { bad = true; break; }
6775
+ }
6776
+ setAlarmActive('weather', bad);
6777
+ } catch(_){}
6778
+ }
6779
+ setInterval(_checkWeatherAlarm, 5*60*1000);
6706
6780
  /* ══════════════ CALC SONDA ══════════════ */
6707
6781
  /* ═══ SHARED INPUT STYLE (touch-friendly, wide for fingers) ═══ */
6708
6782
  /* ═══ SHARED INPUT STYLE (touch-friendly) ═══ */
@@ -8110,13 +8184,17 @@ function m_syncBottomBarGraphs(){
8110
8184
  var v = srcVal.textContent.trim();
8111
8185
  if (v && v !== '—') dstVal.textContent = v;
8112
8186
  }
8113
- /* Grade del abrigo: A=verde, B=verde claro, C=naranja, D=naranja oscuro, F=rojo */
8187
+ /* Grade del abrigo: A=verde, B=verde claro, C=naranja, D=naranja oscuro, F=rojo.
8188
+ Rev316: el % del donut leía un mapa hardcoded por letra (A=100, B=80, ...)
8189
+ que NO coincidía con el % real del shelter modal. Ahora lee el valor REAL
8190
+ (asm.scorePercent) del _shelterCache. Si no hay cache aún caemos al mapa
8191
+ como antes. */
8114
8192
  var srcGrade = document.getElementById('wx-shelter-grade');
8115
8193
  var dstGrade = document.getElementById('m-bb-grade');
8116
8194
  var dstDonut = document.getElementById('m-bb-donut');
8117
8195
  var dstDonutText = document.getElementById('m-bb-donut-text');
8118
8196
  var gradeColors = { 'A':'#3aa856','B':'#9ed02e','C':'#ffb23f','D':'#ff7043','F':'#f44336' };
8119
- var donutPct = { 'A':100, 'B':80, 'C':60, 'D':40, 'F':20 };
8197
+ var donutPctFallback = { 'A':100, 'B':80, 'C':60, 'D':40, 'F':20 };
8120
8198
  if (srcGrade && dstGrade){
8121
8199
  var g = (srcGrade.textContent.trim() || '—').toUpperCase();
8122
8200
  if (g && g !== '—' && g !== '?') {
@@ -8124,15 +8202,19 @@ function m_syncBottomBarGraphs(){
8124
8202
  var bgColor = gradeColors[g] || 'rgba(120,120,120,.35)';
8125
8203
  dstGrade.style.background = bgColor;
8126
8204
  dstGrade.style.color = '#fff';
8205
+ /* Pct real del cache shelter; fallback al mapa por letra si no hay. */
8206
+ var realPct = null;
8207
+ try {
8208
+ var _asm = (typeof _shelterCache !== 'undefined' && _shelterCache) ? _shelterCache.assessment : null;
8209
+ if (_asm && typeof _asm.scorePercent === 'number') realPct = Math.round(_asm.scorePercent);
8210
+ } catch(_){}
8211
+ if (realPct === null) realPct = donutPctFallback[g] || 0;
8127
8212
  if (dstDonut) {
8128
- var pct = donutPct[g] || 0;
8129
- dstDonut.setAttribute('stroke-dasharray', pct + ',100');
8213
+ dstDonut.setAttribute('stroke-dasharray', realPct + ',100');
8130
8214
  dstDonut.setAttribute('stroke', bgColor);
8131
8215
  }
8132
- /* Rev286: porcentaje (con %) dentro del donut, no letra. */
8133
8216
  if (dstDonutText) {
8134
- var p = donutPct[g] || 0;
8135
- dstDonutText.textContent = p + '%';
8217
+ dstDonutText.textContent = realPct + '%';
8136
8218
  }
8137
8219
  }
8138
8220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-mareas-ihm",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "Complete SignalK anchor watch & marine safety webapp. Voice alarms for anchor drag, AIS collision risk and grounding. Shelter / anchorage forecast (wind exposure + wave history). Weather forecast (Open-Meteo). Depth & scope calculator. Multi-source nautical charts (Esri Satellite, IHM ENC S-52, bathymetry, MBTiles offline, OpenStreetMap, OpenSeaMap). Favourite anchorages. Multi-device real-time sync (SSE). Includes official IHM Spain tide predictions. Mobile-first UI in ES/EN.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1303,26 +1303,24 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1303
1303
  <!-- Rev296: botones izquierdos dentro de #m-leftbar (scroll vertical, mismo
1304
1304
  estilo que #m-sidebar derecha). -->
1305
1305
  <div id="m-leftbar">
1306
- <button id="m-ham" title="Menú" onclick="document.getElementById('m-menu').classList.add('open')"><span class="ico">☰</span></button>
1307
- <button id="m-cartas" title="Activar / desactivar audio" onclick="toggleAudioEnable&&toggleAudioEnable()">
1306
+ <button id="m-ham" data-i18n-title="lb_ham_tip" title="Menú" onclick="document.getElementById('m-menu').classList.add('open')"><span class="ico">☰</span></button>
1307
+ <button id="m-cartas" data-i18n-title="lb_audio_tip" title="Activar / desactivar audio" onclick="toggleAudioEnable&&toggleAudioEnable()">
1308
1308
  <span class="ico" id="m-mute-icon">🔊</span>
1309
1309
  <span class="lbl" data-i18n="m_silencio">Silencio</span>
1310
1310
  </button>
1311
- <button id="m-snooze" title="Snooze 5 min" onclick="m_toggleSnooze()">
1311
+ <button id="m-snooze" data-i18n-title="lb_snooze_tip" title="Snooze 5 min" onclick="m_toggleSnooze()">
1312
1312
  <span class="ico" id="m-snooze-icon">💤</span>
1313
1313
  <span class="lbl" id="m-snooze-lbl" data-i18n="m_snooze">Snooze</span>
1314
1314
  </button>
1315
- <button id="m-fav" title="Fondeo favorito" onclick="m_favClick()">
1315
+ <button id="m-fav" data-i18n-title="lb_fav_tip" title="Fondeo favorito" onclick="m_favClick()">
1316
1316
  <span class="ico">❤</span>
1317
1317
  <span class="lbl" data-i18n="m_favorito">Favorito</span>
1318
1318
  </button>
1319
- <!-- Rev296: acceso directo a Curvas y Mareas también desde la left bar
1320
- (entre Favorito y KIP). Mismos handlers que los del sidebar derecho. -->
1321
- <button id="m-lb-curvas" title="Curvas" onclick="openPopup('curvas-pop');fetchCurvas&&fetchCurvas()">
1319
+ <button id="m-lb-curvas" data-i18n-title="sb_curvas" title="Curvas" onclick="openPopup('curvas-pop');fetchCurvas&&fetchCurvas()">
1322
1320
  <span class="ico">〰️</span>
1323
1321
  <span class="lbl" data-i18n="sb_curvas">Curvas</span>
1324
1322
  </button>
1325
- <button id="m-lb-mareas" title="Mareas (TidesView)" onclick="m_openMareas&&m_openMareas()">
1323
+ <button id="m-lb-mareas" data-i18n-title="lb_mareas_tip" title="Mareas (TidesView)" onclick="m_openMareas&&m_openMareas()">
1326
1324
  <span class="ico" style="display:inline-flex;align-items:center;justify-content:center">
1327
1325
  <svg viewBox="0 0 44 44" width="30" height="30">
1328
1326
  <rect x="2" y="2" width="40" height="14" rx="2" fill="#ffd966"/>
@@ -1334,10 +1332,10 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1334
1332
  </span>
1335
1333
  <span class="lbl" data-i18n="sb_mareas">Mareas</span>
1336
1334
  </button>
1337
- <button id="m-kip" title="Abrir KIP dashboard" onclick="window.location.href = window.location.origin + '/@mxtommy/kip/'">
1335
+ <button id="m-kip" data-i18n-title="lb_kip_tip" title="Abrir KIP dashboard" onclick="window.location.href = window.location.origin + '/@mxtommy/kip/'">
1338
1336
  <span class="ico" style="font-weight:900;letter-spacing:1px">KIP</span>
1339
1337
  </button>
1340
- <button id="m-fb" title="Abrir Freeboard-SK" onclick="window.location.href = window.location.origin + '/@signalk/freeboard-sk/'">
1338
+ <button id="m-fb" data-i18n-title="lb_fb_tip" title="Abrir Freeboard-SK" onclick="window.location.href = window.location.origin + '/@signalk/freeboard-sk/'">
1341
1339
  <img src="assets/freeboard.png" alt="Freeboard"
1342
1340
  style="width:42px;height:42px;object-fit:contain;background:#fff;border-radius:6px;padding:2px;display:block;box-sizing:border-box"/>
1343
1341
  </button>
@@ -1389,7 +1387,7 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1389
1387
  <span class="vl" id="m-bb-wind-kt-txt">—<span style="font-size:11px;font-weight:700;opacity:.75;margin-left:2px">kts</span></span>
1390
1388
  </div>
1391
1389
  </div>
1392
- <div class="it" data-i18n-title="bb_sonda_tip" title="Profundidad bajo quilla · Sonda" onclick="openPopup&&openPopup('sonda-pop')"><span class="lb" data-i18n="bb_sonda">Sonda</span><span class="vl" id="m-bb-sonda">—</span><span id="m-bb-sonda-status" style="display:none;font-size:9px;font-weight:800;margin-top:2px;text-align:center;line-height:1.1"></span></div>
1390
+ <div class="it" data-i18n-title="bb_sonda_tip" title="Profundidad bajo quilla · Sonda" onclick="openPopup&&openPopup('sonda-pop');typeof fetchSondaData==='function'&&fetchSondaData()"><span class="lb" data-i18n="bb_sonda">Sonda</span><span class="vl" id="m-bb-sonda">—</span><span id="m-bb-sonda-status" style="display:none;font-size:9px;font-weight:800;margin-top:2px;text-align:center;line-height:1.1"></span></div>
1393
1391
  <div class="it" data-i18n-title="bb_cadena_tip" title="Cadena recomendada" onclick="m_openInfoModal&&m_openInfoModal()"><span class="lb" data-i18n="bb_cadena_rec">Cadena rec.</span><span class="vl" id="m-bb-cad">—</span></div>
1394
1392
  <div class="it" data-i18n-title="bb_dist_tip" title="Distancia al ancla" onclick="m_openInfoModal&&m_openInfoModal()"><span class="lb" data-i18n="bb_dist_ancla">Dist. ancla</span><span class="vl" id="m-bb-dist">—</span></div>
1395
1393
  <div class="it m-bb-graph" data-i18n-title="bb_presion_tip" title="Presión barométrica · línea naranja = AHORA · click → Meteo" onclick="toggleMeteo&&toggleMeteo()">
@@ -1593,18 +1591,20 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1593
1591
  </div>
1594
1592
  <!-- Popup info histórico ola: leyenda colores + tabla detallada. -->
1595
1593
  <div class="popup-overlay" id="wave-hist-info-pop" onclick="closePopup('wave-hist-info-pop')">
1596
- <div class="popup-box" onclick="event.stopPropagation()" style="width:620px;max-width:96vw;max-height:88vh;overflow-y:auto;padding:18px 22px">
1597
- <button onclick="closePopup('wave-hist-info-pop')" style="position:absolute;top:8px;right:12px;background:none;border:none;color:#aaa;font-size:24px;cursor:pointer;line-height:1">✕</button>
1598
- <h4 style="margin:0 0 8px 0;font-size:19px">Historial de olas — leyenda y detalle</h4>
1599
- <div style="font-size:14px;color:#9ad;margin-bottom:14px">Cada barra resume 15 minutos. Color = intensidad media medida por el IMU del barco. Altura = oscilación del bin.</div>
1600
- <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-bottom:18px">
1601
- <div style="text-align:center"><div style="width:100%;height:18px;background:#3aa856;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700" data-i18n="m_calma">Calma</div><div style="font-size:10px;color:#888">&lt;0.5°</div></div>
1602
- <div style="text-align:center"><div style="width:100%;height:18px;background:#a3c813;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700">Rizada</div><div style="font-size:10px;color:#888">0.5–1.5°</div></div>
1603
- <div style="text-align:center"><div style="width:100%;height:18px;background:#f3c812;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700">Moderada</div><div style="font-size:10px;color:#888">1.5–3°</div></div>
1604
- <div style="text-align:center"><div style="width:100%;height:18px;background:#f37812;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700">Agitada</div><div style="font-size:10px;color:#888">36°</div></div>
1605
- <div style="text-align:center"><div style="width:100%;height:18px;background:#e74c3c;border-radius:3px"></div><div style="font-size:12px;color:#fff;margin-top:2px;font-weight:700">Fuerte</div><div style="font-size:10px;color:#888">&gt;6°</div></div>
1594
+ <!-- Rev316: ventana wave-hist-info ampliada (antes 620px / 88vh) y todo
1595
+ bilingüe. Estilo título alineado con resto de modales (.m-modal-title). -->
1596
+ <div class="popup-box" onclick="event.stopPropagation()" style="width:900px;max-width:96vw;max-height:92vh;overflow-y:auto;padding:28px 32px">
1597
+ <button onclick="closePopup('wave-hist-info-pop')" style="position:absolute;top:10px;right:14px;background:none;border:none;color:#aaa;font-size:28px;cursor:pointer;line-height:1">✕</button>
1598
+ <h4 style="margin:0 0 12px 0;font-size:24px;font-weight:900;color:#fff" data-i18n="sh_wh_title">Historial de olas — leyenda y detalle</h4>
1599
+ <div style="font-size:16px;color:#9ad;margin-bottom:18px" data-i18n="sh_wh_subtitle">Cada barra resume 15 minutos. Color = intensidad media medida por el IMU del barco. Altura = oscilación del bin.</div>
1600
+ <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:22px">
1601
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#3aa856;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="m_calma">Calma</div><div style="font-size:12px;color:#888">&lt;0.5°</div></div>
1602
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#a3c813;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="sh_wh_rizada">Rizada</div><div style="font-size:12px;color:#888">0.51.5°</div></div>
1603
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#f3c812;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="sh_wh_moderada">Moderada</div><div style="font-size:12px;color:#888">1.5–3°</div></div>
1604
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#f37812;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="sh_wh_agitada">Agitada</div><div style="font-size:12px;color:#888">3–6°</div></div>
1605
+ <div style="text-align:center"><div style="width:100%;height:22px;background:#e74c3c;border-radius:3px"></div><div style="font-size:14px;color:#fff;margin-top:4px;font-weight:700" data-i18n="sh_wh_fuerte">Fuerte</div><div style="font-size:12px;color:#888">&gt;6°</div></div>
1606
1606
  </div>
1607
- <div id="wave-hist-info-table" style="font-size:13px;color:#cfd8dc"></div>
1607
+ <div id="wave-hist-info-table" style="font-size:15px;color:#cfd8dc"></div>
1608
1608
  </div>
1609
1609
  </div>
1610
1610
  <!-- Rev79: popup explicacion del % de proteccion -->
@@ -1776,6 +1776,12 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1776
1776
  <div class="alarm-info"><b style="color:var(--red)" data-i18n="alarma_sonda">🌊 Alarma Sonda</b><br><small style="color:#aaa" data-i18n="alarma_sonda_desc">Riesgo de varada por profundidad</small><div class="alarm-st" id="alarm-sonda-st"></div></div>
1777
1777
  <div class="alarm-sw"><input type="checkbox" id="chk-alarm-sonda" onclick="event.stopPropagation()" onchange="setAlarm('sonda',this.checked)"><label for="chk-alarm-sonda" onclick="event.stopPropagation()"></label></div>
1778
1778
  </div>
1779
+ <!-- Rev316: nuevo selector "Mala condición climática" — alerta cuando
1780
+ la previsión meteo en las próximas 6h tiene viento > 25 kt o ola > 1.5 m. -->
1781
+ <div class="alarm-row" id="ar-weather" onclick="var c=document.getElementById('chk-alarm-weather');c.checked=!c.checked;setAlarm('weather',c.checked)">
1782
+ <div class="alarm-info"><b style="color:#ffb23f" data-i18n="alarma_meteo">⛈️ Mala condición climática</b><br><small style="color:#aaa" data-i18n="alarma_meteo_desc">Aviso si previsión 6h supera viento 25 kt u ola 1.5 m</small><div class="alarm-st" id="alarm-weather-st"></div></div>
1783
+ <div class="alarm-sw"><input type="checkbox" id="chk-alarm-weather" onclick="event.stopPropagation()" onchange="setAlarm('weather',this.checked)"><label for="chk-alarm-weather" onclick="event.stopPropagation()"></label></div>
1784
+ </div>
1779
1785
  </div>
1780
1786
  <div id="alarm-active-list" style="margin-top:6px"></div>
1781
1787
  <!-- Rev184: orden correcto — debajo de alarmas va el VOLUMEN, luego
@@ -2078,6 +2084,31 @@ var _i18n={
2078
2084
  m_viento:{es:'Viento',en:'Wind'},
2079
2085
  m_olas:{es:'Olas',en:'Waves'},
2080
2086
  m_mar_calma:{es:'Mar en calma',en:'Calm sea'},
2087
+ /* Rev316: wave-hist-info popup */
2088
+ sh_wh_title:{es:'Historial de olas — leyenda y detalle',en:'Wave history — legend & details'},
2089
+ sh_wh_subtitle:{es:'Cada barra resume 15 minutos. Color = intensidad media medida por el IMU del barco. Altura = oscilación del bin.',en:'Each bar summarises 15 minutes. Color = average intensity measured by boat IMU. Bar height = bin oscillation.'},
2090
+ sh_wh_no_data:{es:'No hay datos todavía. Echa el ancla y espera al menos 15 min para que aparezcan barras.',en:'No data yet. Drop anchor and wait at least 15 min for bars to appear.'},
2091
+ sh_wh_inicio:{es:'Inicio',en:'Start'},
2092
+ sh_wh_intensidad:{es:'Intensidad',en:'Intensity'},
2093
+ sh_wh_periodo:{es:'Período',en:'Period'},
2094
+ sh_wh_altura:{es:'Altura',en:'Height'},
2095
+ sh_wh_rms:{es:'RMS',en:'RMS'},
2096
+ sh_wh_rizada:{es:'Rizada',en:'Rippled'},
2097
+ sh_wh_moderada:{es:'Moderada',en:'Moderate'},
2098
+ sh_wh_agitada:{es:'Agitada',en:'Rough'},
2099
+ sh_wh_fuerte:{es:'Fuerte',en:'Strong'},
2100
+ sh_wh_actual:{es:'ACTUAL',en:'CURRENT'},
2101
+ /* Rev316: tooltips left bar */
2102
+ lb_ham_tip:{es:'Menú',en:'Menu'},
2103
+ lb_audio_tip:{es:'Activar / desactivar audio',en:'Enable / disable audio'},
2104
+ lb_snooze_tip:{es:'Snooze 5 min',en:'Snooze 5 min'},
2105
+ lb_fav_tip:{es:'Fondeo favorito',en:'Favourite anchorage'},
2106
+ lb_mareas_tip:{es:'Mareas (TidesView)',en:'Tides (TidesView)'},
2107
+ lb_kip_tip:{es:'Abrir KIP dashboard',en:'Open KIP dashboard'},
2108
+ lb_fb_tip:{es:'Abrir Freeboard-SK',en:'Open Freeboard-SK'},
2109
+ /* Rev316: alarma meteo */
2110
+ alarma_meteo:{es:'⛈️ Mala condición climática',en:'⛈️ Bad weather conditions'},
2111
+ alarma_meteo_desc:{es:'Aviso si previsión 6h supera viento 25 kt u ola 1.5 m',en:'Warning if 6h forecast exceeds wind 25 kt or wave 1.5 m'},
2081
2112
  /* Rev313: modal guardar favorito */
2082
2113
  fav_guardar_title:{es:'❤ Guardar fondeo favorito',en:'❤ Save favourite anchorage'},
2083
2114
  fav_guardar_hint:{es:'Nombre del favorito (vacío = nombre automático por geolocalización):',en:'Favourite name (empty = auto name by geolocation):'},
@@ -4852,25 +4883,32 @@ function _renderWaveHistInfo(){
4852
4883
  var bins=Array.isArray(j.bins)?j.bins.slice():[];
4853
4884
  if(j.currentBin)bins.push(j.currentBin);
4854
4885
  if(bins.length===0){
4855
- el.innerHTML='<div style="color:#888;text-align:center;padding:12px">No hay datos todavía. Echa el ancla y espera al menos 15 min para que aparezcan barras.</div>';
4886
+ var _noDataMsg=(typeof T==='function')?T('sh_wh_no_data','No hay datos todavía. Echa el ancla y espera al menos 15 min para que aparezcan barras.'):'No hay datos todavía. Echa el ancla y espera al menos 15 min para que aparezcan barras.';
4887
+ el.innerHTML='<div style="color:#888;text-align:center;padding:12px">'+_noDataMsg+'</div>';
4856
4888
  return;
4857
4889
  }
4858
- /* Encabezado tabla */
4859
- var rows=['<div style="display:grid;grid-template-columns:80px 1fr 60px 70px 90px;gap:4px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.18);font-weight:800;color:#9ad;font-size:12px;letter-spacing:.8px;text-transform:uppercase">',
4860
- '<span>Inicio</span>','<span>Intensidad</span>','<span style="text-align:right">Período</span>','<span style="text-align:right">Altura</span>','<span style="text-align:right">RMS</span>',
4890
+ /* Rev316: headers i18n. */
4891
+ var _tw=function(k,fb){ return (typeof T==='function')?T(k,fb):fb; };
4892
+ var _hInicio=_tw('sh_wh_inicio','Inicio'), _hInten=_tw('sh_wh_intensidad','Intensidad'),
4893
+ _hPer=_tw('sh_wh_periodo','Período'), _hAlt=_tw('sh_wh_altura','Altura'), _hRms=_tw('sh_wh_rms','RMS');
4894
+ var _capCalma=_tw('m_calma','Calma'), _capRiz=_tw('sh_wh_rizada','Rizada'),
4895
+ _capMod=_tw('sh_wh_moderada','Moderada'), _capAgi=_tw('sh_wh_agitada','Agitada'), _capFue=_tw('sh_wh_fuerte','Fuerte');
4896
+ var _capMap={calma:_capCalma,rizada:_capRiz,moderada:_capMod,agitada:_capAgi,fuerte:_capFue};
4897
+ var _badgeActual=_tw('sh_wh_actual','ACTUAL');
4898
+ var rows=['<div style="display:grid;grid-template-columns:80px 1fr 60px 70px 90px;gap:4px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.18);font-weight:800;color:#9ad;font-size:13px;letter-spacing:.8px;text-transform:uppercase">',
4899
+ '<span>'+_hInicio+'</span>','<span>'+_hInten+'</span>','<span style="text-align:right">'+_hPer+'</span>','<span style="text-align:right">'+_hAlt+'</span>','<span style="text-align:right">'+_hRms+'</span>',
4861
4900
  '</div>'];
4862
- /* Más recientes arriba */
4863
4901
  var sorted=bins.slice().reverse();
4864
4902
  for(var i=0;i<sorted.length;i++){
4865
4903
  var b=sorted[i];
4866
4904
  var d=new Date(b.binStartMs);
4867
4905
  var hhmm=('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2);
4868
4906
  var color=_WAVE_INT_COLORS[b.intensityMode]||'#888';
4869
- var intCap=(b.intensityMode||'').charAt(0).toUpperCase()+(b.intensityMode||'').slice(1);
4907
+ var intCap=_capMap[b.intensityMode||'']||((b.intensityMode||'').charAt(0).toUpperCase()+(b.intensityMode||'').slice(1));
4870
4908
  var per=(typeof b.periodAvgSec==='number'&&b.periodAvgSec>0)?b.periodAvgSec.toFixed(1)+'s':'—';
4871
4909
  var alt=(typeof b.heightSigMaxM==='number'&&b.heightSigMaxM>0)?b.heightSigMaxM.toFixed(2).replace('.',',')+' m':'<5cm';
4872
4910
  var rms=(typeof b.rmsMaxDeg==='number'&&b.rmsMaxDeg>0)?b.rmsMaxDeg.toFixed(2)+'°':'—';
4873
- var curBadge=b.isCurrent?' <span style="font-size:10px;color:#ffeb3b;font-weight:800;margin-left:4px">ACTUAL</span>':'';
4911
+ var curBadge=b.isCurrent?' <span style="font-size:10px;color:#ffeb3b;font-weight:800;margin-left:4px">'+_badgeActual+'</span>':'';
4874
4912
  rows.push('<div style="display:grid;grid-template-columns:80px 1fr 60px 70px 90px;gap:4px;padding:5px 0;border-bottom:1px solid rgba(255,255,255,.06);align-items:center">'+
4875
4913
  '<span style="color:#fff;font-weight:700">'+hhmm+'</span>'+
4876
4914
  '<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:'+color+';margin-right:6px;vertical-align:middle"></span>'+intCap+curBadge+'</span>'+
@@ -6701,8 +6739,44 @@ function setAlarm(type,on){
6701
6739
  document.getElementById('alarm-sonda-st').textContent=T('activa');document.getElementById('alarm-sonda-st').style.color='var(--grn)';
6702
6740
  }
6703
6741
  });
6742
+ }else if(type==='weather'){
6743
+ /* Rev316: alarma meteo — frontend-only por ahora. Estado en localStorage
6744
+ y check periódico contra _shelterCache.assessment.hours (próximas 6 h).
6745
+ Si viento > 25 kt u ola > 1.5 m → setAlarmActive('weather', true). */
6746
+ _alarmWeatherEnabled = on;
6747
+ localStorage.setItem('ihm-weather-alarm', on?'1':'0');
6748
+ var st = document.getElementById('alarm-weather-st');
6749
+ if (st) {
6750
+ st.textContent = on ? T('activa','activa') : T('desactivada','desactivada');
6751
+ st.style.color = on ? 'var(--grn)' : '#666';
6752
+ }
6753
+ if (!on) {
6754
+ try { setAlarmActive('weather', false); } catch(_){}
6755
+ } else {
6756
+ try { _checkWeatherAlarm(); } catch(_){}
6757
+ }
6704
6758
  }
6705
6759
  }
6760
+ /* Rev316: estado y comprobación de alarma meteo (frontend-only).
6761
+ Mira las próximas 6h de _shelterCache.assessment.hours buscando viento>25kt
6762
+ u ola>1.5m. Se invoca tras tocar el switch y cada 5 min mientras esté on. */
6763
+ var _alarmWeatherEnabled = (localStorage.getItem('ihm-weather-alarm')==='1');
6764
+ function _checkWeatherAlarm(){
6765
+ if (!_alarmWeatherEnabled) return;
6766
+ try {
6767
+ var asm = (typeof _shelterCache!=='undefined' && _shelterCache) ? _shelterCache.assessment : null;
6768
+ if (!asm || !asm.hours || !asm.hours.length) return;
6769
+ var bad = false;
6770
+ for (var i=0; i<Math.min(6, asm.hours.length); i++){
6771
+ var hr = asm.hours[i];
6772
+ var w = hr.windKt || hr.exposureKt || 0;
6773
+ var wave = hr.waveM || 0;
6774
+ if (w > 25 || wave > 1.5) { bad = true; break; }
6775
+ }
6776
+ setAlarmActive('weather', bad);
6777
+ } catch(_){}
6778
+ }
6779
+ setInterval(_checkWeatherAlarm, 5*60*1000);
6706
6780
  /* ══════════════ CALC SONDA ══════════════ */
6707
6781
  /* ═══ SHARED INPUT STYLE (touch-friendly, wide for fingers) ═══ */
6708
6782
  /* ═══ SHARED INPUT STYLE (touch-friendly) ═══ */
@@ -8110,13 +8184,17 @@ function m_syncBottomBarGraphs(){
8110
8184
  var v = srcVal.textContent.trim();
8111
8185
  if (v && v !== '—') dstVal.textContent = v;
8112
8186
  }
8113
- /* Grade del abrigo: A=verde, B=verde claro, C=naranja, D=naranja oscuro, F=rojo */
8187
+ /* Grade del abrigo: A=verde, B=verde claro, C=naranja, D=naranja oscuro, F=rojo.
8188
+ Rev316: el % del donut leía un mapa hardcoded por letra (A=100, B=80, ...)
8189
+ que NO coincidía con el % real del shelter modal. Ahora lee el valor REAL
8190
+ (asm.scorePercent) del _shelterCache. Si no hay cache aún caemos al mapa
8191
+ como antes. */
8114
8192
  var srcGrade = document.getElementById('wx-shelter-grade');
8115
8193
  var dstGrade = document.getElementById('m-bb-grade');
8116
8194
  var dstDonut = document.getElementById('m-bb-donut');
8117
8195
  var dstDonutText = document.getElementById('m-bb-donut-text');
8118
8196
  var gradeColors = { 'A':'#3aa856','B':'#9ed02e','C':'#ffb23f','D':'#ff7043','F':'#f44336' };
8119
- var donutPct = { 'A':100, 'B':80, 'C':60, 'D':40, 'F':20 };
8197
+ var donutPctFallback = { 'A':100, 'B':80, 'C':60, 'D':40, 'F':20 };
8120
8198
  if (srcGrade && dstGrade){
8121
8199
  var g = (srcGrade.textContent.trim() || '—').toUpperCase();
8122
8200
  if (g && g !== '—' && g !== '?') {
@@ -8124,15 +8202,19 @@ function m_syncBottomBarGraphs(){
8124
8202
  var bgColor = gradeColors[g] || 'rgba(120,120,120,.35)';
8125
8203
  dstGrade.style.background = bgColor;
8126
8204
  dstGrade.style.color = '#fff';
8205
+ /* Pct real del cache shelter; fallback al mapa por letra si no hay. */
8206
+ var realPct = null;
8207
+ try {
8208
+ var _asm = (typeof _shelterCache !== 'undefined' && _shelterCache) ? _shelterCache.assessment : null;
8209
+ if (_asm && typeof _asm.scorePercent === 'number') realPct = Math.round(_asm.scorePercent);
8210
+ } catch(_){}
8211
+ if (realPct === null) realPct = donutPctFallback[g] || 0;
8127
8212
  if (dstDonut) {
8128
- var pct = donutPct[g] || 0;
8129
- dstDonut.setAttribute('stroke-dasharray', pct + ',100');
8213
+ dstDonut.setAttribute('stroke-dasharray', realPct + ',100');
8130
8214
  dstDonut.setAttribute('stroke', bgColor);
8131
8215
  }
8132
- /* Rev286: porcentaje (con %) dentro del donut, no letra. */
8133
8216
  if (dstDonutText) {
8134
- var p = donutPct[g] || 0;
8135
- dstDonutText.textContent = p + '%';
8217
+ dstDonutText.textContent = realPct + '%';
8136
8218
  }
8137
8219
  }
8138
8220
  }