signalk-binnacle 0.5.0 → 0.6.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 ADDED
@@ -0,0 +1,1288 @@
1
+ # Changelog
2
+
3
+ All notable changes to Binnacle are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to follow
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ <a id="v060"></a>
8
+
9
+ ## [0.6.0] - 2026-06-12
10
+
11
+ A reliability and correctness pass across the whole app: course following, the collision and anchor
12
+ watches, weather, charts, tides, and profiles, with the safety alarms now holding up in a
13
+ backgrounded browser tab. Plus: the app menu is a new tile launcher, every readout follows the
14
+ server's imperial-or-metric unit preference, and route editing loads on demand.
15
+
16
+ ### Added
17
+
18
+ - Imperial and metric display units across the whole app, following the Signal K server's unit
19
+ preferences (Server Config, Unit Preferences) with a per-profile local fallback on older
20
+ servers. Depth, anchor distances and radius, MOB range, measured legs, tide heights and station
21
+ range, temperatures, pressure, precipitation, wave heights, and visibility all convert; knots,
22
+ nautical miles, bearings, and the hPa isobar convention stay nautical.
23
+ - The app menu is now a launcher: large icon tiles grouped Navigate, Conditions, Safety, and
24
+ Settings over a dimming scrim, bottom-anchored on phones for one-handed reach, with Forecast
25
+ now findable in the menu. Both alarm mutes moved into a new Alarms panel beside the collision
26
+ thresholds.
27
+ - The measure layer supports opacity like every other overlay, starting Measure re-shows a hidden
28
+ measure layer, the Tides panel cross-links its stations layer with a show-on-chart toggle, and
29
+ layer opacity sliders have a floor so a checked safety layer can never be dimmed invisible.
30
+ - The Terra Draw route editor loads on first use instead of at startup, trimming the initial
31
+ bundle by about 137 kB for faster cold loads on Pi-class displays.
32
+ - Standard waypoints: drop one from a long press on the chart, see them as named markers, and
33
+ locate, go to, rename, or delete them from the new Waypoints panel. They live in the server's
34
+ own waypoint resources, so they interoperate with Freeboard-SK and every other client.
35
+ - An Active alerts list in the Alarms panel: every notification on the boat (engine, NMEA2000,
36
+ autopilot, or any plugin) surfaces with severity, time, and one-tap Silence and Acknowledge
37
+ that propagate to every station on a 2.28 server.
38
+ - Collision and MOB alerts ride the server's v2 Notifications API when available (server-managed
39
+ ids; muting locally silences the boat-wide alert), with the v1 delta publish kept for older
40
+ servers. Server capabilities are detected once from the features endpoint.
41
+ - The anchor watch speaks the standard Anchor API the moment a server ships it (the proposal's
42
+ drop, raise, radius, and reposition routes, feature-detected), ahead of the existing
43
+ anchoralarm-plugin path and the client-local watch.
44
+ - Custom chart symbols from the signalk-symbol-manager plugin: a note whose icon reference
45
+ resolves to a managed symbol renders that symbol (scale and anchor honored), and a provided
46
+ "waypoint" symbol replaces the built-in waypoint marker. At night-red, user artwork is remapped
47
+ into the red band so the theme's no-color rule holds. Without the plugin, every icon stays
48
+ built-in.
49
+
50
+ - Worldwide tides through the signalk-tides plugin when the server runs it (NOAA, Neaps,
51
+ WorldTides, or StormGlass per its configuration), with the NOAA CO-OPS path unchanged as the
52
+ fallback; the Tides panel says which source served.
53
+ - AIS target trails from the tracks plugin: faded wakes behind moving targets, themed for all
54
+ three themes, fetched only when the plugin is present and the layer is visible.
55
+ - Offline charts that actually work: PMTiles archives are cached as blocks in browser storage at
56
+ the protocol layer, so previously viewed chart areas render offline in every context, including
57
+ the plain-http default where no service worker can run (the old service-worker chart cache
58
+ provably never stored anything: range responses cannot enter the Cache API). Plugin-served
59
+ raster chart tiles, the seamark, bathymetry, boundary, and ice overlays, the base-map style,
60
+ and CO-OPS predictions gain service-worker caching over https, with per-cache bounds and quota
61
+ protection; opaque cross-origin responses are no longer cached (each one padded several MB of
62
+ quota).
63
+ - Tide stations and predictions, chart notes, and the vessel conditions panel now persist in
64
+ browser storage, so a reload with no signal replays the last data for the area, each item
65
+ declaring its own age, over plain http as well as https.
66
+ - When the base map style itself is unreachable (plain http at sea with no internet), the map
67
+ starts on a minimal water-colored fallback instead of staying blank, so cached charts and
68
+ every overlay still load. The real base map returns on the next load with connectivity.
69
+ - A Trends panel: depth, apparent wind, barometric pressure, and speed over the last 24 hours
70
+ as themed graphs, served by the server's v2 History API when a history provider runs
71
+ (signalk-questdb, signalk-to-influxdb2, or signalk-parquet), with provider fallback when the
72
+ default provider has no data. Without one, the graphs show the current session, sampled live.
73
+ - A "Track history (24 h)" chart layer: the vessel's server-recorded last day as a dashed line
74
+ under the live track, gap-split across stops, opt-in from the Layers panel and only queried
75
+ while shown.
76
+
77
+ ### Removed
78
+
79
+ - The browser-local PMTiles file upload. Chart files belong on the server: install the
80
+ signalk-pmtiles-plugin and drop .pmtiles files in its charts folder, and they appear in
81
+ Binnacle on every device automatically. Adding a chart by URL is unchanged and still syncs to
82
+ the server. Previously uploaded browser-local charts are dropped cleanly at upgrade.
83
+
84
+ ### Changed
85
+
86
+ - Delta batching in the stream worker now runs on a timer instead of requestAnimationFrame, and
87
+ AIS staleness pruning runs on a wall clock instead of the render loop, so live data keeps
88
+ flowing and the collision and anchor alarms keep evaluating while the tab is hidden.
89
+ - An AIS target that stops reporting is now dropped after seven minutes, so anchored traffic with
90
+ a slow AIS refresh no longer flickers in and out of the target list.
91
+ - Track recording no longer accumulates points while the boat sits at anchor: session gaps are
92
+ detected from fix continuity, not motion.
93
+ - Cancelling MOB and raising the anchor are now two-tap confirms, and deleting a profile or a
94
+ saved track asks first, matching the route delete.
95
+ - Escape handling is one shared topmost stack across the panels, the menu, and the measure tool,
96
+ so Escape always closes the surface on top and never one underneath.
97
+ - Layer drag-to-reorder now stays within the layer's own category instead of crossing into the
98
+ next section.
99
+ - Opening the Tides panel on a cold start is faster: the tide predictions and the current-station
100
+ lookup now fetch concurrently instead of back to back.
101
+
102
+ ### Fixed
103
+
104
+ - Editing a route no longer strips its waypoint names.
105
+ - The nav strip no longer shows the next waypoint's arrival time as the whole-route ETA.
106
+ - An active course now survives a page reload: the course state hydrates on first connect, and
107
+ the route's Active badge tracks the server, including courses started or cleared from another
108
+ station.
109
+ - Arrival no longer re-alarms from GPS jitter at the arrival circle; the alarm latches until the
110
+ boat clearly leaves the circle.
111
+ - A failed waypoint skip and a partially failed GPX import are now reported instead of passing
112
+ silently.
113
+ - At night-red the own vessel, the AIS targets, and the note icons are no longer hidden along
114
+ with the base map's sprite icons.
115
+ - An acknowledged collision alert re-arms once the situation clears, and a contact's severity
116
+ downgrade has hysteresis, so the alarm can neither stay silently dismissed nor flap between
117
+ danger and warning.
118
+ - A target reporting speed without a course is no longer modeled as steaming due north, and
119
+ contacts with provider-supplied CPA keep classifying during an own-fix dropout.
120
+ - The anchor watch announces a degraded state when GPS is lost, and an anchor-marker drag the
121
+ system cancels no longer silently relocates the anchor.
122
+ - The MOB strip dashes out bearing and range on a stale fix instead of presenting frozen numbers
123
+ as live.
124
+ - Wind particle colors now match the legend's absolute scale.
125
+ - Radar frames refetch on schedule: the cache no longer extends its own expiry on every read.
126
+ - A partial forecast no longer stretches stale wave pixels over a new viewport, and overlapping
127
+ forecast loads can no longer finish out of order.
128
+ - The conditions panel's forecast section falls back to the free grid when a provider returns an
129
+ empty series, and the weather panel's clock notes stay live during a long open.
130
+ - Deleting a user chart no longer leaves it in the persisted layer state, and renaming one
131
+ updates its Layers row and its server resource.
132
+ - Tide times are correct when the browser's time zone differs from the station's (predictions
133
+ are now requested in GMT), tide data refetches after midnight at anchor, and the on-chart tide
134
+ label no longer shows past events.
135
+ - Tide fetches are skipped entirely while nothing displays them.
136
+ - Note markers recover after a failed or superseded fetch instead of freezing until reload.
137
+ - A transient network failure at startup no longer wipes the stored auth token, and a failed
138
+ access request retries instead of hanging at "Requesting access".
139
+ - Profiles no longer show "unsaved changes" on every launch, deleting all profiles no longer
140
+ resurrects the starter profiles, and a synced device no longer marks a profile active without
141
+ applying it. A failed profile import shows an error.
142
+ - A refused alarm Silence or Acknowledge now shows an error in the Alarms panel instead of the
143
+ alarm just continuing to sound, and a collision alert whose server notification was cleared
144
+ from another station is re-raised instead of going silent.
145
+ - Cleared notifications no longer linger in the Active alerts list, and an unchanged
146
+ notification broadcast no longer re-renders the panel.
147
+ - AIS wakes now clear after a few minutes of failed refreshes instead of freezing in place, and
148
+ waypoint markers have their own color in each theme.
149
+ - The weather panel's layer pills stay on one scrollable row at every window width instead of
150
+ wrapping into a second header row.
151
+ - A trend history load that resolves out of order can no longer overwrite a newer result, and a
152
+ failed load shows its failure note instead of loading forever.
153
+ - A trend metric requested twice on one path with different aggregates now maps to its own
154
+ column instead of mirroring the first.
155
+ - A tide reading replayed from the offline cache remeasures the station distance from the
156
+ current position, so a reading cached a few kilometers away cannot misjudge the coverage
157
+ radius or misstate the range.
158
+ - Muting the collision alarm from the danger strip now reports a refused boat-wide silence in
159
+ the Alarms panel, matching the panel's own Silence and Acknowledge.
160
+ - Losing authorization mid-session no longer makes the collision notifier abandon its server
161
+ notification id; the v1 delta fallback carries the change until the server accepts again.
162
+ - A unit preset changed on the server while the link was down is picked up on reconnect.
163
+ - The offline cache's third-party host matchers accept only the real weather and radar domains
164
+ and their subdomains, not lookalike hostnames that merely end in the same letters.
165
+
166
+ ### Internal
167
+
168
+ - Map tile, WebGL shader, and PMTiles resources are released on teardown, dead exports and the
169
+ unused weather view persistence were removed, and assorted hot-path allocations were trimmed.
170
+ - Shared bbox helpers, a shared test fetch stub, the shared input primitive, and one global
171
+ segmented-control rule replaced per-feature copies.
172
+ - The notification mirror compares the four status flags directly instead of serializing the
173
+ status object on every delta, a coordinate-cell quantizer shared by the tides and weather
174
+ caches replaced two copies, an IndexedDB store that degrades to memory now logs one
175
+ diagnostic breadcrumb, and the alarm mute rows sit on the shared button base. New tests cover
176
+ the session trend recorder, the notification dedup, the refused silence and acknowledge
177
+ paths, and the history provider fallbacks.
178
+
179
+ <a id="v050"></a>
180
+
181
+ ## [0.5.0] - 2026-06-11
182
+
183
+ A safety-focused redesign of the man-overboard confirm, a broad weather-panel upgrade (more
184
+ decision data, honest provenance, and accessibility), and a new app icon.
185
+
186
+ ### Added
187
+
188
+ - **Gusts without a provider.** The free forecast grid now carries wind gusts, so the reefing
189
+ number shows in the tap readout and the vessel conditions panel even with no weather provider
190
+ configured.
191
+ - **Barometric tendency.** The conditions panel shows the trend a sailor decides by ("falling
192
+ 1.2 hPa/3 h"): the provider's own tendency when it reports one, otherwise computed from the
193
+ trailing three hours of the forecast grid.
194
+ - **More conditions data.** Wave and swell direction (labeled "from"), visibility, and water
195
+ temperature appear when the source carries them, and the current block is tagged Observed or
196
+ Forecast with its valid time and zone.
197
+ - **Forecast provenance.** A footer states the source and fetch time, the stale note says how old
198
+ the shown forecast is, and a grid missing its requested wave fields is qualified rather than
199
+ passed off as complete.
200
+ - **Radar honesty.** The legend names the frame the loop is painting (for example "frame
201
+ -40 min"), extrapolated nowcast frames are labeled as such, cached radar is flagged when
202
+ offline, and the radar hides while the time slider is away from now instead of painting live
203
+ rain over a three-day-out wind field.
204
+ - **Slider orientation.** A tick marks now on the forecast slider, the label carries Past or
205
+ Forecast plus the time zone, and a one-shot note explains the zoom cap the first time you
206
+ pinch into it.
207
+ - **New app icon**, aligned with the rest of the plugin family: a compass rose on a white
208
+ compass-card badge over the shared ocean-wave mark.
209
+
210
+ ### Changed
211
+
212
+ - **Man overboard confirm.** Pressing MOB now opens a centered dialog. The position is captured
213
+ at the press, so the seconds spent confirming can no longer carry the mark away from the
214
+ person; the confirm only gates the alarm. One full-width Mark man overboard button sits in the
215
+ one-handed thumb zone with a quiet Cancel stacked above it, the dialog self-dismisses after 15
216
+ seconds with a visible countdown (a re-press shortly after reuses the earlier press-time fix),
217
+ and without a GPS fix the boat-wide alarm still raises, position-less, with a clear warning.
218
+ The recovery strip adds the wall-clock Marked time for the log and the VHF relay.
219
+ - **Weather opens at now.** The time slider seeds to the forecast step nearest now instead of
220
+ the start of the series, which begins up to a day in the past.
221
+ - **Conditions track the slider.** With a weather provider configured, the Here panel re-picks
222
+ the forecast step as the slider moves, and it falls back to the free grid when the provider
223
+ fails instead of freezing a one-shot sample.
224
+ - **Warnings.** Sorted most severe first, with the issuing source and the validity window, at a
225
+ readable size; free mode now says warnings are unavailable instead of showing a silently empty
226
+ list.
227
+ - **Legends.** Wind in whole 10-knot bands, cloud in whole percent, and weather readouts in
228
+ whole knots.
229
+ - **Night-red.** The map attribution control follows the theme on both maps instead of rendering
230
+ as a bright white bar.
231
+
232
+ ### Fixed
233
+
234
+ - The tapped readout blends the two forecast steps exactly as the drawn fields do, so the number
235
+ can no longer disagree with the picture under the finger by a full step.
236
+ - Weather provider detection re-runs when the auth token arrives or the stream reconnects; one
237
+ failed probe no longer locks the whole session onto the free sources.
238
+ - The latest observation is picked by date rather than response order, and a provider's last
239
+ forecast step no longer answers for a time days past its horizon.
240
+ - A rate-limited marine (waves) endpoint no longer blocks the healthy atmospheric fetch, so
241
+ turning waves off recovers immediately.
242
+ - Provider precipitation is labeled as the accumulation it is (mm), not a rate.
243
+ - The course strip no longer covers the weather panel's slider and legend while a route is
244
+ active; the strips lift to the panel's top edge instead.
245
+ - Accessibility: the forecast slider announces real times instead of epoch milliseconds, manual
246
+ time changes are announced, the floating map notes are reliable live regions that stack
247
+ instead of overlapping, scrubbing stops playback so the thumb is not yanked back mid-drag,
248
+ Enter on the focused mini-map samples the center, and the tap readout can be pinned (hover or
249
+ focus) and dismissed.
250
+
251
+ <a id="v040"></a>
252
+
253
+ ## [0.4.0] - 2026-06-10
254
+
255
+ Four new at-sea features (an anchor watch, a man-overboard button, a measure tool, and an AIS
256
+ target list) plus shell refinements from helm feedback.
257
+
258
+ ### Added
259
+
260
+ - **Anchor watch.** Drop the anchor at the boat, set the swing radius by hand or capture it from
261
+ the live distance plus a margin, and get a drag alarm after three consecutive fixes outside the
262
+ circle. The alarm latches until acknowledged, so a boat that swings back inside cannot silently
263
+ clear an alarm you never saw, and the watch survives a reload. When the signalk-anchoralarm-plugin
264
+ is installed, Binnacle drives it instead, so the alarm keeps running with the browser closed; the
265
+ panel says which mode is watching. On the chart: the swing circle, a rode line, and a drop-point
266
+ marker you can drag to where the hook actually lies.
267
+ - **Man overboard.** An always-visible MOB button centered in the top bar. One tap pops out a
268
+ large confirm (so a stray tap can never raise the alarm, and the window self-dismisses); the
269
+ confirm marks the spot, publishes the boat-wide `notifications.mob` alarm so every station sees
270
+ it, flies the chart to the mark, and raises a recovery strip with live bearing, range, and
271
+ elapsed time. Steering to the mark stays a deliberate second tap (Steer to MOB) through the
272
+ course system, never automatic, since a coupled autopilot may follow the course. An MOB raised
273
+ by another station shows here too, and the mark survives a reload.
274
+ - **Measure tool.** Arm it from the menu, tap points on the chart, and read each leg's range and
275
+ bearing plus the running total, with the total labeled at the last point. Undo, Clear, Done, or
276
+ Escape.
277
+ - **AIS target list.** Every tracked target as a tappable card with name or MMSI, live range and
278
+ bearing, SOG, and CPA and TCPA when available, sortable by range, CPA, or name, with the
279
+ lookout's severity coloring risky contacts and a tap flying the chart to the target.
280
+ - A depth readout in the anchor panel when a sounder publishes `environment.depth.belowTransducer`.
281
+
282
+ ### Changed
283
+
284
+ - The footer's "Connected" text is now a compact status dot: green by day and dusk, a calm dim red
285
+ in night-red (which forbids green), and the caution color while the stream is down. The label
286
+ remains for screen readers and the hover title, and the dot stays on phones where the word used
287
+ to be hidden.
288
+ - The trailing position cluster's numerals now take the same instrument-readout size as the
289
+ leading AIS, SOG, and COG readouts, so the footer reads as one instrument row.
290
+ - The four feature alarms now share one edge-triggered core (`GatedAlarm`), with the collision
291
+ alarm keeping its escalation-overrides-mute policy on top; tones are unchanged, and each alarm
292
+ remains audibly distinct (MOB above the collision two-beep, anchor between collision and
293
+ arrival).
294
+
295
+ ### Fixed
296
+
297
+ - The MOB trigger's boat-wide notification never actually left the browser: the position object
298
+ read from the live store is a reactive proxy, which cannot be structured-cloned into the stream
299
+ worker, so the publish threw and was lost while the local strip looked fine. The mark is now
300
+ snapshotted into a plain object, with a regression test, and the round trip is verified against
301
+ a live server.
302
+
303
+ <a id="v031"></a>
304
+
305
+ ## [0.3.1] - 2026-06-10
306
+
307
+ A pass over the existing features for safety, honesty under failure, performance on modest hardware,
308
+ and accessibility, plus a few navigation additions.
309
+
310
+ ### Added
311
+
312
+ - Honest data-staleness signals. When the position feed stops, the footer shows a calm "No GPS fix"
313
+ note and dashes SOG and COG instead of presenting a frozen speed and course as if they were live,
314
+ and the collision watch and the course guidance stop computing against the stale fix. The connection
315
+ badge turns to a caution color and reads "Reconnecting" or "Not connected" during an outage instead
316
+ of staying "Connected".
317
+ - A cross-track deviation needle (a CDI) on the nav strip, so steering to track is a glance rather than
318
+ a number read, with a caution color when it pegs at full scale.
319
+ - An on-screen arrival banner paired with the arrival tone, for a helm with the volume low.
320
+ - A footer "AIS" chip showing how many targets the collision watch is tracking, so an empty danger
321
+ strip reads as all-clear rather than as a possible failure.
322
+ - A status note in the weather panel (loading, offline, or showing the last forecast) instead of a
323
+ blank or silently outdated map.
324
+
325
+ ### Changed
326
+
327
+ - The collision-alarm mute is now session-only and auto-expires after ten minutes, then re-arms; it is
328
+ no longer persisted across a reload or carried by a profile, so a mute set in a crowded anchorage
329
+ cannot silently follow you into the next passage. A close, imminent contact (inside about 0.1 nm and
330
+ two minutes) overrides both mute and acknowledge, so a real emergency always sounds. Acknowledging a
331
+ danger now keeps the strip on screen, dimmed, with its CPA and TCPA, while the target is still
332
+ closing, instead of hiding it.
333
+ - Deleting a route now asks to confirm, with distinct wording when the delete will also stop active
334
+ navigation. The nav strip disables waypoint-skip at the first and last points and keeps a gutter
335
+ before Stop, so a mis-tap cannot end navigation.
336
+ - The footer SOG and COG step up to the full instrument-readout size, the numbers a helmsman glances
337
+ at most.
338
+ - Performance on the Raspberry Pi: the chart's overlays no longer sync at full frame rate while idle
339
+ (they update on real map repaints plus a low-frequency tick and pause when the tab is hidden), the
340
+ weather fields and the wind particle field stop forcing continuous GPU work, and a long track
341
+ simplifies incrementally rather than re-processing the whole track on every fix.
342
+ - Signal K conformance: TCPA accepts the spec's ISO-8601 duration form, the nav strip prefers the
343
+ server's estimated time of arrival when a provider supplies it, the client-side course fallback uses
344
+ consistent rhumb-line geometry, and a target that stops reporting is dropped after three minutes
345
+ rather than six.
346
+ - Resilience: in-app requests time out instead of hanging on a half-open link, the local chart and
347
+ track stores recover after a transient IndexedDB failure instead of dropping to memory for the
348
+ session, the stream reconnects immediately when the network returns, and persisted charts and
349
+ profiles are validated on load so a drifted entry is dropped rather than trusted.
350
+ - Accessibility: opening a panel moves focus into it, the Forecast and Here toggles use aria-expanded,
351
+ in-place confirm and review steps move focus to their new control, a failed chart import is
352
+ announced, and the app menu stays open when a mute toggle is flipped.
353
+
354
+ ### Fixed
355
+
356
+ - The wind particle field no longer freezes after switching away from and back to the browser tab.
357
+
358
+ <a id="v030"></a>
359
+
360
+ ## [0.3.0] - 2026-06-09
361
+
362
+ ### Added
363
+
364
+ - Profiles: named bundles of your settings (theme, which layers are on, their opacity and order, the
365
+ weather layers, the collision thresholds, the track and planning settings, and the alarm mutes) that
366
+ you save, switch between, rename, delete, and set a default for. A switcher pill in the top bar shows
367
+ the active profile and opens a Profiles panel; applying a profile updates the chart live, and tweaking
368
+ a setting marks the profile as edited so you can save the change or discard it by switching away.
369
+ Three starter profiles (Coastal day, Night passage, and At anchor) seed on first run. Profiles are
370
+ stored locally, and when you are logged in to a secured SignalK server they also sync through the
371
+ server's applicationData store so they follow you across devices. The sync degrades cleanly: an
372
+ unsecured server, or a login that cannot use the applicationData store, keeps profiles local, and a
373
+ login that can read the store but not write it stops after one rejected write rather than retrying on
374
+ every edit, so it never floods the console. You can also export a profile to a JSON file and import
375
+ profiles from one, to back them up or share them between boats.
376
+
377
+ - Course planning on the chart. Long-press (touch) or right-click (desktop) a point and choose "Go to
378
+ here" to navigate straight to it via the Course API, with the destination shown on the nav strip.
379
+ - Route interchange via GPX. Export any saved route to a GPX file other plotters, MFDs, and
380
+ Freeboard-SK read, and import routes from a GPX file back into Binnacle, closing the round trip.
381
+ - Passage planning in the route editor. A persisted plan speed turns the leg table into a passage
382
+ plan, showing the cumulative time to reach each waypoint and a whole-route Time alongside the
383
+ distance, plus a per-leg distance and bearing table that updates live as waypoints are dragged.
384
+ - Track-to-route workflows. Save the current track as a reusable route, reverse a saved route for the
385
+ return leg, navigate home by retracing the current track, and skip the active route's waypoint
386
+ forward or back from the nav strip. The nav strip also shows the whole-route distance and arrival
387
+ time when a multi-leg route is active.
388
+ - A minimize control on the Routes panel. On a phone the panel is a bottom sheet that covers the chart,
389
+ so a chevron in the header collapses it to just the header bar while it stays open, freeing the chart
390
+ to tap waypoints into a route. The control only appears at phone widths.
391
+
392
+ ### Changed
393
+
394
+ - The Layers panel now leads with "My routes and tracks" above "Traffic and live data". The panel
395
+ order is kept aligned with the map stack so drag-to-reorder lands coherently, so this also raises
396
+ the routes and track layers above AIS and the reference overlays on the chart; the own vessel and
397
+ the collision rings stay pinned on top.
398
+ - The Tracks panel now renders saved tracks as the same elevated cards as the Routes panel, each
399
+ showing the track's distance and duration, and the current-track stats line was tightened to a
400
+ label, value, and unit grid that removes the trailing whitespace and aligns the values in a column.
401
+ - At night-red, the base map's pre-colored sprite icons (road and transit shields, aerodrome marks)
402
+ are now hidden along with the POI dots, so the chart stays pure red on black with no stray blue,
403
+ green, or white icons. The text labels stay visible.
404
+ - The route line, the note selection ring, and the AIS target triangles gained a dark casing or halo,
405
+ so they keep their bright color but no longer sit low-contrast against the light day water. The
406
+ casing is invisible on the dark dusk and night-red maps, where the bright shape reads on its own.
407
+
408
+ ### Fixed
409
+
410
+ - Importing a GPX route no longer aborts on a malformed numeric character entity: a code point outside
411
+ the Unicode range now stays literal instead of throwing an uncaught error that ended the whole import.
412
+ - The Routes opacity slider now dims the waypoint labels along with the route line and markers, the way
413
+ the tides and notes overlays already did.
414
+ - The active-route strip's previous and next waypoint buttons are now a full 44px touch target, so they
415
+ are usable underway.
416
+ - The precipitation legend now reads up to 40 mm/h, matching the range the precipitation field paints.
417
+
418
+ ### Internal
419
+
420
+ - A simplification pass over the overlay work: each new overlay slice (seamarks, protected areas,
421
+ boundaries, ocean conditions) now exposes a band-owning factory, so the chart widget no longer
422
+ hardcodes which map band each draws into (matching the depth-charts sibling). Deduplicated the
423
+ chart zoom-range expression and the tides upcoming-events computation, moved the station-distance
424
+ formatter into the tides display module, and tidied a handful of comments.
425
+ - A simplification pass over the plotter and UI changes: the saved-item card list moved to one global
426
+ `.saved` system in `app.css` consumed by both panels, a shared `downloadBlob` helper backs the
427
+ track and route exporters, the GPX escape and unescape pair moved to one `xml-entities` module, the
428
+ distance-over-speed time uses the shared `etaSeconds` everywhere, the meters-per-degree constant is
429
+ shared from `$shared/nav`, the nav-strip `RouteProgress` type has one definition, the plan-speed
430
+ field uses the shared `.input`, and the whole-route distance derives from the leg table so the total
431
+ and the per-leg numbers cannot drift.
432
+ - A simplification pass over the contrast work: the route, selection-ring, and AIS dark contrast aids share
433
+ one `DARK_SCRIM` constant and an `rgbaCss` helper in `$shared/map` instead of three drifting literals,
434
+ and the SlideOver minimize takes one `{ collapsed, onToggle }` object so the state and its toggle are
435
+ always supplied together.
436
+ - A simplification pass over the profiles feature: the active-card accent edge-bar and "Active" badge moved
437
+ to the one global `.saved` system in `app.css` consumed by the Routes, Tracks, and Profiles panels, a
438
+ shared `pickTextFile` helper in `$shared/ui` backs both the route GPX import and the profile JSON
439
+ import instead of two hidden-input handlers, the profile importer validates the theme against the
440
+ shared `THEMES` list, and the default profile now auto-applies on a fresh device that has a default
441
+ but no active profile. The server sync also stops pushing after one rejected write, so a read-only
442
+ token attaches for reads but does not fire a doomed write on every later edit.
443
+ - A project-wide modularization pass. The labeled current-item stats grid that the
444
+ Routes editor and the Tracks panel duplicated verbatim is now one global `.stat-grid` system in
445
+ `app.css`. A shared `downloadText(filename, text, type)` helper backs the route GPX, track GeoJSON,
446
+ and profile JSON exporters, so the Blob construction lives in one place. The four weather colormaps
447
+ (wind, waves, precipitation, and cloud) build on a new `themedRamp(day, night)` factory in
448
+ `color-ramp.ts`, so the night-red ramp swap is defined once instead of repeated per colormap. Two
449
+ dependency-cruiser rules, `no-cross-slice-shared` and `no-cross-slice-entities`, now enforce that a
450
+ shared or entities slice reaches a sibling only through its index public API, matching the existing
451
+ `no-cross-feature` rule.
452
+ - The deferred modularization items. A shared `fetchJsonOrUndefined` helper backs the free-API weather
453
+ clients and the course REST hydration; a shared `MemoryCache` backs the weather grid cache and the
454
+ tides per-station caches; the `SlideOver` shell gained a subtitle and a footer so the last hand-rolled
455
+ panel (the note detail) folds into it; a shared `SavedList` and `VisibilityToggle` back the Routes,
456
+ Tracks, and Profiles cards; the coordinate guards moved to a `shared/geo` slice so geometry no longer
457
+ imports the SignalK transport slice for a type; and the portable profile settings are a single
458
+ declarative registry typed so a forgotten setting fails the build.
459
+ - A whole-repo cleanup. Correctness fixes (the GPX entity guard, the route label opacity, a
460
+ marine-merge step-count guard, a deferred object-URL revoke, and a wind-arrow bounds guard), a round
461
+ of dead index re-export trimming, the four repeated course hydrate-and-seed sites folded into one
462
+ helper, and assorted comment and legend fixes.
463
+
464
+ <a id="v021"></a>
465
+
466
+ ## [0.2.1] - 2026-06-09
467
+
468
+ ### Fixed
469
+
470
+ - Points of interest (Crow's Nest and ActiveCaptain) no longer flicker. A slow or rate-limited
471
+ provider response made the markers vanish and reappear, because a failed fetch was treated as
472
+ "no POIs" and cleared them. A failed fetch now keeps the markers on screen, and the overlay
473
+ caches fetched sets by area, so panning back, panning a little, or zooming in reuses a recent
474
+ fetch instead of re-hitting the network.
475
+ - Active marine warnings (gale, storm, and small-craft advisories) no longer flicker off the
476
+ conditions panel when a weather-provider request transiently fails: the last warnings are kept
477
+ until a real update replaces them.
478
+ - An active route's guidance (the nav strip, arrival circle, and auto-advance) no longer freezes on
479
+ stale values after a stream reconnect. The v2 course data is re-hydrated on reconnect, since
480
+ resubscribing cannot redeliver it under subscribe=none, and the route list is refreshed too.
481
+ - Server charts no longer blank on a transient failure at map load: the fetch now distinguishes a
482
+ failed request from a reachable server with no charts, matching routes and points of interest.
483
+ - Imported-chart storage is reclaimed at startup: a PMTiles blob left behind by a failed save or a
484
+ delete that ran while storage was degraded is now swept once its descriptor is gone, so orphaned
485
+ blobs cannot accumulate on disk.
486
+ - The Points-of-interest layer no longer fetches from the provider or re-clusters while it is toggled
487
+ off: a hidden layer now does no network or rendering work until it is shown again.
488
+ - Silenced a stream of "styleimagemissing" console warnings: the base map style references a few
489
+ sprite icons and landuse patterns its published sprite does not contain, so a transparent
490
+ placeholder is supplied for each. The map is unaffected; the console stays clean.
491
+
492
+ ### Internal
493
+
494
+ - Bounded two more caches to a fixed size: the note-detail memo, and the persistent weather-grid
495
+ store (now matching its in-memory tier).
496
+ - A whole-codebase cleanup pass: deduplicated the local-date computation shared by
497
+ the tides fetch window and its session-cache key, sourced the vessel and AIS default colors from
498
+ the day theme token, dropped over-broad slice exports, reset the stream reconnect backoff on an
499
+ explicit disconnect, and tidied several comments, an accessibility region, and a redundant live
500
+ region. No user-facing behavior change beyond the fixes above.
501
+
502
+ <a id="v020"></a>
503
+
504
+ ## [0.2.0] - 2026-06-08
505
+
506
+ ### Added
507
+
508
+ - A Tides panel (US waters), opened from the app menu, showing the nearest NOAA tide station's next
509
+ high and low with heights in meters and feet, a 48-hour tide curve with a "now" marker, and the
510
+ nearest tidal-current station's next flood or ebb with its rate and set. The nearest tide and
511
+ current stations are also markable on the chart from the Layers panel. It degrades to a clear
512
+ message outside US coverage, and the data is cached for the session.
513
+ - New built-in chart overlays, all free, key-free, and verified against the live services. Each
514
+ starts hidden, toggles from the Layers panel, and carries its source attribution:
515
+ - OpenSeaMap seamarks: a global overlay of navigation aids (buoys, beacons, lights, and harbors).
516
+ - Marine protected areas: EMODnet protected areas with Natura 2000 nested under them (EU), and the
517
+ NOAA MPA Inventory (US).
518
+ - Maritime boundaries: the inter-country jurisdiction lines and the territorial sea (12 nm), so you
519
+ can see when a passage crosses into another country's waters.
520
+ - Ocean conditions: NASA GIBS sea-surface temperature and sea ice concentration (global, daily),
521
+ which appear in a new Ocean conditions section of the Layers panel and default to translucent.
522
+ - A review step when importing a chart: after the file or URL is read, you can rename it and check
523
+ its type, zoom range, and size before saving, instead of it saving immediately.
524
+ - URL-based imported charts now register on the Signal K server as a chart resource, so other
525
+ devices on the same server discover them, and deleting the chart removes the server resource too.
526
+ The chart stays manageable locally (a synced chart is shown once, not twice). File-based imports
527
+ stay on the importing device, since a stock server cannot host the file bytes.
528
+
529
+ ### Changed
530
+
531
+ - The Layers panel is reorganized into collapsible categories (Traffic and live data, Navigation
532
+ aids, Areas and boundaries, My routes and tracks, Ocean conditions, and Charts and depth), each
533
+ with a row count, so a long flat list reads as a few sections. The two most-used categories open by
534
+ default and the rest collapse to cut clutter; each category remembers whether you left it open or
535
+ closed. Per-layer toggles, opacity, the facet-group cards, and drag-to-reorder are unchanged.
536
+ - A better default chart order: the US NOAA ENC nautical chart leads, then US BlueTopo bathymetry,
537
+ then EMODnet (Europe), then GEBCO (global), so the most detailed free coverage is on top. You can
538
+ still drag any layer to reorder it.
539
+ - Cleaner, consistent layer names: sentence case throughout, plural for collection layers (Track is
540
+ now Tracks), and a unified "source, type, region" format for the single-layer bathymetry overlays
541
+ (for example "GEBCO bathymetry (global)"). The weather "Cloud cover" layer is now "Cloud" to match
542
+ the other single-word weather layers.
543
+ - A denser, more consistent app menu and Layers panel: list rows use a compact row size while action
544
+ buttons keep the larger touch target, the opacity sliders sit in shorter rows, and the gaps are
545
+ snapped to one spacing scale, so more layers and menu items fit without scrolling.
546
+ - Three depth sources now present as labeled groups in the Layers panel, each with a base facet and a
547
+ nested survey-quality facet: "NOAA ENC (US)" (Base chart, Data quality (ZOC)), "EMODnet (Europe)"
548
+ (Bathymetry, Quality index), and "BlueTopo (US)" (Bathymetry, Uncertainty). The quality facets show
549
+ how reliable each cell is: ZOC zones for the ENC, EMODnet's combined quality index, and BlueTopo's
550
+ per-cell vertical uncertainty. Within a group the facet toggles are aligned in one column, a single
551
+ drag handle and opacity slider serve the whole group, and the quality facet only enables while the
552
+ base facet is on (turning the base off hides it). A generic sub-layer grouping mechanism backs this,
553
+ so any future multi-facet chart can group its facets the same way.
554
+
555
+ ### Fixed
556
+
557
+ - Deleting a user-imported chart now actually removes it. The layer row, the map overlay, and the
558
+ stored descriptor were all left in place because the delete handler read the chart id after the
559
+ panel had already cleared its selection, so the removal threw and never ran.
560
+ - A chart imported while a dark theme (dusk or night-red) is active now takes the theme immediately,
561
+ instead of staying in day colors until the next theme change. Overlays registered after the first
562
+ recolor are now themed at registration.
563
+ - The nearest tide and tidal-current readings no longer briefly go stale around local midnight: the
564
+ session cache now rolls over on the same local day the NOAA forecast window uses, rather than on
565
+ the UTC day.
566
+
567
+ ### Internal
568
+
569
+ - A whole-codebase cleanup pass: named the shared millisecond constants once (MINUTE_MS, HOUR_MS,
570
+ DAY_MS), added a placeholder-aware nautical-miles readout, deduplicated the base-theme style
571
+ lookups, removed a dead weather-provider field and an empty re-export shim, tightened the
572
+ storage and chart-adapter export surface, and aligned the route and wave overlay label font and
573
+ arrow color with the other overlays. No user-facing behavior change beyond the fixes above.
574
+
575
+ <a id="v013"></a>
576
+
577
+ ## [0.1.3] - 2026-06-08
578
+
579
+ ### Added
580
+
581
+ - A fully keyboard-navigable app menu: arrow keys move between items, Home and End jump to the ends,
582
+ and it follows the WAI-ARIA menu pattern with grouped sections (Navigation and Alarms), roving
583
+ focus, and announced toggle states.
584
+ - Reduced-motion support: when the system prefers reduced motion, the chart's center-on-boat,
585
+ fly-to, and fit-to-bounds camera moves jump instantly instead of animating.
586
+ - An opt-in NOAA ENC data-quality overlay. The NOAA ENC chart is now one clean chart layer plus a
587
+ separate, default-hidden "data quality" layer carrying the Zones of Confidence (CATZOC) ratings,
588
+ so the chart no longer always paints the data-quality triangles and overscale patterns on top.
589
+ - One-tap alarm muting from the danger strip, a muted-alarm badge in the top bar, and a spoken
590
+ collision summary written to a live region for assistive technology.
591
+ - Motion and depth across the interface: the slide-over panels, the app menu, and the weather panel
592
+ now reveal with a short reduced-motion-aware transition, the theme toggle animates its icon on each
593
+ cycle, and the panels and menu carry a layered shadow. A more confident day palette and a larger
594
+ instrument-readout type tier make the hero numbers (SOG, the nav metrics, the conditions) dominate
595
+ their labels.
596
+
597
+ ### Changed
598
+
599
+ - Bearings are now labeled true (123 degrees T) on the COG, BTW, and wind readouts, time-to-go shows
600
+ hours and minutes past an hour (2h 05m) instead of a bare minute count, and the collision strip and
601
+ its spoken summary are graded danger versus caution by the worst contact rather than always
602
+ sounding full danger.
603
+ - The design system was consolidated: one shared lit-toggle, input, slider,
604
+ and button-row vocabulary replaces the per-component copies, the danger and nav strips now stack
605
+ instead of overlapping so course guidance survives a close-quarters contact, and the danger strip
606
+ stays above the weather panel so Mute and Acknowledge are always reachable.
607
+
608
+ - The app menu is redesigned. Tracks, Routes, and Layers are now edge-docked slide-over panels
609
+ promoted from inline accordions, each with a back-to-menu button so you can move between panels
610
+ without reopening the menu, and the menu groups its items under section headers.
611
+ - Center, Follow, and Forecast now sit together in the bottom status strip as three matching labeled
612
+ pill buttons, in that order. Follow and Forecast show a clear lit on-state, kept dim enough for
613
+ night-red.
614
+ - A whole-codebase cleanup pass with no change to behavior beyond the fixes below: the Signal K
615
+ frame pipeline hands the per-frame value map straight to the store instead of rebuilding it each
616
+ frame, the active-route readouts compute each leg's geometry once per change rather than several
617
+ times per render, the Layers drag measures row positions once at drag start instead of on every
618
+ pointer move, and duplicated formatting, geometry, WMS, and map-image helpers were consolidated.
619
+
620
+ ### Fixed
621
+
622
+ - The bottom status strip no longer overlaps or wraps unevenly on a phone. It stacks into a clean
623
+ layout: the live readouts above, and the Center, Follow, and Forecast controls on one row below.
624
+ - The Tracks panel's statistics now align in a single value column.
625
+ - The collision danger strip no longer double-announces to screen readers. The app keeps a single
626
+ concise spoken summary of the danger, and the on-screen contact list is now a silent visual
627
+ landmark, so assistive technology reads the danger once instead of twice.
628
+ - The animated wind field now honors the system reduced-motion preference, falling back to the static
629
+ wind arrows instead of running a continuous particle animation.
630
+ - The active-route strip no longer re-reads its whole readout line to a screen reader every second;
631
+ only the destination name announces, when a waypoint advances.
632
+ - On a phone the note detail and a leading panel no longer overlap as stacked bottom sheets (they are
633
+ mutually exclusive at narrow widths), the brand drops its version string so the top-bar controls
634
+ keep room, and the weather "Here" conditions open as a full-width sheet rather than covering the
635
+ small map.
636
+ - Form inputs theme their placeholder text, the day caution color is darker for contrast and is
637
+ clearly distinct from the alarm red, and the Forecast control exposes its dialog to assistive tech.
638
+
639
+ <a id="v012"></a>
640
+
641
+ ## [0.1.2] - 2026-06-05
642
+
643
+ ### Fixed
644
+
645
+ - Importing a chart now flies the map to the chart's bounds, so a PMTiles archive (by file or URL)
646
+ is immediately visible instead of staying off-screen when it covers a different area than the
647
+ current view. Previously the chart was added to the Layers panel but the map did not move, so it
648
+ looked like nothing happened. (A new fitBounds map command, fired only on a user import, never on
649
+ the charts restored at startup.)
650
+
651
+ ### Changed
652
+
653
+ - The layer-toggle checkbox no longer shrinks when a layer name is long: it stays square (the name
654
+ ellipsizes instead).
655
+
656
+ <a id="v011"></a>
657
+
658
+ ## [0.1.1] - 2026-06-05
659
+
660
+ ### Changed
661
+
662
+ - App Store polish, reviewed against the Signal K AppStore publishing doc. The appIcon is now
663
+ 256x256 (the previous 72x72 was below the documented 128x128 minimum). The README is scannable,
664
+ since the server's Webapps view renders it: the screenshots gallery that showed as raw HTML there
665
+ is removed (the screenshots stay in `signalk.screenshots` for the App Store detail page), and the
666
+ duplicate feature inventory is collapsed into one concise list. The title is now "WebGL chart
667
+ plotter for Signal K" across the README, the PWA manifest, and the repository description.
668
+
669
+ <a id="v010"></a>
670
+
671
+ ## [0.1.0] - 2026-06-05
672
+
673
+ ### Added
674
+
675
+ - Routes: plan a passage and follow it. Open Routes from the menu, draw a route on the chart (tap to
676
+ add waypoints, drag a point to move it, tap a midpoint to insert one), and watch the leg count and
677
+ total distance update live as you draw. Save it to the Signal K server (`/resources/routes` as a
678
+ GeoJSON LineString), and it syncs to every device and lists with show or hide, edit, activate, and
679
+ delete. Activating a route hands it to the Signal K v2 Course API, and a nav strip shows the active
680
+ waypoint, cross-track error with a steer-left or steer-right side, distance and bearing to the
681
+ waypoint, velocity made good, and time to go, with an arrival alarm at the arrival circle. The
682
+ guidance prefers the server's course calculations and computes them on the client when the
683
+ course-provider plugin is absent (a "computing locally" badge says when), the same graceful
684
+ degrade as the collision CPA. A failed save keeps the route under edit so nothing is lost, and the
685
+ on-chart editing line is themed for day, dusk, and night-red. Route editing uses Terra Draw.
686
+
687
+ - The weather forecast is cached in IndexedDB, so it survives a reload and a return to a recent view
688
+ reuses it instead of re-fetching. Unlike the service-worker cache, which browsers expose only in a
689
+ secure context, IndexedDB works over plain HTTP, so this is the offline-leaning weather cache for
690
+ the many users without SSL. Each grid is stored with a one-hour expiry, expiries are kept apart from
691
+ the grids so pruning never loads them, and the store degrades to memory and never throws when
692
+ IndexedDB is unavailable. Verified over https: after a reload, opening the forecast served the grid
693
+ from IndexedDB with zero Open-Meteo requests.
694
+
695
+ - Weather. A dedicated weather mini-map, opened by the Forecast button centered in the status strip,
696
+ keeps the navigation chart clean and the weather within its data resolution. The mini-map is capped
697
+ at zoom 7 (RainViewer's real radar resolution) and panned independently of the chart, so weather can
698
+ never be zoomed past what the data supports: no "zoom not supported" tiles, no pretending a coarse
699
+ grid has street-level detail. In the panel you toggle Wind, Pressure, Waves, Precipitation, Cloud
700
+ cover, or Rain radar, scrub the coming days with a time slider, read a per-layer color-ramp legend,
701
+ and tap anywhere for the wind, pressure, sea state, and rain at that point and time. Wind draws as
702
+ speed-colored arrows, mean-sea-level pressure as labeled isobar contours (marching squares, 4 hPa),
703
+ significant wave height, precipitation, and cloud cover as smooth color fields, and precipitation
704
+ radar as an animated RainViewer loop. The four area fills (waves, precipitation, cloud, and radar)
705
+ are mutually exclusive, one at a time, so they never stack into mud; wind arrows and pressure
706
+ isobars stay freely combinable on top. A "Here" panel shows the current conditions, a short
707
+ forecast, and any gale or storm warnings for the vessel's own position.
708
+ - Weather data prefers a configured Signal K weather provider (for example AccuWeather) for point
709
+ data: the tap readout and the "Here" conditions and warnings come from the provider when one is
710
+ set, and fall back automatically to the free, browser-only sources when none is configured. Area
711
+ data is always free, because no provider exposes gridded fields through Signal K: the atmospheric
712
+ grid and the marine wave field come from Open-Meteo, and radar from RainViewer, with no key and no
713
+ server plugin. Results are cached by viewport in memory so panning reuses a recent fetch, and the
714
+ Open-Meteo responses, the RainViewer frame index, and the radar tiles are cached by the service
715
+ worker for offline use. Layers beyond wind and waves are off on first open, themed for day, dusk,
716
+ and night-red (a deep, low-brightness red on black at night, no blue; the radar raster is
717
+ desaturated and dimmed).
718
+ - Wind draws as an animated WebGL particle field, the glanceable signature layer: thousands of
719
+ particles stream through the forecast wind with fading trails, colored by speed (the day ramp, and
720
+ a pure red-on-black ramp at night). It is a custom MapLibre layer running a GPU particle simulation
721
+ over the forecast u/v, projected so pan and zoom only reproject the particles and the trails reset
722
+ cleanly on a move. It falls back to the speed-colored arrow layer when WebGL is unavailable, and
723
+ the animation runs only while the Wind layer is on.
724
+
725
+ - Approving Binnacle's Signal K access is now self-explanatory and recognizable. The request uses a
726
+ named client id (`binnacle-<short>`) instead of a bare UUID, so it is easy to spot in the Signal K
727
+ access-requests list, and the "Requesting access" banner shows that id plus a one-click "Approve in
728
+ Signal K" shortcut that opens the admin access-requests page. A legacy bare-UUID client id is
729
+ upgraded to the named form on load, keeping any existing token.
730
+
731
+ - Follow boat: a "Follow boat" item in the menu locks the chart to the vessel, recentering on each
732
+ new position fix at your current zoom. It centers immediately when turned on, and a manual pan of
733
+ the chart releases the lock (a scroll-zoom keeps it). It is off by default and does not persist
734
+ across reloads. The one-shot "Center on boat" remains for a quick recenter that also zooms in when
735
+ you are zoomed far out.
736
+
737
+ - A Layers panel and drag-to-reorder for every layer. The old inline "Layers" submenu is now a
738
+ "Layers and charts" launcher that opens a left-docked slide-over listing every layer top of the map
739
+ first, grouped into "Charts and Depth" and "Overlays" with the own vessel and active alarms pinned on
740
+ top. Each row toggles, sets opacity, and drags (by pointer or keyboard) to restack the z-order, and
741
+ the order persists across visits. The panel docks opposite the note detail so both can be open at
742
+ once, and it themes for day, dusk, and night-red.
743
+
744
+ - Streaming depth charts. Four free hosted bathymetry and chart sources toggle on from the Charts
745
+ and Depth section, off by default and cached as you pan: GEBCO global bathymetry, EMODnet (Europe), the
746
+ NOAA ENC chart (US), and NOAA BlueTopo (US). They are reference overlays, not certified for
747
+ navigation. A raster source cannot be recolored, so at night it is desaturated and dimmed (no blue,
748
+ low brightness) rather than left full-color.
749
+
750
+ - Import your own charts. Add a PMTiles archive by URL or by dropping a file into a themed drop zone;
751
+ Binnacle reads its name, bounds, zoom, and whether it is vector or raster, lists it under Charts
752
+ and Depth as a normal reorderable layer, and stores an uploaded file in the browser for offline use. A
753
+ per-chart detail view renames it, shows its metadata, and deletes it (stating the storage freed).
754
+ Both vector and raster PMTiles render; full S-52 styling of converted ENC depth features is a later
755
+ spec.
756
+
757
+ - Note detail panel: tapping a point of interest now opens a slide-in side panel with native,
758
+ structured detail instead of a plain-text popup that bounced you to an external viewer. Binnacle
759
+ consumes Crow's Nest's presentation-neutral `properties.crowsNest` sections from
760
+ `/resources/notes/{id}`, rendering each item by kind (measures with units, availability badges,
761
+ rating stars, flag toggles, links, and notes), and falls back cleanly to the plain-text
762
+ description for any other notes provider or schema version. The marker icon now uses the
763
+ explicit POI type when present, the structured values render as text with scheme-checked links
764
+ (no HTML injection), and the panel is themed for day, dusk, and night-red and becomes a bottom
765
+ sheet on a narrow screen.
766
+
767
+ - Tracks: Binnacle now records and shows where you have been. The active track is drawn behind the
768
+ boat as you move, colored by speed (dark for slow, bright for fast) or a single solid color, and a
769
+ break in the line marks a GPS dropout or a gap between sessions. The whole voyage is kept in the
770
+ browser (IndexedDB) and reappears after a refresh. A "Tracks" submenu in the menu pauses and
771
+ resumes recording, shows live voyage stats (distance, duration, and average and maximum speed),
772
+ saves the current track to the Signal K server (`/resources/tracks` as GeoJSON), clears it, and
773
+ toggles the color mode. Saved tracks list with show or hide on the chart, delete, and a GeoJSON
774
+ export you can download. Track speeds are stored in SI (m/s) and converted to knots only at the
775
+ display edge; the track layer is a normal layer, so it toggles and fades from the Layers submenu.
776
+
777
+ - Lookout collision thresholds are now editable (differentiator step 6). A "Collision thresholds"
778
+ submenu in the menu sets the danger and warning CPA (nautical miles) and TCPA (minutes); changes
779
+ apply live to the assessment and persist across visits, with a reset to defaults. Values are stored
780
+ in SI and edited at the display edge in nm and minutes.
781
+
782
+ - Lookout publishes its collision alert to Signal K (differentiator step 5). When the assessment
783
+ crosses a threshold, Binnacle writes `notifications.navigation.collision` over the streaming API
784
+ (state alarm for danger, warn for warning, with the appropriate method) so other Signal K clients
785
+ and devices share the alarm; it clears to normal when the risk passes. It is published only when
786
+ the state or worst contact changes, not on every per-second tick.
787
+
788
+ - Lookout now sounds an audible collision alarm (differentiator step 4). When an AIS contact
789
+ crosses the danger CPA/TCPA threshold, a repeating two-beep tone plays, synthesized with the Web
790
+ Audio API so nothing is downloaded. Acknowledging the contact on the danger strip silences it, and
791
+ a new or more severe contact re-arms it; a "Mute alarm" toggle in the menu turns sound off entirely
792
+ and persists. The audio primes on your first interaction with the page (browsers block sound until
793
+ a gesture). Warnings stay visual only.
794
+
795
+ - An app menu in the top bar gives app-wide options a home. It stays a single button until
796
+ opened, then drops a themed popout, and closes on selection, Escape, or a click outside; the
797
+ trigger is a labeled disclosure (aria-haspopup, aria-expanded, aria-controls). The menu is generic:
798
+ it renders whatever action items it is given plus optional collapsible submenus, so adding an
799
+ option is one `MenuItem` (or one `MenuSubmenu`) in the app shell, never a change to the menu
800
+ itself. It hosts a "Center on boat" action that flies the map to the vessel and a "Layers"
801
+ submenu.
802
+
803
+ - Binnacle now remembers your session across a page refresh. The map reopens at the last center and
804
+ zoom, and each layer's visibility and opacity are restored, alongside the theme that was already
805
+ persisted. The view is saved to local storage after panning settles (one write per gesture, not
806
+ per frame), layer changes are saved as they happen, and a corrupt or out-of-range saved view is
807
+ ignored in favor of the default world view.
808
+
809
+ - Points-of-interest markers now use per-category icons and a rich detail popup. Each note is sorted
810
+ into a category (anchorage, marina, fuel, services, inlet, boat ramp, bridge, hazard, navaid,
811
+ structure, or a generic point of interest), matched from the provider's skIcon against the live
812
+ Crow's Nest / ActiveCaptain vocabulary with a keyword fallback for unfamiliar variants, so
813
+ navigation lights and channel buoys read as navaids, creek inlets as inlets, and boat ramps and
814
+ bridges as themselves instead of plain pins. Each category draws as a themed disc with a glyph:
815
+ Lucide glyphs (anchor, sailboat, fuel pump, wrench, waves, landmark, triangle-alert, map-pin) per
816
+ the spec's chosen app icon family, plus custom slipway and bridge marks. Hazards take the alarm hue,
817
+ navaids the caution hue, the rest the POI hue; all recolor with the theme (night-red stays in the
818
+ red band).
819
+ Clicking a marker opens a themed popup with the name, category, any description and source
820
+ attribution, and an http(s)-only link to the provider's detail page, and the selected marker gets a
821
+ highlight ring.
822
+
823
+ - Navaids now render type-specific symbols instead of one generic marker. The note name is parsed
824
+ into a kind (lighthouse, light, buoy, daybeacon, or generic) and, for buoys and daybeacons, a
825
+ lateral side from the aid's number using the US IALA-B convention (even = red, starboard hand;
826
+ odd = green, port hand). Lights draw as a magenta flare, lighthouses as a lantern-topped tower,
827
+ starboard marks as a red cone or triangle, port marks as a green cylinder or square, so a channel
828
+ reads at a glance. The side is carried by shape as well as color, so it survives night-red (where
829
+ red and green collapse to two red shades). This infers symbols from the note text; full S-52
830
+ symbology keyed off S-57 ENC attributes (shape, color, category) remains the later vector-chart
831
+ spec, since notes carry no such attributes.
832
+
833
+ - Points-of-interest markers cluster at lower zoom and split apart as you zoom in, so a busy harbor
834
+ shows a single counted disc instead of a stack of overlapping markers; clicking a cluster zooms to
835
+ expand it. Marker size scales gently with zoom.
836
+
837
+ - Points-of-interest overlay: Binnacle now renders Signal K `notes` resources on the map, so POI
838
+ providers like signalk-crows-nest (Active Captain, OpenSeaMap, NOAA, USCG light list) show up.
839
+ The overlay fetches notes scoped to the current viewport (`?bbox=...`, no `provider` so every
840
+ notes provider merges, which is how Freeboard-SK retrieves them), refetched as the map moves and
841
+ gated below zoom 9. POIs draw as themed dots with names at zoom 12 and up, and toggle from the
842
+ layers panel. Earlier nothing showed because Binnacle had no consumer for `notes` resources; the
843
+ data was being served correctly all along.
844
+
845
+ - Lookout collision chart highlight (differentiator step 3): dangerous AIS contacts now get a graded
846
+ ring on the chart in the safety z-band, danger and warning colored from the theme (day, dusk, and
847
+ night-red, which keeps both in the red band with danger brighter). The overlay is dirty-checked
848
+ against the assessment so it only rebuilds when a contact's id, severity, or position changes, is
849
+ theme-aware through the layer manager's applyTheme broadcast, and toggles from the layers panel.
850
+
851
+ - Lookout danger strip: collision danger now surfaces on screen. A strip floats at the bottom of
852
+ the chart listing the most dangerous AIS contacts with their closest point of approach in nautical
853
+ miles and time to closest approach in minutes, color-graded by severity, with an acknowledge
854
+ control and a "computing locally" note when the values are the client-side fallback rather than a
855
+ Signal K provider. The strip is absent when nothing is dangerous, so a calm night watch stays dark,
856
+ and it updates as traffic moves. This is the first on-screen slice of the active-safety Lookout
857
+ feature; the chart highlight, audible alarm, notifications, and thresholds panel follow.
858
+ - Offline and PWA caching: Binnacle is now an installable progressive web app. A service worker
859
+ precaches the app shell, runtime-caches the OpenFreeMap base map and the Signal K PMTiles charts
860
+ cache-first (range-request aware) as they are viewed, and never caches the live Signal K stream or
861
+ REST API, so anywhere the navigator has looked renders offline while live data stays fresh. The
862
+ top bar offers an update when a new build is published, and the status strip shows an offline
863
+ indicator. Service workers require a secure context, so this activates when the Signal K server is
864
+ served over HTTPS; over plain HTTP the app degrades cleanly to online-only with no errors.
865
+ - The status strip shows the map's center latitude and longitude and the zoom level, updating as
866
+ the chart is panned and zoomed, formatted at the display edge with hemisphere suffixes.
867
+ - Lookout (active-safety, first slice): the headless collision data layer behind the upcoming
868
+ danger strip. A pure, test-first closest-point-of-approach module computes CPA and TCPA from the
869
+ own vessel and a target's position and velocity, a persisted-settings helper holds
870
+ user-configurable danger and warning thresholds with sensible defaults, and a collision
871
+ assessment ranks AIS contacts by severity, preferring the server's `navigation.closestApproach`
872
+ when a provider supplies it and falling back to the computed values otherwise. The danger strip,
873
+ chart highlight, audible alarm, and Signal K notification publishing follow in later slices.
874
+ - Theming: a design-token system with day, dusk, and night-red palettes, switched by a single
875
+ theme controller that sets `data-theme` on the document and persists the choice. Every surface
876
+ recolors from CSS custom properties, a top-bar toggle cycles the themes, and the map base
877
+ recolors via `setPaintProperty` (keeping tiles and overlays). Night-red is pure red on true
878
+ black with no blue, and a dedicated alarm token stays distinguishable in every palette.
879
+ - Identity: self-hosted Inter (UI) and JetBrains Mono (tabular numeric readouts) typography
880
+ bundled for offline use, Lucide icons for the theme toggle and the layers panel, the own-ship
881
+ and AIS symbols recolored per theme so the chart shows no blue on the night-red theme (the own
882
+ ship turns red and AIS a night-safe amber), and the build version shown in the top bar.
883
+ - AIS targets: the worker learns the self vessel from the `hello` handshake and routes other
884
+ vessels' deltas into a per-context AIS stream, the store accumulates each target and prunes
885
+ ones that go silent past a six-minute window, an `AisTargets` entity interprets each target
886
+ into display units, and an AIS overlay renders them as GPU symbols in the traffic band that
887
+ rotate with course and skip rebuilding when nothing changed. The app subscribes `vessels.*` at
888
+ a controlled rate, and CPA and TCPA are read from `navigation.closestApproach` when a provider
889
+ supplies them.
890
+ - Charts: a generic chart-source adapter turns any Signal K chart resource into MapLibre source
891
+ and layer specs, branching on the chart type (raster tilelayer, WMS, WMTS, and S-57, plus
892
+ vector tileJSON with PMTiles resolved to the `pmtiles://` protocol) and honoring bounds and
893
+ zoom limits. Each chart wraps as a basemap-band overlay on the existing layer manager, the
894
+ charts client discovers them from `/resources/charts` (v2, falling back to v1, degrading to an
895
+ empty list offline), and a layers panel gives each chart a visibility toggle and an opacity
896
+ slider.
897
+ - Verify-before-push git hooks (`.githooks/`, installed via `npm run hooks`): a fast format,
898
+ lint, and boundary check before each commit, and the full type-check, test, and build gate
899
+ before each push, so a broken tree cannot be committed or pushed.
900
+
901
+ ### Changed
902
+
903
+ - A final whole-codebase cleanup before the 0.1.0 release, no feature change. The
904
+ panel, button, icon, label, and instrument-strip styling moved into shared `app.css` utilities
905
+ (`.icon-btn` with accent and danger modifiers, `.btn-ghost`, `.btn-pill`, a shared bottom-strip
906
+ metrics row, a `.caps-label` for the uppercase section labels, and a 4px-based `--space-*` spacing
907
+ scale for the common padding, gap, and margin values): the Routes panel now renders the same
908
+ slide-over shell as the Layers and note panels instead of having the app shell hand-roll its dock
909
+ chrome, every panel header reads the shared `.panel-title`, and the row-action icon buttons and
910
+ ghost buttons stop being re-declared per component. The Signal K
911
+ resource clients (routes, charts, tracks, and course) now share one `fetchKeyedResource` plus
912
+ `putResource` and `deleteResource` instead of three copies of the v2-then-v1 fetch and five copies
913
+ of the PUT and DELETE wrapper; the three IndexedDB stores share one `openIdbDatabase` opener and one
914
+ `degradeToMemory` policy; the weather grid blends through the shared `lerp`; user-chart ids and the
915
+ save-name prompt use shared helpers; the store iterates own keys with `Object.entries`; the
916
+ longitude-delta normalize is total over any input; and the unused `routeLegs` was removed.
917
+
918
+ - The Signal K webapp manifest is complete for the App Store and the 0.1.0 release: five screenshots
919
+ (the chart with AIS, route planning, charts and depth, an anchorage point-of-interest detail, and
920
+ the weather mini-map) and a "Works well with" list (Crow's Nest for the points of interest Binnacle
921
+ renders, signalk-ssl for the HTTPS its offline cache needs, and signalk-virtual-weather-sensors as a
922
+ weather provider it reads). A cross-platform webapp CI builds, tests, and packs on Linux, macOS, and
923
+ Windows on Node 22 and 24, and a release publishes to npm with a provenance attestation.
924
+
925
+ - A UI consistency pass covering design tokens, layout, typography,
926
+ accessibility, and marine HMI conventions brought the whole interface to one standard. New tokens defined for all three themes (a large
927
+ radius, a shared hover and press timing, a caution-tier warning color, and an alarm tint) replace
928
+ the values components used to hand-code. Panel titles are consistent headings, the numeric readouts
929
+ share the mono instrument font, the bottom-strip titles match the panel title scale, and the
930
+ night-red border is deepened so panels stay separated where the shadow is dropped. The on-chart
931
+ route editing color now reads the one map-theme source instead of a duplicated table. The four
932
+ slide-over and overlay panels share one dismiss behavior (Escape closes the topmost, and focus
933
+ returns to the control that opened it).
934
+
935
+ - Routing cleanup pass covering geodesy and the route domain, course guidance and the resource
936
+ clients, the overlay, editor, and chart wiring, and the routing UI and app wiring, no feature
937
+ change. The Earth-radius constant and the antimeridian longitude-delta normalize are now
938
+ shared by the rhumb-line geometry and the collision CPA projection instead of duplicated, a
939
+ `steerSide` helper centralizes the port-versus-starboard cross-track convention, a `clientId` helper
940
+ folds the two copies of the secure-context id fallback, and the route distance no longer allocates a
941
+ leg array just to sum it. Stopping an active course now clears every course cell, where before it
942
+ left the previous point, the active route, and the arrival circle stale, and the seeding and
943
+ clearing of those cells moved onto the course entity that owns them. Dead route-editor methods and a
944
+ redundant overlay visibility flag were removed. Tests went from 412 to 415.
945
+
946
+ - The weather mini-map opens centered on the navigation chart's current view, so the forecast is for
947
+ the area you are looking at, rather than reopening at its own last position. The zoom is still capped
948
+ to the mini-map's maximum so weather never zooms past its data resolution.
949
+
950
+ - Second whole-repo cleanup pass covering weather, the Signal K data layer, map and charts, notes
951
+ and tracks, safety and chrome, and app and build infrastructure, no feature change.
952
+ The wind particle field caches its GPU uniform and attribute locations once at setup instead of
953
+ re-querying them every frame, the layer manager applies the stacking order once per batch when the
954
+ chart and overlays first load rather than restacking after each of a dozen-plus registrations, and
955
+ shared helpers fold repeated logic: a `DEG_TO_RAD` constant for hot numeric loops, an `HOUR_MS`
956
+ constant, an `applyRasterTheme` for the night-red raster treatment shared by the chart, depth, and
957
+ radar layers, an `asKeyedObject` guard shared by the chart, note, track, and weather resource
958
+ clients, a `toLonLat` mapper for the track coordinate builders, and one `RAIN_VISIBLE_MM_H`
959
+ threshold. Dead Signal K path constants were removed.
960
+
961
+ - A new app build now surfaces an Update control instead of silently reloading. The progressive web
962
+ app uses prompt registration rather than auto-update, so a fresh build never reloads the chart out
963
+ from under you mid-passage; the Update control lets the navigator choose when to apply it.
964
+
965
+ - Whole-repo cleanup pass, weather-weighted, no behavior change. One shared
966
+ `emptyFeatureCollection` in `$shared/map` replaces the per-overlay copies (vessel, track, ais,
967
+ notes, and weather), a shared `headingDegrees` helper folds the vessel and AIS heading fallback,
968
+ the weather mini-map's viewport cache is now bounded, the wind overlay reports both its candidate
969
+ layer ids so a rare WebGL fallback still restacks, the notes cluster click no longer double-fires,
970
+ the radar opacity is one constant, and dead surface was removed (the unused `cellIndex` export, the
971
+ unused weather `type` field and `weatherCacheKey` export, and a pass-through wrapper).
972
+
973
+ - Points of interest cluster later and say what they hold. Markers now uncluster from zoom 12 (up
974
+ from 14), so the zoom you usually navigate at shows individual POIs instead of group circles, while
975
+ the wider view (zoom 9 to 11) still clusters so it does not turn into a mash of overlapping pins. A
976
+ cluster no longer reads as a generic purple circle: it shows the colored icon of its most important
977
+ member (a red hazard disc if it holds any hazard, the amber navaid disc otherwise the point-of-
978
+ interest disc), inside a ring that marks it as a group, with a count badge. Clicking a cluster still
979
+ zooms it apart.
980
+
981
+ - Tidied the Signal K auth flow internals, with no behavior change. The focus and cross-tab
982
+ storage listeners now live inside `AuthController` (like `OnlineStatus` owns its own listeners)
983
+ instead of the app shell parsing the stored auth JSON itself, a single in-flight guard stops a
984
+ duplicate access-request poll when a tab return fires focus and visibilitychange together, and
985
+ the own-vessel and AIS subscriptions are issued in one call.
986
+
987
+ - Cleanup pass over the depth-charts work. The two IndexedDB stores now
988
+ share one open-and-transaction helper; the unused PMTiles store list and total-size methods were
989
+ dropped; byte-size formatting moved to a shared `formatBytes`; and a few small dead guards, a
990
+ redundant array copy, and duplicated layer-id lists were tidied.
991
+
992
+ - Whole-repo cleanup pass. The collision assessment is memoized with
993
+ `$derived`, so the O(targets) CPA loop runs once per real change instead of several times per
994
+ frame (it was recomputed on every animation frame by the overlay and twice per alarm tick). CPA
995
+ and TCPA display formatting is centralized in `shared/lib` (`formatCpaNm`, `formatTcpaMin`). The
996
+ Signal K socket gates every handler on still being the current socket, so a superseded socket
997
+ cannot inject a delta or schedule a second reconnect. POI classification is case-insensitive, the
998
+ notes overlay skips per-frame work when the map is idle, the layers view updates one item in place
999
+ instead of rebuilding the list on every slider tick, the menu submenu is tied to its content with
1000
+ `aria-controls`, and the empty-spec chart overlay no longer installs a dangling listener. Renamed
1001
+ `radiansToDegrees` to `radiansToBearing` (it normalizes to 0..360), exported `SELF_CONTEXT`, and
1002
+ added `nauticalMilesToMeters`. No behavior change beyond the perf and robustness fixes.
1003
+
1004
+ - Second whole-repo cleanup pass. The vessel, AIS, and collision
1005
+ entities now expose speed and course in SI (m/s and radians); knots and compass-bearing
1006
+ conversion moved to the display edge (the status strip, the vessel and AIS overlays, and a shared
1007
+ `formatKnots`), removing a knots-to-m/s and degrees-to-radians round-trip in the collision math.
1008
+ The worker hands the AIS batch to the main thread as a nested `Map` across the Comlink boundary,
1009
+ dropping a per-frame object rebuild on each side. The collision overlay dirty-checks the
1010
+ assessment by reference instead of building a per-frame signature string. Gap-splitting for track
1011
+ simplification and export is one shared `splitAtGaps` helper, the theme and settings localStorage
1012
+ writes are guarded against quota and private-mode failures, the menu-icon, connection-phase, and
1013
+ theme label maps are typed to their unions, and dead code (an unused `metersToNauticalMiles`
1014
+ export, a `LatLon` re-export, the entry-module default export, and a redundant callback wrapper)
1015
+ was removed.
1016
+
1017
+ - The layers controls (per-layer visibility and opacity) moved off the chart into the app menu.
1018
+ They were a panel floating over the top-left of the map; they now live in a collapsible "Layers"
1019
+ submenu inside the menu, so the chart is unobstructed and the controls share one place with other
1020
+ app options. Each layer's visibility and opacity still persist across refreshes.
1021
+
1022
+ - The own-vessel marker is now a boat hull instead of a flat triangle. It is drawn with the 2D
1023
+ canvas (filled hull, darker outline, sharp bow, flat transom) at 2x for retina crispness, rotates
1024
+ to `headingTrue` (falling back to `courseOverGroundTrue`), and recolors with the theme (blue by
1025
+ day, red at night). The pointed bow makes the heading unambiguous. The symbol-overlay factory
1026
+ gained an optional `pixelRatio` so an icon can be drawn at 2x.
1027
+
1028
+ - A tiled chart now hands off to the base map when you zoom past its native detail. Each chart's
1029
+ draw layers are capped one zoom level beyond the source's native maximum (read from the loaded
1030
+ tile metadata, so it is archive-agnostic), so zooming in past the chart's scale reveals the sharp
1031
+ base map instead of a blocky overzoomed chart. The chart stays authoritative within its own zoom
1032
+ range and aligned with the base beyond it.
1033
+
1034
+ - The collision danger strip's Acknowledge control now works: acknowledging suppresses the current
1035
+ worst contact, and a new or more severe contact automatically re-arms the alert. The full
1036
+ mute/alarm lifecycle remains a later Lookout step.
1037
+
1038
+ - Whole-repo cleanup pass: a chart now themes its own draw layers through an `applyTheme` broadcast
1039
+ from the layer manager, so the widget no longer reaches into chart layers by id (the source-layer
1040
+ to color mapping lives in one place); the vessel and AIS overlays are built from a shared
1041
+ `createSymbolOverlay` factory instead of duplicated scaffolding; the vector draw order and per
1042
+ source-layer styling are a single ordered list; a `mapstyleJSON` chart is a clean no-op pending
1043
+ the style pipeline rather than a broken source; AIS target views are memoized by version so
1044
+ own-vessel motion no longer rebuilds the list; the AIS staleness scan is throttled off the
1045
+ per-frame path; the subscription registry gained a refcounted `remove` and the worker routes
1046
+ unsubscribe through it so a dropped path is not resurrected on reconnect; `PersistedValue` reports
1047
+ `fromStorage` by key presence rather than a value compare; coordinate formatting and the
1048
+ AIS-target field extractors were de-duplicated; the connecting state is a shared
1049
+ `INITIAL_CONNECTION_STATE`; dead code was removed (`PathCell.receivedAt`, the unused `worst`
1050
+ getter, the `kelvinToCelsius` and `metersToFeet` helpers, identity arithmetic in the icons, and
1051
+ several unreachable null-guards); and the danger strip shows a "+N more" cue instead of silently
1052
+ truncating the contact list.
1053
+
1054
+ - Whole-repo cleanup pass: the chart source and layer ids derive from a single `chartSourceId`
1055
+ helper, the own-vessel overlay skips its per-frame `setData` when position and heading are
1056
+ unchanged, the vessel icon is built once and cached, the connection clears its reconnect timer
1057
+ on disconnect, malformed delta frames and chart fetch errors now warn instead of failing
1058
+ silently, MapLibre source and layer specs are properly typed (no `as never` casts), the layers
1059
+ panel hides the opacity slider for layers that do not support it, the unit converters accept
1060
+ `null`, the connection wakes a single shared own-vessel instead of two, and the shared test
1061
+ fakes (`FakeWebSocket`, `createFakeMap`) live in `src/shared/testing/` rather than being
1062
+ redefined per test. The dependency-cruiser ruleset now covers every Feature-Sliced Design
1063
+ layer direction, including the cross-feature public-API boundary.
1064
+
1065
+ - The map: a MapLibre GL map with a vector base, rendered in the chart area. A framework-free
1066
+ `LayerManager` gives every overlay an independent toggle, opacity, and deterministic z-order
1067
+ via sentinel layers and `beforeId`, so a new overlay later is a new file plus one
1068
+ registration. The own vessel renders as a GPU symbol layer that rotates with heading (falling
1069
+ back to course over ground), updated from the Signal K store each animation frame. Includes
1070
+ the PMTiles protocol registration for future offline tiles.
1071
+
1072
+ - Real-time data layer: a Web Worker hosts the Signal K WebSocket client, bridged to the main
1073
+ thread with Comlink, delivering one batched frame per animation frame. A path-keyed runes
1074
+ store of independently reactive cells lets a component bound to one Signal K path avoid
1075
+ re-running when an unrelated path changes. Includes delta reconciliation, a per-frame
1076
+ last-write-wins batcher, a refcounted subscription registry, full-jitter reconnection with
1077
+ resubscribe on open, and an own-vessel entity that converts SI values to display units at the
1078
+ edge. The shell shows live connection state and own-vessel SOG and COG.
1079
+
1080
+ - Project floor (Phase 1 of the foundation): a Svelte 5, Vite, and TypeScript application
1081
+ that builds as a Signal K webapp, serving static files from `public/` at `/binnacle/`.
1082
+ - Feature-Sliced Design layout (`app`, `views`, `widgets`, `features`, `entities`, and
1083
+ `shared`) with module boundaries enforced by dependency-cruiser.
1084
+ - SI unit-conversion module in `shared`, built test-first, covering meters per second to
1085
+ knots, radians to a normalized degree bearing, Kelvin to Celsius, meters to feet, and
1086
+ meters to nautical miles.
1087
+ - Verification toolchain: Biome for lint and format, svelte-check for type-checking, Vitest
1088
+ for unit tests, Playwright for an end-to-end smoke test, and a CI workflow running the
1089
+ full gate on Node 24.
1090
+ - Foundation design spec and Phase 1 implementation plan under `docs/superpowers`.
1091
+ - README, an Apache-2.0 LICENSE, and a Buy Me a Coffee funding link (README badge,
1092
+ `.github/FUNDING.yml`, and the `package.json` funding field).
1093
+
1094
+ ### Fixed
1095
+
1096
+ - The active-route Stop button and the collision-alarm Acknowledge button on the bottom strips were
1097
+ inert. When both strips moved to the shared `.bottom-strip` class, the app shell's `pointer-events`
1098
+ override still targeted their old `.nav-strip` and `.danger-strip` selectors, so each slot's
1099
+ `pointer-events: none` reached the button. The override now targets `.bottom-strip`, restoring both
1100
+ safety-critical actions.
1101
+
1102
+ - Night-red contract violations are corrected: the AIS target was orange (it is
1103
+ now in the red band, with a test guarding it), and the rain-radar legend showed literal blue and
1104
+ green chips at night (now a red intensity ramp). The collision warning severity and an empty track's
1105
+ stats are no longer misleading (a distinct warning color in the danger strip and the thresholds, and
1106
+ a placeholder instead of a zero), and the weather panel no longer renders with square corners
1107
+ because a referenced radius token was undefined.
1108
+
1109
+ - Accessibility fixes: the slide-over panels close on Escape and restore focus, the
1110
+ layer visibility checkboxes and the threshold inputs have explicit names, the note-detail and
1111
+ weather conditions async states announce as status or alert, the add-chart URL field is labeled, the
1112
+ chart rename commits on Enter, the layer-reorder handle advertises its arrow-key shortcut, and the
1113
+ menu popout uses dvh so a long menu stays reachable on a phone.
1114
+
1115
+ - Saving a route now works. The Signal K server validates the standard route resource, and two
1116
+ things failed that validation silently: an unnamed waypoint wrote an empty metadata entry the
1117
+ schema rejects (every entry must carry a name), and over plain HTTP the route id fell back to a
1118
+ non-UUID string that the resources API refuses for standard types. Routes now omit per-waypoint
1119
+ metadata when no waypoint is named (and name the gaps by position otherwise), and route and track
1120
+ ids are always real v4 UUIDs, generated from `crypto.getRandomValues` where `crypto.randomUUID` is
1121
+ unavailable. Verified end to end against the server: a drawn, unnamed route now saves, lists, and
1122
+ survives a reload.
1123
+
1124
+ - The on-chart route editing line is easier to see and the Routes controls read as actions. The
1125
+ editing line was a blue that blended into the water; it now uses the bright selection accent (amber
1126
+ by day, a light red at night) and a heavier stroke, and the New route and Save buttons are filled
1127
+ with the accent instead of flat gray. Each saved route now sits in its own elevated card: the name
1128
+ is a title on its own line, a mono distance and waypoint-count readout sits beneath it in the same
1129
+ instrument style as the navigation strip, the actions form a clean cluster with the delete pushed to
1130
+ the trailing edge, and the active route is marked with an accent edge bar, an accent tint, and an
1131
+ "Active" pill so the live route is obvious at a glance in every theme.
1132
+
1133
+ - In the menu, Routes now sits above Layers and charts.
1134
+
1135
+ - A vector chart that declares a coverage extent now honors it. The raster chart paths already passed
1136
+ the declared `bounds` to MapLibre, but the vector path dropped it, so a regional vector chart
1137
+ requested and 404'd tiles across the whole world instead of only within its coverage. The vector
1138
+ source now carries `bounds` the same way the raster sources do.
1139
+
1140
+ - A provider-supplied collision contact is now gated the same as a locally computed one at the exact
1141
+ instant of closest approach. The provider branch treated a TCPA of zero (closest approach right now)
1142
+ as a live danger while the computed branch treated the same geometry as no longer closing; both now
1143
+ require a positive TCPA, so the two CPA sources agree and a just-passed contact cannot flicker as a
1144
+ danger from one source but not the other.
1145
+
1146
+ - Weather values now read consistently to one decimal. The legend low and high labels and the wave
1147
+ period readout previously mixed bare integers ("0", "9") with decimals ("0.0", "0.5"); wind, waves,
1148
+ precipitation, and cloud now all show one decimal place ("X.X"). Bearing, pressure, and temperature
1149
+ stay whole numbers, as those units are conventionally integers.
1150
+
1151
+ - Weather caching now fits the data. Forecasts are cached for an hour rather than 30 minutes (Open-Meteo
1152
+ model runs are hours apart, and the time slider already shows the right hour from the cached 5-day
1153
+ window), roughly halving request volume. When only the marine (waves) endpoint fails, commonly an
1154
+ Open-Meteo 429 on its separate host, the forecast grid (wind and pressure) is still shown but is not
1155
+ cached and the loader backs off, so panning no longer re-hits the rate-limited endpoint on every move.
1156
+
1157
+ - The weather mini-map no longer freezes blank when Rain radar is on. The RainViewer raster source
1158
+ starts with no frame URL (real frames arrive later), and MapLibre loads tiles for a layer in the
1159
+ style regardless of its visibility, so an empty tiles array crashed its tile-URL builder and then
1160
+ the raster render program, freezing the panel. The source now seeds a transparent placeholder tile
1161
+ so tile loading always succeeds, and the layer stays hidden until a real frame is applied so it is
1162
+ never drawn empty. Real frames replace the placeholder as before.
1163
+
1164
+ - Weather is gentler on the free data sources and correct over fresh water. The atmospheric forecast
1165
+ no longer forces Open-Meteo's sea cell selection, which picked wrong or missing cells over inland
1166
+ and freshwater areas such as the Great Lakes; sea selection now applies only to the marine wave
1167
+ request. A failed grid fetch backs off for a minute instead of retrying on every pan, the per-load
1168
+ grid is sampled to fewer points so a load fits a single request, and the viewport cache is capped.
1169
+ The "Here" conditions panel keys its lookups on a position rounded to about 110 meters, so GPS
1170
+ jitter no longer refetches (and no longer spams a Signal K weather provider with point requests) on
1171
+ every fix.
1172
+
1173
+ - The Forecast button is no longer clipped at the bottom of the status strip, and wind readouts show
1174
+ one decimal place (matching waves), even at zero.
1175
+
1176
+ - Signal K access approval now connects on its own. Previously, after you approved Binnacle in the
1177
+ Signal K UI and returned to the tab, it kept polling a stale request and only a second tab would
1178
+ connect. Binnacle now rechecks the pending request the moment the tab regains focus (background
1179
+ tabs throttle the poll timer), re-requests a fresh one if the old request expired, connects the
1180
+ stream reactively the instant access is granted (no reload), and adopts a token approved in another
1181
+ tab via the storage event. The old one-shot blocking connect that required a reload or a second tab
1182
+ is gone.
1183
+
1184
+ - A raster chart layer now follows the night-red theme (desaturated and dimmed) instead of staying
1185
+ full-saturation and full-brightness, matching the streaming depth layers.
1186
+
1187
+ - The track and PMTiles stores no longer lose records when IndexedDB degrades mid-session: every
1188
+ write is mirrored to the in-memory fallback, so data written before the failure survives.
1189
+
1190
+ - Switching the theme from night-red or dusk back to day no longer leaves the base map broken. The
1191
+ day restore brings the water and land fills back to their real colors and clears the dark label
1192
+ halo from street names. `fill-pattern` is a paint property in MapLibre, not a layout one, so the
1193
+ day-restore snapshot had silently dropped every fill layer, and the label halo was reset only when
1194
+ the source style already defined one.
1195
+
1196
+ - In the day theme, an unchecked layer checkbox no longer renders as a solid black square. The day
1197
+ theme declared a dark `color-scheme` despite being a light theme, so native controls rendered in
1198
+ dark mode; day now uses a light `color-scheme`, and checkboxes follow the theme accent (no blue at
1199
+ night).
1200
+
1201
+ - The base map now recolors fully for the theme. Previously only the background and water were
1202
+ themed, so over land at higher zoom the OpenFreeMap roads stayed white and the parks and landcover
1203
+ green even in night-red, breaking the pure-red-on-black contract. Every base layer is now recolored
1204
+ from its source layer (water, landcover, landuse, transportation, building, boundary, and text
1205
+ labels) per theme, fill patterns are cleared so the flat themed color shows, and label text gets a
1206
+ background-colored halo. Day and dusk gain a calmer palette consistent with the app; night-red is
1207
+ red-on-black across the whole map.
1208
+
1209
+ - A vector (PMTiles) chart could drop tiles to blank gaps at low-to-mid zoom (around z9) on some
1210
+ GPUs, even though the tiles fetched fine (HTTP 206) and decoded correctly. The cause was render
1211
+ load: the archive ships `landuse` un-simplified from low zoom, so a single z9 tile can carry
1212
+ roughly 1700 polygons that are invisible at that scale but heavy to draw over the full base map,
1213
+ which a weaker or high-DPI GPU silently fails to render. The chart now holds `landuse` until z12,
1214
+ where it is actually legible, cutting the low-zoom chart draw load sharply. Each chart layer's
1215
+ minzoom is preserved through the max-zoom cap.
1216
+
1217
+ - A vector (PMTiles) chart could show blank gaps over a real network. The archive is read
1218
+ uncached (`cache: 'no-store'`, to dodge a Chrome disk-cache write failure), so each chart tile
1219
+ depends on a live HTTP range read; a transient drop or a server hiccup under a burst of reads (a
1220
+ zoom that pulls in new tiles) blanked that tile until a later zoom re-requested it. The PMTiles
1221
+ source now retries a failed or 5xx range read (short backoff, up to two tries) while still
1222
+ honoring a caller abort, so a transient failure no longer leaves a hole.
1223
+
1224
+ - AIS targets disappeared from the chart after about six minutes of page uptime. The worker stamped
1225
+ each frame's epoch with 0 (`requestAnimationFrame` is absent in the worker, so the batcher's
1226
+ fallback passed 0) while the overlay pruned staleness against `performance.now()`, so pruning
1227
+ tracked uptime, not real staleness. The worker now stamps a wall clock (`Date.now`) and the
1228
+ overlay prunes with the same clock.
1229
+ - The AIS change counter bumped on every worker frame, not only on AIS changes, because the worker
1230
+ always emits an `ais` object (empty when only the own vessel moved). It now bumps only when a
1231
+ context actually updates, so the AIS overlay and the collision assessment no longer rebuild every
1232
+ frame.
1233
+ - The stored-token auth probe now omits credentials, so a live session cookie cannot mask a stale
1234
+ token and leave the WebSocket streaming nothing.
1235
+
1236
+ - Vector charts (MVT/PMTiles) rendered nothing on the map. A vector tile source paints nothing on
1237
+ its own: MapLibre needs a draw layer per source-layer, and the chart adapter both routed these
1238
+ charts to a raster source and, on the vector path, emitted no draw layers. The adapter now routes
1239
+ any chart marked `mvt`/`pbf`, typed `tileJSON`/`mapstyleJSON`, or ending in `.pmtiles` to a vector
1240
+ source and generates themed fill and line draw layers per source-layer. It covers the two dominant
1241
+ vector base-map schemas (Protomaps and OpenMapTiles) and, because Signal K's charts API often
1242
+ returns an empty layer list for an archive, falls back to the full known set when none are
1243
+ declared; MapLibre silently ignores a draw layer whose source-layer is absent. The chart layers
1244
+ recolor with the day, dusk, and night-red themes, and per-layer opacity now uses the correct paint
1245
+ property for fill, line, and raster layers.
1246
+
1247
+ - PMTiles vector charts failed to render with `ERR_CACHE_WRITE_FAILURE`: a large archive served
1248
+ with a weak ETag over range requests makes Chrome fail the HTTP disk-cache write, which rejects
1249
+ the whole fetch and blanks the chart. Binnacle now registers each PMTiles archive with a source
1250
+ that fetches ranges with `cache: 'no-store'`, bypassing the browser cache for these reads. Durable
1251
+ offline caching of these archives is a later spec.
1252
+
1253
+ - Collision assessment no longer raises a false alarm on an opening or already-passed AIS target:
1254
+ the provider closest-approach path now drops a contact whose time to closest approach is negative
1255
+ (the closest approach is in the past), matching the computed path's behavior.
1256
+ - Closest-point-of-approach math now normalizes the longitude difference, so a vessel pair
1257
+ straddling the antimeridian computes a real short range instead of a bogus near-360-degree offset.
1258
+ - The AIS change counter is now reactive state, so a future reactive consumer is notified rather
1259
+ than only the per-frame poll, removing a latent reactivity trap.
1260
+
1261
+ - Own-vessel readouts (SOG and COG) stayed blank while live data flowed. The store creates a
1262
+ path cell lazily on first access; the first access was the shell's reactive readout, so a
1263
+ brand-new `$state` source was created during the effect's tracking pass and never subscribed,
1264
+ and later updates did not re-render. `OwnVessel` now creates its cells at construction, before
1265
+ the readout runs, so the reactive read tracks an existing cell.
1266
+ - The auth probe could mistake a secured server for an unsecured one. It checked a REST path that
1267
+ a browser session cookie satisfies, so it concluded "unsecured" and connected the WebSocket
1268
+ stream without a token, which the cookie does not authenticate, leaving no live data. The probe
1269
+ now checks a stored token first and runs the anonymous check with credentials omitted, so a
1270
+ cookie cannot mask the need for a token.
1271
+ - The Signal K worker crashed at load with "Class extends value undefined" because the worker
1272
+ graph imported the server-side `@signalk/server-api` package, whose entry re-exports a
1273
+ `FullSignalK` class extending Node's `EventEmitter`; bundled into the browser worker with
1274
+ `events` externalized, that base class resolved to `undefined`. The worker now mirrors the few
1275
+ Signal K wire types it needs locally and no longer imports the package, dropping the worker
1276
+ bundle from about 164 KB to about 7 KB and removing the dependency entirely.
1277
+ - The chart area rendered all blue offshore because the base map was fetched from a CDN
1278
+ (`tiles.openfreemap.org`), which is unreachable on a boat with no internet, leaving an empty
1279
+ map that showed the page background through it. Binnacle now ships a bundled, offline base
1280
+ style that the theme recolors, with Signal K charts layered on top. Bundled vector base tiles
1281
+ are a later spec; this removes the CDN dependency in line with the offline-first rule.
1282
+
1283
+ ### Security
1284
+
1285
+ - The points-of-interest popup's "View details" link now follows only `http:` and `https:` URLs.
1286
+ A note's link comes from a resource provider Binnacle does not control, so a `javascript:` or
1287
+ `data:` URL would otherwise execute when clicked; non-http schemes and unparseable URLs are now
1288
+ dropped.