@mxtommy/kip 4.8.0-beta.1 → 4.8.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 CHANGED
@@ -1,3 +1,13 @@
1
+ # v4.8.0
2
+ ## New Features
3
+ * Solar Charger Widget: Get instant clarity on your solar system with a compact, purpose-built Solar Charger Widget. Track individual panels or full arrays in real time, including State of Charge, remaining capacity, remaining time, voltage, current, power flow, and temperature. Device discovery is automatic, and Zones support keeps warnings and alarms state highly visible.
4
+ * AC/DC Charger Widget: Monitor charging performance at a glance with a compact AC/DC Charger Widget. View single or multiple chargers with charge mode, voltage, current, power and temperature. Chargers are discovered automatically.
5
+ ## Improvements
6
+ * Battery Monitor Widget visual cleanup for better readability and tighter consistency with the electrical widget family.
7
+ * Framework upgrades and core refactoring to improve long-term maintainability and runtime performance.
8
+ ## Fixes
9
+ * Improved Battery Monitor text contrast when color state is Alert (yellow), making critical values easier to read. Fixes #1027
10
+ * Restored Countdown Timer visibility in Add Widgets.
1
11
  # v4.7.0
2
12
  ## New Features
3
13
  * Battery Monitor Widget: Stay on top of your vessel’s power system with a dedicated compact Battery Monitor Widget. Instantly view individual batteries or whole banks, including State of Charge, remaining capacity, remaining time, voltage, current, power flow, and temperature. Batteries are detected automatically, with Signal K Zones support for clear warning and alarm visibility at a glance.
package/LICENSE CHANGED
@@ -19,3 +19,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
+
23
+ This repository may not be used to train machine learning or AI models
24
+ without explicit permission from the author.
package/README.md CHANGED
@@ -96,34 +96,27 @@ Organize your dashboards and access tools.
96
96
 
97
97
  ## Widget Library
98
98
  All KIP widgets are visual presentation controls that are very versatile, with multiple advanced configuration options available to suit your needs:
