@mxtommy/kip 4.6.0 → 4.7.0-beta.3

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 (43) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/package.json +5 -5
  3. package/plugin/history-series.service.js +91 -13
  4. package/plugin/index.js +148 -48
  5. package/plugin/kip-plugin/src/history-series.service.js +537 -0
  6. package/plugin/kip-plugin/src/index.js +1194 -0
  7. package/plugin/kip-plugin/src/openApi.json +787 -0
  8. package/plugin/kip-plugin/src/sqlite-history-storage.service.js +1122 -0
  9. package/plugin/openApi.json +14 -0
  10. package/plugin/sqlite-history-storage.service.js +13 -19
  11. package/plugin/src/app/core/contracts/kip-series-contract.js +14 -0
  12. package/plugin/src/app/core/contracts/kip-series-contract.spec.js +68 -0
  13. package/public/3rdpartylicenses.txt +36 -0
  14. package/public/assets/help-docs/dashboards.md +3 -3
  15. package/public/assets/help-docs/datainspector.md +3 -3
  16. package/public/assets/help-docs/history-api.md +31 -21
  17. package/public/assets/help-docs/menu.json +1 -1
  18. package/public/assets/help-docs/nodered-control-flows.md +4 -4
  19. package/public/assets/help-docs/putcontrols.md +6 -6
  20. package/public/assets/help-docs/widget-historical-series.md +55 -11
  21. package/public/assets/help-docs/zones.md +1 -1
  22. package/public/assets/svg/icons.svg +17 -0
  23. package/public/{chunk-AC6VD2FN.js → chunk-2GOHQZH5.js} +4 -4
  24. package/public/{chunk-3JA4CQ7T.js → chunk-4YDVZHMH.js} +4 -4
  25. package/public/{chunk-UYHRT3PR.js → chunk-7ZZK5KBC.js} +1 -1
  26. package/public/{chunk-MDNGWQNG.js → chunk-AB34PRGT.js} +7 -7
  27. package/public/{chunk-356CW47X.js → chunk-AQROQY2F.js} +1 -1
  28. package/public/{chunk-ZY3U4H4Z.js → chunk-AZC2WKQI.js} +1 -1
  29. package/public/{chunk-CYTLQDGF.js → chunk-BQPPRM7O.js} +1 -1
  30. package/public/{chunk-BMHMHQFO.js → chunk-CRJ2S7XQ.js} +2 -2
  31. package/public/{chunk-QU3JR4YV.js → chunk-FYDLTNP4.js} +1 -1
  32. package/public/{chunk-B4NYOD6L.js → chunk-IENESD5Q.js} +1 -1
  33. package/public/{chunk-PPF5S5CV.js → chunk-M37BLWHF.js} +5 -5
  34. package/public/chunk-MXUEYEZU.js +5 -0
  35. package/public/chunk-OXKOGBYN.js +50 -0
  36. package/public/{chunk-NJISHUGY.js → chunk-PTLDR7X7.js} +1 -1
  37. package/public/{chunk-5SAXWR6Z.js → chunk-QV7OHVD5.js} +8 -8
  38. package/public/{chunk-ZXO4VMEH.js → chunk-SDPSUOCJ.js} +1 -1
  39. package/public/{chunk-MGLD6QDJ.js → chunk-YY4ZUJFI.js} +5 -5
  40. package/public/index.html +1 -1
  41. package/public/{main-I33LH3HC.js → main-5HQ6OYM7.js} +1 -1
  42. package/public/chunk-6A4NRSCL.js +0 -5
  43. package/public/chunk-P3M6SJQT.js +0 -50
package/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # v4.6.0
2
2
  ## Improvements
3
- * Built-in Time-Series storage and History-API provider now use the native node:sqlite feature, eliminating all binary and external dependencies.
3
+ * Built-in Time-Series storage and History-API provider now use the native node:sqlite feature, eliminating binary and external dependencies.
4
4
  * Requires Node.js 22.5.0 or newer. If you use an older Node.js version, you must select an alternative History-API provider.
