@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.
- package/CHANGELOG.md +12 -3
- package/package.json +13 -15
- package/plugin/history-series.service.js +14 -24
- package/plugin/index.js +139 -95
- package/plugin/{duckdb-parquet-storage.service.js → sqlite-history-storage.service.js} +330 -503
- package/public/assets/help-docs/widget-historical-series.md +5 -5
- package/public/{chunk-TVNXBPFF.js → chunk-356CW47X.js} +1 -1
- package/public/{chunk-NFJ4RQSE.js → chunk-3JA4CQ7T.js} +1 -1
- package/public/{chunk-VXTTEFRP.js → chunk-5SAXWR6Z.js} +8 -8
- package/public/{chunk-67V4XHCY.js → chunk-6A4NRSCL.js} +1 -1
- package/public/{chunk-P7JKENHI.js → chunk-AC6VD2FN.js} +1 -1
- package/public/{chunk-TBNKOU7M.js → chunk-B4NYOD6L.js} +1 -1
- package/public/{chunk-WH5CIUSB.js → chunk-BGGO4PGD.js} +1 -1
- package/public/{chunk-KQEEYPK3.js → chunk-BMHMHQFO.js} +1 -1
- package/public/{chunk-RCYOZLZB.js → chunk-CSIELI2Z.js} +2 -2
- package/public/{chunk-R36UY4Q4.js → chunk-CYTLQDGF.js} +1 -1
- package/public/{chunk-YI3MZWRZ.js → chunk-HSKVTFFQ.js} +1 -1
- package/public/{chunk-IH4CEW4C.js → chunk-MDNGWQNG.js} +8 -8
- package/public/{chunk-VPF5756E.js → chunk-MGLD6QDJ.js} +1 -1
- package/public/{chunk-P4CRTB7N.js → chunk-NJISHUGY.js} +1 -1
- package/public/{chunk-ISF5E3CX.js → chunk-P3M6SJQT.js} +11 -11
- package/public/{chunk-WQSJFJLW.js → chunk-POMIQBAL.js} +2 -2
- package/public/{chunk-SJFJEOSG.js → chunk-PPF5S5CV.js} +1 -1
- package/public/{chunk-OPTBDYBL.js → chunk-PUPM3HUQ.js} +1 -1
- package/public/chunk-PZ6I6W3H.js +16 -0
- package/public/{chunk-VXCYPAWR.js → chunk-QU3JR4YV.js} +1 -1
- package/public/{chunk-Q2ANAJAD.js → chunk-SUWMN3AE.js} +1 -1
- package/public/{chunk-CD5TQSCS.js → chunk-UYHRT3PR.js} +1 -1
- package/public/{chunk-FZFDGAQO.js → chunk-WJFXI5PQ.js} +1 -1
- package/public/{chunk-I4SJ5UNN.js → chunk-ZXO4VMEH.js} +1 -1
- package/public/{chunk-XBSU7OGT.js → chunk-ZY3U4H4Z.js} +1 -1
- package/public/index.html +1 -1
- package/public/{main-B6TXB3EB.js → main-I33LH3HC.js} +1 -1
- package/plugin/plugin-auth.service.js +0 -75
- package/public/chunk-BTFZS2TW.js +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
# v4.
|
|
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.
|
|
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.
|
|
70
|
-
"@angular/common": "21.2.
|
|
71
|
-
"@angular/compiler": "21.2.
|
|
72
|
-
"@angular/core": "21.2.
|
|
73
|
-
"@angular/forms": "21.2.
|
|
74
|
-
"@angular/material": "21.2.
|
|
75
|
-
"@angular/animations": "21.2.
|
|
76
|
-
"@angular/platform-browser": "21.2.
|
|
77
|
-
"@angular/platform-browser-dynamic": "21.2.
|
|
78
|
-
"@angular/router": "21.2.
|
|
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.
|
|
84
|
-
"@angular/language-service": "21.2.
|
|
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()
|
|
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
|
-
|
|
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
|
|
315
|
-
const
|
|
316
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
//
|
|
321
|
-
sampleTime = Math.max(
|
|
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
|
|
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
|
|
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
|
|
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()
|
|
79
|
-
const storageService = new
|
|
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
|
|
83
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
103
|
+
return null;
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
|
-
function
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
177
|
+
sqliteInitializationPromise,
|
|
159
178
|
new Promise(resolvePromise => {
|
|
160
179
|
setTimeout(() => resolvePromise(false), timeoutMs);
|
|
161
180
|
})
|
|
162
181
|
]);
|
|
163
|
-
if (!ready && !storageService.
|
|
164
|
-
server.error(`[SERIES STORAGE]
|
|
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('
|
|
201
|
+
if (normalized.includes('sqlite')
|
|
183
202
|
|| normalized.includes('storage unavailable')
|
|
184
203
|
|| normalized.includes('not initialized')
|
|
185
|
-
||
|
|
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
|
|
204
|
-
await
|
|
205
|
-
if (storageService.
|
|
222
|
+
async function ensureSqliteReadyForRequest(res) {
|
|
223
|
+
await waitForSqliteInitialization();
|
|
224
|
+
if (storageService.isSqliteReady()) {
|
|
206
225
|
return true;
|
|
207
226
|
}
|
|
208
|
-
sendFail(res, 503,
|
|
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
|
|
397
|
-
if (!storageService.
|
|
398
|
-
throw new Error(
|
|
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
|
|
412
|
-
if (!storageService.
|
|
413
|
-
throw new Error(
|
|
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
|
|
427
|
-
if (!storageService.
|
|
428
|
-
throw new Error(
|
|
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('
|
|
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.
|
|
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
|
|
587
|
-
if (
|
|
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}
|
|
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
|
-
|
|
598
|
-
void
|
|
599
|
-
server.debug(`[KIP][STORAGE]
|
|
600
|
-
if (ready && storageService.
|
|
601
|
-
if (
|
|
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('
|
|
610
|
-
server.setPluginStatus(`
|
|
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('
|
|
616
|
-
server.setPluginStatus(`
|
|
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('
|
|
624
|
-
server.setPluginStatus(`
|
|
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(`
|
|
635
|
-
logOperationalMode('
|
|
636
|
-
server.setPluginStatus(`
|
|
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.
|
|
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]
|
|
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
|
-
|
|
672
|
-
|
|
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
|
-
|
|
755
|
+
sqliteInitializationPromise = null;
|
|
727
756
|
const msg = 'Stopped.';
|
|
728
757
|
server.setPluginStatus(msg);
|
|
729
758
|
},
|
|
730
|
-
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
|
|
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
|
|
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
|
|
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
|
|
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()
|
|
1060
|
+
const simulated = new history_series_service_1.HistorySeriesService(() => Date.now());
|
|
1017
1061
|
historySeries.listSeries().forEach(series => {
|
|
1018
1062
|
simulated.upsertSeries(series);
|
|
1019
1063
|
});
|