@camstack/system 1.2.468 → 1.2.469

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 (89) hide show
  1. package/dist/addon-utils.mjs +1 -1
  2. package/dist/{agent-https-DJ8OzQOr.js → agent-https-5OgOR-eF.js} +1 -1
  3. package/dist/{agent-https-DR1wmhYY.mjs → agent-https-b-zX78oB.mjs} +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +4 -4
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +5 -5
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +1 -1
  11. package/dist/builtins/autotrack/index.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +5 -5
  14. package/dist/builtins/calendars/calendars.addon.js +1 -1
  15. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  16. package/dist/builtins/camera-grid/index.js +1 -1
  17. package/dist/builtins/camera-grid/index.mjs +1 -1
  18. package/dist/builtins/composer/composer.addon.js +2 -2
  19. package/dist/builtins/composer/composer.addon.mjs +2 -2
  20. package/dist/builtins/console-logging/index.js +1 -1
  21. package/dist/builtins/console-logging/index.mjs +1 -1
  22. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  23. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  24. package/dist/builtins/device-history/capture/capture-engine.d.ts +24 -4
  25. package/dist/builtins/device-history/device-history.addon.js +319 -44
  26. package/dist/builtins/device-history/device-history.addon.mjs +319 -44
  27. package/dist/builtins/device-history/query/history-query.d.ts +19 -2
  28. package/dist/builtins/device-history/retention/orphans.d.ts +50 -0
  29. package/dist/builtins/device-history/retention/retention.d.ts +9 -2
  30. package/dist/builtins/device-history/runtime/history-runtime.d.ts +10 -2
  31. package/dist/builtins/device-history/store/history-store.d.ts +5 -0
  32. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  33. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  34. package/dist/builtins/device-manager/device-meta-types.d.ts +2 -2
  35. package/dist/builtins/hub-forwarder/index.js +1 -1
  36. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  37. package/dist/builtins/keychain/index.js +3 -3
  38. package/dist/builtins/keychain/index.mjs +3 -3
  39. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  40. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  41. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  42. package/dist/builtins/local-auth/local-auth.addon.mjs +5 -5
  43. package/dist/builtins/local-network/local-network.addon.js +2 -2
  44. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  45. package/dist/builtins/loki-logging/index.js +1 -1
  46. package/dist/builtins/loki-logging/index.mjs +1 -1
  47. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  48. package/dist/builtins/native-metrics/native-metrics.addon.mjs +10 -10
  49. package/dist/builtins/platform-probe/index.js +1 -1
  50. package/dist/builtins/platform-probe/index.mjs +8 -8
  51. package/dist/builtins/record-entries/index.js +1 -1
  52. package/dist/builtins/record-entries/index.mjs +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  54. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  55. package/dist/builtins/snapshot/index.js +1 -1
  56. package/dist/builtins/snapshot/index.mjs +1 -1
  57. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  58. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +20 -20
  59. package/dist/builtins/sqlite-storage/index.js +1 -1
  60. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  61. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  62. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  63. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  64. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  65. package/dist/builtins/system-config/system-config.addon.js +3 -3
  66. package/dist/builtins/system-config/system-config.addon.mjs +115 -115
  67. package/dist/builtins/winston-logging/index.js +1 -1
  68. package/dist/builtins/winston-logging/index.mjs +1 -1
  69. package/dist/{composition-sources-5rpWQhA1.js → composition-sources-BGXTiUZm.js} +1 -1
  70. package/dist/{composition-sources-Qto-S6MA.mjs → composition-sources-DTdzzyf0.mjs} +1 -1
  71. package/dist/{constants-CuO3rcw4.js → constants-2mPqJjkR.js} +1 -1
  72. package/dist/{constants-BWli8Cju.mjs → constants-md_POijo.mjs} +1 -1
  73. package/dist/{coral-accelerators-D_S1Fo5h.mjs → coral-accelerators-Qhn5TOYZ.mjs} +5 -5
  74. package/dist/{custom-action-registry-BGYDjTqY.mjs → custom-action-registry-DZSCLBuT.mjs} +37 -37
  75. package/dist/{device-ca-CGcaSheI.js → device-ca-CC58lmNV.js} +1 -1
  76. package/dist/{device-ca-C5cLlah1.mjs → device-ca-CFdyrqeJ.mjs} +1 -1
  77. package/dist/{dist-BpUtAgqR.mjs → dist-CtVVtyTX.mjs} +247 -119
  78. package/dist/{dist-l6zKmHEb.js → dist-DuBtzSCh.js} +258 -118
  79. package/dist/file-host-worker.js +1 -1
  80. package/dist/file-host-worker.mjs +1 -1
  81. package/dist/{hub-mdns-ciao-CIno_SNn.js → hub-mdns-ciao-BLsX_2cL.js} +2 -2
  82. package/dist/{hub-mdns-ciao-0KWDyIMD.mjs → hub-mdns-ciao-CmuWLPci.mjs} +2 -2
  83. package/dist/index.js +7 -7
  84. package/dist/index.mjs +225 -225
  85. package/dist/{restore-archive-B71JI-sP.js → restore-archive-CxPAuaNs.js} +1 -1
  86. package/dist/{restore-archive-DKxAZ9pI.mjs → restore-archive-D2w3z_ob.mjs} +94 -94
  87. package/dist/{wal-maintenance-BXYwFecW.mjs → wal-maintenance-DeluFl_1.mjs} +0 -0
  88. package/dist/{wal-maintenance-B6LSQ_OZ.js → wal-maintenance-EK28XXnc.js} +0 -0
  89. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { $r as BaseAddon, $t as deviceHistoryCapability, D as DEVICE_HISTORY_ENERGY_KEY, St as armRemovalReconcile, Ta as EventCategory, Zr as DeviceType, ci as HISTORY_DEFAULT_MIN_INTERVAL_MS, qi as emitReadiness, tn as deviceRemovalHookCapability, z as HISTORY_CATALOG } from "../../dist-BpUtAgqR.mjs";
