signalk-mareas-ihm 2.3.0 → 2.3.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.1] - 2026-06-23
4
+
5
+ ### English
6
+
7
+ **Skipper feedback round 1 (Vicente / Tunatunes Vigo)**
8
+
9
+ - **New bottom-bar widget "Drop to LW"** — large number = how many centimetres the tide will drop until the next low water (negative). Sub-label = expected final depth. Visible permanently (not only on grounding alert). Lets you decide whether to anchor with enough margin.
10
+ - **New bottom-bar widget "At LW"** — large number = expected depth at next low water (consistent with the main SONDA cell, raw sensor reference, not below-surface). Sub-label = LW time. Amber if <2m, red if <1m.
11
+ - **Depth cell sub-label rewritten** — now compact "(final X.X m)" in parentheses below the depth reading. Coherent with the new widgets above.
12
+ - **Bottom-bar config bug fixed** — disabling and re-enabling cells in the config UI now actually persists. The backend whitelist was filtering out new keys silently.
13
+ - **Left arrow of bottom-bar always visible** when the bar is open, matching the right arrow. Previously the left arrow hid when no horizontal scroll was available, getting covered by the bar.
14
+
15
+ **Signal K data hygiene (skipper feedback)**
16
+
17
+ - `environment.tide.vessel.finalExpctDepthBKeel` — **fixed historic bug**: the path name said "below keel" but the value was actually "below surface" (off by the boat's draft). Now finally publishes the real below-keel value.
18
+ - `environment.tide.finalExpctDepthBKeelResume` — same fix applied to the text summary "Min. depth X m at HH:MM".
19
+ - New: `environment.tide.expectedDropToLW` (m) — how much the tide will drop until next low water.
20
+ - New: `environment.depth.belowKeelExpectedAtLW` (m) — expected depth under keel at next LW.
21
+ - Removed `environment.depth.belowSurfaceExpectedAtLW` (it was confusing — skipper asked for below-keel only).
22
+
23
+ **Dep hygiene**
24
+ - `@signalk/server-api` range tagged with explicit patch `^2.0.0` (was `^2.0`) — cosmetic, fixes Socket.dev "floating dependency" warning. NPM resolution unchanged.
25
+
26
+ ### Español
27
+
28
+ **Feedback navegante ronda 1 (Vicente / Tunatunes Vigo)**
29
+
30
+ - **Nuevo widget bottom-bar "Dif. Bajamar"** — número grande = cuántos centímetros bajará la marea hasta la próxima bajamar (negativo). Sub-label = profundidad final esperada. Visible permanentemente (no solo cuando hay alerta de varada). Permite decidir si fondear con margen suficiente.
31
+ - **Nuevo widget bottom-bar "En B.M."** — número grande = profundidad esperada en la próxima bajamar (coherente con la celda SONDA principal, referencia del sensor crudo, no bajo superficie). Sub-label = hora de la bajamar. Ámbar si <2m, rojo si <1m.
32
+ - **Sub-label de la celda Sonda reescrito** — ahora compacto "(final X.X m)" entre paréntesis bajo el valor de sonda. Coherente con los nuevos widgets.
33
+ - **Bug del config del bottom-bar arreglado** — desactivar y reactivar celdas en el config UI ahora persiste de verdad. El backend filtraba silenciosamente las claves nuevas.
34
+ - **Flecha izquierda del bottom-bar siempre visible** cuando la barra está abierta, igual que la derecha. Antes solo aparecía si había scroll horizontal posible, quedando tapada por la barra.
35
+
36
+ **Limpieza de datos en Signal K (feedback navegante)**
37
+
38
+ - `environment.tide.vessel.finalExpctDepthBKeel` — **bug histórico corregido**: el nombre del path decía "bajo quilla" pero el valor era "bajo superficie" (desfase = calado del barco). Ahora por fin publica el valor real bajo quilla.
39
+ - `environment.tide.finalExpctDepthBKeelResume` — el mismo fix aplicado al resumen textual "Prof. mínima X m a las HH:MM".
40
+ - Nuevo: `environment.tide.expectedDropToLW` (m) — cuánto bajará la marea hasta la próxima bajamar.
41
+ - Nuevo: `environment.depth.belowKeelExpectedAtLW` (m) — sonda esperada bajo quilla en la próxima bajamar.
42
+ - Eliminado `environment.depth.belowSurfaceExpectedAtLW` (creaba confusión — el navegante pidió solo bajo quilla).
43
+
44
+ **Limpieza de deps**
45
+ - `@signalk/server-api` rango etiquetado con patch explícito `^2.0.0` (antes `^2.0`) — cosmético, resuelve el aviso "floating dependency" de Socket.dev. Resolución de NPM idéntica.
46
+
3
47
  ## [2.3.0] - 2026-06-22
4
48
 
5
49
  ### English
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "2.3.0",
3
- "timestamp": "20260622-2245",
4
- "gitHash": "a3e8ddd",
2
+ "version": "2.3.1",
3
+ "timestamp": "20260623-0211",
4
+ "gitHash": "1fe769e",
5
5
  "gitDirty": false,
6
- "builtAt": "2026-06-22T20:45:08.742Z"
6
+ "builtAt": "2026-06-23T00:11:05.914Z"
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 = "Rev487";
52
+ const PLUGIN_REVISION = "Rev492";
53
53
  // Rev478 (C-17): schemaVersion=2. Introduce bloque `grounding` (FSM Physics/
54
54
  // Config/Notification de Rev477) y `gpsAgeMs` (C-12). Frontend cacheado con
55
55
  // expected=1 debe forzar reload. SERVER_INSTANCE_ID detecta restart vivo del
@@ -297,7 +297,10 @@ const MAX_SIDE_BUTTONS_PER_SIDE = 5;
297
297
  // Whitelist de keys soportadas. Cualquier key NO en este set se descarta.
298
298
  // El orden recibido define el orden de pintado; las celdas no listadas se ocultan.
299
299
  // Whitelist incluye celdas opt-in (sunrise, wx_6h, wave) que NO van en defaults.
300
- const BB_CELL_KEYS = ["sog", "heading", "wind", "sonda", "tide", "pres", "abrigo", "calidad", "cad_rec", "dist_ancla", "h_fondeo", "sunrise", "wx_6h", "wave"];
300
+ /* Rev491 (feedback Vicente): añadidas dif_lw y prof_min a la whitelist del
301
+ backend. Sin esto, sanitizeBBOrder() las filtraba al guardar, haciendo que
302
+ no se pudieran reactivar tras desactivarlas en config. */
303
+ const BB_CELL_KEYS = ["sog", "heading", "wind", "sonda", "dif_lw", "prof_min", "tide", "pres", "abrigo", "calidad", "cad_rec", "dist_ancla", "h_fondeo", "sunrise", "wx_6h", "wave"];
301
304
  const DEFAULT_BB_ORDER = ["sog", "heading", "wind", "sonda", "tide", "pres", "abrigo", "calidad", "cad_rec", "dist_ancla", "h_fondeo"];
302
305
  function sanitizeBBOrder(arr) {
303
306
  if (!Array.isArray(arr))
@@ -5260,6 +5263,13 @@ self.addEventListener("fetch", (e) => { /* no-op: pass through to network */ });
5260
5263
  risk: !!_currentGroundingRisk.risk,
5261
5264
  physicalRisk: !!_currentGroundingRisk.physicalRisk,
5262
5265
  expectedMinDepthM: _currentGroundingRisk.expectedMinDepth ?? null,
5266
+ // Rev488 (feedback Vicente): "Bajará máximo X cm" — la magnitud que el
5267
+ // navegante usa para decidir si fondea con margen suficiente. El evaluator
5268
+ // ya lo calcula como remainingDrop = tideNow - nextLowHeight. Lo exponemos
5269
+ // siempre (no solo cuando hay riesgo) para que el bottom-bar muestre el
5270
+ // sub-label permanente.
5271
+ remainingDropM: _currentGroundingRisk.remainingDrop ?? null,
5272
+ depthNowM: _currentGroundingRisk.depthNow ?? null,
5263
5273
  effectiveDraftM: _currentGroundingRisk.effectiveDraft ?? null,
5264
5274
  safetyMarginM: _currentGroundingRisk.safetyMargin ?? null,
5265
5275
  nextLowTimeIso: _currentGroundingRisk.nextLowTime ?? null,
@@ -10842,9 +10852,35 @@ self.addEventListener("fetch", (e) => { /* no-op: pass through to network */ });
10842
10852
  // v1.127: ALWAYS reflects physical reality, regardless of whether alarm is enabled.
10843
10853
  // This is what KIP/external instruments need. Alarm state is in groundingAlarm.
10844
10854
  vesselValues.push({ path: "environment.tide.vessel.groundingRisk", value: physicalRisk });
10845
- // Expected minimum depth at next low tide (2 decimals)
10846
- if (expectedMinDepth != null)
10847
- vesselValues.push({ path: "environment.tide.vessel.finalExpctDepthBKeel", value: Math.round(expectedMinDepth * 100) / 100 });
10855
+ // Rev489-491 (feedback Vicente): publicar SOLO valores BELOW KEEL en SK.
10856
+ // Vicente: "Elimina todos los datos que sean SURFACE, lo que nos interesa
10857
+ // es el dato Below keel o below transducer (que debera ser elegido en la
10858
+ // configuracion). Por ahora belowKeel."
10859
+ //
10860
+ // Paths publicados:
10861
+ // environment.tide.expectedDropToLW = bajada hasta proxima bajamar (m)
10862
+ // environment.depth.belowKeelExpectedAtLW = sonda final esperada bajo
10863
+ // quilla (m). Coherente con environment.depth.belowKeel actual.
10864
+ // environment.tide.vessel.finalExpctDepthBKeel = MISMO valor que
10865
+ // belowKeelExpectedAtLW. Compatibilidad con KIP/integraciones que ya
10866
+ // usaban este path historico. CORREGIDO Rev491: antes publicaba el
10867
+ // valor belowSurface por bug historico (el nombre era BKeel pero el
10868
+ // valor era surface). Ahora corregido a belowKeel real.
10869
+ //
10870
+ // SK paths ELIMINADOS Rev491 (eran SURFACE, confusion garantizada):
10871
+ // environment.depth.belowSurfaceExpectedAtLW
10872
+ if (remainingDrop != null) {
10873
+ vesselValues.push({ path: "environment.tide.expectedDropToLW", value: Math.round(remainingDrop * 100) / 100 });
10874
+ }
10875
+ if (expectedMinDepth != null && typeof draft === "number" && draft > 0) {
10876
+ /* CUIDADO: baseDraft incluye el safetyMargin. Para belowKeel real
10877
+ restamos SOLO el calado, no el baseDraft. */
10878
+ const belowKeelAtLW = Math.round((expectedMinDepth - draft) * 100) / 100;
10879
+ vesselValues.push({ path: "environment.depth.belowKeelExpectedAtLW", value: belowKeelAtLW });
10880
+ /* Path historico — ahora publica el MISMO valor (belowKeel real) para
10881
+ que finalmente haga honor a su nombre "BKeel". */
10882
+ vesselValues.push({ path: "environment.tide.vessel.finalExpctDepthBKeel", value: belowKeelAtLW });
10883
+ }
10848
10884
  // v1.127: NEW — Free water between effective draft and seabed at next low tide.
10849
10885
  // Positive = safe clearance. Negative = GROUNDING (keel below seabed).
10850
10886
  if (expectedMinDepth != null && eff != null) {
package/dist/mobile.html CHANGED
@@ -1892,6 +1892,22 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1892
1892
  <span class="vl" id="m-bb-tide-val">—</span>
1893
1893
  <span id="m-bb-tide-next" style="display:block;font-size:10px;font-weight:800;margin-top:1px;line-height:1;color:#cde">—</span>
1894
1894
  </div>
1895
+ <!-- Rev489 (feedback Vicente): widget "Dif. Bajamar" — cuánto bajará la
1896
+ marea hasta la próxima bajamar (cm). Click abre modal Cálculo Sonda. -->
1897
+ <div class="it" data-bb-cell="dif_lw" data-i18n-title="bb_dif_lw_tip" title="Diferencia hasta próxima bajamar · click → Cálculo Sonda" onclick="openPopup&&openPopup('sonda-pop');typeof fetchSondaData==='function'&&fetchSondaData()">
1898
+ <span class="lb" data-i18n="bb_dif_lw">Dif. Bajamar</span>
1899
+ <span class="vl" id="m-bb-dif-lw">—</span>
1900
+ <span id="m-bb-dif-lw-end" style="display:block;font-size:10px;font-weight:800;margin-top:1px;line-height:1;color:#cde">—</span>
1901
+ </div>
1902
+ <!-- Rev490 (feedback Vicente): widget "Prof. Mínima" — sonda esperada en la
1903
+ próxima bajamar. Numero grande = profundidad final en metros (coherente
1904
+ con el numero grande de SONDA, misma referencia). Sub-label = hora de la
1905
+ proxima bajamar. Click abre modal Cálculo Sonda. -->
1906
+ <div class="it" data-bb-cell="prof_min" data-i18n-title="bb_prof_min_tip" title="Profundidad mínima esperada en la próxima bajamar · click → Cálculo Sonda" onclick="openPopup&&openPopup('sonda-pop');typeof fetchSondaData==='function'&&fetchSondaData()">
1907
+ <span class="lb" data-i18n="bb_prof_min">Prof. mín.</span>
1908
+ <span class="vl" id="m-bb-prof-min">—</span>
1909
+ <span id="m-bb-prof-min-when" style="display:block;font-size:10px;font-weight:800;margin-top:1px;line-height:1;color:#cde">—</span>
1910
+ </div>
1895
1911
  <div class="it" data-bb-cell="cad_rec" 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>
1896
1912
  <div class="it" data-bb-cell="dist_ancla" 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>
1897
1913
  <!-- Rev331 (item 13): HORA del fondeo (HH:MM del drop, +N si han pasado días). Siempre visible con '—' cuando no fondeado. -->
@@ -2711,10 +2727,36 @@ var _unitsSystem = localStorage.getItem('ihm-visor-units') || 'metric_nautical';
2711
2727
  window._sideButtons = j.buttons.slice();
2712
2728
  try{ if(typeof m_renderSideButtons === 'function') m_renderSideButtons(window._sideButtons); }catch(_){}
2713
2729
  }
2714
- /* Sprint F (Rev454): cargar y aplicar orden/visibilidad del bottom-bar. */
2730
+ /* Sprint F (Rev454): cargar y aplicar orden/visibilidad del bottom-bar.
2731
+ Rev491: AUTO-MIGRACIoN — si el bbOrder guardado no contiene celdas
2732
+ nuevas (dif_lw, prof_min, etc.) las insertamos automaticamente tras
2733
+ 'sonda' para que el usuario las vea sin tener que entrar a config. */
2715
2734
  if (j && Array.isArray(j.bbOrder)) {
2716
- window._bbOrder = j.bbOrder.slice();
2735
+ var loaded = j.bbOrder.slice();
2736
+ var MIGRATE_TARGETS = ['dif_lw','prof_min'];
2737
+ var anchorKey = 'sonda';
2738
+ var anchorIdx = loaded.indexOf(anchorKey);
2739
+ var didMigrate = false;
2740
+ MIGRATE_TARGETS.forEach(function(k){
2741
+ if (loaded.indexOf(k) < 0) {
2742
+ if (anchorIdx >= 0) {
2743
+ loaded.splice(anchorIdx + 1, 0, k);
2744
+ anchorIdx++; // siguiente nueva celda va tras esta
2745
+ } else {
2746
+ loaded.push(k);
2747
+ }
2748
+ didMigrate = true;
2749
+ }
2750
+ });
2751
+ window._bbOrder = loaded;
2717
2752
  try{ if(typeof m_applyBBOrder === 'function') m_applyBBOrder(window._bbOrder); }catch(_){}
2753
+ /* Si migramos, persistir para que el siguiente boot no vuelva a hacerlo. */
2754
+ if (didMigrate) {
2755
+ try {
2756
+ fetch(A+'/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({bbOrder:loaded})}).catch(function(){});
2757
+ console.log('[IHM-BB] migrated bbOrder, added missing cells:', MIGRATE_TARGETS.filter(function(k){return j.bbOrder.indexOf(k)<0;}));
2758
+ } catch(_){}
2759
+ }
2718
2760
  }
2719
2761
  }).catch(function(){});
2720
2762
  })();
