@mxtommy/kip 4.5.2 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +12 -3
  2. package/package.json +13 -15
  3. package/plugin/history-series.service.js +14 -24
  4. package/plugin/index.js +139 -95
  5. package/plugin/{duckdb-parquet-storage.service.js → sqlite-history-storage.service.js} +330 -503
  6. package/public/assets/help-docs/widget-historical-series.md +5 -5
  7. package/public/{chunk-TVNXBPFF.js → chunk-356CW47X.js} +1 -1
  8. package/public/{chunk-NFJ4RQSE.js → chunk-3JA4CQ7T.js} +1 -1
  9. package/public/{chunk-VXTTEFRP.js → chunk-5SAXWR6Z.js} +8 -8
  10. package/public/{chunk-67V4XHCY.js → chunk-6A4NRSCL.js} +1 -1
  11. package/public/{chunk-P7JKENHI.js → chunk-AC6VD2FN.js} +1 -1
  12. package/public/{chunk-TBNKOU7M.js → chunk-B4NYOD6L.js} +1 -1
  13. package/public/{chunk-WH5CIUSB.js → chunk-BGGO4PGD.js} +1 -1
  14. package/public/{chunk-KQEEYPK3.js → chunk-BMHMHQFO.js} +1 -1
  15. package/public/{chunk-RCYOZLZB.js → chunk-CSIELI2Z.js} +2 -2
  16. package/public/{chunk-R36UY4Q4.js → chunk-CYTLQDGF.js} +1 -1
  17. package/public/{chunk-YI3MZWRZ.js → chunk-HSKVTFFQ.js} +1 -1
  18. package/public/{chunk-IH4CEW4C.js → chunk-MDNGWQNG.js} +8 -8
  19. package/public/{chunk-VPF5756E.js → chunk-MGLD6QDJ.js} +1 -1
  20. package/public/{chunk-P4CRTB7N.js → chunk-NJISHUGY.js} +1 -1
  21. package/public/{chunk-ISF5E3CX.js → chunk-P3M6SJQT.js} +11 -11
  22. package/public/{chunk-WQSJFJLW.js → chunk-POMIQBAL.js} +2 -2
  23. package/public/{chunk-SJFJEOSG.js → chunk-PPF5S5CV.js} +1 -1
  24. package/public/{chunk-OPTBDYBL.js → chunk-PUPM3HUQ.js} +1 -1
  25. package/public/chunk-PZ6I6W3H.js +16 -0
  26. package/public/{chunk-VXCYPAWR.js → chunk-QU3JR4YV.js} +1 -1
  27. package/public/{chunk-Q2ANAJAD.js → chunk-SUWMN3AE.js} +1 -1
  28. package/public/{chunk-CD5TQSCS.js → chunk-UYHRT3PR.js} +1 -1
  29. package/public/{chunk-FZFDGAQO.js → chunk-WJFXI5PQ.js} +1 -1
  30. package/public/{chunk-I4SJ5UNN.js → chunk-ZXO4VMEH.js} +1 -1
  31. package/public/{chunk-XBSU7OGT.js → chunk-ZY3U4H4Z.js} +1 -1
  32. package/public/index.html +1 -1
  33. package/public/{main-B6TXB3EB.js → main-I33LH3HC.js} +1 -1
  34. package/plugin/plugin-auth.service.js +0 -75
  35. package/public/chunk-BTFZS2TW.js +0 -16
package/CHANGELOG.md CHANGED
@@ -1,12 +1,21 @@
1
- # v4.5.2
1
+ # v4.6.0
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.
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).
6
+ ## Fixes
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
+ * Uninstallation does not remove all files. Fixes #981
9
+ * Reduce unwarranted installation size.
10
+ # v4.5.2 - Deprecated version due to lack of VenusOS (32bit) DuckDB binary support
2
11
  ## Fixes
3
12
  * DuckDB initialized when features are not enabled.
4
13
  * Parquet data compression and pruning not executing.
5
- # v4.5.1
14
+ # v4.5.1 - Deprecated version due to lack of VenusOS (32bit) DuckDB binary support
6
15
  ## Fixes
7
16
  * DuckDB dependency causing build and installation errors. Fixes #979
8
17
  * Reduced installation size. Fixes #980
9
- # v4.5.0
18
+ # v4.5.0 - Deprecated version due to lack of VenusOS (32bit) DuckDB binary support
10
19
  ## New Features
11
20
  * Effortlessly review your vessel’s history with the new Widget Historical Charts—automatically track, store, and visualize key data. Instantly access up to the last full day of performance: just two-finger tap or right-click any widget to open a seamless history dialog—no setup, no clutter, just the trends you need. (Requires Signal K v2.22.1)