1
+ import { B as HISTORY_ITEM_CATALOG, Ct as armRemovalReconcile, D as DEVICE_HISTORY_ENERGY_KEY, Da as EventCategory, Ji as emitReadiness, Qr as DeviceType, Zi as historyItemFieldPath, ei as BaseAddon, en as deviceHistoryCapability, li as HISTORY_DEFAULT_MIN_INTERVAL_MS, nn as deviceRemovalHookCapability, z as HISTORY_CATALOG } from "../../dist-CtVVtyTX.mjs";
2
2
  //#region src/builtins/device-history/time/bucket-time.ts
3
3
  /**
4
4
  * Bucket arithmetic for the three rollup tiers (D850, plan §4/§5.5).
@@ -185,13 +185,25 @@ var RetentionJob = class {
185
185
  }
186
186
  return deleted;
187
187
  }
188
- /** Tier by tier in slices, then the series row (and whatever the slices missed). */
188
+ /**
189
+ * Tier by tier in slices, then the series row (and whatever the slices missed).
190
+ * Answers the rows the slices deleted per tier. The caller removes the series
191
+ * from the registry, which releases its per-series state.
192
+ */
189
193
  async purgeSeries(series, now) {
194
+ const deleted = {
195
+ raw: 0,
196
+ "5m": 0,
197
+ "1h": 0,
198
+ "1d": 0
199
+ };
190
200
  for (const tier of TIERS) {
191
201
  const lo = tier === "raw" || tier === "5m" ? Math.max(series.firstTs - BUCKET_LEAD_MS, now - SHORT_TIER_LOOKBACK_MS) : series.firstTs - BUCKET_LEAD_MS;
192
- await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
202
+ deleted[tier] = await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
203
+ this.deps.counters.addDeleted(tier, deleted[tier]);
193
204
  }
194
205
  await this.deps.store.deleteSeries(series.id);
206
+ return deleted;
195
207
  }
196
208
  /**
197
209
  * Delete every series of a DELETED device now (D865). Driven by a tombstone
@@ -558,14 +570,14 @@ var DeviceDirectory = class {
558
570
  * `runtimeState` key or a path into a FIXED object (`dustBin.level`); the
559
571
  * catalog guard keeps it out of arrays and records, so a plain walk is enough.
560
572
  */
