signalk-pypilot-newui 2.5.0 → 2.7.0

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,7 +1,553 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.7.0 — 2026-09-14 — Rev246..Rev277: connection resilience + audit follow-up + full i18n audit
4
+
5
+ **Rev277 QA fix (latency of detection)**:
6
+
7
+ - **Faster offline detection**: ping cadence 5 s → 2 s, pong window
8
+ 8 s → 5 s. Two consecutive missed pings now raise `healthy=false`
9
+ in roughly half the previous time — ~5-6 s worst case instead of
10
+ ~10-14 s. 30 pings/min is a rounding error next to pypilot_web's
11
+ own ~20 Hz value stream, so no measurable extra load on the
12
+ Pi Zero W.
13
+ - **Active ping on reconnect**: instead of waiting for the next 2 s
14
+ tick, the fresh socket emits a `ping` the moment the `connect`
15
+ event fires. A live pypilot_web pongs in milliseconds, and the
16
+ first value stream that follows clears `coreOffline` immediately.
17
+ Trims another ~2-3 s off the "back online" transition Carlos
18
+ observed on Rev276.
19
+
20
+ **Rev276 QA fix**:
21
+
22
+ - **Snapshot survives a failed restore**. Previously the disconnect
23
+ snapshot was consumed the moment `_pypilotMaybeOfferRestore`
24
+ read it — so if the user pressed "Yes" and the restore failed
25
+ (e.g. `POST /engage` returned HTTP 500 because pypilot dropped
26
+ again mid-flow), the next reconnection had no snap to re-offer
27
+ and the sailor silently lost the chance to restore. Now: the
28
+ snap is purged only on **success**, on explicit **No**, or on
29
+ the 60 s timeout. On failure it is kept.
30
+ - **Snapshot capture no longer clobbers a valid pending snap**. If
31
+ pypilot disconnects again while we still hold an unACKed snap
32
+ from an earlier outage, the current call keeps the older
33
+ snapshot (with the original disconnect timestamp) rather than
34
+ overwriting it with a fresh one that may carry
35
+ `engaged=false` from the failed restore.
36
+ - **Micro-hiccups (<20 s) no longer purge the snap**. Previously
37
+ any reconnection swallowed the snapshot; now sub-20 s recoveries
38
+ leave it available so if a longer outage follows the sailor
39
+ still gets the banner.
40
+
41
+ **Rev275 QA fix**:
42
+
43
+ - **Restore banner no longer stuck on success**. `verdict("ok")`
44
+ only spoke "Target restaurado" and forgot to close the
45
+ "Restaurando..." banner — sticky until the next reload. Now the
46
+ banner briefly turns green with the success message and
47
+ auto-hides in 3 s.
48
+
49
+ **Rev274 QA fix (Carlos re-test)**:
50
+
51
+ - **Restore verdict window widened from 3 s to 10 s with polling**.
52
+ On Rev273 the 3 s deadline still cleared before `pypilotHealthy`
53
+ could flip false (pong window is ~8 s), so the visor still
54
+ spoke "target restaurado" a few seconds before spotting the
55
+ disconnect. Now: after the three HTTP writes succeed, the banner
56
+ shows "Restaurando... esperando confirmación pypilot" and the
57
+ code polls every 500 ms up to 10 s. Any negative evidence
58
+ (`pypilotHealthy=false` or `engaged=false`) aborts to failure
59
+ immediately with a specific reason; success voice only fires if
60
+ both survive the whole window.
61
+
62
+ **Rev273 QA fix**:
63
+
64
+ - **Restore banner no longer lies "restaurado" on 200 without
65
+ confirmation** (Carlos QA on Rev272). SK Autopilot API v2 returns
66
+ 200 as soon as the request reaches the provider, before pypilot
67
+ has had a chance to confirm. The success verdict is deferred and
68
+ gated on `state.pypilotHealthy !== false` +
69
+ `state.engaged === true` — otherwise the failure banner + voice
70
+ fire with a specific reason.
71
+
72
+ **Rev270..Rev271 delta on top of Rev269**:
73
+
74
+ - **i18n audit follow-up** (Rev270): every dynamic string that leaked
75
+ through the previous pass moved to the dictionary — the tack log
76
+ modal (empty state, chip labels, badge tags, recovery line,
77
+ confirm), the maneuver HUD verdicts (VIRADA / TRASLUCHADA), the
78
+ Trips card in Setup + its list rows + status line + WhatsApp
79
+ caption + KPI grid + highlights + map legend + all detail modal
80
+ cards + the PNG report card header/footer + share picker labels +
81
+ restore banner. Renamed variable clash in the trip loop where
82
+ `t` shadowed the i18n helper. Fixed accidental "AnchorWatch Pro"
83
+ reference in the trip caption (memory `feedback_no_commercial_refs`).
84
+ - **Second external audit closed** (Rev271): the follow-up report
85
+ raised 7 findings against Rev270. Five are landed here as small
86
+ targeted fixes; two (concurrent adjustTarget race, angular
87
+ normalisation defence for out-of-range inputs) are deferred to
88
+ Rev272 with rationale in the commit log.
89
+
90
+ **Rev271 fixes**:
91
+
92
+ - **Core-offline sticky flag** (audit R03). `PypilotClient` now
93
+ distinguishes between "the socket to pypilot_web is up" and "the
94
+ pypilot core is alive". A `pypilot_disconnect` event from the web
95
+ server sets a sticky `coreOffline` that only clears when the core
96
+ actually starts talking again — a fresh catalog delivery or a live
97
+ `pypilot` value. `healthy` and `set()` both consume this flag, so
98
+ a socket that keeps ponging while the core is dead can no longer
99
+ report success on writes. New events `pypilot_offline` /
100
+ `pypilot_online` are wired into `index.ts` to update plugin status
101
+ and mark the AP provider offline.
102
+ - **`allowWrites` gate is now global** (audit R05). Previously only
103
+ the plugin's own HTTP routes honoured it — the Autopilot API v2
104
+ provider interface, the KIP action PUT handlers and the momentary
105
+ switches all bypassed it. The check now lives in
106
+ `PypilotClient.set()` (single point every writer routes through)
107
+ plus a `writesOff()` 403 response on every PUT handler so
108
+ KIP/WilhelmSK/freeboard see a legible reason instead of a silent
109
+ no-op.
110
+ - **Generation counter cancels stale engage retries** (audit
111
+ R01+R04). `AutopilotProvider` bumps `engageGen` on every
112
+ `setState` call and cancels any pending NAV-mode 500 ms timer.
113
+ `_setWithRetry` accepts an `isStale()` callback that aborts the
114
+ backoff between attempts. A disengage landing mid-retry now
115
+ invalidates the still-pending engage, and vice versa; the delayed
116
+ NAV activation can no longer wake up after a disengage.
117
+ - **Restore banner sequenced with hard-fail** (audit R02). The "yes,
118
+ restore" path now runs mode → target → engage in that order, each
119
+ step guarded by `throwIfNotOk(res, op)` that treats both an
120
+ absent response and any HTTP >=400 as a stop condition. Target
121
+ is prepared before engage so the AP does not latch on the current
122
+ heading first and then jerk. Failure is spoken ("Restore failed"
123
+ / "Restauración fallida") and shown as a red line in the banner
124
+ instead of the previous silent `catch`.
125
+ - **Access-request precedence bug** (audit T09). Fixed `j.href ||
126
+ j.requestId ? ... : null` operator precedence that was building
127
+ `/requests/undefined` when the server returned an explicit
128
+ `href`. Now `j.href || (j.requestId ? ... : null)`.
129
+
130
+ **Rev272 fixes** (close the remaining two audit points):
131
+
132
+ - **`adjustTarget` serialisation** (audit R06). Two concurrent
133
+ `+Δ` calls used to read the same base from `data.target` before
134
+ awaiting the write and both wrote `base+Δ`, losing one increment.
135
+ A single `_adjustChain` promise now serialises adjusts through
136
+ the provider so the second call reads the base written by the
137
+ first. Errors on one call do not poison the next caller's turn.
138
+ - **`shortestArcRad` bounded + finite guards** (audit R07). The
139
+ angular normalisation loop is now a single modulo (constant cost)
140
+ and returns 0 on a non-finite delta. `setTarget` and
141
+ `adjustTarget` also reject `NaN` / `Infinity` / astronomically
142
+ large radians at the boundary with a clear error, so a
143
+ misbehaving integration cannot poison provider state.
144
+
145
+ ---
146
+
147
+ ## 2.7.0 headline — Rev246..Rev269: connection resilience + reliability + full i18n audit
148
+
149
+ Sea-trial-driven release. After several sessions of **intensive
150
+ sailing and beta testing** on Tunatunes we surfaced (and closed) a
151
+ long list of connection-resilience holes, TTS gaps, i18n drifts and
152
+ UX rough edges that only show up when the boat is under way, the
153
+ TinyPilot resets, or the link flakes. The whole batch was audited
154
+ twice by an external LLM auditor between iterations before being
155
+ folded back in.
156
+
157
+ ### English
158
+
159
+ **Added**
160
+
161
+ - **Full "Pypilot down" UX cycle**: passive UI reflection tied to a
162
+ pong-based heartbeat. When the socket goes silent for &gt; 8 s the
163
+ target diamond and TGT chip hide immediately, AP-affecting
164
+ controls grey out with a `.pypilot-offline` class, and the
165
+ alarm rule `pypilot-disconnected` fires with a spoken message
166
+ ("Pypilot desconectado desde hace N segundos") whose h / min / s
167
+ units are read as full words instead of abbreviations. On
168
+ reconnect: green "Pypilot operativo" toast + voice announcement
169
+ + controls reactivate.
170
+ - **Three-tier outage handler**: on reconnect the visor compares
171
+ the outage length against the last known target. &lt; 20 s: no
172
+ action (probably a hiccup). 20 s – 5 min: banner offers to
173
+ restore the previous target with S&iacute; / No buttons. &gt; 5 min:
174
+ nothing (too long, sailor most likely wants a fresh engage).
175
+ - **Optimistic write-then-throw pattern** with `client.set()`
176
+ returning `boolean`. `apEngage / apDisengage / apSetMode /
177
+ apSetTargetRad / apTack` reject BEFORE mutating optimistic state,
178
+ so a phantom command against an offline TinyPilot cannot leave
179
+ the UI stuck in "engaged" waiting for a delta that will never
180
+ arrive.
181
+ - **`_setWithRetry` helper** (250 ms + 750 ms backoff) around the
182
+ autopilot-provider adapter for transient socket blips.
183
+ - **Watchdog + pong heartbeat** in `pypilot-client.ts`: `healthy`
184
+ getter combines `socket.connected` with `lastPongMs &lt;= 8 s`.
185
+ Alarm engine consumes `client.healthy` (not `.connected`) so
186
+ the network layer and the alarm engine agree on liveness. Alarm
187
+ ordering also reworked so `sensor-lost` no longer fires _before_
188
+ `pypilot-disconnected` when the whole socket dies.
189
+ - **Multi-tab session isolation**: owner keys now live in
190
+ `sessionStorage` (per-tab) not `localStorage`, so two visor tabs
191
+ do not race to release focus watches.
192
+ - **Signed-delta rotation counter** (Rev247) for the HUD progress
193
+ during tack maneuvers: accumulates the SIGNED angular delta so
194
+ IMU / compass jitter cancels around zero and the counter no
195
+ longer drifts to "0&deg; remaining" with the boat still 100&deg;
196
+ short. The 0.5&deg; threshold that produced the false report
197
+ was removed entirely.
198
+ - **Full i18n audit** across EN / ES / DE / FR: purged mixed
199
+ Spanish text from EN-only modals (Boat menu, Tack log, Trip
200
+ detail, Trip share, Gust marker overlay, Bottom-bar picker,
201
+ nav-toggle aria-label), added `data-lang` variants, moved the
202
+ bottom-bar strings to i18n keys, added `data-i18n-title` on the
203
+ tack buttons. Rebrand "Gota Chain" (which "nobody understood",
204
+ per Carlos) as **"Target &amp; Wind arrows"** / "Flechas
205
+ Target y Viento" everywhere it is public-facing (README, Info
206
+ modal, NPM screenshot title).
207
+ - **Spanish orthography pass**: `"COMPAS"` &rarr; `"COMP&Aacute;S"`
208
+ in the ES mode label, `<code>compas / gps / viento / real</code>`
209
+ &rarr; the literal `<code>compass / gps / wind / true</code>`
210
+ string (they are pypilot enum values, not translatable words).
211
+ - **Full instructions section** in the Info modal (EN + ES)
212
+ covering every tab now that Chart / Race / Doctor / Smart Pilot
213
+ / Alarms / Sensor Quality / Session recorder / Log capture /
214
+ Debug console / Remote Control Console / Watchdog have all
215
+ landed.
216
+ - **Health status endpoint** (`/status`) now returns
217
+ `healthy: boolean` alongside `connected`.
218
+ - **Focus-watch owner map**: `_focusWatches` moved to
219
+ `Map<key, Map<owner, entry>>` so a stale owner from a crashed
220
+ tab does not hold a watch alive.
221
+
222
+ **Fixed**
223
+
224
+ - **Phantom writes on offline socket**: `client.set()` no longer
225
+ returns a silent success when the socket has no live pong. Every
226
+ API-level writer either resolves after the retry succeeds or
227
+ throws with `"pypilot offline: <path>"`. Fixes a class of "the
228
+ UI says engaged but the boat is going straight" reports.
229
+ - **Slow reconnect after power-cycling pypilot**: watchdog interval
230
+ reduced to 15 s with a 20 s liveness threshold, and the ap
231
+ provider now reconciles state on the reconnect delta so the
232
+ visor picks up the fresh mode / target immediately.
233
+ - **`refreshPypilotValues` merge bug**: pilot / profile switch was
234
+ masked by the "don't overwrite existing keys" heuristic. Delta
235
+ handlers for `ap.pilot` and `profile` now pass `{replace: true}`.
236
+ - **`gainReady` exit too early**: the readiness gate was leaving
237
+ as soon as any gain from any pilot arrived. Now scoped to
238
+ `state.pilot` so a stale pilot's gains cannot short-circuit the
239
+ boot pull for the active one.
240
+ - **Tack pill "executing" missing target**: the pill fell back to
241
+ `null` after the arming phase closed. Now falls back through
242
+ `_tackPendingFinalTargetRad ?? _countdownPlannedTargetRad ??
243
+ state.target`.
244
+ - **TGT stays visible with AP engaged + connection lost**: no
245
+ disengage delta arrives when the pypilot vanishes, so the
246
+ target was staying pinned. Now the formatter and
247
+ `renderTargetArrow` bail on `state.pypilotHealthy === false`;
248
+ `scheduleRenderControl()` and `renderTargetArrow()` are also
249
+ kicked on every healthy transition.
250
+ - **Voice not heard on reconnect** (Rev264): added
251
+ `_alSpeak("Pypilot operativo")` to
252
+ `_pypilotShowReconnectedToast` so the sailor gets the audible
253
+ confirmation and does not have to look at the screen.
254
+
255
+ **Architecture**
256
+
257
+ - Rejected an active-ping-before-write pattern proposed mid-sprint:
258
+ it is a TOCTOU (time-of-check-to-time-of-use) anti-pattern that
259
+ adds latency to every write without giving a real safety
260
+ guarantee. Kept the passive-reflection approach instead. Design
261
+ discussion preserved in the commit log.
262
+
263
+ ### Espa&ntilde;ol
264
+
265
+ **A&ntilde;adido**
266
+
267
+ - **Ciclo UX completo "Pypilot ca&iacute;do"**: la UI refleja el
268
+ estado real de la conexi&oacute;n de forma pasiva a partir de un
269
+ heartbeat pong. Cuando el socket lleva &gt; 8 s en silencio, el
270
+ diamante del target y el chip TGT desaparecen inmediatamente,
271
+ los controles que afectan al AP se atenúan con la clase
272
+ `.pypilot-offline`, y la regla `pypilot-disconnected` dispara
273
+ con voz en palabras completas ("Pypilot desconectado desde hace
274
+ N segundos") en lugar de abreviaturas. Al reconectar: toast
275
+ verde "Pypilot operativo" + voz + controles activos otra vez.
276
+ - **Gestor de outage en 3 tramos**: al reconectar el visor compara
277
+ la duraci&oacute;n de la ca&iacute;da contra el &uacute;ltimo
278
+ target conocido. &lt; 20 s: no hace nada (probable microcorte).
279
+ 20 s – 5 min: banner con S&iacute; / No para restaurar el
280
+ target anterior. &gt; 5 min: nada (demasiado tiempo, el
281
+ navegante querr&aacute; enganchar de cero).
282
+ - **Patr&oacute;n optimistic write-then-throw** con `client.set()`
283
+ devolviendo `boolean`. `apEngage / apDisengage / apSetMode /
284
+ apSetTargetRad / apTack` rechazan ANTES de tocar el estado
285
+ optimista, para que un comando fantasma contra una TinyPilot
286
+ ca&iacute;da no deje la UI "enganchada" esperando un delta que
287
+ no va a llegar.
288
+ - **`_setWithRetry`** (250 ms + 750 ms backoff) alrededor del
289
+ autopilot-provider adapter para blips transitorios del socket.
290
+ - **Watchdog + heartbeat pong** en `pypilot-client.ts`: el getter
291
+ `healthy` combina `socket.connected` con `lastPongMs &lt;= 8 s`.
292
+ El motor de alarmas usa `client.healthy` (no `.connected`) para
293
+ que la red y las alarmas est&eacute;n de acuerdo sobre la
294
+ vitalidad. El orden de alarmas tambi&eacute;n se rehizo para
295
+ que `sensor-lost` no salte _antes_ que `pypilot-disconnected`
296
+ cuando cae el socket entero.
297
+ - **Aislamiento multi-pesta&ntilde;a**: las claves de owner van
298
+ ahora en `sessionStorage` (por pesta&ntilde;a) en lugar de
299
+ `localStorage`, para que dos visores abiertos no compitan por
300
+ soltar los focus watches.
301
+ - **Contador con delta con signo** (Rev247) para el HUD del
302
+ countdown de virada: acumula el delta angular CON SIGNO para
303
+ que el jitter del IMU / compass se cancele en torno a cero y el
304
+ contador no acabe en "0&deg; restantes" con el barco todav&iacute;a
305
+ a 100&deg; del rumbo final. El threshold de 0.5&deg; que
306
+ produc&iacute;a el falso reporte se elimin&oacute; por completo.
307
+ - **Auditor&iacute;a i18n completa** en EN / ES / DE / FR: se
308
+ purg&oacute; el espa&ntilde;ol en modales que se ven en cualquier
309
+ idioma (men&uacute; Barco, Historial de bordos, Detalle de viaje,
310
+ Compartir viaje, capa Marca de r&aacute;fagas, selector barra
311
+ inferior, aria del nav-toggle), se a&ntilde;adieron variantes
312
+ `data-lang`, las strings de la barra inferior pasaron a claves
313
+ i18n y se a&ntilde;adi&oacute; `data-i18n-title` en los botones
314
+ de virada. Se renombr&oacute; "Gota Chain" (que "no lo entiende
315
+ ni Claude", palabras de Carlos) a **"Flechas Target y Viento"**
316
+ / "Target &amp; Wind arrows" en todo lo que se ve fuera del
317
+ c&oacute;digo (README, modal Info, t&iacute;tulo de screenshot
318
+ NPM).
319
+ - **Pase de ortograf&iacute;a espa&ntilde;ola**: `"COMPAS"` &rarr;
320
+ `"COMP&Aacute;S"` en el label de modo ES, y el bloque
321
+ `<code>compas / gps / viento / real</code>` pas&oacute; a la
322
+ string literal `<code>compass / gps / wind / true</code>`
323
+ (son valores del enum de pypilot, no palabras traducibles).
324
+ - **Secci&oacute;n de Instrucciones completas** en el modal Info
325
+ (EN + ES) cubriendo todas las pesta&ntilde;as ahora que Chart /
326
+ Race / Doctor / Piloto Inteligente / Alarmas / Calidad de
327
+ sensores / Grabador de sesiones / Captura de logs / Consola de
328
+ debug / Consola de control remoto / Watchdog est&aacute;n todas
329
+ en su sitio.
330
+ - **Endpoint `/status`** ahora devuelve `healthy: boolean` junto
331
+ con `connected`.
332
+ - **Focus-watch owner map**: `_focusWatches` pas&oacute; a ser
333
+ `Map<key, Map<owner, entry>>` para que un owner colgado de una
334
+ pesta&ntilde;a muerta no mantenga vivo un watch.
335
+
336
+ **Corregido**
337
+
338
+ - **Escrituras fantasma con el socket ca&iacute;do**: `client.set()`
339
+ ya no devuelve un &eacute;xito silencioso cuando el socket no
340
+ tiene pong vivo. Cada writer de nivel API o bien resuelve tras
341
+ el retry o bien lanza `"pypilot offline: <path>"`. Cierra la
342
+ clase de reportes "la UI dice enganchado pero el barco va recto".
343
+ - **Reconexi&oacute;n lenta tras cortar y encender pypilot**: el
344
+ watchdog baja a 15 s de intervalo con umbral de vivacidad de
345
+ 20 s, y el provider reconcilia estado en el delta de reconexi&oacute;n
346
+ para que el visor recoja el modo / target frescos al instante.
347
+ - **Bug de merge en `refreshPypilotValues`**: el cambio de piloto /
348
+ perfil quedaba enmascarado por la heur&iacute;stica de "no
349
+ sobreescribir claves existentes". Los delta handlers de
350
+ `ap.pilot` y `profile` ahora pasan `{replace: true}`.
351
+ - **`gainReady` sal&iacute;a antes de tiempo**: la puerta de
352
+ readiness se abr&iacute;a al llegar cualquier gain de cualquier
353
+ piloto. Ahora est&aacute; limitada al `state.pilot` activo,
354
+ para que los gains de un piloto stale no cortocircuiten el
355
+ pull de boot del activo.
356
+ - **Pill de virada "ejecutando" sin target**: la pill acababa en
357
+ `null` al cerrar la fase de arming. Ahora hace fallback por
358
+ `_tackPendingFinalTargetRad ?? _countdownPlannedTargetRad ??
359
+ state.target`.
360
+ - **TGT visible con AP enganchado + conexi&oacute;n ca&iacute;da**:
361
+ no llega delta de desenganche cuando pypilot se va, as&iacute;
362
+ que el target se quedaba clavado. El formateador y
363
+ `renderTargetArrow` ahora respetan `state.pypilotHealthy ===
364
+ false`; adem&aacute;s se disparan `scheduleRenderControl()` y
365
+ `renderTargetArrow()` en cada cambio de healthy.
366
+ - **Voz muda al reconectar** (Rev264): se a&ntilde;adi&oacute;
367
+ `_alSpeak("Pypilot operativo")` en
368
+ `_pypilotShowReconnectedToast` para que el navegante tenga
369
+ confirmaci&oacute;n audible sin mirar la pantalla.
370
+
371
+ **Arquitectura**
372
+
373
+ - Se rechaz&oacute; un patr&oacute;n de ping-antes-de-escribir
374
+ propuesto a mitad de sprint: es un anti-patr&oacute;n TOCTOU
375
+ (time-of-check-to-time-of-use) que a&ntilde;ade latencia a cada
376
+ escritura sin dar una garant&iacute;a real de seguridad. Se
377
+ mantuvo la reflexi&oacute;n pasiva. La discusi&oacute;n queda en
378
+ el log de commits para futura referencia.
379
+
380
+ ## 2.6.0 — 2026-09-10 — Rev178..Rev180: Trip Report sprint (still experimental)
381
+
382
+ Big feature bundle. Every trip you take (from the moment
383
+ `navigation.state` leaves "moored" until you're moored again) is
384
+ recorded in the background and turned into a shareable report card.
385
+
386
+ ### English
387
+
388
+ **Added**
389
+
390
+ - **Trip recorder** (new module `trip-recorder.ts`): auto-starts when
391
+ `signalk-autostate` (or any plugin publishing `navigation.state`)
392
+ moves the vessel off "moored". Closes and computes a summary when
393
+ the vessel is back to "moored". Trips < 60 s or with < 60 samples
394
+ are discarded automatically.
395
+ - **Live-accumulated KPIs** so summary computation is O(1) even on
396
+ Pi 4: distance (haversine), TWS/AWS avg + max, SOG avg + max, heel
397
+ avg + max, tack count, points-of-sail split (upwind / reach /
398
+ downwind), battery voltage min + sag count, mode share, engaged
399
+ percentage. Highlights (peak wind, peak SOG, min voltage) carry
400
+ their timestamps so the map can pin them.
401
+ - **"Viajes" card in Setup** (order 27) with live trip status + list
402
+ of past trips. Each row: date, distance, duration, avg SOG, avg
403
+ TWS, tack count.
404
+ - **Trip detail modal** with a 17-KPI grid + emoji-tagged highlight
405
+ strip (⛵ ráfaga, 🌀 vel max, 🔋 volt bajo).
406
+ - **OpenSeaMap trace on the map**: Leaflet lazy-loaded from CDN on
407
+ first modal open, OSM base + OpenSeaMap seamark overlay, polyline
408
+ coloured by SOG bucket (< 2 / 2-4 / 4-6 / > 6 kn), markers at
409
+ start (green), end (red), peak wind, peak SOG, min voltage, and
410
+ each detected tack. Auto-fit to the trace bounding box.
411
+ - **"Compartir por WhatsApp" button**: renders a portrait 900×1400
412
+ PNG report card (title, 10 KPI cards, mini trace with the same
413
+ colour buckets, endpoints and highlight emojis, footer). Uses the
414
+ Web Share API when available so the sailor picks any target app;
415
+ falls back to a download + prefilled `wa.me?text=` link on
416
+ platforms without file share.
417
+ - **HTTP endpoints**: `/trip-recorder/status`, `/list`,
418
+ `/summary/:id`, `/download/:id`, `DELETE /trip/:id`.
419
+
420
+ **Fixed (Rev177 carry-over)**
421
+
422
+ - Tack countdown never closes: added AWA-sign / heading-rotation
423
+ detection so the maneuver auto-completes in visor even when
424
+ pypilot never publishes `ap.tack.state="none"`.
425
+ - Gust ghost: fade 15 s (was 10), cooldown 15 s (was 20), thresholds
426
+ lowered (jump ≥ 3 kn, ratio ≥ 1.25, shift ≥ 18°). In true-wind
427
+ mode the ghost now keys off TWS/TWA and is drawn in the teal T
428
+ palette instead of the amber A. Fixes the "aparece poco / no
429
+ atiende a rafagas / en modo real deberia mirar real" feedback.
430
+ - `gustStrategy` default flipped from "warn" to "off" so the
431
+ supervisor cannot freeze the target / boost D / temp-heavy the
432
+ pilot without the sailor explicitly opting in. On the last sea
433
+ trial one of these strategies latched and the AP "se quedo
434
+ pillado, yendose orzada sin buscar rumbo".
435
+ - Reload race: after the plugin catalog lands we re-run
436
+ `refreshPypilotValues()` so gain / calibration sliders that
437
+ rendered before the catalog was known reposition themselves at
438
+ the real pypilot values. Fixes "al recargar la pagina muchas
439
+ veces no carga los valores de sliders o de modo".
440
+
441
+ **Known limits (still experimental)**
442
+
443
+ - Trasluchar (jibe): pypilot's tack primitive only crosses the wind
444
+ by proa. A jibe needs a different primitive (aproado-style). Not
445
+ implemented yet.
446
+ - Trip report card renders as a static PNG. The interactive map is
447
+ only visible inside the visor.
448
+ - CDN load of Leaflet needs internet. On a boat with no data link
449
+ the trace panel just says "sin conexion a los tiles del mapa" and
450
+ the modal still shows all the KPIs.
451
+
452
+ ## 2.5.2 — 2026-09-10 — Rev176: session-recorder containment
453
+
454
+ Follow-up patch on 2.5.0. After analysing the on-water sessions
455
+ Carlos captured with Rev175 we found a pile of 1-sample JSONL
456
+ files caused by an external HTTP client (likely a KIP dashboard
457
+ tab, an idle visor tab, or a competing pypilot_web instance)
458
+ cycling `POST /engage` -> `POST /disengage` every 5-6 s. Every
459
+ flicker opened and closed a session file. This tranche contains
460
+ the damage and fixes several analysis blind spots.
461
+
462
+ ### English
463
+
464
+ **Fixed**
465
+
466
+ - **Engaged debounce (8 s)** in the historian tick. A new
467
+ engage/disengage transition is only propagated to the session
468
+ recorder after the raw value has held for 8 s. This filters
469
+ the rebounds that were opening one JSONL every ~5 s during the
470
+ test window on 2026-09-08.
471
+ - **Bounce diagnostics**: filtered flips are counted and exposed
472
+ through `/session-recorder/status` (`bouncesFiltered`,
473
+ `lastBounceMs`, `lastBounceDetails`), plus a `[bounce]` line
474
+ in `app.debug` so the next culprit can be traced without
475
+ waiting for a repeat.
476
+ - **Discard short sessions on close**: if a session has less
477
+ than 30 samples OR less than 30 s of duration when the AP
478
+ disengages, the JSONL is unlinked instead of being flushed
479
+ and kept. Belt-and-suspenders on top of the debounce.
480
+ - **Retroactive purge**: on plugin start the recorder sweeps
481
+ `nav-sessions/` and deletes every closed file that meets the
482
+ same criterion. The tester's 54-file directory came down to 7
483
+ useful sessions after Rev176 booted.
484
+ - **Manual purge endpoint**: `POST /plugins/signalk-pypilot-newui/session-recorder/purge-short`
485
+ triggers the sweep on demand.
486
+ - **hdgErr per AP mode**: the recorder stored
487
+ `headingCmd - headingActual` regardless of mode, which was
488
+ garbage in `wind` / `true wind` where `headingCmd` is a wind
489
+ angle, not a heading. Now compass-family modes still store the
490
+ heading delta, `wind` mode stores `headingCmd - AWA`, and
491
+ `true wind` mode stores `headingCmd - TWA`. Offline analysers
492
+ finally get a real number for wind modes.
493
+ - **Pi Zero log-capture no longer re-injects the tail on
494
+ restart**: the per-source `lastLineHash` map is now persisted
495
+ to `pizero-log-state.json` and reloaded on plugin start. The
496
+ file used to grow 32x on the tester's setup because every
497
+ `-Restart` deploy reset the in-memory cursor and re-appended
498
+ the entire tail.
499
+
500
+ **Changed**
501
+
502
+ - **Gust ghost arrow recoloured to orange** so it stands out
503
+ from the live amber A arrow when the two land near the same
504
+ angle. The overlay-card preview matches.
505
+ - **Gust ghost detection thresholds lowered** (Rev175 carry-
506
+ over): AWS jump 4 kn / ratio 1.35, AWA shift 20 deg, baseline
507
+ min 2 kn, cooldown 20 s. Tuned against 2 h of real navigation
508
+ data (~18 potential fires for a 14 kn-avg sail).
509
+
510
+ **Investigation notes (for the curious)**
511
+
512
+ The 71-second racimo on 2026-09-08 19:01-02 (Rev166 that day)
513
+ shows the pattern:
514
+
515
+ engage -> PUT target -> PUT mode -> disengage -> engage -> ...
516
+
517
+ repeating every 5-6 s at the SK Autopilot API v2 endpoint. Not
518
+ manual: too regular. Culprit unidentified with the logs at
519
+ hand - the SK journal does not record client IP/UA. Suspects
520
+ still alive:
521
+
522
+ - A custom KIP dashboard widget that toggles engage on a tick.
523
+ - Two open visor tabs where one has a stale state machine and
524
+ re-issues engage on each SSE reconnect.
525
+ - A feedback loop through `signalk-pypilot` mirroring an incoming
526
+ bus delta as an outgoing SK write.
527
+
528
+ pypilot_web on the KIP tablet was cleared as a suspect - the
529
+ captured pypilot log shows its socket.io connections spaced
530
+ minutes apart, not seconds.
531
+
532
+ ## 2.5.1 — 2026-09-09 — README bilingual
533
+
534
+ Docs-only patch. The EXPERIMENTAL banner in README and CHANGELOG
535
+ now appears in English *and* Spanish so the SignalK App Store
536
+ description communicates the sea-trial-pending status to English
537
+ speakers too. No code changes.
538
+
3
539
  ## 2.5.0 — 2026-09-09 — Rev154..Rev174
4
540
 
541
+ > ## ⚠️ Ships with features that have not been tested at sea yet
542
+ >
543
+ > This release brings NEW features that we **could not validate under
544
+ > sail** yet (there was not enough wind the day we cut it). Install
545
+ > it if you fancy helping us test them — and if anything looks off,
546
+ > **tell us**: https://github.com/Aitonos/signalk-pypilot-newui/issues
547
+ >
548
+ > Your feedback is GOLD for tuning the thresholds before we sign
549
+ > these features off as stable for everyone.
550
+ >
5
551
  > ## ⚠️ Version con funciones aun sin probar en el mar
6
552
  >
7
553
  > Esta version trae funciones NUEVAS que **no hemos podido validar
package/README.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # signalk-pypilot-newui
2
2
 
3
+ > ### ⚠️ 2.5.0 ships new features that have not been tested at sea yet
4
+ >
5
+ > The day we cut this release there was not enough wind to validate
6
+ > the tack countdown, the post-tack stats, the tack log, the gust
7
+ > marker, or the "auto brain" that switches profiles and reacts to
8
+ > gusts for you. **Your feedback is GOLD:** install it, try it, and if
9
+ > anything looks off, tell us at
10
+ > https://github.com/Aitonos/signalk-pypilot-newui/issues
11
+ > That is how we tune the thresholds before we sign these features
12
+ > off as stable.
13
+ >
3
14
  > ### ⚠️ 2.5.0 trae funciones que aun no hemos probado en el mar
4
15
  >
5
16
  > El dia de sacarla no habia viento suficiente para validar el
@@ -207,11 +218,11 @@ in-session Revs on top of the 2.1.0 visual foundation. Highlights:
207
218
  other) with the same look as the Tune tab, a lock checkbox with
208
219
  the padlock emoji, a frozen "before:" baseline that never moves
209
220
  as you drag, and a per-slider ↺ restore button.
210
- - **Gota Chain shape swap** — after iterations with skippers using
211
- the plugin: the AMBER "A" arrow (apparent wind) is now the LARGE
212
- piece and the TEAL "T" arrow (true wind) the SMALL outer one.
213
- Colors and letters kept their canonical meaning; only the SVG
214
- paths moved so the amber piece is visually the dominant one.
221
+ - **Target/wind arrows shape swap** — after iterations with skippers
222
+ using the plugin: the AMBER "A" arrow (apparent wind) is now the
223
+ LARGE piece and the TEAL "T" arrow (true wind) the SMALL outer
224
+ one. Colors and letters kept their canonical meaning; only the
225
+ SVG paths moved so the amber piece is visually the dominant one.
215
226
  - **Full i18n audit** across EN / ES / FR / DE — every hard-coded
216
227
  English string in the non-EN bundles purged, alarm messages and
217
228
  Doctor suggestion reason / effect exposed as i18n keys + args so
@@ -247,9 +258,9 @@ pypilot version, restart pypilot_web).
247
258
  - Compass rose with HDG readout, colour-coded cardinals (N orange, E green,
248
259
  S blue, W red) + degree ticks, hull-fixed green/red sailing wedges, and 4
249
260
  configurable corner tiles (long-press to reconfigure).
250
- - **Gota Chain** overlay: three concentric arrow pieces on the rose — cyan
261
+ - **Concentric target & wind arrows**: three arrow pieces on the rose — cyan
251
262
  target diamond, amber `A` (AWA), sea-green `T` (TWA). Interlock into one
252
- clean droplet when all three angles coincide; the V-hembra of any piece
263
+ clean droplet when all three angles coincide; the V-notch of any piece
253
264
  that drifts opens up as a visible hollow, and a colour ring-tick marks
254
265
  the exact angle on the compass. Optional **COG line** from the bow
255
266
  (yellow chevrons) and **current vector** (SOG − SOW, blue wavy arrow)
package/dist/alarms.js CHANGED
@@ -30,7 +30,12 @@ exports.RULE_UNABLE_DUTY_MIN = 0.7; // 70% duty
30
30
  exports.RULE_SERVO_OVERCURR_A = 5.0;
31
31
  exports.RULE_SERVO_TEMP_C = 60;
32
32
  exports.RULE_LOW_VOLTAGE_V = 11.0;
33
- exports.RULE_PYPILOT_DISC_SEC = 10;
33
+ // Rev256 lowered this from 10 s to 3 s. Rev258 (Carlos): with the new
34
+ // pong-aware `healthy` check the socket is flagged offline after ~8 s
35
+ // of missed pings (much earlier than TCP heartbeat would), so we can
36
+ // afford a very short sustain here. Total from real pypilot power-off
37
+ // to alarm firing: ~8 s + 1 s = 9 s.
38
+ exports.RULE_PYPILOT_DISC_SEC = 1;
34
39
  exports.DEFAULT_RULES = [
35
40
  {
36
41
  id: "heading-deviation",
@@ -188,6 +193,13 @@ exports.DEFAULT_RULES = [
188
193
  evaluate: (c) => {
189
194
  if (!c.quality)
190
195
  return false;
196
+ // Rev256 (Carlos): if pypilot itself is disconnected, EVERY
197
+ // pypilot-fed sensor will read as "lost" too. Suppress this
198
+ // alarm while pypilot is down - the dedicated pypilot-disconnected
199
+ // alarm already tells the sailor the real cause and cascade
200
+ // noise only hides it.
201
+ if (!c.connected)
202
+ return false;
191
203
  return c.quality.items.some((it) => it.level === "lost");
192
204
  },
193
205
  message: (c) => {
@@ -197,7 +209,7 @@ exports.DEFAULT_RULES = [
197
209
  },
198
210
  {
199
211
  id: "pypilot-disconnected",
200
- label: "pypilot disconnected",
212
+ label: "Pypilot desconectado",
201
213
  severity: "alarm",
202
214
  defaultEnabled: true,
203
215
  sustainSec: exports.RULE_PYPILOT_DISC_SEC,
@@ -205,9 +217,26 @@ exports.DEFAULT_RULES = [
205
217
  evaluate: (c) => !c.connected,
206
218
  message: (c) => {
207
219
  if (c.disconnectedSinceMs == null)
208
- return "pypilot_web disconnected";
209
- const secs = Math.floor((c.nowMs - c.disconnectedSinceMs) / 1000);
210
- return `pypilot_web disconnected ${secs}s`;
220
+ return "Pypilot desconectado";
221
+ // Rev265/266 (Carlos): palabras completas ("segundos", "minutos",
222
+ // "horas") - las abreviaturas "s" / "min" el TTS las lee mal ("dos
223
+ // ese" en vez de "dos segundos"). Uso plural/singular correcto.
224
+ const totalSec = Math.floor((c.nowMs - c.disconnectedSinceMs) / 1000);
225
+ const h = Math.floor(totalSec / 3600);
226
+ const m = Math.floor((totalSec % 3600) / 60);
227
+ const s = totalSec % 60;
228
+ const w = (n, sing, plur) => `${n} ${n === 1 ? sing : plur}`;
229
+ let tail;
230
+ if (h > 0) {
231
+ tail = `${w(h, "hora", "horas")}, ${w(m, "minuto", "minutos")} y ${w(s, "segundo", "segundos")}`;
232
+ }
233
+ else if (m > 0) {
234
+ tail = `${w(m, "minuto", "minutos")} y ${w(s, "segundo", "segundos")}`;
235
+ }
236
+ else {
237
+ tail = w(s, "segundo", "segundos");
238
+ }
239
+ return `Pypilot desconectado desde hace ${tail}`;
211
240
  },
212
241
  },
213
242
  ];