12
21
  * Dashboards now start with fully populated Data Charts, powered by KIP’s managed Time-Series History-API provider or other compatible history providers. (Requires Signal K v2.22.1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxtommy/kip",
3
- "version": "4.5.2",
3
+ "version": "4.6.0",
4
4
  "description": "An advanced and versatile marine instrumentation package to display Signal K data.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -66,22 +66,22 @@
66
66
  },
67
67
  "schematics": "tools/schematics/collection.json",
68
68
  "devDependencies": {
69
- "@angular/cdk": "21.2.0",
70
- "@angular/common": "21.2.0",
71
- "@angular/compiler": "21.2.0",
72
- "@angular/core": "21.2.0",
73
- "@angular/forms": "21.2.0",
74
- "@angular/material": "21.2.0",
75
- "@angular/animations": "21.2.0",
76
- "@angular/platform-browser": "21.2.0",
77
- "@angular/platform-browser-dynamic": "21.2.0",
78
- "@angular/router": "21.2.0",
69
+ "@angular/cdk": "21.2.1",
70
+ "@angular/common": "21.2.1",
71
+ "@angular/compiler": "21.2.1",
72
+ "@angular/core": "21.2.1",
73
+ "@angular/forms": "21.2.1",
74
+ "@angular/material": "21.2.1",
75
+ "@angular/animations": "21.2.1",
76
+ "@angular/platform-browser": "21.2.1",
77
+ "@angular/platform-browser-dynamic": "21.2.1",
78
+ "@angular/router": "21.2.1",
79
79
  "@angular-devkit/build-angular": "^21.1.4",
80
80
  "@angular-devkit/schematics-cli": "^20.1.6",
81
81
  "@angular/build": "^21.1.4",
82
82
  "@angular/cli": "^21.1.4",
83
- "@angular/compiler-cli": "21.2.0",
84
- "@angular/language-service": "21.2.0",
83
+ "@angular/compiler-cli": "21.2.1",
84
+ "@angular/language-service": "21.2.1",
85
85
  "@types/canvas-gauges": "^2.1.8",
86
86
  "@types/d3": "^7.4.3",
87
87
  "@types/jasmine": "~3.6.0",
@@ -131,8 +131,6 @@
131
131
  "zone.js": "^0.15.1"
132
132
  },
133
133
  "dependencies": {
134
- "@dsnp/parquetjs": "^1.8.7",
135
- "@duckdb/node-api": "^1.4.4-r.2",
136
134
  "@signalk/server-api": "^2.22.0"
137
135
  }
138
136
  }
@@ -6,13 +6,11 @@ exports.HistorySeriesService = void 0;
6
6
  */