561
- function isRecord$1(value) {
573
+ function isRecord$2(value) {
562
574
  return typeof value === "object" && value !== null && !Array.isArray(value);
563
575
  }
564
576
  /** The value at `path`, or `undefined` when any step is missing or not an object. */
565
577
  function readPath(slice, path) {
566
578
  let current = slice;
567
579
  for (const step of path.split(".")) {
568
- if (!isRecord$1(current)) return void 0;
580
+ if (!isRecord$2(current)) return void 0;
569
581
  current = current[step];
570
582
  }
571
583
  return current;
@@ -591,18 +603,41 @@ function readUnit(slice, unitFrom) {
591
603
  }
592
604
  //#endregion
593
605
  //#region src/builtins/device-history/capture/capture-engine.ts
606
+ /**
607
+ * Capture of capability fields (D850, plan §5.3), driven by the GENERATED
608
+ * history catalog — a field is recorded because its `*.cap.ts` says so, never
609
+ * because someone listed it here.
610
+ *
611
+ * For a `DeviceStateChanged { deviceId, capName, slice }` the engine:
612
+ * 1. ignores a cap with no catalog entry (the common case — cheap, uncounted);
613
+ * 2. skips an opted-out device (counted), a device whose type is not yet
614
+ * known (parked, then replayed once the hub answers), and a `hub` /
615
+ * `container` (counted: not a real device);
616
+ * 3. per field: skips an excluded device type, an absent or non-numeric value
617
+ * (null is UNKNOWN, never 0), then runs {@link decideCapture} against the
618
+ * last RECORDED value — change-only, deadband, capture floor;
619
+ * 4. buffers the point for the batched write.
620
+ *
621
+ * A change inside the capture floor is kept PENDING and emitted when the floor
622
+ * has passed ({@link CaptureEngine.tickPending}), so the end of a burst is never
623
+ * lost. Reconcile ({@link CaptureEngine.reconcile}) restores what a dropped
624
+ * event missed from the state mirror's snapshot.
625
+ */
594
626
  var CAP_KEY_PREFIX = "cap:";
595
627
  function capSeriesKey(capName, fieldPath) {
596
628
  return `${CAP_KEY_PREFIX}${capName}.${fieldPath}`;
597
629
  }
630
+ /** The series key of an item field: `cap:<capName>.<path>[].<field>`. */
631
+ function capItemSeriesKey(capName, items, field) {
632
+ return capSeriesKey(capName, historyItemFieldPath(items.path, field));
633
+ }
598
634
  function humanize(capName, fieldPath) {
599
635
  const field = fieldPath.replace(/([A-Z])/g, " $1").replace(/\./g, " ").toLowerCase();
600
636
  return `${capName.replace(/-/g, " ")} ${field}`;
601
637
  }
602
- /** The metric rows of every catalog field (owner `device-history`). */
603
- function catalogMetrics(catalog, now) {
604
- return Object.entries(catalog).flatMap(([capName, fields]) => Object.entries(fields).map(([path, spec]) => ({
605
- key: capSeriesKey(capName, path),
638
+ function metricOf(key, capName, path, spec, now) {
639
+ return {
640
+ key,
606
641
  source: "cap",
607
642
  ownerAddon: "device-history",
608
643
  subjectKind: "device",
@@ -612,7 +647,16 @@ function catalogMetrics(catalog, now) {
612
647
  ingest: "points",
613
648
  declaredAt: now,
614
649
  values: spec.kind === "enum" ? spec.values ?? [] : null
615
- })));
650
+ };
651
+ }
652
+ /** The metric rows of every catalog field and keyed-item field (owner `device-history`). */
653
+ function catalogMetrics(catalog, itemCatalog, now) {
654
+ const fields = Object.entries(catalog).flatMap(([capName, specs]) => Object.entries(specs).map(([path, spec]) => metricOf(capSeriesKey(capName, path), capName, path, spec, now)));
655
+ const items = Object.entries(itemCatalog).flatMap(([capName, block]) => Object.entries(block.fields).map(([field, spec]) => metricOf(capItemSeriesKey(capName, block, field), capName, field, spec, now)));
656
+ return [...fields, ...items];
657
+ }
658
+ function isRecord$1(value) {
659
+ return typeof value === "object" && value !== null && !Array.isArray(value);
616
660
  }
617
661
  /** Devices waiting on a type lookup; beyond this the oldest is dropped (and counted). */
618
662
  var MAX_PARKED_DEVICES = 5e3;
@@ -639,7 +683,8 @@ var CaptureEngine = class {
639
683
  /** One `DeviceStateChanged`. Synchronous: the bus must never wait on us. */
640
684
  handleState(deviceId, capName, slice, at) {
641
685
  const fields = this.deps.catalog[capName];
642
- if (fields === void 0) return;
686
+ const items = this.deps.itemCatalog[capName];
687
+ if (fields === void 0 && items === void 0) return;
643
688
  const { counters, directory } = this.deps;
644
689
  counters.eventsSeen += 1;
645
690
  if (this.deps.optedOut().has(deviceId)) {
@@ -655,14 +700,46 @@ var CaptureEngine = class {
655
700
  counters.skip("not-a-real-device");
656
701
  return;
657
702
  }
658
- for (const [path, spec] of Object.entries(fields)) {
703
+ this.captureSlice(deviceId, type, capName, slice, at, false);
704
+ }
705
+ /** Every catalogued field and keyed item of one cap slice, for a real device. */
706
+ captureSlice(deviceId, type, capName, slice, at, reconciling) {
707
+ for (const [path, spec] of Object.entries(this.deps.catalog[capName] ?? {})) {
659
708
  if (spec.excludeDeviceTypes?.includes(type) === true) {
660
- counters.skip("excluded-device-type");
709
+ if (!reconciling) this.deps.counters.skip("excluded-device-type");
661
710
  continue;
662
711
  }
663
- const value = this.readValue(slice, capName, path, spec);
712
+ const key = capSeriesKey(capName, path);
713
+ const value = this.readValue(slice, key, path, spec);
664
714
  if (value === null) continue;
665
- this.consider(deviceId, capName, path, spec, value, at, false);
715
+ this.consider(deviceId, key, "", spec, value, at, reconciling);
716
+ }
717
+ const items = this.deps.itemCatalog[capName];
718
+ if (items !== void 0) this.captureItems(deviceId, type, capName, items, slice, at, reconciling);
719
+ }
720
+ /**
721
+ * A keyed array (D917): each item with a usable key contributes its fields
722
+ * under that key as the series scope. A value that is not an array is no
723
+ * reading; an item without a string key cannot be followed and is counted.
724
+ */
725
+ captureItems(deviceId, type, capName, items, slice, at, reconciling) {
726
+ const list = readPath(slice, items.path);
727
+ if (!Array.isArray(list)) return;
728
+ const { counters } = this.deps;
729
+ if (list.length > 64) counters.skip("item-overflow", list.length - 64);
730
+ for (const entry of list.slice(0, 64)) {
731
+ const scope = isRecord$1(entry) ? entry[items.key] : void 0;
732
+ if (!isRecord$1(entry) || typeof scope !== "string" || scope.length === 0 || scope.length > 128) {
733
+ counters.skip("item-key-invalid");
734
+ continue;
735
+ }
736
+ for (const [field, spec] of Object.entries(items.fields)) {
737
+ if (spec.excludeDeviceTypes?.includes(type) === true) continue;
738
+ const key = capItemSeriesKey(capName, items, field);
739
+ const value = this.readValue(entry, key, field, spec);
740
+ if (value === null) continue;
741
+ this.consider(deviceId, key, scope, spec, value, at, reconciling);
742
+ }
666
743
  }
667
744
  }
668
745
  /**
@@ -670,7 +747,7 @@ var CaptureEngine = class {
670
747
  * `enum` state becomes the ordinal of its value in the metric's list; a value
671
748
  * the full list cannot take is refused and counted (`enum-overflow`).
672
749
  */
673
- readValue(slice, capName, path, spec) {
750
+ readValue(slice, key, path, spec) {
674
751
  if (spec.skipWhenSet !== void 0) {
675
752
  const gate = readPath(slice, spec.skipWhenSet);
676
753
  if (gate !== void 0 && gate !== null) return null;
@@ -678,20 +755,19 @@ var CaptureEngine = class {
678
755
  if (spec.kind !== "enum") return readNumeric(slice, path, spec.kind);
679
756
  const raw = readPath(slice, path);
680
757
  if (typeof raw !== "string" || raw.length === 0) return null;
681
- const ordinal = this.deps.registry.ordinalOf(capSeriesKey(capName, path), raw);
758
+ const ordinal = this.deps.registry.ordinalOf(key, raw);
682
759
  if (ordinal === null) this.deps.counters.skip("enum-overflow");
683
760
  return ordinal;
684
761
  }
685
- consider(deviceId, capName, path, spec, value, at, reconciling) {
686
- const key = capSeriesKey(capName, path);
762
+ consider(deviceId, key, scope, spec, value, at, reconciling) {
687
763
  const subject = {
688
764
  kind: "device",
689
765
  deviceId
690
766
  };
691
- const identity = identityOf(key, `d:${deviceId}`, "");
767
+ const identity = identityOf(key, `d:${deviceId}`, scope);
692
768
  const decision = decideCapture(spec, this.recorded.get(identity) ?? null, value, at, { ignoreFloor: reconciling });
693
769
  if (decision.action === "emit") {
694
- this.emit(identity, key, subject, value, at);
770
+ this.emit(identity, key, subject, scope, value, at);
695
771
  if (reconciling) this.deps.counters.reconciledPoints += 1;
696
772
  return;
697
773
  }
@@ -699,6 +775,7 @@ var CaptureEngine = class {
699
775
  this.pending.set(identity, {
700
776
  key,
701
777
  subject,
778
+ scope,
702
779
  spec,
703
780
  value
704
781
  });
@@ -707,7 +784,7 @@ var CaptureEngine = class {
707
784
  this.pending.delete(identity);
708
785
  this.deps.counters.skip(decision.reason);
709
786
  }
710
- emit(identity, key, subject, value, ts) {
787
+ emit(identity, key, subject, scope, value, ts) {
711
788
  this.recorded.set(identity, {
712
789
  value,
713
790
  ts
@@ -717,7 +794,7 @@ var CaptureEngine = class {
717
794
  this.deps.writer.enqueue({
718
795
  key,
719
796
  subject,
720
- scope: "",
797
+ scope,
721
798
  ts,
722
799
  value
723
800
  });
@@ -734,7 +811,7 @@ var CaptureEngine = class {
734
811
  }
735
812
  const notBefore = (recorded?.ts ?? now) + minIntervalOf(p.spec);
736
813
  if (now < notBefore) continue;
737
- this.emit(identity, p.key, p.subject, p.value, notBefore);
814
+ this.emit(identity, p.key, p.subject, p.scope, p.value, notBefore);
738
815
  this.deps.counters.trailingEmitted += 1;
739
816
  }
740
817
  }
@@ -782,16 +859,7 @@ var CaptureEngine = class {
782
859
  if (!Number.isInteger(deviceId) || optedOut.has(deviceId)) continue;
783
860
  const type = this.deps.directory.typeOf(deviceId);
784
861
  if (type === void 0 || !isRealDeviceType(type)) continue;
785
- for (const [capName, fields] of Object.entries(this.deps.catalog)) {
786
- const slice = caps[capName];
787
- if (slice === void 0) continue;
788
- for (const [path, spec] of Object.entries(fields)) {
789
- if (spec.excludeDeviceTypes?.includes(type) === true) continue;
790
- const value = this.readValue(slice, capName, path, spec);
791
- if (value === null) continue;
792
- this.consider(deviceId, capName, path, spec, value, now, true);
793
- }
794
- }
862
+ for (const [capName, slice] of Object.entries(caps)) this.captureSlice(deviceId, type, capName, slice, now, true);
795
863
  }
796
864
  }
797
865
  /**
@@ -817,8 +885,8 @@ var CaptureEngine = class {
817
885
  return this.pending.size;
818
886
  }
819
887
  /** The last recorded state of a series, for specs. */
820
- lastRecorded(key, subjectId) {
821
- return this.recorded.get(identityOf(key, subjectId, ""));
888
+ lastRecorded(key, subjectId, scope = "") {
889
+ return this.recorded.get(identityOf(key, subjectId, scope));
822
890
  }
823
891
  };
824
892
  //#endregion
@@ -1287,8 +1355,10 @@ function errorText(err) {
1287
1355
  }
1288
1356
  var HistoryQuery = class {
1289
1357
  deps;
1358
+ itemFields;
1290
1359
  constructor(deps) {
1291
1360
  this.deps = deps;
1361
+ this.itemFields = itemFieldsByKey(deps.itemCatalog);
1292
1362
  }
1293
1363
  async query(input) {
1294
1364
  if (input.key === "derived:energy.kwh") return this.queryEnergy(input);
@@ -1476,12 +1546,14 @@ var HistoryQuery = class {
1476
1546
  for (const s of registry.forSubject(subjectIdOf(subject))) {
1477
1547
  const metric = registry.metric(s.key);
1478
1548
  if (metric === void 0) continue;
1549
+ const item = this.itemFields.get(s.key);
1550
+ const named = item === void 0 || subject.kind !== "device" ? null : await this.itemNaming(subject.deviceId, item, s.scope, metric.unit, slices);
1479
1551
  infos.push({
1480
1552
  key: s.key,
1481
1553
  scope: s.scope,
1482
1554
  kind: metric.kind,
1483
- unit: await this.unitOf(subject, s, metric.unit, slices),
1484
- displayName: metric.displayName,
1555
+ unit: named !== null ? named.unit : await this.unitOf(subject, s, metric.unit, slices),
1556
+ displayName: named !== null ? `${named.label} · ${metric.displayName}` : metric.displayName,
1485
1557
  source: metric.source,
1486
1558
  values: metric.kind === "enum" ? metric.values ?? [] : null,
1487
1559
  firstTs: s.firstTs,
@@ -1502,15 +1574,38 @@ var HistoryQuery = class {
1502
1574
  });
1503
1575
  return infos.toSorted((a, b) => a.key.localeCompare(b.key) || a.scope.localeCompare(b.scope));
1504
1576
  }
1577
+ /**
1578
+ * An item series (D917) is named after its item and takes its `unitFrom`
1579
+ * from it, both read from the device's CURRENT slice: the item's `label`,
1580
+ * else its key (the scope) when the item is gone or the slice unreadable.
1581
+ */
1582
+ async itemNaming(deviceId, item, scope, fixed, slices) {
1583
+ const slice = await this.sliceOf(deviceId, item.capName, slices);
1584
+ const list = slice === null ? void 0 : readPath(slice, item.items.path);
1585
+ const entry = Array.isArray(list) ? list.find((e) => isObject(e) && e[item.items.key] === scope) : void 0;
1586
+ const rawLabel = entry !== void 0 && item.items.label !== void 0 ? entry[item.items.label] : void 0;
1587
+ const label = typeof rawLabel === "string" && rawLabel.length > 0 ? rawLabel : scope;
1588
+ if (fixed !== null || item.spec.unitFrom === void 0) return {
1589
+ label,
1590
+ unit: fixed
1591
+ };
1592
+ return {
1593
+ label,
1594
+ unit: entry === void 0 ? null : readUnit(entry, item.spec.unitFrom)
1595
+ };
1596
+ }
1597
+ sliceOf(deviceId, capName, slices) {
1598
+ const pending = slices.get(capName) ?? this.deps.readCapSlice(deviceId, capName).catch(() => null);
1599
+ slices.set(capName, pending);
1600
+ return pending;
1601
+ }
1505
1602
  /** The metric's fixed unit, else the device's own (a `unitFrom` field), else unknown. */
1506
1603
  async unitOf(subject, series, fixed, slices) {
1507
1604
  if (fixed !== null || subject.kind !== "device" || !series.key.startsWith("cap:")) return fixed;
1508
1605
  const spec = this.specOf(series.key);
1509
1606
  if (spec?.unitFrom === void 0) return null;
1510
1607
  const capName = series.key.slice(CAP_KEY_PREFIX.length, series.key.indexOf("."));
1511
- const pending = slices.get(capName) ?? this.deps.readCapSlice(subject.deviceId, capName).catch(() => null);
1512
- slices.set(capName, pending);
1513
- const slice = await pending;
1608
+ const slice = await this.sliceOf(subject.deviceId, capName, slices);
1514
1609
  return slice === null ? null : readUnit(slice, spec.unitFrom);
1515
1610
  }
1516
1611
  specOf(key) {
@@ -1520,12 +1615,158 @@ var HistoryQuery = class {
1520
1615
  return this.deps.catalog[body.slice(0, dot)]?.[body.slice(dot + 1)];
1521
1616
  }
1522
1617
  };
1618
+ function isObject(value) {
1619
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1620
+ }
1621
+ /** Series key → keyed-item field, for every item field of the catalog. */
1622
+ function itemFieldsByKey(itemCatalog) {
1623
+ return new Map(Object.entries(itemCatalog).flatMap(([capName, items]) => Object.entries(items.fields).map(([field, spec]) => [capItemSeriesKey(capName, items, field), {
1624
+ capName,
1625
+ items,
1626
+ spec
1627
+ }])));
1628
+ }
1523
1629
  /** Every covering tier but raw; the coarsest stays when it is the only one. */
1524
1630
  function withoutRaw(tiers) {
1525
1631
  const rollups = tiers.filter((t) => t !== "raw");
1526
1632
  return rollups.length > 0 ? rollups : ["5m"];
1527
1633
  }
1528
1634
  //#endregion
1635
+ //#region src/builtins/device-history/retention/orphans.ts
1636
+ /**
1637
+ * The orphan verdict of one series, or `null` when it is linked. PURE. A node
1638
+ * series is never orphan here (its subject is a node, not a device).
1639
+ */
1640
+ function judgeOrphan(series, declaredCapKeys, missingSince) {
1641
+ if (series.deviceId === null) return null;
1642
+ if (missingSince !== void 0) return {
1643
+ reason: "device-gone",
1644
+ missingSince
1645
+ };
1646
+ if (series.key.startsWith("cap:") && !declaredCapKeys.has(series.key)) return {
1647
+ reason: "undeclared",
1648
+ missingSince: null
1649
+ };
1650
+ return null;
1651
+ }
1652
+ var OrphanSeries = class {
1653
+ deps;
1654
+ constructor(deps) {
1655
+ this.deps = deps;
1656
+ }
1657
+ verdictOf(series) {
1658
+ const { departures, declaredCapKeys } = this.deps;
1659
+ return judgeOrphan(series, declaredCapKeys, series.deviceId === null ? void 0 : departures.missingSince(series.deviceId));
1660
+ }
1661
+ /** Every orphan, ordered by device, key and scope, the first {@link DEVICE_HISTORY_ORPHAN_LIST_MAX} counted. */
1662
+ async list() {
1663
+ const found = this.deps.registry.all().flatMap((series) => {
1664
+ const verdict = this.verdictOf(series);
1665
+ return verdict === null || series.deviceId === null ? [] : [{
1666
+ series,
1667
+ deviceId: series.deviceId,
1668
+ verdict
1669
+ }];
1670
+ }).toSorted((a, b) => a.deviceId - b.deviceId || a.series.key.localeCompare(b.series.key) || a.series.scope.localeCompare(b.series.scope));
1671
+ const out = [];
1672
+ for (const { series, deviceId, verdict } of found.slice(0, 200)) out.push({
1673
+ seriesId: series.id,
1674
+ deviceId,
1675
+ key: series.key,
1676
+ scope: series.scope,
1677
+ displayName: this.deps.registry.metric(series.key)?.displayName ?? null,
1678
+ reason: verdict.reason,
1679
+ missingSince: verdict.missingSince,
1680
+ firstTs: series.firstTs,
1681
+ lastTs: series.lastTs,
1682
+ rows: await this.rowsOf(series.id)
1683
+ });
1684
+ return {
1685
+ series: out,
1686
+ truncated: found.length > 200
1687
+ };
1688
+ }
1689
+ async rowsOf(seriesId) {
1690
+ const { store } = this.deps;
1691
+ return {
1692
+ raw: await store.countSeriesRows("raw", seriesId),
1693
+ "5m": await store.countSeriesRows("5m", seriesId),
1694
+ "1h": await store.countSeriesRows("1h", seriesId),
1695
+ "1d": await store.countSeriesRows("1d", seriesId)
1696
+ };
1697
+ }
1698
+ /** Delete orphans by id, one at a time, each re-judged now. One entry per id, in order. */
1699
+ async delete(seriesIds) {
1700
+ const out = [];
1701
+ for (const seriesId of seriesIds) out.push(await this.deleteOne(seriesId));
1702
+ return out;
1703
+ }
1704
+ async deleteOne(seriesId) {
1705
+ const { registry, retention, logger, clock } = this.deps;
1706
+ const series = registry.all().find((s) => s.id === seriesId);
1707
+ if (series === void 0) return {
1708
+ seriesId,
1709
+ ok: false,
1710
+ reason: "not-found"
1711
+ };
1712
+ const verdict = this.verdictOf(series);
1713
+ if (verdict === null || series.deviceId === null) {
1714
+ logger.info("device-history: refused to delete a series that is not orphan", {
1715
+ ...series.deviceId === null ? {} : { tags: { deviceId: series.deviceId } },
1716
+ meta: {
1717
+ seriesId,
1718
+ key: series.key,
1719
+ scope: series.scope
1720
+ }
1721
+ });
1722
+ return {
1723
+ seriesId,
1724
+ ok: false,
1725
+ reason: "not-orphan"
1726
+ };
1727
+ }
1728
+ try {
1729
+ const deleted = await retention.purgeSeries(series, clock());
1730
+ registry.remove(series.id);
1731
+ const rows = {
1732
+ raw: deleted.raw,
1733
+ "5m": deleted["5m"],
1734
+ "1h": deleted["1h"],
1735
+ "1d": deleted["1d"]
1736
+ };
1737
+ logger.info("device-history: deleted an orphan series", {
1738
+ tags: { deviceId: series.deviceId },
1739
+ meta: {
1740
+ seriesId,
1741
+ key: series.key,
1742
+ scope: series.scope,
1743
+ reason: verdict.reason,
1744
+ rows
1745
+ }
1746
+ });
1747
+ return {
1748
+ seriesId,
1749
+ ok: true,
1750
+ deleted: rows
1751
+ };
1752
+ } catch (err) {
1753
+ logger.warn("device-history: deleting an orphan series failed — it stays listed, retry the delete", {
1754
+ tags: { deviceId: series.deviceId },
1755
+ meta: {
1756
+ seriesId,
1757
+ key: series.key,
1758
+ error: err instanceof Error ? err.message : String(err)
1759
+ }
1760
+ });
1761
+ return {
1762
+ seriesId,
1763
+ ok: false,
1764
+ reason: "failed"
1765
+ };
1766
+ }
1767
+ }
1768
+ };
1769
+ //#endregion
1529
1770
  //#region src/builtins/device-history/store/history-schema.ts
1530
1771
  var HISTORY_NAMESPACE = "device-history";
1531
1772
  var COLLECTIONS = {
@@ -2136,6 +2377,17 @@ var HistoryStore = class {
2136
2377
  key: String(seriesId)
2137
2378
  });
2138
2379
  }
2380
+ /**
2381
+ * One series' rows of a tier: a range of the primary key (`series_id = ?`),
2382
+ * never a scan of the tier (D447, asserted in `history-query-plans.spec.ts`).
2383
+ */
2384
+ async countSeriesRows(tier, seriesId) {
2385
+ return this.door.count.query({
2386
+ namespace: HISTORY_NAMESPACE,
2387
+ collection: collectionOf(tier),
2388
+ filter: { where: { series_id: seriesId } }
2389
+ });
2390
+ }
2139
2391
  async countRows(tier) {
2140
2392
  return this.door.count.query({
2141
2393
  namespace: HISTORY_NAMESPACE,
@@ -2696,6 +2948,7 @@ var HistoryRuntime = class {
2696
2948
  query;
2697
2949
  retentionJob;
2698
2950
  departures;
2951
+ orphans;
2699
2952
  optedOutDevices = /* @__PURE__ */ new Set();
2700
2953
  tierCounts = /* @__PURE__ */ new Map();
2701
2954
  startedAt;
@@ -2704,7 +2957,7 @@ var HistoryRuntime = class {
2704
2957
  ready = false;
2705
2958
  constructor(deps) {
2706
2959
  this.deps = deps;
2707
- const { logger, clock, catalog } = deps;
2960
+ const { logger, clock, catalog, itemCatalog } = deps;
2708
2961
  this.startedAt = clock();
2709
2962
  this.store = new HistoryStore(deps.door);
2710
2963
  this.registry = new SeriesRegistry(this.store);
@@ -2728,6 +2981,7 @@ var HistoryRuntime = class {
2728
2981
  const optedOut = () => this.optedOutDevices;
2729
2982
  this.capture = new CaptureEngine({
2730
2983
  catalog,
2984
+ itemCatalog,
2731
2985
  registry: this.registry,
2732
2986
  store: this.store,
2733
2987
  directory: this.directory,
@@ -2761,6 +3015,7 @@ var HistoryRuntime = class {
2761
3015
  clock,
2762
3016
  retention: deps.retention,
2763
3017
  catalog,
3018
+ itemCatalog,
2764
3019
  readCapSlice: deps.readCapSlice
2765
3020
  });
2766
3021
  this.departures = new DepartureTracker(this.store, this.registry, logger);
@@ -2777,11 +3032,20 @@ var HistoryRuntime = class {
2777
3032
  counters: this.counters,
2778
3033
  logger
2779
3034
  });
3035
+ this.orphans = new OrphanSeries({
3036
+ store: this.store,
3037
+ registry: this.registry,
3038
+ departures: this.departures,
3039
+ retention: this.retentionJob,
3040
+ declaredCapKeys: new Set(catalogMetrics(catalog, itemCatalog, 0).map((m) => m.key)),
3041
+ logger,
3042
+ clock
3043
+ });
2780
3044
  }
2781
3045
  async prepare() {
2782
3046
  await this.store.declare();
2783
3047
  await this.registry.load();
2784
- for (const metric of catalogMetrics(this.deps.catalog, this.deps.clock())) await this.registry.declareMetric(metric);
3048
+ for (const metric of catalogMetrics(this.deps.catalog, this.deps.itemCatalog, this.deps.clock())) await this.registry.declareMetric(metric);
2785
3049
  this.optedOutDevices = await this.store.loadOptOuts();
2786
3050
  await this.departures.load();
2787
3051
  await this.directory.refresh();
@@ -2900,6 +3164,14 @@ var HistoryRuntime = class {
2900
3164
  this.optedOutDevices = new Set([...this.optedOutDevices].filter((id) => id !== deviceId));
2901
3165
  return { series };
2902
3166
  }
3167
+ /** Device series no longer linked to a device — reported, never purged here. */
3168
+ listOrphans() {
3169
+ return this.orphans.list();
3170
+ }
3171
+ /** Delete orphan series by id; a series that is not orphan NOW is refused. */
3172
+ deleteOrphans(seriesIds) {
3173
+ return this.orphans.delete(seriesIds);
3174
+ }
2903
3175
  /** Series held per device, for the admin clean-up listing. */
2904
3176
  seriesByDevice() {
2905
3177
  const out = /* @__PURE__ */ new Map();
@@ -3095,7 +3367,9 @@ var DeviceHistoryAddon = class extends BaseAddon {
3095
3367
  recordBuckets: async (input) => this.requireRuntime().ingest.recordBuckets(callerOf(input), input.buckets),
3096
3368
  getDeviceEnabled: async (input) => ({ enabled: this.requireRuntime().isEnabled(input.deviceId) }),
3097
3369
  setDeviceEnabled: async (input) => ({ enabled: await this.requireRuntime().setEnabled(input.deviceId, input.enabled) }),
3098
- getStatus: async () => this.requireRuntime().status()
3370
+ getStatus: async () => this.requireRuntime().status(),
3371
+ listOrphanSeries: async () => this.requireRuntime().listOrphans(),
3372
+ deleteOrphanSeries: async (input) => ({ results: await this.requireRuntime().deleteOrphans(input.seriesIds) })
3099
3373
  };
3100
3374
  this.emitStoreReadiness("starting");
3101
3375
  this.ctx.logger.info("Initialized — device-history provider registered (store opens on hub link)");
@@ -3131,6 +3405,7 @@ var DeviceHistoryAddon = class extends BaseAddon {
3131
3405
  const runtime = new HistoryRuntime({
3132
3406
  door: api.settingsStore,
3133
3407
  catalog: HISTORY_CATALOG,
3408
+ itemCatalog: HISTORY_ITEM_CATALOG,
3134
3409
  deviceSource: {
3135
3410
  listAll: async () => {
3136
3411
  return (await api.deviceManager.listAll.query({ projection: "slim" })).map((d) => ({
@@ -19,11 +19,11 @@
19
19
  * One place makes that choice, so two charts of one device cannot disagree, and
20
20
  * it covers every hour of history recorded before the key existed.
21
21
  */
22
- import { type DeviceHistoryQueryManyEntry, type DeviceHistoryQueryManyItem, type DeviceHistoryQueryPoint, type DeviceHistoryResolution, type DeviceHistorySeriesInfo, type DeviceHistorySubject } from "@camstack/types";
22
+ import { type CapabilityHistoryField, type CapabilityHistoryItems, type DeviceHistoryQueryManyEntry, type DeviceHistoryQueryManyItem, type DeviceHistoryQueryPoint, type DeviceHistoryResolution, type DeviceHistorySeriesInfo, type DeviceHistorySubject } from "@camstack/types";
23
23
  import { type RetentionPolicy } from "../retention/retention.js";
24
24
  import type { HistoryStore } from "../store/history-store.js";
25
25
  import { type SeriesRegistry } from "../store/series-registry.js";
26
- import { type HistoryCatalog } from "../capture/capture-engine.js";
26
+ import { type HistoryCatalog, type HistoryItemCatalog } from "../capture/capture-engine.js";
27
27
  type Tier = DeviceHistoryResolution;
28
28
  /** The power-meter series the energy key is derived from, meter first. */
29
29
  export declare const ENERGY_METER_KEY: string;
@@ -66,11 +66,13 @@ export interface QueryDeps {
66
66
  readonly clock: () => number;
67
67
  readonly retention: () => RetentionPolicy;
68
68
  readonly catalog: HistoryCatalog;
69
+ readonly itemCatalog: HistoryItemCatalog;
69
70
  /** The device's current slice of a cap, for a `unitFrom` series; `null` when unreadable. */
70
71
  readonly readCapSlice: (deviceId: number, capName: string) => Promise<Readonly<Record<string, unknown>> | null>;
71
72
  }
72
73
  export declare class HistoryQuery {
73
74
  private readonly deps;
75
+ private readonly itemFields;
74
76
  constructor(deps: QueryDeps);
75
77
  query(input: QueryInput): Promise<QueryOutput>;
76
78
  /** Several reads over one window; one failing item answers its error, not the batch's. */
@@ -88,8 +90,23 @@ export declare class HistoryQuery {
88
90
  private energySource;
89
91
  private queryEnergy;
90
92
  listSeries(subject: DeviceHistorySubject): Promise<readonly DeviceHistorySeriesInfo[]>;
93
+ /**
94
+ * An item series (D917) is named after its item and takes its `unitFrom`
95
+ * from it, both read from the device's CURRENT slice: the item's `label`,
96
+ * else its key (the scope) when the item is gone or the slice unreadable.
97
+ */
98
+ private itemNaming;
99
+ private sliceOf;
91
100
  /** The metric's fixed unit, else the device's own (a `unitFrom` field), else unknown. */
92
101
  private unitOf;
93
102
  private specOf;
94
103
  }
104
+ /** One keyed-item field (D917), by its series key. */
105
+ interface ItemField {
106
+ readonly capName: string;
107
+ readonly items: CapabilityHistoryItems;
108
+ readonly spec: CapabilityHistoryField;
109
+ }
110
+ /** Series key → keyed-item field, for every item field of the catalog. */
111
+ export declare function itemFieldsByKey(itemCatalog: HistoryItemCatalog): ReadonlyMap<string, ItemField>;
95
112
  export {};