5
- * **IMPORTANT:** Before upgrading Node.js, always confirm your Signal K server version supports the required Node.js version. See the [Signal K installation documentation](https://demo.signalk.org/documentation/Installation.html).
5
+ * **IMPORTANT:** Before upgrading Node.js, always confirm your Signal K server and OS supports the required Node.js version. See the [Signal K installation documentation](https://demo.signalk.org/documentation/Installation.html).
6
6
  ## Fixes
7
7
  * Extending v4.5.x features to VenusOS (32bit OS) - Error: Failed to start: Error loading duckdb native binding: unsupported arch 'arm' for platform 'linux'. Fixes #979
8
8
  * Uninstallation does not remove all files. Fixes #981
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxtommy/kip",
3
- "version": "4.6.0",
3
+ "version": "4.7.0-beta.3",
4
4
  "description": "An advanced and versatile marine instrumentation package to display Signal K data.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -76,10 +76,10 @@
76
76
  "@angular/platform-browser": "21.2.1",
77
77
  "@angular/platform-browser-dynamic": "21.2.1",
78
78
  "@angular/router": "21.2.1",
79
- "@angular-devkit/build-angular": "^21.1.4",
79
+ "@angular-devkit/build-angular": "^21.2.1",
80
80
  "@angular-devkit/schematics-cli": "^20.1.6",
81
- "@angular/build": "^21.1.4",
82
- "@angular/cli": "^21.1.4",
81
+ "@angular/build": "^21.2.1",
82
+ "@angular/cli": "^21.2.1",
83
83
  "@angular/compiler-cli": "21.2.1",
84
84
  "@angular/language-service": "21.2.1",
85
85
  "@types/canvas-gauges": "^2.1.8",
@@ -89,7 +89,7 @@
89
89
  "@types/js-quantities": "^1.6.6",
90
90
  "@types/lodash-es": "^4.17.9",
91
91
  "@types/node": "^24.1.0",
92
- "angular-eslint": "21.2.0",
92
+ "angular-eslint": "21.3.0",
93
93
  "codelyzer": "^6.0.0",
94
94
  "eslint": "^9.29.0",
95
95
  "jasmine-core": "~4.0.1",
@@ -6,11 +6,14 @@ exports.HistorySeriesService = void 0;
6
6
  */
7
7
  class HistorySeriesService {
8
8
  nowProvider;
9
+ selfContext;
9
10
  seriesById = new Map();
11
+ enabledSeriesKeysByPath = new Map();
10
12
  lastAcceptedTimestampBySeriesKey = new Map();
11
13
  sampleSink = null;
12
- constructor(nowProvider = () => Date.now()) {
14
+ constructor(nowProvider = () => Date.now(), selfContext = null) {
13
15
  this.nowProvider = nowProvider;
16
+ this.selfContext = selfContext;
14
17
  }
15
18
  /**
16
19
  * Returns all configured series sorted by `seriesId`.
@@ -56,6 +59,7 @@ class HistorySeriesService {
56
59
  const normalized = this.normalizeSeries(input);
57
60
  const key = normalized.seriesId;
58
61
  this.seriesById.set(key, normalized);
62
+ this.rebuildEnabledPathIndex();
59
63
  return normalized;
60
64
  }
61
65
  /**
@@ -90,6 +94,7 @@ class HistorySeriesService {
90
94
  this.seriesById.delete(key);
91
95
  this.lastAcceptedTimestampBySeriesKey.delete(key);
92
96
  });
97
+ this.rebuildEnabledPathIndex();
93
98
  return true;
94
99
  }
95
100
  /**
@@ -103,7 +108,7 @@ class HistorySeriesService {
103
108
  * console.log(result.created, result.deleted);
104
109
  */
105
110
  reconcileSeries(desiredSeries) {
106
- const now = Date.now();
111
+ const now = this.nowProvider();
107
112
  const desiredById = new Map();
108
113
  desiredSeries.forEach(entry => {
109
114
  const normalized = this.normalizeSeries(entry);
@@ -122,7 +127,7 @@ class HistorySeriesService {
122
127
  created += 1;
123
128
  return;
124
129
  }
125
- if (JSON.stringify(existing) !== JSON.stringify(desired)) {
130
+ if (!this.areSeriesEquivalent(existing, desired)) {
126
131
  this.seriesById.set(seriesKey, desiredWithReconcile);
127
132
  updated += 1;
128
133
  }
@@ -138,6 +143,7 @@ class HistorySeriesService {
138
143
  deleted += 1;
139
144
  }
140
145
  });
146
+ this.rebuildEnabledPathIndex();
141
147
  return {
142
148
  created,
143
149
  updated,
@@ -157,12 +163,10 @@ class HistorySeriesService {
157
163
  * service.recordSample('abc', 12.4, Date.now());
158
164
  */
159
165
  recordSample(seriesId, value, timestamp) {
160
- const seriesEntry = Array.from(this.seriesById.entries())
161
- .find(([, series]) => series.seriesId === seriesId);
162
- if (!seriesEntry) {
166
+ if (!this.seriesById.has(seriesId)) {
163
167
  return false;
164
168
  }
165
- return this.recordSampleByKey(seriesEntry[0], value, timestamp);
169
+ return this.recordSampleByKey(seriesId, value, timestamp);
166
170
  }
167
171
  recordSampleByKey(seriesKey, value, timestamp) {
168
172
  const series = this.seriesById.get(seriesKey);
@@ -213,8 +217,13 @@ class HistorySeriesService {
213
217
  }
214
218
  let recorded = 0;
215
219
  leafSamples.forEach(leaf => {
216
- this.seriesById.forEach((series, seriesKey) => {
217
- if (series.path !== leaf.path || series.enabled === false) {
220
+ const seriesKeys = this.enabledSeriesKeysByPath.get(leaf.path);
221
+ if (!seriesKeys || seriesKeys.length === 0) {
222
+ return;
223
+ }
224
+ seriesKeys.forEach(seriesKey => {
225
+ const series = this.seriesById.get(seriesKey);
226
+ if (!series) {
218
227
  return;
219
228
  }
220
229
  const seriesContext = series.context ?? 'vessels.self';
@@ -293,6 +302,68 @@ class HistorySeriesService {
293
302
  });
294
303
  return Array.from(contexts).sort();
295
304
  }
305
+ rebuildEnabledPathIndex() {
306
+ this.enabledSeriesKeysByPath.clear();
307
+ this.seriesById.forEach((series, seriesKey) => {
308
+ if (series.enabled === false) {
309
+ return;
310
+ }
311
+ const keys = this.enabledSeriesKeysByPath.get(series.path) ?? [];
312
+ keys.push(seriesKey);
313
+ this.enabledSeriesKeysByPath.set(series.path, keys);
314
+ });
315
+ }
316
+ areSeriesEquivalent(left, right) {
317
+ const leftComparable = this.toComparableSeries(left);
318
+ const rightComparable = this.toComparableSeries(right);
319
+ return leftComparable.seriesId === rightComparable.seriesId
320
+ && leftComparable.datasetUuid === rightComparable.datasetUuid
321
+ && leftComparable.ownerWidgetUuid === rightComparable.ownerWidgetUuid
322
+ && leftComparable.ownerWidgetSelector === rightComparable.ownerWidgetSelector
323
+ && leftComparable.path === rightComparable.path
324
+ && leftComparable.expansionMode === rightComparable.expansionMode
325
+ && this.areStringArraysEquivalent(leftComparable.allowedBatteryIds, rightComparable.allowedBatteryIds)
326
+ && leftComparable.source === rightComparable.source
327
+ && leftComparable.context === rightComparable.context
328
+ && leftComparable.timeScale === rightComparable.timeScale
329
+ && leftComparable.period === rightComparable.period
330
+ && leftComparable.retentionDurationMs === rightComparable.retentionDurationMs
331
+ && leftComparable.sampleTime === rightComparable.sampleTime
332
+ && leftComparable.enabled === rightComparable.enabled
333
+ && this.areStringArraysEquivalent(leftComparable.methods, rightComparable.methods);
334
+ }
335
+ toComparableSeries(series) {
336
+ const { reconcileTs, ...comparable } = series;
337
+ void reconcileTs;
338
+ return {
339
+ ...comparable,
340
+ allowedBatteryIds: this.normalizeComparableStringArray(comparable.allowedBatteryIds),
341
+ methods: this.normalizeComparableStringArray(comparable.methods)
342
+ };
343
+ }
344
+ areStringArraysEquivalent(left, right) {
345
+ const normalizedLeft = this.normalizeComparableStringArray(left) ?? [];
346
+ const normalizedRight = this.normalizeComparableStringArray(right) ?? [];
347
+ if (normalizedLeft.length !== normalizedRight.length) {
348
+ return false;
349
+ }
350
+ return normalizedLeft.every((value, index) => value === normalizedRight[index]);
351
+ }
352
+ normalizeComparableStringArray(values) {
353
+ if (!Array.isArray(values) || values.length === 0) {
354
+ return undefined;
355
+ }
356
+ return [...values]
357
+ .filter((value) => typeof value === 'string')
358
+ .sort((left, right) => left.localeCompare(right));
359
+ }
360
+ isChartWidget(ownerWidgetSelector, ownerWidgetUuid) {
361
+ if (ownerWidgetSelector === 'widget-data-chart' || ownerWidgetSelector === 'widget-windtrends-chart') {
362
+ return true;
363
+ }
364
+ return ownerWidgetUuid?.startsWith('widget-windtrends-chart') === true
365
+ || ownerWidgetUuid?.startsWith('widget-data-chart') === true;
366
+ }
296
367
  normalizeSeries(input) {
297
368
  const seriesId = (input.seriesId || input.datasetUuid || '').trim();
298
369
  if (!seriesId) {
@@ -310,8 +381,8 @@ class HistorySeriesService {
310
381
  if (!path) {
311
382
  throw new Error('path is required');
312
383
  }
313
- // Determine if this is a chart type widget
314
- const isDataWidget = ownerWidgetUuid?.startsWith('widget-windtrends-chart') || ownerWidgetUuid?.startsWith('widget-data-chart');
384
+ const ownerWidgetSelector = typeof input.ownerWidgetSelector === 'string' ? input.ownerWidgetSelector.trim() : undefined;
385
+ const isDataWidget = this.isChartWidget(ownerWidgetSelector, ownerWidgetUuid);
315
386
  const retentionMs = this.resolveRetentionMs(input);
316
387
  let sampleTime;
317
388
  if (isDataWidget) {
@@ -329,6 +400,7 @@ class HistorySeriesService {
329
400
  seriesId,
330
401
  datasetUuid,
331
402
  ownerWidgetUuid,
403
+ ownerWidgetSelector,
332
404
  path,
333
405
  source: input.source ?? 'default',
334
406
  context: input.context ?? 'vessels.self',
@@ -408,11 +480,17 @@ class HistorySeriesService {
408
480
  if (seriesContext === sampleContext) {
409
481
  return true;
410
482
  }
411
- if (seriesContext === 'vessels.self') {
412
- return sampleContext === 'vessels.self' || sampleContext.startsWith('vessels.');
483
+ if (this.isSelfContext(seriesContext) && this.isSelfContext(sampleContext)) {
484
+ return true;
413
485
  }
414
486
  return false;
415
487
  }
488
+ isSelfContext(context) {
489
+ if (context === 'vessels.self') {
490
+ return true;
491
+ }
492
+ return !!this.selfContext && context === this.selfContext;
493
+ }
416
494
  isSourceMatch(seriesSource, sampleSource) {
417
495
  if (seriesSource === '*' || seriesSource === 'any') {
418
496
  return true;
package/plugin/index.js CHANGED
@@ -37,6 +37,14 @@ const server_api_1 = require("@signalk/server-api");
37
37
  const openapi = __importStar(require("./openApi.json"));
38
38
  const history_series_service_1 = require("./history-series.service");
39
39
  const sqlite_history_storage_service_1 = require("./sqlite-history-storage.service");
40
+ async function defaultGetSqliteModule() {
41
+ try {
42
+ return await Promise.resolve().then(() => __importStar(require('node:sqlite')));
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
40
48
  const start = (server) => {
41
49
  const mutableOpenApi = JSON.parse(JSON.stringify(openapi.default ?? openapi));
42
50
  const API_PATHS = {
@@ -79,15 +87,14 @@ const start = (server) => {
79
87
  }
80
88
  }
81
89
  };
82
- const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now());
83
- const storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService();
90
+ const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now(), typeof server.selfId === 'string' && server.selfId.trim().length > 0 ? `vessels.${server.selfId.trim()}` : null);
91
+ const storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService(server.getDataDirPath());
84
92
  let retentionSweepTimer = null;
85
93
  let storageFlushTimer = null;
86
94
  let sqliteInitializationPromise = null;
87
95
  const SQLITE_INIT_WAIT_TIMEOUT_MS = 5000;
88
96
  const MIN_NODE_SQLITE_VERSION = '22.5.0';
89
97
  let streamUnsubscribes = [];
90
- let historyApiRegistry = null;
91
98
  let historyApiProviderRegistered = false;
92
99
  let runtimeSqliteUnavailableMessage = null;
93
100
  function logRuntimeDependencyVersions() {
@@ -95,16 +102,12 @@ const start = (server) => {
95
102
  const sqliteAvailability = modeConfig && modeConfig.nodeSqliteAvailable ? 'available' : 'unavailable';
96
103
  server.debug(`[KIP][RUNTIME] ${nodeIdentity} node:sqlite=${sqliteAvailability}`);
97
104
  }
98
- async function getSqliteModule() {
99
- try {
100
- return await Promise.resolve().then(() => __importStar(require('node:sqlite')));
101
- }
102
- catch {
103
- return null;
104
- }
105
- }
106
105
  async function detectSqliteRuntime() {
107
- const sqliteModule = await getSqliteModule();
106
+ const exportedStart = start;
107
+ const resolveSqliteModule = typeof exportedStart.getSqliteModule === 'function'
108
+ ? exportedStart.getSqliteModule
109
+ : defaultGetSqliteModule;
110
+ const sqliteModule = await resolveSqliteModule();
108
111
  if (!sqliteModule) {
109
112
  runtimeSqliteUnavailableMessage = `node:sqlite requires Node ${MIN_NODE_SQLITE_VERSION}+`;
110
113
  return false;
@@ -145,6 +148,97 @@ const start = (server) => {
145
148
  nodeSqliteAvailable: nodeSqliteAvailable !== false
146
149
  };
147
150
  }
151
+ function slugify(value) {
152
+ return value
153
+ .toLowerCase()
154
+ .replace(/[^a-z0-9]+/g, '-')
155
+ .replace(/^-+|-+$/g, '');
156
+ }
157
+ function resolveBmsBatteryIdsFromSelfPath() {
158
+ const batteriesPath = server.getSelfPath('electrical.batteries');
159
+ const readCandidate = (node) => {
160
+ if (!node || typeof node !== 'object' || Array.isArray(node)) {
161
+ return null;
162
+ }
163
+ const root = node;
164
+ if (Object.prototype.hasOwnProperty.call(root, 'value')) {
165
+ const value = root.value;
166
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
167
+ return value;
168
+ }
169
+ return null;
170
+ }
171
+ return root;
172
+ };
173
+ const candidates = readCandidate(batteriesPath);
174
+ if (!candidates) {
175
+ return [];
176
+ }
177
+ return Object.keys(candidates)
178
+ .filter(id => /^[a-z0-9_-]+$/i.test(id))
179
+ .sort((left, right) => left.localeCompare(right));
180
+ }
181
+ function getExistingConcreteBmsSeries(templateSeries, existingSeries) {
182
+ return existingSeries
183
+ .filter(series => series.ownerWidgetUuid === templateSeries.ownerWidgetUuid)
184
+ .filter(series => series.expansionMode == null)
185
+ .filter(series => series.seriesId !== templateSeries.seriesId)
186
+ .map(series => ({ ...series }));
187
+ }
188
+ function mergeSeriesDefinitions(series) {
189
+ const mergedById = new Map();
190
+ series.forEach(item => {
191
+ mergedById.set(item.seriesId, item);
192
+ });
193
+ return Array.from(mergedById.values());
194
+ }
195
+ function expandTemplateSeriesDefinitions(payload, existingSeries = []) {
196
+ const bmsMetrics = ['capacity.stateOfCharge', 'current'];
197
+ const expandedById = new Map();
198
+ const discoveredBatteryIds = resolveBmsBatteryIdsFromSelfPath();
199
+ payload.forEach(series => {
200
+ const isBmsTemplate = series.expansionMode === 'bms-battery-tree' && series.ownerWidgetSelector === 'widget-bms';
201
+ if (!isBmsTemplate) {
202
+ expandedById.set(series.seriesId, series);
203
+ return;
204
+ }
205
+ if (discoveredBatteryIds.length === 0) {
206
+ getExistingConcreteBmsSeries(series, existingSeries).forEach(existing => {
207
+ expandedById.set(existing.seriesId, existing);
208
+ });
209
+ return;
210
+ }
211
+ const allowedBatteryIds = Array.isArray(series.allowedBatteryIds)
212
+ ? series.allowedBatteryIds
213
+ .filter((id) => typeof id === 'string')
214
+ .map(id => id.trim())
215
+ .filter(id => id.length > 0)
216
+ : [];
217
+ const allowedSet = allowedBatteryIds.length > 0 ? new Set(allowedBatteryIds) : null;
218
+ const batteryIds = discoveredBatteryIds.filter(id => !allowedSet || allowedSet.has(id));
219
+ if (batteryIds.length === 0) {
220
+ return;
221
+ }
222
+ const source = series.source ?? 'default';
223
+ const sourceKey = slugify(source || 'default') || 'default';
224
+ batteryIds.forEach(batteryId => {
225
+ bmsMetrics.forEach(metric => {
226
+ const path = `self.electrical.batteries.${batteryId}.${metric}`;
227
+ const seriesId = `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`;
228
+ expandedById.set(seriesId, {
229
+ ...series,
230
+ seriesId,
231
+ datasetUuid: `${series.ownerWidgetUuid}:bms:${batteryId}:${metric}:${sourceKey}`,
232
+ path,
233
+ retentionDurationMs: Number.isFinite(series.retentionDurationMs) ? series.retentionDurationMs : 24 * 60 * 60 * 1000,
234
+ expansionMode: null,
235
+ allowedBatteryIds: null
236
+ });
237
+ });
238
+ });
239
+ });
240
+ return Array.from(expandedById.values());
241
+ }
148
242
  function getDisplaySelfPath(displayId, suffix) {
149
243
  const tail = suffix ? `.${suffix}` : '';
150
244
  const want = `displays.${displayId}${tail}`;
@@ -470,7 +564,17 @@ const start = (server) => {
470
564
  server.debug('[KIP][HISTORY_PROVIDER] registration skipped reason=config-disabled');
471
565
  return;
472
566
  }
473
- const host = server;
567
+ const serverWithHistoryApi = server;
568
+ const registerHistoryApiProvider = typeof serverWithHistoryApi.registerHistoryApiProvider === 'function'
569
+ ? serverWithHistoryApi.registerHistoryApiProvider.bind(serverWithHistoryApi)
570
+ : (typeof serverWithHistoryApi.history?.registerHistoryApiProvider === 'function'
571
+ ? serverWithHistoryApi.history.registerHistoryApiProvider.bind(serverWithHistoryApi.history)
572
+ : null);
573
+ // guard when running in SK variants that do not support History API registration
574
+ if (!registerHistoryApiProvider) {
575
+ server.debug('[KIP][HISTORY_PROVIDER] registration skipped reason=api-unavailable');
576
+ return;
577
+ }
474
578
  const apiProvider = {
475
579
  getValues: async (query) => {
476
580
  const resolved = await resolveHistoryValues(buildHistoryQueryFromValuesRequest(query));
@@ -485,26 +589,9 @@ const start = (server) => {
485
589
  getPaths: (query) => resolveHistoryPaths(buildHistoryQueryFromRangeRequest(query)),
486
590
  getContexts: (query) => resolveHistoryContexts(buildHistoryQueryFromRangeRequest(query))
487
591
  };
488
- const registry = host.history && typeof host.history.registerHistoryApiProvider === 'function'
489
- ? host.history
490
- : (typeof host.registerHistoryApiProvider === 'function'
491
- ? {
492
- registerHistoryApiProvider: host.registerHistoryApiProvider.bind(host),
493
- unregisterHistoryApiProvider: typeof host.unregisterHistoryApiProvider === 'function'
494
- ? host.unregisterHistoryApiProvider.bind(host)
495
- : undefined
496
- }
497
- : null);
498
- if (registry && typeof registry.registerHistoryApiProvider === 'function') {
499
- registry.registerHistoryApiProvider(apiProvider);
500
- historyApiProviderRegistered = true;
501
- if (typeof registry.unregisterHistoryApiProvider === 'function') {
502
- historyApiRegistry = { unregisterHistoryApiProvider: registry.unregisterHistoryApiProvider.bind(registry) };
503
- }
504
- server.debug('[KIP][HISTORY_PROVIDER] registration success provider=kip');
505
- return;
506
- }
507
- server.debug('[KIP][HISTORY_PROVIDER] registration skipped reason=api-unavailable');
592
+ registerHistoryApiProvider(apiProvider);
593
+ historyApiProviderRegistered = true;
594
+ server.debug('[KIP][HISTORY_PROVIDER] registration success provider=kip');
508
595
  }
509
596
  function rebuildSeriesCaptureSubscriptions() {
510
597
  stopSeriesCapture();
@@ -603,9 +690,6 @@ const start = (server) => {
603
690
  if (!modeConfig.nodeSqliteAvailable) {
604
691
  server.error(`[KIP][RUNTIME] node:sqlite unavailable. ${runtimeSqliteUnavailableMessage}`);
605
692
  }
606
- const serverWithApp = server;
607
- const dataDirPath = serverWithApp.app?.getDataDirPath?.();
608
- storageService.setDataDirPath(typeof dataDirPath === 'string' ? dataDirPath : null);
609
693
  storageService.setRuntimeAvailability(modeConfig.nodeSqliteAvailable, runtimeSqliteUnavailableMessage ?? undefined);
610
694
  logRuntimeDependencyVersions();
611
695
  logOperationalMode('start-configured');
@@ -742,16 +826,16 @@ const start = (server) => {
742
826
  .catch(() => undefined)
743
827
  .then(() => storageService.close(storageLifecycleToken))
744
828
  .catch(() => undefined);
745
- if (historyApiRegistry) {
746
- try {
747
- historyApiRegistry.unregisterHistoryApiProvider();
748
- server.debug('[KIP][HISTORY_PROVIDER] unregister success provider=kip');
749
- }
750
- catch (error) {
751
- server.error(`[HISTORY PROVIDER] unregister failed: ${String(error.message || error)}`);
752
- }
753
- historyApiRegistry = null;
829
+ const serverWithHistoryApi = server;
830
+ const unregisterHistoryApiProvider = typeof serverWithHistoryApi.unregisterHistoryApiProvider === 'function'
831
+ ? serverWithHistoryApi.unregisterHistoryApiProvider.bind(serverWithHistoryApi)
832
+ : (typeof serverWithHistoryApi.history?.unregisterHistoryApiProvider === 'function'
833
+ ? serverWithHistoryApi.history.unregisterHistoryApiProvider.bind(serverWithHistoryApi.history)
834
+ : null);
835
+ if (unregisterHistoryApiProvider) {
836
+ unregisterHistoryApiProvider();
754
837
  }
838
+ historyApiProviderRegistered = false;
755
839
  sqliteInitializationPromise = null;
756
840
  const msg = 'Stopped.';
757
841
  server.setPluginStatus(msg);
@@ -1057,14 +1141,28 @@ const start = (server) => {
1057
1141
  if (!Array.isArray(payload)) {
1058
1142
  return sendFail(res, 400, 'Body must be an array of series definitions');
1059
1143
  }
1060
- const simulated = new history_series_service_1.HistorySeriesService(() => Date.now());
1061
- historySeries.listSeries().forEach(series => {
1144
+ const simulated = new history_series_service_1.HistorySeriesService(() => Date.now(), typeof server.selfId === 'string' && server.selfId.trim().length > 0 ? `vessels.${server.selfId.trim()}` : null);
1145
+ const currentSeries = mergeSeriesDefinitions([
1146
+ ...(await storageService.getSeriesDefinitions()),
1147
+ ...historySeries.listSeries()
1148
+ ]);
1149
+ currentSeries.forEach(series => {
1062
1150
  simulated.upsertSeries(series);
1063
1151
  });
1064
1152
  const scopedPayload = payload.map(series => ({
1065
1153
  ...series
1066
1154
  }));
1067
- const result = simulated.reconcileSeries(scopedPayload);
1155
+ const isBatteryDiscoveryUnavailable = resolveBmsBatteryIdsFromSelfPath().length === 0;
1156
+ const preservedBmsSeries = isBatteryDiscoveryUnavailable
1157
+ ? scopedPayload
1158
+ .filter(series => series.expansionMode === 'bms-battery-tree' && series.ownerWidgetSelector === 'widget-bms')
1159
+ .flatMap(series => currentSeries.filter(current => current.ownerWidgetUuid === series.ownerWidgetUuid && current.expansionMode == null && current.seriesId !== series.seriesId))
1160
+ : [];
1161
+ const expandedPayload = mergeSeriesDefinitions([
1162
+ ...expandTemplateSeriesDefinitions(scopedPayload, currentSeries),
1163
+ ...preservedBmsSeries
1164
+ ]);
1165
+ const result = simulated.reconcileSeries(expandedPayload);
1068
1166
  const nextSeries = simulated.listSeries();
1069
1167
  await storageService.replaceSeriesDefinitions(nextSeries);
1070
1168
  const seriesOutsideScope = historySeries.listSeries();
@@ -1093,4 +1191,6 @@ const start = (server) => {
1093
1191
  };
1094
1192
  return plugin;
1095
1193
  };
1194
+ const startWithHooks = start;
1195
+ startWithHooks.getSqliteModule = defaultGetSqliteModule;
1096
1196
  module.exports = start;