@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
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-l6zKmHEb.js");
6
+ const require_dist = require("../../dist-DuBtzSCh.js");
7
7
  //#region src/builtins/device-history/time/bucket-time.ts
8
8
  /**
9
9
  * Bucket arithmetic for the three rollup tiers (D850, plan §4/§5.5).
@@ -190,13 +190,25 @@ var RetentionJob = class {
190
190
  }
191
191
  return deleted;
192
192
  }
193
- /** Tier by tier in slices, then the series row (and whatever the slices missed). */
193
+ /**
194
+ * Tier by tier in slices, then the series row (and whatever the slices missed).
195
+ * Answers the rows the slices deleted per tier. The caller removes the series
196
+ * from the registry, which releases its per-series state.
197
+ */
194
198
  async purgeSeries(series, now) {
199
+ const deleted = {
200
+ raw: 0,
201
+ "5m": 0,
202
+ "1h": 0,
203
+ "1d": 0
204
+ };
195
205
  for (const tier of TIERS) {
196
206
  const lo = tier === "raw" || tier === "5m" ? Math.max(series.firstTs - BUCKET_LEAD_MS, now - SHORT_TIER_LOOKBACK_MS) : series.firstTs - BUCKET_LEAD_MS;
197
- await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
207
+ deleted[tier] = await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
208
+ this.deps.counters.addDeleted(tier, deleted[tier]);
198
209
  }
199
210
  await this.deps.store.deleteSeries(series.id);
211
+ return deleted;
200
212
  }
201
213
  /**
202
214
  * Delete every series of a DELETED device now (D865). Driven by a tombstone
@@ -563,14 +575,14 @@ var DeviceDirectory = class {
563
575
  * `runtimeState` key or a path into a FIXED object (`dustBin.level`); the
564
576
  * catalog guard keeps it out of arrays and records, so a plain walk is enough.
565
577
  */
