signalk-siparu 0.2.9 → 0.2.11

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.
Files changed (41) hide show
  1. package/NOTICE +42 -0
  2. package/README.md +4 -3
  3. package/package.json +3 -2
  4. package/plugin/dist/aggregate.d.ts +27 -0
  5. package/plugin/dist/aggregate.js +94 -0
  6. package/plugin/dist/ais.d.ts +1 -0
  7. package/plugin/dist/ais.js +7 -0
  8. package/plugin/dist/aisreceiver.d.ts +17 -0
  9. package/plugin/dist/aisreceiver.js +95 -0
  10. package/plugin/dist/config.js +2 -1
  11. package/plugin/dist/contract.d.ts +126 -0
  12. package/plugin/dist/contract.js +29 -0
  13. package/plugin/dist/index.js +31 -6
  14. package/plugin/dist/live.d.ts +11 -2
  15. package/plugin/dist/live.js +102 -10
  16. package/plugin/dist/query.d.ts +3 -1
  17. package/plugin/dist/query.js +43 -48
  18. package/plugin/dist/rollup.d.ts +3 -1
  19. package/plugin/dist/rollup.js +21 -130
  20. package/plugin/dist/time.d.ts +2 -0
  21. package/plugin/dist/time.js +8 -0
  22. package/plugin/dist/units.d.ts +1 -0
  23. package/plugin/dist/units.js +26 -1
  24. package/public/assets/Logbook-DpHBKbVS.js +1 -0
  25. package/public/assets/Map-D84VPs-d.js +111 -0
  26. package/public/assets/Remote-B66DSZVi.js +1 -0
  27. package/public/assets/SecurityHelp-D_IuBUYm.js +1 -0
  28. package/public/assets/Voyage-DBV-C78R.js +1 -0
  29. package/public/assets/export-BS6h3d6E.js +9 -0
  30. package/public/assets/index-BwOZQYHE.js +20 -0
  31. package/public/assets/index-QctSvxcL.css +1 -0
  32. package/public/assets/jsx-runtime-CaR_m4Xc.js +1 -0
  33. package/public/assets/style-D1uNa2dX.js +762 -0
  34. package/public/index.html +3 -2
  35. package/public/assets/Logbook-VNvBbXql.js +0 -1
  36. package/public/assets/Map-Bdfx5YSk.js +0 -105
  37. package/public/assets/Voyage-CrdZ-Mdn.js +0 -5
  38. package/public/assets/index-Bws3NPSA.css +0 -1
  39. package/public/assets/index-C_sXr_1m.js +0 -21
  40. package/public/assets/style-B2k4QVOw.css +0 -1
  41. package/public/assets/style-DroXqNI8.js +0 -762
package/NOTICE ADDED
@@ -0,0 +1,42 @@
1
+ Siparu
2
+ Copyright (c) 2026 Siparu
3
+
4
+ This product is licensed under the Apache License, Version 2.0 (see LICENSE).
5
+
6
+ It includes third-party components under their own licenses:
7
+
8
+ -------------------------------------------------------------------------------
9
+ vector-seamarks - https://github.com/josxha/vector-seamarks
10
+ Copyright (c) 2024, Joscha Eckert (josxha)
11
+ BSD 2-Clause License
12
+
13
+ Redistribution and use in source and binary forms, with or without
14
+ modification, are permitted provided that the following conditions are met:
15
+
16
+ 1. Redistributions of source code must retain the above copyright notice,
17
+ this list of conditions and the following disclaimer.
18
+ 2. Redistributions in binary form must reproduce the above copyright notice,
19
+ this list of conditions and the following disclaimer in the documentation
20
+ and/or other materials provided with the distribution.
21
+
22
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
26
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
+ POSSIBILITY OF SUCH DAMAGE.
33
+
34
+ Adapted style layers in webapp/src/map/seamarks-layers.json; seamark sprite
35
+ sheets are served from the tile host.
36
+
37
+ -------------------------------------------------------------------------------
38
+ Map data and assets served from the tile host (not bundled in this package):
39
+
40
+ OpenStreetMap contributors - ODbL 1.0 - https://www.openstreetmap.org/copyright
41
+ Protomaps basemaps - BSD-3-Clause - https://github.com/protomaps/basemaps
42
+ Noto Sans glyphs - SIL OFL 1.1 - https://github.com/protomaps/basemaps-assets
package/README.md CHANGED
@@ -138,9 +138,10 @@ code off the helm. A caller with no browser headers, curl or a script on her own
138
138
  is not affected. Neither is the reading surface above.
