signalk-mareas-ihm 1.2.0 → 1.2.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/README.md CHANGED
@@ -74,6 +74,14 @@ El calado del barco se lee automáticamente de Signal K (`design.draft`) o se pu
74
74
 
75
75
  ### Changelog
76
76
 
77
+ #### v1.2.1
78
+ - **UI Fondeo renovada**: cajas resultado con títulos uniformes 14px, Cadena y Profundidad en naranja, Estado Marea muestra resumen, Status Profundidad con estado varada, Publicar SK en barra verde.
79
+ - **Config overlay**: panel configuración se superpone sin scroll, con botón Calcular naranja.
80
+ - **Botones 170px**: todos los botones de la barra principal ampliados.
81
+ - **Fix alarma**: notificaciones OpenPlotter se limpian correctamente (state:normal).
82
+ - **Fix hBow**: corregido borrado de altura de roldana.
83
+ - **Fix Publicar SK**: estado persistente entre reinicios.
84
+
77
85
  #### v1.2.0
78
86
  - ⚓ **Calculadora de fondeo**: nueva pestaña FONDEO con modelo de scope náutico. Cadena recomendada, radio de borneo, publicación opcional en Signal K (`environment.anchor.mareasIhm.*`).
79
87
  - 🔇 **Fix notificaciones**: la alarma de varada suena una sola vez (antes se repetía cada 60s).
@@ -174,6 +182,14 @@ Vessel draft is read automatically from Signal K (`design.draft`) or can be conf
174
182
 
175
183
  ### Changelog
176
184
 
185
+ #### v1.2.1
186
+ - **UI Fondeo renovada**: cajas resultado con títulos uniformes 14px, Cadena y Profundidad en naranja, Estado Marea muestra resumen, Status Profundidad con estado varada, Publicar SK en barra verde.
187
+ - **Config overlay**: panel configuración se superpone sin scroll, con botón Calcular naranja.
188
+ - **Botones 170px**: todos los botones de la barra principal ampliados.
189
+ - **Fix alarma**: notificaciones OpenPlotter se limpian correctamente (state:normal).
190
+ - **Fix hBow**: corregido borrado de altura de roldana.
191
+ - **Fix Publicar SK**: estado persistente entre reinicios.
192
+
177
193
  #### v1.2.0
178
194
  - ⚓ **Anchoring calculator**: new ANCHOR tab with nautical scope model. Recommended chain, swing radius, optional Signal K publication (`environment.anchor.mareasIhm.*`).
179
195
  - 🔇 **Notification fix**: grounding alarm sounds once (previously repeated every 60s).
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ function isPositionValue(v) {
35
35
  }
36
36
  // Plugin version — must match package.json "version" field.
37
37
  // Shown in the WebApp instructions modal and published as environment.tide.pluginVersion.
38
- const PLUGIN_VERSION = "1.2.0";
38
+ const PLUGIN_VERSION = "1.2.1";
39
39
  const DEFAULT_LANG = "es";
40
40
  const NEARBY_THRESHOLD_METERS = 30000;
41
41
  // v1.1.0: Default fallback station when no GPS and no lastStation (first boot).