@@ -2723,8 +2765,11 @@ var _unitsSystem = localStorage.getItem('ihm-visor-units') || 'metric_nautical';
2723
2765
  Whitelist de keys + defaults. Cada celda tiene data-bb-cell="key".
2724
2766
  m_applyBBOrder(arr): reordena los hijos del #m-bottom-bar segun arr y
2725
2767
  oculta los que no esten en arr (display:none). */
2726
- window._BB_CELL_KEYS = ["sog","heading","wind","sonda","tide","pres","abrigo","calidad","cad_rec","dist_ancla","h_fondeo","sunrise","wx_6h","wave"];
2727
- window._BB_DEFAULT_ORDER = ["sog","heading","wind","sonda","tide","pres","abrigo","calidad","cad_rec","dist_ancla","h_fondeo"];
2768
+ /* Rev489-490: dos celdas nuevas tras "sonda""dif_lw" (bajada en cm hasta
2769
+ próxima bajamar) y "prof_min" (sonda esperada en esa bajamar). Vicente
2770
+ pidió ambas para fondear con margen. */
2771
+ window._BB_CELL_KEYS = ["sog","heading","wind","sonda","dif_lw","prof_min","tide","pres","abrigo","calidad","cad_rec","dist_ancla","h_fondeo","sunrise","wx_6h","wave"];
2772
+ window._BB_DEFAULT_ORDER = ["sog","heading","wind","sonda","dif_lw","prof_min","tide","pres","abrigo","calidad","cad_rec","dist_ancla","h_fondeo"];
2728
2773
  window._bbOrder = window._bbOrder || window._BB_DEFAULT_ORDER.slice();
