@mxtommy/kip 4.5.0-beta.1 → 4.5.1
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 +18 -0
- package/README.md +11 -7
- package/package.json +21 -8
- package/plugin/duckdb-parquet-storage.service.js +1182 -0
- package/plugin/history-series.service.js +439 -0
- package/plugin/index.js +705 -30
- package/plugin/openApi.json +253 -3
- package/plugin/plugin-auth.service.js +75 -0
- package/public/assets/help-docs/chartplotter.md +5 -18
- package/public/assets/help-docs/community.md +0 -3
- package/public/assets/help-docs/configuration.md +1 -1
- package/public/assets/help-docs/contact-us.md +0 -4
- package/public/assets/help-docs/dashboards.md +20 -18
- package/public/assets/help-docs/datainspector.md +7 -5
- package/public/assets/help-docs/history-api.md +116 -0
- package/public/assets/help-docs/menu.json +18 -6
- package/public/assets/help-docs/nodered-control-flows.md +125 -0
- package/public/assets/help-docs/putcontrols.md +101 -60
- package/public/assets/help-docs/welcome.md +6 -7
- package/public/assets/help-docs/widget-historical-series.md +66 -0
- package/public/assets/help-docs/zones.md +5 -10
- package/public/chunk-A6DQJFP4.js +16 -0
- package/public/chunk-B75MT7ND.js +1 -0
- package/public/{chunk-T6TFVZVM.js → chunk-CEB42O2C.js} +1 -1
- package/public/chunk-CHGXAEKT.js +2 -0
- package/public/chunk-D7VDX7ZF.js +5 -0
- package/public/{chunk-ZQER6AIQ.js → chunk-DEGYRCMI.js} +1 -1
- package/public/{chunk-M2B5OYGO.js → chunk-DEM56G4S.js} +1 -1
- package/public/chunk-DYTBBUMI.js +4 -0
- package/public/chunk-EQ2N7KDA.js +3 -0
- package/public/chunk-FNF7M3AE.js +1 -0
- package/public/chunk-IHURI4IH.js +5 -0
- package/public/{chunk-YIYYVDFO.js → chunk-IYRLINL7.js} +2 -2
- package/public/{chunk-5FEX27I4.js → chunk-JB4YVVNW.js} +1 -1
- package/public/chunk-JGGMFMY5.js +1 -0
- package/public/chunk-KPHICV76.js +5 -0
- package/public/{chunk-QZKCRH3H.js → chunk-KZ5DUKAX.js} +1 -1
- package/public/{chunk-HMOOTAEA.js → chunk-LQDSU4WS.js} +3 -3
- package/public/{chunk-IXQ7KIFY.js → chunk-MGPPVLZ7.js} +1 -1
- package/public/{chunk-QVCLOCEC.js → chunk-R7RQHWKJ.js} +1 -1
- package/public/chunk-RONXIZ2U.js +9 -0
- package/public/chunk-S72JTJPN.js +6 -0
- package/public/{chunk-KFFAA7DL.js → chunk-VCY32MWT.js} +8 -8
- package/public/chunk-YCEXTKGG.js +1 -0
- package/public/chunk-YKJKIWXO.js +6 -0
- package/public/chunk-ZV7IYYEQ.js +50 -0
- package/public/index.html +1 -1
- package/public/main-FQESQQV6.js +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -84
- package/.github/ISSUE_TEMPLATE/config.yml +0 -5
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -35
- package/.github/copilot-instructions.md +0 -205
- package/.github/instructions/angular.instructions.md +0 -123
- package/.github/instructions/best-practices.instructions.md +0 -59
- package/.github/instructions/project.instructions.md +0 -432
- package/.github/workflows/ci.yml +0 -37
- package/docs/widget-schematic.md +0 -102
- package/images/ActionSidenav.png +0 -0
- package/images/ChartplotterMode.png +0 -0
- package/images/KIPDemo.png +0 -0
- package/images/KipBrightness-1024.png +0 -0
- package/images/KipConfig-Units-1024.png +0 -0
- package/images/KipConfig-display-1024x488.png +0 -0
- package/images/KipFreeboard-SK-1024.png +0 -0
- package/images/KipGaugeSample1-1024x545.png +0 -0
- package/images/KipGaugeSample2-1024x488.png +0 -0
- package/images/KipGaugeSample3-1024x508.png +0 -0
- package/images/KipNightMode-1024.png +0 -0
- package/images/KipWidgetConfig-layout-1024.png +0 -0
- package/images/KipWidgetConfig-paths-1024x488.png +0 -0
- package/images/Options.png +0 -0
- package/images/exterior_user_installs.png +0 -0
- package/images/formfactor.png +0 -0
- package/public/assets/help-docs/datasets.md +0 -95
- package/public/chunk-2OB7ZJBR.js +0 -3
- package/public/chunk-6GGJZDRE.js +0 -1
- package/public/chunk-6V4GGGXE.js +0 -2
- package/public/chunk-A5BW6BUM.js +0 -1
- package/public/chunk-DGE5YFPU.js +0 -5
- package/public/chunk-G6M3Z3BY.js +0 -53
- package/public/chunk-GMGZLXY7.js +0 -4
- package/public/chunk-GUZ3BDVZ.js +0 -2
- package/public/chunk-ICDGHQFP.js +0 -6
- package/public/chunk-JCNE4QHQ.js +0 -15
- package/public/chunk-K6XYUNG4.js +0 -8
- package/public/chunk-LGCQEN7V.js +0 -4
- package/public/chunk-O3JH7UTR.js +0 -1
- package/public/chunk-Q3USFT4F.js +0 -2
- package/public/chunk-VIKU7BH7.js +0 -1
- package/public/chunk-XMQPXXLW.js +0 -8
- package/public/main-4URMGBQS.js +0 -1
- package/rm-npmjs-beta.sh +0 -50
- package/tools/schematics/collection.json +0 -9
- package/tools/schematics/create-host2-widget/files/readme/README.md.template +0 -109
- package/tools/schematics/create-host2-widget/files/spec/widget-__name@dasherize__.component.spec.ts +0 -38
- package/tools/schematics/create-host2-widget/files/widget/widget-__name@dasherize__.component.html +0 -6
- package/tools/schematics/create-host2-widget/files/widget/widget-__name@dasherize__.component.scss +0 -5
- package/tools/schematics/create-host2-widget/files/widget/widget-__name@dasherize__.component.ts.template +0 -94
- package/tools/schematics/create-host2-widget/index.js +0 -138
- package/tools/schematics/create-host2-widget/schema.json +0 -89
- package/tools/schematics/create-host2-widget/test/create-host2-widget.spec.ts +0 -70
- package/tools/schematics/create-host2-widget/utils/formatting.js +0 -119
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HistorySeriesService = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Manages history capture series definitions and serves History API-compatible query results.
|
|
6
|
+
*/
|
|
7
|
+
class HistorySeriesService {
|
|
8
|
+
nowProvider;
|
|
9
|
+
retainSamplesInMemory;
|
|
10
|
+
seriesById = new Map();
|
|
11
|
+
lastAcceptedTimestampBySeriesKey = new Map();
|
|
12
|
+
sampleSink = null;
|
|
13
|
+
constructor(nowProvider = () => Date.now(), retainSamplesInMemory = true) {
|
|
14
|
+
this.nowProvider = nowProvider;
|
|
15
|
+
this.retainSamplesInMemory = retainSamplesInMemory;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns all configured series sorted by `seriesId`.
|
|
19
|
+
*
|
|
20
|
+
* @returns Ordered list of series definitions.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* const list = service.listSeries();
|
|
24
|
+
* console.log(list.length);
|
|
25
|
+
*/
|
|
26
|
+
listSeries() {
|
|
27
|
+
return Array.from(this.seriesById.values()).sort((left, right) => {
|
|
28
|
+
return left.seriesId.localeCompare(right.seriesId);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Finds a series by identifier.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} seriesId Series identifier.
|
|
35
|
+
* @returns {ISeriesDefinition | null} Matching series or null.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const row = service.findSeriesById('series-1');
|
|
39
|
+
*/
|
|
40
|
+
findSeriesById(seriesId) {
|
|
41
|
+
return this.seriesById.get(seriesId) ?? null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Creates or updates a series definition.
|
|
45
|
+
*
|
|
46
|
+
* @param {ISeriesDefinition} input The incoming series definition payload.
|
|
47
|
+
* @returns {ISeriesDefinition} The normalized and stored series definition.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* service.upsertSeries({
|
|
51
|
+
* seriesId: 'abc',
|
|
52
|
+
* datasetUuid: 'abc',
|
|
53
|
+
* ownerWidgetUuid: 'widget-1',
|
|
54
|
+
* path: 'navigation.speedOverGround'
|
|
55
|
+
* });
|
|
56
|
+
*/
|
|
57
|
+
upsertSeries(input) {
|
|
58
|
+
const normalized = this.normalizeSeries(input);
|
|
59
|
+
const key = normalized.seriesId;
|
|
60
|
+
this.seriesById.set(key, normalized);
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Registers a callback invoked for every accepted sample.
|
|
65
|
+
*
|
|
66
|
+
* @param {(sample: IRecordedSeriesSample) => void | null} sink Callback used to forward samples to persistent storage.
|
|
67
|
+
* @returns {void}
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* service.setSampleSink(sample => console.log(sample.seriesId));
|
|
71
|
+
*/
|
|
72
|
+
setSampleSink(sink) {
|
|
73
|
+
this.sampleSink = sink;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Deletes an existing series definition and all captured samples for the series.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} seriesId Unique series identifier.
|
|
79
|
+
* @returns {boolean} True when a series existed and was deleted.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const deleted = service.deleteSeries('abc');
|
|
83
|
+
*/
|
|
84
|
+
deleteSeries(seriesId) {
|
|
85
|
+
const keysToDelete = Array.from(this.seriesById.entries())
|
|
86
|
+
.filter(([, series]) => series.seriesId === seriesId)
|
|
87
|
+
.map(([key]) => key);
|
|
88
|
+
if (keysToDelete.length === 0) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
keysToDelete.forEach(key => {
|
|
92
|
+
this.seriesById.delete(key);
|
|
93
|
+
this.lastAcceptedTimestampBySeriesKey.delete(key);
|
|
94
|
+
});
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reconciles the entire desired series set against the current state.
|
|
99
|
+
*
|
|
100
|
+
* @param {ISeriesDefinition[]} desiredSeries Full desired series payload list.
|
|
101
|
+
* @returns {{ created: number; updated: number; deleted: number; total: number }} Reconciliation summary.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* const result = service.reconcileSeries([{ seriesId: 's1', datasetUuid: 's1', ownerWidgetUuid: 'w1', path: 'p' }]);
|
|
105
|
+
* console.log(result.created, result.deleted);
|
|
106
|
+
*/
|
|
107
|
+
reconcileSeries(desiredSeries) {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const desiredById = new Map();
|
|
110
|
+
desiredSeries.forEach(entry => {
|
|
111
|
+
const normalized = this.normalizeSeries(entry);
|
|
112
|
+
const key = normalized.seriesId;
|
|
113
|
+
desiredById.set(key, normalized);
|
|
114
|
+
});
|
|
115
|
+
let created = 0;
|
|
116
|
+
let updated = 0;
|
|
117
|
+
let deleted = 0;
|
|
118
|
+
desiredById.forEach((desired, seriesKey) => {
|
|
119
|
+
const existing = this.seriesById.get(seriesKey);
|
|
120
|
+
// Always update reconcile_ts on reconcile
|
|
121
|
+
const desiredWithReconcile = { ...desired, reconcileTs: now };
|
|
122
|
+
if (!existing) {
|
|
123
|
+
this.seriesById.set(seriesKey, desiredWithReconcile);
|
|
124
|
+
created += 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (JSON.stringify(existing) !== JSON.stringify(desired)) {
|
|
128
|
+
this.seriesById.set(seriesKey, desiredWithReconcile);
|
|
129
|
+
updated += 1;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Even if not updated, update reconcileTs
|
|
133
|
+
this.seriesById.set(seriesKey, { ...existing, reconcileTs: now });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
Array.from(this.seriesById.keys()).forEach(seriesKey => {
|
|
137
|
+
if (!desiredById.has(seriesKey)) {
|
|
138
|
+
this.seriesById.delete(seriesKey);
|
|
139
|
+
this.lastAcceptedTimestampBySeriesKey.delete(seriesKey);
|
|
140
|
+
deleted += 1;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
created,
|
|
145
|
+
updated,
|
|
146
|
+
deleted,
|
|
147
|
+
total: this.seriesById.size
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Records a single numeric sample for a configured series.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} seriesId Unique series identifier.
|
|
154
|
+
* @param {number} value Numeric sample value.
|
|
155
|
+
* @param {number} timestamp Sample timestamp in milliseconds.
|
|
156
|
+
* @returns {boolean} True when sample was accepted.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* service.recordSample('abc', 12.4, Date.now());
|
|
160
|
+
*/
|
|
161
|
+
recordSample(seriesId, value, timestamp) {
|
|
162
|
+
const seriesEntry = Array.from(this.seriesById.entries())
|
|
163
|
+
.find(([, series]) => series.seriesId === seriesId);
|
|
164
|
+
if (!seriesEntry) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return this.recordSampleByKey(seriesEntry[0], value, timestamp);
|
|
168
|
+
}
|
|
169
|
+
recordSampleByKey(seriesKey, value, timestamp) {
|
|
170
|
+
const series = this.seriesById.get(seriesKey);
|
|
171
|
+
if (!series || series.enabled === false || !Number.isFinite(value) || !Number.isFinite(timestamp)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const samplingIntervalMs = this.resolveSampleTimeMs(series.sampleTime);
|
|
175
|
+
const previousTimestamp = this.lastAcceptedTimestampBySeriesKey.get(seriesKey);
|
|
176
|
+
if (previousTimestamp !== undefined && (timestamp - previousTimestamp) < samplingIntervalMs) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const context = series.context ?? 'vessels.self';
|
|
180
|
+
const source = series.source ?? 'default';
|
|
181
|
+
this.sampleSink?.({
|
|
182
|
+
seriesId: series.seriesId,
|
|
183
|
+
datasetUuid: series.datasetUuid,
|
|
184
|
+
ownerWidgetUuid: series.ownerWidgetUuid,
|
|
185
|
+
path: series.path,
|
|
186
|
+
context,
|
|
187
|
+
source,
|
|
188
|
+
timestamp,
|
|
189
|
+
value
|
|
190
|
+
});
|
|
191
|
+
this.lastAcceptedTimestampBySeriesKey.set(seriesKey, timestamp);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Records a sample based on a Signal K stream value by matching path/context/source against configured series.
|
|
196
|
+
*
|
|
197
|
+
* @param {{ path?: unknown; value?: unknown; timestamp?: unknown; context?: unknown; source?: unknown; $source?: unknown }} sample Signal K normalized value entry.
|
|
198
|
+
* @returns {number} Number of configured series that accepted the sample.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* const count = service.recordFromSignalKSample({ path: 'navigation.speedOverGround', value: 6.2, timestamp: new Date().toISOString() });
|
|
202
|
+
*/
|
|
203
|
+
recordFromSignalKSample(sample) {
|
|
204
|
+
const path = this.normalizePathIdentifier(typeof sample.path === 'string' ? sample.path : '');
|
|
205
|
+
if (!path) {
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
const ts = this.resolveTimestamp(sample.timestamp);
|
|
209
|
+
const context = typeof sample.context === 'string' && sample.context ? sample.context : 'vessels.self';
|
|
210
|
+
const source = this.resolveSource(sample);
|
|
211
|
+
const leafSamples = this.extractNumericLeafSamples(path, sample.value);
|
|
212
|
+
if (leafSamples.length === 0) {
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
let recorded = 0;
|
|
216
|
+
leafSamples.forEach(leaf => {
|
|
217
|
+
this.seriesById.forEach((series, seriesKey) => {
|
|
218
|
+
if (series.path !== leaf.path || series.enabled === false) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const seriesContext = series.context ?? 'vessels.self';
|
|
222
|
+
if (!this.isContextMatch(seriesContext, context)) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const seriesSource = series.source ?? 'default';
|
|
226
|
+
if (!this.isSourceMatch(seriesSource, source)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (this.recordSampleByKey(seriesKey, leaf.value, ts)) {
|
|
230
|
+
recorded += 1;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
return recorded;
|
|
235
|
+
}
|
|
236
|
+
extractNumericLeafSamples(basePath, value) {
|
|
237
|
+
const samples = [];
|
|
238
|
+
const addNumeric = (samplePath, sampleValue) => {
|
|
239
|
+
const normalizedPath = this.normalizePathIdentifier(samplePath);
|
|
240
|
+
if (!normalizedPath) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const numericValue = Number(sampleValue);
|
|
244
|
+
if (!Number.isFinite(numericValue)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
samples.push({ path: normalizedPath, value: numericValue });
|
|
248
|
+
};
|
|
249
|
+
const walk = (currentPath, currentValue) => {
|
|
250
|
+
if (currentValue && typeof currentValue === 'object') {
|
|
251
|
+
if (Array.isArray(currentValue)) {
|
|
252
|
+
currentValue.forEach((entry, index) => {
|
|
253
|
+
walk(`${currentPath}.${index}`, entry);
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
Object.entries(currentValue).forEach(([key, child]) => {
|
|
258
|
+
walk(`${currentPath}.${key}`, child);
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
addNumeric(currentPath, currentValue);
|
|
263
|
+
};
|
|
264
|
+
walk(basePath, value);
|
|
265
|
+
return samples;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Returns all known history paths.
|
|
269
|
+
*
|
|
270
|
+
* @returns {string[]} Ordered unique path list.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* const paths = service.getPaths();
|
|
274
|
+
*/
|
|
275
|
+
getPaths() {
|
|
276
|
+
const paths = new Set();
|
|
277
|
+
this.seriesById.forEach(series => {
|
|
278
|
+
paths.add(series.path);
|
|
279
|
+
});
|
|
280
|
+
return Array.from(paths).sort();
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Returns all known history contexts.
|
|
284
|
+
*
|
|
285
|
+
* @returns {string[]} Ordered unique context list.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* const contexts = service.getContexts();
|
|
289
|
+
*/
|
|
290
|
+
getContexts() {
|
|
291
|
+
const contexts = new Set();
|
|
292
|
+
this.seriesById.forEach(series => {
|
|
293
|
+
contexts.add(series.context ?? 'vessels.self');
|
|
294
|
+
});
|
|
295
|
+
return Array.from(contexts).sort();
|
|
296
|
+
}
|
|
297
|
+
normalizeSeries(input) {
|
|
298
|
+
const seriesId = (input.seriesId || input.datasetUuid || '').trim();
|
|
299
|
+
if (!seriesId) {
|
|
300
|
+
throw new Error('seriesId is required');
|
|
301
|
+
}
|
|
302
|
+
const datasetUuid = (input.datasetUuid || seriesId).trim();
|
|
303
|
+
if (!datasetUuid) {
|
|
304
|
+
throw new Error('datasetUuid is required');
|
|
305
|
+
}
|
|
306
|
+
const ownerWidgetUuid = (input.ownerWidgetUuid || '').trim();
|
|
307
|
+
if (!ownerWidgetUuid) {
|
|
308
|
+
throw new Error('ownerWidgetUuid is required');
|
|
309
|
+
}
|
|
310
|
+
const path = this.normalizePathIdentifier(input.path || '');
|
|
311
|
+
if (!path) {
|
|
312
|
+
throw new Error('path is required');
|
|
313
|
+
}
|
|
314
|
+
// Determine if this is a data chart based widget
|
|
315
|
+
const dsSampleTime = ownerWidgetUuid?.startsWith('widget-windtrends-chart') ||
|
|
316
|
+
ownerWidgetUuid?.startsWith('widget-data-chart');
|
|
317
|
+
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));
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
sampleTime = this.resolveSampleTimeMs(input.sampleTime);
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
...input,
|
|
328
|
+
seriesId,
|
|
329
|
+
datasetUuid,
|
|
330
|
+
ownerWidgetUuid,
|
|
331
|
+
path,
|
|
332
|
+
source: input.source ?? 'default',
|
|
333
|
+
context: input.context ?? 'vessels.self',
|
|
334
|
+
enabled: input.enabled !== false,
|
|
335
|
+
retentionDurationMs: retentionMs,
|
|
336
|
+
sampleTime
|
|
337
|
+
};
|
|
338
|
+
}
|
|
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
|
+
resolveRetentionMs(series) {
|
|
351
|
+
if (Number.isFinite(series.retentionDurationMs) && series.retentionDurationMs > 0) {
|
|
352
|
+
return series.retentionDurationMs;
|
|
353
|
+
}
|
|
354
|
+
const period = Number(series.period ?? 0);
|
|
355
|
+
const scale = String(series.timeScale ?? '').toLowerCase();
|
|
356
|
+
if (scale === 'last minute')
|
|
357
|
+
return 60_000;
|
|
358
|
+
if (scale === 'last 5 minutes')
|
|
359
|
+
return 5 * 60_000;
|
|
360
|
+
if (scale === 'last 30 minutes')
|
|
361
|
+
return 30 * 60_000;
|
|
362
|
+
if (scale === 'second')
|
|
363
|
+
return Math.max(0, period) * 1_000;
|
|
364
|
+
if (scale === 'minute')
|
|
365
|
+
return Math.max(0, period) * 60_000;
|
|
366
|
+
if (scale === 'hour')
|
|
367
|
+
return Math.max(0, period) * 60 * 60_000;
|
|
368
|
+
if (scale === 'day')
|
|
369
|
+
return Math.max(0, period) * 24 * 60 * 60_000;
|
|
370
|
+
return 24 * 60 * 60_000;
|
|
371
|
+
}
|
|
372
|
+
normalizePathIdentifier(path) {
|
|
373
|
+
const trimmed = String(path).trim();
|
|
374
|
+
if (!trimmed) {
|
|
375
|
+
return '';
|
|
376
|
+
}
|
|
377
|
+
if (trimmed.startsWith('vessels.self.')) {
|
|
378
|
+
return trimmed.slice('vessels.self.'.length);
|
|
379
|
+
}
|
|
380
|
+
if (trimmed.startsWith('self.')) {
|
|
381
|
+
return trimmed.slice('self.'.length);
|
|
382
|
+
}
|
|
383
|
+
return trimmed;
|
|
384
|
+
}
|
|
385
|
+
resolveTimestamp(timestamp) {
|
|
386
|
+
if (typeof timestamp === 'number' && Number.isFinite(timestamp)) {
|
|
387
|
+
return timestamp;
|
|
388
|
+
}
|
|
389
|
+
if (typeof timestamp === 'string') {
|
|
390
|
+
const parsed = Date.parse(timestamp);
|
|
391
|
+
if (Number.isFinite(parsed)) {
|
|
392
|
+
return parsed;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return this.nowProvider();
|
|
396
|
+
}
|
|
397
|
+
resolveSource(sample) {
|
|
398
|
+
if (typeof sample.$source === 'string' && sample.$source.trim().length > 0) {
|
|
399
|
+
return sample.$source;
|
|
400
|
+
}
|
|
401
|
+
if (typeof sample.source === 'string' && sample.source.trim().length > 0) {
|
|
402
|
+
return sample.source;
|
|
403
|
+
}
|
|
404
|
+
if (sample.source && typeof sample.source === 'object') {
|
|
405
|
+
const source = sample.source;
|
|
406
|
+
const label = typeof source.label === 'string' ? source.label : '';
|
|
407
|
+
const src = typeof source.src === 'string' || typeof source.src === 'number' ? String(source.src) : '';
|
|
408
|
+
if (label && src) {
|
|
409
|
+
return `${label}.${src}`;
|
|
410
|
+
}
|
|
411
|
+
if (label) {
|
|
412
|
+
return label;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return 'default';
|
|
416
|
+
}
|
|
417
|
+
isContextMatch(seriesContext, sampleContext) {
|
|
418
|
+
if (seriesContext === sampleContext) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
if (seriesContext === 'vessels.self') {
|
|
422
|
+
return sampleContext === 'vessels.self' || sampleContext.startsWith('vessels.');
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
isSourceMatch(seriesSource, sampleSource) {
|
|
427
|
+
if (seriesSource === '*' || seriesSource === 'any') {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
if (seriesSource === 'default') {
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (!sampleSource) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
return seriesSource === sampleSource;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
exports.HistorySeriesService = HistorySeriesService;
|