139
139
 
140
140
  The two voyage edits are reachable from the boat's own network only. What the
141
- shore can ask her is a closed set of five questions, all of them reads
142
- (`history`, `snapshots`, `voyages`, `track`, `phases`); nothing on that socket
143
- changes anything aboard, and the REST surface above is not proxied to it.
141
+ shore can ask her is a closed set of ten questions, all of them reads
142
+ (`history`, `snapshots`, `voyages`, `track`, `phases`, `rollups`, `stats`,
143
+ `fuel`, `ais`, `health`); nothing on that socket changes anything aboard, and
144
+ the REST surface above is not proxied to it.
144
145
 
145
146
  ### The uplink, when she is paired
146
147
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-siparu",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",
@@ -9,7 +9,8 @@
9
9
  "main": "plugin/dist/index.js",
10
10
  "files": [
11
11
  "plugin/dist",
12
- "public"
12
+ "public",
13
+ "NOTICE"
13
14
  ],
14
15
  "signalk-plugin-enabled-by-default": true,
15
16
  "signalk": {
@@ -0,0 +1,27 @@
1
+ import { MetricAgg, MetricField, RollupHour } from './contract';
2
+ export declare const SPEED_GUARD_KN = 80;
3
+ export interface NumAgg extends MetricAgg {
4
+ min: number;
5
+ max: number;
6
+ avg: number;
7
+ n: number;
8
+ }
9
+ export declare function haversineNm(lat1: number, lon1: number, lat2: number, lon2: number): number;
10
+ export declare function mergeAggs(aggs: readonly MetricAgg[]): NumAgg | null;
11
+ export interface WindowAgg {
12
+ count: number;
13
+ first_ts: number;
14
+ last_ts: number;
15
+ distance_nm: number;
16
+ pos_first: {
17
+ lat: number;
18
+ lon: number;
19
+ } | null;
20
+ pos_last: {
21
+ lat: number;
22
+ lon: number;
23
+ } | null;
24
+ metrics: Partial<Record<MetricField, MetricAgg>>;
25
+ path_metrics?: Record<string, MetricAgg>;
26
+ }
27
+ export declare function mergeHours(hours: readonly RollupHour[], guardKn?: number): WindowAgg;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SPEED_GUARD_KN = void 0;
4
+ exports.haversineNm = haversineNm;
5
+ exports.mergeAggs = mergeAggs;
6
+ exports.mergeHours = mergeHours;
7
+ const contract_1 = require("./contract");
8
+ const EARTH_RADIUS_NM = 3440.065;
9
+ exports.SPEED_GUARD_KN = 80;
10
+ const LINEAR_FIELDS = (0, contract_1.metricFields)('linear');
11
+ const LAST_ONLY_FIELDS = (0, contract_1.metricFields)('angular', 'text');
12
+ function haversineNm(lat1, lon1, lat2, lon2) {
13
+ const toRad = (d) => (d * Math.PI) / 180;
14
+ const dLat = toRad(lat2 - lat1);
15
+ const dLon = toRad(lon2 - lon1);
16
+ const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
17
+ return 2 * EARTH_RADIUS_NM * Math.asin(Math.min(1, Math.sqrt(a)));
18
+ }
19
+ function mergeAggs(aggs) {
20
+ let min = Infinity;
21
+ let max = -Infinity;
22
+ let sum = 0;
23
+ let n = 0;
24
+ let last = null;
25
+ for (const m of aggs) {
26
+ if (typeof m.min !== 'number' || typeof m.max !== 'number' || typeof m.avg !== 'number')
27
+ continue;
28
+ const mn = m.n ?? 1;
29
+ if (m.min < min)
30
+ min = m.min;
31
+ if (m.max > max)
32
+ max = m.max;
33
+ sum += m.avg * mn;
34
+ n += mn;
35
+ if (m.last !== null && m.last !== undefined)
36
+ last = m.last;
37
+ }
38
+ return n > 0 ? { min, max, avg: sum / n, n, last } : null;
39
+ }
40
+ function mergeHours(hours, guardKn = exports.SPEED_GUARD_KN) {
41
+ const sorted = [...hours].sort((a, b) => a.hour.localeCompare(b.hour));
42
+ const metrics = {};
43
+ for (const field of LINEAR_FIELDS) {
44
+ const agg = mergeAggs(sorted.map((h) => h.metrics[field]).filter((m) => m !== undefined));
45
+ if (agg)
46
+ metrics[field] = agg;
47
+ }
48
+ for (const field of LAST_ONLY_FIELDS) {
49
+ for (let i = sorted.length - 1; i >= 0; i--) {
50
+ const m = sorted[i]?.metrics[field];
51
+ if (m && m.last !== null && m.last !== undefined) {
52
+ metrics[field] = { last: m.last };
53
+ break;
54
+ }
55
+ }
56
+ }
57
+ const path_metrics = {};
58
+ const keys = new Set();
59
+ for (const h of sorted)
60
+ if (h.path_metrics)
61
+ for (const k of Object.keys(h.path_metrics))
62
+ keys.add(k);
63
+ for (const key of keys) {
64
+ const agg = mergeAggs(sorted.map((h) => h.path_metrics?.[key]).filter((m) => m !== undefined));
65
+ if (agg)
66
+ path_metrics[key] = agg;
67
+ }
68
+ let distance = sorted.reduce((acc, h) => acc + h.distance_nm, 0);
69
+ for (let i = 0; i < sorted.length - 1; i++) {
70
+ const a = sorted[i];
71
+ const b = sorted[i + 1];
72
+ if (!a?.pos_last || !b?.pos_first)
73
+ continue;
74
+ const dtH = (b.first_ts - a.last_ts) / 3_600_000;
75
+ if (dtH <= 0)
76
+ continue;
77
+ const segNm = haversineNm(a.pos_last.lat, a.pos_last.lon, b.pos_first.lat, b.pos_first.lon);
78
+ if (segNm / dtH <= guardKn)
79
+ distance += segNm;
80
+ }
81
+ return {
82
+ count: sorted.reduce((acc, h) => acc + h.count, 0),
83
+ first_ts: sorted[0]?.first_ts ?? 0,
84
+ last_ts: sorted[sorted.length - 1]?.last_ts ?? 0,
85
+ distance_nm: distance,
86
+ pos_first: sorted.map((h) => h.pos_first).find((p) => p !== null) ?? null,
87
+ pos_last: [...sorted]
88
+ .reverse()
89
+ .map((h) => h.pos_last)
90
+ .find((p) => p !== null) ?? null,
91
+ metrics,
92
+ ...(Object.keys(path_metrics).length > 0 ? { path_metrics } : {})
93
+ };
94
+ }
@@ -6,3 +6,4 @@ export interface AisQuery {
6
6
  }
7
7
  export declare function buildAisFeed(vessels: unknown, selfContext: string, now: number, q: AisQuery): AisFeed;
8
8
  export declare function clampAisQuery(maxNm?: number, maxAgeMin?: number, limit?: number): AisQuery;
9
+ export declare function seesOtherVessels(vessels: unknown, selfContext: string): boolean;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildAisFeed = buildAisFeed;
4
4
  exports.clampAisQuery = clampAisQuery;
5
+ exports.seesOtherVessels = seesOtherVessels;
5
6
  const rollup_1 = require("./rollup");
6
7
  const MS_TO_KN = 1.9438;
7
8
  function nodeValue(node) {
@@ -111,3 +112,9 @@ function clampAisQuery(maxNm, maxAgeMin, limit) {
111
112
  limit: clamp(limit, 30, 1, 400)
112
113
  };
113
114
  }
115
+ function seesOtherVessels(vessels, selfContext) {
116
+ if (!vessels || typeof vessels !== 'object')
117
+ return false;
118
+ const selfKey = selfContext.replace(/^vessels\./, '').trim();
119
+ return Object.keys(vessels).some((k) => k !== selfKey);
120
+ }
@@ -0,0 +1,17 @@
1
+ export interface AisReceiverStatus {
2
+ receiver_seen: boolean;
3
+ first_seen_ts: number | null;
4
+ }
5
+ export declare class AisReceiverMemory {
6
+ private readonly file;
7
+ private seen;
8
+ private written;
9
+ private firstSeenTs;
10
+ private writeChain;
11
+ constructor(dataDir: string);
12
+ load(): void;
13
+ note(now: number): void;
14
+ status(): AisReceiverStatus;
15
+ flush(): Promise<void>;
16
+ private write;
17
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AisReceiverMemory = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const FILE_VERSION = 1;
40
+ class AisReceiverMemory {
41
+ file;
42
+ seen = false;
43
+ written = false;
44
+ firstSeenTs = null;
45
+ writeChain = Promise.resolve();
46
+ constructor(dataDir) {
47
+ this.file = path.join(dataDir, 'ais.json');
48
+ }
49
+ load() {
50
+ let text;
51
+ try {
52
+ text = fs.readFileSync(this.file, 'utf8');
53
+ }
54
+ catch {
55
+ return;
56
+ }
57
+ this.seen = true;
58
+ this.written = true;
59
+ try {
60
+ const raw = JSON.parse(text);
61
+ if (raw.v === FILE_VERSION && typeof raw.first_seen_ts === 'number') {
62
+ this.firstSeenTs = raw.first_seen_ts;
63
+ }
64
+ }
65
+ catch {
66
+ }
67
+ }
68
+ note(now) {
69
+ if (this.written)
70
+ return;
71
+ this.written = true;
72
+ if (!this.seen) {
73
+ this.seen = true;
74
+ this.firstSeenTs = now;
75
+ }
76
+ const shape = { v: FILE_VERSION, first_seen_ts: this.firstSeenTs ?? now };
77
+ this.write(async () => {
78
+ const tmp = `${this.file}.tmp`;
79
+ await fs.promises.writeFile(tmp, JSON.stringify(shape, null, 2), 'utf8');
80
+ await fs.promises.rename(tmp, this.file);
81
+ });
82
+ }
83
+ status() {
84
+ return { receiver_seen: this.seen, first_seen_ts: this.firstSeenTs };
85
+ }
86
+ flush() {
87
+ return this.writeChain.catch(() => undefined);
88
+ }
89
+ write(run) {
90
+ this.writeChain = this.writeChain.then(run, run).catch(() => {
91
+ this.written = false;
92
+ });
93
+ }
94
+ }
95
+ exports.AisReceiverMemory = AisReceiverMemory;
@@ -4,6 +4,7 @@ exports.CONFIG_SCHEMA = exports.RELAY_URL = exports.KN_TO_MS = exports.INTERNAL
4
4
  exports.isCalendarMonthDay = isCalendarMonthDay;
5
5
  exports.safeRelayUrl = safeRelayUrl;
6
6
  exports.resolveOptions = resolveOptions;
7
+ const aggregate_1 = require("./aggregate");
7
8
  exports.DEFAULTS = {
8
9
  boatName: '',
9
10
  snapshotSeconds: 60,
@@ -29,7 +30,7 @@ exports.INTERNAL = {
29
30
  samplePeriodMs: 2000,
30
31
  staleMs: 30_000,
31
32
  fabricationHorizonMs: 120_000,
32
- rollupSpeedGuardKn: 80,
33
+ rollupSpeedGuardKn: aggregate_1.SPEED_GUARD_KN,
33
34
  degradedAfterMs: 5 * 60_000
34
35
  };
35
36
  exports.KN_TO_MS = 0.514444;
@@ -26,10 +26,37 @@ export interface Snapshot {
26
26
  path_values?: Record<string, number>;
27
27
  }
28
28
  export type MetricField = Exclude<keyof Snapshot, 'ts' | 'path_values'>;
29
+ export type MetricKind = 'position' | 'linear' | 'angular' | 'text';
30
+ export declare const METRIC_KIND: {
31
+ readonly lat: "position";
32
+ readonly lon: "position";
33
+ readonly sog: "linear";
34
+ readonly rate_of_turn: "linear";
35
+ readonly magnetic_variation: "linear";
36
+ readonly magnetic_deviation: "linear";
37
+ readonly wind_speed_apparent: "linear";
38
+ readonly wind_speed_true: "linear";
39
+ readonly wind_gust: "linear";
40
+ readonly air_temp_k: "linear";
41
+ readonly air_pressure_pa: "linear";
42
+ readonly depth: "linear";
43
+ readonly water_temp_k: "linear";
44
+ readonly gps_satellites: "linear";
45
+ readonly cog: "angular";
46
+ readonly heading_mag: "angular";
47
+ readonly heading_true: "angular";
48
+ readonly wind_angle_apparent: "angular";
49
+ readonly wind_direction_true: "angular";
50
+ readonly nav_state: "text";
51
+ readonly ais_class: "text";
52
+ readonly depth_datum: "text";
53
+ };
54
+ export declare function metricFields(...kinds: MetricKind[]): MetricField[];
29
55
  export interface MetricAgg {
30
56
  min?: number;
31
57
  max?: number;
32
58
  avg?: number;
59
+ n?: number;
33
60
  last: number | string | null;
34
61
  }
35
62
  export interface RollupHour {
@@ -77,6 +104,7 @@ export interface SnapshotsQuery {
77
104
  export interface SnapshotsResult {
78
105
  rows: Snapshot[];
79
106
  clamped: boolean;
107
+ minutesFrom?: number;
80
108
  }
81
109
  export interface PathSeriesPoint {
82
110
  ts: number;
@@ -89,6 +117,7 @@ export interface PathSeriesResult {
89
117
  path: string;
90
118
  points: PathSeriesPoint[];
91
119
  clamped: boolean;
120
+ minutesFrom?: number;
92
121
  }
93
122
  export interface HistoryRequest {
94
123
  type: 'history';
@@ -191,6 +220,8 @@ export interface InventoryResult {
191
220
  }
192
221
  export interface HealthResult {
193
222
  status: 'ok' | 'degraded';
223
+ security_off?: boolean;
224
+ pairing_locked?: boolean;
194
225
  now: number;
195
226
  started_at: number;
196
227
  version: string;
@@ -218,6 +249,10 @@ export interface HealthResult {
218
249
  last_hour: string | null;
219
250
  hours_pending: number;
220
251
  };
252
+ ais: {
253
+ receiver_seen: boolean;
254
+ first_seen_ts: number | null;
255
+ };
221
256
  sealing: {
222
257
  devices: number;
223
258
  mode: 'sealed' | 'blocked' | 'none';
@@ -347,3 +382,94 @@ export interface DevicePublicKey {
347
382
  pub: string;
348
383
  approvals?: DeviceApproval[];
349
384
  }
385
+ export interface RollupsRequest {
386
+ type: 'rollups';
387
+ id: string;
388
+ from: number;
389
+ to: number;
390
+ }
391
+ export interface RollupsResult {
392
+ rows: RollupHour[];
393
+ }
394
+ export type RollupsResponse = {
395
+ type: 'rollups';
396
+ id: string;
397
+ result: RollupsResult;
398
+ } | {
399
+ type: 'rollups';
400
+ id: string;
401
+ error: {
402
+ code: string;
403
+ message: string;
404
+ };
405
+ };
406
+ export interface StatsRequest {
407
+ type: 'stats';
408
+ id: string;
409
+ }
410
+ export type StatsResponse = {
411
+ type: 'stats';
412
+ id: string;
413
+ result: VoyageStatsResult;
414
+ } | {
415
+ type: 'stats';
416
+ id: string;
417
+ error: {
418
+ code: string;
419
+ message: string;
420
+ };
421
+ };
422
+ export interface FuelPathsView {
423
+ available: string[];
424
+ selected: string[];
425
+ }
426
+ export interface FuelRequest {
427
+ type: 'fuel';
428
+ id: string;
429
+ }
430
+ export type FuelResponse = {
431
+ type: 'fuel';
432
+ id: string;
433
+ result: FuelPathsView;
434
+ } | {
435
+ type: 'fuel';
436
+ id: string;
437
+ error: {
438
+ code: string;
439
+ message: string;
440
+ };
441
+ };
442
+ export interface AisRequest {
443
+ type: 'ais';
444
+ id: string;
445
+ maxNm?: number;
446
+ limit?: number;
447
+ }
448
+ export type AisResponse = {
449
+ type: 'ais';
450
+ id: string;
451
+ result: AisFeed;
452
+ } | {
453
+ type: 'ais';
454
+ id: string;
455
+ error: {
456
+ code: string;
457
+ message: string;
458
+ };
459
+ };
460
+ export interface HealthRequest {
461
+ type: 'health';
462
+ id: string;
463
+ }
464
+ export type HealthResponse = {
465
+ type: 'health';
466
+ id: string;
467
+ result: HealthResult;
468
+ } | {
469
+ type: 'health';
470
+ id: string;
471
+ error: {
472
+ code: string;
473
+ message: string;
474
+ };
475
+ };
@@ -1,2 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.METRIC_KIND = void 0;
4
+ exports.metricFields = metricFields;
5
+ exports.METRIC_KIND = {
6
+ lat: 'position',
7
+ lon: 'position',
8
+ sog: 'linear',
9
+ rate_of_turn: 'linear',
10
+ magnetic_variation: 'linear',
11
+ magnetic_deviation: 'linear',
12
+ wind_speed_apparent: 'linear',
13
+ wind_speed_true: 'linear',
14
+ wind_gust: 'linear',
15
+ air_temp_k: 'linear',
16
+ air_pressure_pa: 'linear',
17
+ depth: 'linear',
18
+ water_temp_k: 'linear',
19
+ gps_satellites: 'linear',
20
+ cog: 'angular',
21
+ heading_mag: 'angular',
22
+ heading_true: 'angular',
23
+ wind_angle_apparent: 'angular',
24
+ wind_direction_true: 'angular',
25
+ nav_state: 'text',
26
+ ais_class: 'text',
27
+ depth_datum: 'text'
28
+ };
29
+ function metricFields(...kinds) {
30
+ return Object.keys(exports.METRIC_KIND).filter((f) => kinds.includes(exports.METRIC_KIND[f]));
31
+ }
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  const fs = __importStar(require("node:fs"));
36
36
  const path = __importStar(require("node:path"));
37
37
  const ais_1 = require("./ais");
38
+ const aisreceiver_1 = require("./aisreceiver");
38
39
  const charts_1 = require("./charts");
39
40
  const config_1 = require("./config");
40
41
  const fingerprint_1 = require("./fingerprint");
@@ -84,6 +85,7 @@ module.exports = (app) => {
84
85
  let boatKeyStore = null;
85
86
  let sealingLatch = null;
86
87
  let anchorStore = null;
88
+ let aisMemory = null;
87
89
  let screensNow = null;
88
90
  let reloadAnchor = null;
89
91
  let timer = null;
@@ -183,6 +185,14 @@ module.exports = (app) => {
183
185
  }
184
186
  return s;
185
187
  }
188
+ function fuelPathsView() {
189
+ return {
190
+ available: state
191
+ ? Object.keys(state.dynamicPaths(Date.now())).filter((p) => p.startsWith('propulsion.') && p.endsWith('.fuel.rate'))
192
+ : [],
193
+ selected: opts?.fuelRatePaths ?? []
194
+ };
195
+ }
186
196
  async function health() {
187
197
  if (!state || !store || !rollups || !query)
188
198
  throw new Error('not started');
@@ -195,6 +205,8 @@ module.exports = (app) => {
195
205
  started_at: startedAt,
196
206
  version: packageVersion(),
197
207
  boat_name: opts.boatName || app.getSelfPath('name') || null,
208
+ security_off: (0, pairing_1.securityOff)(app, {}),
209
+ pairing_locked: (0, pairing_1.securityOff)(app, {}) && !(opts?.acceptOpenNetwork ?? false),
198
210
  last_delta_ts: state.lastDeltaTs,
199
211
  last_snapshot_ts: lastSnapshotTs,
200
212
  snapshots_today: snapshotsToday,
@@ -207,6 +219,7 @@ module.exports = (app) => {
207
219
  oldest_raw: usage.oldest
208
220
  },
209
221
  rollup: await rollups.status(now),
222
+ ais: aisMemory?.status() ?? { receiver_seen: false, first_seen_ts: null },
210
223
  sealing: (() => {
211
224
  const view = screensNow?.();
212
225
  const held = sealingState();
@@ -320,8 +333,17 @@ module.exports = (app) => {
320
333
  }
321
334
  }
322
335
  });
336
+ const ais = new aisreceiver_1.AisReceiverMemory(app.getDataDirPath());
337
+ aisMemory = ais;
338
+ ais.load();
339
+ const noteAisTraffic = () => {
340
+ if ((0, ais_1.seesOtherVessels)(app.getPath('vessels'), app.selfContext))
341
+ ais.note(Date.now());
342
+ };
343
+ noteAisTraffic();
323
344
  timer = setInterval(() => {
324
345
  void writeSnapshot();
346
+ noteAisTraffic();
325
347
  }, opts.snapshotSeconds * 1000);
326
348
  (0, rest_1.setRestDeps)({
327
349
  live,
@@ -391,6 +413,11 @@ module.exports = (app) => {
391
413
  onPhasesQuery: async (limit) => ({
392
414
  phases: pl.list(Math.min(Math.max(1, limit || 50), 500))
393
415
  }),
416
+ onRollupsQuery: async (from, to) => ({ rows: await ru.readHourly(from, to) }),
417
+ onStatsQuery: () => vl.stats(ru, Date.now()),
418
+ onFuelQuery: async () => fuelPathsView(),
419
+ onAisQuery: async (maxNm, limit) => (0, ais_1.buildAisFeed)(app.getPath('vessels'), app.selfContext, Date.now(), (0, ais_1.clampAisQuery)(maxNm, undefined, limit)),
420
+ onHealthQuery: () => health(),
394
421
  seal: (frame) => seal.seal(frame),
395
422
  onWatching: () => ks.prompt(),
396
423
  openAsk: (envelope) => seal.open(envelope),
@@ -453,6 +480,8 @@ module.exports = (app) => {
453
480
  await sealingLatch.flush();
454
481
  if (anchorStore)
455
482
  await anchorStore.flush();
483
+ if (aisMemory)
484
+ await aisMemory.flush();
456
485
  state = null;
457
486
  store = null;
458
487
  rollups = null;
@@ -466,6 +495,7 @@ module.exports = (app) => {
466
495
  boatKeyStore = null;
467
496
  sealingLatch = null;
468
497
  anchorStore = null;
498
+ aisMemory = null;
469
499
  screensNow = null;
470
500
  reloadAnchor = null;
471
501
  app.setPluginStatus('Stopped');
@@ -498,12 +528,7 @@ module.exports = (app) => {
498
528
  app,
499
529
  acceptOpenNetwork: () => opts?.acceptOpenNetwork ?? false,
500
530
  getConfig: () => currentConfig,
501
- fuelPathsView: () => ({
502
- available: state
503
- ? Object.keys(state.dynamicPaths(Date.now())).filter((p) => p.startsWith('propulsion.') && p.endsWith('.fuel.rate'))
504
- : [],
505
- selected: opts?.fuelRatePaths ?? []
506
- }),
531
+ fuelPathsView,
507
532
  restart: (configuration) => pluginRestart?.(configuration)
508
533
  });
509
534
  (0, voyage_routes_1.registerVoyageEditRoutes)(router, {