2729
2774
  function m_applyBBOrder(arr){
2730
2775
  var bar = document.getElementById('m-bottom-bar');
@@ -3615,6 +3660,14 @@ var _i18n={
3615
3660
  /* Sprint C (Rev452): celda marea bottom-bar. */
3616
3661
  bb_marea:{es:'Marea',en:'Tide'},
3617
3662
  bb_marea_tip:{es:'Estado marea: subiendo (▲) o bajando (▼) · % entre extremos · proximo extremo y hora · click → Curvas',en:'Tide state: rising (▲) or falling (▼) · % between extremes · next extreme + time · click → Tide chart'},
3663
+ /* Rev489 (feedback Vicente): nueva celda Diferencia Bajamar. */
3664
+ bb_dif_lw:{es:'Dif. Bajamar',en:'Drop to LW'},
3665
+ bb_dif_lw_tip:{es:'Cuánto bajará la marea hasta la próxima bajamar (cm) y sonda final esperada · click → Cálculo Sonda',en:'How much the tide will drop until next low water (cm) and expected final depth · click → Depth calc'},
3666
+ /* Rev490 (feedback Vicente): nueva celda Profundidad Mínima esperada.
3667
+ Rev491: titulo cambiado a "En B.M." (En Bajamar) — mas claro para el
3668
+ navegante que "Prof. mín." segun Vicente. */
3669
+ bb_prof_min:{es:'En B.M.',en:'At LW'},
3670
+ bb_prof_min_tip:{es:'Profundidad mínima esperada en la próxima bajamar (coherente con sonda actual) · click → Cálculo Sonda',en:'Minimum expected depth at next low water (consistent with current depth display) · click → Depth calc'},
3618
3671
  tide_pm_short:{es:'PM',en:'HW'},
3619
3672
  tide_bm_short:{es:'BM',en:'LW'},
3620
3673
  /* Sprint F (Rev454): nueva celda Heading + motor bottom-bar configurable. */
@@ -5341,6 +5394,9 @@ function _groundingDisplay(s){
5341
5394
  physicalRisk: (g.physics.state === 'danger') || legacyPhysRisk,
5342
5395
  currentDepthM: (typeof g.physics.depthBelowSurfaceM === 'number') ? g.physics.depthBelowSurfaceM : null,
5343
5396
  expectedMinDepthM:(typeof g.physics.expectedMinDepthM === 'number') ? g.physics.expectedMinDepthM : null,
5397
+ /* Rev488: remainingDropM = cuanto bajara la marea hasta la proxima bajamar.
5398
+ Vicente lo pidio expresamente como dato principal para decidir fondear. */
5399
+ remainingDropM: (typeof (s.groundingDetail||{}).remainingDropM === 'number') ? s.groundingDetail.remainingDropM : null,
5344
5400
  effectiveDraftM: (typeof g.config.effectiveDraftM === 'number') ? g.config.effectiveDraftM
5345
5401
  : (typeof g.config.draftM === 'number' && typeof g.config.safetyMarginM === 'number'
5346
5402
  ? (g.config.draftM + g.config.safetyMarginM) * 1.15 : null),
@@ -5363,6 +5419,7 @@ function _groundingDisplay(s){
5363
5419
  physicalRisk: typeof gd.physicalRisk === 'boolean' ? gd.physicalRisk : null,
5364
5420
  currentDepthM: (typeof gd.depthNowM === 'number') ? gd.depthNowM : null,
5365
5421
  expectedMinDepthM:(typeof gd.expectedMinDepthM === 'number') ? gd.expectedMinDepthM : null,
5422
+ remainingDropM: (typeof gd.remainingDropM === 'number') ? gd.remainingDropM : null,
5366
5423
  effectiveDraftM: (typeof gd.effectiveDraftM === 'number') ? gd.effectiveDraftM : null,
5367
5424
  nextLowTimeIso: gd.nextLowTimeIso || null,
5368
5425
  timeUntilMin: (typeof gd.timeUntilMin === 'number') ? gd.timeUntilMin : null,
@@ -11644,10 +11701,110 @@ function m_pollBottomBar(){
11644
11701
  sublabel = (typeof T==='function') ? T('bb_sonda_riesgo','⚠ Atención: riesgo varada') : '⚠ Atención: riesgo varada';
11645
11702
  }
11646
11703
  sst.textContent = sublabel; sst.style.color = '#f44336'; sst.style.display = 'block';
11647
- } else if (s.tideResume) {
11648
- sst.textContent = (typeof T==='function')?T('bb_sonda_ok','OK · sin riesgo de varada'):'OK · sin riesgo de varada'; sst.style.color = '#66ffaa'; sst.style.display = 'block';
11649
- } else { sst.style.display = 'none'; }
11704
+ } else {
11705
+ /* Rev488 (feedback Vicente): mostrar SIEMPRE el dato de la proxima
11706
+ bajamar, este la marea subiendo o bajando. El navegante decide si
11707
+ fondea en funcion de "cuanto bajara hasta la proxima LW" y "que
11708
+ profundidad final habra". Si la marea esta subiendo y la proxima LW
11709
+ es similar o mas alta que ahora (raro pero posible en cambios de
11710
+ coeficiente), el drop sera ~0 y mostramos solo la profundidad final.
11711
+
11712
+ FIX coherencia (Vicente QA): el backend calcula expectedMinDepth en
11713
+ referencia belowSurface (incluye calado), pero el numero grande de
11714
+ SONDA muestra valor RAW del sensor (sin calado). Para que "Fin Y.Z"
11715
+ sea visualmente coherente con el numero grande, calculamos fin como
11716
+ "valor_display_actual - bajada", no usamos el del backend directo.
11717
+ Ej: sonda raw 4.2m, bajada 1.59m -> fin 2.6m (no 3.9m). */
11718
+ var dropM = _gdisp.remainingDropM;
11719
+ var finM;
11720
+ if (typeof _lastDepthM === 'number' && isFinite(_lastDepthM)
11721
+ && typeof dropM === 'number' && isFinite(dropM)) {
11722
+ finM = _lastDepthM - dropM;
11723
+ } else {
11724
+ finM = _gdisp.expectedMinDepthM; /* fallback backend (referencia belowSurface) */
11725
+ }
11726
+ var en2 = (_lang === 'en');
11727
+ if (typeof finM === 'number' && isFinite(finM)) {
11728
+ var critical = finM < 1.0; /* < 1m de sonda final = atencion aunque alarma OFF */
11729
+ /* Rev490 (feedback usuario): sub-label SONDA mas compacto entre
11730
+ parentesis. Los detalles "bajada -Xcm" + "hora LW" ya viven en
11731
+ los widgets propios "Dif. Bajamar" y "Prof. min.". */
11732
+ sst.textContent = '(' + (en2 ? 'final ' : 'final ') + unitFmt.depth(finM, 1) + ')';
11733
+ sst.style.color = critical ? '#ffb23f' : '#66ffaa';
11734
+ sst.style.display = 'block';
11735
+ } else if (s.tideResume) {
11736
+ /* Fallback: sin datos numericos pero hay resumen de marea. */
11737
+ sst.textContent = (typeof T==='function')?T('bb_sonda_ok','OK · sin riesgo de varada'):'OK · sin riesgo de varada';
11738
+ sst.style.color = '#66ffaa';
11739
+ sst.style.display = 'block';
11740
+ } else { sst.style.display = 'none'; }
11741
+ }
11650
11742
  }
11743
+ /* Rev489 (feedback Vicente): widget propio "Diferencia Bajamar". Numero
11744
+ grande = bajada en cm (negativo); sub-label = sonda esperada al final.
11745
+ Tiene su propio scope: lee del helper _gdisp y recalcula fin coherente
11746
+ con el numero de sonda mostrado (depthSounderRawM, no belowSurface). */
11747
+ try {
11748
+ var difEl = document.getElementById('m-bb-dif-lw');
11749
+ var difEnd = document.getElementById('m-bb-dif-lw-end');
11750
+ if (difEl) {
11751
+ var _dropM2 = _gdisp ? _gdisp.remainingDropM : null;
11752
+ var _finM2;
11753
+ if (typeof _lastDepthM === 'number' && isFinite(_lastDepthM)
11754
+ && typeof _dropM2 === 'number' && isFinite(_dropM2)) {
11755
+ _finM2 = _lastDepthM - _dropM2;
11756
+ } else if (_gdisp) {
11757
+ _finM2 = _gdisp.expectedMinDepthM;
11758
+ }
11759
+ if (typeof _dropM2 === 'number' && isFinite(_dropM2) && _dropM2 > 0.005) {
11760
+ difEl.textContent = '-' + Math.round(_dropM2*100) + ' cm';
11761
+ difEl.style.color = (typeof _finM2 === 'number' && _finM2 < 1.0) ? '#ffb23f' : '#ffd166';
11762
+ } else if (typeof _dropM2 === 'number' && isFinite(_dropM2)) {
11763
+ difEl.textContent = '0 cm';
11764
+ difEl.style.color = '#9aa';
11765
+ } else {
11766
+ difEl.textContent = '—';
11767
+ difEl.style.color = '';
11768
+ }
11769
+ if (difEnd) {
11770
+ if (typeof _finM2 === 'number' && isFinite(_finM2)) {
11771
+ var enL = (_lang === 'en');
11772
+ difEnd.textContent = (enL ? 'End ' : 'Fin ') + unitFmt.depth(_finM2, 1);
11773
+ difEnd.style.color = (_finM2 < 1.0) ? '#ff7043' : '#cde';
11774
+ } else {
11775
+ difEnd.textContent = '—';
11776
+ difEnd.style.color = '';
11777
+ }
11778
+ }
11779
+ }
11780
+ /* Rev490: widget "Prof. mín." — sonda esperada en proxima bajamar. */
11781
+ var pmEl = document.getElementById('m-bb-prof-min');
11782
+ var pmWhen = document.getElementById('m-bb-prof-min-when');
11783
+ if (pmEl) {
11784
+ if (typeof _finM2 === 'number' && isFinite(_finM2)) {
11785
+ pmEl.textContent = unitFmt.depth(_finM2, 1);
11786
+ pmEl.style.color = (_finM2 < 1.0) ? '#ff5252' : (_finM2 < 2.0 ? '#ffb23f' : '');
11787
+ } else {
11788
+ pmEl.textContent = '—';
11789
+ pmEl.style.color = '';
11790
+ }
11791
+ if (pmWhen) {
11792
+ var lwIso = _gdisp ? _gdisp.nextLowTimeIso : null;
11793
+ if (lwIso) {
11794
+ try {
11795
+ var dLW = new Date(lwIso);
11796
+ var pad2 = function(n){ return ('0'+n).slice(-2); };
11797
+ var enL2 = (_lang === 'en');
11798
+ pmWhen.textContent = (enL2 ? 'at LW ' : 'a BM ') + pad2(dLW.getHours()) + ':' + pad2(dLW.getMinutes());
11799
+ pmWhen.style.color = '#cde';
11800
+ } catch(_){ pmWhen.textContent = '—'; }
11801
+ } else {
11802
+ pmWhen.textContent = '—';
11803
+ pmWhen.style.color = '';
11804
+ }
11805
+ }
11806
+ }
11807
+ } catch(_){}
11651
11808
  /* Cadena recomendada (en metros) */
11652
11809
  var cad = s.chainRecommended;
11653
11810
  if (cad == null && s.approach) cad = s.approach.chainRecNow;
@@ -12495,7 +12652,11 @@ function m_updateBBScrollIndicators(){
12495
12652
  L.textContent = '▲'; L.classList.add('show');
12496
12653
  R.textContent = '▲'; R.classList.add('show');
12497
12654
  } else {
12498
- L.textContent = '◀'; L.classList.toggle('show', canL);
12655
+ /* Rev491 (feedback Vicente QA): replicar comportamiento derecha exacto.
12656
+ La izquierda ahora tambien SIEMPRE visible cuando la bar esta abierta,
12657
+ igual que la derecha. Antes solo se mostraba si canL (scroll disponible),
12658
+ lo que la dejaba oculta cuando el bottom-bar cabia entero en pantalla. */
12659
+ L.textContent = '◀'; L.classList.add('show');
12499
12660
  R.textContent = '▶'; R.classList.add('show');
12500
12661
  }
12501
12662
  _syncToggleGlow();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-mareas-ihm",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "User-friendly UI, multi-layer charts and transparencies (online and offline), AIS anti-collision surveillance, multi-device ringing alarms (drift, AIS, grounding, weather), shelter forecast and assessment.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -101,24 +101,24 @@
101
101
  ],
