signalk-binnacle 0.5.0 → 0.6.1

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