566
- function isRecord$1(value) {
578
+ function isRecord$2(value) {
567
579
  return typeof value === "object" && value !== null && !Array.isArray(value);
568
580
  }
569
581
  /** The value at `path`, or `undefined` when any step is missing or not an object. */
570
582
  function readPath(slice, path) {
571
583
  let current = slice;
572
584
  for (const step of path.split(".")) {
573
- if (!isRecord$1(current)) return void 0;
585
+ if (!isRecord$2(current)) return void 0;
574
586
  current = current[step];
575
587
  }
576
588
  return current;
@@ -596,18 +608,41 @@ function readUnit(slice, unitFrom) {
596
608
  }
597
609
  //#endregion
598
610
  //#region src/builtins/device-history/capture/capture-engine.ts
611
+ /**
612
+ * Capture of capability fields (D850, plan §5.3), driven by the GENERATED
613
+ * history catalog — a field is recorded because its `*.cap.ts` says so, never
614
+ * because someone listed it here.
615
+ *
616
+ * For a `DeviceStateChanged { deviceId, capName, slice }` the engine:
617
+ * 1. ignores a cap with no catalog entry (the common case — cheap, uncounted);
618
+ * 2. skips an opted-out device (counted), a device whose type is not yet
619
+ * known (parked, then replayed once the hub answers), and a `hub` /
620
+ * `container` (counted: not a real device);
621
+ * 3. per field: skips an excluded device type, an absent or non-numeric value
622
+ * (null is UNKNOWN, never 0), then runs {@link decideCapture} against the
623
+ * last RECORDED value — change-only, deadband, capture floor;
624
+ * 4. buffers the point for the batched write.
625
+ *
626
+ * A change inside the capture floor is kept PENDING and emitted when the floor
627
+ * has passed ({@link CaptureEngine.tickPending}), so the end of a burst is never
628
+ * lost. Reconcile ({@link CaptureEngine.reconcile}) restores what a dropped
629
+ * event missed from the state mirror's snapshot.
630
+ */
599
631
  var CAP_KEY_PREFIX = "cap:";
600
632
  function capSeriesKey(capName, fieldPath) {
601
633
  return `${CAP_KEY_PREFIX}${capName}.${fieldPath}`;
602
634
  }
635
+ /** The series key of an item field: `cap:<capName>.<path>[].<field>`. */
636
+ function capItemSeriesKey(capName, items, field) {
637
+ return capSeriesKey(capName, require_dist.historyItemFieldPath(items.path, field));
638
+ }
603
639
  function humanize(capName, fieldPath) {
604
640
  const field = fieldPath.replace(/([A-Z])/g, " $1").replace(/\./g, " ").toLowerCase();
605
641
  return `${capName.replace(/-/g, " ")} ${field}`;
606
642
  }
607
- /** The metric rows of every catalog field (owner `device-history`). */
608
- function catalogMetrics(catalog, now) {
609
- return Object.entries(catalog).flatMap(([capName, fields]) => Object.entries(fields).map(([path, spec]) => ({
610
- key: capSeriesKey(capName, path),
643
+ function metricOf(key, capName, path, spec, now) {
644
+ return {
645
+ key,
611
646
  source: "cap",
612
647
  ownerAddon: "device-history",
613
648
  subjectKind: "device",
@@ -617,7 +652,16 @@ function catalogMetrics(catalog, now) {
617
652
  ingest: "points",
618
653
  declaredAt: now,
619
654
  values: spec.kind === "enum" ? spec.values ?? [] : null
620
- })));
655
+ };
656
+ }
657
+ /** The metric rows of every catalog field and keyed-item field (owner `device-history`). */
658
+ function catalogMetrics(catalog, itemCatalog, now) {
659
+ const fields = Object.entries(catalog).flatMap(([capName, specs]) => Object.entries(specs).map(([path, spec]) => metricOf(capSeriesKey(capName, path), capName, path, spec, now)));
660
+ const items = Object.entries(itemCatalog).flatMap(([capName, block]) => Object.entries(block.fields).map(([field, spec]) => metricOf(capItemSeriesKey(capName, block, field), capName, field, spec, now)));
661
+ return [...fields, ...items];
662
+ }
663
+ function isRecord$1(value) {
664
+ return typeof value === "object" && value !== null && !Array.isArray(value);
621
665
  }
622
666
  /** Devices waiting on a type lookup; beyond this the oldest is dropped (and counted). */
623
667
  var MAX_PARKED_DEVICES = 5e3;
@@ -644,7 +688,8 @@ var CaptureEngine = class {
644
688
  /** One `DeviceStateChanged`. Synchronous: the bus must never wait on us. */
645
689
  handleState(deviceId, capName, slice, at) {
646
690
  const fields = this.deps.catalog[capName];
647
- if (fields === void 0) return;
691
+ const items = this.deps.itemCatalog[capName];
692
+ if (fields === void 0 && items === void 0) return;
648
693
  const { counters, directory } = this.deps;
649
694
  counters.eventsSeen += 1;
650
695
  if (this.deps.optedOut().has(deviceId)) {
@@ -660,14 +705,46 @@ var CaptureEngine = class {
660
705
  counters.skip("not-a-real-device");
661
706
  return;
662
707
  }
663
- for (const [path, spec] of Object.entries(fields)) {
708
+ this.captureSlice(deviceId, type, capName, slice, at, false);
709
+ }
710
+ /** Every catalogued field and keyed item of one cap slice, for a real device. */
711
+ captureSlice(deviceId, type, capName, slice, at, reconciling) {
712
+ for (const [path, spec] of Object.entries(this.deps.catalog[capName] ?? {})) {
664
713
  if (spec.excludeDeviceTypes?.includes(type) === true) {
665
- counters.skip("excluded-device-type");
714
+ if (!reconciling) this.deps.counters.skip("excluded-device-type");
666
715
  continue;
667
716
  }
668
- const value = this.readValue(slice, capName, path, spec);
717
+ const key = capSeriesKey(capName, path);
718
+ const value = this.readValue(slice, key, path, spec);
669
719
  if (value === null) continue;
670
- this.consider(deviceId, capName, path, spec, value, at, false);
720
+ this.consider(deviceId, key, "", spec, value, at, reconciling);
721
+ }
722
+ const items = this.deps.itemCatalog[capName];
723
+ if (items !== void 0) this.captureItems(deviceId, type, capName, items, slice, at, reconciling);
724
+ }
725
+ /**
726
+ * A keyed array (D917): each item with a usable key contributes its fields
727
+ * under that key as the series scope. A value that is not an array is no
728
+ * reading; an item without a string key cannot be followed and is counted.
729
+ */
730
+ captureItems(deviceId, type, capName, items, slice, at, reconciling) {
731
+ const list = readPath(slice, items.path);
732
+ if (!Array.isArray(list)) return;
733
+ const { counters } = this.deps;
734
+ if (list.length > 64) counters.skip("item-overflow", list.length - 64);
735
+ for (const entry of list.slice(0, 64)) {
736
+ const scope = isRecord$1(entry) ? entry[items.key] : void 0;
737
+ if (!isRecord$1(entry) || typeof scope !== "string" || scope.length === 0 || scope.length > 128) {
738
+ counters.skip("item-key-invalid");
739
+ continue;
740
+ }
741
+ for (const [field, spec] of Object.entries(items.fields)) {
742
+ if (spec.excludeDeviceTypes?.includes(type) === true) continue;
743
+ const key = capItemSeriesKey(capName, items, field);
744
+ const value = this.readValue(entry, key, field, spec);
745
+ if (value === null) continue;
746
+ this.consider(deviceId, key, scope, spec, value, at, reconciling);
747
+ }
671
748
  }
672
749
  }
673
750
  /**
@@ -675,7 +752,7 @@ var CaptureEngine = class {
675
752
  * `enum` state becomes the ordinal of its value in the metric's list; a value
676
753
  * the full list cannot take is refused and counted (`enum-overflow`).
677
754
  */
678
- readValue(slice, capName, path, spec) {
755
+ readValue(slice, key, path, spec) {
679
756
  if (spec.skipWhenSet !== void 0) {
680
757
  const gate = readPath(slice, spec.skipWhenSet);
681
758
  if (gate !== void 0 && gate !== null) return null;
@@ -683,20 +760,19 @@ var CaptureEngine = class {
683
760
  if (spec.kind !== "enum") return readNumeric(slice, path, spec.kind);
684
761
  const raw = readPath(slice, path);
685
762
  if (typeof raw !== "string" || raw.length === 0) return null;
686
- const ordinal = this.deps.registry.ordinalOf(capSeriesKey(capName, path), raw);
763
+ const ordinal = this.deps.registry.ordinalOf(key, raw);
687
764
  if (ordinal === null) this.deps.counters.skip("enum-overflow");
688
765
  return ordinal;
689
766
  }
690
- consider(deviceId, capName, path, spec, value, at, reconciling) {
691
- const key = capSeriesKey(capName, path);
767
+ consider(deviceId, key, scope, spec, value, at, reconciling) {
692
768
  const subject = {
693
769
  kind: "device",
694
770
  deviceId
695
771
  };
696
- const identity = identityOf(key, `d:${deviceId}`, "");
772
+ const identity = identityOf(key, `d:${deviceId}`, scope);
697
773
  const decision = decideCapture(spec, this.recorded.get(identity) ?? null, value, at, { ignoreFloor: reconciling });
698
774
  if (decision.action === "emit") {
699
- this.emit(identity, key, subject, value, at);
775
+ this.emit(identity, key, subject, scope, value, at);
700
776
  if (reconciling) this.deps.counters.reconciledPoints += 1;
701
777
  return;
702
778
  }
@@ -704,6 +780,7 @@ var CaptureEngine = class {
704
780
  this.pending.set(identity, {
705
781
  key,
706
782
  subject,
783
+ scope,
707
784
  spec,
708
785
  value
709
786
  });
@@ -712,7 +789,7 @@ var CaptureEngine = class {
712
789
  this.pending.delete(identity);
713
790
  this.deps.counters.skip(decision.reason);
714
791
  }
715
- emit(identity, key, subject, value, ts) {
792
+ emit(identity, key, subject, scope, value, ts) {
716
793
  this.recorded.set(identity, {
717
794
  value,
718
795
  ts
@@ -722,7 +799,7 @@ var CaptureEngine = class {
722
799
  this.deps.writer.enqueue({
723
800
  key,
724
801
  subject,
725
- scope: "",
802
+ scope,
726
803
  ts,
727
804
  value
728
805
  });
@@ -739,7 +816,7 @@ var CaptureEngine = class {
739
816
  }
740
817
  const notBefore = (recorded?.ts ?? now) + minIntervalOf(p.spec);
741
818
  if (now < notBefore) continue;
742
- this.emit(identity, p.key, p.subject, p.value, notBefore);
819
+ this.emit(identity, p.key, p.subject, p.scope, p.value, notBefore);
743
820
  this.deps.counters.trailingEmitted += 1;
744
821
  }
745
822
  }
@@ -787,16 +864,7 @@ var CaptureEngine = class {
787
864
  if (!Number.isInteger(deviceId) || optedOut.has(deviceId)) continue;
788
865
  const type = this.deps.directory.typeOf(deviceId);
789
866
  if (type === void 0 || !isRealDeviceType(type)) continue;
790
- for (const [capName, fields] of Object.entries(this.deps.catalog)) {
791
- const slice = caps[capName];
792
- if (slice === void 0) continue;
793
- for (const [path, spec] of Object.entries(fields)) {
794
- if (spec.excludeDeviceTypes?.includes(type) === true) continue;
795
- const value = this.readValue(slice, capName, path, spec);
796
- if (value === null) continue;
797
- this.consider(deviceId, capName, path, spec, value, now, true);
798
- }
799
- }
867
+ for (const [capName, slice] of Object.entries(caps)) this.captureSlice(deviceId, type, capName, slice, now, true);
800
868
  }
801
869
  }
802
870
  /**
@@ -822,8 +890,8 @@ var CaptureEngine = class {
822
890
  return this.pending.size;
823
891
  }
824
892
  /** The last recorded state of a series, for specs. */
825
- lastRecorded(key, subjectId) {
826
- return this.recorded.get(identityOf(key, subjectId, ""));
893
+ lastRecorded(key, subjectId, scope = "") {
894
+ return this.recorded.get(identityOf(key, subjectId, scope));
827
895
  }
828
896
  };
829
897
  //#endregion
@@ -1292,8 +1360,10 @@ function errorText(err) {
1292
1360
  }
1293
1361
  var HistoryQuery = class {
1294
1362
  deps;
1363
+ itemFields;
1295
1364
  constructor(deps) {
1296
1365
  this.deps = deps;
1366
+ this.itemFields = itemFieldsByKey(deps.itemCatalog);
1297
1367
  }
1298
1368
  async query(input) {
1299
1369
  if (input.key === "derived:energy.kwh") return this.queryEnergy(input);
@@ -1481,12 +1551,14 @@ var HistoryQuery = class {
1481
1551
  for (const s of registry.forSubject(subjectIdOf(subject))) {
1482
1552
  const metric = registry.metric(s.key);
1483
1553
  if (metric === void 0) continue;
1554
+ const item = this.itemFields.get(s.key);
1555
+ const named = item === void 0 || subject.kind !== "device" ? null : await this.itemNaming(subject.deviceId, item, s.scope, metric.unit, slices);
1484
1556
  infos.push({
1485
1557
  key: s.key,
1486
1558
  scope: s.scope,
1487
1559
  kind: metric.kind,
1488
- unit: await this.unitOf(subject, s, metric.unit, slices),
1489
- displayName: metric.displayName,
1560
+ unit: named !== null ? named.unit : await this.unitOf(subject, s, metric.unit, slices),
1561
+ displayName: named !== null ? `${named.label} · ${metric.displayName}` : metric.displayName,
1490
1562
  source: metric.source,
1491
1563
  values: metric.kind === "enum" ? metric.values ?? [] : null,
1492
1564
  firstTs: s.firstTs,
@@ -1507,15 +1579,38 @@ var HistoryQuery = class {
1507
1579
  });
1508
1580
  return infos.toSorted((a, b) => a.key.localeCompare(b.key) || a.scope.localeCompare(b.scope));
1509
1581
  }
1582
+ /**
1583
+ * An item series (D917) is named after its item and takes its `unitFrom`
1584
+ * from it, both read from the device's CURRENT slice: the item's `label`,
1585
+ * else its key (the scope) when the item is gone or the slice unreadable.
1586
+ */
1587
+ async itemNaming(deviceId, item, scope, fixed, slices) {
1588
+ const slice = await this.sliceOf(deviceId, item.capName, slices);
1589
+ const list = slice === null ? void 0 : readPath(slice, item.items.path);
1590
+ const entry = Array.isArray(list) ? list.find((e) => isObject(e) && e[item.items.key] === scope) : void 0;
1591
+ const rawLabel = entry !== void 0 && item.items.label !== void 0 ? entry[item.items.label] : void 0;
1592
+ const label = typeof rawLabel === "string" && rawLabel.length > 0 ? rawLabel : scope;
1593
+ if (fixed !== null || item.spec.unitFrom === void 0) return {
1594
+ label,
1595
+ unit: fixed
1596
+ };
1597
+ return {
1598
+ label,
1599
+ unit: entry === void 0 ? null : readUnit(entry, item.spec.unitFrom)
1600
+ };
1601
+ }
1602
+ sliceOf(deviceId, capName, slices) {
1603
+ const pending = slices.get(capName) ?? this.deps.readCapSlice(deviceId, capName).catch(() => null);
1604
+ slices.set(capName, pending);
1605
+ return pending;
1606
+ }
1510
1607
  /** The metric's fixed unit, else the device's own (a `unitFrom` field), else unknown. */
1511
1608
  async unitOf(subject, series, fixed, slices) {
1512
1609
  if (fixed !== null || subject.kind !== "device" || !series.key.startsWith("cap:")) return fixed;
1513
1610
  const spec = this.specOf(series.key);
1514
1611
  if (spec?.unitFrom === void 0) return null;
1515
1612
  const capName = series.key.slice(CAP_KEY_PREFIX.length, series.key.indexOf("."));
1516
- const pending = slices.get(capName) ?? this.deps.readCapSlice(subject.deviceId, capName).catch(() => null);
1517
- slices.set(capName, pending);
1518
- const slice = await pending;
1613
+ const slice = await this.sliceOf(subject.deviceId, capName, slices);
1519
1614
  return slice === null ? null : readUnit(slice, spec.unitFrom);
1520
1615
  }
1521
1616
  specOf(key) {
@@ -1525,12 +1620,158 @@ var HistoryQuery = class {
1525
1620
  return this.deps.catalog[body.slice(0, dot)]?.[body.slice(dot + 1)];
1526
1621
  }
1527
1622
  };
1623
+ function isObject(value) {
1624
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1625
+ }
1626
+ /** Series key → keyed-item field, for every item field of the catalog. */
1627
+ function itemFieldsByKey(itemCatalog) {
1628
+ return new Map(Object.entries(itemCatalog).flatMap(([capName, items]) => Object.entries(items.fields).map(([field, spec]) => [capItemSeriesKey(capName, items, field), {
1629
+ capName,
1630
+ items,
1631
+ spec
1632
+ }])));
1633
+ }
1528
1634
  /** Every covering tier but raw; the coarsest stays when it is the only one. */
1529
1635
  function withoutRaw(tiers) {
1530
1636
  const rollups = tiers.filter((t) => t !== "raw");
1531
1637
  return rollups.length > 0 ? rollups : ["5m"];
1532
1638
  }
1533
1639
  //#endregion
1640
+ //#region src/builtins/device-history/retention/orphans.ts
1641
+ /**
1642
+ * The orphan verdict of one series, or `null` when it is linked. PURE. A node
1643
+ * series is never orphan here (its subject is a node, not a device).
1644
+ */
1645
+ function judgeOrphan(series, declaredCapKeys, missingSince) {
1646
+ if (series.deviceId === null) return null;
1647
+ if (missingSince !== void 0) return {
1648
+ reason: "device-gone",
1649
+ missingSince
1650
+ };
1651
+ if (series.key.startsWith("cap:") && !declaredCapKeys.has(series.key)) return {
1652
+ reason: "undeclared",
1653
+ missingSince: null
1654
+ };
1655
+ return null;
1656
+ }
1657
+ var OrphanSeries = class {
1658
+ deps;
1659
+ constructor(deps) {
1660
+ this.deps = deps;
1661
+ }
1662
+ verdictOf(series) {
1663
+ const { departures, declaredCapKeys } = this.deps;
1664
+ return judgeOrphan(series, declaredCapKeys, series.deviceId === null ? void 0 : departures.missingSince(series.deviceId));
1665
+ }
1666
+ /** Every orphan, ordered by device, key and scope, the first {@link DEVICE_HISTORY_ORPHAN_LIST_MAX} counted. */
1667
+ async list() {
1668
+ const found = this.deps.registry.all().flatMap((series) => {
1669
+ const verdict = this.verdictOf(series);
1670
+ return verdict === null || series.deviceId === null ? [] : [{
1671
+ series,
1672
+ deviceId: series.deviceId,
1673
+ verdict
1674
+ }];
1675
+ }).toSorted((a, b) => a.deviceId - b.deviceId || a.series.key.localeCompare(b.series.key) || a.series.scope.localeCompare(b.series.scope));
1676
+ const out = [];
1677
+ for (const { series, deviceId, verdict } of found.slice(0, 200)) out.push({
1678
+ seriesId: series.id,
1679
+ deviceId,
1680
+ key: series.key,
1681
+ scope: series.scope,
1682
+ displayName: this.deps.registry.metric(series.key)?.displayName ?? null,
1683
+ reason: verdict.reason,
1684
+ missingSince: verdict.missingSince,
1685
+ firstTs: series.firstTs,
1686
+ lastTs: series.lastTs,
1687
+ rows: await this.rowsOf(series.id)
1688
+ });
1689
+ return {
1690
+ series: out,
1691
+ truncated: found.length > 200
1692
+ };
1693
+ }
1694
+ async rowsOf(seriesId) {
1695
+ const { store } = this.deps;
1696
+ return {
1697
+ raw: await store.countSeriesRows("raw", seriesId),
1698
+ "5m": await store.countSeriesRows("5m", seriesId),
1699
+ "1h": await store.countSeriesRows("1h", seriesId),
1700
+ "1d": await store.countSeriesRows("1d", seriesId)
1701
+ };
1702
+ }
1703
+ /** Delete orphans by id, one at a time, each re-judged now. One entry per id, in order. */
1704
+ async delete(seriesIds) {
1705
+ const out = [];
1706
+ for (const seriesId of seriesIds) out.push(await this.deleteOne(seriesId));
1707
+ return out;
1708
+ }
1709
+ async deleteOne(seriesId) {
1710
+ const { registry, retention, logger, clock } = this.deps;
1711
+ const series = registry.all().find((s) => s.id === seriesId);
1712
+ if (series === void 0) return {
1713
+ seriesId,
1714
+ ok: false,
1715
+ reason: "not-found"
1716
+ };
1717
+ const verdict = this.verdictOf(series);
1718
+ if (verdict === null || series.deviceId === null) {
1719
+ logger.info("device-history: refused to delete a series that is not orphan", {
1720
+ ...series.deviceId === null ? {} : { tags: { deviceId: series.deviceId } },
1721
+ meta: {
1722
+ seriesId,
1723
+ key: series.key,
1724
+ scope: series.scope
1725
+ }
1726
+ });
1727
+ return {
1728
+ seriesId,
1729
+ ok: false,
1730
+ reason: "not-orphan"
1731
+ };
1732
+ }
1733
+ try {
1734
+ const deleted = await retention.purgeSeries(series, clock());
1735
+ registry.remove(series.id);
1736
+ const rows = {
1737
+ raw: deleted.raw,
1738
+ "5m": deleted["5m"],
1739
+ "1h": deleted["1h"],
1740
+ "1d": deleted["1d"]
1741
+ };
1742
+ logger.info("device-history: deleted an orphan series", {
1743
+ tags: { deviceId: series.deviceId },
1744
+ meta: {
1745
+ seriesId,
1746
+ key: series.key,
1747
+ scope: series.scope,
1748
+ reason: verdict.reason,
1749
+ rows
1750
+ }
1751
+ });
1752
+ return {
1753
+ seriesId,
1754
+ ok: true,
1755
+ deleted: rows
1756
+ };
1757
+ } catch (err) {
1758
+ logger.warn("device-history: deleting an orphan series failed — it stays listed, retry the delete", {
1759
+ tags: { deviceId: series.deviceId },
1760
+ meta: {
1761
+ seriesId,
1762
+ key: series.key,
1763
+ error: err instanceof Error ? err.message : String(err)
1764
+ }
1765
+ });
1766
+ return {
1767
+ seriesId,
1768
+ ok: false,
1769
+ reason: "failed"
1770
+ };
1771
+ }
1772
+ }
1773
+ };
1774
+ //#endregion
1534
1775
  //#region src/builtins/device-history/store/history-schema.ts
1535
1776
  var HISTORY_NAMESPACE = "device-history";
1536
1777
  var COLLECTIONS = {
@@ -2141,6 +2382,17 @@ var HistoryStore = class {
2141
2382
  key: String(seriesId)
2142
2383
  });
2143
2384
  }
2385
+ /**
2386
+ * One series' rows of a tier: a range of the primary key (`series_id = ?`),
2387
+ * never a scan of the tier (D447, asserted in `history-query-plans.spec.ts`).
2388
+ */
2389
+ async countSeriesRows(tier, seriesId) {
2390
+ return this.door.count.query({
2391
+ namespace: HISTORY_NAMESPACE,
2392
+ collection: collectionOf(tier),
2393
+ filter: { where: { series_id: seriesId } }
2394
+ });
2395
+ }
2144
2396
  async countRows(tier) {
2145
2397
  return this.door.count.query({
2146
2398
  namespace: HISTORY_NAMESPACE,
@@ -2701,6 +2953,7 @@ var HistoryRuntime = class {
2701
2953
  query;
2702
2954
  retentionJob;
2703
2955
  departures;
2956
+ orphans;
2704
2957
  optedOutDevices = /* @__PURE__ */ new Set();
2705
2958
  tierCounts = /* @__PURE__ */ new Map();
2706
2959
  startedAt;
@@ -2709,7 +2962,7 @@ var HistoryRuntime = class {
2709
2962
  ready = false;
2710
2963
  constructor(deps) {
2711
2964
  this.deps = deps;
2712
- const { logger, clock, catalog } = deps;
2965
+ const { logger, clock, catalog, itemCatalog } = deps;
2713
2966
  this.startedAt = clock();
2714
2967
  this.store = new HistoryStore(deps.door);
2715
2968
  this.registry = new SeriesRegistry(this.store);
@@ -2733,6 +2986,7 @@ var HistoryRuntime = class {
2733
2986
  const optedOut = () => this.optedOutDevices;
2734
2987
  this.capture = new CaptureEngine({
2735
2988
  catalog,
2989
+ itemCatalog,
2736
2990
  registry: this.registry,
2737
2991
  store: this.store,
2738
2992
  directory: this.directory,
@@ -2766,6 +3020,7 @@ var HistoryRuntime = class {
2766
3020
  clock,
2767
3021
  retention: deps.retention,
2768
3022
  catalog,
3023
+ itemCatalog,
2769
3024
  readCapSlice: deps.readCapSlice
2770
3025
  });
2771
3026
  this.departures = new DepartureTracker(this.store, this.registry, logger);
@@ -2782,11 +3037,20 @@ var HistoryRuntime = class {
2782
3037
  counters: this.counters,
2783
3038
  logger
2784
3039
  });
3040
+ this.orphans = new OrphanSeries({
3041
+ store: this.store,
3042
+ registry: this.registry,
3043
+ departures: this.departures,
3044
+ retention: this.retentionJob,
3045
+ declaredCapKeys: new Set(catalogMetrics(catalog, itemCatalog, 0).map((m) => m.key)),
3046
+ logger,
3047
+ clock
3048
+ });
2785
3049
  }
2786
3050
  async prepare() {
2787
3051
  await this.store.declare();
2788
3052
  await this.registry.load();
2789
- for (const metric of catalogMetrics(this.deps.catalog, this.deps.clock())) await this.registry.declareMetric(metric);
3053
+ for (const metric of catalogMetrics(this.deps.catalog, this.deps.itemCatalog, this.deps.clock())) await this.registry.declareMetric(metric);
2790
3054
  this.optedOutDevices = await this.store.loadOptOuts();
2791
3055
  await this.departures.load();
2792
3056
  await this.directory.refresh();
@@ -2905,6 +3169,14 @@ var HistoryRuntime = class {
2905
3169
  this.optedOutDevices = new Set([...this.optedOutDevices].filter((id) => id !== deviceId));
2906
3170
  return { series };
2907
3171
  }
3172
+ /** Device series no longer linked to a device — reported, never purged here. */
3173
+ listOrphans() {
3174
+ return this.orphans.list();
3175
+ }
3176
+ /** Delete orphan series by id; a series that is not orphan NOW is refused. */
3177
+ deleteOrphans(seriesIds) {
3178
+ return this.orphans.delete(seriesIds);
3179
+ }
2908
3180
  /** Series held per device, for the admin clean-up listing. */
2909
3181
  seriesByDevice() {
2910
3182
  const out = /* @__PURE__ */ new Map();
@@ -3100,7 +3372,9 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
3100
3372
  recordBuckets: async (input) => this.requireRuntime().ingest.recordBuckets(callerOf(input), input.buckets),
3101
3373
  getDeviceEnabled: async (input) => ({ enabled: this.requireRuntime().isEnabled(input.deviceId) }),
3102
3374
  setDeviceEnabled: async (input) => ({ enabled: await this.requireRuntime().setEnabled(input.deviceId, input.enabled) }),
3103
- getStatus: async () => this.requireRuntime().status()
3375
+ getStatus: async () => this.requireRuntime().status(),
3376
+ listOrphanSeries: async () => this.requireRuntime().listOrphans(),
3377
+ deleteOrphanSeries: async (input) => ({ results: await this.requireRuntime().deleteOrphans(input.seriesIds) })
3104
3378
  };
3105
3379
  this.emitStoreReadiness("starting");
3106
3380
  this.ctx.logger.info("Initialized — device-history provider registered (store opens on hub link)");
@@ -3136,6 +3410,7 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
3136
3410
  const runtime = new HistoryRuntime({
3137
3411
  door: api.settingsStore,
3138
3412
  catalog: require_dist.HISTORY_CATALOG,
3413
+ itemCatalog: require_dist.HISTORY_ITEM_CATALOG,
3139
3414
  deviceSource: {
3140
3415
  listAll: async () => {
3141
3416
  return (await api.deviceManager.listAll.query({ projection: "slim" })).map((d) => ({