102
102
  "license": "Apache-2.0",
103
103
  "dependencies": {
104
- "@signalk/server-api": "^2.0",
104
+ "@signalk/server-api": "^2.0.0",
105
105
  "better-sqlite3": "^11.0.0",
106
106
  "leaflet": "^1.9.4",
107
- "moment-timezone": "^0.5.48",
107
+ "moment-timezone": "^0.6.2",
108
108
  "pdf-parse": "^1.1.1"
109
109
  },
110
110
  "devDependencies": {
111
111
  "@eslint/js": "^9.21.0",
112
112
  "@react-hook/resize-observer": "^2.0.2",
113
113
  "@tailwindcss/vite": "^4.1.3",
114
- "@types/node": "^25.0.3",
114
+ "@types/node": "^26.0.0",
115
115
  "@types/react": "^19.0.12",
116
116
  "@types/react-dom": "^19.0.4",
117
117
  "@vitejs/plugin-react": "^5.0.2",
118
118
  "eslint": "^9.21.0",
119
119
  "eslint-plugin-react-hooks": "^7.0.1",
120
- "eslint-plugin-react-refresh": "^0.4.19",
121
- "globals": "^16.3.0",
120
+ "eslint-plugin-react-refresh": "^0.5.3",
121
+ "globals": "^17.6.0",
122
122
  "react": "^19.1.0",
123
123
  "react-dom": "^19.1.0",
124
124
  "signalk-server": "^2.13.5",
@@ -1892,6 +1892,22 @@ body.mobile-ui #shelter-hours .sh-hr{flex:0 0 60px!important}
1892
1892
  <span class="vl" id="m-bb-tide-val">—</span>
1893
1893
  <span id="m-bb-tide-next" style="display:block;font-size:10px;font-weight:800;margin-top:1px;line-height:1;color:#cde">—</span>
1894
1894
  </div>
1895
+ <!-- Rev489 (feedback Vicente): widget "Dif. Bajamar" — cuánto bajará la
1896
+ marea hasta la próxima bajamar (cm). Click abre modal Cálculo Sonda. -->
1897
+ <div class="it" data-bb-cell="dif_lw" data-i18n-title="bb_dif_lw_tip" title="Diferencia hasta próxima bajamar · click → Cálculo Sonda" onclick="openPopup&&openPopup('sonda-pop');typeof fetchSondaData==='function'&&fetchSondaData()">
1898
+ <span class="lb" data-i18n="bb_dif_lw">Dif. Bajamar</span>
1899
+ <span class="vl" id="m-bb-dif-lw">—</span>
1900
+ <span id="m-bb-dif-lw-end" style="display:block;font-size:10px;font-weight:800;margin-top:1px;line-height:1;color:#cde">—</span>
1901
+ </div>
1902
+ <!-- Rev490 (feedback Vicente): widget "Prof. Mínima" — sonda esperada en la
1903
+ próxima bajamar. Numero grande = profundidad final en metros (coherente
1904
+ con el numero grande de SONDA, misma referencia). Sub-label = hora de la
1905
+ proxima bajamar. Click abre modal Cálculo Sonda. -->
1906
+ <div class="it" data-bb-cell="prof_min" data-i18n-title="bb_prof_min_tip" title="Profundidad mínima esperada en la próxima bajamar · click → Cálculo Sonda" onclick="openPopup&&openPopup('sonda-pop');typeof fetchSondaData==='function'&&fetchSondaData()">
1907
+ <span class="lb" data-i18n="bb_prof_min">Prof. mín.</span>
1908
+ <span class="vl" id="m-bb-prof-min">—</span>
1909
+ <span id="m-bb-prof-min-when" style="display:block;font-size:10px;font-weight:800;margin-top:1px;line-height:1;color:#cde">—</span>
1910
+ </div>
1895
1911
  <div class="it" data-bb-cell="cad_rec" 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>
1896
1912
  <div class="it" data-bb-cell="dist_ancla" 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>
1897
1913
  <!-- Rev331 (item 13): HORA del fondeo (HH:MM del drop, +N si han pasado días). Siempre visible con '—' cuando no fondeado. -->
@@ -2711,10 +2727,36 @@ var _unitsSystem = localStorage.getItem('ihm-visor-units') || 'metric_nautical';
2711
2727
  window._sideButtons = j.buttons.slice();
2712
2728
  try{ if(typeof m_renderSideButtons === 'function') m_renderSideButtons(window._sideButtons); }catch(_){}
2713
2729
  }