7
7
  class HistorySeriesService {
8
8
  nowProvider;
9
- retainSamplesInMemory;
10
9
  seriesById = new Map();
11
10
  lastAcceptedTimestampBySeriesKey = new Map();
12
11
  sampleSink = null;
13
- constructor(nowProvider = () => Date.now(), retainSamplesInMemory = true) {
12
+ constructor(nowProvider = () => Date.now()) {
14
13
  this.nowProvider = nowProvider;
15
- this.retainSamplesInMemory = retainSamplesInMemory;
16
14
  }
17
15
  /**
18
16
  * Returns all configured series sorted by `seriesId`.
@@ -171,9 +169,10 @@ class HistorySeriesService {
171
169
  if (!series || series.enabled === false || !Number.isFinite(value) || !Number.isFinite(timestamp)) {
172
170
  return false;
173
171
  }
174
- const samplingIntervalMs = this.resolveSampleTimeMs(series.sampleTime);
175
172
  const previousTimestamp = this.lastAcceptedTimestampBySeriesKey.get(seriesKey);
176
- if (previousTimestamp !== undefined && (timestamp - previousTimestamp) < samplingIntervalMs) {
173
+ // Enforces a minimum of 1 second to prevent excessive sampling on short retention durations
174
+ const minSampleTime = Math.max(Number(series.sampleTime) || 0, 1000);
175
+ if (previousTimestamp !== undefined && (timestamp - previousTimestamp) < minSampleTime) {
177
176
  return false;
178
177
  }
179
178
  const context = series.context ?? 'vessels.self';
@@ -311,17 +310,19 @@ class HistorySeriesService {
311
310
  if (!path) {
312
311
  throw new Error('path is required');
313
312
  }
314
- // Determine if this is a data chart based widget
315
- const dsSampleTime = ownerWidgetUuid?.startsWith('widget-windtrends-chart') ||
316
- ownerWidgetUuid?.startsWith('widget-data-chart');
313
+ // Determine if this is a chart type widget
314
+ const isDataWidget = ownerWidgetUuid?.startsWith('widget-windtrends-chart') || ownerWidgetUuid?.startsWith('widget-data-chart');
315
+ const retentionMs = this.resolveRetentionMs(input);
317
316
  let sampleTime;
318
- const retentionMs = input.retentionDurationMs ?? this.resolveRetentionMs(input);
319
- if (dsSampleTime && Number.isFinite(retentionMs) && retentionMs > 0) {
320
- // regular widget sampleTime: 15 sec
321
- sampleTime = Math.max(1, Math.trunc(15000));
317
+ if (isDataWidget) {
318
+ // For chart type widgets we use retention duration to dynamically calculate sampleTime to
319
+ // aims for around 120 samples.
320
+ sampleTime = retentionMs ? Math.max(1000, Math.round(retentionMs / 120)) : 1000;
322
321
  }
323
322
  else {
324
- sampleTime = this.resolveSampleTimeMs(input.sampleTime);
323
+ // Non chart type widgets, ie historical Time-Series, have a fixed sampleTime of 15 sec that is
324
+ // a good median amount of samples for the dynamically queryable chart display range (15 min up to 24h).
325
+ sampleTime = 15000; // ms
325
326
  }
326
327
  return {
327
328
  ...input,
@@ -336,17 +337,6 @@ class HistorySeriesService {
336
337
  sampleTime
337
338
  };
338
339
  }
339
- resolveSampleTimeMs(sampleTime) {
340
- const parsed = Number(sampleTime);
341
- if (Number.isFinite(parsed) && parsed > 0) {
342
- return Math.max(1, Math.trunc(parsed));
343
- }
344
- return 1000;
345
- }
346
- buildSeriesMapKey(seriesId) {
347
- // userScope removed; now returns only seriesId
348
- return String(seriesId).trim();
349
- }
350
340
  resolveRetentionMs(series) {
351
341
  if (Number.isFinite(series.retentionDurationMs) && series.retentionDurationMs > 0) {
352
342
  return series.retentionDurationMs;
package/plugin/index.js CHANGED
@@ -34,12 +34,10 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const server_api_1 = require("@signalk/server-api");
37
- const module_1 = require("module");
38
37
  const openapi = __importStar(require("./openApi.json"));
39
38
  const history_series_service_1 = require("./history-series.service");
40
- const duckdb_parquet_storage_service_1 = require("./duckdb-parquet-storage.service");
39
+ const sqlite_history_storage_service_1 = require("./sqlite-history-storage.service");
41
40
  const start = (server) => {
42
- const packageRequire = (0, module_1.createRequire)(__filename);
43
41
  const mutableOpenApi = JSON.parse(JSON.stringify(openapi.default ?? openapi));
44
42
  const API_PATHS = {
45
43
  DISPLAYS: `/displays`,
@@ -61,6 +59,12 @@ const start = (server) => {
61
59
  title: 'Remote Control and Data Series',
62
60
  description: 'NOTE: All plugin settings are also managed from within KIP\'s Display Options panel. Changes made here will be overridden when KIP applies settings from the Display Options.',
63
61
  properties: {
62
+ nodeSqliteAvailable: {
63
+ type: 'boolean',
64
+ title: 'node:sqlite Available',
65
+ description: 'Indicates if node:sqlite is available in the current runtime (requires Node.js version 22.5.0 or newer). This is set automatically and is read-only.\n\nBefore upgrading Node.js, always verify compatibility with your Signal K server version at https://demo.signalk.org/documentation.',
66
+ readOnly: true
67
+ },
64
68
  historySeriesServiceEnabled: {
65
69
  type: 'boolean',
66
70
  title: 'Enable Automatic Historical Time-Series Capture and Management',
@@ -70,38 +74,59 @@ const start = (server) => {
70
74
  registerAsHistoryApiProvider: {
71
75
  type: 'boolean',
72
76
  title: 'Enable Query Provider',
73
- description: 'The built-in History-API provider is the querying engine that uses the Historical Time-Series data to seed the widget historical panel, the Data Chart and Wind Trends widgets. If you want to use another History-API provider, turn this off and configure your chosen History-API compatible provider accordingly and KIP will query that provider.',
77
+ description: 'The built-in History-API query provider is a feature that enables the plugin to respond to History-API requests. If you want to use another History-API provider, disable this option and configure your chosen History-API compatible provider accordingly and KIP will query that provider.',
74
78
  default: true
75
79
  }
76
80
  }
77
81
  };
78
- const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now(), false);
79
- const storageService = new duckdb_parquet_storage_service_1.DuckDbParquetStorageService();
82
+ const historySeries = new history_series_service_1.HistorySeriesService(() => Date.now());
83
+ const storageService = new sqlite_history_storage_service_1.SqliteHistoryStorageService();
80
84
  let retentionSweepTimer = null;
81
85
  let storageFlushTimer = null;
82
- let duckDbInitializationPromise = null;
83
- const DUCKDB_INIT_WAIT_TIMEOUT_MS = 5000;
86
+ let sqliteInitializationPromise = null;
87
+ const SQLITE_INIT_WAIT_TIMEOUT_MS = 5000;
88
+ const MIN_NODE_SQLITE_VERSION = '22.5.0';
84
89
  let streamUnsubscribes = [];
85
90
  let historyApiRegistry = null;
86
- let historySeriesServiceEnabled = true;
87
- let registerAsHistoryApiProvider = true;
88
91
  let historyApiProviderRegistered = false;
89
- function resolveDependencyIdentity(dependencyName) {
92
+ let runtimeSqliteUnavailableMessage = null;
93
+ function logRuntimeDependencyVersions() {
94
+ const nodeIdentity = `node@${process.version}`;
95
+ const sqliteAvailability = modeConfig && modeConfig.nodeSqliteAvailable ? 'available' : 'unavailable';
96
+ server.debug(`[KIP][RUNTIME] ${nodeIdentity} node:sqlite=${sqliteAvailability}`);
97
+ }
98
+ async function getSqliteModule() {
90
99
  try {
91
- const pkg = packageRequire(`${dependencyName}/package.json`);
92
- const name = typeof pkg.name === 'string' && pkg.name.trim() ? pkg.name.trim() : dependencyName;
93
- const version = typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version.trim() : 'unknown';
94
- return `${name}@${version}`;
100
+ return await Promise.resolve().then(() => __importStar(require('node:sqlite')));
95
101
  }
96
102
  catch {
97
- return `${dependencyName}@unavailable`;
103
+ return null;
98
104
  }
99
105
  }
100
- function logRuntimeDependencyVersions() {
101
- const nodeIdentity = `node@${process.version}`;
102
- const duckDbNodeIdentity = resolveDependencyIdentity('@duckdb/node-api');
103
- const parquetIdentity = resolveDependencyIdentity('@dsnp/parquetjs');
104
- server.debug(`[KIP][RUNTIME] ${nodeIdentity} duckdb=${duckDbNodeIdentity} parquet=${parquetIdentity}`);
106
+ async function detectSqliteRuntime() {
107
+ const sqliteModule = await getSqliteModule();
108
+ if (!sqliteModule) {
109
+ runtimeSqliteUnavailableMessage = `node:sqlite requires Node ${MIN_NODE_SQLITE_VERSION}+`;
110
+ return false;
111
+ }
112
+ if (!sqliteModule.DatabaseSync && !sqliteModule.Database) {
113
+ runtimeSqliteUnavailableMessage = 'node:sqlite module missing required exports';
114
+ return false;
115
+ }
116
+ runtimeSqliteUnavailableMessage = null;
117
+ return true;
118
+ }
119
+ function getSqliteUnavailableMessage() {
120
+ if (!(modeConfig && modeConfig.nodeSqliteAvailable)) {
121
+ return `node:sqlite is not supported in the installed Node.js runtime. Node ${MIN_NODE_SQLITE_VERSION}+ is required.`;
122
+ }
123
+ const details = storageService.getLastInitError();
124
+ return details
125
+ ? `node:sqlite storage unavailable: ${details}`
126
+ : 'node:sqlite storage unavailable';
127
+ }
128
+ function isSqliteUnavailable() {
129
+ return !(modeConfig && modeConfig.nodeSqliteAvailable) || Boolean(storageService.getLastInitError());
105
130
  }
106
131
  function resolveHistoryModeConfig(settings) {
107
132
  const root = (settings && typeof settings === 'object' ? settings : {});
@@ -111,12 +136,15 @@ const start = (server) => {
111
136
  const registerAsHistoryApiProviderSetting = typeof root.registerAsHistoryApiProvider === 'boolean'
112
137
  ? root.registerAsHistoryApiProvider
113
138
  : undefined;
139
+ const nodeSqliteAvailable = typeof root.nodeSqliteAvailable === 'boolean'
140
+ ? root.nodeSqliteAvailable
141
+ : undefined;
114
142
  return {
115
143
  historySeriesServiceEnabled: historySeriesServiceEnabledSetting !== false,
116
- registerAsHistoryApiProvider: registerAsHistoryApiProviderSetting !== false
144
+ registerAsHistoryApiProvider: registerAsHistoryApiProviderSetting !== false,
145
+ nodeSqliteAvailable: nodeSqliteAvailable !== false
117
146
  };
118
147
  }
119
- // Helpers
120
148
  function getDisplaySelfPath(displayId, suffix) {
121
149
  const tail = suffix ? `.${suffix}` : '';
122
150
  const want = `displays.${displayId}${tail}`;
@@ -140,28 +168,19 @@ const start = (server) => {
140
168
  function getHistorySeriesServiceDisabledMessage() {
141
169
  return 'KIP history-series service is disabled by plugin configuration';
142
170
  }
143
- function getDuckDbUnavailableMessage() {
144
- const details = storageService.getLastInitError();
145
- return details
146
- ? `DuckDB storage unavailable: ${details}`
147
- : 'DuckDB storage unavailable';
148
- }
149
- function isDuckDbUnavailable() {
150
- return Boolean(storageService.getLastInitError());
151
- }
152
- async function waitForDuckDbInitialization(timeoutMs = DUCKDB_INIT_WAIT_TIMEOUT_MS) {
153
- if (!duckDbInitializationPromise) {
154
- return storageService.isDuckDbParquetReady();
171
+ async function waitForSqliteInitialization(timeoutMs = SQLITE_INIT_WAIT_TIMEOUT_MS) {
172
+ if (!sqliteInitializationPromise) {
173
+ return storageService.isSqliteReady();
155
174
  }
156
175
  try {
157
176
  const ready = await Promise.race([
158
- duckDbInitializationPromise,
177
+ sqliteInitializationPromise,
159
178
  new Promise(resolvePromise => {
160
179
  setTimeout(() => resolvePromise(false), timeoutMs);
161
180
  })
162
181
  ]);
163
- if (!ready && !storageService.isDuckDbParquetReady()) {
164
- server.error(`[SERIES STORAGE] DuckDB initialization wait timed out after ${timeoutMs}ms`);
182
+ if (!ready && !storageService.isSqliteReady()) {
183
+ server.error(`[SERIES STORAGE] node:sqlite initialization wait timed out after ${timeoutMs}ms`);
165
184
  }
166
185
  return ready;
167
186
  }
@@ -179,10 +198,10 @@ const start = (server) => {
179
198
  || normalized.includes('expected an iso')) {
180
199
  return { statusCode: 400, message };
181
200
  }
182
- if (normalized.includes('duckdb')
201
+ if (normalized.includes('sqlite')
183
202
  || normalized.includes('storage unavailable')
184
203
  || normalized.includes('not initialized')
185
- || isDuckDbUnavailable()) {
204
+ || isSqliteUnavailable()) {
186
205
  return { statusCode: 503, message };
187
206
  }
188
207
  return { statusCode: 500, message };
@@ -192,23 +211,27 @@ const start = (server) => {
192
211
  return current ? JSON.parse(JSON.stringify(current)) : null;
193
212
  }
194
213
  function isHistorySeriesServiceEnabled() {
195
- return historySeriesServiceEnabled;
214
+ return !!(modeConfig && modeConfig.historySeriesServiceEnabled && modeConfig.nodeSqliteAvailable);
196
215
  }
197
216
  function isHistoryApiProviderEnabled() {
198
- return registerAsHistoryApiProvider;
217
+ return !!(modeConfig && modeConfig.registerAsHistoryApiProvider && modeConfig.nodeSqliteAvailable);
199
218
  }
200
219
  function logOperationalMode(stage) {
201
220
  server.debug(`[HISTORY MODE] stage=${stage} historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
202
221
  }
203
- async function ensureDuckDbReadyForRequest(res) {
204
- await waitForDuckDbInitialization();
205
- if (storageService.isDuckDbParquetReady()) {
222
+ async function ensureSqliteReadyForRequest(res) {
223
+ await waitForSqliteInitialization();
224
+ if (storageService.isSqliteReady()) {
206
225
  return true;
207
226
  }
208
- sendFail(res, 503, getDuckDbUnavailableMessage());
227
+ sendFail(res, 503, getSqliteUnavailableMessage());
209
228
  return false;
210
229
  }
211
230
  function ensureHistorySeriesServiceEnabledForRequest(res) {
231
+ if (!(modeConfig && modeConfig.nodeSqliteAvailable)) {
232
+ sendFail(res, 503, getSqliteUnavailableMessage());
233
+ return false;
234
+ }
212
235
  if (isHistorySeriesServiceEnabled()) {
213
236
  return true;
214
237
  }
@@ -393,9 +416,9 @@ const start = (server) => {
393
416
  };
394
417
  }
395
418
  async function resolveHistoryPaths(query) {
396
- await waitForDuckDbInitialization();
397
- if (!storageService.isDuckDbParquetReady()) {
398
- throw new Error(getDuckDbUnavailableMessage());
419
+ await waitForSqliteInitialization();
420
+ if (!storageService.isSqliteReady()) {
421
+ throw new Error(getSqliteUnavailableMessage());
399
422
  }
400
423
  try {
401
424
  await storageService.flush();
@@ -408,9 +431,9 @@ const start = (server) => {
408
431
  });
409
432
  }
410
433
  async function resolveHistoryContexts(query) {
411
- await waitForDuckDbInitialization();
412
- if (!storageService.isDuckDbParquetReady()) {
413
- throw new Error(getDuckDbUnavailableMessage());
434
+ await waitForSqliteInitialization();
435
+ if (!storageService.isSqliteReady()) {
436
+ throw new Error(getSqliteUnavailableMessage());
414
437
  }
415
438
  try {
416
439
  await storageService.flush();
@@ -423,9 +446,9 @@ const start = (server) => {
423
446
  });
424
447
  }
425
448
  async function resolveHistoryValues(query) {
426
- await waitForDuckDbInitialization();
427
- if (!storageService.isDuckDbParquetReady()) {
428
- throw new Error(getDuckDbUnavailableMessage());
449
+ await waitForSqliteInitialization();
450
+ if (!storageService.isSqliteReady()) {
451
+ throw new Error(getSqliteUnavailableMessage());
429
452
  }
430
453
  try {
431
454
  await storageService.flush();
@@ -437,7 +460,7 @@ const start = (server) => {
437
460
  ...query
438
461
  });
439
462
  if (!values) {
440
- throw new Error('DuckDB storage did not return history values.');
463
+ throw new Error('node:sqlite storage did not return history values.');
441
464
  }
442
465
  return values;
443
466
  }
@@ -556,49 +579,52 @@ const start = (server) => {
556
579
  clearInterval(storageFlushTimer);
557
580
  storageFlushTimer = null;
558
581
  }
559
- if (!storageService.isDuckDbParquetEnabled()) {
582
+ if (!storageService.isSqliteEnabled()) {
560
583
  return;
561
584
  }
562
585
  storageFlushTimer = setInterval(() => {
563
586
  void storageService.flush()
564
- .then(result => {
565
- if (result.inserted > 0 || result.exported > 0) {
566
- server.debug(`[KIP][STORAGE] flush inserted=${result.inserted} exported=${result.exported}`);
567
- }
568
- })
569
587
  .catch(error => {
570
588
  server.error(`[SERIES STORAGE] flush failed: ${String(error.message || error)}`);
571
589
  });
572
590
  }, intervalMs);
573
591
  storageFlushTimer.unref?.();
574
592
  }
593
+ let modeConfig = null;
575
594
  const plugin = {
576
595
  id: 'kip',
577
596
  name: 'KIP',
578
597
  description: 'KIP server plugin',
579
- start: (settings) => {
598
+ start: async (settings) => {
580
599
  server.debug('[KIP][LIFECYCLE] start');
600
+ modeConfig = resolveHistoryModeConfig(settings);
601
+ // Overwrite runtime-detected properties in modeConfig
602
+ modeConfig.nodeSqliteAvailable = await detectSqliteRuntime();
603
+ if (!modeConfig.nodeSqliteAvailable) {
604
+ server.error(`[KIP][RUNTIME] node:sqlite unavailable. ${runtimeSqliteUnavailableMessage}`);
605
+ }
606
+ const serverWithApp = server;
607
+ const dataDirPath = serverWithApp.app?.getDataDirPath?.();
608
+ storageService.setDataDirPath(typeof dataDirPath === 'string' ? dataDirPath : null);
609
+ storageService.setRuntimeAvailability(modeConfig.nodeSqliteAvailable, runtimeSqliteUnavailableMessage ?? undefined);
581
610
  logRuntimeDependencyVersions();
582
- const modeConfig = resolveHistoryModeConfig(settings);
583
- historySeriesServiceEnabled = modeConfig.historySeriesServiceEnabled;
584
- registerAsHistoryApiProvider = modeConfig.registerAsHistoryApiProvider;
585
611
  logOperationalMode('start-configured');
586
- const needsDuckDb = historySeriesServiceEnabled || registerAsHistoryApiProvider;
587
- if (needsDuckDb) {
612
+ const needsSqlite = (modeConfig.historySeriesServiceEnabled || modeConfig.registerAsHistoryApiProvider) && modeConfig.nodeSqliteAvailable;
613
+ if (needsSqlite) {
588
614
  storageService.setLogger({
589
615
  debug: (msg) => server.debug(msg),
590
616
  error: (msg) => server.error(msg)
591
617
  });
592
618
  const storageConfig = storageService.configure();
593
- server.debug(`[KIP][STORAGE] config engine=${storageConfig.engine} db=${storageConfig.databaseFile} parquetDir=${storageConfig.parquetDirectory} flushMs=${storageConfig.flushIntervalMs} parquetWindowMs=${storageConfig.parquetWindowMs} parquetCompression=${storageConfig.parquetCompression}`);
619
+ server.debug(`[KIP][STORAGE] config engine=${storageConfig.engine} db=${storageConfig.databaseFile} flushMs=${storageConfig.flushIntervalMs}`);
594
620
  historySeries.setSampleSink(sample => {
595
621
  storageService.enqueueSample(sample);
596
622
  });
597
- duckDbInitializationPromise = storageService.initialize();
598
- void duckDbInitializationPromise.then((ready) => {
599
- server.debug(`[KIP][STORAGE] duckdbReady=${ready}`);
600
- if (ready && storageService.isDuckDbParquetEnabled()) {
601
- if (isHistorySeriesServiceEnabled()) {
623
+ sqliteInitializationPromise = storageService.initialize();
624
+ void sqliteInitializationPromise.then((ready) => {
625
+ server.debug(`[KIP][STORAGE] sqliteReady=${ready}`);
626
+ if (ready && storageService.isSqliteEnabled()) {
627
+ if (modeConfig && modeConfig.historySeriesServiceEnabled) {
602
628
  void storageService.getSeriesDefinitions()
603
629
  .then((storedSeries) => {
604
630
  if (storedSeries.length > 0) {
@@ -606,22 +632,22 @@ const start = (server) => {
606
632
  rebuildSeriesCaptureSubscriptions();
607
633
  }
608
634
  startStorageFlushTimer(storageConfig.flushIntervalMs);
609
- logOperationalMode('duckdb-ready');
610
- server.setPluginStatus(`KIP plugin started with DuckDB/Parquet history storage. Loaded ${storedSeries.length} persisted series. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
635
+ logOperationalMode('sqlite-ready');
636
+ server.setPluginStatus(`Providing: Remote Control${historyApiProviderRegistered ? ', History service' : ', No History service'}${storedSeries.length > 0 ? `, ${storedSeries.length} Time-Series` : ', No Time-Series'}.`);
611
637
  })
612
638
  .catch((loadError) => {
613
639
  server.error(`[SERIES STORAGE] failed to load persisted series: ${String(loadError.message || loadError)}`);
614
640
  startStorageFlushTimer(storageConfig.flushIntervalMs);
615
- logOperationalMode('duckdb-ready-series-load-failed');
616
- server.setPluginStatus(`KIP plugin started with DuckDB/Parquet history storage. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
641
+ logOperationalMode('sqlite-ready-series-load-failed');
642
+ server.setPluginStatus(`Providing: Remote Control${historyApiProviderRegistered ? ', History service' : ', No History service'}, No Time-Series.`);
617
643
  });
618
644
  }
619
645
  else {
620
646
  historySeries.reconcileSeries([]);
621
647
  stopSeriesCapture();
622
648
  startStorageFlushTimer(storageConfig.flushIntervalMs);
623
- logOperationalMode('duckdb-ready-series-disabled');
624
- server.setPluginStatus(`KIP plugin started with history-series service disabled. historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
649
+ logOperationalMode('sqlite-ready-series-disabled');
650
+ server.setPluginStatus(`Providing: Remote Control${historyApiProviderRegistered ? ', History service' : ', No History service'}, No Time-Series.`);
625
651
  }
626
652
  return;
627
653
  }
@@ -631,9 +657,9 @@ const start = (server) => {
631
657
  }
632
658
  const initError = storageService.getLastInitError();
633
659
  if (initError) {
634
- server.setPluginError(`DuckDB unavailable. ${initError}`);
635
- logOperationalMode('duckdb-unavailable');
636
- server.setPluginStatus(`KIP plugin started with DuckDB unavailable. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
660
+ server.setPluginError(`node:sqlite unavailable. ${initError}`);
661
+ logOperationalMode('sqlite-unavailable');
662
+ server.setPluginStatus(`Providing: Remote Control${historyApiProviderRegistered ? ', History service' : ', No History service'}, No Time-Series.`);
637
663
  }
638
664
  });
639
665
  if (retentionSweepTimer) {
@@ -641,7 +667,7 @@ const start = (server) => {
641
667
  }
642
668
  retentionSweepTimer = setInterval(() => {
643
669
  try {
644
- if (storageService.isDuckDbParquetReady()) {
670
+ if (storageService.isSqliteReady()) {
645
671
  const lifecycleToken = storageService.getLifecycleToken();
646
672
  void storageService.pruneExpiredSamples(Date.now(), lifecycleToken)
647
673
  .then(removedPersistedRows => {
@@ -656,20 +682,23 @@ const start = (server) => {
656
682
  });
657
683
  })
658
684
  .catch(error => {
659
- server.error(`[SERIES RETENTION] duckdbPrune failed: ${String(error.message || error)}`);
685
+ server.error(`[SERIES RETENTION] node:sqlite Prune failed: ${String(error.message || error)}`);
660
686
  });
661
687
  }
662
688
  }
663
689
  catch (error) {
664
- server.error(`[SERIES RETENTION] sweep failed: ${String(error.message || error)}`);
690
+ server.error(`[SERIES RETENTION] node:sqlite sweep failed: ${String(error.message || error)}`);
665
691
  }
666
692
  }, 60 * 60_000);
667
693
  retentionSweepTimer.unref?.();
668
694
  rebuildSeriesCaptureSubscriptions();
669
695
  }
670
696
  else {
671
- server.debug('[KIP][STORAGE] duckdb init skipped reason=config-disabled');
672
- duckDbInitializationPromise = null;
697
+ if (modeConfig && !modeConfig.nodeSqliteAvailable && (modeConfig.historySeriesServiceEnabled || modeConfig.registerAsHistoryApiProvider)) {
698
+ server.setPluginStatus(getSqliteUnavailableMessage());
699
+ }
700
+ server.debug('[KIP][STORAGE] sqlite init skipped reason=config-disabled-or-runtime');
701
+ sqliteInitializationPromise = null;
673
702
  stopSeriesCapture();
674
703
  }
675
704
  if (server.registerPutHandler) {
@@ -723,11 +752,26 @@ const start = (server) => {
723
752
  }
724
753
  historyApiRegistry = null;
725
754
  }
726
- duckDbInitializationPromise = null;
755
+ sqliteInitializationPromise = null;
727
756
  const msg = 'Stopped.';
728
757
  server.setPluginStatus(msg);
729
758
  },
730
- schema: () => CONFIG_SCHEMA,
759
+ schema: () => {
760
+ // Return schema with live modeConfig values
761
+ const schema = JSON.parse(JSON.stringify(CONFIG_SCHEMA));
762
+ if (schema && schema.properties && modeConfig) {
763
+ if (typeof modeConfig.nodeSqliteAvailable === 'boolean') {
764
+ schema.properties.nodeSqliteAvailable.default = modeConfig.nodeSqliteAvailable;
765
+ }
766
+ if (typeof modeConfig.historySeriesServiceEnabled === 'boolean') {
767
+ schema.properties.historySeriesServiceEnabled.default = modeConfig.historySeriesServiceEnabled;
768
+ }
769
+ if (typeof modeConfig.registerAsHistoryApiProvider === 'boolean') {
770
+ schema.properties.registerAsHistoryApiProvider.default = modeConfig.registerAsHistoryApiProvider;
771
+ }
772
+ }
773
+ return schema;
774
+ },
731
775
  registerWithRouter(router) {
732
776
  server.debug(`[KIP][ROUTES] register displays=${API_PATHS.DISPLAYS} instance=${API_PATHS.INSTANCE} screenIndex=${API_PATHS.SCREEN_INDEX} activeScreen=${API_PATHS.ACTIVATE_SCREEN}`);
733
777
  // Validate/normalize :displayId where present
@@ -920,7 +964,7 @@ const start = (server) => {
920
964
  if (!ensureHistorySeriesServiceEnabledForRequest(res)) {
921
965
  return;
922
966
  }
923
- if (!(await ensureDuckDbReadyForRequest(res))) {
967
+ if (!(await ensureSqliteReadyForRequest(res))) {
924
968
  return;
925
969
  }
926
970
  return sendOk(res, historySeries.listSeries());
@@ -937,7 +981,7 @@ const start = (server) => {
937
981
  if (!ensureHistorySeriesServiceEnabledForRequest(res)) {
938
982
  return;
939
983
  }
940
- if (!(await ensureDuckDbReadyForRequest(res))) {
984
+ if (!(await ensureSqliteReadyForRequest(res))) {
941
985
  return;
942
986
  }
943
987
  const seriesId = String(req.params.seriesId ?? '').trim();
@@ -978,7 +1022,7 @@ const start = (server) => {
978
1022
  if (!ensureHistorySeriesServiceEnabledForRequest(res)) {
979
1023
  return;
980
1024
  }
981
- if (!(await ensureDuckDbReadyForRequest(res))) {
1025
+ if (!(await ensureSqliteReadyForRequest(res))) {
982
1026
  return;
983
1027
  }
984
1028
  const seriesId = String(req.params.seriesId ?? '').trim();
@@ -1006,14 +1050,14 @@ const start = (server) => {
1006
1050
  if (!ensureHistorySeriesServiceEnabledForRequest(res)) {
1007
1051
  return;
1008
1052
  }
1009
- if (!(await ensureDuckDbReadyForRequest(res))) {
1053
+ if (!(await ensureSqliteReadyForRequest(res))) {
1010
1054
  return;
1011
1055
  }
1012
1056
  const payload = req.body;
1013
1057
  if (!Array.isArray(payload)) {
1014
1058
  return sendFail(res, 400, 'Body must be an array of series definitions');
1015
1059
  }
1016
- const simulated = new history_series_service_1.HistorySeriesService(() => Date.now(), false);
1060
+ const simulated = new history_series_service_1.HistorySeriesService(() => Date.now());
1017
1061
  historySeries.listSeries().forEach(series => {
1018
1062
  simulated.upsertSeries(series);
1019
1063
  });