@camstack/system 1.2.467 → 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.
- package/dist/addon-utils.mjs +1 -1
- package/dist/{agent-https-DJ8OzQOr.js → agent-https-5OgOR-eF.js} +1 -1
- package/dist/{agent-https-DR1wmhYY.mjs → agent-https-b-zX78oB.mjs} +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +4 -4
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +5 -5
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +5 -5
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-history/capture/capture-engine.d.ts +24 -4
- package/dist/builtins/device-history/device-history.addon.js +319 -44
- package/dist/builtins/device-history/device-history.addon.mjs +319 -44
- package/dist/builtins/device-history/query/history-query.d.ts +19 -2
- package/dist/builtins/device-history/retention/orphans.d.ts +50 -0
- package/dist/builtins/device-history/retention/retention.d.ts +9 -2
- package/dist/builtins/device-history/runtime/history-runtime.d.ts +10 -2
- package/dist/builtins/device-history/store/history-store.d.ts +5 -0
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-meta-types.d.ts +2 -2
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +3 -3
- package/dist/builtins/keychain/index.mjs +3 -3
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +2 -2
- package/dist/builtins/local-auth/local-auth.addon.mjs +5 -5
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +10 -10
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +8 -8
- package/dist/builtins/record-entries/index.js +1 -1
- package/dist/builtins/record-entries/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +20 -20
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +115 -115
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-5rpWQhA1.js → composition-sources-BGXTiUZm.js} +1 -1
- package/dist/{composition-sources-Qto-S6MA.mjs → composition-sources-DTdzzyf0.mjs} +1 -1
- package/dist/{constants-CuO3rcw4.js → constants-2mPqJjkR.js} +1 -1
- package/dist/{constants-BWli8Cju.mjs → constants-md_POijo.mjs} +1 -1
- package/dist/{coral-accelerators-D_S1Fo5h.mjs → coral-accelerators-Qhn5TOYZ.mjs} +5 -5
- package/dist/{custom-action-registry-BGYDjTqY.mjs → custom-action-registry-DZSCLBuT.mjs} +37 -37
- package/dist/{device-ca-CGcaSheI.js → device-ca-CC58lmNV.js} +1 -1
- package/dist/{device-ca-C5cLlah1.mjs → device-ca-CFdyrqeJ.mjs} +1 -1
- package/dist/{dist-BpUtAgqR.mjs → dist-CtVVtyTX.mjs} +247 -119
- package/dist/{dist-l6zKmHEb.js → dist-DuBtzSCh.js} +258 -118
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-CIno_SNn.js → hub-mdns-ciao-BLsX_2cL.js} +2 -2
- package/dist/{hub-mdns-ciao-0KWDyIMD.mjs → hub-mdns-ciao-CmuWLPci.mjs} +2 -2
- package/dist/index.js +7 -7
- package/dist/index.mjs +225 -225
- package/dist/{restore-archive-B71JI-sP.js → restore-archive-CxPAuaNs.js} +1 -1
- package/dist/{restore-archive-DKxAZ9pI.mjs → restore-archive-D2w3z_ob.mjs} +94 -94
- package/dist/{wal-maintenance-BXYwFecW.mjs → wal-maintenance-DeluFl_1.mjs} +0 -0
- package/dist/{wal-maintenance-B6LSQ_OZ.js → wal-maintenance-EK28XXnc.js} +0 -0
- 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-
|
|
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
|
-
/**
|
|
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$
|
|
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$
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
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) => ({
|