2714
- /* Sprint F (Rev454): cargar y aplicar orden/visibilidad del bottom-bar. */
2730
+ /* Sprint F (Rev454): cargar y aplicar orden/visibilidad del bottom-bar.
2731
+ Rev491: AUTO-MIGRACIoN — si el bbOrder guardado no contiene celdas
2732
+ nuevas (dif_lw, prof_min, etc.) las insertamos automaticamente tras
2733
+ 'sonda' para que el usuario las vea sin tener que entrar a config. */
2715
2734
  if (j && Array.isArray(j.bbOrder)) {
2716
- window._bbOrder = j.bbOrder.slice();
2735
+ var loaded = j.bbOrder.slice();
2736
+ var MIGRATE_TARGETS = ['dif_lw','prof_min'];
2737
+ var anchorKey = 'sonda';
2738
+ var anchorIdx = loaded.indexOf(anchorKey);
2739
+ var didMigrate = false;
2740
+ MIGRATE_TARGETS.forEach(function(k){
2741
+ if (loaded.indexOf(k) < 0) {
2742
+ if (anchorIdx >= 0) {
2743
+ loaded.splice(anchorIdx + 1, 0, k);
2744
+ anchorIdx++; // siguiente nueva celda va tras esta
2745
+ } else {
2746
+ loaded.push(k);
2747
+ }
2748
+ didMigrate = true;
2749
+ }
2750
+ });
2751
+ window._bbOrder = loaded;
2717
2752
  try{ if(typeof m_applyBBOrder === 'function') m_applyBBOrder(window._bbOrder); }catch(_){}
2753
+ /* Si migramos, persistir para que el siguiente boot no vuelva a hacerlo. */
2754
+ if (didMigrate) {
2755
+ try {
2756
+ fetch(A+'/settings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({bbOrder:loaded})}).catch(function(){});
2757
+ console.log('[IHM-BB] migrated bbOrder, added missing cells:', MIGRATE_TARGETS.filter(function(k){return j.bbOrder.indexOf(k)<0;}));
2758
+ } catch(_){}
2759
+ }
2718
2760
  }
2719
2761
  }).catch(function(){});