99
- - **Numeric**: Create gauges to display any numerical data sent by your system: SOG, depth, wind speed, VMG, refrigerator temperature, weather data, etc.
100
- - **Text**: Create gauges to display any textual data sent by your system: MPPT state, vessel details, next waypoint, Fusion radio song information, noon and sun phases, any system component configuration detail or status available, etc.
101
- - **Date & Time**: A timezone-aware control with flexible presentation formatting support.
102
- - **Position**: Position coordinates in textual format.
103
- - **Simple Linear gauge**: A visual display for electrical numerical data: chargers, MPPT, shunt, etc.
104
- - **Linear gauge**: Visually display any numerical data on a vertical or horizontal scale: tank and reservoir levels, battery remaining capacity, etc.
105
- - **Radial gauge**: Visually display any numerical data on a radial scale: boat speed, wind speed, engine RPM, etc.
106
- - **Compass gauge**: A card or marine compass to display directional data such as heading, bearing to next waypoint, wind angle, etc.
107
- - **Radial and linear Steel gauge**: Old-school look & feel gauges.
108
- - **Level gauge**: Dual-scale heel angle indicator combining a high‑precision ±5° fine level with a wide ±40° coarse arc for fast trim tuning and broader heel / sea‑state monitoring.
109
- - **Pitch & Roll gauge**: Horizon-style attitude indicator showing live pitch and roll for monitoring trim, heel, and sea-state response.
110
- - **Battery Monitor**: Stay on top of batteries or whole banks state with a compact view displaying State of Charge, remaining capacity, remaining time, voltage, current, power flow, and temperature.
111
- - **Solar Charger**: Track solar generation and charging performance at a glance with live panel output, battery-side metrics, and clear charger and relay status indicators.
112
- - **Windsteer**: Combines wind, wind sectors, heading, COG, and waypoint info for sailboat wind steering.
113
- - **Racesteer**: Sailboat race steering display fusing polar performance data with live conditions for optimal tactics.
114
- - **Countdown Timer**: A simple start sequences timer.
115
- - **Racer Start Timer**: Advanced race countdown timer with OCS (On Course Side) detection and automatic dashboard switching.
116
- - **Start Line Insight**: Analyze and visualize the start line for tactical racing advantage, including favored end and distance-to-line.
117
- - **Boolean Control Panel**: A digital switchboard to configure and operate remote devices: lights, bilge pumps, solenoids, or any Signal K compatible device that supports On/Off operations.
118
- - **Slider**: A versatile control that allows users to adjust values within a defined range by sliding. Commonly used for settings like light intensity, volume control, or any parameter requiring fine-tuned adjustments.
119
- - **Multi State Switch**: Lists all available device modes/states (e.g., On, Off, Charge Only, Invert Only), highlights the current state, and lets you select a new state to send to the device and see the result.
120
- - **Zones State Panel**: Monitor the health/state of multiple sensors and devices at a glance. Configure multiple paths per panel; each control uses KIP’s zone severity colors and status messages so warnings and alarms stand out immediately.
121
- - **Freeboard-SK Chart Plotter**: High-quality Signal K chartplotter integration widget.
122
- - **Autopilot Head**: Operate your autopilot from any device remotely.
99
+ - **Compact Linear** Simple horizontal linear gauge with a large value label and modern look.
100
+ - **Linear** Horizontal or vertical linear gauge with zone highlighting.
101
+ - **Radial** Radial gauge with configurable dials and zone highlighting.
102
+ - **Compass** Rotating compass gauge with multiple cardinal indicator options.
103
+ - **Level Gauge** Dual-scale heel angle indicator for trim tuning and sea-state monitoring.
104
+ - **Pitch & Roll** Horizon-style attitude indicator showing live pitch and roll degrees.
105
+ - **Classic Steel** Traditional steel-look linear & radial gauges with range sizes and zone highlights.
106
+ - **Windsteer** Combines wind, wind sectors, heading, COG, and waypoint info for wind steering.
107
+ - **Wind Trends** Real-time True Wind trends with dual axes for direction and speed, live values, and averages.
108
+ - **Battery Monitor** - Display batteries or whole banks state State of Charge, remaining capacity, remaining time, voltage, current, power flow, and temperature.
109
+ - **Solar Charger**- Track solar generation and charging performance at a glance with live panel output, battery-side metrics, and clear charger and relay status indicators.
110
+ - **AC/DC Charger**- Monitor charging performance at a glance with a compact AC/DC Charger Widget. View single or multiple chargers with charge mode, voltage, current, power and temperature. Chargers are discovered automatically.
111
+ - **Freeboard-SK** Adds the Freeboard-SK chart plotter as a widget with automatic sign-in.
112
+ - **Autopilot Head** Typical autopilot controls for compatible Signal K Autopilot devices.
113
+ - **Realtime Data Chart** Visualizes data on a real-time chart with actuals, averages, and min/max.
123
114
  - **AIS Radar**: Display AIS targets with range rings, interactive target details, and quick zoom and filtering controls.
124
- - **Data Chart**: Visualize data trends over time.
125
- - **Embedded Webpage**: A powerful way to display web-based apps published on your Signal K server, such as Grafana and Node-RED dashboards, or your own standalone web app.
126
- - **Label**: A static text widget.
115
+ - **Embed Webpage Viewer** Embeds external web apps (Grafana, Node-RED, etc.) into your dashboard.
116
+ - **Racesteer** Race steering display fusing polar performance data with live conditions for optimal tactics.
117
+ - **Racer - Start Line Insight** – Set and adjust start line ends, see distance, favored end, and line bias; integrates with Freeboard SK.
118
+ - **Racer - Start Timer** – Advanced racing countdown timer with OCS status and auto dashboard switching.
119
+ - **Countdown Timer** – Simple race start countdown timer with start, pause, sync, and reset options.
127
120
 
128
121
  Get the latest version of KIP to see what's new!
129
122
 
@@ -141,7 +134,7 @@ Grafana integration with other widgets
141
134
  ![Embedded Webpage Concept Image](./images/KipGaugeSample3-1024x508.png)
142
135
 
143
136
  ## Historical Data