@@ -1050,20 +1050,19 @@ export default function (app) {
1050
1050
  // Previously, the clear only happened on the next evaluation cycle (~60s), causing phantom alarms.
1051
1051
  function emitNotificationClear() {
1052
1052
  try {
1053
- // v1.2.0: Remove the notification path entirely (null) instead of emitting "normal".
1054
- // This prevents apps like avnav from showing stale "normal" as an active alarm.
1053
+ // v1.2.1 FIX: Use state:"normal" instead of null. Null didn't clear OpenPlotter notifications.
1055
1054
  const clearDelta = {
1056
1055
  context: ("vessels." + app.selfId),
1057
1056
  updates: [{
1058
1057
  timestamp: new Date().toISOString(),
1059
1058
  values: [{
1060
1059
  path: "notifications.signalk-mareas-ihm.grounding",
1061
- value: null
1060
+ value: { state: "normal", method: [], message: "" }
1062
1061
  }]
1063
1062
  }]
1064
1063
  };
1065
1064
  app.handleMessage(plugin.id, clearDelta);
1066
- lastGroundingNotifState = "cleared";
1065
+ lastGroundingNotifState = "normal"; // v1.2.1: "normal" not "cleared" — prevents re-emit on next cycle
1067
1066
  }
1068
1067
  catch { /* never break user action */ }
1069
1068
  }
@@ -1105,6 +1104,8 @@ export default function (app) {
1105
1104
  expressApp.get("/signalk-mareas-ihm/api/anchor/config", async (_req, res) => {
1106
1105
  try {
1107
1106
  const saved = (await ihmCache.get("anchorConfig"));
1107
+ if (saved && typeof saved.autoPublish === "boolean")
1108
+ anchorAutoPublish = saved.autoPublish;
1108
1109
  res.json({ ...ANCHOR_DEFAULTS, ...saved, autoPublish: anchorAutoPublish });
1109
1110
  }
1110
1111
  catch (e) {
@@ -1123,8 +1124,10 @@ export default function (app) {
1123
1124
  roundingStep: [1, 5].includes(Number(b.roundingStep)) ? Number(b.roundingStep) : ANCHOR_DEFAULTS.roundingStep,
1124
1125
  };
1125
1126
  await ihmCache.set("anchorConfig", cfg);
1126
- if (typeof b.autoPublish === "boolean")
1127
+ if (typeof b.autoPublish === "boolean") {
1127
1128
  anchorAutoPublish = b.autoPublish;
1129
+ await ihmCache.set("anchorConfig", { ...cfg, autoPublish: anchorAutoPublish });
1130
+ }
1128
1131
  res.json({ ok: true, ...cfg, autoPublish: anchorAutoPublish });
1129
1132
  }
1130
1133
  catch (e) {
@@ -1141,28 +1144,57 @@ export default function (app) {
1141
1144
  anchorAutoPublish = b.autoPublish;
1142
1145
  const saved = (await ihmCache.get("anchorConfig"));
1143
1146
  const cfg = { ...ANCHOR_DEFAULTS, ...saved };
1144
- // Read SK values
1145
- const skDepth = (() => { try {
1146
- const v = app.getSelfPath("environment.depth.belowTransducer.value");
1147
- return typeof v === "number" ? v : null;
1148
- }
1149
- catch {
1150
- return null;
1151
- } })();
1152
- const skTideNow = (() => { try {
1153
- const v = app.getSelfPath("environment.tide.heightNow.value");
1154
- return typeof v === "number" ? v : null;
1155
- }
1156
- catch {
1157
- return null;
1158
- } })();
1159
- const skNextHighH = (() => { try {
1160
- const v = app.getSelfPath("environment.tide.heightNextHigh.value");
1161
- return typeof v === "number" ? v : null;
1147
+ // Read SK values — unified depth reader with diagnostics
1148
+ const toNum = (v) => { if (v == null || v === "")
1149
+ return null; const n = Number(v); return (typeof n === "number" && isFinite(n) && n > 0) ? n : null; };
1150
+ const readSkNumber = (path) => {
1151
+ try {
1152
+ // Try path.value (SK v2 unwrapped)
1153
+ let d = toNum(app.getSelfPath(path + ".value"));
1154
+ if (d)
1155
+ return d;
1156
+ // Try raw path (SK v1 may return {value: X} or plain number)
1157
+ const raw = app.getSelfPath(path);
1158
+ if (typeof raw === "number" && isFinite(raw) && raw > 0)
1159
+ return raw;
1160
+ if (raw && typeof raw === "object") {
1161
+ d = toNum(raw.value ?? raw.maximum);
1162
+ if (d)
1163
+ return d;
1164
+ }
1165
+ return null;
1166
+ }
1167
+ catch {
1168
+ return null;
1169
+ }
1170
+ };
1171
+ // Try ALL standard depth paths: belowTransducer > belowSurface > belowKeel
1172
+ let skDepth = null;
1173
+ let depthPathUsed = "none";
1174
+ const depthPaths = [
1175
+ "environment.depth.belowTransducer",
1176
+ "environment.depth.belowSurface",
1177
+ "environment.depth.belowKeel",
1178
+ ];
1179
+ const depthDiag = {};
1180
+ for (const dp of depthPaths) {
1181
+ try {
1182
+ const rawDot = app.getSelfPath(dp + ".value");
1183
+ const rawObj = app.getSelfPath(dp);
1184
+ depthDiag[dp] = { dotValue: rawDot ?? null, raw: (typeof rawObj === "object" ? JSON.stringify(rawObj)?.slice(0, 200) : rawObj) ?? null };
1185
+ }
1186
+ catch (e) {
1187
+ depthDiag[dp] = { error: e?.message };
1188
+ }
1189
+ const v = readSkNumber(dp);
1190
+ if (v) {
1191
+ skDepth = v;
1192
+ depthPathUsed = dp;
1193
+ break;
1194
+ }
1162
1195
  }
1163
- catch {
1164
- return null;
1165
- } })();
1196
+ const skTideNow = readSkNumber("environment.tide.heightNow");
1197
+ const skNextHighH = readSkNumber("environment.tide.heightNextHigh");
1166
1198
  const skNextHighT = (() => { try {
1167
1199
  const v = app.getSelfPath("environment.tide.timeNextHigh.value");
1168
1200
  return typeof v === "string" ? Date.parse(v) : null;
@@ -1170,13 +1202,7 @@ export default function (app) {
1170
1202
  catch {
1171
1203
  return null;
1172
1204
  } })();
1173
- const skNextLowH = (() => { try {
1174
- const v = app.getSelfPath("environment.tide.heightNextLow.value");
1175
- return typeof v === "number" ? v : null;
1176
- }
1177
- catch {
1178
- return null;
1179
- } })();
1205
+ const skNextLowH = readSkNumber("environment.tide.heightNextLow");
1180
1206
  const skNextLowT = (() => { try {
1181
1207
  const v = app.getSelfPath("environment.tide.timeNextLow.value");
1182
1208
  return typeof v === "string" ? Date.parse(v) : null;
@@ -1184,13 +1210,7 @@ export default function (app) {
1184
1210
  catch {
1185
1211
  return null;
1186
1212
  } })();
1187
- const skLoa = (() => { try {
1188
- const v = app.getSelfPath("design.length.overall.value");
1189
- return typeof v === "number" ? v : null;
1190
- }
1191
- catch {
1192
- return null;
1193
- } })();
1213
+ const skLoa = readSkNumber("design.length.overall");
1194
1214
  const skTendency = (() => { try {
1195
1215
  const v = app.getSelfPath("environment.tide.tendencypercentage.value");
1196
1216
  return typeof v === "string" ? v : null;
@@ -1221,7 +1241,39 @@ export default function (app) {
1221
1241
  if (anchorAutoPublish && Number.isFinite(result.chainL) && !result.partial) {
1222
1242
  publishAnchoringDeltas(result);
1223
1243
  }
1224
- res.json({ ...result, depthMax, depthMin, draftEffective, tendencyPercentage: skTendency });
1244
+ res.json({ ...result, depthMax, depthMin, draftEffective, tendencyPercentage: skTendency, depthPathUsed, skDepthFound: skDepth, depthDiag });
1245
+ }
1246
+ catch (e) {
1247
+ res.status(500).json({ error: e?.message ?? String(e) });
1248
+ }
1249
+ });
1250
+ // v1.2.1: Diagnostic endpoint — shows exactly what Signal K returns for depth paths
1251
+ // Access: http://localhost:3000/signalk-mareas-ihm/api/anchor/depth-debug
1252
+ expressApp.get("/signalk-mareas-ihm/api/anchor/depth-debug", async (_req, res) => {
1253
+ try {
1254
+ const paths = [
1255
+ "environment.depth.belowTransducer",
1256
+ "environment.depth.belowSurface",
1257
+ "environment.depth.belowKeel",
1258
+ "environment.depth",
1259
+ ];
1260
+ const results = {};
1261
+ for (const p of paths) {
1262
+ try {
1263
+ const raw = app.getSelfPath(p);
1264
+ const dotValue = (() => { try {
1265
+ return app.getSelfPath(p + ".value");
1266
+ }
1267
+ catch {
1268
+ return "ERROR";
1269
+ } })();
1270
+ results[p] = { raw: raw ?? "UNDEFINED", dotValue: dotValue ?? "UNDEFINED", rawType: typeof raw, dotValueType: typeof dotValue };
1271
+ }
1272
+ catch (e) {
1273
+ results[p] = { error: e?.message ?? String(e) };
1274
+ }
1275
+ }
1276
+ res.json({ timestamp: new Date().toISOString(), depthPaths: results });
1225
1277
  }
1226
1278
  catch (e) {
1227
1279
  res.status(500).json({ error: e?.message ?? String(e) });
@@ -1560,9 +1612,24 @@ export default function (app) {
1560
1612
  return (n && n > 0) ? n : null;
1561
1613
  };
1562
1614
  const readDepthBelowTransducer = () => {
1563
- const raw = app.getSelfPath("environment.depth.belowTransducer.value") || app.getSelfPath("environment.depth.belowTransducer");
1564
- const n = toNum(raw);
1565
- return (n && n > 0) ? n : null;
1615
+ // Try ALL standard depth paths: belowTransducer > belowSurface > belowKeel
1616
+ for (const p of ["environment.depth.belowTransducer", "environment.depth.belowSurface", "environment.depth.belowKeel"]) {
1617
+ try {
1618
+ let d = toNum(app.getSelfPath(p + ".value"));
1619
+ if (d && d > 0)
1620
+ return d;
1621
+ const raw = app.getSelfPath(p);
1622
+ if (typeof raw === "number" && isFinite(raw) && raw > 0)
1623
+ return raw;
1624
+ if (raw && typeof raw === "object") {
1625
+ d = toNum(raw.value ?? raw.maximum);
1626
+ if (d && d > 0)
1627
+ return d;
1628
+ }
1629
+ }
1630
+ catch { /* try next */ }
1631
+ }
1632
+ return null;
1566
1633
  };
1567
1634
  // --- Draft resolution (manual vs Signal K) ---
1568
1635
  const cachedDraft = toNum(caladoCfg?.draft);
@@ -1746,9 +1813,10 @@ export default function (app) {
1746
1813
  };
1747
1814
  app.handleMessage(plugin.id, draftDelta);
1748
1815
  }
1749
- // v1.2.0: Only emit notifications on STATE TRANSITIONS to avoid spam.
1750
- // Apps like avnav interpret ANY notification on the path as active alarm.
1751
- // Alarm sounds ONCE; stays latched until user does snooze/off.
1816
+ // v1.2.1: Notification state machine — emit on transitions, clear stale notifications
1817
+ // BUG FIX: After plugin restart, lastGroundingNotifState="cleared" but old alarm notification
1818
+ // may still be active in SK data model. By checking !== "normal" instead of === "alarm",
1819
+ // we emit ONE clear on startup to kill stale zombie notifications.
1752
1820
  const shouldNotify = notifyRisk;
1753
1821
  const soundMuted = alarmaCfg?.soundMuted === true;
1754
1822
  const notifMethod = soundMuted ? ["visual", "push"] : ["visual", "sound", "push"];
@@ -1767,22 +1835,20 @@ export default function (app) {
1767
1835
  app.handleMessage(plugin.id, notifDelta);
1768
1836
  lastGroundingNotifState = "alarm";
1769
1837
  }
1770
- else if (!shouldNotify && lastGroundingNotifState === "alarm") {
1771
- // TRANSITION alarm → clear: remove the notification path entirely.
1772
- // Using null removes the path from the SK data model, so apps like avnav
1773
- // won't see a stale "normal" notification as an active alarm.
1838
+ else if (!shouldNotify && lastGroundingNotifState !== "normal") {
1839
+ // TRANSITION → clear: covers both "alarm"→"normal" AND "cleared"→"normal" (startup)
1774
1840
  const clearDelta = {
1775
1841
  context: ("vessels." + app.selfId),
1776
1842
  updates: [{
1777
1843
  timestamp: now.toISOString(),
1778
1844
  values: [{
1779
1845
  path: "notifications.signalk-mareas-ihm.grounding",
1780
- value: null
1846
+ value: { state: "normal", method: [], message: tr("Sin riesgo de varada", "No grounding risk") }
1781
1847
  }]
1782
1848
  }]
1783
1849
  };
1784
1850
  app.handleMessage(plugin.id, clearDelta);
1785
- lastGroundingNotifState = "cleared";
1851
+ lastGroundingNotifState = "normal";
1786
1852
  }
1787
1853
  // If already "cleared" or "normal" and no risk → do nothing (no spam).
1788
1854
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-mareas-ihm",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Official tide predictions from the Spanish Hydrographic Institute (IHM).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",