signalk-siparu 0.2.11 → 0.2.13

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/LICENSE CHANGED
@@ -187,7 +187,7 @@
187
187
  same "printed page" as the copyright notice for easier
188
188
  identification within third-party archives.
189
189
 
190
- Copyright [yyyy] [name of copyright owner]
190
+ Copyright 2026 Siparu Ltd
191
191
 
192
192
  Licensed under the Apache License, Version 2.0 (the "License");
193
193
  you may not use this file except in compliance with the License.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-siparu",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Kept aboard, proven ashore. Position, wind, depth and voyage history, recorded on the boat and readable from anywhere: sealed to the devices you name, signed on the way out. A read-only Signal K plugin with a built-in dashboard.",
5
5
  "keywords": [
6
6
  "signalk-node-server-plugin",
@@ -93,6 +93,17 @@ function safeChartPath(dataDir, rel) {
93
93
  return null;
94
94
  if (!SERVABLE_EXT.has(path.extname(abs).toLowerCase()))
95
95
  return null;
96
+ let real;
97
+ let realDir;
98
+ try {
99
+ real = fs.realpathSync(abs);
100
+ realDir = fs.realpathSync(dir);
101
+ }
102
+ catch {
103
+ return abs;
104
+ }
105
+ if (real !== realDir && !real.startsWith(realDir + path.sep))
106
+ return null;
96
107
  return abs;
97
108
  }
98
109
  function chartContentType(absPath) {
@@ -244,6 +244,11 @@ export interface HealthResult {
244
244
  cap_bytes: number;
245
245
  raw_files: number;
246
246
  oldest_raw: string | null;
247
+ writes?: {
248
+ ok: boolean;
249
+ failures: number;
250
+ last_error: string | null;
251
+ };
247
252
  };
248
253
  rollup: {
249
254
  last_hour: string | null;
@@ -48,6 +48,8 @@ const remotelink_1 = require("./remotelink");
48
48
  const rest_1 = require("./rest");
49
49
  const rollup_1 = require("./rollup");
50
50
  const store_1 = require("./store");
51
+ const recording_1 = require("./recording");
52
+ const keygate_1 = require("./keygate");
51
53
  const time_1 = require("./time");
52
54
  const live_1 = require("./live");
53
55
  const track_1 = require("./track");
@@ -155,26 +157,31 @@ module.exports = (app) => {
155
157
  const snap = state.snapshot(now, true, config_1.INTERNAL.fabricationHorizonMs);
156
158
  const numeric = state.numericDynamicPaths(now, config_1.INTERNAL.fabricationHorizonMs);
157
159
  const stored = Object.keys(numeric).length > 0 ? { ...snap, path_values: numeric } : snap;
158
- await store.append(stored);
160
+ const written = await store.append(stored);
159
161
  if (voyages)
160
- await voyages.feed(stored);
162
+ await voyages.feed(stored).catch((err) => app.debug(`voyage feed failed: ${err}`));
161
163
  if (phases)
162
- await phases.feed(stored);
163
- lastSnapshotTs = now;
164
- const today = (0, time_1.dayKey)(now);
165
- if (today !== countedDay) {
166
- countedDay = today;
167
- snapshotsToday = 0;
164
+ await phases.feed(stored).catch((err) => app.debug(`phase feed failed: ${err}`));
165
+ if (written) {
166
+ lastSnapshotTs = now;
167
+ const today = (0, time_1.dayKey)(now);
168
+ if (today !== countedDay) {
169
+ countedDay = today;
170
+ snapshotsToday = 0;
171
+ }
172
+ snapshotsToday++;
168
173
  }
169
- snapshotsToday++;
170
174
  const age = state.lastDeltaTs === null ? null : Math.round((now - state.lastDeltaTs) / 1000);
171
175
  const held = sealingState();
172
176
  const silent = held.mode === 'blocked' ? ` - NOT reporting ashore: ${held.reason}` : '';
173
- app.setPluginStatus(`Recording - ${snapshotsToday} rows today${age === null ? ', no data yet' : age > 60 ? `, data age ${age}s` : ''}${silent}`);
177
+ app.setPluginStatus((0, recording_1.statusLine)(store.writes(), snapshotsToday, age) + silent);
174
178
  }
175
179
  function sealingState() {
176
180
  if (!sealer || !ensureRemoteLink().getRemote())
177
181
  return { mode: 'none', reason: null };
182
+ const gate = keysStand();
183
+ if (gate)
184
+ return gate;
178
185
  const s = sealer.state();
179
186
  const view = screensNow?.();
180
187
  if (s.mode === 'blocked' && view?.pinned && view.accepted.length === 0 && view.skipped.length > 0) {
@@ -185,6 +192,12 @@ module.exports = (app) => {
185
192
  }
186
193
  return s;
187
194
  }
195
+ function keysStand() {
196
+ if (!keySync || !boatKeyStore)
197
+ return null;
198
+ const status = keySync.status();
199
+ return (0, keygate_1.keyGate)({ state: status.state, lastError: status.lastError, refused: boatKeyStore.refused() });
200
+ }
188
201
  function fuelPathsView() {
189
202
  return {
190
203
  available: state
@@ -198,7 +211,8 @@ module.exports = (app) => {
198
211
  throw new Error('not started');
199
212
  const now = Date.now();
200
213
  const usage = await store.rawUsage();
201
- const degraded = state.lastDeltaTs === null || now - state.lastDeltaTs > config_1.INTERNAL.degradedAfterMs;
214
+ const writes = store.writes();
215
+ const degraded = state.lastDeltaTs === null || now - state.lastDeltaTs > config_1.INTERNAL.degradedAfterMs || !writes.ok;
202
216
  return {
203
217
  status: degraded ? 'degraded' : 'ok',
204
218
  now,
@@ -216,7 +230,8 @@ module.exports = (app) => {
216
230
  raw_bytes: usage.bytes,
217
231
  cap_bytes: opts.maxStorageMB * 1024 * 1024,
218
232
  raw_files: usage.files,
219
- oldest_raw: usage.oldest
233
+ oldest_raw: usage.oldest,
234
+ writes
220
235
  },
221
236
  rollup: await rollups.status(now),
222
237
  ais: aisMemory?.status() ?? { receiver_seen: false, first_seen_ts: null },
@@ -293,7 +308,7 @@ module.exports = (app) => {
293
308
  const pl = new phaselog_1.PhaseLog(st, opts, (msg) => app.debug(msg));
294
309
  phases = pl;
295
310
  st.onHourClosed = async () => {
296
- await ru.catchUp(Date.now());
311
+ await ru.catchUp(Date.now()).catch((err) => app.debug(`rollup catch-up failed: ${err}`));
297
312
  await st.enforceCap();
298
313
  };
299
314
  try {
@@ -304,7 +319,7 @@ module.exports = (app) => {
304
319
  app.setPluginStatus('Starting');
305
320
  st.init(now)
306
321
  .then(async () => {
307
- await ru.catchUp(Date.now());
322
+ await ru.catchUp(Date.now()).catch((err) => app.error(`rollup catch-up failed at start: ${err}`));
308
323
  await st.enforceCap();
309
324
  await vl.init(Date.now());
310
325
  await pl.init(Date.now());
@@ -418,7 +433,7 @@ module.exports = (app) => {
418
433
  onFuelQuery: async () => fuelPathsView(),
419
434
  onAisQuery: async (maxNm, limit) => (0, ais_1.buildAisFeed)(app.getPath('vessels'), app.selfContext, Date.now(), (0, ais_1.clampAisQuery)(maxNm, undefined, limit)),
420
435
  onHealthQuery: () => health(),
421
- seal: (frame) => seal.seal(frame),
436
+ seal: (frame) => keysStand() ?? seal.seal(frame),
422
437
  onWatching: () => ks.prompt(),
423
438
  openAsk: (envelope) => seal.open(envelope),
424
439
  sealAnswer: (payload, id) => seal.answer(payload, id),
@@ -0,0 +1,10 @@
1
+ export interface KeyGateInput {
2
+ state: string;
3
+ lastError: string | null;
4
+ refused: boolean;
5
+ }
6
+ export interface KeyGateVerdict {
7
+ mode: 'blocked';
8
+ reason: string;
9
+ }
10
+ export declare function keyGate(input: KeyGateInput): KeyGateVerdict | null;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.keyGate = keyGate;
4
+ function keyGate(input) {
5
+ if (input.state === 'mismatch') {
6
+ return {
7
+ mode: 'blocked',
8
+ reason: input.lastError ?? 'Siparu already holds different keys for this boat. Unlink her and pair again.'
9
+ };
10
+ }
11
+ if (input.refused) {
12
+ return {
13
+ mode: 'blocked',
14
+ reason: input.lastError ??
15
+ 'The key file on this boat cannot be read. Restore keys.json from a backup of her data directory, or unlink her and pair again.'
16
+ };
17
+ }
18
+ return null;
19
+ }
@@ -10,12 +10,14 @@ export interface BoatPublicKeys {
10
10
  export declare class BoatKeyStore {
11
11
  private readonly file;
12
12
  private keys;
13
+ private unreadable;
13
14
  private writeChain;
14
15
  constructor(dataDir: string);
15
16
  load(): void;
17
+ refused(): boolean;
16
18
  get(): BoatKeys | undefined;
17
19
  publicKeys(): BoatPublicKeys | undefined;
18
- ensure(): Promise<BoatKeys>;
20
+ ensure(): Promise<BoatKeys | undefined>;
19
21
  flush(): Promise<void>;
20
22
  private persist;
21
23
  }
@@ -48,22 +48,32 @@ function isPair(value) {
48
48
  class BoatKeyStore {
49
49
  file;
50
50
  keys;
51
+ unreadable = false;
51
52
  writeChain = Promise.resolve();
52
53
  constructor(dataDir) {
53
54
  this.file = path.join(dataDir, 'keys.json');
54
55
  }
55
56
  load() {
57
+ this.keys = undefined;
58
+ this.unreadable = false;
59
+ let text;
60
+ try {
61
+ text = fs.readFileSync(this.file, 'utf8');
62
+ }
63
+ catch (e) {
64
+ this.unreadable = e?.code !== 'ENOENT';
65
+ return;
66
+ }
67
+ this.unreadable = true;
56
68
  let parsed;
57
69
  try {
58
- parsed = JSON.parse(fs.readFileSync(this.file, 'utf8'));
70
+ parsed = JSON.parse(text);
59
71
  }
60
72
  catch {
61
- this.keys = undefined;
62
73
  return;
63
74
  }
64
75
  const raw = (parsed ?? {});
65
76
  if (raw.v !== FILE_VERSION || !isPair(raw.identity) || !isPair(raw.inbox)) {
66
- this.keys = undefined;
67
77
  return;
68
78
  }
69
79
  try {
@@ -72,11 +82,15 @@ class BoatKeyStore {
72
82
  assertHalvesAgree(identity, Buffer.from(raw.identity.pub, 'base64url'));
73
83
  assertHalvesAgree(inbox, Buffer.from(raw.inbox.pub, 'base64url'));
74
84
  this.keys = { identity, inbox };
85
+ this.unreadable = false;
75
86
  }
76
87
  catch {
77
88
  this.keys = undefined;
78
89
  }
79
90
  }
91
+ refused() {
92
+ return this.unreadable;
93
+ }
80
94
  get() {
81
95
  return this.keys;
82
96
  }
@@ -91,6 +105,8 @@ class BoatKeyStore {
91
105
  async ensure() {
92
106
  if (this.keys)
93
107
  return this.keys;
108
+ if (this.unreadable)
109
+ return undefined;
94
110
  const identity = (0, crypto_1.generateKeyPairSync)('ed25519');
95
111
  const inbox = (0, crypto_1.generateKeyPairSync)('x25519');
96
112
  this.keys = { identity: identity.privateKey, inbox: inbox.privateKey };
@@ -129,8 +129,10 @@ class KeySync {
129
129
  if (!pub) {
130
130
  this.failures++;
131
131
  this.state = 'failing';
132
- this.lastError = 'Could not create the keys for this boat on disk.';
133
- this.deps.debug('keysync: no keys to publish');
132
+ this.lastError = this.deps.keys.refused()
133
+ ? 'The key file on this boat cannot be read. Restore keys.json from a backup of her data directory, or unlink her and pair again.'
134
+ : 'Could not create the keys for this boat on disk.';
135
+ this.deps.debug(`keysync: ${this.lastError}`);
134
136
  return;
135
137
  }
136
138
  const controller = new AbortController();
@@ -89,6 +89,8 @@ export declare class LiveUplink {
89
89
  private openSealedAsk;
90
90
  private sendFrame;
91
91
  private scheduleFrame;
92
+ private wayStreak;
93
+ private noteWay;
92
94
  private nextFrameDelayMs;
93
95
  private handleHistory;
94
96
  private handleSnapshots;
@@ -7,6 +7,9 @@ exports.FRAME_EVERY_MS = 2_000;
7
7
  exports.STILL_FRAME_EVERY_MS = 60_000;
8
8
  exports.REPORTING_STALE_MS = exports.STILL_FRAME_EVERY_MS * 3;
9
9
  const UNDERWAY_SOG_MS = 0.3 * 0.514444;
10
+ const CLEARLY_UNDERWAY_SOG_MS = 1.5 * 0.514444;
11
+ const UNDERWAY_STREAK = 3;
12
+ const STILL_NAV_STATES = new Set(['anchored', 'moored', 'aground']);
10
13
  exports.PING_EVERY_MS = 25_000;
11
14
  const RECONNECT_BASE_MS = 5_000;
12
15
  const RECONNECT_CEILING_MS = 15 * 60_000;
@@ -211,6 +214,7 @@ class LiveUplink {
211
214
  const frame = this.deps.frame();
212
215
  const sog = frame.sog;
213
216
  this.lastSog = typeof sog === 'number' && Number.isFinite(sog) ? sog : null;
217
+ this.noteWay(this.lastSog, frame.nav_state);
214
218
  const verdict = this.deps.seal(frame);
215
219
  if (verdict.mode === 'blocked') {
216
220
  return;
@@ -234,12 +238,26 @@ class LiveUplink {
234
238
  this.scheduleFrame(gen);
235
239
  }, this.nextFrameDelayMs());
236
240
  }
241
+ wayStreak = 0;
242
+ noteWay(sog, navState) {
243
+ const heldStill = typeof navState === 'string' && STILL_NAV_STATES.has(navState);
244
+ if (sog === null || sog < UNDERWAY_SOG_MS) {
245
+ this.wayStreak = 0;
246
+ }
247
+ else if (sog >= CLEARLY_UNDERWAY_SOG_MS) {
248
+ this.wayStreak = UNDERWAY_STREAK;
249
+ }
250
+ else if (heldStill) {
251
+ this.wayStreak = 0;
252
+ }
253
+ else {
254
+ this.wayStreak = Math.min(this.wayStreak + 1, UNDERWAY_STREAK);
255
+ }
256
+ }
237
257
  nextFrameDelayMs() {
238
258
  if (this.fixedFrameMs !== null)
239
259
  return this.fixedFrameMs;
240
- if (this.lastSog !== null && this.lastSog < UNDERWAY_SOG_MS)
241
- return exports.STILL_FRAME_EVERY_MS;
242
- return exports.FRAME_EVERY_MS;
260
+ return this.wayStreak >= UNDERWAY_STREAK ? exports.FRAME_EVERY_MS : exports.STILL_FRAME_EVERY_MS;
243
261
  }
244
262
  handleHistory(gen, data) {
245
263
  const handler = this.deps.onHistoryQuery;
@@ -453,7 +471,8 @@ class LiveUplink {
453
471
  if (code === CLOSE_PLAN_REQUIRED) {
454
472
  this.unentitled = true;
455
473
  this.lastError =
456
- 'Remote watching is not active on this account. She is recording as usual, and starts sending again when it is.';
474
+ 'Remote watching is not active on this account. She is recording as usual, and looks ' +
475
+ 'again every fifteen minutes. Your account at siparu.app/app says where it stands.';
457
476
  this.deps.debug(`live uplink: ${this.lastError}`);
458
477
  this.redial(STAND_OFF_MS);
459
478
  return;
@@ -0,0 +1,6 @@
1
+ export interface WriteVerdict {
2
+ ok: boolean;
3
+ failures: number;
4
+ last_error: string | null;
5
+ }
6
+ export declare function statusLine(writes: WriteVerdict, rowsToday: number, dataAgeSeconds: number | null): string;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.statusLine = statusLine;
4
+ function statusLine(writes, rowsToday, dataAgeSeconds) {
5
+ const age = dataAgeSeconds === null ? ', no data yet' : dataAgeSeconds > 60 ? `, data age ${dataAgeSeconds}s` : '';
6
+ if (!writes.ok) {
7
+ return `NOT recording - the disk refused the last row (${writes.last_error ?? 'unknown error'}); ${rowsToday} rows today before it did${age}`;
8
+ }
9
+ return `Recording - ${rowsToday} rows today${age}`;
10
+ }
@@ -9,12 +9,20 @@ export declare class Store {
9
9
  private currentHourKey;
10
10
  private knownKeys;
11
11
  private bytes;
12
+ private lastWriteOk;
13
+ private writeFailures;
14
+ private lastWriteError;
12
15
  onHourClosed: ((closedHour: string) => Promise<void>) | null;
13
16
  constructor(dataDir: string, capBytes: number, log: Logger);
14
17
  init(now: number): Promise<void>;
15
18
  private enqueue;
16
19
  rawPath(hour: string): string;
17
- append(snap: Snapshot): Promise<void>;
20
+ append(snap: Snapshot): Promise<boolean>;
21
+ writes(): {
22
+ ok: boolean;
23
+ failures: number;
24
+ last_error: string | null;
25
+ };
18
26
  listRawKeys(): Promise<string[]>;
19
27
  readRaw(hour: string): Promise<Snapshot[]>;
20
28
  rawUsage(): Promise<{
@@ -47,6 +47,9 @@ class Store {
47
47
  currentHourKey = null;
48
48
  knownKeys = new Set();
49
49
  bytes = 0;
50
+ lastWriteOk = true;
51
+ writeFailures = 0;
52
+ lastWriteError = null;
50
53
  onHourClosed = null;
51
54
  constructor(dataDir, capBytes, log) {
52
55
  this.capBytes = capBytes;
@@ -75,17 +78,31 @@ class Store {
75
78
  return path.join(this.rawDir, `${hour}.ndjson`);
76
79
  }
77
80
  append(snap) {
81
+ let written = false;
78
82
  return this.enqueue(async () => {
79
83
  const key = (0, time_1.hourKey)(snap.ts);
80
84
  const closed = this.currentHourKey && key !== this.currentHourKey ? this.currentHourKey : null;
81
85
  this.currentHourKey = key;
82
86
  const line = JSON.stringify(snap) + '\n';
83
- await fs.appendFile(this.rawPath(key), line, 'utf8');
84
- this.knownKeys.add(key);
85
- this.bytes += Buffer.byteLength(line);
87
+ try {
88
+ await fs.appendFile(this.rawPath(key), line, 'utf8');
89
+ this.knownKeys.add(key);
90
+ this.bytes += Buffer.byteLength(line);
91
+ written = true;
92
+ this.lastWriteOk = true;
93
+ }
94
+ catch (err) {
95
+ this.writeFailures++;
96
+ this.lastWriteOk = false;
97
+ this.lastWriteError = describeError(err);
98
+ this.log(`store error: raw append failed: ${this.lastWriteError}`);
99
+ }
86
100
  if (closed && this.onHourClosed)
87
101
  await this.onHourClosed(closed);
88
- });
102
+ }).then(() => written);
103
+ }
104
+ writes() {
105
+ return { ok: this.lastWriteOk, failures: this.writeFailures, last_error: this.lastWriteError };
89
106
  }
90
107
  async listRawKeys() {
91
108
  return [...this.knownKeys].sort();
@@ -129,6 +146,12 @@ class Store {
129
146
  }
130
147
  }
131
148
  exports.Store = Store;
149
+ function describeError(err) {
150
+ const e = err;
151
+ const code = typeof e?.code === 'string' ? e.code : null;
152
+ const message = e instanceof Error ? e.message : String(err);
153
+ return code && !message.startsWith(code) ? `${code}: ${message}` : message;
154
+ }
132
155
  function parseNdjson(text) {
133
156
  const out = [];
134
157
  for (const line of text.split('\n')) {
@@ -138,7 +138,8 @@ class Uplink {
138
138
  this.lastError = this.rejected
139
139
  ? 'Siparu no longer recognises this boat. Pair her again.'
140
140
  : this.unentitled
141
- ? 'Remote watching is not active on this account. She is recording as usual, and starts sending again when it is.'
141
+ ? 'Remote watching is not active on this account. She is recording as usual, and looks ' +
142
+ 'again every fifteen minutes. Your account at siparu.app/app says where it stands.'
142
143
  : `Relay refused the frame (${res.status}).`;
143
144
  this.deps.debug(`uplink: ${this.lastError}`);
144
145
  }
@@ -0,0 +1 @@
1
+ import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./jsx-runtime-CaR_m4Xc.js";import{t as r}from"./visibleInterval-CFxPzgzx.js";import{A as i,C as a,D as o,E as s,N as c,O as l,P as u,S as d,T as f,_ as p,b as m,c as h,d as g,g as _,k as v,l as y,o as b,p as x,s as S,v as C,w}from"./index-jUObqoMX.js";import{a as T,d as E,i as D,l as O,n as k,o as A,r as j,t as M,u as N}from"./export-CQSqkXJD.js";var P=e(t(),1);function F(e){let t=new Date(e),n=e=>String(e).padStart(2,`0`);return`${n(t.getUTCHours())}:${n(t.getUTCMinutes())}`}var I={key:`ts`,head:`UTC`,book:`bridge`,cell:e=>F(e.ts)};function L(e,t,n,r){if(e===null)return`·`;let i=Math.abs(e),a=Math.floor(i),o=(i-a)*60;return`${String(a).padStart(t,`0`)}°${o.toFixed(3).padStart(6,`0`)}' ${e<0?r:n}`}function R(e){return[{col:{key:`lat`,head:`LAT`,book:`bridge`,lanes:2,cell:e=>L(e.lat,2,`N`,`S`)},has:e=>e.lat!==null},{col:{key:`lon`,head:`LON`,book:`bridge`,lanes:2,cell:e=>L(e.lon,3,`E`,`W`)},has:e=>e.lon!==null},{col:{key:`sog`,head:`SOG`,book:`bridge`,unit:`kn`,cell:e=>m(o(e.sog),1)},has:e=>e.sog!==null},{col:{key:`cog`,head:`COG`,book:`bridge`,cell:e=>{let t=s(e.cog);return t===null?`·`:Math.round(t)+`°`}},has:e=>e.cog!==null},{col:{key:`hdg`,head:`HDG`,book:`bridge`,cell:e=>{let t=s(e.heading_true??e.heading_mag);return t===null?`·`:Math.round(t)+`°`}},has:e=>(e.heading_true??e.heading_mag)!==null},{col:{key:`wind`,head:e===`kn`?`TWS`:`BFT`,book:`bridge`,unit:e===`kn`?`kn`:void 0,tappable:!0,cell:t=>{let n=w(t.wind_speed_true);return n===null?`·`:String(e===`kn`?Math.round(n):a(n)??`·`)}},has:e=>e.wind_speed_true!==null},{col:{key:`awa`,head:`AWA`,book:`bridge`,cell:e=>{let t=s(e.wind_angle_apparent);if(t===null)return`·`;let n=(t+180)%360-180;return`${Math.abs(Math.round(n))}°${n<0?`P`:`S`}`}},has:e=>e.wind_angle_apparent!==null},{col:{key:`baro`,head:`BARO`,book:`bridge`,unit:`hPa`,dim:!0,cell:e=>{let t=f(e.air_pressure_pa);return t===null?`·`:String(Math.round(t))}},has:e=>e.air_pressure_pa!==null},{col:{key:`air`,head:`AIR`,book:`bridge`,unit:`°C`,cell:e=>m(d(e.air_temp_k),1)},has:e=>e.air_temp_k!==null},{col:{key:`sea`,head:`SEA`,book:`bridge`,unit:`°C`,cell:e=>m(d(e.water_temp_k),1)},has:e=>e.water_temp_k!==null},{col:{key:`depth`,head:`DEP`,book:`bridge`,unit:`m`,cell:e=>e.depth===null?`·`:e.depth.toFixed(1)},has:e=>e.depth!==null}]}var z={Revolutions:`RPM`,Temperature:`TEMP`,"Oil pressure":`OIL`,"Oil temperature":`OILT`,"Coolant temperature":`COOL`,"Coolant pressure":`COOLP`,"Exhaust temperature":`EXH`,"Intake manifold temperature":`INTK`,"Boost pressure":`BOOST`,"Engine load":`LOAD`,"Engine torque":`TORQ`,"Run time":`HRS`,"Fuel rate":`FUEL`,"Fuel per mile":`L/NM`,"Fuel used (since reset)":`USED`,"Fuel pressure":`FUELP`,"Alternator voltage":`ALT`,"Gearbox oil temperature":`GBOXT`,"Gearbox oil pressure":`GBOXP`,"Current level":`LEVEL`,Voltage:`VOLT`,Current:`AMP`};function B(e){return z[e]??e.split(` `)[0].toUpperCase()}function V(e){return e.sub===null?e.tab===`tanks`?`Current level`:null:e.sub}function H(e){let t=/^(Engine|Generator|Fuel|Fresh water|Black water|Grey water|Lubrication)\s+(\S+)$/.exec(e);return t?`${t[1].split(` `).map(e=>e[0]).join(``)}${t[2]}`.toUpperCase():e.slice(0,1).toUpperCase()}function ee(e){let t=[];for(let n of e)for(let e of Object.keys(n.path_values??{}))t.includes(e)||t.push(e);let n=[];for(let e of t){let t=l(e);if(!t)continue;let r=V(t);r!==null&&n.push({path:e,d:{...t,sub:r}})}let r=new Map;for(let{d:e}of n)r.has(e.tab)||r.set(e.tab,new Set),r.get(e.tab).add(e.label);let a=[];for(let{path:e,d:t}of n){let n=B(t.sub);a.push({col:{key:`p:${e}`,head:r.get(t.tab).size>1?`${H(t.label)} ${n}`:n,book:`engine`,tab:t.tab,unit:i(e),cell:t=>{let n=t.path_values?.[e];if(typeof n!=`number`)return`·`;let r=v(e,n).value;return Number.isFinite(r)?Number.isInteger(r)?String(r):r.toFixed(1):`·`}},has:t=>typeof t.path_values?.[e]==`number`})}return a}function te(e,t){return[I,...[...R(t),...ee(e)].filter(t=>e.some(t.has)).map(e=>e.col)]}function ne(e,t){return e.filter(e=>e.key===`ts`||e.book===t)}var U={engine:`ENGINES`,generator:`GENERATORS`,tanks:`TANKS`};function re(e){let t=[],n=new Map,r=new Map;for(let i of e)for(let e of Object.keys(i.path_values??{})){let i=l(e);if(!i)continue;let a=V(i);if(a===null)continue;n.has(i.tab)||(t.push(i.tab),n.set(i.tab,new Map),r.set(i.tab,[]));let o=n.get(i.tab);o.has(i.label)||o.set(i.label,new Map),o.get(i.label).set(a,e);let s=r.get(i.tab);s.includes(a)||s.push(a)}return t.map(e=>{let t=[...n.get(e)].map(([e,t])=>({key:e,head:e.toUpperCase(),paths:Object.fromEntries(t)})),a=r.get(e).map(e=>{let n=t.map(t=>t.paths[e]).find(e=>e!==void 0);return{key:e,head:B(e),unit:n===void 0?``:i(n)}});return{tab:e,head:U[e]??e.toUpperCase(),units:t,metrics:a}})}function W(e,t,n){let r=t.paths[n.key];if(r===void 0)return`·`;let i=e.path_values?.[r];if(typeof i!=`number`)return`·`;let a=v(r,i).value;return Number.isFinite(a)?Number.isInteger(a)?String(a):a.toFixed(1):`·`}function G(e,t){let[n,r]=(0,P.useState)(e),[i,a]=(0,P.useState)(!1);return(0,P.useEffect)(()=>{if(e===n)return;a(!0);let i=setTimeout(()=>{r(e),a(!1)},t);return()=>clearTimeout(i)},[e,n,t]),[n,i]}var K=e=>`lb:columns:${e}`,q={off:[]};function J(e){return e.key===`ts`}function Y(e,t){return J(e)||!t.off.includes(e.key)}function ie(e,t){return e.filter(e=>Y(e,t))}function X(e){return e.filter(e=>!J(e))}function ae(e,t){return J(t)?e:{off:Y(t,e)?[...e.off,t.key]:e.off.filter(e=>e!==t.key)}}function oe(e,t){return{off:t?[]:X(e).map(e=>e.key)}}function se(e,t){return ue(e.off)===ue(t.off)}function ce(e){try{let t=globalThis.localStorage?.getItem(K(e));if(!t)return q;let n=JSON.parse(t);return{off:Array.isArray(n.off)?n.off.filter(e=>typeof e==`string`):[]}}catch{return q}}function le(e,t){try{globalThis.localStorage?.setItem(K(e),JSON.stringify(t))}catch{}}function ue(e){return[...new Set(e)].sort().join(` `)}var Z=n();function de({cols:e,applied:t,onApply:n,onCancel:r}){let[i,a]=(0,P.useState)(t),o=se(i,t),s=X(e),c=s.filter(e=>Y(e,i)).length;return(0,Z.jsxs)(`div`,{className:`lb-pick`,children:[(0,Z.jsxs)(`div`,{className:`lbp-book`,children:[(0,Z.jsxs)(`div`,{className:`lbp-h`,children:[(0,Z.jsx)(`span`,{className:`lbp-n`,children:`Columns`}),(0,Z.jsxs)(`span`,{className:`lbp-s`,children:[c,` of `,s.length]}),(0,Z.jsxs)(`span`,{className:`lbp-all`,children:[(0,Z.jsx)(`button`,{onClick:()=>a(oe(e,!0)),children:`All`}),(0,Z.jsx)(`button`,{onClick:()=>a(oe(e,!1)),children:`None`})]})]}),(0,Z.jsx)(`div`,{className:`lbp-chips`,children:s.map(e=>(0,Z.jsx)(`button`,{className:`lbp-c${Y(e,i)?` on`:``}`,"aria-pressed":Y(e,i),onClick:()=>a(ae(i,e)),children:e.head},e.key))})]}),(0,Z.jsxs)(`div`,{className:`lbp-act`,children:[(0,Z.jsx)(`button`,{onClick:r,children:`Cancel`}),(0,Z.jsx)(`button`,{className:`lbp-go`,disabled:o,onClick:()=>n(i),children:`Show these columns`})]})]})}var fe={"1m":1,"1h":60,"6h":360,"1d":1440},pe={"1m":`Every minute`,"1h":`Hourly`,"6h":`Six-hourly`,"1d":`Daily`},me={"1m":60,"1h":48,"6h":40,"1d":30},he=15e3;function ge(e){let t=c(),[n,i]=(0,P.useState)(0),[a,o]=(0,P.useState)(!1),[s,l]=(0,P.useState)([]),[u,d]=(0,P.useState)(null),[f,p]=(0,P.useState)(!1),[m,h]=(0,P.useState)(e);m!==e&&(h(e),i(0),o(!1));let g=fe[e],_=me[e]+n*me[e],v=(0,P.useCallback)(async()=>{p(!0),d(null);try{let e=_+1,n=g===1?(await t.logbook.minutes({limit:e,order:`desc`})).rows:await t.logbook.snapshots({bucket:g,limit:e,order:`desc`});o(n.length>_),l(n.slice(0,_))}catch(e){d(e.message)}finally{p(!1)}},[g,_]);return(0,P.useEffect)(()=>{v();let e=r(v,he);return()=>e()},[v]),{snaps:s,err:u,busy:f,hasMore:a,loadMore:(0,P.useCallback)(()=>i(e=>e+1),[])}}function _e(){let e=c(),[t,n]=(0,P.useState)(C()),[i,a]=(0,P.useState)([]),[o,s]=(0,P.useState)(null),[l,u]=(0,P.useState)(!1),d=(0,P.useRef)(!0),f=x(6e4),m=C(new Date(f)),h=(0,P.useMemo)(()=>p(t),[t]),g=h+24*36e5,_=t===m;(0,P.useEffect)(()=>{d.current&&t!==m&&n(m)},[m,t]);let v=(0,P.useCallback)(e=>{d.current=e===C(),n(e)},[]),y=(0,P.useCallback)(async()=>{u(!0),s(null);try{let t=await e.logbook.snapshots({from:h,to:g,limit:5e3,order:`desc`,bucket:60});a(t)}catch(e){s(e.message)}finally{u(!1)}},[h,g]);return(0,P.useEffect)(()=>{if(y(),!_)return;let e=r(y,he);return()=>e()},[y,_]),{dateStr:t,setDateStr:v,dayStart:h,isToday:_,snaps:i,err:o,busy:l,prevDay:(0,P.useCallback)(()=>v(C(new Date(h-864e5))),[h,v]),nextDay:(0,P.useCallback)(()=>v(C(new Date(h+864e5))),[h,v]),goToday:(0,P.useCallback)(()=>v(C()),[v])}}var ve=5e3;async function ye(e,t,n){let r=await e({from:t,to:n,limit:5001,order:`desc`});return{rows:r.rows.slice(0,ve),truncated:r.clamped||r.rows.length>5e3,minutesFrom:r.minutesFrom}}function be(e,t,n,r=`last`){let i=c(),[a,o]=(0,P.useState)([]),[s,l]=(0,P.useState)(null),[u,d]=(0,P.useState)(!1),[f,m]=(0,P.useState)(!1),[h,g]=(0,P.useState)(!1),[_,v]=(0,P.useState)(null),[y,b]=(0,P.useState)([]),x=(0,P.useMemo)(()=>p(e),[e]),S=(0,P.useMemo)(()=>p(t)+864e5-1,[t]),C=fe[n],w=(0,P.useCallback)(async()=>{if(Number.isNaN(x)||Number.isNaN(S)||S<x){o([]),b([]),m(!1),l(null),g(!0);return}g(!1),d(!0),l(null);try{if(b([]),C===1){let e=await ye(e=>i.logbook.minutes(e),x,S);v(e.minutesFrom),m(e.truncated),o(e.rows);return}let e=await i.logbook.rollupHours(x,S);v(null);let t=N(e,n),a=t.map(e=>E(e,r)),s=e=>e.slice(Math.max(0,e.length-ve));m(a.length>ve),o(s(a)),r!==`last`&&b(s(t.map(e=>E(e,`last`))))}catch(e){l(e.message)}finally{d(!1),g(!0)}},[x,S,C,n,r]);return(0,P.useEffect)(()=>{w()},[w]),{snaps:a,err:s,busy:u,truncated:f,loaded:h,minutesFrom:_,plain:y}}var xe=[`1m`,`1h`,`6h`,`1d`],Se=[{v:`csv`,name:`CSV`,note:`a file for a spreadsheet`},{v:`pdf`,name:`PDF`,note:`the page, through your printer`}],Ce=[{v:`screen`,name:`Screen`},{v:`paper`,name:`Paper`}],we=[`last`,`avg`,`min`,`max`];function Te(e,t,n){return e===`pdf`?[n]:t.includes(n)?t.filter(e=>e!==n):[...t,n]}function Ee(e,t){return e===`pdf`?[t[0]??`last`]:t}function De(){return C(new Date(Date.now()-7*864e5))}function Oe({initial:e,onView:t,onSave:n}){let[r,i]=(0,P.useState)(e?.from??De()),[a,o]=(0,P.useState)(e?.to??C()),[s,c]=(0,P.useState)(e?.gran??`1h`),[l,u]=(0,P.useState)(e?.format??`csv`),[d,f]=(0,P.useState)(e?.stats??[`last`]),[p,m]=(0,P.useState)(e?.distance??!1),[h,g]=(0,P.useState)(e?.samples??!1),[_,v]=(0,P.useState)(e?.style??`screen`),y=a<r,b={from:r,to:a,gran:s,format:l,stats:Ee(l,d),distance:p,samples:h,style:_},x=s!==`1m`,S=l===`pdf`,w=x&&!S&&d.length===0&&!p&&!h,T=e=>f(t=>Te(l,t,e)),E=Ee(l,d),D=e=>E.includes(e);return(0,Z.jsxs)(`div`,{className:`lb-pick lb-exp`,children:[(0,Z.jsxs)(`div`,{className:`lbp-books`,children:[(0,Z.jsxs)(`div`,{className:`lbp-book`,children:[(0,Z.jsxs)(`div`,{className:`lbp-h`,children:[(0,Z.jsx)(`span`,{className:`lbp-n`,children:`Window`}),(0,Z.jsx)(`span`,{className:`lbp-s`,children:`local days, both ends included`})]}),(0,Z.jsxs)(`div`,{className:`lbe-dates`,children:[(0,Z.jsxs)(`label`,{children:[(0,Z.jsx)(`span`,{children:`From`}),(0,Z.jsx)(`input`,{type:`date`,className:`dt`,value:r,max:C(),onChange:e=>i(e.target.value)})]}),(0,Z.jsxs)(`label`,{children:[(0,Z.jsx)(`span`,{children:`To`}),(0,Z.jsx)(`input`,{type:`date`,className:`dt`,value:a,max:C(),onChange:e=>o(e.target.value)})]})]})]}),(0,Z.jsxs)(`div`,{className:`lbp-book`,children:[(0,Z.jsxs)(`div`,{className:`lbp-h`,children:[(0,Z.jsx)(`span`,{className:`lbp-n`,children:`Interval`}),(0,Z.jsx)(`span`,{className:`lbp-s`,children:`one row per`})]}),(0,Z.jsx)(`div`,{className:`lbp-chips`,children:xe.map(e=>(0,Z.jsx)(`button`,{className:`lbp-c${s===e?` on`:``}`,onClick:()=>c(e),children:pe[e]},e))})]}),(0,Z.jsxs)(`div`,{className:`lbp-book`,children:[(0,Z.jsxs)(`div`,{className:`lbp-h`,children:[(0,Z.jsx)(`span`,{className:`lbp-n`,children:`Save as`}),(0,Z.jsx)(`span`,{className:`lbp-s`,children:Se.find(e=>e.v===l)?.note})]}),(0,Z.jsx)(`div`,{className:`lbp-chips`,children:Se.map(e=>(0,Z.jsx)(`button`,{className:`lbp-c${l===e.v?` on`:``}`,onClick:()=>u(e.v),children:e.name},e.v))})]}),(0,Z.jsxs)(`div`,{className:`lbp-book${S?``:` off`}`,children:[(0,Z.jsxs)(`div`,{className:`lbp-h`,children:[(0,Z.jsx)(`span`,{className:`lbp-n`,children:`Style`}),(0,Z.jsx)(`span`,{className:`lbp-s`,children:S?_===`screen`?`the app's own dark ink`:`white, for a printer`:`the page's ink; a file carries none`})]}),(0,Z.jsx)(`div`,{className:`lbp-chips`,children:Ce.map(e=>(0,Z.jsx)(`button`,{className:`lbp-c${_===e.v?` on`:``}`,disabled:!S,onClick:()=>v(e.v),children:e.name},e.v))})]})]}),x&&(0,Z.jsxs)(`div`,{className:`lbp-book lbp-figs`,children:[(0,Z.jsxs)(`div`,{className:`lbp-h`,children:[(0,Z.jsx)(`span`,{className:`lbp-n`,children:`Figures`}),(0,Z.jsx)(`span`,{className:`lbp-s`,children:S?`one: a table cell holds one number`:`one or more: a column each`})]}),(0,Z.jsxs)(`div`,{className:`lbp-chips`,children:[we.map(e=>(0,Z.jsx)(`button`,{className:`lbp-c${D(e)?` on`:``}`,onClick:()=>T(e),children:O[e]},e)),(0,Z.jsx)(`button`,{className:`lbp-c${p?` on`:``}`,disabled:S,onClick:()=>m(!p),children:`Distance`}),(0,Z.jsx)(`button`,{className:`lbp-c${h?` on`:``}`,disabled:S,onClick:()=>g(!h),children:`Samples`})]})]}),y&&(0,Z.jsx)(`div`,{className:`lbe-warn`,children:`That window ends before it begins.`}),w&&(0,Z.jsx)(`div`,{className:`lbe-warn`,children:`Choose at least one figure to put in the file.`}),(0,Z.jsxs)(`div`,{className:`lbp-act`,children:[S&&(0,Z.jsx)(`button`,{disabled:y,onClick:()=>t(b),children:`View`}),(0,Z.jsxs)(`button`,{className:`lbp-go`,disabled:y||w,onClick:()=>n(b),children:[`Save `,l.toUpperCase()]})]})]})}function Q({open:e,style:t,cls:n,children:r}){let[i,a]=(0,P.useState)(e);return e&&!i&&a(!0),i?(0,Z.jsx)(`div`,{className:`lb-open${e?``:` shut`}${n??``}`,style:t,onAnimationEnd:t=>{t.target===t.currentTarget&&!e&&a(!1)},children:(0,Z.jsx)(`div`,{className:`lb-open-in`,children:r})}):null}function ke(e){let t=e-32-58;return Math.max(1,Math.floor(t/52))}function Ae(e){return e.reduce((e,t)=>e+(t.lanes??1),0)}function je(e,t){if(t===null||e.length===0)return e;let n=ke(t),r=e.slice(0,1);for(let t of e.slice(1)){let e=t.lanes??1;if(e>n)break;n-=e,r.push(t)}return r}function Me(e,t,n){return t===null?e:e.slice(0,ke(t-(n?108:0)))}var Ne=180,Pe=[`1m`,`1h`,`6h`,`1d`],Fe={"1m":`Last hour`,"1h":`Last 2 days`,"6h":`Last 10 days`,"1d":`Last month`};function Ie({book:e}){let t=c(),[n,r]=(0,P.useState)(`live`),[i,a]=(0,P.useState)(`1h`),[o,s]=(0,P.useState)(null),[l,u]=G((0,P.useMemo)(()=>({book:e,mode:n,gran:i,family:o}),[e,n,i,o]),Ne),{book:d,mode:f,gran:m,family:h}=l,_=n!==f,[v,y]=(0,P.useState)(()=>localStorage.getItem(`lb:windUnit`)||`kn`),b=()=>y(e=>{let t=e===`kn`?`bft`:`kn`;return localStorage.setItem(`lb:windUnit`,t),t}),x=(0,P.useRef)([]),S=(0,P.useRef)(0),[C,w]=(0,P.useState)(()=>ce(d)),[T,D]=(0,P.useState)(d);T!==d&&(D(d),w(ce(d)),x.current=[],S.current=0);let[O,F]=(0,P.useState)(!1),I=e=>{w(e),le(d,e),F(!1)},[L,R]=g(),z=(0,P.useRef)(null),B=(0,P.useRef)(null),V=(0,P.useCallback)(e=>{B.current=e,L(e)},[L]),H=(0,P.useRef)(null);(0,P.useLayoutEffect)(()=>{let e=B.current;if(!e)return;let t=()=>e.querySelectorAll(`.lb-ctrl, .lb-pick, .lb-frame`);if(u){let e=new Map;t().forEach(t=>e.set(t,t.getBoundingClientRect().width)),H.current=e;return}let n=H.current;if(H.current=null,!n||window.matchMedia(`(prefers-reduced-motion: reduce)`).matches)return;let r=parseFloat(getComputedStyle(e).getPropertyValue(`--lb-fade-in`))||0;t().forEach(e=>{let t=n.get(e);if(t===void 0||typeof e.animate!=`function`)return;let i=e.getBoundingClientRect().width;Math.abs(i-t)<1||e.animate([{maxWidth:`${t}px`},{maxWidth:`${i}px`}],{duration:r,easing:`cubic-bezier(0.32, 0.72, 0, 1)`})})},[u,l]);let[ee,U]=(0,P.useState)(!1),[re,W]=(0,P.useState)(null),[K,q]=(0,P.useState)(!1),[J,Y]=(0,P.useState)(null),[X,ae]=(0,P.useState)(null),oe={book:d,mode:n,setMode:r,gran:i,setGran:a,shownGran:m,family:o,setFamily:s,shownFamily:h,leaving:u,modeLeaving:_,lanesHold:z,groupsHold:x,chosenHold:S,windUnit:v,toggleWind:b,selection:C,picking:O,setPicking:F,applySelection:I,width:R,exporting:ee,setExporting:U,request:re,onView:e=>{W(e),r(`range`),Y(null)},onSave:async e=>{if(U(!1),Y(null),ae(null),e.format===`pdf`){W(e),r(`range`),q(!0);return}try{let n=p(e.from),r=p(e.to)+864e5-1;if(e.gran===`1m`){let{rows:e,truncated:i}=await ye(e=>t.logbook.minutes(e),n,r),a=ie(ne(te(e,v),d),C);k(j(`logbook-${d}`,n,`csv`,i?`-partial`:``),`text/csv`,A(e,a)),i&&ae(`That window holds more than ${ve} minutes, so the file stops at the most recent ${ve} and its name ends in -partial. A longer interval covers the same days in fewer rows.`);return}let i=N(await t.logbook.rollupHours(n,r),e.gran),a=e.stats.map(e=>({stat:e,cols:ie(ne(te(i.map(t=>E(t,e)),v),d),C)})).filter(e=>e.cols.length>1);k(j(`logbook-${d}`,n,`csv`),`text/csv`,M(i,a,{distance:e.distance,samples:e.samples}))}catch(e){Y(e.message)}},saveErr:J,saveNote:X};return(0,Z.jsxs)(`div`,{className:`lb`,ref:V,children:[(0,Z.jsx)(`style`,{children:`@media print { @page { size: A4 landscape; } }`}),f===`live`?(0,Z.jsx)(it,{...oe}):f===`day`?(0,Z.jsx)(at,{...oe}):(0,Z.jsx)(ft,{...oe,req:re,printing:K,donePrinting:()=>q(!1)})]})}function Le({mode:e,setMode:t}){return(0,Z.jsxs)(`div`,{className:`seg`,children:[(0,Z.jsx)(`button`,{className:e===`live`?`on`:``,onClick:()=>t(`live`),children:`Live`}),(0,Z.jsx)(`button`,{className:e===`day`?`on`:``,onClick:()=>t(`day`),children:`Day`})]})}function Re({leaving:e,children:t}){return(0,Z.jsx)(`div`,{className:`lb-win${e?` leaving`:``}`,children:t})}function ze({groups:e,family:t,setFamily:n}){if(e.length<2)return null;let r=e.find(e=>e.tab===t)??e[0];return(0,Z.jsx)(`div`,{className:`seg`,children:e.map(e=>(0,Z.jsx)(`button`,{className:e.tab===r.tab?`on`:``,onClick:()=>n(e.tab),children:e.head},e.tab))})}function Be(e,t,n){if(e===null||p(t.from)>=e)return null;let r=new Date(e).toLocaleDateString(void 0,{day:`numeric`,month:`short`}),i=n[n.length-1];return`Minutes reach back to ${r}.${i!==void 0&&i.ts<e?` Earlier days in this window read one row per hour.`:``}`}function Ve(e,t,n){let r=O[e].toLowerCase(),i=t.length===1?t[0]:`${t.slice(0,-1).join(`, `)} and ${t[t.length-1]}`,a=n.some(e=>e.lat!==null&&e.lon!==null),o=t.length===1;return`${i} ${o?`has`:`have`} no ${r}, so on each row ${o?`it carries`:`they carry`} the reading that window closed on${a?`, as the position does`:``}.`}function He(e,t){if(t.length===0)return e;let n=new Map(t.map(e=>[e.ts,e]));return e.map(e=>{let t=n.get(e.ts);if(!t)return e;let r={...e};for(let[e,n]of Object.entries(t))e!==`path_values`&&(r[e]===null||r[e]===void 0)&&n!=null&&(r[e]=n);if(t.path_values){let n={...e.path_values??{}};for(let[e,r]of Object.entries(t.path_values))(n[e]===void 0||n[e]===null)&&(n[e]=r);r.path_values=n}return r})}function Ue(e){return p(e.to)+864e5-1>=Date.now()&&e.gran!==`1m`?`No row has closed at this interval yet. A shorter interval shows today's.`:`Nothing was logged between these dates.`}function We(e,t,n){return e.length>1||t===null?$(e):{"--lb-cols":n.current??ke(t)}}function Ge({what:e}){return(0,Z.jsxs)(`div`,{className:`sp-empty`,children:[(0,Z.jsx)(`div`,{className:`em-t`,children:`No snapshots`}),(0,Z.jsx)(`div`,{className:`em-s`,children:e})]})}var Ke=(e,t)=>`u:${e}:${t}`;function qe(e,t,n,r,i,a,o,s,c,l=!0){let u=Ye(e,t,n,r,i,a,o,s,c,l);if(t!==`engine`||u.groups.length<2)return u;let d=u,f=Je(u);for(let o of u.groups){if(o===u.group||o.tab===a)continue;let s=Ye(e,t,n,r,i,o.tab,{current:null},{current:[]},{current:0},l),c=Je(s);c>f&&(d=s,f=c)}return d===u?u:{...u,block:d.block,cls:d.cls}}function Je(e){let t=Math.max(1,Number(e.block[`--lb-cols`]??1));return 58+(e.cls.includes(`u`)?108:0)+t*164}function Ye(e,t,n,r,i,a,o,s,c,l){let u=t===`engine`?re(e):[];u.length>0&&(s.current=u);let d=u.length===0&&e.length===0?s.current:u,f=u.find(e=>e.tab===a)??u[0];if(f&&f.units.length>1){let e=f.metrics.map(e=>({key:Ke(f.tab,e.key),head:e.head})),t=f.metrics.filter(e=>Y({key:Ke(f.tab,e.key)},r)),n=l?Me(t,i,!0):t,a={...f,metrics:n};return o.current=Math.max(1,n.length),t.length>0&&(c.current=t.length),{groups:d,group:a,drawn:[],pick:e,btn:{shown:n.length,chosen:t.length,kept:c.current,offered:e.length},block:{"--lb-cols":Math.max(1,n.length)},cls:` u`}}let p=ne(te(e,n),t),m=f?p.filter(e=>e.key===`ts`||e.tab===f.tab):p,h=ie(m,r),g=l?je(h,i):h;return g.length>1&&(o.current=Ae(g.slice(1))),h.length>1&&(c.current=h.length-1),{groups:d,group:null,drawn:g,pick:m,btn:{shown:g.length-1,chosen:h.length-1,kept:c.current,offered:m.length-1},block:We(g,i,o),cls:``}}function Xe(e){return{"--lb-cols":Math.max(1,e.metrics.length)}}function $(e){return{"--lb-cols":Ae(e.slice(1))}}function Ze(e){return(e.lanes??1)>1?` w${e.lanes}`:``}function Qe(e,t){return t===`last`?pe[e]:`${pe[e]} · ${O[t]}`}function $e(e,t,n,r){return r===0?String(n):e<t?`${e} of ${t}`:String(t)}function et({shown:e,chosen:t,kept:n,offered:r,open:i,onOpen:a}){let o=r===0;return(0,Z.jsxs)(`button`,{className:`lb-colbtn${i?` on`:``}${o?` quiet`:``}`,onClick:a,disabled:o,children:[`Columns · `,(0,Z.jsx)(`b`,{children:$e(e,t,n,r)})]})}function tt({open:e,onOpen:t}){return(0,Z.jsx)(`button`,{className:`lb-colbtn${e?` on`:``}`,onClick:t,children:`View`})}function nt({cols:e,group:t,toggleWind:n}){if(t){let e=t.units.length>1;return(0,Z.jsxs)(`div`,{className:`lb-cols${e?` u`:``}`,style:Xe(t),children:[(0,Z.jsx)(`span`,{children:`UTC`}),e&&(0,Z.jsx)(`span`,{className:`un`,children:`Unit`}),t.metrics.map(e=>(0,Z.jsxs)(`span`,{children:[e.head,(0,Z.jsx)(rt,{of:e.unit})]},e.key))]})}return(0,Z.jsx)(`div`,{className:`lb-cols`,style:$(e),children:e.map(e=>e.tappable?(0,Z.jsxs)(`span`,{className:`tap${Ze(e)}`,onClick:n,title:`Tap: knots ⇄ Beaufort`,children:[e.head,(0,Z.jsx)(rt,{of:e.unit})]},e.key):(0,Z.jsxs)(`span`,{className:Ze(e).trim(),children:[e.head,(0,Z.jsx)(rt,{of:e.unit})]},e.key))})}function rt({of:e}){return(0,Z.jsx)(`b`,{className:`lb-u`,children:e===void 0||e===``?`\xA0`:e})}function it({book:e,mode:t,setMode:n,gran:r,setGran:i,shownGran:a,family:o,setFamily:s,shownFamily:c,leaving:l,modeLeaving:u,lanesHold:d,groupsHold:f,chosenHold:p,windUnit:m,toggleWind:h,selection:g,picking:_,setPicking:v,applySelection:y,width:b,exporting:x,setExporting:S,request:C,onView:w,onSave:T,saveErr:E,saveNote:D}){let{snaps:O,err:k,busy:A,hasMore:j,loadMore:M}=ge(a),{groups:N,group:P,drawn:F,pick:I,btn:L,block:R,cls:z}=qe(O,e,m,g,b,c,d,f,p);return(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsxs)(`div`,{className:`lb-ctrl${z}`,style:R,children:[(0,Z.jsx)(Le,{mode:t,setMode:n}),(0,Z.jsx)(Re,{leaving:u,children:(0,Z.jsx)(`div`,{className:`seg`,children:Pe.map(e=>(0,Z.jsx)(`button`,{className:r===e?`on`:``,onClick:()=>i(e),children:e},e))})}),(0,Z.jsx)(ze,{groups:N,family:o,setFamily:s}),(0,Z.jsxs)(`div`,{className:`lb-acts`,children:[(0,Z.jsx)(et,{shown:L.shown,chosen:L.chosen,kept:L.kept,offered:L.offered,open:_,onOpen:()=>v(!_)}),(0,Z.jsx)(tt,{open:x,onOpen:()=>S(!x)})]})]}),(0,Z.jsx)(Q,{open:_,style:R,cls:z,children:(0,Z.jsx)(de,{cols:I,applied:g,onApply:y,onCancel:()=>v(!1)})}),(0,Z.jsx)(Q,{open:x,style:R,cls:z,children:(0,Z.jsx)(Oe,{initial:C??void 0,onView:w,onSave:T})}),E&&(0,Z.jsx)(`div`,{className:`lb-err`,children:E}),D&&(0,Z.jsx)(`div`,{className:`lb-note`,children:D}),(0,Z.jsxs)(`div`,{className:`lb-frame${z}${l?` leaving`:``}`,style:R,children:[(0,Z.jsx)(ct,{book:e,window:Fe[a],interval:pe[a]}),(0,Z.jsxs)(`div`,{className:`lb-day`,style:$(F),children:[(0,Z.jsx)(`span`,{children:Fe[a]}),(0,Z.jsx)(`b`,{children:O.length})]}),(0,Z.jsx)(nt,{cols:F,group:P,toggleWind:h}),k&&(0,Z.jsx)(`div`,{className:`lb-err`,children:k}),!A&&!k&&O.length===0?(0,Z.jsx)(Ge,{what:`Nothing was logged in this window (${Fe[a].toLowerCase()}).`}):(0,Z.jsx)(pt,{snaps:O,cols:F,group:P,footer:j?(0,Z.jsx)(`button`,{className:`lb-more`,onClick:M,disabled:A,children:A?`Loading…`:`Load ${me[a]} more`}):null}),(0,Z.jsx)(lt,{})]})]})}function at({book:e,mode:t,setMode:n,family:r,setFamily:i,shownFamily:a,leaving:o,modeLeaving:s,lanesHold:c,groupsHold:l,chosenHold:u,windUnit:d,toggleWind:f,selection:p,picking:m,setPicking:h,applySelection:g,width:_,exporting:v,setExporting:y,request:b,onView:x,onSave:S,saveErr:w,saveNote:T}){let{dateStr:E,setDateStr:D,isToday:O,snaps:k,err:A,busy:j,prevDay:M,nextDay:N,goToday:P}=_e(),{groups:F,group:I,drawn:L,pick:R,btn:z,block:B,cls:V}=qe(k,e,d,p,_,a,c,l,u),H=O?`Today · ${new Date(E).toLocaleDateString(`en-GB`,{day:`2-digit`,month:`short`,timeZone:`UTC`})}`:new Date(E).toLocaleDateString(`en-GB`,{weekday:`short`,day:`2-digit`,month:`short`,timeZone:`UTC`}).replace(/,/g,``);return(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsxs)(`div`,{className:`lb-ctrl${V}`,style:B,children:[(0,Z.jsx)(Le,{mode:t,setMode:n}),(0,Z.jsx)(Re,{leaving:s,children:(0,Z.jsxs)(`div`,{className:`lb-date`,children:[(0,Z.jsx)(`button`,{className:`lb-step`,onClick:M,"aria-label":`Previous day`,children:`‹`}),(0,Z.jsx)(`input`,{type:`date`,className:`dt`,value:E,max:C(),onChange:e=>D(e.target.value)}),(0,Z.jsx)(`button`,{className:`lb-step`,onClick:N,disabled:O,"aria-label":`Next day`,children:`›`}),(0,Z.jsx)(`button`,{onClick:P,disabled:O,children:`Now`})]})}),(0,Z.jsx)(ze,{groups:F,family:r,setFamily:i}),(0,Z.jsxs)(`div`,{className:`lb-acts`,children:[(0,Z.jsx)(et,{shown:z.shown,chosen:z.chosen,kept:z.kept,offered:z.offered,open:m,onOpen:()=>h(!m)}),(0,Z.jsx)(tt,{open:v,onOpen:()=>y(!v)})]})]}),(0,Z.jsx)(Q,{open:m,style:B,cls:V,children:(0,Z.jsx)(de,{cols:R,applied:p,onApply:g,onCancel:()=>h(!1)})}),(0,Z.jsx)(Q,{open:v,style:B,cls:V,children:(0,Z.jsx)(Oe,{initial:b??void 0,onView:x,onSave:S})}),w&&(0,Z.jsx)(`div`,{className:`lb-err`,children:w}),T&&(0,Z.jsx)(`div`,{className:`lb-note`,children:T}),(0,Z.jsxs)(`div`,{className:`lb-frame${V}${o?` leaving`:``}`,style:B,children:[(0,Z.jsx)(ct,{book:e,window:H,interval:pe[`1h`]}),(0,Z.jsxs)(`div`,{className:`lb-day`,style:$(L),children:[(0,Z.jsx)(`span`,{children:H}),(0,Z.jsx)(`b`,{children:k.length})]}),(0,Z.jsx)(nt,{cols:L,group:I,toggleWind:f}),A&&(0,Z.jsx)(`div`,{className:`lb-err`,children:A}),!j&&k.length===0?(0,Z.jsx)(Ge,{what:`No telemetry was logged for this day.`}):(0,Z.jsx)(pt,{snaps:k,cols:L,group:I,footer:null}),(0,Z.jsx)(lt,{})]})]})}function ot(){let e=new Date,t=e.toLocaleDateString(`en-GB`,{day:`2-digit`,month:`short`,year:`numeric`,timeZone:`UTC`}),n=e=>String(e).padStart(2,`0`);return`${t.toUpperCase()} · ${n(e.getUTCHours())}:${n(e.getUTCMinutes())} UTC`}var st={bridge:`CHIEF OFFICER`,engine:`CHIEF ENGINEER`};function ct({book:e,window:t,interval:n}){return(0,Z.jsxs)(`div`,{className:`lb-print-hd`,children:[(0,Z.jsxs)(`div`,{className:`ph-id`,children:[(0,Z.jsxs)(`span`,{className:`sp-lockup`,children:[(0,Z.jsx)(S,{className:`sp-glyph`}),(0,Z.jsx)(`span`,{className:`ph-wm`,children:`Siparu`})]}),(0,Z.jsxs)(`span`,{className:`ph-book`,children:[`LOGBOOK · `,(0,Z.jsx)(`b`,{children:e.toUpperCase()}),` · `,st[e]]})]}),(0,Z.jsxs)(`div`,{className:`ph-meta`,children:[(0,Z.jsxs)(`div`,{children:[(0,Z.jsx)(`span`,{className:`l`,children:`GENERATED`}),(0,Z.jsx)(`b`,{children:ot()})]}),(0,Z.jsxs)(`div`,{children:[(0,Z.jsx)(`span`,{className:`l`,children:`WINDOW`}),(0,Z.jsx)(`b`,{children:`${t} · ${n} · UTC`.toUpperCase()})]})]})]})}function lt(){return(0,Z.jsx)(`div`,{className:`lb-print-ft`,children:(0,Z.jsx)(`span`,{children:`siparu.app`})})}function ut(e){return new Date(e).toLocaleDateString(`en-GB`,{weekday:`short`,day:`2-digit`,month:`short`,year:`numeric`,timeZone:`UTC`}).replace(/,\s*/,` · `).toUpperCase()}function dt(e,t){let n=(e,t)=>new Date(e).toLocaleDateString(`en-GB`,{day:`2-digit`,month:`short`,...t?{year:`numeric`}:{},timeZone:`UTC`});return e===t?n(t,!0):`${n(e,!1)} - ${n(t,!0)}`}function ft({book:e,req:t,setMode:n,family:r,setFamily:i,shownFamily:a,leaving:o,modeLeaving:s,lanesHold:c,groupsHold:l,chosenHold:u,windUnit:d,toggleWind:f,selection:p,picking:m,setPicking:h,applySelection:g,width:_,exporting:v,setExporting:y,request:b,onView:x,onSave:S,saveErr:w,saveNote:E,printing:O,donePrinting:k}){let A={from:C(),to:C(),gran:`1h`,format:`csv`,stats:[`last`],distance:!1,samples:!1,style:`screen`},j=t??A,M=j.stats[0]??`last`,{snaps:N,err:F,busy:I,truncated:L,loaded:R,minutesFrom:z,plain:B}=be(j.from,j.to,j.gran,M),V=(0,P.useMemo)(()=>He([...N].sort((e,t)=>e.ts-t.ts),B),[N,B]),{groups:H,group:ee,drawn:U,pick:re,btn:W,block:G,cls:K}=qe(V,e,d,p,_,a,c,l,u,!1),q=(0,P.useCallback)(()=>k(),[k]);(0,P.useEffect)(()=>{!O||!R||I||(q(),D(T(`Siparu-Logbook`,Date.now()),j.style))},[O,R,I,q,j.style]);let J=dt(j.from,j.to),Y=Qe(j.gran,M),X=(0,P.useMemo)(()=>{if(B.length===0)return[];let t=t=>ie(ne(te(t,d),e),p).map(e=>e.head),n=new Set(t(N));return t(B).filter(e=>!n.has(e))},[B,N,d,e,p]);return(0,Z.jsxs)(Z.Fragment,{children:[(0,Z.jsxs)(`div`,{className:`lb-ctrl${K}`,style:G,children:[(0,Z.jsx)(Le,{mode:`range`,setMode:n}),(0,Z.jsx)(Re,{leaving:s,children:(0,Z.jsxs)(`button`,{className:`lb-colbtn on`,onClick:()=>y(!v),children:[J,` · `,(0,Z.jsx)(`b`,{children:Y})]})}),(0,Z.jsx)(ze,{groups:H,family:r,setFamily:i}),(0,Z.jsx)(`div`,{className:`lb-acts`,children:(0,Z.jsx)(et,{shown:W.shown,chosen:W.chosen,kept:W.kept,offered:W.offered,open:m,onOpen:()=>h(!m)})})]}),(0,Z.jsx)(Q,{open:m,style:G,cls:K,children:(0,Z.jsx)(de,{cols:re,applied:p,onApply:g,onCancel:()=>h(!1)})}),(0,Z.jsx)(Q,{open:v,style:G,cls:K,children:(0,Z.jsx)(Oe,{initial:b??void 0,onView:x,onSave:S})}),w&&(0,Z.jsx)(`div`,{className:`lb-err`,children:w}),E&&(0,Z.jsx)(`div`,{className:`lb-note`,children:E}),(0,Z.jsxs)(`div`,{className:`lb-frame dated${K}${o?` leaving`:``}`,style:G,children:[(0,Z.jsx)(ct,{book:e,window:J,interval:Y}),(0,Z.jsxs)(`div`,{className:`lb-day`,style:$(U),children:[(0,Z.jsxs)(`span`,{children:[J,` · `,Y]}),(0,Z.jsx)(`b`,{children:L?`${N.length} of more`:N.length})]}),(0,Z.jsx)(nt,{cols:U,group:ee,toggleWind:f}),F&&(0,Z.jsx)(`div`,{className:`lb-err`,children:F}),X.length>0&&(0,Z.jsx)(`div`,{className:`lb-note`,children:Ve(M,X,N)}),L&&(0,Z.jsxs)(`div`,{className:`lb-note`,children:[`This window holds more than `,5e3,` rows. The most recent `,5e3,` are here; a longer interval covers the same days in fewer.`]}),Be(z,j,N)&&(0,Z.jsx)(`div`,{className:`lb-note`,children:Be(z,j,N)}),!I&&V.length===0?(0,Z.jsx)(Ge,{what:Ue(j)}):(0,Z.jsx)(pt,{snaps:V,cols:U,group:ee,footer:null,dated:!0}),(0,Z.jsx)(lt,{})]})]})}function pt({snaps:e,cols:t,group:n,footer:r,dated:i=!1}){let a=[],o=null;for(let r of e){if(i){let e=ut(r.ts);e!==o&&(a.push((0,Z.jsx)(mt,{day:e,cols:t,group:n},`sep-${r.ts}`)),o=e)}a.push(n?(0,Z.jsx)(ht,{s:r,group:n},r.ts):(0,Z.jsx)(gt,{s:r,cols:t},r.ts))}return(0,Z.jsxs)(`div`,{className:`lb-rows`,style:n?Xe(n):$(t),children:[a,r]})}function mt({day:e,cols:t,group:n}){let r=n!==null&&n.units.length>1,i=n?n.metrics.map(e=>({key:e.key,head:e.head,unit:e.unit,cls:``})):t.slice(1).map(e=>({key:e.key,head:e.head,unit:e.unit,cls:Ze(e).trim()}));return(0,Z.jsxs)(`div`,{className:`lb-sep${r?` u`:``}`,children:[(0,Z.jsx)(`span`,{className:`sd`,children:e}),r&&(0,Z.jsx)(`span`,{className:`sh`}),i.map(e=>(0,Z.jsxs)(`span`,{className:e.cls?`sh ${e.cls}`:`sh`,children:[e.head,(0,Z.jsx)(rt,{of:e.unit})]},e.key))]})}function ht({s:e,group:t}){let n=t.units.length>1;return(0,Z.jsx)(Z.Fragment,{children:t.units.map((r,i)=>(0,Z.jsxs)(`div`,{className:`lb-row${n?` u`:``}${i>0?` cont`:``}`,children:[(0,Z.jsx)(`span`,{className:`tm`,children:F(e.ts)}),n&&(0,Z.jsx)(`span`,{className:`un`,children:r.head}),t.metrics.map(t=>(0,Z.jsx)(`span`,{className:`v`,children:W(e,r,t)},t.key))]},r.key))})}function gt({s:e,cols:t}){return(0,Z.jsx)(`div`,{className:`lb-row`,children:t.map((t,n)=>(0,Z.jsx)(`span`,{className:n===0?`tm`:`${t.dim?`v dim`:`v`}${Ze(t)}`,children:t.cell(e)},t.key))})}var _t=[{to:`/logbook/bridge`,name:`Bridge`,keeper:`Chief officer`,holds:`Position, course and speed, the weather she ran in and the water under her.`,Icon:h},{to:`/logbook/engine`,name:`Engine`,keeper:`Chief engineer`,holds:`Engines, generators and tanks, as she reported them.`,Icon:y}];function vt(){let e=_();return(0,Z.jsx)(`div`,{className:`lb-door`,children:(0,Z.jsx)(`div`,{className:`lbd-cards`,children:_t.map(({to:t,name:n,keeper:r,holds:i,Icon:a})=>(0,Z.jsxs)(u,{to:e(t),replace:!0,className:`lbd-card`,children:[(0,Z.jsx)(a,{size:42}),(0,Z.jsx)(`span`,{className:`lbd-n`,children:n}),(0,Z.jsx)(`span`,{className:`lbd-k`,children:r}),(0,Z.jsx)(`span`,{className:`lbd-h`,children:i}),(0,Z.jsxs)(`span`,{className:`lbd-go`,children:[`Open `,(0,Z.jsx)(b,{size:15})]})]},t))})})}function yt({book:e}){return e?(0,Z.jsx)(Ie,{book:e}):(0,Z.jsx)(vt,{})}export{yt as default};
@@ -1,4 +1,4 @@
1
- import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./jsx-runtime-CaR_m4Xc.js";import{t as r}from"./visibleInterval-CFxPzgzx.js";import{N as i,a,f as o,m as s,o as c,p as l,s as u}from"./index-BwOZQYHE.js";import{a as d,i as f,n as p,o as m,r as h,s as g,t as _}from"./style-D1uNa2dX.js";var v=e(t(),1),y={fill:`#ffb938`,stroke:`#b38326`,glow:`rgba(255,185,56,0.65)`,dot:`#050706`};function b(e,t=y){let n=t.fill,r=t.stroke;return e===null||Number.isNaN(e)?`
1
+ import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./jsx-runtime-CaR_m4Xc.js";import{t as r}from"./visibleInterval-CFxPzgzx.js";import{D as i,E as a,N as o,b as s,r as c,x as l,y as u}from"./index-jUObqoMX.js";import{a as d,i as f,n as p,o as m,r as h,s as g,t as _}from"./style-D1uNa2dX.js";var v=e(t(),1),y={fill:`#ffb938`,stroke:`#b38326`,glow:`rgba(255,185,56,0.65)`,dot:`#050706`};function b(e,t=y){let n=t.fill,r=t.stroke;return e===null||Number.isNaN(e)?`
2
2
  <div style="
3
3
  width: 14px; height: 14px;
4
4
  background: ${n};
@@ -36,7 +36,7 @@ import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./j
36
36
  stroke-width="1.6"
37
37
  stroke-linejoin="round"
38
38
  style="filter: drop-shadow(0 0 3px ${n}88);"/>
39
- </svg>`}var S={s:`s`,min:`m`,h:`h`,d:`d`};function C(e,t=Date.now()){let{value:n,unit:r}=s((t-e)/1e3);return`${n}${S[r]} ago`}var w=e(g(),1),T=`siparu.aisPrefs`;function E(e,t,n){return Number.isFinite(e)?Math.min(n,Math.max(t,e)):t}function D(){try{let e=localStorage.getItem(T);if(!e)return{maxNm:5,limit:30};let t=JSON.parse(e);return{maxNm:E(Number(t.maxNm),1,20),limit:E(Number(t.limit),5,100)}}catch{return{maxNm:5,limit:30}}}function O(e){try{localStorage.setItem(T,JSON.stringify(e))}catch{}}var ee=[7.4246,43.7384],te=8,ne=1e4,k=3e4,A=5*6e4,j=24*3600*1e3,M=3e4,N=250;function P(e,t,n){return Math.min(n,Math.max(t,Math.round(e)))}function F(){return document.documentElement.dataset.theme===`day`?`day`:`night`}function I(e){return e.length<2?{type:`FeatureCollection`,features:[]}:{type:`FeatureCollection`,features:[{type:`Feature`,properties:{},geometry:{type:`LineString`,coordinates:e.map(e=>[e.lon,e.lat])}}]}}function L(e){let t=i(),n=(0,v.useRef)(null),a=(0,v.useRef)(null),s=(0,v.useRef)(null),c=(0,v.useRef)(null),l=(0,v.useRef)([]),u=(0,v.useRef)(!1),g=(0,v.useRef)(I([])),y=(0,v.useRef)(e);(0,v.useEffect)(()=>{y.current=e});let[b,x]=(0,v.useState)(!0),[S,C]=(0,v.useState)(0),[T,E]=(0,v.useState)(!1),[L,R]=(0,v.useState)(null),[z,B]=(0,v.useState)(null),[V,H]=(0,v.useState)(()=>D().maxNm),[U,W]=(0,v.useState)(()=>D().limit),G=(0,v.useCallback)(e=>{let t=P(e,1,20);H(t),O({maxNm:t,limit:U})},[U]),K=(0,v.useCallback)(e=>{let t=P(e,5,100);W(t),O({maxNm:V,limit:t})},[V]),[q,J]=(0,v.useState)([]),[Y,X]=(0,v.useState)(null),Z=(0,v.useRef)([]);(0,v.useEffect)(()=>{Z.current=q},[q]);let Q=(0,v.useCallback)(()=>{let e=a.current;if(!e)return;let t=Z.current;if(t.length>=2){let n=new w.default.LngLatBounds;for(let e of t)n.extend([e.lon,e.lat]);e.fitBounds(n,{padding:40,maxZoom:14});return}let n=s.current;n&&e.easeTo({center:n.getLngLat(),zoom:Math.max(e.getZoom(),12)})},[]),$=(0,v.useCallback)(e=>{let t=e.getSource(p);t&&t.setData(g.current)},[]);return(0,v.useEffect)(()=>{if(!n.current||a.current)return;let e=n.current,r=!1,i=null,o=null,u;return(async()=>{d();let n=await m(t);if(r)return;let s=h(n);s||B(`Chart unavailable - position and track only`);let c=e=>f(e,n,{track:y.current.track});i=new w.default.Map({container:e,style:c(F()),center:ee,zoom:te,attributionControl:{compact:!1,customAttribution:_},dragRotate:!1,pitchWithRotate:!1}),i.touchZoomRotate.disableRotation(),i.keyboard.disableRotation(),a.current=i,i.addControl(new w.default.NavigationControl({showCompass:!1}),y.current.zoomPosition??`bottom-left`);let l=!1;i.on(`sourcedata`,e=>{let t=!!e.tile;e.sourceId===s&&t&&!l&&(l=!0,B(null))}),u=window.setTimeout(()=>{!l&&s&&B(`Chart tiles unreachable - position and track only`)},ne),i.on(`style.load`,()=>{i&&$(i)}),o=new MutationObserver(()=>{i&&(i.setStyle(c(F())),$(i))}),o.observe(document.documentElement,{attributes:!0,attributeFilter:[`data-theme`]})})(),()=>{r=!0,window.clearTimeout(u),o?.disconnect(),l.current.forEach(e=>e.remove()),l.current=[],s.current=null,c.current=null,i?.remove(),a.current=null}},[$]),(0,v.useEffect)(()=>{let e=!0;return t.health().then(t=>{e&&(R(t.boat_name),E(t.ais?.receiver_seen===!0))}).catch(()=>{}),()=>{e=!1}},[]),(0,v.useEffect)(()=>{if(!b||!T){l.current.forEach(e=>e.remove()),l.current=[],C(0);return}let e=!1,n=async()=>{if(t.ais)try{let n=await t.ais.targets({maxNm:V,limit:U});if(e)return;let r=a.current;if(!r)return;l.current.forEach(e=>e.remove()),l.current=[];for(let e of n.targets){let t=e.cog_deg??e.heading_deg??null,n=document.createElement(`div`);n.className=`sp-ais-marker`,n.innerHTML=y.current.makeAis(t);let i=new w.default.Marker({element:n,anchor:`center`}).setLngLat([e.lon,e.lat]).setPopup(new w.default.Popup({closeButton:!0,offset:12,maxWidth:`280px`}).setHTML(y.current.aisPopupHtml(e))).addTo(r);l.current.push(i)}C(n.targets.length)}catch{}},i=window.setTimeout(n,N),o=r(n,M);return()=>{e=!0,window.clearTimeout(i),o()}},[b,T,V,U]),(0,v.useEffect)(()=>{let e=!1,n=async()=>{try{let n=Date.now(),r=await t.logbook.snapshots({from:n-j,order:`asc`,bucket:1,limit:2e3});if(e)return;let i=r.filter(e=>e.lat!==null&&e.lon!==null).map(e=>({lat:e.lat,lon:e.lon}));J(i)}catch{}};n();let i=r(n,A);return()=>{e=!0,i()}},[]),(0,v.useEffect)(()=>{g.current=I(q);let e=a.current;if(e&&($(e),!u.current&&q.length>=2)){u.current=!0;let t=new w.default.LngLatBounds;for(let e of q)t.extend([e.lon,e.lat]);e.fitBounds(t,{padding:40,maxZoom:14})}},[q,$]),(0,v.useEffect)(()=>{let e=!1,n=async()=>{try{let n=await t.logbook.snapshotLatest();if(e)return;X(n);let r=a.current;if(!r||n.lat===null||n.lon===null)return;let i=o(n.heading_true),l=y.current.makeBoat(i),d=[n.lon,n.lat];if(s.current)s.current.setLngLat(d),s.current.getElement().innerHTML=l,c.current?.setHTML(y.current.popupHtml(n,Date.now()));else{let e=document.createElement(`div`);e.className=`sp-boat-marker`,e.innerHTML=l;let t=new w.default.Popup({closeButton:!0,offset:16,maxWidth:`300px`}).setHTML(y.current.popupHtml(n,Date.now()));c.current=t,s.current=new w.default.Marker({element:e,anchor:`center`}).setLngLat(d).setPopup(t).addTo(r),u.current||r.easeTo({center:d,zoom:Math.max(r.getZoom(),11)})}}catch{}};n();let i=r(n,k);return()=>{e=!0,i()}},[]),{containerRef:n,aisOn:b,setAisOn:x,aisCount:S,aisMaxNm:V,setAisMaxNm:G,aisLimit:U,setAisLimit:K,aisAvailable:T,boatName:L,latest:Y,chartNote:z,recenter:Q}}var R=n();function z({open:e,onClose:t,maxNm:n,setMaxNm:r,limit:i,setLimit:a,variant:o}){let s=(0,v.useRef)(null);return(0,v.useEffect)(()=>{let t=s.current;if(!t||!e)return;let n=e=>e.stopPropagation(),r=[`pointerdown`,`mousedown`,`touchstart`,`dblclick`,`wheel`];for(let e of r)t.addEventListener(e,n);return()=>{for(let e of r)t.removeEventListener(e,n)}},[e]),(0,v.useEffect)(()=>{if(!e)return;let n=e=>{e.key===`Escape`&&t()};return window.addEventListener(`keydown`,n),()=>window.removeEventListener(`keydown`,n)},[e,t]),(0,v.useEffect)(()=>{if(!e)return;let n=e=>{let n=s.current;if(!n||!(e.target instanceof Node)||n.contains(e.target))return;let r=n.parentElement;r&&r.contains(e.target)||t()},r=window.setTimeout(()=>{document.addEventListener(`mousedown`,n),document.addEventListener(`touchstart`,n)},0);return()=>{window.clearTimeout(r),document.removeEventListener(`mousedown`,n),document.removeEventListener(`touchstart`,n)}},[e,t]),e?(0,R.jsxs)(`div`,{ref:s,className:`ais-filter ais-filter-${o}`,role:`dialog`,children:[(0,R.jsxs)(`div`,{className:`aisf-row`,children:[(0,R.jsxs)(`div`,{className:`aisf-label`,children:[(0,R.jsx)(`span`,{children:`Range`}),(0,R.jsxs)(`span`,{className:`aisf-value`,children:[n,` NM`]})]}),(0,R.jsx)(`input`,{type:`range`,min:1,max:20,step:1,value:n,onChange:e=>r(Number(e.target.value))})]}),(0,R.jsxs)(`div`,{className:`aisf-row`,children:[(0,R.jsxs)(`div`,{className:`aisf-label`,children:[(0,R.jsx)(`span`,{children:`Targets`}),(0,R.jsx)(`span`,{className:`aisf-value`,children:i})]}),(0,R.jsx)(`input`,{type:`range`,min:5,max:100,step:5,value:i,onChange:e=>a(Number(e.target.value))})]})]}):null}var B=`bottom-left`,V=`
39
+ </svg>`}var S={s:`s`,min:`m`,h:`h`,d:`d`};function C(e,t=Date.now()){let{value:n,unit:r}=c((t-e)/1e3);return`${n}${S[r]} ago`}var w=e(g(),1),T=`siparu.aisPrefs`;function E(e,t,n){return Number.isFinite(e)?Math.min(n,Math.max(t,e)):t}function D(){try{let e=localStorage.getItem(T);if(!e)return{maxNm:5,limit:30};let t=JSON.parse(e);return{maxNm:E(Number(t.maxNm),1,20),limit:E(Number(t.limit),5,100)}}catch{return{maxNm:5,limit:30}}}function O(e){try{localStorage.setItem(T,JSON.stringify(e))}catch{}}var ee=[7.4246,43.7384],te=8,ne=1e4,k=3e4,A=5*6e4,j=24*3600*1e3,M=3e4,N=250;function P(e,t,n){return Math.min(n,Math.max(t,Math.round(e)))}function F(){return document.documentElement.dataset.theme===`day`?`day`:`night`}function I(e){return e.length<2?{type:`FeatureCollection`,features:[]}:{type:`FeatureCollection`,features:[{type:`Feature`,properties:{},geometry:{type:`LineString`,coordinates:e.map(e=>[e.lon,e.lat])}}]}}function L(e){let t=o(),n=(0,v.useRef)(null),i=(0,v.useRef)(null),s=(0,v.useRef)(null),c=(0,v.useRef)(null),l=(0,v.useRef)([]),u=(0,v.useRef)(!1),g=(0,v.useRef)(I([])),y=(0,v.useRef)(e);(0,v.useEffect)(()=>{y.current=e});let[b,x]=(0,v.useState)(!0),[S,C]=(0,v.useState)(0),[T,E]=(0,v.useState)(!1),[L,R]=(0,v.useState)(null),[z,B]=(0,v.useState)(null),[V,H]=(0,v.useState)(()=>D().maxNm),[U,W]=(0,v.useState)(()=>D().limit),G=(0,v.useCallback)(e=>{let t=P(e,1,20);H(t),O({maxNm:t,limit:U})},[U]),K=(0,v.useCallback)(e=>{let t=P(e,5,100);W(t),O({maxNm:V,limit:t})},[V]),[q,J]=(0,v.useState)([]),[Y,X]=(0,v.useState)(null),Z=(0,v.useRef)([]);(0,v.useEffect)(()=>{Z.current=q},[q]);let Q=(0,v.useCallback)(()=>{let e=i.current;if(!e)return;let t=Z.current;if(t.length>=2){let n=new w.default.LngLatBounds;for(let e of t)n.extend([e.lon,e.lat]);e.fitBounds(n,{padding:40,maxZoom:14});return}let n=s.current;n&&e.easeTo({center:n.getLngLat(),zoom:Math.max(e.getZoom(),12)})},[]),$=(0,v.useCallback)(e=>{let t=e.getSource(p);t&&t.setData(g.current)},[]);return(0,v.useEffect)(()=>{if(!n.current||i.current)return;let e=n.current,r=!1,a=null,o=null,u;return(async()=>{d();let n=await m(t);if(r)return;let s=h(n);s||B(`Chart unavailable - position and track only`);let c=e=>f(e,n,{track:y.current.track});a=new w.default.Map({container:e,style:c(F()),center:ee,zoom:te,attributionControl:{compact:!1,customAttribution:_},dragRotate:!1,pitchWithRotate:!1}),a.touchZoomRotate.disableRotation(),a.keyboard.disableRotation(),i.current=a,a.addControl(new w.default.NavigationControl({showCompass:!1}),y.current.zoomPosition??`bottom-left`);let l=!1;a.on(`sourcedata`,e=>{let t=!!e.tile;e.sourceId===s&&t&&!l&&(l=!0,B(null))}),u=window.setTimeout(()=>{!l&&s&&B(`Chart tiles unreachable - position and track only`)},ne),a.on(`style.load`,()=>{a&&$(a)}),o=new MutationObserver(()=>{a&&(a.setStyle(c(F())),$(a))}),o.observe(document.documentElement,{attributes:!0,attributeFilter:[`data-theme`]})})(),()=>{r=!0,window.clearTimeout(u),o?.disconnect(),l.current.forEach(e=>e.remove()),l.current=[],s.current=null,c.current=null,a?.remove(),i.current=null}},[$]),(0,v.useEffect)(()=>{let e=!0;return t.health().then(t=>{e&&(R(t.boat_name),E(t.ais?.receiver_seen===!0))}).catch(()=>{}),()=>{e=!1}},[]),(0,v.useEffect)(()=>{if(!b||!T){l.current.forEach(e=>e.remove()),l.current=[],C(0);return}let e=!1,n=async()=>{if(t.ais)try{let n=await t.ais.targets({maxNm:V,limit:U});if(e)return;let r=i.current;if(!r)return;l.current.forEach(e=>e.remove()),l.current=[];for(let e of n.targets){let t=e.cog_deg??e.heading_deg??null,n=document.createElement(`div`);n.className=`sp-ais-marker`,n.innerHTML=y.current.makeAis(t);let i=new w.default.Marker({element:n,anchor:`center`}).setLngLat([e.lon,e.lat]).setPopup(new w.default.Popup({closeButton:!0,offset:12,maxWidth:`280px`}).setHTML(y.current.aisPopupHtml(e))).addTo(r);l.current.push(i)}C(n.targets.length)}catch{}},a=window.setTimeout(n,N),o=r(n,M);return()=>{e=!0,window.clearTimeout(a),o()}},[b,T,V,U]),(0,v.useEffect)(()=>{let e=!1,n=async()=>{try{let n=Date.now(),r=await t.logbook.snapshots({from:n-j,order:`asc`,bucket:1,limit:2e3});if(e)return;let i=r.filter(e=>e.lat!==null&&e.lon!==null).map(e=>({lat:e.lat,lon:e.lon}));J(i)}catch{}};n();let i=r(n,A);return()=>{e=!0,i()}},[]),(0,v.useEffect)(()=>{g.current=I(q);let e=i.current;if(e&&($(e),!u.current&&q.length>=2)){u.current=!0;let t=new w.default.LngLatBounds;for(let e of q)t.extend([e.lon,e.lat]);e.fitBounds(t,{padding:40,maxZoom:14})}},[q,$]),(0,v.useEffect)(()=>{let e=!1,n=async()=>{try{let n=await t.logbook.snapshotLatest();if(e)return;X(n);let r=i.current;if(!r||n.lat===null||n.lon===null)return;let o=a(n.heading_true),l=y.current.makeBoat(o),d=[n.lon,n.lat];if(s.current)s.current.setLngLat(d),s.current.getElement().innerHTML=l,c.current?.setHTML(y.current.popupHtml(n,Date.now()));else{let e=document.createElement(`div`);e.className=`sp-boat-marker`,e.innerHTML=l;let t=new w.default.Popup({closeButton:!0,offset:16,maxWidth:`300px`}).setHTML(y.current.popupHtml(n,Date.now()));c.current=t,s.current=new w.default.Marker({element:e,anchor:`center`}).setLngLat(d).setPopup(t).addTo(r),u.current||r.easeTo({center:d,zoom:Math.max(r.getZoom(),11)})}}catch{}};n();let o=r(n,k);return()=>{e=!0,o()}},[]),{containerRef:n,aisOn:b,setAisOn:x,aisCount:S,aisMaxNm:V,setAisMaxNm:G,aisLimit:U,setAisLimit:K,aisAvailable:T,boatName:L,latest:Y,chartNote:z,recenter:Q}}var R=n();function z({open:e,onClose:t,maxNm:n,setMaxNm:r,limit:i,setLimit:a,variant:o}){let s=(0,v.useRef)(null);return(0,v.useEffect)(()=>{let t=s.current;if(!t||!e)return;let n=e=>e.stopPropagation(),r=[`pointerdown`,`mousedown`,`touchstart`,`dblclick`,`wheel`];for(let e of r)t.addEventListener(e,n);return()=>{for(let e of r)t.removeEventListener(e,n)}},[e]),(0,v.useEffect)(()=>{if(!e)return;let n=e=>{e.key===`Escape`&&t()};return window.addEventListener(`keydown`,n),()=>window.removeEventListener(`keydown`,n)},[e,t]),(0,v.useEffect)(()=>{if(!e)return;let n=e=>{let n=s.current;if(!n||!(e.target instanceof Node)||n.contains(e.target))return;let r=n.parentElement;r&&r.contains(e.target)||t()},r=window.setTimeout(()=>{document.addEventListener(`mousedown`,n),document.addEventListener(`touchstart`,n)},0);return()=>{window.clearTimeout(r),document.removeEventListener(`mousedown`,n),document.removeEventListener(`touchstart`,n)}},[e,t]),e?(0,R.jsxs)(`div`,{ref:s,className:`ais-filter ais-filter-${o}`,role:`dialog`,children:[(0,R.jsxs)(`div`,{className:`aisf-row`,children:[(0,R.jsxs)(`div`,{className:`aisf-label`,children:[(0,R.jsx)(`span`,{children:`Range`}),(0,R.jsxs)(`span`,{className:`aisf-value`,children:[n,` NM`]})]}),(0,R.jsx)(`input`,{type:`range`,min:1,max:20,step:1,value:n,onChange:e=>r(Number(e.target.value))})]}),(0,R.jsxs)(`div`,{className:`aisf-row`,children:[(0,R.jsxs)(`div`,{className:`aisf-label`,children:[(0,R.jsx)(`span`,{children:`Targets`}),(0,R.jsx)(`span`,{className:`aisf-value`,children:i})]}),(0,R.jsx)(`input`,{type:`range`,min:5,max:100,step:5,value:i,onChange:e=>a(Number(e.target.value))})]})]}):null}var B=`bottom-left`,V=`
40
40
  .mp .maplibregl-map { background: var(--map-sea); font-family: var(--sp-font); font-size: 11px; outline: none; }
41
41
  .mp .maplibregl-canvas { outline: none; }
42
42
 
@@ -88,15 +88,15 @@ import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./j
88
88
  .mp .ais-filter input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 14px; margin-top: -6px; background: var(--accent); }
89
89
  .mp .ais-filter input[type="range"]::-moz-range-track { height: 2px; background: var(--rule); }
90
90
  .mp .ais-filter input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border: none; background: var(--accent); }
91
- `,H={fill:`#f2f3f5`,stroke:`#e5484d`,glow:`rgba(229,72,77,0.45)`,dot:`#e5484d`},U=`color: var(--muted); font-family: var(--sp-font); font-size: 8.5px; letter-spacing: 0.1em; text-transform: uppercase;`,W=`color: var(--text); font-family: var(--sp-font); font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;`,G=`color: var(--text); font-family: var(--sp-font); font-weight: 600; font-size: 14px; margin-bottom: 8px;`,K=`font-family: var(--sp-font); font-size: 8px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: #fff; background: var(--map-ais-hi); padding: 3px 5px;`,q=e=>e.replace(/[&<>"']/g,e=>({"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`})[e]);function J(e,t,n){let r=l(e.sog),i=r===null?null:o(e.cog),s=o(e.heading_true),d=(e.nav_state??`·`).toUpperCase();return`
91
+ `,H={fill:`#f2f3f5`,stroke:`#e5484d`,glow:`rgba(229,72,77,0.45)`,dot:`#e5484d`},U=`color: var(--muted); font-family: var(--sp-font); font-size: 8.5px; letter-spacing: 0.1em; text-transform: uppercase;`,W=`color: var(--text); font-family: var(--sp-font); font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;`,G=`color: var(--text); font-family: var(--sp-font); font-weight: 600; font-size: 14px; margin-bottom: 8px;`,K=`font-family: var(--sp-font); font-size: 8px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: #fff; background: var(--map-ais-hi); padding: 3px 5px;`,q=e=>e.replace(/[&<>"']/g,e=>({"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`})[e]);function J(e,t,n){let r=i(e.sog),o=r===null?null:a(e.cog),c=a(e.heading_true),d=(e.nav_state??`·`).toUpperCase();return`
92
92
  <div style="${G}">${q(n)}</div>
93
93
  <div style="display:grid;grid-template-columns:auto 1fr;column-gap:14px;row-gap:4px;align-items:baseline;">
94
- <span style="${U}">POS</span><span style="${W}">${a(e.lat,[`N`,`S`],2)} · ${a(e.lon,[`E`,`W`],3)}</span>
95
- <span style="${U}">SOG</span><span style="${W}">${c(r,1,` kt`)}</span>
96
- <span style="${U}">COG</span><span style="${W}">${i===null?`·`:c(i,0,`°`)}</span>
97
- <span style="${U}">HDG</span><span style="${W}">${s===null?`·`:c(s,0,`°`)}</span>
94
+ <span style="${U}">POS</span><span style="${W}">${u(e.lat,[`N`,`S`],2)} · ${u(e.lon,[`E`,`W`],3)}</span>
95
+ <span style="${U}">SOG</span><span style="${W}">${s(r,1,` kt`)}</span>
96
+ <span style="${U}">COG</span><span style="${W}">${o===null?`·`:s(o,0,`°`)}</span>
97
+ <span style="${U}">HDG</span><span style="${W}">${c===null?`·`:s(c,0,`°`)}</span>
98
98
  <span style="${U}">NAV</span><span style="${W}">${q(d)}</span>
99
- <span style="${U}">UPD</span><span style="${W}">${C(e.ts,t)} <span style="color:var(--muted)">(${u(e.ts).slice(11,19)})</span></span>
99
+ <span style="${U}">UPD</span><span style="${W}">${C(e.ts,t)} <span style="color:var(--muted)">(${l(e.ts).slice(11,19)})</span></span>
100
100
  </div>
101
101
  `}function Y(e){let t=e.name||`MMSI ${e.mmsi}`,n=e.sog_kn===null?`·`:`${e.sog_kn.toFixed(1)} kt`,r=e.cog_deg===null?`·`:`${String(Math.round(e.cog_deg)).padStart(3,`0`)}°`,i=e.distance_nm===null?`·`:`${e.distance_nm.toFixed(1)} NM`,a=e.length_m===null?`·`:`${Math.round(e.length_m)} m`,o=[e.ship_type,e.ais_class?`Class ${e.ais_class}`:null].filter(Boolean).join(` · `)||`·`;return`
102
102
  <div style="${G}; display:flex; justify-content:space-between; gap:10px; align-items:center;">${q(t)} <span style="${K}">AIS</span></div>
@@ -108,4 +108,4 @@ import{a as e}from"./rolldown-runtime-CNC7AqOf.js";import{n as t,t as n}from"./j
108
108
  <span style="${U}">DIST</span><span style="${W}">${i}</span>
109
109
  <span style="${U}">NAV</span><span style="${W}">${q((e.nav_state??`·`).toUpperCase())}</span>
110
110
  </div>
111
- `}function X(e){return e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()}function Z(){let{containerRef:e,aisOn:t,setAisOn:n,aisCount:r,aisMaxNm:i,setAisMaxNm:s,aisLimit:c,setAisLimit:u,latest:d,chartNote:f,aisAvailable:p,boatName:m}=L({track:{color:`#c23a3f`,width:1.8,dash:[2.8,2.2]},zoomPosition:B,popupHtml:(e,t)=>J(e,t,m??`This vessel`),aisPopupHtml:Y,makeBoat:e=>b(e,H),makeAis:e=>x(e,`#7a8a86`)}),[h,g]=(0,v.useState)(!1),_=d?o(d.heading_true):null,y=d?l(d.sog):null,S=d?.nav_state?X(d.nav_state):null,C=d?.lat!=null&&d?.lon!=null,w=C?`${a(d.lat,[`N`,`S`],2)} ${a(d.lon,[`E`,`W`],3)}`:`·`,T=[d&&!C?`Awaiting fix - the boat is not on this chart yet`:null,f].filter(e=>e!==null);return(0,R.jsxs)(`div`,{className:`mp`,children:[(0,R.jsx)(`style`,{children:V}),(0,R.jsx)(`div`,{ref:e,className:`mp-canvas`}),(0,R.jsxs)(`div`,{className:`mp-strip`,children:[(0,R.jsx)(`span`,{className:`co`,children:w}),(0,R.jsxs)(`span`,{className:`vec`,children:[(0,R.jsx)(`b`,{children:_===null?`·`:`${Math.round(_)}°`}),` · `,(0,R.jsx)(`b`,{children:y===null?`·`:y.toFixed(1)}),`kn`]}),S&&(0,R.jsx)(`span`,{className:`navp`,children:S})]}),T.length>0&&(0,R.jsx)(`div`,{className:`mp-notes`,children:T.map(e=>(0,R.jsx)(`div`,{className:`mp-chartnote`,children:e},e))}),p&&(0,R.jsxs)(`div`,{className:`mp-ctrl`,children:[(0,R.jsx)(z,{open:h,onClose:()=>g(!1),maxNm:i,setMaxNm:s,limit:c,setLimit:u,variant:`marine`}),(0,R.jsxs)(`button`,{className:`mp-btn${t?` on`:``}`,onClick:()=>n(!t),children:[`AIS`,t&&(0,R.jsx)(`span`,{className:`n`,children:r})]}),(0,R.jsxs)(`button`,{className:`mp-btn`,onClick:()=>g(e=>!e),children:[`Range · `,(0,R.jsx)(`span`,{className:`n`,children:i}),` Nm`]})]})]})}function Q(){return(0,R.jsx)(Z,{})}export{Q as default};
111
+ `}function X(e){return e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()}function Z(){let{containerRef:e,aisOn:t,setAisOn:n,aisCount:r,aisMaxNm:o,setAisMaxNm:s,aisLimit:c,setAisLimit:l,latest:d,chartNote:f,aisAvailable:p,boatName:m}=L({track:{color:`#c23a3f`,width:1.8,dash:[2.8,2.2]},zoomPosition:B,popupHtml:(e,t)=>J(e,t,m??`This vessel`),aisPopupHtml:Y,makeBoat:e=>b(e,H),makeAis:e=>x(e,`#7a8a86`)}),[h,g]=(0,v.useState)(!1),_=d?a(d.heading_true):null,y=d?i(d.sog):null,S=d?.nav_state?X(d.nav_state):null,C=d?.lat!=null&&d?.lon!=null,w=C?`${u(d.lat,[`N`,`S`],2)} ${u(d.lon,[`E`,`W`],3)}`:`·`,T=[d&&!C?`Awaiting fix - the boat is not on this chart yet`:null,f].filter(e=>e!==null);return(0,R.jsxs)(`div`,{className:`mp`,children:[(0,R.jsx)(`style`,{children:V}),(0,R.jsx)(`div`,{ref:e,className:`mp-canvas`}),(0,R.jsxs)(`div`,{className:`mp-strip`,children:[(0,R.jsx)(`span`,{className:`co`,children:w}),(0,R.jsxs)(`span`,{className:`vec`,children:[(0,R.jsx)(`b`,{children:_===null?`·`:`${Math.round(_)}°`}),` · `,(0,R.jsx)(`b`,{children:y===null?`·`:y.toFixed(1)}),`kn`]}),S&&(0,R.jsx)(`span`,{className:`navp`,children:S})]}),T.length>0&&(0,R.jsx)(`div`,{className:`mp-notes`,children:T.map(e=>(0,R.jsx)(`div`,{className:`mp-chartnote`,children:e},e))}),p&&(0,R.jsxs)(`div`,{className:`mp-ctrl`,children:[(0,R.jsx)(z,{open:h,onClose:()=>g(!1),maxNm:o,setMaxNm:s,limit:c,setLimit:l,variant:`marine`}),(0,R.jsxs)(`button`,{className:`mp-btn${t?` on`:``}`,onClick:()=>n(!t),children:[`AIS`,t&&(0,R.jsx)(`span`,{className:`n`,children:r})]}),(0,R.jsxs)(`button`,{className:`mp-btn`,onClick:()=>g(e=>!e),children:[`Range · `,(0,R.jsx)(`span`,{className:`n`,children:o}),` Nm`]})]})]})}function Q(){return(0,R.jsx)(Z,{})}export{Q as default};