2720
2762
  })();
@@ -2723,8 +2765,11 @@ var _unitsSystem = localStorage.getItem('ihm-visor-units') || 'metric_nautical';
2723
2765
  Whitelist de keys + defaults. Cada celda tiene data-bb-cell="key".
2724
2766
  m_applyBBOrder(arr): reordena los hijos del #m-bottom-bar segun arr y
2725
2767
  oculta los que no esten en arr (display:none). */
2726
- window._BB_CELL_KEYS = ["sog","heading","wind","sonda","tide","pres","abrigo","calidad","cad_rec","dist_ancla","h_fondeo","sunrise","wx_6h","wave"];
2727
- window._BB_DEFAULT_ORDER = ["sog","heading","wind","sonda","tide","pres","abrigo","calidad","cad_rec","dist_ancla","h_fondeo"];
2768
+ /* Rev489-490: dos celdas nuevas tras "sonda""dif_lw" (bajada en cm hasta
2769
+ próxima bajamar) y "prof_min" (sonda esperada en esa bajamar). Vicente
2770
+ pidió ambas para fondear con margen. */
2771
+ window._BB_CELL_KEYS = ["sog","heading","wind","sonda","dif_lw","prof_min","tide","pres","abrigo","calidad","cad_rec","dist_ancla","h_fondeo","sunrise","wx_6h","wave"];
2772
+ window._BB_DEFAULT_ORDER = ["sog","heading","wind","sonda","dif_lw","prof_min","tide","pres","abrigo","calidad","cad_rec","dist_ancla","h_fondeo"];
2728
2773
  window._bbOrder = window._bbOrder || window._BB_DEFAULT_ORDER.slice();