144
- Experience effortless insight into your vessel’s past with KIP’s Widget Historical Charts—automatically track, store, and visualize key data, unlocking instant access charts showing up to the last full day of performance. Whether you’re sailing or docked, simply tap or right-click widgets to reveal a seamless history dialog—no setup, no clutter, just the trends you need. With full support for Data Driven widgets, live-to-history transitions, KIP puts your boat’s story at your fingertips—so you can make smarter decisions, spot patterns, and sail with confidence.
137
+ Experience effortless insight into your vessel’s past with KIP’s Widget Historical Charts—automatically track, store, and visualize key data, unlocking instant access charts showing up to the last full day of performance. Whether you’re sailing or docked, simply two-finger tap or right-click widgets to reveal a seamless history dialog—no setup, no clutter, just the trends you need. When combining data visualisation using Data Driven widgets, live-to-history transitions, KIP puts your boat’s story at your fingertips—so you can make smarter decisions, spot patterns, and sail with confidence.
145
138
 
146
139
  ## Night Modes
147
140
  Keep your night vision with automatic or manual day and night switching to a color preserving dim mode or an all Red theme. The images below look very dark, but at night... they are perfect!
@@ -265,17 +258,26 @@ Once done with your work, from your fork's working branch, make a GitHub pull re
265
258
  For comprehensive development guidance, please refer to these instruction files:
266
259
 
267
260
  ### Primary Instructions
268
- - **[COPILOT.md](./COPILOT.md)**: Complete KIP project guidelines including architecture, services, widget development patterns, theming, and Signal K integration.
261
+ - **[Project Instructions](./.github/instructions/project.instructions.md)**: KIP policy owner for architecture/domain rules, including widget creation and Host2 contracts.
262
+ - **[COPILOT.md](./COPILOT.md)**: Architecture context, rationale, and evolution notes (non-policy).
269
263
  - **[Angular Instructions](./.github/instructions/angular.instructions.md)**: Modern Angular v21+ coding standards, component patterns, and framework best practices.
270
264
  - **[Copilot Agent Instructions](./.github/copilot-instructions.md)**: Architecture details and coding-agent guardrails for this repository.
271
265
 
272
266
  ### Development Workflow
273
- 1. **Start Here**: Read `COPILOT.md` for KIP-specific architecture and patterns.
267
+ 1. **Start Here**: Read `.github/instructions/project.instructions.md` for KIP policy contracts.
274
268
  2. **Angular Standards**: Follow `.github/instructions/angular.instructions.md` for modern Angular development.
275
- 3. **Setup & Build**: Use this README for project setup and build commands.
269
+ 3. **Architecture Context**: Use `COPILOT.md` for rationale and dated architecture notes.
270
+ 4. **Setup & Build**: Use this README for project setup and build commands.
271
+
272
+ ### Widget Creation Workflow
273
+ 1. Scaffold with `npm run generate:widget` (Host2 schematic-first path).
274
+ 2. Use `docs/widget-schematic.md` for CLI flags, prompting behavior, and troubleshooting.
275
+ 3. Follow Host2 runtime/stream patterns in `.agents/skills/kip-host2-widget/SKILL.md`.
276
+ 4. Apply widget creation implementation checklist from `.agents/skills/kip-widget-creation/SKILL.md`.
277
+ 5. Keep enforceable behavior aligned with `.github/instructions/project.instructions.md` (`Widget Creation Domain Rules`).
276
278
 
277
279
  ### Key Priorities
278
- - **Widget Development**: Use the Host2 widget pattern (signals + directives) and scaffold new widgets with the `create-host2-widget` schematic (see `COPILOT.md`).
280
+ - **Widget Development**: Use Host2 patterns and scaffold with the `create-host2-widget` schematic (see `docs/widget-schematic.md`).
279
281
  - **Angular Patterns**: Use signals, standalone components, and modern control flow.
280
282
  - **Theming**: Follow KIP's theme system for consistent UI.
281
283
  - **Code Quality**: Run `npm run lint` before commits.
@@ -288,3 +290,6 @@ KIP has its own Discord Signal K channel for getting in touch. Join us at https:
288
290
  # Features, Ideas, Bugs
289
291
  See KIP's GitHub project for the latest feature requests:
290
292
  https://github.com/mxtommy/Kip/issues
293
+
294
+ This repository may not be used to train machine learning or AI models
295
+ without explicit permission from the author.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxtommy/kip",
3
- "version": "4.8.0-beta.1",
3
+ "version": "4.8.0",
4
4
  "description": "An advanced and versatile marine instrumentation package to display Signal K data.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HistorySeriesService = exports.isKipTemplateSeriesDefinition = exports.isKipSolarTemplateSeriesDefinition = exports.isKipSeriesEnabled = exports.isKipConcreteSeriesDefinition = exports.isKipBmsTemplateSeriesDefinition = void 0;
3
+ exports.HistorySeriesService = exports.isKipTemplateSeriesDefinition = exports.isKipSolarTemplateSeriesDefinition = exports.isKipSeriesEnabled = exports.isKipElectricalTemplateSeriesDefinition = exports.isKipConcreteSeriesDefinition = exports.isKipBmsTemplateSeriesDefinition = void 0;
4
4
  const kip_series_contract_1 = require("./kip-series-contract");
5
+ Object.defineProperty(exports, "isKipElectricalTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipElectricalTemplateSeriesDefinition; } });
5
6
  Object.defineProperty(exports, "isKipBmsTemplateSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipBmsTemplateSeriesDefinition; } });
6
7
  Object.defineProperty(exports, "isKipConcreteSeriesDefinition", { enumerable: true, get: function () { return kip_series_contract_1.isKipConcreteSeriesDefinition; } });
7
8
  Object.defineProperty(exports, "isKipSeriesEnabled", { enumerable: true, get: function () { return kip_series_contract_1.isKipSeriesEnabled; } });
@@ -227,6 +228,18 @@ class HistorySeriesService {
227
228
  if (!seriesKeys || seriesKeys.length === 0) {
228
229
  return;
229
230
  }
231
+ const hasSpecificSourceMatch = seriesKeys.some(seriesKey => {
232
+ const series = this.seriesById.get(seriesKey);
233
+ if (!series) {
234
+ return false;
235
+ }
236
+ const seriesContext = series.context ?? 'vessels.self';
237
+ if (!this.isContextMatch(seriesContext, context)) {
238
+ return false;
239
+ }
240
+ const seriesSource = series.source ?? 'default';
241
+ return seriesSource !== 'default' && seriesSource === source;
242
+ });
230
243
  seriesKeys.forEach(seriesKey => {
231
244
  const series = this.seriesById.get(seriesKey);
232
245
  if (!series) {
@@ -240,6 +253,9 @@ class HistorySeriesService {
240
253
  if (!this.isSourceMatch(seriesSource, source)) {
241
254
  return;
242
255
  }
256
+ if (seriesSource === 'default' && source !== 'default' && hasSpecificSourceMatch) {
257
+ return;
258
+ }
243
259
  if (this.recordSampleByKey(seriesKey, leaf.value, ts)) {
244
260
  recorded += 1;
245
261
  }
@@ -328,8 +344,9 @@ class HistorySeriesService {
328
344
  && leftComparable.ownerWidgetSelector === rightComparable.ownerWidgetSelector
329
345
  && leftComparable.path === rightComparable.path
330
346
  && leftComparable.expansionMode === rightComparable.expansionMode
331
- && this.areStringArraysEquivalent(leftComparable.allowedBatteryIds, rightComparable.allowedBatteryIds)
332
- && this.areStringArraysEquivalent(leftComparable.allowedSolarIds, rightComparable.allowedSolarIds)
347
+ && leftComparable.familyKey === rightComparable.familyKey
348
+ && this.areStringArraysEquivalent(leftComparable.allowedIds, rightComparable.allowedIds)
349
+ && this.areTrackedDevicesEquivalent(leftComparable.trackedDevices, rightComparable.trackedDevices)
333
350
  && leftComparable.source === rightComparable.source
334
351
  && leftComparable.context === rightComparable.context
335
352
  && leftComparable.timeScale === rightComparable.timeScale
@@ -344,11 +361,22 @@ class HistorySeriesService {
344
361
  void reconcileTs;
345
362
  return {
346
363
  ...comparable,
347
- allowedBatteryIds: this.normalizeComparableStringArray(comparable.allowedBatteryIds),
348
- allowedSolarIds: this.normalizeComparableStringArray(comparable.allowedSolarIds),
364
+ allowedIds: this.normalizeComparableStringArray(comparable.allowedIds),
365
+ trackedDevices: this.normalizeComparableTrackedDevices(comparable.trackedDevices),
349
366
  methods: this.normalizeComparableStringArray(comparable.methods)
350
367
  };
351
368
  }
369
+ areTrackedDevicesEquivalent(left, right) {
370
+ const normalizedLeft = this.normalizeComparableTrackedDevices(left) ?? [];
371
+ const normalizedRight = this.normalizeComparableTrackedDevices(right) ?? [];
372
+ if (normalizedLeft.length !== normalizedRight.length) {
373
+ return false;
374
+ }
375
+ return normalizedLeft.every((value, index) => {
376
+ const candidate = normalizedRight[index];
377
+ return value.id === candidate?.id && value.source === candidate?.source;
378
+ });
379
+ }
352
380
  areStringArraysEquivalent(left, right) {
353
381
  const normalizedLeft = this.normalizeComparableStringArray(left) ?? [];
354
382
  const normalizedRight = this.normalizeComparableStringArray(right) ?? [];
@@ -365,6 +393,31 @@ class HistorySeriesService {
365
393
  .filter((value) => typeof value === 'string')
366
394
  .sort((left, right) => left.localeCompare(right));
367
395
  }
396
+ normalizeComparableTrackedDevices(values) {
397
+ if (!Array.isArray(values) || values.length === 0) {
398
+ return undefined;
399
+ }
400
+ const normalizedByKey = new Map();
401
+ values.forEach(value => {
402
+ if (!value || typeof value !== 'object') {
403
+ return;
404
+ }
405
+ const id = typeof value.id === 'string' ? value.id.trim() : '';
406
+ const sourceText = typeof value.source === 'string' ? value.source.trim() : '';
407
+ const source = sourceText.length > 0 ? sourceText : 'default';
408
+ if (!id) {
409
+ return;
410
+ }
411
+ normalizedByKey.set(`${id}||${source}`, { id, source });
412
+ });
413
+ if (normalizedByKey.size === 0) {
414
+ return undefined;
415
+ }
416
+ return Array.from(normalizedByKey.values()).sort((left, right) => {
417
+ const idCompare = left.id.localeCompare(right.id);
418
+ return idCompare !== 0 ? idCompare : left.source.localeCompare(right.source);
419
+ });
420
+ }
368
421
  isChartWidget(ownerWidgetSelector, ownerWidgetUuid) {
369
422
  if (ownerWidgetSelector === 'widget-data-chart' || ownerWidgetSelector === 'widget-windtrends-chart') {
370
423
  return true;
@@ -391,18 +444,29 @@ class HistorySeriesService {
391
444
  }
392
445
  const ownerWidgetSelector = typeof input.ownerWidgetSelector === 'string' ? input.ownerWidgetSelector.trim() : null;
393
446
  const expansionMode = input.expansionMode ?? null;
394
- if (expansionMode === 'bms-battery-tree' && ownerWidgetSelector !== 'widget-bms') {
395
- throw new Error('BMS template series must use ownerWidgetSelector "widget-bms"');
396
- }
397
- if (expansionMode === 'solar-tree' && ownerWidgetSelector !== 'widget-solar-charger') {
398
- throw new Error('Solar template series must use ownerWidgetSelector "widget-solar-charger"');
447
+ const familyKey = expansionMode ? this.expansionModeToFamilyKey(expansionMode) : null;
448
+ let normalizedTemplateSelector = null;
449
+ if (expansionMode) {
450
+ const selectorByMode = {
451
+ 'bms-battery-tree': 'widget-bms',
452
+ 'solar-tree': 'widget-solar-charger',
453
+ 'charger-tree': 'widget-charger',
454
+ 'inverter-tree': 'widget-inverter',
455
+ 'alternator-tree': 'widget-alternator',
456
+ 'ac-tree': 'widget-ac'
457
+ };
458
+ const requiredSelector = selectorByMode[expansionMode];
459
+ if (ownerWidgetSelector !== requiredSelector) {
460
+ throw new Error(`Template series mode "${expansionMode}" must use ownerWidgetSelector "${requiredSelector}"`);
461
+ }
462
+ normalizedTemplateSelector = requiredSelector;
399
463
  }
400
464
  const normalizedMethods = this.normalizeComparableStringArray(input.methods);
401
- const normalizedAllowedBatteryIds = expansionMode === 'bms-battery-tree'
402
- ? this.normalizeComparableStringArray(input.allowedBatteryIds)
465
+ const normalizedAllowedIds = expansionMode
466
+ ? this.normalizeComparableStringArray(input.allowedIds)
403
467
  : undefined;
404
- const normalizedAllowedSolarIds = expansionMode === 'solar-tree'
405
- ? this.normalizeComparableStringArray(input.allowedSolarIds)
468
+ const normalizedTrackedDevices = expansionMode
469
+ ? this.normalizeComparableTrackedDevices(input.trackedDevices)
406
470
  : undefined;
407
471
  const isDataWidget = this.isChartWidget(ownerWidgetSelector, ownerWidgetUuid);
408
472
  const retentionMs = this.resolveRetentionMs(input);
@@ -423,6 +487,7 @@ class HistorySeriesService {
423
487
  ownerWidgetUuid,
424
488
  ownerWidgetSelector,
425
489
  path,
490
+ familyKey,
426
491
  source: input.source ?? 'default',
427
492
  context: input.context ?? 'vessels.self',
428
493
  timeScale: input.timeScale ?? null,
@@ -433,34 +498,44 @@ class HistorySeriesService {
433
498
  methods: normalizedMethods,
434
499
  reconcileTs: input.reconcileTs
435
500
  };
436
- if (expansionMode === 'bms-battery-tree') {
501
+ if (expansionMode) {
437
502
  const templateSeries = {
438
503
  ...normalizedBase,
439
- ownerWidgetSelector: 'widget-bms',
504
+ ownerWidgetSelector: normalizedTemplateSelector,
440
505
  expansionMode,
441
- allowedBatteryIds: normalizedAllowedBatteryIds ?? null,
442
- allowedSolarIds: null
443
- };
444
- return templateSeries;
445
- }
446
- if (expansionMode === 'solar-tree') {
447
- const templateSeries = {
448
- ...normalizedBase,
449
- ownerWidgetSelector: 'widget-solar-charger',
450
- expansionMode,
451
- allowedBatteryIds: null,
452
- allowedSolarIds: normalizedAllowedSolarIds ?? null
506
+ familyKey,
507
+ allowedIds: normalizedAllowedIds ?? null,
508
+ trackedDevices: normalizedTrackedDevices ?? null
453
509
  };
454
510
  return templateSeries;
455
511
  }
456
512
  const concreteSeries = {
457
513
  ...normalizedBase,
458
514
  expansionMode: null,
459
- allowedBatteryIds: null,
460
- allowedSolarIds: null
515
+ familyKey: null,
516
+ allowedIds: null,
517
+ trackedDevices: null
461
518
  };
462
519
  return concreteSeries;
463
520
  }
521
+ expansionModeToFamilyKey(mode) {
522
+ switch (mode) {
523
+ case 'bms-battery-tree':
524
+ return 'batteries';
525
+ case 'solar-tree':
526
+ return 'solar';
527
+ case 'charger-tree':
528
+ return 'chargers';
529
+ case 'inverter-tree':
530
+ return 'inverters';
531
+ case 'alternator-tree':
532
+ return 'alternators';
533
+ case 'ac-tree':
534
+ return 'ac';
535
+ default:
536
+ throw new Error(`Unsupported expansion mode: ${String(mode)}`);
537
+ }
538
+ }
464
539
  resolveRetentionMs(series) {
465
540
  if (Number.isFinite(series.retentionDurationMs) && series.retentionDurationMs > 0) {
466
541
  return series.retentionDurationMs;