@mxtommy/kip 4.5.1 → 4.6.0-beta.2

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/CHANGELOG.md +4 -0
  2. package/package.json +13 -15
  3. package/plugin/history-series.service.js +14 -24
  4. package/plugin/index.js +198 -146
  5. package/plugin/{duckdb-parquet-storage.service.js → sqlite-history-storage.service.js} +327 -381
  6. package/public/{chunk-D7VDX7ZF.js → chunk-67V4XHCY.js} +1 -1
  7. package/public/{chunk-EQ2N7KDA.js → chunk-BEQKBGLG.js} +2 -2
  8. package/public/{chunk-JGGMFMY5.js → chunk-BJEHRCYP.js} +1 -1
  9. package/public/chunk-BTFZS2TW.js +16 -0
  10. package/public/chunk-FZFDGAQO.js +1 -0
  11. package/public/{chunk-VCY32MWT.js → chunk-FZSLNGBK.js} +8 -8
  12. package/public/{chunk-RONXIZ2U.js → chunk-J6EEFXKZ.js} +3 -3
  13. package/public/{chunk-IYRLINL7.js → chunk-KWTS7JF7.js} +1 -1
  14. package/public/chunk-NFJ4RQSE.js +4 -0
  15. package/public/{chunk-DEM56G4S.js → chunk-OPTBDYBL.js} +1 -1
  16. package/public/{chunk-YCEXTKGG.js → chunk-P4CRTB7N.js} +1 -1
  17. package/public/{chunk-IHURI4IH.js → chunk-P7JKENHI.js} +3 -3
  18. package/public/chunk-Q2ANAJAD.js +1 -0
  19. package/public/{chunk-B75MT7ND.js → chunk-R36UY4Q4.js} +1 -1
  20. package/public/{chunk-CHGXAEKT.js → chunk-RCYOZLZB.js} +1 -1
  21. package/public/chunk-RFNZ4AQG.js +50 -0
  22. package/public/{chunk-KPHICV76.js → chunk-SJFJEOSG.js} +1 -1
  23. package/public/{chunk-MGPPVLZ7.js → chunk-TBNKOU7M.js} +1 -1
  24. package/public/chunk-TVNXBPFF.js +6 -0
  25. package/public/{chunk-S72JTJPN.js → chunk-VPF5756E.js} +1 -1
  26. package/public/chunk-VXCYPAWR.js +1 -0
  27. package/public/{chunk-R7RQHWKJ.js → chunk-WH5CIUSB.js} +1 -1
  28. package/public/{chunk-LQDSU4WS.js → chunk-WQSJFJLW.js} +1 -1
  29. package/public/{chunk-KZ5DUKAX.js → chunk-XBSU7OGT.js} +1 -1
  30. package/public/{chunk-CEB42O2C.js → chunk-YI3MZWRZ.js} +1 -1
  31. package/public/index.html +1 -1
  32. package/public/main-TZOV3JCT.js +1 -0
  33. package/plugin/plugin-auth.service.js +0 -75
  34. package/public/chunk-A6DQJFP4.js +0 -16
  35. package/public/chunk-DEGYRCMI.js +0 -1
  36. package/public/chunk-DYTBBUMI.js +0 -4
  37. package/public/chunk-FNF7M3AE.js +0 -1
  38. package/public/chunk-JB4YVVNW.js +0 -1
  39. package/public/chunk-YKJKIWXO.js +0 -6
  40. package/public/chunk-ZV7IYYEQ.js +0 -50
  41. package/public/main-FQESQQV6.js +0 -1
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,114 +579,128 @@ 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
- storageService.setLogger({
587
- debug: (msg) => server.debug(msg),
588
- error: (msg) => server.error(msg)
589
- });
590
- const storageConfig = storageService.configure();
591
- server.debug(`[KIP][STORAGE] config engine=${storageConfig.engine} db=${storageConfig.databaseFile} parquetDir=${storageConfig.parquetDirectory} flushMs=${storageConfig.flushIntervalMs}`);
592
- historySeries.setSampleSink(sample => {
593
- storageService.enqueueSample(sample);
594
- });
595
- duckDbInitializationPromise = storageService.initialize();
596
- void duckDbInitializationPromise.then((ready) => {
597
- server.debug(`[KIP][STORAGE] duckdbReady=${ready}`);
598
- if (ready && storageService.isDuckDbParquetEnabled()) {
599
- if (isHistorySeriesServiceEnabled()) {
600
- void storageService.getSeriesDefinitions()
601
- .then((storedSeries) => {
602
- if (storedSeries.length > 0) {
603
- historySeries.reconcileSeries(storedSeries);
604
- rebuildSeriesCaptureSubscriptions();
605
- }
606
- startStorageFlushTimer(storageConfig.flushIntervalMs);
607
- logOperationalMode('duckdb-ready');
608
- server.setPluginStatus(`KIP plugin started with DuckDB/Parquet history storage. Loaded ${storedSeries.length} persisted series. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
609
- })
610
- .catch((loadError) => {
611
- server.error(`[SERIES STORAGE] failed to load persisted series: ${String(loadError.message || loadError)}`);
612
+ const needsSqlite = (modeConfig.historySeriesServiceEnabled || modeConfig.registerAsHistoryApiProvider) && modeConfig.nodeSqliteAvailable;
613
+ if (needsSqlite) {
614
+ storageService.setLogger({
615
+ debug: (msg) => server.debug(msg),
616
+ error: (msg) => server.error(msg)
617
+ });
618
+ const storageConfig = storageService.configure();
619
+ server.debug(`[KIP][STORAGE] config engine=${storageConfig.engine} db=${storageConfig.databaseFile} flushMs=${storageConfig.flushIntervalMs}`);
620
+ historySeries.setSampleSink(sample => {
621
+ storageService.enqueueSample(sample);
622
+ });
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) {
628
+ void storageService.getSeriesDefinitions()
629
+ .then((storedSeries) => {
630
+ if (storedSeries.length > 0) {
631
+ historySeries.reconcileSeries(storedSeries);
632
+ rebuildSeriesCaptureSubscriptions();
633
+ }
634
+ startStorageFlushTimer(storageConfig.flushIntervalMs);
635
+ logOperationalMode('sqlite-ready');
636
+ server.setPluginStatus(`KIP plugin started with node:sqlite history storage. Loaded ${storedSeries.length} persisted series. historySeriesServiceEnabled=${modeConfig.historySeriesServiceEnabled} historyApiProviderEnabled=${modeConfig.registerAsHistoryApiProvider} historyApiProviderRegistered=${historyApiProviderRegistered}`);
637
+ })
638
+ .catch((loadError) => {
639
+ server.error(`[SERIES STORAGE] failed to load persisted series: ${String(loadError.message || loadError)}`);
640
+ startStorageFlushTimer(storageConfig.flushIntervalMs);
641
+ logOperationalMode('sqlite-ready-series-load-failed');
642
+ server.setPluginStatus(`KIP plugin started with node:sqlite history storage. historySeriesServiceEnabled=${modeConfig.historySeriesServiceEnabled} historyApiProviderEnabled=${modeConfig.registerAsHistoryApiProvider} historyApiProviderRegistered=${historyApiProviderRegistered}`);
643
+ });
644
+ }
645
+ else {
646
+ historySeries.reconcileSeries([]);
647
+ stopSeriesCapture();
612
648
  startStorageFlushTimer(storageConfig.flushIntervalMs);
613
- logOperationalMode('duckdb-ready-series-load-failed');
614
- server.setPluginStatus(`KIP plugin started with DuckDB/Parquet history storage. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
615
- });
649
+ logOperationalMode('sqlite-ready-series-disabled');
650
+ server.setPluginStatus(`KIP plugin started with history-series service disabled. historyApiProviderEnabled=${modeConfig.registerAsHistoryApiProvider} historyApiProviderRegistered=${historyApiProviderRegistered}`);
651
+ }
652
+ return;
616
653
  }
617
- else {
618
- historySeries.reconcileSeries([]);
619
- stopSeriesCapture();
620
- startStorageFlushTimer(storageConfig.flushIntervalMs);
621
- logOperationalMode('duckdb-ready-series-disabled');
622
- server.setPluginStatus(`KIP plugin started with history-series service disabled. historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
654
+ if (storageFlushTimer) {
655
+ clearInterval(storageFlushTimer);
656
+ storageFlushTimer = null;
623
657
  }
624
- return;
625
- }
626
- if (storageFlushTimer) {
627
- clearInterval(storageFlushTimer);
628
- storageFlushTimer = null;
629
- }
630
- const initError = storageService.getLastInitError();
631
- if (initError) {
632
- server.setPluginError(`DuckDB unavailable. ${initError}`);
633
- logOperationalMode('duckdb-unavailable');
634
- server.setPluginStatus(`KIP plugin started with DuckDB unavailable. historySeriesServiceEnabled=${isHistorySeriesServiceEnabled()} historyApiProviderEnabled=${isHistoryApiProviderEnabled()} historyApiProviderRegistered=${historyApiProviderRegistered}`);
658
+ const initError = storageService.getLastInitError();
659
+ if (initError) {
660
+ server.setPluginError(`node:sqlite unavailable. ${initError}`);
661
+ logOperationalMode('sqlite-unavailable');
662
+ server.setPluginStatus(`KIP plugin started with node:sqlite unavailable. historySeriesServiceEnabled=${modeConfig?.historySeriesServiceEnabled} historyApiProviderEnabled=${modeConfig?.registerAsHistoryApiProvider} historyApiProviderRegistered=${historyApiProviderRegistered}`);
663
+ }
664
+ });
665
+ if (retentionSweepTimer) {
666
+ clearInterval(retentionSweepTimer);
635
667
  }
636
- });
637
- if (retentionSweepTimer) {
638
- clearInterval(retentionSweepTimer);
639
- }
640
- retentionSweepTimer = setInterval(() => {
641
- try {
642
- if (storageService.isDuckDbParquetReady()) {
643
- const lifecycleToken = storageService.getLifecycleToken();
644
- void storageService.pruneExpiredSamples(Date.now(), lifecycleToken)
645
- .then(removedPersistedRows => {
646
- if (removedPersistedRows > 0) {
647
- server.debug(`[KIP][RETENTION] pruneExpired removedRows=${removedPersistedRows}`);
648
- }
649
- return storageService.pruneOrphanedSamples(lifecycleToken)
650
- .then(removedOrphanRows => {
651
- if (removedOrphanRows > 0) {
652
- server.debug(`[KIP][RETENTION] pruneOrphaned removedRows=${removedOrphanRows}`);
668
+ retentionSweepTimer = setInterval(() => {
669
+ try {
670
+ if (storageService.isSqliteReady()) {
671
+ const lifecycleToken = storageService.getLifecycleToken();
672
+ void storageService.pruneExpiredSamples(Date.now(), lifecycleToken)
673
+ .then(removedPersistedRows => {
674
+ if (removedPersistedRows > 0) {
675
+ server.debug(`[KIP][RETENTION] pruneExpired removedRows=${removedPersistedRows}`);
653
676
  }
677
+ return storageService.pruneOrphanedSamples(lifecycleToken)
678
+ .then(removedOrphanRows => {
679
+ if (removedOrphanRows > 0) {
680
+ server.debug(`[KIP][RETENTION] pruneOrphaned removedRows=${removedOrphanRows}`);
681
+ }
682
+ });
683
+ })
684
+ .catch(error => {
685
+ server.error(`[SERIES RETENTION] node:sqlite Prune failed: ${String(error.message || error)}`);
654
686
  });
655
- })
656
- .catch(error => {
657
- server.error(`[SERIES RETENTION] duckdbPrune failed: ${String(error.message || error)}`);
658
- });
687
+ }
659
688
  }
689
+ catch (error) {
690
+ server.error(`[SERIES RETENTION] node:sqlite sweep failed: ${String(error.message || error)}`);
691
+ }
692
+ }, 60 * 60_000);
693
+ retentionSweepTimer.unref?.();
694
+ rebuildSeriesCaptureSubscriptions();
695
+ }
696
+ else {
697
+ if (modeConfig && !modeConfig.nodeSqliteAvailable && (modeConfig.historySeriesServiceEnabled || modeConfig.registerAsHistoryApiProvider)) {
698
+ server.setPluginError(getSqliteUnavailableMessage());
660
699
  }
661
- catch (error) {
662
- server.error(`[SERIES RETENTION] sweep failed: ${String(error.message || error)}`);
663
- }
664
- }, 60 * 60_000);
665
- retentionSweepTimer.unref?.();
666
- rebuildSeriesCaptureSubscriptions();
700
+ server.debug('[KIP][STORAGE] sqlite init skipped reason=config-disabled-or-runtime');
701
+ sqliteInitializationPromise = null;
702
+ stopSeriesCapture();
703
+ }
667
704
  if (server.registerPutHandler) {
668
705
  server.debug(`[KIP][COMMAND] registerPutHandlers context=${PUT_CONTEXT}`);
669
706
  server.registerPutHandler(PUT_CONTEXT, COMMAND_PATHS.SET_DISPLAY, (context, path, value) => {
@@ -715,11 +752,26 @@ const start = (server) => {
715
752
  }
716
753
  historyApiRegistry = null;
717
754
  }
718
- duckDbInitializationPromise = null;
755
+ sqliteInitializationPromise = null;
719
756
  const msg = 'Stopped.';
720
757
  server.setPluginStatus(msg);
721
758
  },
722
- 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
+ },
723
775
  registerWithRouter(router) {
724
776
  server.debug(`[KIP][ROUTES] register displays=${API_PATHS.DISPLAYS} instance=${API_PATHS.INSTANCE} screenIndex=${API_PATHS.SCREEN_INDEX} activeScreen=${API_PATHS.ACTIVATE_SCREEN}`);
725
777
  // Validate/normalize :displayId where present
@@ -912,7 +964,7 @@ const start = (server) => {
912
964
  if (!ensureHistorySeriesServiceEnabledForRequest(res)) {
913
965
  return;
914
966
  }
915
- if (!(await ensureDuckDbReadyForRequest(res))) {
967
+ if (!(await ensureSqliteReadyForRequest(res))) {
916
968
  return;
917
969
  }
918
970
  return sendOk(res, historySeries.listSeries());
@@ -929,7 +981,7 @@ const start = (server) => {
929
981
  if (!ensureHistorySeriesServiceEnabledForRequest(res)) {
930
982
  return;
931
983
  }
932
- if (!(await ensureDuckDbReadyForRequest(res))) {
984
+ if (!(await ensureSqliteReadyForRequest(res))) {
933
985
  return;
934
986
  }
935
987
  const seriesId = String(req.params.seriesId ?? '').trim();
@@ -970,7 +1022,7 @@ const start = (server) => {
970
1022
  if (!ensureHistorySeriesServiceEnabledForRequest(res)) {
971
1023
  return;
972
1024
  }
973
- if (!(await ensureDuckDbReadyForRequest(res))) {
1025
+ if (!(await ensureSqliteReadyForRequest(res))) {
974
1026
  return;
975
1027
  }
976
1028
  const seriesId = String(req.params.seriesId ?? '').trim();
@@ -998,14 +1050,14 @@ const start = (server) => {
998
1050
  if (!ensureHistorySeriesServiceEnabledForRequest(res)) {
999
1051
  return;
1000
1052
  }
1001
- if (!(await ensureDuckDbReadyForRequest(res))) {
1053
+ if (!(await ensureSqliteReadyForRequest(res))) {
1002
1054
  return;
1003
1055
  }
1004
1056
  const payload = req.body;
1005
1057
  if (!Array.isArray(payload)) {
1006
1058
  return sendFail(res, 400, 'Body must be an array of series definitions');
1007
1059
  }
1008
- const simulated = new history_series_service_1.HistorySeriesService(() => Date.now(), false);
1060
+ const simulated = new history_series_service_1.HistorySeriesService(() => Date.now());
1009
1061
  historySeries.listSeries().forEach(series => {
1010
1062
  simulated.upsertSeries(series);
1011
1063
  });