2729
2774
  function m_applyBBOrder(arr){
2730
2775
  var bar = document.getElementById('m-bottom-bar');
@@ -3615,6 +3660,14 @@ var _i18n={
3615
3660
  /* Sprint C (Rev452): celda marea bottom-bar. */
3616
3661
  bb_marea:{es:'Marea',en:'Tide'},
3617
3662
  bb_marea_tip:{es:'Estado marea: subiendo (▲) o bajando (▼) · % entre extremos · proximo extremo y hora · click → Curvas',en:'Tide state: rising (▲) or falling (▼) · % between extremes · next extreme + time · click → Tide chart'},
3663
+ /* Rev489 (feedback Vicente): nueva celda Diferencia Bajamar. */
3664
+ bb_dif_lw:{es:'Dif. Bajamar',en:'Drop to LW'},
3665
+ bb_dif_lw_tip:{es:'Cuánto bajará la marea hasta la próxima bajamar (cm) y sonda final esperada · click → Cálculo Sonda',en:'How much the tide will drop until next low water (cm) and expected final depth · click → Depth calc'},
3666
+ /* Rev490 (feedback Vicente): nueva celda Profundidad Mínima esperada.
3667
+ Rev491: titulo cambiado a "En B.M." (En Bajamar) — mas claro para el
3668
+ navegante que "Prof. mín." segun Vicente. */
3669
+ bb_prof_min:{es:'En B.M.',en:'At LW'},
3670
+ bb_prof_min_tip:{es:'Profundidad mínima esperada en la próxima bajamar (coherente con sonda actual) · click → Cálculo Sonda',en:'Minimum expected depth at next low water (consistent with current depth display) · click → Depth calc'},
3618
3671
  tide_pm_short:{es:'PM',en:'HW'},
3619
3672
  tide_bm_short:{es:'BM',en:'LW'},
3620
3673
  /* Sprint F (Rev454): nueva celda Heading + motor bottom-bar configurable. */
@@ -5341,6 +5394,9 @@ function _groundingDisplay(s){
5341
5394
  physicalRisk: (g.physics.state === 'danger') || legacyPhysRisk,
5342
5395
  currentDepthM: (typeof g.physics.depthBelowSurfaceM === 'number') ? g.physics.depthBelowSurfaceM : null,
5343
5396
  expectedMinDepthM:(typeof g.physics.expectedMinDepthM === 'number') ? g.physics.expectedMinDepthM : null,
5397
+ /* Rev488: remainingDropM = cuanto bajara la marea hasta la proxima bajamar.
5398
+ Vicente lo pidio expresamente como dato principal para decidir fondear. */
5399
+ remainingDropM: (typeof (s.groundingDetail||{}).remainingDropM === 'number') ? s.groundingDetail.remainingDropM : null,
5344
5400
  effectiveDraftM: (typeof g.config.effectiveDraftM === 'number') ? g.config.effectiveDraftM
5345
5401
  : (typeof g.config.draftM === 'number' && typeof g.config.safetyMarginM === 'number'
5346
5402
  ? (g.config.draftM + g.config.safetyMarginM) * 1.15 : null),
@@ -5363,6 +5419,7 @@ function _groundingDisplay(s){
5363
5419
  physicalRisk: typeof gd.physicalRisk === 'boolean' ? gd.physicalRisk : null,
5364
5420
  currentDepthM: (typeof gd.depthNowM === 'number') ? gd.depthNowM : null,
5365
5421
  expectedMinDepthM:(typeof gd.expectedMinDepthM === 'number') ? gd.expectedMinDepthM : null,
5422
+ remainingDropM: (typeof gd.remainingDropM === 'number') ? gd.remainingDropM : null,
5366
5423
  effectiveDraftM: (typeof gd.effectiveDraftM === 'number') ? gd.effectiveDraftM : null,
5367
5424
  nextLowTimeIso: gd.nextLowTimeIso || null,
5368
5425
  timeUntilMin: (typeof gd.timeUntilMin === 'number') ? gd.timeUntilMin : null,
@@ -11644,10 +11701,110 @@ function m_pollBottomBar(){
11644
11701
  sublabel = (typeof T==='function') ? T('bb_sonda_riesgo','⚠ Atención: riesgo varada') : '⚠ Atención: riesgo varada';
11645
11702
  }
11646
11703
  sst.textContent = sublabel; sst.style.color = '#f44336'; sst.style.display = 'block';
11647
- } else if (s.tideResume) {
11648
- sst.textContent = (typeof T==='function')?T('bb_sonda_ok','OK · sin riesgo de varada'):'OK · sin riesgo de varada'; sst.style.color = '#66ffaa'; sst.style.display = 'block';
11649
- } else { sst.style.display = 'none'; }
11704
+ } else {
11705
+ /* Rev488 (feedback Vicente): mostrar SIEMPRE el dato de la proxima
11706
+ bajamar, este la marea subiendo o bajando. El navegante decide si
11707
+ fondea en funcion de "cuanto bajara hasta la proxima LW" y "que
11708
+ profundidad final habra". Si la marea esta subiendo y la proxima LW
11709
+ es similar o mas alta que ahora (raro pero posible en cambios de
11710
+ coeficiente), el drop sera ~0 y mostramos solo la profundidad final.
11711
+
11712
+ FIX coherencia (Vicente QA): el backend calcula expectedMinDepth en
11713
+ referencia belowSurface (incluye calado), pero el numero grande de
11714
+ SONDA muestra valor RAW del sensor (sin calado). Para que "Fin Y.Z"
11715
+ sea visualmente coherente con el numero grande, calculamos fin como
11716
+ "valor_display_actual - bajada", no usamos el del backend directo.
11717
+ Ej: sonda raw 4.2m, bajada 1.59m -> fin 2.6m (no 3.9m). */
11718
+ var dropM = _gdisp.remainingDropM;
11719
+ var finM;
11720
+ if (typeof _lastDepthM === 'number' && isFinite(_lastDepthM)
11721
+ && typeof dropM === 'number' && isFinite(dropM)) {
11722
+ finM = _lastDepthM - dropM;
11723
+ } else {
11724
+ finM = _gdisp.expectedMinDepthM; /* fallback backend (referencia belowSurface) */
11725
+ }
11726
+ var en2 = (_lang === 'en');
11727
+ if (typeof finM === 'number' && isFinite(finM)) {
11728
+ var critical = finM < 1.0; /* < 1m de sonda final = atencion aunque alarma OFF */
11729
+ /* Rev490 (feedback usuario): sub-label SONDA mas compacto entre
11730
+ parentesis. Los detalles "bajada -Xcm" + "hora LW" ya viven en
11731
+ los widgets propios "Dif. Bajamar" y "Prof. min.". */
11732
+ sst.textContent = '(' + (en2 ? 'final ' : 'final ') + unitFmt.depth(finM, 1) + ')';
11733
+ sst.style.color = critical ? '#ffb23f' : '#66ffaa';
11734
+ sst.style.display = 'block';
11735
+ } else if (s.tideResume) {
11736
+ /* Fallback: sin datos numericos pero hay resumen de marea. */
11737
+ sst.textContent = (typeof T==='function')?T('bb_sonda_ok','OK · sin riesgo de varada'):'OK · sin riesgo de varada';
11738
+ sst.style.color = '#66ffaa';
11739
+ sst.style.display = 'block';
11740
+ } else { sst.style.display = 'none'; }
11741
+ }
11650
11742
  }
11743
+ /* Rev489 (feedback Vicente): widget propio "Diferencia Bajamar". Numero
11744
+ grande = bajada en cm (negativo); sub-label = sonda esperada al final.
11745
+ Tiene su propio scope: lee del helper _gdisp y recalcula fin coherente
11746
+ con el numero de sonda mostrado (depthSounderRawM, no belowSurface). */
11747
+ try {
11748
+ var difEl = document.getElementById('m-bb-dif-lw');
11749
+ var difEnd = document.getElementById('m-bb-dif-lw-end');
11750
+ if (difEl) {
11751
+ var _dropM2 = _gdisp ? _gdisp.remainingDropM : null;
11752
+ var _finM2;
11753
+ if (typeof _lastDepthM === 'number' && isFinite(_lastDepthM)
11754
+ && typeof _dropM2 === 'number' && isFinite(_dropM2)) {
11755
+ _finM2 = _lastDepthM - _dropM2;
11756
+ } else if (_gdisp) {
11757
+ _finM2 = _gdisp.expectedMinDepthM;
11758
+ }
11759
+ if (typeof _dropM2 === 'number' && isFinite(_dropM2) && _dropM2 > 0.005) {
11760
+ difEl.textContent = '-' + Math.round(_dropM2*100) + ' cm';
11761
+ difEl.style.color = (typeof _finM2 === 'number' && _finM2 < 1.0) ? '#ffb23f' : '#ffd166';
11762
+ } else if (typeof _dropM2 === 'number' && isFinite(_dropM2)) {
11763
+ difEl.textContent = '0 cm';
11764
+ difEl.style.color = '#9aa';
11765
+ } else {
11766
+ difEl.textContent = '—';
11767
+ difEl.style.color = '';
11768
+ }
11769
+ if (difEnd) {
11770
+ if (typeof _finM2 === 'number' && isFinite(_finM2)) {
11771
+ var enL = (_lang === 'en');
11772
+ difEnd.textContent = (enL ? 'End ' : 'Fin ') + unitFmt.depth(_finM2, 1);
11773
+ difEnd.style.color = (_finM2 < 1.0) ? '#ff7043' : '#cde';
11774
+ } else {
11775
+ difEnd.textContent = '—';
11776
+ difEnd.style.color = '';
11777
+ }
11778
+ }
11779
+ }
11780
+ /* Rev490: widget "Prof. mín." — sonda esperada en proxima bajamar. */
11781
+ var pmEl = document.getElementById('m-bb-prof-min');
11782
+ var pmWhen = document.getElementById('m-bb-prof-min-when');
11783
+ if (pmEl) {
11784
+ if (typeof _finM2 === 'number' && isFinite(_finM2)) {
11785
+ pmEl.textContent = unitFmt.depth(_finM2, 1);
11786
+ pmEl.style.color = (_finM2 < 1.0) ? '#ff5252' : (_finM2 < 2.0 ? '#ffb23f' : '');
11787
+ } else {
11788
+ pmEl.textContent = '—';
11789
+ pmEl.style.color = '';
11790
+ }
11791
+ if (pmWhen) {
11792
+ var lwIso = _gdisp ? _gdisp.nextLowTimeIso : null;
11793
+ if (lwIso) {
11794
+ try {
11795
+ var dLW = new Date(lwIso);
11796
+ var pad2 = function(n){ return ('0'+n).slice(-2); };
11797
+ var enL2 = (_lang === 'en');
11798
+ pmWhen.textContent = (enL2 ? 'at LW ' : 'a BM ') + pad2(dLW.getHours()) + ':' + pad2(dLW.getMinutes());
11799
+ pmWhen.style.color = '#cde';
11800
+ } catch(_){ pmWhen.textContent = '—'; }
11801
+ } else {
11802
+ pmWhen.textContent = '—';
11803
+ pmWhen.style.color = '';
11804
+ }
11805
+ }
11806
+ }
11807
+ } catch(_){}
11651
11808
  /* Cadena recomendada (en metros) */
11652
11809
  var cad = s.chainRecommended;
11653
11810
  if (cad == null && s.approach) cad = s.approach.chainRecNow;
@@ -12495,7 +12652,11 @@ function m_updateBBScrollIndicators(){
12495
12652
  L.textContent = '▲'; L.classList.add('show');
12496
12653
  R.textContent = '▲'; R.classList.add('show');
12497
12654
  } else {
12498
- L.textContent = '◀'; L.classList.toggle('show', canL);
12655
+ /* Rev491 (feedback Vicente QA): replicar comportamiento derecha exacto.
12656
+ La izquierda ahora tambien SIEMPRE visible cuando la bar esta abierta,
12657
+ igual que la derecha. Antes solo se mostraba si canL (scroll disponible),
12658
+ lo que la dejaba oculta cuando el bottom-bar cabia entero en pantalla. */
12659
+ L.textContent = '◀'; L.classList.add('show');
12499
12660
  R.textContent = '▶'; R.classList.add('show');
12500
12661
  }
12501
12662
  _syncToggleGlow();